]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch '1.0.x' into testing
authorEvan Prodromou <evan@status.net>
Tue, 2 Aug 2011 19:20:00 +0000 (15:20 -0400)
committerEvan Prodromou <evan@status.net>
Tue, 2 Aug 2011 19:20:00 +0000 (15:20 -0400)
22 files changed:
README
classes/Avatar.php
classes/Memcached_DataObject.php
classes/Notice.php
classes/Profile.php
classes/Reply.php
classes/User_group.php
lib/framework.php
lib/noticelist.php
lib/threadednoticelist.php
lib/util.php
plugins/Event/EventPlugin.php
plugins/Event/event.css
plugins/Event/event.js [new file with mode: 0644]
plugins/Event/eventform.php
plugins/Event/eventlistitem.php
plugins/Event/eventtimelist.php [new file with mode: 0644]
plugins/Event/newevent.php
plugins/Event/timelist.php [new file with mode: 0644]
plugins/Meteor/MeteorPlugin.php
plugins/OMB/OMBPlugin.php
theme/neo/css/display.css

diff --git a/README b/README
index 6a0633144b8772ad7745f17a5d0add8836dd6646..5db5d9bfa2fc5e9aa0a0e666d5caea6b5ef792d8 100644 (file)
--- a/README
+++ b/README
@@ -2,19 +2,19 @@
 README
 ------
 
-StatusNet 0.9.7 "World Leader Pretend"
-17 March 2011
+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'
@@ -96,47 +96,27 @@ 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.9 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.
-
-A full changelog is available at http://status.net/wiki/StatusNet_0.9.7.
+- 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.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
 =============
@@ -246,9 +226,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.)
@@ -256,7 +236,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
@@ -494,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.
 
@@ -634,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
@@ -671,7 +651,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
@@ -692,7 +672,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
@@ -1753,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
index 0b5141ba53965420a4de1c9070118888c717fbd5..bdf3739bbf86b6541853f7e2594cc4b330c7a9e5 100644 (file)
@@ -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()
index 0eae9fb42a835e79087a0845f2449f85afd52e28..9c92003e5c1ec505afcd4ce1d64b1352faf5289a 100644 (file)
@@ -75,13 +75,46 @@ 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);
+       
+       $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 +126,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 +143,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, $kv);
                        $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)
index 5caecff8f353a392fa42ae9bb0db2fab2790d07c..918190a24ce723c3770b4aea4d9aafe03a2914c4 100644 (file)
@@ -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()
     {
@@ -1366,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();
     }
 
     /**
@@ -1433,25 +1432,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();
     }
 
     /**
@@ -2382,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;
             }
         }
@@ -2492,4 +2479,28 @@ class Notice extends Memcached_DataObject
        return $scope;
     }
 
+       static function fillProfiles($notices)
+       {
+               $map = self::getProfiles($notices);
+               
+               foreach ($notices as $notice) {
+                       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); 
+       }
 }
index 23534dfdfd2f14ff8282452d9fcff38035fcf4e8..d5008d9fb8581ff03af5b98f6b50266f4d0883a7 100644 (file)
@@ -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
 
@@ -63,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))) {
@@ -78,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');
@@ -225,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)
@@ -268,16 +291,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)
@@ -1357,7 +1371,22 @@ 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;
+       }
+       
+       $avatars = Avatar::pivotGet('profile_id', $ids, array('width' => $width,
+                                                                                                                         'height' => $width));
+       
+       foreach ($profiles as $profile) {
+           $profile->_fillAvatar($width, $avatars[$profile->id]);
+       }
+    }
 }
index 9ba623ba3fea630d9f8a3fb76e5265dbf5f3a2fb..acda0fecb4ec7fa9cd84963696a434c1e3640ba3 100644 (file)
@@ -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
      */
