From d8b9ed07e61168d224ca33afc4b83f6f84681481 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 1 Aug 2011 11:14:20 -0400 Subject: [PATCH 01/23] update README --- README | 55 ++++++++++++++++--------------------------------------- 1 file changed, 16 insertions(+), 39 deletions(-) diff --git a/README b/README index 4a8aba104e..bb9f7a2ea2 100644 --- a/README +++ b/README @@ -2,8 +2,8 @@ README ------ -StatusNet 0.9.7 "World Leader Pretend" -17 March 2011 +StatusNet 0.9.8 "Letter Never Sent" +1 August 2011 This is the README file for StatusNet, the Open Source microblogging platform. It includes installation instructions, descriptions of @@ -96,47 +96,24 @@ for additional terms. New this version ================ -This is a security, bug and feature release since version 0.9.6 released on -23 October 2010. - -For best compatibility with client software and site federation, and a -lot of bug fixes, it is highly recommended that all public sites -upgrade to the new version. Upgrades require new database indexes for -best performance; see Upgrade below. +This is a security release since version 0.9.7 released on 11 March +2011. It fixes security bug #3260. All sites running version 0.9.7 or +below are recommended to upgrade to 0.9.8 immediately. Notable changes this version: -- GroupPrivateMessage plugin lets users send private messages - to a group. (Similar to "private groups" on Yammer.) -- Support for Twitter streaming API in Twitter bridge plugin -- Support for a new Activity Streams-based API using AtomPub, allowing - richer API data. See http://status.net/wiki/AtomPub for details. -- Unified Facebook plugin, replacing previous Facebook application - and Facebook Connect plugin. -- A plugin to send out a daily summary email to network users. -- In-line thumbnails of some attachments (video, images) and oEmbed objects. -- Local copies of remote profiles to let moderators manage OStatus users. -- Upgrade upstream JS, minify everything. -- Allow pushing plugin JS, CSS, and static files to a CDN. -- Configurable nickname rules. -- Better support for bit.ly URL shortener. -- InProcessCache plugin for additional caching on top of memcached. -- Support for Activity Streams JSON feeds on many streams. -- User-initiated backup and restore of account data in Activity Streams - format. -- Bookmark plugin for making del.icio.us-like social bookmarking sites, - including del.icio.us backup file import. Supports OStatus. -- SQLProfile plugin to tune SQL queries. -- Better sorting on timelines to support restored or imported data. -- Hundreds of translations from http://translatewiki.net/ -- Hundreds of performance tunings, bug fixes, and UI improvements. -- Remove deprecated data from Activity Streams Atom output, to the - extent possible. -- NewMenu plugin for new layout of menu items. -- Experimental support for moving an account from one server to - another, using new AtomPub API. +- Fix bug #3260, a cross-site scripting (XSS) bug that allows an + attacker to inject JavaScript into a page with a carefully structured URL. +- Updated code for Google Analytics to reflect new API. +- Various fixes for Bookmark plugin. +- Updates to reCAPTCHA plugin based on changes to API. +- New plugin to move the site notice to the sidebar. +- Add rss.me to notice source list. +- Updates to data backup/restore. +- Correct use of "likes" in Facebook plugin. +- Ignore failures in Twitter plugin. -A full changelog is available at http://status.net/wiki/StatusNet_0.9.7. +A full changelog is available at http://status.net/wiki/StatusNet_0.9.8. Prerequisites ============= From e0238e7c171984cbb07533085b5304bcc433633b Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 1 Aug 2011 11:15:49 -0400 Subject: [PATCH 02/23] Update version number --- lib/common.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/common.php b/lib/common.php index 17375c4b62..95192a67b1 100644 --- a/lib/common.php +++ b/lib/common.php @@ -22,13 +22,13 @@ 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_BASE_VERSION', '0.9.7'); -define('STATUSNET_LIFECYCLE', 'fix1'); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', '' for release +define('STATUSNET_BASE_VERSION', '0.9.8'); +define('STATUSNET_LIFECYCLE', ''); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', '' for release define('STATUSNET_VERSION', STATUSNET_BASE_VERSION . STATUSNET_LIFECYCLE); define('LACONICA_VERSION', STATUSNET_VERSION); // compatibility -define('STATUSNET_CODENAME', 'World Leader Pretend'); +define('STATUSNET_CODENAME', 'Letter Never Sent'); define('AVATAR_PROFILE_SIZE', 96); define('AVATAR_STREAM_SIZE', 48); From 874f1db38980bd5a748b2a70159bd70ef7798c08 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 1 Aug 2011 14:51:59 -0400 Subject: [PATCH 03/23] Pre-fill profiles in notice streams --- classes/Notice.php | 29 ++++++++++++++++++++++++++++- classes/Profile.php | 5 +++++ lib/noticestream.php | 5 ++++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index 5caecff8f3..6540490b9a 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -106,7 +106,7 @@ class Notice extends Memcached_DataObject function getProfile() { if (is_int($this->_profile) && $this->_profile == -1) { - $this->_profile = Profile::staticGet('id', $this->profile_id); + $this->_setProfile(Profile::staticGet('id', $this->profile_id)); if (empty($this->_profile)) { // TRANS: Server exception thrown when a user profile for a notice cannot be found. @@ -117,6 +117,11 @@ class Notice extends Memcached_DataObject return $this->_profile; } + + function _setProfile($profile) + { + $this->_profile = $profile; + } function delete() { @@ -2492,4 +2497,26 @@ class Notice extends Memcached_DataObject return $scope; } + static function fillProfiles($notices) + { + $authors = array(); + + foreach ($notices as $notice) { + if (array_key_exists($notice->profile_id, $authors)) { + $authors[$notice->profile_id][] = $notice; + } else { + $authors[$notice->profile_id] = array($notice); + } + } + + $profile = Profile::multiGet('id', array_keys($authors)); + + $profiles = $profile->fetchAll(); + + foreach ($profiles as $p) { + foreach ($authors[$p->id] as $notice) { + $notice->_setProfile($p); + } + } + } } diff --git a/classes/Profile.php b/classes/Profile.php index 23534dfdfd..f635ee470d 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -49,6 +49,11 @@ class Profile extends Memcached_DataObject return Memcached_DataObject::staticGet('Profile',$k,$v); } + function multiGet($keyCol, $keyVals, $skipNulls=true) + { + return parent::multiGet('Profile', $keyCol, $keyVals, $skipNulls); + } + /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE diff --git a/lib/noticestream.php b/lib/noticestream.php index e9ff47b68c..010bfab60e 100644 --- a/lib/noticestream.php +++ b/lib/noticestream.php @@ -59,6 +59,9 @@ abstract class NoticeStream static function getStreamByIds($ids) { - return Notice::multiGet('id', $ids); + $notices = Notice::multiGet('id', $ids); + // Prefill the profiles + Notice::fillProfiles($notices->fetchAll()); + return $notices; } } From a3ef80941e1c5bac21e591a5c0b9c257e216d542 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 1 Aug 2011 15:18:29 -0400 Subject: [PATCH 04/23] use multiGet() for a profile's groups --- classes/Profile.php | 11 +---------- classes/User_group.php | 5 +++++ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/classes/Profile.php b/classes/Profile.php index f635ee470d..5eebd64a0d 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -273,16 +273,7 @@ class Profile extends Memcached_DataObject self::cacheSet($keypart, implode(',', $ids)); } - $groups = array(); - - foreach ($ids as $id) { - $group = User_group::staticGet('id', $id); - if (!empty($group)) { - $groups[] = $group; - } - } - - return new ArrayWrapper($groups); + return User_group::multiGet('id', $ids); } function isTagged($peopletag) diff --git a/classes/User_group.php b/classes/User_group.php index 6168f219b9..38cc5603db 100644 --- a/classes/User_group.php +++ b/classes/User_group.php @@ -33,6 +33,11 @@ class User_group extends Memcached_DataObject function staticGet($k,$v=NULL) { return Memcached_DataObject::staticGet('User_group',$k,$v); } + + function multiGet($keyCol, $keyVals, $skipNulls=true) + { + return parent::multiGet('User_group', $keyCol, $keyVals, $skipNulls); + } /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE From b9cabd45de8e5077c229bc005c7d607d1fe9dd4a Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 1 Aug 2011 16:43:44 -0400 Subject: [PATCH 05/23] Move prefill call to noticelist class --- lib/noticelist.php | 25 +++++++++++++++---------- lib/noticestream.php | 5 +---- lib/threadednoticelist.php | 23 ++++++++++++++++------- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/lib/noticelist.php b/lib/noticelist.php index f18b2d6684..c52380dfc9 100644 --- a/lib/noticelist.php +++ b/lib/noticelist.php @@ -83,17 +83,16 @@ class NoticeList extends Widget $this->out->elementStart('div', array('id' =>'notices_primary')); $this->out->elementStart('ol', array('class' => 'notices xoxo')); - $cnt = 0; - - while ($this->notice->fetch() && $cnt <= NOTICES_PER_PAGE) { - $cnt++; - - if ($cnt > NOTICES_PER_PAGE) { - break; - } + $notices = $this->notice->fetchAll(); + + $notices = array_slice($notices, 0, NOTICES_PER_PAGE); + + $this->prefill($notices); + + foreach ($notices as $notice) { try { - $item = $this->newListItem($this->notice); + $item = $this->newListItem($notice); $item->show(); } catch (Exception $e) { // we log exceptions and continue @@ -105,7 +104,7 @@ class NoticeList extends Widget $this->out->elementEnd('ol'); $this->out->elementEnd('div'); - return $cnt; + return count($notices); } /** @@ -122,4 +121,10 @@ class NoticeList extends Widget { return new NoticeListItem($notice, $this->out); } + + function prefill(&$notices) + { + // Prefill the profiles + Notice::fillProfiles($notices); + } } diff --git a/lib/noticestream.php b/lib/noticestream.php index 010bfab60e..e9ff47b68c 100644 --- a/lib/noticestream.php +++ b/lib/noticestream.php @@ -59,9 +59,6 @@ abstract class NoticeStream static function getStreamByIds($ids) { - $notices = Notice::multiGet('id', $ids); - // Prefill the profiles - Notice::fillProfiles($notices->fetchAll()); - return $notices; + return Notice::multiGet('id', $ids); } } diff --git a/lib/threadednoticelist.php b/lib/threadednoticelist.php index 43494bab1a..45c11453a7 100644 --- a/lib/threadednoticelist.php +++ b/lib/threadednoticelist.php @@ -76,17 +76,18 @@ class ThreadedNoticeList extends NoticeList $this->out->element('h2', null, _m('HEADER','Notices')); $this->out->elementStart('ol', array('class' => 'notices threaded-notices xoxo')); + $notices = $this->notice->fetchAll(); + $notices = array_slice($notices, 0, NOTICES_PER_PAGE); + + $this->prefill($notices); + $cnt = 0; $conversations = array(); - while ($this->notice->fetch() && $cnt <= NOTICES_PER_PAGE) { - $cnt++; - - if ($cnt > NOTICES_PER_PAGE) { - break; - } + + foreach ($notices as $notice) { // Collapse repeats into their originals... - $notice = $this->notice; + if ($notice->repeat_of) { $orig = Notice::staticGet('id', $notice->repeat_of); if ($orig) { @@ -223,6 +224,8 @@ class ThreadedNoticeListItem extends NoticeListItem $item = new ThreadedNoticeListMoreItem($moreCutoff, $this->out, count($notices)); $item->show(); } + // XXX: replicating NoticeList::prefill(), annoyingly + $this->prefill($notices); foreach (array_reverse($notices) as $notice) { if (Event::handle('StartShowThreadedNoticeSub', array($this, $this->notice, $notice))) { $item = new ThreadedNoticeListSubItem($notice, $this->notice, $this->out); @@ -247,6 +250,12 @@ class ThreadedNoticeListItem extends NoticeListItem parent::showEnd(); } + + function prefill(&$notices) + { + // Prefill the profiles + Notice::fillProfiles($notices); + } } // @todo FIXME: needs documentation. From 200e18cd713fadcc24f5fe3b2c8db1f353f279a7 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Mon, 1 Aug 2011 16:59:43 -0400 Subject: [PATCH 06/23] reduce the number of queries required to get a notice's groups --- classes/Notice.php | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index 6540490b9a..d577408fef 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -1438,25 +1438,14 @@ class Notice extends Memcached_DataObject $gi->notice_id = $this->id; - if ($gi->find()) { - while ($gi->fetch()) { - $ids[] = $gi->group_id; - } - } - + $ids = $gi->fetchAll('group_id'); + self::cacheSet($keypart, implode(',', $ids)); } - $groups = array(); - - foreach ($ids as $id) { - $group = User_group::staticGet('id', $id); - if ($group) { - $groups[] = $group; - } - } - - return $groups; + $groups = User_group::multiGet('id', $ids); + + return $groups->fetchAll(); } /** From b925eeecdec7bfe120ed3613f40046a88bb24488 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 2 Aug 2011 01:15:30 -0700 Subject: [PATCH 07/23] Fix errors thrown by code trying to broadcast profiles via OMB when the OMB plugin isn't installed --- lib/util.php | 10 ++++++---- plugins/OMB/OMBPlugin.php | 12 ++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/util.php b/lib/util.php index d358338519..ffa92fc69f 100644 --- a/lib/util.php +++ b/lib/util.php @@ -1501,16 +1501,18 @@ function common_enqueue_notice($notice) } /** - * Broadcast profile updates to remote subscribers. + * Legacy function to broadcast profile updates to OMB remote subscribers. + * + * XXX: This probably needs killing, but there are several bits of code + * that broadcast profile changes that need to be dealt with. AFAIK + * this function is only used for OMB. -z * * Since this may be slow with a lot of subscribers or bad remote sites, * this is run through the background queues if possible. */ function common_broadcast_profile(Profile $profile) { - $qm = QueueManager::get(); - $qm->enqueue($profile, "profile"); - return true; + Event::handle('BroadcastProfile', array($profile)); } function common_profile_url($nickname) diff --git a/plugins/OMB/OMBPlugin.php b/plugins/OMB/OMBPlugin.php index f5fed60079..38494c8134 100644 --- a/plugins/OMB/OMBPlugin.php +++ b/plugins/OMB/OMBPlugin.php @@ -369,6 +369,18 @@ class OMBPlugin extends Plugin return true; } + /** + * Broadcast a profile over OMB + * + * @param Profile $profile to broadcast + * @return false + */ + function onBroadcastProfile($profile) { + $qm = QueueManager::get(); + $qm->enqueue($profile, "profile"); + return true; + } + /** * Plugin version info * From 60a574ef28e453e2befdb105bb19c998862a3583 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 2 Aug 2011 01:17:56 -0700 Subject: [PATCH 08/23] Work improving the interface of the Event micro-app Squashed commit of the following: commit da50b6b0223fcbc42cf45d01a138f08930917e71 Author: Zach Copley Date: Tue Aug 2 00:35:36 2011 -0700 If end time < start time reset the end time selection commit 6dfc35579e8e4bd0af9d85fc46799bcc462c68b1 Author: Zach Copley Date: Mon Aug 1 23:55:10 2011 -0700 Populate event dates with sensible defaults commit 0bc8d726706cfdc0830687e7f40e941e61691191 Author: Zach Copley Date: Mon Aug 1 23:29:46 2011 -0700 Recalculate times if user changes start or end date commit 6a92a31429b4eb6f442eaf850d59dcc5b121e57f Author: Zach Copley Date: Mon Aug 1 23:03:46 2011 -0700 * Better date/time display * Localize date and time display for user commit 2bf344068a0eb6e3ed90efacbf33c85e7ee5edf7 Author: Zach Copley Date: Mon Aug 1 15:56:21 2011 -0700 Reselect the end time after timelist update commit 62fd0620eb5fcc94c240c0fc0b304aa17509de8d Author: Zach Copley Date: Mon Aug 1 14:40:14 2011 -0700 Fix bug in which end time was not properly in sync with start time + 30mins commit 3c6bcfb2d962f3677082c468a29480d2a1813d73 Author: Zach Copley Date: Mon Aug 1 12:37:00 2011 -0700 Pass exact URL of the timelist action to event.js commit efc74841c5b588cdae686630a1b4c1448e5d742b Author: Zach Copley Date: Mon Aug 1 11:20:45 2011 -0700 Add Ajax error handling to new event action commit 3085f4b3ed93bb930bff1bc475309b4d473ffc83 Author: Zach Copley Date: Fri Jul 22 01:18:13 2011 -0700 Ajaxify event end-time selector commit 8025c1d368d8f862b666702bfab08daf633a34ea Author: Zach Copley Date: Thu Jul 21 21:58:43 2011 -0700 Remove dead code commit 5fbfff47297dea609a07d67a81d430f97f6698ef Merge: bcd845d 3c926af Author: Zach Copley Date: Thu Jul 21 15:21:58 2011 -0700 Merge branch 'eventjs' of gitorious.org:~zcopley/statusnet/zcopleys-clone into eventjs * 'eventjs' of gitorious.org:~zcopley/statusnet/zcopleys-clone: Populate timei selection dropdowns and better CSS (thanks Sammy!) Event start/end as dropdowns Nothing to see here move along Don't allow user to set crazy start and end dates New event dates shouldn't ever be in the past, eh? Move event microapp JavaScript into included .js file Conflicts: plugins/Event/event.js plugins/Event/eventform.php commit bcd845dc56c147c4ba10eedd43cc7aa799bc6a9a Author: Zach Copley Date: Thu Jul 21 15:11:19 2011 -0700 Move the helper functions for filling the start/end times to their own class commit d246d39c4afbffb1e76cd561ab61f15dafd8a988 Author: Zach Copley Date: Wed Jul 20 18:50:38 2011 -0700 Populate time selection dropdowns and better CSS (thanks Sammy!) commit 0778533fef5500db79e40664c5b56aa7d9cc8357 Author: Zach Copley Date: Wed Jul 20 15:54:27 2011 -0700 Event start/end as dropdowns commit e800053fdf2cb12fc1f2eac72762d07571647aa8 Author: Zach Copley Date: Tue Jul 19 14:12:01 2011 -0700 Nothing to see here move along commit a85949b9cc4f3b5bb387785d4b7a717e9d952752 Author: Zach Copley Date: Mon Jul 18 17:48:30 2011 -0700 Don't allow user to set crazy start and end dates commit 87d1301ce8aa8877e753440dd52166bf857b29f3 Author: Zach Copley Date: Sun Jul 17 22:31:24 2011 -0700 New event dates shouldn't ever be in the past, eh? commit 7e05aa5fdc02bfec6107bcf8c748627216d51405 Author: Zach Copley Date: Fri Jul 15 15:36:17 2011 -0700 Move event microapp JavaScript into included .js file commit 3c926af287f80ee389b5bc8a4c1dcc5e0904a14c Author: Zach Copley Date: Wed Jul 20 18:50:38 2011 -0700 Populate time selection dropdowns and better CSS (thanks Sammy!) commit af09c57d5132dba2a6a3e76974e38fdde6422c45 Author: Zach Copley Date: Wed Jul 20 15:54:27 2011 -0700 Event start/end as dropdowns commit b585215ed7deb4dc9d4bbc065d36b6e3f819d710 Author: Zach Copley Date: Tue Jul 19 14:12:01 2011 -0700 Nothing to see here move along commit e1d30ae9b80eded4ed7ef6bdd7515da64ae344de Author: Zach Copley Date: Mon Jul 18 17:48:30 2011 -0700 Don't allow user to set crazy start and end dates commit ad7c99f021980b867f369066b4413bdb1e882986 Author: Zach Copley Date: Sun Jul 17 22:31:24 2011 -0700 New event dates shouldn't ever be in the past, eh? commit 4741f0a327e10e67fc04e2b816ed56351e38b4fa Author: Zach Copley Date: Fri Jul 15 15:36:17 2011 -0700 Move event microapp JavaScript into included .js file --- plugins/Event/EventPlugin.php | 6 +- plugins/Event/event.css | 8 ++ plugins/Event/event.js | 73 ++++++++++++++++++ plugins/Event/eventform.php | 67 ++++++++++++----- plugins/Event/eventlistitem.php | 40 +++++++--- plugins/Event/eventtimelist.php | 119 +++++++++++++++++++++++++++++ plugins/Event/newevent.php | 128 ++++++++++++++++++++------------ plugins/Event/timelist.php | 106 ++++++++++++++++++++++++++ theme/neo/css/display.css | 14 +++- 9 files changed, 483 insertions(+), 78 deletions(-) create mode 100644 plugins/Event/event.js create mode 100644 plugins/Event/eventtimelist.php create mode 100644 plugins/Event/timelist.php diff --git a/plugins/Event/EventPlugin.php b/plugins/Event/EventPlugin.php index 98a7d895ed..f2396b8075 100644 --- a/plugins/Event/EventPlugin.php +++ b/plugins/Event/EventPlugin.php @@ -82,6 +82,7 @@ class EventPlugin extends MicroappPlugin case 'CancelrsvpAction': case 'ShoweventAction': case 'ShowrsvpAction': + case 'TimelistAction': include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; return false; case 'EventListItem': @@ -89,6 +90,7 @@ class EventPlugin extends MicroappPlugin case 'EventForm': case 'RSVPForm': case 'CancelRSVPForm': + case 'EventTimeList': include_once $dir . '/'.strtolower($cls).'.php'; break; case 'Happening': @@ -121,6 +123,8 @@ class EventPlugin extends MicroappPlugin $m->connect('rsvp/:id', array('action' => 'showrsvp'), array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')); + $m->connect('main/event/updatetimes', + array('action' => 'timelist')); return true; } @@ -345,7 +349,7 @@ class EventPlugin extends MicroappPlugin function onEndShowScripts($action) { - $action->inlineScript('$(document).ready(function() { $("#event-startdate").datepicker(); $("#event-enddate").datepicker(); });'); + $action->script($this->path('event.js')); } function onEndShowStyles($action) diff --git a/plugins/Event/event.css b/plugins/Event/event.css index 8c9cbbb082..7fbb67d732 100644 --- a/plugins/Event/event.css +++ b/plugins/Event/event.css @@ -6,3 +6,11 @@ .event-title { margin-left: 0px; } #content .event .entry-title { margin-left: 0px; } #content .event .entry-content { margin-left: 0px; } +.ui-autocomplete { + max-height: 100px; + overflow-y: auto; + /* prevent horizontal scrollbar */ + overflow-x: hidden; + /* add padding to account for vertical scrollbar */ + padding-right: 20px; +} \ No newline at end of file diff --git a/plugins/Event/event.js b/plugins/Event/event.js new file mode 100644 index 0000000000..8ed25a899b --- /dev/null +++ b/plugins/Event/event.js @@ -0,0 +1,73 @@ +$(document).ready(function() { + + var today = new Date(); + + $("#event-startdate").datepicker({ + // Don't let the user set a crazy start date + minDate: today, + onClose: function(dateText, picker) { + // Don't let the user set a crazy end date + var newStartDate = new Date(dateText); + var endDate = new Date($("#event-startdate").val()); + if (endDate < newStartDate) { + $("#event-enddate").val(dateText); + } + if (dateText !== null) { + $("#event-enddate").datepicker('option', 'minDate', new Date(dateText)); + } + }, + onSelect: function() { + var startd = $("#event-startdate").val(); + var endd = $("#event-enddate").val(); + var sdate = new Date(startd); + var edate = new Date(endd); + if (sdate !== edate) { + updateTimes(); + } + } + }); + + $("#event-enddate").datepicker({ + minDate: today, + onSelect: function() { + var startd = $("#event-startdate").val(); + var endd = $("#event-enddate").val(); + var sdate = new Date(startd); + var edate = new Date(endd); + if (sdate !== edate) { + updateTimes(); + } + } + }); + + function updateTimes() { + var startd = $("#event-startdate").val(); + var endd = $("#event-enddate").val(); + + var startt = $("#event-starttime option:selected").val(); + var endt = $("#event-endtime option:selected").val(); + + var sdate = new Date(startd + " " + startt); + var edate = new Date(endd + " " + endt); + var duration = (startd === endd); + + $.getJSON($('#timelist_action_url').val(), + { start: startt, ajax: true, duration: duration }, + function(data) { + var times = []; + $.each(data, function(key, val) { + times.push(''); + }); + + $("#event-endtime").html(times.join('')); + if (startt < endt) { + $("#event-endtime").val(endt).attr("selected", "selected"); + } + }) + } + + $("#event-starttime").change(function(e) { + updateTimes(); + }); + +}); diff --git a/plugins/Event/eventform.php b/plugins/Event/eventform.php index 6a6e17e77b..d7c554bf32 100644 --- a/plugins/Event/eventform.php +++ b/plugins/Event/eventform.php @@ -84,6 +84,17 @@ class EventForm extends Form function formData() { $this->out->elementStart('fieldset', array('id' => 'new_event_data')); + + // Passing in the URL of the Ajax action that the .js for this form hits + // when selecting event start and end times. JavaScript will try to + // use a relative path, unless explicitely told where an action is, + // and that's a bit difficult to calculate since the event form is on + // so many pages with different paths. It might be worth solving this + // globally by putting the base site path in the Identifier-URL meta tag + // or something similar, so it would be easy to calculate the exact path + // for actions and other things in JavaScripts. -z + $this->out->hidden('timelist_action_url', common_local_url('timelist')); + $this->out->elementStart('ul', 'form_data'); $this->li(); @@ -97,49 +108,71 @@ class EventForm extends Form $this->unli(); $this->li(); + + $today = new DateTime('today'); + $today->setTimezone(new DateTimeZone(common_timezone())); + $this->out->input('event-startdate', // TRANS: Field label on event form. _m('LABEL','Start date'), - null, + $today->format('m/d/Y'), // TRANS: Field title on event form. _m('Date the event starts.'), 'startdate'); $this->unli(); $this->li(); - $this->out->input('event-starttime', - // TRANS: Field label on event form. - _m('LABEL','Start time'), - null, - // TRANS: Field title on event form. - _m('Time the event starts.'), - 'starttime'); + + $times = EventTimeList::getTimes(); + + $this->out->dropdown( + 'event-starttime', + // TRANS: Field label on event form. + _m('LABEL','Start time'), + $times, + // TRANS: Field title on event form. + _m('Time the event starts.'), + false, + null + ); + $this->unli(); $this->li(); $this->out->input('event-enddate', // TRANS: Field label on event form. _m('LABEL','End date'), - null, + $today->format('m/d/Y'), // TRANS: Field title on event form. _m('Date the event ends.'), 'enddate'); $this->unli(); $this->li(); - $this->out->input('event-endtime', - // TRANS: Field label on event form. - _m('LABEL','End time'), - null, - // TRANS: Field title on event form. - _m('Time the event ends.'), - 'endtime'); + + // XXX: Initial end time should be at least 30 mins out? We could do + // every 15 minute instead -z + $keys = array_keys($times); + $endStr = date('m/d/y', strtotime('now')) . " {$keys[0]}"; + $end = new DateTime($endStr); + $end->modify('+30'); + + $this->out->dropdown( + 'event-endtime', + // TRANS: Field label on event form. + _m('LABEL','End time'), + EventTimeList::getTimes($end->format('c'), true), + // TRANS: Field title on event form. + _m('Time the event ends.'), + false, + null + ); $this->unli(); $this->li(); $this->out->input('event-location', // TRANS: Field label on event form. - _m('LABEL','Location'), + _m('LABEL','Where?'), null, // TRANS: Field title on event form. _m('Event location.'), diff --git a/plugins/Event/eventlistitem.php b/plugins/Event/eventlistitem.php index 9bf34e765b..fb27704461 100644 --- a/plugins/Event/eventlistitem.php +++ b/plugins/Event/eventlistitem.php @@ -83,13 +83,33 @@ class EventListItem extends NoticeListItemAdapter $out->elementEnd('h3'); // VEVENT/H3 OUT - $startDate = strftime("%x", strtotime($event->start_time)); - $startTime = strftime("%R", strtotime($event->start_time)); + $now = new DateTime(); + $startDate = new DateTime($event->start_time); + $endDate = new DateTime($event->end_time); + $userTz = new DateTimeZone(common_timezone()); - $endDate = strftime("%x", strtotime($event->end_time)); - $endTime = strftime("%R", strtotime($event->end_time)); + // Localize the time for the observer + $now->setTimeZone($userTz); + $startDate->setTimezone($userTz); + $endDate->setTimezone($userTz); - // FIXME: better dates + $thisYear = $now->format('Y'); + $startYear = $startDate->format('Y'); + $endYear = $endDate->format('Y'); + + $dateFmt = 'D, F j, '; // e.g.: Mon, Aug 31 + + if ($startYear != $thisYear || $endYear != $thisYear) { + $dateFmt .= 'Y,'; // append year if we need to think about years + } + + $startDateStr = $startDate->format($dateFmt); + $endDateStr = $endDate->format($dateFmt); + + $timeFmt = 'g:ia'; + + $startTimeStr = $startDate->format($timeFmt); + $endTimeStr = $endDate->format("{$timeFmt} (T)"); $out->elementStart('div', 'event-times'); // VEVENT/EVENT-TIMES IN @@ -98,16 +118,16 @@ class EventListItem extends NoticeListItemAdapter $out->element('abbr', array('class' => 'dtstart', 'title' => common_date_iso8601($event->start_time)), - $startDate . ' ' . $startTime); - $out->text(' - '); - if ($startDate == $endDate) { + $startDateStr . ' ' . $startTimeStr); + $out->text(' – '); + if ($startDateStr == $endDateStr) { $out->element('span', array('class' => 'dtend', 'title' => common_date_iso8601($event->end_time)), - $endTime); + $endTimeStr); } else { $out->element('span', array('class' => 'dtend', 'title' => common_date_iso8601($event->end_time)), - $endDate . ' ' . $endTime); + $endDateStr . ' ' . $endTimeStr); } $out->elementEnd('div'); // VEVENT/EVENT-TIMES OUT diff --git a/plugins/Event/eventtimelist.php b/plugins/Event/eventtimelist.php new file mode 100644 index 0000000000..4ca40cb61f --- /dev/null +++ b/plugins/Event/eventtimelist.php @@ -0,0 +1,119 @@ + + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2011, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * Class to get fancy times for the dropdowns on the new event form + */ +class EventTimeList { + + /** + * Round up to the nearest half hour + * + * @param string $time the time to round (date/time string) + * @return DateTime the rounded time + */ + public static function nearestHalfHour($time) + { + $start = strtotime($time); + + $minutes = date('i', $start); + $hour = date('H', $start); + + if ($minutes >= 30) { + $minutes = '00'; + $hour++; + } else { + $minutes = '30'; + } + + $newTimeStr = date('m/d/y', $start) . " {$hour}:{$minutes}:00"; + return new DateTime($newTimeStr); + } + + /** + * Output a list of times in half-hour intervals + * + * @param string $start Time to start with (date/time string) + * @param boolean $duration Whether to include the duration of the event + * (from the start) + * @return array $times (UTC time string => localized time string) + */ + public static function getTimes($start = 'now', $duration = false) + { + $newTime = self::nearestHalfHour($start); + + $newTime->setTimezone(new DateTimeZone(common_timezone())); + $times = array(); + $len = 0; + + for ($i = 0; $i < 48; $i++) { + + // make sure we store the time as UTC + $newTime->setTimezone(new DateTimeZone('UTC')); + $utcTime = $newTime->format('H:i:s'); + + // localize time for user + $newTime->setTimezone(new DateTimeZone(common_timezone())); + $localTime = $newTime->format('g:ia'); + + // pretty up the end-time option list a bit + if ($duration) { + $len += 30; + $hours = $len / 60; + // for i18n + $hourStr = _m('hour'); + $hoursStr = _m('hrs'); + $minStr = _m('mins'); + switch ($hours) { + case 0: + $total = " (0 {$minStr})"; + break; + case .5: + $total = " (30 {$minStr})"; + break; + case 1: + $total = " (1 {$hourStr})"; + break; + default: + $total = " ({$hours} " . $hoursStr . ')'; + break; + } + $localTime .= $total; + } + + $times[$utcTime] = $localTime; + $newTime->modify('+30min'); // 30 min intervals + } + + return $times; + } + +} + + diff --git a/plugins/Event/newevent.php b/plugins/Event/newevent.php index cadf0e1433..2704501abd 100644 --- a/plugins/Event/newevent.php +++ b/plugins/Event/newevent.php @@ -52,8 +52,8 @@ class NeweventAction extends Action protected $title = null; protected $location = null; protected $description = null; - protected $startTime = null; - protected $endTime = null; + protected $startTime = null; + protected $endTime = null; /** * Returns the title of the action @@ -89,67 +89,78 @@ class NeweventAction extends Action $this->checkSessionToken(); } - $this->title = $this->trimmed('title'); + try { - if (empty($this->title)) { - // TRANS: Client exception thrown when trying to post an event without providing a title. - throw new ClientException(_m('Title required.')); - } + $this->title = $this->trimmed('title'); - $this->location = $this->trimmed('location'); - $this->url = $this->trimmed('url'); - $this->description = $this->trimmed('description'); + if (empty($this->title)) { + // TRANS: Client exception thrown when trying to post an event without providing a title. + throw new ClientException(_m('Title required.')); + } - $startDate = $this->trimmed('startdate'); + $this->location = $this->trimmed('location'); + $this->url = $this->trimmed('url'); + $this->description = $this->trimmed('description'); - if (empty($startDate)) { - // TRANS: Client exception thrown when trying to post an event without providing a start date. - throw new ClientException(_m('Start date required.')); - } + $startDate = $this->trimmed('startdate'); - $startTime = $this->trimmed('starttime'); + if (empty($startDate)) { + // TRANS: Client exception thrown when trying to post an event without providing a start date. + throw new ClientException(_m('Start date required.')); + } - if (empty($startTime)) { - $startTime = '00:00'; - } + $startTime = $this->trimmed('event-starttime'); - $endDate = $this->trimmed('enddate'); + if (empty($startTime)) { + $startTime = '00:00'; + } - if (empty($endDate)) { - // TRANS: Client exception thrown when trying to post an event without providing an end date. - throw new ClientException(_m('End date required.')); - } + $endDate = $this->trimmed('enddate'); - $endTime = $this->trimmed('endtime'); + if (empty($endDate)) { + // TRANS: Client exception thrown when trying to post an event without providing an end date. + throw new ClientException(_m('End date required.')); + } - if (empty($endTime)) { - $endTime = '00:00'; - } + $endTime = $this->trimmed('event-endtime'); - $start = $startDate . ' ' . $startTime; + if (empty($endTime)) { + $endTime = '00:00'; + } - common_debug("Event start: '$start'"); + $start = $startDate . ' ' . $startTime; - $end = $endDate . ' ' . $endTime; + common_debug("Event start: '$start'"); - common_debug("Event start: '$end'"); + $end = $endDate . ' ' . $endTime; - $this->startTime = strtotime($start); - $this->endTime = strtotime($end); + common_debug("Event start: '$end'"); - if ($this->startTime == 0) { - // TRANS: Client exception thrown when trying to post an event with a date that cannot be processed. - // TRANS: %s is the data that could not be processed. - throw new Exception(sprintf(_m('Could not parse date "%s".'), - $start)); - } + $this->startTime = strtotime($start); + $this->endTime = strtotime($end); + if ($this->startTime == 0) { + // TRANS: Client exception thrown when trying to post an event with a date that cannot be processed. + // TRANS: %s is the data that could not be processed. + throw new ClientException(sprintf(_m('Could not parse date "%s".'), + $start)); + } - if ($this->endTime == 0) { - // TRANS: Client exception thrown when trying to post an event with a date that cannot be processed. - // TRANS: %s is the data that could not be processed. - throw new Exception(sprintf(_m('Could not parse date "%s".'), - $end)); + if ($this->endTime == 0) { + // TRANS: Client exception thrown when trying to post an event with a date that cannot be processed. + // TRANS: %s is the data that could not be processed. + throw new ClientException(sprintf(_m('Could not parse date "%s".'), + $end)); + } + } catch (ClientException $ce) { + if ($this->boolean('ajax')) { + $this->outputAjaxError($ce->getMessage()); + return false; + } else { + $this->error = $ce->getMessage(); + $this->showPage(); + return false; + } } return true; @@ -220,9 +231,13 @@ class NeweventAction extends Action RSVP::saveNew($profile, $event, RSVP::POSITIVE); } catch (ClientException $ce) { - $this->error = $ce->getMessage(); - $this->showPage(); - return; + if ($this->boolean('ajax')) { + $this->outputAjaxError($ce->getMessage()); + } else { + $this->error = $ce->getMessage(); + $this->showPage(); + return; + } } if ($this->boolean('ajax')) { @@ -242,6 +257,23 @@ class NeweventAction extends Action } } + // @todo factor this out into a base class + function outputAjaxError($msg) + { + header('Content-Type: text/xml;charset=utf-8'); + $this->xw->startDocument('1.0', 'UTF-8'); + $this->elementStart('html'); + $this->elementStart('head'); + // TRANS: Page title after an AJAX error occurs + $this->element('title', null, _('Ajax Error')); + $this->elementEnd('head'); + $this->elementStart('body'); + $this->element('p', array('id' => 'error'), $msg); + $this->elementEnd('body'); + $this->elementEnd('html'); + return; + } + /** * Show the event form * diff --git a/plugins/Event/timelist.php b/plugins/Event/timelist.php new file mode 100644 index 0000000000..a6e0174180 --- /dev/null +++ b/plugins/Event/timelist.php @@ -0,0 +1,106 @@ +. + * + * @category Event + * @package StatusNet + * @author Zach Copley + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Callback handler to populate end time dropdown + */ +class TimelistAction extends Action { + + private $start; + private $duration; + + /** + * Get ready + * + * @param array $args misc. arguments + * + * @return boolean true + */ + function prepare($args) { + parent::prepare($args); + $this->start = $this->arg('start'); + $this->duration = $this->boolean('duration', false); + return true; + } + + /** + * Handle input and ouput something + * + * @param array $args $_REQUEST arguments + * + * @return void + */ + function handle($args) + { + parent::handle($args); + + if (!common_logged_in()) { + // TRANS: Error message displayed when trying to perform an action that requires a logged in user. + $this->clientError(_('Not logged in.')); + return; + } + + if (!empty($this->start)) { + $times = EventTimeList::getTimes($this->start, $this->duration); + } else { + $this->clientError(_m('Unexpected form submission.')); + return; + } + + if ($this->boolean('ajax')) { + header('Content-Type: application/json; charset=utf-8'); + print json_encode($times); + } else { + $this->clientError(_m('This action is AJAX only.')); + } + } + + /** + * Override the regular error handler to show something more + * ajaxy + * + * @param string $msg error message + * @param int $code error code + */ + function clientError($msg, $code = 400) { + if ($this->boolean('ajax')) { + header('Content-Type: application/json; charset=utf-8'); + print json_encode( + array( + 'success' => false, + 'code' => $code, + 'message' => $msg + ) + ); + } else { + parent::clientError($msg, $code); + } + } +} diff --git a/theme/neo/css/display.css b/theme/neo/css/display.css index d8bb5fc693..d7a4914a7d 100644 --- a/theme/neo/css/display.css +++ b/theme/neo/css/display.css @@ -1171,9 +1171,19 @@ td.entity_profile { width: auto; } -#event-startdate, #event-starttime, #event-enddate, #event-endtime { - width: 120px; +label[for=event-starttime], label[for=event-endtime] { + display: none; +} + +#event-starttime, #event-endtime { + margin-top: -1px; + margin-bottom: -1px; + height: 2em; +} + +#event-startdate, #event-enddate { margin-right: 20px; + width: 120px; } /* Limited-scope specific styles */ From 72ed2972142bc5c1531e290235c325a5a717be42 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 2 Aug 2011 10:46:29 -0400 Subject: [PATCH 09/23] New method Memcached_DataObject::pivotGet() This method lets you get all the objects with a given variable key and another set of "fixed" keys. A good example is getting all the avatars for a notice list; the avatar size stays the same, but the IDs change. Since it's very similar to multiGet(), I refactored that function to use pivotGet(). And, yes, I realize these are kind of hard to follow. --- classes/Memcached_DataObject.php | 61 ++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/classes/Memcached_DataObject.php b/classes/Memcached_DataObject.php index 0eae9fb42a..d964012a14 100644 --- a/classes/Memcached_DataObject.php +++ b/classes/Memcached_DataObject.php @@ -75,13 +75,52 @@ class Memcached_DataObject extends Safe_DataObject * @return array Array of objects, in order */ function multiGet($cls, $keyCol, $keyVals, $skipNulls=true) + { + $result = self::pivotGet($cls, $keyCol, $keyVals); + + common_log(LOG_INFO, sprintf("Got %d results for class %s with %d keys on column %s", + count($result), + $cls, + count($keyVals), + $keyCol)); + + $values = array_values($result); + + if ($skipNulls) { + $tmp = array(); + foreach ($values as $value) { + if (!empty($value)) { + $tmp[] = $value; + } + } + $values = $tmp; + } + + return new ArrayWrapper($values); + } + + /** + * Get multiple items from the database by key + * + * @param string $cls Class to fetch + * @param string $keyCol name of column for key + * @param array $keyVals key values to fetch + * @param boolean $otherCols Other columns to hold fixed + * + * @return array Array mapping $keyVals to objects, or null if not found + */ + static function pivotGet($cls, $keyCol, $keyVals, $otherCols = array()) { $result = array_fill_keys($keyVals, null); $toFetch = array(); foreach ($keyVals as $keyVal) { - $i = self::getcached($cls, $keyCol, $keyVal); + + $kv = array_merge($otherCols, array($keyCol => $keyVal)); + + $i = self::multicache($cls, $kv); + if ($i !== false) { $result[$keyVal] = $i; } else if (!empty($keyVal)) { @@ -93,6 +132,9 @@ class Memcached_DataObject extends Safe_DataObject $i = DB_DataObject::factory($cls); if (empty($i)) { throw new Exception(_('Cannot instantiate class ' . $cls)); + } + foreach ($otherCols as $otherKeyCol => $otherKeyVal) { + $i->$otherKeyCol = $otherKeyVal; } $i->whereAddIn($keyCol, $toFetch, $i->columnType($keyCol)); if ($i->find()) { @@ -107,29 +149,18 @@ class Memcached_DataObject extends Safe_DataObject foreach ($toFetch as $keyVal) { if (empty($result[$keyVal])) { + $kv = array_merge($otherCols, array($keyCol => $keyVal)); // save the fact that no such row exists $c = self::memcache(); if (!empty($c)) { - $ck = self::cachekey($cls, $keyCol, $keyVal); + $ck = self::multicacheKey($cls, $keyCol, $keyVal); $c->set($ck, null); } } } } - $values = array_values($result); - - if ($skipNulls) { - $tmp = array(); - foreach ($values as $value) { - if (!empty($value)) { - $tmp[] = $value; - } - } - $values = $tmp; - } - - return new ArrayWrapper($values); + return $result; } function columnType($columnName) From 9a78d70441d68f6c0eb2ab6c0ef16424879f1caa Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 2 Aug 2011 10:58:25 -0400 Subject: [PATCH 10/23] remove debugging statement in Memcached_DataObject::multiGet() --- classes/Memcached_DataObject.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/classes/Memcached_DataObject.php b/classes/Memcached_DataObject.php index d964012a14..ec36079c1c 100644 --- a/classes/Memcached_DataObject.php +++ b/classes/Memcached_DataObject.php @@ -78,12 +78,6 @@ class Memcached_DataObject extends Safe_DataObject { $result = self::pivotGet($cls, $keyCol, $keyVals); - common_log(LOG_INFO, sprintf("Got %d results for class %s with %d keys on column %s", - count($result), - $cls, - count($keyVals), - $keyCol)); - $values = array_values($result); if ($skipNulls) { From 02880f5a8cdab878b94afd30b90fdd922f4af47e Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 2 Aug 2011 11:09:30 -0400 Subject: [PATCH 11/23] use pkeyGet() instead of getReplies() checking addressee scope --- classes/Notice.php | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index d577408fef..952194d43d 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -1371,17 +1371,11 @@ class Notice extends Memcached_DataObject */ function getReplyProfiles() { - $ids = $this->getReplies(); - $profiles = array(); - - foreach ($ids as $id) { - $profile = Profile::staticGet('id', $id); - if (!empty($profile)) { - $profiles[] = $profile; - } - } + $ids = $this->getReplies(); - return $profiles; + $profiles = Profile::multiGet('id', $ids); + + return $profiles->fetchAll(); } /** @@ -2376,11 +2370,10 @@ class Notice extends Memcached_DataObject if ($this->scope & Notice::ADDRESSEE_SCOPE) { - // XXX: just query for the single reply - - $replies = $this->getReplies(); - - if (!in_array($profile->id, $replies)) { + $repl = Reply::pkeyGet(array('notice_id' => $this->id, + 'profile_id' => $profile->id)); + + if (empty($repl)) { return false; } } From 14fe22e4307044f2eb08264a7b83f9c2de245dba Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 2 Aug 2011 11:15:20 -0400 Subject: [PATCH 12/23] define Reply::pkeyGet() --- classes/Reply.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/classes/Reply.php b/classes/Reply.php index 9ba623ba3f..acda0fecb4 100644 --- a/classes/Reply.php +++ b/classes/Reply.php @@ -22,6 +22,11 @@ class Reply extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE + function pkeyGet($kv) + { + return Memcached_DataObject::pkeyGet('Reply',$kv); + } + /** * Wrapper for record insertion to update related caches */ From 5a132dbef098d0e2ea399ed35b9f09bd96ac27a7 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 2 Aug 2011 11:22:37 -0400 Subject: [PATCH 13/23] correct pagination for noticelist --- lib/noticelist.php | 4 ++-- lib/threadednoticelist.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/noticelist.php b/lib/noticelist.php index c52380dfc9..3bd7b05b4a 100644 --- a/lib/noticelist.php +++ b/lib/noticelist.php @@ -84,7 +84,7 @@ class NoticeList extends Widget $this->out->elementStart('ol', array('class' => 'notices xoxo')); $notices = $this->notice->fetchAll(); - + $total = count($notices); $notices = array_slice($notices, 0, NOTICES_PER_PAGE); $this->prefill($notices); @@ -104,7 +104,7 @@ class NoticeList extends Widget $this->out->elementEnd('ol'); $this->out->elementEnd('div'); - return count($notices); + return $total; } /** diff --git a/lib/threadednoticelist.php b/lib/threadednoticelist.php index 45c11453a7..ab25e85e9b 100644 --- a/lib/threadednoticelist.php +++ b/lib/threadednoticelist.php @@ -77,11 +77,11 @@ class ThreadedNoticeList extends NoticeList $this->out->elementStart('ol', array('class' => 'notices threaded-notices xoxo')); $notices = $this->notice->fetchAll(); + $total = count($notices); $notices = array_slice($notices, 0, NOTICES_PER_PAGE); $this->prefill($notices); - $cnt = 0; $conversations = array(); foreach ($notices as $notice) { @@ -120,7 +120,7 @@ class ThreadedNoticeList extends NoticeList $this->out->elementEnd('ol'); $this->out->elementEnd('div'); - return $cnt; + return $total; } /** From e05f423bea2f3148d1a63da16c0242dafaecfebf Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 2 Aug 2011 11:54:10 -0400 Subject: [PATCH 14/23] properly cache nulls for pivotGet() --- classes/Memcached_DataObject.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/Memcached_DataObject.php b/classes/Memcached_DataObject.php index ec36079c1c..9c92003e5c 100644 --- a/classes/Memcached_DataObject.php +++ b/classes/Memcached_DataObject.php @@ -147,7 +147,7 @@ class Memcached_DataObject extends Safe_DataObject // save the fact that no such row exists $c = self::memcache(); if (!empty($c)) { - $ck = self::multicacheKey($cls, $keyCol, $keyVal); + $ck = self::multicacheKey($cls, $kv); $c->set($ck, null); } } From 06e2422517628bb46fefee71ee0a514756d88864 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 2 Aug 2011 11:54:27 -0400 Subject: [PATCH 15/23] pre-fill avatars for Profiles in a notice list --- classes/Avatar.php | 5 +++++ classes/Notice.php | 32 +++++++++++++++++--------------- classes/Profile.php | 34 +++++++++++++++++++++++++++++++++- lib/noticelist.php | 3 ++- lib/threadednoticelist.php | 3 ++- 5 files changed, 59 insertions(+), 18 deletions(-) diff --git a/classes/Avatar.php b/classes/Avatar.php index 0b5141ba53..bdf3739bbf 100644 --- a/classes/Avatar.php +++ b/classes/Avatar.php @@ -27,6 +27,11 @@ class Avatar extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE + static function pivotGet($keyCol, $keyVals, $otherCols) + { + return Memcached_DataObject::pivotGet('Avatar', $keyCol, $keyVals, $otherCols); + } + // We clean up the file, too function delete() diff --git a/classes/Notice.php b/classes/Notice.php index 952194d43d..918190a24c 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -2481,24 +2481,26 @@ class Notice extends Memcached_DataObject static function fillProfiles($notices) { - $authors = array(); + $map = self::getProfiles($notices); foreach ($notices as $notice) { - if (array_key_exists($notice->profile_id, $authors)) { - $authors[$notice->profile_id][] = $notice; - } else { - $authors[$notice->profile_id] = array($notice); - } - } - - $profile = Profile::multiGet('id', array_keys($authors)); - - $profiles = $profile->fetchAll(); - - foreach ($profiles as $p) { - foreach ($authors[$p->id] as $notice) { - $notice->_setProfile($p); + if (array_key_exists($notice->profile_id, $map)) { + $notice->_setProfile($map[$notice->profile_id]); } } + + return array_values($map); + } + + static function getProfiles(&$notices) + { + $ids = array(); + foreach ($notices as $notice) { + $ids[] = $notice->profile_id; + } + + $ids = array_unique($ids); + + return Memcached_DataObject::pivotGet('Profile', 'id', $ids); } } diff --git a/classes/Profile.php b/classes/Profile.php index 5eebd64a0d..d0dad48d57 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -68,12 +68,18 @@ class Profile extends Memcached_DataObject return $this->_user; } + protected $_avatars = array(); + function getAvatar($width, $height=null) { if (is_null($height)) { $height = $width; } + if (array_key_exists($width, $this->_avatars)) { + return $this->_avatars[$width]; + } + $avatar = null; if (Event::handle('StartProfileGetAvatar', array($this, $width, &$avatar))) { @@ -83,9 +89,16 @@ class Profile extends Memcached_DataObject Event::handle('EndProfileGetAvatar', array($this, $width, &$avatar)); } + $this->_avatars[$width] = $avatar; + return $avatar; } + function _fillAvatar($width, $avatar) + { + $this->_avatars[$width] = $avatar; + } + function getOriginalAvatar() { $avatar = DB_DataObject::factory('avatar'); @@ -1353,7 +1366,26 @@ class Profile extends Memcached_DataObject function __sleep() { $vars = parent::__sleep(); - $skip = array('_user'); + $skip = array('_user', '_avatars'); return array_diff($vars, $skip); } + + static function fillAvatars(&$profiles, $width) + { + $ids = array(); + foreach ($profiles as $profile) { + $ids[] = $profile->id; + } + + common_debug('Got here'); + + $avatars = Avatar::pivotGet('profile_id', $ids, array('width' => $width, + 'height' => $width)); + + common_debug(sprintf('Got %d avatars for %d profiles', count($avatars), count($ids))); + + foreach ($profiles as $profile) { + $profile->_fillAvatar($width, $avatars[$profile->id]); + } + } } diff --git a/lib/noticelist.php b/lib/noticelist.php index 3bd7b05b4a..aaa3b3c986 100644 --- a/lib/noticelist.php +++ b/lib/noticelist.php @@ -125,6 +125,7 @@ class NoticeList extends Widget function prefill(&$notices) { // Prefill the profiles - Notice::fillProfiles($notices); + $profiles = Notice::fillProfiles($notices); + Profile::fillAvatars($profiles, AVATAR_STREAM_SIZE); } } diff --git a/lib/threadednoticelist.php b/lib/threadednoticelist.php index ab25e85e9b..a63d25110f 100644 --- a/lib/threadednoticelist.php +++ b/lib/threadednoticelist.php @@ -254,7 +254,8 @@ class ThreadedNoticeListItem extends NoticeListItem function prefill(&$notices) { // Prefill the profiles - Notice::fillProfiles($notices); + $profiles = Notice::fillProfiles($notices); + Profile::fillAvatars($profiles, AVATAR_MINI_SIZE); } } From 58d798b6079d1c61cee4a4013d234ef224687052 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 2 Aug 2011 12:01:41 -0400 Subject: [PATCH 16/23] Change NoticeList::prefill() to a static function --- lib/noticelist.php | 19 ++++++++++++++++--- lib/threadednoticelist.php | 12 ++---------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/lib/noticelist.php b/lib/noticelist.php index aaa3b3c986..a4781d9daa 100644 --- a/lib/noticelist.php +++ b/lib/noticelist.php @@ -87,7 +87,7 @@ class NoticeList extends Widget $total = count($notices); $notices = array_slice($notices, 0, NOTICES_PER_PAGE); - $this->prefill($notices); + self::prefill($notices); foreach ($notices as $notice) { @@ -122,10 +122,23 @@ class NoticeList extends Widget return new NoticeListItem($notice, $this->out); } - function prefill(&$notices) + static function prefill(&$notices, $avatarSize=AVATAR_STREAM_SIZE) { // Prefill the profiles $profiles = Notice::fillProfiles($notices); - Profile::fillAvatars($profiles, AVATAR_STREAM_SIZE); + // Prefill the avatars + Profile::fillAvatars($profiles, $avatarSize); + + $p = Profile::current(); + + $ids = array(); + + foreach ($notices as $notice) { + $ids[] = $notice->id; + } + + if (!empty($p)) { + Memcached_DataObject::pivotGet('Fave', 'notice_id', $ids, array('user_id' => $p->id)); + } } } diff --git a/lib/threadednoticelist.php b/lib/threadednoticelist.php index a63d25110f..407f7bdde3 100644 --- a/lib/threadednoticelist.php +++ b/lib/threadednoticelist.php @@ -80,7 +80,7 @@ class ThreadedNoticeList extends NoticeList $total = count($notices); $notices = array_slice($notices, 0, NOTICES_PER_PAGE); - $this->prefill($notices); + self::prefill($notices); $conversations = array(); @@ -224,8 +224,7 @@ class ThreadedNoticeListItem extends NoticeListItem $item = new ThreadedNoticeListMoreItem($moreCutoff, $this->out, count($notices)); $item->show(); } - // XXX: replicating NoticeList::prefill(), annoyingly - $this->prefill($notices); + NoticeList::prefill($notices, AVATAR_MINI_SIZE); foreach (array_reverse($notices) as $notice) { if (Event::handle('StartShowThreadedNoticeSub', array($this, $this->notice, $notice))) { $item = new ThreadedNoticeListSubItem($notice, $this->notice, $this->out); @@ -250,13 +249,6 @@ class ThreadedNoticeListItem extends NoticeListItem parent::showEnd(); } - - function prefill(&$notices) - { - // Prefill the profiles - $profiles = Notice::fillProfiles($notices); - Profile::fillAvatars($profiles, AVATAR_MINI_SIZE); - } } // @todo FIXME: needs documentation. From af49545e95a00f343a4041836639c58c2a59e212 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 2 Aug 2011 12:14:55 -0400 Subject: [PATCH 17/23] reduce the number of calls to get profile groups --- classes/Profile.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/classes/Profile.php b/classes/Profile.php index d0dad48d57..be60cfbd08 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -243,9 +243,14 @@ class Profile extends Memcached_DataObject function isMember($group) { - $gm = Group_member::pkeyGet(array('profile_id' => $this->id, - 'group_id' => $group->id)); - return (!empty($gm)); + $groups = $this->getGroups(0, null); + $gs = $groups->fetchAll(); + foreach ($gs as $g) { + if ($group->id == $g->id) { + return true; + } + } + return false; } function isAdmin($group) From 5081c56ea49173bb7c5ddb1b6f48487c35076123 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 2 Aug 2011 13:14:11 -0400 Subject: [PATCH 18/23] remove some debugging stuff in Profile::fillAvatars() --- classes/Profile.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/classes/Profile.php b/classes/Profile.php index be60cfbd08..d5008d9fb8 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -1382,13 +1382,9 @@ class Profile extends Memcached_DataObject $ids[] = $profile->id; } - common_debug('Got here'); - $avatars = Avatar::pivotGet('profile_id', $ids, array('width' => $width, 'height' => $width)); - common_debug(sprintf('Got %d avatars for %d profiles', count($avatars), count($ids))); - foreach ($profiles as $profile) { $profile->_fillAvatar($width, $avatars[$profile->id]); } From 10ce44c2971f65c5fa732d91a74e3fa61d43c3bd Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 2 Aug 2011 13:49:00 -0400 Subject: [PATCH 19/23] cleanse tags of non-tag characters when canonicalizing --- lib/util.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/util.php b/lib/util.php index e5b0c86e06..d8eee3d134 100644 --- a/lib/util.php +++ b/lib/util.php @@ -1096,8 +1096,11 @@ function common_tag_link($tag) function common_canonical_tag($tag) { + // only alphanum + $tag = preg_replace('/[^\pL\pN]/', '', $tag); $tag = mb_convert_case($tag, MB_CASE_LOWER, "UTF-8"); - return str_replace(array('-', '_', '.'), '', $tag); + $tag = substr($tag, 0, 64); + return $tag; } function common_valid_profile_tag($str) From 897e3c87e59b071254fcb0820fa46f3133a69e9d Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 2 Aug 2011 13:49:27 -0400 Subject: [PATCH 20/23] encode values when inserting into MeteorUpdater JS --- plugins/Meteor/MeteorPlugin.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/Meteor/MeteorPlugin.php b/plugins/Meteor/MeteorPlugin.php index 6e93e364f7..3d36c67f57 100644 --- a/plugins/Meteor/MeteorPlugin.php +++ b/plugins/Meteor/MeteorPlugin.php @@ -96,7 +96,11 @@ class MeteorPlugin extends RealtimePlugin function _updateInitialize($timeline, $user_id) { $script = parent::_updateInitialize($timeline, $user_id); - return $script." MeteorUpdater.init(\"$this->webserver\", $this->webport, \"{$timeline}\");"; + $ours = sprintf("MeteorUpdater.init(%s, %s, %s);", + json_encode($this->webserver), + json_encode($this->webport), + json_encode($timeline)); + return $script." ".$ours; } function _connect() From edb3f704b969769ce20b2e1e7d4504a570e42231 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 2 Aug 2011 14:03:12 -0400 Subject: [PATCH 21/23] correctly include UTF-8 alphanum chars in tags --- lib/util.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/util.php b/lib/util.php index d8eee3d134..629d8326fd 100644 --- a/lib/util.php +++ b/lib/util.php @@ -1097,7 +1097,7 @@ function common_tag_link($tag) function common_canonical_tag($tag) { // only alphanum - $tag = preg_replace('/[^\pL\pN]/', '', $tag); + $tag = preg_replace('/[^\pL\pN]/u', '', $tag); $tag = mb_convert_case($tag, MB_CASE_LOWER, "UTF-8"); $tag = substr($tag, 0, 64); return $tag; From 6ce81344719386362d298af34b54d410fdc28d50 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 2 Aug 2011 14:40:23 -0400 Subject: [PATCH 22/23] New release because I'm stupid --- README | 21 ++++++++++++--------- lib/common.php | 4 ++-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/README b/README index bb9f7a2ea2..6508ba9ca7 100644 --- a/README +++ b/README @@ -2,8 +2,8 @@ README ------ -StatusNet 0.9.8 "Letter Never Sent" -1 August 2011 +StatusNet 0.9.9 "9-9" +2 August 2011 This is the README file for StatusNet, the Open Source microblogging platform. It includes installation instructions, descriptions of @@ -98,7 +98,7 @@ New this version This is a security release since version 0.9.7 released on 11 March 2011. It fixes security bug #3260. All sites running version 0.9.7 or -below are recommended to upgrade to 0.9.8 immediately. +below are recommended to upgrade to 0.9.9 immediately. Notable changes this version: @@ -113,7 +113,10 @@ Notable changes this version: - Correct use of "likes" in Facebook plugin. - Ignore failures in Twitter plugin. -A full changelog is available at http://status.net/wiki/StatusNet_0.9.8. +A full changelog is available at http://status.net/wiki/StatusNet_0.9.9. + +NOTE: The short-lived StatusNet 0.9.8 ("Letter Never Sent") did not +adequately fix bug #3260 as originally thought; thus this new release. Prerequisites ============= @@ -224,9 +227,9 @@ especially if you've previously installed PHP/MySQL packages. 1. Unpack the tarball you downloaded on your Web server. Usually a command like this will work: - tar zxf statusnet-0.9.7.tar.gz + tar zxf statusnet-0.9.9.tar.gz - ...which will make a statusnet-0.9.7 subdirectory in your current + ...which will make a statusnet-0.9.9 subdirectory in your current directory. (If you don't have shell access on your Web server, you may have to unpack the tarball on your local computer and FTP the files to the server.) @@ -234,7 +237,7 @@ especially if you've previously installed PHP/MySQL packages. 2. Move the tarball to a directory of your choosing in your Web root directory. Usually something like this will work: - mv statusnet-0.9.7 /var/www/statusnet + mv statusnet-0.9.9 /var/www/statusnet This will make your StatusNet instance available in the statusnet path of your server, like "http://example.net/statusnet". "microblog" or @@ -649,7 +652,7 @@ with this situation. If you've been using StatusNet 0.7, 0.6, 0.5 or lower, or if you've been tracking the "git" version of the software, you will probably want to upgrade and keep your existing data. There is no automated -upgrade procedure in StatusNet 0.9.7. Try these step-by-step +upgrade procedure in StatusNet 0.9.9. Try these step-by-step instructions; read to the end first before trying them. 0. Download StatusNet and set up all the prerequisites as if you were @@ -670,7 +673,7 @@ instructions; read to the end first before trying them. 5. Once all writing processes to your site are turned off, make a final backup of the Web directory and database. 6. Move your StatusNet directory to a backup spot, like "statusnet.bak". -7. Unpack your StatusNet 0.9.7 tarball and move it to "statusnet" or +7. Unpack your StatusNet 0.9.9 tarball and move it to "statusnet" or wherever your code used to be. 8. Copy the config.php file and the contents of the avatar/, background/, file/, and local/ subdirectories from your old directory to your new diff --git a/lib/common.php b/lib/common.php index 95192a67b1..c4bed30118 100644 --- a/lib/common.php +++ b/lib/common.php @@ -22,13 +22,13 @@ 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_BASE_VERSION', '0.9.8'); +define('STATUSNET_BASE_VERSION', '0.9.9'); define('STATUSNET_LIFECYCLE', ''); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', '' for release define('STATUSNET_VERSION', STATUSNET_BASE_VERSION . STATUSNET_LIFECYCLE); define('LACONICA_VERSION', STATUSNET_VERSION); // compatibility -define('STATUSNET_CODENAME', 'Letter Never Sent'); +define('STATUSNET_CODENAME', '9-9'); define('AVATAR_PROFILE_SIZE', 96); define('AVATAR_STREAM_SIZE', 48); From dc690459f5d6ebe8f245ac34a3cb8ae87e369d18 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 2 Aug 2011 15:12:27 -0400 Subject: [PATCH 23/23] 1.0.0beta2 --- README | 20 ++++++++++---------- lib/framework.php | 3 ++- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/README b/README index 194471e552..5db5d9bfa2 100644 --- a/README +++ b/README @@ -2,19 +2,19 @@ README ------ -StatusNet 0.9.9 "9-9" +StatusNet 1.0.0beta2 2 August 2011 -This is the README file for StatusNet, the Open Source microblogging -platform. It includes installation instructions, descriptions of -options you can set, warnings, tips, and general info for -administrators. Information on using StatusNet can be found in the +This is the README file for StatusNet, the Open Source social +networking platform. It includes installation instructions, +descriptions of options you can set, warnings, tips, and general info +for administrators. Information on using StatusNet can be found in the "doc" subdirectory or in the "help" section on-line. About ===== -StatusNet is a Free and Open Source microblogging platform. It helps +StatusNet is a Free and Open Source social networking platform. It helps people in a community, company or group to exchange short (140 characters, by default) messages over the Web. Users can choose which people to "follow" and receive only their friends' or colleagues' @@ -474,7 +474,7 @@ off of amd64 to another server. Public feed ----------- -You can send *all* messages from your microblogging site to a +You can send *all* messages from your social networking site to a third-party service using XMPP. This can be useful for providing search, indexing, bridging, or other cool services. @@ -614,7 +614,7 @@ Private The administrator can set the "private" flag for a site so that it's not visible to non-logged-in users. This might be useful for -workgroups who want to share a microblogging site for project +workgroups who want to share a social networking site for project management, but host it on a public server. Total privacy is not guaranteed or ensured. Also, privacy is @@ -1733,8 +1733,8 @@ There are several ways to get more information about StatusNet. Feedback ======== -* Microblogging messages to http://support.status.net/ are very welcome. -* The microblogging group http://identi.ca/group/statusnet is a good +* Messages to http://support.status.net/ are very welcome. +* The group http://identi.ca/group/statusnet is a good place to discuss the software. * StatusNet has a bug tracker for any defects you may find, or ideas for making things better. http://status.net/bugs diff --git a/lib/framework.php b/lib/framework.php index 7c92d14722..3a338ea888 100644 --- a/lib/framework.php +++ b/lib/framework.php @@ -20,7 +20,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } define('STATUSNET_BASE_VERSION', '1.0.0'); -define('STATUSNET_LIFECYCLE', 'beta1'); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', 'release' +define('STATUSNET_LIFECYCLE', 'beta2'); // 'dev', 'alpha[0-9]+', 'beta[0-9]+', 'rc[0-9]+', 'release' define('STATUSNET_VERSION', STATUSNET_BASE_VERSION . STATUSNET_LIFECYCLE); define('LACONICA_VERSION', STATUSNET_VERSION); // compatibility @@ -156,4 +156,5 @@ function PEAR_ErrorToPEAR_Exception($err) } throw new PEAR_Exception($err->getMessage()); } + PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'PEAR_ErrorToPEAR_Exception');