From 8dafe09ab25335c26e33548e988828a72e984239 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Thu, 21 Jan 2010 11:07:52 -0800 Subject: [PATCH 001/157] Option to log slow db queries or all db queries $config['db']['log_queries'] = true; // all $config['db']['log_slow_queries'] = 10; // queries taking > 10 seconds --- classes/Memcached_DataObject.php | 33 ++++++++++++++++++++++++++++++++ lib/default.php | 4 +++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/classes/Memcached_DataObject.php b/classes/Memcached_DataObject.php index 4ecab9db62..6ceb59e8b4 100644 --- a/classes/Memcached_DataObject.php +++ b/classes/Memcached_DataObject.php @@ -315,6 +315,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/lib/default.php b/lib/default.php index 5b2ae6c7c1..fc6b4ab79c 100644 --- a/lib/default.php +++ b/lib/default.php @@ -67,7 +67,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 From 4b0f953ccf7435d6ebe4a674953f62af36cc978b Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 22 Jan 2010 00:27:08 +0000 Subject: [PATCH 002/157] Quick hack to avoid breaking with geonames off when there's some old cookie state. This code's a little rough and tumble; any breakage halts JS execution and leaves the spinner going and no message submitted. --- js/util.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/js/util.js b/js/util.js index 88016bd6d0..a749c5d9f4 100644 --- a/js/util.js +++ b/js/util.js @@ -205,8 +205,10 @@ var SN = { // StatusNet cookieValue = JSON.parse(cookieValue); NLat = $('#'+SN.C.S.NoticeLat).val(cookieValue.NLat).val(); NLon = $('#'+SN.C.S.NoticeLon).val(cookieValue.NLon).val(); - NLNS = $('#'+SN.C.S.NoticeLocationNs).val(cookieValue.NLNS).val(); - NLID = $('#'+SN.C.S.NoticeLocationId).val(cookieValue.NLID).val(); + if ($('#'+SN.C.S.NoticeLocationNs).val(cookieValue.NLNS)) { + NLNS = $('#'+SN.C.S.NoticeLocationNs).val(cookieValue.NLNS).val(); + NLID = $('#'+SN.C.S.NoticeLocationId).val(cookieValue.NLID).val(); + } } if (cookieValue == 'disabled') { NDG = $('#'+SN.C.S.NoticeDataGeo).attr('checked', false).attr('checked'); @@ -301,8 +303,10 @@ var SN = { // StatusNet $('#'+SN.C.S.NoticeLat).val(NLat); $('#'+SN.C.S.NoticeLon).val(NLon); - $('#'+SN.C.S.NoticeLocationNs).val(NLNS); - $('#'+SN.C.S.NoticeLocationId).val(NLID); + if ($('#'+SN.C.S.NoticeLocationNs)) { + $('#'+SN.C.S.NoticeLocationNs).val(NLNS); + $('#'+SN.C.S.NoticeLocationId).val(NLID); + } $('#'+SN.C.S.NoticeDataGeo).attr('checked', NDG); } }); From 0bb23e6fd724a12bba6766949cd3294b288d8a43 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Thu, 21 Jan 2010 16:34:26 -0800 Subject: [PATCH 003/157] drop debug line from xmppdaemon.php, we're done debugging that --- scripts/xmppdaemon.php | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/xmppdaemon.php b/scripts/xmppdaemon.php index 0c118c53d6..cef9c4bd07 100755 --- a/scripts/xmppdaemon.php +++ b/scripts/xmppdaemon.php @@ -148,7 +148,6 @@ class XMPPDaemon extends Daemon function handle_message(&$pl) { - $this->log(LOG_DEBUG, "Received message: " . str_replace("\n", " ", var_export($pl, true))); $from = jabber_normalize_jid($pl['from']); if ($pl['type'] != 'chat') { From 0e852def6ae5aa529cca0aef1187152fb5a880be Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Thu, 21 Jan 2010 16:42:50 -0800 Subject: [PATCH 004/157] XMPP queued output & initial retooling of DB queue manager to support non-Notice objects. Queue handlers for XMPP individual & firehose output now send their XML stanzas to another output queue instead of connecting directly to the chat server. This lets us have as many general processing threads as we need, while all actual XMPP input and output go through a single daemon with a single connection open. This avoids problems with multiple connected resources: * multiple windows shown in some chat clients (psi, gajim, kopete) * extra load on server * incoming message delivery forwarding issues Database changes: * queue_item drops 'notice_id' in favor of a 'frame' blob. This is based on Craig Andrews' work branch to generalize queues to take any object, but conservatively leaving out the serialization for now. Table updater (preserves any existing queued items) in db/rc3to09.sql Code changes to watch out for: * Queue handlers should now define a handle() method instead of handle_notice() * QueueDaemon and XmppDaemon now share common i/o (IoMaster) and respawning thread management (RespawningDaemon) infrastructure. * The polling XmppConfirmManager has been dropped, as the message is queued directly when saving IM settings. * Enable $config['queue']['debug_memory'] to output current memory usage at each run through the event loop to watch for memory leaks To do: * Adapt XMPP i/o to component connection mode for multi-site support. * XMPP input can also be broken out to a queue, which would allow the actual notice save etc to be handled by general queue threads. * Make sure there are no problems with simply pushing serialized Notice objects to queues. * Find a way to improve interactive performance of the database-backed queue handler; polling is pretty painful to XMPP. * Possibly redo the way QueueHandlers are injected into a QueueManager. The grouping used to split out the XMPP output queue is a bit awkward. --- actions/imsettings.php | 10 +- classes/Queue_item.php | 30 +- classes/statusnet.ini | 6 +- db/08to09.sql | 16 + db/rc3to09.sql | 16 + db/statusnet.sql | 5 +- lib/dbqueuemanager.php | 138 ++----- lib/default.php | 1 + lib/iomaster.php | 39 +- lib/jabber.php | 43 ++- lib/jabberqueuehandler.php | 4 +- lib/ombqueuehandler.php | 2 +- lib/pingqueuehandler.php | 2 +- lib/pluginqueuehandler.php | 2 +- lib/publicqueuehandler.php | 6 +- lib/queued_xmpp.php | 117 ++++++ lib/queuehandler.php | 95 +---- lib/queuemanager.php | 125 +++++-- lib/smsqueuehandler.php | 2 +- lib/spawningdaemon.php | 159 ++++++++ lib/stompqueuemanager.php | 56 +-- lib/util.php | 3 +- lib/xmppconfirmmanager.php | 168 --------- lib/xmppmanager.php | 298 ++++++++++++--- lib/xmppoutqueuehandler.php | 55 +++ plugins/Enjit/enjitqueuehandler.php | 9 +- plugins/Facebook/facebookqueuehandler.php | 2 +- plugins/RSSCloud/RSSCloudPlugin.php | 41 +- plugins/RSSCloud/RSSCloudQueueHandler.php | 50 +-- plugins/TwitterBridge/twitterqueuehandler.php | 2 +- scripts/handlequeued.php | 2 +- scripts/queuedaemon.php | 149 ++------ scripts/xmppdaemon.php | 353 ++---------------- 33 files changed, 951 insertions(+), 1055 deletions(-) create mode 100644 db/rc3to09.sql create mode 100644 lib/queued_xmpp.php create mode 100644 lib/spawningdaemon.php delete mode 100644 lib/xmppconfirmmanager.php create mode 100644 lib/xmppoutqueuehandler.php mode change 100755 => 100644 plugins/RSSCloud/RSSCloudQueueHandler.php diff --git a/actions/imsettings.php b/actions/imsettings.php index 751c6117cd..af4915843d 100644 --- a/actions/imsettings.php +++ b/actions/imsettings.php @@ -309,6 +309,8 @@ class ImsettingsAction extends ConnectSettingsAction $confirm->address_type = 'jabber'; $confirm->user_id = $user->id; $confirm->code = common_confirmation_code(64); + $confirm->sent = common_sql_now(); + $confirm->claimed = common_sql_now(); $result = $confirm->insert(); @@ -318,11 +320,9 @@ class ImsettingsAction extends ConnectSettingsAction return; } - if (!common_config('queue', 'enabled')) { - jabber_confirm_address($confirm->code, - $user->nickname, - $jabber); - } + jabber_confirm_address($confirm->code, + $user->nickname, + $jabber); $msg = sprintf(_('A confirmation code was sent '. 'to the IM address you added. '. diff --git a/classes/Queue_item.php b/classes/Queue_item.php index cf805a6060..f83c2cef18 100644 --- a/classes/Queue_item.php +++ b/classes/Queue_item.php @@ -10,8 +10,8 @@ class Queue_item extends Memcached_DataObject /* the code below is auto generated do not remove the above tag */ public $__table = 'queue_item'; // table name - public $notice_id; // int(4) primary_key not_null - public $transport; // varchar(8) primary_key not_null + public $id; // int(4) primary_key not_null + public $frame; // blob not_null public $created; // datetime() not_null public $claimed; // datetime() @@ -22,14 +22,21 @@ class Queue_item extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE - function sequenceKey() - { return array(false, false); } - - static function top($transport=null) { + /** + * @param mixed $transports name of a single queue or array of queues to pull from + * If not specified, checks all queues in the system. + */ + static function top($transports=null) { $qi = new Queue_item(); - if ($transport) { - $qi->transport = $transport; + if ($transports) { + if (is_array($transports)) { + // @fixme use safer escaping + $list = implode("','", array_map('addslashes', $transports)); + $qi->whereAdd("transport in ('$list')"); + } else { + $qi->transport = $transports; + } } $qi->orderBy('created'); $qi->whereAdd('claimed is null'); @@ -42,7 +49,7 @@ class Queue_item extends Memcached_DataObject # XXX: potential race condition # can we force it to only update if claimed is still null # (or old)? - common_log(LOG_INFO, 'claiming queue item = ' . $qi->notice_id . + common_log(LOG_INFO, 'claiming queue item id = ' . $qi->id . ' for transport ' . $qi->transport); $orig = clone($qi); $qi->claimed = common_sql_now(); @@ -57,9 +64,4 @@ class Queue_item extends Memcached_DataObject $qi = null; return null; } - - function pkeyGet($kv) - { - return Memcached_DataObject::pkeyGet('Queue_item', $kv); - } } diff --git a/classes/statusnet.ini b/classes/statusnet.ini index 73727a6d6a..6ce4495be0 100644 --- a/classes/statusnet.ini +++ b/classes/statusnet.ini @@ -396,14 +396,14 @@ tagged = K tag = K [queue_item] -notice_id = 129 +id = 129 +frame = 66 transport = 130 created = 142 claimed = 14 [queue_item__keys] -notice_id = K -transport = K +id = K [related_group] group_id = 129 diff --git a/db/08to09.sql b/db/08to09.sql index d9c25bc723..b10e47dbcb 100644 --- a/db/08to09.sql +++ b/db/08to09.sql @@ -94,3 +94,19 @@ create table user_location_prefs ( constraint primary key (user_id) ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; +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/rc3to09.sql b/db/rc3to09.sql new file mode 100644 index 0000000000..02dc7a6e2e --- /dev/null +++ b/db/rc3to09.sql @@ -0,0 +1,16 @@ +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/statusnet.sql b/db/statusnet.sql index cb33ccf33e..a9ed66cb4f 100644 --- a/db/statusnet.sql +++ b/db/statusnet.sql @@ -244,13 +244,12 @@ create table remember_me ( ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; create table queue_item ( - - notice_id integer not null comment 'notice queued' references notice (id), + 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', - constraint primary key (notice_id, transport), index queue_item_created_idx (created) ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; diff --git a/lib/dbqueuemanager.php b/lib/dbqueuemanager.php index 889365b649..c6350fc669 100644 --- a/lib/dbqueuemanager.php +++ b/lib/dbqueuemanager.php @@ -31,19 +31,17 @@ class DBQueueManager extends QueueManager { /** - * Saves a notice object reference into the queue item table. + * Saves an object reference into the queue item table. * @return boolean true on success * @throws ServerException on failure */ public function enqueue($object, $queue) { - $notice = $object; - $qi = new Queue_item(); - $qi->notice_id = $notice->id; + $qi->frame = $this->encode($object); $qi->transport = $queue; - $qi->created = $notice->created; + $qi->created = common_sql_now(); $result = $qi->insert(); if (!$result) { @@ -57,146 +55,92 @@ class DBQueueManager extends QueueManager } /** - * Poll every minute for new events during idle periods. + * Poll every 10 seconds for new events during idle periods. * We'll look in more often when there's data available. * * @return int seconds */ public function pollInterval() { - return 60; + return 10; } /** * Run a polling cycle during idle processing in the input loop. - * @return boolean true if we had a hit + * @return boolean true if we should poll again for more data immediately */ public function poll() { $this->_log(LOG_DEBUG, 'Checking for notices...'); - $item = $this->_nextItem(); - if ($item === false) { + $qi = Queue_item::top($this->getQueues()); + if (empty($qi)) { $this->_log(LOG_DEBUG, 'No notices waiting; idling.'); return false; } - if ($item === true) { - // We dequeued an entry for a deleted or invalid notice. - // Consider it a hit for poll rate purposes. - return true; - } - list($queue, $notice) = $item; - $this->_log(LOG_INFO, 'Got notice '. $notice->id . ' for transport ' . $queue); + $queue = $qi->transport; + $item = $this->decode($qi->frame); - // Yay! Got one! - $handler = $this->getHandler($queue); - if ($handler) { - if ($handler->handle_notice($notice)) { - $this->_log(LOG_INFO, "[$queue:notice $notice->id] Successfully handled notice"); - $this->_done($notice, $queue); + if ($item) { + $rep = $this->logrep($item); + $this->_log(LOG_INFO, "Got $rep for transport $queue"); + + $handler = $this->getHandler($queue); + if ($handler) { + if ($handler->handle($item)) { + $this->_log(LOG_INFO, "[$queue:$rep] Successfully handled item"); + $this->_done($qi); + } else { + $this->_log(LOG_INFO, "[$queue:$rep] Failed to handle item"); + $this->_fail($qi); + } } else { - $this->_log(LOG_INFO, "[$queue:notice $notice->id] Failed to handle notice"); - $this->_fail($notice, $queue); + $this->_log(LOG_INFO, "[$queue:$rep] No handler for queue $queue; discarding."); + $this->_done($qi); } } else { - $this->_log(LOG_INFO, "[$queue:notice $notice->id] No handler for queue $queue; discarding."); - $this->_done($notice, $queue); + $this->_log(LOG_INFO, "[$queue] Got empty/deleted item, discarding"); + $this->_fail($qi); } return true; } - /** - * Pop the oldest unclaimed item off the queue set and claim it. - * - * @return mixed false if no items; true if bogus hit; otherwise array(string, Notice) - * giving the queue transport name. - */ - protected function _nextItem() - { - $start = time(); - $result = null; - - $qi = Queue_item::top(); - if (empty($qi)) { - return false; - } - - $queue = $qi->transport; - $notice = Notice::staticGet('id', $qi->notice_id); - if (empty($notice)) { - $this->_log(LOG_INFO, "[$queue:notice $notice->id] dequeued non-existent notice"); - $qi->delete(); - return true; - } - - $result = $notice; - return array($queue, $notice); - } - /** * Delete our claimed item from the queue after successful processing. * - * @param Notice $object - * @param string $queue + * @param QueueItem $qi */ - protected function _done($object, $queue) + protected function _done($qi) { - // XXX: right now, we only handle notices + $queue = $qi->transport; - $notice = $object; - - $qi = Queue_item::pkeyGet(array('notice_id' => $notice->id, - 'transport' => $queue)); - - if (empty($qi)) { - $this->_log(LOG_INFO, "[$queue:notice $notice->id] Cannot find queue item"); - } else { - if (empty($qi->claimed)) { - $this->_log(LOG_WARNING, "[$queue:notice $notice->id] Reluctantly releasing unclaimed queue item"); - } - $qi->delete(); - $qi->free(); + if (empty($qi->claimed)) { + $this->_log(LOG_WARNING, "Reluctantly releasing unclaimed queue item $qi->id from $qi->queue"); } + $qi->delete(); - $this->_log(LOG_INFO, "[$queue:notice $notice->id] done with item"); $this->stats('handled', $queue); - - $notice->free(); } /** * Free our claimed queue item for later reprocessing in case of * temporary failure. * - * @param Notice $object - * @param string $queue + * @param QueueItem $qi */ - protected function _fail($object, $queue) + protected function _fail($qi) { - // XXX: right now, we only handle notices + $queue = $qi->transport; - $notice = $object; - - $qi = Queue_item::pkeyGet(array('notice_id' => $notice->id, - 'transport' => $queue)); - - if (empty($qi)) { - $this->_log(LOG_INFO, "[$queue:notice $notice->id] Cannot find queue item"); + if (empty($qi->claimed)) { + $this->_log(LOG_WARNING, "[$queue:item $qi->id] Ignoring failure for unclaimed queue item"); } else { - if (empty($qi->claimed)) { - $this->_log(LOG_WARNING, "[$queue:notice $notice->id] Ignoring failure for unclaimed queue item"); - } else { - $orig = clone($qi); - $qi->claimed = null; - $qi->update($orig); - $qi = null; - } + $orig = clone($qi); + $qi->claimed = null; + $qi->update($orig); } - $this->_log(LOG_INFO, "[$queue:notice $notice->id] done with queue item"); $this->stats('error', $queue); - - $notice->free(); } protected function _log($level, $msg) diff --git a/lib/default.php b/lib/default.php index fc6b4ab79c..0cb70de2d2 100644 --- a/lib/default.php +++ b/lib/default.php @@ -83,6 +83,7 @@ $default = '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 ), 'license' => array('type' => 'cc', # can be 'cc', 'allrightsreserved', 'private' diff --git a/lib/iomaster.php b/lib/iomaster.php index ce77b53b2e..004e92b3ee 100644 --- a/lib/iomaster.php +++ b/lib/iomaster.php @@ -27,7 +27,7 @@ * @link http://status.net/ */ -class IoMaster +abstract class IoMaster { public $id; @@ -66,23 +66,18 @@ class IoMaster if ($site != common_config('site', 'server')) { StatusNet::init($site); } - - $classes = array(); - if (Event::handle('StartIoManagerClasses', array(&$classes))) { - $classes[] = 'QueueManager'; - if (common_config('xmpp', 'enabled') && !defined('XMPP_EMERGENCY_FLAG')) { - $classes[] = 'XmppManager'; // handles pings/reconnects - $classes[] = 'XmppConfirmManager'; // polls for outgoing confirmations - } - } - Event::handle('EndIoManagerClasses', array(&$classes)); - - foreach ($classes as $class) { - $this->instantiate($class); - } + $this->initManagers(); } } + /** + * Initialize IoManagers for the currently configured site + * which are appropriate to this instance. + * + * Pass class names into $this->instantiate() + */ + abstract function initManagers(); + /** * Pull all local sites from status_network table. * @return array of hostnames @@ -170,7 +165,7 @@ class IoMaster $write = array(); $except = array(); $this->logState('listening'); - common_log(LOG_INFO, "Waiting up to $timeout seconds for socket data..."); + common_log(LOG_DEBUG, "Waiting up to $timeout seconds for socket data..."); $ready = stream_select($read, $write, $except, $timeout, 0); if ($ready === false) { @@ -190,7 +185,7 @@ class IoMaster if ($timeout > 0 && empty($sockets)) { // If we had no listeners, sleep until the pollers' next requested wakeup. - common_log(LOG_INFO, "Sleeping $timeout seconds until next poll cycle..."); + common_log(LOG_DEBUG, "Sleeping $timeout seconds until next poll cycle..."); $this->logState('sleep'); sleep($timeout); } @@ -207,6 +202,8 @@ class IoMaster 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"); } } } @@ -223,8 +220,7 @@ class IoMaster { $softLimit = trim(common_config('queue', 'softlimit')); if (substr($softLimit, -1) == '%') { - $limit = trim(ini_get('memory_limit')); - $limit = $this->parseMemoryLimit($limit); + $limit = $this->parseMemoryLimit(ini_get('memory_limit')); if ($limit > 0) { return intval(substr($softLimit, 0, -1) * $limit / 100); } else { @@ -242,9 +238,10 @@ class IoMaster * @param string $mem * @return int */ - protected function parseMemoryLimit($mem) + public function parseMemoryLimit($mem) { // http://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes + $mem = strtolower(trim($mem)); $size = array('k' => 1024, 'm' => 1024*1024, 'g' => 1024*1024*1024); @@ -253,7 +250,7 @@ class IoMaster } else if (is_numeric($mem)) { return intval($mem); } else { - $mult = strtolower(substr($mem, -1)); + $mult = substr($mem, -1); if (isset($size[$mult])) { return substr($mem, 0, -1) * $size[$mult]; } else { diff --git a/lib/jabber.php b/lib/jabber.php index 4cdfa67465..b6b23521bd 100644 --- a/lib/jabber.php +++ b/lib/jabber.php @@ -85,6 +85,27 @@ class Sharing_XMPP extends XMPPHP_XMPP } } +/** + * Build an XMPP proxy connection that'll save outgoing messages + * to the 'xmppout' queue to be picked up by xmppdaemon later. + */ +function jabber_proxy() +{ + $proxy = new Queued_XMPP(common_config('xmpp', 'host') ? + common_config('xmpp', 'host') : + common_config('xmpp', 'server'), + common_config('xmpp', 'port'), + common_config('xmpp', 'user'), + common_config('xmpp', 'password'), + common_config('xmpp', 'resource') . 'daemon', + common_config('xmpp', 'server'), + common_config('xmpp', 'debug') ? + true : false, + common_config('xmpp', 'debug') ? + XMPPHP_Log::LEVEL_VERBOSE : null); + return $proxy; +} + /** * Lazy-connect the configured Jabber account to the configured server; * if already opened, the same connection will be returned. @@ -143,7 +164,7 @@ function jabber_connect($resource=null) } /** - * send a single notice to a given Jabber address + * Queue send for a single notice to a given Jabber address * * @param string $to JID to send the notice to * @param Notice $notice notice to send @@ -153,10 +174,7 @@ function jabber_connect($resource=null) function jabber_send_notice($to, $notice) { - $conn = jabber_connect(); - if (!$conn) { - return false; - } + $conn = jabber_proxy(); $profile = Profile::staticGet($notice->profile_id); if (!$profile) { common_log(LOG_WARNING, 'Refusing to send notice with ' . @@ -221,10 +239,7 @@ function jabber_format_entry($profile, $notice) function jabber_send_message($to, $body, $type='chat', $subject=null) { - $conn = jabber_connect(); - if (!$conn) { - return false; - } + $conn = jabber_proxy(); $conn->message($to, $body, $type, $subject); return true; } @@ -319,7 +334,7 @@ function jabber_special_presence($type, $to=null, $show=null, $status=null) } /** - * broadcast a notice to all subscribers and reply recipients + * Queue broadcast of a notice to all subscribers and reply recipients * * This function will send a notice to all subscribers on the local server * who have Jabber addresses, and have Jabber notification enabled, and @@ -354,7 +369,7 @@ function jabber_broadcast_notice($notice) $sent_to = array(); - $conn = jabber_connect(); + $conn = jabber_proxy(); $ni = $notice->whoGets(); @@ -389,14 +404,13 @@ function jabber_broadcast_notice($notice) 'Sending notice ' . $notice->id . ' to ' . $user->jabber, __FILE__); $conn->message($user->jabber, $msg, 'chat', null, $entry); - $conn->processTime(0); } return true; } /** - * send a notice to all public listeners + * Queue send of a notice to all public listeners * * For notices that are generated on the local system (by users), we can optionally * forward them to remote listeners by XMPP. @@ -429,7 +443,7 @@ function jabber_public_notice($notice) $msg = jabber_format_notice($profile, $notice); $entry = jabber_format_entry($profile, $notice); - $conn = jabber_connect(); + $conn = jabber_proxy(); foreach ($public as $address) { common_log(LOG_INFO, @@ -437,7 +451,6 @@ function jabber_public_notice($notice) ' to public listener ' . $address, __FILE__); $conn->message($address, $msg, 'chat', null, $entry); - $conn->processTime(0); } $profile->free(); } diff --git a/lib/jabberqueuehandler.php b/lib/jabberqueuehandler.php index b1518866d7..83471f2df7 100644 --- a/lib/jabberqueuehandler.php +++ b/lib/jabberqueuehandler.php @@ -34,14 +34,14 @@ class JabberQueueHandler extends QueueHandler return 'jabber'; } - function handle_notice($notice) + function handle($notice) { require_once(INSTALLDIR.'/lib/jabber.php'); try { return jabber_broadcast_notice($notice); } catch (XMPPHP_Exception $e) { $this->log(LOG_ERR, "Got an XMPPHP_Exception: " . $e->getMessage()); - exit(1); + return false; } } } diff --git a/lib/ombqueuehandler.php b/lib/ombqueuehandler.php index 3ffc1313bc..24896c784c 100644 --- a/lib/ombqueuehandler.php +++ b/lib/ombqueuehandler.php @@ -36,7 +36,7 @@ class OmbQueueHandler extends QueueHandler * @fixme doesn't currently report failure back to the queue manager * because omb_broadcast_notice() doesn't report it to us */ - function handle_notice($notice) + function handle($notice) { if ($this->is_remote($notice)) { $this->log(LOG_DEBUG, 'Ignoring remote notice ' . $notice->id); diff --git a/lib/pingqueuehandler.php b/lib/pingqueuehandler.php index 8bb2180786..4e4d74cb1a 100644 --- a/lib/pingqueuehandler.php +++ b/lib/pingqueuehandler.php @@ -30,7 +30,7 @@ class PingQueueHandler extends QueueHandler { return 'ping'; } - function handle_notice($notice) { + function handle($notice) { require_once INSTALLDIR . '/lib/ping.php'; return ping_broadcast_notice($notice); } diff --git a/lib/pluginqueuehandler.php b/lib/pluginqueuehandler.php index 24d5046997..9653ccad42 100644 --- a/lib/pluginqueuehandler.php +++ b/lib/pluginqueuehandler.php @@ -42,7 +42,7 @@ class PluginQueueHandler extends QueueHandler return 'plugin'; } - function handle_notice($notice) + function handle($notice) { Event::handle('HandleQueuedNotice', array(&$notice)); return true; diff --git a/lib/publicqueuehandler.php b/lib/publicqueuehandler.php index 9ea9ee73a3..c9edb8d5d7 100644 --- a/lib/publicqueuehandler.php +++ b/lib/publicqueuehandler.php @@ -23,7 +23,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { /** * Queue handler for pushing new notices to public XMPP subscribers. - * @fixme correct this exception handling */ class PublicQueueHandler extends QueueHandler { @@ -33,15 +32,14 @@ class PublicQueueHandler extends QueueHandler return 'public'; } - function handle_notice($notice) + function handle($notice) { require_once(INSTALLDIR.'/lib/jabber.php'); try { return jabber_public_notice($notice); } catch (XMPPHP_Exception $e) { $this->log(LOG_ERR, "Got an XMPPHP_Exception: " . $e->getMessage()); - die($e->getMessage()); + return false; } - return true; } } diff --git a/lib/queued_xmpp.php b/lib/queued_xmpp.php new file mode 100644 index 0000000000..4b890c4ca4 --- /dev/null +++ b/lib/queued_xmpp.php @@ -0,0 +1,117 @@ +. + * + * @category Network + * @package StatusNet + * @author Brion Vibber + * @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 . '/lib/jabber.php'; + +class Queued_XMPP extends XMPPHP_XMPP +{ + /** + * Constructor + * + * @param string $host + * @param integer $port + * @param string $user + * @param string $password + * @param string $resource + * @param string $server + * @param boolean $printlog + * @param string $loglevel + */ + public function __construct($host, $port, $user, $password, $resource, $server = null, $printlog = false, $loglevel = null) + { + parent::__construct($host, $port, $user, $password, $resource, $server, $printlog, $loglevel); + // Normally the fulljid isn't filled out until resource binding time; + // we need to save it here since we're not talking to a real server. + $this->fulljid = "{$this->basejid}/{$this->resource}"; + } + + /** + * Send a formatted message to the outgoing queue for later forwarding + * to a real XMPP connection. + * + * @param string $msg + */ + public function send($msg, $timeout=NULL) + { + $qm = QueueManager::get(); + $qm->enqueue(strval($msg), 'xmppout'); + } + + /** + * Since we'll be getting input through a queue system's run loop, + * we'll process one standalone message at a time rather than our + * own XMPP message pump. + * + * @param string $message + */ + public function processMessage($message) { + $frame = array_shift($this->frames); + xml_parse($this->parser, $frame->body, false); + } + + //@{ + /** + * Stream i/o functions disabled; push input through processMessage() + */ + public function connect($timeout = 30, $persistent = false, $sendinit = true) + { + throw new Exception("Can't connect to server from XMPP queue proxy."); + } + + public function disconnect() + { + throw new Exception("Can't connect to server from XMPP queue proxy."); + } + + public function process() + { + throw new Exception("Can't read stream from XMPP queue proxy."); + } + + public function processUntil($event, $timeout=-1) + { + throw new Exception("Can't read stream from XMPP queue proxy."); + } + + public function read() + { + throw new Exception("Can't read stream from XMPP queue proxy."); + } + + public function readyToProcess() + { + throw new Exception("Can't read stream from XMPP queue proxy."); + } + //@} +} + diff --git a/lib/queuehandler.php b/lib/queuehandler.php index 613be6e330..2909cd83b1 100644 --- a/lib/queuehandler.php +++ b/lib/queuehandler.php @@ -22,51 +22,20 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } /** * Base class for queue handlers. * - * As extensions of the Daemon class, each queue handler has the ability - * to launch itself in the background, at which point it'll pass control - * to the configured QueueManager class to poll for updates. + * As of 0.9, queue handlers are short-lived for items as they are + * dequeued by a QueueManager running in an IoMaster in a daemon + * such as queuedaemon.php. + * + * Extensions requiring long-running maintenance or polling should + * register an IoManager. * * Subclasses must override at least the following methods: * - transport - * - handle_notice + * - handle */ -#class QueueHandler extends Daemon class QueueHandler { -# function __construct($id=null, $daemonize=true) -# { -# parent::__construct($daemonize); -# -# if ($id) { -# $this->set_id($id); -# } -# } - - /** - * How many seconds a polling-based queue manager should wait between - * checks for new items to handle. - * - * Defaults to 60 seconds; override to speed up or slow down. - * - * @fixme not really compatible with global queue manager - * @return int timeout in seconds - */ -# function timeout() -# { -# return 60; -# } - -# function class_name() -# { -# return ucfirst($this->transport()) . 'Handler'; -# } - -# function name() -# { -# return strtolower($this->class_name().'.'.$this->get_id()); -# } - /** * Return transport keyword which identifies items this queue handler * services; must be defined for all subclasses. @@ -83,61 +52,17 @@ class QueueHandler /** * Here's the meat of your queue handler -- you're handed a Notice - * object, which you may do as you will with. + * or other object, which you may do as you will with. * * If this function indicates failure, a warning will be logged * and the item is placed back in the queue to be re-run. * - * @param Notice $notice + * @param mixed $object * @return boolean true on success, false on failure */ - function handle_notice($notice) + function handle($object) { return true; } - - /** - * Setup and start of run loop for this queue handler as a daemon. - * Most of the heavy lifting is passed on to the QueueManager's service() - * method, which passes control back to our handle_notice() method for - * each notice that comes in on the queue. - * - * Most of the time this won't need to be overridden in a subclass. - * - * @return boolean true on success, false on failure - */ - function run() - { - if (!$this->start()) { - $this->log(LOG_WARNING, 'failed to start'); - return false; - } - - $this->log(LOG_INFO, 'checking for queued notices'); - - $queue = $this->transport(); - $timeout = $this->timeout(); - - $qm = QueueManager::get(); - - $qm->service($queue, $this); - - $this->log(LOG_INFO, 'finished servicing the queue'); - - if (!$this->finish()) { - $this->log(LOG_WARNING, 'failed to clean up'); - return false; - } - - $this->log(LOG_INFO, 'terminating normally'); - - return true; - } - - - function log($level, $msg) - { - common_log($level, $this->class_name() . ' ('. $this->get_id() .'): '.$msg); - } } diff --git a/lib/queuemanager.php b/lib/queuemanager.php index 291174d3c4..4eb39bfa8c 100644 --- a/lib/queuemanager.php +++ b/lib/queuemanager.php @@ -39,6 +39,10 @@ abstract class QueueManager extends IoManager { static $qm = null; + public $master = null; + public $handlers = array(); + public $groups = array(); + /** * Factory function to pull the appropriate QueueManager object * for this site's configuration. It can then be used to queue @@ -109,6 +113,64 @@ abstract class QueueManager extends IoManager */ abstract function enqueue($object, $queue); + /** + * Build a representation for an object for logging + * @param mixed + * @return string + */ + function logrep($object) { + if (is_object($object)) { + $class = get_class($object); + if (isset($object->id)) { + return "$class $object->id"; + } + return $class; + } + if (is_string($object)) { + $len = strlen($object); + $fragment = mb_substr($object, 0, 32); + if (mb_strlen($object) > 32) { + $fragment .= '...'; + } + return "string '$fragment' ($len bytes)"; + } + return strval($object); + } + + /** + * Encode an object for queued storage. + * Next gen may use serialization. + * + * @param mixed $object + * @return string + */ + protected function encode($object) + { + if ($object instanceof Notice) { + return $object->id; + } else if (is_string($object)) { + return $object; + } else { + throw new ServerException("Can't queue this type", 500); + } + } + + /** + * Decode an object from queued storage. + * Accepts back-compat notice reference entries and strings for now. + * + * @param string + * @return mixed + */ + protected function decode($frame) + { + if (is_numeric($frame)) { + return Notice::staticGet(intval($frame)); + } else { + return $frame; + } + } + /** * Instantiate the appropriate QueueHandler class for the given queue. * @@ -131,13 +193,15 @@ abstract class QueueManager extends IoManager } /** - * Get a list of all registered queue transport names. + * Get a list of registered queue transport names to be used + * for this daemon. * * @return array of strings */ function getQueues() { - return array_keys($this->handlers); + $group = $this->activeGroup(); + return array_keys($this->groups[$group]); } /** @@ -148,33 +212,29 @@ abstract class QueueManager extends IoManager */ function initialize() { + // @fixme we'll want to be able to listen to particular queues... if (Event::handle('StartInitializeQueueManager', array($this))) { - if (!defined('XMPP_ONLY_FLAG')) { // hack! - $this->connect('plugin', 'PluginQueueHandler'); - $this->connect('omb', 'OmbQueueHandler'); - $this->connect('ping', 'PingQueueHandler'); - if (common_config('sms', 'enabled')) { - $this->connect('sms', 'SmsQueueHandler'); - } + $this->connect('plugin', 'PluginQueueHandler'); + $this->connect('omb', 'OmbQueueHandler'); + $this->connect('ping', 'PingQueueHandler'); + if (common_config('sms', 'enabled')) { + $this->connect('sms', 'SmsQueueHandler'); } // XMPP output handlers... - if (common_config('xmpp', 'enabled') && !defined('XMPP_EMERGENCY_FLAG')) { - $this->connect('jabber', 'JabberQueueHandler'); - $this->connect('public', 'PublicQueueHandler'); - - // @fixme this should move up a level or should get an actual queue - $this->connect('confirm', 'XmppConfirmHandler'); - } + $this->connect('jabber', 'JabberQueueHandler'); + $this->connect('public', 'PublicQueueHandler'); + + // @fixme this should get an actual queue + //$this->connect('confirm', 'XmppConfirmHandler'); + + // For compat with old plugins not registering their own handlers. + $this->connect('plugin', 'PluginQueueHandler'); + + $this->connect('xmppout', 'XmppOutQueueHandler', 'xmppdaemon'); - if (!defined('XMPP_ONLY_FLAG')) { // hack! - // For compat with old plugins not registering their own handlers. - $this->connect('plugin', 'PluginQueueHandler'); - } - } - if (!defined('XMPP_ONLY_FLAG')) { // hack! - Event::handle('EndInitializeQueueManager', array($this)); } + Event::handle('EndInitializeQueueManager', array($this)); } /** @@ -183,10 +243,27 @@ abstract class QueueManager extends IoManager * * @param string $transport * @param string $class + * @param string $group */ - public function connect($transport, $class) + public function connect($transport, $class, $group='queuedaemon') { $this->handlers[$transport] = $class; + $this->groups[$group][$transport] = $class; + } + + /** + * @return string queue group to use for this request + */ + function activeGroup() + { + $group = 'queuedaemon'; + if ($this->master) { + // hack hack + if ($this->master instanceof XmppMaster) { + return 'xmppdaemon'; + } + } + return $group; } /** diff --git a/lib/smsqueuehandler.php b/lib/smsqueuehandler.php index 48a96409d0..6085d2b4ac 100644 --- a/lib/smsqueuehandler.php +++ b/lib/smsqueuehandler.php @@ -31,7 +31,7 @@ class SmsQueueHandler extends QueueHandler return 'sms'; } - function handle_notice($notice) + function handle($notice) { require_once(INSTALLDIR.'/lib/mail.php'); return mail_broadcast_notice_sms($notice); diff --git a/lib/spawningdaemon.php b/lib/spawningdaemon.php new file mode 100644 index 0000000000..8baefe88e8 --- /dev/null +++ b/lib/spawningdaemon.php @@ -0,0 +1,159 @@ +. + */ + +/** + * Base class for daemon that can launch one or more processing threads, + * respawning them if they exit. + * + * This is mainly intended for indefinite workloads such as monitoring + * a queue or maintaining an IM channel. + * + * Child classes should implement the + * + * We can then pass individual items through the QueueHandler subclasses + * they belong to. We additionally can handle queues for multiple sites. + * + * @package QueueHandler + * @author Brion Vibber + */ +abstract class SpawningDaemon extends Daemon +{ + protected $threads=1; + + function __construct($id=null, $daemonize=true, $threads=1) + { + parent::__construct($daemonize); + + if ($id) { + $this->set_id($id); + } + $this->threads = $threads; + } + + /** + * Perform some actual work! + * + * @return boolean true on success, false on failure + */ + public abstract function runThread(); + + /** + * Spawn one or more background processes and let them start running. + * Each individual process will execute whatever's in the runThread() + * method, which should be overridden. + * + * Child processes will be automatically respawned when they exit. + * + * @todo possibly allow for not respawning on "normal" exits... + * though ParallelizingDaemon is probably better for workloads + * that have forseeable endpoints. + */ + function run() + { + $children = array(); + for ($i = 1; $i <= $this->threads; $i++) { + $pid = pcntl_fork(); + if ($pid < 0) { + $this->log(LOG_ERROR, "Couldn't fork for thread $i; aborting\n"); + exit(1); + } else if ($pid == 0) { + $this->initAndRunChild($i); + } else { + $this->log(LOG_INFO, "Spawned thread $i as pid $pid"); + $children[$i] = $pid; + } + } + + $this->log(LOG_INFO, "Waiting for children to complete."); + while (count($children) > 0) { + $status = null; + $pid = pcntl_wait($status); + if ($pid > 0) { + $i = array_search($pid, $children); + if ($i === false) { + $this->log(LOG_ERR, "Unrecognized child pid $pid exited!"); + 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); + } else { + $this->log(LOG_INFO, "Respawned thread $i as pid $pid"); + $children[$i] = $pid; + } + } + } + $this->log(LOG_INFO, "All child processes complete."); + return true; + } + + /** + * Initialize things for a fresh thread, call runThread(), and + * exit at completion with appropriate return value. + */ + protected function initAndRunChild($thread) + { + $this->set_id($this->get_id() . "." . $thread); + $this->resetDb(); + $ok = $this->runThread(); + exit($ok ? 0 : 1); + } + + /** + * Reconnect to the database for each child process, + * or they'll get very confused trying to use the + * same socket. + */ + protected function resetDb() + { + // @fixme do we need to explicitly open the db too + // or is this implied? + global $_DB_DATAOBJECT; + unset($_DB_DATAOBJECT['CONNECTIONS']); + + // Reconnect main memcached, or threads will stomp on + // each other and corrupt their requests. + $cache = common_memcache(); + if ($cache) { + $cache->reconnect(); + } + + // Also reconnect memcached for status_network table. + if (!empty(Status_network::$cache)) { + Status_network::$cache->close(); + Status_network::$cache = null; + } + } + + function log($level, $msg) + { + common_log($level, get_class($this) . ' ('. $this->get_id() .'): '.$msg); + } + + function name() + { + return strtolower(get_class($this).'.'.$this->get_id()); + } +} + diff --git a/lib/stompqueuemanager.php b/lib/stompqueuemanager.php index 00590fdb69..f057bd9e41 100644 --- a/lib/stompqueuemanager.php +++ b/lib/stompqueuemanager.php @@ -39,7 +39,6 @@ class StompQueueManager extends QueueManager var $base = null; var $con = null; - protected $master = null; protected $sites = array(); function __construct() @@ -104,11 +103,12 @@ class StompQueueManager extends QueueManager */ function getQueues() { + $group = $this->activeGroup(); $site = common_config('site', 'server'); - if (empty($this->handlers[$site])) { + if (empty($this->groups[$site][$group])) { return array(); } else { - return array_keys($this->handlers[$site]); + return array_keys($this->groups[$site][$group]); } } @@ -118,10 +118,12 @@ class StompQueueManager extends QueueManager * * @param string $transport * @param string $class + * @param string $group */ - public function connect($transport, $class) + public function connect($transport, $class, $group='queuedaemon') { $this->handlers[common_config('site', 'server')][$transport] = $class; + $this->groups[common_config('site', 'server')][$group][$transport] = $class; } /** @@ -130,23 +132,23 @@ class StompQueueManager extends QueueManager */ public function enqueue($object, $queue) { - $notice = $object; + $msg = $this->encode($object); + $rep = $this->logrep($object); $this->_connect(); // XXX: serialize and send entire notice $result = $this->con->send($this->queueName($queue), - $notice->id, // BODY of the message - array ('created' => $notice->created)); + $msg, // BODY of the message + array ('created' => common_sql_now())); if (!$result) { - common_log(LOG_ERR, 'Error sending to '.$queue.' queue'); + common_log(LOG_ERR, "Error sending $rep to $queue queue"); return false; } - common_log(LOG_DEBUG, 'complete remote queueing notice ID = ' - . $notice->id . ' for ' . $queue); + common_log(LOG_DEBUG, "complete remote queueing $rep for $queue"); $this->stats('enqueued', $queue); } @@ -174,7 +176,7 @@ class StompQueueManager extends QueueManager $ok = true; $frames = $this->con->readFrames(); foreach ($frames as $frame) { - $ok = $ok && $this->_handleNotice($frame); + $ok = $ok && $this->_handleItem($frame); } return $ok; } @@ -265,7 +267,7 @@ class StompQueueManager extends QueueManager } /** - * Handle and acknowledge a notice event that's come in through a queue. + * Handle and acknowledge an event that's come in through a queue. * * If the queue handler reports failure, the message is requeued for later. * Missing notices or handler classes will drop the message. @@ -276,7 +278,7 @@ class StompQueueManager extends QueueManager * @param StompFrame $frame * @return bool */ - protected function _handleNotice($frame) + protected function _handleItem($frame) { list($site, $queue) = $this->parseDestination($frame->headers['destination']); if ($site != common_config('site', 'server')) { @@ -284,15 +286,23 @@ class StompQueueManager extends QueueManager StatusNet::init($site); } - $id = intval($frame->body); - $info = "notice $id posted at {$frame->headers['created']} in queue $queue"; + if (is_numeric($frame->body)) { + $id = intval($frame->body); + $info = "notice $id posted at {$frame->headers['created']} in queue $queue"; - $notice = Notice::staticGet('id', $id); - if (empty($notice)) { - $this->_log(LOG_WARNING, "Skipping missing $info"); - $this->con->ack($frame); - $this->stats('badnotice', $queue); - return false; + $notice = Notice::staticGet('id', $id); + if (empty($notice)) { + $this->_log(LOG_WARNING, "Skipping missing $info"); + $this->con->ack($frame); + $this->stats('badnotice', $queue); + return false; + } + + $item = $notice; + } else { + // @fixme should we serialize, or json, or what here? + $info = "string posted at {$frame->headers['created']} in queue $queue"; + $item = $frame->body; } $handler = $this->getHandler($queue); @@ -303,7 +313,7 @@ class StompQueueManager extends QueueManager return false; } - $ok = $handler->handle_notice($notice); + $ok = $handler->handle($item); if (!$ok) { $this->_log(LOG_WARNING, "Failed handling $info"); @@ -311,7 +321,7 @@ class StompQueueManager extends QueueManager // this kind of queue management ourselves; // if we don't ack, it should resend... $this->con->ack($frame); - $this->enqueue($notice, $queue); + $this->enqueue($item, $queue); $this->stats('requeued', $queue); return false; } diff --git a/lib/util.php b/lib/util.php index ef8a5d1f02..fb3b8be876 100644 --- a/lib/util.php +++ b/lib/util.php @@ -1130,7 +1130,8 @@ function common_request_id() $pid = getmypid(); $server = common_config('site', 'server'); if (php_sapi_name() == 'cli') { - return "$server:$pid"; + $script = basename($_SERVER['PHP_SELF']); + return "$server:$script:$pid"; } else { static $req_id = null; if (!isset($req_id)) { diff --git a/lib/xmppconfirmmanager.php b/lib/xmppconfirmmanager.php deleted file mode 100644 index ee4e294fd4..0000000000 --- a/lib/xmppconfirmmanager.php +++ /dev/null @@ -1,168 +0,0 @@ -. - */ - -if (!defined('STATUSNET') && !defined('LACONICA')) { - exit(1); -} - -/** - * Event handler for pushing new confirmations to Jabber users. - * @fixme recommend redoing this on a queue-trigger model - * @fixme expiration of old items got dropped in the past, put it back? - */ -class XmppConfirmManager extends IoManager -{ - - /** - * @return mixed XmppConfirmManager, or false if unneeded - */ - public static function get() - { - if (common_config('xmpp', 'enabled')) { - $site = common_config('site', 'server'); - return new XmppConfirmManager(); - } else { - return false; - } - } - - /** - * Tell the i/o master we need one instance for each supporting site - * being handled in this process. - */ - public static function multiSite() - { - return IoManager::INSTANCE_PER_SITE; - } - - function __construct() - { - $this->site = common_config('site', 'server'); - } - - /** - * 10 seconds? Really? That seems a bit frequent. - */ - function pollInterval() - { - return 10; - } - - /** - * Ping! - * @return boolean true if we found something - */ - function poll() - { - $this->switchSite(); - $confirm = $this->next_confirm(); - if ($confirm) { - $this->handle_confirm($confirm); - return true; - } else { - return false; - } - } - - protected function handle_confirm($confirm) - { - require_once INSTALLDIR . '/lib/jabber.php'; - - common_log(LOG_INFO, 'Sending confirmation for ' . $confirm->address); - $user = User::staticGet($confirm->user_id); - if (!$user) { - common_log(LOG_WARNING, 'Confirmation for unknown user ' . $confirm->user_id); - return; - } - $success = jabber_confirm_address($confirm->code, - $user->nickname, - $confirm->address); - if (!$success) { - common_log(LOG_ERR, 'Confirmation failed for ' . $confirm->address); - # Just let the claim age out; hopefully things work then - return; - } else { - common_log(LOG_INFO, 'Confirmation sent for ' . $confirm->address); - # Mark confirmation sent; need a dupe so we don't have the WHERE clause - $dupe = Confirm_address::staticGet('code', $confirm->code); - if (!$dupe) { - common_log(LOG_WARNING, 'Could not refetch confirm', __FILE__); - return; - } - $orig = clone($dupe); - $dupe->sent = $dupe->claimed; - $result = $dupe->update($orig); - if (!$result) { - common_log_db_error($dupe, 'UPDATE', __FILE__); - # Just let the claim age out; hopefully things work then - return; - } - } - return true; - } - - protected function next_confirm() - { - $confirm = new Confirm_address(); - $confirm->whereAdd('claimed IS null'); - $confirm->whereAdd('sent IS null'); - # XXX: eventually we could do other confirmations in the queue, too - $confirm->address_type = 'jabber'; - $confirm->orderBy('modified DESC'); - $confirm->limit(1); - if ($confirm->find(true)) { - common_log(LOG_INFO, 'Claiming confirmation for ' . $confirm->address); - # working around some weird DB_DataObject behaviour - $confirm->whereAdd(''); # clears where stuff - $original = clone($confirm); - $confirm->claimed = common_sql_now(); - $result = $confirm->update($original); - if ($result) { - common_log(LOG_INFO, 'Succeeded in claim! '. $result); - return $confirm; - } else { - common_log(LOG_INFO, 'Failed in claim!'); - return false; - } - } - return null; - } - - protected function clear_old_confirm_claims() - { - $confirm = new Confirm(); - $confirm->claimed = null; - $confirm->whereAdd('now() - claimed > '.CLAIM_TIMEOUT); - $confirm->update(DB_DATAOBJECT_WHEREADD_ONLY); - $confirm->free(); - unset($confirm); - } - - /** - * Make sure we're on the right site configuration - */ - protected function switchSite() - { - if ($this->site != common_config('site', 'server')) { - common_log(LOG_DEBUG, __METHOD__ . ": switching to site $this->site"); - $this->stats('switch'); - StatusNet::init($this->site); - } - } -} diff --git a/lib/xmppmanager.php b/lib/xmppmanager.php index dfff63a30c..299175dd7d 100644 --- a/lib/xmppmanager.php +++ b/lib/xmppmanager.php @@ -70,6 +70,7 @@ class XmppManager extends IoManager function __construct() { $this->site = common_config('site', 'server'); + $this->resource = common_config('xmpp', 'resource') . 'daemon'; } /** @@ -86,15 +87,19 @@ class XmppManager extends IoManager # Low priority; we don't want to receive messages common_log(LOG_INFO, "INITIALIZE"); - $this->conn = jabber_connect($this->resource()); + $this->conn = jabber_connect($this->resource); if (empty($this->conn)) { common_log(LOG_ERR, "Couldn't connect to server."); return false; } - $this->conn->addEventHandler('message', 'forward_message', $this); + $this->log(LOG_DEBUG, "Initializing stanza handlers."); + + $this->conn->addEventHandler('message', 'handle_message', $this); + $this->conn->addEventHandler('presence', 'handle_presence', $this); $this->conn->addEventHandler('reconnect', 'handle_reconnect', $this); + $this->conn->setReconnectTimeout(600); jabber_send_presence("Send me a message to post a notice", 'available', null, 'available', -1); @@ -175,12 +180,37 @@ class XmppManager extends IoManager } } + /** + * For queue handlers to pass us a message to push out, + * if we're active. + * + * @fixme should this be blocking etc? + * + * @param string $msg XML stanza to send + * @return boolean success + */ + public function send($msg) + { + if ($this->conn && !$this->conn->isDisconnected()) { + $bytes = $this->conn->send($msg); + if ($bytes > 0) { + $this->conn->processTime(0); + return true; + } else { + return false; + } + } else { + // Can't send right now... + return false; + } + } + /** * Send a keepalive ping to the XMPP server. */ protected function sendPing() { - $jid = jabber_daemon_address().'/'.$this->resource(); + $jid = jabber_daemon_address().'/'.$this->resource; $server = common_config('xmpp', 'server'); if (!isset($this->pingid)) { @@ -206,61 +236,239 @@ class XmppManager extends IoManager $this->conn->presence(null, 'available', null, 'available', -1); } - /** - * Callback for Jabber message event. - * - * This connection handles output; if we get a message straight to us, - * forward it on to our XmppDaemon listener for processing. - * - * @param $pl - */ - function forward_message(&$pl) + + function get_user($from) { + $user = User::staticGet('jabber', jabber_normalize_jid($from)); + return $user; + } + + /** + * XMPP callback for handling message input... + * @param array $pl XMPP payload + */ + function handle_message(&$pl) + { + $from = jabber_normalize_jid($pl['from']); + if ($pl['type'] != 'chat') { - common_log(LOG_DEBUG, 'Ignoring message of type ' . $pl['type'] . ' from ' . $pl['from']); + $this->log(LOG_WARNING, "Ignoring message of type ".$pl['type']." from $from."); return; } - $listener = $this->listener(); - if (strtolower($listener) == strtolower($pl['from'])) { - common_log(LOG_WARNING, 'Ignoring loop message.'); + + if (mb_strlen($pl['body']) == 0) { + $this->log(LOG_WARNING, "Ignoring message with empty body from $from."); return; } - common_log(LOG_INFO, 'Forwarding message from ' . $pl['from'] . ' to ' . $listener); - $this->conn->message($this->listener(), $pl['body'], 'chat', null, $this->ofrom($pl['from'])); - } - /** - * Build an block with an ofrom entry for forwarded messages - * - * @param string $from Jabber ID of original sender - * @return string XML fragment - */ - protected function ofrom($from) - { - $address = "\n"; - $address .= "
\n"; - $address .= "\n"; - return $address; - } + // Forwarded from another daemon for us to handle; this shouldn't + // happen any more but we might get some legacy items. + if ($this->is_self($from)) { + $this->log(LOG_INFO, "Got forwarded notice from self ($from)."); + $from = $this->get_ofrom($pl); + $this->log(LOG_INFO, "Originally sent by $from."); + if (is_null($from) || $this->is_self($from)) { + $this->log(LOG_INFO, "Ignoring notice originally sent by $from."); + return; + } + } - /** - * Build the complete JID of the XmppDaemon process which - * handles primary XMPP input for this site. - * - * @return string Jabber ID - */ - protected function listener() - { - if (common_config('xmpp', 'listener')) { - return common_config('xmpp', 'listener'); + $user = $this->get_user($from); + + // For common_current_user to work + global $_cur; + $_cur = $user; + + if (!$user) { + $this->from_site($from, 'Unknown user; go to ' . + common_local_url('imsettings') . + ' to add your address to your account'); + $this->log(LOG_WARNING, 'Message from unknown user ' . $from); + return; + } + if ($this->handle_command($user, $pl['body'])) { + $this->log(LOG_INFO, "Command message by $from handled."); + return; + } else if ($this->is_autoreply($pl['body'])) { + $this->log(LOG_INFO, 'Ignoring auto reply from ' . $from); + return; + } else if ($this->is_otr($pl['body'])) { + $this->log(LOG_INFO, 'Ignoring OTR from ' . $from); + return; } else { - return jabber_daemon_address() . '/' . common_config('xmpp','resource') . 'daemon'; + + $this->log(LOG_INFO, 'Posting a notice from ' . $user->nickname); + + $this->add_notice($user, $pl); + } + + $user->free(); + unset($user); + unset($_cur); + + unset($pl['xml']); + $pl['xml'] = null; + + $pl = null; + unset($pl); + } + + + function is_self($from) + { + return preg_match('/^'.strtolower(jabber_daemon_address()).'/', strtolower($from)); + } + + function get_ofrom($pl) + { + $xml = $pl['xml']; + $addresses = $xml->sub('addresses'); + if (!$addresses) { + $this->log(LOG_WARNING, 'Forwarded message without addresses'); + return null; + } + $address = $addresses->sub('address'); + if (!$address) { + $this->log(LOG_WARNING, 'Forwarded message without address'); + return null; + } + if (!array_key_exists('type', $address->attrs)) { + $this->log(LOG_WARNING, 'No type for forwarded message'); + return null; + } + $type = $address->attrs['type']; + if ($type != 'ofrom') { + $this->log(LOG_WARNING, 'Type of forwarded message is not ofrom'); + return null; + } + if (!array_key_exists('jid', $address->attrs)) { + $this->log(LOG_WARNING, 'No jid for forwarded message'); + return null; + } + $jid = $address->attrs['jid']; + if (!$jid) { + $this->log(LOG_WARNING, 'Could not get jid from address'); + return null; + } + $this->log(LOG_DEBUG, 'Got message forwarded from jid ' . $jid); + return $jid; + } + + function is_autoreply($txt) + { + if (preg_match('/[\[\(]?[Aa]uto[-\s]?[Rr]e(ply|sponse)[\]\)]/', $txt)) { + return true; + } else if (preg_match('/^System: Message wasn\'t delivered. Offline storage size was exceeded.$/', $txt)) { + return true; + } else { + return false; } } - protected function resource() + function is_otr($txt) { - return 'queue' . posix_getpid(); // @fixme PIDs won't be host-unique + if (preg_match('/^\?OTR/', $txt)) { + return true; + } else { + return false; + } + } + + function from_site($address, $msg) + { + $text = '['.common_config('site', 'name') . '] ' . $msg; + jabber_send_message($address, $text); + } + + function handle_command($user, $body) + { + $inter = new CommandInterpreter(); + $cmd = $inter->handle_command($user, $body); + if ($cmd) { + $chan = new XMPPChannel($this->conn); + $cmd->execute($chan); + return true; + } else { + return false; + } + } + + function add_notice(&$user, &$pl) + { + $body = trim($pl['body']); + $content_shortened = common_shorten_links($body); + if (Notice::contentTooLong($content_shortened)) { + $from = jabber_normalize_jid($pl['from']); + $this->from_site($from, sprintf(_('Message too long - maximum is %1$d characters, you sent %2$d.'), + Notice::maxContent(), + mb_strlen($content_shortened))); + return; + } + + try { + $notice = Notice::saveNew($user->id, $content_shortened, 'xmpp'); + } catch (Exception $e) { + $this->log(LOG_ERR, $e->getMessage()); + $this->from_site($user->jabber, $e->getMessage()); + return; + } + + common_broadcast_notice($notice); + $this->log(LOG_INFO, + 'Added notice ' . $notice->id . ' from user ' . $user->nickname); + $notice->free(); + unset($notice); + } + + function handle_presence(&$pl) + { + $from = jabber_normalize_jid($pl['from']); + switch ($pl['type']) { + case 'subscribe': + # We let anyone subscribe + $this->subscribed($from); + $this->log(LOG_INFO, + 'Accepted subscription from ' . $from); + break; + case 'subscribed': + case 'unsubscribed': + case 'unsubscribe': + $this->log(LOG_INFO, + 'Ignoring "' . $pl['type'] . '" from ' . $from); + break; + default: + if (!$pl['type']) { + $user = User::staticGet('jabber', $from); + if (!$user) { + $this->log(LOG_WARNING, 'Presence from unknown user ' . $from); + return; + } + if ($user->updatefrompresence) { + $this->log(LOG_INFO, 'Updating ' . $user->nickname . + ' status from presence.'); + $this->add_notice($user, $pl); + } + $user->free(); + unset($user); + } + break; + } + unset($pl['xml']); + $pl['xml'] = null; + + $pl = null; + unset($pl); + } + + function log($level, $msg) + { + $text = 'XMPPDaemon('.$this->resource.'): '.$msg; + common_log($level, $text); + } + + function subscribed($to) + { + jabber_special_presence('subscribed', $to); } /** diff --git a/lib/xmppoutqueuehandler.php b/lib/xmppoutqueuehandler.php new file mode 100644 index 0000000000..2afa260f18 --- /dev/null +++ b/lib/xmppoutqueuehandler.php @@ -0,0 +1,55 @@ +. + */ + +/** + * Queue handler for pre-processed outgoing XMPP messages. + * Formatted XML stanzas will have been pushed into the queue + * via the Queued_XMPP connection proxy, probably from some + * other queue processor. + * + * Here, the XML stanzas are simply pulled out of the queue and + * pushed out over the wire; an XmppManager is needed to set up + * and maintain the actual server connection. + * + * This queue will be run via XmppDaemon rather than QueueDaemon. + * + * @author Brion Vibber + */ +class XmppOutQueueHandler extends QueueHandler +{ + function transport() { + return 'xmppout'; + } + + /** + * Take a previously-queued XMPP stanza and send it out ot the server. + * @param string $msg + * @return boolean true on success + */ + function handle($msg) + { + assert(is_string($msg)); + + $xmpp = XmppManager::get(); + $ok = $xmpp->send($msg); + + return $ok; + } +} + diff --git a/plugins/Enjit/enjitqueuehandler.php b/plugins/Enjit/enjitqueuehandler.php index f0e706b929..14085cc5e3 100644 --- a/plugins/Enjit/enjitqueuehandler.php +++ b/plugins/Enjit/enjitqueuehandler.php @@ -32,14 +32,7 @@ class EnjitQueueHandler extends QueueHandler return 'enjit'; } - function start() - { - $this->log(LOG_INFO, "Starting EnjitQueueHandler"); - $this->log(LOG_INFO, "Broadcasting to ".common_config('enjit', 'apiurl')); - return true; - } - - function handle_notice($notice) + function handle($notice) { $profile = Profile::staticGet($notice->profile_id); diff --git a/plugins/Facebook/facebookqueuehandler.php b/plugins/Facebook/facebookqueuehandler.php index 1778690e5b..524af7bc45 100644 --- a/plugins/Facebook/facebookqueuehandler.php +++ b/plugins/Facebook/facebookqueuehandler.php @@ -28,7 +28,7 @@ class FacebookQueueHandler extends QueueHandler return 'facebook'; } - function handle_notice($notice) + function handle($notice) { if ($this->_isLocal($notice)) { return facebookBroadcastNotice($notice); diff --git a/plugins/RSSCloud/RSSCloudPlugin.php b/plugins/RSSCloud/RSSCloudPlugin.php index 2de162628f..9f444c8bba 100644 --- a/plugins/RSSCloud/RSSCloudPlugin.php +++ b/plugins/RSSCloud/RSSCloudPlugin.php @@ -138,6 +138,9 @@ class RSSCloudPlugin extends Plugin case 'RSSCloudNotifier': include_once INSTALLDIR . '/plugins/RSSCloud/RSSCloudNotifier.php'; return false; + case 'RSSCloudQueueHandler': + include_once INSTALLDIR . '/plugins/RSSCloud/RSSCloudQueueHandler.php'; + return false; case 'RSSCloudRequestNotifyAction': case 'LoggingAggregatorAction': include_once INSTALLDIR . '/plugins/RSSCloud/' . @@ -193,32 +196,6 @@ class RSSCloudPlugin extends Plugin return true; } - /** - * broadcast the message when not using queuehandler - * - * @param Notice &$notice the notice - * @param array $queue destination queue - * - * @return boolean hook return - */ - - function onUnqueueHandleNotice(&$notice, $queue) - { - if (($queue == 'rsscloud') && ($this->_isLocal($notice))) { - - common_debug('broadcasting rssCloud bound notice ' . $notice->id); - - $profile = $notice->getProfile(); - - $notifier = new RSSCloudNotifier(); - $notifier->notify($profile); - - return false; - } - - return true; - } - /** * Determine whether the notice was locally created * @@ -261,19 +238,15 @@ class RSSCloudPlugin extends Plugin } /** - * Add RSSCloudQueueHandler to the list of valid daemons to - * start + * Register RSSCloud notice queue handler * - * @param array $daemons the list of daemons to run + * @param QueueManager $manager * * @return boolean hook return - * */ - - function onGetValidDaemons($daemons) + function onEndInitializeQueueManager($manager) { - array_push($daemons, INSTALLDIR . - '/plugins/RSSCloud/RSSCloudQueueHandler.php'); + $manager->connect('rsscloud', 'RSSCloudQueueHandler'); return true; } diff --git a/plugins/RSSCloud/RSSCloudQueueHandler.php b/plugins/RSSCloud/RSSCloudQueueHandler.php old mode 100755 new mode 100644 index 693dd27c1f..295c261895 --- a/plugins/RSSCloud/RSSCloudQueueHandler.php +++ b/plugins/RSSCloud/RSSCloudQueueHandler.php @@ -1,4 +1,3 @@ -#!/usr/bin/env php . */ -define('INSTALLDIR', realpath(dirname(__FILE__) . '/../..')); - -$shortoptions = 'i::'; -$longoptions = array('id::'); - -$helptext = <<log(LOG_INFO, "INITIALIZE"); - $this->notifier = new RSSCloudNotifier(); - return true; - } - - function handle_notice($notice) + function handle($notice) { $profile = $notice->getProfile(); - return $this->notifier->notify($profile); + $notifier = new RSSCloudNotifier(); + return $notifier->notify($profile); } - - function finish() - { - } - } -if (have_option('i')) { - $id = get_option_value('i'); -} else if (have_option('--id')) { - $id = get_option_value('--id'); -} else if (count($args) > 0) { - $id = $args[0]; -} else { - $id = null; -} - -$handler = new RSSCloudQueueHandler($id); - -$handler->runOnce(); diff --git a/plugins/TwitterBridge/twitterqueuehandler.php b/plugins/TwitterBridge/twitterqueuehandler.php index 5089ca7b74..b5a624e83d 100644 --- a/plugins/TwitterBridge/twitterqueuehandler.php +++ b/plugins/TwitterBridge/twitterqueuehandler.php @@ -28,7 +28,7 @@ class TwitterQueueHandler extends QueueHandler return 'twitter'; } - function handle_notice($notice) + function handle($notice) { return broadcast_twitter($notice); } diff --git a/scripts/handlequeued.php b/scripts/handlequeued.php index 9031437aac..8158849695 100755 --- a/scripts/handlequeued.php +++ b/scripts/handlequeued.php @@ -50,7 +50,7 @@ if (empty($notice)) { exit(1); } -if (!$handler->handle_notice($notice)) { +if (!$handler->handle($notice)) { print "Failed to handle notice id $noticeId on queue '$queue'.\n"; exit(1); } diff --git a/scripts/queuedaemon.php b/scripts/queuedaemon.php index 162f617e0d..a9cfda6d72 100755 --- a/scripts/queuedaemon.php +++ b/scripts/queuedaemon.php @@ -29,6 +29,8 @@ $longoptions = array('id=', 'foreground', 'all', 'threads=', 'skip-xmpp', 'xmpp- * * Recognizes Linux and Mac OS X; others will return default of 1. * + * @fixme move this to SpawningDaemon, but to get the default val for help + * text we seem to need it before loading infrastructure * @return intval */ function getProcessorCount() @@ -83,143 +85,29 @@ define('CLAIM_TIMEOUT', 1200); * We can then pass individual items through the QueueHandler subclasses * they belong to. */ -class QueueDaemon extends Daemon +class QueueDaemon extends SpawningDaemon { - protected $allsites; - protected $threads=1; + protected $allsites = false; function __construct($id=null, $daemonize=true, $threads=1, $allsites=false) { - parent::__construct($daemonize); - - if ($id) { - $this->set_id($id); - } + parent::__construct($id, $daemonize, $threads); $this->all = $allsites; - $this->threads = $threads; - } - - /** - * How many seconds a polling-based queue manager should wait between - * checks for new items to handle. - * - * Defaults to 60 seconds; override to speed up or slow down. - * - * @return int timeout in seconds - */ - function timeout() - { - return 60; - } - - function name() - { - return strtolower(get_class($this).'.'.$this->get_id()); - } - - function run() - { - if ($this->threads > 1) { - return $this->runThreads(); - } else { - return $this->runLoop(); - } - } - - function runThreads() - { - $children = array(); - for ($i = 1; $i <= $this->threads; $i++) { - $pid = pcntl_fork(); - if ($pid < 0) { - print "Couldn't fork for thread $i; aborting\n"; - exit(1); - } else if ($pid == 0) { - $this->runChild($i); - exit(0); - } else { - $this->log(LOG_INFO, "Spawned thread $i as pid $pid"); - $children[$i] = $pid; - } - } - - $this->log(LOG_INFO, "Waiting for children to complete."); - while (count($children) > 0) { - $status = null; - $pid = pcntl_wait($status); - if ($pid > 0) { - $i = array_search($pid, $children); - if ($i === false) { - $this->log(LOG_ERR, "Unrecognized child pid $pid exited!"); - continue; - } - unset($children[$i]); - $this->log(LOG_INFO, "Thread $i pid $pid exited."); - - $pid = pcntl_fork(); - if ($pid < 0) { - print "Couldn't fork to respawn thread $i; aborting thread.\n"; - } else if ($pid == 0) { - $this->runChild($i); - exit(0); - } else { - $this->log(LOG_INFO, "Respawned thread $i as pid $pid"); - $children[$i] = $pid; - } - } - } - $this->log(LOG_INFO, "All child processes complete."); - return true; - } - - function runChild($thread) - { - $this->set_id($this->get_id() . "." . $thread); - $this->resetDb(); - $this->runLoop(); - } - - /** - * Reconnect to the database for each child process, - * or they'll get very confused trying to use the - * same socket. - */ - function resetDb() - { - // @fixme do we need to explicitly open the db too - // or is this implied? - global $_DB_DATAOBJECT; - unset($_DB_DATAOBJECT['CONNECTIONS']); - - // Reconnect main memcached, or threads will stomp on - // each other and corrupt their requests. - $cache = common_memcache(); - if ($cache) { - $cache->reconnect(); - } - - // Also reconnect memcached for status_network table. - if (!empty(Status_network::$cache)) { - Status_network::$cache->close(); - Status_network::$cache = null; - } } /** * Setup and start of run loop for this queue handler as a daemon. * Most of the heavy lifting is passed on to the QueueManager's service() - * method, which passes control on to the QueueHandler's handle_notice() - * method for each notice that comes in on the queue. - * - * Most of the time this won't need to be overridden in a subclass. + * method, which passes control on to the QueueHandler's handle() + * method for each item that comes in on the queue. * * @return boolean true on success, false on failure */ - function runLoop() + function runThread() { $this->log(LOG_INFO, 'checking for queued notices'); - $master = new IoMaster($this->get_id()); + $master = new QueueMaster($this->get_id()); $master->init($this->all); $master->service(); @@ -229,10 +117,25 @@ class QueueDaemon extends Daemon return true; } +} - function log($level, $msg) +class QueueMaster extends IoMaster +{ + /** + * Initialize IoManagers for the currently configured site + * which are appropriate to this instance. + */ + function initManagers() { - common_log($level, get_class($this) . ' ('. $this->get_id() .'): '.$msg); + $classes = array(); + if (Event::handle('StartQueueDaemonIoManagers', array(&$classes))) { + $classes[] = 'QueueManager'; + } + Event::handle('EndQueueDaemonIoManagers', array(&$classes)); + + foreach ($classes as $class) { + $this->instantiate($class); + } } } diff --git a/scripts/xmppdaemon.php b/scripts/xmppdaemon.php index cef9c4bd07..fd7cf055b4 100755 --- a/scripts/xmppdaemon.php +++ b/scripts/xmppdaemon.php @@ -33,347 +33,46 @@ END_OF_XMPP_HELP; require_once INSTALLDIR.'/scripts/commandline.inc'; -require_once INSTALLDIR . '/lib/common.php'; require_once INSTALLDIR . '/lib/jabber.php'; -require_once INSTALLDIR . '/lib/daemon.php'; -# This is kind of clunky; we create a class to call the global functions -# in jabber.php, which create a new XMPP class. A more elegant (?) solution -# might be to use make this a subclass of XMPP. - -class XMPPDaemon extends Daemon +class XMPPDaemon extends SpawningDaemon { - function __construct($resource=null, $daemonize=true) + function __construct($id=null, $daemonize=true, $threads=1) { - parent::__construct($daemonize); - - static $attrs = array('server', 'port', 'user', 'password', 'host'); - - foreach ($attrs as $attr) - { - $this->$attr = common_config('xmpp', $attr); + if ($threads != 1) { + // This should never happen. :) + throw new Exception("XMPPDaemon can must run single-threaded"); } - - if ($resource) { - $this->resource = $resource . 'daemon'; - } else { - $this->resource = common_config('xmpp', 'resource') . 'daemon'; - } - - $this->jid = $this->user.'@'.$this->server.'/'.$this->resource; - - $this->log(LOG_INFO, "INITIALIZE XMPPDaemon {$this->jid}"); + parent::__construct($id, $daemonize, $threads); } - function connect() + function runThread() { - $connect_to = ($this->host) ? $this->host : $this->server; + common_log(LOG_INFO, 'Waiting to listen to XMPP and queues'); - $this->log(LOG_INFO, "Connecting to $connect_to on port $this->port"); + $master = new XmppMaster($this->get_id()); + $master->init(); + $master->service(); - $this->conn = jabber_connect($this->resource); + common_log(LOG_INFO, 'terminating normally'); - if (!$this->conn) { - return false; - } - - $this->log(LOG_INFO, "Connected"); - - $this->conn->setReconnectTimeout(600); - - $this->log(LOG_INFO, "Sending initial presence."); - - jabber_send_presence("Send me a message to post a notice", 'available', - null, 'available', 100); - - $this->log(LOG_INFO, "Done connecting."); - - return !$this->conn->isDisconnected(); + return true; } - function name() +} + +class XmppMaster extends IoMaster +{ + /** + * Initialize IoManagers for the currently configured site + * which are appropriate to this instance. + */ + function initManagers() { - return strtolower('xmppdaemon.'.$this->resource); - } - - function run() - { - if ($this->connect()) { - - $this->log(LOG_DEBUG, "Initializing stanza handlers."); - - $this->conn->addEventHandler('message', 'handle_message', $this); - $this->conn->addEventHandler('presence', 'handle_presence', $this); - $this->conn->addEventHandler('reconnect', 'handle_reconnect', $this); - - $this->log(LOG_DEBUG, "Beginning processing loop."); - - while ($this->conn->processTime(60)) { - $this->sendPing(); - } - } - } - - function sendPing() - { - if (!isset($this->pingid)) { - $this->pingid = 0; - } else { - $this->pingid++; - } - - $this->log(LOG_DEBUG, "Sending ping #{$this->pingid}"); - - $this->conn->send(""); - } - - function handle_reconnect(&$pl) - { - $this->log(LOG_DEBUG, "Got reconnection callback."); - $this->conn->processUntil('session_start'); - $this->log(LOG_DEBUG, "Sending reconnection presence."); - $this->conn->presence('Send me a message to post a notice', 'available', null, 'available', 100); - unset($pl['xml']); - $pl['xml'] = null; - - $pl = null; - unset($pl); - } - - function get_user($from) - { - $user = User::staticGet('jabber', jabber_normalize_jid($from)); - return $user; - } - - function handle_message(&$pl) - { - $from = jabber_normalize_jid($pl['from']); - - if ($pl['type'] != 'chat') { - $this->log(LOG_WARNING, "Ignoring message of type ".$pl['type']." from $from."); - return; - } - - if (mb_strlen($pl['body']) == 0) { - $this->log(LOG_WARNING, "Ignoring message with empty body from $from."); - return; - } - - # Forwarded from another daemon (probably a broadcaster) for - # us to handle - - if ($this->is_self($from)) { - $this->log(LOG_INFO, "Got forwarded notice from self ($from)."); - $from = $this->get_ofrom($pl); - $this->log(LOG_INFO, "Originally sent by $from."); - if (is_null($from) || $this->is_self($from)) { - $this->log(LOG_INFO, "Ignoring notice originally sent by $from."); - return; - } - } - - $user = $this->get_user($from); - - // For common_current_user to work - global $_cur; - $_cur = $user; - - if (!$user) { - $this->from_site($from, 'Unknown user; go to ' . - common_local_url('imsettings') . - ' to add your address to your account'); - $this->log(LOG_WARNING, 'Message from unknown user ' . $from); - return; - } - if ($this->handle_command($user, $pl['body'])) { - $this->log(LOG_INFO, "Command message by $from handled."); - return; - } else if ($this->is_autoreply($pl['body'])) { - $this->log(LOG_INFO, 'Ignoring auto reply from ' . $from); - return; - } else if ($this->is_otr($pl['body'])) { - $this->log(LOG_INFO, 'Ignoring OTR from ' . $from); - return; - } else { - - $this->log(LOG_INFO, 'Posting a notice from ' . $user->nickname); - - $this->add_notice($user, $pl); - } - - $user->free(); - unset($user); - unset($_cur); - - unset($pl['xml']); - $pl['xml'] = null; - - $pl = null; - unset($pl); - } - - function is_self($from) - { - return preg_match('/^'.strtolower(jabber_daemon_address()).'/', strtolower($from)); - } - - function get_ofrom($pl) - { - $xml = $pl['xml']; - $addresses = $xml->sub('addresses'); - if (!$addresses) { - $this->log(LOG_WARNING, 'Forwarded message without addresses'); - return null; - } - $address = $addresses->sub('address'); - if (!$address) { - $this->log(LOG_WARNING, 'Forwarded message without address'); - return null; - } - if (!array_key_exists('type', $address->attrs)) { - $this->log(LOG_WARNING, 'No type for forwarded message'); - return null; - } - $type = $address->attrs['type']; - if ($type != 'ofrom') { - $this->log(LOG_WARNING, 'Type of forwarded message is not ofrom'); - return null; - } - if (!array_key_exists('jid', $address->attrs)) { - $this->log(LOG_WARNING, 'No jid for forwarded message'); - return null; - } - $jid = $address->attrs['jid']; - if (!$jid) { - $this->log(LOG_WARNING, 'Could not get jid from address'); - return null; - } - $this->log(LOG_DEBUG, 'Got message forwarded from jid ' . $jid); - return $jid; - } - - function is_autoreply($txt) - { - if (preg_match('/[\[\(]?[Aa]uto[-\s]?[Rr]e(ply|sponse)[\]\)]/', $txt)) { - return true; - } else if (preg_match('/^System: Message wasn\'t delivered. Offline storage size was exceeded.$/', $txt)) { - return true; - } else { - return false; - } - } - - function is_otr($txt) - { - if (preg_match('/^\?OTR/', $txt)) { - return true; - } else { - return false; - } - } - - function from_site($address, $msg) - { - $text = '['.common_config('site', 'name') . '] ' . $msg; - jabber_send_message($address, $text); - } - - function handle_command($user, $body) - { - $inter = new CommandInterpreter(); - $cmd = $inter->handle_command($user, $body); - if ($cmd) { - $chan = new XMPPChannel($this->conn); - $cmd->execute($chan); - return true; - } else { - return false; - } - } - - function add_notice(&$user, &$pl) - { - $body = trim($pl['body']); - $content_shortened = common_shorten_links($body); - if (Notice::contentTooLong($content_shortened)) { - $from = jabber_normalize_jid($pl['from']); - $this->from_site($from, sprintf(_('Message too long - maximum is %1$d characters, you sent %2$d.'), - Notice::maxContent(), - mb_strlen($content_shortened))); - return; - } - - try { - $notice = Notice::saveNew($user->id, $content_shortened, 'xmpp'); - } catch (Exception $e) { - $this->log(LOG_ERR, $e->getMessage()); - $this->from_site($user->jabber, $e->getMessage()); - return; - } - - common_broadcast_notice($notice); - $this->log(LOG_INFO, - 'Added notice ' . $notice->id . ' from user ' . $user->nickname); - $notice->free(); - unset($notice); - } - - function handle_presence(&$pl) - { - $from = jabber_normalize_jid($pl['from']); - switch ($pl['type']) { - case 'subscribe': - # We let anyone subscribe - $this->subscribed($from); - $this->log(LOG_INFO, - 'Accepted subscription from ' . $from); - break; - case 'subscribed': - case 'unsubscribed': - case 'unsubscribe': - $this->log(LOG_INFO, - 'Ignoring "' . $pl['type'] . '" from ' . $from); - break; - default: - if (!$pl['type']) { - $user = User::staticGet('jabber', $from); - if (!$user) { - $this->log(LOG_WARNING, 'Presence from unknown user ' . $from); - return; - } - if ($user->updatefrompresence) { - $this->log(LOG_INFO, 'Updating ' . $user->nickname . - ' status from presence.'); - $this->add_notice($user, $pl); - } - $user->free(); - unset($user); - } - break; - } - unset($pl['xml']); - $pl['xml'] = null; - - $pl = null; - unset($pl); - } - - function log($level, $msg) - { - $text = 'XMPPDaemon('.$this->resource.'): '.$msg; - common_log($level, $text); - if (!$this->daemonize) - { - $line = common_log_line($level, $text); - echo $line; - echo "\n"; - } - } - - function subscribed($to) - { - jabber_special_presence('subscribed', $to); + // @fixme right now there's a hack in QueueManager to determine + // which queues to subscribe to based on the master class. + $this->instantiate('QueueManager'); + $this->instantiate('XmppManager'); } } From df9b780706eec9ff67e97803f5f70b475b010281 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 6 Oct 2009 15:29:22 -0400 Subject: [PATCH 005/157] action/doc.php is PHPCS clean --- actions/doc.php | 61 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/actions/doc.php b/actions/doc.php index 836f039d32..5df18a859f 100644 --- a/actions/doc.php +++ b/actions/doc.php @@ -49,7 +49,7 @@ class DocAction extends Action var $title; /** - * Class handler. + * Handle a request * * @param array $args array of arguments * @@ -71,6 +71,7 @@ class DocAction extends Action } $c = file_get_contents($this->filename); + $this->output = common_markup_to_html($c); Event::handle('EndLoadDoc', array($this->title, &$this->output)); @@ -79,30 +80,48 @@ class DocAction extends Action $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 +130,8 @@ class DocAction extends Action /** * Page title. * + * Uses the title of the document. + * * @return page title */ function title() @@ -118,6 +139,14 @@ 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; From 9f815c968f855b75e82224d85314007ec0613aad Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 7 Oct 2009 05:14:25 -0400 Subject: [PATCH 006/157] restructure doc.php for new use --- actions/doc.php | 75 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/actions/doc.php b/actions/doc.php index 5df18a859f..99c2c79665 100644 --- a/actions/doc.php +++ b/actions/doc.php @@ -45,8 +45,18 @@ 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) + { + $this->title = $this->trimmed('title'); + $this->output = null; + + $this->loadDoc(); + return true; + } /** * Handle a request @@ -58,25 +68,6 @@ 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(); } @@ -151,4 +142,46 @@ class DocAction extends Action { 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() + { + $local = array_merge(glob(INSTALLDIR.'/local/doc-src/'.$this->title), + glob(INSTALLDIR.'/local/doc-src/'.$this->title.'.*')); + + if (count($local)) { + return $this->negotiateLanguage($local); + } + + $dist = array_merge(glob(INSTALLDIR.'/doc-src/'.$this->title), + glob(INSTALLDIR.'/doc-src/'.$this->title.'.*')); + + if (count($dist)) { + return $this->negotiateLanguage($dist); + } + + return null; + } + + function negotiateLanguage($files) + { + // FIXME: write this + } } From 104d300799c0817bdbe893f08fd9b46d37a75a80 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 22 Jan 2010 14:13:28 -0500 Subject: [PATCH 007/157] do actual language negotiation for help docs --- actions/doc.php | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/actions/doc.php b/actions/doc.php index 99c2c79665..25d363472a 100644 --- a/actions/doc.php +++ b/actions/doc.php @@ -51,6 +51,8 @@ class DocAction extends Action function prepare($args) { + parent::prepare($args); + $this->title = $this->trimmed('title'); $this->output = null; @@ -163,25 +165,41 @@ class DocAction extends Action function getFilename() { - $local = array_merge(glob(INSTALLDIR.'/local/doc-src/'.$this->title), - glob(INSTALLDIR.'/local/doc-src/'.$this->title.'.*')); - - if (count($local)) { - return $this->negotiateLanguage($local); + if (file_exists(INSTALLDIR.'/local/doc-src/'.$this->title)) { + $localDef = INSTALLDIR.'/local/doc-src/'.$this->title; } - $dist = array_merge(glob(INSTALLDIR.'/doc-src/'.$this->title), - glob(INSTALLDIR.'/doc-src/'.$this->title.'.*')); + $local = glob(INSTALLDIR.'/local/doc-src/'.$this->title.'.*'); - if (count($dist)) { - return $this->negotiateLanguage($dist); + 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($files) + function negotiateLanguage($filenames, $defaultFilename=null) { - // FIXME: write this + // XXX: do this better + + $langcode = common_language(); + + foreach ($filenames as $filename) { + if (preg_match('/\.'.$langcode.'$/', $filename)) { + return $filename; + } + } + + return $defaultFilename; } } From 8bf2a9046bd364f684374b0388c8ea1a71b5ae0b Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Fri, 22 Jan 2010 19:18:14 +0100 Subject: [PATCH 008/157] Fixed innerHTML problem in IE7 and 8 for badge script --- js/identica-badge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/identica-badge.js b/js/identica-badge.js index 8276f22a1d..e43d1c43c7 100644 --- a/js/identica-badge.js +++ b/js/identica-badge.js @@ -223,7 +223,7 @@ function markupPost(raw, server) { }, changeUserTo : function(el) { $.a.user = el.rel; - $.s.h.a.innerHTML = el.rev + $.a.headerText; + $.s.h.a.appendChild(document.createTextNode(el.rev + $.a.headerText)); $.s.h.a.href = 'http://' + $.a.server + '/' + el.id; $.f.runSearch(); }, From 99866a459bff927c4a72c17904d2ecbc1c421af3 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 22 Jan 2010 12:35:05 -0800 Subject: [PATCH 009/157] Fix for stuck queue messages: wrap processing in stomp transactions so our lack of an ACK if PHP dies actually triggers redelivery. Previously, messages once delivered would just get stuck in the queue seemingly forever if they never got ACKed. Note this could lead to partial duplication, for instance if the OMB or Twitter queue handlers die after 1/2 of the outgoing sends. Recommendations: * catch exceptions more aggressively within queue handlers (so only PHP fatal errors are likely to kill in the middle) * for processing that involves sending to multiple clients, consider a second queue similar to the XMPP output, eg for OMB: - first queue gets delivery list and builds message data, enqueueing it for each target address - second queue can handle each individual outgoing message (and attempt redelivery etc separately) This would also protect better against a recurring error preventing delivery in the second part, and could spread out any slow sends over multiple threads. --- lib/stompqueuemanager.php | 68 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/lib/stompqueuemanager.php b/lib/stompqueuemanager.php index f057bd9e41..8f0091a138 100644 --- a/lib/stompqueuemanager.php +++ b/lib/stompqueuemanager.php @@ -41,6 +41,10 @@ class StompQueueManager extends QueueManager protected $sites = array(); + protected $useTransactions = true; + protected $transaction = null; + protected $transactionCount = 0; + function __construct() { parent::__construct(); @@ -201,6 +205,7 @@ class StompQueueManager extends QueueManager } else { $this->doSubscribe(); } + $this->begin(); return true; } @@ -213,6 +218,9 @@ class StompQueueManager extends QueueManager */ public function finish() { + // If there are any outstanding delivered messages we haven't processed, + // free them for another thread to take. + $this->rollback(); if ($this->sites) { foreach ($this->sites as $server) { StatusNet::init($server); @@ -293,7 +301,9 @@ class StompQueueManager extends QueueManager $notice = Notice::staticGet('id', $id); if (empty($notice)) { $this->_log(LOG_WARNING, "Skipping missing $info"); - $this->con->ack($frame); + $this->ack($frame); + $this->commit(); + $this->begin(); $this->stats('badnotice', $queue); return false; } @@ -308,7 +318,9 @@ class StompQueueManager extends QueueManager $handler = $this->getHandler($queue); if (!$handler) { $this->_log(LOG_ERROR, "Missing handler class; skipping $info"); - $this->con->ack($frame); + $this->ack($frame); + $this->commit(); + $this->begin(); $this->stats('badhandler', $queue); return false; } @@ -320,14 +332,18 @@ class StompQueueManager extends QueueManager // FIXME we probably shouldn't have to do // this kind of queue management ourselves; // if we don't ack, it should resend... - $this->con->ack($frame); + $this->ack($frame); $this->enqueue($item, $queue); + $this->commit(); + $this->begin(); $this->stats('requeued', $queue); return false; } $this->_log(LOG_INFO, "Successfully handled $info"); - $this->con->ack($frame); + $this->ack($frame); + $this->commit(); + $this->begin(); $this->stats('handled', $queue); return true; } @@ -369,5 +385,49 @@ class StompQueueManager extends QueueManager { common_log($level, 'StompQueueManager: '.$msg); } + + protected function begin() + { + if ($this->useTransactions) { + if ($this->transaction) { + throw new Exception("Tried to start transaction in the middle of a transaction"); + } + $this->transactionCount++; + $this->transaction = $this->master->id . '-' . $this->transactionCount . '-' . time(); + $this->con->begin($this->transaction); + } + } + + protected function ack($frame) + { + if ($this->useTransactions) { + if (!$this->transaction) { + throw new Exception("Tried to ack but not in a transaction"); + } + } + $this->con->ack($frame, $this->transaction); + } + + protected function commit() + { + if ($this->useTransactions) { + if (!$this->transaction) { + throw new Exception("Tried to commit but not in a transaction"); + } + $this->con->commit($this->transaction); + $this->transaction = null; + } + } + + protected function rollback() + { + if ($this->useTransactions) { + if (!$this->transaction) { + throw new Exception("Tried to rollback but not in a transaction"); + } + $this->con->commit($this->transaction); + $this->transaction = null; + } + } } From 6d055ce09ea516f115a7c21a59d11f9257ce0d9e Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 22 Jan 2010 13:49:05 -0800 Subject: [PATCH 010/157] Fix unqueuemanager for updated QueueHandler interface --- lib/unqueuemanager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/unqueuemanager.php b/lib/unqueuemanager.php index 5595eac052..785de7c8ce 100644 --- a/lib/unqueuemanager.php +++ b/lib/unqueuemanager.php @@ -47,7 +47,7 @@ class UnQueueManager extends QueueManager $handler = $this->getHandler($queue); if ($handler) { - $handler->handle_notice($notice); + $handler->handle($notice); } else { if (Event::handle('UnqueueHandleNotice', array(&$notice, $queue))) { throw new ServerException("UnQueueManager: Unknown queue: $queue"); From 71b3b9ee2be6973c3f55a59811ae103321c5abcc Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 22 Jan 2010 13:58:20 -0800 Subject: [PATCH 011/157] Consolidate PuSH publishing ping into a single POST for all feeds, and fix server response (if any on failure) to go to log instead of stdout. --- plugins/PubSubHubBub/PubSubHubBubPlugin.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/plugins/PubSubHubBub/PubSubHubBubPlugin.php b/plugins/PubSubHubBub/PubSubHubBubPlugin.php index 8286cd5489..ce6086df94 100644 --- a/plugins/PubSubHubBub/PubSubHubBubPlugin.php +++ b/plugins/PubSubHubBub/PubSubHubBubPlugin.php @@ -211,13 +211,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; From a4d733b68dad7f70857309b7da8e95cb164da746 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Fri, 22 Jan 2010 15:04:53 -0800 Subject: [PATCH 012/157] Fix for PoweredByStatusNetPlugin to be localizable (was broken for non-English word order) (Note the .po files will have to be added manually for now as we haven't set TranslateWiki up for plugins I think) --- .../PoweredByStatusNetPlugin.php | 5 +-- .../locale/PoweredByStatusNet.po | 32 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 plugins/PoweredByStatusNet/locale/PoweredByStatusNet.po 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 "" From 51fdba09d4bdc2b933e40c9ea5ea6de76c4aa799 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Thu, 21 Jan 2010 16:49:49 +0100 Subject: [PATCH 013/157] Event hooks for before and after site_notice --- EVENTS.txt | 6 ++++++ lib/action.php | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) 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/lib/action.php b/lib/action.php index 171bea17c7..22425e6821 100644 --- a/lib/action.php +++ b/lib/action.php @@ -373,7 +373,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 { From aaa2dd2712504dc9399d6cc5e1a002eb08ce6d4f Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Tue, 12 Jan 2010 01:02:55 +0000 Subject: [PATCH 014/157] Styles for application details page --- theme/base/css/display.css | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/theme/base/css/display.css b/theme/base/css/display.css index 2e4c88dfa3..5e280bdfee 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -894,6 +894,39 @@ font-weight:normal; margin-right:11px; } +#showapplication .entity_profile { +width:68%; +} +#showapplication .entity_profile img { +max-width:96px; +max-height:96px; +} +#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; +} + /* NOTICE */ .notice, .profile { From f3c732587ca17d7fe693d2f3830bd65f3b1f678f Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Tue, 12 Jan 2010 02:04:20 +0000 Subject: [PATCH 015/157] Styles for application list --- theme/base/css/display.css | 16 +++++++++++++++- theme/default/css/display.css | 2 ++ theme/identica/css/display.css | 2 ++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/theme/base/css/display.css b/theme/base/css/display.css index 5e280bdfee..c33f64aca0 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -894,6 +894,19 @@ font-weight:normal; margin-right:11px; } +/*applications*/ +.applications { +margin-bottom:18px; +float:left; +width:100%; +} +.applications li { +list-style-type:none; +} +.application img { +max-width:96px; +max-height:96px; +} #showapplication .entity_profile { width:68%; } @@ -929,7 +942,8 @@ font-size:1.3em; /* NOTICE */ .notice, -.profile { +.profile, +.application { position:relative; padding-top:11px; padding-bottom:11px; diff --git a/theme/default/css/display.css b/theme/default/css/display.css index 8a2c011752..4a45303caa 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; } @@ -378,6 +379,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..96e3c61bc6 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; } @@ -377,6 +378,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); } From cae113941d9c2aadcbe4763a481b2bb25a205075 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Tue, 12 Jan 2010 02:33:36 +0000 Subject: [PATCH 016/157] Added key icon for application key and secret rest action --- theme/base/images/icons/icons-01.gif | Bin 3607 -> 3650 bytes theme/base/images/icons/twotone/green/key.gif | Bin 0 -> 76 bytes theme/default/css/display.css | 6 +++++- theme/identica/css/display.css | 6 +++++- 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 theme/base/images/icons/twotone/green/key.gif diff --git a/theme/base/images/icons/icons-01.gif b/theme/base/images/icons/icons-01.gif index 06202a047b40daed0f0ceec269d9025232de4dc6..f93d33d79bce50e1e407c98d534c6cbff24d4366 100644 GIT binary patch delta 96 zcmbO(b4Z5A-P6s&GEs`*6#Mi48+raQ3IG59U-2gkBNxMe1|0?<07`9^W&XltIDMjl zq}F7kpC!_klVmOXyvp-$e2>3vRuHD%YdbahO2y=z?nN${lO9D(aobtK$jD#~0Koes A;Q#;t delta 53 zcmX>kGhK$q-P6s&GEs`5gZ+8HMxK96!tqlC6o0ZXaxwg8&|v@qpwwnr<}X}q-}xCC G8LR=HZ4L Date: Tue, 12 Jan 2010 02:51:33 +0000 Subject: [PATCH 017/157] Styles for image max width/height and radio form controls --- theme/base/css/display.css | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/theme/base/css/display.css b/theme/base/css/display.css index c33f64aca0..507733979f 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -903,17 +903,15 @@ width:100%; .applications li { list-style-type:none; } -.application img { +.application img, +#showapplication .entity_profile img, +#editapplication .form_data #application_icon img { max-width:96px; max-height:96px; } #showapplication .entity_profile { width:68%; } -#showapplication .entity_profile img { -max-width:96px; -max-height:96px; -} #showapplication .entity_profile .entity_fn { margin-left:0; } @@ -939,6 +937,10 @@ margin-left:1.795%; font-family:monospace; font-size:1.3em; } +#editapplication .form_data #application_types label.radio, +#editapplication .form_data #default_access_types label.radio { +width:15%; +} /* NOTICE */ .notice, From fa218ec65dde91f129eb17c7d443bec4017d170c Mon Sep 17 00:00:00 2001 From: Craig Andrews Date: Wed, 13 Jan 2010 22:05:22 -0500 Subject: [PATCH 018/157] Revert "Drop the Google Client API-based AJAX geolocation lookup shim -- it fails to ask for user permission, causing us quite a bit of difficulty." This reverts commit 749b8b5b8ca4d1c39d350879aadddbdb9d8b71d5. --- js/geometa.js | 123 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 118 insertions(+), 5 deletions(-) diff --git a/js/geometa.js b/js/geometa.js index 87e3c99a16..21deb18852 100644 --- a/js/geometa.js +++ b/js/geometa.js @@ -1,4 +1,4 @@ -// A shim to implement the W3C Geolocation API Specification using Gears +// 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 @@ -96,9 +96,122 @@ 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(); })(); From 414c80a18d6bbcb75e816a5f4ff10d6c54107d41 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Sat, 16 Jan 2010 19:16:44 +0000 Subject: [PATCH 019/157] Removed extra comma in object --- js/geometa.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/geometa.js b/js/geometa.js index 21deb18852..6bad095ec8 100644 --- a/js/geometa.js +++ b/js/geometa.js @@ -168,7 +168,7 @@ var AjaxGeoLocation = (function() { accuracy: 43000, // same as Gears accuracy over wifi? altitudeAccuracy: null, heading: null, - speed: null, + speed: null }, // extra info that is outside of the bounds of the core API address: { From ab53a8704b13a20f8ae885d2b92883e0c7101424 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Sat, 16 Jan 2010 19:42:32 +0000 Subject: [PATCH 020/157] Some JSlint-ing --- js/geometa.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/js/geometa.js b/js/geometa.js index 6bad095ec8..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 or the Ajax API -if (typeof navigator.geolocation == "undefined" || navigator.geolocation.shim ) (function(){ +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 { @@ -112,7 +111,7 @@ var AjaxGeoLocation = (function() { var queue = []; var addLocationQueue = function(callback) { queue.push(callback); - } + }; var runLocationQueue = function() { if (hasGoogleLoader()) { @@ -121,18 +120,18 @@ var AjaxGeoLocation = (function() { call(); } } - } + }; window['_google_loader_apiLoaded'] = function() { runLocationQueue(); - } + }; var hasGoogleLoader = function() { return (window['google'] && google['loader']); - } + }; var checkGoogleLoader = function(callback) { - if (hasGoogleLoader()) return true; + if (hasGoogleLoader()) { return true; } addLocationQueue(callback); @@ -155,7 +154,7 @@ var AjaxGeoLocation = (function() { var self = this; if (!checkGoogleLoader(function() { self.getCurrentPosition(successCallback, errorCallback, options); - })) return; + })) { return; } if (google.loader.ClientLocation) { var cl = google.loader.ClientLocation; @@ -215,3 +214,4 @@ var AjaxGeoLocation = (function() { navigator.geolocation = (window.google && google.gears) ? GearsGeoLocation() : AjaxGeoLocation(); })(); +} From 68c864c95a10b9018862c8ff3677c1185f1df496 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Sat, 16 Jan 2010 19:44:37 +0000 Subject: [PATCH 021/157] JSLinting on JSON --- js/util.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/js/util.js b/js/util.js index a749c5d9f4..2d19b8df25 100644 --- a/js/util.js +++ b/js/util.js @@ -529,13 +529,13 @@ var SN = { // StatusNet $('#'+SN.C.S.NoticeDataGeo).attr('checked', true); var cookieValue = { - 'NLat': data.lat, - 'NLon': data.lon, - 'NLNS': lns, - 'NLID': lid, - 'NLN': NLN_text, - 'NLNU': location.url, - 'NDG': true + NLat: data.lat, + NLon: data.lon, + NLNS: lns, + NLID: lid, + NLN: NLN_text, + NLNU: location.url, + NDG: true }; $.cookie(SN.C.S.NoticeDataGeoCookie, JSON.stringify(cookieValue)); }); @@ -570,9 +570,9 @@ var SN = { // StatusNet $('#'+SN.C.S.NoticeLon).val(position.coords.longitude); var data = { - 'lat': position.coords.latitude, - 'lon': position.coords.longitude, - 'token': $('#token').val() + lat: position.coords.latitude, + lon: position.coords.longitude, + token: $('#token').val() }; getJSONgeocodeURL(geocodeURL, data); From f043bc62a53250eaf383e9f36f95031fcd4e1160 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Sat, 16 Jan 2010 20:10:46 +0000 Subject: [PATCH 022/157] Added missing position paramater --- js/util.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/js/util.js b/js/util.js index 2d19b8df25..0947319554 100644 --- a/js/util.js +++ b/js/util.js @@ -498,7 +498,7 @@ var SN = { // StatusNet $.cookie(SN.C.S.NoticeDataGeoCookie, 'disabled'); } - function getJSONgeocodeURL(geocodeURL, data) { + function getJSONgeocodeURL(geocodeURL, data, position) { $.getJSON(geocodeURL, data, function(location) { var lns, lid; @@ -575,7 +575,7 @@ var SN = { // StatusNet token: $('#token').val() }; - getJSONgeocodeURL(geocodeURL, data); + getJSONgeocodeURL(geocodeURL, data, position); }, function(error) { @@ -602,7 +602,7 @@ var SN = { // StatusNet 'token': $('#token').val() }; - getJSONgeocodeURL(geocodeURL, data); + getJSONgeocodeURL(geocodeURL, data, position); } else { removeNoticeDataGeo(); From 2a1ab137cc233314b4c115d0687c27c7dbaec96f Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Sat, 16 Jan 2010 20:57:18 +0000 Subject: [PATCH 023/157] Using visibility:hidden instead of display:none for checkbox --- theme/base/css/display.css | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/theme/base/css/display.css b/theme/base/css/display.css index 507733979f..fc11a9234d 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -73,7 +73,7 @@ input.checkbox, input.radio { position:relative; top:2px; -left:0; +left:auto; border:0; } @@ -567,7 +567,8 @@ float:right; font-size:0.8em; } -.form_notice #notice_data-geo_wrap label { +.form_notice #notice_data-geo_wrap label, +.form_notice #notice_data-geo_wrap input { position:absolute; top:25px; right:4px; @@ -578,7 +579,7 @@ height:16px; display:block; } .form_notice #notice_data-geo_wrap input { -display:none; +visibility:hidden; } .form_notice #notice_data-geo_wrap label { font-weight:normal; From 37e642bece04a208a7c7b0bffe8bb1dc3f976d1e Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Sun, 17 Jan 2010 14:04:47 +0000 Subject: [PATCH 024/157] Inline script for maxlength is deprecated --- plugins/MobileProfile/MobileProfilePlugin.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/MobileProfile/MobileProfilePlugin.php b/plugins/MobileProfile/MobileProfilePlugin.php index 14d2500e8f..d426fc282b 100644 --- a/plugins/MobileProfile/MobileProfilePlugin.php +++ b/plugins/MobileProfile/MobileProfilePlugin.php @@ -356,8 +356,6 @@ class MobileProfilePlugin extends WAP20Plugin $contentLimit = Notice::maxContent(); - $form->out->inlineScript('maxLength = ' . $contentLimit . ';'); - if ($contentLimit > 0) { $form->out->element('div', array('id' => 'notice_text-count'), $contentLimit); From 2742494fe82d5c12016d46e2de5a849efe2ebee6 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Sun, 17 Jan 2010 19:45:35 +0000 Subject: [PATCH 025/157] Updated UI for notice aside content and notice options in MobileProfile --- plugins/MobileProfile/mp-screen.css | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/plugins/MobileProfile/mp-screen.css b/plugins/MobileProfile/mp-screen.css index 3eefc0c8e0..472fbb0013 100644 --- a/plugins/MobileProfile/mp-screen.css +++ b/plugins/MobileProfile/mp-screen.css @@ -179,10 +179,12 @@ padding-bottom:4px; } .notice div.entry-content { margin-left:0; -width:62.5%; +width:75%; +max-width:100%; +min-width:0; } .notice-options { -width:34%; +width:50px; margin-right:1%; } @@ -190,12 +192,29 @@ margin-right:1%; width:16px; height:16px; } -.notice-options a, -.notice-options input { +.notice .notice-options a, +.notice .notice-options input { box-shadow:none; -moz-box-shadow:none; -webkit-box-shadow:none; } +.notice .notice-options a, +.notice .notice-options form { +margin:-4px 0 0 0; +} +.notice .notice-options .notice_repeat, +.notice .notice-options .notice_delete { +margin-top:18px; +} +.notice .notice-options .notice_reply, +.notice .notice-options .notice_repeat { +margin-left:18px; +} + + +.notice .notice-options .notice_delete { +float:left; +} .entity_profile { width:auto; From 940fc463c814ac26cdec9e3afdf67fd9187519a3 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Sun, 17 Jan 2010 22:31:47 +0000 Subject: [PATCH 026/157] Took out focus out of textare when location share is enabled/disabled. Also avoids the conflict with the URL fragment on the conversation page. --- js/util.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/js/util.js b/js/util.js index 0947319554..d93467d4a5 100644 --- a/js/util.js +++ b/js/util.js @@ -628,8 +628,6 @@ var SN = { // StatusNet else { removeNoticeDataGeo(); } - - $('#'+SN.C.S.NoticeDataText).focus(); }).change(); } }, From f92a9dad8c68c8a1355a2f00b29d21209b62dd34 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Mon, 18 Jan 2010 11:12:05 +0000 Subject: [PATCH 027/157] Moved farbtastic's stylesheet to use relative paths for its own images --- {theme/base/css => js/farbtastic}/farbtastic.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename {theme/base/css => js/farbtastic}/farbtastic.css (70%) diff --git a/theme/base/css/farbtastic.css b/js/farbtastic/farbtastic.css similarity index 70% rename from theme/base/css/farbtastic.css rename to js/farbtastic/farbtastic.css index 7efcc73c3b..a88e7b868b 100644 --- a/theme/base/css/farbtastic.css +++ b/js/farbtastic/farbtastic.css @@ -16,17 +16,17 @@ height: 101px; } .farbtastic .wheel { - background: url(../../../js/farbtastic/wheel.png) no-repeat; + background: url(wheel.png) no-repeat; width: 195px; height: 195px; } .farbtastic .overlay { - background: url(../../../js/farbtastic/mask.png) no-repeat; + background: url(mask.png) no-repeat; } .farbtastic .marker { width: 17px; height: 17px; margin: -8px 0 0 -8px; overflow: hidden; - background: url(../../../js/farbtastic/marker.png) no-repeat; + background: url(marker.png) no-repeat; } From 0f3658d3da788b071da408839195526a10da5e2e Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Mon, 18 Jan 2010 11:29:05 +0000 Subject: [PATCH 028/157] Updated path to farbtastic stylesheet --- actions/designadminpanel.php | 2 +- lib/designsettings.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/actions/designadminpanel.php b/actions/designadminpanel.php index f862aff0eb..72ad6ade2a 100644 --- a/actions/designadminpanel.php +++ b/actions/designadminpanel.php @@ -289,7 +289,7 @@ class DesignadminpanelAction extends AdminPanelAction function showStylesheets() { parent::showStylesheets(); - $this->cssLink('css/farbtastic.css','base','screen, projection, tv'); + $this->cssLink('js/farbtastic/farbtastic.css',null,'screen, projection, tv'); } /** diff --git a/lib/designsettings.php b/lib/designsettings.php index b70ba0dfca..8e44c03a92 100644 --- a/lib/designsettings.php +++ b/lib/designsettings.php @@ -314,7 +314,7 @@ class DesignSettingsAction extends AccountSettingsAction function showStylesheets() { parent::showStylesheets(); - $this->cssLink('css/farbtastic.css','base','screen, projection, tv'); + $this->cssLink('js/farbtastic/farbtastic.css',null,'screen, projection, tv'); } /** From c367e09d1130d4d4f01e029c4659b18f55ae6d8f Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Mon, 18 Jan 2010 12:55:14 +0000 Subject: [PATCH 029/157] Some JS cleaning up for NoticeLocationAttach (which fixes also fixes a few bugs in WebKit) --- js/util.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/js/util.js b/js/util.js index d93467d4a5..a7339010a7 100644 --- a/js/util.js +++ b/js/util.js @@ -498,7 +498,7 @@ var SN = { // StatusNet $.cookie(SN.C.S.NoticeDataGeoCookie, 'disabled'); } - function getJSONgeocodeURL(geocodeURL, data, position) { + function getJSONgeocodeURL(geocodeURL, data) { $.getJSON(geocodeURL, data, function(location) { var lns, lid; @@ -513,7 +513,7 @@ var SN = { // StatusNet } if (typeof(location.name) == 'undefined') { - NLN_text = position.coords.latitude + ';' + position.coords.longitude; + NLN_text = data.lat + ';' + data.lon; } else { NLN_text = location.name; @@ -575,7 +575,7 @@ var SN = { // StatusNet token: $('#token').val() }; - getJSONgeocodeURL(geocodeURL, data, position); + getJSONgeocodeURL(geocodeURL, data); }, function(error) { @@ -597,12 +597,12 @@ var SN = { // StatusNet else { if (NLat.length > 0 && NLon.length > 0) { var data = { - 'lat': NLat, - 'lon': NLon, - 'token': $('#token').val() + lat: NLat, + lon: NLon, + token: $('#token').val() }; - getJSONgeocodeURL(geocodeURL, data, position); + getJSONgeocodeURL(geocodeURL, data); } else { removeNoticeDataGeo(); From c3ee1af7bee3b7268fc4cfd40ef45d9ca512fe97 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Mon, 18 Jan 2010 17:17:02 +0000 Subject: [PATCH 030/157] Missing null className for incoming email form legend --- actions/emailsettings.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/emailsettings.php b/actions/emailsettings.php index bfef2970da..08608348cd 100644 --- a/actions/emailsettings.php +++ b/actions/emailsettings.php @@ -130,7 +130,7 @@ class EmailsettingsAction extends AccountSettingsAction if (common_config('emailpost', 'enabled') && $user->email) { $this->elementStart('fieldset', array('id' => 'settings_email_incoming')); - $this->element('legend',_('Incoming email')); + $this->element('legend', null, _('Incoming email')); if ($user->incomingemail) { $this->elementStart('p'); $this->element('span', 'address', $user->incomingemail); From e3ee5663abfc5fa873f4b2c76f6f89971c3c9aae Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Wed, 20 Jan 2010 18:32:24 +0100 Subject: [PATCH 031/157] Updated notice item view where a) notice text no longer wraps around (under author's photo) b) supplemental notice content and options will start right under notice text. --- plugins/MobileProfile/mp-screen.css | 12 +++++++++++- theme/base/css/display.css | 22 +++++++++++++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/plugins/MobileProfile/mp-screen.css b/plugins/MobileProfile/mp-screen.css index 472fbb0013..76071352d0 100644 --- a/plugins/MobileProfile/mp-screen.css +++ b/plugins/MobileProfile/mp-screen.css @@ -176,8 +176,18 @@ margin-bottom:0; .profile { padding-top:4px; padding-bottom:4px; +min-height:65px; } -.notice div.entry-content { +#content .notice .entry-title { +float:left; +width:100%; +margin-left:0; +} +#content .notice .author .photo { +position:static; +float:left; +} +#content .notice div.entry-content { margin-left:0; width:75%; max-width:100%; diff --git a/theme/base/css/display.css b/theme/base/css/display.css index fc11a9234d..82670c964a 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -1006,6 +1006,16 @@ float:left; #shownotice .vcard .photo { margin-bottom:4px; } +#content .notice .author .photo { +position:absolute; +top:11px; +left:0; +float:none; +} +#content .notice .entry-title { +margin-left:59px; +} + .vcard .url { text-decoration:none; } @@ -1014,13 +1024,19 @@ text-decoration:underline; } .notice .entry-title { -float:left; -width:100%; overflow:hidden; } .notice .entry-title.ov { overflow:visible; } +#showstream .notice .entry-title, +#showstream .notice div.entry-content { +margin-left:0; +} +#shownotice .notice .entry-title, +#shownotice .notice div.entry-content { +margin-left:110px; +} #shownotice .notice .entry-title { font-size:2.2em; } @@ -1050,7 +1066,6 @@ max-width:70%; } #showstream .notice div.entry-content, #shownotice .notice div.entry-content { -margin-left:0; max-width:79%; } @@ -1114,6 +1129,7 @@ position:relative; font-size:0.95em; width:113px; float:right; +margin-top:3px; margin-right:4px; } From 51775e38bab8f3c41aa19bed84a38d1a7f534e37 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Wed, 20 Jan 2010 18:50:48 +0100 Subject: [PATCH 032/157] Better alignment for notice options in MobileProfile --- plugins/MobileProfile/mp-screen.css | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/plugins/MobileProfile/mp-screen.css b/plugins/MobileProfile/mp-screen.css index 76071352d0..04fa5fb002 100644 --- a/plugins/MobileProfile/mp-screen.css +++ b/plugins/MobileProfile/mp-screen.css @@ -194,7 +194,7 @@ max-width:100%; min-width:0; } .notice-options { -width:50px; +width:43px; margin-right:1%; } @@ -202,6 +202,13 @@ margin-right:1%; width:16px; height:16px; } +.notice-options form.processing { +background-image:none; +} +#wrap .notice-options form.processing input.submit { +background-position:0 47%; +} + .notice .notice-options a, .notice .notice-options input { box-shadow:none; @@ -212,16 +219,16 @@ box-shadow:none; .notice .notice-options form { margin:-4px 0 0 0; } -.notice .notice-options .notice_repeat, +.notice .notice-options .form_repeat, .notice .notice-options .notice_delete { -margin-top:18px; +margin-top:11px; } -.notice .notice-options .notice_reply, -.notice .notice-options .notice_repeat { -margin-left:18px; +.notice .notice-options .form_favor, +.notice .notice-options .form_disfavor, +.notice .notice-options .form_repeat { +margin-right:11px; } - .notice .notice-options .notice_delete { float:left; } From fed111f08aeba6be5bfc5e924975048e6a8f6521 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Thu, 21 Jan 2010 13:23:04 +0100 Subject: [PATCH 033/157] Removed mobile stylesheet from core output. If Mobile support is seeked, MobileProfile plugin should be used. --- lib/action.php | 4 - theme/base/css/mobile.css | 150 -------------------------------------- 2 files changed, 154 deletions(-) delete mode 100644 theme/base/css/mobile.css diff --git a/lib/action.php b/lib/action.php index 22425e6821..e242775585 100644 --- a/lib/action.php +++ b/lib/action.php @@ -199,10 +199,6 @@ class Action extends HTMLOutputter // lawsuit if (Event::handle('StartShowStatusNetStyles', array($this)) && Event::handle('StartShowLaconicaStyles', array($this))) { $this->cssLink('css/display.css',null,'screen, projection, tv'); - if (common_config('site', 'mobile')) { - // TODO: "handheld" CSS for other mobile devices - $this->cssLink('css/mobile.css','base','only screen and (max-device-width: 480px)'); // Mobile WebKit - } $this->cssLink('css/print.css','base','print'); Event::handle('EndShowStatusNetStyles', array($this)); Event::handle('EndShowLaconicaStyles', array($this)); diff --git a/theme/base/css/mobile.css b/theme/base/css/mobile.css deleted file mode 100644 index f6c53ea8dc..0000000000 --- a/theme/base/css/mobile.css +++ /dev/null @@ -1,150 +0,0 @@ -/** theme: base - * - * @package StatusNet - * @author Meitar Moscovitz - * @author Sarven Capadisli - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://status.net/ - */ - -body { -font-size:2.5em; -} - -#wrap { -width:95%; -} - -#header, -#header address, -#anon_notice, -#site_nav_local_views .nav, -#form_notice, -#form_notice .form_data li, -#core, -#content_inner, -#notices_primary, -.notice, -.notice .entry-title, -.notice div.entry-content, -.notice-options, -.notice .notice-options a, -.pagination, -.pagination .nav, -.aside .section { -float:none; -} - -.notice-options .notice_reply, -.notice-options .notice_delete, -.notice-options .form_favor, -.notice-options .form_disfavor { -position:static; -} - -#form_notice, -#anon_notice, -#footer, -#form_notice .form_actions input.submit { -width:auto; -} - -.form_settings label { -width:25%; -} -.form_settings .form_data p.form_guide { -margin-left:26%; -} - -#site_nav_global_primary { -width:75%; -} - -.entity_profile { -width:65%; -} -.entity_actions { -margin-left:0; -} - -#form_notice, -#anon_notice { -clear:both; -} - -#content, -#aside_primary { -width:96%; -padding-left:2%; -padding-right:2%; -} - -#site_notice { -position:static; -float:right; -clear:right; -width:75%; -margin-right:0; -margin-bottom:11px; -} - -.notices { -font-size:1.5em; -} - -#form_notice textarea { -width:80%; -height:5em; -} -#form_notice .form_note { -right:20%; -top:6em; -} - - -.vcard .photo, -.section .vcard .photo { -margin-right:18px; -} -.notice, -.profile { -margin-bottom:18px; -} - -.notices .entry-title, -.notices div.entry-content { -width:90%; -} -.notice div.entry-content { -margin-left:0; -} - -.notice .author .photo { -height:4.5em; -width:4.5em; -} -.notice-options { -position:absolute; -top:0; -right:0; -padding-left:7%; -width:3%; -} - -.notice-options .notice_delete a { -float:left; -} -.pagination .nav { -overflow:auto; -} - -#export_data { -display:none; -} - -#site_nav_local_views li { -margin-right:4px; -} -#site_nav_local_views a { -padding:18px 11px; -} From 38fe4ad9586706c69b46d68612d5a20415433ae7 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Sun, 24 Jan 2010 15:34:40 +0100 Subject: [PATCH 034/157] Added version info for MobileProfile plugin --- plugins/MobileProfile/MobileProfilePlugin.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/plugins/MobileProfile/MobileProfilePlugin.php b/plugins/MobileProfile/MobileProfilePlugin.php index d426fc282b..5c913836dc 100644 --- a/plugins/MobileProfile/MobileProfilePlugin.php +++ b/plugins/MobileProfile/MobileProfilePlugin.php @@ -414,7 +414,15 @@ class MobileProfilePlugin extends WAP20Plugin return $proto.'://'.$serverpart.'/'.$pathpart.$relative; } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'MobileProfile', + 'version' => STATUSNET_VERSION, + 'author' => 'Sarven Capadisli', + 'homepage' => 'http://status.net/wiki/Plugin:MobileProfile', + 'rawdescription' => + _m('XHTML MobileProfile output for supporting user agents.')); + return true; + } } - - -?> From 238998eca017a3ddd6cc0c01a75f8ace37661b5d Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 24 Jan 2010 18:18:24 -0500 Subject: [PATCH 035/157] save nickname and wildcard when setting up status network --- classes/Status_network.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/classes/Status_network.php b/classes/Status_network.php index ef8e1ed431..445f8a5a3c 100644 --- a/classes/Status_network.php +++ b/classes/Status_network.php @@ -48,6 +48,7 @@ class Status_network extends DB_DataObject static $cache = null; static $base = null; + static $wildcard = null; /** * @param string $dbhost @@ -187,7 +188,12 @@ class Status_network extends DB_DataObject $config['db']['database'] = "mysqli://$dbuser:$dbpass@$dbhost/$dbname"; - $config['site']['name'] = $sn->sitename; + $config['site']['name'] = $sn->sitename; + $config['site']['nickname'] = $sn->nickname; + + self::$wildcard = $wildcard; + + $config['site']['wildcard'] =& self::$wildcard; if (!empty($sn->hostname)) { $config['site']['server'] = $sn->hostname; @@ -230,4 +236,13 @@ class Status_network extends DB_DataObject exit; } + + function getServerName() + { + if (!empty($this->hostname)) { + return $this->hostname; + } else { + return $this->nickname . '.' . self::$wildcard; + } + } } From 7cd666ae928b003a8096289009bfd7290f8f6c73 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 24 Jan 2010 18:19:13 -0500 Subject: [PATCH 036/157] defaults for nickname and wildcard --- lib/default.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/default.php b/lib/default.php index 0cb70de2d2..e3a043de12 100644 --- a/lib/default.php +++ b/lib/default.php @@ -30,6 +30,8 @@ $default = array('site' => array('name' => 'Just another StatusNet microblog', + 'nickname' => 'statusnet', + 'wildcard' => null, 'server' => $_server, 'theme' => 'default', 'path' => $_path, From 60ce2ff3d01f22900d62b3d30ed89bc5e9d55167 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Sun, 24 Jan 2010 15:44:09 -0800 Subject: [PATCH 037/157] Use new StatusNetwork->serverName() to get full domain for wildcard config until we rebuild queues to be based on nicknames. Fixes live bug with new *.status.net sites breaking queuedaemon.php --- lib/iomaster.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iomaster.php b/lib/iomaster.php index 004e92b3ee..3bf82bc6b4 100644 --- a/lib/iomaster.php +++ b/lib/iomaster.php @@ -88,7 +88,7 @@ abstract class IoMaster $sn = new Status_network(); $sn->find(); while ($sn->fetch()) { - $hosts[] = $sn->hostname; + $hosts[] = $sn->getServerName(); } return $hosts; } From c0b832d19fc478d67752a6692bf71f178346e14a Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 10 Nov 2009 17:10:56 -0800 Subject: [PATCH 038/157] Add new OAuth application tables and DataObjects. Also add a new column for consumer secret to consumer table. --- classes/Consumer.php | 5 +++-- classes/Oauth_application.php | 33 ++++++++++++++++++++++++++++++ classes/Oauth_application_user.php | 24 ++++++++++++++++++++++ db/statusnet.sql | 26 +++++++++++++++++++++++ 4 files changed, 86 insertions(+), 2 deletions(-) create mode 100755 classes/Oauth_application.php create mode 100755 classes/Oauth_application_user.php diff --git a/classes/Consumer.php b/classes/Consumer.php index d5b7b7e33a..d17f183a88 100644 --- a/classes/Consumer.php +++ b/classes/Consumer.php @@ -11,9 +11,10 @@ class Consumer extends Memcached_DataObject 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) diff --git a/classes/Oauth_application.php b/classes/Oauth_application.php new file mode 100755 index 0000000000..6ad2db6dde --- /dev/null +++ b/classes/Oauth_application.php @@ -0,0 +1,33 @@ + Date: Thu, 12 Nov 2009 19:34:13 -0800 Subject: [PATCH 039/157] Changed the OAuth app tables to refer to profiles instead of users. Added an owner column to oauth_application. --- classes/Oauth_application.php | 23 ++++++++++++----------- classes/Oauth_application_user.php | 14 +++++++------- db/statusnet.sql | 9 +++++---- 3 files changed, 24 insertions(+), 22 deletions(-) mode change 100755 => 100644 classes/Oauth_application.php mode change 100755 => 100644 classes/Oauth_application_user.php diff --git a/classes/Oauth_application.php b/classes/Oauth_application.php old mode 100755 new mode 100644 index 6ad2db6dde..e2862bf97f --- a/classes/Oauth_application.php +++ b/classes/Oauth_application.php @@ -2,32 +2,33 @@ /** * Table Definition for oauth_application */ -require_once 'classes/Memcached_DataObject'; +require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; -class Oauth_application extends Memcached_DataObject +class Oauth_application extends Memcached_DataObject { ###START_AUTOCODE /* the code below is auto generated do not remove the above tag */ public $__table = 'oauth_application'; // table name public $id; // int(4) primary_key not_null + public $owner; // int(4) not_null public $consumer_key; // varchar(255) not_null public $name; // varchar(255) not_null - public $description; // varchar(255) + public $description; // varchar(255) public $icon; // varchar(255) not_null - public $source_url; // varchar(255) - public $organization; // varchar(255) - public $homepage; // varchar(255) + public $source_url; // varchar(255) + public $organization; // varchar(255) + public $homepage; // varchar(255) public $callback_url; // varchar(255) not_null - public $type; // tinyint(1) - public $access_type; // tinyint(1) + public $type; // tinyint(1) + public $access_type; // tinyint(1) public $created; // datetime not_null public $modified; // timestamp not_null default_CURRENT_TIMESTAMP /* Static get */ - function staticGet($k,$v=null) - { return Memcached_DataObject::staticGet('Oauth_application_user',$k,$v); } - + function staticGet($k,$v=NULL) { + return Memcached_DataObject::staticGet('Oauth_application',$k,$v); + } /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE } diff --git a/classes/Oauth_application_user.php b/classes/Oauth_application_user.php old mode 100755 new mode 100644 index a8922f5e77..9e45ece25f --- a/classes/Oauth_application_user.php +++ b/classes/Oauth_application_user.php @@ -2,23 +2,23 @@ /** * Table Definition for oauth_application_user */ -require_once 'classes/Memcached_DataObject'; +require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; -class Oauth_application_user extends Memcached_DataObject +class Oauth_application_user extends Memcached_DataObject { ###START_AUTOCODE /* the code below is auto generated do not remove the above tag */ public $__table = 'oauth_application_user'; // table name - public $user_id; // int(4) primary_key not_null + public $profile_id; // int(4) primary_key not_null public $application_id; // int(4) primary_key not_null - public $access_type; // tinyint(1) + public $access_type; // tinyint(1) public $created; // datetime not_null /* Static get */ - function staticGet($k,$v=null) - { return Memcached_DataObject::staticGet('Oauth_application_user',$k,$v); } - + function staticGet($k,$v=NULL) { + return Memcached_DataObject::staticGet('Oauth_application_user',$k,$v); + } /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE } diff --git a/db/statusnet.sql b/db/statusnet.sql index be0d140927..03e6115e5f 100644 --- a/db/statusnet.sql +++ b/db/statusnet.sql @@ -210,6 +210,7 @@ create table nonce ( 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', @@ -219,18 +220,18 @@ create table oauth_application ( homepage varchar(255) comment 'homepage for the organization', callback_url varchar(255) not null comment 'url to redirect to after authentication', type tinyint default 0 comment 'type of app, 0 = browser, 1 = desktop', - access_type tinyint default 0 comment 'default access type, 0 = read-write, 1 = read-only', + 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 ( - user_id integer not null comment 'id of the application user' references user (id), + 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, 0 = read-write, 1 = read-only', + access_type tinyint default 0 comment 'access type, bit 1 = read, bit 2 = write, bit 3 = revoked', created datetime not null comment 'date this record was created', - constraint primary key (user_id, application_id) + constraint primary key (profile_id, application_id) ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; /* These are used by JanRain OpenID library */ From ae46bc5fff21120ef867fb8ab59833a8f26adf47 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 12 Nov 2009 19:42:18 -0800 Subject: [PATCH 040/157] Started work on interface for displaying connected OAuth apps --- actions/applicationsettings.php | 135 ++++++++++++++++++++++++++++++++ actions/oauthclients.php | 108 +++++++++++++++++++++++++ classes/Profile.php | 23 ++++++ lib/applicationlist.php | 111 ++++++++++++++++++++++++++ lib/connectsettingsaction.php | 4 +- lib/router.php | 4 +- 6 files changed, 383 insertions(+), 2 deletions(-) create mode 100644 actions/applicationsettings.php create mode 100644 actions/oauthclients.php create mode 100644 lib/applicationlist.php diff --git a/actions/applicationsettings.php b/actions/applicationsettings.php new file mode 100644 index 0000000000..16c571feee --- /dev/null +++ b/actions/applicationsettings.php @@ -0,0 +1,135 @@ +. + * + * @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 ApplicationSettingsAction extends ConnectSettingsAction +{ + /** + * Title of the page + * + * @return string Title of the page + */ + + function title() + { + return _('Connected Applications'); + } + + /** + * 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); + + if ($application) { + $al = new ApplicationList($application, $this->user, $this); + $cnt = $al->show(); + if (0 == $cnt) { + $this->showEmptyListMessage(); + } + } + + $this->pagination($this->page > 1, $cnt > APPS_PER_PAGE, + $this->page, 'applicationsettings', + 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; + } + + } + + 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'); + } + +} diff --git a/actions/oauthclients.php b/actions/oauthclients.php new file mode 100644 index 0000000000..9a29e158e1 --- /dev/null +++ b/actions/oauthclients.php @@ -0,0 +1,108 @@ +. + * + * @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'; + +/** + * 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 OauthClientsAction extends ConnectSettingsAction +{ + /** + * Title of the page + * + * @return string Title of the page + */ + + function title() + { + return _('Applications using %%site_name%%'); + } + + /** + * 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(); + + } + + /** + * 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/classes/Profile.php b/classes/Profile.php index 25d908dbf9..687215b11b 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -352,6 +352,29 @@ class Profile extends Memcached_DataObject return $profile; } + function getApplications($offset = 0, $limit = null) + { + $qry = + 'SELECT oauth_application_user.* ' . + 'FROM oauth_application_user ' . + 'WHERE profile_id = %d ' . + 'ORDER BY 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/lib/applicationlist.php b/lib/applicationlist.php new file mode 100644 index 0000000000..fed784bb63 --- /dev/null +++ b/lib/applicationlist.php @@ -0,0 +1,111 @@ +. + * + * @category Public + * @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 Public + * @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) + { + parent::__construct($action); + + $this->application = $application; + $this->owner = $owner; + $this->action = $action; + } + + function show() + { + $this->out->elementStart('ul', 'applications xoxo'); + + $cnt = 0; + + while ($this->application->fetch()) { + $cnt++; + if($cnt > APPS_PER_PAGE) { + break; + } + $this->showapplication(); + } + + $this->out->elementEnd('ul'); + + return $cnt; + } + + function showApplication() + { + $this->out->elementStart('li', array('class' => 'application', + 'id' => 'oauthclient-' . $this->application->id)); + + $user = common_current_user(); + + $this->out->raw($this->application->name); + + $this->out->elementEnd('li'); + } + + /* Override this in subclasses. */ + + function showOwnerControls() + { + return; + } + + function highlight($text) + { + return htmlspecialchars($text); + } +} diff --git a/lib/connectsettingsaction.php b/lib/connectsettingsaction.php index e5fb8727ba..4b5059540d 100644 --- a/lib/connectsettingsaction.php +++ b/lib/connectsettingsaction.php @@ -116,6 +116,9 @@ class ConnectSettingsNav extends Widget _('Updates by SMS')); } + $menu['applicationsettings'] = array(_('Applications'), + _('OAuth connected applications')); + foreach ($menu as $menuaction => $menudesc) { $this->action->menuItem(common_local_url($menuaction), $menudesc[0], @@ -131,4 +134,3 @@ class ConnectSettingsNav extends Widget } - diff --git a/lib/router.php b/lib/router.php index 6b87ed27f6..9b2aa025ef 100644 --- a/lib/router.php +++ b/lib/router.php @@ -140,11 +140,13 @@ class Router // settings - foreach (array('profile', 'avatar', 'password', 'im', + foreach (array('profile', 'avatar', 'password', 'im', 'application', 'email', 'sms', 'userdesign', 'other') as $s) { $m->connect('settings/'.$s, array('action' => $s.'settings')); } + $m->connect('settings/oauthclients', array('action' => 'oauthclients')); + // search foreach (array('group', 'people', 'notice') as $s) { From 9d958fd539ecb74356013c11a2fd2c4c8b6a0ed1 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Fri, 13 Nov 2009 19:02:18 -0800 Subject: [PATCH 041/157] Reorganized the OAuth app URLs and more work on the register app workflow --- actions/{oauthclients.php => apps.php} | 4 +- actions/newapplication.php | 202 ++++++++++++++++ ...tings.php => oauthconnectionssettings.php} | 4 +- lib/applicationeditform.php | 215 ++++++++++++++++++ lib/connectsettingsaction.php | 8 +- lib/router.php | 15 +- 6 files changed, 436 insertions(+), 12 deletions(-) rename actions/{oauthclients.php => apps.php} (96%) create mode 100644 actions/newapplication.php rename actions/{applicationsettings.php => oauthconnectionssettings.php} (96%) create mode 100644 lib/applicationeditform.php diff --git a/actions/oauthclients.php b/actions/apps.php similarity index 96% rename from actions/oauthclients.php rename to actions/apps.php index 9a29e158e1..d4cea1e3e9 100644 --- a/actions/oauthclients.php +++ b/actions/apps.php @@ -45,7 +45,7 @@ require_once INSTALLDIR . '/lib/connectsettingsaction.php'; * @see SettingsAction */ -class OauthClientsAction extends ConnectSettingsAction +class AppsAction extends ConnectSettingsAction { /** * Title of the page @@ -55,7 +55,7 @@ class OauthClientsAction extends ConnectSettingsAction function title() { - return _('Applications using %%site_name%%'); + return _('OAuth applications'); } /** diff --git a/actions/newapplication.php b/actions/newapplication.php new file mode 100644 index 0000000000..a78a856b18 --- /dev/null +++ b/actions/newapplication.php @@ -0,0 +1,202 @@ +. + * + * @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 Action +{ + 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 create a group.')); + return false; + } + + return true; + } + + /** + * Handle the request + * + * On GET, show the form. On POST, try to save the group. + * + * @param array $args unused + * + * @return void + */ + + function handle($args) + { + parent::handle($args); + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + $this->trySave(); + } else { + $this->showForm(); + } + } + + 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('application'); + $callback_url = $this->trimmed('callback_url'); + $this->type = $this->trimmed('type'); + $this->access_type = $this->trimmed('access_type'); + + if (!is_null($name) && mb_strlen($name) > 255) { + $this->showForm(_('Name is too long (max 255 chars).')); + return; + } else if (User_group::descriptionTooLong($description)) { + $this->showForm(sprintf( + _('description is too long (max %d chars).'), + Oauth_application::maxDescription())); + return; + } elseif (!is_null($source_url) + && (strlen($source_url) > 0) + && !Validate::uri( + $source_url, + array('allowed_schemes' => array('http', 'https')) + ) + ) + { + $this->showForm(_('Source URL is not valid.')); + return; + } elseif (!is_null($homepage) + && (strlen($homepage) > 0) + && !Validate::uri( + $homepage, + array('allowed_schemes' => array('http', 'https')) + ) + ) + { + $this->showForm(_('Homepage is not a valid URL.')); + return; + } elseif (!is_null($callback_url) + && (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 = $souce_url; + $app->organization = $organization; + $app->homepage = $homepage; + $app->callback_url = $callback_url; + $app->type = $type; + $app->access_type = $access_type; + + // generate consumer key and secret + + $app->created = common_sql_now(); + + $result = $app->insert(); + + if (!$result) { + common_log_db_error($group, 'INSERT', __FILE__); + $this->serverError(_('Could not create application.')); + } + + $group->query('COMMIT'); + + common_redirect($group->homeUrl(), 303); + + } + +} + diff --git a/actions/applicationsettings.php b/actions/oauthconnectionssettings.php similarity index 96% rename from actions/applicationsettings.php rename to actions/oauthconnectionssettings.php index 16c571feee..6ec9f70273 100644 --- a/actions/applicationsettings.php +++ b/actions/oauthconnectionssettings.php @@ -46,7 +46,7 @@ require_once INSTALLDIR . '/lib/applicationlist.php'; * @see SettingsAction */ -class ApplicationSettingsAction extends ConnectSettingsAction +class OauthconnectionssettingsAction extends ConnectSettingsAction { /** * Title of the page @@ -95,7 +95,7 @@ class ApplicationSettingsAction extends ConnectSettingsAction } $this->pagination($this->page > 1, $cnt > APPS_PER_PAGE, - $this->page, 'applicationsettings', + $this->page, 'connectionssettings', array('nickname' => $this->user->nickname)); } diff --git a/lib/applicationeditform.php b/lib/applicationeditform.php new file mode 100644 index 0000000000..3fd45876a7 --- /dev/null +++ b/lib/applicationeditform.php @@ -0,0 +1,215 @@ +. + * + * @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'; + } + } + + /** + * 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() + { + if ($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, _('Register a new application')); + } + + /** + * Data elements of the form + * + * @return void + */ + + function formData() + { + if ($this->application) { + $id = $this->application->id; + $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 = ''; + $name = ''; + $description = ''; + $source_url = ''; + $organization = ''; + $homepage = ''; + $callback_url = ''; + $this->type = ''; + $this->access_type = ''; + } + + $this->out->elementStart('ul', 'form_data'); + $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'); + $this->out->input('description', _('Description'), + ($this->out->arg('Description')) ? $this->out->arg('discription') : $description); + $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') : $orgranization, + _('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 of 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'); + $this->out->input('type', _('Application type'), + ($this->out->arg('type')) ? $this->out->arg('type') : $type, + _('Type of application, browser or desktop')); + $this->out->elementEnd('li'); + + $this->out->elementStart('li'); + $this->out->input('access_type', _('Default access'), + ($this->out->arg('access_type')) ? $this->out->arg('access_type') : $access_type, + _('Default access for this application: read-write, or read-only')); + $this->out->elementEnd('li'); + + $this->out->elementEnd('ul'); + } + + /** + * Action elements + * + * @return void + */ + + function formActions() + { + $this->out->submit('submit', _('Save')); + } +} diff --git a/lib/connectsettingsaction.php b/lib/connectsettingsaction.php index 4b5059540d..b9c14799e0 100644 --- a/lib/connectsettingsaction.php +++ b/lib/connectsettingsaction.php @@ -115,9 +115,11 @@ class ConnectSettingsNav extends Widget array(_('SMS'), _('Updates by SMS')); } - - $menu['applicationsettings'] = array(_('Applications'), - _('OAuth connected applications')); + + $menu['oauthconnectionssettings'] = array( + _('Connections'), + _('Authorized connected applications') + ); foreach ($menu as $menuaction => $menudesc) { $this->action->menuItem(common_local_url($menuaction), diff --git a/lib/router.php b/lib/router.php index 9b2aa025ef..7b65ae215c 100644 --- a/lib/router.php +++ b/lib/router.php @@ -140,13 +140,11 @@ class Router // settings - foreach (array('profile', 'avatar', 'password', 'im', 'application', + foreach (array('profile', 'avatar', 'password', 'im', 'oauthconnections', 'email', 'sms', 'userdesign', 'other') as $s) { $m->connect('settings/'.$s, array('action' => $s.'settings')); } - - $m->connect('settings/oauthclients', array('action' => 'oauthclients')); - + // search foreach (array('group', 'people', 'notice') as $s) { @@ -636,12 +634,19 @@ class Router // user stuff foreach (array('subscriptions', 'subscribers', - 'nudge', 'all', 'foaf', 'xrds', + 'nudge', 'all', 'foaf', 'xrds', 'apps', 'replies', 'inbox', 'outbox', 'microsummary') as $a) { $m->connect(':nickname/'.$a, array('action' => $a), array('nickname' => '[a-zA-Z0-9]{1,64}')); } + + $m->connect('apps/new', array('action' => 'newapplication')); + + $m->connect(':nickname/apps/edit', + array('action' => 'editapplication'), + array('nickname' => '['.NICKNAME_FMT.']{1,64}') + ); foreach (array('subscriptions', 'subscribers') as $a) { $m->connect(':nickname/'.$a.'/:tag', From 035c475b45959057099c503d2cdcff8c8145e198 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Fri, 13 Nov 2009 19:10:38 -0800 Subject: [PATCH 042/157] It might help if I checkd in statusnet.ini. --- classes/statusnet.ini | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/classes/statusnet.ini b/classes/statusnet.ini index 6ce4495be0..0cbe60a5a5 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,35 @@ 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 +created = 142 + +[oauth_application_user__keys] +profile_id = K +application_id = K + [profile] id = 129 nickname = 130 From 3c2b05d222a55cd1e148f3f887bf55e924898f1b Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Mon, 16 Nov 2009 16:58:49 -0800 Subject: [PATCH 043/157] Workflow for registering new OAuth apps pretty much done. --- actions/apps.php | 63 +++++- actions/editapplication.php | 246 +++++++++++++++++++++ actions/newapplication.php | 133 ++++++++---- actions/oauthconnectionssettings.php | 13 ++ actions/showapplication.php | 306 +++++++++++++++++++++++++++ classes/Consumer.php | 16 +- classes/Oauth_application.php | 44 ++++ db/statusnet.sql | 2 +- lib/applicationeditform.php | 135 +++++++++--- lib/applicationlist.php | 46 +++- lib/default.php | 2 + lib/router.php | 27 ++- 12 files changed, 950 insertions(+), 83 deletions(-) create mode 100644 actions/editapplication.php create mode 100644 actions/showapplication.php diff --git a/actions/apps.php b/actions/apps.php index d4cea1e3e9..e6500599f7 100644 --- a/actions/apps.php +++ b/actions/apps.php @@ -31,7 +31,8 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once INSTALLDIR . '/lib/connectsettingsaction.php'; +require_once INSTALLDIR . '/lib/settingsaction.php'; +require_once INSTALLDIR . '/lib/applicationlist.php'; /** * Show a user's registered OAuth applications @@ -45,8 +46,23 @@ require_once INSTALLDIR . '/lib/connectsettingsaction.php'; * @see SettingsAction */ -class AppsAction extends ConnectSettingsAction +class AppsAction 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 * @@ -79,6 +95,49 @@ class AppsAction extends ConnectSettingsAction { $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->element('a', + array('href' => common_local_url( + 'newapplication', + array('nickname' => $user->nickname) + ) + ), + 'Register a new application »'); + + $this->pagination( + $this->page > 1, + $cnt > APPS_PER_PAGE, + $this->page, + 'apps', + array('nickname' => $user->nickname) + ); + } + + 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'); } /** diff --git a/actions/editapplication.php b/actions/editapplication.php new file mode 100644 index 0000000000..3af482844f --- /dev/null +++ b/actions/editapplication.php @@ -0,0 +1,246 @@ +. + * + * @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 $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); + + 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 group. + * + * @param array $args unused + * + * @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; + } + + $cur = common_current_user(); + + if ($this->arg('cancel')) { + common_redirect(common_local_url('showapplication', + array( + 'nickname' => $cur->nickname, + 'id' => $this->app->id) + ), 303); + } elseif ($this->arg('save')) { + $this->trySave(); + } else { + $this->clientError(_('Unexpected form submission.')); + } + } else { + $this->showForm(); + } + } + + 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('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 (empty($callback_url)) { + $this->showForm(_('Callback is required.')); + 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)); + + $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; + + if ($access_type == 'r') { + $this->app->setAccessFlags(true, false); + } else { + $this->app->setAccessFlags(true, true); + } + + $result = $this->app->update($orig); + + if (!$result) { + common_log_db_error($app, 'UPDATE', __FILE__); + $this->serverError(_('Could not update application.')); + } + + common_redirect(common_local_url('apps', + array('nickname' => $cur->nickname)), 303); + } + +} + diff --git a/actions/newapplication.php b/actions/newapplication.php index a78a856b18..9d8635270a 100644 --- a/actions/newapplication.php +++ b/actions/newapplication.php @@ -43,7 +43,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { * @link http://status.net/ */ -class NewApplicationAction extends Action +class NewApplicationAction extends OwnerDesignAction { var $msg; @@ -61,7 +61,7 @@ class NewApplicationAction extends Action parent::prepare($args); if (!common_logged_in()) { - $this->clientError(_('You must be logged in to create a group.')); + $this->clientError(_('You must be logged in to register an application.')); return false; } @@ -81,8 +81,19 @@ class NewApplicationAction extends Action function handle($args) { parent::handle($args); + if ($_SERVER['REQUEST_METHOD'] == 'POST') { - $this->trySave(); + + $cur = common_current_user(); + + if ($this->arg('cancel')) { + common_redirect(common_local_url('apps', + array('nickname' => $cur->nickname)), 303); + } elseif ($this->arg('save')) { + $this->trySave(); + } else { + $this->clientError(_('Unexpected form submission.')); + } } else { $this->showForm(); } @@ -112,55 +123,73 @@ class NewApplicationAction extends Action function trySave() { - $name = $this->trimmed('name'); - $description = $this->trimmed('description'); - $source_url = $this->trimmed('source_url'); - $organization = $this->trimmed('organization'); - $homepage = $this->trimmed('application'); - $callback_url = $this->trimmed('callback_url'); - $this->type = $this->trimmed('type'); - $this->access_type = $this->trimmed('access_type'); - - if (!is_null($name) && mb_strlen($name) > 255) { + $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('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; - } else if (User_group::descriptionTooLong($description)) { + } elseif (empty($description)) { + $this->showForm(_('Description is required.')); + return; + } elseif (Oauth_application::descriptionTooLong($description)) { $this->showForm(sprintf( - _('description is too long (max %d chars).'), + _('Description is too long (max %d chars).'), Oauth_application::maxDescription())); return; - } elseif (!is_null($source_url) - && (strlen($source_url) > 0) + } 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 (!is_null($homepage) - && (strlen($homepage) > 0) + } 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 (!is_null($callback_url) - && (strlen($callback_url) > 0) + return; + } elseif (empty($callback_url)) { + $this->showForm(_('Callback is required.')); + 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 @@ -171,31 +200,53 @@ class NewApplicationAction extends Action $app->query('BEGIN'); - $app->name = $name; - $app->owner = $cur->id; - $app->description = $description; - $app->source_url = $souce_url; + $app->name = $name; + $app->owner = $cur->id; + $app->description = $description; + $app->source_url = $source_url; $app->organization = $organization; - $app->homepage = $homepage; + $app->homepage = $homepage; $app->callback_url = $callback_url; - $app->type = $type; - $app->access_type = $access_type; - + $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 - - $app->created = common_sql_now(); + + $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; $result = $app->insert(); if (!$result) { - common_log_db_error($group, 'INSERT', __FILE__); + common_log_db_error($app, 'INSERT', __FILE__); $this->serverError(_('Could not create application.')); + $app->query('ROLLBACK'); } - - $group->query('COMMIT'); - common_redirect($group->homeUrl(), 303); - + $app->query('COMMIT'); + + common_redirect(common_local_url('apps', + array('nickname' => $cur->nickname)), 303); + } } diff --git a/actions/oauthconnectionssettings.php b/actions/oauthconnectionssettings.php index 6ec9f70273..e4b5af1586 100644 --- a/actions/oauthconnectionssettings.php +++ b/actions/oauthconnectionssettings.php @@ -132,4 +132,17 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction $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('apps', array('nickname' => $cur->nickname))), + 'here.'); + $this->elementEnd('p'); + } + } diff --git a/actions/showapplication.php b/actions/showapplication.php new file mode 100644 index 0000000000..6b8eff4a60 --- /dev/null +++ b/actions/showapplication.php @@ -0,0 +1,306 @@ +. + * + * @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 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(); + + $this->elementStart('div', 'entity_actions'); + + $this->element('a', + array('href' => + common_local_url( + 'editapplication', + array( + 'nickname' => $this->owner->nickname, + 'id' => $this->application->id + ) + ) + ), 'Edit application'); + + $this->elementStart('form', array( + 'id' => 'forma_reset_key', + 'class' => 'form_reset_key', + 'method' => 'POST', + 'action' => common_local_url('showapplication', + array('nickname' => $cur->nickname, + 'id' => $this->application->id)))); + + $this->elementStart('fieldset'); + $this->hidden('token', common_session_token()); + $this->submit('reset', _('Reset Consumer key/secret')); + $this->elementEnd('fieldset'); + $this->elementEnd('form'); + + $this->elementEnd('div'); + + $consumer = $this->application->getConsumer(); + + $this->elementStart('div', 'entity-application'); + + $this->elementStart('ul', 'entity_application_details'); + + $this->elementStart('li', 'entity_application_name'); + $this->element('span', array('class' => 'big'), $this->application->name); + $this->raw(sprintf(_(' by %1$s'), $this->application->organization)); + $this->elementEnd('li'); + + $this->element('li', 'entity_application_description', $this->application->description); + + $this->elementStart('li', 'entity_application_statistics'); + + $defaultAccess = ($this->application->access_type & Oauth_application::$writeAccess) + ? 'read-write' : 'read-only'; + $profile = Profile::staticGet($this->application->owner); + $userCnt = 0; // XXX: count how many users use the app + + $this->raw(sprintf( + _('Created by %1$s - %2$s access by default - %3$d users.'), + $profile->getBestName(), + $defaultAccess, + $userCnt + )); + + $this->elementEnd('li'); + + $this->elementEnd('ul'); + + $this->elementStart('dl', 'entity_consumer_key'); + $this->element('dt', null, _('Consumer key')); + $this->element('dd', 'label', $consumer->consumer_key); + $this->elementEnd('dl'); + + $this->elementStart('dl', 'entity_consumer_secret'); + $this->element('dt', null, _('Consumer secret')); + $this->element('dd', 'label', $consumer->consumer_secret); + $this->elementEnd('dl'); + + $this->elementStart('dl', 'entity_request_token_url'); + $this->element('dt', null, _('Request token URL')); + $this->element('dd', 'label', common_local_url('oauthrequesttoken')); + $this->elementEnd('dl'); + + $this->elementStart('dl', 'entity_access_token_url'); + $this->element('dt', null, _('Access token URL')); + $this->element('dd', 'label', common_local_url('oauthaccesstoken')); + $this->elementEnd('dl'); + + $this->elementStart('dl', 'entity_authorize_url'); + $this->element('dt', null, _('Authorize URL')); + $this->element('dd', 'label', common_local_url('oauthauthorize')); + $this->elementEnd('dl'); + + $this->element('p', 'oauth-signature-note', + '*We support hmac-sha1 signatures. We do not support the plaintext signature method.'); + + $this->elementEnd('div'); + + $this->elementStart('div', 'entity-list-apps'); + $this->element('a', + array( + 'href' => common_local_url( + 'apps', + array('nickname' => $this->owner->nickname) + ) + ), + 'View your applications'); + $this->elementEnd('div'); + } + + 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/classes/Consumer.php b/classes/Consumer.php index d17f183a88..ad64a8491b 100644 --- a/classes/Consumer.php +++ b/classes/Consumer.php @@ -4,7 +4,7 @@ */ 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 */ @@ -22,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/Oauth_application.php b/classes/Oauth_application.php index e2862bf97f..ef1bbf6d95 100644 --- a/classes/Oauth_application.php +++ b/classes/Oauth_application.php @@ -31,4 +31,48 @@ class Oauth_application extends Memcached_DataObject } /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE + + // Bit flags + public static $readAccess = 1; + public static $writeAccess = 2; + + public static $browser = 1; + public static $desktop = 2; + + function getConsumer() + { + return Consumer::staticGet('consumer_key', $this->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; + } + } + } diff --git a/db/statusnet.sql b/db/statusnet.sql index 03e6115e5f..a2740b60cc 100644 --- a/db/statusnet.sql +++ b/db/statusnet.sql @@ -219,7 +219,7 @@ create table oauth_application ( organization varchar(255) comment 'name of the organization running the application', homepage varchar(255) comment 'homepage for the organization', callback_url varchar(255) not null comment 'url to redirect to after authentication', - type tinyint default 0 comment 'type of app, 0 = browser, 1 = desktop', + 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' diff --git a/lib/applicationeditform.php b/lib/applicationeditform.php index 3fd45876a7..ed187ba0b4 100644 --- a/lib/applicationeditform.php +++ b/lib/applicationeditform.php @@ -100,11 +100,16 @@ class ApplicationEditForm extends Form function action() { - if ($this->application) { + $cur = common_current_user(); + + if (!empty($this->application)) { return common_local_url('editapplication', - array('id' => $this->application->id)); + array('id' => $this->application->id, + 'nickname' => $cur->nickname) + ); } else { - return common_local_url('newapplication'); + return common_local_url('newapplication', + array('nickname' => $cur->nickname)); } } @@ -116,7 +121,7 @@ class ApplicationEditForm extends Form function formLegend() { - $this->out->element('legend', null, _('Register a new application')); + $this->out->element('legend', null, _('Edit application')); } /** @@ -130,7 +135,7 @@ class ApplicationEditForm extends Form if ($this->application) { $id = $this->application->id; $name = $this->application->name; - $description = $this->application->description; + $description = $this->application->description; $source_url = $this->application->source_url; $organization = $this->application->organization; $homepage = $this->application->homepage; @@ -151,34 +156,46 @@ class ApplicationEditForm extends Form $this->out->elementStart('ul', 'form_data'); $this->out->elementStart('li'); - + $this->out->hidden('application_id', $id); + $this->out->hidden('token', common_session_token()); + $this->out->input('name', _('Name'), ($this->out->arg('name')) ? $this->out->arg('name') : $name); - + $this->out->elementEnd('li'); - + $this->out->elementStart('li'); - $this->out->input('description', _('Description'), - ($this->out->arg('Description')) ? $this->out->arg('discription') : $description); + + $maxDesc = Oauth_application::maxDesc(); + if ($maxDesc > 0) { + $descInstr = sprintf(_('Describe your application in %d chars'), + $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->elementEnd('li'); $this->out->elementStart('li'); - $this->out->input('Organization', _('Organization'), - ($this->out->arg('organization')) ? $this->out->arg('organization') : $orgranization, + $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 of the homepage of the organization')); + _('URL for the homepage of the organization')); $this->out->elementEnd('li'); $this->out->elementStart('li'); @@ -188,17 +205,86 @@ class ApplicationEditForm extends Form $this->out->elementEnd('li'); $this->out->elementStart('li'); - $this->out->input('type', _('Application type'), - ($this->out->arg('type')) ? $this->out->arg('type') : $type, - _('Type of application, browser or desktop')); + + $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->applicaiton->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'); - $this->out->input('access_type', _('Default access'), - ($this->out->arg('access_type')) ? $this->out->arg('access_type') : $access_type, - _('Default access for this application: read-write, or read-only')); + + $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'); } @@ -210,6 +296,7 @@ class ApplicationEditForm extends Form function formActions() { - $this->out->submit('submit', _('Save')); + $this->out->submit('save', _('Save')); + $this->out->submit('cancel', _('Cancel')); } } diff --git a/lib/applicationlist.php b/lib/applicationlist.php index fed784bb63..3141ea9741 100644 --- a/lib/applicationlist.php +++ b/lib/applicationlist.php @@ -20,7 +20,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * - * @category Public + * @category Application * @package StatusNet * @author Zach Copley * @copyright 2008-2009 StatusNet, Inc. @@ -39,7 +39,7 @@ define('APPS_PER_PAGE', 20); /** * Widget to show a list of OAuth applications * - * @category Public + * @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 @@ -50,10 +50,10 @@ 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; @@ -87,14 +87,42 @@ class ApplicationList extends Widget function showApplication() { - $this->out->elementStart('li', array('class' => 'application', - 'id' => 'oauthclient-' . $this->application->id)); $user = common_current_user(); - $this->out->raw($this->application->name); - - $this->out->elementEnd('li'); + $this->out->elementStart('li', array('class' => 'application', + 'id' => 'oauthclient-' . $this->application->id)); + + $this->out->elementStart('a', + array('href' => common_local_url( + 'showapplication', + array( + 'nickname' => $user->nickname, + 'id' => $this->application->id + ) + ), + 'class' => 'url') + ); + + $this->out->raw($this->application->name); + $this->out->elementEnd('a'); + + $this->out->raw(' by '); + + $this->out->elementStart('a', + array( + 'href' => $this->application->homepage, + 'class' => 'url' + ) + ); + $this->out->raw($this->application->organization); + $this->out->elementEnd('a'); + + $this->out->elementStart('p', 'note'); + $this->out->raw($this->application->description); + $this->out->elementEnd('p'); + + $this->out->elementEnd('li'); } /* Override this in subclasses. */ diff --git a/lib/default.php b/lib/default.php index e3a043de12..b6ee72279d 100644 --- a/lib/default.php +++ b/lib/default.php @@ -211,6 +211,8 @@ $default = 'uploads' => true, 'filecommand' => '/usr/bin/file', ), + 'application' => + array('desclimit' => null), 'group' => array('maxaliases' => 3, 'desclimit' => null), diff --git a/lib/router.php b/lib/router.php index 7b65ae215c..a8dbbf6d0f 100644 --- a/lib/router.php +++ b/lib/router.php @@ -641,12 +641,29 @@ class Router array('nickname' => '[a-zA-Z0-9]{1,64}')); } - $m->connect('apps/new', array('action' => 'newapplication')); - - $m->connect(':nickname/apps/edit', - array('action' => 'editapplication'), - array('nickname' => '['.NICKNAME_FMT.']{1,64}') + $m->connect(':nickname/apps', + array('action' => 'apps'), + array('nickname' => '['.NICKNAME_FMT.']{1,64}')); + $m->connect(':nickname/apps/show/:id', + array('action' => 'showapplication'), + array('nickname' => '['.NICKNAME_FMT.']{1,64}', + 'id' => '[0-9]+') ); + $m->connect(':nickname/apps/new', + array('action' => 'newapplication'), + array('nickname' => '['.NICKNAME_FMT.']{1,64}')); + $m->connect(':nickname/apps/edit/:id', + array('action' => 'editapplication'), + array('nickname' => '['.NICKNAME_FMT.']{1,64}', + 'id' => '[0-9]+') + ); + + $m->connect('oauth/request_token', + array('action' => 'oauthrequesttoken')); + $m->connect('oauth/access_token', + array('action' => 'oauthaccesstoken')); + $m->connect('oauth/authorize', + array('action' => 'oauthauthorize')); foreach (array('subscriptions', 'subscribers') as $a) { $m->connect(':nickname/'.$a.'/:tag', From 1e5b2a497e3c70e4af5f93e2326c93beed15fed1 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Mon, 16 Nov 2009 18:12:39 -0800 Subject: [PATCH 044/157] Added session token checking. --- actions/newapplication.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/actions/newapplication.php b/actions/newapplication.php index 9d8635270a..ec0f2e7af2 100644 --- a/actions/newapplication.php +++ b/actions/newapplication.php @@ -84,6 +84,13 @@ class NewApplicationAction extends OwnerDesignAction 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; + } + $cur = common_current_user(); if ($this->arg('cancel')) { From 48e5f2b3c5164aa9e47289e5b243e2e1189b71ef Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 Jan 2010 01:55:57 -0800 Subject: [PATCH 045/157] Add icons/icon upload to Oauth apps --- actions/editapplication.php | 81 +++++++++++++++++---------- actions/newapplication.php | 100 ++++++++++++++++++++++++++-------- actions/showapplication.php | 9 ++- classes/Oauth_application.php | 13 +++++ lib/applicationeditform.php | 43 ++++++++++++++- lib/applicationlist.php | 4 ++ 6 files changed, 193 insertions(+), 57 deletions(-) diff --git a/actions/editapplication.php b/actions/editapplication.php index 3af482844f..6b8dd501c9 100644 --- a/actions/editapplication.php +++ b/actions/editapplication.php @@ -81,7 +81,7 @@ class EditApplicationAction extends OwnerDesignAction /** * Handle the request * - * On GET, show the form. On POST, try to save the group. + * On GET, show the form. On POST, try to save the app. * * @param array $args unused * @@ -91,31 +91,49 @@ class EditApplicationAction extends OwnerDesignAction function handle($args) { parent::handle($args); + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + $this->handlePost($args); + } else { + $this->showForm(); + } + } - // CSRF protection - $token = $this->trimmed('token'); - if (!$token || $token != common_session_token()) { - $this->clientError(_('There was a problem with your session token.')); - return; - } + function handlePost($args) + { + // Workaround for PHP returning empty $_POST and $_FILES when POST + // length > post_max_size in php.ini - $cur = common_current_user(); - - if ($this->arg('cancel')) { - common_redirect(common_local_url('showapplication', - array( - 'nickname' => $cur->nickname, - 'id' => $this->app->id) - ), 303); - } elseif ($this->arg('save')) { - $this->trySave(); - } else { - $this->clientError(_('Unexpected form submission.')); - } - } else { - $this->showForm(); + 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( + 'nickname' => $cur->nickname, + 'id' => $this->app->id) + ), 303); + } elseif ($this->arg('save')) { + $this->trySave(); + } else { + $this->clientError(_('Unexpected form submission.')); + } } function showForm($msg=null) @@ -149,7 +167,7 @@ class EditApplicationAction extends OwnerDesignAction $homepage = $this->trimmed('homepage'); $callback_url = $this->trimmed('callback_url'); $type = $this->arg('app_type'); - $access_type = $this->arg('access_type'); + $access_type = $this->arg('default_access_type'); if (empty($name)) { $this->showForm(_('Name is required.')); @@ -214,6 +232,7 @@ class EditApplicationAction extends OwnerDesignAction // Checked in prepare() above assert(!is_null($cur)); + assert(!is_null($this->app)); $orig = clone($this->app); @@ -225,16 +244,18 @@ class EditApplicationAction extends OwnerDesignAction $this->app->callback_url = $callback_url; $this->app->type = $type; - if ($access_type == 'r') { - $this->app->setAccessFlags(true, false); - } else { - $this->app->setAccessFlags(true, true); - } - $result = $this->app->update($orig); + common_debug("access_type = $access_type"); + + if ($access_type == 'r') { + $this->app->access_type = 1; + } else { + $this->app->access_type = 3; + } + if (!$result) { - common_log_db_error($app, 'UPDATE', __FILE__); + common_log_db_error($this->app, 'UPDATE', __FILE__); $this->serverError(_('Could not update application.')); } diff --git a/actions/newapplication.php b/actions/newapplication.php index ec0f2e7af2..a0e61d288c 100644 --- a/actions/newapplication.php +++ b/actions/newapplication.php @@ -71,7 +71,7 @@ class NewApplicationAction extends OwnerDesignAction /** * Handle the request * - * On GET, show the form. On POST, try to save the group. + * On GET, show the form. On POST, try to save the app. * * @param array $args unused * @@ -83,29 +83,46 @@ class NewApplicationAction extends OwnerDesignAction 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; - } - - $cur = common_current_user(); - - if ($this->arg('cancel')) { - common_redirect(common_local_url('apps', - array('nickname' => $cur->nickname)), 303); - } elseif ($this->arg('save')) { - $this->trySave(); - } else { - $this->clientError(_('Unexpected form submission.')); - } + $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('apps', + array('nickname' => $cur->nickname)), 303); + } elseif ($this->arg('save')) { + $this->trySave(); + } else { + $this->clientError(_('Unexpected form submission.')); + } + } + function showForm($msg=null) { $this->msg = $msg; @@ -130,14 +147,14 @@ class NewApplicationAction extends OwnerDesignAction function trySave() { - $name = $this->trimmed('name'); + $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('access_type'); + $access_type = $this->arg('default_access_type'); if (empty($name)) { $this->showForm(_('Name is required.')); @@ -241,14 +258,16 @@ class NewApplicationAction extends OwnerDesignAction $app->consumer_key = $consumer->consumer_key; - $result = $app->insert(); + $this->app_id = $app->insert(); - if (!$result) { + if (!$this->app_id) { common_log_db_error($app, 'INSERT', __FILE__); $this->serverError(_('Could not create application.')); $app->query('ROLLBACK'); } + $this->uploadLogo($app); + $app->query('COMMIT'); common_redirect(common_local_url('apps', @@ -256,5 +275,40 @@ class NewApplicationAction extends OwnerDesignAction } + /** + * Handle an image upload + * + * Does all the magic for handling an image upload, and crops the + * image by default. + * + * @return void + */ + + function uploadLogo($app) + { + 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($app->id, + image_type_to_extension($imagefile->type), + null, + 'oauth-app-icon-'.common_timestamp()); + + $filepath = Avatar::path($filename); + + move_uploaded_file($imagefile->filepath, $filepath); + + $app->setOriginal($filename); + } + } + } diff --git a/actions/showapplication.php b/actions/showapplication.php index 6b8eff4a60..6d19b9561c 100644 --- a/actions/showapplication.php +++ b/actions/showapplication.php @@ -55,7 +55,6 @@ class ShowApplicationAction extends OwnerDesignAction var $owner = null; - var $msg = null; var $success = null; @@ -187,6 +186,14 @@ class ShowApplicationAction extends OwnerDesignAction $this->elementStart('ul', 'entity_application_details'); + $this->elementStart('li', 'entity_application-icon'); + + if (!empty($this->application->icon)) { + $this->element('img', array('src' => $this->application->icon)); + } + + $this->elementEnd('li'); + $this->elementStart('li', 'entity_application_name'); $this->element('span', array('class' => 'big'), $this->application->name); $this->raw(sprintf(_(' by %1$s'), $this->application->organization)); diff --git a/classes/Oauth_application.php b/classes/Oauth_application.php index ef1bbf6d95..d4de6d82e1 100644 --- a/classes/Oauth_application.php +++ b/classes/Oauth_application.php @@ -75,4 +75,17 @@ class Oauth_application extends Memcached_DataObject } } + 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); + } + } diff --git a/lib/applicationeditform.php b/lib/applicationeditform.php index ed187ba0b4..4d3bb06e75 100644 --- a/lib/applicationeditform.php +++ b/lib/applicationeditform.php @@ -81,6 +81,21 @@ class ApplicationEditForm extends Form } } + /** + * 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 * @@ -134,6 +149,7 @@ class ApplicationEditForm extends Form { 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; @@ -144,6 +160,7 @@ class ApplicationEditForm extends Form $this->access_type = $this->application->access_type; } else { $id = ''; + $icon = ''; $name = ''; $description = ''; $source_url = ''; @@ -154,11 +171,31 @@ class ApplicationEditForm extends Form $this->access_type = ''; } + $this->out->hidden('token', common_session_token()); + $this->out->elementStart('ul', 'form_data'); - $this->out->elementStart('li'); + + $this->out->elementStart('li'); + + 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->hidden('token', common_session_token()); $this->out->input('name', _('Name'), ($this->out->arg('name')) ? $this->out->arg('name') : $name); @@ -215,7 +252,7 @@ class ApplicationEditForm extends Form // Default to Browser if ($this->application->type == Oauth_application::$browser - || empty($this->applicaiton->type)) { + || empty($this->application->type)) { $attrs['checked'] = 'checked'; } diff --git a/lib/applicationlist.php b/lib/applicationlist.php index 3141ea9741..5392ddab8c 100644 --- a/lib/applicationlist.php +++ b/lib/applicationlist.php @@ -93,6 +93,10 @@ class ApplicationList extends Widget $this->out->elementStart('li', array('class' => 'application', 'id' => 'oauthclient-' . $this->application->id)); + if (!empty($this->application->icon)) { + $this->out->element('img', array('src' => $this->application->icon)); + } + $this->out->elementStart('a', array('href' => common_local_url( 'showapplication', From 6472331be51bc6d0e670603b2a89fb66022f6b51 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 Jan 2010 13:19:21 -0800 Subject: [PATCH 046/157] Stubs for API OAuth token exchange stuff --- actions/apioauthaccesstoken.php | 49 ++++++++++++++++++++++++++++++++ actions/apioauthauthorize.php | 49 ++++++++++++++++++++++++++++++++ actions/apioauthrequesttoken.php | 49 ++++++++++++++++++++++++++++++++ lib/router.php | 6 ++-- 4 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 actions/apioauthaccesstoken.php create mode 100644 actions/apioauthauthorize.php create mode 100644 actions/apioauthrequesttoken.php diff --git a/actions/apioauthaccesstoken.php b/actions/apioauthaccesstoken.php new file mode 100644 index 0000000000..db82f656ad --- /dev/null +++ b/actions/apioauthaccesstoken.php @@ -0,0 +1,49 @@ +. + * + * @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/api.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 ApiAction +{ + +} diff --git a/actions/apioauthauthorize.php b/actions/apioauthauthorize.php new file mode 100644 index 0000000000..8839d9571c --- /dev/null +++ b/actions/apioauthauthorize.php @@ -0,0 +1,49 @@ +. + * + * @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/api.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 Action +{ + +} diff --git a/actions/apioauthrequesttoken.php b/actions/apioauthrequesttoken.php new file mode 100644 index 0000000000..c1ccd4b7d7 --- /dev/null +++ b/actions/apioauthrequesttoken.php @@ -0,0 +1,49 @@ +. + * + * @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/api.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 ApiAction +{ + +} diff --git a/lib/router.php b/lib/router.php index a8dbbf6d0f..0703d75970 100644 --- a/lib/router.php +++ b/lib/router.php @@ -659,11 +659,11 @@ class Router ); $m->connect('oauth/request_token', - array('action' => 'oauthrequesttoken')); + array('action' => 'apioauthrequesttoken')); $m->connect('oauth/access_token', - array('action' => 'oauthaccesstoken')); + array('action' => 'apioauthaccesstoken')); $m->connect('oauth/authorize', - array('action' => 'oauthauthorize')); + array('action' => 'apioauthauthorize')); foreach (array('subscriptions', 'subscribers') as $a) { $m->connect(':nickname/'.$a.'/:tag', From fa81a580bb9eea76e7739f37010b35e4b919f410 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 Jan 2010 18:33:17 -0800 Subject: [PATCH 047/157] Action for issuing a request token --- actions/apioauthrequesttoken.php | 41 ++++++++++++++- lib/apioauthstore.php | 90 ++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 lib/apioauthstore.php diff --git a/actions/apioauthrequesttoken.php b/actions/apioauthrequesttoken.php index c1ccd4b7d7..1bbd7d295b 100644 --- a/actions/apioauthrequesttoken.php +++ b/actions/apioauthrequesttoken.php @@ -32,6 +32,7 @@ if (!defined('STATUSNET')) { } require_once INSTALLDIR . '/lib/api.php'; +require_once INSTALLDIR . '/lib/apioauthstore.php'; /** * Get an OAuth request token @@ -43,7 +44,45 @@ require_once INSTALLDIR . '/lib/api.php'; * @link http://status.net/ */ -class ApiOauthRequestTokenAction extends ApiAction +class ApiOauthRequestTokenAction extends Action { + /** + * Is read only? + * + * @return boolean false + */ + function isReadOnly() + { + return false; + } + + /** + * 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_WARN, $e->getMessage()); + common_debug(var_export($req, true)); + header('HTTP/1.1 401 Unauthorized'); + header('Content-Type: text/html; charset=utf-8'); + print $e->getMessage() . "\n"; + } + } } diff --git a/lib/apioauthstore.php b/lib/apioauthstore.php new file mode 100644 index 0000000000..a92a4d6e49 --- /dev/null +++ b/lib/apioauthstore.php @@ -0,0 +1,90 @@ +. + */ + +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 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 + if ($rt->find(true) && $rt->state == 1) { // authorized + common_debug('request token found.', __FILE__); + $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 subscription + // XXX: mixing levels here + $sub = Subscription::staticGet('token', $rt->tok); + if (!$sub) { + return null; + } + common_debug('subscription for request token found', __FILE__); + $orig_sub = clone($sub); + $sub->token = $at->tok; + $sub->secret = $at->secret; + if (!$sub->update($orig_sub)) { + return null; + } else { + common_debug('subscription updated to use access token', __FILE__); + return new OAuthToken($at->tok, $at->secret); + } + } + } else { + return null; + } + } + +} + From e9e448bcee69b0c39badf353faedb4c29af3f502 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Sun, 10 Jan 2010 21:35:46 -0800 Subject: [PATCH 048/157] Workflow for request tokens and authorizing request tokens --- actions/apioauthauthorize.php | 326 ++++++++++++++++++++++++++++++- actions/apioauthrequesttoken.php | 5 +- actions/showapplication.php | 6 +- lib/router.php | 19 +- 4 files changed, 338 insertions(+), 18 deletions(-) diff --git a/actions/apioauthauthorize.php b/actions/apioauthauthorize.php index 8839d9571c..895a0c6e53 100644 --- a/actions/apioauthauthorize.php +++ b/actions/apioauthauthorize.php @@ -31,7 +31,7 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR . '/lib/api.php'; +require_once INSTALLDIR . '/lib/apioauthstore.php'; /** * Authorize an OAuth request token @@ -45,5 +45,329 @@ require_once INSTALLDIR . '/lib/api.php'; class ApiOauthAuthorizeAction extends Action { + 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(var_export($_REQUEST, true)); + + $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(); + + return true; + } + + function getApp() + { + // Look up the full req token + + $req_token = $this->store->lookup_token(null, + 'request', + $this->oauth_token); + + if (empty($req_token)) { + + common_debug("Couldn't find request token!"); + + $this->clientError(_('Bad request.')); + return; + } + + // Look up the app + + $app = new Oauth_application(); + $app->consumer_key = $req_token->consumer_key; + $result = $app->find(true); + + if (!empty($result)) { + $this->app = $app; + return true; + + } else { + common_debug("couldn't find the app!"); + return false; + } + } + + /** + * 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') { + /* Use a 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; + } + + $this->handlePost(); + + } else { + + common_debug('ApiOauthAuthorize::handle()'); + + if (empty($this->oauth_token)) { + + common_debug("No request token found."); + + $this->clientError(_('Bad request.')); + return; + } + + if (!$this->getApp()) { + $this->clientError(_('Bad request.')); + return; + } + + common_debug("Requesting auth for app: $app->name."); + + $this->showForm(); + } + } + + function handlePost() + { + /* Use a 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; + } + + if (!$this->getApp()) { + $this->clientError(_('Bad request.')); + return; + } + + // is the user already logged in? + + // check creds + + if (!common_logged_in()) { + $user = common_check_user($this->nickname, $this->password); + if (empty($user)) { + $this->showForm(_("Invalid nickname / password!")); + return; + } + } + + if ($this->arg('allow')) { + + $this->store->authorize_token($this->oauth_token); + + // if we have a callback redirect and provide the token + + if (!empty($this->callback)) { + $target_url = $this->callback . '?oauth_token=' . $this->oauth_token; + common_redirect($target_url, 303); + } + + // otherwise inform the user that the rt was authorized + + $this->elementStart('p'); + + // XXX: Do 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(); + // $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'); + } + + /** + * Show page notice + * + * Display a notice for how to use the page, or the + * error if it exists. + * + * @return void + */ + + function showPageNotice() + { + if ($this->error) { + $this->element('p', 'error', $this->error); + } else { + $instr = $this->getInstructions(); + $output = common_markup_to_html($instr); + + $this->raw($output); + } + } + + /** + * Shows the authorization form. + * + * @return void + */ + + function showContent() + { + $this->elementStart('form', array('method' => 'post', + 'id' => 'form_login', + 'class' => 'form_settings', + 'action' => common_local_url('apioauthauthorize'))); + + $this->hidden('token', common_session_token()); + $this->hidden('oauth_token', $this->oauth_token); + $this->hidden('oauth_callback', $this->callback); + + $this->elementStart('fieldset'); + + $this->elementStart('ul'); + $this->elementStart('li'); + if (!empty($this->app->icon)) { + $this->element('img', array('src' => $this->app->icon)); + } + $this->elementEnd('li'); + $this->elementStart('li'); + + $access = ($this->app->access_type & Oauth_application::$writeAccess) ? + 'access and update' : 'access'; + + $msg = _("The application %s by %s would like " . + "the ability to %s your account data."); + + $this->raw(sprintf($msg, + $this->app->name, + $this->app->organization, + $access)); + + $this->elementEnd('li'); + $this->elementEnd('ul'); + + $this->elementEnd('fieldset'); + + if (!common_logged_in()) { + + $this->elementStart('fieldset'); + $this->element('legend', null, _('Login')); + $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', + 'name' => 'deny', + 'type' => 'submit', + 'value' => _('Deny'))); + + $this->element('input', array('id' => 'allow_submit', + 'class' => 'submit', + 'name' => 'allow', + 'type' => 'submit', + 'value' => _('Allow'))); + + $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 index 1bbd7d295b..53aca6b96b 100644 --- a/actions/apioauthrequesttoken.php +++ b/actions/apioauthrequesttoken.php @@ -31,7 +31,6 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR . '/lib/api.php'; require_once INSTALLDIR . '/lib/apioauthstore.php'; /** @@ -70,6 +69,7 @@ class ApiOauthRequestTokenAction extends Action $datastore = new ApiStatusNetOAuthDataStore(); $server = new OAuthServer($datastore); $hmac_method = new OAuthSignatureMethod_HMAC_SHA1(); + $server->add_signature_method($hmac_method); try { @@ -77,8 +77,7 @@ class ApiOauthRequestTokenAction extends Action $token = $server->fetch_request_token($req); print $token; } catch (OAuthException $e) { - common_log(LOG_WARN, $e->getMessage()); - common_debug(var_export($req, true)); + common_log(LOG_WARN, '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/showapplication.php b/actions/showapplication.php index 6d19b9561c..5156fa6f0c 100644 --- a/actions/showapplication.php +++ b/actions/showapplication.php @@ -231,17 +231,17 @@ class ShowApplicationAction extends OwnerDesignAction $this->elementStart('dl', 'entity_request_token_url'); $this->element('dt', null, _('Request token URL')); - $this->element('dd', 'label', common_local_url('oauthrequesttoken')); + $this->element('dd', 'label', common_local_url('apioauthrequesttoken')); $this->elementEnd('dl'); $this->elementStart('dl', 'entity_access_token_url'); $this->element('dt', null, _('Access token URL')); - $this->element('dd', 'label', common_local_url('oauthaccesstoken')); + $this->element('dd', 'label', common_local_url('apioauthaccesstoken')); $this->elementEnd('dl'); $this->elementStart('dl', 'entity_authorize_url'); $this->element('dt', null, _('Authorize URL')); - $this->element('dd', 'label', common_local_url('oauthauthorize')); + $this->element('dd', 'label', common_local_url('apioauthauthorize')); $this->elementEnd('dl'); $this->element('p', 'oauth-signature-note', diff --git a/lib/router.php b/lib/router.php index 0703d75970..420f5a0a10 100644 --- a/lib/router.php +++ b/lib/router.php @@ -50,7 +50,8 @@ class Router var $m = null; static $inst = null; static $bare = array('requesttoken', 'accesstoken', 'userauthorization', - 'postnotice', 'updateprofile', 'finishremotesubscribe'); + 'postnotice', 'updateprofile', 'finishremotesubscribe', + 'apioauthrequesttoken', 'apioauthaccesstoken'); static function get() { @@ -144,7 +145,7 @@ class Router 'email', 'sms', 'userdesign', 'other') as $s) { $m->connect('settings/'.$s, array('action' => $s.'settings')); } - + // search foreach (array('group', 'people', 'notice') as $s) { @@ -640,11 +641,11 @@ class Router array('action' => $a), array('nickname' => '[a-zA-Z0-9]{1,64}')); } - - $m->connect(':nickname/apps', + + $m->connect(':nickname/apps', array('action' => 'apps'), array('nickname' => '['.NICKNAME_FMT.']{1,64}')); - $m->connect(':nickname/apps/show/:id', + $m->connect(':nickname/apps/show/:id', array('action' => 'showapplication'), array('nickname' => '['.NICKNAME_FMT.']{1,64}', 'id' => '[0-9]+') @@ -652,18 +653,14 @@ class Router $m->connect(':nickname/apps/new', array('action' => 'newapplication'), array('nickname' => '['.NICKNAME_FMT.']{1,64}')); - $m->connect(':nickname/apps/edit/:id', + $m->connect(':nickname/apps/edit/:id', array('action' => 'editapplication'), array('nickname' => '['.NICKNAME_FMT.']{1,64}', 'id' => '[0-9]+') ); - $m->connect('oauth/request_token', - array('action' => 'apioauthrequesttoken')); - $m->connect('oauth/access_token', - array('action' => 'apioauthaccesstoken')); $m->connect('oauth/authorize', - array('action' => 'apioauthauthorize')); + array('action' => 'apioauthauthorize')); foreach (array('subscriptions', 'subscribers') as $a) { $m->connect(':nickname/'.$a.'/:tag', From c473a39a7da07fbe5b80fec4c08111a554691c3a Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Sun, 10 Jan 2010 23:03:30 -0800 Subject: [PATCH 049/157] Associate request tokens with OAuth apps and app users --- actions/apioauthauthorize.php | 64 +++++++++++++++++++++++------- classes/Oauth_application_user.php | 24 ++++++++++- classes/statusnet.ini | 4 ++ db/statusnet.sql | 5 ++- 4 files changed, 81 insertions(+), 16 deletions(-) diff --git a/actions/apioauthauthorize.php b/actions/apioauthauthorize.php index 895a0c6e53..48d5087efc 100644 --- a/actions/apioauthauthorize.php +++ b/actions/apioauthauthorize.php @@ -125,19 +125,12 @@ class ApiOauthAuthorizeAction extends Action parent::handle($args); if ($_SERVER['REQUEST_METHOD'] == 'POST') { - /* Use a 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; - } $this->handlePost(); } else { - common_debug('ApiOauthAuthorize::handle()'); + // XXX: make better error messages if (empty($this->oauth_token)) { @@ -160,7 +153,7 @@ class ApiOauthAuthorizeAction extends Action function handlePost() { - /* Use a session token for CSRF protection. */ + // check session token for CSRF protection. $token = $this->trimmed('token'); @@ -175,25 +168,66 @@ class ApiOauthAuthorizeAction extends Action return; } - // is the user already logged in? - // 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 you're doing this you + // probably don'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 new req token with the user and the app + + $appUser = new Oauth_application_user(); + + $appUser->profile_id = $user->id; + $appUser->application_id = $this->app->id; + $appUser->access_type = $this->app->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 if (!empty($this->callback)) { + + // XXX: Need better way to build this redirect url. + $target_url = $this->callback . '?oauth_token=' . $this->oauth_token; common_redirect($target_url, 303); } @@ -202,7 +236,7 @@ class ApiOauthAuthorizeAction extends Action $this->elementStart('p'); - // XXX: Do verifier code? + // XXX: Do OAuth 1.0a verifier code? $this->raw(sprintf(_("The request token %s has been authorized. " . 'Please exchange it for an access token.'), @@ -233,7 +267,9 @@ class ApiOauthAuthorizeAction extends Action function showScripts() { parent::showScripts(); - // $this->autofocus('nickname'); + if (!common_logged_in()) { + $this->autofocus('nickname'); + } } /** diff --git a/classes/Oauth_application_user.php b/classes/Oauth_application_user.php index 9e45ece25f..e4c018f219 100644 --- a/classes/Oauth_application_user.php +++ b/classes/Oauth_application_user.php @@ -13,12 +13,34 @@ class Oauth_application_user extends Memcached_DataObject public $profile_id; // int(4) primary_key not_null public $application_id; // int(4) primary_key not_null public $access_type; // tinyint(1) + public $token; // varchar(255) + public $secret; // varchar(255) + public $verifier; // varchar(255) public $created; // datetime not_null + public $modified; // timestamp not_null default_CURRENT_TIMESTAMP /* Static get */ function staticGet($k,$v=NULL) { - return Memcached_DataObject::staticGet('Oauth_application_user',$k,$v); + return Memcached_DataObject::staticGet('Oauth_application_user',$k,$v); } /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE + + static function getByKeys($user, $app) + { + if (empty($user) || empty($app)) { + return null; + } + + $oau = new Oauth_application_user(); + + $oau->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/statusnet.ini b/classes/statusnet.ini index 0cbe60a5a5..43f6c4466d 100644 --- a/classes/statusnet.ini +++ b/classes/statusnet.ini @@ -372,7 +372,11 @@ id = N profile_id = 129 application_id = 129 access_type = 17 +token = 2 +secret = 2 +verifier = 2 created = 142 +modified = 384 [oauth_application_user__keys] profile_id = K diff --git a/db/statusnet.sql b/db/statusnet.sql index a2740b60cc..eb47060674 100644 --- a/db/statusnet.sql +++ b/db/statusnet.sql @@ -229,8 +229,11 @@ 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 'authorization token', + secret varchar(255) comment 'token secret', + verifier varchar(255) not null comment 'verification code', 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; From a0b84387737b016168eb3b9a1c6dee1980724f66 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Mon, 11 Jan 2010 01:11:50 -0800 Subject: [PATCH 050/157] Exchanging authorized request tokens for access tokens working --- actions/apioauthaccesstoken.php | 60 ++++++++++++++++++++++++++++- classes/Oauth_application.php | 14 +++++++ lib/apioauthstore.php | 68 +++++++++++++++++++++++---------- 3 files changed, 119 insertions(+), 23 deletions(-) diff --git a/actions/apioauthaccesstoken.php b/actions/apioauthaccesstoken.php index db82f656ad..9b99724d0c 100644 --- a/actions/apioauthaccesstoken.php +++ b/actions/apioauthaccesstoken.php @@ -31,7 +31,7 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR . '/lib/api.php'; +require_once INSTALLDIR . '/lib/apioauthstore.php'; /** * Exchange an authorized OAuth request token for an access token @@ -43,7 +43,63 @@ require_once INSTALLDIR . '/lib/api.php'; * @link http://status.net/ */ -class ApiOauthAccessTokenAction extends ApiAction +class ApiOauthAccessTokenAction extends Action { + /** + * Is read only? + * + * @return boolean false + */ + function isReadOnly() + { + return false; + } + + /** + * 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_WARN, '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.'); + $this->outputError("Badness."); + return; + } + + 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/classes/Oauth_application.php b/classes/Oauth_application.php index d4de6d82e1..5df8b9459c 100644 --- a/classes/Oauth_application.php +++ b/classes/Oauth_application.php @@ -88,4 +88,18 @@ class Oauth_application extends Memcached_DataObject 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; + } + } diff --git a/lib/apioauthstore.php b/lib/apioauthstore.php index a92a4d6e49..290ce89730 100644 --- a/lib/apioauthstore.php +++ b/lib/apioauthstore.php @@ -39,19 +39,45 @@ class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore function new_access_token($token, $consumer) { common_debug('new_access_token("'.$token->key.'","'.$consumer->key.'")', __FILE__); - $rt = new Token(); + + $rt = new Token(); $rt->consumer_key = $consumer->key; $rt->tok = $token->key; $rt->type = 0; // request - if ($rt->find(true) && $rt->state == 1) { // authorized + + $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__); - $at = new Token(); + + // 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."); + 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()) { + + if (!$at->insert()) { $e = $at->_lastError; common_debug('access token "'.$at->tok.'" not inserted: "'.$e->message.'"', __FILE__); return null; @@ -64,23 +90,23 @@ class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore return null; } common_debug('request token "'.$rt->tok.'" updated', __FILE__); - // Update subscription - // XXX: mixing levels here - $sub = Subscription::staticGet('token', $rt->tok); - if (!$sub) { - return null; - } - common_debug('subscription for request token found', __FILE__); - $orig_sub = clone($sub); - $sub->token = $at->tok; - $sub->secret = $at->secret; - if (!$sub->update($orig_sub)) { - return null; - } else { - common_debug('subscription updated to use access token', __FILE__); - return new OAuthToken($at->tok, $at->secret); - } - } + + // update the token from req to access for the user + + $orig = clone($appUser); + $appUser->token = $at->tok; + $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; } From c2337ab47c183ab9758f5db8632ac9cd480a282a Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Mon, 11 Jan 2010 12:17:36 -0800 Subject: [PATCH 051/157] Decided we didn't need to keep the token secret in the Oauth_application_user record --- classes/Oauth_application_user.php | 1 - classes/statusnet.ini | 1 - db/statusnet.sql | 3 +-- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/classes/Oauth_application_user.php b/classes/Oauth_application_user.php index e4c018f219..a05371f563 100644 --- a/classes/Oauth_application_user.php +++ b/classes/Oauth_application_user.php @@ -14,7 +14,6 @@ class Oauth_application_user extends Memcached_DataObject public $application_id; // int(4) primary_key not_null public $access_type; // tinyint(1) public $token; // varchar(255) - public $secret; // varchar(255) public $verifier; // 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 43f6c4466d..b358bbf60a 100644 --- a/classes/statusnet.ini +++ b/classes/statusnet.ini @@ -373,7 +373,6 @@ profile_id = 129 application_id = 129 access_type = 17 token = 2 -secret = 2 verifier = 2 created = 142 modified = 384 diff --git a/db/statusnet.sql b/db/statusnet.sql index eb47060674..c3160bcf87 100644 --- a/db/statusnet.sql +++ b/db/statusnet.sql @@ -229,8 +229,7 @@ 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 'authorization token', - secret varchar(255) comment 'token secret', + token varchar(255) comment 'request or access token', verifier varchar(255) not null comment 'verification code', created datetime not null comment 'date this record was created', modified timestamp comment 'date this record was modified', From 11bd98025c1e41921359b634461772d22a1c059f Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Mon, 11 Jan 2010 12:52:56 -0800 Subject: [PATCH 052/157] Issue a warning when someone tries to exchange an unauthorized or otherwise bad req token for an access token. --- actions/apioauthaccesstoken.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/actions/apioauthaccesstoken.php b/actions/apioauthaccesstoken.php index 9b99724d0c..67359d765d 100644 --- a/actions/apioauthaccesstoken.php +++ b/actions/apioauthaccesstoken.php @@ -88,11 +88,10 @@ class ApiOauthAccessTokenAction extends Action if (empty($atok)) { common_debug('couldn\'t get access token.'); - $this->outputError("Badness."); - return; + print "Token exchange failed. Has the request token been authorized?\n"; + } else { + print $atok; } - - print $atok; } function outputError($msg) From c78937537ed17eabb665ec6e4344b564799cbccc Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Mon, 11 Jan 2010 14:11:43 -0800 Subject: [PATCH 053/157] Better detial in connected OAuth applications list --- actions/oauthconnectionssettings.php | 32 +++++++++--- classes/Profile.php | 9 ++-- lib/applicationlist.php | 74 +++++++++++++++++++--------- 3 files changed, 82 insertions(+), 33 deletions(-) diff --git a/actions/oauthconnectionssettings.php b/actions/oauthconnectionssettings.php index e4b5af1586..56e7b02fba 100644 --- a/actions/oauthconnectionssettings.php +++ b/actions/oauthconnectionssettings.php @@ -48,6 +48,16 @@ require_once INSTALLDIR . '/lib/applicationlist.php'; class OauthconnectionssettingsAction extends ConnectSettingsAction { + + var $page = null; + + function prepare($args) + { + parent::prepare($args); + $this->page = ($this->arg('page')) ? ($this->arg('page') + 0) : 1; + return true; + } + /** * Title of the page * @@ -59,6 +69,11 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction return _('Connected Applications'); } + function isReadOnly($args) + { + return true; + } + /** * Instructions for use * @@ -86,13 +101,16 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction $application = $profile->getApplications($offset, $limit); - if ($application) { - $al = new ApplicationList($application, $this->user, $this); - $cnt = $al->show(); - if (0 == $cnt) { - $this->showEmptyListMessage(); - } - } + $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', diff --git a/classes/Profile.php b/classes/Profile.php index 687215b11b..fef2a21710 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -355,10 +355,11 @@ class Profile extends Memcached_DataObject function getApplications($offset = 0, $limit = null) { $qry = - 'SELECT oauth_application_user.* ' . - 'FROM oauth_application_user ' . - 'WHERE profile_id = %d ' . - 'ORDER BY created DESC '; + 'SELECT a.* ' . + 'FROM oauth_application_user u, oauth_application a ' . + 'WHERE u.profile_id = %d ' . + 'AND a.id = u.application_id ' . + 'ORDER BY u.created DESC '; if ($offset > 0) { if (common_config('db','type') == 'pgsql') { diff --git a/lib/applicationlist.php b/lib/applicationlist.php index 5392ddab8c..e305437f4c 100644 --- a/lib/applicationlist.php +++ b/lib/applicationlist.php @@ -57,13 +57,14 @@ class ApplicationList extends Widget /** Action object using us. */ var $action = null; - function __construct($application, $owner=null, $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() @@ -97,36 +98,65 @@ class ApplicationList extends Widget $this->out->element('img', array('src' => $this->application->icon)); } - $this->out->elementStart('a', - array('href' => common_local_url( - 'showapplication', - array( - 'nickname' => $user->nickname, - 'id' => $this->application->id - ) - ), - 'class' => 'url') - ); + if (!$this->connections) { + + $this->out->elementStart('a', + array('href' => + common_local_url('showapplication', + array('nickname' => $user->nickname, + 'id' => $this->application->id)), + 'class' => 'url') + ); $this->out->raw($this->application->name); $this->out->elementEnd('a'); - - $this->out->raw(' by '); - + } else { $this->out->elementStart('a', - array( - 'href' => $this->application->homepage, - 'class' => 'url' - ) - ); - $this->out->raw($this->application->organization); - $this->out->elementEnd('a'); + array('href' => $this->application->source_url, + 'class' => 'url')); - $this->out->elementStart('p', 'note'); + $this->out->raw($this->application->name); + $this->out->elementEnd('a'); + } + + $this->out->raw(' by '); + + $this->out->elementStart('a', + array( + 'href' => $this->application->homepage, + 'class' => 'url' + ) + ); + $this->out->raw($this->application->organization); + $this->out->elementEnd('a'); + + $this->out->elementStart('p', 'note'); $this->out->raw($this->application->description); $this->out->elementEnd('p'); + $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_exact_date($appUser->modified) . + " $access for access."; + + $this->out->raw($txt); $this->out->elementEnd('li'); + + // XXX: Add revoke access button + } } /* Override this in subclasses. */ From 4fb9b43aa2c174e0b5ac3e260cafd2b5fcfa9368 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Mon, 11 Jan 2010 22:46:35 +0000 Subject: [PATCH 054/157] Updated markup for application edit form submits --- lib/applicationeditform.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/applicationeditform.php b/lib/applicationeditform.php index 4d3bb06e75..f8fcb3e3f5 100644 --- a/lib/applicationeditform.php +++ b/lib/applicationeditform.php @@ -333,7 +333,9 @@ class ApplicationEditForm extends Form function formActions() { - $this->out->submit('save', _('Save')); - $this->out->submit('cancel', _('Cancel')); + $this->out->submit('cancel', _('Cancel'), 'submit form_action-primary', + 'cancel', _('Cancel')); + $this->out->submit('save', _('Save'), 'submit form_action-secondary', + 'save', _('Save')); } } From 61f71a4a597bb2eab2b56a80a71e867c1539739d Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Mon, 11 Jan 2010 22:54:46 +0000 Subject: [PATCH 055/157] Updated markup for application registration and view links --- actions/apps.php | 7 +++++-- actions/showapplication.php | 8 ++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/actions/apps.php b/actions/apps.php index e6500599f7..7c7b24570f 100644 --- a/actions/apps.php +++ b/actions/apps.php @@ -114,13 +114,16 @@ class AppsAction extends SettingsAction } } + $this->elementStart('p', array('id' => 'application_register')); $this->element('a', array('href' => common_local_url( 'newapplication', array('nickname' => $user->nickname) - ) + ), + 'class' => 'more' ), - 'Register a new application »'); + 'Register a new application'); + $this->elementEnd('p'); $this->pagination( $this->page > 1, diff --git a/actions/showapplication.php b/actions/showapplication.php index 5156fa6f0c..3e191148ae 100644 --- a/actions/showapplication.php +++ b/actions/showapplication.php @@ -249,16 +249,16 @@ class ShowApplicationAction extends OwnerDesignAction $this->elementEnd('div'); - $this->elementStart('div', 'entity-list-apps'); + $this->elementStart('p', array('id' => 'application_action')); $this->element('a', array( 'href' => common_local_url( 'apps', - array('nickname' => $this->owner->nickname) - ) + array('nickname' => $this->owner->nickname)), + 'class' => 'more' ), 'View your applications'); - $this->elementEnd('div'); + $this->elementEnd('p'); } function resetKey() From c8a4d0d6c2cafe1d3d4285c4b634f1dc52e91d8b Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Mon, 11 Jan 2010 23:51:12 +0000 Subject: [PATCH 056/157] Updated markup for application details --- actions/showapplication.php | 70 +++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/actions/showapplication.php b/actions/showapplication.php index 3e191148ae..33bc51f931 100644 --- a/actions/showapplication.php +++ b/actions/showapplication.php @@ -152,7 +152,8 @@ class ShowApplicationAction extends OwnerDesignAction $cur = common_current_user(); $this->elementStart('div', 'entity_actions'); - + $this->elementStart('ul'); + $this->elementStart('li'); $this->element('a', array('href' => common_local_url( @@ -163,7 +164,9 @@ class ShowApplicationAction extends OwnerDesignAction ) ) ), 'Edit application'); + $this->elementEnd('li'); + $this->elementStart('li'); $this->elementStart('form', array( 'id' => 'forma_reset_key', 'class' => 'form_reset_key', @@ -177,32 +180,39 @@ class ShowApplicationAction extends OwnerDesignAction $this->submit('reset', _('Reset Consumer key/secret')); $this->elementEnd('fieldset'); $this->elementEnd('form'); - + $this->elementEnd('li'); + $this->elementEnd('ul'); $this->elementEnd('div'); $consumer = $this->application->getConsumer(); - $this->elementStart('div', 'entity-application'); + $this->elementStart('div', 'entity_application'); + $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)); + } + $this->elementEnd('dd'); + $this->elementEnd('dl'); - $this->elementStart('ul', 'entity_application_details'); - - $this->elementStart('li', 'entity_application-icon'); - - if (!empty($this->application->icon)) { - $this->element('img', array('src' => $this->application->icon)); - } - - $this->elementEnd('li'); - - $this->elementStart('li', 'entity_application_name'); - $this->element('span', array('class' => 'big'), $this->application->name); + $this->elementStart('dl', 'entity_fn'); + $this->element('dt', null, _('Name')); + $this->elementStart('dd'); + $this->element('span', null, $this->application->name); $this->raw(sprintf(_(' by %1$s'), $this->application->organization)); - $this->elementEnd('li'); + $this->elementEnd('dd'); + $this->elementEnd('dl'); - $this->element('li', 'entity_application_description', $this->application->description); - - $this->elementStart('li', 'entity_application_statistics'); + $this->elementStart('dl', 'entity_note'); + $this->element('dt', null, _('Description')); + $this->element('dd', null, $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); @@ -214,39 +224,39 @@ class ShowApplicationAction extends OwnerDesignAction $defaultAccess, $userCnt )); + $this->elementEnd('dd'); + $this->elementEnd('dl'); + $this->elementEnd('div'); - $this->elementEnd('li'); - - $this->elementEnd('ul'); - + $this->elementStart('div', array('id' => 'entity_data')); + $this->element('h2', null, _('Application info')); $this->elementStart('dl', 'entity_consumer_key'); $this->element('dt', null, _('Consumer key')); - $this->element('dd', 'label', $consumer->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', 'label', $consumer->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', 'label', common_local_url('apioauthrequesttoken')); + $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', 'label', common_local_url('apioauthaccesstoken')); + $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', 'label', common_local_url('apioauthauthorize')); + $this->element('dd', null, common_local_url('apioauthauthorize')); $this->elementEnd('dl'); $this->element('p', 'oauth-signature-note', - '*We support hmac-sha1 signatures. We do not support the plaintext signature method.'); - + '* We support hmac-sha1 signatures. We do not support the plaintext signature method.'); $this->elementEnd('div'); $this->elementStart('p', array('id' => 'application_action')); From 0b90f7645e5bd54c99c6fb7c1fe105e183b9e8c1 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Tue, 12 Jan 2010 01:01:45 +0000 Subject: [PATCH 057/157] Updated class for application list --- lib/applicationlist.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/applicationlist.php b/lib/applicationlist.php index e305437f4c..23c727bd61 100644 --- a/lib/applicationlist.php +++ b/lib/applicationlist.php @@ -69,7 +69,7 @@ class ApplicationList extends Widget function show() { - $this->out->elementStart('ul', 'applications xoxo'); + $this->out->elementStart('ul', array('id' => 'applications')); $cnt = 0; From c2ffd6612887675e55a9d9398517e23ee95c9117 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Tue, 12 Jan 2010 01:02:25 +0000 Subject: [PATCH 058/157] Updated markup for application details page. Similar to user/group profile page. --- actions/showapplication.php | 90 +++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 43 deletions(-) diff --git a/actions/showapplication.php b/actions/showapplication.php index 33bc51f931..db28395c29 100644 --- a/actions/showapplication.php +++ b/actions/showapplication.php @@ -151,63 +151,33 @@ class ShowApplicationAction extends OwnerDesignAction $cur = common_current_user(); - $this->elementStart('div', 'entity_actions'); - $this->elementStart('ul'); - $this->elementStart('li'); - $this->element('a', - array('href' => - common_local_url( - 'editapplication', - array( - 'nickname' => $this->owner->nickname, - 'id' => $this->application->id - ) - ) - ), 'Edit application'); - $this->elementEnd('li'); - - $this->elementStart('li'); - $this->elementStart('form', array( - 'id' => 'forma_reset_key', - 'class' => 'form_reset_key', - 'method' => 'POST', - 'action' => common_local_url('showapplication', - array('nickname' => $cur->nickname, - 'id' => $this->application->id)))); - - $this->elementStart('fieldset'); - $this->hidden('token', common_session_token()); - $this->submit('reset', _('Reset Consumer key/secret')); - $this->elementEnd('fieldset'); - $this->elementEnd('form'); - $this->elementEnd('li'); - $this->elementEnd('ul'); - $this->elementEnd('div'); - $consumer = $this->application->getConsumer(); - $this->elementStart('div', 'entity_application'); + $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)); + $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('span', null, $this->application->name); - $this->raw(sprintf(_(' by %1$s'), $this->application->organization)); - $this->elementEnd('dd'); + $this->element('dd', 'fn', $this->application->name); + $this->elementEnd('dl'); + + $this->elementStart('dl', 'entity_org'); + $this->element('dt', null, _('Organization')); + $this->element('dd', 'org', $this->application->organization); $this->elementEnd('dl'); $this->elementStart('dl', 'entity_note'); $this->element('dt', null, _('Description')); - $this->element('dd', null, $this->application->description); + $this->element('dd', 'note', $this->application->description); $this->elementEnd('dl'); $this->elementStart('dl', 'entity_statistics'); @@ -228,7 +198,41 @@ class ShowApplicationAction extends OwnerDesignAction $this->elementEnd('dl'); $this->elementEnd('div'); - $this->elementStart('div', array('id' => 'entity_data')); + $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( + 'nickname' => $this->owner->nickname, + '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('nickname' => $cur->nickname, + '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')); @@ -255,8 +259,8 @@ class ShowApplicationAction extends OwnerDesignAction $this->element('dd', null, common_local_url('apioauthauthorize')); $this->elementEnd('dl'); - $this->element('p', 'oauth-signature-note', - '* We support hmac-sha1 signatures. We do not support the plaintext signature method.'); + $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')); From ba0c82b391ad3ec71bb7efe60b03f4f95f9ecaf5 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Tue, 12 Jan 2010 01:13:36 +0000 Subject: [PATCH 059/157] Added anchors to application source and homepage --- actions/showapplication.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/actions/showapplication.php b/actions/showapplication.php index db28395c29..f2ff8b9002 100644 --- a/actions/showapplication.php +++ b/actions/showapplication.php @@ -167,12 +167,20 @@ class ShowApplicationAction extends OwnerDesignAction $this->elementStart('dl', 'entity_fn'); $this->element('dt', null, _('Name')); - $this->element('dd', 'fn', $this->application->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->element('dd', 'org', $this->application->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'); From d998c4b1b89492e02d56ba8a395387078472f163 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Tue, 12 Jan 2010 01:29:09 +0000 Subject: [PATCH 060/157] Fixed tabbing --- lib/applicationlist.php | 87 ++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 49 deletions(-) diff --git a/lib/applicationlist.php b/lib/applicationlist.php index 23c727bd61..b404767ba0 100644 --- a/lib/applicationlist.php +++ b/lib/applicationlist.php @@ -69,7 +69,7 @@ class ApplicationList extends Widget function show() { - $this->out->elementStart('ul', array('id' => 'applications')); + $this->out->elementStart('ul', 'applications'); $cnt = 0; @@ -94,69 +94,58 @@ class ApplicationList extends Widget $this->out->elementStart('li', array('class' => 'application', 'id' => 'oauthclient-' . $this->application->id)); - if (!empty($this->application->icon)) { - $this->out->element('img', array('src' => $this->application->icon)); - } + if (!empty($this->application->icon)) { + $this->out->element('img', array('src' => $this->application->icon)); + } - if (!$this->connections) { + if (!$this->connections) { + $this->out->elementStart('a', + array('href' => common_local_url('showapplication', + array('nickname' => $user->nickname, + 'id' => $this->application->id)), + 'class' => 'url')); - $this->out->elementStart('a', - array('href' => - common_local_url('showapplication', - array('nickname' => $user->nickname, - 'id' => $this->application->id)), - 'class' => 'url') - ); + $this->out->raw($this->application->name); + $this->out->elementEnd('a'); + } else { + $this->out->elementStart('a', array('href' => $this->application->source_url, + 'class' => 'url')); - $this->out->raw($this->application->name); - $this->out->elementEnd('a'); - } else { - $this->out->elementStart('a', - array('href' => $this->application->source_url, - 'class' => 'url')); + $this->out->raw($this->application->name); + $this->out->elementEnd('a'); + } - $this->out->raw($this->application->name); - $this->out->elementEnd('a'); - } + $this->out->raw(' by '); - $this->out->raw(' by '); + $this->out->elementStart('a', array('href' => $this->application->homepage, + 'class' => 'url')); + $this->out->raw($this->application->organization); + $this->out->elementEnd('a'); - $this->out->elementStart('a', - array( - 'href' => $this->application->homepage, - 'class' => 'url' - ) - ); - $this->out->raw($this->application->organization); - $this->out->elementEnd('a'); - - $this->out->elementStart('p', 'note'); + $this->out->elementStart('p', 'note'); $this->out->raw($this->application->description); $this->out->elementEnd('p'); - $this->out->elementEnd('li'); + if ($this->connections) { + $appUser = Oauth_application_user::getByKeys($this->owner, $this->application); - if ($this->connections) { + if (empty($appUser)) { + common_debug("empty appUser!"); + } - $appUser = Oauth_application_user::getByKeys($this->owner, $this->application); + $this->out->elementStart('li'); - if (empty($appUser)) { - common_debug("empty appUser!"); - } + $access = ($this->application->access_type & Oauth_application::$writeAccess) + ? 'read-write' : 'read-only'; - $this->out->elementStart('li'); + $txt = 'Approved ' . common_exact_date($appUser->modified) . + " $access for access."; - $access = ($this->application->access_type & Oauth_application::$writeAccess) - ? 'read-write' : 'read-only'; + $this->out->raw($txt); + $this->out->elementEnd('li'); - $txt = 'Approved ' . common_exact_date($appUser->modified) . - " $access for access."; - - $this->out->raw($txt); - $this->out->elementEnd('li'); - - // XXX: Add revoke access button - } + // XXX: Add revoke access button + } } /* Override this in subclasses. */ From 8e91e053923f395d4dc0ad7f3f328953c9b17145 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Mon, 11 Jan 2010 17:30:56 -0800 Subject: [PATCH 061/157] Make API auth handle OAuth requests w/access tokens --- lib/apiauth.php | 114 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 2 deletions(-) diff --git a/lib/apiauth.php b/lib/apiauth.php index 7102764cba..3229ab19fd 100644 --- a/lib/apiauth.php +++ b/lib/apiauth.php @@ -28,7 +28,7 @@ * @author Evan Prodromou * @author mEDI * @author Sarven Capadisli - * @author Zach Copley + * @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/ @@ -39,6 +39,7 @@ if (!defined('STATUSNET')) { } require_once INSTALLDIR . '/lib/api.php'; +require_once INSTALLDIR . '/lib/apioauthstore.php'; /** * Actions extending this class will require auth @@ -52,6 +53,8 @@ require_once INSTALLDIR . '/lib/api.php'; class ApiAuthAction extends ApiAction { + var $access_token; + var $oauth_access_type; /** * Take arguments for running, and output basic auth header if needed @@ -67,12 +70,119 @@ class ApiAuthAction extends ApiAction parent::prepare($args); if ($this->requiresAuth()) { - $this->checkBasicAuthUser(); + + $this->consumer_key = $this->arg('oauth_consumer_key'); + $this->access_token = $this->arg('oauth_token'); + + if (!empty($this->access_token)) { + $this->checkOAuthRequest(); + } else { + $this->checkBasicAuthUser(); + } } return true; } + function checkOAuthRequest() + { + common_debug("We have an OAuth request."); + + $datastore = new ApiStatusNetOAuthDataStore(); + $server = new OAuthServer($datastore); + $hmac_method = new OAuthSignatureMethod_HMAC_SHA1(); + + $server->add_signature_method($hmac_method); + + $this->cleanRequest(); + + try { + + $req = OAuthRequest::from_request(); + $server->verify_request($req); + + common_debug("Good OAuth request!"); + + $app = Oauth_application::getByConsumerKey($this->consumer_key); + + if (empty($app)) { + + // this should really not happen + common_log(LOG_WARN, + "Couldn't find the OAuth app for consumer key: $this->consumer_key"); + + throw new OAuthException('No application for that consumer key.'); + } + + $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)) { + + // read or read-write + $this->oauth_access_type = $appUser->access_type; + + // If access_type == 0 we have either a request token + // or a bad / revoked access token + + if ($this->oauth_access_type != 0) { + + $this->auth_user = User::staticGet('id', $appUser->profile_id); + + $msg = "API OAuth authentication for user '%s' (id: %d) on behalf of " . + "application '%s' (id: %d)."; + + common_log(LOG_INFO, sprintf($msg, + $this->auth_user->nickname, + $this->auth_user->id, + $app->name, + $app->id)); + return true; + } 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_WARN, 'API OAuthException - ' . $e->getMessage()); + common_debug(var_export($req, true)); + $this->showOAuthError($e->getMessage()); + exit(); + } + } + + function showOAuthError($msg) + { + header('HTTP/1.1 401 Unauthorized'); + header('Content-Type: text/html; charset=utf-8'); + print $msg . "\n"; + } + + 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']); + } + /** * Does this API resource require authentication? * From 40c6d09c9f3a3a1167015782ac4972681ce98772 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Tue, 12 Jan 2010 01:36:08 +0000 Subject: [PATCH 062/157] Added missing end tag --- lib/applicationlist.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/applicationlist.php b/lib/applicationlist.php index b404767ba0..b1dcc39a96 100644 --- a/lib/applicationlist.php +++ b/lib/applicationlist.php @@ -110,7 +110,6 @@ class ApplicationList extends Widget } else { $this->out->elementStart('a', array('href' => $this->application->source_url, 'class' => 'url')); - $this->out->raw($this->application->name); $this->out->elementEnd('a'); } @@ -125,6 +124,7 @@ class ApplicationList extends Widget $this->out->elementStart('p', 'note'); $this->out->raw($this->application->description); $this->out->elementEnd('p'); + $this->out->elementEnd('li'); if ($this->connections) { $appUser = Oauth_application_user::getByKeys($this->owner, $this->application); From 34cc03c61792792e9b23e9a28e7730268b22d3d1 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Tue, 12 Jan 2010 01:41:38 +0000 Subject: [PATCH 063/157] Moved application image inside the anchor --- lib/applicationlist.php | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/applicationlist.php b/lib/applicationlist.php index b1dcc39a96..8961da4355 100644 --- a/lib/applicationlist.php +++ b/lib/applicationlist.php @@ -94,10 +94,6 @@ class ApplicationList extends Widget $this->out->elementStart('li', array('class' => 'application', 'id' => 'oauthclient-' . $this->application->id)); - if (!empty($this->application->icon)) { - $this->out->element('img', array('src' => $this->application->icon)); - } - if (!$this->connections) { $this->out->elementStart('a', array('href' => common_local_url('showapplication', @@ -105,15 +101,18 @@ class ApplicationList extends Widget 'id' => $this->application->id)), 'class' => 'url')); - $this->out->raw($this->application->name); - $this->out->elementEnd('a'); } else { $this->out->elementStart('a', array('href' => $this->application->source_url, 'class' => 'url')); - $this->out->raw($this->application->name); - $this->out->elementEnd('a'); } + if (!empty($this->application->icon)) { + $this->out->element('img', array('src' => $this->application->icon)); + } + + $this->out->raw($this->application->name); + $this->out->elementEnd('a'); + $this->out->raw(' by '); $this->out->elementStart('a', array('href' => $this->application->homepage, From 276c4a2a231cce0b449e97d2bdba0a822b898e08 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Tue, 12 Jan 2010 01:44:15 +0000 Subject: [PATCH 064/157] Added vcard and photo classes --- lib/applicationlist.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/applicationlist.php b/lib/applicationlist.php index 8961da4355..6ca210537a 100644 --- a/lib/applicationlist.php +++ b/lib/applicationlist.php @@ -94,6 +94,7 @@ class ApplicationList extends Widget $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', @@ -107,11 +108,13 @@ class ApplicationList extends Widget } if (!empty($this->application->icon)) { - $this->out->element('img', array('src' => $this->application->icon)); + $this->out->element('img', array('src' => $this->application->icon, + 'class' => 'photo')); } $this->out->raw($this->application->name); $this->out->elementEnd('a'); + $this->out->elementEnd('span'); $this->out->raw(' by '); From a009052036250d33b38329dd0363dd42652e89da Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Tue, 12 Jan 2010 01:52:59 +0000 Subject: [PATCH 065/157] A little minimization --- lib/applicationlist.php | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/applicationlist.php b/lib/applicationlist.php index 6ca210537a..15c2d588a3 100644 --- a/lib/applicationlist.php +++ b/lib/applicationlist.php @@ -109,23 +109,20 @@ class ApplicationList extends Widget if (!empty($this->application->icon)) { $this->out->element('img', array('src' => $this->application->icon, - 'class' => 'photo')); + 'class' => 'photo avatar')); } - $this->out->raw($this->application->name); + $this->out->element('span', 'fn', $this->application->name); $this->out->elementEnd('a'); $this->out->elementEnd('span'); $this->out->raw(' by '); - $this->out->elementStart('a', array('href' => $this->application->homepage, - 'class' => 'url')); - $this->out->raw($this->application->organization); - $this->out->elementEnd('a'); + $this->out->element('a', array('href' => $this->application->homepage, + 'class' => 'url'), + $this->application->organization); - $this->out->elementStart('p', 'note'); - $this->out->raw($this->application->description); - $this->out->elementEnd('p'); + $this->out->element('p', 'note', $this->application->description); $this->out->elementEnd('li'); if ($this->connections) { From 8d02a897dc6269b44d529938b2c6298e19a2ec59 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Tue, 12 Jan 2010 02:50:54 +0000 Subject: [PATCH 066/157] Updated markup for application edit form; image, radios --- lib/applicationeditform.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/applicationeditform.php b/lib/applicationeditform.php index f8fcb3e3f5..e9ab467804 100644 --- a/lib/applicationeditform.php +++ b/lib/applicationeditform.php @@ -175,7 +175,7 @@ class ApplicationEditForm extends Form $this->out->elementStart('ul', 'form_data'); - $this->out->elementStart('li'); + $this->out->elementStart('li', array('id' => 'application_icon')); if (!empty($icon)) { $this->out->element('img', array('src' => $icon)); @@ -193,7 +193,7 @@ class ApplicationEditForm extends Form 'value' => ImageFile::maxFileSizeInt())); $this->out->elementEnd('li'); - $this->out->elementStart('li'); + $this->out->elementStart('li'); $this->out->hidden('application_id', $id); @@ -241,7 +241,7 @@ class ApplicationEditForm extends Form _('URL to redirect to after authentication')); $this->out->elementEnd('li'); - $this->out->elementStart('li'); + $this->out->elementStart('li', array('id' => 'application_types')); $attrs = array('name' => 'app_type', 'type' => 'radio', @@ -280,7 +280,7 @@ class ApplicationEditForm extends Form $this->out->element('p', 'form_guide', _('Type of application, browser or desktop')); $this->out->elementEnd('li'); - $this->out->elementStart('li'); + $this->out->elementStart('li', array('id' => 'default_access_types')); $attrs = array('name' => 'default_access_type', 'type' => 'radio', From 7694955cd654becf8f03dc4eca271d8188141596 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 13 Jan 2010 01:16:42 +0000 Subject: [PATCH 067/157] Callback URL can be null --- db/statusnet.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/statusnet.sql b/db/statusnet.sql index c3160bcf87..b1867e7f04 100644 --- a/db/statusnet.sql +++ b/db/statusnet.sql @@ -218,7 +218,7 @@ create table oauth_application ( 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) not null comment 'url to redirect to after authentication', + 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', From adfca0180847571b9474db76a0c4daa407acf22b Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 13 Jan 2010 01:22:37 +0000 Subject: [PATCH 068/157] Can now edit/change application icon --- actions/editapplication.php | 124 ++++++++++++++++------------------ actions/newapplication.php | 81 +++++++--------------- classes/Oauth_application.php | 53 ++++++++++++--- 3 files changed, 126 insertions(+), 132 deletions(-) diff --git a/actions/editapplication.php b/actions/editapplication.php index 6b8dd501c9..a0ed3117a7 100644 --- a/actions/editapplication.php +++ b/actions/editapplication.php @@ -93,47 +93,47 @@ class EditApplicationAction extends OwnerDesignAction parent::handle($args); if ($_SERVER['REQUEST_METHOD'] == 'POST') { - $this->handlePost($args); - } else { - $this->showForm(); - } + $this->handlePost($args); + } else { + $this->showForm(); + } } function handlePost($args) { - // Workaround for PHP returning empty $_POST and $_FILES when POST + // 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.'); + '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; - } + // 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(); + $cur = common_current_user(); - if ($this->arg('cancel')) { - common_redirect(common_local_url('showapplication', - array( - 'nickname' => $cur->nickname, - 'id' => $this->app->id) - ), 303); - } elseif ($this->arg('save')) { - $this->trySave(); - } else { - $this->clientError(_('Unexpected form submission.')); - } + if ($this->arg('cancel')) { + common_redirect(common_local_url('showapplication', + array( + 'nickname' => $cur->nickname, + 'id' => $this->app->id) + ), 303); + } elseif ($this->arg('save')) { + $this->trySave(); + } else { + $this->clientError(_('Unexpected form submission.')); + } } function showForm($msg=null) @@ -170,8 +170,8 @@ class EditApplicationAction extends OwnerDesignAction $access_type = $this->arg('default_access_type'); if (empty($name)) { - $this->showForm(_('Name is required.')); - return; + $this->showForm(_('Name is required.')); + return; } elseif (mb_strlen($name) > 255) { $this->showForm(_('Name is too long (max 255 chars).')); return; @@ -181,20 +181,17 @@ class EditApplicationAction extends OwnerDesignAction } elseif (Oauth_application::descriptionTooLong($description)) { $this->showForm(sprintf( _('Description is too long (max %d chars).'), - Oauth_application::maxDescription())); + 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.')); + } 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; @@ -204,35 +201,30 @@ class EditApplicationAction extends OwnerDesignAction } 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 (empty($callback_url)) { - $this->showForm(_('Callback is required.')); - return; - } elseif (strlen($callback_url) > 0 - && !Validate::uri( - $source_url, - array('allowed_schemes' => array('http', 'https')) - ) - ) - { - $this->showForm(_('Callback URL is not valid.')); - 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)); + assert(!is_null($this->app)); $orig = clone($this->app); @@ -244,9 +236,7 @@ class EditApplicationAction extends OwnerDesignAction $this->app->callback_url = $callback_url; $this->app->type = $type; - $result = $this->app->update($orig); - - common_debug("access_type = $access_type"); + common_debug("access_type = $access_type"); if ($access_type == 'r') { $this->app->access_type = 1; @@ -254,11 +244,15 @@ class EditApplicationAction extends OwnerDesignAction $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('apps', array('nickname' => $cur->nickname)), 303); } diff --git a/actions/newapplication.php b/actions/newapplication.php index a0e61d288c..3d42b657be 100644 --- a/actions/newapplication.php +++ b/actions/newapplication.php @@ -83,7 +83,7 @@ class NewApplicationAction extends OwnerDesignAction parent::handle($args); if ($_SERVER['REQUEST_METHOD'] == 'POST') { - $this->handlePost($args); + $this->handlePost($args); } else { $this->showForm(); } @@ -91,36 +91,36 @@ class NewApplicationAction extends OwnerDesignAction function handlePost($args) { - // Workaround for PHP returning empty $_POST and $_FILES when POST + // 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.'); + '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; - } + // 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(); + $cur = common_current_user(); - if ($this->arg('cancel')) { - common_redirect(common_local_url('apps', - array('nickname' => $cur->nickname)), 303); - } elseif ($this->arg('save')) { - $this->trySave(); - } else { - $this->clientError(_('Unexpected form submission.')); - } + if ($this->arg('cancel')) { + common_redirect(common_local_url('apps', + array('nickname' => $cur->nickname)), 303); + } elseif ($this->arg('save')) { + $this->trySave(); + } else { + $this->clientError(_('Unexpected form submission.')); + } } function showForm($msg=null) @@ -147,7 +147,7 @@ class NewApplicationAction extends OwnerDesignAction function trySave() { - $name = $this->trimmed('name'); + $name = $this->trimmed('name'); $description = $this->trimmed('description'); $source_url = $this->trimmed('source_url'); $organization = $this->trimmed('organization'); @@ -200,8 +200,8 @@ class NewApplicationAction extends OwnerDesignAction { $this->showForm(_('Homepage is not a valid URL.')); return; - } elseif (empty($callback_url)) { - $this->showForm(_('Callback is required.')); + } elseif (mb_strlen($callback_url) > 255) { + $this->showForm(_('Callback is too long.')); return; } elseif (strlen($callback_url) > 0 && !Validate::uri( @@ -266,7 +266,7 @@ class NewApplicationAction extends OwnerDesignAction $app->query('ROLLBACK'); } - $this->uploadLogo($app); + $this->app->uploadLogo(); $app->query('COMMIT'); @@ -275,40 +275,5 @@ class NewApplicationAction extends OwnerDesignAction } - /** - * Handle an image upload - * - * Does all the magic for handling an image upload, and crops the - * image by default. - * - * @return void - */ - - function uploadLogo($app) - { - 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($app->id, - image_type_to_extension($imagefile->type), - null, - 'oauth-app-icon-'.common_timestamp()); - - $filepath = Avatar::path($filename); - - move_uploaded_file($imagefile->filepath, $filepath); - - $app->setOriginal($filename); - } - } - } diff --git a/classes/Oauth_application.php b/classes/Oauth_application.php index 5df8b9459c..a6b5390872 100644 --- a/classes/Oauth_application.php +++ b/classes/Oauth_application.php @@ -27,7 +27,7 @@ class Oauth_application extends Memcached_DataObject /* Static get */ function staticGet($k,$v=NULL) { - return Memcached_DataObject::staticGet('Oauth_application',$k,$v); + return Memcached_DataObject::staticGet('Oauth_application',$k,$v); } /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE @@ -90,16 +90,51 @@ class Oauth_application extends Memcached_DataObject static function getByConsumerKey($key) { - if (empty($key)) { - return null; - } + if (empty($key)) { + return null; + } - $app = new Oauth_application(); - $app->consumer_key = $key; - $app->limit(1); - $result = $app->find(true); + $app = new Oauth_application(); + $app->consumer_key = $key; + $app->limit(1); + $result = $app->find(true); - return empty($result) ? null : $app; + 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); + } } } From 8da5e98cba12c32f0b75a90d1ff0007b73f0fc8d Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 13 Jan 2010 05:06:35 +0000 Subject: [PATCH 069/157] OAuth 1.0 working now --- actions/apioauthaccesstoken.php | 40 ++++----- actions/apioauthauthorize.php | 111 +++++++++++++++---------- actions/apioauthrequesttoken.php | 24 ++++-- lib/apiauth.php | 138 ++++++++++++++----------------- lib/apioauth.php | 122 +++++++++++++++++++++++++++ lib/apioauthstore.php | 69 +++++++++------- lib/router.php | 11 ++- 7 files changed, 330 insertions(+), 185 deletions(-) create mode 100644 lib/apioauth.php diff --git a/actions/apioauthaccesstoken.php b/actions/apioauthaccesstoken.php index 67359d765d..085ef6f0b1 100644 --- a/actions/apioauthaccesstoken.php +++ b/actions/apioauthaccesstoken.php @@ -31,7 +31,7 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR . '/lib/apioauthstore.php'; +require_once INSTALLDIR . '/lib/apioauth.php'; /** * Exchange an authorized OAuth request token for an access token @@ -43,19 +43,9 @@ require_once INSTALLDIR . '/lib/apioauthstore.php'; * @link http://status.net/ */ -class ApiOauthAccessTokenAction extends Action +class ApiOauthAccessTokenAction extends ApiOauthAction { - /** - * Is read only? - * - * @return boolean false - */ - function isReadOnly() - { - return false; - } - /** * Class handler. * @@ -73,7 +63,7 @@ class ApiOauthAccessTokenAction extends Action $server->add_signature_method($hmac_method); - $atok = null; + $atok = null; try { $req = OAuthRequest::from_request(); @@ -81,24 +71,24 @@ class ApiOauthAccessTokenAction extends Action } catch (OAuthException $e) { common_log(LOG_WARN, 'API OAuthException - ' . $e->getMessage()); - common_debug(var_export($req, true)); - $this->outputError($e->getMessage()); - return; + 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; - } + 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"; + 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 index 48d5087efc..cdf9cb7df3 100644 --- a/actions/apioauthauthorize.php +++ b/actions/apioauthauthorize.php @@ -31,7 +31,7 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR . '/lib/apioauthstore.php'; +require_once INSTALLDIR . '/lib/apioauth.php'; /** * Authorize an OAuth request token @@ -43,7 +43,7 @@ require_once INSTALLDIR . '/lib/apioauthstore.php'; * @link http://status.net/ */ -class ApiOauthAuthorizeAction extends Action +class ApiOauthAuthorizeAction extends ApiOauthAction { var $oauth_token; var $callback; @@ -67,7 +67,7 @@ class ApiOauthAuthorizeAction extends Action { parent::prepare($args); - common_debug(var_export($_REQUEST, true)); + common_debug("apioauthauthorize"); $this->nickname = $this->trimmed('nickname'); $this->password = $this->arg('password'); @@ -130,7 +130,7 @@ class ApiOauthAuthorizeAction extends Action } else { - // XXX: make better error messages + // XXX: make better error messages if (empty($this->oauth_token)) { @@ -145,7 +145,8 @@ class ApiOauthAuthorizeAction extends Action return; } - common_debug("Requesting auth for app: $app->name."); + $name = $this->app->name; + common_debug("Requesting auth for app: " . $name); $this->showForm(); } @@ -153,6 +154,8 @@ class ApiOauthAuthorizeAction extends Action function handlePost() { + common_debug("handlePost()"); + // check session token for CSRF protection. $token = $this->trimmed('token'); @@ -170,7 +173,7 @@ class ApiOauthAuthorizeAction extends Action // check creds - $user = null; + $user = null; if (!common_logged_in()) { $user = common_check_user($this->nickname, $this->password); @@ -179,64 +182,86 @@ class ApiOauthAuthorizeAction extends Action return; } } else { - $user = common_current_user(); - } + $user = common_current_user(); + } if ($this->arg('allow')) { - // mark the req token as authorized + // 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 you're doing this you - // probably don't want any old tokens anyway. + // 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); + $appUser = Oauth_application_user::getByKeys($user, $this->app); - if (!empty($appUser)) { - $result = $appUser->delete(); + 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; - } - } + if (!$result) { + common_log_db_error($appUser, 'DELETE', __FILE__); + throw new ServerException(_('DB error deleting OAuth app user.')); + return; + } + } - // associated the new req token with the user and the app + // associated the authorized req token with the user and the app - $appUser = new Oauth_application_user(); + $appUser = new Oauth_application_user(); - $appUser->profile_id = $user->id; - $appUser->application_id = $this->app->id; - $appUser->access_type = $this->app->access_type; - $appUser->token = $this->oauth_token; - $appUser->created = common_sql_now(); + $appUser->profile_id = $user->id; + $appUser->application_id = $this->app->id; - $result = $appUser->insert(); + // 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. - if (!$result) { - common_log_db_error($appUser, 'INSERT', __FILE__); - throw new ServerException(_('DB error inserting OAuth app user.')); - return; - } + $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. + // 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"); - $target_url = $this->callback . '?oauth_token=' . $this->oauth_token; 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? + // XXX: Do OAuth 1.0a verifier code $this->raw(sprintf(_("The request token %s has been authorized. " . 'Please exchange it for an access token.'), @@ -267,9 +292,9 @@ class ApiOauthAuthorizeAction extends Action function showScripts() { parent::showScripts(); - if (!common_logged_in()) { - $this->autofocus('nickname'); - } + if (!common_logged_in()) { + $this->autofocus('nickname'); + } } /** @@ -313,9 +338,9 @@ class ApiOauthAuthorizeAction extends Action function showContent() { $this->elementStart('form', array('method' => 'post', - 'id' => 'form_login', - 'class' => 'form_settings', - 'action' => common_local_url('apioauthauthorize'))); + 'id' => 'form_login', + 'class' => 'form_settings', + 'action' => common_local_url('apioauthauthorize'))); $this->hidden('token', common_session_token()); $this->hidden('oauth_token', $this->oauth_token); diff --git a/actions/apioauthrequesttoken.php b/actions/apioauthrequesttoken.php index 53aca6b96b..467640b9aa 100644 --- a/actions/apioauthrequesttoken.php +++ b/actions/apioauthrequesttoken.php @@ -31,7 +31,7 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR . '/lib/apioauthstore.php'; +require_once INSTALLDIR . '/lib/apioauth.php'; /** * Get an OAuth request token @@ -43,16 +43,28 @@ require_once INSTALLDIR . '/lib/apioauthstore.php'; * @link http://status.net/ */ -class ApiOauthRequestTokenAction extends Action +class ApiOauthRequestTokenAction extends ApiOauthAction { /** - * Is read only? + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag * - * @return boolean false */ - function isReadOnly() + + function prepare($args) { - return false; + parent::prepare($args); + + $this->callback = $this->arg('oauth_callback'); + + if (!empty($this->callback)) { + common_debug("callback: $this->callback"); + } + + return true; } /** diff --git a/lib/apiauth.php b/lib/apiauth.php index 3229ab19fd..431f3ac4fd 100644 --- a/lib/apiauth.php +++ b/lib/apiauth.php @@ -39,7 +39,7 @@ if (!defined('STATUSNET')) { } require_once INSTALLDIR . '/lib/api.php'; -require_once INSTALLDIR . '/lib/apioauthstore.php'; +require_once INSTALLDIR . '/lib/apioauth.php'; /** * Actions extending this class will require auth @@ -71,14 +71,14 @@ class ApiAuthAction extends ApiAction if ($this->requiresAuth()) { - $this->consumer_key = $this->arg('oauth_consumer_key'); - $this->access_token = $this->arg('oauth_token'); + $this->consumer_key = $this->arg('oauth_consumer_key'); + $this->access_token = $this->arg('oauth_token'); - if (!empty($this->access_token)) { - $this->checkOAuthRequest(); - } else { - $this->checkBasicAuthUser(); - } + if (!empty($this->access_token)) { + $this->checkOAuthRequest(); + } else { + $this->checkBasicAuthUser(); + } } return true; @@ -86,101 +86,83 @@ class ApiAuthAction extends ApiAction function checkOAuthRequest() { - common_debug("We have an OAuth request."); + common_debug("We have an OAuth request."); - $datastore = new ApiStatusNetOAuthDataStore(); - $server = new OAuthServer($datastore); - $hmac_method = new OAuthSignatureMethod_HMAC_SHA1(); + $datastore = new ApiStatusNetOAuthDataStore(); + $server = new OAuthServer($datastore); + $hmac_method = new OAuthSignatureMethod_HMAC_SHA1(); - $server->add_signature_method($hmac_method); + $server->add_signature_method($hmac_method); - $this->cleanRequest(); + ApiOauthAction::cleanRequest(); - try { + try { - $req = OAuthRequest::from_request(); - $server->verify_request($req); + $req = OAuthRequest::from_request(); + $server->verify_request($req); - common_debug("Good OAuth request!"); + common_debug("Good OAuth request!"); - $app = Oauth_application::getByConsumerKey($this->consumer_key); + $app = Oauth_application::getByConsumerKey($this->consumer_key); - if (empty($app)) { + if (empty($app)) { - // this should really not happen - common_log(LOG_WARN, - "Couldn't find the OAuth app for consumer key: $this->consumer_key"); + // this should really not happen + common_log(LOG_WARN, + "Couldn't find the OAuth app for consumer key: $this->consumer_key"); - throw new OAuthException('No application for that consumer key.'); - } + throw new OAuthException('No application for that consumer key.'); + } - $appUser = Oauth_application_user::staticGet('token', - $this->access_token); + $appUser = Oauth_application_user::staticGet('token', + $this->access_token); - // XXX: check that app->id and appUser->application_id and consumer all - // match? + // XXX: check that app->id and appUser->application_id and consumer all + // match? - if (!empty($appUser)) { + if (!empty($appUser)) { - // read or read-write - $this->oauth_access_type = $appUser->access_type; + // read or read-write + $this->oauth_access_type = $appUser->access_type; - // If access_type == 0 we have either a request token - // or a bad / revoked access token + // If access_type == 0 we have either a request token + // or a bad / revoked access token - if ($this->oauth_access_type != 0) { + if ($this->oauth_access_type != 0) { - $this->auth_user = User::staticGet('id', $appUser->profile_id); + $this->auth_user = User::staticGet('id', $appUser->profile_id); - $msg = "API OAuth authentication for user '%s' (id: %d) on behalf of " . - "application '%s' (id: %d)."; + $msg = "API OAuth authentication for user '%s' (id: %d) on behalf of " . + "application '%s' (id: %d)."; - common_log(LOG_INFO, sprintf($msg, - $this->auth_user->nickname, - $this->auth_user->id, - $app->name, - $app->id)); - return true; - } else { - throw new OAuthException('Bad access token.'); - } - } else { + common_log(LOG_INFO, sprintf($msg, + $this->auth_user->nickname, + $this->auth_user->id, + $app->name, + $app->id)); + return true; + } else { + throw new OAuthException('Bad access token.'); + } + } else { - // also should not happen - throw new OAuthException('No user for that token.'); - } + // also should not happen + throw new OAuthException('No user for that token.'); + } - } catch (OAuthException $e) { - common_log(LOG_WARN, 'API OAuthException - ' . $e->getMessage()); - common_debug(var_export($req, true)); - $this->showOAuthError($e->getMessage()); - exit(); - } + } catch (OAuthException $e) { + common_log(LOG_WARN, 'API OAuthException - ' . $e->getMessage()); + common_debug(var_export($req, true)); + $this->showOAuthError($e->getMessage()); + exit(); + } } function showOAuthError($msg) { - header('HTTP/1.1 401 Unauthorized'); - header('Content-Type: text/html; charset=utf-8'); - print $msg . "\n"; - } - - 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']); + header('HTTP/1.1 401 Unauthorized'); + header('Content-Type: text/html; charset=utf-8'); + print $msg . "\n"; } /** 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 index 290ce89730..c39ddbb0f3 100644 --- a/lib/apioauthstore.php +++ b/lib/apioauthstore.php @@ -40,44 +40,44 @@ class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore { common_debug('new_access_token("'.$token->key.'","'.$consumer->key.'")', __FILE__); - $rt = new Token(); + $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 (empty($app)) { + common_debug("empty app!"); + } - if ($rt->find(true) && $rt->state == 1) { // authorized + if ($rt->find(true) && $rt->state == 1) { // authorized common_debug('request token found.', __FILE__); - // find the associated user of the app + // 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); + $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."); - return null; - } + if (!empty($result)) { + common_debug("Oath app user found."); + } else { + common_debug("Oauth app user not found."); + return null; + } - // go ahead and make the access token + // go ahead and make the access token - $at = new 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()) { + if (!$at->insert()) { $e = $at->_lastError; common_debug('access token "'.$at->tok.'" not inserted: "'.$e->message.'"', __FILE__); return null; @@ -91,21 +91,30 @@ class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore } common_debug('request token "'.$rt->tok.'" updated', __FILE__); - // update the token from req to access for the user + // update the token from req to access for the user - $orig = clone($appUser); - $appUser->token = $at->tok; - $result = $appUser->update($orig); + $orig = clone($appUser); + $appUser->token = $at->tok; - if (empty($result)) { - common_debug('couldn\'t update OAuth app user.'); - return null; - } + // 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. - // Okay, good + $appUser->access_type = $app->access_type; - return new OAuthToken($at->tok, $at->secret); - } + $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/router.php b/lib/router.php index 420f5a0a10..d6e448c2f9 100644 --- a/lib/router.php +++ b/lib/router.php @@ -50,8 +50,7 @@ class Router var $m = null; static $inst = null; static $bare = array('requesttoken', 'accesstoken', 'userauthorization', - 'postnotice', 'updateprofile', 'finishremotesubscribe', - 'apioauthrequesttoken', 'apioauthaccesstoken'); + 'postnotice', 'updateprofile', 'finishremotesubscribe'); static function get() { @@ -659,7 +658,13 @@ class Router 'id' => '[0-9]+') ); - $m->connect('oauth/authorize', + $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')); foreach (array('subscriptions', 'subscribers') as $a) { From 693b16174ad4142d1a543f78878c84c552ce6d74 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 13 Jan 2010 05:31:48 +0000 Subject: [PATCH 070/157] Fix icon upload on new apps --- actions/newapplication.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/newapplication.php b/actions/newapplication.php index 3d42b657be..7bb81095dd 100644 --- a/actions/newapplication.php +++ b/actions/newapplication.php @@ -266,7 +266,7 @@ class NewApplicationAction extends OwnerDesignAction $app->query('ROLLBACK'); } - $this->app->uploadLogo(); + $app->uploadLogo(); $app->query('COMMIT'); From e101a6df6ba1cbec4664bb81fc81655e5db18b0f Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 13 Jan 2010 07:33:51 +0000 Subject: [PATCH 071/157] Rework application registration workflow to be more private --- actions/editapplication.php | 8 +-- actions/newapplication.php | 8 +-- actions/{apps.php => oauthappssettings.php} | 10 +--- actions/oauthconnectionssettings.php | 2 +- actions/showapplication.php | 25 +++------ lib/applicationeditform.php | 61 ++++++++++----------- lib/applicationlist.php | 13 ++--- lib/router.php | 23 +++----- 8 files changed, 58 insertions(+), 92 deletions(-) rename actions/{apps.php => oauthappssettings.php} (94%) diff --git a/actions/editapplication.php b/actions/editapplication.php index a0ed3117a7..a6db87c61e 100644 --- a/actions/editapplication.php +++ b/actions/editapplication.php @@ -125,10 +125,7 @@ class EditApplicationAction extends OwnerDesignAction if ($this->arg('cancel')) { common_redirect(common_local_url('showapplication', - array( - 'nickname' => $cur->nickname, - 'id' => $this->app->id) - ), 303); + array('id' => $this->app->id)), 303); } elseif ($this->arg('save')) { $this->trySave(); } else { @@ -253,8 +250,7 @@ class EditApplicationAction extends OwnerDesignAction $this->app->uploadLogo(); - common_redirect(common_local_url('apps', - array('nickname' => $cur->nickname)), 303); + common_redirect(common_local_url('oauthappssettings'), 303); } } diff --git a/actions/newapplication.php b/actions/newapplication.php index 7bb81095dd..c499fe7c76 100644 --- a/actions/newapplication.php +++ b/actions/newapplication.php @@ -114,8 +114,7 @@ class NewApplicationAction extends OwnerDesignAction $cur = common_current_user(); if ($this->arg('cancel')) { - common_redirect(common_local_url('apps', - array('nickname' => $cur->nickname)), 303); + common_redirect(common_local_url('oauthappssettings'), 303); } elseif ($this->arg('save')) { $this->trySave(); } else { @@ -147,7 +146,7 @@ class NewApplicationAction extends OwnerDesignAction function trySave() { - $name = $this->trimmed('name'); + $name = $this->trimmed('name'); $description = $this->trimmed('description'); $source_url = $this->trimmed('source_url'); $organization = $this->trimmed('organization'); @@ -270,8 +269,7 @@ class NewApplicationAction extends OwnerDesignAction $app->query('COMMIT'); - common_redirect(common_local_url('apps', - array('nickname' => $cur->nickname)), 303); + common_redirect(common_local_url('oauthappssettings'), 303); } diff --git a/actions/apps.php b/actions/oauthappssettings.php similarity index 94% rename from actions/apps.php rename to actions/oauthappssettings.php index 7c7b24570f..6c0670b17b 100644 --- a/actions/apps.php +++ b/actions/oauthappssettings.php @@ -46,7 +46,7 @@ require_once INSTALLDIR . '/lib/applicationlist.php'; * @see SettingsAction */ -class AppsAction extends SettingsAction +class OauthappssettingsAction extends SettingsAction { var $page = 0; @@ -116,10 +116,7 @@ class AppsAction extends SettingsAction $this->elementStart('p', array('id' => 'application_register')); $this->element('a', - array('href' => common_local_url( - 'newapplication', - array('nickname' => $user->nickname) - ), + array('href' => common_local_url('newapplication'), 'class' => 'more' ), 'Register a new application'); @@ -129,8 +126,7 @@ class AppsAction extends SettingsAction $this->page > 1, $cnt > APPS_PER_PAGE, $this->page, - 'apps', - array('nickname' => $user->nickname) + 'oauthappssettings' ); } diff --git a/actions/oauthconnectionssettings.php b/actions/oauthconnectionssettings.php index 56e7b02fba..99bb9022b2 100644 --- a/actions/oauthconnectionssettings.php +++ b/actions/oauthconnectionssettings.php @@ -158,7 +158,7 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction $this->elementStart('p'); $this->raw(_('Developers can edit the registration settings for their applications ')); $this->element('a', - array('href' => common_local_url('apps', array('nickname' => $cur->nickname))), + array('href' => common_local_url('oauthappssettings')), 'here.'); $this->elementEnd('p'); } diff --git a/actions/showapplication.php b/actions/showapplication.php index f2ff8b9002..bd33371368 100644 --- a/actions/showapplication.php +++ b/actions/showapplication.php @@ -211,15 +211,9 @@ class ShowApplicationAction extends OwnerDesignAction $this->elementStart('ul'); $this->elementStart('li', 'entity_edit'); $this->element('a', - array('href' => - common_local_url( - 'editapplication', - array( - 'nickname' => $this->owner->nickname, - 'id' => $this->application->id - ) - ) - ), 'Edit'); + array('href' => common_local_url('editapplication', + array('id' => $this->application->id))), + 'Edit'); $this->elementEnd('li'); $this->elementStart('li', 'entity_reset_keysecret'); @@ -228,8 +222,7 @@ class ShowApplicationAction extends OwnerDesignAction 'class' => 'form_reset_key', 'method' => 'POST', 'action' => common_local_url('showapplication', - array('nickname' => $cur->nickname, - 'id' => $this->application->id)))); + array('id' => $this->application->id)))); $this->elementStart('fieldset'); $this->hidden('token', common_session_token()); @@ -273,13 +266,9 @@ class ShowApplicationAction extends OwnerDesignAction $this->elementStart('p', array('id' => 'application_action')); $this->element('a', - array( - 'href' => common_local_url( - 'apps', - array('nickname' => $this->owner->nickname)), - 'class' => 'more' - ), - 'View your applications'); + array('href' => common_local_url('oauthappssettings'), + 'class' => 'more'), + 'View your applications'); $this->elementEnd('p'); } diff --git a/lib/applicationeditform.php b/lib/applicationeditform.php index e9ab467804..040d3bf74b 100644 --- a/lib/applicationeditform.php +++ b/lib/applicationeditform.php @@ -119,12 +119,9 @@ class ApplicationEditForm extends Form if (!empty($this->application)) { return common_local_url('editapplication', - array('id' => $this->application->id, - 'nickname' => $cur->nickname) - ); + array('id' => $this->application->id)); } else { - return common_local_url('newapplication', - array('nickname' => $cur->nickname)); + return common_local_url('newapplication'); } } @@ -149,7 +146,7 @@ class ApplicationEditForm extends Form { if ($this->application) { $id = $this->application->id; - $icon = $this->application->icon; + $icon = $this->application->icon; $name = $this->application->name; $description = $this->application->description; $source_url = $this->application->source_url; @@ -160,7 +157,7 @@ class ApplicationEditForm extends Form $this->access_type = $this->application->access_type; } else { $id = ''; - $icon = ''; + $icon = ''; $name = ''; $description = ''; $source_url = ''; @@ -171,26 +168,26 @@ class ApplicationEditForm extends Form $this->access_type = ''; } - $this->out->hidden('token', common_session_token()); + $this->out->hidden('token', common_session_token()); $this->out->elementStart('ul', 'form_data'); - $this->out->elementStart('li', array('id' => 'application_icon')); + $this->out->elementStart('li', array('id' => 'application_icon')); - if (!empty($icon)) { - $this->out->element('img', array('src' => $icon)); - } + if (!empty($icon)) { + $this->out->element('img', array('src' => $icon)); + } - $this->out->element('label', array('for' => 'app_icon'), - _('Icon')); + $this->out->element('label', array('for' => 'app_icon'), + _('Icon')); $this->out->element('input', array('name' => 'app_icon', - 'type' => 'file', - 'id' => '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())); + 'type' => 'hidden', + 'id' => 'MAX_FILE_SIZE', + 'value' => ImageFile::maxFileSizeInt())); $this->out->elementEnd('li'); $this->out->elementStart('li'); @@ -207,13 +204,13 @@ class ApplicationEditForm extends Form $maxDesc = Oauth_application::maxDesc(); if ($maxDesc > 0) { $descInstr = sprintf(_('Describe your application in %d chars'), - $maxDesc); + $maxDesc); } else { $descInstr = _('Describe your application'); } $this->out->textarea('description', _('Description'), ($this->out->arg('description')) ? $this->out->arg('description') : $description, - $descInstr); + $descInstr); $this->out->elementEnd('li'); @@ -259,8 +256,8 @@ class ApplicationEditForm extends Form $this->out->element('input', $attrs); $this->out->element('label', array('for' => 'app_type-browser', - 'class' => 'radio'), - _('Browser')); + 'class' => 'radio'), + _('Browser')); $attrs = array('name' => 'app_type', 'type' => 'radio', @@ -275,8 +272,8 @@ class ApplicationEditForm extends Form $this->out->element('input', $attrs); $this->out->element('label', array('for' => 'app_type-desktop', - 'class' => 'radio'), - _('Desktop')); + 'class' => 'radio'), + _('Desktop')); $this->out->element('p', 'form_guide', _('Type of application, browser or desktop')); $this->out->elementEnd('li'); @@ -298,8 +295,8 @@ class ApplicationEditForm extends Form $this->out->element('input', $attrs); $this->out->element('label', array('for' => 'default_access_type-ro', - 'class' => 'radio'), - _('Read-only')); + 'class' => 'radio'), + _('Read-only')); $attrs = array('name' => 'default_access_type', 'type' => 'radio', @@ -309,15 +306,15 @@ class ApplicationEditForm extends Form 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')); + 'class' => 'radio'), + _('Read-write')); $this->out->element('p', 'form_guide', _('Default access for this application: read-only, or read-write')); $this->out->elementEnd('li'); @@ -334,8 +331,8 @@ class ApplicationEditForm extends Form function formActions() { $this->out->submit('cancel', _('Cancel'), 'submit form_action-primary', - 'cancel', _('Cancel')); + 'cancel', _('Cancel')); $this->out->submit('save', _('Save'), 'submit form_action-secondary', - 'save', _('Save')); + 'save', _('Save')); } } diff --git a/lib/applicationlist.php b/lib/applicationlist.php index 15c2d588a3..f2eaefb401 100644 --- a/lib/applicationlist.php +++ b/lib/applicationlist.php @@ -64,7 +64,7 @@ class ApplicationList extends Widget $this->application = $application; $this->owner = $owner; $this->action = $action; - $this->connections = $connections; + $this->connections = $connections; } function show() @@ -97,10 +97,9 @@ class ApplicationList extends Widget $this->out->elementStart('span', 'vcard author'); if (!$this->connections) { $this->out->elementStart('a', - array('href' => common_local_url('showapplication', - array('nickname' => $user->nickname, - 'id' => $this->application->id)), - 'class' => 'url')); + array('href' => common_local_url('showapplication', + array('id' => $this->application->id)), + 'class' => 'url')); } else { $this->out->elementStart('a', array('href' => $this->application->source_url, @@ -154,8 +153,4 @@ class ApplicationList extends Widget return; } - function highlight($text) - { - return htmlspecialchars($text); - } } diff --git a/lib/router.php b/lib/router.php index d6e448c2f9..42bff27788 100644 --- a/lib/router.php +++ b/lib/router.php @@ -141,7 +141,7 @@ class Router // settings foreach (array('profile', 'avatar', 'password', 'im', 'oauthconnections', - 'email', 'sms', 'userdesign', 'other') as $s) { + 'oauthapps', 'email', 'sms', 'userdesign', 'other') as $s) { $m->connect('settings/'.$s, array('action' => $s.'settings')); } @@ -634,28 +634,23 @@ class Router // user stuff foreach (array('subscriptions', 'subscribers', - 'nudge', 'all', 'foaf', 'xrds', 'apps', + '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}')); } - $m->connect(':nickname/apps', - array('action' => 'apps'), - array('nickname' => '['.NICKNAME_FMT.']{1,64}')); - $m->connect(':nickname/apps/show/:id', + $m->connect('settings/oauthapps/show/:id', array('action' => 'showapplication'), - array('nickname' => '['.NICKNAME_FMT.']{1,64}', - 'id' => '[0-9]+') + array('id' => '[0-9]+') ); - $m->connect(':nickname/apps/new', - array('action' => 'newapplication'), - array('nickname' => '['.NICKNAME_FMT.']{1,64}')); - $m->connect(':nickname/apps/edit/:id', + $m->connect('settings/oauthapps/new', + array('action' => 'newapplication') + ); + $m->connect('settings/oauthapps/edit/:id', array('action' => 'editapplication'), - array('nickname' => '['.NICKNAME_FMT.']{1,64}', - 'id' => '[0-9]+') + array('id' => '[0-9]+') ); $m->connect('api/oauth/request_token', From c0eee277d1058c9c291b3c4474cc8a72cb8c6d0e Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 13 Jan 2010 11:31:15 +0000 Subject: [PATCH 072/157] Make sure applications are really looked up by consumer key --- actions/apioauthauthorize.php | 42 +++-------------------------------- lib/apioauthstore.php | 40 ++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/actions/apioauthauthorize.php b/actions/apioauthauthorize.php index cdf9cb7df3..0966ba1d71 100644 --- a/actions/apioauthauthorize.php +++ b/actions/apioauthauthorize.php @@ -74,42 +74,11 @@ class ApiOauthAuthorizeAction extends ApiOauthAction $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; } - function getApp() - { - // Look up the full req token - - $req_token = $this->store->lookup_token(null, - 'request', - $this->oauth_token); - - if (empty($req_token)) { - - common_debug("Couldn't find request token!"); - - $this->clientError(_('Bad request.')); - return; - } - - // Look up the app - - $app = new Oauth_application(); - $app->consumer_key = $req_token->consumer_key; - $result = $app->find(true); - - if (!empty($result)) { - $this->app = $app; - return true; - - } else { - common_debug("couldn't find the app!"); - return false; - } - } - /** * Handle input, produce output * @@ -140,7 +109,8 @@ class ApiOauthAuthorizeAction extends ApiOauthAction return; } - if (!$this->getApp()) { + if (empty($this->app)) { + common_debug('No app for that token.'); $this->clientError(_('Bad request.')); return; } @@ -166,11 +136,6 @@ class ApiOauthAuthorizeAction extends ApiOauthAction return; } - if (!$this->getApp()) { - $this->clientError(_('Bad request.')); - return; - } - // check creds $user = null; @@ -416,7 +381,6 @@ class ApiOauthAuthorizeAction extends ApiOauthAction function getInstructions() { return _('Allow or deny access to your account information.'); - } /** diff --git a/lib/apioauthstore.php b/lib/apioauthstore.php index c39ddbb0f3..32110d0575 100644 --- a/lib/apioauthstore.php +++ b/lib/apioauthstore.php @@ -36,6 +36,44 @@ class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore $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__); @@ -64,7 +102,7 @@ class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore if (!empty($result)) { common_debug("Oath app user found."); } else { - common_debug("Oauth app user not found."); + common_debug("Oauth app user not found. app id $app->id token $rt->tok"); return null; } From ba68e042a8acb9dd1054e0bc1c5cd4dfd415642e Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 13 Jan 2010 17:52:25 +0000 Subject: [PATCH 073/157] Fix user count --- actions/showapplication.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/actions/showapplication.php b/actions/showapplication.php index bd33371368..b21b994aa2 100644 --- a/actions/showapplication.php +++ b/actions/showapplication.php @@ -194,10 +194,13 @@ class ShowApplicationAction extends OwnerDesignAction $defaultAccess = ($this->application->access_type & Oauth_application::$writeAccess) ? 'read-write' : 'read-only'; $profile = Profile::staticGet($this->application->owner); - $userCnt = 0; // XXX: count how many users use the app + + $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.'), + _('created by %1$s - %2$s access by default - %3$d users'), $profile->getBestName(), $defaultAccess, $userCnt @@ -222,7 +225,7 @@ class ShowApplicationAction extends OwnerDesignAction 'class' => 'form_reset_key', 'method' => 'POST', 'action' => common_local_url('showapplication', - array('id' => $this->application->id)))); + array('id' => $this->application->id)))); $this->elementStart('fieldset'); $this->hidden('token', common_session_token()); From 7b3c099f953c1569c18d81fdb8d4230e927d429e Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 13 Jan 2010 18:20:03 +0000 Subject: [PATCH 074/157] Ensure only the application's owner can edit it --- actions/editapplication.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/actions/editapplication.php b/actions/editapplication.php index a6db87c61e..9cc3e3cead 100644 --- a/actions/editapplication.php +++ b/actions/editapplication.php @@ -45,9 +45,9 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { class EditApplicationAction extends OwnerDesignAction { - var $msg = null; - - var $app = null; + var $msg = null; + var $owner = null; + var $app = null; function title() { @@ -68,7 +68,14 @@ class EditApplicationAction extends OwnerDesignAction } $id = (int)$this->arg('id'); - $this->app = Oauth_application::staticGet($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.')); From cff2cfd7a7566542ce860410e7ef006a84869c7e Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 13 Jan 2010 18:33:13 +0000 Subject: [PATCH 075/157] Fix approval date and label on apps list --- lib/applicationlist.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/applicationlist.php b/lib/applicationlist.php index f2eaefb401..6eae261353 100644 --- a/lib/applicationlist.php +++ b/lib/applicationlist.php @@ -136,8 +136,8 @@ class ApplicationList extends Widget $access = ($this->application->access_type & Oauth_application::$writeAccess) ? 'read-write' : 'read-only'; - $txt = 'Approved ' . common_exact_date($appUser->modified) . - " $access for access."; + $txt = 'Approved ' . common_date_string($appUser->modified) . + " - $access access."; $this->out->raw($txt); $this->out->elementEnd('li'); From 6d58ef4abb12d735b6be777ea79f99a07c68694a Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Wed, 13 Jan 2010 20:10:09 +0000 Subject: [PATCH 076/157] Updated apioauthauthorize markup and styles --- actions/apioauthauthorize.php | 46 +++++++++-------------------------- theme/base/css/display.css | 10 ++++++-- 2 files changed, 20 insertions(+), 36 deletions(-) diff --git a/actions/apioauthauthorize.php b/actions/apioauthauthorize.php index 0966ba1d71..72d1426511 100644 --- a/actions/apioauthauthorize.php +++ b/actions/apioauthauthorize.php @@ -273,27 +273,6 @@ class ApiOauthAuthorizeAction extends ApiOauthAction return _('An application would like to connect to your account'); } - /** - * Show page notice - * - * Display a notice for how to use the page, or the - * error if it exists. - * - * @return void - */ - - function showPageNotice() - { - if ($this->error) { - $this->element('p', 'error', $this->error); - } else { - $instr = $this->getInstructions(); - $output = common_markup_to_html($instr); - - $this->raw($output); - } - } - /** * Shows the authorization form. * @@ -303,40 +282,38 @@ class ApiOauthAuthorizeAction extends ApiOauthAction function showContent() { $this->elementStart('form', array('method' => 'post', - 'id' => 'form_login', + '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('fieldset'); - - $this->elementStart('ul'); + $this->elementStart('ul', 'form_data'); $this->elementStart('li'); + $this->elementStart('p'); if (!empty($this->app->icon)) { $this->element('img', array('src' => $this->app->icon)); } - $this->elementEnd('li'); - $this->elementStart('li'); $access = ($this->app->access_type & Oauth_application::$writeAccess) ? 'access and update' : 'access'; - $msg = _("The application %s by %s would like " . - "the ability to %s your account data."); + $msg = _("The application %s by %s would like " . + "the ability to %s your account data."); $this->raw(sprintf($msg, $this->app->name, $this->app->organization, $access)); - + $this->elementEnd('p'); $this->elementEnd('li'); $this->elementEnd('ul'); - $this->elementEnd('fieldset'); - if (!common_logged_in()) { $this->elementStart('fieldset'); @@ -355,17 +332,18 @@ class ApiOauthAuthorizeAction extends ApiOauthAction } $this->element('input', array('id' => 'deny_submit', - 'class' => 'submit', + 'class' => 'submit submit form_action-primary', 'name' => 'deny', 'type' => 'submit', 'value' => _('Deny'))); $this->element('input', array('id' => 'allow_submit', - 'class' => 'submit', + 'class' => 'submit submit form_action-secondary', 'name' => 'allow', 'type' => 'submit', 'value' => _('Allow'))); + $this->elementEnd('fieldset'); $this->elementEnd('form'); } diff --git a/theme/base/css/display.css b/theme/base/css/display.css index 82670c964a..ff81d37273 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; } @@ -906,10 +907,15 @@ list-style-type:none; } .application img, #showapplication .entity_profile img, -#editapplication .form_data #application_icon img { +#editapplication .form_data #application_icon, +#apioauthauthorize .form_data img { max-width:96px; max-height:96px; } +#apioauthauthorize .form_data img { +margin-right:18px; +float:left; +} #showapplication .entity_profile { width:68%; } From dbcbc2fe7ff0368910a49ed5675b0edc1e2bf18d Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Wed, 13 Jan 2010 20:43:23 +0000 Subject: [PATCH 077/157] Changed legend text from Login to Account because it is not really logging iny --- actions/apioauthauthorize.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/apioauthauthorize.php b/actions/apioauthauthorize.php index 72d1426511..fa074c4e76 100644 --- a/actions/apioauthauthorize.php +++ b/actions/apioauthauthorize.php @@ -317,7 +317,7 @@ class ApiOauthAuthorizeAction extends ApiOauthAction if (!common_logged_in()) { $this->elementStart('fieldset'); - $this->element('legend', null, _('Login')); + $this->element('legend', null, _('Account')); $this->elementStart('ul', 'form_data'); $this->elementStart('li'); $this->input('nickname', _('Nickname')); From 9e7f47652d860d7f1e296dd369c4e68814bf2636 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 13 Jan 2010 21:11:08 +0000 Subject: [PATCH 078/157] Revoke access token UI --- actions/oauthconnectionssettings.php | 62 ++++++++++++++++++++++++---- actions/showapplication.php | 1 + classes/Oauth_application_user.php | 2 +- classes/Profile.php | 3 +- lib/applicationlist.php | 14 ++++++- 5 files changed, 71 insertions(+), 11 deletions(-) diff --git a/actions/oauthconnectionssettings.php b/actions/oauthconnectionssettings.php index 99bb9022b2..b17729b821 100644 --- a/actions/oauthconnectionssettings.php +++ b/actions/oauthconnectionssettings.php @@ -50,10 +50,12 @@ 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; } @@ -101,16 +103,16 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction $application = $profile->getApplications($offset, $limit); - $cnt == 0; + $cnt == 0; - if (!empty($application)) { - $al = new ApplicationList($application, $user, $this, true); - $cnt = $al->show(); - } + if (!empty($application)) { + $al = new ApplicationList($application, $user, $this, true); + $cnt = $al->show(); + } - if ($cnt == 0) { - $this->showEmptyListMessage(); - } + if ($cnt == 0) { + $this->showEmptyListMessage(); + } $this->pagination($this->page > 1, $cnt > APPS_PER_PAGE, $this->page, 'connectionssettings', @@ -139,6 +141,50 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction 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() diff --git a/actions/showapplication.php b/actions/showapplication.php index b21b994aa2..049206375d 100644 --- a/actions/showapplication.php +++ b/actions/showapplication.php @@ -92,6 +92,7 @@ class ShowApplicationAction extends OwnerDesignAction if ($cur->id != $this->owner->id) { $this->clientError(_('You are not the owner of this application.'), 401); + return false; } return true; diff --git a/classes/Oauth_application_user.php b/classes/Oauth_application_user.php index a05371f563..618d68133c 100644 --- a/classes/Oauth_application_user.php +++ b/classes/Oauth_application_user.php @@ -34,7 +34,7 @@ class Oauth_application_user extends Memcached_DataObject $oau = new Oauth_application_user(); $oau->profile_id = $user->id; - $oau->application_id = $app->id; + $oau->application_id = $app->id; $oau->limit(1); $result = $oau->find(true); diff --git a/classes/Profile.php b/classes/Profile.php index fef2a21710..1076fb2cb3 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -358,7 +358,8 @@ class Profile extends Memcached_DataObject 'SELECT a.* ' . 'FROM oauth_application_user u, oauth_application a ' . 'WHERE u.profile_id = %d ' . - 'AND a.id = u.application_id ' . + 'AND a.id = u.application_id ' . + 'AND u.access_type > 0 ' . 'ORDER BY u.created DESC '; if ($offset > 0) { diff --git a/lib/applicationlist.php b/lib/applicationlist.php index 6eae261353..3abb1f8aa7 100644 --- a/lib/applicationlist.php +++ b/lib/applicationlist.php @@ -142,7 +142,19 @@ class ApplicationList extends Widget $this->out->raw($txt); $this->out->elementEnd('li'); - // XXX: Add revoke access button + $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'); } } From d33040089d660d897183df8e94c2a65bb8c8c85f Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 13 Jan 2010 21:14:22 +0000 Subject: [PATCH 079/157] Remove verifier from Oauth_application_user (not needed there) --- classes/Oauth_application_user.php | 1 - classes/statusnet.ini | 1 - db/statusnet.sql | 1 - 3 files changed, 3 deletions(-) diff --git a/classes/Oauth_application_user.php b/classes/Oauth_application_user.php index 618d68133c..57986281f9 100644 --- a/classes/Oauth_application_user.php +++ b/classes/Oauth_application_user.php @@ -14,7 +14,6 @@ class Oauth_application_user extends Memcached_DataObject public $application_id; // int(4) primary_key not_null public $access_type; // tinyint(1) public $token; // varchar(255) - public $verifier; // 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 b358bbf60a..2b5f2c225b 100644 --- a/classes/statusnet.ini +++ b/classes/statusnet.ini @@ -373,7 +373,6 @@ profile_id = 129 application_id = 129 access_type = 17 token = 2 -verifier = 2 created = 142 modified = 384 diff --git a/db/statusnet.sql b/db/statusnet.sql index b1867e7f04..3a74f9d7a0 100644 --- a/db/statusnet.sql +++ b/db/statusnet.sql @@ -230,7 +230,6 @@ create table oauth_application_user ( 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', - verifier varchar(255) not null comment 'verification code', 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) From 6efbf2777ac1ba934829a8e9ae381ca280621c0c Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 13 Jan 2010 21:31:19 +0000 Subject: [PATCH 080/157] Add verifier and verified callback to token for OAuth 1.0a --- classes/Token.php | 6 ++++-- classes/statusnet.ini | 2 ++ db/statusnet.sql | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) 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 2b5f2c225b..6203650a69 100644 --- a/classes/statusnet.ini +++ b/classes/statusnet.ini @@ -516,6 +516,8 @@ tok = 130 secret = 130 type = 145 state = 17 +verifier = 2 +verified_callback = 2 created = 142 modified = 384 diff --git a/db/statusnet.sql b/db/statusnet.sql index 3a74f9d7a0..17de4fd0d4 100644 --- a/db/statusnet.sql +++ b/db/statusnet.sql @@ -189,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', From de70b91a3a42b07c86d3a0cd8868ded6510fd91c Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 13 Jan 2010 16:52:33 -0800 Subject: [PATCH 081/157] Some rough test scripts for poking at the OAuth system --- tests/oauth/README | 22 +++++++ tests/oauth/exchangetokens.php | 105 ++++++++++++++++++++++++++++++++ tests/oauth/getrequesttoken.php | 71 +++++++++++++++++++++ tests/oauth/oauth.ini | 10 +++ tests/oauth/verifycreds.php | 101 ++++++++++++++++++++++++++++++ 5 files changed, 309 insertions(+) create mode 100644 tests/oauth/README create mode 100755 tests/oauth/exchangetokens.php create mode 100755 tests/oauth/getrequesttoken.php create mode 100644 tests/oauth/oauth.ini create mode 100755 tests/oauth/verifycreds.php diff --git a/tests/oauth/README b/tests/oauth/README new file mode 100644 index 0000000000..ea4aabadbe --- /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 and request token, token secret and a url to authorize it. Once +you get the token/secret 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 an 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..5ef0e571eb --- /dev/null +++ b/tests/oauth/oauth.ini @@ -0,0 +1,10 @@ +; Setup OAuth info here +apiroot = "http://dev.controlyourself.ca/zach/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/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); +} + From c2c930a8556cca866dfa0eba2fe1a8242eef71f2 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 13 Jan 2010 16:56:52 -0800 Subject: [PATCH 082/157] Fixed some spelling mistakes in the README --- tests/oauth/README | 6 +++--- tests/oauth/oauth.ini | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/oauth/README b/tests/oauth/README index ea4aabadbe..dd76feb0c6 100644 --- a/tests/oauth/README +++ b/tests/oauth/README @@ -10,12 +10,12 @@ Example usage: php getrequesttoken.php -Gets and request token, token secret and a url to authorize it. Once -you get the token/secret you can exchange it for an access token... +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 an protected API +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/oauth.ini b/tests/oauth/oauth.ini index 5ef0e571eb..16b747fe43 100644 --- a/tests/oauth/oauth.ini +++ b/tests/oauth/oauth.ini @@ -1,5 +1,5 @@ ; Setup OAuth info here -apiroot = "http://dev.controlyourself.ca/zach/api" +apiroot = "http://YOURSTATUSNET/api" request_token_url = "/oauth/request_token" authorize_url = "/oauth/authorize" From 1f8ddf716d0b54cc40aa89e595fe2232a10e7a2a Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 14 Jan 2010 02:16:03 +0000 Subject: [PATCH 083/157] Check for read vs. read-write access on OAuth authenticated API mehtods. --- lib/api.php | 5 +++++ lib/apiauth.php | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+) 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 431f3ac4fd..8374c24a7f 100644 --- a/lib/apiauth.php +++ b/lib/apiauth.php @@ -78,12 +78,27 @@ class ApiAuthAction extends ApiAction $this->checkOAuthRequest(); } else { $this->checkBasicAuthUser(); + // By default, all basic auth users have read and write access + + $this->access = self::READ_WRITE; } } return true; } + function handle($args) + { + parent::handle($args); + + if ($this->isReadOnly($args) == false) { + if ($this->access == self::READ_ONLY) { + $this->clientError(_('API method requires write access.'), 401); + exit(); + } + } + } + function checkOAuthRequest() { common_debug("We have an OAuth request."); @@ -130,6 +145,10 @@ class ApiAuthAction extends ApiAction if ($this->oauth_access_type != 0) { + // Set the read or read-write access for the api call + $this->access = ($appUser->access_type & Oauth_application::$writeAccess) + ? self::READ_WRITE : self::READ_ONLY; + $this->auth_user = User::staticGet('id', $appUser->profile_id); $msg = "API OAuth authentication for user '%s' (id: %d) on behalf of " . @@ -220,6 +239,7 @@ class ApiAuthAction extends ApiAction exit; } } + return true; } From 6a4c88afe723b246cf3a2158ac2c1ad4161bdd43 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Thu, 14 Jan 2010 02:32:59 +0000 Subject: [PATCH 084/157] More relaxed selector for application icon and form checkbox --- theme/base/css/display.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/theme/base/css/display.css b/theme/base/css/display.css index ff81d37273..84e9426c77 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -907,7 +907,7 @@ list-style-type:none; } .application img, #showapplication .entity_profile img, -#editapplication .form_data #application_icon, +.form_data #application_icon img, #apioauthauthorize .form_data img { max-width:96px; max-height:96px; @@ -944,9 +944,9 @@ margin-left:1.795%; font-family:monospace; font-size:1.3em; } -#editapplication .form_data #application_types label.radio, -#editapplication .form_data #default_access_types label.radio { -width:15%; +.form_data #application_types label.radio, +.form_data #default_access_types label.radio { +width:14.5%; } /* NOTICE */ From 8b24b5ac7bea2098d3c85e342526c2102e2a6fb9 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 20 Jan 2010 18:01:07 -0800 Subject: [PATCH 085/157] Add Start/EndSetApiUser events when setting API user via OAuth --- lib/apiauth.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/apiauth.php b/lib/apiauth.php index 8374c24a7f..f513ed2c9a 100644 --- a/lib/apiauth.php +++ b/lib/apiauth.php @@ -149,7 +149,10 @@ class ApiAuthAction extends ApiAction $this->access = ($appUser->access_type & Oauth_application::$writeAccess) ? self::READ_WRITE : self::READ_ONLY; - $this->auth_user = User::staticGet('id', $appUser->profile_id); + 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)."; From 4daf76212a6802863d20c6af7597eddded227ae8 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 14 Jan 2010 02:38:01 +0000 Subject: [PATCH 086/157] - Had to remove checking read vs. read-write in OAuth authenticated methods - Will now pick up source attr from OAuth app --- actions/apiaccountverifycredentials.php | 14 ++++++++++++++ actions/apistatusesupdate.php | 5 +++++ lib/apiauth.php | 14 +++++--------- 3 files changed, 24 insertions(+), 9 deletions(-) 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/apistatusesupdate.php b/actions/apistatusesupdate.php index f594bbf393..f8bf7cf874 100644 --- a/actions/apistatusesupdate.php +++ b/actions/apistatusesupdate.php @@ -85,6 +85,11 @@ class ApiStatusesUpdateAction extends ApiAuthAction $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'; } diff --git a/lib/apiauth.php b/lib/apiauth.php index f513ed2c9a..37070d212f 100644 --- a/lib/apiauth.php +++ b/lib/apiauth.php @@ -55,6 +55,7 @@ class ApiAuthAction extends ApiAction { var $access_token; var $oauth_access_type; + var $oauth_source; /** * Take arguments for running, and output basic auth header if needed @@ -90,13 +91,6 @@ class ApiAuthAction extends ApiAction function handle($args) { parent::handle($args); - - if ($this->isReadOnly($args) == false) { - if ($this->access == self::READ_ONLY) { - $this->clientError(_('API method requires write access.'), 401); - exit(); - } - } } function checkOAuthRequest() @@ -116,8 +110,6 @@ class ApiAuthAction extends ApiAction $req = OAuthRequest::from_request(); $server->verify_request($req); - common_debug("Good OAuth request!"); - $app = Oauth_application::getByConsumerKey($this->consumer_key); if (empty($app)) { @@ -129,6 +121,10 @@ class ApiAuthAction extends ApiAction 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); From e6cf293db8c465b21899fd328152a991501a5038 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 24 Jan 2010 22:39:32 -0500 Subject: [PATCH 087/157] Recover caching logic lost in bad merge I made a bad merge on Jan 10th from master to 0.9.x. This lost a number of memcache enhancements made on the 0.9.x branch. I've been able to re-do the manual merge, and this represents the changes. Most of them are related to caching on insert. --- classes/Memcached_DataObject.php | 148 +++++++++++++++++++++---------- 1 file changed, 100 insertions(+), 48 deletions(-) diff --git a/classes/Memcached_DataObject.php b/classes/Memcached_DataObject.php index 33645a3e8b..f59213c2cb 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); } From e400437d5783a5a1a03feb3ea3e5b6a40a71ed60 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 24 Jan 2010 22:50:07 -0500 Subject: [PATCH 088/157] fix interpolation for positional arguments in showstream --- actions/showstream.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); } From fc7afed92478117d009d835a4d1cd0565ada1089 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 24 Jan 2010 22:52:03 -0500 Subject: [PATCH 089/157] fix interpolation for positional arguments in replies --- actions/replies.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); } From 02526f1100621aa522ccb6beccc0a4e507da328a Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 24 Jan 2010 22:53:29 -0500 Subject: [PATCH 090/157] fix interpolation of positional arguments to sprintf in outbox --- actions/outbox.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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); } } From 9077db00a50a6123ece70eb690b658c19c5aeb27 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 24 Jan 2010 22:54:25 -0500 Subject: [PATCH 091/157] fix interpolation of positional arguments to sprintf in inbox --- actions/inbox.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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); } } From 73fdec6c12c308732f0673f2d10ab1542c927a33 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 24 Jan 2010 22:55:29 -0500 Subject: [PATCH 092/157] fix interpolation of positional arguments to sprintf in usergroups --- actions/usergroups.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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); } From 019dad95e12191632a3828fb0950a80f305bbf01 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 24 Jan 2010 22:56:41 -0500 Subject: [PATCH 093/157] fix interpolation of positional arguments to sprintf in show favorites --- actions/showfavorites.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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); } From a9da43a41644b3e1ff27e8250fd858d2c2840e7b Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 24 Jan 2010 22:57:33 -0500 Subject: [PATCH 094/157] fix interpolation of positional arguments to sprintf in show group --- actions/showgroup.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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); } From 089305ac7a89ba42286973feb14ca23687ec0995 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Sun, 24 Jan 2010 22:59:22 -0500 Subject: [PATCH 095/157] fix interpolation of positional arguments to sprintf in tag action --- actions/tag.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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)), From 73f6250b9d8a1f65bfcbf76b05f087bd6e118bea Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Mon, 25 Jan 2010 14:55:04 +0000 Subject: [PATCH 096/157] An update to geolocation cookie to use a single file and set the expiry date to 30 days from now. --- js/util.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/js/util.js b/js/util.js index a7339010a7..8d52d859b7 100644 --- a/js/util.js +++ b/js/util.js @@ -495,7 +495,7 @@ var SN = { // StatusNet $('#'+SN.C.S.NoticeLocationId).val(''); $('#'+SN.C.S.NoticeDataGeo).attr('checked', false); - $.cookie(SN.C.S.NoticeDataGeoCookie, 'disabled'); + $.cookie(SN.C.S.NoticeDataGeoCookie, 'disabled', { path: '/', expires: SN.U.GetDateFromNow(30) }); } function getJSONgeocodeURL(geocodeURL, data) { @@ -537,7 +537,8 @@ var SN = { // StatusNet NLNU: location.url, NDG: true }; - $.cookie(SN.C.S.NoticeDataGeoCookie, JSON.stringify(cookieValue)); + + $.cookie(SN.C.S.NoticeDataGeoCookie, JSON.stringify(cookieValue), { path: '/', expires: SN.U.GetDateFromNow(30) }); }); } @@ -658,6 +659,13 @@ var SN = { // StatusNet } return false; }); + }, + + GetDateFromNow: function(days) { + var date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + + return date; } }, From c10d5320dd5b308795c5a5bda2441fabfe278a84 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 25 Jan 2010 09:07:24 -0800 Subject: [PATCH 097/157] Disable PubSubHubBub hub pings automatically on private site (hub wouldn't be able to read feeds anyway) [Might be good to think of a core way to mark a plugin as disabled when it initializes.] --- plugins/PubSubHubBub/PubSubHubBubPlugin.php | 44 ++++++++++++++++----- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/plugins/PubSubHubBub/PubSubHubBubPlugin.php b/plugins/PubSubHubBub/PubSubHubBubPlugin.php index ce6086df94..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(); @@ -243,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; } From 055a00bcaeaa50c36143bd151bf4433c5c87d174 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 25 Jan 2010 09:36:20 -0800 Subject: [PATCH 098/157] drop now-unused --skip-xmpp and --xmpp-only options from queuedaemon.php --- scripts/queuedaemon.php | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/scripts/queuedaemon.php b/scripts/queuedaemon.php index a9cfda6d72..bedd14b1a3 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 @@ -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(); From ee4ca8f2601bc5a2dd258fea176037e03e23feb8 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 25 Jan 2010 09:41:40 -0800 Subject: [PATCH 099/157] quick fix to console.php: don't save blank lines into readline history --- scripts/console.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 { From 1ab24832960eef698731cf0d3e72fccfa336392d Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Mon, 25 Jan 2010 13:48:24 -0800 Subject: [PATCH 100/157] Fix presence notification on XMPP thread (now foreground, not background) --- lib/xmppmanager.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/xmppmanager.php b/lib/xmppmanager.php index 299175dd7d..985e7c32e4 100644 --- a/lib/xmppmanager.php +++ b/lib/xmppmanager.php @@ -101,7 +101,7 @@ class XmppManager extends IoManager $this->conn->addEventHandler('reconnect', 'handle_reconnect', $this); $this->conn->setReconnectTimeout(600); - jabber_send_presence("Send me a message to post a notice", 'available', null, 'available', -1); + jabber_send_presence("Send me a message to post a notice", 'available', null, 'available', 100); return !is_null($this->conn); } @@ -233,7 +233,7 @@ class XmppManager extends IoManager common_log(LOG_NOTICE, 'XMPP reconnected'); $this->conn->processUntil('session_start'); - $this->conn->presence(null, 'available', null, 'available', -1); + $this->conn->presence(null, 'available', null, 'available', 100); } From d17b7fa19ba2bfec9aa3d734c0f30e001256ddc0 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Tue, 26 Jan 2010 01:58:10 +0100 Subject: [PATCH 101/157] Setting the geo location cookie expire date far into the future: 2029 ;) --- js/util.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/js/util.js b/js/util.js index 8d52d859b7..b864867fd1 100644 --- a/js/util.js +++ b/js/util.js @@ -495,7 +495,7 @@ var SN = { // StatusNet $('#'+SN.C.S.NoticeLocationId).val(''); $('#'+SN.C.S.NoticeDataGeo).attr('checked', false); - $.cookie(SN.C.S.NoticeDataGeoCookie, 'disabled', { path: '/', expires: SN.U.GetDateFromNow(30) }); + $.cookie(SN.C.S.NoticeDataGeoCookie, 'disabled', { path: '/', expires: SN.U.GetFullYear(2029, 0, 1) }); } function getJSONgeocodeURL(geocodeURL, data) { @@ -538,7 +538,7 @@ var SN = { // StatusNet NDG: true }; - $.cookie(SN.C.S.NoticeDataGeoCookie, JSON.stringify(cookieValue), { path: '/', expires: SN.U.GetDateFromNow(30) }); + $.cookie(SN.C.S.NoticeDataGeoCookie, JSON.stringify(cookieValue), { path: '/', expires: SN.U.GetFullYear(2029, 0, 1) }); }); } @@ -661,9 +661,9 @@ var SN = { // StatusNet }); }, - GetDateFromNow: function(days) { + GetFullYear: function(year, month, day) { var date = new Date(); - date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + date.setFullYear(year, month, day); return date; } From 655573c213113861cfd82a42ef999dde1aeea149 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 26 Jan 2010 00:21:05 -0500 Subject: [PATCH 102/157] Single-user mode New configuration options to define a single-user mode. This hides most of the "community" pages, like the public timeline and groups. The main user's timeline becomes the main page, and most other URLs are changed. Switching back and forth between 1-user and multi-user mode is probably hazardous. Squashed commit of the following: commit d814aa5c92d14a27a12baba7893f3f8bf63f1d08 Author: Evan Prodromou Date: Tue Jan 26 00:17:27 2010 -0500 don't show inbox and outbox in single-user mode commit 47f19b9523a7015d4c6e460b73ea32c839e00aa1 Author: Evan Prodromou Date: Tue Jan 26 00:15:22 2010 -0500 show correct URL for logo in single-user mode commit 552010cffc33eadbc512ec5a67619dbc2015239a Author: Evan Prodromou Date: Tue Jan 26 00:15:06 2010 -0500 make singleuser its own config section commit 786ab260a3ca172e57b555c75ca10946d8f258a1 Author: Evan Prodromou Date: Tue Jan 26 00:05:19 2010 -0500 make single-user mode work commit 5b21d7309b3a8dd5a4e0f29aea76f7897f1818b1 Author: Evan Prodromou Date: Mon Jan 25 23:45:55 2010 -0500 add single-user mode --- README | 9 ++ lib/action.php | 8 +- lib/default.php | 5 +- lib/personalgroupnav.php | 7 +- lib/router.php | 215 +++++++++++++++++++++++++-------------- 5 files changed, 161 insertions(+), 83 deletions(-) diff --git a/README b/README index 6022887899..f83873ca84 100644 --- a/README +++ b/README @@ -1492,6 +1492,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/lib/action.php b/lib/action.php index e242775585..3ffc452dec 100644 --- a/lib/action.php +++ b/lib/action.php @@ -392,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/default.php b/lib/default.php index b6ee72279d..35115542f2 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', @@ -260,4 +260,7 @@ $default = ), 'admin' => array('panels' => array('design', 'site', 'user', 'paths')), + 'singleuser' => + array('enabled' => false, + 'nickname' => null), ); 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/router.php b/lib/router.php index 42bff27788..c0ddc8db38 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', @@ -145,6 +139,18 @@ class Router $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,37 +623,6 @@ class Router $m->connect('api/search.json', array('action' => 'twitapisearchjson')); $m->connect('api/trends.json', array('action' => 'twitapitrends')); - $m->connect('admin/site', array('action' => 'siteadminpanel')); - $m->connect('admin/design', array('action' => 'designadminpanel')); - $m->connect('admin/user', array('action' => 'useradminpanel')); - $m->connect('admin/paths', array('action' => 'pathsadminpanel')); - - $m->connect('getfile/:filename', - array('action' => 'getfile'), - array('filename' => '[A-Za-z0-9._-]+')); - - // user stuff - - 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}')); - } - - $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]+') - ); - $m->connect('api/oauth/request_token', array('action' => 'apioauthrequesttoken')); @@ -662,47 +632,136 @@ class Router $m->connect('api/oauth/authorize', array('action' => 'apioauthauthorize')); - foreach (array('subscriptions', 'subscribers') as $a) { - $m->connect(':nickname/'.$a.'/:tag', - array('action' => $a), - array('tag' => '[a-zA-Z0-9]+', + // 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/paths', array('action' => 'pathsadminpanel')); + + $m->connect('getfile/:filename', + array('action' => 'getfile'), + array('filename' => '[A-Za-z0-9._-]+')); + + // In the "root" + + 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}')); + + $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)); } From 7fc5588c5dfc0a74cbc1c791bc445e399207c443 Mon Sep 17 00:00:00 2001 From: Julien C Date: Tue, 8 Dec 2009 22:16:03 +0100 Subject: [PATCH 103/157] Allow logging in using Twitter Signed-off-by: Julien C --- plugins/TwitterBridge/TwitterBridgePlugin.php | 26 ++ plugins/TwitterBridge/twitter_connect.gif | Bin 0 -> 2205 bytes .../TwitterBridge/twitterauthorization.php | 431 ++++++++++++++++-- plugins/TwitterBridge/twitterlogin.php | 95 ++++ 4 files changed, 526 insertions(+), 26 deletions(-) create mode 100644 plugins/TwitterBridge/twitter_connect.gif create mode 100644 plugins/TwitterBridge/twitterlogin.php diff --git a/plugins/TwitterBridge/TwitterBridgePlugin.php b/plugins/TwitterBridge/TwitterBridgePlugin.php index 57b3c1c995..e39ec7be03 100644 --- a/plugins/TwitterBridge/TwitterBridgePlugin.php +++ b/plugins/TwitterBridge/TwitterBridgePlugin.php @@ -72,9 +72,34 @@ 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 Twitter Connect + * + * @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; + } + /** * Add the Twitter Settings page to the Connect Settings menu @@ -108,6 +133,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/twitter_connect.gif b/plugins/TwitterBridge/twitter_connect.gif new file mode 100644 index 0000000000000000000000000000000000000000..e3d8f3ed7b88b41d67b37f8be92f7af532f25816 GIT binary patch literal 2205 zcmV;O2x9j~Nk%w1VZ#6!0OkMyGOg67-0df;(tOI~iqPgGrqG4s`T%UCxby!1|NoiR z>U7=s1AMdT`u<+N-BG#QRK4EB^Zq5M(rv}y7M#u^rO(Ca^Et8E-TMFJ`~FDL^d_m& z)9v;kq|V9r{#DfX;`8}Eve_!A)uZeE@&5l)!QoB5-kj|Hw&L)u@BPyF{urLnAE(ej zver1x^fa&6@cH}_k-@g%?_j{*LbTXLwcN7q{)x}#PPo~V>HH9lzii$4dEogls@Ef@ z(E0rS-uM5??Dvb&=MRp+5RbqptJ5f}(jTPHD6G>UrqCXx&m^ePDXh~Wr_djz&`rMG z1ADR_rq3s=(oVnL0BfTlrO+Rx&mN@CO1Rt;na)PI-NfheLbBN*rqCj!(?ho0A)wG- zz~3pS)Fi0Y8lTW0q|+p;(Lb@)GqKb?wcH||$@%#F&h7Z^_W4e?*)_7;!|(k;&-9nt z@g}IzF09r>x7hLd{3WB(_W1j|?EUrq{-*H$)babK<@2rL@&Et+A^8LV00000EC2ui z0K)(o000O7fKX5jgM|!-g@cESjEIDaj*Ehmh>(_+jgx|ejhv5>mywd2pP!1Dq@s+Q zn4XZ5kEEKZu&<7Zhf`BeySu!;Pr<)WCcbLA$h^P0!ph9Ty~xSW!N9}P)X&k?%e~#e zy(ZVg&d}b?;>^0m=DySC&C$ityX@B6hekz6MNCHgNB{;D$b<>N7)A;zT*$DY!-o(f zN}SlRh{cN-Giuz(F{6=>AVZ2ANwVa~B1QyZS;?}c%a(bL!m5v!~CW zKzZ66N)+V2Lt2v7NXoRS)220Ibeu}Hs@1DiDQexy5UW>?2?x~}OIFU0KWre7UCTCx z!Ju&e;~sqrU)3KbX(}ykl+JX3MK>yFa*#gfF+{vfEi|>fkFZ> zC~)2cbHXsddI?x?o|b-2kU#(%AV{cSVesjvaVQvP!2=RRDnW$z zDSJXF8mR(eqVPhY$64TKiw{b0NC+>)`ods)Xo$eAx1Nx}8r8`f!xHZWkbs@;m1qH` zFsvvT2ttS%pmBA!X&wi4&KaPfg9;IbmV*v(-V3E|fGG$Y2s^M*}&ekirQRQ)~eL26!~g zFcCg@c_n}z3kt%O!UnK}U=u7c@Dj!;umrX*C+L94K9op+4>40Eg(g}cxAkfg~5}hvbbkk)by~NZYIN$)jG!#sx$7rXmwwE0r zqA?u_WRSxMBiLYr#3LXuC>#0v%LmFC;$M&B!e3SU;-7m!M|<*Lws#v z02Rn#4q*TT5LAGE2{=OzcUA!#;t+i|sKC~ASb;PA%K;T|1Dyg;0WtY7h6&h06Q5Yb zEDoRw=;Oc*6p%z2YLSU(Orsjts6#?{;f-GCK^s)Cg9qy22X-Jq0|Zk7IKEK_1?Ykx z2ib)r3;=yw8bA`j#6lbha%WwDUl&Z6gslk@2Qm;72Vhu*0hCDpPZgMez0g=PEOJ1H zL=@$q&X+#-v5x@w8i4s?SpfPe;C)$qA1zfG02KxRUmz%B7THM5V(PF9gBSxG0bmCV zeBgl#;K2eGumc_lW(>-FBN39&gl>BCo8SzmILArOa+>p;-AsZy*U8Ryy7QgzjHf!$ zNzZetlMrG6gFf#Ngamxx1Ja!4G+i))1c)J@`Wylaww79YPYEK+~Gq^rkq?sZMEHg`WEK zr$7y=P=`uXpYHUiNKL9zmx|OX9N`cmT|r30Aeaci!KxAZ$ODx6AW|}{z=B5zt60ZM z*0P%QtY}TETGz_fwz_q!#*C|6b!deS41xl9%_{}ozz4^bMh;%p>kumN*2123u82*n zViRlF#*(!HRyc%SCrjDNQeX&5$gE~J%h}F)_OqH5s})8|+R~c#w5Uz3YNNo~*1Gn! zu#K&3S$l=r+V-}%&8=>Cn_DGB;SgB}u5gD-+~Njz2v-2Da+k~8<~sMe(2cHVXG`7c zS~m(xIK% + * @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/ @@ -50,6 +50,10 @@ require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php'; */ class TwitterauthorizationAction extends Action { + var $twuid = null; + var $tw_fields = null; + var $access_token = null; + /** * Initialize class members. Looks for 'oauth_token' parameter. * @@ -76,29 +80,56 @@ class TwitterauthorizationAction extends Action function handle($args) { 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')); + } } + + + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + // User was not logged in to StatusNet before + $this->twuid = $this->trimmed('twuid'); + $this->tw_fields = array("name" => $this->trimmed('name'), "fullname" => $this->trimmed('fullname')); + $this->access_token = new OAuthToken($this->trimmed('access_token_key'), $this->trimmed('access_token_secret')); - $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')); - } - - // $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(); + $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(); + } } } @@ -170,16 +201,25 @@ class TwitterauthorizationAction extends Action $this->serverError(_m('Couldn\'t link your Twitter account.')); } - // Save the access token and Twitter user info - - $this->saveForeignLink($atok, $twitter_user); + if (common_logged_in()) { + // Save the access token and Twitter user info + $this->saveForeignLink($atok, $twitter_user); + } + else{ + $this->twuid = $twitter_user->id; + $this->tw_fields = array("name" => $twitter_user->screen_name, "fullname" => $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')); + } } /** @@ -220,5 +260,344 @@ class TwitterauthorizationAction extends Action save_twitter_user($twitter_user->id, $twitter_user->screen_name); } + + + + 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_name', $this->tw_fields['name']); + $this->hidden('tw_fields_fullname', $this->tw_fields['fullname']); + + $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['fullname']); + + $args = array('nickname' => $nickname, 'fullname' => $fullname); + + if (!empty($invite)) { + $args['code'] = $invite->code; + } + + $user = User::register($args); + + $result = $this->flinkUser($user->id, $this->twuid); + + if (!$result) { + $this->serverError(_('Error connecting user to Twitter.')); + return; + } + + common_set_user($user); + common_real_login(true); + + common_debug('Twitter Connect Plugin - ' . + "Registered new user $user->id from Twitter user $this->fbuid"); + + 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('Twitter Connect Plugin - ' . + "Legit user to connect to Twitter: $nickname"); + } + + $result = $this->flinkUser($user->id, $this->twuid); + + if (!$result) { + $this->serverError(_('Error connecting user to Twitter.')); + return; + } + + common_debug('Twitter Connnect Plugin - ' . + "Connected Twitter user $this->fbuid 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('Twitter Connect Plugin - ' . + "Connected Twitter user $this->fbuid to local user $user->id"); + + // Return to Twitter connection settings tab + common_redirect(common_local_url('twittersettings'), 303); + } + + function tryLogin() + { + common_debug('Twitter Connect Plugin - ' . + "Trying login for Twitter user $this->fbuid."); + + $flink = Foreign_link::getByForeignID($this->twuid, TWITTER_SERVICE); + + if (!empty($flink)) { + $user = $flink->getUser(); + + if (!empty($user)) { + + common_debug('Twitter Connect 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('Twitter Connect 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 flinkUser($user_id, $twuid) + { + $flink = new Foreign_link(); + + $flink->user_id = $user_id; + $flink->foreign_id = $twuid; + $flink->service = TWITTER_SERVICE; + + $creds = TwitterOAuthClient::packToken($this->access_token); + + $flink->credentials = $creds; + $flink->created = common_sql_now(); + + // Defaults: noticesync on, everything else off + + $flink->set_flags(true, false, false, false); + + $flink_id = $flink->insert(); + + if (empty($flink_id)) { + common_log_db_error($flink, 'INSERT', __FILE__); + $this->serverError(_('Couldn\'t link your Twitter account.')); + } + + save_twitter_user($twuid, $this->tw_fields['name']); + + return $flink_id; + } + + + 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..ae468ea15c --- /dev/null +++ b/plugins/TwitterBridge/twitterlogin.php @@ -0,0 +1,95 @@ +. + * + * @category Settings + * @package StatusNet + * @author Evan Prodromou + * @copyright 2008-2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php'; + +/** + * Settings for Twitter integration + * + * @category Settings + * @package StatusNet + * @author Evan Prodromou + * @author Julien Chaumond + * @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'))); + $this->element('img', array('src' => common_path('plugins/TwitterBridge/twitter_connect.gif'), + 'alt' => 'Connect my Twitter account')); + $this->elementEnd('a'); + } + + function showLocalNav() + { + $nav = new LoginGroupNav($this); + $nav->show(); + } +} From 2d97e15cd6b5fbda82218b97ce287a56e2665fb9 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 26 Jan 2010 01:25:33 +0000 Subject: [PATCH 104/157] - Twitter username wasn't getting stored in Foreign_user when linking Twitter account (fixed) - Updates to comments --- .../TwitterBridge/twitterauthorization.php | 109 ++++++++++-------- 1 file changed, 58 insertions(+), 51 deletions(-) diff --git a/plugins/TwitterBridge/twitterauthorization.php b/plugins/TwitterBridge/twitterauthorization.php index 8ae725374f..3f7316b7a4 100644 --- a/plugins/TwitterBridge/twitterauthorization.php +++ b/plugins/TwitterBridge/twitterauthorization.php @@ -53,7 +53,7 @@ class TwitterauthorizationAction extends Action var $twuid = null; var $tw_fields = null; var $access_token = null; - + /** * Initialize class members. Looks for 'oauth_token' parameter. * @@ -80,31 +80,37 @@ class TwitterauthorizationAction extends Action function handle($args) { parent::handle($args); - + 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')); } } - - + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + // User was not logged in to StatusNet before + $this->twuid = $this->trimmed('twuid'); - $this->tw_fields = array("name" => $this->trimmed('name'), "fullname" => $this->trimmed('fullname')); + + $this->tw_fields = array('name' => $this->trimmed('tw_fields_name'), + 'fullname' => $this->trimmed('tw_fields_fullname')); + $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.'), @@ -124,7 +130,7 @@ class TwitterauthorizationAction extends Action // $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 { @@ -181,6 +187,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'], @@ -196,16 +204,19 @@ 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.')); } if (common_logged_in()) { + // Save the access token and Twitter user info + $this->saveForeignLink($atok, $twitter_user); - } - else{ + + } else { + $this->twuid = $twitter_user->id; $this->tw_fields = array("name" => $twitter_user->screen_name, "fullname" => $twitter_user->name); $this->access_token = $atok; @@ -216,7 +227,7 @@ class TwitterauthorizationAction extends Action unset($_SESSION['twitter_request_token']); unset($_SESSION['twitter_request_token_secret']); - + if (common_logged_in()) { common_redirect(common_local_url('twittersettings')); } @@ -260,8 +271,34 @@ class TwitterauthorizationAction extends Action save_twitter_user($twitter_user->id, $twitter_user->screen_name); } + function flinkUser($user_id, $twuid) + { + $flink = new Foreign_link(); + $flink->user_id = $user_id; + $flink->foreign_id = $twuid; + $flink->service = TWITTER_SERVICE; + $creds = TwitterOAuthClient::packToken($this->access_token); + + $flink->credentials = $creds; + $flink->created = common_sql_now(); + + // Defaults: noticesync on, everything else off + + $flink->set_flags(true, false, false, false); + + $flink_id = $flink->insert(); + + if (empty($flink_id)) { + common_log_db_error($flink, 'INSERT', __FILE__); + $this->serverError(_('Couldn\'t link your Twitter account.')); + } + + save_twitter_user($twuid, $this->tw_fields['name']); + + return $flink_id; + } function showPageNotice() { @@ -430,7 +467,7 @@ class TwitterauthorizationAction extends Action common_set_user($user); common_real_login(true); - common_debug('Twitter Connect Plugin - ' . + common_debug('TwitterBridge Plugin - ' . "Registered new user $user->id from Twitter user $this->fbuid"); common_redirect(common_local_url('showstream', array('nickname' => $user->nickname)), @@ -450,7 +487,7 @@ class TwitterauthorizationAction extends Action $user = User::staticGet('nickname', $nickname); if (!empty($user)) { - common_debug('Twitter Connect Plugin - ' . + common_debug('TwitterBridge Plugin - ' . "Legit user to connect to Twitter: $nickname"); } @@ -461,7 +498,7 @@ class TwitterauthorizationAction extends Action return; } - common_debug('Twitter Connnect Plugin - ' . + common_debug('TwitterBridge Plugin - ' . "Connected Twitter user $this->fbuid to local user $user->id"); common_set_user($user); @@ -481,17 +518,17 @@ class TwitterauthorizationAction extends Action return; } - common_debug('Twitter Connect Plugin - ' . + common_debug('TwitterBridge Plugin - ' . "Connected Twitter user $this->fbuid to local user $user->id"); // Return to Twitter connection settings tab common_redirect(common_local_url('twittersettings'), 303); } - + function tryLogin() { - common_debug('Twitter Connect Plugin - ' . - "Trying login for Twitter user $this->fbuid."); + common_debug('TwitterBridge Plugin - ' . + "Trying login for Twitter user $this->twuid."); $flink = Foreign_link::getByForeignID($this->twuid, TWITTER_SERVICE); @@ -500,7 +537,7 @@ class TwitterauthorizationAction extends Action if (!empty($user)) { - common_debug('Twitter Connect Plugin - ' . + common_debug('TwitterBridge Plugin - ' . "Logged in Twitter user $flink->foreign_id as user $user->id ($user->nickname)"); common_set_user($user); @@ -510,7 +547,7 @@ class TwitterauthorizationAction extends Action } else { - common_debug('Twitter Connect Plugin - ' . + common_debug('TwitterBridge Plugin - ' . "No flink found for twuid: $this->twuid - new user"); $this->showForm(null, $this->bestNewNickname()); @@ -531,37 +568,7 @@ class TwitterauthorizationAction extends Action common_redirect($url, 303); } - - function flinkUser($user_id, $twuid) - { - $flink = new Foreign_link(); - $flink->user_id = $user_id; - $flink->foreign_id = $twuid; - $flink->service = TWITTER_SERVICE; - - $creds = TwitterOAuthClient::packToken($this->access_token); - - $flink->credentials = $creds; - $flink->created = common_sql_now(); - - // Defaults: noticesync on, everything else off - - $flink->set_flags(true, false, false, false); - - $flink_id = $flink->insert(); - - if (empty($flink_id)) { - common_log_db_error($flink, 'INSERT', __FILE__); - $this->serverError(_('Couldn\'t link your Twitter account.')); - } - - save_twitter_user($twuid, $this->tw_fields['name']); - - return $flink_id; - } - - function bestNewNickname() { if (!empty($this->tw_fields['name'])) { From 1c1abfc284c3b81cfcd4cd95b80a0e5c120bd962 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 26 Jan 2010 01:51:40 +0000 Subject: [PATCH 105/157] Ask the user to set a password before disconnecting from Twitter --- plugins/TwitterBridge/twittersettings.php | 33 ++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) 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); } /** From ce44008d13e39d3e3a95af7d422a82f5ecce61e0 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 26 Jan 2010 02:40:44 +0000 Subject: [PATCH 106/157] Use "Sign in with Twitter" auth pattern and official Twitter button for Twitter-based login. See: http://apiwiki.twitter.com/Sign-in-with-Twitter --- plugins/TwitterBridge/twitter_connect.gif | Bin 2205 -> 0 bytes plugins/TwitterBridge/twitterauthorization.php | 8 +++++--- plugins/TwitterBridge/twitterlogin.php | 11 ++++++----- plugins/TwitterBridge/twitteroauthclient.php | 7 +++++-- 4 files changed, 16 insertions(+), 10 deletions(-) delete mode 100644 plugins/TwitterBridge/twitter_connect.gif diff --git a/plugins/TwitterBridge/twitter_connect.gif b/plugins/TwitterBridge/twitter_connect.gif deleted file mode 100644 index e3d8f3ed7b88b41d67b37f8be92f7af532f25816..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2205 zcmV;O2x9j~Nk%w1VZ#6!0OkMyGOg67-0df;(tOI~iqPgGrqG4s`T%UCxby!1|NoiR z>U7=s1AMdT`u<+N-BG#QRK4EB^Zq5M(rv}y7M#u^rO(Ca^Et8E-TMFJ`~FDL^d_m& z)9v;kq|V9r{#DfX;`8}Eve_!A)uZeE@&5l)!QoB5-kj|Hw&L)u@BPyF{urLnAE(ej zver1x^fa&6@cH}_k-@g%?_j{*LbTXLwcN7q{)x}#PPo~V>HH9lzii$4dEogls@Ef@ z(E0rS-uM5??Dvb&=MRp+5RbqptJ5f}(jTPHD6G>UrqCXx&m^ePDXh~Wr_djz&`rMG z1ADR_rq3s=(oVnL0BfTlrO+Rx&mN@CO1Rt;na)PI-NfheLbBN*rqCj!(?ho0A)wG- zz~3pS)Fi0Y8lTW0q|+p;(Lb@)GqKb?wcH||$@%#F&h7Z^_W4e?*)_7;!|(k;&-9nt z@g}IzF09r>x7hLd{3WB(_W1j|?EUrq{-*H$)babK<@2rL@&Et+A^8LV00000EC2ui z0K)(o000O7fKX5jgM|!-g@cESjEIDaj*Ehmh>(_+jgx|ejhv5>mywd2pP!1Dq@s+Q zn4XZ5kEEKZu&<7Zhf`BeySu!;Pr<)WCcbLA$h^P0!ph9Ty~xSW!N9}P)X&k?%e~#e zy(ZVg&d}b?;>^0m=DySC&C$ityX@B6hekz6MNCHgNB{;D$b<>N7)A;zT*$DY!-o(f zN}SlRh{cN-Giuz(F{6=>AVZ2ANwVa~B1QyZS;?}c%a(bL!m5v!~CW zKzZ66N)+V2Lt2v7NXoRS)220Ibeu}Hs@1DiDQexy5UW>?2?x~}OIFU0KWre7UCTCx z!Ju&e;~sqrU)3KbX(}ykl+JX3MK>yFa*#gfF+{vfEi|>fkFZ> zC~)2cbHXsddI?x?o|b-2kU#(%AV{cSVesjvaVQvP!2=RRDnW$z zDSJXF8mR(eqVPhY$64TKiw{b0NC+>)`ods)Xo$eAx1Nx}8r8`f!xHZWkbs@;m1qH` zFsvvT2ttS%pmBA!X&wi4&KaPfg9;IbmV*v(-V3E|fGG$Y2s^M*}&ekirQRQ)~eL26!~g zFcCg@c_n}z3kt%O!UnK}U=u7c@Dj!;umrX*C+L94K9op+4>40Eg(g}cxAkfg~5}hvbbkk)by~NZYIN$)jG!#sx$7rXmwwE0r zqA?u_WRSxMBiLYr#3LXuC>#0v%LmFC;$M&B!e3SU;-7m!M|<*Lws#v z02Rn#4q*TT5LAGE2{=OzcUA!#;t+i|sKC~ASb;PA%K;T|1Dyg;0WtY7h6&h06Q5Yb zEDoRw=;Oc*6p%z2YLSU(Orsjts6#?{;f-GCK^s)Cg9qy22X-Jq0|Zk7IKEK_1?Ykx z2ib)r3;=yw8bA`j#6lbha%WwDUl&Z6gslk@2Qm;72Vhu*0hCDpPZgMez0g=PEOJ1H zL=@$q&X+#-v5x@w8i4s?SpfPe;C)$qA1zfG02KxRUmz%B7THM5V(PF9gBSxG0bmCV zeBgl#;K2eGumc_lW(>-FBN39&gl>BCo8SzmILArOa+>p;-AsZy*U8Ryy7QgzjHf!$ zNzZetlMrG6gFf#Ngamxx1Ja!4G+i))1c)J@`Wylaww79YPYEK+~Gq^rkq?sZMEHg`WEK zr$7y=P=`uXpYHUiNKL9zmx|OX9N`cmT|r30Aeaci!KxAZ$ODx6AW|}{z=B5zt60ZM z*0P%QtY}TETGz_fwz_q!#*C|6b!deS41xl9%_{}ozz4^bMh;%p>kumN*2123u82*n zViRlF#*(!HRyc%SCrjDNQeX&5$gE~J%h}F)_OqH5s})8|+R~c#w5Uz3YNNo~*1Gn! zu#K&3S$l=r+V-}%&8=>Cn_DGB;SgB}u5gD-+~Njz2v-2Da+k~8<~sMe(2cHVXG`7c zS~m(xIK%signin = $this->boolean('signin'); $this->oauth_token = $this->arg('oauth_token'); return true; @@ -160,7 +162,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', diff --git a/plugins/TwitterBridge/twitterlogin.php b/plugins/TwitterBridge/twitterlogin.php index ae468ea15c..ae67b4c154 100644 --- a/plugins/TwitterBridge/twitterlogin.php +++ b/plugins/TwitterBridge/twitterlogin.php @@ -46,7 +46,6 @@ require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php'; * @see SettingsAction */ - class TwitterloginAction extends Action { function handle($args) @@ -67,7 +66,7 @@ class TwitterloginAction extends Action function getInstructions() { - return _('Login with your Twitter Account'); + return _('Login with your Twitter account'); } function showPageNotice() @@ -81,9 +80,11 @@ class TwitterloginAction extends Action function showContent() { - $this->elementStart('a', array('href' => common_local_url('twitterauthorization'))); - $this->element('img', array('src' => common_path('plugins/TwitterBridge/twitter_connect.gif'), - 'alt' => 'Connect my Twitter account')); + $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'); } 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')); } From f7450d2ca8b420260caf69bf1cded687d8eaf7b5 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 26 Jan 2010 07:29:40 +0000 Subject: [PATCH 107/157] - Remove redundant function - clean up log msgs --- .../TwitterBridge/twitterauthorization.php | 76 +++++++------------ 1 file changed, 29 insertions(+), 47 deletions(-) diff --git a/plugins/TwitterBridge/twitterauthorization.php b/plugins/TwitterBridge/twitterauthorization.php index 15408668fc..a95cdebb97 100644 --- a/plugins/TwitterBridge/twitterauthorization.php +++ b/plugins/TwitterBridge/twitterauthorization.php @@ -101,7 +101,7 @@ class TwitterauthorizationAction extends Action $this->twuid = $this->trimmed('twuid'); - $this->tw_fields = array('name' => $this->trimmed('tw_fields_name'), + $this->tw_fields = array('screen_name' => $this->trimmed('tw_fields_screen_name'), 'fullname' => $this->trimmed('tw_fields_fullname')); $this->access_token = new OAuthToken($this->trimmed('access_token_key'), $this->trimmed('access_token_secret')); @@ -215,12 +215,15 @@ class TwitterauthorizationAction extends Action // Save the access token and Twitter user info - $this->saveForeignLink($atok, $twitter_user); + $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("name" => $twitter_user->screen_name, "fullname" => $twitter_user->name); + $this->tw_fields = array("screen_name" => $twitter_user->screen_name, + "name" => $twitter_user->name); $this->access_token = $atok; $this->tryLogin(); } @@ -239,19 +242,18 @@ class TwitterauthorizationAction extends Action * 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); @@ -265,40 +267,11 @@ class TwitterauthorizationAction extends Action $flink_id = $flink->insert(); - if (empty($flink_id)) { - common_log_db_error($flink, 'INSERT', __FILE__); - $this->serverError(_m('Couldn\'t link your Twitter account.')); - } - - save_twitter_user($twitter_user->id, $twitter_user->screen_name); - } - - function flinkUser($user_id, $twuid) - { - $flink = new Foreign_link(); - - $flink->user_id = $user_id; - $flink->foreign_id = $twuid; - $flink->service = TWITTER_SERVICE; - - $creds = TwitterOAuthClient::packToken($this->access_token); - - $flink->credentials = $creds; - $flink->created = common_sql_now(); - - // Defaults: noticesync on, everything else off - - $flink->set_flags(true, false, false, false); - - $flink_id = $flink->insert(); - if (empty($flink_id)) { common_log_db_error($flink, 'INSERT', __FILE__); $this->serverError(_('Couldn\'t link your Twitter account.')); } - save_twitter_user($twuid, $this->tw_fields['name']); - return $flink_id; } @@ -361,8 +334,8 @@ class TwitterauthorizationAction extends Action $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->hidden('tw_fields_fullname', $this->tw_fields['fullname']); $this->elementStart('fieldset'); $this->hidden('token', common_session_token()); @@ -449,7 +422,7 @@ class TwitterauthorizationAction extends Action return; } - $fullname = trim($this->tw_fields['fullname']); + $fullname = trim($this->tw_fields['name']); $args = array('nickname' => $nickname, 'fullname' => $fullname); @@ -459,7 +432,11 @@ class TwitterauthorizationAction extends Action $user = User::register($args); - $result = $this->flinkUser($user->id, $this->twuid); + $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.')); @@ -470,7 +447,7 @@ class TwitterauthorizationAction extends Action common_real_login(true); common_debug('TwitterBridge Plugin - ' . - "Registered new user $user->id from Twitter user $this->fbuid"); + "Registered new user $user->id from Twitter user $this->twuid"); common_redirect(common_local_url('showstream', array('nickname' => $user->nickname)), 303); @@ -493,7 +470,11 @@ class TwitterauthorizationAction extends Action "Legit user to connect to Twitter: $nickname"); } - $result = $this->flinkUser($user->id, $this->twuid); + $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.')); @@ -501,7 +482,7 @@ class TwitterauthorizationAction extends Action } common_debug('TwitterBridge Plugin - ' . - "Connected Twitter user $this->fbuid to local user $user->id"); + "Connected Twitter user $this->twuid to local user $user->id"); common_set_user($user); common_real_login(true); @@ -521,7 +502,7 @@ class TwitterauthorizationAction extends Action } common_debug('TwitterBridge Plugin - ' . - "Connected Twitter user $this->fbuid to local user $user->id"); + "Connected Twitter user $this->twuid to local user $user->id"); // Return to Twitter connection settings tab common_redirect(common_local_url('twittersettings'), 303); @@ -532,7 +513,8 @@ class TwitterauthorizationAction extends Action common_debug('TwitterBridge Plugin - ' . "Trying login for Twitter user $this->twuid."); - $flink = Foreign_link::getByForeignID($this->twuid, TWITTER_SERVICE); + $flink = Foreign_link::getByForeignID($this->twuid, + TWITTER_SERVICE); if (!empty($flink)) { $user = $flink->getUser(); From 02957d28541b3164df117d54788d1152c85ff00b Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 26 Jan 2010 07:50:01 +0000 Subject: [PATCH 108/157] Add Julien C to author comments --- plugins/TwitterBridge/TwitterBridgePlugin.php | 12 +++++------- plugins/TwitterBridge/twitterauthorization.php | 8 +++++--- plugins/TwitterBridge/twitterlogin.php | 15 ++++++++------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/plugins/TwitterBridge/TwitterBridgePlugin.php b/plugins/TwitterBridge/TwitterBridgePlugin.php index e39ec7be03..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,16 +74,13 @@ 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 Twitter Connect + * Add a login tab for 'Sign in with Twitter' * * @param Action &action the current action * @@ -99,7 +98,6 @@ class TwitterBridgePlugin extends Plugin return true; } - /** * Add the Twitter Settings page to the Connect Settings menu diff --git a/plugins/TwitterBridge/twitterauthorization.php b/plugins/TwitterBridge/twitterauthorization.php index a95cdebb97..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 Copley - * @copyright 2009 StatusNet, Inc. + * @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,9 +42,10 @@ 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/ * diff --git a/plugins/TwitterBridge/twitterlogin.php b/plugins/TwitterBridge/twitterlogin.php index ae67b4c154..79421fb27d 100644 --- a/plugins/TwitterBridge/twitterlogin.php +++ b/plugins/TwitterBridge/twitterlogin.php @@ -2,7 +2,7 @@ /** * StatusNet, the distributed open-source microblogging tool * - * Settings for Twitter integration + * 'Sign in with Twitter' login page * * PHP version 5 * @@ -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 Settings + * @category Login * @package StatusNet - * @author Evan Prodromou - * @copyright 2008-2009 StatusNet, Inc. + * @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/ */ @@ -34,12 +35,12 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { require_once INSTALLDIR . '/plugins/TwitterBridge/twitter.php'; /** - * Settings for Twitter integration + * Page for logging in with Twitter * - * @category Settings + * @category Login * @package StatusNet - * @author Evan Prodromou * @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/ * From e05c32572234dc0b4adb19be7bbe9879f49ec7ee Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Tue, 26 Jan 2010 19:13:05 +0100 Subject: [PATCH 109/157] Updated geolocation sharing in notice form for Realtime pop --- plugins/Realtime/realtimeupdate.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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; } From ad6f0501ff24cb287dedd21271d58c91e64b6b43 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 26 Jan 2010 09:25:39 -0800 Subject: [PATCH 110/157] Site metadata tags in status_network: single 'tags' field, pipe-separated. $sn->tags() returns tag list as array; $sn->hasTag('blah') to check for a particular tag only Could be used to control things in config file: $sn = Status_network::setupSite($_server, $_path, $_wildcard); if (!$sn) { die("No such site"); } if ($sn->hasTag('individual')) { /* blah */ } Note memcached keys are unchanged; if tags are changed from an external tool clear: statusnet::status_network:: for s 'nickname', 'hostname', and 'pathname' --- classes/Status_network.php | 20 ++++++++++++++++++++ db/site.sql | 2 ++ 2 files changed, 22 insertions(+) diff --git a/classes/Status_network.php b/classes/Status_network.php index 445f8a5a3c..f1314d6151 100644 --- a/classes/Status_network.php +++ b/classes/Status_network.php @@ -39,6 +39,7 @@ 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); } @@ -245,4 +246,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/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' From 58be61b6417119de1b03ef50e166369c4005e4d1 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 26 Jan 2010 11:49:49 -0800 Subject: [PATCH 111/157] Control channel for queue daemons to request graceful shutdown, restart, or update to listen to a newly added or reconfigured site. queuectl.php --update -s queuectl.php --stop queuectl.php --restart Default control channel is /topic/statusnet-control. For external utilities to send a site update ping direct to the queue server, connect via Stomp and send a message formatted thus: update: (Nickname here, *not* server hostname! The rest of the queues will be updated to use nicknames later.) Note that all currently-connected queue daemons will get these notifications, including both queuedaemon.php and xmppdaemon.php. (XMPP will ignore site update requests for sites that it's not handling.) Limitations: * only implemented for stomp queue manager so far * --update may not yet handle a changed server name properly * --restart won't reload PHP code files that were already loaded at startup. Still need to stop and restart the daemons from 'outside' when updating code base. --- classes/Status_network.php | 11 ++- lib/default.php | 1 + lib/iomaster.php | 53 ++++++++++--- lib/queuemanager.php | 17 ++++ lib/spawningdaemon.php | 58 ++++++++++---- lib/stompqueuemanager.php | 159 ++++++++++++++++++++++++++++++++++--- scripts/queuectl.php | 85 ++++++++++++++++++++ scripts/queuedaemon.php | 2 +- scripts/xmppdaemon.php | 2 +- 9 files changed, 347 insertions(+), 41 deletions(-) create mode 100755 scripts/queuectl.php diff --git a/classes/Status_network.php b/classes/Status_network.php index f1314d6151..4bda24b6a0 100644 --- a/classes/Status_network.php +++ b/classes/Status_network.php @@ -42,7 +42,16 @@ class Status_network extends DB_DataObject 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 diff --git a/lib/default.php b/lib/default.php index 35115542f2..d2dd8ab33c 100644 --- a/lib/default.php +++ b/lib/default.php @@ -81,6 +81,7 @@ $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) 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/queuemanager.php b/lib/queuemanager.php index 1bc8de1f78..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 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..89f3d74cce 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; } /** @@ -180,7 +213,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 +239,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 +266,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 +277,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 +310,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 +348,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 +379,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 +410,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 +493,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/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 bedd14b1a3..c2e2351c39 100755 --- a/scripts/queuedaemon.php +++ b/scripts/queuedaemon.php @@ -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; } } 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; } } From 03685bba1ea6c1b8f2c3db8d2f095a221b29a464 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 26 Jan 2010 15:11:09 -0800 Subject: [PATCH 112/157] - Remove redudant/unused 'server' setting from site admin panel - Move 'fancy urls' checkbox from site admin panel to paths admin panel --- actions/pathsadminpanel.php | 33 ++++++++++++++++++++++++++++++--- actions/siteadminpanel.php | 21 +++------------------ 2 files changed, 33 insertions(+), 21 deletions(-) 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/siteadminpanel.php b/actions/siteadminpanel.php index dd388a18a2..1cb57ec091 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,7 +95,7 @@ class SiteadminpanelAction extends AdminPanelAction 'site', 'textlimit', 'dupelimit'), 'snapshot' => array('run', 'reporturl', 'frequency')); - static $booleans = array('site' => array('private', 'inviteonly', 'closed', 'fancy')); + static $booleans = array('site' => array('private', 'inviteonly', 'closed')); $values = array(); @@ -299,22 +299,7 @@ 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->elementStart('fieldset', array('id' => 'settings_admin_access')); $this->out->element('legend', null, _('Access')); $this->out->elementStart('ul', 'form_data'); $this->li(); From aad42427ccd4d3824efcbce43d8e9dba6d5b475d Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 26 Jan 2010 15:56:19 -0800 Subject: [PATCH 113/157] New access admin panel for site registration settings --- actions/accessadminpanel.php | 192 +++++++++++++++++++++++++++++++++++ actions/siteadminpanel.php | 37 ------- lib/adminpanelaction.php | 13 ++- lib/default.php | 2 +- lib/router.php | 1 + 5 files changed, 203 insertions(+), 42 deletions(-) create mode 100644 actions/accessadminpanel.php 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/siteadminpanel.php b/actions/siteadminpanel.php index 1cb57ec091..8c8f8b3742 100644 --- a/actions/siteadminpanel.php +++ b/actions/siteadminpanel.php @@ -95,8 +95,6 @@ class SiteadminpanelAction extends AdminPanelAction 'site', 'textlimit', 'dupelimit'), 'snapshot' => array('run', 'reporturl', 'frequency')); - static $booleans = array('site' => array('private', 'inviteonly', 'closed')); - $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,29 +285,6 @@ class SiteAdminPanelForm extends AdminForm $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/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/default.php b/lib/default.php index d2dd8ab33c..37fb65b533 100644 --- a/lib/default.php +++ b/lib/default.php @@ -260,7 +260,7 @@ $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/router.php b/lib/router.php index c0ddc8db38..03765b39dd 100644 --- a/lib/router.php +++ b/lib/router.php @@ -637,6 +637,7 @@ class Router $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', From 492950b784ccf76ad96a24732c4329e3b1ca5ac8 Mon Sep 17 00:00:00 2001 From: Siebrand Mazeland Date: Thu, 14 Jan 2010 23:36:13 +0100 Subject: [PATCH 114/157] Fix inconsistent title case in page title --- actions/oauthconnectionssettings.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/oauthconnectionssettings.php b/actions/oauthconnectionssettings.php index b17729b821..c2e8d441b0 100644 --- a/actions/oauthconnectionssettings.php +++ b/actions/oauthconnectionssettings.php @@ -68,7 +68,7 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction function title() { - return _('Connected Applications'); + return _('Connected applications'); } function isReadOnly($args) From 97e1acdc329c3f4dbabce37ff5985f655aa91541 Mon Sep 17 00:00:00 2001 From: Siebrand Mazeland Date: Thu, 14 Jan 2010 23:37:06 +0100 Subject: [PATCH 115/157] Fix casing for HMAC-SHA1. --- actions/showapplication.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/actions/showapplication.php b/actions/showapplication.php index 049206375d..a6ff425c7c 100644 --- a/actions/showapplication.php +++ b/actions/showapplication.php @@ -265,7 +265,7 @@ class ShowApplicationAction extends OwnerDesignAction $this->elementEnd('dl'); $this->element('p', 'note', - _('Note: We support hmac-sha1 signatures. We do not support the plaintext signature method.')); + _('Note: We support HMAC-SHA1 signatures. We do not support the plaintext signature method.')); $this->elementEnd('div'); $this->elementStart('p', array('id' => 'application_action')); @@ -325,4 +325,3 @@ class ShowApplicationAction extends OwnerDesignAction } } - From 4202ffff9184eaf0934184309ef8f3b9a90d60d9 Mon Sep 17 00:00:00 2001 From: Siebrand Mazeland Date: Thu, 14 Jan 2010 23:38:29 +0100 Subject: [PATCH 116/157] Make more complete sentence. --- lib/applicationeditform.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/applicationeditform.php b/lib/applicationeditform.php index 040d3bf74b..6f03a9beda 100644 --- a/lib/applicationeditform.php +++ b/lib/applicationeditform.php @@ -203,7 +203,7 @@ class ApplicationEditForm extends Form $maxDesc = Oauth_application::maxDesc(); if ($maxDesc > 0) { - $descInstr = sprintf(_('Describe your application in %d chars'), + $descInstr = sprintf(_('Describe your application in %d characters'), $maxDesc); } else { $descInstr = _('Describe your application'); From 923b7de3c661098cfe3d5bdc0878d9329e1ee2bf Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 27 Jan 2010 08:41:26 +0000 Subject: [PATCH 117/157] - Check for read-only vs. read-write access to protected API resources (OAuth) - Some cleanup --- actions/apistatusesupdate.php | 18 +++-- lib/apiauth.php | 126 ++++++++++++++++++---------------- 2 files changed, 75 insertions(+), 69 deletions(-) diff --git a/actions/apistatusesupdate.php b/actions/apistatusesupdate.php index 31c9b20ce2..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,7 +79,6 @@ 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'); @@ -145,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; } @@ -172,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) { @@ -184,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 { @@ -211,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; @@ -234,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); @@ -255,7 +254,6 @@ class ApiStatusesUpdateAction extends ApiAuthAction $upload->attachToNotice($this->notice); } - } $this->showNotice(); diff --git a/lib/apiauth.php b/lib/apiauth.php index 37070d212f..ad9651ff26 100644 --- a/lib/apiauth.php +++ b/lib/apiauth.php @@ -29,7 +29,7 @@ * @author mEDI * @author Sarven Capadisli * @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/ */ @@ -53,9 +53,11 @@ require_once INSTALLDIR . '/lib/apioauth.php'; class ApiAuthAction extends ApiAction { - var $access_token; - var $oauth_access_type; - var $oauth_source; + var $auth_user_nickname = null; + var $auth_user_password = null; + var $access_token = null; + var $oauth_source = null; + var $auth_user = null; /** * Take arguments for running, and output basic auth header if needed @@ -70,18 +72,28 @@ 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->consumer_key = $this->arg('oauth_consumer_key'); - $this->access_token = $this->arg('oauth_token'); - if (!empty($this->access_token)) { $this->checkOAuthRequest(); } else { $this->checkBasicAuthUser(); - // By default, all basic auth users have read and write access + } - $this->access = self::READ_WRITE; + // 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(); + } } } @@ -95,8 +107,6 @@ class ApiAuthAction extends ApiAction function checkOAuthRequest() { - common_debug("We have an OAuth request."); - $datastore = new ApiStatusNetOAuthDataStore(); $server = new OAuthServer($datastore); $hmac_method = new OAuthSignatureMethod_HMAC_SHA1(); @@ -114,9 +124,10 @@ class ApiAuthAction extends ApiAction if (empty($app)) { - // this should really not happen - common_log(LOG_WARN, - "Couldn't find the OAuth app for consumer key: $this->consumer_key"); + // 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.'); } @@ -128,20 +139,18 @@ class ApiAuthAction extends ApiAction $appUser = Oauth_application_user::staticGet('token', $this->access_token); - // XXX: check that app->id and appUser->application_id and consumer all + // XXX: Check that app->id and appUser->application_id and consumer all // match? if (!empty($appUser)) { - // read or read-write - $this->oauth_access_type = $appUser->access_type; - // If access_type == 0 we have either a request token // or a bad / revoked access token - if ($this->oauth_access_type != 0) { + if ($appUser->access_type != 0) { + + // Set the access level for the api call - // Set the read or read-write access for the api call $this->access = ($appUser->access_type & Oauth_application::$writeAccess) ? self::READ_WRITE : self::READ_ONLY; @@ -151,38 +160,34 @@ class ApiAuthAction extends ApiAction } $msg = "API OAuth authentication for user '%s' (id: %d) on behalf of " . - "application '%s' (id: %d)."; + "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)); + $app->id, + ($this->access = self::READ_WRITE) ? + 'read-write' : 'read-only' + )); return true; } else { throw new OAuthException('Bad access token.'); } } else { - // also should not happen + // Also should not happen + throw new OAuthException('No user for that token.'); - } + } } catch (OAuthException $e) { - common_log(LOG_WARN, 'API OAuthException - ' . $e->getMessage()); - common_debug(var_export($req, true)); - $this->showOAuthError($e->getMessage()); - exit(); + common_log(LOG_WARNING, 'API OAuthException - ' . $e->getMessage()); + $this->showAuthError(); + exit; } } - function showOAuthError($msg) - { - header('HTTP/1.1 401 Unauthorized'); - header('Content-Type: text/html; charset=utf-8'); - print $msg . "\n"; - } - /** * Does this API resource require authentication? * @@ -207,39 +212,44 @@ class ApiAuthAction extends ApiAction $realm = common_config('site', 'name') . ' API'; - if (!isset($this->auth_user)) { + if (!isset($this->auth_user_nickname)) { 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; Event::handle('EndSetApiUser', array($user)); } + // By default, basic auth users have rw access + + $this->access = self::READ_WRITE; + if (empty($this->auth_user)) { // 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(); + + $this->showAuthError(); exit; } } - - return true; } /** @@ -253,32 +263,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; } } @@ -289,7 +297,7 @@ class ApiAuthAction extends ApiAction * @return void */ - function showBasicAuthError() + function showAuthError() { header('HTTP/1.1 401 Unauthorized'); $msg = 'Could not authenticate you.'; From 756da7bc5174f58f714c858f0100f04b3561a250 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 27 Jan 2010 08:45:56 +0000 Subject: [PATCH 118/157] s/LOG_WARN/LOG_WARNING/ --- actions/apioauthaccesstoken.php | 2 +- actions/apioauthrequesttoken.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/actions/apioauthaccesstoken.php b/actions/apioauthaccesstoken.php index 085ef6f0b1..887df4c20d 100644 --- a/actions/apioauthaccesstoken.php +++ b/actions/apioauthaccesstoken.php @@ -70,7 +70,7 @@ class ApiOauthAccessTokenAction extends ApiOauthAction $atok = $server->fetch_access_token($req); } catch (OAuthException $e) { - common_log(LOG_WARN, 'API OAuthException - ' . $e->getMessage()); + common_log(LOG_WARNING, 'API OAuthException - ' . $e->getMessage()); common_debug(var_export($req, true)); $this->outputError($e->getMessage()); return; diff --git a/actions/apioauthrequesttoken.php b/actions/apioauthrequesttoken.php index 467640b9aa..4fa626d866 100644 --- a/actions/apioauthrequesttoken.php +++ b/actions/apioauthrequesttoken.php @@ -89,7 +89,7 @@ class ApiOauthRequestTokenAction extends ApiOauthAction $token = $server->fetch_request_token($req); print $token; } catch (OAuthException $e) { - common_log(LOG_WARN, 'API OAuthException - ' . $e->getMessage()); + 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"; From 54d04a0c911269036a1fc3708fec6d70c33d5032 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 27 Jan 2010 09:59:40 +0000 Subject: [PATCH 119/157] Test script to update your status via OAuth --- tests/oauth/statusupdate.php | 115 +++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 tests/oauth/statusupdate.php 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); +} + From 04a37fa1c703955a51862033d83e59836c35d74f Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Wed, 27 Jan 2010 15:08:33 +0000 Subject: [PATCH 120/157] Better alignment for notice in shownotice page --- theme/base/css/display.css | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/theme/base/css/display.css b/theme/base/css/display.css index 84e9426c77..65dd159900 100644 --- a/theme/base/css/display.css +++ b/theme/base/css/display.css @@ -1039,12 +1039,13 @@ overflow:visible; #showstream .notice div.entry-content { margin-left:0; } -#shownotice .notice .entry-title, -#shownotice .notice div.entry-content { -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 { From c52951cef5508452733f84dec7815daf4aca1016 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 27 Jan 2010 11:37:22 -0500 Subject: [PATCH 121/157] Optionally set a separate Javascript server and path We have about 10-12 JavaScript pages per Web page. They usually are based on the same server as the Web pages, but since they're static files, it makes sense to offload them to a lite server that handles static files well. This commit lets you set a separate Javascript server and path for the default Javascript code in StatusNet. Squashed commit of the following: commit 139d1622fdafe5ad00c820224416d9021efc3234 Author: Evan Prodromou Date: Wed Jan 27 11:30:24 2010 -0500 modules that call htmloutputter::script() don't prescribe js/ path commit c6ca3174af73efed55eaed5ff1e2a3bdc77d2d87 Author: Evan Prodromou Date: Wed Jan 27 11:28:07 2010 -0500 configurable server and path for javascript files --- actions/avatarsettings.php | 4 ++-- actions/designadminpanel.php | 4 ++-- actions/grouplogo.php | 4 ++-- lib/action.php | 16 ++++++++-------- lib/default.php | 3 +++ lib/designsettings.php | 4 ++-- lib/htmloutputter.php | 28 +++++++++++++++++++++++++++- plugins/Facebook/facebookaction.php | 2 +- 8 files changed, 47 insertions(+), 18 deletions(-) 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/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/lib/action.php b/lib/action.php index 3ffc452dec..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; }'); diff --git a/lib/default.php b/lib/default.php index 37fb65b533..10ea348640 100644 --- a/lib/default.php +++ b/lib/default.php @@ -120,6 +120,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 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/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'); } /** From 7aeb03f7277de247a642b613775383480bccc63f Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 27 Jan 2010 08:53:55 -0800 Subject: [PATCH 122/157] quick fix: use common_path() on realtime update JS so it works with the new JS path code (will pull from main server for now) --- plugins/Realtime/RealtimePlugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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(); From c51539804a7f6f63b3f47fd838b9527262440e3d Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 27 Jan 2010 09:24:59 -0800 Subject: [PATCH 123/157] Add persistent:true property to Stomp messages so ActiveMQ doesn't decide to discard them even though persistence is enabled on the broker. :) (Thanks Aric!) --- lib/stompqueuemanager.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/stompqueuemanager.php b/lib/stompqueuemanager.php index 89f3d74cce..19e8c49b5c 100644 --- a/lib/stompqueuemanager.php +++ b/lib/stompqueuemanager.php @@ -178,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"); From 61a7a7b36b4f4b504dde880695dff5113dbe2a48 Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Thu, 21 Jan 2010 18:18:32 +0100 Subject: [PATCH 124/157] Plugin for Universal Ad Package. Outputs four most widely used ad types. --- plugins/UAP/UAPPlugin.php | 176 ++++++++++++++++++++++++++++++++++++++ plugins/UAP/uap.css | 50 +++++++++++ 2 files changed, 226 insertions(+) create mode 100644 plugins/UAP/UAPPlugin.php create mode 100644 plugins/UAP/uap.css diff --git a/plugins/UAP/UAPPlugin.php b/plugins/UAP/UAPPlugin.php new file mode 100644 index 0000000000..13c1017230 --- /dev/null +++ b/plugins/UAP/UAPPlugin.php @@ -0,0 +1,176 @@ +. + * + * @category Action + * @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/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +/** + * Outputs the following ad types (based on UAP): + * Medium Rectangle 300x250 + * Rectangle 180x150 + * Leaderboard 728x90 + * Wide Skyscraper 160x600 + * + * Any number of ad types can be used. Enable all using example: + * addPlugin('UAP', array( + * 'MediumRectangle' => '', + * 'Rectangle' => '', + * 'Leaderboard' => '', + * 'WideSkyscraper' => '' + * ) + * ); + * + * @category Plugin + * @package StatusNet + * @author Sarven Capadisli + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class UAPPlugin extends Plugin +{ + public $MediumRectangle = null; + public $Rectangle = null; + public $Leaderboard = null; + public $WideSkyscraper = null; + + function __construct($uap = array()) + { + $this->uap = $uap; + + parent::__construct(); + } + + function onInitializePlugin() + { + foreach($this->uap as $key => $value) { + switch(strtolower($key)) { + case 'MediumRectangle': default: + $this->MediumRectangle = $value; + break; + case 'Rectangle': + $this->Rectangle = $value; + break; + case 'Leaderboard': + $this->Leaderboard = $value; + break; + case 'WideSkyscraper': + $this->WideSkyscraper = $value; + break; + } + } + } + + function onEndShowStatusNetStyles($action) + { + $action->cssLink(common_path('plugins/UAP/uap.css'), + null, 'screen, projection, tv'); + return true; + } + + //MediumRectangle ad + function onStartShowAside($action) + { + if (!$this->MediumRectangle) { + return true; + } + + $this->showAd($action, array('id' => 'ad_medium-rectangle'), + $this->MediumRectangle); + + return true; + } + +/* + //Rectangle ad + function onEndShowSiteNotice($action) + { + if (!$this->Rectangle) { + return true; + } + + $this->showAd($action, array('id' => 'ad_rectangle'), + $this->Rectangle); + + return true; + } +*/ + + //Leaderboard and Rectangle ad + function onStartShowHeader($action) + { + if ($this->Leaderboard) { + $this->showAd($action, array('id' => 'ad_leaderboard'), + $this->Leaderboard); + } + + if ($this->Rectangle) { + $this->showAd($action, array('id' => 'ad_rectangle'), + $this->Rectangle); + } + + return true; + } + + //WideSkyscraper ad + function onEndShowAside($action) + { + if (!$this->WideSkyscraper) { + return true; + } + + $this->showAd($action, array('id' => 'ad_wide-skyscraper'), + $this->WideSkyscraper); + + return true; + } + + //Output ad container + function showAd($action, $attr=array(), $value) + { + $classes = ($attr['class']) ? $attr['class'].' ' : ''; + + $action->elementStart('div', array('id' => $attr['id'], + 'class' => $classes.'ad')); + $action->raw($value); + $action->elementEnd('div'); + } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'UAP', + 'version' => STATUSNET_VERSION, + 'author' => 'Sarven Capadisli', + 'homepage' => 'http://status.net/wiki/Plugin:UAP', + 'rawdescription' => + _m('Outputs ad placements based on Universal Ad Package')); + return true; + } +} diff --git a/plugins/UAP/uap.css b/plugins/UAP/uap.css new file mode 100644 index 0000000000..90abe51807 --- /dev/null +++ b/plugins/UAP/uap.css @@ -0,0 +1,50 @@ +/** 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:right; +margin-right:18px; +} + +#ad_leaderboard { +width:728px; +height:90px; + +margin:29px 18px 0 0; +clear:both; +} + +#ad_wide-skyscraper { +width:160px; +height:600px; + +margin-top:18px; +margin-left:8.5%; +} From 58fde0dcb5342cb3d3ff44c2e54a077eea0e063e Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Thu, 21 Jan 2010 18:28:02 +0100 Subject: [PATCH 125/157] Lowercased switch cases in UAP Plugin --- plugins/UAP/UAPPlugin.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/UAP/UAPPlugin.php b/plugins/UAP/UAPPlugin.php index 13c1017230..f3b16548d2 100644 --- a/plugins/UAP/UAPPlugin.php +++ b/plugins/UAP/UAPPlugin.php @@ -72,16 +72,16 @@ class UAPPlugin extends Plugin { foreach($this->uap as $key => $value) { switch(strtolower($key)) { - case 'MediumRectangle': default: + case 'mediumrectangle': default: $this->MediumRectangle = $value; break; - case 'Rectangle': + case 'rectangle': $this->Rectangle = $value; break; - case 'Leaderboard': + case 'leaderboard': $this->Leaderboard = $value; break; - case 'WideSkyscraper': + case 'wideskyscraper': $this->WideSkyscraper = $value; break; } From 1758ed453bcf2abd11bc2fde8768632099fe0de3 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 27 Jan 2010 06:25:22 -0500 Subject: [PATCH 126/157] move UAP plugin to core --- plugins/UAP/UAPPlugin.php => lib/uapplugin.php | 0 {plugins/UAP => theme/base/css}/uap.css | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename plugins/UAP/UAPPlugin.php => lib/uapplugin.php (100%) rename {plugins/UAP => theme/base/css}/uap.css (100%) diff --git a/plugins/UAP/UAPPlugin.php b/lib/uapplugin.php similarity index 100% rename from plugins/UAP/UAPPlugin.php rename to lib/uapplugin.php diff --git a/plugins/UAP/uap.css b/theme/base/css/uap.css similarity index 100% rename from plugins/UAP/uap.css rename to theme/base/css/uap.css From 7c54591472b4ff997ea099792c95d40b98798381 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 27 Jan 2010 07:19:36 -0500 Subject: [PATCH 127/157] make uapplugin an abstract class --- lib/uapplugin.php | 199 ++++++++++++++++++++++++---------------------- 1 file changed, 105 insertions(+), 94 deletions(-) diff --git a/lib/uapplugin.php b/lib/uapplugin.php index f3b16548d2..5160d96629 100644 --- a/lib/uapplugin.php +++ b/lib/uapplugin.php @@ -22,6 +22,7 @@ * @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/ @@ -32,145 +33,155 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { } /** + * 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 * - * Any number of ad types can be used. Enable all using example: - * addPlugin('UAP', array( - * 'MediumRectangle' => '', - * 'Rectangle' => '', - * 'Leaderboard' => '', - * 'WideSkyscraper' => '' - * ) - * ); - * * @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/ */ -class UAPPlugin extends Plugin +abstract class UAPPlugin extends Plugin { public $MediumRectangle = null; - public $Rectangle = null; - public $Leaderboard = null; - public $WideSkyscraper = null; + public $Rectangle = null; + public $Leaderboard = null; + public $WideSkyscraper = null; - function __construct($uap = array()) - { - $this->uap = $uap; - - parent::__construct(); - } - - function onInitializePlugin() - { - foreach($this->uap as $key => $value) { - switch(strtolower($key)) { - case 'mediumrectangle': default: - $this->MediumRectangle = $value; - break; - case 'rectangle': - $this->Rectangle = $value; - break; - case 'leaderboard': - $this->Leaderboard = $value; - break; - case 'wideskyscraper': - $this->WideSkyscraper = $value; - break; - } - } - } + /** + * Output our dedicated stylesheet + * + * @param Action $action Action being shown + * + * @return boolean hook flag + */ function onEndShowStatusNetStyles($action) { - $action->cssLink(common_path('plugins/UAP/uap.css'), - null, 'screen, projection, tv'); + // XXX: allow override by theme + $action->cssLink('css/uap.css', 'base', 'screen, projection, tv'); return true; } - //MediumRectangle ad + /** + * Add a medium rectangle ad at the beginning of sidebar + * + * @param Action $action Action being shown + * + * @return boolean hook flag + */ + function onStartShowAside($action) { - if (!$this->MediumRectangle) { - return true; - } + if (!is_null($this->mediumRectangle)) { - $this->showAd($action, array('id' => 'ad_medium-rectangle'), - $this->MediumRectangle); + $action->elementStart('div', + array('class' => 'ad_medium-rectangle ad')); + + $this->showMediumRectangle($action); + + $action->elementEnd('div'); + } return true; } -/* - //Rectangle ad - function onEndShowSiteNotice($action) - { - if (!$this->Rectangle) { - return true; - } + /** + * Add a leaderboard and/or rectangle in the header + * + * @param Action $action Action being shown + * + * @return boolean hook flag + */ - $this->showAd($action, array('id' => 'ad_rectangle'), - $this->Rectangle); - - return true; - } -*/ - - //Leaderboard and Rectangle ad function onStartShowHeader($action) { - if ($this->Leaderboard) { - $this->showAd($action, array('id' => 'ad_leaderboard'), - $this->Leaderboard); + if (!is_null($this->leaderboard)) { + $action->elementStart('div', + array('class' => 'ad_leaderboard ad')); + $this->showLeaderboard($action); + $action->elementEnd('div'); } - if ($this->Rectangle) { - $this->showAd($action, array('id' => 'ad_rectangle'), - $this->Rectangle); + if (!is_null($this->rectangle)) { + $action->elementStart('div', + array('class' => 'ad_rectangle ad')); + $this->showRectangle($action); + $action->elementEnd('div'); } return true; } - //WideSkyscraper ad + /** + * Add a wide skyscraper after the aside + * + * @param Action $action Action being shown + * + * @return boolean hook flag + */ + function onEndShowAside($action) { - if (!$this->WideSkyscraper) { - return true; + if (!is_null($this->wideSkyscraper)) { + $action->elementStart('div', + array('class' => 'ad_wide-skyscraper ad')); + + $this->showWideSkyscraper($action); + + $action->elementEnd('div'); } - - $this->showAd($action, array('id' => 'ad_wide-skyscraper'), - $this->WideSkyscraper); - return true; } - //Output ad container - function showAd($action, $attr=array(), $value) - { - $classes = ($attr['class']) ? $attr['class'].' ' : ''; + /** + * Show a medium rectangle ad + * + * @param Action $action Action being shown + * + * @return void + */ - $action->elementStart('div', array('id' => $attr['id'], - 'class' => $classes.'ad')); - $action->raw($value); - $action->elementEnd('div'); - } + abstract protected function showMediumRectangle($action); - function onPluginVersion(&$versions) - { - $versions[] = array('name' => 'UAP', - 'version' => STATUSNET_VERSION, - 'author' => 'Sarven Capadisli', - 'homepage' => 'http://status.net/wiki/Plugin:UAP', - 'rawdescription' => - _m('Outputs ad placements based on Universal Ad Package')); - return true; - } + /** + * 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); } From 9decd9806c675ce820a1443507846ba9c57ec72b Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 27 Jan 2010 07:26:32 -0500 Subject: [PATCH 128/157] Add BlankAdPlugin to test ad layout in different themes --- plugins/BlankAdPlugin.php | 121 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 plugins/BlankAdPlugin.php diff --git a/plugins/BlankAdPlugin.php b/plugins/BlankAdPlugin.php new file mode 100644 index 0000000000..bf3a225c01 --- /dev/null +++ b/plugins/BlankAdPlugin.php @@ -0,0 +1,121 @@ +. + * + * @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 paragraphs with defined background colors. It's + * mostly useful for debugging theme layout. + * + * To use this plugin, set the parameter for the ad size you want to use + * to the background you want to use. For example, to make a leaderboard + * that's red: + * + * addPlugin('BlankAd', array('leaderboard' => 'red')); + * + * @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) + { + $style = 'width: 300px; height: 250px; background-color: ' . + $this->mediumRectangle; + + $action->element('p', array('style' => $style), ''); + } + + /** + * Show a rectangle 'ad' + * + * @param Action $action Action being shown + * + * @return void + */ + + protected function showRectangle($action) + { + $style = 'width: 180px; height: 150px; background-color: ' . + $this->rectangle; + + $action->element('p', array('style' => $style), ''); + } + + /** + * Show a wide skyscraper ad + * + * @param Action $action Action being shown + * + * @return void + */ + + protected function showWideSkyscraper($action) + { + $style = 'width: 160px; height: 600px; background-color: ' . + $this->wideSkyscraper; + + $action->element('p', array('style' => $style), ''); + } + + /** + * Show a leaderboard ad + * + * @param Action $action Action being shown + * + * @return void + */ + + protected function showLeaderboard($action) + { + $style = 'width: 728px; height: 90px; background-color: ' . + $this->leaderboard; + + $action->element('p', array('style' => $style), ''); + } +} \ No newline at end of file From b412ebab11e4ef55bc06b6ea5dfe11fd66614f50 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 27 Jan 2010 07:34:31 -0500 Subject: [PATCH 129/157] move BlankAdPlugin to its own dir --- plugins/BlankAd/BlankAdPlugin.php | 121 ++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 plugins/BlankAd/BlankAdPlugin.php diff --git a/plugins/BlankAd/BlankAdPlugin.php b/plugins/BlankAd/BlankAdPlugin.php new file mode 100644 index 0000000000..bf3a225c01 --- /dev/null +++ b/plugins/BlankAd/BlankAdPlugin.php @@ -0,0 +1,121 @@ +. + * + * @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 paragraphs with defined background colors. It's + * mostly useful for debugging theme layout. + * + * To use this plugin, set the parameter for the ad size you want to use + * to the background you want to use. For example, to make a leaderboard + * that's red: + * + * addPlugin('BlankAd', array('leaderboard' => 'red')); + * + * @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) + { + $style = 'width: 300px; height: 250px; background-color: ' . + $this->mediumRectangle; + + $action->element('p', array('style' => $style), ''); + } + + /** + * Show a rectangle 'ad' + * + * @param Action $action Action being shown + * + * @return void + */ + + protected function showRectangle($action) + { + $style = 'width: 180px; height: 150px; background-color: ' . + $this->rectangle; + + $action->element('p', array('style' => $style), ''); + } + + /** + * Show a wide skyscraper ad + * + * @param Action $action Action being shown + * + * @return void + */ + + protected function showWideSkyscraper($action) + { + $style = 'width: 160px; height: 600px; background-color: ' . + $this->wideSkyscraper; + + $action->element('p', array('style' => $style), ''); + } + + /** + * Show a leaderboard ad + * + * @param Action $action Action being shown + * + * @return void + */ + + protected function showLeaderboard($action) + { + $style = 'width: 728px; height: 90px; background-color: ' . + $this->leaderboard; + + $action->element('p', array('style' => $style), ''); + } +} \ No newline at end of file From b2b95bd21fa3995b77af5096da80d52bf6938c3b Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 27 Jan 2010 07:41:12 -0500 Subject: [PATCH 130/157] make BlankAd dir and change to use a 1x1 image --- plugins/BlankAd/redpixel.png | Bin 0 -> 159 bytes plugins/BlankAdPlugin.php | 121 ----------------------------------- 2 files changed, 121 deletions(-) create mode 100644 plugins/BlankAd/redpixel.png delete mode 100644 plugins/BlankAdPlugin.php diff --git a/plugins/BlankAd/redpixel.png b/plugins/BlankAd/redpixel.png new file mode 100644 index 0000000000000000000000000000000000000000..26299a552574ed7b95a6eba5270067f44ad765eb GIT binary patch literal 159 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2s6ii6yp7}lMWc?slj7I;J!Gca%q zgD@k*tT_@uLG}_)Usv{9jM6-cJf2o(vVlU9C9V-A&iT2ysd*&~&PAz-C8;S2<(VZJ w3hti10pX2&;y^__o-U3d9M_Y7oIk+8z|6?VAaS>M15lj7)78&qol`;+06pa;zW@LL literal 0 HcmV?d00001 diff --git a/plugins/BlankAdPlugin.php b/plugins/BlankAdPlugin.php deleted file mode 100644 index bf3a225c01..0000000000 --- a/plugins/BlankAdPlugin.php +++ /dev/null @@ -1,121 +0,0 @@ -. - * - * @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 paragraphs with defined background colors. It's - * mostly useful for debugging theme layout. - * - * To use this plugin, set the parameter for the ad size you want to use - * to the background you want to use. For example, to make a leaderboard - * that's red: - * - * addPlugin('BlankAd', array('leaderboard' => 'red')); - * - * @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) - { - $style = 'width: 300px; height: 250px; background-color: ' . - $this->mediumRectangle; - - $action->element('p', array('style' => $style), ''); - } - - /** - * Show a rectangle 'ad' - * - * @param Action $action Action being shown - * - * @return void - */ - - protected function showRectangle($action) - { - $style = 'width: 180px; height: 150px; background-color: ' . - $this->rectangle; - - $action->element('p', array('style' => $style), ''); - } - - /** - * Show a wide skyscraper ad - * - * @param Action $action Action being shown - * - * @return void - */ - - protected function showWideSkyscraper($action) - { - $style = 'width: 160px; height: 600px; background-color: ' . - $this->wideSkyscraper; - - $action->element('p', array('style' => $style), ''); - } - - /** - * Show a leaderboard ad - * - * @param Action $action Action being shown - * - * @return void - */ - - protected function showLeaderboard($action) - { - $style = 'width: 728px; height: 90px; background-color: ' . - $this->leaderboard; - - $action->element('p', array('style' => $style), ''); - } -} \ No newline at end of file From e4393ee6dbcfbcf660d898dd1eb5fe91c4e3b80d Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 27 Jan 2010 07:51:25 -0500 Subject: [PATCH 131/157] Add the moved BlankAdPlugin --- plugins/BlankAd/BlankAdPlugin.php | 45 ++++++++++++++++--------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/plugins/BlankAd/BlankAdPlugin.php b/plugins/BlankAd/BlankAdPlugin.php index bf3a225c01..5f46ddd48d 100644 --- a/plugins/BlankAd/BlankAdPlugin.php +++ b/plugins/BlankAd/BlankAdPlugin.php @@ -35,14 +35,13 @@ if (!defined('STATUSNET')) { * Plugin for testing ad layout * * This plugin uses the UAPPlugin framework to output ad content. However, - * its ad content is just paragraphs with defined background colors. It's - * mostly useful for debugging theme layout. + * 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 the background you want to use. For example, to make a leaderboard - * that's red: + * to true (or anything non-null). For example, to make a leaderboard: * - * addPlugin('BlankAd', array('leaderboard' => 'red')); + * addPlugin('BlankAd', array('leaderboard' => true)); * * @category Plugin * @package StatusNet @@ -65,10 +64,11 @@ class BlankAdPlugin extends UAPPlugin protected function showMediumRectangle($action) { - $style = 'width: 300px; height: 250px; background-color: ' . - $this->mediumRectangle; - - $action->element('p', array('style' => $style), ''); + $action->element('img', + array('width' => 300, + 'height' => 250, + 'src' => common_path('plugins/BlankAd/redpixel.png')), + ''); } /** @@ -81,10 +81,11 @@ class BlankAdPlugin extends UAPPlugin protected function showRectangle($action) { - $style = 'width: 180px; height: 150px; background-color: ' . - $this->rectangle; - - $action->element('p', array('style' => $style), ''); + $action->element('img', + array('width' => 180, + 'height' => 50, + 'src' => common_path('plugins/BlankAd/redpixel.png')), + ''); } /** @@ -97,10 +98,11 @@ class BlankAdPlugin extends UAPPlugin protected function showWideSkyscraper($action) { - $style = 'width: 160px; height: 600px; background-color: ' . - $this->wideSkyscraper; - - $action->element('p', array('style' => $style), ''); + $action->element('img', + array('width' => 160, + 'height' => 600, + 'src' => common_path('plugins/BlankAd/redpixel.png')), + ''); } /** @@ -113,9 +115,10 @@ class BlankAdPlugin extends UAPPlugin protected function showLeaderboard($action) { - $style = 'width: 728px; height: 90px; background-color: ' . - $this->leaderboard; - - $action->element('p', array('style' => $style), ''); + $action->element('img', + array('width' => 728, + 'height' => 90, + 'src' => common_path('plugins/BlankAd/redpixel.png')), + ''); } } \ No newline at end of file From 4ad931ad38f1eba1f5b00a923854544dcb4aa0d6 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 27 Jan 2010 08:13:05 -0500 Subject: [PATCH 132/157] wrong height for rectangle in BlankAd --- plugins/BlankAd/BlankAdPlugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/BlankAd/BlankAdPlugin.php b/plugins/BlankAd/BlankAdPlugin.php index 5f46ddd48d..0e2719aed0 100644 --- a/plugins/BlankAd/BlankAdPlugin.php +++ b/plugins/BlankAd/BlankAdPlugin.php @@ -83,7 +83,7 @@ class BlankAdPlugin extends UAPPlugin { $action->element('img', array('width' => 180, - 'height' => 50, + 'height' => 150, 'src' => common_path('plugins/BlankAd/redpixel.png')), ''); } From e9feafc3ca4641bf5c51bbced3454962ed89b8ef Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 27 Jan 2010 08:13:24 -0500 Subject: [PATCH 133/157] CSS ids and classes fixed in UAPPlugin --- lib/uapplugin.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/uapplugin.php b/lib/uapplugin.php index 5160d96629..4364b1e190 100644 --- a/lib/uapplugin.php +++ b/lib/uapplugin.php @@ -87,7 +87,8 @@ abstract class UAPPlugin extends Plugin if (!is_null($this->mediumRectangle)) { $action->elementStart('div', - array('class' => 'ad_medium-rectangle ad')); + array('id' => 'ad_medium-rectangle', + 'class' => 'ad')); $this->showMediumRectangle($action); @@ -109,14 +110,16 @@ abstract class UAPPlugin extends Plugin { if (!is_null($this->leaderboard)) { $action->elementStart('div', - array('class' => 'ad_leaderboard ad')); + array('id' => 'ad_leaderboard', + 'class' => 'ad')); $this->showLeaderboard($action); $action->elementEnd('div'); } if (!is_null($this->rectangle)) { $action->elementStart('div', - array('class' => 'ad_rectangle ad')); + array('id' => 'ad_rectangle', + 'class' => 'ad')); $this->showRectangle($action); $action->elementEnd('div'); } @@ -136,7 +139,8 @@ abstract class UAPPlugin extends Plugin { if (!is_null($this->wideSkyscraper)) { $action->elementStart('div', - array('class' => 'ad_wide-skyscraper ad')); + array('id' => 'ad_wide-skyscraper', + 'class' => 'ad')); $this->showWideSkyscraper($action); From e3bd97bfe46ee7d063a45eec282b28faee1ca83f Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Wed, 27 Jan 2010 15:14:00 +0100 Subject: [PATCH 134/157] Aligning wide skyscraper to the right instead of left --- theme/base/css/uap.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/theme/base/css/uap.css b/theme/base/css/uap.css index 90abe51807..b95e9b2ce4 100644 --- a/theme/base/css/uap.css +++ b/theme/base/css/uap.css @@ -45,6 +45,7 @@ clear:both; width:160px; height:600px; +float:right; margin-top:18px; -margin-left:8.5%; +margin-right:8.25%; } From 1c875a53952506ca95716992511421d110a37d4d Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Wed, 27 Jan 2010 15:32:59 +0100 Subject: [PATCH 135/157] Moved rectangle ad into aside and leaderboard to the right in header. Intention for this layout was to reduce whitespace in header area --- lib/uapplugin.php | 15 ++++++++++++++- theme/base/css/uap.css | 9 ++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/uapplugin.php b/lib/uapplugin.php index 4364b1e190..69b68f9367 100644 --- a/lib/uapplugin.php +++ b/lib/uapplugin.php @@ -99,7 +99,7 @@ abstract class UAPPlugin extends Plugin } /** - * Add a leaderboard and/or rectangle in the header + * Add a leaderboard in the header * * @param Action $action Action being shown * @@ -116,6 +116,19 @@ abstract class UAPPlugin extends Plugin $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', diff --git a/theme/base/css/uap.css b/theme/base/css/uap.css index b95e9b2ce4..d7fa02c7f9 100644 --- a/theme/base/css/uap.css +++ b/theme/base/css/uap.css @@ -29,15 +29,18 @@ margin-bottom:18px; width:180px; height:150px; -float:right; -margin-right:18px; +float:none; +clear:both; +margin:0 auto; +margin-bottom:29px; } #ad_leaderboard { width:728px; height:90px; -margin:29px 18px 0 0; +margin:11px 18px 0 0; +float:right; clear:both; } From ea123800e991352d862c05a2f81e82ef53e72eeb Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 27 Jan 2010 10:44:49 -0500 Subject: [PATCH 136/157] move leaderboard to after the header --- lib/uapplugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/uapplugin.php b/lib/uapplugin.php index 69b68f9367..e3801f08ae 100644 --- a/lib/uapplugin.php +++ b/lib/uapplugin.php @@ -106,7 +106,7 @@ abstract class UAPPlugin extends Plugin * @return boolean hook flag */ - function onStartShowHeader($action) + function onEndShowHeader($action) { if (!is_null($this->leaderboard)) { $action->elementStart('div', From 760be76fc000c546ac38e96fa1cc5f822b7e6993 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 27 Jan 2010 10:45:44 -0500 Subject: [PATCH 137/157] camelcase the uap param names --- lib/uapplugin.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/uapplugin.php b/lib/uapplugin.php index e3801f08ae..ef35bafbfb 100644 --- a/lib/uapplugin.php +++ b/lib/uapplugin.php @@ -54,10 +54,10 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { abstract class UAPPlugin extends Plugin { - public $MediumRectangle = null; - public $Rectangle = null; - public $Leaderboard = null; - public $WideSkyscraper = null; + public $mediumRectangle = null; + public $rectangle = null; + public $leaderboard = null; + public $wideSkyscraper = null; /** * Output our dedicated stylesheet From 5e31ecd82aaf102382380b1352503303893cf9ba Mon Sep 17 00:00:00 2001 From: Sarven Capadisli Date: Wed, 27 Jan 2010 16:52:18 +0100 Subject: [PATCH 138/157] Centred leaderboard ad --- theme/base/css/uap.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/theme/base/css/uap.css b/theme/base/css/uap.css index d7fa02c7f9..73be5f0c14 100644 --- a/theme/base/css/uap.css +++ b/theme/base/css/uap.css @@ -39,8 +39,8 @@ margin-bottom:29px; width:728px; height:90px; -margin:11px 18px 0 0; -float:right; +margin:0 auto 18px; +float:none; clear:both; } From 47645228da4a9af8ffb0dc1f11c1da995f4791cb Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 27 Jan 2010 13:58:55 -0800 Subject: [PATCH 139/157] Add new oauth tables and modifications to 'consumer' table for rc4 --- db/rc3to09.sql | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/db/rc3to09.sql b/db/rc3to09.sql index 02dc7a6e2e..8342c4bc6f 100644 --- a/db/rc3to09.sql +++ b/db/rc3to09.sql @@ -14,3 +14,35 @@ insert into queue_item_new (frame,transport,created,claimed) 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', + 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; + From 9a54745fcd252e27cc664da7cee67b48d0f4f501 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 27 Jan 2010 13:59:58 -0800 Subject: [PATCH 140/157] Rename rc3to09.sql to rc3torc4.sql to avoid confusion if we add a last-minute change after this! --- db/{rc3to09.sql => rc3torc4.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename db/{rc3to09.sql => rc3torc4.sql} (100%) diff --git a/db/rc3to09.sql b/db/rc3torc4.sql similarity index 100% rename from db/rc3to09.sql rename to db/rc3torc4.sql From b0a325f7d0418575cdb46b7074c4cd2317f04980 Mon Sep 17 00:00:00 2001 From: Michele Date: Sun, 17 Jan 2010 11:21:07 +0100 Subject: [PATCH 141/157] HTTP auth provided is evaluated even if it's not required --- lib/apiauth.php | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/lib/apiauth.php b/lib/apiauth.php index ad9651ff26..ac5e997c78 100644 --- a/lib/apiauth.php +++ b/lib/apiauth.php @@ -84,16 +84,22 @@ class ApiAuthAction extends ApiAction } else { $this->checkBasicAuthUser(); } + } else { - // Reject API calls with the wrong access level + // Check to see if a basic auth user is there even + // if one's not required - 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(); - } + $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; } } @@ -206,13 +212,13 @@ 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_nickname)) { + if (!isset($this->auth_user_nickname) && $required) { header('WWW-Authenticate: Basic realm="' . $realm . '"'); // show error if the user clicks 'cancel' @@ -222,11 +228,10 @@ class ApiAuthAction extends ApiAction } else { - $user = common_check_user($this->auth_user_nickname, - $this->auth_user_password); - if (Event::handle('StartSetApiUser', array(&$user))) { - $this->auth_user = $user; + $this->auth_user = common_check_user($this->auth_user_nickname, + $this->auth_user_password); + Event::handle('EndSetApiUser', array($user)); } From 00d9b215f4de97f94035880041179879fcf0d201 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 27 Jan 2010 17:29:36 -0500 Subject: [PATCH 142/157] Plugin to support Google Adsense A plugin to easily add Google Adsense blocks to a StatusNet site. --- plugins/Adsense/AdsensePlugin.php | 160 ++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 plugins/Adsense/AdsensePlugin.php diff --git a/plugins/Adsense/AdsensePlugin.php b/plugins/Adsense/AdsensePlugin.php new file mode 100644 index 0000000000..dc2b32bc8f --- /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 From 2aba2eeeaf78f4e407abe23643ff6027b0ea53dd Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 27 Jan 2010 17:55:33 -0500 Subject: [PATCH 143/157] width and height should be integers in AdsensePlugin --- plugins/Adsense/AdsensePlugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Adsense/AdsensePlugin.php b/plugins/Adsense/AdsensePlugin.php index dc2b32bc8f..ab2b9a6fb3 100644 --- a/plugins/Adsense/AdsensePlugin.php +++ b/plugins/Adsense/AdsensePlugin.php @@ -150,8 +150,8 @@ class AdsensePlugin extends UAPPlugin { $code = 'google_ad_client = "'.$this->client.'"; '; $code .= 'google_ad_slot = "'.$slot.'"; '; - $code .= 'google_ad_width = "'.$width.'"; '; - $code .= 'google_ad_height = "'.$height.'"; '; + $code .= 'google_ad_width = '.$width.'; '; + $code .= 'google_ad_height = '.$height.'; '; $action->inlineScript($code); From 06cd3358970bfc27ff027cd1d8dc6974950c3793 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 27 Jan 2010 10:08:24 -0800 Subject: [PATCH 144/157] Add scripts/sendemail.php to send email to a user's address. Updated setup_status_network.sh to create a user with the site's nick, accept site tags, and send a mail to the user (if a template is set) Email and tag params added to the end: setup_status_net.sh mysite 'My Site' 'owner@example.com' '1user' (If multiple tags are needed, separate them with a pipe "|". Be sure to quote properly!) New parameters for setup.cfg need to be set: export PHPBASE=/var/www/statusnet export WILDCARD=example.net export MAILTEMPLATE=/etc/statusnet/newsite-mail.txt export MAILSUBJECT="Your new StatusNet site" $PHPBASE is the base dir for a callable StatusNet install, used to run command-line scripts for user setup. $WILDCARD is the wildcard domain, needed to build a full server name to pass into command-line scripts. $MAILTEMPLATE points to a file containing an e-mail message template. '$nickname', '$sitename', and '$userpass' can be used in the template for substitution. $MAILSUBJECT is the subject line for said email. To skip sending an email on creation, leave $MAILTEMPLATE blank or point to a non-existing file. --- scripts/sendemail.php | 82 +++++++++++++++++++++++++++++++++ scripts/setup.cfg.sample | 5 +- scripts/setup_status_network.sh | 50 ++++++++++++++++++-- 3 files changed, 132 insertions(+), 5 deletions(-) create mode 100755 scripts/sendemail.php 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..a0f10b3528 100644 --- a/scripts/setup.cfg.sample +++ b/scripts/setup.cfg.sample @@ -11,4 +11,7 @@ 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" diff --git a/scripts/setup_status_network.sh b/scripts/setup_status_network.sh index 777711fb55..d468df3aeb 100755 --- a/scripts/setup_status_network.sh +++ b/scripts/setup_status_network.sh @@ -2,9 +2,22 @@ source /etc/statusnet/setup.cfg -export nickname=$1 -export sitename=$2 +# setup_status_net.sh mysite 'My Site' 'owner@example.com' '1user' +export nickname="$1" +export sitename="$2" +export email="$3" +export tags="$4" + +# 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 +34,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 +43,32 @@ 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" \ + -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 From 0373ab6fa437309ab790d5de4d43e241ba510c93 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 27 Jan 2010 18:35:02 -0500 Subject: [PATCH 145/157] Plugin to enable OpenX ads --- plugins/OpenX/OpenXPlugin.php | 166 ++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 plugins/OpenX/OpenXPlugin.php diff --git a/plugins/OpenX/OpenXPlugin.php b/plugins/OpenX/OpenXPlugin.php new file mode 100644 index 0000000000..a0d02ec117 --- /dev/null +++ b/plugins/OpenX/OpenXPlugin.php @@ -0,0 +1,166 @@ +. + * + * @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); +} + +$_OpenXPlugin_Script = <<<\/scr"+"ipt>"); +ENDOFSCRIPT; + +/** + * 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 integer $zone Zone to show + * + * @return void + */ + + protected function showAd($zone) + { + global $_OpenXPlugin_Script; + + $this->inlineScript(sprintf($_OpenXPlugin_Script, $this->adScript, $zone)); + return true; + } +} \ No newline at end of file From e7a5471287598902d4fefbb93f1247d8c025e66b Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 27 Jan 2010 15:37:18 -0800 Subject: [PATCH 146/157] add additional post-install shell script option for setup_status_network.sh to do any other site-specific setup --- scripts/setup.cfg.sample | 1 + scripts/setup_status_network.sh | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/scripts/setup.cfg.sample b/scripts/setup.cfg.sample index a0f10b3528..f247a3bcae 100644 --- a/scripts/setup.cfg.sample +++ b/scripts/setup.cfg.sample @@ -15,3 +15,4 @@ 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 d468df3aeb..ae392191f1 100755 --- a/scripts/setup_status_network.sh +++ b/scripts/setup_status_network.sh @@ -72,3 +72,9 @@ then else echo "No mail template, not sending email." fi + +if [ -f "$POSTINSTALL" ] +then + echo "Running $POSTINSTALL ..." + source "$POSTINSTALL" +fi From 97a1ef14d2bfd32ec14db57b56d861e9cad7d02d Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 27 Jan 2010 18:39:55 -0500 Subject: [PATCH 147/157] using an action for output in OpenX plugin --- plugins/OpenX/OpenXPlugin.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/OpenX/OpenXPlugin.php b/plugins/OpenX/OpenXPlugin.php index a0d02ec117..96ed82b447 100644 --- a/plugins/OpenX/OpenXPlugin.php +++ b/plugins/OpenX/OpenXPlugin.php @@ -151,16 +151,17 @@ class OpenXPlugin extends UAPPlugin /** * Show an ad using OpenX * - * @param integer $zone Zone to show + * @param Action $action Action being shown + * @param integer $zone Zone to show * * @return void */ - protected function showAd($zone) + protected function showAd($action, $zone) { global $_OpenXPlugin_Script; - $this->inlineScript(sprintf($_OpenXPlugin_Script, $this->adScript, $zone)); + $action->inlineScript(sprintf($_OpenXPlugin_Script, $this->adScript, $zone)); return true; } } \ No newline at end of file From dd413ff4faeada13571711a0e4cc51e5ab25f7cc Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 27 Jan 2010 18:44:46 -0500 Subject: [PATCH 148/157] move script into OpenXPlugin::showAd() so it works --- plugins/OpenX/OpenXPlugin.php | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/plugins/OpenX/OpenXPlugin.php b/plugins/OpenX/OpenXPlugin.php index 96ed82b447..59485f25d8 100644 --- a/plugins/OpenX/OpenXPlugin.php +++ b/plugins/OpenX/OpenXPlugin.php @@ -31,22 +31,6 @@ if (!defined('STATUSNET')) { exit(1); } -$_OpenXPlugin_Script = <<<\/scr"+"ipt>"); -ENDOFSCRIPT; - /** * Plugin for OpenX Ad Server * @@ -159,9 +143,23 @@ class OpenXPlugin extends UAPPlugin protected function showAd($action, $zone) { - global $_OpenXPlugin_Script; +$scr = <<<\/scr"+"ipt>"); +ENDOFSCRIPT; - $action->inlineScript(sprintf($_OpenXPlugin_Script, $this->adScript, $zone)); + $action->inlineScript(sprintf($scr, $this->adScript, $zone)); return true; } } \ No newline at end of file From fe531dfc6c9c700b5566db634a9078d4b4b87d87 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 27 Jan 2010 16:03:01 -0800 Subject: [PATCH 149/157] Shuffle params on setup_status_network; adding fullname and pushing tags up --- scripts/setup_status_network.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/setup_status_network.sh b/scripts/setup_status_network.sh index ae392191f1..f502a169a2 100755 --- a/scripts/setup_status_network.sh +++ b/scripts/setup_status_network.sh @@ -2,12 +2,13 @@ source /etc/statusnet/setup.cfg -# setup_status_net.sh mysite 'My Site' 'owner@example.com' '1user' +# setup_status_net.sh mysite 'My Site' '1user' 'owner@example.com' 'Firsty McLastname' export nickname="$1" export sitename="$2" -export email="$3" -export tags="$4" +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. @@ -47,6 +48,7 @@ done php $PHPBASE/scripts/registeruser.php \ -s"$server" \ -n"$nickname" \ + -f"$fullname" \ -w"$userpass" \ -e"$email" From 420ae06faf033b799c677845045b50b259801dbd Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 28 Jan 2010 00:39:40 +0000 Subject: [PATCH 150/157] These API methods should return true for ->isReadOnly($args)! --- actions/apiaccountratelimitstatus.php | 17 ++++++++++++++++- actions/apifriendshipsexists.php | 15 +++++++++++++++ actions/apifriendshipsshow.php | 16 +++++++++++++++- actions/apigroupismember.php | 15 +++++++++++++++ actions/apigroupshow.php | 15 +++++++++++++++ actions/apihelptest.php | 15 +++++++++++++++ actions/apistatusnetconfig.php | 15 +++++++++++++++ actions/apistatusnetversion.php | 15 +++++++++++++++ actions/apiusershow.php | 15 +++++++++++++++ 9 files changed, 136 insertions(+), 2 deletions(-) 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/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/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; + } + } From 324590c46eba2900164d0487a2d525ea4e9f93b8 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 28 Jan 2010 00:41:44 +0000 Subject: [PATCH 151/157] Some adjustments to the way API auth works after merging testing and 0.9.x --- lib/apiauth.php | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/lib/apiauth.php b/lib/apiauth.php index ac5e997c78..d441014add 100644 --- a/lib/apiauth.php +++ b/lib/apiauth.php @@ -57,7 +57,6 @@ class ApiAuthAction extends ApiAction var $auth_user_password = null; var $access_token = null; var $oauth_source = null; - var $auth_user = null; /** * Take arguments for running, and output basic auth header if needed @@ -82,22 +81,27 @@ class ApiAuthAction extends ApiAction if (!empty($this->access_token)) { $this->checkOAuthRequest(); } else { - $this->checkBasicAuthUser(); + $this->checkBasicAuthUser(true); } } else { // Check to see if a basic auth user is there even // if one's not required - $this->checkBasicAuthUser(false); + if (empty($this->access_token)) { + $this->checkBasicAuthUser(false); + } } // Reject API calls with the wrong access level if ($this->isReadOnly($args) == false) { + + common_debug(get_class($this) . ' is not read-only!'); + if ($this->access != self::READ_WRITE) { - $msg = 'API resource requires read-write access, ' . - 'but you only have read access.'; + $msg = _('API resource requires read-write access, ' . + 'but you only have read access.'); $this->clientError($msg, 401, $this->format); exit; } @@ -176,7 +180,7 @@ class ApiAuthAction extends ApiAction ($this->access = self::READ_WRITE) ? 'read-write' : 'read-only' )); - return true; + return; } else { throw new OAuthException('Bad access token.'); } @@ -228,9 +232,14 @@ class ApiAuthAction extends ApiAction } else { + $user = common_check_user($this->auth_user_nickname, + $this->auth_user_password); + if (Event::handle('StartSetApiUser', array(&$user))) { - $this->auth_user = common_check_user($this->auth_user_nickname, - $this->auth_user_password); + + if (!empty($user)) { + $this->auth_user = $user; + } Event::handle('EndSetApiUser', array($user)); } @@ -239,18 +248,18 @@ class ApiAuthAction extends ApiAction $this->access = self::READ_WRITE; - if (empty($this->auth_user)) { + 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." - ); - + $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; } From f296f04abdc545f03daf55676282a2ae7108f79e Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 28 Jan 2010 01:24:40 +0000 Subject: [PATCH 152/157] Remove debugging statement --- lib/apiauth.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/apiauth.php b/lib/apiauth.php index d441014add..c684a6caee 100644 --- a/lib/apiauth.php +++ b/lib/apiauth.php @@ -96,9 +96,6 @@ class ApiAuthAction extends ApiAction // Reject API calls with the wrong access level if ($this->isReadOnly($args) == false) { - - common_debug(get_class($this) . ' is not read-only!'); - if ($this->access != self::READ_WRITE) { $msg = _('API resource requires read-write access, ' . 'but you only have read access.'); From 07d50a012af70d9bd35a26f645d791ae7ba29986 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 27 Jan 2010 17:34:13 -0800 Subject: [PATCH 153/157] fix update script -- read the diff wrong and put a couple fields on wrong table (whoops) --- db/rc3torc4.sql | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/db/rc3torc4.sql b/db/rc3torc4.sql index 8342c4bc6f..917c1f1c41 100644 --- a/db/rc3torc4.sql +++ b/db/rc3torc4.sql @@ -15,7 +15,9 @@ 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', + 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'; From 5182cc686dacfcf64130b591a3b7ded8d4a0c9dc Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 28 Jan 2010 01:39:18 +0000 Subject: [PATCH 154/157] Numbered format specifiers --- actions/apioauthauthorize.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/actions/apioauthauthorize.php b/actions/apioauthauthorize.php index fa074c4e76..15c3a9dad5 100644 --- a/actions/apioauthauthorize.php +++ b/actions/apioauthauthorize.php @@ -303,8 +303,9 @@ class ApiOauthAuthorizeAction extends ApiOauthAction $access = ($this->app->access_type & Oauth_application::$writeAccess) ? 'access and update' : 'access'; - $msg = _("The application %s by %s would like " . - "the ability to %s your account data."); + $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, From 3abfb454a381806a0f237e1463d2ba9a8612c22a Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 27 Jan 2010 18:39:17 -0800 Subject: [PATCH 155/157] Adds an emergency switch so we can run inbox distribution at save time (bypassing 'distrib' queue) Set $config['queue']['inboxes'] = false to do so --- classes/Notice.php | 10 +++++++--- lib/default.php | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) 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/lib/default.php b/lib/default.php index 10ea348640..c729193b52 100644 --- a/lib/default.php +++ b/lib/default.php @@ -87,6 +87,7 @@ $default = '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' From c805a5e51874aa231c728c14402542648edb24bf Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 27 Jan 2010 18:55:22 -0800 Subject: [PATCH 156/157] Update queue, translation notes in readme --- README | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/README b/README index f83873ca84..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 ------- From ee4ea3f3e1eb64df2dd3ba677f5201f8787482a8 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 27 Jan 2010 21:59:38 -0500 Subject: [PATCH 157/157] increment software beta version --- lib/common.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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');