diff --git a/EVENTS.txt b/EVENTS.txt
index 6e6afa070f..1ed670697b 100644
--- a/EVENTS.txt
+++ b/EVENTS.txt
@@ -150,6 +150,12 @@ StartAddressData: Allows the site owner to provide additional information about
EndAddressData: At the end of
- $action: the current action
+StartShowSiteNotice: Before showing site notice
+- $action: the current action
+
+EndShowSiteNotice: After showing site notice
+- $action: the current action
+
StartLoginGroupNav: Before showing the login and register navigation menu
- $action: the current action
diff --git a/README b/README
index 6022887899..da278f7412 100644
--- a/README
+++ b/README
@@ -2,8 +2,8 @@
README
------
-StatusNet 0.9.0 ("Stand") Beta 3
-20 Jan 2010
+StatusNet 0.9.0 ("Stand") Beta 4
+27 Jan 2010
This is the README file for StatusNet (formerly Laconica), the Open
Source microblogging platform. It includes installation instructions,
@@ -597,26 +597,19 @@ server is probably a good idea for high-volume sites.
needs as a parameter the install path; if you run it from the
StatusNet dir, "." should suffice.
-This will run eight (for now) queue handlers:
+This will run the queue handlers:
+* queuedaemon.php - polls for queued items for inbox processing and
+ pushing out to OMB, SMS, XMPP, etc.
* xmppdaemon.php - listens for new XMPP messages from users and stores
- them as notices in the database.
-* jabberqueuehandler.php - sends queued notices in the database to
- registered users who should receive them.
-* publicqueuehandler.php - sends queued notices in the database to
- public feed listeners.
-* ombqueuehandler.php - sends queued notices to OpenMicroBlogging
- recipients on foreign servers.
-* smsqueuehandler.php - sends queued notices to SMS-over-email addresses
- of registered users.
-* xmppconfirmhandler.php - sends confirmation messages to registered
- users.
+ them as notices in the database; also pulls queued XMPP output from
+ queuedaemon.php to push out to clients.
-Note that these queue daemons are pretty raw, and need your care. In
-particular, they leak memory, and you may want to restart them on a
-regular (daily or so) basis with a cron job. Also, if they lose
-the connection to the XMPP server for too long, they'll simply die. It
-may be a good idea to use a daemon-monitoring service, like 'monit',
+These two daemons will automatically restart in most cases of failure
+including memory leaks (if a memory_limit is set), but may still die
+or behave oddly if they lose connections to the XMPP or queue servers.
+
+It may be a good idea to use a daemon-monitoring service, like 'monit',
to check their status and keep them running.
All the daemons write their process IDs (pids) to /var/run/ by
@@ -626,7 +619,7 @@ daemons.
Since version 0.8.0, it's now possible to use a STOMP server instead of
our kind of hacky home-grown DB-based queue solution. See the "queues"
config section below for how to configure to use STOMP. As of this
-writing, the software has been tested with ActiveMQ (
+writing, the software has been tested with ActiveMQ.
Sitemaps
--------
@@ -712,10 +705,12 @@ subdirectory to add a new language to your system. You'll need to
compile the ".po" files into ".mo" files, however.
Contributions of translation information to StatusNet are very easy:
-you can use the Web interface at http://status.net/pootle/ to add one
+you can use the Web interface at TranslateWiki.net to add one
or a few or lots of new translations -- or even new languages. You can
also download more up-to-date .po files there, if you so desire.
+For info on helping with translations, see http://status.net/wiki/Translations
+
Backups
-------
@@ -1492,6 +1487,15 @@ disabled: whether to enable this command. If enabled, users who send
should enable it only after you've convinced yourself that
it is safe. Default is 'false'.
+singleuser
+----------
+
+If an installation has only one user, this can simplify a lot of the
+interface. It also makes the user's profile the root URL.
+
+enabled: Whether to run in "single user mode". Default false.
+nickname: nickname of the single user.
+
Plugins
=======
diff --git a/actions/accessadminpanel.php b/actions/accessadminpanel.php
new file mode 100644
index 0000000000..4768e2faf9
--- /dev/null
+++ b/actions/accessadminpanel.php
@@ -0,0 +1,192 @@
+.
+ *
+ * @category Settings
+ * @package StatusNet
+ * @author Zach Copley
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Administer site access settings
+ *
+ * @category Admin
+ * @package StatusNet
+ * @author Zach Copley
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class AccessadminpanelAction extends AdminPanelAction
+{
+ /**
+ * Returns the page title
+ *
+ * @return string page title
+ */
+
+ function title()
+ {
+ return _('Access');
+ }
+
+ /**
+ * Instructions for using this form.
+ *
+ * @return string instructions
+ */
+
+ function getInstructions()
+ {
+ return _('Site access settings');
+ }
+
+ /**
+ * Show the site admin panel form
+ *
+ * @return void
+ */
+
+ function showForm()
+ {
+ $form = new AccessAdminPanelForm($this);
+ $form->show();
+ return;
+ }
+
+ /**
+ * Save settings from the form
+ *
+ * @return void
+ */
+
+ function saveSettings()
+ {
+ static $booleans = array('site' => array('private', 'inviteonly', 'closed'));
+
+ foreach ($booleans as $section => $parts) {
+ foreach ($parts as $setting) {
+ $values[$section][$setting] = ($this->boolean($setting)) ? 1 : 0;
+ }
+ }
+
+ $config = new Config();
+
+ $config->query('BEGIN');
+
+ foreach ($booleans as $section => $parts) {
+ foreach ($parts as $setting) {
+ Config::save($section, $setting, $values[$section][$setting]);
+ }
+ }
+
+ $config->query('COMMIT');
+
+ return;
+ }
+
+}
+
+class AccessAdminPanelForm extends AdminForm
+{
+ /**
+ * ID of the form
+ *
+ * @return int ID of the form
+ */
+
+ function id()
+ {
+ return 'form_site_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('accessadminpanel');
+ }
+
+ /**
+ * Data elements of the form
+ *
+ * @return void
+ */
+
+ function formData()
+ {
+ $this->out->elementStart('fieldset', array('id' => 'settings_admin_access'));
+ $this->out->element('legend', null, _('Registration'));
+ $this->out->elementStart('ul', 'form_data');
+ $this->li();
+ $this->out->checkbox('private', _('Private'),
+ (bool) $this->value('private'),
+ _('Prohibit anonymous users (not logged in) from viewing site?'));
+ $this->unli();
+
+ $this->li();
+ $this->out->checkbox('inviteonly', _('Invite only'),
+ (bool) $this->value('inviteonly'),
+ _('Make registration invitation only.'));
+ $this->unli();
+
+ $this->li();
+ $this->out->checkbox('closed', _('Closed'),
+ (bool) $this->value('closed'),
+ _('Disable new registrations.'));
+ $this->unli();
+ $this->out->elementEnd('ul');
+ $this->out->elementEnd('fieldset');
+ }
+
+ /**
+ * Action elements
+ *
+ * @return void
+ */
+
+ function formActions()
+ {
+ $this->out->submit('submit', _('Save'), 'submit', null, _('Save access settings'));
+ }
+
+}
diff --git a/actions/apiaccountratelimitstatus.php b/actions/apiaccountratelimitstatus.php
index 1a5afd552c..f19e315bf8 100644
--- a/actions/apiaccountratelimitstatus.php
+++ b/actions/apiaccountratelimitstatus.php
@@ -105,7 +105,22 @@ class ApiAccountRateLimitStatusAction extends ApiBareAuthAction
print json_encode($out);
}
- $this->endDocument($this->format);
+ $this->endDocument($this->format);
+ }
+
+ /**
+ * Return true if read only.
+ *
+ * MAY override
+ *
+ * @param array $args other arguments
+ *
+ * @return boolean is read only action?
+ */
+
+ function isReadOnly($args)
+ {
+ return true;
}
}
diff --git a/actions/apiaccountverifycredentials.php b/actions/apiaccountverifycredentials.php
index 08b201dbff..1095d51626 100644
--- a/actions/apiaccountverifycredentials.php
+++ b/actions/apiaccountverifycredentials.php
@@ -82,4 +82,18 @@ class ApiAccountVerifyCredentialsAction extends ApiAuthAction
}
+ /**
+ * Is this action read only?
+ *
+ * @param array $args other arguments
+ *
+ * @return boolean true
+ *
+ **/
+
+ function isReadOnly($args)
+ {
+ return true;
+ }
+
}
diff --git a/actions/apifriendshipsexists.php b/actions/apifriendshipsexists.php
index c040b9f6ad..ca62b5f514 100644
--- a/actions/apifriendshipsexists.php
+++ b/actions/apifriendshipsexists.php
@@ -116,4 +116,19 @@ class ApiFriendshipsExistsAction extends ApiPrivateAuthAction
}
}
+ /**
+ * Return true if read only.
+ *
+ * MAY override
+ *
+ * @param array $args other arguments
+ *
+ * @return boolean is read only action?
+ */
+
+ function isReadOnly($args)
+ {
+ return true;
+ }
+
}
diff --git a/actions/apifriendshipsshow.php b/actions/apifriendshipsshow.php
index 73ecc9249a..f29e637137 100644
--- a/actions/apifriendshipsshow.php
+++ b/actions/apifriendshipsshow.php
@@ -87,7 +87,6 @@ class ApiFriendshipsShowAction extends ApiBareAuthAction
return true;
}
-
/**
* Determines whether this API resource requires auth. Overloaded to look
* return true in case source_id and source_screen_name are both empty
@@ -165,4 +164,19 @@ class ApiFriendshipsShowAction extends ApiBareAuthAction
}
+ /**
+ * Return true if read only.
+ *
+ * MAY override
+ *
+ * @param array $args other arguments
+ *
+ * @return boolean is read only action?
+ */
+
+ function isReadOnly($args)
+ {
+ return true;
+ }
+
}
diff --git a/actions/apigroupismember.php b/actions/apigroupismember.php
index 69ead0b531..97f8435614 100644
--- a/actions/apigroupismember.php
+++ b/actions/apigroupismember.php
@@ -119,4 +119,19 @@ class ApiGroupIsMemberAction extends ApiBareAuthAction
}
}
+ /**
+ * Return true if read only.
+ *
+ * MAY override
+ *
+ * @param array $args other arguments
+ *
+ * @return boolean is read only action?
+ */
+
+ function isReadOnly($args)
+ {
+ return true;
+ }
+
}
diff --git a/actions/apigroupshow.php b/actions/apigroupshow.php
index 7aa49b1bf3..95d6f95afa 100644
--- a/actions/apigroupshow.php
+++ b/actions/apigroupshow.php
@@ -149,4 +149,19 @@ class ApiGroupShowAction extends ApiPrivateAuthAction
return null;
}
+ /**
+ * Return true if read only.
+ *
+ * MAY override
+ *
+ * @param array $args other arguments
+ *
+ * @return boolean is read only action?
+ */
+
+ function isReadOnly($args)
+ {
+ return true;
+ }
+
}
diff --git a/actions/apihelptest.php b/actions/apihelptest.php
index 7b4017531c..d0e9e4926f 100644
--- a/actions/apihelptest.php
+++ b/actions/apihelptest.php
@@ -92,5 +92,20 @@ class ApiHelpTestAction extends ApiPrivateAuthAction
}
}
+ /**
+ * Return true if read only.
+ *
+ * MAY override
+ *
+ * @param array $args other arguments
+ *
+ * @return boolean is read only action?
+ */
+
+ function isReadOnly($args)
+ {
+ return true;
+ }
+
}
diff --git a/actions/apioauthaccesstoken.php b/actions/apioauthaccesstoken.php
new file mode 100644
index 0000000000..887df4c20d
--- /dev/null
+++ b/actions/apioauthaccesstoken.php
@@ -0,0 +1,94 @@
+.
+ *
+ * @category API
+ * @package StatusNet
+ * @author Zach Copley
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+require_once INSTALLDIR . '/lib/apioauth.php';
+
+/**
+ * Exchange an authorized OAuth request token for an access token
+ *
+ * @category API
+ * @package StatusNet
+ * @author Zach Copley
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class ApiOauthAccessTokenAction extends ApiOauthAction
+{
+
+ /**
+ * Class handler.
+ *
+ * @param array $args array of arguments
+ *
+ * @return void
+ */
+ function handle($args)
+ {
+ parent::handle($args);
+
+ $datastore = new ApiStatusNetOAuthDataStore();
+ $server = new OAuthServer($datastore);
+ $hmac_method = new OAuthSignatureMethod_HMAC_SHA1();
+
+ $server->add_signature_method($hmac_method);
+
+ $atok = null;
+
+ try {
+ $req = OAuthRequest::from_request();
+ $atok = $server->fetch_access_token($req);
+
+ } catch (OAuthException $e) {
+ common_log(LOG_WARNING, 'API OAuthException - ' . $e->getMessage());
+ common_debug(var_export($req, true));
+ $this->outputError($e->getMessage());
+ return;
+ }
+
+ if (empty($atok)) {
+ common_debug('couldn\'t get access token.');
+ print "Token exchange failed. Has the request token been authorized?\n";
+ } else {
+ print $atok;
+ }
+ }
+
+ function outputError($msg)
+ {
+ header('HTTP/1.1 401 Unauthorized');
+ header('Content-Type: text/html; charset=utf-8');
+ print $msg . "\n";
+ }
+}
+
diff --git a/actions/apioauthauthorize.php b/actions/apioauthauthorize.php
new file mode 100644
index 0000000000..15c3a9dad5
--- /dev/null
+++ b/actions/apioauthauthorize.php
@@ -0,0 +1,377 @@
+.
+ *
+ * @category API
+ * @package StatusNet
+ * @author Zach Copley
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+require_once INSTALLDIR . '/lib/apioauth.php';
+
+/**
+ * Authorize an OAuth request token
+ *
+ * @category API
+ * @package StatusNet
+ * @author Zach Copley
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class ApiOauthAuthorizeAction extends ApiOauthAction
+{
+ var $oauth_token;
+ var $callback;
+ var $app;
+ var $nickname;
+ var $password;
+ var $store;
+
+ /**
+ * Is this a read-only action?
+ *
+ * @return boolean false
+ */
+
+ function isReadOnly($args)
+ {
+ return false;
+ }
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ common_debug("apioauthauthorize");
+
+ $this->nickname = $this->trimmed('nickname');
+ $this->password = $this->arg('password');
+ $this->oauth_token = $this->arg('oauth_token');
+ $this->callback = $this->arg('oauth_callback');
+ $this->store = new ApiStatusNetOAuthDataStore();
+ $this->app = $this->store->getAppByRequestToken($this->oauth_token);
+
+ return true;
+ }
+
+ /**
+ * Handle input, produce output
+ *
+ * Switches on request method; either shows the form or handles its input.
+ *
+ * @param array $args $_REQUEST data
+ *
+ * @return void
+ */
+
+ function handle($args)
+ {
+ parent::handle($args);
+
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+
+ $this->handlePost();
+
+ } else {
+
+ // XXX: make better error messages
+
+ if (empty($this->oauth_token)) {
+
+ common_debug("No request token found.");
+
+ $this->clientError(_('Bad request.'));
+ return;
+ }
+
+ if (empty($this->app)) {
+ common_debug('No app for that token.');
+ $this->clientError(_('Bad request.'));
+ return;
+ }
+
+ $name = $this->app->name;
+ common_debug("Requesting auth for app: " . $name);
+
+ $this->showForm();
+ }
+ }
+
+ function handlePost()
+ {
+ common_debug("handlePost()");
+
+ // check session token for CSRF protection.
+
+ $token = $this->trimmed('token');
+
+ if (!$token || $token != common_session_token()) {
+ $this->showForm(_('There was a problem with your session token. '.
+ 'Try again, please.'));
+ return;
+ }
+
+ // check creds
+
+ $user = null;
+
+ if (!common_logged_in()) {
+ $user = common_check_user($this->nickname, $this->password);
+ if (empty($user)) {
+ $this->showForm(_("Invalid nickname / password!"));
+ return;
+ }
+ } else {
+ $user = common_current_user();
+ }
+
+ if ($this->arg('allow')) {
+
+ // mark the req token as authorized
+
+ $this->store->authorize_token($this->oauth_token);
+
+ // Check to see if there was a previous token associated
+ // with this user/app and kill it. If the user is doing this she
+ // probably doesn't want any old tokens anyway.
+
+ $appUser = Oauth_application_user::getByKeys($user, $this->app);
+
+ if (!empty($appUser)) {
+ $result = $appUser->delete();
+
+ if (!$result) {
+ common_log_db_error($appUser, 'DELETE', __FILE__);
+ throw new ServerException(_('DB error deleting OAuth app user.'));
+ return;
+ }
+ }
+
+ // associated the authorized req token with the user and the app
+
+ $appUser = new Oauth_application_user();
+
+ $appUser->profile_id = $user->id;
+ $appUser->application_id = $this->app->id;
+
+ // Note: do not copy the access type from the application.
+ // The access type should always be 0 when the OAuth app
+ // user record has a request token associated with it.
+ // Access type gets assigned once an access token has been
+ // granted. The OAuth app user record then gets updated
+ // with the new access token and access type.
+
+ $appUser->token = $this->oauth_token;
+ $appUser->created = common_sql_now();
+
+ $result = $appUser->insert();
+
+ if (!$result) {
+ common_log_db_error($appUser, 'INSERT', __FILE__);
+ throw new ServerException(_('DB error inserting OAuth app user.'));
+ return;
+ }
+
+ // if we have a callback redirect and provide the token
+
+ // A callback specified in the app setup overrides whatever
+ // is passed in with the request.
+
+ common_debug("Req token is authorized - doing callback");
+
+ if (!empty($this->app->callback_url)) {
+ $this->callback = $this->app->callback_url;
+ }
+
+ if (!empty($this->callback)) {
+
+ // XXX: Need better way to build this redirect url.
+
+ $target_url = $this->getCallback($this->callback,
+ array('oauth_token' => $this->oauth_token));
+
+ common_debug("Doing callback to $target_url");
+
+ common_redirect($target_url, 303);
+ } else {
+ common_debug("callback was empty!");
+ }
+
+ // otherwise inform the user that the rt was authorized
+
+ $this->elementStart('p');
+
+ // XXX: Do OAuth 1.0a verifier code
+
+ $this->raw(sprintf(_("The request token %s has been authorized. " .
+ 'Please exchange it for an access token.'),
+ $this->oauth_token));
+
+ $this->elementEnd('p');
+
+ } else if ($this->arg('deny')) {
+
+ $this->elementStart('p');
+
+ $this->raw(sprintf(_("The request token %s has been denied."),
+ $this->oauth_token));
+
+ $this->elementEnd('p');
+ } else {
+ $this->clientError(_('Unexpected form submission.'));
+ return;
+ }
+ }
+
+ function showForm($error=null)
+ {
+ $this->error = $error;
+ $this->showPage();
+ }
+
+ function showScripts()
+ {
+ parent::showScripts();
+ if (!common_logged_in()) {
+ $this->autofocus('nickname');
+ }
+ }
+
+ /**
+ * Title of the page
+ *
+ * @return string title of the page
+ */
+
+ function title()
+ {
+ return _('An application would like to connect to your account');
+ }
+
+ /**
+ * Shows the authorization form.
+ *
+ * @return void
+ */
+
+ function showContent()
+ {
+ $this->elementStart('form', array('method' => 'post',
+ 'id' => 'form_apioauthauthorize',
+ 'class' => 'form_settings',
+ 'action' => common_local_url('apioauthauthorize')));
+ $this->elementStart('fieldset');
+ $this->element('legend', array('id' => 'apioauthauthorize_allowdeny'),
+ _('Allow or deny access'));
+
+ $this->hidden('token', common_session_token());
+ $this->hidden('oauth_token', $this->oauth_token);
+ $this->hidden('oauth_callback', $this->callback);
+
+ $this->elementStart('ul', 'form_data');
+ $this->elementStart('li');
+ $this->elementStart('p');
+ if (!empty($this->app->icon)) {
+ $this->element('img', array('src' => $this->app->icon));
+ }
+
+ $access = ($this->app->access_type & Oauth_application::$writeAccess) ?
+ 'access and update' : 'access';
+
+ $msg = _('The application %1$s by ' .
+ '%2$s would like the ability ' .
+ 'to %3$s your account data.');
+
+ $this->raw(sprintf($msg,
+ $this->app->name,
+ $this->app->organization,
+ $access));
+ $this->elementEnd('p');
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+
+ if (!common_logged_in()) {
+
+ $this->elementStart('fieldset');
+ $this->element('legend', null, _('Account'));
+ $this->elementStart('ul', 'form_data');
+ $this->elementStart('li');
+ $this->input('nickname', _('Nickname'));
+ $this->elementEnd('li');
+ $this->elementStart('li');
+ $this->password('password', _('Password'));
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+
+ $this->elementEnd('fieldset');
+
+ }
+
+ $this->element('input', array('id' => 'deny_submit',
+ 'class' => 'submit submit form_action-primary',
+ 'name' => 'deny',
+ 'type' => 'submit',
+ 'value' => _('Deny')));
+
+ $this->element('input', array('id' => 'allow_submit',
+ 'class' => 'submit submit form_action-secondary',
+ 'name' => 'allow',
+ 'type' => 'submit',
+ 'value' => _('Allow')));
+
+ $this->elementEnd('fieldset');
+ $this->elementEnd('form');
+ }
+
+ /**
+ * Instructions for using the form
+ *
+ * For "remembered" logins, we make the user re-login when they
+ * try to change settings. Different instructions for this case.
+ *
+ * @return void
+ */
+
+ function getInstructions()
+ {
+ return _('Allow or deny access to your account information.');
+ }
+
+ /**
+ * A local menu
+ *
+ * Shows different login/register actions.
+ *
+ * @return void
+ */
+
+ function showLocalNav()
+ {
+ }
+
+}
diff --git a/actions/apioauthrequesttoken.php b/actions/apioauthrequesttoken.php
new file mode 100644
index 0000000000..4fa626d866
--- /dev/null
+++ b/actions/apioauthrequesttoken.php
@@ -0,0 +1,99 @@
+.
+ *
+ * @category API
+ * @package StatusNet
+ * @author Zach Copley
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+require_once INSTALLDIR . '/lib/apioauth.php';
+
+/**
+ * Get an OAuth request token
+ *
+ * @category API
+ * @package StatusNet
+ * @author Zach Copley
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class ApiOauthRequestTokenAction extends ApiOauthAction
+{
+ /**
+ * Take arguments for running
+ *
+ * @param array $args $_REQUEST args
+ *
+ * @return boolean success flag
+ *
+ */
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ $this->callback = $this->arg('oauth_callback');
+
+ if (!empty($this->callback)) {
+ common_debug("callback: $this->callback");
+ }
+
+ return true;
+ }
+
+ /**
+ * Class handler.
+ *
+ * @param array $args array of arguments
+ *
+ * @return void
+ */
+ function handle($args)
+ {
+ parent::handle($args);
+
+ $datastore = new ApiStatusNetOAuthDataStore();
+ $server = new OAuthServer($datastore);
+ $hmac_method = new OAuthSignatureMethod_HMAC_SHA1();
+
+ $server->add_signature_method($hmac_method);
+
+ try {
+ $req = OAuthRequest::from_request();
+ $token = $server->fetch_request_token($req);
+ print $token;
+ } catch (OAuthException $e) {
+ common_log(LOG_WARNING, 'API OAuthException - ' . $e->getMessage());
+ header('HTTP/1.1 401 Unauthorized');
+ header('Content-Type: text/html; charset=utf-8');
+ print $e->getMessage() . "\n";
+ }
+ }
+
+}
diff --git a/actions/apistatusesupdate.php b/actions/apistatusesupdate.php
index 9d831b9dbb..bf367e1e18 100644
--- a/actions/apistatusesupdate.php
+++ b/actions/apistatusesupdate.php
@@ -28,7 +28,7 @@
* @author Mike Cochrane
* @author Robin Millette
* @author Zach Copley
- * @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/
*/
@@ -79,12 +79,16 @@ class ApiStatusesUpdateAction extends ApiAuthAction
{
parent::prepare($args);
- $this->user = $this->auth_user;
$this->status = $this->trimmed('status');
$this->source = $this->trimmed('source');
$this->lat = $this->trimmed('lat');
$this->lon = $this->trimmed('long');
+ // try to set the source attr from OAuth app
+ if (empty($this->source)) {
+ $this->source = $this->oauth_source;
+ }
+
if (empty($this->source) || in_array($this->source, self::$reserved_sources)) {
$this->source = 'api';
}
@@ -140,7 +144,7 @@ class ApiStatusesUpdateAction extends ApiAuthAction
return;
}
- if (empty($this->user)) {
+ if (empty($this->auth_user)) {
$this->clientError(_('No such user.'), 404, $this->format);
return;
}
@@ -167,7 +171,7 @@ class ApiStatusesUpdateAction extends ApiAuthAction
// Check for commands
$inter = new CommandInterpreter();
- $cmd = $inter->handle_command($this->user, $status_shortened);
+ $cmd = $inter->handle_command($this->auth_user, $status_shortened);
if ($cmd) {
@@ -179,7 +183,7 @@ class ApiStatusesUpdateAction extends ApiAuthAction
// And, it returns your last status whether the cmd was successful
// or not!
- $this->notice = $this->user->getCurrentNotice();
+ $this->notice = $this->auth_user->getCurrentNotice();
} else {
@@ -206,7 +210,7 @@ class ApiStatusesUpdateAction extends ApiAuthAction
$upload = null;
try {
- $upload = MediaFile::fromUpload('media', $this->user);
+ $upload = MediaFile::fromUpload('media', $this->auth_user);
} catch (ClientException $ce) {
$this->clientError($ce->getMessage());
return;
@@ -229,19 +233,19 @@ class ApiStatusesUpdateAction extends ApiAuthAction
$options = array('reply_to' => $reply_to);
- if ($this->user->shareLocation()) {
+ if ($this->auth_user->shareLocation()) {
$locOptions = Notice::locationOptions($this->lat,
$this->lon,
null,
null,
- $this->user->getProfile());
+ $this->auth_user->getProfile());
$options = array_merge($options, $locOptions);
}
$this->notice =
- Notice::saveNew($this->user->id,
+ Notice::saveNew($this->auth_user->id,
$content,
$this->source,
$options);
@@ -250,7 +254,6 @@ class ApiStatusesUpdateAction extends ApiAuthAction
$upload->attachToNotice($this->notice);
}
-
}
$this->showNotice();
diff --git a/actions/apistatusnetconfig.php b/actions/apistatusnetconfig.php
index ab96f2e5f9..dc1ab8685b 100644
--- a/actions/apistatusnetconfig.php
+++ b/actions/apistatusnetconfig.php
@@ -138,5 +138,20 @@ class ApiStatusnetConfigAction extends ApiAction
}
}
+ /**
+ * Return true if read only.
+ *
+ * MAY override
+ *
+ * @param array $args other arguments
+ *
+ * @return boolean is read only action?
+ */
+
+ function isReadOnly($args)
+ {
+ return true;
+ }
+
}
diff --git a/actions/apistatusnetversion.php b/actions/apistatusnetversion.php
index 5109cd8062..d094807597 100644
--- a/actions/apistatusnetversion.php
+++ b/actions/apistatusnetversion.php
@@ -98,5 +98,20 @@ class ApiStatusnetVersionAction extends ApiPrivateAuthAction
}
}
+ /**
+ * Return true if read only.
+ *
+ * MAY override
+ *
+ * @param array $args other arguments
+ *
+ * @return boolean is read only action?
+ */
+
+ function isReadOnly($args)
+ {
+ return true;
+ }
+
}
diff --git a/actions/apiusershow.php b/actions/apiusershow.php
index a7fe0dcc1e..6c8fad49ba 100644
--- a/actions/apiusershow.php
+++ b/actions/apiusershow.php
@@ -123,4 +123,19 @@ class ApiUserShowAction extends ApiPrivateAuthAction
}
+ /**
+ * Return true if read only.
+ *
+ * MAY override
+ *
+ * @param array $args other arguments
+ *
+ * @return boolean is read only action?
+ */
+
+ function isReadOnly($args)
+ {
+ return true;
+ }
+
}
diff --git a/actions/avatarsettings.php b/actions/avatarsettings.php
index cf45255520..6a7398746a 100644
--- a/actions/avatarsettings.php
+++ b/actions/avatarsettings.php
@@ -416,8 +416,8 @@ class AvatarsettingsAction extends AccountSettingsAction
parent::showScripts();
if ($this->mode == 'crop') {
- $this->script('js/jcrop/jquery.Jcrop.min.js');
- $this->script('js/jcrop/jquery.Jcrop.go.js');
+ $this->script('jcrop/jquery.Jcrop.min.js');
+ $this->script('jcrop/jquery.Jcrop.go.js');
}
$this->autofocus('avatarfile');
diff --git a/actions/designadminpanel.php b/actions/designadminpanel.php
index 72ad6ade2a..30e8bde1a4 100644
--- a/actions/designadminpanel.php
+++ b/actions/designadminpanel.php
@@ -302,8 +302,8 @@ class DesignadminpanelAction extends AdminPanelAction
{
parent::showScripts();
- $this->script('js/farbtastic/farbtastic.js');
- $this->script('js/userdesign.go.js');
+ $this->script('farbtastic/farbtastic.js');
+ $this->script('userdesign.go.js');
$this->autofocus('design_background-image_file');
}
diff --git a/actions/doc.php b/actions/doc.php
index 836f039d32..25d363472a 100644
--- a/actions/doc.php
+++ b/actions/doc.php
@@ -45,11 +45,23 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
*/
class DocAction extends Action
{
- var $filename;
- var $title;
+ var $output = null;
+ var $filename = null;
+ var $title = null;
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ $this->title = $this->trimmed('title');
+ $this->output = null;
+
+ $this->loadDoc();
+ return true;
+ }
/**
- * Class handler.
+ * Handle a request
*
* @param array $args array of arguments
*
@@ -58,51 +70,51 @@ class DocAction extends Action
function handle($args)
{
parent::handle($args);
-
- $this->title = $this->trimmed('title');
- $this->output = null;
-
- if (Event::handle('StartLoadDoc', array(&$this->title, &$this->output))) {
-
- $this->filename = INSTALLDIR.'/doc-src/'.$this->title;
- if (!file_exists($this->filename)) {
- $this->clientError(_('No such document.'));
- return;
- }
-
- $c = file_get_contents($this->filename);
- $this->output = common_markup_to_html($c);
-
- Event::handle('EndLoadDoc', array($this->title, &$this->output));
- }
-
$this->showPage();
}
- // overrrided to add entry-title class
- function showPageTitle() {
+ /**
+ * Page title
+ *
+ * Gives the page title of the document. Override default for hAtom entry.
+ *
+ * @return void
+ */
+
+ function showPageTitle()
+ {
$this->element('h1', array('class' => 'entry-title'), $this->title());
}
- // overrided to add hentry, and content-inner classes
+ /**
+ * Block for content.
+ *
+ * Overrides default from Action to wrap everything in an hAtom entry.
+ *
+ * @return void.
+ */
+
function showContentBlock()
- {
- $this->elementStart('div', array('id' => 'content', 'class' => 'hentry'));
- $this->showPageTitle();
- $this->showPageNoticeBlock();
- $this->elementStart('div', array('id' => 'content_inner',
- 'class' => 'entry-content'));
- // show the actual content (forms, lists, whatever)
- $this->showContent();
- $this->elementEnd('div');
- $this->elementEnd('div');
- }
+ {
+ $this->elementStart('div', array('id' => 'content', 'class' => 'hentry'));
+ $this->showPageTitle();
+ $this->showPageNoticeBlock();
+ $this->elementStart('div', array('id' => 'content_inner',
+ 'class' => 'entry-content'));
+ // show the actual content (forms, lists, whatever)
+ $this->showContent();
+ $this->elementEnd('div');
+ $this->elementEnd('div');
+ }
/**
* Display content.
*
- * @return nothing
+ * Shows the content of the document.
+ *
+ * @return void
*/
+
function showContent()
{
$this->raw($this->output);
@@ -111,6 +123,8 @@ class DocAction extends Action
/**
* Page title.
*
+ * Uses the title of the document.
+ *
* @return page title
*/
function title()
@@ -118,8 +132,74 @@ class DocAction extends Action
return ucfirst($this->title);
}
+ /**
+ * These pages are read-only.
+ *
+ * @param array $args unused.
+ *
+ * @return boolean read-only flag (false)
+ */
+
function isReadOnly($args)
{
return true;
}
+
+ function loadDoc()
+ {
+ if (Event::handle('StartLoadDoc', array(&$this->title, &$this->output))) {
+
+ $this->filename = $this->getFilename();
+
+ if (empty($this->filename)) {
+ throw new ClientException(sprintf(_('No such document "%s"'), $this->title), 404);
+ }
+
+ $c = file_get_contents($this->filename);
+
+ $this->output = common_markup_to_html($c);
+
+ Event::handle('EndLoadDoc', array($this->title, &$this->output));
+ }
+ }
+
+ function getFilename()
+ {
+ if (file_exists(INSTALLDIR.'/local/doc-src/'.$this->title)) {
+ $localDef = INSTALLDIR.'/local/doc-src/'.$this->title;
+ }
+
+ $local = glob(INSTALLDIR.'/local/doc-src/'.$this->title.'.*');
+
+ if (count($local) || isset($localDef)) {
+ return $this->negotiateLanguage($local, $localDef);
+ }
+
+ if (file_exists(INSTALLDIR.'/doc-src/'.$this->title)) {
+ $distDef = INSTALLDIR.'/doc-src/'.$this->title;
+ }
+
+ $dist = glob(INSTALLDIR.'/doc-src/'.$this->title.'.*');
+
+ if (count($dist) || isset($distDef)) {
+ return $this->negotiateLanguage($dist, $distDef);
+ }
+
+ return null;
+ }
+
+ function negotiateLanguage($filenames, $defaultFilename=null)
+ {
+ // XXX: do this better
+
+ $langcode = common_language();
+
+ foreach ($filenames as $filename) {
+ if (preg_match('/\.'.$langcode.'$/', $filename)) {
+ return $filename;
+ }
+ }
+
+ return $defaultFilename;
+ }
}
diff --git a/actions/editapplication.php b/actions/editapplication.php
new file mode 100644
index 0000000000..9cc3e3cead
--- /dev/null
+++ b/actions/editapplication.php
@@ -0,0 +1,264 @@
+.
+ *
+ * @category Applications
+ * @package StatusNet
+ * @author Zach Copley
+ * @copyright 2008-2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+/**
+ * Edit the details of an OAuth application
+ *
+ * This is the form for editing an application
+ *
+ * @category Application
+ * @package StatusNet
+ * @author Zach Copley
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class EditApplicationAction extends OwnerDesignAction
+{
+ var $msg = null;
+ var $owner = null;
+ var $app = null;
+
+ function title()
+ {
+ return _('Edit Application');
+ }
+
+ /**
+ * Prepare to run
+ */
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ if (!common_logged_in()) {
+ $this->clientError(_('You must be logged in to edit an application.'));
+ return false;
+ }
+
+ $id = (int)$this->arg('id');
+
+ $this->app = Oauth_application::staticGet($id);
+ $this->owner = User::staticGet($this->app->owner);
+ $cur = common_current_user();
+
+ if ($cur->id != $this->owner->id) {
+ $this->clientError(_('You are not the owner of this application.'), 401);
+ }
+
+ if (!$this->app) {
+ $this->clientError(_('No such application.'));
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Handle the request
+ *
+ * On GET, show the form. On POST, try to save the app.
+ *
+ * @param array $args unused
+ *
+ * @return void
+ */
+
+ function handle($args)
+ {
+ parent::handle($args);
+
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ $this->handlePost($args);
+ } else {
+ $this->showForm();
+ }
+ }
+
+ function handlePost($args)
+ {
+ // Workaround for PHP returning empty $_POST and $_FILES when POST
+ // length > post_max_size in php.ini
+
+ if (empty($_FILES)
+ && empty($_POST)
+ && ($_SERVER['CONTENT_LENGTH'] > 0)
+ ) {
+ $msg = _('The server was unable to handle that much POST ' .
+ 'data (%s bytes) due to its current configuration.');
+ $this->clientException(sprintf($msg, $_SERVER['CONTENT_LENGTH']));
+ return;
+ }
+
+ // CSRF protection
+ $token = $this->trimmed('token');
+ if (!$token || $token != common_session_token()) {
+ $this->clientError(_('There was a problem with your session token.'));
+ return;
+ }
+
+ $cur = common_current_user();
+
+ if ($this->arg('cancel')) {
+ common_redirect(common_local_url('showapplication',
+ array('id' => $this->app->id)), 303);
+ } elseif ($this->arg('save')) {
+ $this->trySave();
+ } else {
+ $this->clientError(_('Unexpected form submission.'));
+ }
+ }
+
+ function showForm($msg=null)
+ {
+ $this->msg = $msg;
+ $this->showPage();
+ }
+
+ function showContent()
+ {
+ $form = new ApplicationEditForm($this, $this->app);
+ $form->show();
+ }
+
+ function showPageNotice()
+ {
+ if (!empty($this->msg)) {
+ $this->element('p', 'error', $this->msg);
+ } else {
+ $this->element('p', 'instructions',
+ _('Use this form to edit your application.'));
+ }
+ }
+
+ function trySave()
+ {
+ $name = $this->trimmed('name');
+ $description = $this->trimmed('description');
+ $source_url = $this->trimmed('source_url');
+ $organization = $this->trimmed('organization');
+ $homepage = $this->trimmed('homepage');
+ $callback_url = $this->trimmed('callback_url');
+ $type = $this->arg('app_type');
+ $access_type = $this->arg('default_access_type');
+
+ if (empty($name)) {
+ $this->showForm(_('Name is required.'));
+ return;
+ } elseif (mb_strlen($name) > 255) {
+ $this->showForm(_('Name is too long (max 255 chars).'));
+ return;
+ } elseif (empty($description)) {
+ $this->showForm(_('Description is required.'));
+ return;
+ } elseif (Oauth_application::descriptionTooLong($description)) {
+ $this->showForm(sprintf(
+ _('Description is too long (max %d chars).'),
+ Oauth_application::maxDescription()));
+ return;
+ } elseif (mb_strlen($source_url) > 255) {
+ $this->showForm(_('Source URL is too long.'));
+ return;
+ } elseif ((mb_strlen($source_url) > 0)
+ && !Validate::uri($source_url,
+ array('allowed_schemes' => array('http', 'https'))))
+ {
+ $this->showForm(_('Source URL is not valid.'));
+ return;
+ } elseif (empty($organization)) {
+ $this->showForm(_('Organization is required.'));
+ return;
+ } elseif (mb_strlen($organization) > 255) {
+ $this->showForm(_('Organization is too long (max 255 chars).'));
+ return;
+ } elseif (empty($homepage)) {
+ $this->showForm(_('Organization homepage is required.'));
+ return;
+ } elseif ((mb_strlen($homepage) > 0)
+ && !Validate::uri($homepage,
+ array('allowed_schemes' => array('http', 'https'))))
+ {
+ $this->showForm(_('Homepage is not a valid URL.'));
+ return;
+ } elseif (mb_strlen($callback_url) > 255) {
+ $this->showForm(_('Callback is too long.'));
+ return;
+ } elseif (mb_strlen($callback_url) > 0
+ && !Validate::uri($source_url,
+ array('allowed_schemes' => array('http', 'https'))
+ ))
+ {
+ $this->showForm(_('Callback URL is not valid.'));
+ return;
+ }
+
+ $cur = common_current_user();
+
+ // Checked in prepare() above
+
+ assert(!is_null($cur));
+ assert(!is_null($this->app));
+
+ $orig = clone($this->app);
+
+ $this->app->name = $name;
+ $this->app->description = $description;
+ $this->app->source_url = $source_url;
+ $this->app->organization = $organization;
+ $this->app->homepage = $homepage;
+ $this->app->callback_url = $callback_url;
+ $this->app->type = $type;
+
+ common_debug("access_type = $access_type");
+
+ if ($access_type == 'r') {
+ $this->app->access_type = 1;
+ } else {
+ $this->app->access_type = 3;
+ }
+
+ $result = $this->app->update($orig);
+
+ if (!$result) {
+ common_log_db_error($this->app, 'UPDATE', __FILE__);
+ $this->serverError(_('Could not update application.'));
+ }
+
+ $this->app->uploadLogo();
+
+ common_redirect(common_local_url('oauthappssettings'), 303);
+ }
+
+}
+
diff --git a/actions/grouplogo.php b/actions/grouplogo.php
index f197aef33e..3c9b562962 100644
--- a/actions/grouplogo.php
+++ b/actions/grouplogo.php
@@ -437,8 +437,8 @@ class GrouplogoAction extends GroupDesignAction
parent::showScripts();
if ($this->mode == 'crop') {
- $this->script('js/jcrop/jquery.Jcrop.min.js');
- $this->script('js/jcrop/jquery.Jcrop.go.js');
+ $this->script('jcrop/jquery.Jcrop.min.js');
+ $this->script('jcrop/jquery.Jcrop.go.js');
}
$this->autofocus('avatarfile');
diff --git a/actions/inbox.php b/actions/inbox.php
index f605cc9e8b..8330f753ff 100644
--- a/actions/inbox.php
+++ b/actions/inbox.php
@@ -56,10 +56,10 @@ class InboxAction extends MailboxAction
function title()
{
if ($this->page > 1) {
- return sprintf(_("Inbox for %1$s - page %2$d"), $this->user->nickname,
+ return sprintf(_('Inbox for %1$s - page %2$d'), $this->user->nickname,
$this->page);
} else {
- return sprintf(_("Inbox for %s"), $this->user->nickname);
+ return sprintf(_('Inbox for %s'), $this->user->nickname);
}
}
diff --git a/actions/newapplication.php b/actions/newapplication.php
new file mode 100644
index 0000000000..c499fe7c76
--- /dev/null
+++ b/actions/newapplication.php
@@ -0,0 +1,277 @@
+.
+ *
+ * @category Applications
+ * @package StatusNet
+ * @author Zach Copley
+ * @copyright 2008-2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+/**
+ * Add a new application
+ *
+ * This is the form for adding a new application
+ *
+ * @category Application
+ * @package StatusNet
+ * @author Zach Copley
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class NewApplicationAction extends OwnerDesignAction
+{
+ var $msg;
+
+ function title()
+ {
+ return _('New Application');
+ }
+
+ /**
+ * Prepare to run
+ */
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ if (!common_logged_in()) {
+ $this->clientError(_('You must be logged in to register an application.'));
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Handle the request
+ *
+ * On GET, show the form. On POST, try to save the app.
+ *
+ * @param array $args unused
+ *
+ * @return void
+ */
+
+ function handle($args)
+ {
+ parent::handle($args);
+
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ $this->handlePost($args);
+ } else {
+ $this->showForm();
+ }
+ }
+
+ function handlePost($args)
+ {
+ // Workaround for PHP returning empty $_POST and $_FILES when POST
+ // length > post_max_size in php.ini
+
+ if (empty($_FILES)
+ && empty($_POST)
+ && ($_SERVER['CONTENT_LENGTH'] > 0)
+ ) {
+ $msg = _('The server was unable to handle that much POST ' .
+ 'data (%s bytes) due to its current configuration.');
+ $this->clientException(sprintf($msg, $_SERVER['CONTENT_LENGTH']));
+ return;
+ }
+
+ // CSRF protection
+ $token = $this->trimmed('token');
+ if (!$token || $token != common_session_token()) {
+ $this->clientError(_('There was a problem with your session token.'));
+ return;
+ }
+
+ $cur = common_current_user();
+
+ if ($this->arg('cancel')) {
+ common_redirect(common_local_url('oauthappssettings'), 303);
+ } elseif ($this->arg('save')) {
+ $this->trySave();
+ } else {
+ $this->clientError(_('Unexpected form submission.'));
+ }
+ }
+
+ function showForm($msg=null)
+ {
+ $this->msg = $msg;
+ $this->showPage();
+ }
+
+ function showContent()
+ {
+ $form = new ApplicationEditForm($this);
+ $form->show();
+ }
+
+ function showPageNotice()
+ {
+ if ($this->msg) {
+ $this->element('p', 'error', $this->msg);
+ } else {
+ $this->element('p', 'instructions',
+ _('Use this form to register a new application.'));
+ }
+ }
+
+ function trySave()
+ {
+ $name = $this->trimmed('name');
+ $description = $this->trimmed('description');
+ $source_url = $this->trimmed('source_url');
+ $organization = $this->trimmed('organization');
+ $homepage = $this->trimmed('homepage');
+ $callback_url = $this->trimmed('callback_url');
+ $type = $this->arg('app_type');
+ $access_type = $this->arg('default_access_type');
+
+ if (empty($name)) {
+ $this->showForm(_('Name is required.'));
+ return;
+ } elseif (mb_strlen($name) > 255) {
+ $this->showForm(_('Name is too long (max 255 chars).'));
+ return;
+ } elseif (empty($description)) {
+ $this->showForm(_('Description is required.'));
+ return;
+ } elseif (Oauth_application::descriptionTooLong($description)) {
+ $this->showForm(sprintf(
+ _('Description is too long (max %d chars).'),
+ Oauth_application::maxDescription()));
+ return;
+ } elseif (empty($source_url)) {
+ $this->showForm(_('Source URL is required.'));
+ return;
+ } elseif ((strlen($source_url) > 0)
+ && !Validate::uri(
+ $source_url,
+ array('allowed_schemes' => array('http', 'https'))
+ )
+ )
+ {
+ $this->showForm(_('Source URL is not valid.'));
+ return;
+ } elseif (empty($organization)) {
+ $this->showForm(_('Organization is required.'));
+ return;
+ } elseif (mb_strlen($organization) > 255) {
+ $this->showForm(_('Organization is too long (max 255 chars).'));
+ return;
+ } elseif (empty($homepage)) {
+ $this->showForm(_('Organization homepage is required.'));
+ return;
+ } elseif ((strlen($homepage) > 0)
+ && !Validate::uri(
+ $homepage,
+ array('allowed_schemes' => array('http', 'https'))
+ )
+ )
+ {
+ $this->showForm(_('Homepage is not a valid URL.'));
+ return;
+ } elseif (mb_strlen($callback_url) > 255) {
+ $this->showForm(_('Callback is too long.'));
+ return;
+ } elseif (strlen($callback_url) > 0
+ && !Validate::uri(
+ $source_url,
+ array('allowed_schemes' => array('http', 'https'))
+ )
+ )
+ {
+ $this->showForm(_('Callback URL is not valid.'));
+ return;
+ }
+
+ $cur = common_current_user();
+
+ // Checked in prepare() above
+
+ assert(!is_null($cur));
+
+ $app = new Oauth_application();
+
+ $app->query('BEGIN');
+
+ $app->name = $name;
+ $app->owner = $cur->id;
+ $app->description = $description;
+ $app->source_url = $source_url;
+ $app->organization = $organization;
+ $app->homepage = $homepage;
+ $app->callback_url = $callback_url;
+ $app->type = $type;
+
+ // Yeah, I dunno why I chose bit flags. I guess so I could
+ // copy this value directly to Oauth_application_user
+ // access_type which I think does need bit flags -- Z
+
+ if ($access_type == 'r') {
+ $app->setAccessFlags(true, false);
+ } else {
+ $app->setAccessFlags(true, true);
+ }
+
+ $app->created = common_sql_now();
+
+ // generate consumer key and secret
+
+ $consumer = Consumer::generateNew();
+
+ $result = $consumer->insert();
+
+ if (!$result) {
+ common_log_db_error($consumer, 'INSERT', __FILE__);
+ $this->serverError(_('Could not create application.'));
+ }
+
+ $app->consumer_key = $consumer->consumer_key;
+
+ $this->app_id = $app->insert();
+
+ if (!$this->app_id) {
+ common_log_db_error($app, 'INSERT', __FILE__);
+ $this->serverError(_('Could not create application.'));
+ $app->query('ROLLBACK');
+ }
+
+ $app->uploadLogo();
+
+ $app->query('COMMIT');
+
+ common_redirect(common_local_url('oauthappssettings'), 303);
+
+ }
+
+}
+
diff --git a/actions/oauthappssettings.php b/actions/oauthappssettings.php
new file mode 100644
index 0000000000..6c0670b17b
--- /dev/null
+++ b/actions/oauthappssettings.php
@@ -0,0 +1,166 @@
+.
+ *
+ * @category Settings
+ * @package StatusNet
+ * @author Zach Copley
+ * @copyright 2008-2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR . '/lib/settingsaction.php';
+require_once INSTALLDIR . '/lib/applicationlist.php';
+
+/**
+ * Show a user's registered OAuth applications
+ *
+ * @category Settings
+ * @package StatusNet
+ * @author Zach Copley
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ *
+ * @see SettingsAction
+ */
+
+class OauthappssettingsAction extends SettingsAction
+{
+ var $page = 0;
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+ $this->page = ($this->arg('page')) ? ($this->arg('page') + 0) : 1;
+
+ if (!common_logged_in()) {
+ $this->clientError(_('You must be logged in to list your applications.'));
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Title of the page
+ *
+ * @return string Title of the page
+ */
+
+ function title()
+ {
+ return _('OAuth applications');
+ }
+
+ /**
+ * Instructions for use
+ *
+ * @return instructions for use
+ */
+
+ function getInstructions()
+ {
+ return _('Applications you have registered');
+ }
+
+ /**
+ * Content area of the page
+ *
+ * @return void
+ */
+
+ function showContent()
+ {
+ $user = common_current_user();
+
+ $offset = ($this->page - 1) * APPS_PER_PAGE;
+ $limit = APPS_PER_PAGE + 1;
+
+ $application = new Oauth_application();
+ $application->owner = $user->id;
+ $application->limit($offset, $limit);
+ $application->orderBy('created DESC');
+ $application->find();
+
+ $cnt = 0;
+
+ if ($application) {
+ $al = new ApplicationList($application, $user, $this);
+ $cnt = $al->show();
+ if (0 == $cnt) {
+ $this->showEmptyListMessage();
+ }
+ }
+
+ $this->elementStart('p', array('id' => 'application_register'));
+ $this->element('a',
+ array('href' => common_local_url('newapplication'),
+ 'class' => 'more'
+ ),
+ 'Register a new application');
+ $this->elementEnd('p');
+
+ $this->pagination(
+ $this->page > 1,
+ $cnt > APPS_PER_PAGE,
+ $this->page,
+ 'oauthappssettings'
+ );
+ }
+
+ function showEmptyListMessage()
+ {
+ $message = sprintf(_('You have not registered any applications yet.'));
+
+ $this->elementStart('div', 'guide');
+ $this->raw(common_markup_to_html($message));
+ $this->elementEnd('div');
+ }
+
+ /**
+ * Handle posts to this form
+ *
+ * Based on the button that was pressed, muxes out to other functions
+ * to do the actual task requested.
+ *
+ * All sub-functions reload the form with a message -- success or failure.
+ *
+ * @return void
+ */
+
+ function handlePost()
+ {
+ // CSRF protection
+
+ $token = $this->trimmed('token');
+ if (!$token || $token != common_session_token()) {
+ $this->showForm(_('There was a problem with your session token. '.
+ 'Try again, please.'));
+ return;
+ }
+
+ }
+
+}
diff --git a/actions/oauthconnectionssettings.php b/actions/oauthconnectionssettings.php
new file mode 100644
index 0000000000..c2e8d441b0
--- /dev/null
+++ b/actions/oauthconnectionssettings.php
@@ -0,0 +1,212 @@
+.
+ *
+ * @category Settings
+ * @package StatusNet
+ * @author Zach Copley
+ * @copyright 2008-2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR . '/lib/connectsettingsaction.php';
+require_once INSTALLDIR . '/lib/applicationlist.php';
+
+/**
+ * Show connected OAuth applications
+ *
+ * @category Settings
+ * @package StatusNet
+ * @author Zach Copley
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ *
+ * @see SettingsAction
+ */
+
+class OauthconnectionssettingsAction extends ConnectSettingsAction
+{
+
+ var $page = null;
+ var $id = null;
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+ $this->id = (int)$this->arg('id');
+ $this->page = ($this->arg('page')) ? ($this->arg('page') + 0) : 1;
+ return true;
+ }
+
+ /**
+ * Title of the page
+ *
+ * @return string Title of the page
+ */
+
+ function title()
+ {
+ return _('Connected applications');
+ }
+
+ function isReadOnly($args)
+ {
+ return true;
+ }
+
+ /**
+ * Instructions for use
+ *
+ * @return instructions for use
+ */
+
+ function getInstructions()
+ {
+ return _('You have allowed the following applications to access you account.');
+ }
+
+ /**
+ * Content area of the page
+ *
+ * @return void
+ */
+
+ function showContent()
+ {
+ $user = common_current_user();
+ $profile = $user->getProfile();
+
+ $offset = ($this->page - 1) * APPS_PER_PAGE;
+ $limit = APPS_PER_PAGE + 1;
+
+ $application = $profile->getApplications($offset, $limit);
+
+ $cnt == 0;
+
+ if (!empty($application)) {
+ $al = new ApplicationList($application, $user, $this, true);
+ $cnt = $al->show();
+ }
+
+ if ($cnt == 0) {
+ $this->showEmptyListMessage();
+ }
+
+ $this->pagination($this->page > 1, $cnt > APPS_PER_PAGE,
+ $this->page, 'connectionssettings',
+ array('nickname' => $this->user->nickname));
+ }
+
+ /**
+ * Handle posts to this form
+ *
+ * Based on the button that was pressed, muxes out to other functions
+ * to do the actual task requested.
+ *
+ * All sub-functions reload the form with a message -- success or failure.
+ *
+ * @return void
+ */
+
+ function handlePost()
+ {
+ // CSRF protection
+
+ $token = $this->trimmed('token');
+ if (!$token || $token != common_session_token()) {
+ $this->showForm(_('There was a problem with your session token. '.
+ 'Try again, please.'));
+ return;
+ }
+
+ if ($this->arg('revoke')) {
+ $this->revokeAccess($this->id);
+
+ // XXX: Show some indicator to the user of what's been done.
+
+ $this->showPage();
+ } else {
+ $this->clientError(_('Unexpected form submission.'), 401);
+ return false;
+ }
+ }
+
+ function revokeAccess($appId)
+ {
+ $cur = common_current_user();
+
+ $app = Oauth_application::staticGet('id', $appId);
+
+ if (empty($app)) {
+ $this->clientError(_('No such application.'), 404);
+ return false;
+ }
+
+ $appUser = Oauth_application_user::getByKeys($cur, $app);
+
+ if (empty($appUser)) {
+ $this->clientError(_('You are not a user of that application.'), 401);
+ return false;
+ }
+
+ $orig = clone($appUser);
+ $appUser->access_type = 0; // No access
+ $result = $appUser->update();
+
+ if (!$result) {
+ common_log_db_error($orig, 'UPDATE', __FILE__);
+ $this->clientError(_('Unable to revoke access for app: ' . $app->id));
+ return false;
+ }
+
+ $msg = 'User %s (id: %d) revoked access to app %s (id: %d)';
+ common_log(LOG_INFO, sprintf($msg, $cur->nickname,
+ $cur->id, $app->name, $app->id));
+
+ }
+
+ function showEmptyListMessage()
+ {
+ $message = sprintf(_('You have not authorized any applications to use your account.'));
+
+ $this->elementStart('div', 'guide');
+ $this->raw(common_markup_to_html($message));
+ $this->elementEnd('div');
+ }
+
+ function showSections()
+ {
+ $cur = common_current_user();
+
+ $this->element('h2', null, 'Developers');
+ $this->elementStart('p');
+ $this->raw(_('Developers can edit the registration settings for their applications '));
+ $this->element('a',
+ array('href' => common_local_url('oauthappssettings')),
+ 'here.');
+ $this->elementEnd('p');
+ }
+
+}
diff --git a/actions/outbox.php b/actions/outbox.php
index de30de0183..b81d4b9d0d 100644
--- a/actions/outbox.php
+++ b/actions/outbox.php
@@ -55,10 +55,10 @@ class OutboxAction extends MailboxAction
function title()
{
if ($this->page > 1) {
- return sprintf(_("Outbox for %1$s - page %2$d"),
+ return sprintf(_('Outbox for %1$s - page %2$d'),
$this->user->nickname, $page);
} else {
- return sprintf(_("Outbox for %s"), $this->user->nickname);
+ return sprintf(_('Outbox for %s'), $this->user->nickname);
}
}
diff --git a/actions/pathsadminpanel.php b/actions/pathsadminpanel.php
index 3779fcfaaa..9155a7e428 100644
--- a/actions/pathsadminpanel.php
+++ b/actions/pathsadminpanel.php
@@ -24,7 +24,7 @@
* @author Evan Prodromou
* @author Zach Copley
* @author Sarven Capadisli
- * @copyright 2008-2009 StatusNet, Inc.
+ * @copyright 2008-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/
*/
@@ -98,6 +98,11 @@ class PathsadminpanelAction extends AdminPanelAction
'background' => array('server', 'dir', 'path')
);
+ // XXX: If we're only going to have one boolean on thi page we
+ // can remove some of the boolean processing code --Z
+
+ static $booleans = array('site' => array('fancy'));
+
$values = array();
foreach ($settings as $section => $parts) {
@@ -106,6 +111,12 @@ class PathsadminpanelAction extends AdminPanelAction
}
}
+ foreach ($booleans as $section => $parts) {
+ foreach ($parts as $setting) {
+ $values[$section][$setting] = ($this->boolean($setting)) ? 1 : 0;
+ }
+ }
+
$this->validate($values);
// assert(all values are valid);
@@ -120,7 +131,13 @@ class PathsadminpanelAction extends AdminPanelAction
}
}
- $config->query('COMMIT');
+ foreach ($booleans as $section => $parts) {
+ foreach ($parts as $setting) {
+ Config::save($section, $setting, $values[$section][$setting]);
+ }
+ }
+
+ $config->query('COMMIT');
return;
}
@@ -213,10 +230,14 @@ class PathsAdminPanelForm extends AdminForm
function formData()
{
- $this->out->elementStart('fieldset', array('id' => 'settings_paths_locale'));
+ $this->out->elementStart('fieldset', array('id' => 'settings_paths_locale'));
$this->out->element('legend', null, _('Site'), 'site');
$this->out->elementStart('ul', 'form_data');
+ $this->li();
+ $this->input('server', _('Server'), _('Site\'s server hostname.'));
+ $this->unli();
+
$this->li();
$this->input('path', _('Path'), _('Site path'));
$this->unli();
@@ -225,6 +246,12 @@ class PathsAdminPanelForm extends AdminForm
$this->input('locale_path', _('Path to locales'), _('Directory path to locales'), 'site');
$this->unli();
+ $this->li();
+ $this->out->checkbox('fancy', _('Fancy URLs'),
+ (bool) $this->value('fancy'),
+ _('Use fancy (more readable and memorable) URLs?'));
+ $this->unli();
+
$this->out->elementEnd('ul');
$this->out->elementEnd('fieldset');
diff --git a/actions/replies.php b/actions/replies.php
index 2e50f1c3c4..164c328db3 100644
--- a/actions/replies.php
+++ b/actions/replies.php
@@ -124,7 +124,7 @@ class RepliesAction extends OwnerDesignAction
if ($this->page == 1) {
return sprintf(_("Replies to %s"), $this->user->nickname);
} else {
- return sprintf(_("Replies to %1$s, page %2$d"),
+ return sprintf(_('Replies to %1$s, page %2$d'),
$this->user->nickname,
$this->page);
}
diff --git a/actions/showapplication.php b/actions/showapplication.php
new file mode 100644
index 0000000000..a6ff425c7c
--- /dev/null
+++ b/actions/showapplication.php
@@ -0,0 +1,327 @@
+.
+ *
+ * @category Application
+ * @package StatusNet
+ * @author Zach Copley
+ * @copyright 2008-2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+/**
+ * Show an OAuth application
+ *
+ * @category Application
+ * @package StatusNet
+ * @author Zach Copley
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class ShowApplicationAction extends OwnerDesignAction
+{
+ /**
+ * Application to show
+ */
+
+ var $application = null;
+
+ /**
+ * User who owns the app
+ */
+
+ var $owner = null;
+
+ var $msg = null;
+
+ var $success = null;
+
+ /**
+ * Load attributes based on database arguments
+ *
+ * Loads all the DB stuff
+ *
+ * @param array $args $_REQUEST array
+ *
+ * @return success flag
+ */
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+
+ $id = (int)$this->arg('id');
+
+ $this->application = Oauth_application::staticGet($id);
+ $this->owner = User::staticGet($this->application->owner);
+
+ if (!common_logged_in()) {
+ $this->clientError(_('You must be logged in to view an application.'));
+ return false;
+ }
+
+ if (empty($this->application)) {
+ $this->clientError(_('No such application.'), 404);
+ return false;
+ }
+
+ $cur = common_current_user();
+
+ if ($cur->id != $this->owner->id) {
+ $this->clientError(_('You are not the owner of this application.'), 401);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Handle the request
+ *
+ * Shows info about the app
+ *
+ * @return void
+ */
+
+ function handle($args)
+ {
+ parent::handle($args);
+
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+
+ // CSRF protection
+ $token = $this->trimmed('token');
+ if (!$token || $token != common_session_token()) {
+ $this->clientError(_('There was a problem with your session token.'));
+ return;
+ }
+
+ if ($this->arg('reset')) {
+ $this->resetKey();
+ }
+ } else {
+ $this->showPage();
+ }
+ }
+
+ /**
+ * Title of the page
+ *
+ * @return string title of the page
+ */
+
+ function title()
+ {
+ if (!empty($this->application->name)) {
+ return 'Application: ' . $this->application->name;
+ }
+ }
+
+ function showPageNotice()
+ {
+ if (!empty($this->msg)) {
+ $this->element('div', ($this->success) ? 'success' : 'error', $this->msg);
+ }
+ }
+
+ function showContent()
+ {
+
+ $cur = common_current_user();
+
+ $consumer = $this->application->getConsumer();
+
+ $this->elementStart('div', 'entity_profile vcard');
+ $this->element('h2', null, _('Application profile'));
+ $this->elementStart('dl', 'entity_depiction');
+ $this->element('dt', null, _('Icon'));
+ $this->elementStart('dd');
+ if (!empty($this->application->icon)) {
+ $this->element('img', array('src' => $this->application->icon,
+ 'class' => 'photo logo'));
+ }
+ $this->elementEnd('dd');
+ $this->elementEnd('dl');
+
+ $this->elementStart('dl', 'entity_fn');
+ $this->element('dt', null, _('Name'));
+ $this->elementStart('dd');
+ $this->element('a', array('href' => $this->application->source_url,
+ 'class' => 'url fn'),
+ $this->application->name);
+ $this->elementEnd('dd');
+ $this->elementEnd('dl');
+
+ $this->elementStart('dl', 'entity_org');
+ $this->element('dt', null, _('Organization'));
+ $this->elementStart('dd');
+ $this->element('a', array('href' => $this->application->homepage,
+ 'class' => 'url'),
+ $this->application->organization);
+ $this->elementEnd('dd');
+ $this->elementEnd('dl');
+
+ $this->elementStart('dl', 'entity_note');
+ $this->element('dt', null, _('Description'));
+ $this->element('dd', 'note', $this->application->description);
+ $this->elementEnd('dl');
+
+ $this->elementStart('dl', 'entity_statistics');
+ $this->element('dt', null, _('Statistics'));
+ $this->elementStart('dd');
+ $defaultAccess = ($this->application->access_type & Oauth_application::$writeAccess)
+ ? 'read-write' : 'read-only';
+ $profile = Profile::staticGet($this->application->owner);
+
+ $appUsers = new Oauth_application_user();
+ $appUsers->application_id = $this->application->id;
+ $userCnt = $appUsers->count();
+
+ $this->raw(sprintf(
+ _('created by %1$s - %2$s access by default - %3$d users'),
+ $profile->getBestName(),
+ $defaultAccess,
+ $userCnt
+ ));
+ $this->elementEnd('dd');
+ $this->elementEnd('dl');
+ $this->elementEnd('div');
+
+ $this->elementStart('div', 'entity_actions');
+ $this->element('h2', null, _('Application actions'));
+ $this->elementStart('ul');
+ $this->elementStart('li', 'entity_edit');
+ $this->element('a',
+ array('href' => common_local_url('editapplication',
+ array('id' => $this->application->id))),
+ 'Edit');
+ $this->elementEnd('li');
+
+ $this->elementStart('li', 'entity_reset_keysecret');
+ $this->elementStart('form', array(
+ 'id' => 'forma_reset_key',
+ 'class' => 'form_reset_key',
+ 'method' => 'POST',
+ 'action' => common_local_url('showapplication',
+ array('id' => $this->application->id))));
+
+ $this->elementStart('fieldset');
+ $this->hidden('token', common_session_token());
+ $this->submit('reset', _('Reset key & secret'));
+ $this->elementEnd('fieldset');
+ $this->elementEnd('form');
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+ $this->elementEnd('div');
+
+ $this->elementStart('div', 'entity_data');
+ $this->element('h2', null, _('Application info'));
+ $this->elementStart('dl', 'entity_consumer_key');
+ $this->element('dt', null, _('Consumer key'));
+ $this->element('dd', null, $consumer->consumer_key);
+ $this->elementEnd('dl');
+
+ $this->elementStart('dl', 'entity_consumer_secret');
+ $this->element('dt', null, _('Consumer secret'));
+ $this->element('dd', null, $consumer->consumer_secret);
+ $this->elementEnd('dl');
+
+ $this->elementStart('dl', 'entity_request_token_url');
+ $this->element('dt', null, _('Request token URL'));
+ $this->element('dd', null, common_local_url('apioauthrequesttoken'));
+ $this->elementEnd('dl');
+
+ $this->elementStart('dl', 'entity_access_token_url');
+ $this->element('dt', null, _('Access token URL'));
+ $this->element('dd', null, common_local_url('apioauthaccesstoken'));
+ $this->elementEnd('dl');
+
+ $this->elementStart('dl', 'entity_authorize_url');
+ $this->element('dt', null, _('Authorize URL'));
+ $this->element('dd', null, common_local_url('apioauthauthorize'));
+ $this->elementEnd('dl');
+
+ $this->element('p', 'note',
+ _('Note: We support HMAC-SHA1 signatures. We do not support the plaintext signature method.'));
+ $this->elementEnd('div');
+
+ $this->elementStart('p', array('id' => 'application_action'));
+ $this->element('a',
+ array('href' => common_local_url('oauthappssettings'),
+ 'class' => 'more'),
+ 'View your applications');
+ $this->elementEnd('p');
+ }
+
+ function resetKey()
+ {
+ $this->application->query('BEGIN');
+
+ $consumer = $this->application->getConsumer();
+ $result = $consumer->delete();
+
+ if (!$result) {
+ common_log_db_error($consumer, 'DELETE', __FILE__);
+ $this->success = false;
+ $this->msg = ('Unable to reset consumer key and secret.');
+ $this->showPage();
+ return;
+ }
+
+ $consumer = Consumer::generateNew();
+
+ $result = $consumer->insert();
+
+ if (!$result) {
+ common_log_db_error($consumer, 'INSERT', __FILE__);
+ $this->application->query('ROLLBACK');
+ $this->success = false;
+ $this->msg = ('Unable to reset consumer key and secret.');
+ $this->showPage();
+ return;
+ }
+
+ $orig = clone($this->application);
+ $this->application->consumer_key = $consumer->consumer_key;
+ $result = $this->application->update($orig);
+
+ if (!$result) {
+ common_log_db_error($application, 'UPDATE', __FILE__);
+ $this->application->query('ROLLBACK');
+ $this->success = false;
+ $this->msg = ('Unable to reset consumer key and secret.');
+ $this->showPage();
+ return;
+ }
+
+ $this->application->query('COMMIT');
+
+ $this->success = true;
+ $this->msg = ('Consumer key and secret reset.');
+ $this->showPage();
+ }
+
+}
diff --git a/actions/showfavorites.php b/actions/showfavorites.php
index 6023f01567..f2d0822936 100644
--- a/actions/showfavorites.php
+++ b/actions/showfavorites.php
@@ -74,9 +74,9 @@ class ShowfavoritesAction extends OwnerDesignAction
function title()
{
if ($this->page == 1) {
- return sprintf(_("%s's favorite notices"), $this->user->nickname);
+ return sprintf(_('%s\'s favorite notices'), $this->user->nickname);
} else {
- return sprintf(_("%1$s's favorite notices, page %2$d"),
+ return sprintf(_('%1$s\'s favorite notices, page %2$d'),
$this->user->nickname,
$this->page);
}
diff --git a/actions/showgroup.php b/actions/showgroup.php
index 06ae572e81..8042a49513 100644
--- a/actions/showgroup.php
+++ b/actions/showgroup.php
@@ -79,9 +79,9 @@ class ShowgroupAction extends GroupDesignAction
}
if ($this->page == 1) {
- return sprintf(_("%s group"), $base);
+ return sprintf(_('%s group'), $base);
} else {
- return sprintf(_("%1$s group, page %2$d"),
+ return sprintf(_('%1$s group, page %2$d'),
$base,
$this->page);
}
diff --git a/actions/showstream.php b/actions/showstream.php
index 75e10858d0..90ff67073a 100644
--- a/actions/showstream.php
+++ b/actions/showstream.php
@@ -76,7 +76,7 @@ class ShowstreamAction extends ProfileAction
if ($this->page == 1) {
return $base;
} else {
- return sprintf(_("%1$s, page %2$d"),
+ return sprintf(_('%1$s, page %2$d'),
$base,
$this->page);
}
diff --git a/actions/siteadminpanel.php b/actions/siteadminpanel.php
index dd388a18a2..8c8f8b3742 100644
--- a/actions/siteadminpanel.php
+++ b/actions/siteadminpanel.php
@@ -24,7 +24,7 @@
* @author Evan Prodromou
* @author Zach Copley
* @author Sarven Capadisli
- * @copyright 2008-2009 StatusNet, Inc.
+ * @copyright 2008-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/
*/
@@ -95,8 +95,6 @@ class SiteadminpanelAction extends AdminPanelAction
'site', 'textlimit', 'dupelimit'),
'snapshot' => array('run', 'reporturl', 'frequency'));
- static $booleans = array('site' => array('private', 'inviteonly', 'closed', 'fancy'));
-
$values = array();
foreach ($settings as $section => $parts) {
@@ -105,12 +103,6 @@ class SiteadminpanelAction extends AdminPanelAction
}
}
- foreach ($booleans as $section => $parts) {
- foreach ($parts as $setting) {
- $values[$section][$setting] = ($this->boolean($setting)) ? 1 : 0;
- }
- }
-
// This throws an exception on validation errors
$this->validate($values);
@@ -127,12 +119,6 @@ class SiteadminpanelAction extends AdminPanelAction
}
}
- foreach ($booleans as $section => $parts) {
- foreach ($parts as $setting) {
- Config::save($section, $setting, $values[$section][$setting]);
- }
- }
-
$config->query('COMMIT');
return;
@@ -299,44 +285,6 @@ class SiteAdminPanelForm extends AdminForm
$this->out->elementEnd('ul');
$this->out->elementEnd('fieldset');
- $this->out->elementStart('fieldset', array('id' => 'settings_admin_urls'));
- $this->out->element('legend', null, _('URLs'));
- $this->out->elementStart('ul', 'form_data');
- $this->li();
- $this->input('server', _('Server'), _('Site\'s server hostname.'));
- $this->unli();
-
- $this->li();
- $this->out->checkbox('fancy', _('Fancy URLs'),
- (bool) $this->value('fancy'),
- _('Use fancy (more readable and memorable) URLs?'));
- $this->unli();
- $this->out->elementEnd('ul');
- $this->out->elementEnd('fieldset');
-
- $this->out->elementStart('fieldset', array('id' => 'settings_admin_access'));
- $this->out->element('legend', null, _('Access'));
- $this->out->elementStart('ul', 'form_data');
- $this->li();
- $this->out->checkbox('private', _('Private'),
- (bool) $this->value('private'),
- _('Prohibit anonymous users (not logged in) from viewing site?'));
- $this->unli();
-
- $this->li();
- $this->out->checkbox('inviteonly', _('Invite only'),
- (bool) $this->value('inviteonly'),
- _('Make registration invitation only.'));
- $this->unli();
-
- $this->li();
- $this->out->checkbox('closed', _('Closed'),
- (bool) $this->value('closed'),
- _('Disable new registrations.'));
- $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');
diff --git a/actions/tag.php b/actions/tag.php
index 12857236ef..e91df6ea97 100644
--- a/actions/tag.php
+++ b/actions/tag.php
@@ -63,9 +63,9 @@ class TagAction extends Action
function title()
{
if ($this->page == 1) {
- return sprintf(_("Notices tagged with %s"), $this->tag);
+ return sprintf(_('Notices tagged with %s'), $this->tag);
} else {
- return sprintf(_("Notices tagged with %1$s, page %2$d"),
+ return sprintf(_('Notices tagged with %1$s, page %2$d'),
$this->tag,
$this->page);
}
@@ -85,7 +85,7 @@ class TagAction extends Action
array('tag' => $this->tag)),
sprintf(_('Notice feed for tag %s (RSS 1.0)'),
$this->tag)),
- new Feed(Feed::RSS2,
+ new Feed(Feed::RSS2,
common_local_url('ApiTimelineTag',
array('format' => 'rss',
'tag' => $this->tag)),
diff --git a/actions/usergroups.php b/actions/usergroups.php
index 5042261432..97faabae65 100644
--- a/actions/usergroups.php
+++ b/actions/usergroups.php
@@ -59,9 +59,9 @@ class UsergroupsAction extends OwnerDesignAction
function title()
{
if ($this->page == 1) {
- return sprintf(_("%s groups"), $this->user->nickname);
+ return sprintf(_('%s groups'), $this->user->nickname);
} else {
- return sprintf(_("%1$s groups, page %2$d"),
+ return sprintf(_('%1$s groups, page %2$d'),
$this->user->nickname,
$this->page);
}
diff --git a/classes/Consumer.php b/classes/Consumer.php
index d5b7b7e33a..ad64a8491b 100644
--- a/classes/Consumer.php
+++ b/classes/Consumer.php
@@ -4,16 +4,17 @@
*/
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
-class Consumer extends Memcached_DataObject
+class Consumer extends Memcached_DataObject
{
###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */
public $__table = 'consumer'; // table name
public $consumer_key; // varchar(255) primary_key not_null
+ public $consumer_secret; // varchar(255) not_null
public $seed; // char(32) not_null
- public $created; // datetime() not_null
- public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
+ public $created; // datetime not_null
+ public $modified; // timestamp not_null default_CURRENT_TIMESTAMP
/* Static get */
function staticGet($k,$v=null)
@@ -21,4 +22,18 @@ class Consumer extends Memcached_DataObject
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
+
+ static function generateNew()
+ {
+ $cons = new Consumer();
+ $rand = common_good_rand(16);
+
+ $cons->seed = $rand;
+ $cons->consumer_key = md5(time() + $rand);
+ $cons->consumer_secret = md5(md5(time() + time() + $rand));
+ $cons->created = common_sql_now();
+
+ return $cons;
+ }
+
}
diff --git a/classes/Memcached_DataObject.php b/classes/Memcached_DataObject.php
index 2c9dcf5953..2cc6377f83 100644
--- a/classes/Memcached_DataObject.php
+++ b/classes/Memcached_DataObject.php
@@ -66,7 +66,6 @@ class Memcached_DataObject extends DB_DataObject
// Clear this out so we don't accidentally break global
// state in *this* process.
$this->_DB_resultid = null;
-
// We don't have any local DBO refs, so clear these out.
$this->_link_loaded = false;
}
@@ -91,9 +90,7 @@ class Memcached_DataObject extends DB_DataObject
unset($i);
}
$i = Memcached_DataObject::getcached($cls, $k, $v);
- if ($i) {
- return $i;
- } else {
+ if ($i === false) { // false == cache miss
$i = DB_DataObject::factory($cls);
if (empty($i)) {
$i = false;
@@ -101,22 +98,34 @@ class Memcached_DataObject extends DB_DataObject
}
$result = $i->get($k, $v);
if ($result) {
+ // Hit!
$i->encache();
- return $i;
} else {
+ // save the fact that no such row exists
+ $c = self::memcache();
+ if (!empty($c)) {
+ $ck = self::cachekey($cls, $k, $v);
+ $c->set($ck, null);
+ }
$i = false;
- return $i;
}
}
+ return $i;
}
- function &pkeyGet($cls, $kv)
+ /**
+ * @fixme Should this return false on lookup fail to match staticGet?
+ */
+ function pkeyGet($cls, $kv)
{
$i = Memcached_DataObject::multicache($cls, $kv);
- if ($i) {
+ if ($i !== false) { // false == cache miss
return $i;
} else {
- $i = new $cls();
+ $i = DB_DataObject::factory($cls);
+ if (empty($i)) {
+ return false;
+ }
foreach ($kv as $k => $v) {
$i->$k = $v;
}
@@ -124,6 +133,11 @@ class Memcached_DataObject extends DB_DataObject
$i->encache();
} else {
$i = null;
+ $c = self::memcache();
+ if (!empty($c)) {
+ $ck = self::multicacheKey($cls, $kv);
+ $c->set($ck, null);
+ }
}
return $i;
}
@@ -132,6 +146,9 @@ class Memcached_DataObject extends DB_DataObject
function insert()
{
$result = parent::insert();
+ if ($result) {
+ $this->encache(); // in case of cached negative lookups
+ }
return $result;
}
@@ -186,6 +203,17 @@ class Memcached_DataObject extends DB_DataObject
function keyTypes()
{
+ // ini-based classes return number-indexed arrays. handbuilt
+ // classes return column => keytype. Make this uniform.
+
+ $keys = $this->keys();
+
+ $keyskeys = array_keys($keys);
+
+ if (is_string($keyskeys[0])) {
+ return $keys;
+ }
+
global $_DB_DATAOBJECT;
if (!isset($_DB_DATAOBJECT['INI'][$this->_database][$this->__table."__keys"])) {
$this->databaseStructure();
@@ -197,6 +225,7 @@ class Memcached_DataObject extends DB_DataObject
function encache()
{
$c = $this->memcache();
+
if (!$c) {
return false;
} else if ($this->tableName() == 'user' && is_object($this->id)) {
@@ -206,64 +235,86 @@ class Memcached_DataObject extends DB_DataObject
str_replace("\n", " ", $e->getTraceAsString()));
return false;
} else {
- $pkey = array();
- $pval = array();
- $types = $this->keyTypes();
- ksort($types);
- foreach ($types as $key => $type) {
- if ($type == 'K') {
- $pkey[] = $key;
- $pval[] = $this->$key;
- } else {
- $c->set($this->cacheKey($this->tableName(), $key, $this->$key), $this);
- }
- }
- # XXX: should work for both compound and scalar pkeys
- $pvals = implode(',', $pval);
- $pkeys = implode(',', $pkey);
- $c->set($this->cacheKey($this->tableName(), $pkeys, $pvals), $this);
+ $keys = $this->_allCacheKeys();
+
+ foreach ($keys as $key) {
+ $c->set($key, $this);
+ }
}
}
function decache()
{
$c = $this->memcache();
+
if (!$c) {
return false;
- } else {
- $pkey = array();
- $pval = array();
- $types = $this->keyTypes();
- ksort($types);
- foreach ($types as $key => $type) {
- if ($type == 'K') {
- $pkey[] = $key;
- $pval[] = $this->$key;
- } else {
- $c->delete($this->cacheKey($this->tableName(), $key, $this->$key));
- }
- }
- # should work for both compound and scalar pkeys
- # XXX: comma works for now but may not be safe separator for future keys
- $pvals = implode(',', $pval);
- $pkeys = implode(',', $pkey);
- $c->delete($this->cacheKey($this->tableName(), $pkeys, $pvals));
}
+
+ $keys = $this->_allCacheKeys();
+
+ foreach ($keys as $key) {
+ $c->delete($key, $this);
+ }
+ }
+
+ function _allCacheKeys()
+ {
+ $ckeys = array();
+
+ $types = $this->keyTypes();
+ ksort($types);
+
+ $pkey = array();
+ $pval = array();
+
+ foreach ($types as $key => $type) {
+
+ assert(!empty($key));
+
+ if ($type == 'U') {
+ if (empty($this->$key)) {
+ continue;
+ }
+ $ckeys[] = $this->cacheKey($this->tableName(), $key, $this->$key);
+ } else if ($type == 'K' || $type == 'N') {
+ $pkey[] = $key;
+ $pval[] = $this->$key;
+ } else {
+ throw new Exception("Unknown key type $key => $type for " . $this->tableName());
+ }
+ }
+
+ assert(count($pkey) > 0);
+
+ // XXX: should work for both compound and scalar pkeys
+ $pvals = implode(',', $pval);
+ $pkeys = implode(',', $pkey);
+
+ $ckeys[] = $this->cacheKey($this->tableName(), $pkeys, $pvals);
+
+ return $ckeys;
}
function multicache($cls, $kv)
{
ksort($kv);
- $c = Memcached_DataObject::memcache();
+ $c = self::memcache();
if (!$c) {
return false;
} else {
- $pkeys = implode(',', array_keys($kv));
- $pvals = implode(',', array_values($kv));
- return $c->get(Memcached_DataObject::cacheKey($cls, $pkeys, $pvals));
+ return $c->get(self::multicacheKey($cls, $kv));
}
}
+ static function multicacheKey($cls, $kv)
+ {
+ ksort($kv);
+ $pkeys = implode(',', array_keys($kv));
+ $pvals = implode(',', array_values($kv));
+ return self::cacheKey($cls, $pkeys, $pvals);
+ }
+
function getSearchEngine($table)
{
require_once INSTALLDIR.'/lib/search_engines.php';
@@ -298,7 +349,8 @@ class Memcached_DataObject extends DB_DataObject
$key_part = common_keyize($cls).':'.md5($qry);
$ckey = common_cache_key($key_part);
$stored = $c->get($ckey);
- if ($stored) {
+
+ if ($stored !== false) {
return new ArrayWrapper($stored);
}
@@ -313,6 +365,39 @@ class Memcached_DataObject extends DB_DataObject
return new ArrayWrapper($cached);
}
+ /**
+ * sends query to database - this is the private one that must work
+ * - internal functions use this rather than $this->query()
+ *
+ * Overridden to do logging.
+ *
+ * @param string $string
+ * @access private
+ * @return mixed none or PEAR_Error
+ */
+ function _query($string)
+ {
+ $start = microtime(true);
+ $result = parent::_query($string);
+ $delta = microtime(true) - $start;
+
+ $limit = common_config('db', 'log_slow_queries');
+ if (($limit > 0 && $delta >= $limit) || common_config('db', 'log_queries')) {
+ $clean = $this->sanitizeQuery($string);
+ common_log(LOG_DEBUG, sprintf("DB query (%0.3fs): %s", $delta, $clean));
+ }
+ return $result;
+ }
+
+ // Sanitize a query for logging
+ // @fixme don't trim spaces in string literals
+ function sanitizeQuery($string)
+ {
+ $string = preg_replace('/\s+/', ' ', $string);
+ $string = trim($string);
+ return $string;
+ }
+
// We overload so that 'SET NAMES "utf8"' is called for
// each connection
diff --git a/classes/Notice.php b/classes/Notice.php
index 0966697e21..6b364fb5ce 100644
--- a/classes/Notice.php
+++ b/classes/Notice.php
@@ -326,9 +326,13 @@ class Notice extends Memcached_DataObject
# XXX: someone clever could prepend instead of clearing the cache
$notice->blowOnInsert();
- $qm = QueueManager::get();
-
- $qm->enqueue($notice, 'distrib');
+ if (common_config('queue', 'inboxes')) {
+ $qm = QueueManager::get();
+ $qm->enqueue($notice, 'distrib');
+ } else {
+ $handler = new DistribQueueHandler();
+ $handler->handle($notice);
+ }
return $notice;
}
diff --git a/classes/Oauth_application.php b/classes/Oauth_application.php
new file mode 100644
index 0000000000..a6b5390872
--- /dev/null
+++ b/classes/Oauth_application.php
@@ -0,0 +1,140 @@
+consumer_key);
+ }
+
+ static function maxDesc()
+ {
+ $desclimit = common_config('application', 'desclimit');
+ // null => use global limit (distinct from 0!)
+ if (is_null($desclimit)) {
+ $desclimit = common_config('site', 'textlimit');
+ }
+ return $desclimit;
+ }
+
+ static function descriptionTooLong($desc)
+ {
+ $desclimit = self::maxDesc();
+ return ($desclimit > 0 && !empty($desc) && (mb_strlen($desc) > $desclimit));
+ }
+
+ function setAccessFlags($read, $write)
+ {
+ if ($read) {
+ $this->access_type |= self::$readAccess;
+ } else {
+ $this->access_type &= ~self::$readAccess;
+ }
+
+ if ($write) {
+ $this->access_type |= self::$writeAccess;
+ } else {
+ $this->access_type &= ~self::$writeAccess;
+ }
+ }
+
+ function setOriginal($filename)
+ {
+ $imagefile = new ImageFile($this->id, Avatar::path($filename));
+
+ // XXX: Do we want to have a bunch of different size icons? homepage, stream, mini?
+ // or just one and control size via CSS? --Zach
+
+ $orig = clone($this);
+ $this->icon = Avatar::url($filename);
+ common_debug(common_log_objstring($this));
+ return $this->update($orig);
+ }
+
+ static function getByConsumerKey($key)
+ {
+ if (empty($key)) {
+ return null;
+ }
+
+ $app = new Oauth_application();
+ $app->consumer_key = $key;
+ $app->limit(1);
+ $result = $app->find(true);
+
+ return empty($result) ? null : $app;
+ }
+
+ /**
+ * Handle an image upload
+ *
+ * Does all the magic for handling an image upload, and crops the
+ * image by default.
+ *
+ * @return void
+ */
+
+ function uploadLogo()
+ {
+ if ($_FILES['app_icon']['error'] ==
+ UPLOAD_ERR_OK) {
+
+ try {
+ $imagefile = ImageFile::fromUpload('app_icon');
+ } catch (Exception $e) {
+ common_debug("damn that sucks");
+ $this->showForm($e->getMessage());
+ return;
+ }
+
+ $filename = Avatar::filename($this->id,
+ image_type_to_extension($imagefile->type),
+ null,
+ 'oauth-app-icon-'.common_timestamp());
+
+ $filepath = Avatar::path($filename);
+
+ move_uploaded_file($imagefile->filepath, $filepath);
+
+ $this->setOriginal($filename);
+ }
+ }
+
+}
diff --git a/classes/Oauth_application_user.php b/classes/Oauth_application_user.php
new file mode 100644
index 0000000000..57986281f9
--- /dev/null
+++ b/classes/Oauth_application_user.php
@@ -0,0 +1,44 @@
+profile_id = $user->id;
+ $oau->application_id = $app->id;
+ $oau->limit(1);
+
+ $result = $oau->find(true);
+
+ return empty($result) ? null : $oau;
+ }
+
+}
diff --git a/classes/Profile.php b/classes/Profile.php
index 25d908dbf9..1076fb2cb3 100644
--- a/classes/Profile.php
+++ b/classes/Profile.php
@@ -352,6 +352,31 @@ class Profile extends Memcached_DataObject
return $profile;
}
+ function getApplications($offset = 0, $limit = null)
+ {
+ $qry =
+ 'SELECT a.* ' .
+ 'FROM oauth_application_user u, oauth_application a ' .
+ 'WHERE u.profile_id = %d ' .
+ 'AND a.id = u.application_id ' .
+ 'AND u.access_type > 0 ' .
+ 'ORDER BY u.created DESC ';
+
+ if ($offset > 0) {
+ if (common_config('db','type') == 'pgsql') {
+ $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
+ } else {
+ $qry .= ' LIMIT ' . $offset . ', ' . $limit;
+ }
+ }
+
+ $application = new Oauth_application();
+
+ $cnt = $application->query(sprintf($qry, $this->id));
+
+ return $application;
+ }
+
function subscriptionCount()
{
$c = common_memcache();
diff --git a/classes/Status_network.php b/classes/Status_network.php
index 445f8a5a3c..4bda24b6a0 100644
--- a/classes/Status_network.php
+++ b/classes/Status_network.php
@@ -39,9 +39,19 @@ class Status_network extends DB_DataObject
public $logo; // varchar(255)
public $created; // datetime() not_null
public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
+ public $tags; // text
/* Static get */
- function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('Status_network',$k,$v); }
+ function staticGet($k,$v=NULL) {
+ $i = DB_DataObject::staticGet('Status_network',$k,$v);
+
+ // Don't use local process cache; if we're fetching multiple
+ // times it's because we're reloading it in a long-running
+ // process; we need a fresh copy!
+ global $_DB_DATAOBJECT;
+ unset($_DB_DATAOBJECT['CACHE']['status_network']);
+ return $i;
+ }
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
@@ -245,4 +255,23 @@ class Status_network extends DB_DataObject
return $this->nickname . '.' . self::$wildcard;
}
}
+
+ /**
+ * Return site meta-info tags as an array
+ * @return array of strings
+ */
+ function getTags()
+ {
+ return array_filter(explode("|", strval($this->tags)));
+ }
+
+ /**
+ * Check if this site record has a particular meta-info tag attached.
+ * @param string $tag
+ * @return bool
+ */
+ function hasTag($tag)
+ {
+ return in_array($tag, $this->getTags());
+ }
}
diff --git a/classes/Token.php b/classes/Token.php
index 1fabd72f13..a129d1fd11 100644
--- a/classes/Token.php
+++ b/classes/Token.php
@@ -4,7 +4,7 @@
*/
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
-class Token extends Memcached_DataObject
+class Token extends Memcached_DataObject
{
###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */
@@ -14,7 +14,9 @@ class Token extends Memcached_DataObject
public $tok; // char(32) primary_key not_null
public $secret; // char(32) not_null
public $type; // tinyint(1) not_null
- public $state; // tinyint(1)
+ public $state; // tinyint(1)
+ public $verifier; // varchar(255)
+ public $verified_callback; // varchar(255)
public $created; // datetime() not_null
public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
diff --git a/classes/statusnet.ini b/classes/statusnet.ini
index 6ce4495be0..6203650a69 100644
--- a/classes/statusnet.ini
+++ b/classes/statusnet.ini
@@ -39,6 +39,7 @@ code = K
[consumer]
consumer_key = 130
+consumer_secret = 130
seed = 130
created = 142
modified = 384
@@ -348,6 +349,37 @@ created = 142
tag = K
notice_id = K
+[oauth_application]
+id = 129
+owner = 129
+consumer_key = 130
+name = 130
+description = 2
+icon = 130
+source_url = 2
+organization = 2
+homepage = 2
+callback_url = 130
+type = 17
+access_type = 17
+created = 142
+modified = 384
+
+[oauth_application__keys]
+id = N
+
+[oauth_application_user]
+profile_id = 129
+application_id = 129
+access_type = 17
+token = 2
+created = 142
+modified = 384
+
+[oauth_application_user__keys]
+profile_id = K
+application_id = K
+
[profile]
id = 129
nickname = 130
@@ -484,6 +516,8 @@ tok = 130
secret = 130
type = 145
state = 17
+verifier = 2
+verified_callback = 2
created = 142
modified = 384
diff --git a/db/rc3to09.sql b/db/rc3to09.sql
deleted file mode 100644
index 02dc7a6e2e..0000000000
--- a/db/rc3to09.sql
+++ /dev/null
@@ -1,16 +0,0 @@
-create table queue_item_new (
- id integer auto_increment primary key comment 'unique identifier',
- frame blob not null comment 'data: object reference or opaque string',
- transport varchar(8) not null comment 'queue for what? "email", "jabber", "sms", "irc", ...',
- created datetime not null comment 'date this record was created',
- claimed datetime comment 'date this item was claimed',
-
- index queue_item_created_idx (created)
-
-) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
-
-insert into queue_item_new (frame,transport,created,claimed)
- select notice_id,transport,created,claimed from queue_item;
-alter table queue_item rename to queue_item_old;
-alter table queue_item_new rename to queue_item;
-
diff --git a/db/rc3torc4.sql b/db/rc3torc4.sql
new file mode 100644
index 0000000000..917c1f1c41
--- /dev/null
+++ b/db/rc3torc4.sql
@@ -0,0 +1,50 @@
+create table queue_item_new (
+ id integer auto_increment primary key comment 'unique identifier',
+ frame blob not null comment 'data: object reference or opaque string',
+ transport varchar(8) not null comment 'queue for what? "email", "jabber", "sms", "irc", ...',
+ created datetime not null comment 'date this record was created',
+ claimed datetime comment 'date this item was claimed',
+
+ index queue_item_created_idx (created)
+
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+
+insert into queue_item_new (frame,transport,created,claimed)
+ select notice_id,transport,created,claimed from queue_item;
+alter table queue_item rename to queue_item_old;
+alter table queue_item_new rename to queue_item;
+
+alter table consumer
+ add consumer_secret varchar(255) not null comment 'secret value';
+
+alter table token
+ add verifier varchar(255) comment 'verifier string for OAuth 1.0a',
+ add verified_callback varchar(255) comment 'verified callback URL for OAuth 1.0a';
+
+create table oauth_application (
+ id integer auto_increment primary key comment 'unique identifier',
+ owner integer not null comment 'owner of the application' references profile (id),
+ consumer_key varchar(255) not null comment 'application consumer key' references consumer (consumer_key),
+ name varchar(255) not null comment 'name of the application',
+ description varchar(255) comment 'description of the application',
+ icon varchar(255) not null comment 'application icon',
+ source_url varchar(255) comment 'application homepage - used for source link',
+ organization varchar(255) comment 'name of the organization running the application',
+ homepage varchar(255) comment 'homepage for the organization',
+ callback_url varchar(255) comment 'url to redirect to after authentication',
+ type tinyint default 0 comment 'type of app, 1 = browser, 2 = desktop',
+ access_type tinyint default 0 comment 'default access type, bit 1 = read, bit 2 = write',
+ created datetime not null comment 'date this record was created',
+ modified timestamp comment 'date this record was modified'
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+
+create table oauth_application_user (
+ profile_id integer not null comment 'user of the application' references profile (id),
+ application_id integer not null comment 'id of the application' references oauth_application (id),
+ access_type tinyint default 0 comment 'access type, bit 1 = read, bit 2 = write, bit 3 = revoked',
+ token varchar(255) comment 'request or access token',
+ created datetime not null comment 'date this record was created',
+ modified timestamp comment 'date this record was modified',
+ constraint primary key (profile_id, application_id)
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+
diff --git a/db/site.sql b/db/site.sql
index a9f64e5a5d..791303bd54 100644
--- a/db/site.sql
+++ b/db/site.sql
@@ -14,6 +14,8 @@ create table status_network (
sitename varchar(255) comment 'display name',
theme varchar(255) comment 'theme name',
logo varchar(255) comment 'site logo',
+
+ tags text comment 'site meta-info tags (pipe-separated)',
created datetime not null comment 'date this record was created',
modified timestamp comment 'date this record was modified'
diff --git a/db/statusnet.sql b/db/statusnet.sql
index a9ed66cb4f..17de4fd0d4 100644
--- a/db/statusnet.sql
+++ b/db/statusnet.sql
@@ -176,6 +176,7 @@ create table fave (
create table consumer (
consumer_key varchar(255) primary key comment 'unique identifier, root URL',
+ consumer_secret varchar(255) not null comment 'secret value',
seed char(32) not null comment 'seed for new tokens by this consumer',
created datetime not null comment 'date this record was created',
@@ -188,6 +189,8 @@ create table token (
secret char(32) not null comment 'secret value',
type tinyint not null default 0 comment 'request or access',
state tinyint default 0 comment 'for requests, 0 = initial, 1 = authorized, 2 = used',
+ verifier varchar(255) comment 'verifier string for OAuth 1.0a',
+ verified_callback varchar(255) comment 'verified callback URL for OAuth 1.0a',
created datetime not null comment 'date this record was created',
modified timestamp comment 'date this record was modified',
@@ -207,6 +210,33 @@ create table nonce (
constraint primary key (consumer_key, ts, nonce)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+create table oauth_application (
+ id integer auto_increment primary key comment 'unique identifier',
+ owner integer not null comment 'owner of the application' references profile (id),
+ consumer_key varchar(255) not null comment 'application consumer key' references consumer (consumer_key),
+ name varchar(255) not null comment 'name of the application',
+ description varchar(255) comment 'description of the application',
+ icon varchar(255) not null comment 'application icon',
+ source_url varchar(255) comment 'application homepage - used for source link',
+ organization varchar(255) comment 'name of the organization running the application',
+ homepage varchar(255) comment 'homepage for the organization',
+ callback_url varchar(255) comment 'url to redirect to after authentication',
+ type tinyint default 0 comment 'type of app, 1 = browser, 2 = desktop',
+ access_type tinyint default 0 comment 'default access type, bit 1 = read, bit 2 = write',
+ created datetime not null comment 'date this record was created',
+ modified timestamp comment 'date this record was modified'
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+
+create table oauth_application_user (
+ profile_id integer not null comment 'user of the application' references profile (id),
+ application_id integer not null comment 'id of the application' references oauth_application (id),
+ access_type tinyint default 0 comment 'access type, bit 1 = read, bit 2 = write, bit 3 = revoked',
+ token varchar(255) comment 'request or access token',
+ created datetime not null comment 'date this record was created',
+ modified timestamp comment 'date this record was modified',
+ constraint primary key (profile_id, application_id)
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+
/* These are used by JanRain OpenID library */
create table oid_associations (
diff --git a/js/geometa.js b/js/geometa.js
index 87e3c99a16..bba59b4486 100644
--- a/js/geometa.js
+++ b/js/geometa.js
@@ -1,5 +1,5 @@
-// A shim to implement the W3C Geolocation API Specification using Gears
-if (typeof navigator.geolocation == "undefined" || navigator.geolocation.shim ) (function(){
+// A shim to implement the W3C Geolocation API Specification using Gears or the Ajax API
+if (typeof navigator.geolocation == "undefined" || navigator.geolocation.shim ) { (function(){
// -- BEGIN GEARS_INIT
(function() {
@@ -23,8 +23,7 @@ if (typeof navigator.geolocation == "undefined" || navigator.geolocation.shim )
}
} catch (e) {
// Safari
- if ((typeof navigator.mimeTypes != 'undefined')
- && navigator.mimeTypes["application/x-googlegears"]) {
+ if ((typeof navigator.mimeTypes != 'undefined') && navigator.mimeTypes["application/x-googlegears"]) {
factory = document.createElement("object");
factory.style.display = "none";
factory.width = 0;
@@ -64,8 +63,8 @@ var GearsGeoLocation = (function() {
return function(position) {
callback(position);
self.lastPosition = position;
- }
- }
+ };
+ };
// -- PUBLIC
return {
@@ -96,9 +95,123 @@ var GearsGeoLocation = (function() {
};
});
-// If you have Gears installed use that
-if (window.google && google.gears) {
- navigator.geolocation = GearsGeoLocation();
-}
+var AjaxGeoLocation = (function() {
+ // -- PRIVATE
+ var loading = false;
+ var loadGoogleLoader = function() {
+ if (!hasGoogleLoader() && !loading) {
+ loading = true;
+ var s = document.createElement('script');
+ s.src = (document.location.protocol == "https:"?"https://":"http://") + 'www.google.com/jsapi?callback=_google_loader_apiLoaded';
+ s.type = "text/javascript";
+ document.getElementsByTagName('body')[0].appendChild(s);
+ }
+ };
+
+ var queue = [];
+ var addLocationQueue = function(callback) {
+ queue.push(callback);
+ };
+
+ var runLocationQueue = function() {
+ if (hasGoogleLoader()) {
+ while (queue.length > 0) {
+ var call = queue.pop();
+ call();
+ }
+ }
+ };
+
+ window['_google_loader_apiLoaded'] = function() {
+ runLocationQueue();
+ };
+
+ var hasGoogleLoader = function() {
+ return (window['google'] && google['loader']);
+ };
+
+ var checkGoogleLoader = function(callback) {
+ if (hasGoogleLoader()) { return true; }
+
+ addLocationQueue(callback);
+
+ loadGoogleLoader();
+
+ return false;
+ };
+
+ loadGoogleLoader(); // start to load as soon as possible just in case
+
+ // -- PUBLIC
+ return {
+ shim: true,
+
+ type: "ClientLocation",
+
+ lastPosition: null,
+
+ getCurrentPosition: function(successCallback, errorCallback, options) {
+ var self = this;
+ if (!checkGoogleLoader(function() {
+ self.getCurrentPosition(successCallback, errorCallback, options);
+ })) { return; }
+
+ if (google.loader.ClientLocation) {
+ var cl = google.loader.ClientLocation;
+
+ var position = {
+ coords: {
+ latitude: cl.latitude,
+ longitude: cl.longitude,
+ altitude: null,
+ accuracy: 43000, // same as Gears accuracy over wifi?
+ altitudeAccuracy: null,
+ heading: null,
+ speed: null
+ },
+ // extra info that is outside of the bounds of the core API
+ address: {
+ city: cl.address.city,
+ country: cl.address.country,
+ country_code: cl.address.country_code,
+ region: cl.address.region
+ },
+ timestamp: new Date()
+ };
+
+ successCallback(position);
+
+ this.lastPosition = position;
+ } else if (errorCallback === "function") {
+ errorCallback({ code: 3, message: "Using the Google ClientLocation API and it is not able to calculate a location."});
+ }
+ },
+
+ watchPosition: function(successCallback, errorCallback, options) {
+ this.getCurrentPosition(successCallback, errorCallback, options);
+
+ var self = this;
+ var watchId = setInterval(function() {
+ self.getCurrentPosition(successCallback, errorCallback, options);
+ }, 10000);
+
+ return watchId;
+ },
+
+ clearWatch: function(watchId) {
+ clearInterval(watchId);
+ },
+
+ getPermission: function(siteName, imageUrl, extraMessage) {
+ // for now just say yes :)
+ return true;
+ }
+
+ };
+});
+
+// If you have Gears installed use that, else use Ajax ClientLocation
+navigator.geolocation = (window.google && google.gears) ? GearsGeoLocation() : AjaxGeoLocation();
})();
+}
diff --git a/lib/action.php b/lib/action.php
index e9207a66a6..cc4f4aad07 100644
--- a/lib/action.php
+++ b/lib/action.php
@@ -246,18 +246,18 @@ class Action extends HTMLOutputter // lawsuit
{
if (Event::handle('StartShowScripts', array($this))) {
if (Event::handle('StartShowJQueryScripts', array($this))) {
- $this->script('js/jquery.min.js');
- $this->script('js/jquery.form.js');
- $this->script('js/jquery.cookie.js');
- $this->script('js/json2.js');
- $this->script('js/jquery.joverlay.min.js');
+ $this->script('jquery.min.js');
+ $this->script('jquery.form.js');
+ $this->script('jquery.cookie.js');
+ $this->script('json2.js');
+ $this->script('jquery.joverlay.min.js');
Event::handle('EndShowJQueryScripts', array($this));
}
if (Event::handle('StartShowStatusNetScripts', array($this)) &&
Event::handle('StartShowLaconicaScripts', array($this))) {
- $this->script('js/xbImportNode.js');
- $this->script('js/util.js');
- $this->script('js/geometa.js');
+ $this->script('xbImportNode.js');
+ $this->script('util.js');
+ $this->script('geometa.js');
// Frame-busting code to avoid clickjacking attacks.
$this->element('script', array('type' => 'text/javascript'),
'if (window.top !== window.self) { window.top.location.href = window.self.location.href; }');
@@ -369,7 +369,11 @@ class Action extends HTMLOutputter // lawsuit
$this->elementStart('div', array('id' => 'header'));
$this->showLogo();
$this->showPrimaryNav();
- $this->showSiteNotice();
+ if (Event::handle('StartShowSiteNotice', array($this))) {
+ $this->showSiteNotice();
+
+ Event::handle('EndShowSiteNotice', array($this));
+ }
if (common_logged_in()) {
$this->showNoticeForm();
} else {
@@ -388,8 +392,14 @@ class Action extends HTMLOutputter // lawsuit
$this->elementStart('address', array('id' => 'site_contact',
'class' => 'vcard'));
if (Event::handle('StartAddressData', array($this))) {
+ if (common_config('singleuser', 'enabled')) {
+ $url = common_local_url('showstream',
+ array('nickname' => common_config('singleuser', 'nickname')));
+ } else {
+ $url = common_local_url('public');
+ }
$this->elementStart('a', array('class' => 'url home bookmark',
- 'href' => common_local_url('public')));
+ 'href' => $url));
if (common_config('site', 'logo') || file_exists(Theme::file('logo.png'))) {
$this->element('img', array('class' => 'logo photo',
'src' => (common_config('site', 'logo')) ? common_config('site', 'logo') : Theme::path('logo.png'),
diff --git a/lib/adminpanelaction.php b/lib/adminpanelaction.php
index a6981ac611..f62bfa458a 100644
--- a/lib/adminpanelaction.php
+++ b/lib/adminpanelaction.php
@@ -319,12 +319,17 @@ class AdminPanelNav extends Widget
if ($this->canAdmin('user')) {
$this->out->menuItem(common_local_url('useradminpanel'), _('User'),
- _('Paths configuration'), $action_name == 'useradminpanel', 'nav_design_admin_panel');
+ _('User configuration'), $action_name == 'useradminpanel', 'nav_design_admin_panel');
}
- if ($this->canAdmin('paths')) {
- $this->out->menuItem(common_local_url('pathsadminpanel'), _('Paths'),
- _('Paths configuration'), $action_name == 'pathsadminpanel', 'nav_design_admin_panel');
+ if ($this->canAdmin('access')) {
+ $this->out->menuItem(common_local_url('accessadminpanel'), _('Access'),
+ _('Access configuration'), $action_name == 'accessadminpanel', 'nav_design_admin_panel');
+ }
+
+ if ($this->canAdmin('paths')) {
+ $this->out->menuItem(common_local_url('pathsadminpanel'), _('Paths'),
+ _('Paths configuration'), $action_name == 'pathsadminpanel', 'nav_design_admin_panel');
}
Event::handle('EndAdminPanelNav', array($this));
diff --git a/lib/api.php b/lib/api.php
index 707e4ac21a..794b140507 100644
--- a/lib/api.php
+++ b/lib/api.php
@@ -53,6 +53,9 @@ if (!defined('STATUSNET')) {
class ApiAction extends Action
{
+ const READ_ONLY = 1;
+ const READ_WRITE = 2;
+
var $format = null;
var $user = null;
var $auth_user = null;
@@ -62,6 +65,8 @@ class ApiAction extends Action
var $since_id = null;
var $since = null;
+ var $access = self::READ_ONLY; // read (default) or read-write
+
/**
* Initialization.
*
diff --git a/lib/apiauth.php b/lib/apiauth.php
index 7102764cba..c684a6caee 100644
--- a/lib/apiauth.php
+++ b/lib/apiauth.php
@@ -28,8 +28,8 @@
* @author Evan Prodromou
* @author mEDI
* @author Sarven Capadisli
- * @author Zach Copley
- * @copyright 2009 StatusNet, Inc.
+ * @author Zach Copley
+ * @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/
*/
@@ -39,6 +39,7 @@ if (!defined('STATUSNET')) {
}
require_once INSTALLDIR . '/lib/api.php';
+require_once INSTALLDIR . '/lib/apioauth.php';
/**
* Actions extending this class will require auth
@@ -52,6 +53,10 @@ require_once INSTALLDIR . '/lib/api.php';
class ApiAuthAction extends ApiAction
{
+ var $auth_user_nickname = null;
+ var $auth_user_password = null;
+ var $access_token = null;
+ var $oauth_source = null;
/**
* Take arguments for running, and output basic auth header if needed
@@ -66,13 +71,130 @@ class ApiAuthAction extends ApiAction
{
parent::prepare($args);
+ $this->consumer_key = $this->arg('oauth_consumer_key');
+ $this->access_token = $this->arg('oauth_token');
+
+ // NOTE: $this->auth_user has to get set in prepare(), not handle(),
+ // because subclasses do stuff with it in their prepares.
+
if ($this->requiresAuth()) {
- $this->checkBasicAuthUser();
+ if (!empty($this->access_token)) {
+ $this->checkOAuthRequest();
+ } else {
+ $this->checkBasicAuthUser(true);
+ }
+ } else {
+
+ // Check to see if a basic auth user is there even
+ // if one's not required
+
+ if (empty($this->access_token)) {
+ $this->checkBasicAuthUser(false);
+ }
+ }
+
+ // Reject API calls with the wrong access level
+
+ if ($this->isReadOnly($args) == false) {
+ if ($this->access != self::READ_WRITE) {
+ $msg = _('API resource requires read-write access, ' .
+ 'but you only have read access.');
+ $this->clientError($msg, 401, $this->format);
+ exit;
+ }
}
return true;
}
+ function handle($args)
+ {
+ parent::handle($args);
+ }
+
+ function checkOAuthRequest()
+ {
+ $datastore = new ApiStatusNetOAuthDataStore();
+ $server = new OAuthServer($datastore);
+ $hmac_method = new OAuthSignatureMethod_HMAC_SHA1();
+
+ $server->add_signature_method($hmac_method);
+
+ ApiOauthAction::cleanRequest();
+
+ try {
+
+ $req = OAuthRequest::from_request();
+ $server->verify_request($req);
+
+ $app = Oauth_application::getByConsumerKey($this->consumer_key);
+
+ if (empty($app)) {
+
+ // this should probably not happen
+ common_log(LOG_WARNING,
+ 'Couldn\'t find the OAuth app for consumer key: ' .
+ $this->consumer_key);
+
+ throw new OAuthException('No application for that consumer key.');
+ }
+
+ // set the source attr
+
+ $this->oauth_source = $app->name;
+
+ $appUser = Oauth_application_user::staticGet('token',
+ $this->access_token);
+
+ // XXX: Check that app->id and appUser->application_id and consumer all
+ // match?
+
+ if (!empty($appUser)) {
+
+ // If access_type == 0 we have either a request token
+ // or a bad / revoked access token
+
+ if ($appUser->access_type != 0) {
+
+ // Set the access level for the api call
+
+ $this->access = ($appUser->access_type & Oauth_application::$writeAccess)
+ ? self::READ_WRITE : self::READ_ONLY;
+
+ if (Event::handle('StartSetApiUser', array(&$user))) {
+ $this->auth_user = User::staticGet('id', $appUser->profile_id);
+ Event::handle('EndSetApiUser', array($user));
+ }
+
+ $msg = "API OAuth authentication for user '%s' (id: %d) on behalf of " .
+ "application '%s' (id: %d) with %s access.";
+
+ common_log(LOG_INFO, sprintf($msg,
+ $this->auth_user->nickname,
+ $this->auth_user->id,
+ $app->name,
+ $app->id,
+ ($this->access = self::READ_WRITE) ?
+ 'read-write' : 'read-only'
+ ));
+ return;
+ } else {
+ throw new OAuthException('Bad access token.');
+ }
+ } else {
+
+ // Also should not happen
+
+ throw new OAuthException('No user for that token.');
+ }
+
+ } catch (OAuthException $e) {
+ common_log(LOG_WARNING, 'API OAuthException - ' . $e->getMessage());
+ $this->showAuthError();
+ exit;
+ }
+ }
+
/**
* Does this API resource require authentication?
*
@@ -91,44 +213,54 @@ class ApiAuthAction extends ApiAction
* @return boolean true or false
*/
- function checkBasicAuthUser()
+ function checkBasicAuthUser($required = true)
{
$this->basicAuthProcessHeader();
$realm = common_config('site', 'name') . ' API';
- if (!isset($this->auth_user)) {
+ if (!isset($this->auth_user_nickname) && $required) {
header('WWW-Authenticate: Basic realm="' . $realm . '"');
// show error if the user clicks 'cancel'
- $this->showBasicAuthError();
+ $this->showAuthError();
exit;
} else {
- $nickname = $this->auth_user;
- $password = $this->auth_pw;
- $user = common_check_user($nickname, $password);
+
+ $user = common_check_user($this->auth_user_nickname,
+ $this->auth_user_password);
+
if (Event::handle('StartSetApiUser', array(&$user))) {
- $this->auth_user = $user;
+
+ if (!empty($user)) {
+ $this->auth_user = $user;
+ }
+
Event::handle('EndSetApiUser', array($user));
}
- if (empty($this->auth_user)) {
+ // By default, basic auth users have rw access
+
+ $this->access = self::READ_WRITE;
+
+ if (empty($this->auth_user) && $required) {
// basic authentication failed
list($proxy, $ip) = common_client_ip();
- common_log(
- LOG_WARNING,
- 'Failed API auth attempt, nickname = ' .
- "$nickname, proxy = $proxy, ip = $ip."
- );
- $this->showBasicAuthError();
+
+ $msg = sprintf(_('Failed API auth attempt, nickname = %1$s, ' .
+ 'proxy = %2$s, ip = %3$s'),
+ $this->auth_user_nickname,
+ $proxy,
+ $ip);
+ common_log(LOG_WARNING, $msg);
+ $this->showAuthError();
exit;
}
}
- return true;
}
/**
@@ -142,32 +274,30 @@ class ApiAuthAction extends ApiAction
{
if (isset($_SERVER['AUTHORIZATION'])
|| isset($_SERVER['HTTP_AUTHORIZATION'])
- ) {
- $authorization_header = isset($_SERVER['HTTP_AUTHORIZATION'])
- ? $_SERVER['HTTP_AUTHORIZATION'] : $_SERVER['AUTHORIZATION'];
+ ) {
+ $authorization_header = isset($_SERVER['HTTP_AUTHORIZATION'])
+ ? $_SERVER['HTTP_AUTHORIZATION'] : $_SERVER['AUTHORIZATION'];
}
if (isset($_SERVER['PHP_AUTH_USER'])) {
- $this->auth_user = $_SERVER['PHP_AUTH_USER'];
- $this->auth_pw = $_SERVER['PHP_AUTH_PW'];
+ $this->auth_user_nickname = $_SERVER['PHP_AUTH_USER'];
+ $this->auth_user_password = $_SERVER['PHP_AUTH_PW'];
} elseif (isset($authorization_header)
&& strstr(substr($authorization_header, 0, 5), 'Basic')) {
- // decode the HTTP_AUTHORIZATION header on php-cgi server self
+ // Decode the HTTP_AUTHORIZATION header on php-cgi server self
// on fcgid server the header name is AUTHORIZATION
$auth_hash = base64_decode(substr($authorization_header, 6));
- list($this->auth_user, $this->auth_pw) = explode(':', $auth_hash);
+ list($this->auth_user_nickname,
+ $this->auth_user_password) = explode(':', $auth_hash);
- // set all to null on a empty basic auth request
+ // Set all to null on a empty basic auth request
- if ($this->auth_user == "") {
- $this->auth_user = null;
- $this->auth_pw = null;
+ if (empty($this->auth_user_nickname)) {
+ $this->auth_user_nickname = null;
+ $this->auth_password = null;
}
- } else {
- $this->auth_user = null;
- $this->auth_pw = null;
}
}
@@ -178,7 +308,7 @@ class ApiAuthAction extends ApiAction
* @return void
*/
- function showBasicAuthError()
+ function showAuthError()
{
header('HTTP/1.1 401 Unauthorized');
$msg = 'Could not authenticate you.';
diff --git a/lib/apioauth.php b/lib/apioauth.php
new file mode 100644
index 0000000000..4cb8a67754
--- /dev/null
+++ b/lib/apioauth.php
@@ -0,0 +1,122 @@
+.
+ *
+ * @category API
+ * @package StatusNet
+ * @author Zach Copley
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+require_once INSTALLDIR . '/lib/apioauthstore.php';
+
+/**
+ * Base action for API OAuth enpoints. Clean up the
+ * the request, and possibly some other common things
+ * here.
+ *
+ * @category API
+ * @package StatusNet
+ * @author Zach Copley
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class ApiOauthAction extends Action
+{
+ /**
+ * Is this a read-only action?
+ *
+ * @return boolean false
+ */
+
+ function isReadOnly($args)
+ {
+ return false;
+ }
+
+ function prepare($args)
+ {
+ parent::prepare($args);
+ return true;
+ }
+
+ /**
+ * Handle input, produce output
+ *
+ * Switches on request method; either shows the form or handles its input.
+ *
+ * @param array $args $_REQUEST data
+ *
+ * @return void
+ */
+
+ function handle($args)
+ {
+ parent::handle($args);
+ self::cleanRequest();
+ }
+
+ static function cleanRequest()
+ {
+ // kill evil effects of magical slashing
+
+ if (get_magic_quotes_gpc() == 1) {
+ $_POST = array_map('stripslashes', $_POST);
+ $_GET = array_map('stripslashes', $_GET);
+ }
+
+ // strip out the p param added in index.php
+
+ // XXX: should we strip anything else? Or alternatively
+ // only allow a known list of params?
+
+ unset($_GET['p']);
+ unset($_POST['p']);
+ }
+
+ function getCallback($url, $params)
+ {
+ foreach ($params as $k => $v) {
+ $url = $this->appendQueryVar($url,
+ OAuthUtil::urlencode_rfc3986($k),
+ OAuthUtil::urlencode_rfc3986($v));
+ }
+
+ return $url;
+ }
+
+ function appendQueryVar($url, $k, $v) {
+ $url = preg_replace('/(.*)(\?|&)' . $k . '=[^&]+?(&)(.*)/i', '$1$2$4', $url . '&');
+ $url = substr($url, 0, -1);
+ if (strpos($url, '?') === false) {
+ return ($url . '?' . $k . '=' . $v);
+ } else {
+ return ($url . '&' . $k . '=' . $v);
+ }
+ }
+
+}
diff --git a/lib/apioauthstore.php b/lib/apioauthstore.php
new file mode 100644
index 0000000000..32110d0575
--- /dev/null
+++ b/lib/apioauthstore.php
@@ -0,0 +1,163 @@
+.
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
+
+require_once INSTALLDIR . '/lib/oauthstore.php';
+
+class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore
+{
+
+ function lookup_consumer($consumer_key)
+ {
+ $con = Consumer::staticGet('consumer_key', $consumer_key);
+
+ if (!$con) {
+ return null;
+ }
+
+ return new OAuthConsumer($con->consumer_key,
+ $con->consumer_secret);
+ }
+
+ function getAppByRequestToken($token_key)
+ {
+ // Look up the full req tokenx
+
+ $req_token = $this->lookup_token(null,
+ 'request',
+ $token_key);
+
+ if (empty($req_token)) {
+ common_debug("couldn't get request token from oauth datastore");
+ return null;
+ }
+
+ // Look up the full Token
+
+ $token = new Token();
+ $token->tok = $req_token->key;
+ $result = $token->find(true);
+
+ if (empty($result)) {
+ common_debug('Couldn\'t find req token in the token table.');
+ return null;
+ }
+
+ // Look up the app
+
+ $app = new Oauth_application();
+ $app->consumer_key = $token->consumer_key;
+ $result = $app->find(true);
+
+ if (!empty($result)) {
+ return $app;
+ } else {
+ common_debug("Couldn't find the app!");
+ return null;
+ }
+ }
+
+ function new_access_token($token, $consumer)
+ {
+ common_debug('new_access_token("'.$token->key.'","'.$consumer->key.'")', __FILE__);
+
+ $rt = new Token();
+ $rt->consumer_key = $consumer->key;
+ $rt->tok = $token->key;
+ $rt->type = 0; // request
+
+ $app = Oauth_application::getByConsumerKey($consumer->key);
+
+ if (empty($app)) {
+ common_debug("empty app!");
+ }
+
+ if ($rt->find(true) && $rt->state == 1) { // authorized
+ common_debug('request token found.', __FILE__);
+
+ // find the associated user of the app
+
+ $appUser = new Oauth_application_user();
+ $appUser->application_id = $app->id;
+ $appUser->token = $rt->tok;
+ $result = $appUser->find(true);
+
+ if (!empty($result)) {
+ common_debug("Oath app user found.");
+ } else {
+ common_debug("Oauth app user not found. app id $app->id token $rt->tok");
+ return null;
+ }
+
+ // go ahead and make the access token
+
+ $at = new Token();
+ $at->consumer_key = $consumer->key;
+ $at->tok = common_good_rand(16);
+ $at->secret = common_good_rand(16);
+ $at->type = 1; // access
+ $at->created = DB_DataObject_Cast::dateTime();
+
+ if (!$at->insert()) {
+ $e = $at->_lastError;
+ common_debug('access token "'.$at->tok.'" not inserted: "'.$e->message.'"', __FILE__);
+ return null;
+ } else {
+ common_debug('access token "'.$at->tok.'" inserted', __FILE__);
+ // burn the old one
+ $orig_rt = clone($rt);
+ $rt->state = 2; // used
+ if (!$rt->update($orig_rt)) {
+ return null;
+ }
+ common_debug('request token "'.$rt->tok.'" updated', __FILE__);
+
+ // update the token from req to access for the user
+
+ $orig = clone($appUser);
+ $appUser->token = $at->tok;
+
+ // It's at this point that we change the access type
+ // to whatever the application's access is. Request
+ // tokens should always have an access type of 0, and
+ // therefore be unuseable for making requests for
+ // protected resources.
+
+ $appUser->access_type = $app->access_type;
+
+ $result = $appUser->update($orig);
+
+ if (empty($result)) {
+ common_debug('couldn\'t update OAuth app user.');
+ return null;
+ }
+
+ // Okay, good
+
+ return new OAuthToken($at->tok, $at->secret);
+ }
+
+ } else {
+ return null;
+ }
+ }
+
+}
+
diff --git a/lib/applicationeditform.php b/lib/applicationeditform.php
new file mode 100644
index 0000000000..6f03a9beda
--- /dev/null
+++ b/lib/applicationeditform.php
@@ -0,0 +1,338 @@
+.
+ *
+ * @category Form
+ * @package StatusNet
+ * @author Zach Copley
+ * @copyright 2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR . '/lib/form.php';
+
+/**
+ * Form for editing an application
+ *
+ * @category Form
+ * @package StatusNet
+ * @author Zach Copley
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ *
+ */
+
+class ApplicationEditForm extends Form
+{
+ /**
+ * group for user to join
+ */
+
+ var $application = null;
+
+ /**
+ * Constructor
+ *
+ * @param Action $out output channel
+ * @param User_group $group group to join
+ */
+
+ function __construct($out=null, $application=null)
+ {
+ parent::__construct($out);
+
+ $this->application = $application;
+ }
+
+ /**
+ * ID of the form
+ *
+ * @return string ID of the form
+ */
+
+ function id()
+ {
+ if ($this->application) {
+ return 'form_application_edit-' . $this->application->id;
+ } else {
+ return 'form_application_add';
+ }
+ }
+
+ /**
+ * HTTP method used to submit the form
+ *
+ * For image data we need to send multipart/form-data
+ * so we set that here too
+ *
+ * @return string the method to use for submitting
+ */
+
+ function method()
+ {
+ $this->enctype = 'multipart/form-data';
+ return 'post';
+ }
+
+ /**
+ * class of the form
+ *
+ * @return string of the form class
+ */
+
+ function formClass()
+ {
+ return 'form_settings';
+ }
+
+ /**
+ * Action of the form
+ *
+ * @return string URL of the action
+ */
+
+ function action()
+ {
+ $cur = common_current_user();
+
+ if (!empty($this->application)) {
+ return common_local_url('editapplication',
+ array('id' => $this->application->id));
+ } else {
+ return common_local_url('newapplication');
+ }
+ }
+
+ /**
+ * Name of the form
+ *
+ * @return void
+ */
+
+ function formLegend()
+ {
+ $this->out->element('legend', null, _('Edit application'));
+ }
+
+ /**
+ * Data elements of the form
+ *
+ * @return void
+ */
+
+ function formData()
+ {
+ if ($this->application) {
+ $id = $this->application->id;
+ $icon = $this->application->icon;
+ $name = $this->application->name;
+ $description = $this->application->description;
+ $source_url = $this->application->source_url;
+ $organization = $this->application->organization;
+ $homepage = $this->application->homepage;
+ $callback_url = $this->application->callback_url;
+ $this->type = $this->application->type;
+ $this->access_type = $this->application->access_type;
+ } else {
+ $id = '';
+ $icon = '';
+ $name = '';
+ $description = '';
+ $source_url = '';
+ $organization = '';
+ $homepage = '';
+ $callback_url = '';
+ $this->type = '';
+ $this->access_type = '';
+ }
+
+ $this->out->hidden('token', common_session_token());
+
+ $this->out->elementStart('ul', 'form_data');
+
+ $this->out->elementStart('li', array('id' => 'application_icon'));
+
+ if (!empty($icon)) {
+ $this->out->element('img', array('src' => $icon));
+ }
+
+ $this->out->element('label', array('for' => 'app_icon'),
+ _('Icon'));
+ $this->out->element('input', array('name' => 'app_icon',
+ 'type' => 'file',
+ 'id' => 'app_icon'));
+ $this->out->element('p', 'form_guide', _('Icon for this application'));
+ $this->out->element('input', array('name' => 'MAX_FILE_SIZE',
+ 'type' => 'hidden',
+ 'id' => 'MAX_FILE_SIZE',
+ 'value' => ImageFile::maxFileSizeInt()));
+ $this->out->elementEnd('li');
+
+ $this->out->elementStart('li');
+
+ $this->out->hidden('application_id', $id);
+
+ $this->out->input('name', _('Name'),
+ ($this->out->arg('name')) ? $this->out->arg('name') : $name);
+
+ $this->out->elementEnd('li');
+
+ $this->out->elementStart('li');
+
+ $maxDesc = Oauth_application::maxDesc();
+ if ($maxDesc > 0) {
+ $descInstr = sprintf(_('Describe your application in %d characters'),
+ $maxDesc);
+ } else {
+ $descInstr = _('Describe your application');
+ }
+ $this->out->textarea('description', _('Description'),
+ ($this->out->arg('description')) ? $this->out->arg('description') : $description,
+ $descInstr);
+
+ $this->out->elementEnd('li');
+
+ $this->out->elementStart('li');
+ $this->out->input('source_url', _('Source URL'),
+ ($this->out->arg('source_url')) ? $this->out->arg('source_url') : $source_url,
+ _('URL of the homepage of this application'));
+ $this->out->elementEnd('li');
+
+ $this->out->elementStart('li');
+ $this->out->input('organization', _('Organization'),
+ ($this->out->arg('organization')) ? $this->out->arg('organization') : $organization,
+ _('Organization responsible for this application'));
+ $this->out->elementEnd('li');
+
+ $this->out->elementStart('li');
+ $this->out->input('homepage', _('Homepage'),
+ ($this->out->arg('homepage')) ? $this->out->arg('homepage') : $homepage,
+ _('URL for the homepage of the organization'));
+ $this->out->elementEnd('li');
+
+ $this->out->elementStart('li');
+ $this->out->input('callback_url', ('Callback URL'),
+ ($this->out->arg('callback_url')) ? $this->out->arg('callback_url') : $callback_url,
+ _('URL to redirect to after authentication'));
+ $this->out->elementEnd('li');
+
+ $this->out->elementStart('li', array('id' => 'application_types'));
+
+ $attrs = array('name' => 'app_type',
+ 'type' => 'radio',
+ 'id' => 'app_type-browser',
+ 'class' => 'radio',
+ 'value' => Oauth_application::$browser);
+
+ // Default to Browser
+
+ if ($this->application->type == Oauth_application::$browser
+ || empty($this->application->type)) {
+ $attrs['checked'] = 'checked';
+ }
+
+ $this->out->element('input', $attrs);
+
+ $this->out->element('label', array('for' => 'app_type-browser',
+ 'class' => 'radio'),
+ _('Browser'));
+
+ $attrs = array('name' => 'app_type',
+ 'type' => 'radio',
+ 'id' => 'app_type-dekstop',
+ 'class' => 'radio',
+ 'value' => Oauth_application::$desktop);
+
+ if ($this->application->type == Oauth_application::$desktop) {
+ $attrs['checked'] = 'checked';
+ }
+
+ $this->out->element('input', $attrs);
+
+ $this->out->element('label', array('for' => 'app_type-desktop',
+ 'class' => 'radio'),
+ _('Desktop'));
+ $this->out->element('p', 'form_guide', _('Type of application, browser or desktop'));
+ $this->out->elementEnd('li');
+
+ $this->out->elementStart('li', array('id' => 'default_access_types'));
+
+ $attrs = array('name' => 'default_access_type',
+ 'type' => 'radio',
+ 'id' => 'default_access_type-r',
+ 'class' => 'radio',
+ 'value' => 'r');
+
+ // default to read-only access
+
+ if ($this->application->access_type & Oauth_application::$readAccess
+ || empty($this->application->access_type)) {
+ $attrs['checked'] = 'checked';
+ }
+
+ $this->out->element('input', $attrs);
+
+ $this->out->element('label', array('for' => 'default_access_type-ro',
+ 'class' => 'radio'),
+ _('Read-only'));
+
+ $attrs = array('name' => 'default_access_type',
+ 'type' => 'radio',
+ 'id' => 'default_access_type-rw',
+ 'class' => 'radio',
+ 'value' => 'rw');
+
+ if ($this->application->access_type & Oauth_application::$readAccess
+ && $this->application->access_type & Oauth_application::$writeAccess
+ ) {
+ $attrs['checked'] = 'checked';
+ }
+
+ $this->out->element('input', $attrs);
+
+ $this->out->element('label', array('for' => 'default_access_type-rw',
+ 'class' => 'radio'),
+ _('Read-write'));
+ $this->out->element('p', 'form_guide', _('Default access for this application: read-only, or read-write'));
+
+ $this->out->elementEnd('li');
+
+ $this->out->elementEnd('ul');
+ }
+
+ /**
+ * Action elements
+ *
+ * @return void
+ */
+
+ function formActions()
+ {
+ $this->out->submit('cancel', _('Cancel'), 'submit form_action-primary',
+ 'cancel', _('Cancel'));
+ $this->out->submit('save', _('Save'), 'submit form_action-secondary',
+ 'save', _('Save'));
+ }
+}
diff --git a/lib/applicationlist.php b/lib/applicationlist.php
new file mode 100644
index 0000000000..3abb1f8aa7
--- /dev/null
+++ b/lib/applicationlist.php
@@ -0,0 +1,168 @@
+.
+ *
+ * @category Application
+ * @package StatusNet
+ * @author Zach Copley
+ * @copyright 2008-2009 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR . '/lib/widget.php';
+
+define('APPS_PER_PAGE', 20);
+
+/**
+ * Widget to show a list of OAuth applications
+ *
+ * @category Application
+ * @package StatusNet
+ * @author Zach Copley
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+class ApplicationList extends Widget
+{
+ /** Current application, application query */
+ var $application = null;
+
+ /** Owner of this list */
+ var $owner = null;
+
+ /** Action object using us. */
+ var $action = null;
+
+ function __construct($application, $owner=null, $action=null, $connections = false)
+ {
+ parent::__construct($action);
+
+ $this->application = $application;
+ $this->owner = $owner;
+ $this->action = $action;
+ $this->connections = $connections;
+ }
+
+ function show()
+ {
+ $this->out->elementStart('ul', 'applications');
+
+ $cnt = 0;
+
+ while ($this->application->fetch()) {
+ $cnt++;
+ if($cnt > APPS_PER_PAGE) {
+ break;
+ }
+ $this->showapplication();
+ }
+
+ $this->out->elementEnd('ul');
+
+ return $cnt;
+ }
+
+ function showApplication()
+ {
+
+ $user = common_current_user();
+
+ $this->out->elementStart('li', array('class' => 'application',
+ 'id' => 'oauthclient-' . $this->application->id));
+
+ $this->out->elementStart('span', 'vcard author');
+ if (!$this->connections) {
+ $this->out->elementStart('a',
+ array('href' => common_local_url('showapplication',
+ array('id' => $this->application->id)),
+ 'class' => 'url'));
+
+ } else {
+ $this->out->elementStart('a', array('href' => $this->application->source_url,
+ 'class' => 'url'));
+ }
+
+ if (!empty($this->application->icon)) {
+ $this->out->element('img', array('src' => $this->application->icon,
+ 'class' => 'photo avatar'));
+ }
+
+ $this->out->element('span', 'fn', $this->application->name);
+ $this->out->elementEnd('a');
+ $this->out->elementEnd('span');
+
+ $this->out->raw(' by ');
+
+ $this->out->element('a', array('href' => $this->application->homepage,
+ 'class' => 'url'),
+ $this->application->organization);
+
+ $this->out->element('p', 'note', $this->application->description);
+ $this->out->elementEnd('li');
+
+ if ($this->connections) {
+ $appUser = Oauth_application_user::getByKeys($this->owner, $this->application);
+
+ if (empty($appUser)) {
+ common_debug("empty appUser!");
+ }
+
+ $this->out->elementStart('li');
+
+ $access = ($this->application->access_type & Oauth_application::$writeAccess)
+ ? 'read-write' : 'read-only';
+
+ $txt = 'Approved ' . common_date_string($appUser->modified) .
+ " - $access access.";
+
+ $this->out->raw($txt);
+ $this->out->elementEnd('li');
+
+ $this->out->elementStart('li', 'entity_revoke');
+ $this->out->elementStart('form', array('id' => 'form_revoke_app',
+ 'class' => 'form_revoke_app',
+ 'method' => 'POST',
+ 'action' =>
+ common_local_url('oauthconnectionssettings')));
+ $this->out->elementStart('fieldset');
+ $this->out->hidden('id', $this->application->id);
+ $this->out->hidden('token', common_session_token());
+ $this->out->submit('revoke', _('Revoke'));
+ $this->out->elementEnd('fieldset');
+ $this->out->elementEnd('form');
+ $this->out->elementEnd('li');
+ }
+ }
+
+ /* Override this in subclasses. */
+
+ function showOwnerControls()
+ {
+ return;
+ }
+
+}
diff --git a/lib/common.php b/lib/common.php
index ada48b339d..b4e4a653c8 100644
--- a/lib/common.php
+++ b/lib/common.php
@@ -22,7 +22,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
//exit with 200 response, if this is checking fancy from the installer
if (isset($_REQUEST['p']) && $_REQUEST['p'] == 'check-fancy') { exit; }
-define('STATUSNET_VERSION', '0.9.0beta3');
+define('STATUSNET_VERSION', '0.9.0beta4');
define('LACONICA_VERSION', STATUSNET_VERSION); // compatibility
define('STATUSNET_CODENAME', 'Stand');
diff --git a/lib/connectsettingsaction.php b/lib/connectsettingsaction.php
index e5fb8727ba..b9c14799e0 100644
--- a/lib/connectsettingsaction.php
+++ b/lib/connectsettingsaction.php
@@ -115,6 +115,11 @@ class ConnectSettingsNav extends Widget
array(_('SMS'),
_('Updates by SMS'));
}
+
+ $menu['oauthconnectionssettings'] = array(
+ _('Connections'),
+ _('Authorized connected applications')
+ );
foreach ($menu as $menuaction => $menudesc) {
$this->action->menuItem(common_local_url($menuaction),
@@ -131,4 +136,3 @@ class ConnectSettingsNav extends Widget
}
-
diff --git a/lib/default.php b/lib/default.php
index 57199b356f..c729193b52 100644
--- a/lib/default.php
+++ b/lib/default.php
@@ -56,7 +56,7 @@ $default =
'dupelimit' => 60, # default for same person saying the same thing
'textlimit' => 140,
'indent' => true,
- 'use_x_sendfile' => false,
+ 'use_x_sendfile' => false
),
'db' =>
array('database' => 'YOU HAVE TO SET THIS IN config.php',
@@ -69,7 +69,9 @@ $default =
'db_driver' => 'DB', # XXX: JanRain libs only work with DB
'quote_identifiers' => false,
'type' => 'mysql',
- 'schemacheck' => 'runtime'), // 'runtime' or 'script'
+ 'schemacheck' => 'runtime', // 'runtime' or 'script'
+ 'log_queries' => false, // true to log all DB queries
+ 'log_slow_queries' => 0), // if set, log queries taking over N seconds
'syslog' =>
array('appname' => 'statusnet', # for syslog
'priority' => 'debug', # XXX: currently ignored
@@ -79,11 +81,13 @@ $default =
'subsystem' => 'db', # default to database, or 'stomp'
'stomp_server' => null,
'queue_basename' => '/queue/statusnet/',
+ 'control_channel' => '/topic/statusnet-control', // broadcasts to all queue daemons
'stomp_username' => null,
'stomp_password' => null,
'monitor' => null, // URL to monitor ping endpoint (work in progress)
'softlimit' => '90%', // total size or % of memory_limit at which to restart queue threads gracefully
'debug_memory' => false, // true to spit memory usage to log
+ 'inboxes' => true, // true to do inbox distribution & output queueing from in background via 'distrib' queue
),
'license' =>
array('type' => 'cc', # can be 'cc', 'allrightsreserved', 'private'
@@ -117,6 +121,9 @@ $default =
array('server' => null,
'dir' => null,
'path'=> null),
+ 'javascript' =>
+ array('server' => null,
+ 'path'=> null),
'throttle' =>
array('enabled' => false, // whether to throttle edits; false by default
'count' => 20, // number of allowed messages in timespan
@@ -209,6 +216,8 @@ $default =
'uploads' => true,
'filecommand' => '/usr/bin/file',
),
+ 'application' =>
+ array('desclimit' => null),
'group' =>
array('maxaliases' => 3,
'desclimit' => null),
@@ -255,5 +264,8 @@ $default =
'OpenID' => null),
),
'admin' =>
- array('panels' => array('design', 'site', 'user', 'paths')),
+ array('panels' => array('design', 'site', 'user', 'paths', 'access')),
+ 'singleuser' =>
+ array('enabled' => false,
+ 'nickname' => null),
);
diff --git a/lib/designsettings.php b/lib/designsettings.php
index 8e44c03a92..4955e92199 100644
--- a/lib/designsettings.php
+++ b/lib/designsettings.php
@@ -327,8 +327,8 @@ class DesignSettingsAction extends AccountSettingsAction
{
parent::showScripts();
- $this->script('js/farbtastic/farbtastic.js');
- $this->script('js/userdesign.go.js');
+ $this->script('farbtastic/farbtastic.js');
+ $this->script('userdesign.go.js');
$this->autofocus('design_background-image_file');
}
diff --git a/lib/htmloutputter.php b/lib/htmloutputter.php
index 31660ce954..317f5ea612 100644
--- a/lib/htmloutputter.php
+++ b/lib/htmloutputter.php
@@ -351,14 +351,40 @@ class HTMLOutputter extends XMLOutputter
function script($src, $type='text/javascript')
{
if(Event::handle('StartScriptElement', array($this,&$src,&$type))) {
+
$url = parse_url($src);
+
if( empty($url['scheme']) && empty($url['host']) && empty($url['query']) && empty($url['fragment']))
{
- $src = common_path($src) . '?version=' . STATUSNET_VERSION;
+ $path = common_config('javascript', 'path');
+
+ if (empty($path)) {
+ $path = common_config('site', 'path') . '/js/';
+ }
+
+ if ($path[strlen($path)-1] != '/') {
+ $path .= '/';
+ }
+
+ if ($path[0] != '/') {
+ $path = '/'.$path;
+ }
+
+ $server = common_config('javascript', 'server');
+
+ if (empty($server)) {
+ $server = common_config('site', 'server');
+ }
+
+ // XXX: protocol
+
+ $src = 'http://'.$server.$path.$src . '?version=' . STATUSNET_VERSION;
}
+
$this->element('script', array('type' => $type,
'src' => $src),
' ');
+
Event::handle('EndScriptElement', array($this,$src,$type));
}
}
diff --git a/lib/iomaster.php b/lib/iomaster.php
index 3bf82bc6b4..bcab3542be 100644
--- a/lib/iomaster.php
+++ b/lib/iomaster.php
@@ -38,6 +38,9 @@ abstract class IoMaster
protected $pollTimeouts = array();
protected $lastPoll = array();
+ public $shutdown = false; // Did we do a graceful shutdown?
+ public $respawn = true; // Should we respawn after shutdown?
+
/**
* @param string $id process ID to use in logging/monitoring
*/
@@ -144,7 +147,7 @@ abstract class IoMaster
$this->logState('init');
$this->start();
- while (true) {
+ while (!$this->shutdown) {
$timeouts = array_values($this->pollTimeouts);
$timeouts[] = 60; // default max timeout
@@ -196,22 +199,31 @@ abstract class IoMaster
$this->logState('idle');
$this->idle();
- $memoryLimit = $this->softMemoryLimit();
- if ($memoryLimit > 0) {
- $usage = memory_get_usage();
- if ($usage > $memoryLimit) {
- common_log(LOG_INFO, "Queue thread hit soft memory limit ($usage > $memoryLimit); gracefully restarting.");
- break;
- } else if (common_config('queue', 'debug_memory')) {
- common_log(LOG_DEBUG, "Memory usage $usage");
- }
- }
+ $this->checkMemory();
}
$this->logState('shutdown');
$this->finish();
}
+ /**
+ * Check runtime memory usage, possibly triggering a graceful shutdown
+ * and thread respawn if we've crossed the soft limit.
+ */
+ protected function checkMemory()
+ {
+ $memoryLimit = $this->softMemoryLimit();
+ if ($memoryLimit > 0) {
+ $usage = memory_get_usage();
+ if ($usage > $memoryLimit) {
+ common_log(LOG_INFO, "Queue thread hit soft memory limit ($usage > $memoryLimit); gracefully restarting.");
+ $this->requestRestart();
+ } else if (common_config('queue', 'debug_memory')) {
+ common_log(LOG_DEBUG, "Memory usage $usage");
+ }
+ }
+ }
+
/**
* Return fully-parsed soft memory limit in bytes.
* @return intval 0 or -1 if not set
@@ -354,5 +366,24 @@ abstract class IoMaster
$owners[] = "thread:" . $this->id;
$this->monitor->stats($key, $owners);
}
+
+ /**
+ * For IoManagers to request a graceful shutdown at end of event loop.
+ */
+ public function requestShutdown()
+ {
+ $this->shutdown = true;
+ $this->respawn = false;
+ }
+
+ /**
+ * For IoManagers to request a graceful restart at end of event loop.
+ */
+ public function requestRestart()
+ {
+ $this->shutdown = true;
+ $this->respawn = true;
+ }
+
}
diff --git a/lib/personalgroupnav.php b/lib/personalgroupnav.php
index cdde1feca0..25db5baa92 100644
--- a/lib/personalgroupnav.php
+++ b/lib/personalgroupnav.php
@@ -78,9 +78,9 @@ class PersonalGroupNav extends Widget
function show()
{
$user = null;
-
+
// FIXME: we should probably pass this in
-
+
$action = $this->action->trimmed('action');
$nickname = $this->action->trimmed('nickname');
@@ -117,7 +117,8 @@ class PersonalGroupNav extends Widget
$cur = common_current_user();
- if ($cur && $cur->id == $user->id) {
+ if ($cur && $cur->id == $user->id &&
+ !common_config('singleuser', 'enabled')) {
$this->out->menuItem(common_local_url('inbox', array('nickname' =>
$nickname)),
diff --git a/lib/queuemanager.php b/lib/queuemanager.php
index e5cf8239e9..afe710e884 100644
--- a/lib/queuemanager.php
+++ b/lib/queuemanager.php
@@ -100,6 +100,23 @@ abstract class QueueManager extends IoManager
$this->initialize();
}
+ /**
+ * Optional; ping any running queue handler daemons with a notification
+ * such as announcing a new site to handle or requesting clean shutdown.
+ * This avoids having to restart all the daemons manually to update configs
+ * and such.
+ *
+ * Called from scripts/queuectl.php controller utility.
+ *
+ * @param string $event event key
+ * @param string $param optional parameter to append to key
+ * @return boolean success
+ */
+ public function sendControlSignal($event, $param='')
+ {
+ throw new Exception(get_class($this) . " does not support control signals.");
+ }
+
/**
* Store an object (usually/always a Notice) into the given queue
* for later processing. No guarantee is made on when it will be
@@ -225,7 +242,6 @@ abstract class QueueManager extends IoManager
// XMPP output handlers...
$this->connect('jabber', 'JabberQueueHandler');
$this->connect('public', 'PublicQueueHandler');
-
// @fixme this should get an actual queue
//$this->connect('confirm', 'XmppConfirmHandler');
diff --git a/lib/router.php b/lib/router.php
index 6b87ed27f6..03765b39dd 100644
--- a/lib/router.php
+++ b/lib/router.php
@@ -73,12 +73,6 @@ class Router
if (Event::handle('StartInitializeRouter', array(&$m))) {
- // In the "root"
-
- $m->connect('', array('action' => 'public'));
- $m->connect('rss', array('action' => 'publicrss'));
- $m->connect('featuredrss', array('action' => 'featuredrss'));
- $m->connect('favoritedrss', array('action' => 'favoritedrss'));
$m->connect('opensearch/people', array('action' => 'opensearch',
'type' => 'people'));
$m->connect('opensearch/notice', array('action' => 'opensearch',
@@ -140,11 +134,23 @@ class Router
// settings
- foreach (array('profile', 'avatar', 'password', 'im',
- 'email', 'sms', 'userdesign', 'other') as $s) {
+ foreach (array('profile', 'avatar', 'password', 'im', 'oauthconnections',
+ 'oauthapps', 'email', 'sms', 'userdesign', 'other') as $s) {
$m->connect('settings/'.$s, array('action' => $s.'settings'));
}
+ $m->connect('settings/oauthapps/show/:id',
+ array('action' => 'showapplication'),
+ array('id' => '[0-9]+')
+ );
+ $m->connect('settings/oauthapps/new',
+ array('action' => 'newapplication')
+ );
+ $m->connect('settings/oauthapps/edit/:id',
+ array('action' => 'editapplication'),
+ array('id' => '[0-9]+')
+ );
+
// search
foreach (array('group', 'people', 'notice') as $s) {
@@ -227,11 +233,6 @@ class Router
array('action' => 'peopletag'),
array('tag' => '[a-zA-Z0-9]+'));
- $m->connect('featured/', array('action' => 'featured'));
- $m->connect('featured', array('action' => 'featured'));
- $m->connect('favorited/', array('action' => 'favorited'));
- $m->connect('favorited', array('action' => 'favorited'));
-
// groups
$m->connect('group/new', array('action' => 'newgroup'));
@@ -622,66 +623,146 @@ class Router
$m->connect('api/search.json', array('action' => 'twitapisearchjson'));
$m->connect('api/trends.json', array('action' => 'twitapitrends'));
+ $m->connect('api/oauth/request_token',
+ array('action' => 'apioauthrequesttoken'));
+
+ $m->connect('api/oauth/access_token',
+ array('action' => 'apioauthaccesstoken'));
+
+ $m->connect('api/oauth/authorize',
+ array('action' => 'apioauthauthorize'));
+
+ // Admin
+
$m->connect('admin/site', array('action' => 'siteadminpanel'));
$m->connect('admin/design', array('action' => 'designadminpanel'));
$m->connect('admin/user', array('action' => 'useradminpanel'));
+ $m->connect('admin/access', array('action' => 'accessadminpanel'));
$m->connect('admin/paths', array('action' => 'pathsadminpanel'));
$m->connect('getfile/:filename',
array('action' => 'getfile'),
array('filename' => '[A-Za-z0-9._-]+'));
- // user stuff
+ // In the "root"
- foreach (array('subscriptions', 'subscribers',
- 'nudge', 'all', 'foaf', 'xrds',
- 'replies', 'inbox', 'outbox', 'microsummary') as $a) {
- $m->connect(':nickname/'.$a,
- array('action' => $a),
+ if (common_config('singleuser', 'enabled')) {
+
+ $nickname = common_config('singleuser', 'nickname');
+
+ foreach (array('subscriptions', 'subscribers',
+ 'all', 'foaf', 'xrds',
+ 'replies', 'microsummary') as $a) {
+ $m->connect($a,
+ array('action' => $a,
+ 'nickname' => $nickname));
+ }
+
+ foreach (array('subscriptions', 'subscribers') as $a) {
+ $m->connect($a.'/:tag',
+ array('action' => $a,
+ 'nickname' => $nickname),
+ array('tag' => '[a-zA-Z0-9]+'));
+ }
+
+ foreach (array('rss', 'groups') as $a) {
+ $m->connect($a,
+ array('action' => 'user'.$a,
+ 'nickname' => $nickname));
+ }
+
+ foreach (array('all', 'replies', 'favorites') as $a) {
+ $m->connect($a.'/rss',
+ array('action' => $a.'rss',
+ 'nickname' => $nickname));
+ }
+
+ $m->connect('favorites',
+ array('action' => 'showfavorites',
+ 'nickname' => $nickname));
+
+ $m->connect('avatar/:size',
+ array('action' => 'avatarbynickname',
+ 'nickname' => $nickname),
+ array('size' => '(original|96|48|24)'));
+
+ $m->connect('tag/:tag/rss',
+ array('action' => 'userrss',
+ 'nickname' => $nickname),
+ array('tag' => '[a-zA-Z0-9]+'));
+
+ $m->connect('tag/:tag',
+ array('action' => 'showstream',
+ 'nickname' => $nickname),
+ array('tag' => '[a-zA-Z0-9]+'));
+
+ $m->connect('',
+ array('action' => 'showstream',
+ 'nickname' => $nickname));
+
+ } else {
+
+ $m->connect('', array('action' => 'public'));
+ $m->connect('rss', array('action' => 'publicrss'));
+ $m->connect('featuredrss', array('action' => 'featuredrss'));
+ $m->connect('favoritedrss', array('action' => 'favoritedrss'));
+ $m->connect('featured/', array('action' => 'featured'));
+ $m->connect('featured', array('action' => 'featured'));
+ $m->connect('favorited/', array('action' => 'favorited'));
+ $m->connect('favorited', array('action' => 'favorited'));
+
+ foreach (array('subscriptions', 'subscribers',
+ 'nudge', 'all', 'foaf', 'xrds',
+ 'replies', 'inbox', 'outbox', 'microsummary') as $a) {
+ $m->connect(':nickname/'.$a,
+ array('action' => $a),
+ array('nickname' => '[a-zA-Z0-9]{1,64}'));
+ }
+
+ foreach (array('subscriptions', 'subscribers') as $a) {
+ $m->connect(':nickname/'.$a.'/:tag',
+ array('action' => $a),
+ array('tag' => '[a-zA-Z0-9]+',
+ 'nickname' => '[a-zA-Z0-9]{1,64}'));
+ }
+
+ foreach (array('rss', 'groups') as $a) {
+ $m->connect(':nickname/'.$a,
+ array('action' => 'user'.$a),
+ array('nickname' => '[a-zA-Z0-9]{1,64}'));
+ }
+
+ foreach (array('all', 'replies', 'favorites') as $a) {
+ $m->connect(':nickname/'.$a.'/rss',
+ array('action' => $a.'rss'),
+ array('nickname' => '[a-zA-Z0-9]{1,64}'));
+ }
+
+ $m->connect(':nickname/favorites',
+ array('action' => 'showfavorites'),
array('nickname' => '[a-zA-Z0-9]{1,64}'));
- }
- foreach (array('subscriptions', 'subscribers') as $a) {
- $m->connect(':nickname/'.$a.'/:tag',
- array('action' => $a),
- array('tag' => '[a-zA-Z0-9]+',
+ $m->connect(':nickname/avatar/:size',
+ array('action' => 'avatarbynickname'),
+ array('size' => '(original|96|48|24)',
'nickname' => '[a-zA-Z0-9]{1,64}'));
- }
- foreach (array('rss', 'groups') as $a) {
- $m->connect(':nickname/'.$a,
- array('action' => 'user'.$a),
+ $m->connect(':nickname/tag/:tag/rss',
+ array('action' => 'userrss'),
+ array('nickname' => '[a-zA-Z0-9]{1,64}'),
+ array('tag' => '[a-zA-Z0-9]+'));
+
+ $m->connect(':nickname/tag/:tag',
+ array('action' => 'showstream'),
+ array('nickname' => '[a-zA-Z0-9]{1,64}'),
+ array('tag' => '[a-zA-Z0-9]+'));
+
+ $m->connect(':nickname',
+ array('action' => 'showstream'),
array('nickname' => '[a-zA-Z0-9]{1,64}'));
}
- foreach (array('all', 'replies', 'favorites') as $a) {
- $m->connect(':nickname/'.$a.'/rss',
- array('action' => $a.'rss'),
- array('nickname' => '[a-zA-Z0-9]{1,64}'));
- }
-
- $m->connect(':nickname/favorites',
- array('action' => 'showfavorites'),
- array('nickname' => '[a-zA-Z0-9]{1,64}'));
-
- $m->connect(':nickname/avatar/:size',
- array('action' => 'avatarbynickname'),
- array('size' => '(original|96|48|24)',
- 'nickname' => '[a-zA-Z0-9]{1,64}'));
-
- $m->connect(':nickname/tag/:tag/rss',
- array('action' => 'userrss'),
- array('nickname' => '[a-zA-Z0-9]{1,64}'),
- array('tag' => '[a-zA-Z0-9]+'));
-
- $m->connect(':nickname/tag/:tag',
- array('action' => 'showstream'),
- array('nickname' => '[a-zA-Z0-9]{1,64}'),
- array('tag' => '[a-zA-Z0-9]+'));
-
- $m->connect(':nickname',
- array('action' => 'showstream'),
- array('nickname' => '[a-zA-Z0-9]{1,64}'));
+ // user stuff
Event::handle('RouterInitialized', array($m));
}
diff --git a/lib/spawningdaemon.php b/lib/spawningdaemon.php
index 8baefe88e8..b1961d6880 100644
--- a/lib/spawningdaemon.php
+++ b/lib/spawningdaemon.php
@@ -36,6 +36,11 @@ abstract class SpawningDaemon extends Daemon
{
protected $threads=1;
+ const EXIT_OK = 0;
+ const EXIT_ERR = 1;
+ const EXIT_SHUTDOWN = 100;
+ const EXIT_RESTART = 101;
+
function __construct($id=null, $daemonize=true, $threads=1)
{
parent::__construct($daemonize);
@@ -49,7 +54,7 @@ abstract class SpawningDaemon extends Daemon
/**
* Perform some actual work!
*
- * @return boolean true on success, false on failure
+ * @return int exit code; use self::EXIT_SHUTDOWN to request not to respawn.
*/
public abstract function runThread();
@@ -84,23 +89,30 @@ abstract class SpawningDaemon extends Daemon
while (count($children) > 0) {
$status = null;
$pid = pcntl_wait($status);
- if ($pid > 0) {
+ if ($pid > 0 && pcntl_wifexited($status)) {
+ $exitCode = pcntl_wexitstatus($status);
+
$i = array_search($pid, $children);
if ($i === false) {
- $this->log(LOG_ERR, "Unrecognized child pid $pid exited!");
+ $this->log(LOG_ERR, "Unrecognized child pid $pid exited with status $exitCode");
continue;
}
unset($children[$i]);
- $this->log(LOG_INFO, "Thread $i pid $pid exited.");
-
- $pid = pcntl_fork();
- if ($pid < 0) {
- $this->log(LOG_ERROR, "Couldn't fork to respawn thread $i; aborting thread.\n");
- } else if ($pid == 0) {
- $this->initAndRunChild($i);
+
+ if ($this->shouldRespawn($exitCode)) {
+ $this->log(LOG_INFO, "Thread $i pid $pid exited with status $exitCode; respawing.");
+
+ $pid = pcntl_fork();
+ if ($pid < 0) {
+ $this->log(LOG_ERROR, "Couldn't fork to respawn thread $i; aborting thread.\n");
+ } else if ($pid == 0) {
+ $this->initAndRunChild($i);
+ } else {
+ $this->log(LOG_INFO, "Respawned thread $i as pid $pid");
+ $children[$i] = $pid;
+ }
} else {
- $this->log(LOG_INFO, "Respawned thread $i as pid $pid");
- $children[$i] = $pid;
+ $this->log(LOG_INFO, "Thread $i pid $pid exited with status $exitCode; closing out thread.");
}
}
}
@@ -108,6 +120,24 @@ abstract class SpawningDaemon extends Daemon
return true;
}
+ /**
+ * Determine whether to respawn an exited subprocess based on its exit code.
+ * Otherwise we'll respawn all exits by default.
+ *
+ * @param int $exitCode
+ * @return boolean true to respawn
+ */
+ protected function shouldRespawn($exitCode)
+ {
+ if ($exitCode == self::EXIT_SHUTDOWN) {
+ // Thread requested a clean shutdown.
+ return false;
+ } else {
+ // Otherwise we should always respawn!
+ return true;
+ }
+ }
+
/**
* Initialize things for a fresh thread, call runThread(), and
* exit at completion with appropriate return value.
@@ -116,8 +146,8 @@ abstract class SpawningDaemon extends Daemon
{
$this->set_id($this->get_id() . "." . $thread);
$this->resetDb();
- $ok = $this->runThread();
- exit($ok ? 0 : 1);
+ $exitCode = $this->runThread();
+ exit($exitCode);
}
/**
diff --git a/lib/stompqueuemanager.php b/lib/stompqueuemanager.php
index 8f0091a138..19e8c49b5c 100644
--- a/lib/stompqueuemanager.php
+++ b/lib/stompqueuemanager.php
@@ -38,8 +38,10 @@ class StompQueueManager extends QueueManager
var $password = null;
var $base = null;
var $con = null;
+ protected $control;
protected $sites = array();
+ protected $subscriptions = array();
protected $useTransactions = true;
protected $transaction = null;
@@ -52,6 +54,7 @@ class StompQueueManager extends QueueManager
$this->username = common_config('queue', 'stomp_username');
$this->password = common_config('queue', 'stomp_password');
$this->base = common_config('queue', 'queue_basename');
+ $this->control = common_config('queue', 'control_channel');
}
/**
@@ -77,6 +80,36 @@ class StompQueueManager extends QueueManager
$this->initialize();
}
+ /**
+ * Optional; ping any running queue handler daemons with a notification
+ * such as announcing a new site to handle or requesting clean shutdown.
+ * This avoids having to restart all the daemons manually to update configs
+ * and such.
+ *
+ * Currently only relevant for multi-site queue managers such as Stomp.
+ *
+ * @param string $event event key
+ * @param string $param optional parameter to append to key
+ * @return boolean success
+ */
+ public function sendControlSignal($event, $param='')
+ {
+ $message = $event;
+ if ($param != '') {
+ $message .= ':' . $param;
+ }
+ $this->_connect();
+ $result = $this->con->send($this->control,
+ $message,
+ array ('created' => common_sql_now()));
+ if ($result) {
+ $this->_log(LOG_INFO, "Sent control ping to queue daemons: $message");
+ return true;
+ } else {
+ $this->_log(LOG_ERR, "Failed sending control ping to queue daemons: $message");
+ return false;
+ }
+ }
/**
* Instantiate the appropriate QueueHandler class for the given queue.
@@ -86,7 +119,7 @@ class StompQueueManager extends QueueManager
*/
function getHandler($queue)
{
- $handlers = $this->handlers[common_config('site', 'server')];
+ $handlers = $this->handlers[$this->currentSite()];
if (isset($handlers[$queue])) {
$class = $handlers[$queue];
if (class_exists($class)) {
@@ -108,7 +141,7 @@ class StompQueueManager extends QueueManager
function getQueues()
{
$group = $this->activeGroup();
- $site = common_config('site', 'server');
+ $site = $this->currentSite();
if (empty($this->groups[$site][$group])) {
return array();
} else {
@@ -126,8 +159,8 @@ class StompQueueManager extends QueueManager
*/
public function connect($transport, $class, $group='queuedaemon')
{
- $this->handlers[common_config('site', 'server')][$transport] = $class;
- $this->groups[common_config('site', 'server')][$group][$transport] = $class;
+ $this->handlers[$this->currentSite()][$transport] = $class;
+ $this->groups[$this->currentSite()][$group][$transport] = $class;
}
/**
@@ -145,7 +178,8 @@ class StompQueueManager extends QueueManager
$result = $this->con->send($this->queueName($queue),
$msg, // BODY of the message
- array ('created' => common_sql_now()));
+ array ('created' => common_sql_now(),
+ 'persistent' => 'true'));
if (!$result) {
common_log(LOG_ERR, "Error sending $rep to $queue queue");
@@ -180,7 +214,16 @@ class StompQueueManager extends QueueManager
$ok = true;
$frames = $this->con->readFrames();
foreach ($frames as $frame) {
- $ok = $ok && $this->_handleItem($frame);
+ $dest = $frame->headers['destination'];
+ if ($dest == $this->control) {
+ if (!$this->handleControlSignal($frame)) {
+ // We got a control event that requests a shutdown;
+ // close out and stop handling anything else!
+ break;
+ }
+ } else {
+ $ok = $ok && $this->handleItem($frame);
+ }
}
return $ok;
}
@@ -197,6 +240,9 @@ class StompQueueManager extends QueueManager
public function start($master)
{
parent::start($master);
+ $this->_connect();
+
+ $this->con->subscribe($this->control);
if ($this->sites) {
foreach ($this->sites as $server) {
StatusNet::init($server);
@@ -221,6 +267,7 @@ class StompQueueManager extends QueueManager
// If there are any outstanding delivered messages we haven't processed,
// free them for another thread to take.
$this->rollback();
+ $this->con->unsubscribe($this->control);
if ($this->sites) {
foreach ($this->sites as $server) {
StatusNet::init($server);
@@ -231,7 +278,16 @@ class StompQueueManager extends QueueManager
}
return true;
}
-
+
+ /**
+ * Get identifier of the currently active site configuration
+ * @return string
+ */
+ protected function currentSite()
+ {
+ return common_config('site', 'server'); // @fixme switch to nickname
+ }
+
/**
* Lazy open connection to Stomp queue server.
*/
@@ -255,22 +311,29 @@ class StompQueueManager extends QueueManager
*/
protected function doSubscribe()
{
+ $site = $this->currentSite();
$this->_connect();
foreach ($this->getQueues() as $queue) {
$rawqueue = $this->queueName($queue);
+ $this->subscriptions[$site][$queue] = $rawqueue;
$this->_log(LOG_INFO, "Subscribing to $rawqueue");
$this->con->subscribe($rawqueue);
}
}
-
+
/**
* Subscribe from all enabled notice queues for the current site.
*/
protected function doUnsubscribe()
{
+ $site = $this->currentSite();
$this->_connect();
- foreach ($this->getQueues() as $queue) {
- $this->con->unsubscribe($this->queueName($queue));
+ if (!empty($this->subscriptions[$site])) {
+ foreach ($this->subscriptions[$site] as $queue => $rawqueue) {
+ $this->_log(LOG_INFO, "Unsubscribing from $rawqueue");
+ $this->con->unsubscribe($rawqueue);
+ unset($this->subscriptions[$site][$queue]);
+ }
}
}
@@ -286,10 +349,10 @@ class StompQueueManager extends QueueManager
* @param StompFrame $frame
* @return bool
*/
- protected function _handleItem($frame)
+ protected function handleItem($frame)
{
list($site, $queue) = $this->parseDestination($frame->headers['destination']);
- if ($site != common_config('site', 'server')) {
+ if ($site != $this->currentSite()) {
$this->stats('switch');
StatusNet::init($site);
}
@@ -317,7 +380,7 @@ class StompQueueManager extends QueueManager
$handler = $this->getHandler($queue);
if (!$handler) {
- $this->_log(LOG_ERROR, "Missing handler class; skipping $info");
+ $this->_log(LOG_ERR, "Missing handler class; skipping $info");
$this->ack($frame);
$this->commit();
$this->begin();
@@ -348,6 +411,77 @@ class StompQueueManager extends QueueManager
return true;
}
+ /**
+ * Process a control signal broadcast.
+ *
+ * @param array $frame Stomp frame
+ * @return bool true to continue; false to stop further processing.
+ */
+ protected function handleControlSignal($frame)
+ {
+ $message = trim($frame->body);
+ if (strpos($message, ':') !== false) {
+ list($event, $param) = explode(':', $message, 2);
+ } else {
+ $event = $message;
+ $param = '';
+ }
+
+ $shutdown = false;
+
+ if ($event == 'shutdown') {
+ $this->master->requestShutdown();
+ $shutdown = true;
+ } else if ($event == 'restart') {
+ $this->master->requestRestart();
+ $shutdown = true;
+ } else if ($event == 'update') {
+ $this->updateSiteConfig($param);
+ } else {
+ $this->_log(LOG_ERR, "Ignoring unrecognized control message: $message");
+ }
+
+ $this->ack($frame);
+ $this->commit();
+ $this->begin();
+ return $shutdown;
+ }
+
+ /**
+ * Set us up with queue subscriptions for a new site added at runtime,
+ * triggered by a broadcast to the 'statusnet-control' topic.
+ *
+ * @param array $frame Stomp frame
+ * @return bool true to continue; false to stop further processing.
+ */
+ protected function updateSiteConfig($nickname)
+ {
+ if (empty($this->sites)) {
+ if ($nickname == common_config('site', 'nickname')) {
+ StatusNet::init(common_config('site', 'server'));
+ $this->doUnsubscribe();
+ $this->doSubscribe();
+ } else {
+ $this->_log(LOG_INFO, "Ignoring update ping for other site $nickname");
+ }
+ } else {
+ $sn = Status_network::staticGet($nickname);
+ if ($sn) {
+ $server = $sn->getServerName(); // @fixme do config-by-nick
+ StatusNet::init($server);
+ if (empty($this->sites[$server])) {
+ $this->addSite($server);
+ }
+ $this->_log(LOG_INFO, "(Re)subscribing to queues for site $nickname / $server");
+ $this->doUnsubscribe();
+ $this->doSubscribe();
+ $this->stats('siteupdate');
+ } else {
+ $this->_log(LOG_ERR, "Ignoring ping for unrecognized new site $nickname");
+ }
+ }
+ }
+
/**
* Combines the queue_basename from configuration with the
* site server name and queue name to give eg:
@@ -360,7 +494,7 @@ class StompQueueManager extends QueueManager
protected function queueName($queue)
{
return common_config('queue', 'queue_basename') .
- common_config('site', 'server') . '/' . $queue;
+ $this->currentSite() . '/' . $queue;
}
/**
diff --git a/lib/uapplugin.php b/lib/uapplugin.php
new file mode 100644
index 0000000000..ef35bafbfb
--- /dev/null
+++ b/lib/uapplugin.php
@@ -0,0 +1,204 @@
+.
+ *
+ * @category Action
+ * @package StatusNet
+ * @author Sarven Capadisli
+ * @author Evan Prodromou
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+/**
+ * Abstract superclass for advertising plugins
+ *
+ * Plugins for showing ads should derive from this plugin.
+ *
+ * Outputs the following ad types (based on UAP):
+ *
+ * Medium Rectangle 300x250
+ * Rectangle 180x150
+ * Leaderboard 728x90
+ * Wide Skyscraper 160x600
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Sarven Capadisli
+ * @author Evan Prodromou
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+abstract class UAPPlugin extends Plugin
+{
+ public $mediumRectangle = null;
+ public $rectangle = null;
+ public $leaderboard = null;
+ public $wideSkyscraper = null;
+
+ /**
+ * Output our dedicated stylesheet
+ *
+ * @param Action $action Action being shown
+ *
+ * @return boolean hook flag
+ */
+
+ function onEndShowStatusNetStyles($action)
+ {
+ // XXX: allow override by theme
+ $action->cssLink('css/uap.css', 'base', 'screen, projection, tv');
+ return true;
+ }
+
+ /**
+ * Add a medium rectangle ad at the beginning of sidebar
+ *
+ * @param Action $action Action being shown
+ *
+ * @return boolean hook flag
+ */
+
+ function onStartShowAside($action)
+ {
+ if (!is_null($this->mediumRectangle)) {
+
+ $action->elementStart('div',
+ array('id' => 'ad_medium-rectangle',
+ 'class' => 'ad'));
+
+ $this->showMediumRectangle($action);
+
+ $action->elementEnd('div');
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a leaderboard in the header
+ *
+ * @param Action $action Action being shown
+ *
+ * @return boolean hook flag
+ */
+
+ function onEndShowHeader($action)
+ {
+ if (!is_null($this->leaderboard)) {
+ $action->elementStart('div',
+ array('id' => 'ad_leaderboard',
+ 'class' => 'ad'));
+ $this->showLeaderboard($action);
+ $action->elementEnd('div');
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a rectangle before aside sections
+ *
+ * @param Action $action Action being shown
+ *
+ * @return boolean hook flag
+ */
+
+ function onStartShowSections($action)
+ {
+ if (!is_null($this->rectangle)) {
+ $action->elementStart('div',
+ array('id' => 'ad_rectangle',
+ 'class' => 'ad'));
+ $this->showRectangle($action);
+ $action->elementEnd('div');
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a wide skyscraper after the aside
+ *
+ * @param Action $action Action being shown
+ *
+ * @return boolean hook flag
+ */
+
+ function onEndShowAside($action)
+ {
+ if (!is_null($this->wideSkyscraper)) {
+ $action->elementStart('div',
+ array('id' => 'ad_wide-skyscraper',
+ 'class' => 'ad'));
+
+ $this->showWideSkyscraper($action);
+
+ $action->elementEnd('div');
+ }
+ return true;
+ }
+
+ /**
+ * Show a medium rectangle ad
+ *
+ * @param Action $action Action being shown
+ *
+ * @return void
+ */
+
+ abstract protected function showMediumRectangle($action);
+
+ /**
+ * Show a rectangle ad
+ *
+ * @param Action $action Action being shown
+ *
+ * @return void
+ */
+
+ abstract protected function showRectangle($action);
+
+ /**
+ * Show a wide skyscraper ad
+ *
+ * @param Action $action Action being shown
+ *
+ * @return void
+ */
+
+ abstract protected function showWideSkyscraper($action);
+
+ /**
+ * Show a leaderboard ad
+ *
+ * @param Action $action Action being shown
+ *
+ * @return void
+ */
+
+ abstract protected function showLeaderboard($action);
+}
diff --git a/plugins/Adsense/AdsensePlugin.php b/plugins/Adsense/AdsensePlugin.php
new file mode 100644
index 0000000000..ab2b9a6fb3
--- /dev/null
+++ b/plugins/Adsense/AdsensePlugin.php
@@ -0,0 +1,160 @@
+.
+ *
+ * @category Ads
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @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);
+}
+
+/**
+ * Plugin to add Google Adsense to StatusNet sites
+ *
+ * This plugin lets you add Adsense ad units to your StatusNet site.
+ *
+ * We support the 4 ad sizes for the Universal Ad Platform (UAP):
+ *
+ * Medium Rectangle
+ * (Small) Rectangle
+ * Leaderboard
+ * Wide Skyscraper
+ *
+ * They fit in different places on the default theme. Some themes
+ * might interact quite poorly with this plugin.
+ *
+ * To enable advertising, you must sign up with Google Adsense and
+ * get a client ID.
+ *
+ * https://www.google.com/adsense/
+ *
+ * You'll also need to create an Adsense for Content unit in one
+ * of the four sizes described above. At the end of the process,
+ * note the "google_ad_client" and "google_ad_slot" values in the
+ * resultant Javascript.
+ *
+ * Add the plugin to config.php like so:
+ *
+ * addPlugin('Adsense', array('client' => 'Your client ID',
+ * 'rectangle' => 'slot'));
+ *
+ * Here, your client ID is the value of google_ad_client and the
+ * slot is the value of google_ad_slot. Note that if you create
+ * a different size, you'll need to provide different arguments:
+ * 'mediumRectangle', 'leaderboard', or 'wideSkyscraper'.
+ *
+ * If for some reason your ad server is different from the default,
+ * use the 'adScript' parameter to set the full path to the ad script.
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ *
+ * @seeAlso UAPPlugin
+ */
+
+class AdsensePlugin extends UAPPlugin
+{
+ public $adScript = 'http://pagead2.googlesyndication.com/pagead/show_ads.js';
+ public $client = null;
+
+ /**
+ * Show a medium rectangle 'ad'
+ *
+ * @param Action $action Action being shown
+ *
+ * @return void
+ */
+
+ protected function showMediumRectangle($action)
+ {
+ $this->showAdsenseCode($action, 300, 250, $this->mediumRectangle);
+ }
+
+ /**
+ * Show a rectangle 'ad'
+ *
+ * @param Action $action Action being shown
+ *
+ * @return void
+ */
+
+ protected function showRectangle($action)
+ {
+ $this->showAdsenseCode($action, 180, 150, $this->rectangle);
+ }
+
+ /**
+ * Show a wide skyscraper ad
+ *
+ * @param Action $action Action being shown
+ *
+ * @return void
+ */
+
+ protected function showWideSkyscraper($action)
+ {
+ $this->showAdsenseCode($action, 160, 600, $this->wideSkyscraper);
+ }
+
+ /**
+ * Show a leaderboard ad
+ *
+ * @param Action $action Action being shown
+ *
+ * @return void
+ */
+
+ protected function showLeaderboard($action)
+ {
+ $this->showAdsenseCode($action, 728, 90, $this->leaderboard);
+ }
+
+ /**
+ * Output the bits of JavaScript code to show Adsense
+ *
+ * @param Action $action Action being shown
+ * @param integer $width Width of the block
+ * @param integer $height Height of the block
+ * @param string $slot Slot identifier
+ *
+ * @return void
+ */
+
+ protected function showAdsenseCode($action, $width, $height, $slot)
+ {
+ $code = 'google_ad_client = "'.$this->client.'"; ';
+ $code .= 'google_ad_slot = "'.$slot.'"; ';
+ $code .= 'google_ad_width = '.$width.'; ';
+ $code .= 'google_ad_height = '.$height.'; ';
+
+ $action->inlineScript($code);
+
+ $action->script($this->adScript);
+ }
+}
\ No newline at end of file
diff --git a/plugins/BlankAd/BlankAdPlugin.php b/plugins/BlankAd/BlankAdPlugin.php
new file mode 100644
index 0000000000..0e2719aed0
--- /dev/null
+++ b/plugins/BlankAd/BlankAdPlugin.php
@@ -0,0 +1,124 @@
+.
+ *
+ * @category Ads
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @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);
+}
+
+/**
+ * Plugin for testing ad layout
+ *
+ * This plugin uses the UAPPlugin framework to output ad content. However,
+ * its ad content is just images with one red pixel stretched to the
+ * right size. It's mostly useful for debugging theme layout.
+ *
+ * To use this plugin, set the parameter for the ad size you want to use
+ * to true (or anything non-null). For example, to make a leaderboard:
+ *
+ * addPlugin('BlankAd', array('leaderboard' => true));
+ *
+ * @category Plugin
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ *
+ * @seeAlso Location
+ */
+
+class BlankAdPlugin extends UAPPlugin
+{
+ /**
+ * Show a medium rectangle 'ad'
+ *
+ * @param Action $action Action being shown
+ *
+ * @return void
+ */
+
+ protected function showMediumRectangle($action)
+ {
+ $action->element('img',
+ array('width' => 300,
+ 'height' => 250,
+ 'src' => common_path('plugins/BlankAd/redpixel.png')),
+ '');
+ }
+
+ /**
+ * Show a rectangle 'ad'
+ *
+ * @param Action $action Action being shown
+ *
+ * @return void
+ */
+
+ protected function showRectangle($action)
+ {
+ $action->element('img',
+ array('width' => 180,
+ 'height' => 150,
+ 'src' => common_path('plugins/BlankAd/redpixel.png')),
+ '');
+ }
+
+ /**
+ * Show a wide skyscraper ad
+ *
+ * @param Action $action Action being shown
+ *
+ * @return void
+ */
+
+ protected function showWideSkyscraper($action)
+ {
+ $action->element('img',
+ array('width' => 160,
+ 'height' => 600,
+ 'src' => common_path('plugins/BlankAd/redpixel.png')),
+ '');
+ }
+
+ /**
+ * Show a leaderboard ad
+ *
+ * @param Action $action Action being shown
+ *
+ * @return void
+ */
+
+ protected function showLeaderboard($action)
+ {
+ $action->element('img',
+ array('width' => 728,
+ 'height' => 90,
+ 'src' => common_path('plugins/BlankAd/redpixel.png')),
+ '');
+ }
+}
\ No newline at end of file
diff --git a/plugins/BlankAd/redpixel.png b/plugins/BlankAd/redpixel.png
new file mode 100644
index 0000000000..26299a5525
Binary files /dev/null and b/plugins/BlankAd/redpixel.png differ
diff --git a/plugins/Facebook/facebookaction.php b/plugins/Facebook/facebookaction.php
index 815fee094c..389e1ea81f 100644
--- a/plugins/Facebook/facebookaction.php
+++ b/plugins/Facebook/facebookaction.php
@@ -89,7 +89,7 @@ class FacebookAction extends Action
function showScripts()
{
- $this->script('js/facebookapp.js');
+ $this->script('facebookapp.js');
}
/**
diff --git a/plugins/OpenX/OpenXPlugin.php b/plugins/OpenX/OpenXPlugin.php
new file mode 100644
index 0000000000..59485f25d8
--- /dev/null
+++ b/plugins/OpenX/OpenXPlugin.php
@@ -0,0 +1,165 @@
+.
+ *
+ * @category Ads
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @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);
+}
+
+/**
+ * Plugin for OpenX Ad Server
+ *
+ * This plugin supports the OpenX ad server, http://www.openx.org/
+ *
+ * We support the 4 ad sizes for the Universal Ad Platform (UAP):
+ *
+ * Medium Rectangle
+ * (Small) Rectangle
+ * Leaderboard
+ * Wide Skyscraper
+ *
+ * They fit in different places on the default theme. Some themes
+ * might interact quite poorly with this plugin.
+ *
+ * To enable advertising, you will need an OpenX server. You'll need
+ * to set up a "zone" for your StatusNet site that identifies a
+ * kind of ad you want to place (of the above 4 sizes).
+ *
+ * Add the plugin to config.php like so:
+ *
+ * addPlugin('OpenX', array('adScript' => 'full path to script',
+ * 'rectangle' => 1));
+ *
+ * Here, the 'adScript' parameter is the full path to the OpenX
+ * ad script, like 'http://example.com/www/delivery/ajs.php'. Note
+ * that we don't do any magic to swap between HTTP and HTTPS, so
+ * if you want HTTPS, say so.
+ *
+ * The 'rectangle' parameter is the zone ID for that ad space on
+ * your site. If you've configured another size, try 'mediumRectangle',
+ * 'leaderboard', or 'wideSkyscraper'.
+ *
+ * If for some reason your ad server is different from the default,
+ * use the 'adScript' parameter to set the full path to the ad script.
+ *
+ * @category Ads
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ *
+ * @seeAlso UAPPlugin
+ */
+
+class OpenXPlugin extends UAPPlugin
+{
+ public $adScript = null;
+
+ /**
+ * Show a medium rectangle 'ad'
+ *
+ * @param Action $action Action being shown
+ *
+ * @return void
+ */
+
+ protected function showMediumRectangle($action)
+ {
+ $this->showAd($action, $this->mediumRectangle);
+ }
+
+ /**
+ * Show a rectangle 'ad'
+ *
+ * @param Action $action Action being shown
+ *
+ * @return void
+ */
+
+ protected function showRectangle($action)
+ {
+ $this->showAd($action, $this->rectangle);
+ }
+
+ /**
+ * Show a wide skyscraper ad
+ *
+ * @param Action $action Action being shown
+ *
+ * @return void
+ */
+
+ protected function showWideSkyscraper($action)
+ {
+ $this->showAd($action, $this->wideSkyscraper);
+ }
+
+ /**
+ * Show a leaderboard ad
+ *
+ * @param Action $action Action being shown
+ *
+ * @return void
+ */
+
+ protected function showLeaderboard($action)
+ {
+ $this->showAd($action, $this->leaderboard);
+ }
+
+ /**
+ * Show an ad using OpenX
+ *
+ * @param Action $action Action being shown
+ * @param integer $zone Zone to show
+ *
+ * @return void
+ */
+
+ protected function showAd($action, $zone)
+ {
+$scr = <<<\/scr"+"ipt>");
+ENDOFSCRIPT;
+
+ $action->inlineScript(sprintf($scr, $this->adScript, $zone));
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php b/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php
index c59fcca890..14d1608d3c 100644
--- a/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php
+++ b/plugins/PoweredByStatusNet/PoweredByStatusNetPlugin.php
@@ -46,8 +46,9 @@ class PoweredByStatusNetPlugin extends Plugin
function onEndAddressData($action)
{
$action->elementStart('span', 'poweredby');
- $action->text(_('powered by'));
- $action->element('a', array('href' => 'http://status.net/'), 'StatusNet');
+ $action->raw(sprintf(_m('powered by %s'),
+ sprintf('%s',
+ _m('StatusNet'))));
$action->elementEnd('span');
return true;
diff --git a/plugins/PoweredByStatusNet/locale/PoweredByStatusNet.po b/plugins/PoweredByStatusNet/locale/PoweredByStatusNet.po
new file mode 100644
index 0000000000..bd39124efe
--- /dev/null
+++ b/plugins/PoweredByStatusNet/locale/PoweredByStatusNet.po
@@ -0,0 +1,32 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-01-22 15:03-0800\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: PoweredByStatusNetPlugin.php:49
+#, php-format
+msgid "powered by %s"
+msgstr ""
+
+#: PoweredByStatusNetPlugin.php:51
+msgid "StatusNet"
+msgstr ""
+
+#: PoweredByStatusNetPlugin.php:64
+msgid ""
+"Outputs powered by StatusNet after site "
+"name."
+msgstr ""
diff --git a/plugins/PubSubHubBub/PubSubHubBubPlugin.php b/plugins/PubSubHubBub/PubSubHubBubPlugin.php
index 8286cd5489..a880dc8666 100644
--- a/plugins/PubSubHubBub/PubSubHubBubPlugin.php
+++ b/plugins/PubSubHubBub/PubSubHubBubPlugin.php
@@ -79,6 +79,21 @@ class PubSubHubBubPlugin extends Plugin
parent::__construct();
}
+ /**
+ * Check if plugin should be active; may be mass-enabled.
+ * @return boolean
+ */
+
+ function enabled()
+ {
+ if (common_config('site', 'private')) {
+ // PuSH relies on public feeds
+ return false;
+ }
+ // @fixme check for being on a private network?
+ return true;
+ }
+
/**
* Hooks the StartApiAtom event
*
@@ -92,8 +107,9 @@ class PubSubHubBubPlugin extends Plugin
function onStartApiAtom($action)
{
- $action->element('link', array('rel' => 'hub', 'href' => $this->hub), null);
-
+ if ($this->enabled()) {
+ $action->element('link', array('rel' => 'hub', 'href' => $this->hub), null);
+ }
return true;
}
@@ -110,9 +126,11 @@ class PubSubHubBubPlugin extends Plugin
function onStartApiRss($action)
{
- $action->element('atom:link', array('rel' => 'hub',
- 'href' => $this->hub),
- null);
+ if ($this->enabled()) {
+ $action->element('atom:link', array('rel' => 'hub',
+ 'href' => $this->hub),
+ null);
+ }
return true;
}
@@ -130,6 +148,9 @@ class PubSubHubBubPlugin extends Plugin
function onHandleQueuedNotice($notice)
{
+ if (!$this->enabled()) {
+ return false;
+ }
$publisher = new Publisher($this->hub);
$feeds = array();
@@ -211,13 +232,20 @@ class PubSubHubBubPlugin extends Plugin
'format' => 'atom'));
}
}
+ $feeds = array_unique($feeds);
- foreach (array_unique($feeds) as $feed) {
- if (!$publisher->publish_update($feed)) {
- common_log_line(LOG_WARNING,
- $feed.' was not published to hub at '.
- $this->hub.':'.$publisher->last_response());
- }
+ ob_start();
+ $ok = $publisher->publish_update($feeds);
+ $push_last_response = ob_get_clean();
+
+ if (!$ok) {
+ common_log(LOG_WARNING,
+ 'Failure publishing ' . count($feeds) . ' feeds to hub at '.
+ $this->hub.': '.$push_last_response);
+ } else {
+ common_log(LOG_INFO,
+ 'Published ' . count($feeds) . ' feeds to hub at '.
+ $this->hub.': '.$push_last_response);
}
return true;
@@ -236,16 +264,21 @@ class PubSubHubBubPlugin extends Plugin
function onPluginVersion(&$versions)
{
+ $about = _m('The PubSubHubBub plugin pushes RSS/Atom updates '.
+ 'to a PubSubHubBub hub.');
+ if (!$this->enabled()) {
+ $about = '' . $about . ' ' .
+ _m('(inactive on private site)');
+ }
$versions[] = array('name' => 'PubSubHubBub',
'version' => STATUSNET_VERSION,
'author' => 'Craig Andrews',
'homepage' =>
'http://status.net/wiki/Plugin:PubSubHubBub',
'rawdescription' =>
- _m('The PubSubHubBub plugin pushes RSS/Atom updates '.
- 'to a PubSubHubBub hub.'));
+ $about);
return true;
}
diff --git a/plugins/Realtime/RealtimePlugin.php b/plugins/Realtime/RealtimePlugin.php
index 89640f5beb..16e28e94d3 100644
--- a/plugins/Realtime/RealtimePlugin.php
+++ b/plugins/Realtime/RealtimePlugin.php
@@ -87,7 +87,7 @@ class RealtimePlugin extends Plugin
$scripts = $this->_getScripts();
foreach ($scripts as $script) {
- $action->script($script);
+ $action->script(common_path($script));
}
$user = common_current_user();
diff --git a/plugins/Realtime/realtimeupdate.css b/plugins/Realtime/realtimeupdate.css
index 56f869354d..31e7c2ae66 100644
--- a/plugins/Realtime/realtimeupdate.css
+++ b/plugins/Realtime/realtimeupdate.css
@@ -18,7 +18,8 @@ display:none;
}
.realtime-popup #form_notice label[for=notice_data-attach],
-.realtime-popup #form_notice #notice_data-attach {
+.realtime-popup #form_notice #notice_data-attach,
+.realtime-popup #form_notice label[for=notice_data-geo] {
top:0;
}
diff --git a/plugins/TwitterBridge/TwitterBridgePlugin.php b/plugins/TwitterBridge/TwitterBridgePlugin.php
index 57b3c1c995..c7f57ffc77 100644
--- a/plugins/TwitterBridge/TwitterBridgePlugin.php
+++ b/plugins/TwitterBridge/TwitterBridgePlugin.php
@@ -20,7 +20,8 @@
* @category Plugin
* @package StatusNet
* @author Zach Copley
- * @copyright 2009 Control Yourself, Inc.
+ * @author Julien C
+ * @copyright 2009-2010 Control Yourself, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
@@ -41,6 +42,7 @@ define('TWITTERBRIDGEPLUGIN_VERSION', '0.9');
* @category Plugin
* @package StatusNet
* @author Zach Copley
+ * @author Julien C
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
* @link http://twitter.com/
@@ -72,6 +74,27 @@ class TwitterBridgePlugin extends Plugin
$m->connect('twitter/authorization',
array('action' => 'twitterauthorization'));
$m->connect('settings/twitter', array('action' => 'twittersettings'));
+ $m->connect('main/twitterlogin', array('action' => 'twitterlogin'));
+
+ return true;
+ }
+
+ /*
+ * Add a login tab for 'Sign in with Twitter'
+ *
+ * @param Action &action the current action
+ *
+ * @return void
+ */
+ function onEndLoginGroupNav(&$action)
+ {
+
+ $action_name = $action->trimmed('action');
+
+ $action->menuItem(common_local_url('twitterlogin'),
+ _('Twitter'),
+ _('Login or register using Twitter'),
+ 'twitterlogin' === $action_name);
return true;
}
@@ -108,6 +131,7 @@ class TwitterBridgePlugin extends Plugin
switch ($cls) {
case 'TwittersettingsAction':
case 'TwitterauthorizationAction':
+ case 'TwitterloginAction':
include_once INSTALLDIR . '/plugins/TwitterBridge/' .
strtolower(mb_substr($cls, 0, -6)) . '.php';
return false;
diff --git a/plugins/TwitterBridge/twitterauthorization.php b/plugins/TwitterBridge/twitterauthorization.php
index 4af2f03941..b2657ff61f 100644
--- a/plugins/TwitterBridge/twitterauthorization.php
+++ b/plugins/TwitterBridge/twitterauthorization.php
@@ -19,10 +19,11 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*
- * @category TwitterauthorizationAction
+ * @category Plugin
* @package StatusNet
- * @author Zach Copely
- * @copyright 2009 StatusNet, Inc.
+ * @author Zach Copley
+ * @author Julien C
+ * @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/
*/
@@ -41,15 +42,21 @@ require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php';
* (Foreign_link) between the StatusNet user and Twitter user and stores the
* access token and secret in the link.
*
- * @category Twitter
+ * @category Plugin
* @package StatusNet
* @author Zach Copley
+ * @author Julien C
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*
*/
class TwitterauthorizationAction extends Action
{
+ var $twuid = null;
+ var $tw_fields = null;
+ var $access_token = null;
+ var $signin = null;
+
/**
* Initialize class members. Looks for 'oauth_token' parameter.
*
@@ -61,6 +68,7 @@ class TwitterauthorizationAction extends Action
{
parent::prepare($args);
+ $this->signin = $this->boolean('signin');
$this->oauth_token = $this->arg('oauth_token');
return true;
@@ -77,28 +85,61 @@ class TwitterauthorizationAction extends Action
{
parent::handle($args);
- if (!common_logged_in()) {
- $this->clientError(_m('Not logged in.'), 403);
+ if (common_logged_in()) {
+ $user = common_current_user();
+ $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);
+
+ // If there's already a foreign link record, it means we already
+ // have an access token, and this is unecessary. So go back.
+
+ if (isset($flink)) {
+ common_redirect(common_local_url('twittersettings'));
+ }
}
- $user = common_current_user();
- $flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
- // If there's already a foreign link record, it means we already
- // have an access token, and this is unecessary. So go back.
+ // User was not logged in to StatusNet before
- if (isset($flink)) {
- common_redirect(common_local_url('twittersettings'));
- }
+ $this->twuid = $this->trimmed('twuid');
- // $this->oauth_token is only populated once Twitter authorizes our
- // request token. If it's empty we're at the beginning of the auth
- // process
+ $this->tw_fields = array('screen_name' => $this->trimmed('tw_fields_screen_name'),
+ 'fullname' => $this->trimmed('tw_fields_fullname'));
- if (empty($this->oauth_token)) {
- $this->authorizeRequestToken();
+ $this->access_token = new OAuthToken($this->trimmed('access_token_key'), $this->trimmed('access_token_secret'));
+
+ $token = $this->trimmed('token');
+
+ if (!$token || $token != common_session_token()) {
+ $this->showForm(_('There was a problem with your session token. Try again, please.'));
+ return;
+ }
+
+ if ($this->arg('create')) {
+ if (!$this->boolean('license')) {
+ $this->showForm(_('You can\'t register if you don\'t agree to the license.'),
+ $this->trimmed('newname'));
+ return;
+ }
+ $this->createNewUser();
+ } else if ($this->arg('connect')) {
+ $this->connectNewUser();
+ } else {
+ common_debug('Twitter Connect Plugin - ' .
+ print_r($this->args, true));
+ $this->showForm(_('Something weird happened.'),
+ $this->trimmed('newname'));
+ }
} else {
- $this->saveAccessToken();
+ // $this->oauth_token is only populated once Twitter authorizes our
+ // request token. If it's empty we're at the beginning of the auth
+ // process
+
+ if (empty($this->oauth_token)) {
+ $this->authorizeRequestToken();
+ } else {
+ $this->saveAccessToken();
+ }
}
}
@@ -123,7 +164,7 @@ class TwitterauthorizationAction extends Action
$_SESSION['twitter_request_token'] = $req_tok->key;
$_SESSION['twitter_request_token_secret'] = $req_tok->secret;
- $auth_link = $client->getAuthorizeLink($req_tok);
+ $auth_link = $client->getAuthorizeLink($req_tok, $this->signin);
} catch (OAuthClientException $e) {
$msg = sprintf('OAuth client cURL error - code: %1s, msg: %2s',
@@ -150,6 +191,8 @@ class TwitterauthorizationAction extends Action
$this->serverError(_m('Couldn\'t link your Twitter account.'));
}
+ $twitter_user = null;
+
try {
$client = new TwitterOAuthClient($_SESSION['twitter_request_token'],
@@ -165,40 +208,54 @@ class TwitterauthorizationAction extends Action
$twitter_user = $client->verifyCredentials();
} catch (OAuthClientException $e) {
- $msg = sprintf('OAuth client cURL error - code: %1$s, msg: %2$s',
+ $msg = sprintf('OAuth client error - code: %1$s, msg: %2$s',
$e->getCode(), $e->getMessage());
$this->serverError(_m('Couldn\'t link your Twitter account.'));
}
- // Save the access token and Twitter user info
+ if (common_logged_in()) {
- $this->saveForeignLink($atok, $twitter_user);
+ // Save the access token and Twitter user info
+
+ $user = common_current_user();
+ $this->saveForeignLink($user->id, $twitter_user->id, $atok);
+ save_twitter_user($twitter_user->id, $twitter_user->name);
+
+ } else {
+
+ $this->twuid = $twitter_user->id;
+ $this->tw_fields = array("screen_name" => $twitter_user->screen_name,
+ "name" => $twitter_user->name);
+ $this->access_token = $atok;
+ $this->tryLogin();
+ }
// Clean up the the mess we made in the session
unset($_SESSION['twitter_request_token']);
unset($_SESSION['twitter_request_token_secret']);
- common_redirect(common_local_url('twittersettings'));
+ if (common_logged_in()) {
+ common_redirect(common_local_url('twittersettings'));
+ }
}
/**
* Saves a Foreign_link between Twitter user and local user,
* which includes the access token and secret.
*
- * @param OAuthToken $access_token the access token to save
- * @param mixed $twitter_user twitter API user object
+ * @param int $user_id StatusNet user ID
+ * @param int $twuid Twitter user ID
+ * @param OAuthToken $token the access token to save
*
* @return nothing
*/
- function saveForeignLink($access_token, $twitter_user)
+ function saveForeignLink($user_id, $twuid, $access_token)
{
- $user = common_current_user();
-
$flink = new Foreign_link();
- $flink->user_id = $user->id;
- $flink->foreign_id = $twitter_user->id;
+ $flink->user_id = $user_id;
+ $flink->foreign_id = $twuid;
$flink->service = TWITTER_SERVICE;
$creds = TwitterOAuthClient::packToken($access_token);
@@ -214,10 +271,325 @@ class TwitterauthorizationAction extends Action
if (empty($flink_id)) {
common_log_db_error($flink, 'INSERT', __FILE__);
- $this->serverError(_m('Couldn\'t link your Twitter account.'));
+ $this->serverError(_('Couldn\'t link your Twitter account.'));
}
- save_twitter_user($twitter_user->id, $twitter_user->screen_name);
+ return $flink_id;
+ }
+
+ function showPageNotice()
+ {
+ if ($this->error) {
+ $this->element('div', array('class' => 'error'), $this->error);
+ } else {
+ $this->element('div', 'instructions',
+ sprintf(_('This is the first time you\'ve logged into %s so we must connect your Twitter account to a local account. You can either create a new account, or connect with your existing account, if you have one.'), common_config('site', 'name')));
+ }
+ }
+
+ function title()
+ {
+ return _('Twitter Account Setup');
+ }
+
+ function showForm($error=null, $username=null)
+ {
+ $this->error = $error;
+ $this->username = $username;
+
+ $this->showPage();
+ }
+
+ function showPage()
+ {
+ parent::showPage();
+ }
+
+ function showContent()
+ {
+ if (!empty($this->message_text)) {
+ $this->element('p', null, $this->message);
+ return;
+ }
+
+ $this->elementStart('form', array('method' => 'post',
+ 'id' => 'form_settings_twitter_connect',
+ 'class' => 'form_settings',
+ 'action' => common_local_url('twitterauthorization')));
+ $this->elementStart('fieldset', array('id' => 'settings_twitter_connect_options'));
+ $this->element('legend', null, _('Connection options'));
+ $this->elementStart('ul', 'form_data');
+ $this->elementStart('li');
+ $this->element('input', array('type' => 'checkbox',
+ 'id' => 'license',
+ 'class' => 'checkbox',
+ 'name' => 'license',
+ 'value' => 'true'));
+ $this->elementStart('label', array('class' => 'checkbox', 'for' => 'license'));
+ $this->text(_('My text and files are available under '));
+ $this->element('a', array('href' => common_config('license', 'url')),
+ common_config('license', 'title'));
+ $this->text(_(' except this private data: password, email address, IM address, phone number.'));
+ $this->elementEnd('label');
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+ $this->hidden('access_token_key', $this->access_token->key);
+ $this->hidden('access_token_secret', $this->access_token->secret);
+ $this->hidden('twuid', $this->twuid);
+ $this->hidden('tw_fields_screen_name', $this->tw_fields['screen_name']);
+ $this->hidden('tw_fields_name', $this->tw_fields['name']);
+
+ $this->elementStart('fieldset');
+ $this->hidden('token', common_session_token());
+ $this->element('legend', null,
+ _('Create new account'));
+ $this->element('p', null,
+ _('Create a new user with this nickname.'));
+ $this->elementStart('ul', 'form_data');
+ $this->elementStart('li');
+ $this->input('newname', _('New nickname'),
+ ($this->username) ? $this->username : '',
+ _('1-64 lowercase letters or numbers, no punctuation or spaces'));
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+ $this->submit('create', _('Create'));
+ $this->elementEnd('fieldset');
+
+ $this->elementStart('fieldset');
+ $this->element('legend', null,
+ _('Connect existing account'));
+ $this->element('p', null,
+ _('If you already have an account, login with your username and password to connect it to your Twitter account.'));
+ $this->elementStart('ul', 'form_data');
+ $this->elementStart('li');
+ $this->input('nickname', _('Existing nickname'));
+ $this->elementEnd('li');
+ $this->elementStart('li');
+ $this->password('password', _('Password'));
+ $this->elementEnd('li');
+ $this->elementEnd('ul');
+ $this->submit('connect', _('Connect'));
+ $this->elementEnd('fieldset');
+
+ $this->elementEnd('fieldset');
+ $this->elementEnd('form');
+ }
+
+ function message($msg)
+ {
+ $this->message_text = $msg;
+ $this->showPage();
+ }
+
+ function createNewUser()
+ {
+ if (common_config('site', 'closed')) {
+ $this->clientError(_('Registration not allowed.'));
+ return;
+ }
+
+ $invite = null;
+
+ if (common_config('site', 'inviteonly')) {
+ $code = $_SESSION['invitecode'];
+ if (empty($code)) {
+ $this->clientError(_('Registration not allowed.'));
+ return;
+ }
+
+ $invite = Invitation::staticGet($code);
+
+ if (empty($invite)) {
+ $this->clientError(_('Not a valid invitation code.'));
+ return;
+ }
+ }
+
+ $nickname = $this->trimmed('newname');
+
+ if (!Validate::string($nickname, array('min_length' => 1,
+ 'max_length' => 64,
+ 'format' => NICKNAME_FMT))) {
+ $this->showForm(_('Nickname must have only lowercase letters and numbers and no spaces.'));
+ return;
+ }
+
+ if (!User::allowed_nickname($nickname)) {
+ $this->showForm(_('Nickname not allowed.'));
+ return;
+ }
+
+ if (User::staticGet('nickname', $nickname)) {
+ $this->showForm(_('Nickname already in use. Try another one.'));
+ return;
+ }
+
+ $fullname = trim($this->tw_fields['name']);
+
+ $args = array('nickname' => $nickname, 'fullname' => $fullname);
+
+ if (!empty($invite)) {
+ $args['code'] = $invite->code;
+ }
+
+ $user = User::register($args);
+
+ $result = $this->saveForeignLink($user->id,
+ $this->twuid,
+ $this->access_token);
+
+ save_twitter_user($this->twuid, $this->tw_fields['screen_name']);
+
+ if (!$result) {
+ $this->serverError(_('Error connecting user to Twitter.'));
+ return;
+ }
+
+ common_set_user($user);
+ common_real_login(true);
+
+ common_debug('TwitterBridge Plugin - ' .
+ "Registered new user $user->id from Twitter user $this->twuid");
+
+ common_redirect(common_local_url('showstream', array('nickname' => $user->nickname)),
+ 303);
+ }
+
+ function connectNewUser()
+ {
+ $nickname = $this->trimmed('nickname');
+ $password = $this->trimmed('password');
+
+ if (!common_check_user($nickname, $password)) {
+ $this->showForm(_('Invalid username or password.'));
+ return;
+ }
+
+ $user = User::staticGet('nickname', $nickname);
+
+ if (!empty($user)) {
+ common_debug('TwitterBridge Plugin - ' .
+ "Legit user to connect to Twitter: $nickname");
+ }
+
+ $result = $this->saveForeignLink($user->id,
+ $this->twuid,
+ $this->access_token);
+
+ save_twitter_user($this->twuid, $this->tw_fields['screen_name']);
+
+ if (!$result) {
+ $this->serverError(_('Error connecting user to Twitter.'));
+ return;
+ }
+
+ common_debug('TwitterBridge Plugin - ' .
+ "Connected Twitter user $this->twuid to local user $user->id");
+
+ common_set_user($user);
+ common_real_login(true);
+
+ $this->goHome($user->nickname);
+ }
+
+ function connectUser()
+ {
+ $user = common_current_user();
+
+ $result = $this->flinkUser($user->id, $this->twuid);
+
+ if (empty($result)) {
+ $this->serverError(_('Error connecting user to Twitter.'));
+ return;
+ }
+
+ common_debug('TwitterBridge Plugin - ' .
+ "Connected Twitter user $this->twuid to local user $user->id");
+
+ // Return to Twitter connection settings tab
+ common_redirect(common_local_url('twittersettings'), 303);
+ }
+
+ function tryLogin()
+ {
+ common_debug('TwitterBridge Plugin - ' .
+ "Trying login for Twitter user $this->twuid.");
+
+ $flink = Foreign_link::getByForeignID($this->twuid,
+ TWITTER_SERVICE);
+
+ if (!empty($flink)) {
+ $user = $flink->getUser();
+
+ if (!empty($user)) {
+
+ common_debug('TwitterBridge Plugin - ' .
+ "Logged in Twitter user $flink->foreign_id as user $user->id ($user->nickname)");
+
+ common_set_user($user);
+ common_real_login(true);
+ $this->goHome($user->nickname);
+ }
+
+ } else {
+
+ common_debug('TwitterBridge Plugin - ' .
+ "No flink found for twuid: $this->twuid - new user");
+
+ $this->showForm(null, $this->bestNewNickname());
+ }
+ }
+
+ function goHome($nickname)
+ {
+ $url = common_get_returnto();
+ if ($url) {
+ // We don't have to return to it again
+ common_set_returnto(null);
+ } else {
+ $url = common_local_url('all',
+ array('nickname' =>
+ $nickname));
+ }
+
+ common_redirect($url, 303);
+ }
+
+ function bestNewNickname()
+ {
+ if (!empty($this->tw_fields['name'])) {
+ $nickname = $this->nicknamize($this->tw_fields['name']);
+ if ($this->isNewNickname($nickname)) {
+ return $nickname;
+ }
+ }
+
+ return null;
+ }
+
+ // Given a string, try to make it work as a nickname
+
+ function nicknamize($str)
+ {
+ $str = preg_replace('/\W/', '', $str);
+ $str = str_replace(array('-', '_'), '', $str);
+ return strtolower($str);
+ }
+
+ function isNewNickname($str)
+ {
+ if (!Validate::string($str, array('min_length' => 1,
+ 'max_length' => 64,
+ 'format' => NICKNAME_FMT))) {
+ return false;
+ }
+ if (!User::allowed_nickname($str)) {
+ return false;
+ }
+ if (User::staticGet('nickname', $str)) {
+ return false;
+ }
+ return true;
}
}
diff --git a/plugins/TwitterBridge/twitterlogin.php b/plugins/TwitterBridge/twitterlogin.php
new file mode 100644
index 0000000000..79421fb27d
--- /dev/null
+++ b/plugins/TwitterBridge/twitterlogin.php
@@ -0,0 +1,97 @@
+.
+ *
+ * @category Login
+ * @package StatusNet
+ * @author Julien Chaumond
+ * @author Zach Copley
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET') && !defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php';
+
+/**
+ * Page for logging in with Twitter
+ *
+ * @category Login
+ * @package StatusNet
+ * @author Julien Chaumond
+ * @author Zach Copley
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ *
+ * @see SettingsAction
+ */
+
+class TwitterloginAction extends Action
+{
+ function handle($args)
+ {
+ parent::handle($args);
+
+ if (common_is_real_login()) {
+ $this->clientError(_('Already logged in.'));
+ }
+
+ $this->showPage();
+ }
+
+ function title()
+ {
+ return _('Twitter Login');
+ }
+
+ function getInstructions()
+ {
+ return _('Login with your Twitter account');
+ }
+
+ function showPageNotice()
+ {
+ $instr = $this->getInstructions();
+ $output = common_markup_to_html($instr);
+ $this->elementStart('div', 'instructions');
+ $this->raw($output);
+ $this->elementEnd('div');
+ }
+
+ function showContent()
+ {
+ $this->elementStart('a', array('href' => common_local_url('twitterauthorization',
+ null,
+ array('signin' => true))));
+ $this->element('img', array('src' => common_path('plugins/TwitterBridge/Sign-in-with-Twitter-lighter.png'),
+ 'alt' => 'Sign in with Twitter'));
+ $this->elementEnd('a');
+ }
+
+ function showLocalNav()
+ {
+ $nav = new LoginGroupNav($this);
+ $nav->show();
+ }
+}
diff --git a/plugins/TwitterBridge/twitteroauthclient.php b/plugins/TwitterBridge/twitteroauthclient.php
index bad2b74ca3..277e7ab409 100644
--- a/plugins/TwitterBridge/twitteroauthclient.php
+++ b/plugins/TwitterBridge/twitteroauthclient.php
@@ -45,6 +45,7 @@ class TwitterOAuthClient extends OAuthClient
{
public static $requestTokenURL = 'https://twitter.com/oauth/request_token';
public static $authorizeURL = 'https://twitter.com/oauth/authorize';
+ public static $signinUrl = 'https://twitter.com/oauth/authenticate';
public static $accessTokenURL = 'https://twitter.com/oauth/access_token';
/**
@@ -97,9 +98,11 @@ class TwitterOAuthClient extends OAuthClient
*
* @return the link
*/
- function getAuthorizeLink($request_token)
+ function getAuthorizeLink($request_token, $signin = false)
{
- return parent::getAuthorizeLink(self::$authorizeURL,
+ $url = ($signin) ? self::$signinUrl : self::$authorizeURL;
+
+ return parent::getAuthorizeLink($url,
$request_token,
common_local_url('twitterauthorization'));
}
diff --git a/plugins/TwitterBridge/twittersettings.php b/plugins/TwitterBridge/twittersettings.php
index bc9a636a15..0137060e9c 100644
--- a/plugins/TwitterBridge/twittersettings.php
+++ b/plugins/TwitterBridge/twittersettings.php
@@ -121,8 +121,35 @@ class TwittersettingsAction extends ConnectSettingsAction
$this->elementEnd('p');
$this->element('p', 'form_note',
_m('Connected Twitter account'));
+ $this->elementEnd('fieldset');
- $this->submit('remove', _m('Remove'));
+ $this->elementStart('fieldset');
+
+ $this->element('legend', null, _m('Disconnect my account from Twitter'));
+
+ if (!$user->password) {
+
+ $this->elementStart('p', array('class' => 'form_guide'));
+ $this->text(_m('Disconnecting your Twitter ' .
+ 'could make it impossible to log in! Please '));
+ $this->element('a',
+ array('href' => common_local_url('passwordsettings')),
+ _m('set a password'));
+
+ $this->text(_m(' first.'));
+ $this->elementEnd('p');
+ } else {
+
+ $note = _m('Keep your %1$s account but disconnect from Twitter. ' .
+ 'You can use your %1$s password to log in.');
+
+ $site = common_config('site', 'name');
+
+ $this->element('p', 'instructions',
+ sprintf($note, $site));
+
+ $this->submit('disconnect', _m('Disconnect'));
+ }
$this->elementEnd('fieldset');
@@ -205,7 +232,7 @@ class TwittersettingsAction extends ConnectSettingsAction
if ($this->arg('save')) {
$this->savePreferences();
- } else if ($this->arg('remove')) {
+ } else if ($this->arg('disconnect')) {
$this->removeTwitterAccount();
} else {
$this->showForm(_m('Unexpected form submission.'));
@@ -231,7 +258,7 @@ class TwittersettingsAction extends ConnectSettingsAction
return;
}
- $this->showForm(_m('Twitter account removed.'), true);
+ $this->showForm(_m('Twitter account disconnected.'), true);
}
/**
diff --git a/scripts/console.php b/scripts/console.php
index 8b62a3a967..4d207c261b 100755
--- a/scripts/console.php
+++ b/scripts/console.php
@@ -45,10 +45,12 @@ function read_input_line($prompt)
if (CONSOLE_INTERACTIVE) {
if (CONSOLE_READLINE) {
$line = readline($prompt);
- readline_add_history($line);
- if (defined('CONSOLE_HISTORY')) {
- // Save often; it's easy to hit fatal errors.
- readline_write_history(CONSOLE_HISTORY);
+ if (trim($line) != '') {
+ readline_add_history($line);
+ if (defined('CONSOLE_HISTORY')) {
+ // Save often; it's easy to hit fatal errors.
+ readline_write_history(CONSOLE_HISTORY);
+ }
}
return $line;
} else {
diff --git a/scripts/queuectl.php b/scripts/queuectl.php
new file mode 100755
index 0000000000..1c9ea33536
--- /dev/null
+++ b/scripts/queuectl.php
@@ -0,0 +1,85 @@
+#!/usr/bin/env php
+.
+ */
+
+/**
+ * Sends control signals to running queue daemons.
+ *
+ * @author Brion Vibber
+ * @package QueueHandler
+ */
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
+
+$shortoptions = 'ur';
+$longoptions = array('update', 'restart', 'stop');
+
+$helptext = <<sendControlSignal($event, $param)) {
+ print " sent.\n";
+ } else {
+ print " FAILED.\n";
+ }
+}
+
+$actions = 0;
+
+if (have_option('u') || have_option('--update')) {
+ $nickname = common_config('site', 'nickname');
+ doSendControl("Sending site update signal to queue daemons for $nickname",
+ "update", $nickname);
+ $actions++;
+}
+
+if (have_option('r') || have_option('--restart')) {
+ doSendControl("Sending graceful restart signal to queue daemons...",
+ "restart");
+ $actions++;
+}
+
+if (have_option('--stop')) {
+ doSendControl("Sending graceful shutdown signal to queue daemons...",
+ "shutdown");
+ $actions++;
+}
+
+if (!$actions) {
+ show_help();
+}
+
diff --git a/scripts/queuedaemon.php b/scripts/queuedaemon.php
index a9cfda6d72..c2e2351c39 100755
--- a/scripts/queuedaemon.php
+++ b/scripts/queuedaemon.php
@@ -21,7 +21,7 @@
define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
$shortoptions = 'fi:at:';
-$longoptions = array('id=', 'foreground', 'all', 'threads=', 'skip-xmpp', 'xmpp-only');
+$longoptions = array('id=', 'foreground', 'all', 'threads=');
/**
* Attempts to get a count of the processors available on the current system
@@ -115,7 +115,7 @@ class QueueDaemon extends SpawningDaemon
$this->log(LOG_INFO, 'terminating normally');
- return true;
+ return $master->respawn ? self::EXIT_RESTART : self::EXIT_SHUTDOWN;
}
}
@@ -163,13 +163,6 @@ if (!$threads) {
$daemonize = !(have_option('f') || have_option('--foreground'));
$all = have_option('a') || have_option('--all');
-if (have_option('--skip-xmpp')) {
- define('XMPP_EMERGENCY_FLAG', true);
-}
-if (have_option('--xmpp-only')) {
- define('XMPP_ONLY_FLAG', true);
-}
-
$daemon = new QueueDaemon($id, $daemonize, $threads, $all);
$daemon->runOnce();
diff --git a/scripts/sendemail.php b/scripts/sendemail.php
new file mode 100755
index 0000000000..436e085bed
--- /dev/null
+++ b/scripts/sendemail.php
@@ -0,0 +1,82 @@
+#!/usr/bin/env php
+.
+ */
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
+
+$shortoptions = 'i:n:';
+$longoptions = array('id=', 'nickname=', 'subject=');
+
+$helptext = <<
+Sends given email text to user.
+
+ -i --id id of the user to query
+ -n --nickname nickname of the user to query
+ --subject mail subject line (required)
+
+END_OF_USEREMAIL_HELP;
+
+require_once INSTALLDIR.'/scripts/commandline.inc';
+
+if (have_option('i', 'id')) {
+ $id = get_option_value('i', 'id');
+ $user = User::staticGet('id', $id);
+ if (empty($user)) {
+ print "Can't find user with ID $id\n";
+ exit(1);
+ }
+} else if (have_option('n', 'nickname')) {
+ $nickname = get_option_value('n', 'nickname');
+ $user = User::staticGet('nickname', $nickname);
+ if (empty($user)) {
+ print "Can't find user with nickname '$nickname'\n";
+ exit(1);
+ }
+} else {
+ print "You must provide a user by --id or --nickname\n";
+ exit(1);
+}
+
+if (empty($user->email)) {
+ // @fixme unconfirmed address?
+ print "No email registered for user '$user->nickname'\n";
+ exit(1);
+}
+
+if (!have_option('subject')) {
+ echo "You must provide a subject line for the mail in --subject='...' param.\n";
+ exit(1);
+}
+$subject = get_option_value('subject');
+
+if (posix_isatty(STDIN)) {
+ print "You must provide message input on stdin!\n";
+ exit(1);
+}
+$body = file_get_contents('php://stdin');
+
+print "Sending to $user->email...";
+if (mail_to_user($user, $subject, $body)) {
+ print " done\n";
+} else {
+ print " failed.\n";
+ exit(1);
+}
+
diff --git a/scripts/setup.cfg.sample b/scripts/setup.cfg.sample
index 8d03b06f5e..f247a3bcae 100644
--- a/scripts/setup.cfg.sample
+++ b/scripts/setup.cfg.sample
@@ -11,4 +11,8 @@ export AVATARBASE=/var/www/avatar.example.net
export BACKGROUNDBASE=/var/www/background.example.net
export FILEBASE=/var/www/file.example.net
export PWDGEN="pwgen 20"
-
+export PHPBASE=/var/www/statusnet
+export WILDCARD=example.net
+export MAILTEMPLATE=/etc/statusnet/newsite-mail.txt
+export MAILSUBJECT="Your new StatusNet site"
+export POSTINSTALL=/etc/statusnet/morestuff.sh
diff --git a/scripts/setup_status_network.sh b/scripts/setup_status_network.sh
index 777711fb55..f502a169a2 100755
--- a/scripts/setup_status_network.sh
+++ b/scripts/setup_status_network.sh
@@ -2,9 +2,23 @@
source /etc/statusnet/setup.cfg
-export nickname=$1
-export sitename=$2
+# setup_status_net.sh mysite 'My Site' '1user' 'owner@example.com' 'Firsty McLastname'
+export nickname="$1"
+export sitename="$2"
+export tags="$3"
+export email="$4"
+export fullname="$5"
+
+# Fixme: if this is changed later we need to update profile URLs
+# for the created user.
+export server="$nickname.$WILDCARD"
+
+# End-user info
+export userpass=`$PWDGEN`
+export roles="administrator moderator owner"
+
+# DB info
export password=`$PWDGEN`
export database=$nickname$DBBASE
export username=$nickname$USERBASE
@@ -21,8 +35,8 @@ mysql -h $DBHOST -u $ADMIN --password=$ADMINPASS $SITEDB << ENDOFCOMMANDS
GRANT ALL ON $database.* TO '$username'@'localhost' IDENTIFIED BY '$password';
GRANT ALL ON $database.* TO '$username'@'%' IDENTIFIED BY '$password';
-INSERT INTO status_network (nickname, dbhost, dbuser, dbpass, dbname, sitename, created)
-VALUES ('$nickname', '$DBHOSTNAME', '$username', '$password', '$database', '$sitename', now());
+INSERT INTO status_network (nickname, dbhost, dbuser, dbpass, dbname, sitename, created, tags)
+VALUES ('$nickname', '$DBHOSTNAME', '$username', '$password', '$database', '$sitename', now(), '$tags');
ENDOFCOMMANDS
@@ -30,3 +44,39 @@ for top in $AVATARBASE $FILEBASE $BACKGROUNDBASE; do
mkdir $top/$nickname
chmod a+w $top/$nickname
done
+
+php $PHPBASE/scripts/registeruser.php \
+ -s"$server" \
+ -n"$nickname" \
+ -f"$fullname" \
+ -w"$userpass" \
+ -e"$email"
+
+for role in $roles
+do
+ php $PHPBASE/scripts/userrole.php \
+ -s"$server" \
+ -n"$nickname" \
+ -r"$role"
+done
+
+if [ -f "$MAILTEMPLATE" ]
+then
+ # fixme how safe is this? are sitenames sanitized?
+ cat $MAILTEMPLATE | \
+ sed "s/\$nickname/$nickname/" | \
+ sed "s/\$sitename/$sitename/" | \
+ sed "s/\$userpass/$userpass/" | \
+ php $PHPBASE/scripts/sendemail.php \
+ -s"$server" \
+ -n"$nickname" \
+ --subject="$MAILSUBJECT"
+else
+ echo "No mail template, not sending email."
+fi
+
+if [ -f "$POSTINSTALL" ]
+then
+ echo "Running $POSTINSTALL ..."
+ source "$POSTINSTALL"
+fi
diff --git a/scripts/xmppdaemon.php b/scripts/xmppdaemon.php
index fd7cf055b4..46dd9b90cc 100755
--- a/scripts/xmppdaemon.php
+++ b/scripts/xmppdaemon.php
@@ -56,7 +56,7 @@ class XMPPDaemon extends SpawningDaemon
common_log(LOG_INFO, 'terminating normally');
- return true;
+ return $master->respawn ? self::EXIT_RESTART : self::EXIT_SHUTDOWN;
}
}
diff --git a/tests/oauth/README b/tests/oauth/README
new file mode 100644
index 0000000000..dd76feb0c6
--- /dev/null
+++ b/tests/oauth/README
@@ -0,0 +1,22 @@
+Some very rough test scripts for hitting up the OAuth endpoints.
+
+Note: this works best if you register an OAuth application, leaving
+the callback URL blank.
+
+Put your instance info and consumer key and secret in oauth.ini
+
+Example usage:
+--------------
+
+php getrequesttoken.php
+
+Gets a request token, token secret and a url to authorize it. Once
+you authorize the request token you can exchange it for an access token...
+
+php exchangetokens.php --oauth_token=b9a79548a88c1aa9a5bea73103c6d41d --token_secret=4a47d9337fc0202a14ab552e17a3b657
+
+Once you have your access token, go ahead and try a protected API
+resource:
+
+php verifycreds.php --oauth_token=cf2de7665f0dda0a82c2dc39b01be7f9 --token_secret=4524c3b712200138e1a4cff2e9ca83d8
+
diff --git a/tests/oauth/exchangetokens.php b/tests/oauth/exchangetokens.php
new file mode 100755
index 0000000000..2394826c7e
--- /dev/null
+++ b/tests/oauth/exchangetokens.php
@@ -0,0 +1,105 @@
+#!/usr/bin/env php
+.
+ */
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../..'));
+
+require_once INSTALLDIR . '/extlib/OAuth.php';
+
+$ini = parse_ini_file("oauth.ini");
+
+$test_consumer = new OAuthConsumer($ini['consumer_key'], $ini['consumer_secret']);
+
+$at_endpoint = $ini['apiroot'] . $ini['access_token_url'];
+
+$shortoptions = 't:s:';
+$longoptions = array('oauth_token=', 'token_secret=');
+
+$helptext = <<sign_request($hmac_method, $test_consumer, $rt);
+
+$r = httpRequest($req_req->to_url());
+
+common_debug("Exchange request token = " . var_export($rt, true));
+common_debug("Exchange tokens URL: " . $req_req->to_url());
+
+$body = $r->getBody();
+
+$token_stuff = array();
+parse_str($body, $token_stuff);
+
+print 'Access token : ' . $token_stuff['oauth_token'] . "\n";
+print 'Access token secret : ' . $token_stuff['oauth_token_secret'] . "\n";
+
+function httpRequest($url)
+{
+ $request = HTTPClient::start();
+
+ $request->setConfig(array(
+ 'follow_redirects' => true,
+ 'connect_timeout' => 120,
+ 'timeout' => 120,
+ 'ssl_verify_peer' => false,
+ 'ssl_verify_host' => false
+ ));
+
+ return $request->get($url);
+}
+
diff --git a/tests/oauth/getrequesttoken.php b/tests/oauth/getrequesttoken.php
new file mode 100755
index 0000000000..fc546a0f4c
--- /dev/null
+++ b/tests/oauth/getrequesttoken.php
@@ -0,0 +1,71 @@
+#!/usr/bin/env php
+.
+ */
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../..'));
+
+require_once INSTALLDIR . '/scripts/commandline.inc';
+require_once INSTALLDIR . '/extlib/OAuth.php';
+
+$ini = parse_ini_file("oauth.ini");
+
+$test_consumer = new OAuthConsumer($ini['consumer_key'], $ini['consumer_secret']);
+
+$rt_endpoint = $ini['apiroot'] . $ini['request_token_url'];
+
+$parsed = parse_url($rt_endpoint);
+$params = array();
+
+parse_str($parsed['query'], $params);
+
+$hmac_method = new OAuthSignatureMethod_HMAC_SHA1();
+
+$req_req = OAuthRequest::from_consumer_and_token($test_consumer, NULL, "GET", $rt_endpoint, $params);
+$req_req->sign_request($hmac_method, $test_consumer, NULL);
+
+$r = httpRequest($req_req->to_url());
+
+$body = $r->getBody();
+
+$token_stuff = array();
+parse_str($body, $token_stuff);
+
+$authurl = $ini['apiroot'] . $ini['authorize_url'] . '?oauth_token=' . $token_stuff['oauth_token'];
+
+print 'Request token : ' . $token_stuff['oauth_token'] . "\n";
+print 'Request token secret : ' . $token_stuff['oauth_token_secret'] . "\n";
+print "Authorize URL : $authurl\n";
+
+//var_dump($req_req);
+
+function httpRequest($url)
+{
+ $request = HTTPClient::start();
+
+ $request->setConfig(array(
+ 'follow_redirects' => true,
+ 'connect_timeout' => 120,
+ 'timeout' => 120,
+ 'ssl_verify_peer' => false,
+ 'ssl_verify_host' => false
+ ));
+
+ return $request->get($url);
+}
+
diff --git a/tests/oauth/oauth.ini b/tests/oauth/oauth.ini
new file mode 100644
index 0000000000..16b747fe43
--- /dev/null
+++ b/tests/oauth/oauth.ini
@@ -0,0 +1,10 @@
+; Setup OAuth info here
+apiroot = "http://YOURSTATUSNET/api"
+
+request_token_url = "/oauth/request_token"
+authorize_url = "/oauth/authorize"
+access_token_url = "/oauth/access_token"
+
+consumer_key = "b748968e9bea81a53f3a3c15aa0c686f"
+consumer_secret = "5434e18cce05d9e53cdd48029a62fa41"
+
diff --git a/tests/oauth/statusupdate.php b/tests/oauth/statusupdate.php
new file mode 100644
index 0000000000..4aa230e280
--- /dev/null
+++ b/tests/oauth/statusupdate.php
@@ -0,0 +1,115 @@
+#!/usr/bin/env php
+.
+ **/
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../..'));
+
+require_once INSTALLDIR . '/extlib/OAuth.php';
+
+$shortoptions = 'o:s:u:';
+$longoptions = array('oauth_token=', 'token_secret=', 'update=');
+
+$helptext = <<sign_request($hmac_method, $test_consumer, $at);
+
+$r = httpRequest($req_req->to_url());
+
+$body = $r->getBody();
+
+print "$body\n";
+
+//print $req_req->to_url() . "\n\n";
+
+function httpRequest($url)
+{
+ $request = HTTPClient::start();
+
+ $request->setConfig(array(
+ 'follow_redirects' => true,
+ 'connect_timeout' => 120,
+ 'timeout' => 120,
+ 'ssl_verify_peer' => false,
+ 'ssl_verify_host' => false
+ ));
+
+ return $request->post($url);
+}
+
diff --git a/tests/oauth/verifycreds.php b/tests/oauth/verifycreds.php
new file mode 100755
index 0000000000..873bdb8bdd
--- /dev/null
+++ b/tests/oauth/verifycreds.php
@@ -0,0 +1,101 @@
+#!/usr/bin/env php
+.
+ */
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/../..'));
+
+require_once INSTALLDIR . '/extlib/OAuth.php';
+
+$shortoptions = 'o:s:';
+$longoptions = array('oauth_token=', 'token_secret=');
+
+$helptext = <<sign_request($hmac_method, $test_consumer, $at);
+
+$r = httpRequest($req_req->to_url());
+
+$body = $r->getBody();
+
+print "$body\n";
+
+//print $req_req->to_url() . "\n\n";
+
+function httpRequest($url)
+{
+ $request = HTTPClient::start();
+
+ $request->setConfig(array(
+ 'follow_redirects' => true,
+ 'connect_timeout' => 120,
+ 'timeout' => 120,
+ 'ssl_verify_peer' => false,
+ 'ssl_verify_host' => false
+ ));
+
+ return $request->get($url);
+}
+
diff --git a/theme/base/css/display.css b/theme/base/css/display.css
index 7eff4709c7..fb74b1d0b0 100644
--- a/theme/base/css/display.css
+++ b/theme/base/css/display.css
@@ -177,7 +177,8 @@ font-weight:bold;
#form_password_recover legend,
#form_password_change legend,
.form_entity_block legend,
-#form_filter_bytag legend {
+#form_filter_bytag legend,
+#apioauthauthorize_allowdeny {
display:none;
}
@@ -895,9 +896,63 @@ font-weight:normal;
margin-right:11px;
}
+/*applications*/
+.applications {
+margin-bottom:18px;
+float:left;
+width:100%;
+}
+.applications li {
+list-style-type:none;
+}
+.application img,
+#showapplication .entity_profile img,
+.form_data #application_icon img,
+#apioauthauthorize .form_data img {
+max-width:96px;
+max-height:96px;
+}
+#apioauthauthorize .form_data img {
+margin-right:18px;
+float:left;
+}
+#showapplication .entity_profile {
+width:68%;
+}
+#showapplication .entity_profile .entity_fn {
+margin-left:0;
+}
+#showapplication .entity_profile .entity_fn .fn:before,
+#showapplication .entity_profile .entity_fn .fn:after {
+content:'';
+}
+#showapplication .entity_data {
+clear:both;
+margin-bottom:18px;
+}
+#showapplication .entity_data h2 {
+display:none;
+}
+#showapplication .entity_data dl {
+margin-bottom:18px;
+}
+#showapplication .entity_data dt {
+font-weight:bold;
+}
+#showapplication .entity_data dd {
+margin-left:1.795%;
+font-family:monospace;
+font-size:1.3em;
+}
+.form_data #application_types label.radio,
+.form_data #default_access_types label.radio {
+width:14.5%;
+}
+
/* NOTICE */
.notice,
-.profile {
+.profile,
+.application {
position:relative;
padding-top:11px;
padding-bottom:11px;
@@ -989,7 +1044,12 @@ margin-left:0;
margin-left:110px;
}
#shownotice .notice .entry-title {
+margin-left:110px;
font-size:2.2em;
+min-height:123px;
+}
+#shownotice .notice div.entry-content {
+margin-left:0;
}
.notice p.entry-content {
diff --git a/theme/base/css/uap.css b/theme/base/css/uap.css
new file mode 100644
index 0000000000..73be5f0c14
--- /dev/null
+++ b/theme/base/css/uap.css
@@ -0,0 +1,54 @@
+/** Universal Ad Package styles:
+ * Medium Rectangle 300x250
+ * Rectangle 180x150
+ * Leaderboard 728x90
+ * Wide Skyscraper 160x600
+ *
+ * @package StatusNet
+ * @author Sarven Capadisli
+ * @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/
+ */
+
+
+.ad {
+border:1px solid #CCC;
+float:left;
+}
+
+#ad_medium-rectangle {
+width:300px;
+height:250px;
+
+margin-left:1.35%;
+margin-bottom:18px;
+}
+
+#ad_rectangle {
+width:180px;
+height:150px;
+
+float:none;
+clear:both;
+margin:0 auto;
+margin-bottom:29px;
+}
+
+#ad_leaderboard {
+width:728px;
+height:90px;
+
+margin:0 auto 18px;
+float:none;
+clear:both;
+}
+
+#ad_wide-skyscraper {
+width:160px;
+height:600px;
+
+float:right;
+margin-top:18px;
+margin-right:8.25%;
+}
diff --git a/theme/base/images/icons/icons-01.gif b/theme/base/images/icons/icons-01.gif
index 06202a047b..f93d33d79b 100644
Binary files a/theme/base/images/icons/icons-01.gif and b/theme/base/images/icons/icons-01.gif differ
diff --git a/theme/base/images/icons/twotone/green/key.gif b/theme/base/images/icons/twotone/green/key.gif
new file mode 100644
index 0000000000..ccf357ab28
Binary files /dev/null and b/theme/base/images/icons/twotone/green/key.gif differ
diff --git a/theme/default/css/display.css b/theme/default/css/display.css
index 8a2c011752..3aebb239d3 100644
--- a/theme/default/css/display.css
+++ b/theme/default/css/display.css
@@ -129,6 +129,7 @@ color:#002FA7;
.notice,
.profile,
+.application,
#content tbody tr {
border-top-color:#C8D1D5;
}
@@ -187,7 +188,8 @@ button.close,
.entity_delete input.submit,
.notice-options .repeated,
.form_notice label[for=notice_data-geo],
-button.minimize {
+button.minimize,
+.form_reset_key input.submit {
background-image:url(../../base/images/icons/icons-01.gif);
background-repeat:no-repeat;
background-color:transparent;
@@ -332,6 +334,9 @@ background-position: 5px -1445px;
.entity_delete input.submit {
background-position: 5px -1511px;
}
+.form_reset_key input.submit {
+background-position: 5px -1973px;
+}
/* NOTICES */
.notice .attachment {
@@ -378,6 +383,7 @@ box-shadow:3px 3px 3px rgba(194, 194, 194, 0.3);
-webkit-box-shadow:3px 3px 3px rgba(194, 194, 194, 0.3);
}
#content .notices li:hover,
+#content .applications li:hover,
#content tbody tr:hover {
background-color:rgba(240, 240, 240, 0.2);
}
diff --git a/theme/identica/css/display.css b/theme/identica/css/display.css
index 4ee48459d0..2818196c20 100644
--- a/theme/identica/css/display.css
+++ b/theme/identica/css/display.css
@@ -129,6 +129,7 @@ color:#002FA7;
.notice,
.profile,
+.application,
#content tbody tr {
border-top-color:#CEE1E9;
}
@@ -187,7 +188,8 @@ button.close,
.entity_delete input.submit,
.notice-options .repeated,
.form_notice label[for=notice_data-geo],
-button.minimize {
+button.minimize,
+.form_reset_key input.submit {
background-image:url(../../base/images/icons/icons-01.gif);
background-repeat:no-repeat;
background-color:transparent;
@@ -331,6 +333,9 @@ background-position: 5px -1445px;
.entity_delete input.submit {
background-position: 5px -1511px;
}
+.form_reset_key input.submit {
+background-position: 5px -1973px;
+}
/* NOTICES */
.notice .attachment {
@@ -377,6 +382,7 @@ box-shadow:3px 3px 3px rgba(194, 194, 194, 0.3);
-webkit-box-shadow:3px 3px 3px rgba(194, 194, 194, 0.3);
}
#content .notices li:hover,
+#content .applications li:hover,
#content tbody tr:hover {
background-color:rgba(240, 240, 240, 0.2);
}