index 6168f219b998d51a022a9bb93d6efd51ae2c8724..38cc5603db3c6f77648c8332e68dc69de6e0a611 100644 (file)
@@ -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
index 7c92d14722096c678f2fe142f0f1e3c342e6930d..3a338ea888c6086fb159d65f4ed7ef97b86b8227 100644 (file)
@@ -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');
index f18b2d66841ffa7a2998123c0f30a2fa8cad9157..a4781d9daa7a077cb33917eaad1c931501e94149 100644 (file)
@@ -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();
+               $total   = count($notices);
+               $notices = array_slice($notices, 0, NOTICES_PER_PAGE);
+               
+       self::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 $total;
     }
 
     /**
@@ -122,4 +121,24 @@ class NoticeList extends Widget
     {
         return new NoticeListItem($notice, $this->out);
     }
+    
+    static function prefill(&$notices, $avatarSize=AVATAR_STREAM_SIZE)
+    {
+       // Prefill the profiles
+       $profiles = Notice::fillProfiles($notices);
+       // 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));
+       }
+    }
 }
index 43494bab1a867eb22365543770c907d8abf25cfd..407f7bdde3c608fea1af559f1165064e044cd77c 100644 (file)
@@ -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'));
 
-        $cnt = 0;
+               $notices = $this->notice->fetchAll();
+               $total = count($notices);
+               $notices = array_slice($notices, 0, NOTICES_PER_PAGE);
+               
+       self::prefill($notices);
+       
         $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) {
@@ -119,7 +120,7 @@ class ThreadedNoticeList extends NoticeList
         $this->out->elementEnd('ol');
         $this->out->elementEnd('div');
 
-        return $cnt;
+        return $total;
     }
 
     /**
@@ -223,6 +224,7 @@ class ThreadedNoticeListItem extends NoticeListItem
                         $item = new ThreadedNoticeListMoreItem($moreCutoff, $this->out, count($notices));
                         $item->show();
                     }
+                    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);
index d35833851900758a2354849063050cdfe745e865..f3be1d0ddcb9c45b69c3b9cb83093c1b89a37935 100644 (file)
@@ -1127,8 +1127,11 @@ function common_tag_link($tag)
 
 function common_canonical_tag($tag)
 {
+  // only alphanum
+  $tag = preg_replace('/[^\pL\pN]/u', '', $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)
@@ -1501,16 +1504,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)
index 98a7d895ed0db3f2a289c186c90b7edec2cb7d12..f2396b80751ee7509f32303579355cda7698786e 100644 (file)
@@ -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)
index 8c9cbbb0823bb38a96069d1e8d633a6bcb8d55a2..7fbb67d732f61ea8998f3c3d2c491b2d5668b396 100644 (file)
@@ -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 (file)
index 0000000..8ed25a8
--- /dev/null
@@ -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('<option value="' + key + '">' + val + '</option>');
+            });
+
+            $("#event-endtime").html(times.join(''));
+            if (startt < endt) {
+                $("#event-endtime").val(endt).attr("selected", "selected");
+            }
+        })
+    }
+
+    $("#event-starttime").change(function(e) {
+        updateTimes();
+    });
+
+});
index 6a6e17e77b9dae4e5edfc6948784a0752521ad44..d7c554bf324df2476be4ddeb604f16f9f3ae68d2 100644 (file)
@@ -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.'),
index 9bf34e765b34724591c2934564a8d11e0174406e..fb27704461378cc788c47a2754a7c572c7829574 100644 (file)
@@ -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 (file)
index 0000000..4ca40cb
--- /dev/null
@@ -0,0 +1,119 @@
+<?php
+/**
+ * Helper class for calculating and displaying event times
+ *
+ * PHP version 5
+ *
+ * @category Data
+ * @package  StatusNet
+ * @author   Zach Copley <zach@status.net>
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ *  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;
+    }
+
+}
+
+
index cadf0e14331b9557a4de8a908a914124e1d072ab..2704501abddf5412028861b77c58d3fd9fbb8822 100644 (file)
@@ -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 (file)
index 0000000..a6e0174
--- /dev/null
@@ -0,0 +1,106 @@
+<?php
+/**
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * @category  Event
+ * @package   StatusNet
+ * @author    Zach Copley <zach@status.net>
+ * @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);
+        }
+    }
+}
index 3f963eb7328ec95952af05a2667b1ca19b00a533..1cff453d61fff3c515404608667e737449d50e46 100644 (file)
@@ -103,7 +103,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()
index f5fed6007998411f07e47503fd333d9d426479b7..38494c8134c1e37db63810d2d58726b62f89781e 100644 (file)
@@ -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
      *
index d8bb5fc6930ddaf5a085bb552a9860d81e86ecad..d7a4914a7dfd70926e7744d93a0bbd464816e45b 100644 (file)
@@ -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 */