]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch '0.8.x' into stats
authorEvan Prodromou <evan@controlyourself.ca>
Tue, 26 May 2009 02:47:23 +0000 (22:47 -0400)
committerEvan Prodromou <evan@controlyourself.ca>
Tue, 26 May 2009 02:47:23 +0000 (22:47 -0400)
Conflicts:
README

171 files changed:
.gitignore
EVENTS.txt
README
actions/accesstoken.php
actions/all.php
actions/api.php
actions/attachment.php [new file with mode: 0644]
actions/attachment_ajax.php [new file with mode: 0644]
actions/attachments.php [new file with mode: 0644]
actions/attachments_ajax.php [new file with mode: 0644]
actions/conversation.php
actions/deletenotice.php
actions/designsettings.php
actions/favoritesrss.php
actions/finishopenidlogin.php
actions/grouprss.php
actions/logout.php
actions/newmessage.php
actions/newnotice.php
actions/openidsettings.php
actions/profilesettings.php
actions/recoverpassword.php
actions/register.php
actions/showfavorites.php
actions/showgroup.php
actions/showstream.php
actions/subscribers.php
actions/subscriptions.php
actions/tag.php
actions/twitapifriendships.php
actions/twitapistatuses.php
actions/twitapiusers.php
actions/twittersettings.php
actions/userrss.php
classes/Fave.php
classes/File.php [new file with mode: 0644]
classes/File_oembed.php [new file with mode: 0644]
classes/File_redirection.php [new file with mode: 0644]
classes/File_thumbnail.php [new file with mode: 0644]
classes/File_to_post.php [new file with mode: 0644]
classes/Foreign_link.php
classes/Group_inbox.php [changed mode: 0755->0644]
classes/Group_member.php [changed mode: 0755->0644]
classes/Notice.php
classes/Notice_inbox.php
classes/Notice_tag.php
classes/Profile.php
classes/Related_group.php [changed mode: 0755->0644]
classes/Reply.php
classes/Status_network.php [changed mode: 0755->0644]
classes/User.php
classes/User_group.php [changed mode: 0755->0644]
classes/laconica.ini [changed mode: 0755->0644]
classes/laconica.links.ini
db/foreign_services.sql
db/laconica.sql
db/laconica_pg.sql
db/notice_source.sql
extlib/DB/DataObject.php
extlib/facebook/facebook.php
extlib/facebook/facebook_desktop.php
extlib/facebook/facebookapi_php5_restlib.php [changed mode: 0644->0755]
index.php
install.php
js/farbtastic/farbtastic.go.js
js/identica-badge.js
js/install.js [new file with mode: 0644]
js/jcrop/jquery.Jcrop.go.js
js/jquery.joverlay.min.js [new file with mode: 0644]
js/util.js
lib/Shorturl_api.php
lib/action.php
lib/attachmentlist.php [new file with mode: 0644]
lib/attachmentnoticesection.php [new file with mode: 0644]
lib/attachmentsection.php [new file with mode: 0644]
lib/attachmenttagcloudsection.php [new file with mode: 0644]
lib/common.php
lib/facebookutil.php
lib/frequentattachmentsection.php [new file with mode: 0644]
lib/galleryaction.php
lib/noticelist.php
lib/noticesection.php
lib/personalgroupnav.php
lib/popularnoticesection.php
lib/profileaction.php
lib/router.php
lib/rssaction.php
lib/subgroupnav.php
lib/subpeopletagcloudsection.php [new file with mode: 0644]
lib/subscriberspeopleselftagcloudsection.php [new file with mode: 0644]
lib/subscriberspeopletagcloudsection.php [new file with mode: 0644]
lib/subscriptionspeopleselftagcloudsection.php [new file with mode: 0644]
lib/subscriptionspeopletagcloudsection.php [new file with mode: 0644]
lib/tagcloudsection.php
lib/twitterapi.php
lib/util.php
plugins/Comet/CometPlugin.php [new file with mode: 0644]
plugins/Comet/README [new file with mode: 0644]
plugins/Comet/bayeux.class.inc.php [new file with mode: 0644]
plugins/Comet/jquery.comet.js [new file with mode: 0644]
plugins/Comet/json2.js [new file with mode: 0644]
plugins/Comet/updatetimeline.js [new file with mode: 0644]
plugins/FBConnect/FBCLoginGroupNav.php [new file with mode: 0644]
plugins/FBConnect/FBCSettingsNav.php [new file with mode: 0644]
plugins/FBConnect/FBConnectAuth.php [new file with mode: 0644]
plugins/FBConnect/FBConnectLogin.php [new file with mode: 0644]
plugins/FBConnect/FBConnectPlugin.css [new file with mode: 0644]
plugins/FBConnect/FBConnectPlugin.php [new file with mode: 0644]
plugins/FBConnect/FBConnectSettings.php [new file with mode: 0644]
plugins/FBConnect/fbfavicon.ico [new file with mode: 0644]
plugins/FBConnect/xd_receiver.htm [new file with mode: 0644]
plugins/LinkbackPlugin.php
plugins/WikiHashtagsPlugin.php [new file with mode: 0644]
scripts/getvaliddaemons.php [new file with mode: 0755]
scripts/inboxqueuehandler.php [new file with mode: 0755]
scripts/jabberqueuehandler.php
scripts/memcachedqueuehandler.php [new file with mode: 0755]
scripts/publicqueuehandler.php
scripts/startdaemons.sh
scripts/stopdaemons.sh
scripts/synctwitterfriends.php
scripts/twitterstatusfetcher.php [new file with mode: 0755]
scripts/xmppconfirmhandler.php
scripts/xmppdaemon.php
theme/base/css/display.css
theme/base/images/icons/clip-big.png [new file with mode: 0644]
theme/base/images/icons/clip.png [new file with mode: 0644]
theme/biz/css/base.css [new file with mode: 0644]
theme/biz/css/display.css [new file with mode: 0644]
theme/biz/css/ie.css [new file with mode: 0644]
theme/biz/default-avatar-mini.png [new file with mode: 0644]
theme/biz/default-avatar-profile.png [new file with mode: 0644]
theme/biz/default-avatar-stream.png [new file with mode: 0644]
theme/biz/images/illustrations/illu_pattern-01.png [new file with mode: 0644]
theme/biz/images/illustrations/illu_pattern-02.png [new file with mode: 0644]
theme/biz/logo.png [new file with mode: 0644]
theme/cloudy/css/display.css
theme/cloudy/default-avatar-mini.png
theme/cloudy/default-avatar-profile.png
theme/cloudy/default-avatar-stream.png
theme/cloudy/logo.png
theme/default/css/display.css
theme/default/logo.png [new file with mode: 0644]
theme/h4ck3r/css/base.css [new file with mode: 0644]
theme/h4ck3r/css/display.css [new file with mode: 0644]
theme/h4ck3r/css/ie.css [new file with mode: 0644]
theme/h4ck3r/default-avatar-mini.png [new file with mode: 0644]
theme/h4ck3r/default-avatar-profile.png [new file with mode: 0644]
theme/h4ck3r/default-avatar-stream.png [new file with mode: 0644]
theme/h4ck3r/images/illustrations/illu_h4x0r1ng.gif [new file with mode: 0644]
theme/h4ck3r/logo.png [new file with mode: 0644]
theme/identica/css/display.css
theme/otalk/css/base.css [new file with mode: 0644]
theme/otalk/css/display.css [new file with mode: 0644]
theme/otalk/css/ie.css [new file with mode: 0644]
theme/otalk/default-avatar-mini.png [new file with mode: 0644]
theme/otalk/default-avatar-profile.png [new file with mode: 0644]
theme/otalk/default-avatar-stream.png [new file with mode: 0644]
theme/otalk/images/illustrations/illu_arrow-left-01.gif [new file with mode: 0644]
theme/otalk/images/illustrations/illu_pattern-01.png [new file with mode: 0644]
theme/otalk/logo.png [new file with mode: 0644]
theme/pigeonthoughts/css/base.css [new file with mode: 0644]
theme/pigeonthoughts/css/display.css [new file with mode: 0644]
theme/pigeonthoughts/css/ie.css [new file with mode: 0644]
theme/pigeonthoughts/default-avatar-mini.png [new file with mode: 0644]
theme/pigeonthoughts/default-avatar-profile.png [new file with mode: 0644]
theme/pigeonthoughts/default-avatar-stream.png [new file with mode: 0644]
theme/pigeonthoughts/images/illustrations/illu_pigeons-01.png [new file with mode: 0644]
theme/pigeonthoughts/images/illustrations/illu_pigeons-02.png [new file with mode: 0644]
theme/pigeonthoughts/logo.png [new file with mode: 0644]
theme/readme.txt

index 83a53dfa3fef76899d8cf1316800396452f44961..da6947bfdc3f27978ab534a07be6a96a4797a6ef 100644 (file)
@@ -13,3 +13,6 @@ dataobject.ini
 *.rej
 .#*
 *.swp
+.buildpath
+.project
+.settings
index 5edf59245a5595710497350ad7d2c913cc4481bb..8e917f11de864605b5e561caf1b797f33a3d0e96 100644 (file)
@@ -100,6 +100,20 @@ StartPublicGroupNav: Showing the public group nav menu
 EndPublicGroupNav: At the end of the public group nav menu
 - $action: the current action
 
+StartSubGroupNav: Showing the subscriptions group nav menu
+- $action: the current action
+
+EndSubGroupNav: At the end of the subscriptions group nav menu
+- $action: the current action
+
 RouterInitialized: After the router instance has been initialized
 - $m: the Net_URL_Mapper that has just been set up
 
+StartLogout: Before logging out
+- $action: the logout action
+
+EndLogout: After logging out
+- $action: the logout action
+
+ArgsInitialized: After the argument array has been initialized
+- $args: associative array of arguments, can be modified
diff --git a/README b/README
index 136b537c7d0def0ea0baab1b2eacc0046228da15..9207f3e900ddfa8251d530ac44e8c7e782fc6b7c 100644 (file)
--- a/README
+++ b/README
@@ -1133,6 +1133,9 @@ welcome: nickname of a user account that sends welcome messages to new
          busy servers it may be a good idea to keep that one just for
          'urgent' messages. Default is null; no message.
 
+If either of these special user accounts are specified, the users should
+be created before the configuration is updated.
+
 snapshot
 --------
 
index bb68d3314d49efea773f0cc6f304d308ec2636a2..46b43c70216e106a5722434c38f365d56c0d3cc5 100644 (file)
@@ -59,7 +59,7 @@ class AccesstokenAction extends Action
         try {
             common_debug('getting request from env variables', __FILE__);
             common_remove_magic_from_request();
-            $req = OAuthRequest::from_request('POST', common_locale_url('accesstoken'));
+            $req = OAuthRequest::from_request('POST', common_local_url('accesstoken'));
             common_debug('getting a server', __FILE__);
             $server = omb_oauth_server();
             common_debug('fetching the access token', __FILE__);
index a92e554623136dbdbea88f11788d1ad6ea3b2af6..a53bbea07b4c0f541e91fcab6a4e761651d6b02c 100644 (file)
@@ -93,7 +93,7 @@ class AllAction extends ProfileAction
         if (common_logged_in()) {
             $current_user = common_current_user();
             if ($this->user->id === $current_user->id) {
-                $message .= _('Try subscribing to more people, [join a group](%%action.groups) or post something yourself.');
+                $message .= _('Try subscribing to more people, [join a group](%%action.groups%%) or post something yourself.');
             } else {
                 $message .= sprintf(_('You can try to [nudge %s](../%s) from his profile or [post something to his or her attention](%%%%action.newnotice%%%%?status_textarea=%s).'), $this->user->nickname, $this->user->nickname, '@' . $this->user->nickname);
             }
index d2f0a2eff0f7a47c1038939970d1fe0fd9a2cd32..8762b4bcd3efc974831252987f72b8bb33bd4041 100644 (file)
@@ -130,6 +130,7 @@ class ApiAction extends Action
                                  'statuses/friends_timeline',
                                  'statuses/friends',
                                  'statuses/replies',
+                                 'statuses/mentions',
                                  'statuses/followers',
                                  'favorites/favorites');
 
diff --git a/actions/attachment.php b/actions/attachment.php
new file mode 100644 (file)
index 0000000..b9187ff
--- /dev/null
@@ -0,0 +1,209 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Show notice attachments
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Personal
+ * @package   Laconica
+ * @author    Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2008-2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+//require_once INSTALLDIR.'/lib/personalgroupnav.php';
+//require_once INSTALLDIR.'/lib/feedlist.php';
+require_once INSTALLDIR.'/lib/attachmentlist.php';
+
+/**
+ * Show notice attachments
+ *
+ * @category Personal
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ */
+
+class AttachmentAction extends Action
+{
+    /**
+     * Attachment object to show
+     */
+
+    var $attachment = null;
+
+    /**
+     * Load attributes based on database arguments
+     *
+     * Loads all the DB stuff
+     *
+     * @param array $args $_REQUEST array
+     *
+     * @return success flag
+     */
+
+    function prepare($args)
+    {
+        parent::prepare($args);
+
+        $id = $this->arg('attachment');
+
+        $this->attachment = File::staticGet($id);
+
+        if (!$this->attachment) {
+            $this->clientError(_('No such attachment.'), 404);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Is this action read-only?
+     *
+     * @return boolean true
+     */
+
+    function isReadOnly($args)
+    {
+        return true;
+    }
+
+    /**
+     * Title of the page
+     *
+     * @return string title of the page
+     */
+    function title()
+    {
+        $a = new Attachment($this->attachment);
+        return $a->title();
+    }
+
+    /**
+     * Last-modified date for page
+     *
+     * When was the content of this page last modified? Based on notice,
+     * profile, avatar.
+     *
+     * @return int last-modified date as unix timestamp
+     */
+/*
+    function lastModified()
+    {
+        return max(strtotime($this->notice->created),
+                   strtotime($this->profile->modified),
+                   ($this->avatar) ? strtotime($this->avatar->modified) : 0);
+    }
+*/
+
+    /**
+     * An entity tag for this page
+     *
+     * Shows the ETag for the page, based on the notice ID and timestamps
+     * for the notice, profile, and avatar. It's weak, since we change
+     * the date text "one hour ago", etc.
+     *
+     * @return string etag
+     */
+/*
+    function etag()
+    {
+        $avtime = ($this->avatar) ?
+          strtotime($this->avatar->modified) : 0;
+
+        return 'W/"' . implode(':', array($this->arg('action'),
+                                          common_language(),
+                                          $this->notice->id,
+                                          strtotime($this->notice->created),
+                                          strtotime($this->profile->modified),
+                                          $avtime)) . '"';
+    }
+*/
+
+
+    /**
+     * Handle input
+     *
+     * Only handles get, so just show the page.
+     *
+     * @param array $args $_REQUEST data (unused)
+     *
+     * @return void
+     */
+
+    function handle($args)
+    {
+        parent::handle($args);
+        $this->showPage();
+    }
+
+    /**
+     * Don't show local navigation
+     *
+     * @return void
+     */
+
+    function showLocalNavBlock()
+    {
+    }
+
+    /**
+     * Fill the content area of the page
+     *
+     * Shows a single notice list item.
+     *
+     * @return void
+     */
+
+    function showContent()
+    {
+        $this->elementStart('ul', array('class' => 'attachments'));
+        $ali = new Attachment($this->attachment, $this);
+        $cnt = $ali->show();
+        $this->elementEnd('ul');
+    }
+
+    /**
+     * Don't show page notice
+     *
+     * @return void
+     */
+
+    function showPageNoticeBlock()
+    {
+    }
+
+    /**
+     * Show aside: this attachments appears in what notices
+     *
+     * @return void
+     */
+    function showSections() {
+        $ns = new AttachmentNoticeSection($this);
+        $ns->show();
+        $atcs = new AttachmentTagCloudSection($this);
+        $atcs->show();
+    }
+}
+
diff --git a/actions/attachment_ajax.php b/actions/attachment_ajax.php
new file mode 100644 (file)
index 0000000..1620b27
--- /dev/null
@@ -0,0 +1,141 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Show notice attachments
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Personal
+ * @package   Laconica
+ * @author    Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2008-2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+require_once INSTALLDIR.'/actions/attachment.php';
+
+/**
+ * Show notice attachments
+ *
+ * @category Personal
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ */
+
+class Attachment_ajaxAction extends AttachmentAction
+{
+    /**
+     * Load attributes based on database arguments
+     *
+     * Loads all the DB stuff
+     *
+     * @param array $args $_REQUEST array
+     *
+     * @return success flag
+     */
+
+    function prepare($args)
+    {
+        parent::prepare($args);
+        if (!$this->attachment) {
+            $this->clientError(_('No such attachment.'), 404);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Show page, a template method.
+     *
+     * @return nothing
+     */
+    function showPage()
+    {
+        if (Event::handle('StartShowBody', array($this))) {
+            $this->showCore();
+            Event::handle('EndShowBody', array($this));
+        }
+    }
+
+    /**
+     * Show core.
+     *
+     * Shows local navigation, content block and aside.
+     *
+     * @return nothing
+     */
+    function showCore()
+    {
+        $this->elementStart('div', array('id' => 'core'));
+        if (Event::handle('StartShowContentBlock', array($this))) {
+            $this->showContentBlock();
+            Event::handle('EndShowContentBlock', array($this));
+        }
+        $this->elementEnd('div');
+    }
+
+
+
+    /**
+     * Last-modified date for page
+     *
+     * When was the content of this page last modified? Based on notice,
+     * profile, avatar.
+     *
+     * @return int last-modified date as unix timestamp
+     */
+/*
+    function lastModified()
+    {
+        return max(strtotime($this->notice->created),
+                   strtotime($this->profile->modified),
+                   ($this->avatar) ? strtotime($this->avatar->modified) : 0);
+    }
+*/
+
+    /**
+     * An entity tag for this page
+     *
+     * Shows the ETag for the page, based on the notice ID and timestamps
+     * for the notice, profile, and avatar. It's weak, since we change
+     * the date text "one hour ago", etc.
+     *
+     * @return string etag
+     */
+/*
+    function etag()
+    {
+        $avtime = ($this->avatar) ?
+          strtotime($this->avatar->modified) : 0;
+
+        return 'W/"' . implode(':', array($this->arg('action'),
+                                          common_language(),
+                                          $this->notice->id,
+                                          strtotime($this->notice->created),
+                                          strtotime($this->profile->modified),
+                                          $avtime)) . '"';
+    }
+*/
+}
+
diff --git a/actions/attachments.php b/actions/attachments.php
new file mode 100644 (file)
index 0000000..6b31c83
--- /dev/null
@@ -0,0 +1,292 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Show notice attachments
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Personal
+ * @package   Laconica
+ * @author    Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2008-2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+//require_once INSTALLDIR.'/lib/personalgroupnav.php';
+//require_once INSTALLDIR.'/lib/feedlist.php';
+require_once INSTALLDIR.'/lib/attachmentlist.php';
+
+/**
+ * Show notice attachments
+ *
+ * @category Personal
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ */
+
+class AttachmentsAction extends Action
+{
+    /**
+     * Notice object to show
+     */
+
+    var $notice = null;
+
+    /**
+     * Profile of the notice object
+     */
+
+    var $profile = null;
+
+    /**
+     * Avatar of the profile of the notice object
+     */
+
+    var $avatar = null;
+
+    /**
+     * Is this action read-only?
+     *
+     * @return boolean true
+     */
+
+    function isReadOnly($args)
+    {
+        return true;
+    }
+
+    /**
+     * Last-modified date for page
+     *
+     * When was the content of this page last modified? Based on notice,
+     * profile, avatar.
+     *
+     * @return int last-modified date as unix timestamp
+     */
+
+    function lastModified()
+    {
+        return max(strtotime($this->notice->created),
+                   strtotime($this->profile->modified),
+                   ($this->avatar) ? strtotime($this->avatar->modified) : 0);
+    }
+
+    /**
+     * An entity tag for this page
+     *
+     * Shows the ETag for the page, based on the notice ID and timestamps
+     * for the notice, profile, and avatar. It's weak, since we change
+     * the date text "one hour ago", etc.
+     *
+     * @return string etag
+     */
+
+    function etag()
+    {
+        $avtime = ($this->avatar) ?
+          strtotime($this->avatar->modified) : 0;
+
+        return 'W/"' . implode(':', array($this->arg('action'),
+                                          common_language(),
+                                          $this->notice->id,
+                                          strtotime($this->notice->created),
+                                          strtotime($this->profile->modified),
+                                          $avtime)) . '"';
+    }
+
+    /**
+     * Title of the page
+     *
+     * @return string title of the page
+     */
+
+    function title()
+    {
+        return sprintf(_('%1$s\'s status on %2$s'),
+                       $this->profile->nickname,
+                       common_exact_date($this->notice->created));
+    }
+
+
+    /**
+     * Load attributes based on database arguments
+     *
+     * Loads all the DB stuff
+     *
+     * @param array $args $_REQUEST array
+     *
+     * @return success flag
+     */
+
+    function prepare($args)
+    {
+        parent::prepare($args);
+
+        $id = $this->arg('notice');
+
+        $this->notice = Notice::staticGet($id);
+
+        if (!$this->notice) {
+            $this->clientError(_('No such notice.'), 404);
+            return false;
+        }
+
+
+/*
+// STOP if there are no attachments
+// maybe even redirect if there's a single one
+// RYM FIXME TODO
+            $this->clientError(_('No such attachment.'), 404);
+            return false;
+
+*/
+
+
+
+
+        $this->profile = $this->notice->getProfile();
+
+        if (!$this->profile) {
+            $this->serverError(_('Notice has no profile'), 500);
+            return false;
+        }
+
+        $this->avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE);
+        return true;
+    }
+
+
+
+    /**
+     * Handle input
+     *
+     * Only handles get, so just show the page.
+     *
+     * @param array $args $_REQUEST data (unused)
+     *
+     * @return void
+     */
+
+    function handle($args)
+    {
+        parent::handle($args);
+
+        if ($this->notice->is_local == 0) {
+            if (!empty($this->notice->url)) {
+                common_redirect($this->notice->url, 301);
+            } else if (!empty($this->notice->uri) && preg_match('/^https?:/', $this->notice->uri)) {
+                common_redirect($this->notice->uri, 301);
+            }
+        } else {
+            $f2p = new File_to_post;
+            $f2p->post_id = $this->notice->id;
+            $file = new File;
+            $file->joinAdd($f2p);
+            $file->selectAdd();
+            $file->selectAdd('file.id as id');
+            $count = $file->find(true);
+            if (!$count) return;
+            if (1 === $count) {
+                common_redirect(common_local_url('attachment', array('attachment' => $file->id)), 301);
+            } else {
+                $this->showPage();
+            }
+        }
+    }
+
+    /**
+     * Don't show local navigation
+     *
+     * @return void
+     */
+
+    function showLocalNavBlock()
+    {
+    }
+
+    /**
+     * Fill the content area of the page
+     *
+     * Shows a single notice list item.
+     *
+     * @return void
+     */
+
+    function showContent()
+    {
+        $al = new AttachmentList($this->notice, $this);
+        $cnt = $al->show();
+    }
+
+    /**
+     * Don't show page notice
+     *
+     * @return void
+     */
+
+    function showPageNoticeBlock()
+    {
+    }
+
+    /**
+     * Don't show aside
+     *
+     * @return void
+     */
+
+    function showAside() {
+    }
+
+    /**
+     * Extra <head> content
+     *
+     * We show the microid(s) for the author, if any.
+     *
+     * @return void
+     */
+
+    function extraHead()
+    {
+        $user = User::staticGet($this->profile->id);
+
+        if (!$user) {
+            return;
+        }
+
+        if ($user->emailmicroid && $user->email && $this->notice->uri) {
+            $id = new Microid('mailto:'. $user->email,
+                              $this->notice->uri);
+            $this->element('meta', array('name' => 'microid',
+                                         'content' => $id->toString()));
+        }
+
+        if ($user->jabbermicroid && $user->jabber && $this->notice->uri) {
+            $id = new Microid('xmpp:', $user->jabber,
+                              $this->notice->uri);
+            $this->element('meta', array('name' => 'microid',
+                                         'content' => $id->toString()));
+        }
+    }
+}
+
diff --git a/actions/attachments_ajax.php b/actions/attachments_ajax.php
new file mode 100644 (file)
index 0000000..402d8b5
--- /dev/null
@@ -0,0 +1,115 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Show notice attachments
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Personal
+ * @package   Laconica
+ * @author    Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2008-2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+//require_once INSTALLDIR.'/lib/personalgroupnav.php';
+//require_once INSTALLDIR.'/lib/feedlist.php';
+require_once INSTALLDIR.'/actions/attachments.php';
+
+/**
+ * Show notice attachments
+ *
+ * @category Personal
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ */
+
+class Attachments_ajaxAction extends AttachmentsAction
+{
+    function showContent()
+    {
+    }
+
+    /**
+     * Fill the content area of the page
+     *
+     * Shows a single notice list item.
+     *
+     * @return void
+     */
+
+    function showContentBlock()
+    {
+        $al = new AttachmentList($this->notice, $this);
+        $cnt = $al->show();
+    }
+
+    /**
+     * Extra <head> content
+     *
+     * We show the microid(s) for the author, if any.
+     *
+     * @return void
+     */
+
+    function extraHead()
+    {
+    }
+
+
+    /**
+     * Show page, a template method.
+     *
+     * @return nothing
+     */
+    function showPage()
+    {
+        if (Event::handle('StartShowBody', array($this))) {
+            $this->showCore();
+            Event::handle('EndShowBody', array($this));
+        }
+    }
+
+    /**
+     * Show core.
+     *
+     * Shows local navigation, content block and aside.
+     *
+     * @return nothing
+     */
+    function showCore()
+    {
+        $this->elementStart('div', array('id' => 'core'));
+        if (Event::handle('StartShowContentBlock', array($this))) {
+            $this->showContentBlock();
+            Event::handle('EndShowContentBlock', array($this));
+        }
+        $this->elementEnd('div');
+    }
+
+
+
+
+}
+
index 05cfb76e3c85cc324c74a90f822de90241bae4a4..ef189016aa358bfca4be1c0934902893ab948466 100644 (file)
@@ -11,7 +11,7 @@
  * @link     http://laconi.ca/
  *
  * Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2009, Control Yourself, 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
@@ -31,7 +31,7 @@ if (!defined('LACONICA')) {
     exit(1);
 }
 
-require_once(INSTALLDIR.'/lib/noticelist.php');
+require_once INSTALLDIR.'/lib/noticelist.php';
 
 /**
  * Conversation tree in the browser
@@ -42,9 +42,10 @@ require_once(INSTALLDIR.'/lib/noticelist.php');
  * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
  * @link     http://laconi.ca/
  */
+
 class ConversationAction extends Action
 {
-    var $id = null;
+    var $id   = null;
     var $page = null;
 
     /**
@@ -69,24 +70,47 @@ class ConversationAction extends Action
         return true;
     }
 
+    /**
+     * Handle the action
+     *
+     * @param array $args Web and URL arguments
+     *
+     * @return void
+     */
+
     function handle($args)
     {
         parent::handle($args);
         $this->showPage();
     }
 
+    /**
+     * Returns the page title
+     *
+     * @return string page title
+     */
+
     function title()
     {
         return _("Conversation");
     }
 
+    /**
+     * Show content.
+     *
+     * Display a hierarchical unordered list in the content area.
+     * Uses ConversationTree to do most of the heavy lifting.
+     *
+     * @return void
+     */
+
     function showContent()
     {
         // FIXME this needs to be a tree, not a list
 
         $qry = 'SELECT * FROM notice WHERE conversation = %s ';
 
-        $offset = ($this->page-1)*NOTICES_PER_PAGE;
+        $offset = ($this->page-1) * NOTICES_PER_PAGE;
         $limit  = NOTICES_PER_PAGE + 1;
 
         $txt = sprintf($qry, $this->id);
@@ -95,9 +119,9 @@ class ConversationAction extends Action
                                      'notice:conversation:'.$this->id,
                                      $offset, $limit);
 
-        $nl = new NoticeList($notices, $this);
+        $ct = new ConversationTree($notices, $this);
 
-        $cnt = $nl->show();
+        $cnt = $ct->show();
 
         $this->pagination($this->page > 1, $cnt > NOTICES_PER_PAGE,
                           $this->page, 'conversation', array('id' => $this->id));
@@ -105,3 +129,170 @@ class ConversationAction extends Action
 
 }
 
+/**
+ * Conversation tree
+ *
+ * The widget class for displaying a hierarchical list of notices.
+ *
+ * @category Widget
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://laconi.ca/
+ */
+
+class ConversationTree extends NoticeList
+{
+    var $tree  = null;
+    var $table = null;
+
+    /**
+     * Show the tree of notices
+     *
+     * @return void
+     */
+
+    function show()
+    {
+        $cnt = 0;
+
+        $this->tree  = array();
+        $this->table = array();
+
+        while ($this->notice->fetch()) {
+
+            $cnt++;
+
+            $id     = $this->notice->id;
+            $notice = clone($this->notice);
+
+            $this->table[$id] = $notice;
+
+            if (is_null($notice->reply_to)) {
+                $this->tree['root'] = array($notice->id);
+            } else if (array_key_exists($notice->reply_to, $this->tree)) {
+                $this->tree[$notice->reply_to][] = $notice->id;
+            } else {
+                $this->tree[$notice->reply_to] = array($notice->id);
+            }
+        }
+
+        $this->out->elementStart('div', array('id' =>'notices_primary'));
+        $this->out->element('h2', null, _('Notices'));
+        $this->out->elementStart('ul', array('class' => 'notices'));
+
+        if (array_key_exists('root', $this->tree)) {
+            $rootid = $this->tree['root'][0];
+            $this->showNoticePlus($rootid);
+        }
+
+        $this->out->elementEnd('ul');
+        $this->out->elementEnd('div');
+
+        return $cnt;
+    }
+
+    /**
+     * Shows a notice plus its list of children.
+     *
+     * @param integer $id ID of the notice to show
+     *
+     * @return void
+     */
+
+    function showNoticePlus($id)
+    {
+        $notice = $this->table[$id];
+
+        // We take responsibility for doing the li
+
+        $this->out->elementStart('li', array('class' => 'hentry notice',
+                                             'id' => 'notice-' . $this->notice->id));
+
+        $item = $this->newListItem($notice);
+        $item->show();
+
+        if (array_key_exists($id, $this->tree)) {
+            $children = $this->tree[$id];
+
+            $this->out->elementStart('ul', array('class' => 'notices'));
+
+            foreach ($children as $child) {
+                $this->showNoticePlus($child);
+            }
+
+            $this->out->elementEnd('ul');
+        }
+
+        $this->out->elementEnd('li');
+    }
+
+    /**
+     * Override parent class to return our preferred item.
+     *
+     * @param Notice $notice Notice to display
+     *
+     * @return NoticeListItem a list item to show
+     */
+
+    function newListItem($notice)
+    {
+        return new ConversationTreeItem($notice, $this->out);
+    }
+}
+
+/**
+ * Conversation tree list item
+ *
+ * Special class of NoticeListItem for use inside conversation trees.
+ *
+ * @category Widget
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link     http://laconi.ca/
+ */
+
+class ConversationTreeItem extends NoticeListItem
+{
+    /**
+     * start a single notice.
+     *
+     * The default creates the <li>; we skip, since the ConversationTree
+     * takes care of that.
+     *
+     * @return void
+     */
+
+    function showStart()
+    {
+        return;
+    }
+
+    /**
+     * finish the notice
+     *
+     * The default closes the <li>; we skip, since the ConversationTree
+     * takes care of that.
+     *
+     * @return void
+     */
+
+    function showEnd()
+    {
+        return;
+    }
+
+    /**
+     * show link to notice conversation page
+     *
+     * Since we're only used on the conversation page, we skip this
+     *
+     * @return void
+     */
+
+    function showContext()
+    {
+        return;
+    }
+}
\ No newline at end of file
index 6c350b33ab36e3c45d278f5009f20d1259ef435c..e733f9650ad12eb2e75960f3bdec0df9054a5019 100644 (file)
@@ -112,8 +112,8 @@ class DeletenoticeAction extends DeleteAction
         $this->hidden('token', common_session_token());
         $this->hidden('notice', $this->trimmed('notice'));
         $this->element('p', null, _('Are you sure you want to delete this notice?'));
-        $this->submit('form_action-yes', _('Yes'), 'submit form_action-primary', 'yes');
-        $this->submit('form_action-no', _('No'), 'submit form_action-secondary', 'no');
+        $this->submit('form_action-no', _('No'), 'submit form_action-primary', 'no', _("Do not delete this notice"));
+        $this->submit('form_action-yes', _('Yes'), 'submit form_action-secondary', 'yes', _('Delete this notice'));
         $this->elementEnd('fieldset');
         $this->elementEnd('form');
     }
index cdd950e78c42f1ac966d4779eda1f39beb5e8936..315e5a199c2ce06d29eee3b2c213ba8e06dad906 100644 (file)
@@ -76,14 +76,22 @@ class DesignsettingsAction extends AccountSettingsAction
                                           'action' =>
                                           common_local_url('designsettings')));
         $this->elementStart('fieldset');
-//        $this->element('legend', null, _('Design settings'));
         $this->hidden('token', common_session_token());
 
         $this->elementStart('fieldset', array('id' => 'settings_design_background-image'));
         $this->element('legend', null, _('Change background image'));
         $this->elementStart('ul', 'form_data');
         $this->elementStart('li');
-        $this->element('p', null, _('Upload background image'));
+        $this->element('label', array('for' => 'design_ background-image_file'), 
+                                _('Upload file'));
+        $this->element('input', array('name' => 'design_background-image_file',
+                                      'type' => 'file',
+                                      'id' => 'design_background-image_file'));
+        $this->element('p', 'form_guide', _('You can upload your personal background image. The maximum file size is 2Mb.'));
+        $this->element('input', array('name' => 'MAX_FILE_SIZE',
+                                      'type' => 'hidden',
+                                      'id' => 'MAX_FILE_SIZE',
+                                      'value' => ImageFile::maxFileSizeInt()));
         $this->elementEnd('li');
         $this->elementEnd('ul');
         $this->elementEnd('fieldset');
@@ -91,28 +99,59 @@ class DesignsettingsAction extends AccountSettingsAction
         $this->elementStart('fieldset', array('id' => 'settings_design_color'));
         $this->element('legend', null, _('Change colours'));
         $this->elementStart('ul', 'form_data');
-        $this->elementStart('li');
-        $this->input('color-1', _('Background color'), '#F0F2F5', null);
-        $this->elementEnd('li');
-        $this->elementStart('li');
-        $this->input('color-2', _('Content background color'), '#FFFFFF', null);
-        $this->elementEnd('li');
-        $this->elementStart('li');
-        $this->input('color-3', _('Sidebar background color'), '#CEE1E9', null);
-        $this->elementEnd('li');
-        $this->elementStart('li');
-        $this->input('color-4', _('Text color'), '#000000', null);
-        $this->elementEnd('li');
-        $this->elementStart('li');
-        $this->input('color-5', _('Link color'), '#002E6E', null);
-        $this->elementEnd('li');
-        $this->elementEnd('ul');
-        $this->element('div', array('id' => 'color-picker'));
-        $this->elementEnd('fieldset');
 
+        //This is a JSON object in the DB field. Here for testing. Remove later.
+        $userSwatch = '{"body":{"background-color":"#F0F2F5"},
+                        "#content":{"background-color":"#FFFFFF"},
+                        "#aside_primary":{"background-color":"#CEE1E9"},
+                        "html body":{"color":"#000000"},
+                        "a":{"color":"#002E6E"}}';
+
+        //Default theme swatch -- Where should this be stored?
+        $defaultSwatch = array('body' => array('background-color' => '#F0F2F5'),
+                               '#content' => array('background-color' => '#FFFFFF'),
+                               '#aside_primary' => array('background-color' => '#CEE1E9'),
+                               'html body' => array('color' => '#000000'),
+                               'a' => array('color' => '#002E6E'));
+
+        $userSwatch = ($userSwatch) ? json_decode($userSwatch, true) : $defaultSwatch;
+
+        $s = 0;
+        $labelSwatch = array('Background',
+                             'Content',
+                             'Sidebar',
+                             'Text',
+                             'Links');
+        foreach($userSwatch as $propertyvalue => $value) {
+            $foo = array_values($value);
+            $this->elementStart('li');
+            $this->element('label', array('for' => 'swatch-'.$s), _($labelSwatch[$s]));
+            $this->element('input', array('name' => 'swatch-'.$s, //prefer swatch[$s] ?
+                                          'type' => 'text',
+                                          'id' => 'swatch-'.$s,
+                                          'class' => 'swatch',
+                                          'maxlength' => '7',
+                                          'size' => '7',
+                                          'value' => $foo[0]));
+            $this->elementEnd('li');
+            $s++;
+        }
 
-        $this->submit('save', _('Save'));
+        $this->elementEnd('ul');
+        $this->elementEnd('fieldset');
 
+        $this->element('input', array('id' => 'settings_design_reset',
+                                      'type' => 'reset',
+                                      'value' => 'Reset',
+                                      'class' => 'submit form_action-primary',
+                                      'title' => _('Reset back to default')));
+        $this->submit('save', _('Save'), 'submit form_action-secondary', 'save', _('Save design'));
+
+/*TODO: Check submitted form values: 
+json_encode(form values)
+if submitted Swatch == DefaultSwatch, don't store in DB.
+else store in BD
+*/
         $this->elementEnd('fieldset');
         $this->elementEnd('form');
 
@@ -187,7 +226,7 @@ class DesignsettingsAction extends AccountSettingsAction
 
 
     /**
-     * Add the jCrop stylesheet
+     * Add the Farbtastic stylesheet
      *
      * @return void
      */
@@ -205,7 +244,7 @@ class DesignsettingsAction extends AccountSettingsAction
     }
 
     /**
-     * Add the jCrop scripts
+     * Add the Farbtastic scripts
      *
      * @return void
      */
@@ -214,14 +253,12 @@ class DesignsettingsAction extends AccountSettingsAction
     {
         parent::showScripts();
 
-//        if ($this->mode == 'crop') {
-            $farbtasticPack = common_path('js/farbtastic/farbtastic.js');
-            $farbtasticGo   = common_path('js/farbtastic/farbtastic.go.js');
+        $farbtasticPack = common_path('js/farbtastic/farbtastic.js');
+        $farbtasticGo   = common_path('js/farbtastic/farbtastic.go.js');
 
-            $this->element('script', array('type' => 'text/javascript',
-                                           'src' => $farbtasticPack));
-            $this->element('script', array('type' => 'text/javascript',
-                                           'src' => $farbtasticGo));
-//        }
+        $this->element('script', array('type' => 'text/javascript',
+                                       'src' => $farbtasticPack));
+        $this->element('script', array('type' => 'text/javascript',
+                                       'src' => $farbtasticGo));
     }
 }
index f85bf1b19002f288a04fd2175ee7c94f6f4c5435..6b46b8dec7ecf94b94667aa81173a907b66b9257 100644 (file)
@@ -107,7 +107,7 @@ class FavoritesrssAction extends Rss10Action
         $c    = array('url' => common_local_url('favoritesrss',
                                         array('nickname' =>
                                         $user->nickname)),
-                   'title' => sprintf(_("%s favorite notices"), $user->nickname),
+                   'title' => sprintf(_("%s's favorite notices"), $user->nickname),
                    'link' => common_local_url('showfavorites',
                                         array('nickname' =>
                                         $user->nickname)),
index 952185742f969656ae6b0de9b7bf36451d9e6fbd..b08b96df6c2afd017cecf9852fc663ec41b65e67 100644 (file)
@@ -191,11 +191,28 @@ class FinishopenidloginAction extends Action
     {
         # FIXME: save invite code before redirect, and check here
 
-        if (common_config('site', 'closed') || common_config('site', 'inviteonly')) {
+        if (common_config('site', 'closed')) {
             $this->clientError(_('Registration not allowed.'));
             return;
         }
 
+        $invite = null;
+
+        if (common_config('site', 'inviteonly')) {
+            $code = $_SESSION['invitecode'];
+            if (empty($code)) {
+                $this->clientError(_('Registration not allowed.'));
+                return;
+            }
+
+            $invite = Invitation::staticGet($code);
+
+            if (empty($invite)) {
+                $this->clientError(_('Not a valid invitation code.'));
+                return;
+            }
+        }
+
         $nickname = $this->trimmed('newname');
 
         if (!Validate::string($nickname, array('min_length' => 1,
@@ -257,10 +274,16 @@ class FinishopenidloginAction extends Action
         # XXX: add language
         # XXX: add timezone
 
-        $user = User::register(array('nickname' => $nickname,
-                                     'email' => $email,
-                                     'fullname' => $fullname,
-                                     'location' => $location));
+        $args = array('nickname' => $nickname,
+                      'email' => $email,
+                      'fullname' => $fullname,
+                      'location' => $location);
+
+        if (!empty($invite)) {
+            $args['code'] = $invite->code;
+        }
+
+        $user = User::register($args);
 
         $result = oid_link_user($user->id, $canonical, $display);
 
index a9a2eef877c442396424ada89c55eab7289e27b8..0b7280a11c33648584096b95176eafb63fea7835 100644 (file)
@@ -34,7 +34,7 @@ if (!defined('LACONICA')) {
 
 require_once INSTALLDIR.'/lib/rssaction.php';
 
-define('MEMBERS_PER_SECTION', 81);
+define('MEMBERS_PER_SECTION', 27);
 
 /**
  * Group RSS feed
index 9f3bfe2470a2253d0e93acd9ca3ec2bf976c5423..c34b10987a76985e08c15ad8adc8ac8f5a3211bb 100644 (file)
@@ -70,10 +70,20 @@ class LogoutAction extends Action
         if (!common_logged_in()) {
             $this->clientError(_('Not logged in.'));
         } else {
-            common_set_user(null);
-            common_real_login(false); // not logged in
-            common_forgetme(); // don't log back in!
+            if (Event::handle('StartLogout', array($this))) {
+                $this->logout();
+            }
+            Event::handle('EndLogout', array($this));
+
             common_redirect(common_local_url('public'), 303);
         }
     }
+
+    function logout()
+    {
+        common_set_user(null);
+        common_real_login(false); // not logged in
+        common_forgetme(); // don't log back in!
+    }
+
 }
index 82276ff341c6e6d8ea54d033fe2588aba48e21c3..52d4899ba2d453d8012465eaa60def69458df4a2 100644 (file)
@@ -172,15 +172,54 @@ class NewmessageAction extends Action
 
         $this->notify($user, $this->other, $message);
 
-        $url = common_local_url('outbox', array('nickname' => $user->nickname));
+        if ($this->boolean('ajax')) {
+            $this->startHTML('text/xml;charset=utf-8');
+            $this->elementStart('head');
+            $this->element('title', null, _('Message sent'));
+            $this->elementEnd('head');
+            $this->elementStart('body');
+            $this->element('p', array('id' => 'command_result'),
+                sprintf(_('Direct message to %s sent'),
+                    $this->other->nickname));
+            $this->elementEnd('body');
+            $this->elementEnd('html');
+        } else {
+            $url = common_local_url('outbox',
+                array('nickname' => $user->nickname));
+            common_redirect($url, 303);
+        }
+    }
 
-        common_redirect($url, 303);
+    /**
+     * Show an Ajax-y error message
+     *
+     * Goes back to the browser, where it's shown in a popup.
+     *
+     * @param string $msg Message to show
+     *
+     * @return void
+     */
+
+    function ajaxErrorMsg($msg)
+    {
+        $this->startHTML('text/xml;charset=utf-8', true);
+        $this->elementStart('head');
+        $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');
     }
 
     function showForm($msg = null)
     {
-        $this->msg = $msg;
+        if ($msg && $this->boolean('ajax')) {
+            $this->ajaxErrorMsg($msg);
+            return;
+        }
 
+        $this->msg = $msg;
         $this->showPage();
     }
 
index cbd04c58b226c0a609c44996f7048bcf28f7abcd..ae0ff9636335809f2e9aaca4697d56b567e85332 100644 (file)
@@ -158,7 +158,8 @@ class NewnoticeAction extends Action
             $replyto = 'false';
         }
 
-        $notice = Notice::saveNew($user->id, $content, 'web', 1,
+//        $notice = Notice::saveNew($user->id, $content_shortened, 'web', 1,
+        $notice = Notice::saveNew($user->id, $content_shortened, 'web', 1,
                                   ($replyto == 'false') ? null : $replyto);
 
         if (is_string($notice)) {
@@ -166,6 +167,8 @@ class NewnoticeAction extends Action
             return;
         }
 
+        $this->saveUrls($notice);
+
         common_broadcast_notice($notice);
 
         if ($this->boolean('ajax')) {
@@ -191,6 +194,24 @@ class NewnoticeAction extends Action
         }
     }
 
+    /** save all urls in the notice to the db
+     *
+     * follow redirects and save all available file information
+     * (mimetype, date, size, oembed, etc.)
+     *
+     * @param class $notice Notice to pull URLs from
+     *
+     * @return void
+     */
+    function saveUrls($notice) {
+        common_replace_urls_callback($notice->content, array($this, 'saveUrl'), $notice->id);
+    }
+
+    function saveUrl($data) {
+        list($url, $notice_id) = $data;
+        $zzz = File::processNew($url, $notice_id);
+    }
+
     /**
      * Show an Ajax-y error message
      *
index 92469d20f89a1af6046a09b6ff27e6a2eed02902..5f59ebc014d57bfe9ff4a1279dc3a4714e98fb72 100644 (file)
@@ -67,8 +67,8 @@ class OpenidsettingsAction extends AccountSettingsAction
 
     function getInstructions()
     {
-        return _('[OpenID](%%doc.openid%%) lets you log into many sites ' .
-                 ' with the same user account. '.
+        return _('[OpenID](%%doc.openid%%) lets you log into many sites' .
+                 ' with the same user account.'.
                  ' Manage your associated OpenIDs from here.');
     }
 
index 60f7c0796e2c64246f9064c25db02d70e4361016..fb847680b9c158e85dc319d89029933edddabc2d 100644 (file)
@@ -91,67 +91,68 @@ class ProfilesettingsAction extends AccountSettingsAction
         $this->element('legend', null, _('Profile information'));
         $this->hidden('token', common_session_token());
 
-        # too much common patterns here... abstractable?
-
+        // too much common patterns here... abstractable?
         $this->elementStart('ul', 'form_data');
-        $this->elementStart('li');
-        $this->input('nickname', _('Nickname'),
-                     ($this->arg('nickname')) ? $this->arg('nickname') : $profile->nickname,
-                     _('1-64 lowercase letters or numbers, no punctuation or spaces'));
-        $this->elementEnd('li');
-        $this->elementStart('li');
-        $this->input('fullname', _('Full name'),
-                     ($this->arg('fullname')) ? $this->arg('fullname') : $profile->fullname);
-        $this->elementEnd('li');
-        $this->elementStart('li');
-        $this->input('homepage', _('Homepage'),
-                     ($this->arg('homepage')) ? $this->arg('homepage') : $profile->homepage,
-                     _('URL of your homepage, blog, or profile on another site'));
-        $this->elementEnd('li');
-        $this->elementStart('li');
-        $this->textarea('bio', _('Bio'),
-                        ($this->arg('bio')) ? $this->arg('bio') : $profile->bio,
-                        _('Describe yourself and your interests in 140 chars'));
-        $this->elementEnd('li');
-        $this->elementStart('li');
-        $this->input('location', _('Location'),
-                     ($this->arg('location')) ? $this->arg('location') : $profile->location,
-                     _('Where you are, like "City, State (or Region), Country"'));
-        $this->elementEnd('li');
-        $this->elementStart('li');
-        $this->input('tags', _('Tags'),
-                     ($this->arg('tags')) ? $this->arg('tags') : implode(' ', $user->getSelfTags()),
-                     _('Tags for yourself (letters, numbers, -, ., and _), comma- or space- separated'));
-        $this->elementEnd('li');
-        $this->elementStart('li');
-        $language = common_language();
-        $this->dropdown('language', _('Language'),
-                        get_nice_language_list(), _('Preferred language'),
-                        true, $language);
-        $this->elementEnd('li');
-        $timezone = common_timezone();
-        $timezones = array();
-        foreach(DateTimeZone::listIdentifiers() as $k => $v) {
-            $timezones[$v] = $v;
+        if (Event::handle('StartProfileFormData', array($this))) {
+            $this->elementStart('li');
+            $this->input('nickname', _('Nickname'),
+                         ($this->arg('nickname')) ? $this->arg('nickname') : $profile->nickname,
+                         _('1-64 lowercase letters or numbers, no punctuation or spaces'));
+            $this->elementEnd('li');
+            $this->elementStart('li');
+            $this->input('fullname', _('Full name'),
+                         ($this->arg('fullname')) ? $this->arg('fullname') : $profile->fullname);
+            $this->elementEnd('li');
+            $this->elementStart('li');
+            $this->input('homepage', _('Homepage'),
+                         ($this->arg('homepage')) ? $this->arg('homepage') : $profile->homepage,
+                         _('URL of your homepage, blog, or profile on another site'));
+            $this->elementEnd('li');
+            $this->elementStart('li');
+            $this->textarea('bio', _('Bio'),
+                            ($this->arg('bio')) ? $this->arg('bio') : $profile->bio,
+                            _('Describe yourself and your interests in 140 chars'));
+            $this->elementEnd('li');
+            $this->elementStart('li');
+            $this->input('location', _('Location'),
+                         ($this->arg('location')) ? $this->arg('location') : $profile->location,
+                         _('Where you are, like "City, State (or Region), Country"'));
+            $this->elementEnd('li');
+            Event::handle('EndProfileFormData', array($this));
+            $this->elementStart('li');
+            $this->input('tags', _('Tags'),
+                         ($this->arg('tags')) ? $this->arg('tags') : implode(' ', $user->getSelfTags()),
+                         _('Tags for yourself (letters, numbers, -, ., and _), comma- or space- separated'));
+            $this->elementEnd('li');
+            $this->elementStart('li');
+            $language = common_language();
+            $this->dropdown('language', _('Language'),
+                            get_nice_language_list(), _('Preferred language'),
+                            false, $language);
+            $this->elementEnd('li');
+            $timezone = common_timezone();
+            $timezones = array();
+            foreach(DateTimeZone::listIdentifiers() as $k => $v) {
+                $timezones[$v] = $v;
+            }
+            $this->elementStart('li');
+            $this->dropdown('timezone', _('Timezone'),
+                            $timezones, _('What timezone are you normally in?'),
+                            true, $timezone);
+            $this->elementEnd('li');
+            $this->elementStart('li');
+            $this->checkbox('autosubscribe',
+                            _('Automatically subscribe to whoever '.
+                              'subscribes to me (best for non-humans)'),
+                            ($this->arg('autosubscribe')) ?
+                            $this->boolean('autosubscribe') : $user->autosubscribe);
+            $this->elementEnd('li');
         }
-        $this->elementStart('li');
-        $this->dropdown('timezone', _('Timezone'),
-                        $timezones, _('What timezone are you normally in?'),
-                        true, $timezone);
-        $this->elementEnd('li');
-        $this->elementStart('li');
-        $this->checkbox('autosubscribe',
-                        _('Automatically subscribe to whoever '.
-                          'subscribes to me (best for non-humans)'),
-                        ($this->arg('autosubscribe')) ?
-                        $this->boolean('autosubscribe') : $user->autosubscribe);
-        $this->elementEnd('li');
         $this->elementEnd('ul');
         $this->submit('save', _('Save'));
 
         $this->elementEnd('fieldset');
         $this->elementEnd('form');
-
     }
 
     /**
@@ -165,158 +166,158 @@ class ProfilesettingsAction extends AccountSettingsAction
 
     function handlePost()
     {
-        # CSRF protection
-
+        // CSRF protection
         $token = $this->trimmed('token');
         if (!$token || $token != common_session_token()) {
             $this->showForm(_('There was a problem with your session token. '.
-                               'Try again, please.'));
+                        'Try again, please.'));
             return;
         }
 
-        $nickname = $this->trimmed('nickname');
-        $fullname = $this->trimmed('fullname');
-        $homepage = $this->trimmed('homepage');
-        $bio = $this->trimmed('bio');
-        $location = $this->trimmed('location');
-        $autosubscribe = $this->boolean('autosubscribe');
-        $language = $this->trimmed('language');
-        $timezone = $this->trimmed('timezone');
-        $tagstring = $this->trimmed('tags');
-
-        # Some validation
-
-        if (!Validate::string($nickname, array('min_length' => 1,
-                                               'max_length' => 64,
-                                               'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
-            $this->showForm(_('Nickname must have only lowercase letters and numbers and no spaces.'));
-            return;
-        } else if (!User::allowed_nickname($nickname)) {
-            $this->showForm(_('Not a valid nickname.'));
-            return;
-        } else if (!is_null($homepage) && (strlen($homepage) > 0) &&
-                   !Validate::uri($homepage, array('allowed_schemes' => array('http', 'https')))) {
-            $this->showForm(_('Homepage is not a valid URL.'));
-            return;
-        } else if (!is_null($fullname) && mb_strlen($fullname) > 255) {
-            $this->showForm(_('Full name is too long (max 255 chars).'));
-            return;
-        } else if (!is_null($bio) && mb_strlen($bio) > 140) {
-            $this->showForm(_('Bio is too long (max 140 chars).'));
-            return;
-        } else if (!is_null($location) && mb_strlen($location) > 255) {
-            $this->showForm(_('Location is too long (max 255 chars).'));
-            return;
-        }  else if (is_null($timezone) || !in_array($timezone, DateTimeZone::listIdentifiers())) {
-            $this->showForm(_('Timezone not selected.'));
-            return;
-        } else if ($this->nicknameExists($nickname)) {
-            $this->showForm(_('Nickname already in use. Try another one.'));
-            return;
-        } else if (!is_null($language) && strlen($language) > 50) {
-            $this->showForm(_('Language is too long (max 50 chars).'));
-            return;
-        }
+        if (Event::handle('StartProfileSaveForm', array($this))) {
+
+            $nickname = $this->trimmed('nickname');
+            $fullname = $this->trimmed('fullname');
+            $homepage = $this->trimmed('homepage');
+            $bio = $this->trimmed('bio');
+            $location = $this->trimmed('location');
+            $autosubscribe = $this->boolean('autosubscribe');
+            $language = $this->trimmed('language');
+            $timezone = $this->trimmed('timezone');
+            $tagstring = $this->trimmed('tags');
+
+            // Some validation
+            if (!Validate::string($nickname, array('min_length' => 1,
+                            'max_length' => 64,
+                            'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
+                $this->showForm(_('Nickname must have only lowercase letters and numbers and no spaces.'));
+                return;
+            } else if (!User::allowed_nickname($nickname)) {
+                $this->showForm(_('Not a valid nickname.'));
+                return;
+            } else if (!is_null($homepage) && (strlen($homepage) > 0) &&
+                    !Validate::uri($homepage, array('allowed_schemes' => array('http', 'https')))) {
+                $this->showForm(_('Homepage is not a valid URL.'));
+                return;
+            } else if (!is_null($fullname) && mb_strlen($fullname) > 255) {
+                $this->showForm(_('Full name is too long (max 255 chars).'));
+                return;
+            } else if (!is_null($bio) && mb_strlen($bio) > 140) {
+                $this->showForm(_('Bio is too long (max 140 chars).'));
+                return;
+            } else if (!is_null($location) && mb_strlen($location) > 255) {
+                $this->showForm(_('Location is too long (max 255 chars).'));
+                return;
+            }  else if (is_null($timezone) || !in_array($timezone, DateTimeZone::listIdentifiers())) {
+                $this->showForm(_('Timezone not selected.'));
+                return;
+            } else if ($this->nicknameExists($nickname)) {
+                $this->showForm(_('Nickname already in use. Try another one.'));
+                return;
+            } else if (!is_null($language) && strlen($language) > 50) {
+                $this->showForm(_('Language is too long (max 50 chars).'));
+                return;
+            }
 
-        if ($tagstring) {
-            $tags = array_map('common_canonical_tag', preg_split('/[\s,]+/', $tagstring));
-        } else {
-            $tags = array();
-        }
+            if ($tagstring) {
+                $tags = array_map('common_canonical_tag', preg_split('/[\s,]+/', $tagstring));
+            } else {
+                $tags = array();
+            }
 
-        foreach ($tags as $tag) {
-            if (!common_valid_profile_tag($tag)) {
-                $this->showForm(sprintf(_('Invalid tag: "%s"'), $tag));
-                return;
+            foreach ($tags as $tag) {
+                if (!common_valid_profile_tag($tag)) {
+                    $this->showForm(sprintf(_('Invalid tag: "%s"'), $tag));
+                    return;
+                }
             }
-        }
 
-        $user = common_current_user();
+            $user = common_current_user();
 
-        $user->query('BEGIN');
+            $user->query('BEGIN');
 
-        if ($user->nickname != $nickname ||
-            $user->language != $language ||
-            $user->timezone != $timezone) {
+            if ($user->nickname != $nickname ||
+                    $user->language != $language ||
+                    $user->timezone != $timezone) {
 
-            common_debug('Updating user nickname from ' . $user->nickname . ' to ' . $nickname,
-                         __FILE__);
-            common_debug('Updating user language from ' . $user->language . ' to ' . $language,
-                         __FILE__);
-            common_debug('Updating user timezone from ' . $user->timezone . ' to ' . $timezone,
-                         __FILE__);
+                common_debug('Updating user nickname from ' . $user->nickname . ' to ' . $nickname,
+                        __FILE__);
+                common_debug('Updating user language from ' . $user->language . ' to ' . $language,
+                        __FILE__);
+                common_debug('Updating user timezone from ' . $user->timezone . ' to ' . $timezone,
+                        __FILE__);
 
-            $original = clone($user);
+                $original = clone($user);
 
-            $user->nickname = $nickname;
-            $user->language = $language;
-            $user->timezone = $timezone;
+                $user->nickname = $nickname;
+                $user->language = $language;
+                $user->timezone = $timezone;
 
-            $result = $user->updateKeys($original);
+                $result = $user->updateKeys($original);
 
-            if ($result === false) {
-                common_log_db_error($user, 'UPDATE', __FILE__);
-                $this->serverError(_('Couldn\'t update user.'));
-                return;
-            } else {
-                # Re-initialize language environment if it changed
-                common_init_language();
+                if ($result === false) {
+                    common_log_db_error($user, 'UPDATE', __FILE__);
+                    $this->serverError(_('Couldn\'t update user.'));
+                    return;
+                } else {
+                    // Re-initialize language environment if it changed
+                    common_init_language();
+                }
             }
-        }
-
-        # XXX: XOR
 
-        if ($user->autosubscribe ^ $autosubscribe) {
+// XXX: XOR
+            if ($user->autosubscribe ^ $autosubscribe) {
 
-            $original = clone($user);
+                $original = clone($user);
 
-            $user->autosubscribe = $autosubscribe;
+                $user->autosubscribe = $autosubscribe;
 
-            $result = $user->update($original);
+                $result = $user->update($original);
 
-            if ($result === false) {
-                common_log_db_error($user, 'UPDATE', __FILE__);
-                $this->serverError(_('Couldn\'t update user for autosubscribe.'));
-                return;
+                if ($result === false) {
+                    common_log_db_error($user, 'UPDATE', __FILE__);
+                    $this->serverError(_('Couldn\'t update user for autosubscribe.'));
+                    return;
+                }
             }
-        }
-
-        $profile = $user->getProfile();
 
-        $orig_profile = clone($profile);
+            $profile = $user->getProfile();
 
-        $profile->nickname = $user->nickname;
-        $profile->fullname = $fullname;
-        $profile->homepage = $homepage;
-        $profile->bio = $bio;
-        $profile->location = $location;
-        $profile->profileurl = common_profile_url($nickname);
+            $orig_profile = clone($profile);
 
-        common_debug('Old profile: ' . common_log_objstring($orig_profile), __FILE__);
-        common_debug('New profile: ' . common_log_objstring($profile), __FILE__);
+            $profile->nickname = $user->nickname;
+            $profile->fullname = $fullname;
+            $profile->homepage = $homepage;
+            $profile->bio = $bio;
+            $profile->location = $location;
+            $profile->profileurl = common_profile_url($nickname);
 
-        $result = $profile->update($orig_profile);
+            common_debug('Old profile: ' . common_log_objstring($orig_profile), __FILE__);
+            common_debug('New profile: ' . common_log_objstring($profile), __FILE__);
 
-        if (!$result) {
-            common_log_db_error($profile, 'UPDATE', __FILE__);
-            $this->serverError(_('Couldn\'t save profile.'));
-            return;
-        }
+            $result = $profile->update($orig_profile);
 
-        # Set the user tags
+            if (!$result) {
+                common_log_db_error($profile, 'UPDATE', __FILE__);
+                $this->serverError(_('Couldn\'t save profile.'));
+                return;
+            }
 
-        $result = $user->setSelfTags($tags);
+            // Set the user tags
+            $result = $user->setSelfTags($tags);
 
-        if (!$result) {
-            $this->serverError(_('Couldn\'t save tags.'));
-            return;
-        }
+            if (!$result) {
+                $this->serverError(_('Couldn\'t save tags.'));
+                return;
+            }
 
-        $user->query('COMMIT');
+            $user->query('COMMIT');
+            Event::handle('EndProfileSaveForm', array($this));
+            common_broadcast_profile($profile);
 
-        common_broadcast_profile($profile);
+            $this->showForm(_('Settings saved.'), true);
 
-        $this->showForm(_('Settings saved.'), true);
+        }
     }
 
     function nicknameExists($nickname)
index 620fe7eb8e20ed9c5f05f5615fe19356980e6cba..82263fcd59903cc921e42f4c55bc14c3cf2c94a4 100644 (file)
@@ -151,11 +151,11 @@ class RecoverpasswordAction extends Action
                 $this->element('p', null,
                                _('If you\'ve forgotten or lost your' .
                                  ' password, you can get a new one sent to' .
-                                 ' the email address you have stored ' .
+                                 ' the email address you have stored' .
                                  ' in your account.'));
             } else if ($this->mode == 'reset') {
                 $this->element('p', null,
-                               _('You\'ve been identified. Enter a ' .
+                               _('You\'ve been identified. Enter a' .
                                  ' new password below. '));
             }
             $this->elementEnd('div');
index 5c6fe39d3de3f90cd07375b6b67cf0ac313a1e40..dcbbbdb6a6de9afb238715b7ff1cac3d78f40b7d 100644 (file)
@@ -55,6 +55,45 @@ class RegisterAction extends Action
 
     var $registered = false;
 
+    /**
+     * Prepare page to run
+     *
+     *
+     * @param $args
+     * @return string title
+     */
+
+    function prepare($args)
+    {
+        parent::prepare($args);
+        $this->code = $this->trimmed('code');
+
+        if (empty($this->code)) {
+            common_ensure_session();
+            if (array_key_exists('invitecode', $_SESSION)) {
+                $this->code = $_SESSION['invitecode'];
+            }
+        }
+
+        if (common_config('site', 'inviteonly') && empty($this->code)) {
+            $this->clientError(_('Sorry, only invited people can register.'));
+            return false;
+        }
+
+        if (!empty($this->code)) {
+            $this->invite = Invitation::staticGet('code', $this->code);
+            if (empty($this->invite)) {
+                $this->clientError(_('Sorry, invalid invitation code.'));
+                return false;
+            }
+            // Store this in case we need it
+            common_ensure_session();
+            $_SESSION['invitecode'] = $this->code;
+        }
+
+        return true;
+    }
+
     /**
      * Title of the page
      *
@@ -108,109 +147,109 @@ class RegisterAction extends Action
 
     function tryRegister()
     {
-        $token = $this->trimmed('token');
-        if (!$token || $token != common_session_token()) {
-            $this->showForm(_('There was a problem with your session token. '.
-                              'Try again, please.'));
-            return;
-        }
-
-        $nickname = $this->trimmed('nickname');
-        $email    = $this->trimmed('email');
-        $fullname = $this->trimmed('fullname');
-        $homepage = $this->trimmed('homepage');
-        $bio      = $this->trimmed('bio');
-        $location = $this->trimmed('location');
-
-        // We don't trim these... whitespace is OK in a password!
-
-        $password = $this->arg('password');
-        $confirm  = $this->arg('confirm');
+        if (Event::handle('StartRegistrationTry', array($this))) {
+            $token = $this->trimmed('token');
+            if (!$token || $token != common_session_token()) {
+                $this->showForm(_('There was a problem with your session token. '.
+                                  'Try again, please.'));
+                return;
+            }
 
-        // invitation code, if any
+            $nickname = $this->trimmed('nickname');
+            $email    = $this->trimmed('email');
+            $fullname = $this->trimmed('fullname');
+            $homepage = $this->trimmed('homepage');
+            $bio      = $this->trimmed('bio');
+            $location = $this->trimmed('location');
 
-        $code = $this->trimmed('code');
+            // We don't trim these... whitespace is OK in a password!
+            $password = $this->arg('password');
+            $confirm  = $this->arg('confirm');
 
-        $invite = null;
-
-        if ($code) {
-            $invite = Invitation::staticGet($code);
-        }
+            // invitation code, if any
+            $code = $this->trimmed('code');
 
-        if (common_config('site', 'inviteonly') && !($code && !empty($invite))) {
-            $this->clientError(_('Sorry, only invited people can register.'));
-            return;
-        }
+            if ($code) {
+                $invite = Invitation::staticGet($code);
+            }
 
-        // Input scrubbing
-
-        $nickname = common_canonical_nickname($nickname);
-        $email    = common_canonical_email($email);
-
-        if (!$this->boolean('license')) {
-            $this->showForm(_('You can\'t register if you don\'t '.
-                              'agree to the license.'));
-        } else if ($email && !Validate::email($email, true)) {
-            $this->showForm(_('Not a valid email address.'));
-        } else if (!Validate::string($nickname, array('min_length' => 1,
-                                                      'max_length' => 64,
-                                                      'format' => NICKNAME_FMT))) {
-            $this->showForm(_('Nickname must have only lowercase letters '.
-                              'and numbers and no spaces.'));
-        } else if ($this->nicknameExists($nickname)) {
-            $this->showForm(_('Nickname already in use. Try another one.'));
-        } else if (!User::allowed_nickname($nickname)) {
-            $this->showForm(_('Not a valid nickname.'));
-        } else if ($this->emailExists($email)) {
-            $this->showForm(_('Email address already exists.'));
-        } else if (!is_null($homepage) && (strlen($homepage) > 0) &&
-                   !Validate::uri($homepage,
-                                  array('allowed_schemes' =>
-                                        array('http', 'https')))) {
-            $this->showForm(_('Homepage is not a valid URL.'));
-            return;
-        } else if (!is_null($fullname) && mb_strlen($fullname) > 255) {
-            $this->showForm(_('Full name is too long (max 255 chars).'));
-            return;
-        } else if (!is_null($bio) && mb_strlen($bio) > 140) {
-            $this->showForm(_('Bio is too long (max 140 chars).'));
-            return;
-        } else if (!is_null($location) && mb_strlen($location) > 255) {
-            $this->showForm(_('Location is too long (max 255 chars).'));
-            return;
-        } else if (strlen($password) < 6) {
-            $this->showForm(_('Password must be 6 or more characters.'));
-            return;
-        } else if ($password != $confirm) {
-            $this->showForm(_('Passwords don\'t match.'));
-        } else if ($user = User::register(array('nickname' => $nickname,
-                                                'password' => $password,
-                                                'email' => $email,
-                                                'fullname' => $fullname,
-                                                'homepage' => $homepage,
-                                                'bio' => $bio,
-                                                'location' => $location,
-                                                'code' => $code))) {
-            if (!$user) {
-                $this->showForm(_('Invalid username or password.'));
+            if (common_config('site', 'inviteonly') && !($code && $invite)) {
+                $this->clientError(_('Sorry, only invited people can register.'));
                 return;
             }
-            // success!
-            if (!common_set_user($user)) {
-                $this->serverError(_('Error setting user.'));
+
+            // Input scrubbing
+            $nickname = common_canonical_nickname($nickname);
+            $email    = common_canonical_email($email);
+
+            if (!$this->boolean('license')) {
+                $this->showForm(_('You can\'t register if you don\'t '.
+                                  'agree to the license.'));
+            } else if ($email && !Validate::email($email, true)) {
+                $this->showForm(_('Not a valid email address.'));
+            } else if (!Validate::string($nickname, array('min_length' => 1,
+                                                          'max_length' => 64,
+                                                          'format' => NICKNAME_FMT))) {
+                $this->showForm(_('Nickname must have only lowercase letters '.
+                                  'and numbers and no spaces.'));
+            } else if ($this->nicknameExists($nickname)) {
+                $this->showForm(_('Nickname already in use. Try another one.'));
+            } else if (!User::allowed_nickname($nickname)) {
+                $this->showForm(_('Not a valid nickname.'));
+            } else if ($this->emailExists($email)) {
+                $this->showForm(_('Email address already exists.'));
+            } else if (!is_null($homepage) && (strlen($homepage) > 0) &&
+                       !Validate::uri($homepage,
+                                      array('allowed_schemes' =>
+                                            array('http', 'https')))) {
+                $this->showForm(_('Homepage is not a valid URL.'));
                 return;
+            } else if (!is_null($fullname) && mb_strlen($fullname) > 255) {
+                $this->showForm(_('Full name is too long (max 255 chars).'));
+                return;
+            } else if (!is_null($bio) && mb_strlen($bio) > 140) {
+                $this->showForm(_('Bio is too long (max 140 chars).'));
+                return;
+            } else if (!is_null($location) && mb_strlen($location) > 255) {
+                $this->showForm(_('Location is too long (max 255 chars).'));
+                return;
+            } else if (strlen($password) < 6) {
+                $this->showForm(_('Password must be 6 or more characters.'));
+                return;
+            } else if ($password != $confirm) {
+                $this->showForm(_('Passwords don\'t match.'));
+            } else if ($user = User::register(array('nickname' => $nickname,
+                                                    'password' => $password,
+                                                    'email' => $email,
+                                                    'fullname' => $fullname,
+                                                    'homepage' => $homepage,
+                                                    'bio' => $bio,
+                                                    'location' => $location,
+                                                    'code' => $code))) {
+                if (!$user) {
+                    $this->showForm(_('Invalid username or password.'));
+                    return;
+                }
+                // success!
+                if (!common_set_user($user)) {
+                    $this->serverError(_('Error setting user.'));
+                    return;
+                }
+                // this is a real login
+                common_real_login(true);
+                if ($this->boolean('rememberme')) {
+                    common_debug('Adding rememberme cookie for ' . $nickname);
+                    common_rememberme($user);
+                }
+
+                Event::handle('EndRegistrationTry', array($this));
+
+                // Re-init language env in case it changed (not yet, but soon)
+                common_init_language();
+                $this->showSuccess();
+            } else {
+                $this->showForm(_('Invalid username or password.'));
             }
-            // this is a real login
-            common_real_login(true);
-            if ($this->boolean('rememberme')) {
-                common_debug('Adding rememberme cookie for ' . $nickname);
-                common_rememberme($user);
-            }
-            // Re-init language env in case it changed (not yet, but soon)
-            common_init_language();
-            $this->showSuccess();
-        } else {
-            $this->showForm(_('Invalid username or password.'));
         }
     }
 
@@ -252,22 +291,24 @@ class RegisterAction extends Action
 
     // overrrided to add entry-title class
     function showPageTitle() {
-        $this->element('h1', array('class' => 'entry-title'), $this->title());
+        if (Event::handle('StartShowPageTitle', array($this))) {
+            $this->element('h1', array('class' => 'entry-title'), $this->title());
+        }
     }
 
     // overrided to add hentry, and content-inner class
     function showContentBlock()
-     {
-         $this->elementStart('div', array('id' => 'content', 'class' => 'hentry'));
-         $this->showPageTitle();
-         $this->showPageNoticeBlock();
-         $this->elementStart('div', array('id' => 'content_inner',
-             'class' => 'entry-content'));
-         // show the actual content (forms, lists, whatever)
-         $this->showContent();
-         $this->elementEnd('div');
-         $this->elementEnd('div');
-     }
+    {
+        $this->elementStart('div', array('id' => 'content', 'class' => 'hentry'));
+        $this->showPageTitle();
+        $this->showPageNoticeBlock();
+        $this->elementStart('div', array('id' => 'content_inner',
+                                         'class' => 'entry-content'));
+        // show the actual content (forms, lists, whatever)
+        $this->showContent();
+        $this->elementEnd('div');
+        $this->elementEnd('div');
+    }
 
     /**
      * Instructions or a notice for the page
@@ -362,82 +403,85 @@ class RegisterAction extends Action
         $this->element('legend', null, 'Account settings');
         $this->hidden('token', common_session_token());
 
-        if ($code) {
-            $this->hidden('code', $code);
+        if ($this->code) {
+            $this->hidden('code', $this->code);
         }
 
         $this->elementStart('ul', 'form_data');
-        $this->elementStart('li');
-        $this->input('nickname', _('Nickname'), $this->trimmed('nickname'),
-                     _('1-64 lowercase letters or numbers, '.
-                       'no punctuation or spaces. Required.'));
-        $this->elementEnd('li');
-        $this->elementStart('li');
-        $this->password('password', _('Password'),
-                        _('6 or more characters. Required.'));
-        $this->elementEnd('li');
-        $this->elementStart('li');
-        $this->password('confirm', _('Confirm'),
-                        _('Same as password above. Required.'));
-        $this->elementEnd('li');
-        $this->elementStart('li');
-        if (!empty($invite) && $invite->address_type == 'email') {
-            $this->input('email', _('Email'), $invite->address,
-                         _('Used only for updates, announcements, '.
-                           'and password recovery'));
-        } else {
-            $this->input('email', _('Email'), $this->trimmed('email'),
-                         _('Used only for updates, announcements, '.
-                           'and password recovery'));
-        }
-        $this->elementEnd('li');
-        $this->elementStart('li');
-        $this->input('fullname', _('Full name'),
-                     $this->trimmed('fullname'),
-                     _('Longer name, preferably your "real" name'));
-        $this->elementEnd('li');
-        $this->elementStart('li');
-        $this->input('homepage', _('Homepage'),
-                     $this->trimmed('homepage'),
-                     _('URL of your homepage, blog, '.
-                       'or profile on another site'));
-        $this->elementEnd('li');
-        $this->elementStart('li');
-        $this->textarea('bio', _('Bio'),
-                        $this->trimmed('bio'),
-                        _('Describe yourself and your '.
-                          'interests in 140 chars'));
-        $this->elementEnd('li');
-        $this->elementStart('li');
-        $this->input('location', _('Location'),
-                     $this->trimmed('location'),
-                     _('Where you are, like "City, '.
-                       'State (or Region), Country"'));
-        $this->elementEnd('li');
-        $this->elementStart('li', array('id' => 'settings_rememberme'));
-        $this->checkbox('rememberme', _('Remember me'),
-                        $this->boolean('rememberme'),
-                        _('Automatically login in the future; '.
-                          'not for shared computers!'));
-        $this->elementEnd('li');
-        $attrs = array('type' => 'checkbox',
-                       'id' => 'license',
-                       'class' => 'checkbox',
-                       'name' => 'license',
-                       'value' => 'true');
-        if ($this->boolean('license')) {
-            $attrs['checked'] = 'checked';
+        if (Event::handle('StartRegistrationFormData', array($this))) {
+            $this->elementStart('li');
+            $this->input('nickname', _('Nickname'), $this->trimmed('nickname'),
+                         _('1-64 lowercase letters or numbers, '.
+                           'no punctuation or spaces. Required.'));
+            $this->elementEnd('li');
+            $this->elementStart('li');
+            $this->password('password', _('Password'),
+                            _('6 or more characters. Required.'));
+            $this->elementEnd('li');
+            $this->elementStart('li');
+            $this->password('confirm', _('Confirm'),
+                            _('Same as password above. Required.'));
+            $this->elementEnd('li');
+            $this->elementStart('li');
+            if ($this->invite && $this->invite->address_type == 'email') {
+                $this->input('email', _('Email'), $this->invite->address,
+                             _('Used only for updates, announcements, '.
+                               'and password recovery'));
+            } else {
+                $this->input('email', _('Email'), $this->trimmed('email'),
+                             _('Used only for updates, announcements, '.
+                               'and password recovery'));
+            }
+            $this->elementEnd('li');
+            $this->elementStart('li');
+            $this->input('fullname', _('Full name'),
+                         $this->trimmed('fullname'),
+                         _('Longer name, preferably your "real" name'));
+            $this->elementEnd('li');
+            $this->elementStart('li');
+            $this->input('homepage', _('Homepage'),
+                         $this->trimmed('homepage'),
+                         _('URL of your homepage, blog, '.
+                           'or profile on another site'));
+            $this->elementEnd('li');
+            $this->elementStart('li');
+            $this->textarea('bio', _('Bio'),
+                            $this->trimmed('bio'),
+                            _('Describe yourself and your '.
+                              'interests in 140 chars'));
+            $this->elementEnd('li');
+            $this->elementStart('li');
+            $this->input('location', _('Location'),
+                         $this->trimmed('location'),
+                         _('Where you are, like "City, '.
+                           'State (or Region), Country"'));
+            $this->elementEnd('li');
+            Event::handle('EndRegistrationFormData', array($this));
+            $this->elementStart('li', array('id' => 'settings_rememberme'));
+            $this->checkbox('rememberme', _('Remember me'),
+                            $this->boolean('rememberme'),
+                            _('Automatically login in the future; '.
+                              'not for shared computers!'));
+            $this->elementEnd('li');
+            $attrs = array('type' => 'checkbox',
+                           'id' => 'license',
+                           'class' => 'checkbox',
+                           'name' => 'license',
+                           'value' => 'true');
+            if ($this->boolean('license')) {
+                $attrs['checked'] = 'checked';
+            }
+            $this->elementStart('li');
+            $this->element('input', $attrs);
+            $this->elementStart('label', array('class' => 'checkbox', 'for' => 'license'));
+            $this->text(_('My text and files are available under '));
+            $this->element('a', array('href' => common_config('license', 'url')),
+                           common_config('license', 'title'), _("Creative Commons Attribution 3.0"));
+            $this->text(_(' except this private data: password, '.
+                          'email address, IM address, and phone number.'));
+            $this->elementEnd('label');
+            $this->elementEnd('li');
         }
-        $this->elementStart('li');
-        $this->element('input', $attrs);
-        $this->elementStart('label', array('class' => 'checkbox', 'for' => 'license'));
-        $this->text(_('My text and files are available under '));
-        $this->element('a', array('href' => common_config('license', 'url')),
-                       common_config('license', 'title'), _("Creative Commons Attribution 3.0"));
-        $this->text(_(' except this private data: password, '.
-                      'email address, IM address, and phone number.'));
-        $this->elementEnd('label');
-        $this->elementEnd('li');
         $this->elementEnd('ul');
         $this->submit('submit', _('Register'));
         $this->elementEnd('fieldset');
@@ -519,3 +563,4 @@ class RegisterAction extends Action
         $nav->show();
     }
 }
+
index 6e011d5ca528e22771825f01cee0c61e44c3eea7..eed62a2ab308f79dfdb318ee61cbebf338367d72 100644 (file)
@@ -74,9 +74,9 @@ class ShowfavoritesAction extends Action
     function title()
     {
         if ($this->page == 1) {
-            return sprintf(_("%s favorite notices"), $this->user->nickname);
+            return sprintf(_("%s's favorite notices"), $this->user->nickname);
         } else {
-            return sprintf(_("%s favorite notices, page %d"),
+            return sprintf(_("%s's favorite notices, page %d"),
                            $this->user->nickname,
                            $this->page);
         }
index 025f8383a276d56ab61dfa51ac830fac177ed06e..a7df397273af91e940b8fa816745add5df24f728 100644 (file)
@@ -35,7 +35,7 @@ if (!defined('LACONICA')) {
 require_once INSTALLDIR.'/lib/noticelist.php';
 require_once INSTALLDIR.'/lib/feedlist.php';
 
-define('MEMBERS_PER_SECTION', 81);
+define('MEMBERS_PER_SECTION', 27);
 
 /**
  * Group main page
@@ -361,7 +361,7 @@ class ShowgroupAction extends Action
              $this->element('p', null, _('(None)'));
         }
 
-        if ($cnt == MEMBERS_PER_SECTION) {
+        if ($cnt > MEMBERS_PER_SECTION) {
             $this->element('a', array('href' => common_local_url('groupmembers',
                                                                  array('nickname' => $this->group->nickname))),
                            _('All members'));
index 82665e5b8b3841f1c68015ecb0be4359a617128b..678a3174c151800a482b35c41bfdb71f6245a691 100644 (file)
@@ -68,6 +68,9 @@ class ShowstreamAction extends ProfileAction
         } else {
             $base = $this->user->nickname;
         }
+        if (!empty($this->tag)) {
+            $base .= sprintf(_(' tagged %s'), $this->tag);
+        }
 
         if ($this->page == 1) {
             return $base;
@@ -110,6 +113,15 @@ class ShowstreamAction extends ProfileAction
 
     function getFeeds()
     {
+        if (!empty($this->tag)) {
+            return array(new Feed(Feed::RSS1,
+                common_local_url('userrss',
+                    array('nickname' => $this->user->nickname,
+                        'tag' => $this->tag)),
+                sprintf(_('Notice feed for %s tagged %s (RSS 1.0)'),
+                    $this->user->nickname, $this->tag)));
+        }
+
         return array(new Feed(Feed::RSS1,
                               common_local_url('userrss',
                                                array('nickname' => $this->user->nickname)),
@@ -363,7 +375,9 @@ class ShowstreamAction extends ProfileAction
 
     function showNotices()
     {
-        $notice = $this->user->getNotices(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1);
+        $notice = empty($this->tag)
+            ? $this->user->getNotices(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1)
+            : $this->user->getTaggedNotices(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1, 0, 0, null, $this->tag);
 
         $pnl = new ProfileNoticeList($notice, $this);
         $cnt = $pnl->show();
index d91a7d4fd35241c7c756a3b1b157aa763a43e4d5..4482de9a7c74279b000e042012fbb927a86b5dbd 100644 (file)
@@ -118,6 +118,16 @@ class SubscribersAction extends GalleryAction
         $this->raw(common_markup_to_html($message));
         $this->elementEnd('div');
     }
+
+    function showSections()
+    {
+        parent::showSections();
+        $cloud = new SubscribersPeopleTagCloudSection($this);
+        $cloud->show();
+
+        $cloud2 = new SubscribersPeopleSelfTagCloudSection($this);
+        $cloud2->show();
+    }
 }
 
 class SubscribersList extends ProfileList
index e6f3c54db8c83c67820e8898875c4f7cd18de71d..095b18ad87db49b97f1c16746d3a7366f6b0f7a4 100644 (file)
@@ -125,6 +125,16 @@ class SubscriptionsAction extends GalleryAction
         $this->raw(common_markup_to_html($message));
         $this->elementEnd('div');
     }
+
+    function showSections()
+    {
+        parent::showSections();
+        $cloud = new SubscriptionsPeopleTagCloudSection($this);
+        $cloud->show();
+
+        $cloud2 = new SubscriptionsPeopleSelfTagCloudSection($this);
+        $cloud2->show();
+    }
 }
 
 class SubscriptionsList extends ProfileList
index 02f3e35225cca6976fc7efe5a8b0930b346664eb..47420e4c33c799ee83136f54f003e8116bc0dfcc 100644 (file)
@@ -49,9 +49,10 @@ class TagAction extends Action
     {
         $pop = new PopularNoticeSection($this);
         $pop->show();
+        $freqatt = new FrequentAttachmentSection($this);
+        $freqatt->show();
     }
 
-
     function title()
     {
         if ($this->page == 1) {
index c50c5e84a9dcc948682e80d55c0a26641a726dd6..2f8250e0dc9dfa56b55893b436a15838c8a9b39a 100644 (file)
@@ -133,11 +133,7 @@ class TwitapifriendshipsAction extends TwitterapiAction
             return;
         }
 
-        if ($user_a->isSubscribed($user_b)) {
-            $result = 'true';
-        } else {
-            $result = 'false';
-        }
+        $result = $user_a->isSubscribed($user_b);
 
         switch ($apidata['content-type']) {
          case 'xml':
index 323c4f1f8824c0d197d5ec6b1b788f25efc35515..3abeba36727d2f8ae4ee6708f8d3aab95d00a64d 100644 (file)
@@ -144,10 +144,10 @@ class TwitapistatusesAction extends TwitterapiAction
             break;
          case 'atom':
             if (isset($apidata['api_arg'])) {
-                $selfuri = $selfuri = common_root_url() .
+                $selfuri = common_root_url() .
                     'api/statuses/friends_timeline/' . $apidata['api_arg'] . '.atom';
             } else {
-                $selfuri = $selfuri = common_root_url() .
+                $selfuri = common_root_url() .
                     'api/statuses/friends_timeline.atom';
             }
             $this->show_atom_timeline($notice, $title, $id, $link, $subtitle, null, $selfuri);
@@ -231,10 +231,10 @@ class TwitapistatusesAction extends TwitterapiAction
             break;
          case 'atom':
             if (isset($apidata['api_arg'])) {
-                $selfuri = $selfuri = common_root_url() .
+                $selfuri = common_root_url() .
                     'api/statuses/user_timeline/' . $apidata['api_arg'] . '.atom';
             } else {
-                $selfuri = $selfuri = common_root_url() .
+                $selfuri = common_root_url() .
                  'api/statuses/user_timeline.atom';
             }
             $this->show_atom_timeline($notice, $title, $id, $link, $subtitle, $suplink, $selfuri);
@@ -344,7 +344,7 @@ class TwitapistatusesAction extends TwitterapiAction
         $this->show($args, $apidata);
     }
 
-    function replies($args, $apidata)
+    function mentions($args, $apidata)
     {
 
         parent::handle($args);
@@ -360,11 +360,13 @@ class TwitapistatusesAction extends TwitterapiAction
         $profile = $user->getProfile();
 
         $sitename = common_config('site', 'name');
-        $title = sprintf(_('%1$s / Updates replying to %2$s'), $sitename, $user->nickname);
+        $title = sprintf(_('%1$s / Updates mentioning %2$s'),
+            $sitename, $user->nickname);
         $taguribase = common_config('integration', 'taguri');
-        $id = "tag:$taguribase:Replies:".$user->id;
+        $id = "tag:$taguribase:Mentions:".$user->id;
         $link = common_local_url('replies', array('nickname' => $user->nickname));
-        $subtitle = sprintf(_('%1$s updates that reply to updates from %2$s / %3$s.'), $sitename, $user->nickname, $profile->getBestName());
+        $subtitle = sprintf(_('%1$s updates that reply to updates from %2$s / %3$s.'),
+            $sitename, $user->nickname, $profile->getBestName());
 
         if (!$page) {
             $page = 1;
@@ -385,7 +387,8 @@ class TwitapistatusesAction extends TwitterapiAction
 
         $since = strtotime($this->arg('since'));
 
-        $notice = $user->getReplies((($page-1)*20), $count, $since_id, $before_id, $since);
+        $notice = $user->getReplies((($page-1)*20),
+            $count, $since_id, $before_id, $since);
         $notices = array();
 
         while ($notice->fetch()) {
@@ -400,14 +403,10 @@ class TwitapistatusesAction extends TwitterapiAction
             $this->show_rss_timeline($notices, $title, $link, $subtitle);
             break;
          case 'atom':
-             if (isset($apidata['api_arg'])) {
-                 $selfuri = $selfuri = common_root_url() .
-                     'api/statuses/replies/' . $apidata['api_arg'] . '.atom';
-             } else {
-                 $selfuri = $selfuri = common_root_url() .
-                  'api/statuses/replies.atom';
-             }
-            $this->show_atom_timeline($notices, $title, $id, $link, $subtitle, null, $selfuri);
+            $selfuri = common_root_url() .
+                ltrim($_SERVER['QUERY_STRING'], 'p=');
+            $this->show_atom_timeline($notices, $title, $id, $link, $subtitle,
+                null, $selfuri);
             break;
          case 'json':
             $this->show_json_timeline($notices);
@@ -418,6 +417,11 @@ class TwitapistatusesAction extends TwitterapiAction
 
     }
 
+    function replies($args, $apidata)
+    {
+        call_user_func(array($this, 'mentions'), $args, $apidata);
+    }
+
     function show($args, $apidata)
     {
         parent::handle($args);
index 2894b7486dd31372fe63ca76ca41ed99821922b4..1542cfb33e3d064130093272e014ac4fa6262d81 100644 (file)
@@ -82,8 +82,8 @@ class TwitapiusersAction extends TwitterapiAction
                $twitter_user['profile_text_color'] = '';
                $twitter_user['profile_link_color'] = '';
                $twitter_user['profile_sidebar_fill_color'] = '';
-        $twitter_user['profile_sidebar_border_color'] = '';
-        $twitter_user['profile_background_tile'] = 'false';
+               $twitter_user['profile_sidebar_border_color'] = '';
+               $twitter_user['profile_background_tile'] = false;
 
                $faves = DB_DataObject::factory('fave');
                $faves->user_id = $user->id;
@@ -103,24 +103,16 @@ class TwitapiusersAction extends TwitterapiAction
 
                if (isset($apidata['user'])) {
 
-                       if ($apidata['user']->isSubscribed($profile)) {
-                               $twitter_user['following'] = 'true';
-                       } else {
-                               $twitter_user['following'] = 'false';
-                       }
+                       $twitter_user['following'] = $apidata['user']->isSubscribed($profile);
             
-            // Notifications on?
-                   $sub = Subscription::pkeyGet(array('subscriber' =>
-                       $apidata['user']->id, 'subscribed' => $profile->id));
+                       // Notifications on?
+                       $sub = Subscription::pkeyGet(array('subscriber' =>
+                               $apidata['user']->id, 'subscribed' => $profile->id));
             
-            if ($sub) {
-                if ($sub->jabber || $sub->sms) {
-                    $twitter_user['notifications'] = 'true';
-                } else {
-                    $twitter_user['notifications'] = 'false';
-                }
-            }
-        }
+                       if ($sub) {
+                               $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
+                       }
+               }
         
                if ($apidata['content-type'] == 'xml') {
                        $this->init_document('xml');
index 45725d3ff44e289cdef8b7c1ed80399c30db5ede..2b742788eee55419112a718d271f6e4285960163 100644 (file)
@@ -138,7 +138,7 @@ class TwittersettingsAction extends ConnectSettingsAction
 
         $this->elementStart('ul', 'form_data');
         $this->elementStart('li');
-        $this->checkbox('noticesync',
+        $this->checkbox('noticesend',
                         _('Automatically send my notices to Twitter.'),
                         ($flink) ?
                         ($flink->noticesync & FOREIGN_NOTICE_SEND) :
@@ -158,6 +158,22 @@ class TwittersettingsAction extends ConnectSettingsAction
                         ($flink->friendsync & FOREIGN_FRIEND_RECV) :
                         false);
         $this->elementEnd('li');
+
+        if (common_config('twitterbridge','enabled')) {
+            $this->elementStart('li');
+            $this->checkbox('noticerecv',
+                            _('Import my Friends Timeline.'),
+                            ($flink) ?
+                            ($flink->noticesync & FOREIGN_NOTICE_RECV) :
+                            false);
+            $this->elementEnd('li');
+        } else {
+            // preserve setting even if bidrection bridge toggled off
+            if ($flink && ($flink->noticesync & FOREIGN_NOTICE_RECV)) {
+                $this->hidden('noticerecv', true, 'noticerecv');
+            }
+        }
+
         $this->elementEnd('ul');
 
         if ($flink) {
@@ -261,7 +277,7 @@ class TwittersettingsAction extends ConnectSettingsAction
                                             'alt' =>  ($other->fullname) ?
                                             $other->fullname :
                                             $other->nickname));
-                
+
                 $this->element('span', 'fn nickname', $other->nickname);
                 $this->elementEnd('a');
                 $this->elementEnd('li');
@@ -320,7 +336,8 @@ class TwittersettingsAction extends ConnectSettingsAction
     {
         $screen_name = $this->trimmed('twitter_username');
         $password    = $this->trimmed('twitter_password');
-        $noticesync  = $this->boolean('noticesync');
+        $noticesend  = $this->boolean('noticesend');
+        $noticerecv  = $this->boolean('noticerecv');
         $replysync   = $this->boolean('replysync');
         $friendsync  = $this->boolean('friendsync');
 
@@ -363,7 +380,7 @@ class TwittersettingsAction extends ConnectSettingsAction
         $flink->credentials = $password;
         $flink->created     = common_sql_now();
 
-        $flink->set_flags($noticesync, $replysync, $friendsync);
+        $flink->set_flags($noticesend, $noticerecv, $replysync, $friendsync);
 
         $flink_id = $flink->insert();
 
@@ -375,6 +392,8 @@ class TwittersettingsAction extends ConnectSettingsAction
 
         if ($friendsync) {
             save_twitter_friends($user, $twit_user->id, $screen_name, $password);
+            $flink->last_friendsync = common_sql_now();
+            $flink->update();
         }
 
         $this->showForm(_('Twitter settings saved.'), true);
@@ -419,7 +438,8 @@ class TwittersettingsAction extends ConnectSettingsAction
 
     function savePreferences()
     {
-        $noticesync = $this->boolean('noticesync');
+        $noticesend = $this->boolean('noticesend');
+        $noticerecv = $this->boolean('noticerecv');
         $friendsync = $this->boolean('friendsync');
         $replysync  = $this->boolean('replysync');
 
@@ -448,7 +468,7 @@ class TwittersettingsAction extends ConnectSettingsAction
 
         $original = clone($flink);
 
-        $flink->set_flags($noticesync, $replysync, $friendsync);
+        $flink->set_flags($noticesend, $noticerecv, $replysync, $friendsync);
 
         $result = $flink->update($original);
 
index 5861d9ee3687bd7a4e79e6088ea30a059c690a2b..2280509b22942c372be42745684cfbbe9b292c4b 100644 (file)
@@ -25,14 +25,15 @@ require_once(INSTALLDIR.'/lib/rssaction.php');
 
 class UserrssAction extends Rss10Action
 {
-
     var $user = null;
+    var $tag  = null;
 
     function prepare($args)
     {
         parent::prepare($args);
-        $nickname = $this->trimmed('nickname');
+        $nickname   = $this->trimmed('nickname');
         $this->user = User::staticGet('nickname', $nickname);
+        $this->tag  = $this->trimmed('tag');
 
         if (!$this->user) {
             $this->clientError(_('No such user.'));
@@ -42,6 +43,25 @@ class UserrssAction extends Rss10Action
         }
     }
 
+    function getTaggedNotices($tag = null, $limit=0)
+    {
+        $user = $this->user;
+
+        if (is_null($user)) {
+            return null;
+        }
+
+        $notice = $user->getTaggedNotices(0, ($limit == 0) ? NOTICES_PER_PAGE : $limit, 0, 0, null, $tag);
+
+        $notices = array();
+        while ($notice->fetch()) {
+            $notices[] = clone($notice);
+        }
+
+        return $notices;
+    }
+
+
     function getNotices($limit=0)
     {
 
index 24df5938c23ef1fb4b95f31a353012b43018763d..915b4572ffed5dd11e533a664c55e4d687d725b3 100644 (file)
@@ -4,7 +4,7 @@
  */
 require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
 
-class Fave extends Memcached_DataObject 
+class Fave extends Memcached_DataObject
 {
     ###START_AUTOCODE
     /* the code below is auto generated do not remove the above tag */
@@ -31,9 +31,58 @@ class Fave extends Memcached_DataObject
         }
         return $fave;
     }
-    
+
     function &pkeyGet($kv)
     {
         return Memcached_DataObject::pkeyGet('Fave', $kv);
     }
+
+    function stream($user_id, $offset=0, $limit=NOTICES_PER_PAGE)
+    {
+        $ids = Notice::stream(array('Fave', '_streamDirect'),
+                              array($user_id),
+                              'fave:ids_by_user:'.$user_id,
+                              $offset, $limit);
+        return $ids;
+    }
+
+    function _streamDirect($user_id, $offset, $limit, $since_id, $before_id, $since)
+    {
+        $fav = new Fave();
+
+        $fav->user_id = $user_id;
+
+        $fav->selectAdd();
+        $fav->selectAdd('notice_id');
+
+        if ($since_id != 0) {
+            $fav->whereAdd('notice_id > ' . $since_id);
+        }
+
+        if ($before_id != 0) {
+            $fav->whereAdd('notice_id < ' . $before_id);
+        }
+
+        if (!is_null($since)) {
+            $fav->whereAdd('modified > \'' . date('Y-m-d H:i:s', $since) . '\'');
+        }
+
+        // NOTE: we sort by fave time, not by notice time!
+
+        $fav->orderBy('modified DESC');
+
+        if (!is_null($offset)) {
+            $fav->limit($offset, $limit);
+        }
+
+        $ids = array();
+
+        if ($fav->find()) {
+            while ($fav->fetch()) {
+                $ids[] = $fav->notice_id;
+            }
+        }
+
+        return $ids;
+    }
 }
diff --git a/classes/File.php b/classes/File.php
new file mode 100644 (file)
index 0000000..e591311
--- /dev/null
@@ -0,0 +1,123 @@
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, 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/>.
+ */
+
+if (!defined('LACONICA')) { exit(1); }
+
+require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
+require_once INSTALLDIR.'/classes/File_redirection.php';
+require_once INSTALLDIR.'/classes/File_oembed.php';
+require_once INSTALLDIR.'/classes/File_thumbnail.php';
+require_once INSTALLDIR.'/classes/File_to_post.php';
+//require_once INSTALLDIR.'/classes/File_redirection.php';
+
+/**
+ * Table Definition for file
+ */
+
+class File extends Memcached_DataObject 
+{
+    ###START_AUTOCODE
+    /* the code below is auto generated do not remove the above tag */
+
+    public $__table = 'file';                            // table name
+    public $id;                              // int(11)  not_null primary_key group_by
+    public $url;                             // varchar(255)  unique_key
+    public $mimetype;                        // varchar(50)  
+    public $size;                            // int(11)  group_by
+    public $title;                           // varchar(255)  
+    public $date;                            // int(11)  group_by
+    public $protected;                       // int(1)  group_by
+
+    /* Static get */
+    function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('File',$k,$v); }
+
+    /* the code above is auto generated do not remove the tag below */
+    ###END_AUTOCODE
+
+    function isProtected($url) {
+        return 'http://www.facebook.com/login.php' === $url;
+    }
+
+    function getAttachments($post_id) {
+        $query = "select file.* from file join file_to_post on (file_id = file.id) join notice on (post_id = notice.id) where post_id = " . $this->escape($post_id);
+        $this->query($query);
+        $att = array();
+        while ($this->fetch()) {
+            $att[] = clone($this);
+        }
+        $this->free();
+        return $att;
+    }
+
+    function saveNew($redir_data, $given_url) {
+        $x = new File;
+        $x->url = $given_url;
+        if (!empty($redir_data['protected'])) $x->protected = $redir_data['protected'];
+        if (!empty($redir_data['title'])) $x->title = $redir_data['title'];
+        if (!empty($redir_data['type'])) $x->mimetype = $redir_data['type'];
+        if (!empty($redir_data['size'])) $x->size = intval($redir_data['size']);
+        if (isset($redir_data['time']) && $redir_data['time'] > 0) $x->date = intval($redir_data['time']);
+        $file_id = $x->insert();
+
+        if (isset($redir_data['type'])
+            && ('text/html' === substr($redir_data['type'], 0, 9))
+            && ($oembed_data = File_oembed::_getOembed($given_url))
+            && isset($oembed_data['json'])) {
+
+            File_oembed::saveNew($oembed_data['json'], $file_id);
+        }
+        return $x;
+    }
+
+    function processNew($given_url, $notice_id) {
+        if (empty($given_url)) return -1;   // error, no url to process
+        $given_url = File_redirection::_canonUrl($given_url);
+        if (empty($given_url)) return -1;   // error, no url to process
+        $file = File::staticGet('url', $given_url);
+        if (empty($file->id)) {
+            $file_redir = File_redirection::staticGet('url', $given_url);
+            if (empty($file_redir->id)) {
+                $redir_data = File_redirection::where($given_url);
+                $redir_url = $redir_data['url'];
+                if ($redir_url === $given_url) {
+                    $x = File::saveNew($redir_data, $given_url);
+                    $file_id = $x->id;
+
+                } else {
+                    $x = File::processNew($redir_url, $notice_id);
+                    $file_id = $x->id;
+                    File_redirection::saveNew($redir_data, $file_id, $given_url);
+                }
+            } else {
+                $file_id = $file_redir->file_id;
+            }
+        } else {
+            $file_id = $file->id;
+            $x = $file;
+        }
+
+        if (empty($x)) {
+            $x = File::staticGet($file_id);
+            if (empty($x)) die('Impossible!');
+        }
+       
+        File_to_post::processNew($file_id, $notice_id);
+        return $x;
+    }
+}
diff --git a/classes/File_oembed.php b/classes/File_oembed.php
new file mode 100644 (file)
index 0000000..f1b2cb1
--- /dev/null
@@ -0,0 +1,87 @@
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, 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/>.
+ */
+
+if (!defined('LACONICA')) { exit(1); }
+
+require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
+
+/**
+ * Table Definition for file_oembed
+ */
+
+class File_oembed extends Memcached_DataObject 
+{
+    ###START_AUTOCODE
+    /* the code below is auto generated do not remove the above tag */
+
+    public $__table = 'file_oembed';                     // table name
+    public $id;                              // int(11)  not_null primary_key group_by
+    public $file_id;                         // int(11)  unique_key group_by
+    public $version;                         // varchar(20)  
+    public $type;                            // varchar(20)  
+    public $provider;                        // varchar(50)  
+    public $provider_url;                    // varchar(255)  
+    public $width;                           // int(11)  group_by
+    public $height;                          // int(11)  group_by
+    public $html;                            // blob(65535)  blob
+    public $title;                           // varchar(255)  
+    public $author_name;                     // varchar(50)  
+    public $author_url;                      // varchar(255)  
+    public $url;                             // varchar(255)  
+
+    /* Static get */
+    function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('File_oembed',$k,$v); }
+
+    /* the code above is auto generated do not remove the tag below */
+    ###END_AUTOCODE
+
+
+    function _getOembed($url, $maxwidth = 500, $maxheight = 400, $format = 'json') {
+        $cmd = 'http://oohembed.com/oohembed/?url=' . urlencode($url);
+        if (is_int($maxwidth)) $cmd .= "&maxwidth=$maxwidth";
+        if (is_int($maxheight)) $cmd .= "&maxheight=$maxheight";
+        if (is_string($format)) $cmd .= "&format=$format";
+        $oe = @file_get_contents($cmd);
+        if (false === $oe) return false;
+        return array($format => (('json' === $format) ? json_decode($oe, true) : $oe));
+    }
+
+    function saveNew($data, $file_id) {
+        $file_oembed = new File_oembed;
+        $file_oembed->file_id = $file_id;
+        $file_oembed->version = $data['version'];
+        $file_oembed->type = $data['type'];
+        if (!empty($data['provider_name'])) $file_oembed->provider = $data['provider_name'];
+        if (!isset($file_oembed->provider) && !empty($data['provide'])) $file_oembed->provider = $data['provider'];
+        if (!empty($data['provide_url'])) $file_oembed->provider_url = $data['provider_url'];
+        if (!empty($data['width'])) $file_oembed->width = intval($data['width']);
+        if (!empty($data['height'])) $file_oembed->height = intval($data['height']);
+        if (!empty($data['html'])) $file_oembed->html = $data['html'];
+        if (!empty($data['title'])) $file_oembed->title = $data['title'];
+        if (!empty($data['author_name'])) $file_oembed->author_name = $data['author_name'];
+        if (!empty($data['author_url'])) $file_oembed->author_url = $data['author_url'];
+        if (!empty($data['url'])) $file_oembed->url = $data['url'];
+        $file_oembed->insert();
+        if (!empty($data['thumbnail_url'])) {
+            File_thumbnail::saveNew($data, $file_id);
+        }
+    }
+}
+
+
diff --git a/classes/File_redirection.php b/classes/File_redirection.php
new file mode 100644 (file)
index 0000000..0eae681
--- /dev/null
@@ -0,0 +1,274 @@
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, 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/>.
+ */
+
+if (!defined('LACONICA')) { exit(1); }
+
+require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
+require_once INSTALLDIR.'/classes/File.php';
+require_once INSTALLDIR.'/classes/File_oembed.php';
+
+define('USER_AGENT', 'Laconica user agent / file probe');
+
+
+/**
+ * Table Definition for file_redirection
+ */
+
+class File_redirection extends Memcached_DataObject 
+{
+    ###START_AUTOCODE
+    /* the code below is auto generated do not remove the above tag */
+
+    public $__table = 'file_redirection';                // table name
+    public $id;                              // int(11)  not_null primary_key group_by
+    public $url;                             // varchar(255)  unique_key
+    public $file_id;                         // int(11)  group_by
+    public $redirections;                    // int(11)  group_by
+    public $httpcode;                        // int(11)  group_by
+
+    /* Static get */
+    function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('File_redirection',$k,$v); }
+
+    /* the code above is auto generated do not remove the tag below */
+    ###END_AUTOCODE
+
+
+
+    function _commonCurl($url, $redirs) {
+        $curlh = curl_init();
+        curl_setopt($curlh, CURLOPT_URL, $url);
+        curl_setopt($curlh, CURLOPT_AUTOREFERER, true); // # setup referer header when folowing redirects
+        curl_setopt($curlh, CURLOPT_CONNECTTIMEOUT, 10); // # seconds to wait
+        curl_setopt($curlh, CURLOPT_MAXREDIRS, $redirs); // # max number of http redirections to follow
+        curl_setopt($curlh, CURLOPT_USERAGENT, USER_AGENT);
+        curl_setopt($curlh, CURLOPT_FOLLOWLOCATION, true); // Follow redirects
+        curl_setopt($curlh, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($curlh, CURLOPT_FILETIME, true);
+        curl_setopt($curlh, CURLOPT_HEADER, true); // Include header in output
+        return $curlh;
+    }
+
+    function _redirectWhere_imp($short_url, $redirs = 10, $protected = false) {
+        if ($redirs < 0) return false;
+
+        // let's see if we know this...
+        $a = File::staticGet('url', $short_url);
+        if (empty($a->id)) {
+            $b = File_redirection::staticGet('url', $short_url);
+            if (empty($b->id)) {
+                // we'll have to figure it out
+            } else {
+                // this is a redirect to $b->file_id
+                $a = File::staticGet($b->file_id);
+                $url = $a->url;
+            }
+        } else {
+            // this is a direct link to $a->url
+            $url = $a->url;
+        }
+        if (isset($url)) {
+            return $url;
+        }
+
+
+
+        $curlh = File_redirection::_commonCurl($short_url, $redirs);
+        // Don't include body in output
+        curl_setopt($curlh, CURLOPT_NOBODY, true);
+        curl_exec($curlh);
+        $info = curl_getinfo($curlh);
+        curl_close($curlh);
+
+        if (405 == $info['http_code']) {
+            $curlh = File_redirection::_commonCurl($short_url, $redirs);
+            curl_exec($curlh);
+            $info = curl_getinfo($curlh);
+            curl_close($curlh);
+        }
+
+        if (!empty($info['redirect_count']) && File::isProtected($info['url'])) {
+            return File_redirection::_redirectWhere_imp($short_url, $info['redirect_count'] - 1, true);
+        }
+
+        $ret = array('code' => $info['http_code']
+                , 'redirects' => $info['redirect_count']
+                , 'url' => $info['url']);
+
+        if (!empty($info['content_type'])) $ret['type'] = $info['content_type'];
+        if ($protected) $ret['protected'] = true;
+        if (!empty($info['download_content_length'])) $ret['size'] = $info['download_content_length'];
+        if (isset($info['filetime']) && ($info['filetime'] > 0)) $ret['time'] = $info['filetime'];
+        return $ret;
+    }
+
+    function where($in_url) {
+        $ret = File_redirection::_redirectWhere_imp($in_url);
+        return $ret;
+    }
+
+    function makeShort($long_url) {
+        $long_url = File_redirection::_canonUrl($long_url);
+        // do we already know this long_url and have a short redirection for it?
+        $file       = new File;
+        $file_redir = new File_redirection;
+        $file->url  = $long_url;
+        $file->joinAdd($file_redir);
+        $file->selectAdd('length(file_redirection.url) as len');
+        $file->limit(1);
+        $file->orderBy('len');
+        $file->find(true);
+        if (!empty($file->id)) {
+            return $file->url;
+        }
+
+        // if yet unknown, we must find a short url according to user settings
+        $short_url = File_redirection::_userMakeShort($long_url, common_current_user());
+        return $short_url;
+    }
+
+    function _userMakeShort($long_url, $user) {
+        if (empty($user)) {
+            // common current user does not find a user when called from the XMPP daemon
+            // therefore we'll set one here fix, so that XMPP given URLs may be shortened
+            $user->urlshorteningservice = 'ur1.ca';
+        }
+        $curlh = curl_init();
+        curl_setopt($curlh, CURLOPT_CONNECTTIMEOUT, 20); // # seconds to wait
+        curl_setopt($curlh, CURLOPT_USERAGENT, 'Laconica');
+        curl_setopt($curlh, CURLOPT_RETURNTRANSFER, true);
+
+        switch($user->urlshorteningservice) {
+            case 'ur1.ca':
+                require_once INSTALLDIR.'/lib/Shorturl_api.php';
+                $short_url_service = new LilUrl;
+                $short_url = $short_url_service->shorten($long_url);
+                break;
+
+            case '2tu.us':
+                $short_url_service = new TightUrl;
+                require_once INSTALLDIR.'/lib/Shorturl_api.php';
+                $short_url = $short_url_service->shorten($long_url);
+                break;
+
+            case 'ptiturl.com':
+                require_once INSTALLDIR.'/lib/Shorturl_api.php';
+                $short_url_service = new PtitUrl;
+                $short_url = $short_url_service->shorten($long_url);
+                break;
+
+            case 'bit.ly':
+                curl_setopt($curlh, CURLOPT_URL, 'http://bit.ly/api?method=shorten&long_url='.urlencode($long_url));
+                $short_url = current(json_decode(curl_exec($curlh))->results)->hashUrl;
+                break;
+
+            case 'is.gd':
+                curl_setopt($curlh, CURLOPT_URL, 'http://is.gd/api.php?longurl='.urlencode($long_url));
+                $short_url = curl_exec($curlh);
+                break;
+            case 'snipr.com':
+                curl_setopt($curlh, CURLOPT_URL, 'http://snipr.com/site/snip?r=simple&link='.urlencode($long_url));
+                $short_url = curl_exec($curlh);
+                break;
+            case 'metamark.net':
+                curl_setopt($curlh, CURLOPT_URL, 'http://metamark.net/api/rest/simple?long_url='.urlencode($long_url));
+                $short_url = curl_exec($curlh);
+                break;
+            case 'tinyurl.com':
+                curl_setopt($curlh, CURLOPT_URL, 'http://tinyurl.com/api-create.php?url='.urlencode($long_url));
+                $short_url = curl_exec($curlh);
+                break;
+            default:
+                $short_url = false;
+        }
+
+        curl_close($curlh);
+
+        if ($short_url) {
+            $short_url = (string)$short_url;
+            // store it
+            $file = File::staticGet('url', $long_url);
+            if (empty($file)) {
+                $redir_data = File_redirection::where($long_url);
+                $file = File::saveNew($redir_data, $long_url);
+                $file_id = $file->id;
+                if (!empty($redir_data['oembed']['json'])) {
+                    File_oembed::saveNew($redir_data['oembed']['json'], $file_id);
+                }
+            } else {
+                $file_id = $file->id;
+            }
+            $file_redir = File_redirection::staticGet('url', $short_url);
+            if (empty($file_redir)) {
+                $file_redir = new File_redirection;
+                $file_redir->url = $short_url;
+                $file_redir->file_id = $file_id;
+                $file_redir->insert();
+            }
+            return $short_url;
+        }
+        return $long_url;
+    }
+
+    function _canonUrl($in_url, $default_scheme = 'http://') {
+        if (empty($in_url)) return false;
+        $out_url = $in_url;
+        $p = parse_url($out_url);
+        if (empty($p['host']) || empty($p['scheme'])) {
+            list($scheme) = explode(':', $in_url, 2);
+            switch ($scheme) {
+            case 'fax':
+            case 'tel':
+                $out_url = str_replace('.-()', '', $out_url);
+                break;
+
+            case 'mailto':
+            case 'aim':
+            case 'jabber':
+            case 'xmpp':
+                // don't touch anything
+                break;
+
+            default:
+                $out_url = $default_scheme . ltrim($out_url, '/');
+                $p = parse_url($out_url);
+                if (empty($p['scheme'])) return false;
+                break;
+            }
+        }
+
+        if (('ftp' == $p['scheme']) || ('http' == $p['scheme']) || ('https' == $p['scheme'])) {
+            if (empty($p['host'])) return false;
+            if (empty($p['path'])) {
+                $out_url .= '/';
+            }
+        }
+
+        return $out_url;
+    }
+
+    function saveNew($data, $file_id, $url) {
+        $file_redir = new File_redirection;
+        $file_redir->url = $url;
+        $file_redir->file_id = $file_id;
+        $file_redir->redirections = intval($data['redirects']);
+        $file_redir->httpcode = intval($data['code']);
+        $file_redir->insert();
+    }
+}
+
diff --git a/classes/File_thumbnail.php b/classes/File_thumbnail.php
new file mode 100644 (file)
index 0000000..1a65b92
--- /dev/null
@@ -0,0 +1,55 @@
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, 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/>.
+ */
+
+if (!defined('LACONICA')) { exit(1); }
+
+require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
+
+/**
+ * Table Definition for file_thumbnail
+ */
+
+class File_thumbnail extends Memcached_DataObject 
+{
+    ###START_AUTOCODE
+    /* the code below is auto generated do not remove the above tag */
+
+    public $__table = 'file_thumbnail';                  // table name
+    public $id;                              // int(11)  not_null primary_key group_by
+    public $file_id;                         // int(11)  unique_key group_by
+    public $url;                             // varchar(255)  unique_key
+    public $width;                           // int(11)  group_by
+    public $height;                          // int(11)  group_by
+
+    /* Static get */
+    function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('File_thumbnail',$k,$v); }
+
+    /* the code above is auto generated do not remove the tag below */
+    ###END_AUTOCODE
+
+    function saveNew($data, $file_id) {
+        $tn = new File_thumbnail;
+        $tn->file_id = $file_id;
+        $tn->url = $data['thumbnail_url'];
+        $tn->width = intval($data['thumbnail_width']);
+        $tn->height = intval($data['thumbnail_height']);
+        $tn->insert();
+    }
+}
+
diff --git a/classes/File_to_post.php b/classes/File_to_post.php
new file mode 100644 (file)
index 0000000..00ddebe
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, 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/>.
+ */
+
+if (!defined('LACONICA')) { exit(1); }
+
+require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
+
+/**
+ * Table Definition for file_to_post
+ */
+
+class File_to_post extends Memcached_DataObject 
+{
+    ###START_AUTOCODE
+    /* the code below is auto generated do not remove the above tag */
+
+    public $__table = 'file_to_post';                    // table name
+    public $id;                              // int(11)  not_null primary_key group_by
+    public $file_id;                         // int(11)  multiple_key group_by
+    public $post_id;                         // int(11)  group_by
+
+    /* Static get */
+    function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('File_to_post',$k,$v); }
+
+    /* the code above is auto generated do not remove the tag below */
+    ###END_AUTOCODE
+
+    function processNew($file_id, $notice_id) {
+        static $seen = array();
+        if (empty($seen[$notice_id]) || !in_array($file_id, $seen[$notice_id])) {
+            $f2p = new File_to_post;
+            $f2p->file_id = $file_id;
+            $f2p->post_id = $notice_id;
+            $f2p->insert();
+            if (empty($seen[$notice_id])) {
+                $seen[$notice_id] = array($file_id);
+            } else {
+                $seen[$notice_id][] = $file_id;
+            }
+        }
+
+    }
+}
+
index afc0e2180455644130c757e351303f23c6538e5e..6065609512ce76e8461a3952b910e18cb94c50e1 100644 (file)
@@ -17,6 +17,8 @@ class Foreign_link extends Memcached_DataObject
     public $noticesync;                      // tinyint(1)   not_null default_1
     public $friendsync;                      // tinyint(1)   not_null default_2
     public $profilesync;                     // tinyint(1)   not_null default_1
+    public $last_noticesync;                 // datetime()
+    public $last_friendsync;                 // datetime()
     public $created;                         // datetime()   not_null
     public $modified;                        // timestamp()   not_null default_CURRENT_TIMESTAMP
 
@@ -57,13 +59,19 @@ class Foreign_link extends Memcached_DataObject
         return null;
     }
 
-    function set_flags($noticesync, $replysync, $friendsync)
+    function set_flags($noticesend, $noticerecv, $replysync, $friendsync)
     {
-        if ($noticesync) {
+        if ($noticesend) {
             $this->noticesync |= FOREIGN_NOTICE_SEND;
         } else {
             $this->noticesync &= ~FOREIGN_NOTICE_SEND;
         }
+        
+        if ($noticerecv) {
+            $this->noticesync |= FOREIGN_NOTICE_RECV;
+        } else {
+            $this->noticesync &= ~FOREIGN_NOTICE_RECV;
+        }
 
         if ($replysync) {
             $this->noticesync |= FOREIGN_NOTICE_SEND_REPLY;
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
index a399d97e0cd0f47783c65d0363bfb7d51ca84bc1..1b5c0ab0a55afdc53592b860ec71ca414fa615a5 100644 (file)
@@ -124,8 +124,6 @@ class Notice extends Memcached_DataObject
 
         $profile = Profile::staticGet($profile_id);
 
-        $final =  common_shorten_links($content);
-
         if (!$profile) {
             common_log(LOG_ERR, 'Problem saving notice. Unknown user.');
             return _('Problem saving notice. Unknown user.');
@@ -136,7 +134,7 @@ class Notice extends Memcached_DataObject
             return _('Too many notices too fast; take a breather and post again in a few minutes.');
         }
 
-        if (common_config('site', 'dupelimit') > 0 && !Notice::checkDupes($profile_id, $final)) {
+        if (common_config('site', 'dupelimit') > 0 && !Notice::checkDupes($profile_id, $content)) {
             common_log(LOG_WARNING, 'Dupe posting by profile #' . $profile_id . '; throttled.');
                        return _('Too many duplicate messages too quickly; take a breather and post again in a few minutes.');
         }
@@ -167,8 +165,8 @@ class Notice extends Memcached_DataObject
 
                $notice->reply_to = $reply_to;
                $notice->created = common_sql_now();
-               $notice->content = $final;
-               $notice->rendered = common_render_content($final, $notice);
+               $notice->content = $content;
+               $notice->rendered = common_render_content($content, $notice);
                $notice->source = $source;
                $notice->uri = $uri;
 
@@ -206,7 +204,12 @@ class Notice extends Memcached_DataObject
             $notice->saveTags();
             $notice->saveGroups();
 
-            $notice->addToInboxes();
+            if (common_config('queue', 'enabled')) {
+                $notice->addToAuthorInbox();
+            } else {
+                $notice->addToInboxes();
+            }
+
             $notice->query('COMMIT');
 
             Event::handle('EndNoticeSave', array($notice));
@@ -216,7 +219,11 @@ class Notice extends Memcached_DataObject
         # XXX: someone clever could prepend instead of clearing the cache
 
         if (common_config('memcached', 'enabled')) {
-            $notice->blowCaches();
+            if (common_config('queue', 'enabled')) {
+                $notice->blowAuthorCaches();
+            } else {
+                $notice->blowCaches();
+            }
         }
 
         return $notice;
@@ -270,6 +277,16 @@ class Notice extends Memcached_DataObject
         return true;
     }
 
+    function hasAttachments() {
+        $post = clone $this;
+        $query = "select count(file_id) as n_attachments from file join file_to_post on (file_id = file.id) join notice on (post_id = notice.id) where post_id = " . $post->escape($post->id);
+        $post->query($query);
+        $post->fetch();
+        $n_attachments = intval($post->n_attachments);
+        $post->free();
+        return $n_attachments;
+    }
+
     function blowCaches($blowLast=false)
     {
         $this->blowSubsCache($blowLast);
@@ -280,6 +297,17 @@ class Notice extends Memcached_DataObject
         $this->blowGroupCache($blowLast);
     }
 
+    function blowAuthorCaches($blowLast=false)
+    {
+        // Clear the user's cache
+        $cache = common_memcache();
+        if (!empty($cache)) {
+            $cache->delete(common_cache_key('notice_inbox:by_user:'.$this->profile_id));
+        }
+        $this->blowNoticeCache($blowLast);
+        $this->blowPublicCache($blowLast);
+    }
+
     function blowGroupCache($blowLast=false)
     {
         $cache = common_memcache();
@@ -288,17 +316,17 @@ class Notice extends Memcached_DataObject
             $group_inbox->notice_id = $this->id;
             if ($group_inbox->find()) {
                 while ($group_inbox->fetch()) {
-                    $cache->delete(common_cache_key('group:notices:'.$group_inbox->group_id));
+                    $cache->delete(common_cache_key('user_group:notice_ids:' . $group_inbox->group_id));
                     if ($blowLast) {
-                        $cache->delete(common_cache_key('group:notices:'.$group_inbox->group_id.';last'));
+                        $cache->delete(common_cache_key('user_group:notice_ids:' . $group_inbox->group_id.';last'));
                     }
                     $member = new Group_member();
                     $member->group_id = $group_inbox->group_id;
                     if ($member->find()) {
                         while ($member->fetch()) {
-                            $cache->delete(common_cache_key('user:notices_with_friends:' . $member->profile_id));
+                            $cache->delete(common_cache_key('notice_inbox:by_user:' . $member->profile_id));
                             if ($blowLast) {
-                                $cache->delete(common_cache_key('user:notices_with_friends:' . $member->profile_id . ';last'));
+                                $cache->delete(common_cache_key('notice_inbox:by_user:' . $member->profile_id . ';last'));
                             }
                         }
                     }
@@ -317,10 +345,7 @@ class Notice extends Memcached_DataObject
             $tag->notice_id = $this->id;
             if ($tag->find()) {
                 while ($tag->fetch()) {
-                    $cache->delete(common_cache_key('notice_tag:notice_stream:' . $tag->tag));
-                    if ($blowLast) {
-                        $cache->delete(common_cache_key('notice_tag:notice_stream:' . $tag->tag . ';last'));
-                    }
+                    $tag->blowCache($blowLast);
                 }
             }
             $tag->free();
@@ -341,9 +366,9 @@ class Notice extends Memcached_DataObject
                          'WHERE subscription.subscribed = ' . $this->profile_id);
 
             while ($user->fetch()) {
-                $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id));
+                $cache->delete(common_cache_key('notice_inbox:by_user:'.$user->id));
                 if ($blowLast) {
-                    $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id . ';last'));
+                    $cache->delete(common_cache_key('notice_inbox:by_user:'.$user->id.';last'));
                 }
             }
             $user->free();
@@ -355,10 +380,10 @@ class Notice extends Memcached_DataObject
     {
         if ($this->is_local) {
             $cache = common_memcache();
-            if ($cache) {
-                $cache->delete(common_cache_key('profile:notices:'.$this->profile_id));
+            if (!empty($cache)) {
+                $cache->delete(common_cache_key('profile:notice_ids:'.$this->profile_id));
                 if ($blowLast) {
-                    $cache->delete(common_cache_key('profile:notices:'.$this->profile_id.';last'));
+                    $cache->delete(common_cache_key('profile:notice_ids:'.$this->profile_id.';last'));
                 }
             }
         }
@@ -372,9 +397,9 @@ class Notice extends Memcached_DataObject
             $reply->notice_id = $this->id;
             if ($reply->find()) {
                 while ($reply->fetch()) {
-                    $cache->delete(common_cache_key('user:replies:'.$reply->profile_id));
+                    $cache->delete(common_cache_key('reply:stream:'.$reply->profile_id));
                     if ($blowLast) {
-                        $cache->delete(common_cache_key('user:replies:'.$reply->profile_id.';last'));
+                        $cache->delete(common_cache_key('reply:stream:'.$reply->profile_id.';last'));
                     }
                 }
             }
@@ -404,9 +429,9 @@ class Notice extends Memcached_DataObject
             $fave->notice_id = $this->id;
             if ($fave->find()) {
                 while ($fave->fetch()) {
-                    $cache->delete(common_cache_key('user:faves:'.$fave->user_id));
+                    $cache->delete(common_cache_key('fave:ids_by_user:'.$fave->user_id));
                     if ($blowLast) {
-                        $cache->delete(common_cache_key('user:faves:'.$fave->user_id.';last'));
+                        $cache->delete(common_cache_key('fave:ids_by_user:'.$fave->user_id.';last'));
                     }
                 }
             }
@@ -602,27 +627,80 @@ class Notice extends Memcached_DataObject
         return $wrapper;
     }
 
+    function getStreamByIds($ids)
+    {
+        $cache = common_memcache();
+
+        if (!empty($cache)) {
+            $notices = array();
+            foreach ($ids as $id) {
+                $notices[] = Notice::staticGet('id', $id);
+            }
+            return new ArrayWrapper($notices);
+        } else {
+            $notice = new Notice();
+            $notice->whereAdd('id in (' . implode(', ', $ids) . ')');
+            $notice->orderBy('id DESC');
+
+            $notice->find();
+            return $notice;
+        }
+    }
+
     function publicStream($offset=0, $limit=20, $since_id=0, $before_id=0, $since=null)
     {
+        $ids = Notice::stream(array('Notice', '_publicStreamDirect'),
+                              array(),
+                              'public',
+                              $offset, $limit, $since_id, $before_id, $since);
+
+        return Notice::getStreamByIds($ids);
+    }
+
+    function _publicStreamDirect($offset=0, $limit=20, $since_id=0, $before_id=0, $since=null)
+    {
+        $notice = new Notice();
 
-        $parts = array();
+        $notice->selectAdd(); // clears it
+        $notice->selectAdd('id');
 
-        $qry = 'SELECT * FROM notice ';
+        $notice->orderBy('id DESC');
+
+        if (!is_null($offset)) {
+            $notice->limit($offset, $limit);
+        }
 
         if (common_config('public', 'localonly')) {
-            $parts[] = 'is_local = 1';
+            $notice->whereAdd('is_local = 1');
         } else {
             # -1 == blacklisted
-            $parts[] = 'is_local != -1';
+            $notice->whereAdd('is_local != -1');
+        }
+
+        if ($since_id != 0) {
+            $notice->whereAdd('id > ' . $since_id);
+        }
+
+        if ($before_id != 0) {
+            $notice->whereAdd('id < ' . $before_id);
         }
 
-        if ($parts) {
-            $qry .= ' WHERE ' . implode(' AND ', $parts);
+        if (!is_null($since)) {
+            $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\'');
+        }
+
+        $ids = array();
+
+        if ($notice->find()) {
+            while ($notice->fetch()) {
+                $ids[] = $notice->id;
+            }
         }
 
-        return Notice::getStream($qry,
-                                 'public',
-                                 $offset, $limit, $since_id, $before_id, null, $since);
+        $notice->free();
+        $notice = NULL;
+
+        return $ids;
     }
 
     function addToInboxes()
@@ -648,6 +726,33 @@ class Notice extends Memcached_DataObject
         return;
     }
 
+    function addToAuthorInbox()
+    {
+        $enabled = common_config('inboxes', 'enabled');
+
+        if ($enabled === true || $enabled === 'transitional') {
+            $user = User::staticGet('id', $this->profile_id);
+            if (empty($user)) {
+                return;
+            }
+            $inbox = new Notice_inbox();
+            $UT = common_config('db','type')=='pgsql'?'"user"':'user';
+            $qry = 'INSERT INTO notice_inbox (user_id, notice_id, created) ' .
+              "SELECT $UT.id, " . $this->id . ", '" . $this->created . "' " .
+              "FROM $UT " .
+              "WHERE $UT.id = " . $this->profile_id . ' ' .
+              'AND NOT EXISTS (SELECT user_id, notice_id ' .
+              'FROM notice_inbox ' .
+              "WHERE user_id = " . $this->profile_id . ' '.
+              'AND notice_id = ' . $this->id . ' )';
+            if ($enabled === 'transitional') {
+                $qry .= " AND $UT.inboxed = 1";
+            }
+            $inbox->query($qry);
+        }
+        return;
+    }
+
     function saveGroups()
     {
         $enabled = common_config('inboxes', 'enabled');
@@ -700,24 +805,29 @@ class Notice extends Memcached_DataObject
 
                 // FIXME: do this in an offline daemon
 
-                $inbox = new Notice_inbox();
-                $UT = common_config('db','type')=='pgsql'?'"user"':'user';
-                $qry = 'INSERT INTO notice_inbox (user_id, notice_id, created, source) ' .
-                  "SELECT $UT.id, " . $this->id . ", '" . $this->created . "', 2 " .
-                  "FROM $UT JOIN group_member ON $UT.id = group_member.profile_id " .
-                  'WHERE group_member.group_id = ' . $group->id . ' ' .
-                  'AND NOT EXISTS (SELECT user_id, notice_id ' .
-                  'FROM notice_inbox ' .
-                  "WHERE user_id = $UT.id " .
-                  'AND notice_id = ' . $this->id . ' )';
-                if ($enabled === 'transitional') {
-                    $qry .= " AND $UT.inboxed = 1";
-                }
-                $result = $inbox->query($qry);
+                $this->addToGroupInboxes($group);
             }
         }
     }
 
+    function addToGroupInboxes($group)
+    {
+        $inbox = new Notice_inbox();
+        $UT = common_config('db','type')=='pgsql'?'"user"':'user';
+        $qry = 'INSERT INTO notice_inbox (user_id, notice_id, created, source) ' .
+          "SELECT $UT.id, " . $this->id . ", '" . $this->created . "', 2 " .
+          "FROM $UT JOIN group_member ON $UT.id = group_member.profile_id " .
+          'WHERE group_member.group_id = ' . $group->id . ' ' .
+          'AND NOT EXISTS (SELECT user_id, notice_id ' .
+          'FROM notice_inbox ' .
+          "WHERE user_id = $UT.id " .
+          'AND notice_id = ' . $this->id . ' )';
+        if ($enabled === 'transitional') {
+            $qry .= " AND $UT.inboxed = 1";
+        }
+        $result = $inbox->query($qry);
+    }
+
     function saveReplies()
     {
         // Alternative reply format
@@ -913,4 +1023,59 @@ class Notice extends Memcached_DataObject
                                     array('notice' => $this->id));
         }
     }
+
+    function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $before_id=0, $since=null, $tag=null)
+    {
+        $cache = common_memcache();
+
+        if (empty($cache) ||
+            $since_id != 0 || $before_id != 0 || !is_null($since) ||
+            ($offset + $limit) > NOTICE_CACHE_WINDOW) {
+            return call_user_func_array($fn, array_merge($args, array($offset, $limit, $since_id,
+                                                                      $before_id, $since, $tag)));
+        }
+
+        $idkey = common_cache_key($cachekey);
+
+        $idstr = $cache->get($idkey);
+
+        if (!empty($idstr)) {
+            // Cache hit! Woohoo!
+            $window = explode(',', $idstr);
+            $ids = array_slice($window, $offset, $limit);
+            return $ids;
+        }
+
+        $laststr = $cache->get($idkey.';last');
+
+        if (!empty($laststr)) {
+            $window = explode(',', $laststr);
+            $last_id = $window[0];
+            $new_ids = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW,
+                                                                          $last_id, 0, null, $tag)));
+
+            $new_window = array_merge($new_ids, $window);
+
+            $new_windowstr = implode(',', $new_window);
+
+            $result = $cache->set($idkey, $new_windowstr);
+            $result = $cache->set($idkey . ';last', $new_windowstr);
+
+            $ids = array_slice($new_window, $offset, $limit);
+
+            return $ids;
+        }
+
+        $window = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW,
+                                                                     0, 0, null, $tag)));
+
+        $windowstr = implode(',', $window);
+
+        $result = $cache->set($idkey, $windowstr);
+        $result = $cache->set($idkey . ';last', $windowstr);
+
+        $ids = array_slice($window, $offset, $limit);
+
+        return $ids;
+    }
 }
index 81ddb453859a4721b1c1b7531d618fdc84de263a..dec14b0d18897cf0fcb40ddf8e44305929ca5103 100644 (file)
@@ -1,7 +1,7 @@
 <?php
 /*
  * Laconica - a distributed open-source microblogging tool
- * Copyright (C) 2008, Controlez-Vous, Inc.
+ * Copyright (C) 2008, 2009, Control Yourself, 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
@@ -21,7 +21,11 @@ if (!defined('LACONICA')) { exit(1); }
 
 require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
 
-class Notice_inbox extends Memcached_DataObject 
+// We keep 5 pages of inbox notices in memcache, +1 for pagination check
+
+define('INBOX_CACHE_WINDOW', 101);
+
+class Notice_inbox extends Memcached_DataObject
 {
     ###START_AUTOCODE
     /* the code below is auto generated do not remove the above tag */
@@ -38,4 +42,47 @@ class Notice_inbox extends Memcached_DataObject
 
     /* the code above is auto generated do not remove the tag below */
     ###END_AUTOCODE
+
+    function stream($user_id, $offset, $limit, $since_id, $before_id, $since)
+    {
+        return Notice::stream(array('Notice_inbox', '_streamDirect'),
+                              array($user_id),
+                              'notice_inbox:by_user:'.$user_id,
+                              $offset, $limit, $since_id, $before_id, $since);
+    }
+
+    function _streamDirect($user_id, $offset, $limit, $since_id, $before_id, $since)
+    {
+        $inbox = new Notice_inbox();
+
+        $inbox->user_id = $user_id;
+
+        if ($since_id != 0) {
+            $inbox->whereAdd('notice_id > ' . $since_id);
+        }
+
+        if ($before_id != 0) {
+            $inbox->whereAdd('notice_id < ' . $before_id);
+        }
+
+        if (!is_null($since)) {
+            $inbox->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\'');
+        }
+
+        $inbox->orderBy('notice_id DESC');
+
+        if (!is_null($offset)) {
+            $inbox->limit($offset, $limit);
+        }
+
+        $ids = array();
+
+        if ($inbox->find()) {
+            while ($inbox->fetch()) {
+                $ids[] = $inbox->notice_id;
+            }
+        }
+
+        return $ids;
+    }
 }
index f2247299a4d137fba865e32cc350a97de0590fbb..e5b7722430b99be54a6fe2927b13275f86f44df0 100644 (file)
@@ -37,21 +37,62 @@ class Notice_tag extends Memcached_DataObject
     ###END_AUTOCODE
 
     static function getStream($tag, $offset=0, $limit=20) {
-        $qry =
-          'SELECT notice.* ' .
-          'FROM notice JOIN notice_tag ON notice.id = notice_tag.notice_id ' .
-          "WHERE notice_tag.tag = '%s' ";
-
-        return Notice::getStream(sprintf($qry, $tag),
-                                 'notice_tag:notice_stream:' . common_keyize($tag),
-                                 $offset, $limit);
+
+        $ids = Notice::stream(array('Notice_tag', '_streamDirect'),
+                              array($tag),
+                              'notice_tag:notice_ids:' . common_keyize($tag),
+                              $offset, $limit);
+
+        return Notice::getStreamByIds($ids);
+    }
+
+    function _streamDirect($tag, $offset, $limit, $since_id, $before_id, $since)
+    {
+        $nt = new Notice_tag();
+
+        $nt->tag = $tag;
+
+        $nt->selectAdd();
+        $nt->selectAdd('notice_id');
+
+        if ($since_id != 0) {
+            $nt->whereAdd('notice_id > ' . $since_id);
+        }
+
+        if ($before_id != 0) {
+            $nt->whereAdd('notice_id < ' . $before_id);
+        }
+
+        if (!is_null($since)) {
+            $nt->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\'');
+        }
+
+        $nt->orderBy('notice_id DESC');
+
+        if (!is_null($offset)) {
+            $nt->limit($offset, $limit);
+        }
+
+        $ids = array();
+
+        if ($nt->find()) {
+            while ($nt->fetch()) {
+                $ids[] = $nt->notice_id;
+            }
+        }
+
+        return $ids;
     }
 
-    function blowCache()
+    function blowCache($blowLast=false)
     {
         $cache = common_memcache();
         if ($cache) {
-            $cache->delete(common_cache_key('notice_tag:notice_stream:' . $this->tag));
+            $idkey = common_cache_key('notice_tag:notice_ids:' . common_keyize($this->tag));
+            $cache->delete($idkey);
+            if ($blowLast) {
+                $cache->delete($idkey.';last');
+            }
         }
     }
 
index f3bfe299cfb8d474dfae3ad57e29f2b8517f29cd..afc0ea4f74557b29824ef266a54be6fd59d81202 100644 (file)
@@ -153,16 +153,101 @@ class Profile extends Memcached_DataObject
         return null;
     }
 
-    function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0)
+    function getTaggedNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null, $tag=null)
     {
-        $qry =
-          'SELECT * ' .
-          'FROM notice ' .
-          'WHERE profile_id = %d ';
-
-        return Notice::getStream(sprintf($qry, $this->id),
-                                 'profile:notices:'.$this->id,
-                                 $offset, $limit, $since_id, $before_id);
+        // XXX: I'm not sure this is going to be any faster. It probably isn't.
+        $ids = Notice::stream(array($this, '_streamTaggedDirect'),
+                              array(),
+                              'profile:notice_ids:' . $this->id,
+                              $offset, $limit, $since_id, $before_id, $since, $tag);
+        common_debug(print_r($ids, true));
+        return Notice::getStreamByIds($ids);
+    }
+
+    function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null)
+    {
+        // XXX: I'm not sure this is going to be any faster. It probably isn't.
+        $ids = Notice::stream(array($this, '_streamDirect'),
+                              array(),
+                              'profile:notice_ids:' . $this->id,
+                              $offset, $limit, $since_id, $before_id, $since);
+
+        return Notice::getStreamByIds($ids);
+    }
+
+    function _streamTaggedDirect($offset, $limit, $since_id, $before_id, $since=null, $tag=null)
+    {
+        common_debug('_streamTaggedDirect()');
+        $notice = new Notice();
+        $notice->profile_id = $this->id;
+        $query = "select id from notice join notice_tag on id=notice_id where tag='" . $notice->escape($tag) . "' and profile_id=" . $notice->escape($notice->profile_id);
+        if ($since_id != 0) {
+            $query .= " and id > $since_id";
+        }
+
+        if ($before_id != 0) {
+            $query .= " and id < $before_id";
+        }
+
+        if (!is_null($since)) {
+            $query .= " and created > '" . date('Y-m-d H:i:s', $since) . "'";
+        }
+
+        $query .= ' order by id DESC';
+
+        if (!is_null($offset)) {
+            $query .= " limit $offset, $limit";
+        }
+        $notice->query($query);
+        $ids = array();
+
+        while ($notice->fetch()) {
+            common_debug(print_r($notice, true));
+            $ids[] = $notice->id;
+        }
+
+        return $ids;
+    }
+
+
+
+
+    function _streamDirect($offset, $limit, $since_id, $before_id, $since = null)
+    {
+        $notice = new Notice();
+
+        $notice->profile_id = $this->id;
+
+        $notice->selectAdd();
+        $notice->selectAdd('id');
+
+        if ($since_id != 0) {
+            $notice->whereAdd('id > ' . $since_id);
+        }
+
+        if ($before_id != 0) {
+            $notice->whereAdd('id < ' . $before_id);
+        }
+
+        if (!is_null($since)) {
+            $notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\'');
+        }
+
+        $notice->orderBy('id DESC');
+
+        if (!is_null($offset)) {
+            $notice->limit($offset, $limit);
+        }
+
+        $ids = array();
+
+        if ($notice->find()) {
+            while ($notice->fetch()) {
+                $ids[] = $notice->id;
+            }
+        }
+
+        return $ids;
     }
 
     function isMember($group)
old mode 100755 (executable)
new mode 100644 (file)
index af86aaf878c0ea5392a9579c601c46565a4b884a..4439053b444934f05fb69fa9b4d5b396700f532e 100644 (file)
@@ -4,7 +4,7 @@
  */
 require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
 
-class Reply extends Memcached_DataObject 
+class Reply extends Memcached_DataObject
 {
     ###START_AUTOCODE
     /* the code below is auto generated do not remove the above tag */
@@ -13,7 +13,7 @@ class Reply extends Memcached_DataObject
     public $notice_id;                       // int(4)  primary_key not_null
     public $profile_id;                      // int(4)  primary_key not_null
     public $modified;                        // timestamp()   not_null default_CURRENT_TIMESTAMP
-    public $replied_id;                      // int(4)  
+    public $replied_id;                      // int(4)
 
     /* Static get */
     function staticGet($k,$v=null)
@@ -21,4 +21,47 @@ class Reply extends Memcached_DataObject
 
     /* the code above is auto generated do not remove the tag below */
     ###END_AUTOCODE
+
+    function stream($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null)
+    {
+        $ids = Notice::stream(array('Reply', '_streamDirect'),
+                              array($user_id),
+                              'reply:stream:' . $user_id,
+                              $offset, $limit, $since_id, $before_id, $since);
+        return $ids;
+    }
+
+    function _streamDirect($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null)
+    {
+        $reply = new Reply();
+        $reply->profile_id = $user_id;
+
+        if ($since_id != 0) {
+            $reply->whereAdd('notice_id > ' . $since_id);
+        }
+
+        if ($before_id != 0) {
+            $reply->whereAdd('notice_id < ' . $before_id);
+        }
+
+        if (!is_null($since)) {
+            $reply->whereAdd('modified > \'' . date('Y-m-d H:i:s', $since) . '\'');
+        }
+
+        $reply->orderBy('notice_id DESC');
+
+        if (!is_null($offset)) {
+            $reply->limit($offset, $limit);
+        }
+
+        $ids = array();
+
+        if ($reply->find()) {
+            while ($reply->fetch()) {
+                $ids[] = $reply->notice_id;
+            }
+        }
+
+        return $ids;
+    }
 }
old mode 100755 (executable)
new mode 100644 (file)
index 3b9b5cd839e4b605f985d8cbb280dd50082bcc60..ea8ba40817ac74c3901c9f8f519deb677f616339 100644 (file)
@@ -349,30 +349,31 @@ class User extends Memcached_DataObject
         $cache = common_memcache();
 
         // XXX: Kind of a hack.
+
         if ($cache) {
             // This is the stream of favorite notices, in rev chron
             // order. This forces it into cache.
-            $faves = $this->favoriteNotices(0, NOTICE_CACHE_WINDOW);
-            $cnt = 0;
-            while ($faves->fetch()) {
-                if ($faves->id < $notice->id) {
-                    // If we passed it, it's not a fave
-                    return false;
-                } else if ($faves->id == $notice->id) {
-                    // If it matches a cached notice, then it's a fave
-                    return true;
-                }
-                $cnt++;
+
+            $ids = Fave::stream($this->id, 0, NOTICE_CACHE_WINDOW);
+
+            // If it's in the list, then it's a fave
+
+            if (in_array($notice->id, $ids)) {
+                return true;
             }
+
             // If we're not past the end of the cache window,
             // then the cache has all available faves, so this one
             // is not a fave.
-            if ($cnt < NOTICE_CACHE_WINDOW) {
+
+            if (count($ids) < NOTICE_CACHE_WINDOW) {
                 return false;
             }
+
             // Otherwise, cache doesn't have all faves;
             // fall through to the default
         }
+
         $fave = Fave::pkeyGet(array('user_id' => $this->id,
                                     'notice_id' => $notice->id));
         return ((is_null($fave)) ? false : true);
@@ -401,13 +402,18 @@ class User extends Memcached_DataObject
 
     function getReplies($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null)
     {
-        $qry =
-          'SELECT notice.* ' .
-          'FROM notice JOIN reply ON notice.id = reply.notice_id ' .
-          'WHERE reply.profile_id = %d ';
-        return Notice::getStream(sprintf($qry, $this->id),
-                                 'user:replies:'.$this->id,
-                                 $offset, $limit, $since_id, $before_id, null, $since);
+        $ids = Reply::stream($this->id, $offset, $limit, $since_id, $before_id, $since);
+        common_debug("Ids = " . implode(',', $ids));
+        return Notice::getStreamByIds($ids);
+    }
+
+    function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) {
+        $profile = $this->getProfile();
+        if (!$profile) {
+            return null;
+        } else {
+            return $profile->getTaggedNotices($tag, $offset, $limit, $since_id, $before_id, $since);
+        }
     }
 
     function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null)
@@ -416,19 +422,14 @@ class User extends Memcached_DataObject
         if (!$profile) {
             return null;
         } else {
-            return $profile->getNotices($offset, $limit, $since_id, $before_id);
+            return $profile->getNotices($offset, $limit, $since_id, $before_id, $since);
         }
     }
 
     function favoriteNotices($offset=0, $limit=NOTICES_PER_PAGE)
     {
-        $qry =
-          'SELECT notice.* ' .
-          'FROM notice JOIN fave ON notice.id = fave.notice_id ' .
-          'WHERE fave.user_id = %d ';
-        return Notice::getStream(sprintf($qry, $this->id),
-                                 'user:faves:'.$this->id,
-                                 $offset, $limit);
+        $ids = Fave::stream($this->id, $offset, $limit);
+        return Notice::getStreamByIds($ids);
     }
 
     function noticesWithFriends($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null)
@@ -444,21 +445,17 @@ class User extends Memcached_DataObject
               'SELECT notice.* ' .
               'FROM notice JOIN subscription ON notice.profile_id = subscription.subscribed ' .
               'WHERE subscription.subscriber = %d ';
-            $order = null;
+            return Notice::getStream(sprintf($qry, $this->id),
+                                     'user:notices_with_friends:' . $this->id,
+                                     $offset, $limit, $since_id, $before_id,
+                                     $order, $since);
         } else if ($enabled === true ||
                    ($enabled == 'transitional' && $this->inboxed == 1)) {
 
-            $qry =
-              'SELECT notice.* ' .
-              'FROM notice JOIN notice_inbox ON notice.id = notice_inbox.notice_id ' .
-              'WHERE notice_inbox.user_id = %d ';
-            // NOTE: we override ORDER
-            $order = null;
+            $ids = Notice_inbox::stream($this->id, $offset, $limit, $since_id, $before_id, $since);
+
+            return Notice::getStreamByIds($ids);
         }
-        return Notice::getStream(sprintf($qry, $this->id),
-                                 'user:notices_with_friends:' . $this->id,
-                                 $offset, $limit, $since_id, $before_id,
-                                 $order, $since);
     }
 
     function blowFavesCache()
@@ -467,8 +464,8 @@ class User extends Memcached_DataObject
         if ($cache) {
             // Faves don't happen chronologically, so we need to blow
             // ;last cache, too
-            $cache->delete(common_cache_key('user:faves:'.$this->id));
-            $cache->delete(common_cache_key('user:faves:'.$this->id).';last');
+            $cache->delete(common_cache_key('fave:ids_by_user:'.$this->id));
+            $cache->delete(common_cache_key('fave:ids_by_user:'.$this->id.';last'));
         }
     }
 
old mode 100755 (executable)
new mode 100644 (file)
index d152f9d..7cc31e7
@@ -50,13 +50,50 @@ class User_group extends Memcached_DataObject
 
     function getNotices($offset, $limit)
     {
-        $qry =
-          'SELECT notice.* ' .
-          'FROM notice JOIN group_inbox ON notice.id = group_inbox.notice_id ' .
-          'WHERE group_inbox.group_id = %d ';
-        return Notice::getStream(sprintf($qry, $this->id),
-                                 'group:notices:'.$this->id,
-                                 $offset, $limit);
+        $ids = Notice::stream(array($this, '_streamDirect'),
+                              array(),
+                              'user_group:notice_ids:' . $this->id,
+                              $offset, $limit);
+
+        return Notice::getStreamByIds($ids);
+    }
+
+    function _streamDirect($offset, $limit, $since_id, $before_id, $since)
+    {
+        $inbox = new Group_inbox();
+
+        $inbox->group_id = $this->id;
+
+        $inbox->selectAdd();
+        $inbox->selectAdd('notice_id');
+
+        if ($since_id != 0) {
+            $inbox->whereAdd('notice_id > ' . $since_id);
+        }
+
+        if ($before_id != 0) {
+            $inbox->whereAdd('notice_id < ' . $before_id);
+        }
+
+        if (!is_null($since)) {
+            $inbox->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\'');
+        }
+
+        $inbox->orderBy('notice_id DESC');
+
+        if (!is_null($offset)) {
+            $inbox->limit($offset, $limit);
+        }
+
+        $ids = array();
+
+        if ($inbox->find()) {
+            while ($inbox->fetch()) {
+                $ids[] = $inbox->notice_id;
+            }
+        }
+
+        return $ids;
     }
 
     function allowedNickname($nickname)
@@ -91,7 +128,7 @@ class User_group extends Memcached_DataObject
     function setOriginal($filename)
     {
         $imagefile = new ImageFile($this->id, Avatar::path($filename));
-        
+
         $orig = clone($this);
         $this->original_logo = Avatar::url($filename);
         $this->homepage_logo = Avatar::url($imagefile->resize(AVATAR_PROFILE_SIZE));
old mode 100755 (executable)
new mode 100644 (file)
index dd424bb..92bbb35
@@ -1,4 +1,3 @@
-
 [avatar]
 profile_id = 129
 original = 17
@@ -47,6 +46,64 @@ modified = 384
 notice_id = K
 user_id = K
 
+[file]
+id = 129
+url = 2
+mimetype = 2
+size = 1
+title = 2
+date = 1
+protected = 1
+
+[file__keys]
+id = N
+
+[file_oembed]
+id = 129
+file_id = 1
+version = 2
+type = 2
+provider = 2
+provider_url = 2
+width = 1
+height = 1
+html = 34
+title = 2
+author_name = 2
+author_url = 2
+url = 2
+
+[file_oembed__keys]
+id = N
+
+[file_redirection]
+id = 129
+url = 2
+file_id = 1
+redirections = 1
+httpcode = 1
+
+[file_redirection__keys]
+id = N
+
+[file_thumbnail]
+id = 129
+file_id = 1
+url = 2
+width = 1
+height = 1
+
+[file_thumbnail__keys]
+id = N
+
+[file_to_post]
+id = 129
+file_id = 1
+post_id = 1
+
+[file_to_post__keys]
+id = N
+
 [foreign_link]
 user_id = 129
 foreign_id = 129
@@ -55,6 +112,8 @@ credentials = 2
 noticesync = 145
 friendsync = 145
 profilesync = 145
+last_noticesync = 14
+last_friendsync = 14
 created = 142
 modified = 384
 
index 173b187267d433c832eb542f931564bdf95276e5..95c63f3c09cc1f1f3b565dc47f730ea9427d1ecf 100644 (file)
@@ -41,3 +41,17 @@ subscribed = profile:id
 [fave]
 notice_id = notice:id
 user_id = user:id
+
+[file_oembed]
+file_id = file:id
+
+[file_redirection]
+file_id = file:id
+
+[file_thumbnail]
+file_id = file:id
+
+[file_to_post]
+file_id = file:id
+post_id = notice:id
+
index 557ede0246781024e509146d68353c55f5f02796..79c04cee56edc7e5d8b54ac823bb4d28b9ad9f33 100644 (file)
@@ -2,4 +2,5 @@ insert into foreign_service
     (id, name, description, created)
 values
     ('1','Twitter', 'Twitter Micro-blogging service', now()),
-    ('2','Facebook', 'Facebook', now());
+    ('2','Facebook', 'Facebook', now()),
+    ('3','FacebookConnect', 'Facebook Connect', now());
index 20f0cabd17c070e2ad2ae5b4cdd96b06a3abcb87..0b20bc172c2bdda55940fcdc82e97abad1f47064 100644 (file)
@@ -119,6 +119,7 @@ create table notice (
     index notice_profile_id_idx (profile_id),
     index notice_conversation_idx (conversation),
     index notice_created_idx (created),
+    index notice_replyto_idx (reply_to),
     FULLTEXT(content)
 ) ENGINE=MyISAM CHARACTER SET utf8 COLLATE utf8_general_ci;
 
@@ -290,6 +291,8 @@ create table foreign_link (
      noticesync tinyint not null default 1 comment 'notice synchronization, bit 1 = sync outgoing, bit 2 = sync incoming, bit 3 = filter local replies',
      friendsync tinyint not null default 2 comment 'friend synchronization, bit 1 = sync outgoing, bit 2 = sync incoming',
      profilesync tinyint not null default 1 comment 'profile synchronization, bit 1 = sync outgoing, bit 2 = sync incoming',
+     last_noticesync datetime default null comment 'last time notices were imported',
+     last_friendsync datetime default null comment 'last time friends were imported',
      created datetime not null comment 'date this record was created',
      modified timestamp comment 'date this record was modified',
 
@@ -422,3 +425,60 @@ create table group_inbox (
 
 ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
 
+create table file (
+    id integer primary key auto_increment,
+    url varchar(255), mimetype varchar(50),
+    size integer,
+    title varchar(255),
+    date integer(11),
+    protected integer(1),
+
+    unique(url)
+) ENGINE=MyISAM CHARACTER SET utf8 COLLATE utf8_general_ci;
+
+create table file_oembed (
+    id integer primary key auto_increment,
+    file_id integer,
+    version varchar(20),
+    type varchar(20),
+    provider varchar(50),
+    provider_url varchar(255),
+    width integer,
+    height integer,
+    html text,
+    title varchar(255),
+    author_name varchar(50),
+    author_url varchar(255),
+    url varchar(255),
+
+    unique(file_id)
+) ENGINE=MyISAM CHARACTER SET utf8 COLLATE utf8_general_ci;
+
+create table file_redirection (
+    id integer primary key auto_increment,
+    url varchar(255),
+    file_id integer,
+    redirections integer,
+    httpcode integer,
+
+    unique(url)
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+
+create table file_thumbnail (
+    id integer primary key auto_increment,
+    file_id integer,
+    url varchar(255),
+    width integer,
+    height integer,
+
+    unique(file_id),
+    unique(url)
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
+
+create table file_to_post (
+    id integer primary key auto_increment,
+    file_id integer,
+    post_id integer,
+
+    unique(file_id, post_id)
+) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
index f879d7936ff1c83cf62957c9d9155de84a0abf7c..b213bbd5021f6383822f369586857164a8731a73 100644 (file)
@@ -291,6 +291,8 @@ create table foreign_link (
      noticesync int not null default 1 /* comment 'notice synchronisation, bit 1 = sync outgoing, bit 2 = sync incoming, bit 3 = filter local replies' */,\r
      friendsync int not null default 2 /* comment 'friend synchronisation, bit 1 = sync outgoing, bit 2 = sync incoming */, \r
      profilesync int not null default 1 /* comment 'profile synchronization, bit 1 = sync outgoing, bit 2 = sync incoming' */,\r
+     last_noticesync timestamp default null /* comment 'last time notices were imported' */,\r
+     last_friendsync timestamp default null /* comment 'last time friends were imported' */,\r
      created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,\r
      modified timestamp /* comment 'date this record was modified' */,\r
 \r
@@ -425,6 +427,64 @@ create table group_inbox (
 );\r
 create index group_inbox_created_idx on group_inbox using btree(created);\r
 \r
+\r
+/*attachments and URLs stuff */\r
+create sequence file_seq;\r
+create table file (\r
+    id bigint default nextval('file_seq') primary key /* comment 'unique identifier' */,\r
+    url varchar(255) unique, \r
+    mimetype varchar(50), \r
+    size integer, \r
+    title varchar(255), \r
+    date integer(11), \r
+    protected integer(1)\r
+);\r
+\r
+create sequence file_oembed_seq;\r
+create table file_oembed (\r
+    id bigint default nextval('file_oembed_seq') primary key /* comment 'unique identifier' */,\r
+    file_id bigint unique,\r
+    version varchar(20),\r
+    type varchar(20),\r
+    provider varchar(50),\r
+    provider_url varchar(255),\r
+    width integer,\r
+    height integer,\r
+    html text,\r
+    title varchar(255),\r
+    author_name varchar(50), \r
+    author_url varchar(255), \r
+    url varchar(255), \r
+);\r
+\r
+create sequence file_redirection_seq;\r
+create table file_redirection (\r
+    id bigint default nextval('file_redirection_seq') primary key /* comment 'unique identifier' */,\r
+    url varchar(255) unique, \r
+    file_id bigint, \r
+    redirections integer, \r
+    httpcode integer\r
+);\r
+\r
+create sequence file_thumbnail_seq;\r
+create table file_thumbnail (\r
+    id bigint default nextval('file_thumbnail_seq') primary key /* comment 'unique identifier' */,\r
+    file_id bigint unique, \r
+    url varchar(255) unique, \r
+    width integer, \r
+    height integer \r
+);\r
+\r
+create sequence file_to_post_seq;\r
+create table file_to_post (\r
+    id bigint default nextval('file_to_post_seq') primary key /* comment 'unique identifier' */,\r
+    file_id bigint, \r
+    post_id bigint, \r
+\r
+    unique(file_id, post_id)\r
+);\r
+\r
+\r
 /* Textsearch stuff */\r
 \r
 create index textsearch_idx on profile using gist(textsearch);\r
index d5124e223a8d5b0e695e35d4063ff16b8771e840..f026679d50621d40318d68a3660356b1c3455bd4 100644 (file)
@@ -8,6 +8,7 @@ VALUES
     ('deskbar','Deskbar-Applet','http://www.gnome.org/projects/deskbar-applet/', now()),
     ('Do','Gnome Do','http://do.davebsd.com/wiki/index.php?title=Microblog_Plugin', now()),
     ('Facebook','Facebook','http://apps.facebook.com/identica/', now()),
+    ('feed2omb','feed2omb','http://projects.ciarang.com/p/feed2omb/', now()),
     ('Gwibber','Gwibber','http://launchpad.net/gwibber', now()),
     ('HelloTxt','HelloTxt','http://hellotxt.com/', now()),
     ('identicatools','Laconica Tools','http://bitbucketlabs.net/laconica-tools/', now()),
@@ -23,6 +24,7 @@ VALUES
     ('peoplebrowsr', 'PeopleBrowsr', 'http://www.peoplebrowsr.com/', now()),
     ('Pikchur','Pikchur','http://www.pikchur.com/', now()),
     ('Ping.fm','Ping.fm','http://ping.fm/', now()),
+    ('pingvine','PingVine','http://pingvine.com/', now()),
     ('pocketwit','PockeTwit','http://code.google.com/p/pocketwit/', now()),
     ('posty','Posty','http://spreadingfunkyness.com/posty/', now()),
     ('royalewithcheese','Royale With Cheese','http://p.hellyeah.org/', now()),
@@ -42,6 +44,7 @@ VALUES
     ('twidge','Twidge','http://software.complete.org/twidge', now()),
     ('twidroid','twidroid','http://www.twidroid.com/', now()),
     ('twittelator','Twittelator','http://www.stone.com/iPhone/Twittelator/', now()),
+    ('twitter','Twitter','http://twitter.com/', now()),
     ('twitterfeed','twitterfeed','http://twitterfeed.com/', now()),
     ('twitterphoto','TwitterPhoto','http://richfish.org/twitterphoto/', now()),
     ('twitterpm','Net::Twitter','http://search.cpan.org/dist/Net-Twitter/', now()),
index b1a1a4e2184f77f3bb4d1c0bf22aa45e1b6b9074..0c6a13dc28f7cec5c702d2ad09fb41704bc6b018 100644 (file)
@@ -2357,6 +2357,8 @@ class DB_DataObject extends DB_DataObject_Overload
         $t= explode(' ',microtime());
         $_DB_DATAOBJECT['QUERYENDTIME'] = $time = $t[0]+$t[1];
          
+
+        do {
         
         if ($_DB_driver == 'DB') {
             $result = $DB->query($string);
@@ -2374,8 +2376,19 @@ class DB_DataObject extends DB_DataObject_Overload
                     break;
             }
         }
-        
-       
+
+          // try to reconnect, at most 3 times
+          $again = false;
+          if (is_a($result, 'PEAR_Error')
+          AND $result->getCode() == DB_ERROR_NODBSELECTED
+          AND $cpt++<3) {
+              $DB->disconnect();
+              sleep(1);
+              $DB->connect($DB->dsn);
+              $again = true;
+          }
+          
+        } while ($again);
 
         if (is_a($result,'PEAR_Error')) {
             if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) { 
index 35de6be5090bba96a4038d0fbd01273cab77a1d6..fee1dd086a9600875d0b8d9e9d8cffea46c579cc 100644 (file)
@@ -1,5 +1,5 @@
 <?php
-// Copyright 2004-2008 Facebook. All Rights Reserved.
+// Copyright 2004-2009 Facebook. All Rights Reserved.
 //
 // +---------------------------------------------------------------------------+
 // | Facebook Platform PHP5 client                                             |
 // +---------------------------------------------------------------------------+
 // | For help with this library, contact developers-help@facebook.com          |
 // +---------------------------------------------------------------------------+
-//
+
 include_once 'facebookapi_php5_restlib.php';
 
 define('FACEBOOK_API_VALIDATION_ERROR', 1);
 class Facebook {
   public $api_client;
-
   public $api_key;
   public $secret;
   public $generate_session_secret;
@@ -213,28 +212,55 @@ class Facebook {
     }
   }
 
-  // Invalidate the session currently being used, and clear any state associated with it
+  // Invalidate the session currently being used, and clear any state associated
+  // with it. Note that the user will still remain logged into Facebook.
   public function expire_session() {
     if ($this->api_client->auth_expireSession()) {
-      if (!$this->in_fb_canvas() && isset($_COOKIE[$this->api_key . '_user'])) {
-        $cookies = array('user', 'session_key', 'expires', 'ss');
-        foreach ($cookies as $name) {
-          setcookie($this->api_key . '_' . $name, false, time() - 3600);
-          unset($_COOKIE[$this->api_key . '_' . $name]);
-        }
-        setcookie($this->api_key, false, time() - 3600);
-        unset($_COOKIE[$this->api_key]);
-      }
-
-      // now, clear the rest of the stored state
-      $this->user = 0;
-      $this->api_client->session_key = 0;
+      $this->clear_cookie_state();
       return true;
     } else {
       return false;
     }
   }
 
+  /** Logs the user out of all temporary application sessions as well as their
+   * Facebook session.  Note this will only work if the user has a valid current
+   * session with the application.
+   *
+   * @param string  $next  URL to redirect to upon logging out
+   *
+   */
+   public function logout($next) {
+    $logout_url = $this->get_logout_url($next);
+
+    // Clear any stored state
+    $this->clear_cookie_state();
+
+    $this->redirect($logout_url);
+  }
+
+  /**
+   *  Clears any persistent state stored about the user, including
+   *  cookies and information related to the current session in the
+   *  client.
+   *
+   */
+  public function clear_cookie_state() {
+    if (!$this->in_fb_canvas() && isset($_COOKIE[$this->api_key . '_user'])) {
+       $cookies = array('user', 'session_key', 'expires', 'ss');
+       foreach ($cookies as $name) {
+         setcookie($this->api_key . '_' . $name, false, time() - 3600);
+         unset($_COOKIE[$this->api_key . '_' . $name]);
+       }
+       setcookie($this->api_key, false, time() - 3600);
+       unset($_COOKIE[$this->api_key]);
+     }
+
+     // now, clear the rest of the stored state
+     $this->user = 0;
+     $this->api_client->session_key = 0;
+  }
+
   public function redirect($url) {
     if ($this->in_fb_canvas()) {
       echo '<fb:redirect url="' . $url . '"/>';
@@ -249,7 +275,8 @@ class Facebook {
   }
 
   public function in_frame() {
-    return isset($this->fb_params['in_canvas']) || isset($this->fb_params['in_iframe']);
+    return isset($this->fb_params['in_canvas'])
+        || isset($this->fb_params['in_iframe']);
   }
   public function in_fb_canvas() {
     return isset($this->fb_params['in_canvas']);
@@ -296,14 +323,42 @@ class Facebook {
   }
 
   public function get_add_url($next=null) {
-    return self::get_facebook_url().'/add.php?api_key='.$this->api_key .
-      ($next ? '&next=' . urlencode($next) : '');
+    $page = self::get_facebook_url().'/add.php';
+    $params = array('api_key' => $this->api_key);
+
+    if ($next) {
+      $params['next'] = $next;
+    }
+
+    return $page . '?' . http_build_query($params);
   }
 
   public function get_login_url($next, $canvas) {
-    return self::get_facebook_url().'/login.php?v=1.0&api_key=' . $this->api_key .
-      ($next ? '&next=' . urlencode($next)  : '') .
-      ($canvas ? '&canvas' : '');
+    $page = self::get_facebook_url().'/login.php';
+    $params = array('api_key' => $this->api_key,
+                    'v'       => '1.0');
+
+    if ($next) {
+      $params['next'] = $next;
+    }
+    if ($canvas) {
+      $params['canvas'] = '1';
+    }
+
+    return $page . '?' . http_build_query($params);
+  }
+
+  public function get_logout_url($next) {
+    $page = self::get_facebook_url().'/logout.php';
+    $params = array('app_key'     => $this->api_key,
+                    'session_key' => $this->api_client->session_key);
+
+    if ($next) {
+      $params['connect_next'] = 1;
+      $params['next'] = $next;
+    }
+
+    return $page . '?' . http_build_query($params);
   }
 
   public function set_user($user, $session_key, $expires=null, $session_secret=null) {
@@ -410,7 +465,20 @@ class Facebook {
     return $fb_params;
   }
 
-  /*
+  /**
+   *  Validates the account that a user was trying to set up an
+   *  independent account through Facebook Connect.
+   *
+   *  @param  user The user attempting to set up an independent account.
+   *  @param  hash The hash passed to the reclamation URL used.
+   *  @return bool True if the user is the one that selected the
+   *               reclamation link.
+   */
+  public function verify_account_reclamation($user, $hash) {
+    return $hash == md5($user . $this->secret);
+  }
+
+  /**
    * Validates that a given set of parameters match their signature.
    * Parameters all match a given input prefix, such as "fb_sig".
    *
@@ -422,6 +490,37 @@ class Facebook {
     return self::generate_sig($fb_params, $this->secret) == $expected_sig;
   }
 
+  /**
+   * Validate the given signed public session data structure with
+   * public key of the app that
+   * the session proof belongs to.
+   *
+   * @param $signed_data the session info that is passed by another app
+   * @param string $public_key Optional public key of the app. If this
+   *               is not passed, function will make an API call to get it.
+   * return true if the session proof passed verification.
+   */
+  public function verify_signed_public_session_data($signed_data,
+                                                    $public_key = null) {
+
+    // If public key is not already provided, we need to get it through API
+    if (!$public_key) {
+      $public_key = $this->api_client->auth_getAppPublicKey(
+        $signed_data['api_key']);
+    }
+
+    // Create data to verify
+    $data_to_serialize = $signed_data;
+    unset($data_to_serialize['sig']);
+    $serialized_data = implode('_', $data_to_serialize);
+
+    // Decode signature
+    $signature = base64_decode($signed_data['sig']);
+    $result = openssl_verify($serialized_data, $signature, $public_key,
+                             OPENSSL_ALGO_SHA1);
+    return $result == 1;
+  }
+
   /*
    * Generate a signature using the application secret key.
    *
index 90cdf66bd0777822e2acce3b9315eada561a60a7..e79a2ca3433819126fef76d29ea6251dbdaceb76 100644 (file)
@@ -1,5 +1,5 @@
 <?php
-// Copyright 2004-2008 Facebook. All Rights Reserved.
+// Copyright 2004-2009 Facebook. All Rights Reserved.
 //
 // +---------------------------------------------------------------------------+
 // | Facebook Platform PHP5 client                                             |
old mode 100644 (file)
new mode 100755 (executable)
index 389f40a..3fec06e
@@ -1,9 +1,10 @@
 <?php
+// Copyright 2004-2009 Facebook. All Rights Reserved.
 //
 // +---------------------------------------------------------------------------+
 // | Facebook Platform PHP5 client                                             |
 // +---------------------------------------------------------------------------+
-// | Copyright (c) 2007-2008 Facebook, Inc.                                    |
+// | Copyright (c) 2007-2009 Facebook, Inc.                                    |
 // | All rights reserved.                                                      |
 // |                                                                           |
 // | Redistribution and use in source and binary forms, with or without        |
@@ -32,6 +33,7 @@
 //
 
 include_once 'jsonwrapper/jsonwrapper.php';
+
 class FacebookRestClient {
   public $secret;
   public $session_key;
@@ -50,7 +52,9 @@ class FacebookRestClient {
   public $canvas_user;
   public $batch_mode;
   private $batch_queue;
+  private $pending_batch;
   private $call_as_apikey;
+  private $use_curl_if_available;
 
   const BATCH_MODE_DEFAULT = 0;
   const BATCH_MODE_SERVER_PARALLEL = 0;
@@ -70,7 +74,8 @@ class FacebookRestClient {
     $this->batch_mode = FacebookRestClient::BATCH_MODE_DEFAULT;
     $this->last_call_id = 0;
     $this->call_as_apikey = '';
-      $this->server_addr  = Facebook::get_facebook_url('api') . '/restserver.php';
+    $this->use_curl_if_available = true;
+    $this->server_addr  = Facebook::get_facebook_url('api') . '/restserver.php';
 
     if (!empty($GLOBALS['facebook_config']['debug'])) {
       $this->cur_id = 0;
@@ -122,40 +127,62 @@ function toggleDisplay(id, type) {
     $this->user = $uid;
   }
 
+  /**
+   * Normally, if the cURL library/PHP extension is available, it is used for
+   * HTTP transactions.  This allows that behavior to be overridden, falling
+   * back to a vanilla-PHP implementation even if cURL is installed.
+   *
+   * @param $use_curl_if_available bool whether or not to use cURL if available
+   */
+  public function set_use_curl_if_available($use_curl_if_available) {
+    $this->use_curl_if_available = $use_curl_if_available;
+  }
+
   /**
    * Start a batch operation.
    */
   public function begin_batch() {
-    if($this->batch_queue !== null) {
+    if ($this->pending_batch()) {
       $code = FacebookAPIErrorCodes::API_EC_BATCH_ALREADY_STARTED;
-      throw new FacebookRestClientException($code,
-        FacebookAPIErrorCodes::$api_error_descriptions[$code]);
+      $description = FacebookAPIErrorCodes::$api_error_descriptions[$code];
+      throw new FacebookRestClientException($description, $code);
     }
 
     $this->batch_queue = array();
+    $this->pending_batch = true;
   }
 
   /*
    * End current batch operation
    */
   public function end_batch() {
-    if($this->batch_queue === null) {
+    if (!$this->pending_batch()) {
       $code = FacebookAPIErrorCodes::API_EC_BATCH_NOT_STARTED;
-      throw new FacebookRestClientException($code,
-      FacebookAPIErrorCodes::$api_error_descriptions[$code]);
+      $description = FacebookAPIErrorCodes::$api_error_descriptions[$code];
+      throw new FacebookRestClientException($description, $code);
     }
 
-    $this->execute_server_side_batch();
+    $this->pending_batch = false;
 
+    $this->execute_server_side_batch();
     $this->batch_queue = null;
   }
 
+  /**
+   * are we currently queueing up calls for a batch?
+   */
+  public function pending_batch() {
+    return $this->pending_batch;
+  }
+
   private function execute_server_side_batch() {
     $item_count = count($this->batch_queue);
     $method_feed = array();
     foreach($this->batch_queue as $batch_item) {
-      $method_feed[] = $this->create_post_string($batch_item['m'],
-                                                 $batch_item['p']);
+      $method = $batch_item['m'];
+      $params = $batch_item['p'];
+      $this->finalize_params($method, $params);
+      $method_feed[] = $this->create_post_string($method, $params);
     }
 
     $method_feed_json = json_encode($method_feed);
@@ -202,6 +229,18 @@ function toggleDisplay(id, type) {
     $this->call_as_apikey = '';
   }
 
+
+  /*
+   * If a page is loaded via HTTPS, then all images and static
+   * resources need to be printed with HTTPS urls to avoid
+   * mixed content warnings. If your page loads with an HTTPS
+   * url, then call set_use_ssl_resources to retrieve the correct
+   * urls.
+   */
+  public function set_use_ssl_resources($is_ssl = true) {
+    $this->use_ssl_resources = $is_ssl;
+  }
+
   /**
    * Returns public information for an application (as shown in the application
    * directory) by either application ID, API key, or canvas page name.
@@ -231,7 +270,7 @@ function toggleDisplay(id, type) {
    * @return string  An authentication token.
    */
   public function auth_createToken() {
-    return $this->call_method('facebook.auth.createToken', array());
+    return $this->call_method('facebook.auth.createToken');
   }
 
   /**
@@ -246,8 +285,7 @@ function toggleDisplay(id, type) {
    * @return array  An assoc array containing session_key, uid
    */
   public function auth_getSession($auth_token, $generate_session_secret=false) {
-    //Check if we are in batch mode
-    if($this->batch_queue === null) {
+    if (!$this->pending_batch()) {
       $result = $this->call_method('facebook.auth.getSession',
           array('auth_token' => $auth_token,
                 'generate_session_secret' => $generate_session_secret));
@@ -271,7 +309,7 @@ function toggleDisplay(id, type) {
    *        API_EC_PARAM_UNKNOWN
    */
   public function auth_promoteSession() {
-      return $this->call_method('facebook.auth.promoteSession', array());
+      return $this->call_method('facebook.auth.promoteSession');
   }
 
   /**
@@ -282,7 +320,20 @@ function toggleDisplay(id, type) {
    * @return bool  true if session expiration was successful, false otherwise
    */
   public function auth_expireSession() {
-      return $this->call_method('facebook.auth.expireSession', array());
+      return $this->call_method('facebook.auth.expireSession');
+  }
+
+  /**
+   *  Revokes the given extended permission that the user granted at some
+   *  prior time (for instance, offline_access or email).  If no user is
+   *  provided, it will be revoked for the user of the current session.
+   *
+   *  @param  string  $perm  The permission to revoke
+   *  @param  int     $uid   The user for whom to revoke the permission.
+   */
+  public function auth_revokeExtendedPermission($perm, $uid=null) {
+    return $this->call_method('facebook.auth.revokeExtendedPermission',
+        array('perm' => $perm, 'uid' => $uid));
   }
 
   /**
@@ -302,6 +353,30 @@ function toggleDisplay(id, type) {
           array('uid' => $uid));
   }
 
+  /**
+   * Get public key that is needed to verify digital signature
+   * an app may pass to other apps. The public key is only used by
+   * other apps for verification purposes.
+   * @param  string  API key of an app
+   * @return string  The public key for the app.
+   */
+  public function auth_getAppPublicKey($target_app_key) {
+    return $this->call_method('facebook.auth.getAppPublicKey',
+          array('target_app_key' => $target_app_key));
+  }
+
+  /**
+   * Get a structure that can be passed to another app
+   * as proof of session. The other app can verify it using public
+   * key of this app.
+   *
+   * @return signed public session data structure.
+   */
+  public function auth_getSignedPublicSessionData() {
+    return $this->call_method('facebook.auth.getSignedPublicSessionData',
+                              array());
+  }
+
   /**
    * Returns the number of unconnected friends that exist in this application.
    * This number is determined based on the accounts registered through
@@ -363,8 +438,9 @@ function toggleDisplay(id, type) {
    *
    * @param int $uid            (Optional) User associated with events. A null
    *                            parameter will default to the session user.
-   * @param array $eids         (Optional) Filter by these event ids. A null
-   *                            parameter will get all events for the user.
+   * @param array/string $eids  (Optional) Filter by these event
+   *                            ids. A null parameter will get all events for
+   *                            the user. (A csv list will work but is deprecated)
    * @param int $start_time     (Optional) Filter with this unix time as lower
    *                            bound.  A null or zero parameter indicates no
    *                            lower bound.
@@ -718,12 +794,15 @@ function toggleDisplay(id, type) {
    * @param string $body_general     (Optional) Additional markup that extends
    *                                 the body of a short story.
    * @param int $story_size          (Optional) A story size (see above)
+   * @param string $user_message     (Optional) A user message for a short
+   *                                 story.
    *
    * @return bool  true on success
    */
   public function &feed_publishUserAction(
       $template_bundle_id, $template_data, $target_ids='', $body_general='',
-      $story_size=FacebookRestClient::STORY_SIZE_ONE_LINE) {
+      $story_size=FacebookRestClient::STORY_SIZE_ONE_LINE,
+      $user_message='') {
 
     if (is_array($template_data)) {
       $template_data = json_encode($template_data);
@@ -739,7 +818,107 @@ function toggleDisplay(id, type) {
               'template_data' => $template_data,
               'target_ids' => $target_ids,
               'body_general' => $body_general,
-              'story_size' => $story_size));
+              'story_size' => $story_size,
+              'user_message' => $user_message));
+  }
+
+
+  /**
+   * Publish a post to the user's stream.
+   *
+   * @param $message        the user's message
+   * @param $attachment     the post's attachment (optional)
+   * @param $action links   the post's action links (optional)
+   * @param $target_id      the user on whose wall the post will be posted
+   *                        (optional)
+   * @param $uid            the actor (defaults to session user)
+   * @return string the post id
+   */
+  public function stream_publish(
+    $message, $attachment = null, $action_links = null, $target_id = null,
+    $uid = null) {
+
+    return $this->call_method(
+      'facebook.stream.publish',
+      array('message' => $message,
+            'attachment' => $attachment,
+            'action_links' => $action_links,
+            'target_id' => $target_id,
+            'uid' => $this->get_uid($uid)));
+  }
+
+  /**
+   * Remove a post from the user's stream.
+   * Currently, you may only remove stories you application created.
+   *
+   * @param $post_id  the post id
+   * @param $uid      the actor (defaults to session user)
+   * @return bool
+   */
+  public function stream_remove($post_id, $uid = null) {
+    return $this->call_method(
+      'facebook.stream.remove',
+      array('post_id' => $post_id,
+            'uid' => $this->get_uid($uid)));
+  }
+
+  /**
+   * Add a comment to a stream post
+   *
+   * @param $post_id  the post id
+   * @param $comment  the comment text
+   * @param $uid      the actor (defaults to session user)
+   * @return string the id of the created comment
+   */
+  public function stream_addComment($post_id, $comment, $uid = null) {
+    return $this->call_method(
+      'facebook.stream.addComment',
+      array('post_id' => $post_id,
+            'comment' => $comment,
+            'uid' => $this->get_uid($uid)));
+  }
+
+
+  /**
+   * Remove a comment from a stream post
+   *
+   * @param $comment_id  the comment id
+   * @param $uid      the actor (defaults to session user)
+   * @return bool
+   */
+  public function stream_removeComment($comment_id, $uid = null) {
+    return $this->call_method(
+      'facebook.stream.removeComment',
+      array('comment_id' => $comment_id,
+            'uid' => $this->get_uid($uid)));
+  }
+
+  /**
+   * Add a like to a stream post
+   *
+   * @param $post_id  the post id
+   * @param $uid      the actor (defaults to session user)
+   * @return bool
+   */
+  public function stream_addLike($post_id, $uid = null) {
+    return $this->call_method(
+      'facebook.stream.addLike',
+      array('post_id' => $post_id,
+            'uid' => $this->get_uid($uid)));
+  }
+
+  /**
+   * Remove a like from a stream post
+   *
+   * @param $post_id  the post id
+   * @param $uid      the actor (defaults to session user)
+   * @return bool
+   */
+  public function stream_removeLike($post_id, $uid = null) {
+    return $this->call_method(
+      'facebook.stream.removeLike',
+      array('post_id' => $post_id,
+            'uid' => $this->get_uid($uid)));
   }
 
   /**
@@ -750,7 +929,7 @@ function toggleDisplay(id, type) {
    * @return array  An array of feed story objects.
    */
   public function &feed_getAppFriendStories() {
-    return $this->call_method('facebook.feed.getAppFriendStories', array());
+    return $this->call_method('facebook.feed.getAppFriendStories');
   }
 
   /**
@@ -771,33 +950,42 @@ function toggleDisplay(id, type) {
    * Returns whether or not pairs of users are friends.
    * Note that the Facebook friend relationship is symmetric.
    *
-   * @param array $uids1  array of ids (id_1, id_2,...) of some length X
-   * @param array $uids2  array of ids (id_A, id_B,...) of SAME length X
+   * @param array/string $uids1  list of ids (id_1, id_2,...)
+   *                       of some length X (csv is deprecated)
+   * @param array/string $uids2  list of ids (id_A, id_B,...)
+   *                       of SAME length X (csv is deprecated)
    *
    * @return array  An array with uid1, uid2, and bool if friends, e.g.:
    *   array(0 => array('uid1' => id_1, 'uid2' => id_A, 'are_friends' => 1),
    *         1 => array('uid1' => id_2, 'uid2' => id_B, 'are_friends' => 0)
    *         ...)
+   * @error
+   *    API_EC_PARAM_USER_ID_LIST
    */
   public function &friends_areFriends($uids1, $uids2) {
     return $this->call_method('facebook.friends.areFriends',
-        array('uids1' => $uids1, 'uids2' => $uids2));
+                 array('uids1' => $uids1,
+                       'uids2' => $uids2));
   }
 
   /**
    * Returns the friends of the current session user.
    *
    * @param int $flid  (Optional) Only return friends on this friend list.
+   * @param int $uid   (Optional) Return friends for this user.
    *
    * @return array  An array of friends
    */
-  public function &friends_get($flid=null) {
+  public function &friends_get($flid=null, $uid = null) {
     if (isset($this->friends_list)) {
       return $this->friends_list;
     }
     $params = array();
-    if (isset($this->canvas_user)) {
-      $params['uid'] = $this->canvas_user;
+    if (!$uid && isset($this->canvas_user)) {
+      $uid = $this->canvas_user;
+    }
+    if ($uid) {
+      $params['uid'] = $uid;
     }
     if ($flid) {
       $params['flid'] = $flid;
@@ -812,7 +1000,7 @@ function toggleDisplay(id, type) {
    * @return array  An array of friend list objects
    */
   public function &friends_getLists() {
-    return $this->call_method('facebook.friends.getLists', array());
+    return $this->call_method('facebook.friends.getLists');
   }
 
   /**
@@ -822,7 +1010,7 @@ function toggleDisplay(id, type) {
    * @return array  An array of friends also using the app
    */
   public function &friends_getAppUsers() {
-    return $this->call_method('facebook.friends.getAppUsers', array());
+    return $this->call_method('facebook.friends.getAppUsers');
   }
 
   /**
@@ -830,8 +1018,9 @@ function toggleDisplay(id, type) {
    *
    * @param int $uid     (Optional) User associated with groups.  A null
    *                     parameter will default to the session user.
-   * @param array $gids  (Optional) Group ids to query. A null parameter will
-   *                     get all groups for the user.
+   * @param array/string $gids (Optional) Array of group ids to query. A null
+   *                     parameter will get all groups for the user.
+   *                     (csv is deprecated)
    *
    * @return array  An array of group objects
    */
@@ -889,6 +1078,40 @@ function toggleDisplay(id, type) {
               'path' => $path));
   }
 
+  /**
+   * Retrieves links posted by the given user.
+   *
+   * @param int    $uid      The user whose links you wish to retrieve
+   * @param int    $limit    The maximimum number of links to retrieve
+   * @param array $link_ids (Optional) Array of specific link
+   *                          IDs to retrieve by this user
+   *
+   * @return array  An array of links.
+   */
+  public function &links_get($uid, $limit, $link_ids = null) {
+    return $this->call_method('links.get',
+        array('uid' => $uid,
+              'limit' => $limit,
+              'link_ids' => $link_ids));
+  }
+
+  /**
+   * Posts a link on Facebook.
+   *
+   * @param string $url     URL/link you wish to post
+   * @param string $comment (Optional) A comment about this link
+   * @param int    $uid     (Optional) User ID that is posting this link;
+   *                        defaults to current session user
+   *
+   * @return bool
+   */
+  public function &links_post($url, $comment='', $uid = null) {
+    return $this->call_method('links.post',
+        array('uid' => $uid,
+              'url' => $url,
+              'comment' => $comment));
+  }
+
   /**
    * Permissions API
    */
@@ -945,6 +1168,78 @@ function toggleDisplay(id, type) {
         array('permissions_apikey' => $permissions_apikey));
   }
 
+  /**
+   * Creates a note with the specified title and content.
+   *
+   * @param string $title   Title of the note.
+   * @param string $content Content of the note.
+   * @param int    $uid     (Optional) The user for whom you are creating a
+   *                        note; defaults to current session user
+   *
+   * @return int   The ID of the note that was just created.
+   */
+  public function &notes_create($title, $content, $uid = null) {
+    return $this->call_method('notes.create',
+        array('uid' => $uid,
+              'title' => $title,
+              'content' => $content));
+  }
+
+  /**
+   * Deletes the specified note.
+   *
+   * @param int $note_id  ID of the note you wish to delete
+   * @param int $uid      (Optional) Owner of the note you wish to delete;
+   *                      defaults to current session user
+   *
+   * @return bool
+   */
+  public function &notes_delete($note_id, $uid = null) {
+    return $this->call_method('notes.delete',
+        array('uid' => $uid,
+              'note_id' => $note_id));
+  }
+
+  /**
+   * Edits a note, replacing its title and contents with the title
+   * and contents specified.
+   *
+   * @param int    $note_id  ID of the note you wish to edit
+   * @param string $title    Replacement title for the note
+   * @param string $content  Replacement content for the note
+   * @param int    $uid      (Optional) Owner of the note you wish to edit;
+   *                         defaults to current session user
+   *
+   * @return bool
+   */
+  public function &notes_edit($note_id, $title, $content, $uid = null) {
+    return $this->call_method('notes.edit',
+        array('uid' => $uid,
+              'note_id' => $note_id,
+              'title' => $title,
+              'content' => $content));
+  }
+
+  /**
+   * Retrieves all notes by a user. If note_ids are specified,
+   * retrieves only those specific notes by that user.
+   *
+   * @param int    $uid      User whose notes you wish to retrieve
+   * @param array  $note_ids (Optional) List of specific note
+   *                         IDs by this user to retrieve
+   *
+   * @return array A list of all of the given user's notes, or an empty list
+   *               if the viewer lacks permissions or if there are no visible
+   *               notes.
+   */
+  public function &notes_get($uid, $note_ids = null) {
+
+    return $this->call_method('notes.get',
+        array('uid' => $uid,
+              'note_ids' => $note_ids));
+  }
+
+
   /**
    * Returns the outstanding notifications for the session user.
    *
@@ -954,13 +1249,15 @@ function toggleDisplay(id, type) {
    *               and an eid list of 'event_invites'
    */
   public function &notifications_get() {
-    return $this->call_method('facebook.notifications.get', array());
+    return $this->call_method('facebook.notifications.get');
   }
 
   /**
    * Sends a notification to the specified users.
    *
    * @return A comma separated list of successful recipients
+   * @error
+   *    API_EC_PARAM_USER_ID_LIST
    */
   public function &notifications_send($to_ids, $notification, $type) {
     return $this->call_method('facebook.notifications.send',
@@ -972,12 +1269,14 @@ function toggleDisplay(id, type) {
   /**
    * Sends an email to the specified user of the application.
    *
-   * @param array $recipients  id of the recipients
+   * @param array/string $recipients array of ids of the recipients (csv is deprecated)
    * @param string $subject    subject of the email
    * @param string $text       (plain text) body of the email
    * @param string $fbml       fbml markup for an html version of the email
    *
    * @return string  A comma separated list of successful recipients
+   * @error
+   *    API_EC_PARAM_USER_ID_LIST
    */
   public function &notifications_sendEmail($recipients,
                                            $subject,
@@ -993,9 +1292,9 @@ function toggleDisplay(id, type) {
   /**
    * Returns the requested info fields for the requested set of pages.
    *
-   * @param array  $page_ids  an array of page ids
-   * @param array  $fields    an array of strings describing the info fields
-   *                          desired
+   * @param array/string $page_ids  an array of page ids (csv is deprecated)
+   * @param array/string  $fields    an array of strings describing the
+   *                           info fields desired (csv is deprecated)
    * @param int    $uid       (Optional) limit results to pages of which this
    *                          user is a fan.
    * @param string type       limits results to a particular type of page.
@@ -1090,7 +1389,7 @@ function toggleDisplay(id, type) {
               'tag_text' => $tag_text,
               'x' => $x,
               'y' => $y,
-              'tags' => json_encode($tags),
+              'tags' => (is_array($tags)) ? json_encode($tags) : null,
               'owner_uid' => $this->get_uid($owner_uid)));
   }
 
@@ -1128,7 +1427,8 @@ function toggleDisplay(id, type) {
    * @param int $subj_id  (Optional) Filter by uid of user tagged in the photos.
    * @param int $aid      (Optional) Filter by an album, as returned by
    *                      photos_getAlbums.
-   * @param array $pids   (Optional) Restrict to a list of pids
+   * @param array/string $pids   (Optional) Restrict to an array of pids
+   *                             (csv is deprecated)
    *
    * Note that at least one of these parameters needs to be specified, or an
    * error is returned.
@@ -1143,9 +1443,10 @@ function toggleDisplay(id, type) {
   /**
    * Returns the albums created by the given user.
    *
-   * @param int $uid     (Optional) The uid of the user whose albums you want.
-   *                     A null will return the albums of the session user.
-   * @param array $aids  (Optional) A list of aids to restrict the query.
+   * @param int $uid      (Optional) The uid of the user whose albums you want.
+   *                       A null will return the albums of the session user.
+   * @param string $aids  (Optional) An array of aids to restrict
+   *                       the query. (csv is deprecated)
    *
    * Note that at least one of the (uid, aids) parameters must be specified.
    *
@@ -1171,17 +1472,67 @@ function toggleDisplay(id, type) {
       array('pids' => $pids));
   }
 
+  /**
+   * Uploads a photo.
+   *
+   * @param string $file     The location of the photo on the local filesystem.
+   * @param int $aid         (Optional) The album into which to upload the
+   *                         photo.
+   * @param string $caption  (Optional) A caption for the photo.
+   * @param int uid          (Optional) The user ID of the user whose photo you
+   *                         are uploading
+   *
+   * @return array  An array of user objects
+   */
+  public function photos_upload($file, $aid=null, $caption=null, $uid=null) {
+    return $this->call_upload_method('facebook.photos.upload',
+                                     array('aid' => $aid,
+                                           'caption' => $caption,
+                                           'uid' => $uid),
+                                     $file);
+  }
+
+
+  /**
+   * Uploads a video.
+   *
+   * @param  string $file        The location of the video on the local filesystem.
+   * @param  string $title       (Optional) A title for the video. Titles over 65 characters in length will be truncated.
+   * @param  string $description (Optional) A description for the video.
+   *
+   * @return array  An array with the video's ID, title, description, and a link to view it on Facebook.
+   */
+  public function video_upload($file, $title=null, $description=null) {
+    return $this->call_upload_method('facebook.video.upload',
+                                     array('title' => $title,
+                                           'description' => $description),
+                                     $file,
+                                     Facebook::get_facebook_url('api-video') . '/restserver.php');
+  }
+
+  /**
+   * Returns an array with the video limitations imposed on the current session's
+   * associated user. Maximum length is measured in seconds; maximum size is
+   * measured in bytes.
+   *
+   * @return array  Array with "length" and "size" keys
+   */
+  public function &video_getUploadLimits() {
+    return $this->call_method('facebook.video.getUploadLimits');
+  }
+
   /**
    * Returns the requested info fields for the requested set of users.
    *
-   * @param array $uids    An array of user ids
-   * @param array $fields  An array of info field names desired
+   * @param array/string $uids    An array of user ids (csv is deprecated)
+   * @param array/string $fields  An array of info field names desired (csv is deprecated)
    *
    * @return array  An array of user objects
    */
   public function &users_getInfo($uids, $fields) {
     return $this->call_method('facebook.users.getInfo',
-        array('uids' => $uids, 'fields' => $fields));
+                  array('uids' => $uids,
+                        'fields' => $fields));
   }
 
   /**
@@ -1194,14 +1545,15 @@ function toggleDisplay(id, type) {
    * users, use users.getInfo instead, so that proper privacy rules will be
    * applied.
    *
-   * @param array $uids    An array of user ids
-   * @param array $fields  An array of info field names desired
+   * @param array/string $uids    An array of user ids (csv is deprecated)
+   * @param array/string $fields  An array of info field names desired (csv is deprecated)
    *
    * @return array  An array of user objects
    */
   public function &users_getStandardInfo($uids, $fields) {
     return $this->call_method('facebook.users.getStandardInfo',
-        array('uids' => $uids, 'fields' => $fields));
+                              array('uids' => $uids,
+                                    'fields' => $fields));
   }
 
   /**
@@ -1210,7 +1562,7 @@ function toggleDisplay(id, type) {
    * @return integer  User id
    */
   public function &users_getLoggedInUser() {
-    return $this->call_method('facebook.users.getLoggedInUser', array());
+    return $this->call_method('facebook.users.getLoggedInUser');
   }
 
   /**
@@ -1238,6 +1590,17 @@ function toggleDisplay(id, type) {
     return $this->call_method('facebook.users.isAppUser', array('uid' => $uid));
   }
 
+  /**
+   * Returns whether or not the user corresponding to the current
+   * session object is verified by Facebook. See the documentation
+   * for Users.isVerified for details.
+   *
+   * @return boolean  true if the user is verified
+   */
+  public function &users_isVerified() {
+    return $this->call_method('facebook.users.isVerified');
+  }
+
   /**
    * Sets the users' current status message. Message does NOT contain the
    * word "is" , so make sure to include a verb.
@@ -1268,6 +1631,69 @@ function toggleDisplay(id, type) {
     return $this->call_method('facebook.users.setStatus', $args);
   }
 
+  /**
+   * Gets the stream on behalf of a user using a set of users. This
+   * call will return the latest $limit queries between $start_time
+   * and $end_time.
+   *
+   * @param int    $viewer_id  user making the call (def: session)
+   * @param array  $source_ids users/pages to look at (def: all connections)
+   * @param int    $start_time start time to look for stories (def: 1 day ago)
+   * @param int    $end_time   end time to look for stories (def: now)
+   * @param int    $limit      number of stories to attempt to fetch (def: 30)
+   * @param string $filter_key key returned by stream.getFilters to fetch
+   *
+   * @return array(
+   *           'posts'    => array of posts,
+   *           'profiles' => array of profile metadata of users/pages in posts
+   *           'albums'   => array of album metadata in posts
+   *         )
+   */
+  public function &stream_get($viewer_id = null,
+                              $source_ids = null,
+                              $start_time = 0,
+                              $end_time = 0,
+                              $limit = 30,
+                              $filter_key = '') {
+    $args = array(
+      'viewer_id'  => $viewer_id,
+      'source_ids' => $source_ids,
+      'start_time' => $start_time,
+      'end_time'   => $end_time,
+      'limit'      => $limit,
+      'filter_key' => $filter_key);
+    return $this->call_method('facebook.stream.get', $args);
+  }
+
+  /**
+   * Gets the filters (with relevant filter keys for stream.get) for a
+   * particular user. These filters are typical things like news feed,
+   * friend lists, networks. They can be used to filter the stream
+   * without complex queries to determine which ids belong in which groups.
+   *
+   * @param int $uid user to get filters for
+   *
+   * @return array of stream filter objects
+   */
+  public function &stream_getFilters($uid = null) {
+    $args = array('uid' => $uid);
+    return $this->call_method('facebook.stream.getFilters', $args);
+  }
+
+  /**
+   * Gets the full comments given a post_id from stream.get or the
+   * stream FQL table. Initially, only a set of preview comments are
+   * returned because some posts can have many comments.
+   *
+   * @param string $post_id id of the post to get comments for
+   *
+   * @return array of comment objects
+   */
+  public function &stream_getComments($post_id) {
+    $args = array('post_id' => $post_id);
+    return $this->call_method('facebook.stream.getComments', $args);
+  }
+
   /**
    * Sets the FBML for the profile of the user attached to this session.
    *
@@ -1690,7 +2116,7 @@ function toggleDisplay(id, type) {
    *    API_EC_DATA_UNKNOWN_ERROR
    */
   public function &data_getObjectTypes() {
-    return $this->call_method('facebook.data.getObjectTypes', array());
+    return $this->call_method('facebook.data.getObjectTypes');
   }
 
   /**
@@ -2315,12 +2741,14 @@ function toggleDisplay(id, type) {
    *
    * @param string $integration_point_name  Name of an integration point
    *                                        (see developer wiki for list).
+   * @param int    $uid                     Specific user to check the limit.
    *
    * @return int  Integration point allocation value
    */
-  public function &admin_getAllocation($integration_point_name) {
+  public function &admin_getAllocation($integration_point_name, $uid=null) {
     return $this->call_method('facebook.admin.getAllocation',
-        array('integration_point_name' => $integration_point_name));
+        array('integration_point_name' => $integration_point_name,
+              'uid' => $uid));
   }
 
   /**
@@ -2376,28 +2804,75 @@ function toggleDisplay(id, type) {
    */
   public function admin_getRestrictionInfo() {
     return json_decode(
-        $this->call_method('admin.getRestrictionInfo', array()),
+        $this->call_method('admin.getRestrictionInfo'),
         true);
   }
 
+
+  /**
+   * Bans a list of users from the app. Banned users can't
+   * access the app's canvas page and forums.
+   *
+   * @param array $uids an array of user ids
+   * @return bool true on success
+   */
+  public function admin_banUsers($uids) {
+    return $this->call_method(
+      'admin.banUsers', array('uids' => json_encode($uids)));
+  }
+
+  /**
+   * Unban users that have been previously banned with
+   * admin_banUsers().
+   *
+   * @param array $uids an array of user ids
+   * @return bool true on success
+   */
+  public function admin_unbanUsers($uids) {
+    return $this->call_method(
+      'admin.unbanUsers', array('uids' => json_encode($uids)));
+  }
+
+  /**
+   * Gets the list of users that have been banned from the application.
+   * $uids is an optional parameter that filters the result with the list
+   * of provided user ids. If $uids is provided,
+   * only banned user ids that are contained in $uids are returned.
+   *
+   * @param array $uids an array of user ids to filter by
+   * @return bool true on success
+   */
+
+  public function admin_getBannedUsers($uids = null) {
+    return $this->call_method(
+      'admin.getBannedUsers',
+      array('uids' => $uids ? json_encode($uids) : null));
+  }
+
   /* UTILITY FUNCTIONS */
 
   /**
-   * Calls the specified method with the specified parameters.
+   * Calls the specified normal POST method with the specified parameters.
    *
    * @param string $method  Name of the Facebook method to invoke
    * @param array $params   A map of param names => param values
    *
-   * @return mixed  Result of method call
+   * @return mixed  Result of method call; this returns a reference to support
+   *                'delayed returns' when in a batch context.
+   *     See: http://wiki.developers.facebook.com/index.php/Using_batching_API
    */
-  public function & call_method($method, $params) {
-    //Check if we are in batch mode
-    if($this->batch_queue === null) {
+  public function &call_method($method, $params = array()) {
+    if (!$this->pending_batch()) {
       if ($this->call_as_apikey) {
         $params['call_as_apikey'] = $this->call_as_apikey;
       }
-      $xml = $this->post_request($method, $params);
-      $result = $this->convert_xml_to_result($xml, $method, $params);
+      $data = $this->post_request($method, $params);
+      if (empty($params['format']) || strtolower($params['format']) != 'json') {
+        $result = $this->convert_xml_to_result($data, $method, $params);
+      }
+      else {
+        $result = json_decode($data, true);
+      }
 
       if (is_array($result) && isset($result['error_code'])) {
         throw new FacebookRestClientException($result['error_msg'],
@@ -2413,11 +2888,46 @@ function toggleDisplay(id, type) {
     return $result;
   }
 
-  private function convert_xml_to_result($xml, $method, $params) {
+  /**
+   * Calls the specified file-upload POST method with the specified parameters
+   *
+   * @param string $method Name of the Facebook method to invoke
+   * @param array  $params A map of param names => param values
+   * @param string $file   A path to the file to upload (required)
+   *
+   * @return array A dictionary representing the response.
+   */
+  public function call_upload_method($method, $params, $file, $server_addr = null) {
+    if (!$this->pending_batch()) {
+      if (!file_exists($file)) {
+        $code =
+          FacebookAPIErrorCodes::API_EC_PARAM;
+        $description = FacebookAPIErrorCodes::$api_error_descriptions[$code];
+        throw new FacebookRestClientException($description, $code);
+      }
+
+      $xml = $this->post_upload_request($method, $params, $file, $server_addr);
+      $result = $this->convert_xml_to_result($xml, $method, $params);
+
+      if (is_array($result) && isset($result['error_code'])) {
+        throw new FacebookRestClientException($result['error_msg'],
+                                              $result['error_code']);
+      }
+    }
+    else {
+      $code =
+        FacebookAPIErrorCodes::API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE;
+      $description = FacebookAPIErrorCodes::$api_error_descriptions[$code];
+      throw new FacebookRestClientException($description, $code);
+    }
+
+    return $result;
+  }
+
+  protected function convert_xml_to_result($xml, $method, $params) {
     $sxml = simplexml_load_string($xml);
     $result = self::convert_simplexml_to_array($sxml);
 
-
     if (!empty($GLOBALS['facebook_config']['debug'])) {
       // output the raw xml and its corresponding php object, for debugging:
       print '<div style="margin: 10px 30px; padding: 5px; border: 2px solid black; background: gray; color: white; font-size: 12px; font-weight: bold;">';
@@ -2436,7 +2946,25 @@ function toggleDisplay(id, type) {
     return $result;
   }
 
-  private function create_post_string($method, $params) {
+  private function finalize_params($method, &$params) {
+    $this->add_standard_params($method, $params);
+    // we need to do this before signing the params
+    $this->convert_array_values_to_json($params);
+    $params['sig'] = Facebook::generate_sig($params, $this->secret);
+  }
+
+  private function convert_array_values_to_json(&$params) {
+    foreach ($params as $key => &$val) {
+      if (is_array($val)) {
+        $val = json_encode($val);
+      }
+    }
+  }
+
+  private function add_standard_params($method, &$params) {
+    if ($this->call_as_apikey) {
+      $params['call_as_apikey'] = $this->call_as_apikey;
+    }
     $params['method'] = $method;
     $params['session_key'] = $this->session_key;
     $params['api_key'] = $this->api_key;
@@ -2448,50 +2976,118 @@ function toggleDisplay(id, type) {
     if (!isset($params['v'])) {
       $params['v'] = '1.0';
     }
+    if (isset($this->use_ssl_resources) &&
+        $this->use_ssl_resources) {
+      $params['return_ssl_resources'] = true;
+    }
+  }
+
+  private function create_post_string($method, $params) {
     $post_params = array();
     foreach ($params as $key => &$val) {
-      if (is_array($val)) $val = implode(',', $val);
       $post_params[] = $key.'='.urlencode($val);
     }
-    $secret = $this->secret;
-    $post_params[] = 'sig='.Facebook::generate_sig($params, $secret);
     return implode('&', $post_params);
   }
 
-  public function post_request($method, $params) {
+  private function run_multipart_http_transaction($method, $params, $file, $server_addr) {
 
-    $post_string = $this->create_post_string($method, $params);
+    // the format of this message is specified in RFC1867/RFC1341.
+    // we add twenty pseudo-random digits to the end of the boundary string.
+    $boundary = '--------------------------FbMuLtIpArT' .
+                sprintf("%010d", mt_rand()) .
+                sprintf("%010d", mt_rand());
+    $content_type = 'multipart/form-data; boundary=' . $boundary;
+    // within the message, we prepend two extra hyphens.
+    $delimiter = '--' . $boundary;
+    $close_delimiter = $delimiter . '--';
+    $content_lines = array();
+    foreach ($params as $key => &$val) {
+      $content_lines[] = $delimiter;
+      $content_lines[] = 'Content-Disposition: form-data; name="' . $key . '"';
+      $content_lines[] = '';
+      $content_lines[] = $val;
+    }
+    // now add the file data
+    $content_lines[] = $delimiter;
+    $content_lines[] =
+      'Content-Disposition: form-data; filename="' . $file . '"';
+    $content_lines[] = 'Content-Type: application/octet-stream';
+    $content_lines[] = '';
+    $content_lines[] = file_get_contents($file);
+    $content_lines[] = $close_delimiter;
+    $content_lines[] = '';
+    $content = implode("\r\n", $content_lines);
+    return $this->run_http_post_transaction($content_type, $content, $server_addr);
+  }
 
-    if (function_exists('curl_init')) {
-      // Use CURL if installed...
+  public function post_request($method, $params) {
+    $this->finalize_params($method, $params);
+    $post_string = $this->create_post_string($method, $params);
+    if ($this->use_curl_if_available && function_exists('curl_init')) {
       $useragent = 'Facebook API PHP5 Client 1.1 (curl) ' . phpversion();
       $ch = curl_init();
       curl_setopt($ch, CURLOPT_URL, $this->server_addr);
       curl_setopt($ch, CURLOPT_POSTFIELDS, $post_string);
       curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
       curl_setopt($ch, CURLOPT_USERAGENT, $useragent);
+      curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
+      curl_setopt($ch, CURLOPT_TIMEOUT, 30);
       $result = curl_exec($ch);
       curl_close($ch);
     } else {
-      // Non-CURL based version...
       $content_type = 'application/x-www-form-urlencoded';
-      $user_agent = 'Facebook API PHP5 Client 1.1 (non-curl) '.phpversion();
-      $context =
-        array('http' =>
+      $content = $post_string;
+      $result = $this->run_http_post_transaction($content_type,
+                                                 $content,
+                                                 $this->server_addr);
+    }
+    return $result;
+  }
+
+  private function post_upload_request($method, $params, $file, $server_addr = null) {
+    $server_addr = $server_addr ? $server_addr : $this->server_addr;
+    $this->finalize_params($method, $params);
+    if ($this->use_curl_if_available && function_exists('curl_init')) {
+      // prepending '@' causes cURL to upload the file; the key is ignored.
+      $params['_file'] = '@' . $file;
+      $useragent = 'Facebook API PHP5 Client 1.1 (curl) ' . phpversion();
+      $ch = curl_init();
+      curl_setopt($ch, CURLOPT_URL, $server_addr);
+      // this has to come before the POSTFIELDS set!
+      curl_setopt($ch, CURLOPT_POST, 1 );
+      // passing an array gets curl to use the multipart/form-data content type
+      curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
+      curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+      curl_setopt($ch, CURLOPT_USERAGENT, $useragent);
+      $result = curl_exec($ch);
+      curl_close($ch);
+    } else {
+      $result = $this->run_multipart_http_transaction($method, $params, $file, $server_addr);
+    }
+    return $result;
+  }
+
+  private function run_http_post_transaction($content_type, $content, $server_addr) {
+
+    $user_agent = 'Facebook API PHP5 Client 1.1 (non-curl) ' . phpversion();
+    $content_length = strlen($content);
+    $context =
+      array('http' =>
               array('method' => 'POST',
-                    'header' => 'Content-type: '.$content_type."\r\n".
-                                'User-Agent: '.$user_agent."\r\n".
-                                'Content-length: ' . strlen($post_string),
-                    'content' => $post_string));
-      $contextid=stream_context_create($context);
-      $sock=fopen($this->server_addr, 'r', false, $contextid);
-      if ($sock) {
-        $result='';
-        while (!feof($sock))
-          $result.=fgets($sock, 4096);
-
-        fclose($sock);
+                    'user_agent' => $user_agent,
+                    'header' => 'Content-Type: ' . $content_type . "\r\n" .
+                                'Content-Length: ' . $content_length,
+                    'content' => $content));
+    $context_id = stream_context_create($context);
+    $sock = fopen($server_addr, 'r', false, $context_id);
+
+    $result = '';
+    if ($sock) {
+      while (!feof($sock)) {
+        $result .= fgets($sock, 4096);
       }
+      fclose($sock);
     }
     return $result;
   }
@@ -2541,6 +3137,14 @@ class FacebookAPIErrorCodes {
   const API_EC_METHOD = 3;
   const API_EC_TOO_MANY_CALLS = 4;
   const API_EC_BAD_IP = 5;
+  const API_EC_HOST_API = 6;
+  const API_EC_HOST_UP = 7;
+  const API_EC_SECURE = 8;
+  const API_EC_RATE = 9;
+  const API_EC_PERMISSION_DENIED = 10;
+  const API_EC_DEPRECATED = 11;
+  const API_EC_VERSION = 12;
+  const API_EC_INTERNAL_FQL_ERROR = 13;
 
   /*
    * PARAMETER ERRORS
@@ -2550,27 +3154,121 @@ class FacebookAPIErrorCodes {
   const API_EC_PARAM_SESSION_KEY = 102;
   const API_EC_PARAM_CALL_ID = 103;
   const API_EC_PARAM_SIGNATURE = 104;
+  const API_EC_PARAM_TOO_MANY = 105;
   const API_EC_PARAM_USER_ID = 110;
   const API_EC_PARAM_USER_FIELD = 111;
   const API_EC_PARAM_SOCIAL_FIELD = 112;
+  const API_EC_PARAM_EMAIL = 113;
+  const API_EC_PARAM_USER_ID_LIST = 114;
+  const API_EC_PARAM_FIELD_LIST = 115;
   const API_EC_PARAM_ALBUM_ID = 120;
+  const API_EC_PARAM_PHOTO_ID = 121;
+  const API_EC_PARAM_FEED_PRIORITY = 130;
+  const API_EC_PARAM_CATEGORY = 140;
+  const API_EC_PARAM_SUBCATEGORY = 141;
+  const API_EC_PARAM_TITLE = 142;
+  const API_EC_PARAM_DESCRIPTION = 143;
+  const API_EC_PARAM_BAD_JSON = 144;
   const API_EC_PARAM_BAD_EID = 150;
   const API_EC_PARAM_UNKNOWN_CITY = 151;
+  const API_EC_PARAM_BAD_PAGE_TYPE = 152;
 
   /*
    * USER PERMISSIONS ERRORS
    */
   const API_EC_PERMISSION = 200;
   const API_EC_PERMISSION_USER = 210;
+  const API_EC_PERMISSION_NO_DEVELOPERS = 211;
   const API_EC_PERMISSION_ALBUM = 220;
   const API_EC_PERMISSION_PHOTO = 221;
+  const API_EC_PERMISSION_MESSAGE = 230;
+  const API_EC_PERMISSION_OTHER_USER = 240;
+  const API_EC_PERMISSION_STATUS_UPDATE = 250;
+  const API_EC_PERMISSION_PHOTO_UPLOAD = 260;
+  const API_EC_PERMISSION_VIDEO_UPLOAD = 261;
+  const API_EC_PERMISSION_SMS = 270;
+  const API_EC_PERMISSION_CREATE_LISTING = 280;
+  const API_EC_PERMISSION_CREATE_NOTE = 281;
+  const API_EC_PERMISSION_SHARE_ITEM = 282;
   const API_EC_PERMISSION_EVENT = 290;
+  const API_EC_PERMISSION_LARGE_FBML_TEMPLATE = 291;
+  const API_EC_PERMISSION_LIVEMESSAGE = 292;
   const API_EC_PERMISSION_RSVP_EVENT = 299;
 
-  const FQL_EC_PARSER = 601;
+  /*
+   * DATA EDIT ERRORS
+   */
+  const API_EC_EDIT = 300;
+  const API_EC_EDIT_USER_DATA = 310;
+  const API_EC_EDIT_PHOTO = 320;
+  const API_EC_EDIT_ALBUM_SIZE = 321;
+  const API_EC_EDIT_PHOTO_TAG_SUBJECT = 322;
+  const API_EC_EDIT_PHOTO_TAG_PHOTO = 323;
+  const API_EC_EDIT_PHOTO_FILE = 324;
+  const API_EC_EDIT_PHOTO_PENDING_LIMIT = 325;
+  const API_EC_EDIT_PHOTO_TAG_LIMIT = 326;
+  const API_EC_EDIT_ALBUM_REORDER_PHOTO_NOT_IN_ALBUM = 327;
+  const API_EC_EDIT_ALBUM_REORDER_TOO_FEW_PHOTOS = 328;
+
+  const API_EC_MALFORMED_MARKUP = 329;
+  const API_EC_EDIT_MARKUP = 330;
+
+  const API_EC_EDIT_FEED_TOO_MANY_USER_CALLS = 340;
+  const API_EC_EDIT_FEED_TOO_MANY_USER_ACTION_CALLS = 341;
+  const API_EC_EDIT_FEED_TITLE_LINK = 342;
+  const API_EC_EDIT_FEED_TITLE_LENGTH = 343;
+  const API_EC_EDIT_FEED_TITLE_NAME = 344;
+  const API_EC_EDIT_FEED_TITLE_BLANK = 345;
+  const API_EC_EDIT_FEED_BODY_LENGTH = 346;
+  const API_EC_EDIT_FEED_PHOTO_SRC = 347;
+  const API_EC_EDIT_FEED_PHOTO_LINK = 348;
+
+  const API_EC_EDIT_VIDEO_SIZE = 350;
+  const API_EC_EDIT_VIDEO_INVALID_FILE = 351;
+  const API_EC_EDIT_VIDEO_INVALID_TYPE = 352;
+  const API_EC_EDIT_VIDEO_FILE = 353;
+
+  const API_EC_EDIT_FEED_TITLE_ARRAY = 360;
+  const API_EC_EDIT_FEED_TITLE_PARAMS = 361;
+  const API_EC_EDIT_FEED_BODY_ARRAY = 362;
+  const API_EC_EDIT_FEED_BODY_PARAMS = 363;
+  const API_EC_EDIT_FEED_PHOTO = 364;
+  const API_EC_EDIT_FEED_TEMPLATE = 365;
+  const API_EC_EDIT_FEED_TARGET = 366;
+  const API_EC_EDIT_FEED_MARKUP = 367;
+
+  /**
+   * SESSION ERRORS
+   */
+  const API_EC_SESSION_TIMED_OUT = 450;
+  const API_EC_SESSION_METHOD = 451;
+  const API_EC_SESSION_INVALID = 452;
+  const API_EC_SESSION_REQUIRED = 453;
+  const API_EC_SESSION_REQUIRED_FOR_SECRET = 454;
+  const API_EC_SESSION_CANNOT_USE_SESSION_SECRET = 455;
+
+
+  /**
+   * FQL ERRORS
+   */
+  const FQL_EC_UNKNOWN_ERROR = 600;
+  const FQL_EC_PARSER = 601; // backwards compatibility
+  const FQL_EC_PARSER_ERROR = 601;
   const FQL_EC_UNKNOWN_FIELD = 602;
   const FQL_EC_UNKNOWN_TABLE = 603;
-  const FQL_EC_NOT_INDEXABLE = 604;
+  const FQL_EC_NOT_INDEXABLE = 604; // backwards compatibility
+  const FQL_EC_NO_INDEX = 604;
+  const FQL_EC_UNKNOWN_FUNCTION = 605;
+  const FQL_EC_INVALID_PARAM = 606;
+  const FQL_EC_INVALID_FIELD = 607;
+  const FQL_EC_INVALID_SESSION = 608;
+  const FQL_EC_UNSUPPORTED_APP_TYPE = 609;
+  const FQL_EC_SESSION_SECRET_NOT_ALLOWED = 610;
+  const FQL_EC_DEPRECATED_TABLE = 611;
+  const FQL_EC_EXTENDED_PERMISSION = 612;
+  const FQL_EC_RATE_LIMIT_EXCEEDED = 613;
+
+  const API_EC_REF_SET_FAILED = 700;
 
   /**
    * DATA STORE API ERRORS
@@ -2581,52 +3279,122 @@ class FacebookAPIErrorCodes {
   const API_EC_DATA_OBJECT_NOT_FOUND = 803;
   const API_EC_DATA_OBJECT_ALREADY_EXISTS = 804;
   const API_EC_DATA_DATABASE_ERROR = 805;
+  const API_EC_DATA_CREATE_TEMPLATE_ERROR = 806;
+  const API_EC_DATA_TEMPLATE_EXISTS_ERROR = 807;
+  const API_EC_DATA_TEMPLATE_HANDLE_TOO_LONG = 808;
+  const API_EC_DATA_TEMPLATE_HANDLE_ALREADY_IN_USE = 809;
+  const API_EC_DATA_TOO_MANY_TEMPLATE_BUNDLES = 810;
+  const API_EC_DATA_MALFORMED_ACTION_LINK = 811;
+  const API_EC_DATA_TEMPLATE_USES_RESERVED_TOKEN = 812;
 
   /*
-   * Batch ERROR
+   * APPLICATION INFO ERRORS
    */
-  const API_EC_BATCH_ALREADY_STARTED = 900;
-  const API_EC_BATCH_NOT_STARTED = 901;
-  const API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE = 902;
+  const API_EC_NO_SUCH_APP = 900;
 
+  /*
+   * BATCH ERRORS
+   */
+  const API_EC_BATCH_TOO_MANY_ITEMS = 950;
+  const API_EC_BATCH_ALREADY_STARTED = 951;
+  const API_EC_BATCH_NOT_STARTED = 952;
+  const API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE = 953;
+
+  /*
+   * EVENT API ERRORS
+   */
+  const API_EC_EVENT_INVALID_TIME = 1000;
+
+  /*
+   * INFO BOX ERRORS
+   */
+  const API_EC_INFO_NO_INFORMATION = 1050;
+  const API_EC_INFO_SET_FAILED = 1051;
+
+  /*
+   * LIVEMESSAGE API ERRORS
+   */
+  const API_EC_LIVEMESSAGE_SEND_FAILED = 1100;
+  const API_EC_LIVEMESSAGE_EVENT_NAME_TOO_LONG = 1101;
+  const API_EC_LIVEMESSAGE_MESSAGE_TOO_LONG = 1102;
+
+  /*
+   * CONNECT SESSION ERRORS
+   */
+  const API_EC_CONNECT_FEED_DISABLED = 1300;
+
+  /*
+   * Platform tag bundles errors
+   */
+  const API_EC_TAG_BUNDLE_QUOTA = 1400;
+
+  /*
+   * SHARE
+   */
+  const API_EC_SHARE_BAD_URL = 1500;
+
+  /*
+   * NOTES
+   */
+  const API_EC_NOTE_CANNOT_MODIFY = 1600;
+
+  /*
+   * COMMENTS
+   */
+  const API_EC_COMMENTS_UNKNOWN = 1700;
+  const API_EC_COMMENTS_POST_TOO_LONG = 1701;
+  const API_EC_COMMENTS_DB_DOWN = 1702;
+  const API_EC_COMMENTS_INVALID_XID = 1703;
+  const API_EC_COMMENTS_INVALID_UID = 1704;
+  const API_EC_COMMENTS_INVALID_POST = 1705;
+
+  /**
+   * This array is no longer maintained; to view the description of an error
+   * code, please look at the message element of the API response or visit
+   * the developer wiki at http://wiki.developers.facebook.com/.
+   */
   public static $api_error_descriptions = array(
-      API_EC_SUCCESS           => 'Success',
-      API_EC_UNKNOWN           => 'An unknown error occurred',
-      API_EC_SERVICE           => 'Service temporarily unavailable',
-      API_EC_METHOD            => 'Unknown method',
-      API_EC_TOO_MANY_CALLS    => 'Application request limit reached',
-      API_EC_BAD_IP            => 'Unauthorized source IP address',
-      API_EC_PARAM             => 'Invalid parameter',
-      API_EC_PARAM_API_KEY     => 'Invalid API key',
-      API_EC_PARAM_SESSION_KEY => 'Session key invalid or no longer valid',
-      API_EC_PARAM_CALL_ID     => 'Call_id must be greater than previous',
-      API_EC_PARAM_SIGNATURE   => 'Incorrect signature',
-      API_EC_PARAM_USER_ID     => 'Invalid user id',
-      API_EC_PARAM_USER_FIELD  => 'Invalid user info field',
-      API_EC_PARAM_SOCIAL_FIELD => 'Invalid user field',
-      API_EC_PARAM_ALBUM_ID    => 'Invalid album id',
-      API_EC_PARAM_BAD_EID     => 'Invalid eid',
-      API_EC_PARAM_UNKNOWN_CITY => 'Unknown city',
-      API_EC_PERMISSION        => 'Permissions error',
-      API_EC_PERMISSION_USER   => 'User not visible',
-      API_EC_PERMISSION_ALBUM  => 'Album not visible',
-      API_EC_PERMISSION_PHOTO  => 'Photo not visible',
-      API_EC_PERMISSION_EVENT  => 'Creating and modifying events required the extended permission create_event',
-      API_EC_PERMISSION_RSVP_EVENT => 'RSVPing to events required the extended permission rsvp_event',
-      FQL_EC_PARSER            => 'FQL: Parser Error',
-      FQL_EC_UNKNOWN_FIELD     => 'FQL: Unknown Field',
-      FQL_EC_UNKNOWN_TABLE     => 'FQL: Unknown Table',
-      FQL_EC_NOT_INDEXABLE     => 'FQL: Statement not indexable',
-      FQL_EC_UNKNOWN_FUNCTION  => 'FQL: Attempted to call unknown function',
-      FQL_EC_INVALID_PARAM     => 'FQL: Invalid parameter passed in',
-      API_EC_DATA_UNKNOWN_ERROR => 'Unknown data store API error',
-      API_EC_DATA_INVALID_OPERATION => 'Invalid operation',
-      API_EC_DATA_QUOTA_EXCEEDED => 'Data store allowable quota was exceeded',
-      API_EC_DATA_OBJECT_NOT_FOUND => 'Specified object cannot be found',
-      API_EC_DATA_OBJECT_ALREADY_EXISTS => 'Specified object already exists',
-      API_EC_DATA_DATABASE_ERROR => 'A database error occurred. Please try again',
-      API_EC_BATCH_ALREADY_STARTED => 'begin_batch already called, please make sure to call end_batch first',
-      API_EC_BATCH_NOT_STARTED => 'end_batch called before start_batch',
-      API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE => 'This method is not allowed in batch mode',
+      self::API_EC_SUCCESS           => 'Success',
+      self::API_EC_UNKNOWN           => 'An unknown error occurred',
+      self::API_EC_SERVICE           => 'Service temporarily unavailable',
+      self::API_EC_METHOD            => 'Unknown method',
+      self::API_EC_TOO_MANY_CALLS    => 'Application request limit reached',
+      self::API_EC_BAD_IP            => 'Unauthorized source IP address',
+      self::API_EC_PARAM             => 'Invalid parameter',
+      self::API_EC_PARAM_API_KEY     => 'Invalid API key',
+      self::API_EC_PARAM_SESSION_KEY => 'Session key invalid or no longer valid',
+      self::API_EC_PARAM_CALL_ID     => 'Call_id must be greater than previous',
+      self::API_EC_PARAM_SIGNATURE   => 'Incorrect signature',
+      self::API_EC_PARAM_USER_ID     => 'Invalid user id',
+      self::API_EC_PARAM_USER_FIELD  => 'Invalid user info field',
+      self::API_EC_PARAM_SOCIAL_FIELD => 'Invalid user field',
+      self::API_EC_PARAM_USER_ID_LIST => 'Invalid user id list',
+      self::API_EC_PARAM_FIELD_LIST => 'Invalid field list',
+      self::API_EC_PARAM_ALBUM_ID    => 'Invalid album id',
+      self::API_EC_PARAM_BAD_EID     => 'Invalid eid',
+      self::API_EC_PARAM_UNKNOWN_CITY => 'Unknown city',
+      self::API_EC_PERMISSION        => 'Permissions error',
+      self::API_EC_PERMISSION_USER   => 'User not visible',
+      self::API_EC_PERMISSION_NO_DEVELOPERS  => 'Application has no developers',
+      self::API_EC_PERMISSION_ALBUM  => 'Album not visible',
+      self::API_EC_PERMISSION_PHOTO  => 'Photo not visible',
+      self::API_EC_PERMISSION_EVENT  => 'Creating and modifying events required the extended permission create_event',
+      self::API_EC_PERMISSION_RSVP_EVENT => 'RSVPing to events required the extended permission rsvp_event',
+      self::API_EC_EDIT_ALBUM_SIZE   => 'Album is full',
+      self::FQL_EC_PARSER            => 'FQL: Parser Error',
+      self::FQL_EC_UNKNOWN_FIELD     => 'FQL: Unknown Field',
+      self::FQL_EC_UNKNOWN_TABLE     => 'FQL: Unknown Table',
+      self::FQL_EC_NOT_INDEXABLE     => 'FQL: Statement not indexable',
+      self::FQL_EC_UNKNOWN_FUNCTION  => 'FQL: Attempted to call unknown function',
+      self::FQL_EC_INVALID_PARAM     => 'FQL: Invalid parameter passed in',
+      self::API_EC_DATA_UNKNOWN_ERROR => 'Unknown data store API error',
+      self::API_EC_DATA_INVALID_OPERATION => 'Invalid operation',
+      self::API_EC_DATA_QUOTA_EXCEEDED => 'Data store allowable quota was exceeded',
+      self::API_EC_DATA_OBJECT_NOT_FOUND => 'Specified object cannot be found',
+      self::API_EC_DATA_OBJECT_ALREADY_EXISTS => 'Specified object already exists',
+      self::API_EC_DATA_DATABASE_ERROR => 'A database error occurred. Please try again',
+      self::API_EC_BATCH_ALREADY_STARTED => 'begin_batch already called, please make sure to call end_batch first',
+      self::API_EC_BATCH_NOT_STARTED => 'end_batch called before begin_batch',
+      self::API_EC_BATCH_METHOD_NOT_ALLOWED_IN_BATCH_MODE => 'This method is not allowed in batch mode'
   );
 }
index 3f25e004d7054e896de16df6a14e4cd875234d7b..b79fefc85af4281873a60ae229f6219320ff7d1b 100644 (file)
--- a/index.php
+++ b/index.php
@@ -63,6 +63,10 @@ function handleError($error)
 
 function main()
 {
+    // quick check for fancy URL auto-detection support in installer.
+    if (isset($_SERVER['REDIRECT_URL']) && ('/check-fancy' === $_SERVER['REDIRECT_URL'])) {
+        die("Fancy URL support detection succeeded. We suggest you enable this to get fancy (pretty) URLs.");
+    }
     global $user, $action, $config;
 
     Snapshot::check();
@@ -103,6 +107,8 @@ function main()
 
     $args = array_merge($args, $_REQUEST);
 
+    Event::handle('ArgsInitialize', array(&$args));
+
     $action = $args['action'];
 
     if (!$action || !preg_match('/^[a-zA-Z0-9_-]*$/', $action)) {
index 87a99a650895b3774adc3b2e0c84cf401cb370fe..bc82e5e37a8e0fbb7c5491ce88146b76fadf367d 100644 (file)
@@ -35,15 +35,17 @@ function main()
 
 function checkPrereqs()
 {
+       $pass = true;
+       
     if (file_exists(INSTALLDIR.'/config.php')) {
          ?><p class="error">Config file &quot;config.php&quot; already exists.</p>
          <?php
-        return false;
+        $pass = false;
     }
 
     if (version_compare(PHP_VERSION, '5.0.0', '<')) {
             ?><p class="error">Require PHP version 5 or greater.</p><?php
-                   return false;
+                   $pass = false;
     }
 
     $reqs = array('gd', 'mysql', 'curl',
@@ -52,28 +54,26 @@ function checkPrereqs()
 
     foreach ($reqs as $req) {
         if (!checkExtension($req)) {
-            ?><p class="error">Cannot load required extension &quot;<?php echo $req; ?>&quot;.</p><?php
-                   return false;
+            ?><p class="error">Cannot load required extension: <code><?php echo $req; ?></code></p><?php
+                   $pass = false;
         }
     }
 
        if (!is_writable(INSTALLDIR)) {
-         ?><p class="error">Cannot write config file to &quot;<?php echo INSTALLDIR; ?>&quot;.</p>
-              <p>On your server, try this command:</p>
-              <blockquote>chmod a+w <?php echo INSTALLDIR; ?></blockquote>
+         ?><p class="error">Cannot write config file to: <code><?php echo INSTALLDIR; ?></code></p>
+              <p>On your server, try this command: <code>chmod a+w <?php echo INSTALLDIR; ?></code>
          <?php
-            return false;
+            $pass = false;
        }
 
        if (!is_writable(INSTALLDIR.'/avatar/')) {
-         ?><p class="error">Cannot write avatar directory &quot;<?php echo INSTALLDIR; ?>/avatar/&quot;.</p>
-              <p>On your server, try this command:</p>
-              <blockquote>chmod a+w <?php echo INSTALLDIR; ?>/avatar/</blockquote>
+         ?><p class="error">Cannot write avatar directory: <code><?php echo INSTALLDIR; ?>/avatar/</code></p>
+              <p>On your server, try this command: <code>chmod a+w <?php echo INSTALLDIR; ?>/avatar/</code></p>
          <?
-            return false;
+            $pass = false;
        }
 
-       return true;
+       return $pass;
 }
 
 function checkExtension($name)
@@ -88,96 +88,125 @@ function checkExtension($name)
 
 function showForm()
 {
-?>
-<p>Enter your database connection information below to initialize the database.</p>
-<form method='post' action='install.php'>
-       <fieldset>
-       <ul class='form_data'>
-       <li>
-       <label for='sitename'>Site name</label>
-       <input type='text' id='sitename' name='sitename' />
-       <p>The name of your site</p>
-       </li>
-       <li>
-       <li>
-       <label for='host'>Hostname</label>
-       <input type='text' id='host' name='host' />
-       <p>Database hostname</p>
-       </li>
-       <li>
-       <label for='host'>Database</label>
-       <input type='text' id='database' name='database' />
-       <p>Database name</p>
-       </li>
-       <li>
-       <label for='username'>Username</label>
-       <input type='text' id='username' name='username' />
-       <p>Database username</p>
-       </li>
-       <li>
-       <label for='password'>Password</label>
-       <input type='password' id='password' name='password' />
-       <p>Database password</p>
-       </li>
-       </ul>
-       <input type='submit' name='submit' value='Submit'>
-       </fieldset>
+    $config_path = htmlentities(trim(dirname($_SERVER['REQUEST_URI']), '/'));
+    echo<<<E_O_T
+        </ul>
+    </dd>
+</dl>
+<dl id="page_notice" class="system_notice">
+    <dt>Page notice</dt>
+    <dd>
+        <div class="instructions">
+            <p>Enter your database connection information below to initialize the database.</p>
+        </div>
+    </dd>
+</dl>
+<form method="post" action="install.php" class="form_settings" id="form_install">
+    <fieldset>
+        <legend>Connection settings</legend>
+        <ul class="form_data">
+            <li>
+                <label for="sitename">Site name</label>
+                <input type="text" id="sitename" name="sitename" />
+                <p class="form_guide">The name of your site</p>
+            </li>
+            <li>
+                <label for="fancy-enable">Fancy URLs</label>
+                <input type="radio" name="fancy" id="fancy-enable" value="enable" checked='checked' /> enable<br />
+                <input type="radio" name="fancy" id="fancy-disable" value="" /> disable<br />
+                <p class="form_guide" id='fancy-form_guide'>Enable fancy (pretty) URLs. Auto-detection failed, it depends on Javascript.</p>
+            </li>
+            <li>
+                <label for="host">Hostname</label>
+                <input type="text" id="host" name="host" />
+                <p class="form_guide">Database hostname</p>
+            </li>
+            <li>
+                <label for="host">Site path</label>
+                <input type="text" id="path" name="path" value="$config_path" />
+                <p class="form_guide">Site path, following the "/" after the domain name in the URL. Empty is fine. Field should be filled automatically.</p>
+            </li>
+            <li>
+                <label for="host">Database</label>
+                <input type="text" id="database" name="database" />
+                <p class="form_guide">Database name</p>
+            </li>
+            <li>
+                <label for="username">Username</label>
+                <input type="text" id="username" name="username" />
+                <p class="form_guide">Database username</p>
+            </li>
+            <li>
+                <label for="password">Password</label>
+                <input type="password" id="password" name="password" />
+                <p class="form_guide">Database password</p>
+            </li>
+        </ul>
+        <input type="submit" name="submit" class="submit" value="Submit" />
+    </fieldset>
 </form>
-<?
+
+E_O_T;
 }
 
 function updateStatus($status, $error=false)
 {
 ?>
-       <li>
-<?
-    print $status;
-?>
-       </li>
-<?
+                <li <?php echo ($error) ? 'class="error"': ''; ?>><?print $status;?></li>
+
+<?php
 }
 
 function handlePost()
 {
 ?>
-       <ul>
-<?
-    $host = $_POST['host'];
+
+<?php
+    $host     = $_POST['host'];
     $database = $_POST['database'];
     $username = $_POST['username'];
     $password = $_POST['password'];
     $sitename = $_POST['sitename'];
-
+    $path     = $_POST['path'];
+    $fancy    = !empty($_POST['fancy']);
+?>
+    <dl class="system_notice">
+        <dt>Page notice</dt>
+        <dd>
+            <ul>
+<?php
+       $fail = false;
+       
     if (empty($host)) {
         updateStatus("No hostname specified.", true);
-        showForm();
-        return;
+               $fail = true;
     }
 
     if (empty($database)) {
         updateStatus("No database specified.", true);
-        showForm();
-        return;
+               $fail = true;
     }
 
     if (empty($username)) {
         updateStatus("No username specified.", true);
-        showForm();
-        return;
+               $fail = true;
     }
 
     if (empty($password)) {
         updateStatus("No password specified.", true);
-        showForm();
-        return;
+               $fail = true;
     }
 
     if (empty($sitename)) {
         updateStatus("No sitename specified.", true);
-        showForm();
-        return;
+               $fail = true;
     }
 
+       if($fail){
+               showForm();
+           return;
+       }
+
     updateStatus("Starting installation...");
     updateStatus("Checking database...");
     $conn = mysql_connect($host, $username, $password);
@@ -214,24 +243,30 @@ function handlePost()
     }
     updateStatus("Writing config file...");
     $sqlUrl = "mysqli://$username:$password@$host/$database";
-    $res = writeConf($sitename, $sqlUrl);
+    $res = writeConf($sitename, $sqlUrl, $fancy, $path);
     if (!$res) {
         updateStatus("Can't write config file.", true);
         showForm();
         return;
     }
     updateStatus("Done!");
+    if ($path) $path .= '/';
+    updateStatus("You can visit your <a href='/$path'>new Laconica site</a>.");
 ?>
-       </ul>
-<?
+
+<?php
 }
 
-function writeConf($sitename, $sqlUrl)
+function writeConf($sitename, $sqlUrl, $fancy, $path)
 {
     $res = file_put_contents(INSTALLDIR.'/config.php',
                              "<?php\n".
+                             "if (!defined('LACONICA')) { exit(1); }\n\n".
                              "\$config['site']['name'] = \"$sitename\";\n\n".
-                             "\$config['db']['database'] = \"$sqlUrl\";\n\n");
+                             ($fancy ? "\$config['site']['fancy'] = true;\n\n":'').
+                             "\$config['site']['path'] = \"$path\";\n\n".
+                             "\$config['db']['database'] = \"$sqlUrl\";\n\n".
+                             "?>");
     return $res;
 }
 
@@ -253,21 +288,37 @@ function runDbScript($filename, $conn)
 }
 
 ?>
-<html>
-<head>
-       <title>Install Laconica</title>
-       <link rel="stylesheet" type="text/css" href="theme/base/css/display.css?version=0.7.1" media="screen, projection, tv"/>
-       <link rel="stylesheet" type="text/css" href="theme/base/css/modal.css?version=0.7.1" media="screen, projection, tv"/>
-       <link rel="stylesheet" type="text/css" href="theme/default/css/display.css?version=0.7.1" media="screen, projection, tv"/>
-</head>
-<body>
-       <div id="wrap">
-       <div id="core">
-       <div id="content">
-       <h1>Install Laconica</h1>
+<?php echo"<?"; ?> xml version="1.0" encoding="UTF-8" <?php echo "?>"; ?>
+<!DOCTYPE html
+PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+       "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en_US" lang="en_US">
+    <head>
+        <title>Install Laconica</title>
+        <link rel="stylesheet" type="text/css" href="theme/base/css/display.css?version=0.8" media="screen, projection, tv"/>
+        <link rel="stylesheet" type="text/css" href="theme/default/css/display.css?version=0.8" media="screen, projection, tv"/>
+        <!--[if IE]><link rel="stylesheet" type="text/css" href="theme/base/css/ie.css?version=0.8" /><![endif]-->
+        <!--[if lte IE 6]><link rel="stylesheet" type="text/css" theme/base/css/ie6.css?version=0.8" /><![endif]-->
+        <!--[if IE]><link rel="stylesheet" type="text/css" href="theme/earthy/css/ie.css?version=0.8" /><![endif]-->
+        <script src='js/jquery.min.js'></script>
+        <script src='js/install.js'></script>
+    </head>
+    <body id="install">
+        <div id="wrap">
+            <div id="header">
+                <address id="site_contact" class="vcard">
+                    <a class="url home bookmark" href=".">
+                        <img class="logo photo" src="theme/default/logo.png" alt="Laconica"/>
+                        <span class="fn org">Laconica</span>
+                    </a>
+                </address>
+            </div>
+            <div id="core">
+                <div id="content">
+                    <h1>Install Laconica</h1>
 <?php main(); ?>
-       </div>
-       </div>
-       </div>
-</body>
+                </div>
+            </div>
+        </div>
+    </body>
 </html>
index 21a1530bca5d0d4ac7b7e00c704256858c7d18b2..0149eca7d9f30f2a82d9500238353476368e916c 100644 (file)
@@ -1,10 +1,85 @@
+/** Init for Farbtastic library and page setup
+ *
+ * @package   Laconica
+ * @author Sarven Capadisli <csarven@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
 $(document).ready(function() {
-    var f = $.farbtastic('#color-picker');
-    var colors = $('#settings_design_color input');
-
-    colors
-        .each(function () { f.linkTo(this); })
-        .focus(function() {
-            f.linkTo(this);
-        });
+    function UpdateColors(S) {
+        C = $(S).val();
+        switch (parseInt(S.id.slice(-1))) {
+            case 0: default:
+                $('body').css({'background-color':C});
+                break;
+            case 1:
+                $('#content').css({'background-color':C});
+                break;
+            case 2:
+                $('#aside_primary').css({'background-color':C});
+                break;
+            case 3:
+                $('body').css({'color':C});
+                break;
+            case 4:
+                $('a').css({'color':C});
+                break;
+        }
+    }
+
+    function UpdateFarbtastic(e) {
+        f.linked = e;
+        f.setColor(e.value);
+    }
+
+    function UpdateSwatch(e) {
+        $(e).css({"background-color": e.value,
+                  "color": f.hsl[2] > 0.5 ? "#000": "#fff"});
+    }
+
+    function SynchColors(e) {
+        var S = f.linked;
+        var C = f.color;
+
+        if (S && S.value && S.value != C) {
+            S.value = C;
+            UpdateSwatch(S);
+            UpdateColors(S);
+        }
+    }
+
+    function Init() {
+        $('#settings_design_color').append('<div id="color-picker"></div>');
+        $('#color-picker').hide();
+
+        f = $.farbtastic('#color-picker', SynchColors);
+        swatches = $('#settings_design_color .swatch');
+
+        swatches
+            .each(SynchColors)
+            .blur(function() {
+                $(this).val($(this).val().toUpperCase());
+             })
+            .focus(function() {
+                $('#color-picker').show();
+                UpdateFarbtastic(this);
+            })
+            .change(function() {
+                UpdateFarbtastic(this);
+                UpdateSwatch(this);
+                UpdateColors(this);
+            }).change();
+    }
+
+    var f, swatches;
+    Init();
+    $('#form_settings_design').bind('reset', function(){
+        setTimeout(function(){
+            swatches.each(function(){UpdateColors(this);});
+            $('#color-picker').remove();
+            swatches.unbind();
+            Init();
+        },10);
+    });
 });
index 869230b7a45afd28c222819ba22774b863f77c9b..ffa55ae93e5a8602010465266c3ebf7d7fd8e810 100644 (file)
             var a = document.createElement('A');
             a.innerHTML = 'get this';
             a.target = '_blank';
-            a.href = 'http://identica/doc/badge';
+            a.href = 'http://identi.ca/doc/badge';
             $.s.f.appendChild(a);
             $.s.appendChild($.s.f);
             $.f.getUser();
diff --git a/js/install.js b/js/install.js
new file mode 100644 (file)
index 0000000..32a5411
--- /dev/null
@@ -0,0 +1,18 @@
+$(document).ready(function(){
+    $.ajax({url:'check-fancy',
+        type:'GET',
+        success:function(data, textStatus) {
+            $('#fancy-enable').attr('checked', true);
+            $('#fancy-disable').attr('checked', false);
+            $('#fancy-form_guide').text(data);
+        },
+        error:function(XMLHttpRequest, textStatus, errorThrown) {
+            $('#fancy-enable').attr('checked', false);
+            $('#fancy-disable').attr('checked', true);
+            $('#fancy-enable').attr('disabled', true);
+            $('#fancy-disable').attr('disabled', true);
+            $('#fancy-form_guide').text("Fancy URL support detection failed, disabling this option. Make sure you renamed htaccess.sample to .htaccess.");
+        }
+    });
+});
+
index a0399d540510db1638a9f233dbeb5ef18bcc6bc1..4e1cbfd1e705810938e2a5ee07a9cf304d80c688 100644 (file)
@@ -1,39 +1,48 @@
-               $(function(){
-                       var x = ($('#avatar_crop_x').val()) ? $('#avatar_crop_x').val() : 0;
-                       var y = ($('#avatar_crop_y').val()) ? $('#avatar_crop_y').val() : 0;
-                       var w = ($('#avatar_crop_w').val()) ? $('#avatar_crop_w').val() : $("#avatar_original img").attr("width");
-                       var h = ($('#avatar_crop_h').val()) ? $('#avatar_crop_h').val() : $("#avatar_original img").attr("height");
+/** Init for Jcrop library and page setup
+ *
+ * @package   Laconica
+ * @author Sarven Capadisli <csarven@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
 
-                       jQuery("#avatar_original img").Jcrop({
-                               onChange: showPreview,
-                       setSelect: [ x, y, w, h ],
-                               onSelect: updateCoords,
-                               aspectRatio: 1,
-                               boxWidth: 480,
-                               boxHeight: 480,
-                               bgColor: '#000',
-                               bgOpacity: .4
-                       });
-               });
+$(function(){
+    var x = ($('#avatar_crop_x').val()) ? $('#avatar_crop_x').val() : 0;
+    var y = ($('#avatar_crop_y').val()) ? $('#avatar_crop_y').val() : 0;
+    var w = ($('#avatar_crop_w').val()) ? $('#avatar_crop_w').val() : $("#avatar_original img").attr("width");
+    var h = ($('#avatar_crop_h').val()) ? $('#avatar_crop_h').val() : $("#avatar_original img").attr("height");
 
-               function showPreview(coords) {
-                       var rx = 96 / coords.w;
-                       var ry = 96 / coords.h;
+    jQuery("#avatar_original img").Jcrop({
+        onChange: showPreview,
+        setSelect: [ x, y, w, h ],
+        onSelect: updateCoords,
+        aspectRatio: 1,
+        boxWidth: 480,
+        boxHeight: 480,
+        bgColor: '#000',
+        bgOpacity: .4
+    });
+});
 
-                       var img_width = $("#avatar_original img").attr("width");
-                       var img_height = $("#avatar_original img").attr("height");
+function showPreview(coords) {
+    var rx = 96 / coords.w;
+    var ry = 96 / coords.h;
 
-                       $('#avatar_preview img').css({
-                               width: Math.round(rx *img_width) + 'px',
-                               height: Math.round(ry * img_height) + 'px',
-                               marginLeft: '-' + Math.round(rx * coords.x) + 'px',
-                               marginTop: '-' + Math.round(ry * coords.y) + 'px'
-                       });
-               };
+    var img_width = $("#avatar_original img").attr("width");
+    var img_height = $("#avatar_original img").attr("height");
 
-               function updateCoords(c) {
-                       $('#avatar_crop_x').val(c.x);
-                       $('#avatar_crop_y').val(c.y);
-                       $('#avatar_crop_w').val(c.w);
-                       $('#avatar_crop_h').val(c.h);
-               };
+    $('#avatar_preview img').css({
+        width: Math.round(rx *img_width) + 'px',
+        height: Math.round(ry * img_height) + 'px',
+        marginLeft: '-' + Math.round(rx * coords.x) + 'px',
+        marginTop: '-' + Math.round(ry * coords.y) + 'px'
+    });
+};
+
+function updateCoords(c) {
+    $('#avatar_crop_x').val(c.x);
+    $('#avatar_crop_y').val(c.y);
+    $('#avatar_crop_w').val(c.w);
+    $('#avatar_crop_h').val(c.h);
+};
diff --git a/js/jquery.joverlay.min.js b/js/jquery.joverlay.min.js
new file mode 100644 (file)
index 0000000..c916850
--- /dev/null
@@ -0,0 +1,6 @@
+/* Copyright (c) 2009 Alvaro A. Lima Jr http://alvarojunior.com/jquery/joverlay.html
+ * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
+ * Version: 0.6 (Abr 23, 2009)
+ * Requires: jQuery 1.3+
+ */
+(function($){var f=$.browser.msie&&$.browser.version==6.0;var g=null;$.fn.jOverlay=function(b){var b=$.extend({},$.fn.jOverlay.options,b);if(g!=null){clearTimeout(g)}var c=this.is('*')?this:'#jOverlayContent';var d=f?'absolute':'fixed';var e=b.imgLoading?"<img id='jOverlayLoading' src='"+b.imgLoading+"' style='position:"+d+"; z-index:"+(b.zIndex+9)+";'/>":'';$('body').prepend(e+"<div id='jOverlay' />"+"<div id='jOverlayContent' style='position:"+d+"; z-index:"+(b.zIndex+5)+"; display:none;'/>");$('#jOverlayLoading').load(function(){if(b.center){$.center(this)}});if(f){$("select").hide();$("#jOverlayContent select").show()}$('#jOverlay').css({backgroundColor:b.color,position:d,top:'0px',left:'0px',filter:'alpha(opacity='+(b.opacity*100)+')',opacity:b.opacity,zIndex:b.zIndex,width:!f?'100%':$(window).width()+'px',height:!f?'100%':$(document).height()+'px'}).show();if(this.is('*')){$('#jOverlayContent').html(this.addClass('jOverlayChildren').show()).show();if(b.center){$.center('#jOverlayContent')}if(!b.url&&$.isFunction(b.success)){b.success(this.html())}}if(b.url){$.ajax({type:b.method,data:b.data,url:b.url,success:function(a){$('#jOverlayLoading').fadeOut(600);$(c).html(a).show();if(b.center){$.center('#jOverlayContent')}if($.isFunction(b.success)){b.success(a)}}})}if(f){$(window).scroll(function(){if(b.center){$.center('#jOverlayContent')}});$(window).resize(function(){$('#jOverlay').css({width:$(window).width()+'px',height:$(document).height()+'px'});if(b.center){$.center('#jOverlayContent')}})}$(document).keydown(function(a){if(a.keyCode==27){$.closeOverlay()}});if(b.bgClickToClose){$('#jOverlay').click($.closeOverlay)}if(Number(b.timeout)>0){g=setTimeout($.closeOverlay,Number(b.timeout))}};$.center=function(a){var a=$(a);var b=a.height();var c=a.width();a.css({width:c+'px',marginLeft:'-'+(c/2)+'px',marginTop:'-'+b/2+'px',height:'auto',top:!f?'50%':$(window).scrollTop()+($(window).height()/2)+"px",left:'50%'})};$.fn.jOverlay.options={method:'GET',data:'',url:'',color:'#000',opacity:'0.6',zIndex:9999,center:true,imgLoading:'',bgClickToClose:true,success:null,timeout:0};$.closeOverlay=function(){if(f){$("select").show()}$('#jOverlayContent .jOverlayChildren').hide().prependTo($('body'));$('#jOverlayLoading, #jOverlayContent, #jOverlay').remove()}})(jQuery);
\ No newline at end of file
index 81139744ff9cdab7eb928df3f2e83a5df5611562..31d9eb4f54706a09a384ca2cf0604ae7904373ef 100644 (file)
  */
 
 $(document).ready(function(){
+    $('.attachments').click(function() {$().jOverlay({zIndex:999, success:function(html) {$('.attachment').click(function() {$().jOverlay({url:$(this).attr('href') + '/ajax'}); return false; });
+        }, url:$(this).attr('href') + '/ajax'}); return false; });
+    $('.attachment').click(function() {$().jOverlay({url:$(this).attr('href') + '/ajax'}); return false; });
+
        // count character on keyup
        function counter(event){
                var maxLength = 140;
@@ -166,19 +170,45 @@ $(document).ready(function(){
                                                                                                                                                   $("#notice_action-submit").addClass("disabled");
                                                                                                                                                   return true;
                                                                                                                                                 },
-                                          success: function(xml) {     if ($("#error", xml).length > 0 || $("#command_result", xml).length > 0) {
+                                          timeout: '60000',
+                                          error: function (xhr, textStatus, errorThrown) {     $("#form_notice").removeClass("processing");
+                                                                                                                                               $("#notice_action-submit").removeAttr("disabled");
+                                                                                                                                               $("#notice_action-submit").removeClass("disabled");
+                                                                                                                                               if (textStatus == "timeout") {
+                                                                                                                                                       alert ("Sorry! We had trouble sending your notice. The servers are overloaded. Please try again, and contact the site administrator if this problem persists");
+                                                                                                                                               }
+                                                                                                                                               else {
+                                                                                                                                                       if ($(".error", xhr.responseXML).length > 0) {
+                                                                                                                                                               $('#form_notice').append(document._importNode($(".error", xhr.responseXML).get(0), true));
+                                                                                                                                                       }
+                                                                                                                                                       else {
+                                                                                                                                                               alert("Sorry! We had trouble sending your notice ("+xhr.status+" "+xhr.statusText+"). Please report the problem to the site administrator if this happens again.");
+                                                                                                                                                       }
+                                                                                                                                               }
+                                                                                                                                         },
+                                          success: function(xml) {     if ($("#error", xml).length > 0) {
                                                                                                        var result = document._importNode($("p", xml).get(0), true);
                                                                                                        result = result.textContent || result.innerHTML;
                                                                                                        alert(result);
                                                                                                }
                                                                                                else {
-                                                                                                       $("#notices_primary .notices").prepend(document._importNode($("li", xml).get(0), true));
+                                                                                                   if ($("#command_result", xml).length > 0) {
+                                                                                                           var result = document._importNode($("p", xml).get(0), true);
+                                                                                                           result = result.textContent || result.innerHTML;
+                                                                                                           alert(result);
+                                                    }
+                                                    else {
+                                                         li = $("li", xml).get(0);
+                                                         if ($("#"+li.id).length == 0) {
+                                                              $("#notices_primary .notices").prepend(document._importNode(li, true));
+                                                              $("#notices_primary .notice:first").css({display:"none"});
+                                                              $("#notices_primary .notice:first").fadeIn(2500);
+                                                              NoticeHover();
+                                                              NoticeReply();
+                                                         }
+                                                                                                       }
                                                                                                        $("#notice_data-text").val("");
-                                                                                                       counter();
-                                                                                                       $("#notices_primary .notice:first").css({display:"none"});
-                                                                                                       $("#notices_primary .notice:first").fadeIn(2500);
-                                                                                                       NoticeHover();
-                                                                                                       NoticeReply();
+                                                    counter();
                                                                                                }
                                                                                                $("#form_notice").removeClass("processing");
                                                                                                $("#notice_action-submit").removeAttr("disabled");
@@ -187,7 +217,6 @@ $(document).ready(function(){
                                           };
        $("#form_notice").ajaxForm(PostNotice);
        $("#form_notice").each(addAjaxHidden);
-
     NoticeHover();
     NoticeReply();
 });
index fe106cb8376a06b69ac5ba142329917b808a4427..924aa93a8953ab7b0239660989cc1db849d43e97 100644 (file)
@@ -22,6 +22,7 @@ if (!defined('LACONICA')) { exit(1); }
 class ShortUrlApi
 {
     protected $service_url;
+    protected $long_limit = 27;
 
     function __construct($service_url)
     {
@@ -39,7 +40,7 @@ class ShortUrlApi
     }
 
     private function is_long($url) {
-        return strlen($url) >= 30;
+        return strlen($url) >= $this->long_limit;
     }
 
     protected function http_post($data) {
index 3e43ffe3e3ee1427d53c571ee4fb52b639b31aa6..6a69d26518170eed18f04a7a76ebdb15b4bbd9ce 100644 (file)
@@ -98,15 +98,15 @@ class Action extends HTMLOutputter // lawsuit
             Event::handle('EndShowHTML', array($this));
         }
         if (Event::handle('StartShowHead', array($this))) {
-        $this->showHead();
+            $this->showHead();
             Event::handle('EndShowHead', array($this));
         }
         if (Event::handle('StartShowBody', array($this))) {
-        $this->showBody();
+            $this->showBody();
             Event::handle('EndShowBody', array($this));
         }
         if (Event::handle('StartEndHTML', array($this))) {
-        $this->endHTML();
+            $this->endHTML();
             Event::handle('EndEndHTML', array($this));
         }
     }
@@ -243,6 +243,12 @@ class Action extends HTMLOutputter // lawsuit
                 $this->element('script', array('type' => 'text/javascript',
                                                'src' => common_path('js/jquery.form.js')),
                                ' ');
+
+                $this->element('script', array('type' => 'text/javascript',
+                                               'src' => common_path('js/jquery.joverlay.min.js')),
+                               ' ');
+
+
                 Event::handle('EndShowJQueryScripts', array($this));
             }
             if (Event::handle('StartShowLaconicaScripts', array($this))) {
@@ -347,7 +353,7 @@ class Action extends HTMLOutputter // lawsuit
     {
         $this->elementStart('body', (common_current_user()) ? array('id' => $this->trimmed('action'),
                                                                     'class' => 'user_in')
-                                                            : array('id' => $this->trimmed('action')));
+                            : array('id' => $this->trimmed('action')));
         $this->elementStart('div', array('id' => 'wrap'));
         if (Event::handle('StartShowHeader', array($this))) {
             $this->showHeader();
@@ -431,10 +437,10 @@ class Action extends HTMLOutputter // lawsuit
                                     _('Connect'), _('Connect to SMS, Twitter'), false, 'nav_connect');
                 }
                 $this->menuItem(common_local_url('invite'),
-                                 _('Invite'),
-                                 sprintf(_('Invite friends and colleagues to join you on %s'),
-                                 common_config('site', 'name')),
-                                 false, 'nav_invitecontact');
+                                _('Invite'),
+                                sprintf(_('Invite friends and colleagues to join you on %s'),
+                                        common_config('site', 'name')),
+                                false, 'nav_invitecontact');
                 $this->menuItem(common_local_url('logout'),
                                 _('Logout'), _('Logout from the site'), false, 'nav_logout');
             }
@@ -591,7 +597,10 @@ class Action extends HTMLOutputter // lawsuit
                                         'class' => 'system_notice'));
         $this->element('dt', null, _('Page notice'));
         $this->elementStart('dd');
-        $this->showPageNotice();
+        if (Event::handle('StartShowPageNotice', array($this))) {
+            $this->showPageNotice();
+            Event::handle('EndShowPageNotice', array($this));
+        }
         $this->elementEnd('dd');
         $this->elementEnd('dl');
     }
@@ -629,7 +638,7 @@ class Action extends HTMLOutputter // lawsuit
         $this->elementStart('div', array('id' => 'aside_primary',
                                          'class' => 'aside'));
         if (Event::handle('StartShowExportData', array($this))) {
-        $this->showExportData();
+            $this->showExportData();
             Event::handle('EndShowExportData', array($this));
         }
         if (Event::handle('StartShowSections', array($this))) {
diff --git a/lib/attachmentlist.php b/lib/attachmentlist.php
new file mode 100644 (file)
index 0000000..9485fe3
--- /dev/null
@@ -0,0 +1,300 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * widget for displaying a list of notice attachments
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  UI
+ * @package   Laconica
+ * @author    Evan Prodromou <evan@controlyourself.ca>
+ * @author    Sarven Capadisli <csarven@controlyourself.ca>
+ * @copyright 2008 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+/**
+ * widget for displaying a list of notice attachments
+ *
+ * There are a number of actions that display a list of notices, in
+ * reverse chronological order. This widget abstracts out most of the
+ * code for UI for notice lists. It's overridden to hide some
+ * data for e.g. the profile page.
+ *
+ * @category UI
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ * @see      Notice
+ * @see      StreamAction
+ * @see      NoticeListItem
+ * @see      ProfileNoticeList
+ */
+
+class AttachmentList extends Widget
+{
+    /** the current stream of notices being displayed. */
+
+    var $notice = null;
+
+    /**
+     * constructor
+     *
+     * @param Notice $notice stream of notices from DB_DataObject
+     */
+
+    function __construct($notice, $out=null)
+    {
+        parent::__construct($out);
+        $this->notice = $notice;
+    }
+
+    /**
+     * show the list of notices
+     *
+     * "Uses up" the stream by looping through it. So, probably can't
+     * be called twice on the same list.
+     *
+     * @return int count of notices listed.
+     */
+
+    function show()
+    {
+//        $this->out->elementStart('div', array('id' =>'attachments_primary'));
+        $this->out->elementStart('div', array('id' =>'content'));
+        $this->out->element('h2', null, _('Attachments'));
+        $this->out->elementStart('ul', array('class' => 'attachments'));
+
+        $atts = new File;
+        $att = $atts->getAttachments($this->notice->id);
+        foreach ($att as $n=>$attachment) {
+            $item = $this->newListItem($attachment);
+            $item->show();
+        }
+
+        $this->out->elementEnd('ul');
+        $this->out->elementEnd('div');
+
+        return count($att);
+    }
+
+    /**
+     * returns a new list item for the current notice
+     *
+     * Recipe (factory?) method; overridden by sub-classes to give
+     * a different list item class.
+     *
+     * @param Notice $notice the current notice
+     *
+     * @return NoticeListItem a list item for displaying the notice
+     */
+
+    function newListItem($attachment)
+    {
+        return new AttachmentListItem($attachment, $this->out);
+    }
+}
+
+/**
+ * widget for displaying a single notice
+ *
+ * This widget has the core smarts for showing a single notice: what to display,
+ * where, and under which circumstances. Its key method is show(); this is a recipe
+ * that calls all the other show*() methods to build up a single notice. The
+ * ProfileNoticeListItem subclass, for example, overrides showAuthor() to skip
+ * author info (since that's implicit by the data in the page).
+ *
+ * @category UI
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ * @see      NoticeList
+ * @see      ProfileNoticeListItem
+ */
+
+class AttachmentListItem extends Widget
+{
+    /** The attachment this item will show. */
+
+    var $attachment = null;
+
+    var $oembed = null;
+
+    /**
+     * constructor
+     *
+     * Also initializes the profile attribute.
+     *
+     * @param Notice $notice The notice we'll display
+     */
+
+    function __construct($attachment, $out=null)
+    {
+        parent::__construct($out);
+        $this->attachment  = $attachment;
+        $this->oembed = File_oembed::staticGet('file_id', $this->attachment->id);
+    }
+
+    function title() {
+        if (empty($this->attachment->title)) {
+            if (empty($this->oembed->title)) {
+                $title = $this->attachment->url;
+            } else {
+                $title = $this->oembed->title;
+            }
+        } else {
+            $title = $this->attachment->title;
+        }
+
+        return $title;
+    }
+
+    function linkTitle() {
+        return 'Our page for ' . $this->title();
+    }
+
+    /**
+     * recipe function for displaying a single notice.
+     *
+     * This uses all the other methods to correctly display a notice. Override
+     * it or one of the others to fine-tune the output.
+     *
+     * @return void
+     */
+
+    function show()
+    {
+        $this->showStart();
+        $this->showNoticeAttachment();
+        $this->showEnd();
+    }
+
+    function linkAttr() {
+        return array('class' => 'attachment', 'href' => common_local_url('attachment', array('attachment' => $this->attachment->id)));
+    }
+
+    function showLink() {
+        $attr = $this->linkAttr();
+        $text = $this->linkTitle();
+        $this->out->elementStart('h4');
+        $this->out->element('a', $attr, $text);
+
+        if ($this->attachment->url !== $this->title())
+            $this->out->element('span', null, " ({$this->attachment->url})");
+
+
+        $this->out->elementEnd('h4');
+    }
+
+    function showNoticeAttachment()
+    {
+        $this->showLink();
+        $this->showRepresentation();
+    }
+
+    function showRepresentation() {
+        $thumbnail = File_thumbnail::staticGet('file_id', $this->attachment->id);
+        if (!empty($thumbnail)) {
+            $this->out->elementStart('a', $this->linkAttr()/*'href' => $this->linkTo()*/);
+            $this->out->element('img', array('alt' => 'nothing to say', 'src' => $thumbnail->url, 'width' => $thumbnail->width, 'height' => $thumbnail->height));
+            $this->out->elementEnd('a');
+        }
+    }
+
+    /**
+     * start a single notice.
+     *
+     * @return void
+     */
+
+    function showStart()
+    {
+        // XXX: RDFa
+        // TODO: add notice_type class e.g., notice_video, notice_image
+        $this->out->elementStart('li');
+    }
+
+    /**
+     * finish the notice
+     *
+     * Close the last elements in the notice list item
+     *
+     * @return void
+     */
+
+    function showEnd()
+    {
+        $this->out->elementEnd('li');
+    }
+}
+
+class Attachment extends AttachmentListItem
+{
+    function show() {
+        $this->showNoticeAttachment();
+    }
+
+    function linkAttr() {
+        return array('class' => 'external', 'href' => $this->attachment->url);
+    }
+
+    function linkTitle() {
+        return 'Direct link to ' . $this->title();
+    }
+
+    function showRepresentation() {
+        if (empty($this->oembed->type)) {
+            if (empty($this->attachment->mimetype)) {
+                $this->out->element('pre', null, 'oh well... not sure how to handle the following: ' . print_r($this->attachment, true));
+            } else {
+                switch ($this->attachment->mimetype) {
+                case 'image/gif':
+                case 'image/png':
+                case 'image/jpg':
+                case 'image/jpeg':
+                    $this->out->element('img', array('src' => $this->attachment->url, 'alt' => 'alt'));
+                    break;
+                }
+            }
+        } else {
+            switch ($this->oembed->type) {
+            case 'rich':
+            case 'video':
+            case 'link':
+                if (!empty($this->oembed->html)) {
+                    $this->out->raw($this->oembed->html);
+                }
+                break;
+
+            case 'photo':
+                $this->out->element('img', array('src' => $this->oembed->url, 'width' => $this->oembed->width, 'height' => $this->oembed->height, 'alt' => 'alt'));
+                break;
+
+            default:
+                $this->out->element('pre', null, 'oh well... not sure how to handle the following oembed: ' . print_r($this->oembed, true));
+            }
+        }
+    }
+}
+
diff --git a/lib/attachmentnoticesection.php b/lib/attachmentnoticesection.php
new file mode 100644 (file)
index 0000000..eb31763
--- /dev/null
@@ -0,0 +1,75 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * FIXME
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Widget
+ * @package   Laconica
+ * @author    Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+/**
+ * FIXME
+ *
+ * These are the widgets that show interesting data about a person * group, or site.
+ *
+ * @category Widget
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ */
+
+class AttachmentNoticeSection extends NoticeSection
+{
+    function showContent() {
+        parent::showContent();
+        return false;
+    }
+
+    function getNotices()
+    {
+        $notice = new Notice;
+        $f2p = new File_to_post;
+        $f2p->file_id = $this->out->attachment->id;
+        $notice->joinAdd($f2p);
+        $notice->orderBy('created desc');
+        $notice->selectAdd('post_id as id');
+        $notice->find();
+        return $notice; 
+    }
+
+    function title()
+    {
+        return _('Notices where this attachment appears');
+    }
+
+    function divId()
+    {
+        return 'popular_notices';
+    }
+}
+
diff --git a/lib/attachmentsection.php b/lib/attachmentsection.php
new file mode 100644 (file)
index 0000000..20e620b
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Base class for sections showing lists of attachments
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Widget
+ * @package   Laconica
+ * @author    Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+define('ATTACHMENTS_PER_SECTION', 6);
+
+/**
+ * Base class for sections showing lists of attachments
+ *
+ * These are the widgets that show interesting data about a person
+ * group, or site.
+ *
+ * @category Widget
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ */
+
+class AttachmentSection extends Section
+{
+    function showContent()
+    {
+        $attachments = $this->getAttachments();
+
+        $cnt = 0;
+
+        $this->out->elementStart('ul', 'attachments');
+
+        while ($attachments->fetch() && ++$cnt <= ATTACHMENTS_PER_SECTION) {
+            $this->showAttachment($attachments);
+        }
+
+        $this->out->elementEnd('ul');
+
+        return ($cnt > ATTACHMENTS_PER_SECTION);
+    }
+
+    function getAttachments()
+    {
+        return null;
+    }
+
+    function showAttachment($attachment)
+    {
+        $this->out->elementStart('li');
+        $this->out->element('a', array('class' => 'attachment', 'href' => common_local_url('attachment', array('attachment' => $attachment->file_id))), "Attachment tagged {$attachment->c} times");
+        $this->out->elementEnd('li');
+    }
+}
+
diff --git a/lib/attachmenttagcloudsection.php b/lib/attachmenttagcloudsection.php
new file mode 100644 (file)
index 0000000..50bfcec
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Attachment tag cloud section
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Widget
+ * @package   Laconica
+ * @author    Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+/**
+ * Attachment tag cloud section
+ *
+ * @category Widget
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ */
+
+class AttachmentTagCloudSection extends TagCloudSection
+{
+    function title()
+    {
+        return _('Tags for this attachment');
+    }
+
+    function showTag($tag, $weight, $relative)
+    {
+        if ($relative > 0.5) {
+            $rel =  'tag-cloud-7';
+        } else if ($relative > 0.4) {
+            $rel = 'tag-cloud-6';
+        } else if ($relative > 0.3) {
+            $rel = 'tag-cloud-5';
+        } else if ($relative > 0.2) {
+            $rel = 'tag-cloud-4';
+        } else if ($relative > 0.1) {
+            $rel = 'tag-cloud-3';
+        } else if ($relative > 0.05) {
+            $rel = 'tag-cloud-2';
+        } else {
+            $rel = 'tag-cloud-1';
+        }
+
+        $this->out->elementStart('li', $rel);
+        $this->out->element('a', array('href' => $this->tagUrl($tag)),
+                       $tag);
+        $this->out->elementEnd('li');
+    }
+
+    function getTags()
+    {
+        $notice_tag = new Notice_tag;
+        $query = 'select tag,count(tag) as weight from notice_tag join file_to_post on (notice_tag.notice_id=post_id) join notice on notice_id = notice.id where file_id=' . $notice_tag->escape($this->out->attachment->id) . ' group by tag order by weight desc';
+        $notice_tag->query($query);
+        return $notice_tag;
+    }
+}
+
index e64ca34da83fe5617b750a8c1298c2a2577dc11d..0ce46442deb99e3d928aed6bfa16c96b1a488401 100644 (file)
@@ -143,6 +143,8 @@ $config =
         array('piddir' => '/var/run',
               'user' => false,
               'group' => false),
+        'twitterbridge' =>
+        array('enabled' => false),
         'integration' =>
         array('source' => 'Laconica', # source attribute for Twitter
               'taguri' => $_server.',2009'), # base for tag URIs
@@ -197,7 +199,7 @@ $_config_files[] = INSTALLDIR.'/config.php';
 $_have_a_config = false;
 
 foreach ($_config_files as $_config_file) {
-    if (file_exists($_config_file)) {
+    if (@file_exists($_config_file)) {
         include_once($_config_file);
         $_have_a_config = true;
     }
index ec39872732fa45ae22ed95c88f4f3b34633ee28f..242d2e06f280e1bd0bbe6587d2ece54b7ab1dd04 100644 (file)
@@ -27,9 +27,21 @@ define("FACEBOOK_PROMPTED_UPDATE_PREF", 2);
 
 function getFacebook()
 {
+    static $facebook = null;
+
     $apikey = common_config('facebook', 'apikey');
     $secret = common_config('facebook', 'secret');
-    return new Facebook($apikey, $secret);
+
+    if ($facebook === null) {
+        $facebook = new Facebook($apikey, $secret);
+    }
+
+    if (!$facebook) {
+        common_log(LOG_ERR, 'Could not make new Facebook client obj!',
+            __FILE__);
+    }
+
+    return $facebook;
 }
 
 function updateProfileBox($facebook, $flink, $notice) {
@@ -92,7 +104,6 @@ function isFacebookBound($notice, $flink) {
 
 }
 
-
 function facebookBroadcastNotice($notice)
 {
     $facebook = getFacebook();
diff --git a/lib/frequentattachmentsection.php b/lib/frequentattachmentsection.php
new file mode 100644 (file)
index 0000000..0ce0d18
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * FIXME
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Widget
+ * @package   Laconica
+ * @author    Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+/**
+ * FIXME
+ *
+ * These are the widgets that show interesting data about a person
+ * group, or site.
+ *
+ * @category Widget
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ */
+
+class FrequentAttachmentSection extends AttachmentSection
+{
+    function getAttachments() {
+        $notice_tag = new Notice_tag;
+        $query = 'select file_id, count(file_id) as c from notice_tag join file_to_post on post_id = notice_id where tag="' . $notice_tag->escape($this->out->tag) . '" group by file_id order by c desc';
+        $notice_tag->query($query);
+        return $notice_tag;
+    }
+
+    function title()
+    {
+        return sprintf(_('Attachments frequently tagged with %s'), $this->out->tag);
+    }
+
+    function divId()
+    {
+        return 'frequent_attachments';
+    }
+}
+
index 0484918ce22d59e35d0a4af3e56f47a1d6778ad2..8fa11a7562752f217edb65bb206f797b5732c40a 100644 (file)
@@ -134,9 +134,11 @@ class GalleryAction extends Action
             $this->elementStart('li', array('id'=>'filter_tags_item'));
             $this->elementStart('form', array('name' => 'bytag',
                                                'id' => 'bytag',
+                                               'action' => common_path('?action=' . $this->trimmed('action')),
                                                'method' => 'post'));
             $this->dropdown('tag', _('Tag'), $content,
                             _('Choose a tag to narrow list'), false, $tag);
+            $this->hidden('nickname', $this->user->nickname);
             $this->submit('submit', _('Go'));
             $this->elementEnd('form');
             $this->elementEnd('li');
@@ -169,4 +171,4 @@ class GalleryAction extends Action
     {
         return array();
     }
-}
\ No newline at end of file
+}
index 8fccba73e1c93788926f671b9c2193d0d7a5dcf0..a521321719b1bc6a9287838419d31d8cbd7af724 100644 (file)
@@ -179,6 +179,7 @@ class NoticeListItem extends Widget
     {
         $this->showStart();
         $this->showNotice();
+        $this->showNoticeAttachments();
         $this->showNoticeInfo();
         $this->showNoticeOptions();
         $this->showEnd();
@@ -192,6 +193,48 @@ class NoticeListItem extends Widget
         $this->out->elementEnd('div');
     }
 
+    function showNoticeAttachments()
+    {
+        $f2p = new File_to_post;
+        $f2p->post_id = $this->notice->id;
+        $file = new File;
+        $file->joinAdd($f2p);
+        $file->selectAdd();
+        $file->selectAdd('file.id as id');
+        $count = $file->find(true);
+        if (!$count) return;
+        if (1 === $count) {
+            $href = common_local_url('attachment', array('attachment' => $file->id));
+            $att_class = 'attachment';
+        } else {
+            $href = common_local_url('attachments', array('notice' => $this->notice->id));
+            $att_class = 'attachments';
+        }
+
+        $clip = theme_path('images/icons/clip', 'base');
+        if ('shownotice' === $this->out->args['action']) {
+            $height = '96px';
+            $width = '83%';
+            $width_att = '15%';
+            $clip .= '-big.png';
+            $top = '70px';
+        } else {
+            $height = '48px';
+            $width = '90%';
+            $width_att = '8%';
+            $clip .= '.png';
+            $top = '20px';
+        }
+if(0)
+        $this->out->elementStart('div', 'entry-attachments');
+else
+        $this->out->elementStart('p', array('class' => 'entry-attachments', 'style' => "float: right; width: $width_att; background: url($clip) no-repeat; text-align: right; height: $height;"));
+        $this->out->element('a', array('class' => $att_class, 'style' => "text-decoration: none; padding-top: $top; display: block; height: $height;", 'href' => $href, 'title' => "# of attachments: $count"), $count === 1 ? '' : $count);
+
+
+        $this->out->elementEnd('p');
+    }
+
     function showNoticeInfo()
     {
         $this->out->elementStart('div', 'entry-content');
index 94c2738efdabc3c5b1c60e5da6b3a774c8cd8c63..37aafdaf6cb2c4753d522d0d94f1dea7edb2d08f 100644 (file)
@@ -51,17 +51,13 @@ class NoticeSection extends Section
     function showContent()
     {
         $notices = $this->getNotices();
-
         $cnt = 0;
-
         $this->out->elementStart('ul', 'notices');
-
         while ($notices->fetch() && ++$cnt <= NOTICES_PER_SECTION) {
             $this->showNotice($notices);
         }
 
         $this->out->elementEnd('ul');
-
         return ($cnt > NOTICES_PER_SECTION);
     }
 
@@ -100,6 +96,37 @@ class NoticeSection extends Section
 
         $this->out->elementStart('p', 'entry-content');
         $this->out->raw($notice->rendered);
+
+        $notice_link_cfg = common_config('site', 'notice_link');
+        if ('direct' === $notice_link_cfg) {
+            $this->out->text(' (');
+            $this->out->element('a', array('href' => $notice->uri), 'see');
+            $this->out->text(')');
+        } elseif ('attachment' === $notice_link_cfg) {
+            if ($count = $notice->hasAttachments()) {
+            // link to attachment(s) pages
+                if (1 === $count) {
+                    $f2p = File_to_post::staticGet('post_id', $notice->id);
+                    $href = common_local_url('attachment', array('attachment' => $f2p->file_id));
+                    $att_class = 'attachment';
+                } else {
+                    $href = common_local_url('attachments', array('notice' => $notice->id));
+                    $att_class = 'attachments';
+                }
+
+                $clip = theme_path('images/icons/clip.png', 'base');
+                $this->out->elementStart('a', array('class' => $att_class, 'style' => "font-style: italic;", 'href' => $href, 'title' => "# of attachments: $count"));
+                $this->out->raw(" ($count&nbsp");
+                $this->out->element('img', array('style' => 'display: inline', 'align' => 'top', 'width' => 20, 'height' => 20, 'src' => $clip, 'alt' => 'alt'));
+                $this->out->text(')');
+                $this->out->elementEnd('a');
+            } else {
+                $this->out->text(' (');
+                $this->out->element('a', array('href' => $notice->uri), 'see');
+                $this->out->text(')');
+            }
+        }
+
         $this->out->elementEnd('p');
         if (!empty($notice->value)) {
             $this->out->elementStart('p');
index 63e6138df2dee7b182d0a38e40f128e945afe461..acc0336673f5071e4c9fea95f3f51b4cf22dc03f 100644 (file)
@@ -93,43 +93,45 @@ class PersonalGroupNav extends Widget
 
         $this->out->elementStart('ul', array('class' => 'nav'));
 
-        $this->out->menuItem(common_local_url('all', array('nickname' =>
-                                                       $nickname)),
-                         _('Personal'),
-                         sprintf(_('%s and friends'), (($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname)),
-                         $action == 'all', 'nav_timeline_personal');
-        $this->out->menuItem(common_local_url('replies', array('nickname' =>
-                                                              $nickname)),
-                         _('Replies'),
-                         sprintf(_('Replies to %s'), (($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname)),
-                         $action == 'replies', 'nav_timeline_replies');
-        $this->out->menuItem(common_local_url('showstream', array('nickname' =>
-                                                              $nickname)),
-                         _('Profile'),
-                         ($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname,
-                         $action == 'showstream', 'nav_profile');
-        $this->out->menuItem(common_local_url('showfavorites', array('nickname' =>
-                                                              $nickname)),
-                         _('Favorites'),
-                         sprintf(_('%s\'s favorite notices'), ($user_profile) ? $user_profile->getBestName() : _('User')),
-                         $action == 'showfavorites', 'nav_timeline_favorites');
+        if (Event::handle('StartPersonalGroupNav', array($this))) {
+            $this->out->menuItem(common_local_url('all', array('nickname' =>
+                                                           $nickname)),
+                             _('Personal'),
+                             sprintf(_('%s and friends'), (($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname)),
+                             $action == 'all', 'nav_timeline_personal');
+            $this->out->menuItem(common_local_url('replies', array('nickname' =>
+                                                                  $nickname)),
+                             _('Replies'),
+                             sprintf(_('Replies to %s'), (($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname)),
+                             $action == 'replies', 'nav_timeline_replies');
+            $this->out->menuItem(common_local_url('showstream', array('nickname' =>
+                                                                  $nickname)),
+                             _('Profile'),
+                             ($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname,
+                             $action == 'showstream', 'nav_profile');
+            $this->out->menuItem(common_local_url('showfavorites', array('nickname' =>
+                                                                  $nickname)),
+                             _('Favorites'),
+                             sprintf(_('%s\'s favorite notices'), ($user_profile) ? $user_profile->getBestName() : _('User')),
+                             $action == 'showfavorites', 'nav_timeline_favorites');
 
-        $cur = common_current_user();
+            $cur = common_current_user();
 
-        if ($cur && $cur->id == $user->id) {
+            if ($cur && $cur->id == $user->id) {
 
-            $this->out->menuItem(common_local_url('inbox', array('nickname' =>
-                                                                     $nickname)),
-                             _('Inbox'),
-                             _('Your incoming messages'),
-                             $action == 'inbox');
-            $this->out->menuItem(common_local_url('outbox', array('nickname' =>
-                                                                     $nickname)),
-                             _('Outbox'),
-                             _('Your sent messages'),
-                             $action == 'outbox');
+                $this->out->menuItem(common_local_url('inbox', array('nickname' =>
+                                                                         $nickname)),
+                                 _('Inbox'),
+                                 _('Your incoming messages'),
+                                 $action == 'inbox');
+                $this->out->menuItem(common_local_url('outbox', array('nickname' =>
+                                                                         $nickname)),
+                                 _('Outbox'),
+                                 _('Your sent messages'),
+                                 $action == 'outbox');
+            }
+            Event::handle('EndPersonalGroupNav', array($this));
         }
-
         $this->out->elementEnd('ul');
     }
 }
index a8d47ef542b5a0ea9e2c7c344de0f928c4af9035..375d5538be72ebfa2aa02922a3164f81101f29f8 100644 (file)
@@ -51,7 +51,7 @@ class PopularNoticeSection extends NoticeSection
         if (common_config('db', 'type') == 'pgsql') {
             $weightexpr='sum(exp(-extract(epoch from (now() - fave.modified)) / %s))';
             if (!empty($this->out->tag)) {
-                $tag = pg_escape_string($this->tag);
+                $tag = pg_escape_string($this->out->tag);
             }
         } else {
             $weightexpr='sum(exp(-(now() - fave.modified) / %s))';
index 1f2e309945c2ca3af30ba054c277b95445c7a25c..a3437ff4ddbec567d630c3f25e4985aa8b3f73a8 100644 (file)
@@ -49,16 +49,17 @@ require_once INSTALLDIR.'/lib/groupminilist.php';
 
 class ProfileAction extends Action
 {
-    var $user = null;
-    var $page = null;
+    var $user    = null;
+    var $page    = null;
     var $profile = null;
+    var $tag     = null;
 
     function prepare($args)
     {
         parent::prepare($args);
 
         $nickname_arg = $this->arg('nickname');
-        $nickname = common_canonical_nickname($nickname_arg);
+        $nickname     = common_canonical_nickname($nickname_arg);
 
         // Permanent redirect on non-canonical nickname
 
@@ -85,10 +86,9 @@ class ProfileAction extends Action
             return false;
         }
 
+        $this->tag = $this->trimmed('tag');
         $this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1;
-
         common_set_returnto($this->selfUrl());
-
         return true;
     }
 
@@ -244,4 +244,5 @@ class ProfileAction extends Action
 
         $this->elementEnd('div');
     }
-}
\ No newline at end of file
+}
+
index 5e16f34191d6466671acca84d2b9df6a1de9f3f9..70ee0f3fb05d6302b66994f863ccacde82ee096c 100644 (file)
@@ -151,12 +151,26 @@ class Router
         $m->connect('search/notice/rss?q=:q', array('action' => 'noticesearchrss'),
                     array('q' => '.+'));
 
+        $m->connect('attachment/:attachment/ajax',
+                    array('action' => 'attachment_ajax'),
+                    array('notice' => '[0-9]+'));
+
+        $m->connect('attachment/:attachment',
+                    array('action' => 'attachment'),
+                    array('notice' => '[0-9]+'));
+
         // notice
 
         $m->connect('notice/new', array('action' => 'newnotice'));
         $m->connect('notice/new?replyto=:replyto',
                     array('action' => 'newnotice'),
                     array('replyto' => '[A-Za-z0-9_-]+'));
+        $m->connect('notice/:notice/attachments/ajax',
+                    array('action' => 'attachments_ajax'),
+                    array('notice' => '[0-9]+'));
+        $m->connect('notice/:notice/attachments',
+                    array('action' => 'attachments'),
+                    array('notice' => '[0-9]+'));
         $m->connect('notice/:notice',
                     array('action' => 'shownotice'),
                     array('notice' => '[0-9]+'));
@@ -237,12 +251,12 @@ class Router
         $m->connect('api/statuses/:method',
                     array('action' => 'api',
                           'apiaction' => 'statuses'),
-                    array('method' => '(public_timeline|friends_timeline|user_timeline|update|replies|friends|followers|featured)(\.(atom|rss|xml|json))?'));
+                    array('method' => '(public_timeline|friends_timeline|user_timeline|update|replies|mentions|friends|followers|featured)(\.(atom|rss|xml|json))?'));
 
         $m->connect('api/statuses/:method/:argument',
                     array('action' => 'api',
                           'apiaction' => 'statuses'),
-                    array('method' => '(user_timeline|friends_timeline|replies|show|destroy|friends|followers)'));
+                    array('method' => '(user_timeline|friends_timeline|replies|mentions|show|destroy|friends|followers)'));
 
         // users
 
@@ -412,6 +426,16 @@ class Router
                     array('size' => '(original|96|48|24)',
                           'nickname' => '[a-zA-Z0-9]{1,64}'));
 
+        $m->connect(':nickname/tag/:tag/rss',
+            array('action' => 'userrss'),
+            array('nickname' => '[a-zA-Z0-9]{1,64}'),
+            array('tag' => '[a-zA-Z0-9]+'));
+
+        $m->connect(':nickname/tag/:tag',
+                    array('action' => 'showstream'),
+                    array('nickname' => '[a-zA-Z0-9]{1,64}'),
+                    array('tag' => '[a-zA-Z0-9]+'));
+
         $m->connect(':nickname',
                     array('action' => 'showstream'),
                     array('nickname' => '[a-zA-Z0-9]{1,64}'));
index ddba862dcf9b22113d74787215f3a15f094e6b5a..2f25ed7e47d14150094efaa57b5322d1c56b862b 100644 (file)
@@ -97,7 +97,11 @@ class Rss10Action extends Action
         // Parent handling, including cache check
         parent::handle($args);
         // Get the list of notices
-        $this->notices = $this->getNotices($this->limit);
+        if (empty($this->tag)) {
+            $this->notices = $this->getNotices($this->limit);
+        } else {
+            $this->notices = $this->getTaggedNotices($this->tag, $this->limit);
+        }
         $this->showRss();
     }
 
index 31c3ea0b568e8ed62b1e0524cec920ba621ef958..4a9b36ae8fa1a198c7d2cb7d2be8f8732c4b62cb 100644 (file)
@@ -74,38 +74,44 @@ class SubGroupNav extends Widget
 
         $this->out->elementStart('ul', array('class' => 'nav'));
 
-        $this->out->menuItem(common_local_url('subscriptions',
-                                              array('nickname' =>
-                                                    $this->user->nickname)),
-                             _('Subscriptions'),
-                             sprintf(_('People %s subscribes to'),
-                                     $this->user->nickname),
-                             $action == 'subscriptions',
-                             'nav_subscriptions');
-        $this->out->menuItem(common_local_url('subscribers',
-                                              array('nickname' =>
-                                                    $this->user->nickname)),
-                             _('Subscribers'),
-                             sprintf(_('People subscribed to %s'),
-                                     $this->user->nickname),
-                             $action == 'subscribers',
-                             'nav_subscribers');
-        $this->out->menuItem(common_local_url('usergroups',
-                                              array('nickname' =>
-                                                    $this->user->nickname)),
-                             _('Groups'),
-                             sprintf(_('Groups %s is a member of'),
-                                     $this->user->nickname),
-                             $action == 'usergroups',
-                             'nav_usergroups');
-        if (!is_null($cur) && $this->user->id === $cur->id) {
-            $this->out->menuItem(common_local_url('invite'),
-                                 _('Invite'),
-                                 sprintf(_('Invite friends and colleagues to join you on %s'),
-                                         common_config('site', 'name')),
-                                 $action == 'invite',
-                                 'nav_invite');
+        if (Event::handle('StartSubGroupNav', array($this))) {
+
+            $this->out->menuItem(common_local_url('subscriptions',
+                                                  array('nickname' =>
+                                                        $this->user->nickname)),
+                                 _('Subscriptions'),
+                                 sprintf(_('People %s subscribes to'),
+                                         $this->user->nickname),
+                                 $action == 'subscriptions',
+                                 'nav_subscriptions');
+            $this->out->menuItem(common_local_url('subscribers',
+                                                  array('nickname' =>
+                                                        $this->user->nickname)),
+                                 _('Subscribers'),
+                                 sprintf(_('People subscribed to %s'),
+                                         $this->user->nickname),
+                                 $action == 'subscribers',
+                                 'nav_subscribers');
+            $this->out->menuItem(common_local_url('usergroups',
+                                                  array('nickname' =>
+                                                        $this->user->nickname)),
+                                 _('Groups'),
+                                 sprintf(_('Groups %s is a member of'),
+                                         $this->user->nickname),
+                                 $action == 'usergroups',
+                                 'nav_usergroups');
+            if (!is_null($cur) && $this->user->id === $cur->id) {
+                $this->out->menuItem(common_local_url('invite'),
+                                     _('Invite'),
+                                     sprintf(_('Invite friends and colleagues to join you on %s'),
+                                             common_config('site', 'name')),
+                                     $action == 'invite',
+                                     'nav_invite');
+            }
+
+            Event::handle('EndSubGroupNav', array($this));
         }
+
         $this->out->elementEnd('ul');
     }
 }
diff --git a/lib/subpeopletagcloudsection.php b/lib/subpeopletagcloudsection.php
new file mode 100644 (file)
index 0000000..9f6948d
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Personal tag cloud section
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Widget
+ * @package   Laconica
+ * @author    Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+/**
+ * Personal tag cloud section
+ *
+ * @category Widget
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ */
+
+class SubPeopleTagCloudSection extends TagCloudSection
+{
+    function getTags()
+    {
+        $qry = $this->query();
+        $limit = TAGS_PER_SECTION;
+        $offset = 0;
+
+        if (common_config('db','type') == 'pgsql') {
+            $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset;
+        } else {
+            $qry .= ' LIMIT ' . $offset . ', ' . $limit;
+        }
+
+        $profile_tag = Memcached_DataObject::cachedQuery('Profile_tag',
+                                                 sprintf($qry,
+                                                         $this->out->user->id));
+        return $profile_tag;
+    }
+
+    function tagUrl($tag) {
+        return common_local_url('peopletag', array('tag' => $tag));
+    }
+
+    function showTag($tag, $weight, $relative) {
+        $rel = 'tag-cloud-';
+        $rel .= 1+intval(7 * $relative * $weight - 0.01);
+
+        $this->out->elementStart('li', $rel);
+        $this->out->element('a', array('href' => $this->tagUrl($tag)), $tag);
+        $this->out->elementEnd('li');
+    }
+}
+
diff --git a/lib/subscriberspeopleselftagcloudsection.php b/lib/subscriberspeopleselftagcloudsection.php
new file mode 100644 (file)
index 0000000..115241a
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Personal tag cloud section
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Widget
+ * @package   Laconica
+ * @author    Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+/**
+ * Personal tag cloud section
+ *
+ * @category Widget
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ */
+
+class SubscribersPeopleSelfTagCloudSection extends SubPeopleTagCloudSection
+{
+    function title()
+    {
+        return _('People Tagcloud as self-tagged');
+    }
+
+    function query() {
+//        return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscriber where subscribed=%d and subscribed != subscriber and tagger = tagged group by tag order by weight desc';
+
+
+        return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscriber where subscribed=%d and subscribed != subscriber and tagger = tagged and tag is not null group by tag order by weight desc';
+
+//        return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscribed where subscriber=%d and subscribed != subscriber and tagger = tagged and tag is not null group by tag order by weight desc';
+
+
+    }
+}
+
diff --git a/lib/subscriberspeopletagcloudsection.php b/lib/subscriberspeopletagcloudsection.php
new file mode 100644 (file)
index 0000000..4dafbc1
--- /dev/null
@@ -0,0 +1,61 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Personal tag cloud section
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Widget
+ * @package   Laconica
+ * @author    Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+/**
+ * Personal tag cloud section
+ *
+ * @category Widget
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ */
+
+class SubscribersPeopleTagCloudSection extends SubPeopleTagCloudSection
+{
+    function title()
+    {
+        return _('People Tagcloud as tagged');
+    }
+
+    function tagUrl($tag) {
+        $nickname = $this->out->profile->nickname;
+        return common_local_url('subscribers', array('nickname' => $nickname, 'tag' => $tag));
+    }
+
+    function query() {
+//        return 'select tag, count(tag) as weight from subscription left join profile_tag on subscriber=tagged and subscribed=tagger where subscribed=%d and subscriber != subscribed group by tag order by weight desc';
+        return 'select tag, count(tag) as weight from subscription left join profile_tag on subscriber=tagged and subscribed=tagger where subscribed=%d and subscriber != subscribed and tag is not null group by tag order by weight desc';
+    }
+}
+
diff --git a/lib/subscriptionspeopleselftagcloudsection.php b/lib/subscriptionspeopleselftagcloudsection.php
new file mode 100644 (file)
index 0000000..3896294
--- /dev/null
@@ -0,0 +1,61 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Personal tag cloud section
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Widget
+ * @package   Laconica
+ * @author    Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+/**
+ * Personal tag cloud section
+ *
+ * @category Widget
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ */
+
+class SubscriptionsPeopleSelfTagCloudSection extends SubPeopleTagCloudSection
+{
+    function title()
+    {
+        return _('People Tagcloud as self-tagged');
+    }
+
+    function query() {
+//        return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscriber where subscribed=%d and subscriber != subscribed and tagger = tagged group by tag order by weight desc';
+
+
+
+        return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscribed where subscriber=%d and subscribed != subscriber and tagger = tagged and tag is not null group by tag order by weight desc';
+
+//        return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscriber where subscribed=%d and subscribed != subscriber and tagger = tagged and tag is not null group by tag order by weight desc';
+    }
+}
+
diff --git a/lib/subscriptionspeopletagcloudsection.php b/lib/subscriptionspeopletagcloudsection.php
new file mode 100644 (file)
index 0000000..f9c8672
--- /dev/null
@@ -0,0 +1,61 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Personal tag cloud section
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Widget
+ * @package   Laconica
+ * @author    Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+/**
+ * Personal tag cloud section
+ *
+ * @category Widget
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ */
+
+class SubscriptionsPeopleTagCloudSection extends SubPeopleTagCloudSection
+{
+    function title()
+    {
+        return _('People Tagcloud as tagged');
+    }
+
+    function tagUrl($tag) {
+        $nickname = $this->out->profile->nickname;
+        return common_local_url('subscriptions', array('nickname' => $nickname, 'tag' => $tag));
+    }
+
+    function query() {
+//        return 'select tag, count(tag) as weight from subscription left join profile_tag on subscriber=tagger and subscribed=tagged where subscriber=%d and subscriber != subscribed group by tag order by weight desc';
+        return 'select tag, count(tag) as weight from subscription left join profile_tag on subscriber=tagger and subscribed=tagged where subscriber=%d and subscriber != subscribed and tag is not null group by tag order by weight desc';
+    }
+}
+
index ff2aca6d6ec06eae34895ac3691fdb240aebcd80..62f7d8961282659f7c12a4b0db2b57840cfc74da 100644 (file)
@@ -114,7 +114,11 @@ class TagCloudSection extends Section
 
     function tagUrl($tag)
     {
-        return common_local_url('tag', array('tag' => $tag));
+        if ('showstream' === $this->out->trimmed('action')) {
+            return common_local_url('showstream', array('nickname' => $this->out->profile->nickname, 'tag' => $tag));
+        } else {
+            return common_local_url('tag', array('tag' => $tag));
+        }
     }
 
     function divId()
index b8357c6889545e9a1f5d191b1401637fd0a09e94..caf8c071639212a4ec8c7eb19dc37509fddd197e 100644 (file)
@@ -51,6 +51,26 @@ class TwitterapiAction extends Action
         parent::handle($args);
     }
 
+    /**
+     * Overrides XMLOutputter::element to write booleans as strings (true|false).
+     * See that method's documentation for more info.
+     * 
+     * @param string $tag     Element type or tagname
+     * @param array  $attrs   Array of element attributes, as
+     *                        key-value pairs
+     * @param string $content string content of the element
+     *
+     * @return void
+     */
+    function element($tag, $attrs=null, $content=null)
+    {
+        if (is_bool($content)) {
+            $content = ($content ? 'true' : 'false');
+        }
+
+        return parent::element($tag, $attrs, $content);
+    }
+    
     function twitter_user_array($profile, $get_notice=false)
     {
 
@@ -66,7 +86,7 @@ class TwitterapiAction extends Action
         $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
 
         $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_STREAM_SIZE);
-        $twitter_user['protected'] = 'false'; # not supported by Laconica yet
+        $twitter_user['protected'] = false; # not supported by Laconica yet
         $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
 
         if ($get_notice) {
@@ -86,7 +106,7 @@ class TwitterapiAction extends Action
 
         $twitter_status = array();
         $twitter_status['text'] = $notice->content;
-        $twitter_status['truncated'] = 'false'; # Not possible on Laconica
+        $twitter_status['truncated'] = false; # Not possible on Laconica
         $twitter_status['created_at'] = $this->date_twitter($notice->created);
         $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
             intval($notice->reply_to) : null;
@@ -108,10 +128,9 @@ class TwitterapiAction extends Action
             ($replier_profile) ? $replier_profile->nickname : null;
 
         if (isset($this->auth_user)) {
-            $twitter_status['favorited'] =
-                ($this->auth_user->hasFave($notice)) ? 'true' : 'false';
+            $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
         } else {
-            $twitter_status['favorited'] = 'false';
+            $twitter_status['favorited'] = false;
         }
 
         if ($include_user) {
@@ -418,7 +437,7 @@ class TwitterapiAction extends Action
     function date_twitter($dt)
     {
         $t = strtotime($dt);
-        return date("D M d G:i:s O Y", $t);
+        return date("D M d H:i:s O Y", $t);
     }
 
     // XXX: Candidate for a general utility method somewhere?
index 88876554824d83b74250acfae3ea82c57ffcae27..fbef8764a783fa60b2df42f76f72fb7a537bc40d 100644 (file)
@@ -395,7 +395,7 @@ function common_render_text($text)
     return $r;
 }
 
-function common_replace_urls_callback($text, $callback) {
+function common_replace_urls_callback($text, $callback, $notice_id = null) {
     // Start off with a regex
     $regex = '#'.
     '(?:'.
@@ -466,7 +466,11 @@ function common_replace_urls_callback($text, $callback) {
         $url = (mb_strpos($orig_url, htmlspecialchars($url)) === FALSE) ? $url:htmlspecialchars($url);
 
         // Call user specified func
-        $modified_url = call_user_func($callback, $url);
+        if (empty($notice_id)) {
+            $modified_url = call_user_func($callback, $url);
+        } else {
+            $modified_url = call_user_func($callback, array($url, $notice_id));
+        }
 
         // Replace it!
         $start = mb_strpos($text, $url, $offset);
@@ -481,102 +485,24 @@ function common_linkify($url) {
     // It comes in special'd, so we unspecial it before passing to the stringifying
     // functions
     $url = htmlspecialchars_decode($url);
-    $display = $url;
-    $url = (!preg_match('#^([a-z]+://|(mailto|aim|tel):)#i', $url)) ? 'http://'.$url : $url;
-
-    $attrs = array('href' => $url, 'rel' => 'external');
-
-    if ($longurl = common_longurl($url)) {
-        $attrs['title'] = $longurl;
+    $display = File_redirection::_canonUrl($url);
+    $longurl_data = File_redirection::where($url);
+    if (is_array($longurl_data)) {
+        $longurl = $longurl_data['url'];
+    } elseif (is_string($longurl_data)) {
+        $longurl = $longurl_data;
+    } else {
+        die('impossible to linkify');
     }
 
+    $attrs = array('href' => $longurl, 'rel' => 'external');
     return XMLStringer::estring('a', $attrs, $display);
 }
 
-function common_longurl($short_url)
-{
-    $long_url = common_shorten_link($short_url, true);
-    if ($long_url === $short_url) return false;
-    return $long_url;
-}
-
-function common_longurl2($uri)
-{
-    $uri_e = urlencode($uri);
-    $longurl = unserialize(file_get_contents("http://api.longurl.org/v1/expand?format=php&url=$uri_e"));
-    if (empty($longurl['long_url']) || $uri === $longurl['long_url']) return false;
-    return stripslashes($longurl['long_url']);
-}
-
 function common_shorten_links($text)
 {
     if (mb_strlen($text) <= 140) return $text;
-    static $cache = array();
-    if (isset($cache[$text])) return $cache[$text];
-    // \s = not a horizontal whitespace character (since PHP 5.2.4)
-    return $cache[$text] = common_replace_urls_callback($text, 'common_shorten_link');;
-}
-
-function common_shorten_link($url, $reverse = false)
-{
-    static $url_cache = array();
-    if ($reverse) return isset($url_cache[$url]) ? $url_cache[$url] : $url;
-
-    $user = common_current_user();
-
-    $curlh = curl_init();
-    curl_setopt($curlh, CURLOPT_CONNECTTIMEOUT, 20); // # seconds to wait
-    curl_setopt($curlh, CURLOPT_USERAGENT, 'Laconica');
-    curl_setopt($curlh, CURLOPT_RETURNTRANSFER, true);
-
-    switch($user->urlshorteningservice) {
-     case 'ur1.ca':
-        $short_url_service = new LilUrl;
-        $short_url = $short_url_service->shorten($url);
-        break;
-
-     case '2tu.us':
-        $short_url_service = new TightUrl;
-        $short_url = $short_url_service->shorten($url);
-        break;
-
-     case 'ptiturl.com':
-        $short_url_service = new PtitUrl;
-        $short_url = $short_url_service->shorten($url);
-        break;
-
-     case 'bit.ly':
-        curl_setopt($curlh, CURLOPT_URL, 'http://bit.ly/api?method=shorten&long_url='.urlencode($url));
-        $short_url = current(json_decode(curl_exec($curlh))->results)->hashUrl;
-        break;
-
-     case 'is.gd':
-        curl_setopt($curlh, CURLOPT_URL, 'http://is.gd/api.php?longurl='.urlencode($url));
-        $short_url = curl_exec($curlh);
-        break;
-     case 'snipr.com':
-        curl_setopt($curlh, CURLOPT_URL, 'http://snipr.com/site/snip?r=simple&link='.urlencode($url));
-        $short_url = curl_exec($curlh);
-        break;
-     case 'metamark.net':
-        curl_setopt($curlh, CURLOPT_URL, 'http://metamark.net/api/rest/simple?long_url='.urlencode($url));
-        $short_url = curl_exec($curlh);
-        break;
-     case 'tinyurl.com':
-        curl_setopt($curlh, CURLOPT_URL, 'http://tinyurl.com/api-create.php?url='.urlencode($url));
-        $short_url = curl_exec($curlh);
-        break;
-     default:
-        $short_url = false;
-    }
-
-    curl_close($curlh);
-
-    if ($short_url) {
-        $url_cache[(string)$short_url] = $url;
-        return (string)$short_url;
-    }
-    return $url;
+    return common_replace_urls_callback($text, array('File_redirection', 'makeShort'));
 }
 
 function common_xml_safe_str($str)
@@ -1019,7 +945,7 @@ function common_root_url($ssl=false)
 function common_good_rand($bytes)
 {
     // XXX: use random.org...?
-    if (file_exists('/dev/urandom')) {
+    if (@file_exists('/dev/urandom')) {
         return common_urandom($bytes);
     } else { // FIXME: this is probably not good enough
         return common_mtrand($bytes);
@@ -1385,7 +1311,7 @@ function common_compatible_license($from, $to)
  */
 function common_database_tablename($tablename)
 {
-  
+
   if(common_config('db','quote_identifiers')) {
       $tablename = '"'. $tablename .'"';
   }
diff --git a/plugins/Comet/CometPlugin.php b/plugins/Comet/CometPlugin.php
new file mode 100644 (file)
index 0000000..45251c6
--- /dev/null
@@ -0,0 +1,205 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Plugin to do "real time" updates using Comet/Bayeux
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Plugin
+ * @package   Laconica
+ * @author    Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+/**
+ * Plugin to do realtime updates using Comet
+ *
+ * @category Plugin
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ */
+
+class CometPlugin extends Plugin
+{
+    var $server = null;
+
+    function __construct($server=null, $username=null, $password=null)
+    {
+        $this->server   = $server;
+        $this->username = $username;
+        $this->password = $password;
+
+        parent::__construct();
+    }
+
+    function onEndShowScripts($action)
+    {
+        $timeline = null;
+
+        $this->log(LOG_DEBUG, 'got action ' . $action->trimmed('action'));
+
+        switch ($action->trimmed('action')) {
+         case 'public':
+            $timeline = '/timelines/public';
+            break;
+         case 'tag':
+            $tag = $action->trimmed('tag');
+            if (!empty($tag)) {
+                $timeline = '/timelines/tag/'.$tag;
+            } else {
+                return true;
+            }
+            break;
+         default:
+            return true;
+        }
+
+        $scripts = array('jquery.comet.js', 'json2.js', 'updatetimeline.js');
+
+        foreach ($scripts as $script) {
+            $action->element('script', array('type' => 'text/javascript',
+                                             'src' => common_path('plugins/Comet/'.$script)),
+                         ' ');
+        }
+
+        $user = common_current_user();
+
+        if (!empty($user->id)) {
+            $user_id = $user->id;
+        } else {
+            $user_id = 0;
+        }
+
+        $replyurl = common_local_url('newnotice');
+        $favorurl = common_local_url('favor');
+        // FIXME: need to find a better way to pass this pattern in
+        $deleteurl = common_local_url('deletenotice',
+                                      array('notice' => '0000000000'));
+
+        $action->elementStart('script', array('type' => 'text/javascript'));
+        $action->raw("$(document).ready(function() { updater.init(\"$this->server\", \"$timeline\", $user_id, \"$replyurl\", \"$favorurl\", \"$deleteurl\"); });");
+        $action->elementEnd('script');
+
+        return true;
+    }
+
+    function onEndNoticeSave($notice)
+    {
+        $this->log(LOG_INFO, "Called for save notice.");
+
+        $timelines = array();
+
+        // XXX: Add other timelines; this is just for the public one
+
+        if ($notice->is_local ||
+            ($notice->is_local == 0 && !common_config('public', 'localonly'))) {
+            $timelines[] = '/timelines/public';
+        }
+
+        $tags = $this->getNoticeTags($notice);
+
+        if (!empty($tags)) {
+            foreach ($tags as $tag) {
+                $timelines[] = '/timelines/tag/' . $tag;
+            }
+        }
+
+        if (count($timelines) > 0) {
+            // Require this, since we need it
+            require_once(INSTALLDIR.'/plugins/Comet/bayeux.class.inc.php');
+
+            $json = $this->noticeAsJson($notice);
+
+            // Bayeux? Comet? Huh? These terms confuse me
+            $bay = new Bayeux($this->server, $this->user, $this->password);
+
+            foreach ($timelines as $timeline) {
+                $this->log(LOG_INFO, "Posting notice $notice->id to '$timeline'.");
+                $bay->publish($timeline, $json);
+            }
+
+            $bay = NULL;
+        }
+
+        return true;
+    }
+
+    function noticeAsJson($notice)
+    {
+        // FIXME: this code should be abstracted to a neutral third
+        // party, like Notice::asJson(). I'm not sure of the ethics
+        // of refactoring from within a plugin, so I'm just abusing
+        // the TwitterApiAction method. Don't do this unless you're me!
+
+        require_once(INSTALLDIR.'/lib/twitterapi.php');
+
+        $act = new TwitterApiAction('/dev/null');
+
+        $arr = $act->twitter_status_array($notice, true);
+        $arr['url'] = $notice->bestUrl();
+        $arr['html'] = htmlspecialchars($notice->rendered);
+        $arr['source'] = htmlspecialchars($arr['source']);
+
+        if (!empty($notice->reply_to)) {
+            $reply_to = Notice::staticGet('id', $notice->reply_to);
+            if (!empty($reply_to)) {
+                $arr['in_reply_to_status_url'] = $reply_to->bestUrl();
+            }
+            $reply_to = null;
+        }
+
+        $profile = $notice->getProfile();
+        $arr['user']['profile_url'] = $profile->profileurl;
+
+        return $arr;
+    }
+
+    function getNoticeTags($notice)
+    {
+        $tags = null;
+
+        $nt = new Notice_tag();
+        $nt->notice_id = $notice->id;
+
+        if ($nt->find()) {
+            $tags = array();
+            while ($nt->fetch()) {
+                $tags[] = $nt->tag;
+            }
+        }
+
+        $nt->free();
+        $nt = null;
+
+        return $tags;
+    }
+
+    // Push this up to Plugin
+
+    function log($level, $msg)
+    {
+        common_log($level, get_class($this) . ': '.$msg);
+    }
+}
diff --git a/plugins/Comet/README b/plugins/Comet/README
new file mode 100644 (file)
index 0000000..4abd40a
--- /dev/null
@@ -0,0 +1,26 @@
+This is a plugin to automatically load notices in the browser no
+matter who creates them -- the kind of thing we see with
+search.twitter.com, rejaw.com, or FriendFeed's "real time" news.
+
+NOTE: this is an insecure version; don't roll it out on a production
+server.
+
+It requires a cometd server. I've only had the cometd-java server work
+correctly; something's wiggy with the Twisted-based server.
+
+After you have a cometd server installed, just add this code to your
+config.php:
+
+    require_once(INSTALLDIR.'/plugins/Comet/CometPlugin.php');
+    $cp = new CometPlugin('http://example.com:8080/cometd/');
+
+Change 'example.com:8080' to the name and port of the server you
+installed cometd on.
+
+TODO:
+
+* Needs to be tested with Ajax submission. Probably messes everything
+  up.
+* Add more timelines: personal inbox and tags would be great.
+* Add security. In particular, only let the PHP code publish notices
+  to the cometd server. Currently, it doesn't try to authenticate.
diff --git a/plugins/Comet/bayeux.class.inc.php b/plugins/Comet/bayeux.class.inc.php
new file mode 100644 (file)
index 0000000..39ad8a8
--- /dev/null
@@ -0,0 +1,134 @@
+<?php
+/*
+ *
+ * Phomet: a php comet client
+ *
+ * Copyright (C) 2008 Morgan 'ARR!' Allen <morganrallen@gmail.com> http://morglog.alleycatracing.com
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ */
+
+class Bayeux
+{
+    private $oCurl = '';
+    private $nNextId = 0;
+
+    private $sUser = '';
+    private $sPassword = '';
+
+    public $sUrl = '';
+
+    function __construct($sUrl, $sUser='', $sPassword='')
+    {
+        $this->sUrl = $sUrl;
+
+        $this->oCurl = curl_init();
+
+        $aHeaders = array();
+        $aHeaders[] = 'Connection: Keep-Alive';
+
+        curl_setopt($this->oCurl, CURLOPT_URL, $sUrl);
+        curl_setopt($this->oCurl, CURLOPT_HTTPHEADER, $aHeaders);
+        curl_setopt($this->oCurl, CURLOPT_HEADER, 0);
+        curl_setopt($this->oCurl, CURLOPT_POST, 1);
+        curl_setopt($this->oCurl, CURLOPT_RETURNTRANSFER,1);
+
+        if (!is_null($sUser) && mb_strlen($sUser) > 0) {
+            curl_setopt($this->oCurl, CURLOPT_USERPWD,"$sUser:$sPassword");
+        }
+
+        $this->handShake();
+    }
+
+    function __destruct()
+    {
+        $this->disconnect();
+    }
+
+    function handShake()
+    {
+        $msgHandshake = array();
+        $msgHandshake['channel'] = '/meta/handshake';
+        $msgHandshake['version'] = "1.0";
+        $msgHandshake['minimumVersion'] = "0.9";
+        $msgHandshake['supportedConnectionTypes'] = array('long-polling');
+        $msgHandshake['id'] = $this->nNextId++;
+
+        curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($msgHandshake)))));
+
+        $data = curl_exec($this->oCurl);
+
+        if(curl_errno($this->oCurl))
+          die("Error: " . curl_error($this->oCurl));
+
+        $oReturn = json_decode($data);
+
+        if (is_array($oReturn)) {
+            $oReturn = $oReturn[0];
+        }
+
+        $bSuccessful = ($oReturn->successful) ? true : false;
+
+        if($bSuccessful)
+        {
+            $this->clientId = $oReturn->clientId;
+
+            $this->connect();
+        }
+    }
+
+    public function connect()
+    {
+        $aMsg['channel'] = '/meta/connect';
+        $aMsg['id'] = $this->nNextId++;
+        $aMsg['clientId'] = $this->clientId;
+        $aMsg['connectionType'] = 'long-polling';
+
+        curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($aMsg)))));
+
+        $data = curl_exec($this->oCurl);
+    }
+
+    function disconnect()
+    {
+        $msgHandshake = array();
+        $msgHandshake['channel'] = '/meta/disconnect';
+        $msgHandshake['id'] = $this->nNextId++;
+        $msgHandshake['clientId'] = $this->clientId;
+
+        curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($msgHandshake)))));
+
+        curl_exec($this->oCurl);
+    }
+
+    public function publish($sChannel, $oData)
+    {
+        if(!$sChannel || !$oData)
+          return;
+
+        $aMsg = array();
+
+        $aMsg['channel'] = $sChannel;
+        $aMsg['id'] = $this->nNextId++;
+        $aMsg['data'] = $oData;
+        $aMsg['clientId'] = $this->clientId;
+
+        curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($aMsg)))));
+
+        $data = curl_exec($this->oCurl);
+//        var_dump($data);
+    }
+}
diff --git a/plugins/Comet/jquery.comet.js b/plugins/Comet/jquery.comet.js
new file mode 100644 (file)
index 0000000..6de437f
--- /dev/null
@@ -0,0 +1,1451 @@
+/**
+ * Copyright 2008 Mort Bay Consulting Pty. Ltd.
+ * Dual licensed under the Apache License 2.0 and the MIT license.
+ * ----------------------------------------------------------------------------
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http: *www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ----------------------------------------------------------------------------
+ * Licensed under the MIT license;
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ * ----------------------------------------------------------------------------
+ * $Revision$ $Date$
+ */
+(function($)
+{
+    /**
+     * The constructor for a Comet object.
+     * There is a default Comet instance already created at the variable <code>$.cometd</code>,
+     * and hence that can be used to start a comet conversation with a server.
+     * In the rare case a page needs more than one comet conversation, a new instance can be
+     * created via:
+     * <pre>
+     * var url2 = ...;
+     * var cometd2 = new $.Cometd();
+     * cometd2.init(url2);
+     * </pre>
+     */
+    $.Cometd = function(name)
+    {
+        var _name = name || 'default';
+        var _logPriorities = { debug: 1, info: 2, warn: 3, error: 4 };
+        var _logLevel = 'info';
+        var _url;
+        var _xd = false;
+        var _transport;
+        var _status = 'disconnected';
+        var _messageId = 0;
+        var _clientId = null;
+        var _batch = 0;
+        var _messageQueue = [];
+        var _listeners = {};
+        var _backoff = 0;
+        var _backoffIncrement = 1000;
+        var _maxBackoff = 60000;
+        var _scheduledSend = null;
+        var _extensions = [];
+        var _advice = {};
+        var _handshakeProps;
+
+        /**
+         * Returns the name assigned to this Comet object, or the string 'default'
+         * if no name has been explicitely passed as parameter to the constructor.
+         */
+        this.getName = function()
+        {
+            return _name;
+        };
+
+        /**
+         * Configures the initial comet communication with the comet server.
+         * @param cometURL the URL of the comet server
+         */
+        this.configure = function(cometURL)
+        {
+            _configure(cometURL);
+        };
+
+        function _configure(cometURL)
+        {
+            _url = cometURL;
+            _debug('Initializing comet with url: {}', _url);
+
+            // Check immediately if we're cross domain
+            // If cross domain, the handshake must not send the long polling transport type
+            var urlParts = /(^https?:)?(\/\/(([^:\/\?#]+)(:(\d+))?))?([^\?#]*)/.exec(cometURL);
+            if (urlParts[3]) _xd = urlParts[3] != location.host;
+
+            // Temporary setup a transport to send the initial handshake
+            // The transport may be changed as a result of handshake
+            if (_xd)
+                _transport = newCallbackPollingTransport();
+            else
+                _transport = newLongPollingTransport();
+            _debug('Initial transport is {}', _transport.getType());
+        };
+
+        /**
+         * Configures and establishes the comet communication with the comet server
+         * via a handshake and a subsequent connect.
+         * @param cometURL the URL of the comet server
+         * @param handshakeProps an object to be merged with the handshake message
+         * @see #configure(cometURL)
+         * @see #handshake(handshakeProps)
+         */
+        this.init = function(cometURL, handshakeProps)
+        {
+            _configure(cometURL);
+            _handshake(handshakeProps);
+        };
+
+        /**
+         * Establishes the comet communication with the comet server
+         * via a handshake and a subsequent connect.
+         * @param handshakeProps an object to be merged with the handshake message
+         */
+        this.handshake = function(handshakeProps)
+        {
+            _handshake(handshakeProps);
+        };
+
+        /**
+         * Disconnects from the comet server.
+         * @param disconnectProps an object to be merged with the disconnect message
+         */
+        this.disconnect = function(disconnectProps)
+        {
+            var bayeuxMessage = {
+                channel: '/meta/disconnect'
+            };
+            var message = $.extend({}, disconnectProps, bayeuxMessage);
+            // Deliver immediately
+            // The handshake and connect mechanism make use of startBatch(), and in case
+            // of a failed handshake the disconnect would not be delivered if using _send().
+            _setStatus('disconnecting');
+            _deliver([message], false);
+        };
+
+        /**
+         * Marks the start of a batch of application messages to be sent to the server
+         * in a single request, obtaining a single response containing (possibly) many
+         * application reply messages.
+         * Messages are held in a queue and not sent until {@link #endBatch()} is called.
+         * If startBatch() is called multiple times, then an equal number of endBatch()
+         * calls must be made to close and send the batch of messages.
+         * @see #endBatch()
+         */
+        this.startBatch = function()
+        {
+            _startBatch();
+        };
+
+        /**
+         * Marks the end of a batch of application messages to be sent to the server
+         * in a single request.
+         * @see #startBatch()
+         */
+        this.endBatch = function()
+        {
+            _endBatch(true);
+        };
+
+        /**
+         * Subscribes to the given channel, performing the given callback in the given scope
+         * when a message for the channel arrives.
+         * @param channel the channel to subscribe to
+         * @param scope the scope of the callback
+         * @param callback the callback to call when a message is delivered to the channel
+         * @param subscribeProps an object to be merged with the subscribe message
+         * @return the subscription handle to be passed to {@link #unsubscribe(object)}
+         */
+        this.subscribe = function(channel, scope, callback, subscribeProps)
+        {
+            var subscription = this.addListener(channel, scope, callback);
+
+            // Send the subscription message after the subscription registration to avoid
+            // races where the server would deliver a message to the subscribers, but here
+            // on the client the subscription has not been added yet to the data structures
+            var bayeuxMessage = {
+                channel: '/meta/subscribe',
+                subscription: channel
+            };
+            var message = $.extend({}, subscribeProps, bayeuxMessage);
+            _send(message);
+
+            return subscription;
+        };
+
+        /**
+         * Unsubscribes the subscription obtained with a call to {@link #subscribe(string, object, function)}.
+         * @param subscription the subscription to unsubscribe.
+         */
+        this.unsubscribe = function(subscription, unsubscribeProps)
+        {
+            // Remove the local listener before sending the message
+            // This ensures that if the server fails, this client does not get notifications
+            this.removeListener(subscription);
+            var bayeuxMessage = {
+                channel: '/meta/unsubscribe',
+                subscription: subscription[0]
+            };
+            var message = $.extend({}, unsubscribeProps, bayeuxMessage);
+            _send(message);
+        };
+
+        /**
+         * Publishes a message on the given channel, containing the given content.
+         * @param channel the channel to publish the message to
+         * @param content the content of the message
+         * @param publishProps an object to be merged with the publish message
+         */
+        this.publish = function(channel, content, publishProps)
+        {
+            var bayeuxMessage = {
+                channel: channel,
+                data: content
+            };
+            var message = $.extend({}, publishProps, bayeuxMessage);
+            _send(message);
+        };
+
+        /**
+         * Adds a listener for bayeux messages, performing the given callback in the given scope
+         * when a message for the given channel arrives.
+         * @param channel the channel the listener is interested to
+         * @param scope the scope of the callback
+         * @param callback the callback to call when a message is delivered to the channel
+         * @returns the subscription handle to be passed to {@link #removeListener(object)}
+         * @see #removeListener(object)
+         */
+        this.addListener = function(channel, scope, callback)
+        {
+            // The data structure is a map<channel, subscription[]>, where each subscription
+            // holds the callback to be called and its scope.
+
+            // Normalize arguments
+            if (!callback)
+            {
+                callback = scope;
+                scope = undefined;
+            }
+
+            var subscription = {
+                scope: scope,
+                callback: callback
+            };
+
+            var subscriptions = _listeners[channel];
+            if (!subscriptions)
+            {
+                subscriptions = [];
+                _listeners[channel] = subscriptions;
+            }
+            // Pushing onto an array appends at the end and returns the id associated with the element increased by 1.
+            // Note that if:
+            // a.push('a'); var hb=a.push('b'); delete a[hb-1]; var hc=a.push('c');
+            // then:
+            // hc==3, a.join()=='a',,'c', a.length==3
+            var subscriptionIndex = subscriptions.push(subscription) - 1;
+            _debug('Added listener: channel \'{}\', callback \'{}\', index {}', channel, callback.name, subscriptionIndex);
+
+            // The subscription to allow removal of the listener is made of the channel and the index
+            return [channel, subscriptionIndex];
+        };
+
+        /**
+         * Removes the subscription obtained with a call to {@link #addListener(string, object, function)}.
+         * @param subscription the subscription to unsubscribe.
+         */
+        this.removeListener = function(subscription)
+        {
+            var subscriptions = _listeners[subscription[0]];
+            if (subscriptions)
+            {
+                delete subscriptions[subscription[1]];
+                _debug('Removed listener: channel \'{}\', index {}', subscription[0], subscription[1]);
+            }
+        };
+
+        /**
+         * Removes all listeners registered with {@link #addListener(channel, scope, callback)} or
+         * {@link #subscribe(channel, scope, callback)}.
+         */
+        this.clearListeners = function()
+        {
+            _listeners = {};
+        };
+
+        /**
+         * Returns a string representing the status of the bayeux communication with the comet server.
+         */
+        this.getStatus = function()
+        {
+            return _status;
+        };
+
+        /**
+         * Sets the backoff period used to increase the backoff time when retrying an unsuccessful or failed message.
+         * Default value is 1 second, which means if there is a persistent failure the retries will happen
+         * after 1 second, then after 2 seconds, then after 3 seconds, etc. So for example with 15 seconds of
+         * elapsed time, there will be 5 retries (at 1, 3, 6, 10 and 15 seconds elapsed).
+         * @param period the backoff period to set
+         * @see #getBackoffIncrement()
+         */
+        this.setBackoffIncrement = function(period)
+        {
+            _backoffIncrement = period;
+        };
+
+        /**
+         * Returns the backoff period used to increase the backoff time when retrying an unsuccessful or failed message.
+         * @see #setBackoffIncrement(period)
+         */
+        this.getBackoffIncrement = function()
+        {
+            return _backoffIncrement;
+        };
+
+        /**
+         * Returns the backoff period to wait before retrying an unsuccessful or failed message.
+         */
+        this.getBackoffPeriod = function()
+        {
+            return _backoff;
+        };
+
+        /**
+         * Sets the log level for console logging.
+         * Valid values are the strings 'error', 'warn', 'info' and 'debug', from
+         * less verbose to more verbose.
+         * @param level the log level string
+         */
+        this.setLogLevel = function(level)
+        {
+            _logLevel = level;
+        };
+
+        /**
+         * Registers an extension whose callbacks are called for every incoming message
+         * (that comes from the server to this client implementation) and for every
+         * outgoing message (that originates from this client implementation for the
+         * server).
+         * The format of the extension object is the following:
+         * <pre>
+         * {
+         *     incoming: function(message) { ... },
+         *     outgoing: function(message) { ... }
+         * }
+         * Both properties are optional, but if they are present they will be called
+         * respectively for each incoming message and for each outgoing message.
+         * </pre>
+         * @param name the name of the extension
+         * @param extension the extension to register
+         * @return true if the extension was registered, false otherwise
+         * @see #unregisterExtension(name)
+         */
+        this.registerExtension = function(name, extension)
+        {
+            var existing = false;
+            for (var i = 0; i < _extensions.length; ++i)
+            {
+                var existingExtension = _extensions[i];
+                if (existingExtension.name == name)
+                {
+                    existing = true;
+                    return false;
+                }
+            }
+            if (!existing)
+            {
+                _extensions.push({
+                    name: name,
+                    extension: extension
+                });
+                _debug('Registered extension \'{}\'', name);
+                return true;
+            }
+            else
+            {
+                _info('Could not register extension with name \'{}\': another extension with the same name already exists');
+                return false;
+            }
+        };
+
+        /**
+         * Unregister an extension previously registered with
+         * {@link #registerExtension(name, extension)}.
+         * @param name the name of the extension to unregister.
+         * @return true if the extension was unregistered, false otherwise
+         */
+        this.unregisterExtension = function(name)
+        {
+            var unregistered = false;
+            $.each(_extensions, function(index, extension)
+            {
+                if (extension.name == name)
+                {
+                    _extensions.splice(index, 1);
+                    unregistered = true;
+                    _debug('Unregistered extension \'{}\'', name);
+                    return false;
+                }
+            });
+            return unregistered;
+        };
+
+        /**
+         * Starts a the batch of messages to be sent in a single request.
+         * @see _endBatch(deliverMessages)
+         */
+        function _startBatch()
+        {
+            ++_batch;
+        };
+
+        /**
+         * Ends the batch of messages to be sent in a single request,
+         * optionally delivering messages present in the message queue depending
+         * on the given argument.
+         * @param deliverMessages whether to deliver the messages in the queue or not
+         * @see _startBatch()
+         */
+        function _endBatch(deliverMessages)
+        {
+            --_batch;
+            if (_batch < 0) _batch = 0;
+            if (deliverMessages && _batch == 0 && !_isDisconnected())
+            {
+                var messages = _messageQueue;
+                _messageQueue = [];
+                if (messages.length > 0) _deliver(messages, false);
+            }
+        };
+
+        function _nextMessageId()
+        {
+            return ++_messageId;
+        };
+
+        /**
+         * Converts the given response into an array of bayeux messages
+         * @param response the response to convert
+         * @return an array of bayeux messages obtained by converting the response
+         */
+        function _convertToMessages(response)
+        {
+            if (response === undefined) return [];
+            if (response instanceof Array) return response;
+            if (response instanceof String || typeof response == 'string') return eval('(' + response + ')');
+            if (response instanceof Object) return [response];
+            throw 'Conversion Error ' + response + ', typeof ' + (typeof response);
+        };
+
+        function _setStatus(newStatus)
+        {
+            _debug('{} -> {}', _status, newStatus);
+            _status = newStatus;
+        };
+
+        function _isDisconnected()
+        {
+            return _status == 'disconnecting' || _status == 'disconnected';
+        };
+
+        /**
+         * Sends the initial handshake message
+         */
+        function _handshake(handshakeProps)
+        {
+            _debug('Starting handshake');
+            _clientId = null;
+
+            // Start a batch.
+            // This is needed because handshake and connect are async.
+            // It may happen that the application calls init() then subscribe()
+            // and the subscribe message is sent before the connect message, if
+            // the subscribe message is not held until the connect message is sent.
+            // So here we start a batch to hold temporarly any message until
+            // the connection is fully established.
+            _batch = 0;
+            _startBatch();
+
+            // Save the original properties provided by the user
+            // Deep copy to avoid the user to be able to change them later
+            _handshakeProps = $.extend(true, {}, handshakeProps);
+
+            var bayeuxMessage = {
+                version: '1.0',
+                minimumVersion: '0.9',
+                channel: '/meta/handshake',
+                supportedConnectionTypes: _xd ? ['callback-polling'] : ['long-polling', 'callback-polling']
+            };
+            // Do not allow the user to mess with the required properties,
+            // so merge first the user properties and *then* the bayeux message
+            var message = $.extend({}, handshakeProps, bayeuxMessage);
+
+            // We started a batch to hold the application messages,
+            // so here we must bypass it and deliver immediately.
+            _setStatus('handshaking');
+            _deliver([message], false);
+        };
+
+        function _findTransport(handshakeResponse)
+        {
+            var transportTypes = handshakeResponse.supportedConnectionTypes;
+            if (_xd)
+            {
+                // If we are cross domain, check if the server supports it, that's the only option
+                if ($.inArray('callback-polling', transportTypes) >= 0) return _transport;
+            }
+            else
+            {
+                // Check if we can keep long-polling
+                if ($.inArray('long-polling', transportTypes) >= 0) return _transport;
+
+                // The server does not support long-polling
+                if ($.inArray('callback-polling', transportTypes) >= 0) return newCallbackPollingTransport();
+            }
+            return null;
+        };
+
+        function _delayedHandshake()
+        {
+            _setStatus('handshaking');
+            _delayedSend(function()
+            {
+                _handshake(_handshakeProps);
+            });
+        };
+
+        function _delayedConnect()
+        {
+            _setStatus('connecting');
+            _delayedSend(function()
+            {
+                _connect();
+            });
+        };
+
+        function _delayedSend(operation)
+        {
+            _cancelDelayedSend();
+            var delay = _backoff;
+            _debug("Delayed send: backoff {}, interval {}", _backoff, _advice.interval);
+            if (_advice.interval && _advice.interval > 0)
+                delay += _advice.interval;
+            _scheduledSend = _setTimeout(operation, delay);
+        };
+
+        function _cancelDelayedSend()
+        {
+            if (_scheduledSend !== null) clearTimeout(_scheduledSend);
+            _scheduledSend = null;
+        };
+
+        function _setTimeout(funktion, delay)
+        {
+            return setTimeout(function()
+            {
+                try
+                {
+                    funktion();
+                }
+                catch (x)
+                {
+                    _debug('Exception during scheduled execution of function \'{}\': {}', funktion.name, x);
+                }
+            }, delay);
+        };
+
+        /**
+         * Sends the connect message
+         */
+        function _connect()
+        {
+            _debug('Starting connect');
+            var message = {
+                channel: '/meta/connect',
+                connectionType: _transport.getType()
+            };
+            _setStatus('connecting');
+            _deliver([message], true);
+            _setStatus('connected');
+        };
+
+        function _send(message)
+        {
+            if (_batch > 0)
+                _messageQueue.push(message);
+            else
+                _deliver([message], false);
+        };
+
+        /**
+         * Delivers the messages to the comet server
+         * @param messages the array of messages to send
+         */
+        function _deliver(messages, comet)
+        {
+            // We must be sure that the messages have a clientId.
+            // This is not guaranteed since the handshake may take time to return
+            // (and hence the clientId is not known yet) and the application
+            // may create other messages.
+            $.each(messages, function(index, message)
+            {
+                message['id'] = _nextMessageId();
+                if (_clientId) message['clientId'] = _clientId;
+                messages[index] = _applyOutgoingExtensions(message);
+            });
+
+            var self = this;
+            var envelope = {
+                url: _url,
+                messages: messages,
+                onSuccess: function(request, response)
+                {
+                    try
+                    {
+                        _handleSuccess.call(self, request, response, comet);
+                    }
+                    catch (x)
+                    {
+                        _debug('Exception during execution of success callback: {}', x);
+                    }
+                },
+                onFailure: function(request, reason, exception)
+                {
+                    try
+                    {
+                        _handleFailure.call(self, request, messages, reason, exception, comet);
+                    }
+                    catch (x)
+                    {
+                        _debug('Exception during execution of failure callback: {}', x);
+                    }
+                }
+            };
+            _debug('Sending request to {}, message(s): {}', envelope.url, JSON.stringify(envelope.messages));
+            _transport.send(envelope, comet);
+        };
+
+        function _applyIncomingExtensions(message)
+        {
+            for (var i = 0; i < _extensions.length; ++i)
+            {
+                var extension = _extensions[i];
+                var callback = extension.extension.incoming;
+                if (callback && typeof callback === 'function')
+                {
+                    _debug('Calling incoming extension \'{}\', callback \'{}\'', extension.name, callback.name);
+                    message = _applyExtension(extension.name, callback, message) || message;
+                }
+            }
+            return message;
+        };
+
+        function _applyOutgoingExtensions(message)
+        {
+            for (var i = 0; i < _extensions.length; ++i)
+            {
+                var extension = _extensions[i];
+                var callback = extension.extension.outgoing;
+                if (callback && typeof callback === 'function')
+                {
+                    _debug('Calling outgoing extension \'{}\', callback \'{}\'', extension.name, callback.name);
+                    message = _applyExtension(extension.name, callback, message) || message;
+                }
+            }
+            return message;
+        };
+
+        function _applyExtension(name, callback, message)
+        {
+            try
+            {
+                return callback(message);
+            }
+            catch (x)
+            {
+                _debug('Exception during execution of extension \'{}\': {}', name, x);
+                return message;
+            }
+        };
+
+        function _handleSuccess(request, response, comet)
+        {
+            var messages = _convertToMessages(response);
+            _debug('Received response {}', JSON.stringify(messages));
+
+            // Signal the transport it can deliver other queued requests
+            _transport.complete(request, true, comet);
+
+            for (var i = 0; i < messages.length; ++i)
+            {
+                var message = messages[i];
+                message = _applyIncomingExtensions(message);
+
+                if (message.advice) _advice = message.advice;
+
+                var channel = message.channel;
+                switch (channel)
+                {
+                    case '/meta/handshake':
+                        _handshakeSuccess(message);
+                        break;
+                    case '/meta/connect':
+                        _connectSuccess(message);
+                        break;
+                    case '/meta/disconnect':
+                        _disconnectSuccess(message);
+                        break;
+                    case '/meta/subscribe':
+                        _subscribeSuccess(message);
+                        break;
+                    case '/meta/unsubscribe':
+                        _unsubscribeSuccess(message);
+                        break;
+                    default:
+                        _messageSuccess(message);
+                        break;
+                }
+            }
+        };
+
+        function _handleFailure(request, messages, reason, exception, comet)
+        {
+            var xhr = request.xhr;
+            _debug('Request failed, status: {}, reason: {}, exception: {}', xhr && xhr.status, reason, exception);
+
+            // Signal the transport it can deliver other queued requests
+            _transport.complete(request, false, comet);
+
+            for (var i = 0; i < messages.length; ++i)
+            {
+                var message = messages[i];
+                var channel = message.channel;
+                switch (channel)
+                {
+                    case '/meta/handshake':
+                        _handshakeFailure(xhr, message);
+                        break;
+                    case '/meta/connect':
+                        _connectFailure(xhr, message);
+                        break;
+                    case '/meta/disconnect':
+                        _disconnectFailure(xhr, message);
+                        break;
+                    case '/meta/subscribe':
+                        _subscribeFailure(xhr, message);
+                        break;
+                    case '/meta/unsubscribe':
+                        _unsubscribeFailure(xhr, message);
+                        break;
+                    default:
+                        _messageFailure(xhr, message);
+                        break;
+                }
+            }
+        };
+
+        function _handshakeSuccess(message)
+        {
+            if (message.successful)
+            {
+                _debug('Handshake successful');
+                // Save clientId, figure out transport, then follow the advice to connect
+                _clientId = message.clientId;
+
+                var newTransport = _findTransport(message);
+                if (newTransport === null)
+                {
+                    throw 'Could not agree on transport with server';
+                }
+                else
+                {
+                    if (_transport.getType() != newTransport.getType())
+                    {
+                        _debug('Changing transport from {} to {}', _transport.getType(), newTransport.getType());
+                        _transport = newTransport;
+                    }
+                }
+
+                // Notify the listeners
+                // Here the new transport is in place, as well as the clientId, so
+                // the listener can perform a publish() if it wants, and the listeners
+                // are notified before the connect below.
+                _notifyListeners('/meta/handshake', message);
+
+                var action = _advice.reconnect ? _advice.reconnect : 'retry';
+                switch (action)
+                {
+                    case 'retry':
+                        _delayedConnect();
+                        break;
+                    default:
+                        break;
+                }
+            }
+            else
+            {
+                _debug('Handshake unsuccessful');
+
+                var retry = !_isDisconnected() && _advice.reconnect != 'none';
+                if (!retry) _setStatus('disconnected');
+
+                _notifyListeners('/meta/handshake', message);
+                _notifyListeners('/meta/unsuccessful', message);
+
+                // Only try again if we haven't been disconnected and
+                // the advice permits us to retry the handshake
+                if (retry)
+                {
+                    _increaseBackoff();
+                    _debug('Handshake failure, backing off and retrying in {} ms', _backoff);
+                    _delayedHandshake();
+                }
+            }
+        };
+
+        function _handshakeFailure(xhr, message)
+        {
+            _debug('Handshake failure');
+
+            // Notify listeners
+            var failureMessage = {
+                successful: false,
+                failure: true,
+                channel: '/meta/handshake',
+                request: message,
+                xhr: xhr,
+                advice: {
+                    action: 'retry',
+                    interval: _backoff
+                }
+            };
+
+            var retry = !_isDisconnected() && _advice.reconnect != 'none';
+            if (!retry) _setStatus('disconnected');
+
+            _notifyListeners('/meta/handshake', failureMessage);
+            _notifyListeners('/meta/unsuccessful', failureMessage);
+
+            // Only try again if we haven't been disconnected and the
+            // advice permits us to try again
+            if (retry)
+            {
+                _increaseBackoff();
+                _debug('Handshake failure, backing off and retrying in {} ms', _backoff);
+                _delayedHandshake();
+            }
+        };
+
+        function _connectSuccess(message)
+        {
+            var action = _isDisconnected() ? 'none' : (_advice.reconnect ? _advice.reconnect : 'retry');
+            if (!_isDisconnected()) _setStatus(action == 'retry' ? 'connecting' : 'disconnecting');
+
+            if (message.successful)
+            {
+                _debug('Connect successful');
+
+                // End the batch and allow held messages from the application
+                // to go to the server (see _handshake() where we start the batch).
+                // The batch is ended before notifying the listeners, so that
+                // listeners can batch other cometd operations
+                _endBatch(true);
+
+                // Notify the listeners after the status change but before the next connect
+                _notifyListeners('/meta/connect', message);
+
+                // Connect was successful.
+                // Normally, the advice will say "reconnect: 'retry', interval: 0"
+                // and the server will hold the request, so when a response returns
+                // we immediately call the server again (long polling)
+                switch (action)
+                {
+                    case 'retry':
+                        _resetBackoff();
+                        _delayedConnect();
+                        break;
+                    default:
+                        _resetBackoff();
+                        _setStatus('disconnected');
+                        break;
+                }
+            }
+            else
+            {
+                _debug('Connect unsuccessful');
+
+                // Notify the listeners after the status change but before the next action
+                _notifyListeners('/meta/connect', message);
+                _notifyListeners('/meta/unsuccessful', message);
+
+                // Connect was not successful.
+                // This may happen when the server crashed, the current clientId
+                // will be invalid, and the server will ask to handshake again
+                switch (action)
+                {
+                    case 'retry':
+                        _increaseBackoff();
+                        _delayedConnect();
+                        break;
+                    case 'handshake':
+                        // End the batch but do not deliver the messages until we connect successfully
+                        _endBatch(false);
+                        _resetBackoff();
+                        _delayedHandshake();
+                        break;
+                    case 'none':
+                        _resetBackoff();
+                        _setStatus('disconnected');
+                        break;
+                }
+            }
+        };
+
+        function _connectFailure(xhr, message)
+        {
+            _debug('Connect failure');
+
+            // Notify listeners
+            var failureMessage = {
+                successful: false,
+                failure: true,
+                channel: '/meta/connect',
+                request: message,
+                xhr: xhr,
+                advice: {
+                    action: 'retry',
+                    interval: _backoff
+                }
+            };
+            _notifyListeners('/meta/connect', failureMessage);
+            _notifyListeners('/meta/unsuccessful', failureMessage);
+
+            if (!_isDisconnected())
+            {
+                var action = _advice.reconnect ? _advice.reconnect : 'retry';
+                switch (action)
+                {
+                    case 'retry':
+                        _increaseBackoff();
+                        _debug('Connect failure, backing off and retrying in {} ms', _backoff);
+                        _delayedConnect();
+                        break;
+                    case 'handshake':
+                        _resetBackoff();
+                        _delayedHandshake();
+                        break;
+                    case 'none':
+                        _resetBackoff();
+                        break;
+                    default:
+                        _debug('Unrecognized reconnect value: {}', action);
+                        break;
+                }
+            }
+        };
+
+        function _disconnectSuccess(message)
+        {
+            if (message.successful)
+            {
+                _debug('Disconnect successful');
+                _disconnect(false);
+                _notifyListeners('/meta/disconnect', message);
+            }
+            else
+            {
+                _debug('Disconnect unsuccessful');
+                _disconnect(true);
+                _notifyListeners('/meta/disconnect', message);
+                _notifyListeners('/meta/usuccessful', message);
+            }
+        };
+
+        function _disconnect(abort)
+        {
+            _cancelDelayedSend();
+            if (abort) _transport.abort();
+            _clientId = null;
+            _setStatus('disconnected');
+            _batch = 0;
+            _messageQueue = [];
+            _resetBackoff();
+        };
+
+        function _disconnectFailure(xhr, message)
+        {
+            _debug('Disconnect failure');
+            _disconnect(true);
+
+            var failureMessage = {
+                successful: false,
+                failure: true,
+                channel: '/meta/disconnect',
+                request: message,
+                xhr: xhr,
+                advice: {
+                    action: 'none',
+                    interval: 0
+                }
+            };
+            _notifyListeners('/meta/disconnect', failureMessage);
+            _notifyListeners('/meta/unsuccessful', failureMessage);
+        };
+
+        function _subscribeSuccess(message)
+        {
+            if (message.successful)
+            {
+                _debug('Subscribe successful');
+                _notifyListeners('/meta/subscribe', message);
+            }
+            else
+            {
+                _debug('Subscribe unsuccessful');
+                _notifyListeners('/meta/subscribe', message);
+                _notifyListeners('/meta/unsuccessful', message);
+            }
+        };
+
+        function _subscribeFailure(xhr, message)
+        {
+            _debug('Subscribe failure');
+
+            var failureMessage = {
+                successful: false,
+                failure: true,
+                channel: '/meta/subscribe',
+                request: message,
+                xhr: xhr,
+                advice: {
+                    action: 'none',
+                    interval: 0
+                }
+            };
+            _notifyListeners('/meta/subscribe', failureMessage);
+            _notifyListeners('/meta/unsuccessful', failureMessage);
+        };
+
+        function _unsubscribeSuccess(message)
+        {
+            if (message.successful)
+            {
+                _debug('Unsubscribe successful');
+                _notifyListeners('/meta/unsubscribe', message);
+            }
+            else
+            {
+                _debug('Unsubscribe unsuccessful');
+                _notifyListeners('/meta/unsubscribe', message);
+                _notifyListeners('/meta/unsuccessful', message);
+            }
+        };
+
+        function _unsubscribeFailure(xhr, message)
+        {
+            _debug('Unsubscribe failure');
+
+            var failureMessage = {
+                successful: false,
+                failure: true,
+                channel: '/meta/unsubscribe',
+                request: message,
+                xhr: xhr,
+                advice: {
+                    action: 'none',
+                    interval: 0
+                }
+            };
+            _notifyListeners('/meta/unsubscribe', failureMessage);
+            _notifyListeners('/meta/unsuccessful', failureMessage);
+        };
+
+        function _messageSuccess(message)
+        {
+            if (message.successful === undefined)
+            {
+                if (message.data)
+                {
+                    // It is a plain message, and not a bayeux meta message
+                    _notifyListeners(message.channel, message);
+                }
+                else
+                {
+                    _debug('Unknown message {}', JSON.stringify(message));
+                }
+            }
+            else
+            {
+                if (message.successful)
+                {
+                    _debug('Publish successful');
+                    _notifyListeners('/meta/publish', message);
+                }
+                else
+                {
+                    _debug('Publish unsuccessful');
+                    _notifyListeners('/meta/publish', message);
+                    _notifyListeners('/meta/unsuccessful', message);
+                }
+            }
+        };
+
+        function _messageFailure(xhr, message)
+        {
+            _debug('Publish failure');
+
+            var failureMessage = {
+                successful: false,
+                failure: true,
+                channel: message.channel,
+                request: message,
+                xhr: xhr,
+                advice: {
+                    action: 'none',
+                    interval: 0
+                }
+            };
+            _notifyListeners('/meta/publish', failureMessage);
+            _notifyListeners('/meta/unsuccessful', failureMessage);
+        };
+
+        function _notifyListeners(channel, message)
+        {
+            // Notify direct listeners
+            _notify(channel, message);
+
+            // Notify the globbing listeners
+            var channelParts = channel.split("/");
+            var last = channelParts.length - 1;
+            for (var i = last; i > 0; --i)
+            {
+                var channelPart = channelParts.slice(0, i).join('/') + '/*';
+                // We don't want to notify /foo/* if the channel is /foo/bar/baz,
+                // so we stop at the first non recursive globbing
+                if (i == last) _notify(channelPart, message);
+                // Add the recursive globber and notify
+                channelPart += '*';
+                _notify(channelPart, message);
+            }
+        };
+
+        function _notify(channel, message)
+        {
+            var subscriptions = _listeners[channel];
+            if (subscriptions && subscriptions.length > 0)
+            {
+                for (var i = 0; i < subscriptions.length; ++i)
+                {
+                    var subscription = subscriptions[i];
+                    // Subscriptions may come and go, so the array may have 'holes'
+                    if (subscription)
+                    {
+                        try
+                        {
+                            _debug('Notifying subscription: channel \'{}\', callback \'{}\'', channel, subscription.callback.name);
+                            subscription.callback.call(subscription.scope, message);
+                        }
+                        catch (x)
+                        {
+                            // Ignore exceptions from callbacks
+                            _warn('Exception during execution of callback \'{}\' on channel \'{}\' for message {}, exception: {}', subscription.callback.name, channel, JSON.stringify(message), x);
+                        }
+                    }
+                }
+            }
+        };
+
+        function _resetBackoff()
+        {
+            _backoff = 0;
+        };
+
+        function _increaseBackoff()
+        {
+            if (_backoff < _maxBackoff) _backoff += _backoffIncrement;
+        };
+
+        var _error = this._error = function(text, args)
+        {
+            _log('error', _format.apply(this, arguments));
+        };
+
+        var _warn = this._warn = function(text, args)
+        {
+            _log('warn', _format.apply(this, arguments));
+        };
+
+        var _info = this._info = function(text, args)
+        {
+            _log('info', _format.apply(this, arguments));
+        };
+
+        var _debug = this._debug = function(text, args)
+        {
+            _log('debug', _format.apply(this, arguments));
+        };
+
+        function _log(level, text)
+        {
+            var priority = _logPriorities[level];
+            var configPriority = _logPriorities[_logLevel];
+            if (!configPriority) configPriority = _logPriorities['info'];
+            if (priority >= configPriority)
+            {
+                if (window.console) window.console.log(text);
+            }
+        };
+
+        function _format(text)
+        {
+            var braces = /\{\}/g;
+            var result = '';
+            var start = 0;
+            var count = 0;
+            while (braces.test(text))
+            {
+                result += text.substr(start, braces.lastIndex - start - 2);
+                var arg = arguments[++count];
+                result += arg !== undefined ? arg : '{}';
+                start = braces.lastIndex;
+            }
+            result += text.substr(start, text.length - start);
+            return result;
+        };
+
+        function newLongPollingTransport()
+        {
+            return $.extend({}, new Transport('long-polling'), new LongPollingTransport());
+        };
+
+        function newCallbackPollingTransport()
+        {
+            return $.extend({}, new Transport('callback-polling'), new CallbackPollingTransport());
+        };
+
+        /**
+         * Base object with the common functionality for transports.
+         * The key responsibility is to allow at most 2 outstanding requests to the server,
+         * to avoid that requests are sent behind a long poll.
+         * To achieve this, we have one reserved request for the long poll, and all other
+         * requests are serialized one after the other.
+         */
+        var Transport = function(type)
+        {
+            var _maxRequests = 2;
+            var _requestIds = 0;
+            var _cometRequest = null;
+            var _requests = [];
+            var _packets = [];
+
+            this.getType = function()
+            {
+                return type;
+            };
+
+            this.send = function(packet, comet)
+            {
+                if (comet)
+                    _cometSend(this, packet);
+                else
+                    _send(this, packet);
+            };
+
+            function _cometSend(self, packet)
+            {
+                if (_cometRequest !== null) throw 'Concurrent comet requests not allowed, request ' + _cometRequest.id + ' not yet completed';
+
+                var requestId = ++_requestIds;
+                _debug('Beginning comet request {}', requestId);
+
+                var request = {id: requestId};
+                _debug('Delivering comet request {}', requestId);
+                self.deliver(packet, request);
+                _cometRequest = request;
+            };
+
+            function _send(self, packet)
+            {
+                var requestId = ++_requestIds;
+                _debug('Beginning request {}, {} other requests, {} queued requests', requestId, _requests.length, _packets.length);
+
+                var request = {id: requestId};
+                // Consider the comet request which should always be present
+                if (_requests.length < _maxRequests - 1)
+                {
+                    _debug('Delivering request {}', requestId);
+                    self.deliver(packet, request);
+                    _requests.push(request);
+                }
+                else
+                {
+                    _packets.push([packet, request]);
+                    _debug('Queued request {}, {} queued requests', requestId, _packets.length);
+                }
+            };
+
+            this.complete = function(request, success, comet)
+            {
+                if (comet)
+                    _cometComplete(request);
+                else
+                    _complete(this, request, success);
+            };
+
+            function _cometComplete(request)
+            {
+                var requestId = request.id;
+                if (_cometRequest !== request) throw 'Comet request mismatch, completing request ' + requestId;
+
+                // Reset comet request
+                _cometRequest = null;
+                _debug('Ended comet request {}', requestId);
+            };
+
+            function _complete(self, request, success)
+            {
+                var requestId = request.id;
+                var index = $.inArray(request, _requests);
+                // The index can be negative the request has been aborted
+                if (index >= 0) _requests.splice(index, 1);
+                _debug('Ended request {}, {} other requests, {} queued requests', requestId, _requests.length, _packets.length);
+
+                if (_packets.length > 0)
+                {
+                    var packet = _packets.shift();
+                    if (success)
+                    {
+                        _debug('Dequeueing and sending request {}, {} queued requests', packet[1].id, _packets.length);
+                        _send(self, packet[0]);
+                    }
+                    else
+                    {
+                        _debug('Dequeueing and failing request {}, {} queued requests', packet[1].id, _packets.length);
+                        // Keep the semantic of calling response callbacks asynchronously after the request
+                        setTimeout(function() { packet[0].onFailure(packet[1], 'error'); }, 0);
+                    }
+                }
+            };
+
+            this.abort = function()
+            {
+                for (var i = 0; i < _requests.length; ++i)
+                {
+                    var request = _requests[i];
+                    _debug('Aborting request {}', request.id);
+                    if (request.xhr) request.xhr.abort();
+                }
+                if (_cometRequest)
+                {
+                    _debug('Aborting comet request {}', _cometRequest.id);
+                    if (_cometRequest.xhr) _cometRequest.xhr.abort();
+                }
+                _cometRequest = null;
+                _requests = [];
+                _packets = [];
+            };
+        };
+
+        var LongPollingTransport = function()
+        {
+            this.deliver = function(packet, request)
+            {
+                request.xhr = $.ajax({
+                    url: packet.url,
+                    type: 'POST',
+                    contentType: 'text/json;charset=UTF-8',
+                    beforeSend: function(xhr)
+                    {
+                        xhr.setRequestHeader('Connection', 'Keep-Alive');
+                        return true;
+                    },
+                    data: JSON.stringify(packet.messages),
+                    success: function(response) { packet.onSuccess(request, response); },
+                    error: function(xhr, reason, exception) { packet.onFailure(request, reason, exception); }
+                });
+            };
+        };
+
+        var CallbackPollingTransport = function()
+        {
+            var _maxLength = 2000;
+            this.deliver = function(packet, request)
+            {
+                // Microsoft Internet Explorer has a 2083 URL max length
+                // We must ensure that we stay within that length
+                var messages = JSON.stringify(packet.messages);
+                // Encode the messages because all brackets, quotes, commas, colons, etc
+                // present in the JSON will be URL encoded, taking many more characters
+                var urlLength = packet.url.length + encodeURI(messages).length;
+                _debug('URL length: {}', urlLength);
+                // Let's stay on the safe side and use 2000 instead of 2083
+                // also because we did not count few characters among which
+                // the parameter name 'message' and the parameter 'jsonp',
+                // which sum up to about 50 chars
+                if (urlLength > _maxLength)
+                {
+                    var x = packet.messages.length > 1 ?
+                            'Too many bayeux messages in the same batch resulting in message too big ' +
+                            '(' + urlLength + ' bytes, max is ' + _maxLength + ') for transport ' + this.getType() :
+                            'Bayeux message too big (' + urlLength + ' bytes, max is ' + _maxLength + ') ' +
+                            'for transport ' + this.getType();
+                    // Keep the semantic of calling response callbacks asynchronously after the request
+                    _setTimeout(function() { packet.onFailure(request, 'error', x); }, 0);
+                }
+                else
+                {
+                    $.ajax({
+                        url: packet.url,
+                        type: 'GET',
+                        dataType: 'jsonp',
+                        jsonp: 'jsonp',
+                        beforeSend: function(xhr)
+                        {
+                            xhr.setRequestHeader('Connection', 'Keep-Alive');
+                            return true;
+                        },
+                        data:
+                        {
+                            // In callback-polling, the content must be sent via the 'message' parameter
+                            message: messages
+                        },
+                        success: function(response) { packet.onSuccess(request, response); },
+                        error: function(xhr, reason, exception) { packet.onFailure(request, reason, exception); }
+                    });
+                }
+            };
+        };
+    };
+
+    /**
+     * The JS object that exposes the comet API to applications
+     */
+    $.cometd = new $.Cometd(); // The default instance
+
+})(jQuery);
diff --git a/plugins/Comet/json2.js b/plugins/Comet/json2.js
new file mode 100644 (file)
index 0000000..7e27df5
--- /dev/null
@@ -0,0 +1,478 @@
+/*
+    http://www.JSON.org/json2.js
+    2009-04-16
+
+    Public Domain.
+
+    NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
+
+    See http://www.JSON.org/js.html
+
+    This file creates a global JSON object containing two methods: stringify
+    and parse.
+
+        JSON.stringify(value, replacer, space)
+            value       any JavaScript value, usually an object or array.
+
+            replacer    an optional parameter that determines how object
+                        values are stringified for objects. It can be a
+                        function or an array of strings.
+
+            space       an optional parameter that specifies the indentation
+                        of nested structures. If it is omitted, the text will
+                        be packed without extra whitespace. If it is a number,
+                        it will specify the number of spaces to indent at each
+                        level. If it is a string (such as '\t' or '&nbsp;'),
+                        it contains the characters used to indent at each level.
+
+            This method produces a JSON text from a JavaScript value.
+
+            When an object value is found, if the object contains a toJSON
+            method, its toJSON method will be called and the result will be
+            stringified. A toJSON method does not serialize: it returns the
+            value represented by the name/value pair that should be serialized,
+            or undefined if nothing should be serialized. The toJSON method
+            will be passed the key associated with the value, and this will be
+            bound to the object holding the key.
+
+            For example, this would serialize Dates as ISO strings.
+
+                Date.prototype.toJSON = function (key) {
+                    function f(n) {
+                        // Format integers to have at least two digits.
+                        return n < 10 ? '0' + n : n;
+                    }
+
+                    return this.getUTCFullYear()   + '-' +
+                         f(this.getUTCMonth() + 1) + '-' +
+                         f(this.getUTCDate())      + 'T' +
+                         f(this.getUTCHours())     + ':' +
+                         f(this.getUTCMinutes())   + ':' +
+                         f(this.getUTCSeconds())   + 'Z';
+                };
+
+            You can provide an optional replacer method. It will be passed the
+            key and value of each member, with this bound to the containing
+            object. The value that is returned from your method will be
+            serialized. If your method returns undefined, then the member will
+            be excluded from the serialization.
+
+            If the replacer parameter is an array of strings, then it will be
+            used to select the members to be serialized. It filters the results
+            such that only members with keys listed in the replacer array are
+            stringified.
+
+            Values that do not have JSON representations, such as undefined or
+            functions, will not be serialized. Such values in objects will be
+            dropped; in arrays they will be replaced with null. You can use
+            a replacer function to replace those with JSON values.
+            JSON.stringify(undefined) returns undefined.
+
+            The optional space parameter produces a stringification of the
+            value that is filled with line breaks and indentation to make it
+            easier to read.
+
+            If the space parameter is a non-empty string, then that string will
+            be used for indentation. If the space parameter is a number, then
+            the indentation will be that many spaces.
+
+            Example:
+
+            text = JSON.stringify(['e', {pluribus: 'unum'}]);
+            // text is '["e",{"pluribus":"unum"}]'
+
+
+            text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
+            // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
+
+            text = JSON.stringify([new Date()], function (key, value) {
+                return this[key] instanceof Date ?
+                    'Date(' + this[key] + ')' : value;
+            });
+            // text is '["Date(---current time---)"]'
+
+
+        JSON.parse(text, reviver)
+            This method parses a JSON text to produce an object or array.
+            It can throw a SyntaxError exception.
+
+            The optional reviver parameter is a function that can filter and
+            transform the results. It receives each of the keys and values,
+            and its return value is used instead of the original value.
+            If it returns what it received, then the structure is not modified.
+            If it returns undefined then the member is deleted.
+
+            Example:
+
+            // Parse the text. Values that look like ISO date strings will
+            // be converted to Date objects.
+
+            myData = JSON.parse(text, function (key, value) {
+                var a;
+                if (typeof value === 'string') {
+                    a =
+/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
+                    if (a) {
+                        return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+                            +a[5], +a[6]));
+                    }
+                }
+                return value;
+            });
+
+            myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
+                var d;
+                if (typeof value === 'string' &&
+                        value.slice(0, 5) === 'Date(' &&
+                        value.slice(-1) === ')') {
+                    d = new Date(value.slice(5, -1));
+                    if (d) {
+                        return d;
+                    }
+                }
+                return value;
+            });
+
+
+    This is a reference implementation. You are free to copy, modify, or
+    redistribute.
+
+    This code should be minified before deployment.
+    See http://javascript.crockford.com/jsmin.html
+
+    USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
+    NOT CONTROL.
+*/
+
+/*jslint evil: true */
+
+/*global JSON */
+
+/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
+    call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
+    getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
+    lastIndex, length, parse, prototype, push, replace, slice, stringify,
+    test, toJSON, toString, valueOf
+*/
+
+// Create a JSON object only if one does not already exist. We create the
+// methods in a closure to avoid creating global variables.
+
+if (!this.JSON) {
+    JSON = {};
+}
+(function () {
+
+    function f(n) {
+        // Format integers to have at least two digits.
+        return n < 10 ? '0' + n : n;
+    }
+
+    if (typeof Date.prototype.toJSON !== 'function') {
+
+        Date.prototype.toJSON = function (key) {
+
+            return this.getUTCFullYear()   + '-' +
+                 f(this.getUTCMonth() + 1) + '-' +
+                 f(this.getUTCDate())      + 'T' +
+                 f(this.getUTCHours())     + ':' +
+                 f(this.getUTCMinutes())   + ':' +
+                 f(this.getUTCSeconds())   + 'Z';
+        };
+
+        String.prototype.toJSON =
+        Number.prototype.toJSON =
+        Boolean.prototype.toJSON = function (key) {
+            return this.valueOf();
+        };
+    }
+
+    var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+        escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+        gap,
+        indent,
+        meta = {    // table of character substitutions
+            '\b': '\\b',
+            '\t': '\\t',
+            '\n': '\\n',
+            '\f': '\\f',
+            '\r': '\\r',
+            '"' : '\\"',
+            '\\': '\\\\'
+        },
+        rep;
+
+
+    function quote(string) {
+
+// If the string contains no control characters, no quote characters, and no
+// backslash characters, then we can safely slap some quotes around it.
+// Otherwise we must also replace the offending characters with safe escape
+// sequences.
+
+        escapable.lastIndex = 0;
+        return escapable.test(string) ?
+            '"' + string.replace(escapable, function (a) {
+                var c = meta[a];
+                return typeof c === 'string' ? c :
+                    '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+            }) + '"' :
+            '"' + string + '"';
+    }
+
+
+    function str(key, holder) {
+
+// Produce a string from holder[key].
+
+        var i,          // The loop counter.
+            k,          // The member key.
+            v,          // The member value.
+            length,
+            mind = gap,
+            partial,
+            value = holder[key];
+
+// If the value has a toJSON method, call it to obtain a replacement value.
+
+        if (value && typeof value === 'object' &&
+                typeof value.toJSON === 'function') {
+            value = value.toJSON(key);
+        }
+
+// If we were called with a replacer function, then call the replacer to
+// obtain a replacement value.
+
+        if (typeof rep === 'function') {
+            value = rep.call(holder, key, value);
+        }
+
+// What happens next depends on the value's type.
+
+        switch (typeof value) {
+        case 'string':
+            return quote(value);
+
+        case 'number':
+
+// JSON numbers must be finite. Encode non-finite numbers as null.
+
+            return isFinite(value) ? String(value) : 'null';
+
+        case 'boolean':
+        case 'null':
+
+// If the value is a boolean or null, convert it to a string. Note:
+// typeof null does not produce 'null'. The case is included here in
+// the remote chance that this gets fixed someday.
+
+            return String(value);
+
+// If the type is 'object', we might be dealing with an object or an array or
+// null.
+
+        case 'object':
+
+// Due to a specification blunder in ECMAScript, typeof null is 'object',
+// so watch out for that case.
+
+            if (!value) {
+                return 'null';
+            }
+
+// Make an array to hold the partial results of stringifying this object value.
+
+            gap += indent;
+            partial = [];
+
+// Is the value an array?
+
+            if (Object.prototype.toString.apply(value) === '[object Array]') {
+
+// The value is an array. Stringify every element. Use null as a placeholder
+// for non-JSON values.
+
+                length = value.length;
+                for (i = 0; i < length; i += 1) {
+                    partial[i] = str(i, value) || 'null';
+                }
+
+// Join all of the elements together, separated with commas, and wrap them in
+// brackets.
+
+                v = partial.length === 0 ? '[]' :
+                    gap ? '[\n' + gap +
+                            partial.join(',\n' + gap) + '\n' +
+                                mind + ']' :
+                          '[' + partial.join(',') + ']';
+                gap = mind;
+                return v;
+            }
+
+// If the replacer is an array, use it to select the members to be stringified.
+
+            if (rep && typeof rep === 'object') {
+                length = rep.length;
+                for (i = 0; i < length; i += 1) {
+                    k = rep[i];
+                    if (typeof k === 'string') {
+                        v = str(k, value);
+                        if (v) {
+                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
+                        }
+                    }
+                }
+            } else {
+
+// Otherwise, iterate through all of the keys in the object.
+
+                for (k in value) {
+                    if (Object.hasOwnProperty.call(value, k)) {
+                        v = str(k, value);
+                        if (v) {
+                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
+                        }
+                    }
+                }
+            }
+
+// Join all of the member texts together, separated with commas,
+// and wrap them in braces.
+
+            v = partial.length === 0 ? '{}' :
+                gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
+                        mind + '}' : '{' + partial.join(',') + '}';
+            gap = mind;
+            return v;
+        }
+    }
+
+// If the JSON object does not yet have a stringify method, give it one.
+
+    if (typeof JSON.stringify !== 'function') {
+        JSON.stringify = function (value, replacer, space) {
+
+// The stringify method takes a value and an optional replacer, and an optional
+// space parameter, and returns a JSON text. The replacer can be a function
+// that can replace values, or an array of strings that will select the keys.
+// A default replacer method can be provided. Use of the space parameter can
+// produce text that is more easily readable.
+
+            var i;
+            gap = '';
+            indent = '';
+
+// If the space parameter is a number, make an indent string containing that
+// many spaces.
+
+            if (typeof space === 'number') {
+                for (i = 0; i < space; i += 1) {
+                    indent += ' ';
+                }
+
+// If the space parameter is a string, it will be used as the indent string.
+
+            } else if (typeof space === 'string') {
+                indent = space;
+            }
+
+// If there is a replacer, it must be a function or an array.
+// Otherwise, throw an error.
+
+            rep = replacer;
+            if (replacer && typeof replacer !== 'function' &&
+                    (typeof replacer !== 'object' ||
+                     typeof replacer.length !== 'number')) {
+                throw new Error('JSON.stringify');
+            }
+
+// Make a fake root object containing our value under the key of ''.
+// Return the result of stringifying the value.
+
+            return str('', {'': value});
+        };
+    }
+
+
+// If the JSON object does not yet have a parse method, give it one.
+
+    if (typeof JSON.parse !== 'function') {
+        JSON.parse = function (text, reviver) {
+
+// The parse method takes a text and an optional reviver function, and returns
+// a JavaScript value if the text is a valid JSON text.
+
+            var j;
+
+            function walk(holder, key) {
+
+// The walk method is used to recursively walk the resulting structure so
+// that modifications can be made.
+
+                var k, v, value = holder[key];
+                if (value && typeof value === 'object') {
+                    for (k in value) {
+                        if (Object.hasOwnProperty.call(value, k)) {
+                            v = walk(value, k);
+                            if (v !== undefined) {
+                                value[k] = v;
+                            } else {
+                                delete value[k];
+                            }
+                        }
+                    }
+                }
+                return reviver.call(holder, key, value);
+            }
+
+
+// Parsing happens in four stages. In the first stage, we replace certain
+// Unicode characters with escape sequences. JavaScript handles many characters
+// incorrectly, either silently deleting them, or treating them as line endings.
+
+            cx.lastIndex = 0;
+            if (cx.test(text)) {
+                text = text.replace(cx, function (a) {
+                    return '\\u' +
+                        ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+                });
+            }
+
+// In the second stage, we run the text against regular expressions that look
+// for non-JSON patterns. We are especially concerned with '()' and 'new'
+// because they can cause invocation, and '=' because it can cause mutation.
+// But just to be safe, we want to reject all unexpected forms.
+
+// We split the second stage into 4 regexp operations in order to work around
+// crippling inefficiencies in IE's and Safari's regexp engines. First we
+// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
+// replace all simple value tokens with ']' characters. Third, we delete all
+// open brackets that follow a colon or comma or that begin the text. Finally,
+// we look to see that the remaining characters are only whitespace or ']' or
+// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
+
+            if (/^[\],:{}\s]*$/.
+test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
+replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
+replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
+
+// In the third stage we use the eval function to compile the text into a
+// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
+// in JavaScript: it can begin a block or an object literal. We wrap the text
+// in parens to eliminate the ambiguity.
+
+                j = eval('(' + text + ')');
+
+// In the optional fourth stage, we recursively walk the new structure, passing
+// each name/value pair to a reviver function for possible transformation.
+
+                return typeof reviver === 'function' ?
+                    walk({'': j}, '') : j;
+            }
+
+// If the text is not JSON parseable, then a SyntaxError is thrown.
+
+            throw new SyntaxError('JSON.parse');
+        };
+    }
+}());
diff --git a/plugins/Comet/updatetimeline.js b/plugins/Comet/updatetimeline.js
new file mode 100644 (file)
index 0000000..170949e
--- /dev/null
@@ -0,0 +1,154 @@
+// update the local timeline from a Comet server
+//
+
+var updater = function()
+{
+     var _server;
+     var _timeline;
+     var _userid;
+     var _replyurl;
+     var _favorurl;
+     var _deleteurl;
+     var _cometd;
+
+     return {
+          init: function(server, timeline, userid, replyurl, favorurl, deleteurl)
+          {
+               _cometd = $.cometd; // Uses the default Comet object
+               _cometd.setLogLevel('debug');
+               _cometd.init(server);
+               _server = server;
+               _timeline = timeline;
+               _userid = userid;
+               _favorurl = favorurl;
+               _replyurl = replyurl;
+               _deleteurl = deleteurl;
+               _cometd.subscribe(timeline, receive);
+               $(window).unload(leave);
+          }
+     }
+
+     function leave()
+     {
+          _cometd.disconnect();
+     }
+
+     function receive(message)
+     {
+          id = message.data.id;
+
+          // Don't add it if it already exists
+
+          if ($("#notice-"+id).length > 0) {
+               return;
+          }
+
+          var noticeItem = makeNoticeItem(message.data);
+          $("#notices_primary .notices").prepend(noticeItem, true);
+          $("#notices_primary .notice:first").css({display:"none"});
+          $("#notices_primary .notice:first").fadeIn(1000);
+          NoticeHover();
+          NoticeReply();
+     }
+
+     function makeNoticeItem(data)
+     {
+          user = data['user'];
+          html = data['html'].replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&quot;/g,'"');
+          source = data['source'].replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&quot;/g,'"');
+
+          ni = "<li class=\"hentry notice\" id=\"notice-"+data['id']+"\">"+
+               "<div class=\"entry-title\">"+
+               "<span class=\"vcard author\">"+
+               "<a href=\""+user['profile_url']+"\" class=\"url\">"+
+               "<img src=\""+user['profile_image_url']+"\" class=\"avatar photo\" width=\"48\" height=\"48\" alt=\""+user['screen_name']+"\"/>"+
+               "<span class=\"nickname fn\">"+user['screen_name']+"</span>"+
+               "</a>"+
+               "</span>"+
+               "<p class=\"entry-content\">"+html+"</p>"+
+               "</div>"+
+               "<div class=\"entry-content\">"+
+               "<dl class=\"timestamp\">"+
+               "<dt>Published</dt>"+
+               "<dd>"+
+               "<a rel=\"bookmark\" href=\""+data['url']+"\" >"+
+               "<abbr class=\"published\" title=\""+data['created_at']+"\">a few seconds ago</abbr>"+
+               "</a> "+
+               "</dd>"+
+               "</dl>"+
+               "<dl class=\"device\">"+
+               "<dt>From</dt> "+
+               "<dd>"+source+"</dd>"+ // may have a link, I think
+               "</dl>";
+
+          if (data['in_reply_to_status_id']) {
+               ni = ni+" <dl class=\"response\">"+
+                    "<dt>To</dt>"+
+                    "<dd>"+
+                    "<a href=\""+data['in_reply_to_status_url']+"\" rel=\"in-reply-to\">in reply to</a>"+
+                    "</dd>"+
+                    "</dl>";
+          }
+
+          ni = ni+"</div>"+
+               "<div class=\"notice-options\">";
+
+          if (_userid != 0) {
+               var input = $("form#form_notice fieldset input#token");
+               var session_key = input.val();
+               ni = ni+makeFavoriteForm(data['id'], session_key);
+               ni = ni+makeReplyLink(data['id'], data['user']['screen_name']);
+               if (_userid == data['user']['id']) {
+                    ni = ni+makeDeleteLink(data['id']);
+               }
+          }
+
+          ni = ni+"</div>"+
+               "</li>";
+          return ni;
+     }
+
+     function makeFavoriteForm(id, session_key)
+     {
+          var ff;
+
+          ff = "<form id=\"favor-"+id+"\" class=\"form_favor\" method=\"post\" action=\""+_favorurl+"\">"+
+               "<fieldset>"+
+               "<legend>Favor this notice</legend>"+ // XXX: i18n
+               "<input name=\"token-"+id+"\" type=\"hidden\" id=\"token-"+id+"\" value=\""+session_key+"\"/>"+
+               "<input name=\"notice\" type=\"hidden\" id=\"notice-n"+id+"\" value=\""+id+"\"/>"+
+               "<input type=\"submit\" id=\"favor-submit-"+id+"\" name=\"favor-submit-"+id+"\" class=\"submit\" value=\"Favor\" title=\"Favor this notice\"/>"+
+               "</fieldset>"+
+               "</form>";
+          return ff;
+     }
+
+     function makeReplyLink(id, nickname)
+     {
+          var rl;
+          rl = "<dl class=\"notice_reply\">"+
+               "<dt>Reply to this notice</dt>"+
+               "<dd>"+
+               "<a href=\""+_replyurl+"?replyto="+nickname+"\" title=\"Reply to this notice\">Reply <span class=\"notice_id\">"+id+"</span>"+
+               "</a>"+
+               "</dd>"+
+               "</dl>";
+          return rl;
+     }
+
+     function makeDeleteLink(id)
+     {
+          var dl, delurl;
+          delurl = _deleteurl.replace("0000000000", id);
+
+          dl = "<dl class=\"notice_delete\">"+
+               "<dt>Delete this notice</dt>"+
+               "<dd>"+
+               "<a href=\""+delurl+"\" title=\"Delete this notice\">Delete</a>"+
+               "</dd>"+
+               "</dl>";
+
+          return dl;
+     }
+}();
+
diff --git a/plugins/FBConnect/FBCLoginGroupNav.php b/plugins/FBConnect/FBCLoginGroupNav.php
new file mode 100644 (file)
index 0000000..6eb09c3
--- /dev/null
@@ -0,0 +1,110 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Menu for login group of actions
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Menu
+ * @package   Laconica
+ * @author    Evan Prodromou <evan@controlyourself.ca>
+ * @author    Zach Copley <zach@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+require_once INSTALLDIR . '/lib/widget.php';
+
+/**
+ * Menu for login group of actions
+ *
+ * @category Output
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @author   Zach Copley <zach@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ *
+ * @see      Widget
+ */
+
+class FBCLoginGroupNav extends Widget
+{
+    var $action = null;
+
+    /**
+     * Construction
+     *
+     * @param Action $action current action, used for output
+     */
+
+    function __construct($action=null)
+    {
+        parent::__construct($action);
+        $this->action = $action;
+    }
+
+    /**
+     * Show the menu
+     *
+     * @return void
+     */
+
+    function show()
+    {
+        $this->action->elementStart('dl', array('id' => 'site_nav_local_views'));
+        $this->action->element('dt', null, _('Local views'));
+        $this->action->elementStart('dd');
+
+        // action => array('prompt', 'title')
+        $menu = array();
+
+        $menu['login'] = array(_('Login'),
+                         _('Login with a username and password'));
+
+        if (!(common_config('site','closed') || common_config('site','inviteonly'))) {
+            $menu['register'] = array(_('Register'),
+                                _('Sign up for a new account'));
+        }
+
+        $menu['openidlogin'] = array(_('OpenID'),
+                               _('Login or register with OpenID'));
+
+        $menu['FBConnectLogin'] = array(_('Facebook'),
+                               _('Login or register using Facebook'));
+
+        $action_name = $this->action->trimmed('action');
+        $this->action->elementStart('ul', array('class' => 'nav'));
+
+        foreach ($menu as $menuaction => $menudesc) {
+            $this->action->menuItem(common_local_url($menuaction),
+                                    $menudesc[0],
+                                    $menudesc[1],
+                                    $action_name === $menuaction);
+        }
+
+        $this->action->elementEnd('ul');
+
+        $this->action->elementEnd('dd');
+        $this->action->elementEnd('dl');
+    }
+}
diff --git a/plugins/FBConnect/FBCSettingsNav.php b/plugins/FBConnect/FBCSettingsNav.php
new file mode 100644 (file)
index 0000000..8b84118
--- /dev/null
@@ -0,0 +1,113 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Menu for login group of actions
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Menu
+ * @package   Laconica
+ * @author    Evan Prodromou <evan@controlyourself.ca>
+ * @author    Zach Copley <zach@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+require_once INSTALLDIR . '/lib/widget.php';
+
+/**
+ * A widget for showing the connect group local nav menu
+ *
+ * @category Output
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @author   Zach Copley <zach@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ *
+ * @see      Widget
+ */
+
+class FBCSettingsNav extends Widget
+{
+    var $action = null;
+
+    /**
+     * Construction
+     *
+     * @param Action $action current action, used for output
+     */
+
+    function __construct($action=null)
+    {
+        parent::__construct($action);
+        $this->action = $action;
+    }
+
+    /**
+     * Show the menu
+     *
+     * @return void
+     */
+
+    function show()
+    {
+
+        $this->action->elementStart('dl', array('id' => 'site_nav_local_views'));
+        $this->action->element('dt', null, _('Local views'));
+        $this->action->elementStart('dd');
+
+        # action => array('prompt', 'title')
+        $menu =
+          array('imsettings' =>
+                array(_('IM'),
+                      _('Updates by instant messenger (IM)')),
+                'smssettings' =>
+                array(_('SMS'),
+                      _('Updates by SMS')),
+                'twittersettings' =>
+                array(_('Twitter'),
+                      _('Twitter integration options')),
+                'FBConnectSettings' =>
+                array(_('Facebook'),
+                      _('Facebook Connect settings')));
+
+        $action_name = $this->action->trimmed('action');
+        $this->action->elementStart('ul', array('class' => 'nav'));
+
+        foreach ($menu as $menuaction => $menudesc) {
+            if ($menuaction == 'imsettings' &&
+                !common_config('xmpp', 'enabled')) {
+                continue;
+            }
+            $this->action->menuItem(common_local_url($menuaction),
+                                   $menudesc[0],
+                                   $menudesc[1],
+                                   $action_name === $menuaction);
+        }
+
+        $this->action->elementEnd('ul');
+
+        $this->action->elementEnd('dd');
+        $this->action->elementEnd('dl');
+    }
+}
diff --git a/plugins/FBConnect/FBConnectAuth.php b/plugins/FBConnect/FBConnectAuth.php
new file mode 100644 (file)
index 0000000..233eb83
--- /dev/null
@@ -0,0 +1,416 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Plugin to enable Facebook Connect
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Plugin
+ * @package   Laconica
+ * @author    Zach Copley <zach@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+require_once INSTALLDIR . '/plugins/FBConnect/FBConnectPlugin.php';
+
+class FBConnectauthAction extends Action
+{
+
+    var $fbuid      = null;
+    var $fb_fields  = null;
+
+    function prepare($args) {
+        parent::prepare($args);
+
+        try {
+
+            $this->fbuid = getFacebook()->get_loggedin_user();
+
+            if ($this->fbuid > 0) {
+                $this->fb_fields = $this->getFacebookFields($this->fbuid,
+                    array('first_name', 'last_name', 'name'));
+            } else {
+                common_debug("No Facebook User found.");
+            }
+
+        } catch (Exception $e) {
+            common_log(LOG_WARNING, 'Problem getting Facebook uid: ' .
+                $e->getMessage());
+        }
+
+        return true;
+    }
+
+    function handle($args)
+    {
+        parent::handle($args);
+
+        if (common_is_real_login()) {
+            $this->clientError(_('Already logged in.'));
+        } else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+
+            $token = $this->trimmed('token');
+            if (!$token || $token != common_session_token()) {
+                $this->showForm(_('There was a problem with your session token. Try again, please.'));
+                return;
+            }
+            if ($this->arg('create')) {
+                if (!$this->boolean('license')) {
+                    $this->showForm(_('You can\'t register if you don\'t agree to the license.'),
+                                    $this->trimmed('newname'));
+                    return;
+                }
+                $this->createNewUser();
+            } else if ($this->arg('connect')) {
+                $this->connectUser();
+            } else {
+                common_debug(print_r($this->args, true), __FILE__);
+                $this->showForm(_('Something weird happened.'),
+                                $this->trimmed('newname'));
+            }
+        } else {
+            $this->tryLogin();
+        }
+    }
+
+    function showPageNotice()
+    {
+        if ($this->error) {
+            $this->element('div', array('class' => 'error'), $this->error);
+        } else {
+            $this->element('div', 'instructions',
+                           sprintf(_('This is the first time you\'ve logged into %s so we must connect your Facebook to a local account. You can either create a new account, or connect with your existing account, if you have one.'), common_config('site', 'name')));
+        }
+    }
+
+    function title()
+    {
+        return _('Facebook Account Setup');
+    }
+
+    function showForm($error=null, $username=null)
+    {
+        $this->error = $error;
+        $this->username = $username;
+
+        $this->showPage();
+    }
+
+    function showPage()
+    {
+        parent::showPage();
+    }
+
+    function showContent()
+    {
+        if (!empty($this->message_text)) {
+            $this->element('p', null, $this->message);
+            return;
+        }
+
+        $this->elementStart('form', array('method' => 'post',
+                                          'id' => 'form_settings_facebook_connect',
+                                          'class' => 'form_settings',
+                                          'action' => common_local_url('FBConnectAuth')));
+        $this->elementStart('fieldset', array('id' => 'settings_facebook_connect_options'));
+        $this->element('legend', null, _('Connection options'));
+        $this->elementStart('ul', 'form_data');
+        $this->elementStart('li');
+        $this->element('input', array('type' => 'checkbox',
+                                      'id' => 'license',
+                                      'class' => 'checkbox',
+                                      'name' => 'license',
+                                      'value' => 'true'));
+        $this->elementStart('label', array('class' => 'checkbox', 'for' => 'license'));
+        $this->text(_('My text and files are available under '));
+        $this->element('a', array('href' => common_config('license', 'url')),
+                       common_config('license', 'title'));
+        $this->text(_(' except this private data: password, email address, IM address, phone number.'));
+        $this->elementEnd('label');
+        $this->elementEnd('li');
+        $this->elementEnd('ul');
+
+        $this->elementStart('fieldset');
+        $this->hidden('token', common_session_token());
+        $this->element('legend', null,
+                       _('Create new account'));
+        $this->element('p', null,
+                       _('Create a new user with this nickname.'));
+        $this->elementStart('ul', 'form_data');
+        $this->elementStart('li');
+        $this->input('newname', _('New nickname'),
+                     ($this->username) ? $this->username : '',
+                     _('1-64 lowercase letters or numbers, no punctuation or spaces'));
+        $this->elementEnd('li');
+        $this->elementEnd('ul');
+        $this->submit('create', _('Create'));
+        $this->elementEnd('fieldset');
+
+        $this->elementStart('fieldset');
+        $this->element('legend', null,
+                       _('Connect existing account'));
+        $this->element('p', null,
+                       _('If you already have an account, login with your username and password to connect it to your Facebook.'));
+        $this->elementStart('ul', 'form_data');
+        $this->elementStart('li');
+        $this->input('nickname', _('Existing nickname'));
+        $this->elementEnd('li');
+        $this->elementStart('li');
+        $this->password('password', _('Password'));
+        $this->elementEnd('li');
+        $this->elementEnd('ul');
+        $this->submit('connect', _('Connect'));
+        $this->elementEnd('fieldset');
+
+        $this->elementEnd('fieldset');
+        $this->elementEnd('form');
+    }
+
+    function message($msg)
+    {
+        $this->message_text = $msg;
+        $this->showPage();
+    }
+
+    function createNewUser()
+    {
+
+        if (common_config('site', 'closed')) {
+            $this->clientError(_('Registration not allowed.'));
+            return;
+        }
+
+        $invite = null;
+
+        if (common_config('site', 'inviteonly')) {
+            $code = $_SESSION['invitecode'];
+            if (empty($code)) {
+                $this->clientError(_('Registration not allowed.'));
+                return;
+            }
+
+            $invite = Invitation::staticGet($code);
+
+            if (empty($invite)) {
+                $this->clientError(_('Not a valid invitation code.'));
+                return;
+            }
+        }
+
+        $nickname = $this->trimmed('newname');
+
+        if (!Validate::string($nickname, array('min_length' => 1,
+                                               'max_length' => 64,
+                                               'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
+            $this->showForm(_('Nickname must have only lowercase letters and numbers and no spaces.'));
+            return;
+        }
+
+        if (!User::allowed_nickname($nickname)) {
+            $this->showForm(_('Nickname not allowed.'));
+            return;
+        }
+
+        if (User::staticGet('nickname', $nickname)) {
+            $this->showForm(_('Nickname already in use. Try another one.'));
+            return;
+        }
+
+        $fullname = trim($this->fb_fields['firstname'] .
+            ' ' . $this->fb_fields['lastname']);
+
+        $args = array('nickname' => $nickname, 'fullname' => $fullname);
+
+        if (!empty($invite)) {
+            $args['code'] = $invite->code;
+        }
+
+        $user = User::register($args);
+
+        $result = $this->flinkUser($user->id, $this->fbuid);
+
+        if (!$result) {
+            $this->serverError(_('Error connecting user to Facebook.'));
+            return;
+        }
+
+        common_set_user($user);
+        common_real_login(true);
+
+        common_debug("Registered new user $user->id from Facebook user $this->fbuid");
+
+        common_redirect(common_local_url('showstream', array('nickname' => $user->nickname)),
+                        303);
+    }
+
+    function connectUser()
+    {
+        $nickname = $this->trimmed('nickname');
+        $password = $this->trimmed('password');
+
+        if (!common_check_user($nickname, $password)) {
+            $this->showForm(_('Invalid username or password.'));
+            return;
+        }
+
+        $user = User::staticGet('nickname', $nickname);
+
+        if ($user) {
+            common_debug("Legit user to connect to Facebook: $nickname");
+        }
+
+        $result = $this->flinkUser($user->id, $this->fbuid);
+
+        if (!$result) {
+            $this->serverError(_('Error connecting user to Facebook.'));
+            return;
+        }
+
+        common_debug("Connected Facebook user $this->fbuid to local user $user->id");
+
+        common_set_user($user);
+        common_real_login(true);
+
+        $this->goHome($user->nickname);
+    }
+
+    function tryLogin()
+    {
+        common_debug("Trying Facebook Login...");
+
+        $flink = Foreign_link::getByForeignID($this->fbuid, FACEBOOK_CONNECT_SERVICE);
+
+        if ($flink) {
+            $user = $flink->getUser();
+
+            if ($user) {
+
+                common_debug("Logged in Facebook user $flink->foreign_id as user $user->id ($user->nickname)");
+
+                common_set_user($user);
+                common_real_login(true);
+                $this->goHome($user->nickname);
+            }
+
+        } else {
+
+            common_debug("No flink found for fbuid: $this->fbuid");
+
+            $this->showForm(null, $this->bestNewNickname());
+        }
+    }
+
+    function goHome($nickname)
+    {
+        $url = common_get_returnto();
+        if ($url) {
+            // We don't have to return to it again
+            common_set_returnto(null);
+        } else {
+            $url = common_local_url('all',
+                                    array('nickname' =>
+                                          $nickname));
+        }
+
+        common_redirect($url, 303);
+    }
+
+    function flinkUser($user_id, $fbuid)
+    {
+        $flink = new Foreign_link();
+        $flink->user_id = $user_id;
+        $flink->foreign_id = $fbuid;
+        $flink->service = FACEBOOK_CONNECT_SERVICE;
+        $flink->created = common_sql_now();
+
+        $flink_id = $flink->insert();
+
+        return $flink_id;
+    }
+
+    function bestNewNickname()
+    {
+        if (!empty($this->fb_fields['name'])) {
+            $nickname = $this->nicknamize($this->fb_fields['name']);
+            if ($this->isNewNickname($nickname)) {
+                return $nickname;
+            }
+        }
+
+        // Try the full name
+
+        $fullname = trim($this->fb_fields['firstname'] .
+            ' ' . $this->fb_fields['lastname']);
+
+        if (!empty($fullname)) {
+            $fullname = $this->nicknamize($fullname);
+            if ($this->isNewNickname($fullname)) {
+                return $fullname;
+            }
+        }
+
+        return null;
+    }
+
+     // Given a string, try to make it work as a nickname
+
+     function nicknamize($str)
+     {
+         $str = preg_replace('/\W/', '', $str);
+         return strtolower($str);
+     }
+
+    function isNewNickname($str)
+    {
+        if (!Validate::string($str, array('min_length' => 1,
+                                          'max_length' => 64,
+                                          'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
+            return false;
+        }
+        if (!User::allowed_nickname($str)) {
+            return false;
+        }
+        if (User::staticGet('nickname', $str)) {
+            return false;
+        }
+        return true;
+    }
+
+    // XXX: Consider moving this to lib/facebookutil.php
+    function getFacebookFields($fb_uid, $fields) {
+        try {
+
+            $facebook = getFacebook();
+
+            $infos = $facebook->api_client->users_getInfo($fb_uid, $fields);
+
+            if (empty($infos)) {
+                return null;
+            }
+            return reset($infos);
+
+        } catch (Exception $e) {
+            common_log(LOG_WARNING, "Facebook client failure when requesting " .
+                join(",", $fields) . " on uid " . $fb_uid .
+                    " : ". $e->getMessage());
+            return null;
+        }
+    }
+
+}
diff --git a/plugins/FBConnect/FBConnectLogin.php b/plugins/FBConnect/FBConnectLogin.php
new file mode 100644 (file)
index 0000000..7989dc8
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, 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/>.
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+require_once INSTALLDIR . '/plugins/FBConnect/FBConnectPlugin.php';
+
+class FBConnectLoginAction extends Action
+{
+    function handle($args)
+    {
+        parent::handle($args);
+
+        if (common_is_real_login()) {
+            $this->clientError(_('Already logged in.'));
+        }
+
+        $this->showPage();
+    }
+
+    function getInstructions()
+    {
+        return _('Login with your Facebook Account');
+    }
+
+    function showPageNotice()
+    {
+        $instr = $this->getInstructions();
+        $output = common_markup_to_html($instr);
+        $this->elementStart('div', 'instructions');
+        $this->raw($output);
+        $this->elementEnd('div');
+    }
+
+    function title()
+    {
+        return _('Facebook Login');
+    }
+
+    function showContent() {
+
+        $this->elementStart('fieldset');
+
+
+        $this->element('fb:login-button', array('onlogin' => 'goto_login()',
+            'length' => 'long'));
+
+        $this->elementEnd('fieldset');
+    }
+
+}
diff --git a/plugins/FBConnect/FBConnectPlugin.css b/plugins/FBConnect/FBConnectPlugin.css
new file mode 100644 (file)
index 0000000..564fdae
--- /dev/null
@@ -0,0 +1,37 @@
+/** Styles for Facebook logo and Facebook user profile avatar.
+ *
+ * @package   Laconica
+ * @author Sarven Capadisli <csarven@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+#site_nav_global_primary #nav_fb {
+position:relative;
+margin-left:18px;
+margin-right:-7px;
+}
+
+#nav_fb .fb_profile_pic_rendered img {
+position:relative;
+top:3px;
+left:0;
+display:inline;
+border:1px solid #3B5998;
+padding:1px;
+}
+
+#nav_fb img {
+position:absolute;
+top:-13px;
+left:-11px;
+display:inline;
+}
+
+#settings_facebook_connect_options legend {
+display:none;
+}
+#form_settings_facebook_connect fieldset fieldset legend {
+display:block;
+}
diff --git a/plugins/FBConnect/FBConnectPlugin.php b/plugins/FBConnect/FBConnectPlugin.php
new file mode 100644 (file)
index 0000000..ad5e47e
--- /dev/null
@@ -0,0 +1,278 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Plugin to enable Facebook Connect
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Plugin
+ * @package   Laconica
+ * @author    Zach Copley <zach@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+define("FACEBOOK_CONNECT_SERVICE", 3);
+
+require_once INSTALLDIR . '/lib/facebookutil.php';
+require_once INSTALLDIR . '/plugins/FBConnect/FBConnectAuth.php';
+require_once INSTALLDIR . '/plugins/FBConnect/FBConnectLogin.php';
+require_once INSTALLDIR . '/plugins/FBConnect/FBConnectSettings.php';
+require_once INSTALLDIR . '/plugins/FBConnect/FBCLoginGroupNav.php';
+require_once INSTALLDIR . '/plugins/FBConnect/FBCSettingsNav.php';
+
+/**
+ * Plugin to enable Facebook Connect
+ *
+ * @category Plugin
+ * @package  Laconica
+ * @author   Zach Copley <zach@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ */
+
+class FBConnectPlugin extends Plugin
+{
+    function __construct()
+    {
+        parent::__construct();
+    }
+
+    // Hook in new actions
+    function onRouterInitialized(&$m) {
+        $m->connect('main/facebookconnect', array('action' => 'FBConnectAuth'));
+        $m->connect('main/facebooklogin', array('action' => 'FBConnectLogin'));
+        $m->connect('settings/facebook', array('action' => 'FBConnectSettings'));
+     }
+
+    // Add in xmlns:fb
+    function onStartShowHTML($action)
+    {
+        $httpaccept = isset($_SERVER['HTTP_ACCEPT']) ?
+        $_SERVER['HTTP_ACCEPT'] : null;
+
+        // XXX: allow content negotiation for RDF, RSS, or XRDS
+
+        $cp = common_accept_to_prefs($httpaccept);
+        $sp = common_accept_to_prefs(PAGE_TYPE_PREFS);
+
+        $type = common_negotiate_type($cp, $sp);
+
+        if (!$type) {
+            throw new ClientException(_('This page is not available in a '.
+                                         'media type you accept'), 406);
+        }
+
+        header('Content-Type: '.$type);
+
+        $action->extraHeaders();
+
+        $action->startXML('html',
+            '-//W3C//DTD XHTML 1.0 Strict//EN',
+            'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd');
+
+        $language = $action->getLanguage();
+
+        $action->elementStart('html',
+            array('xmlns'  => 'http://www.w3.org/1999/xhtml',
+                  'xmlns:fb' => 'http://www.facebook.com/2008/fbml',
+                  'xml:lang' => $language,
+                  'lang'     => $language));
+
+        return false;
+    }
+
+    function onEndShowLaconicaScripts($action)
+    {
+        $action->element('script',
+            array('type' => 'text/javascript',
+                  'src'  => 'http://static.ak.connect.facebook.com/js/api_lib/v0.4/FeatureLoader.js.php'),
+                  ' ');
+
+        $apikey = common_config('facebook', 'apikey');
+        $plugin_path = common_path('plugins/FBConnect');
+
+        $login_url = common_local_url('FBConnectAuth');
+        $logout_url = common_local_url('logout');
+
+        $html = sprintf('<script type="text/javascript">FB.init("%s", "%s/xd_receiver.htm");
+
+                            function goto_login() {
+                                window.location = "%s";
+                            }
+
+                            function goto_logout() {
+                                window.location = "%s";
+                            }
+
+                         </script>', $apikey, $plugin_path, $login_url, $logout_url);
+
+
+        $action->raw($html);
+    }
+
+    function onEndShowLaconicaStyles($action)
+    {
+        $action->element('link', array('rel' => 'stylesheet',
+            'type' => 'text/css',
+            'href' => common_path('plugins/FBConnect/FBConnectPlugin.css')));
+    }
+
+    function onStartPrimaryNav($action)
+    {
+        $user = common_current_user();
+
+        if ($user) {
+
+            $action->menuItem(common_local_url('all', array('nickname' => $user->nickname)),
+                _('Home'), _('Personal profile and friends timeline'), false, 'nav_home');
+            $action->menuItem(common_local_url('profilesettings'),
+                _('Account'), _('Change your email, avatar, password, profile'), false, 'nav_account');
+            if (common_config('xmpp', 'enabled')) {
+                $action->menuItem(common_local_url('imsettings'),
+                    _('Connect'), _('Connect to IM, SMS, Twitter'), false, 'nav_connect');
+            } else {
+             $action->menuItem(common_local_url('smssettings'),
+                 _('Connect'), _('Connect to SMS, Twitter'), false, 'nav_connect');
+            }
+            $action->menuItem(common_local_url('invite'),
+                _('Invite'),
+                sprintf(_('Invite friends and colleagues to join you on %s'),
+                common_config('site', 'name')),
+                false, 'nav_invitecontact');
+
+            $flink = Foreign_link::getByUserId($user->id, FACEBOOK_CONNECT_SERVICE);
+            $fbuid = 0;
+
+            if ($flink) {
+
+                try {
+
+                    $facebook = getFacebook();
+                    $fbuid = getFacebook()->get_loggedin_user();
+
+                } catch (Exception $e) {
+                    common_log(LOG_WARNING,
+                        'Problem getting Facebook client: ' .
+                            $e->getMessage());
+                }
+
+                // Display Facebook Logged in indicator w/Facebook favicon
+
+                if ($fbuid > 0) {
+
+                    $action->elementStart('li', array('id' => 'nav_fb'));
+                    $action->elementStart('fb:profile-pic', array('uid' => $flink->foreign_id,
+                        'linked' => 'false',
+                        'width' => 16,
+                        'height' => 16));
+                    $action->elementEnd('fb:profile-pic');
+
+                    $iconurl =  common_path('/plugins/FBConnect/fbfavicon.ico');
+                    $action->element('img', array('src' => $iconurl));
+
+                    $action->elementEnd('li');
+
+                }
+            }
+
+            // Need to override the Logout link to make it do FB stuff
+
+            if ($fbuid > 0) {
+
+                $logout_url = common_local_url('logout');
+                $title =  _('Logout from the site');
+                $text = _('Logout');
+
+                $html = sprintf('<li id="nav_logout"><a href="%s" title="%s" ' .
+                    'onclick="FB.Connect.logout(function() { goto_logout() })">%s</a></li>',
+                    $logout_url, $title, $text);
+
+                $action->raw($html);
+
+             } else {
+                 $action->menuItem(common_local_url('logout'),
+                     _('Logout'), _('Logout from the site'), false, 'nav_logout');
+             }
+         }
+         else {
+             if (!common_config('site', 'closed')) {
+                 $action->menuItem(common_local_url('register'),
+                     _('Register'), _('Create an account'), false, 'nav_register');
+             }
+             $action->menuItem(common_local_url('openidlogin'),
+                 _('OpenID'), _('Login with OpenID'), false, 'nav_openid');
+             $action->menuItem(common_local_url('login'),
+                 _('Login'), _('Login to the site'), false, 'nav_login');
+         }
+
+         $action->menuItem(common_local_url('doc', array('title' => 'help')),
+             _('Help'), _('Help me!'), false, 'nav_help');
+         $action->menuItem(common_local_url('peoplesearch'),
+             _('Search'), _('Search for people or text'), false, 'nav_search');
+
+        return false;
+    }
+
+    function onStartShowLocalNavBlock($action)
+    {
+        $action_name = get_class($action);
+
+        $login_actions = array('LoginAction', 'RegisterAction',
+            'OpenidloginAction', 'FBConnectLoginAction');
+
+        if (in_array($action_name, $login_actions)) {
+            $nav = new FBCLoginGroupNav($action);
+            $nav->show();
+            return false;
+        }
+
+        $connect_actions = array('SmssettingsAction',
+            'TwittersettingsAction', 'FBConnectSettingsAction');
+
+        if (in_array($action_name, $connect_actions)) {
+            $nav = new FBCSettingsNav($action);
+            $nav->show();
+            return false;
+        }
+
+        return true;
+    }
+
+    function onEndLogout($action)
+    {
+        try {
+
+            $facebook = getFacebook();
+            $fbuid = $facebook->get_loggedin_user();
+
+            if ($fbuid > 0) {
+                $facebook->logout(common_local_url('public'));
+            }
+
+        } catch (Exception $e) {
+            common_log(LOG_WARNING, 'Could\'t logout of Facebook: ' .
+                $e->getMessage());
+        }
+    }
+
+}
diff --git a/plugins/FBConnect/FBConnectSettings.php b/plugins/FBConnect/FBConnectSettings.php
new file mode 100644 (file)
index 0000000..7e255f4
--- /dev/null
@@ -0,0 +1,194 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Facebook Connect settings
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Settings
+ * @package   Laconica
+ * @author    Zach Copley <zach@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+require_once INSTALLDIR.'/lib/connectsettingsaction.php';
+
+/**
+ * Facebook Connect settings action
+ *
+ * @category Settings
+ * @package  Laconica
+ * @author   Zach Copley <zach@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ */
+
+class FBConnectSettingsAction extends ConnectSettingsAction
+{
+    /**
+     * Title of the page
+     *
+     * @return string Title of the page
+     */
+
+    function title()
+    {
+        return _('Facebook Connect Settings');
+    }
+
+    /**
+     * Instructions for use
+     *
+     * @return instructions for use
+     */
+
+    function getInstructions()
+    {
+        return _('Manage how your account connects to Facebook');
+    }
+
+    /**
+     * Content area of the page
+     *
+     * Shows a form for uploading an avatar.
+     *
+     * @return void
+     */
+
+    function showContent()
+    {
+        $user = common_current_user();
+
+        $flink = Foreign_link::getByUserID($user->id, FACEBOOK_CONNECT_SERVICE);
+
+        if (!$flink) {
+
+            $this->element('p', 'form_note',
+                _('There is no Facebook user connected to this account.'));
+
+            $this->element('fb:login-button', array('onlogin' => 'goto_login()',
+                'length' => 'long'));
+
+            return;
+        }
+
+        $this->element('p', 'form_note',
+                       _('Connected Facebook user:'));
+
+        $this->elementStart('p', array('class' => 'facebook-user-display'));
+        $this->elementStart('fb:profile-pic',
+            array('uid' => $flink->foreign_id,
+                  'size' => 'square',
+                  'linked' => 'true',
+                  'facebook-logo' => 'true'));
+        $this->elementEnd('fb:profile-pic');
+
+        $this->elementStart('fb:name', array('uid' => $flink->foreign_id));
+        $this->elementEnd('fb:name');
+        $this->elementEnd('p');
+
+        $this->elementStart('form', array('method' => 'post',
+                                          'id' => 'form_settings_facebook',
+                                          'class' => 'form_settings',
+                                          'action' =>
+                                          common_local_url('FBConnectSettings')));
+
+        $this->hidden('token', common_session_token());
+
+        $this->elementStart('fieldset');
+
+        $this->element('legend', null, _('Disconnect my account from Facebook'));
+
+        if (!$user->password) {
+
+            $this->elementStart('p', array('class' => 'form_guide'));
+            $this->text(_('Disconnecting your Faceboook ' .
+                          'would make it impossible to log in! Please '));
+            $this->element('a',
+                array('href' => common_local_url('passwordsettings')),
+                    _('set a password'));
+
+            $this->text(_(' first.'));
+            $this->elementEnd('p');
+        } else {
+            $this->submit('disconnect', _('Disconnect'));
+         }
+
+        $this->elementEnd('fieldset');
+        $this->elementEnd('form');
+    }
+
+    /**
+     * Handle post
+     *
+     * Disconnects the current Facebook user from the current user's account
+     *
+     * @return void
+     */
+
+    function handlePost()
+    {
+        // CSRF protection
+        $token = $this->trimmed('token');
+        if (!$token || $token != common_session_token()) {
+            $this->showForm(_('There was a problem with your session token. '.
+                              'Try again, please.'));
+            return;
+        }
+
+        if ($this->arg('disconnect')) {
+
+            $user = common_current_user();
+
+            $flink = Foreign_link::getByUserID($user->id, FACEBOOK_CONNECT_SERVICE);
+            $result = $flink->delete();
+
+            if ($result === false) {
+                common_log_db_error($user, 'DELETE', __FILE__);
+                $this->serverError(_('Couldn\'t delete link to Facebook.'));
+                return;
+            }
+
+            try {
+
+                // XXX: not sure what exactly to do here
+
+                $facebook = getFacebook();
+                $facebook->clear_cookie_state();
+
+            } catch (Exception $e) {
+                common_log(LOG_WARNING,
+                    'Couldn\'t clear Facebook cookies: ' .
+                        $e->getMessage());
+            }
+
+            $this->showForm(_('Facebook user disconnected.'), true);
+
+        } else {
+            $this->showForm(_('Not sure what you\'re trying to do.'));
+            return;
+        }
+
+    }
+
+}
diff --git a/plugins/FBConnect/fbfavicon.ico b/plugins/FBConnect/fbfavicon.ico
new file mode 100644 (file)
index 0000000..c57c034
Binary files /dev/null and b/plugins/FBConnect/fbfavicon.ico differ
diff --git a/plugins/FBConnect/xd_receiver.htm b/plugins/FBConnect/xd_receiver.htm
new file mode 100644 (file)
index 0000000..43fb2c4
--- /dev/null
@@ -0,0 +1,10 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" >
+<head>
+    <title>cross domain receiver page</title>
+</head>
+<body>
+    <script src="http://static.ak.connect.facebook.com/js/api_lib/v0.4/XdCommReceiver.debug.js" type="text/javascript"></script>
+</body>
+</html>
index 881ead99ec29d091d5a13729b9fa1d3eb8f87076..93a0294c4ca9cbd23cd720c4909089b667494136 100644 (file)
@@ -121,6 +121,12 @@ class LinkbackPlugin extends Plugin
     {
         $args = array($this->notice->uri, $url);
 
+        if (!extension_loaded('xmlrpc')) {
+            if (!dl('xmlrpc.so')) {
+                common_log(LOG_ERR, "Can't pingback; xmlrpc extension not available.");
+            }
+        }
+
         $request = xmlrpc_encode_request('pingback.ping', $args);
         $context = stream_context_create(array('http' => array('method' => "POST",
                                                                'header' =>
@@ -141,7 +147,7 @@ class LinkbackPlugin extends Plugin
     }
 
     // Largely cadged from trackback_cls.php by
-    // Ran Aroussi <ran@blogish.org>, GPL2
+    // Ran Aroussi <ran@blogish.org>, GPL2 or any later version
     // http://phptrackback.sourceforge.net/
 
     function getTrackback($text, $url)
diff --git a/plugins/WikiHashtagsPlugin.php b/plugins/WikiHashtagsPlugin.php
new file mode 100644 (file)
index 0000000..6d186a5
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Plugin to show WikiHashtags content in the sidebar
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category  Plugin
+ * @package   Laconica
+ * @author    Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2008 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+    exit(1);
+}
+
+define('WIKIHASHTAGSPLUGIN_VERSION', '0.1');
+
+/**
+ * Plugin to use WikiHashtags
+ *
+ * @category Plugin
+ * @package  Laconica
+ * @author   Evan Prodromou <evan@controlyourself.ca>
+ * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link     http://laconi.ca/
+ *
+ * @see      Event
+ */
+
+class WikiHashtagsPlugin extends Plugin
+{
+    function __construct($code=null)
+    {
+        parent::__construct();
+    }
+
+    function onStartShowSections($action)
+    {
+        $name = $action->trimmed('action');
+
+        if ($name == 'tag') {
+
+            $taginput = $action->trimmed('tag');
+            $tag = common_canonical_tag($taginput);
+
+            if (!empty($tag)) {
+
+                $url = sprintf('http://hashtags.wikia.com/index.php?title=%s&action=render',
+                               urlencode($tag));
+                $editurl = sprintf('http://hashtags.wikia.com/index.php?title=%s&action=edit',
+                                   urlencode($tag));
+
+                $context = stream_context_create(array('http' => array('method' => "GET",
+                                                                       'header' =>
+                                                                       "User-Agent: " . $this->userAgent())));
+                $html = @file_get_contents($url, false, $context);
+
+                $action->elementStart('div', array('id' => 'wikihashtags', 'class' => 'section'));
+
+                if (!empty($html)) {
+                    $action->element('style', null,
+                                     "span.editsection { display: none }\n".
+                                     "table.toc { display: none }");
+                    $action->raw($html);
+                    $action->elementStart('p');
+                    $action->element('a', array('href' => $editurl,
+                                                'title' => sprintf(_('Edit the article for #%s on WikiHashtags'), $tag)),
+                                     _('Edit'));
+                    $action->element('a', array('href' => 'http://www.gnu.org/copyleft/fdl.html',
+                                                'title' => _('Shared under the terms of the GNU Free Documentation License'),
+                                                'rel' => 'license'),
+                                     'GNU FDL');
+                    $action->elementEnd('p');
+                } else {
+                    $action->element('a', array('href' => $editurl),
+                                     sprintf(_('Start the article for #%s on WikiHashtags'), $tag));
+                }
+
+                $action->elementEnd('div');
+            }
+        }
+
+        return true;
+    }
+
+    function userAgent()
+    {
+        return 'WikiHashtagsPlugin/'.WIKIHASHTAGSPLUGIN_VERSION .
+          ' Laconica/' . LACONICA_VERSION;
+    }
+}
diff --git a/scripts/getvaliddaemons.php b/scripts/getvaliddaemons.php
new file mode 100755 (executable)
index 0000000..a10233e
--- /dev/null
@@ -0,0 +1,55 @@
+#!/usr/bin/env php
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, 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/>.
+ */
+
+/**
+ * Utility script to get a list of daemons that should run, based on the
+ * current configuration. This is used by startdaemons.sh to determine what
+ * it should and shouldn't start up. The output is a list of space-separated
+ * daemon names.
+ */
+
+
+# Abort if called from a web server
+if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
+    print "This script must be run from the command line\n";
+    exit();
+}
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
+define('LACONICA', true);
+
+require_once(INSTALLDIR . '/lib/common.php');
+
+if(common_config('xmpp','enabled')) {
+    echo "xmppdaemon.php jabberqueuehandler.php publicqueuehandler.php ";
+    echo "xmppconfirmhandler.php ";
+}
+if(common_config('memcached','enabled')) {
+    echo "memcachedqueuehandler.php ";
+}
+if(common_config('twitterbridge','enabled')) {
+    echo "twitterstatusfetcher.php ";
+}
+echo "ombqueuehandler.php ";
+echo "twitterqueuehandler.php ";
+echo "facebookqueuehandler.php ";
+echo "pingqueuehandler.php ";
+echo "inboxqueuehandler.php ";
+echo "smsqueuehandler.php ";
diff --git a/scripts/inboxqueuehandler.php b/scripts/inboxqueuehandler.php
new file mode 100755 (executable)
index 0000000..73d31e8
--- /dev/null
@@ -0,0 +1,69 @@
+#!/usr/bin/env php
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008,2009 Control Yourself, 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/>.
+ */
+
+// Abort if called from a web server
+
+if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
+    print "This script must be run from the command line\n";
+    exit();
+}
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
+define('LACONICA', true);
+
+require_once(INSTALLDIR . '/lib/common.php');
+require_once(INSTALLDIR . '/lib/queuehandler.php');
+
+set_error_handler('common_error_handler');
+
+class InboxQueueHandler extends QueueHandler
+{
+    function transport()
+    {
+        return 'inbox';
+    }
+
+       function start() {
+               $this->log(LOG_INFO, "INITIALIZE");
+               return true;
+       }
+
+    function handle_notice($notice)
+    {
+        $this->log(LOG_INFO, "Distributing notice to inboxes for $notice->id");
+        $notice->addToInboxes();
+        $notice->blowSubsCache();
+        return true;
+    }
+
+       function finish() {
+       }
+}
+
+ini_set("max_execution_time", "0");
+ini_set("max_input_time", "0");
+set_time_limit(0);
+mb_internal_encoding('UTF-8');
+
+$id = ($argc > 1) ? $argv[1] : null;
+
+$handler = new InboxQueueHandler($id);
+
+$handler->runOnce();
index 924fc45453ecc2ef08d5b570962c38d27ab6d839..8b6e974c0aa7af3a87cca507ccfbf10f4d6b1920 100755 (executable)
@@ -54,6 +54,13 @@ class JabberQueueHandler extends XmppQueueHandler
     }
 }
 
+// Abort immediately if xmpp is not enabled, otherwise the daemon chews up
+// lots of CPU trying to connect to unconfigured servers
+if (common_config('xmpp','enabled')==false) {
+    print "Aborting daemon - xmpp is disabled\n";
+    exit();
+}
+
 ini_set("max_execution_time", "0");
 ini_set("max_input_time", "0");
 set_time_limit(0);
@@ -63,4 +70,4 @@ $resource = ($argc > 1) ? $argv[1] : (common_config('xmpp','resource') . '-queue
 
 $handler = new JabberQueueHandler($resource);
 
-$handler->runOnce();
\ No newline at end of file
+$handler->runOnce();
diff --git a/scripts/memcachedqueuehandler.php b/scripts/memcachedqueuehandler.php
new file mode 100755 (executable)
index 0000000..185b781
--- /dev/null
@@ -0,0 +1,70 @@
+#!/usr/bin/env php
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008,2009 Control Yourself, 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/>.
+ */
+
+// Abort if called from a web server
+
+if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
+    print "This script must be run from the command line\n";
+    exit();
+}
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
+define('LACONICA', true);
+
+require_once(INSTALLDIR . '/lib/common.php');
+require_once(INSTALLDIR . '/lib/queuehandler.php');
+
+set_error_handler('common_error_handler');
+
+class MemcachedQueueHandler extends QueueHandler
+{
+    function transport()
+    {
+        return 'memcache';
+    }
+
+       function start() {
+               $this->log(LOG_INFO, "INITIALIZE");
+               return true;
+       }
+
+    function handle_notice($notice)
+    {
+        // XXX: fork here
+        $this->log(LOG_INFO, "Blowing memcached for $notice->id");
+        $notice->blowCaches();
+        return true;
+    }
+
+       function finish() {
+       }
+
+}
+
+ini_set("max_execution_time", "0");
+ini_set("max_input_time", "0");
+set_time_limit(0);
+mb_internal_encoding('UTF-8');
+
+$id = ($argc > 1) ? $argv[1] : null;
+
+$handler = new MemcachedQueueHandler($id);
+
+$handler->runOnce();
index 5075c12df578aeeec4f45ce8db0bc430902dfe6f..b0fa22d438e6a66c0c3931e33965a739e4afe087 100755 (executable)
@@ -52,6 +52,13 @@ class PublicQueueHandler extends XmppQueueHandler
     }
 }
 
+// Abort immediately if xmpp is not enabled, otherwise the daemon chews up
+// lots of CPU trying to connect to unconfigured servers
+if (common_config('xmpp','enabled')==false) {
+    print "Aborting daemon - xmpp is disabled\n";
+    exit();
+}
+
 ini_set("max_execution_time", "0");
 ini_set("max_input_time", "0");
 set_time_limit(0);
index c3729761d06a65f63455b76fc6df1d12e702d58a..3869e95c4c3cf5ae2735f54dbff600349af6f7d8 100755 (executable)
 # Note that the 'maildaemon' needs to run as a mail filter.
 
 DIR=`dirname $0`
+DAEMONS=`php $DIR/getvaliddaemons.php`
 
-for f in xmppdaemon.php jabberqueuehandler.php publicqueuehandler.php \
-         xmppconfirmhandler.php smsqueuehandler.php ombqueuehandler.php \
-         twitterqueuehandler.php facebookqueuehandler.php pingqueuehandler.php; do
+for f in $DAEMONS; do
 
          echo -n "Starting $f...";
         php $DIR/$f
index 2bb8f9ecb2a6d02c7b66b443c35778c17b95de72..764037e8ff0bb4f06d7d6041d0f06437ab22d748 100755 (executable)
@@ -24,7 +24,8 @@ SDIR=`dirname $0`
 DIR=`php $SDIR/getpiddir.php`
 
 for f in jabberhandler ombhandler publichandler smshandler pinghandler \
-        xmppconfirmhandler xmppdaemon twitterhandler facebookhandler ; do
+        xmppconfirmhandler xmppdaemon twitterhandler facebookhandler \
+         memcachehandler inboxhandler twitterstatusfetcher; do
 
        FILES="$DIR/$f.*.pid"
        for ff in "$FILES" ; do
index 794301f0f02f8b4af9f6fae0294f1a2bc6ec4c2b..bd08ba58d6a9bcba0e1bcfd3af3941f4aaa461d5 100755 (executable)
@@ -32,8 +32,25 @@ define('LACONICA', true);
 
 require_once(INSTALLDIR . '/lib/common.php');
 
+// Make a lockfile
+$lockfilename = lockFilename();
+if (!($lockfile = @fopen($lockfilename, "w"))) {
+    print "Already running... exiting.\n";
+    exit(1);
+}
+
+// Obtain an exlcusive lock on file (will fail if script is already going)
+if (!@flock( $lockfile, LOCK_EX | LOCK_NB, &$wouldblock) || $wouldblock) {
+    // Script already running - abort
+    @fclose($lockfile);
+    print "Already running... exiting.\n";
+    exit(1);
+}
+
 $flink = new Foreign_link();
 $flink->service = 1; // Twitter
+$flink->orderBy('last_friendsync');
+$flink->limit(25);  // sync this many users during this run
 $cnt = $flink->find();
 
 print "Updating Twitter friends subscriptions for $cnt users.\n";
@@ -60,8 +77,11 @@ while ($flink->fetch()) {
             continue;
         }
 
-        $result = save_twitter_friends($user, $fuser->id,
-                       $fuser->nickname, $flink->credentials);
+        save_twitter_friends($user, $fuser->id, $fuser->nickname, $flink->credentials);
+
+        $flink->last_friendsync = common_sql_now();
+        $flink->update();
+
         if (defined('SCRIPT_DEBUG')) {
             print "\nDONE\n";
         } else {
@@ -70,4 +90,18 @@ while ($flink->fetch()) {
     }
 }
 
+function lockFilename()
+{
+    $piddir = common_config('daemon', 'piddir');
+    if (!$piddir) {
+        $piddir = '/var/run';
+    }
+
+    return $piddir . '/synctwitterfriends.lock';
+}
+
+// Cleanup
+fclose($lockfile);
+unlink($lockfilename);
+
 exit(0);
diff --git a/scripts/twitterstatusfetcher.php b/scripts/twitterstatusfetcher.php
new file mode 100755 (executable)
index 0000000..a61ce1b
--- /dev/null
@@ -0,0 +1,577 @@
+#!/usr/bin/env php
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, 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/>.
+ */
+
+// Abort if called from a web server
+if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
+    print "This script must be run from the command line\n";
+    exit();
+}
+
+define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
+define('LACONICA', true);
+
+// Tune number of processes and how often to poll Twitter
+// XXX: Should these things be in config.php?
+define('MAXCHILDREN', 2);
+define('POLL_INTERVAL', 60); // in seconds
+
+// Uncomment this to get useful logging
+define('SCRIPT_DEBUG', true);
+
+require_once(INSTALLDIR . '/lib/common.php');
+require_once(INSTALLDIR . '/lib/daemon.php');
+
+class TwitterStatusFetcher extends Daemon
+{
+
+    private $children = array();
+
+    function name()
+    {
+        return ('twitterstatusfetcher.generic');
+    }
+
+    function run()
+    {
+        do {
+
+            $flinks = $this->refreshFlinks();
+
+            foreach ($flinks as $f){
+
+                // We have to disconnect from the DB before forking so
+                // each sub-process will open its own connection and
+                // avoid stomping on the others
+
+                $conn = &$f->getDatabaseConnection();
+                $conn->disconnect();
+
+                $pid = pcntl_fork();
+
+                if ($pid == -1) {
+                    die ("Couldn't fork!");
+                }
+
+                if ($pid) {
+
+                    // Parent
+                    if (defined('SCRIPT_DEBUG')) {
+                        common_debug("Parent: forked new status fetcher process " . $pid);
+                    }
+
+                    $this->children[] = $pid;
+
+                } else {
+
+                    // Child
+                    $this->getTimeline($f);
+                    exit();
+                }
+
+                // Remove child from ps list as it finishes
+                while(($c = pcntl_wait($status, WNOHANG OR WUNTRACED)) > 0) {
+
+                    if (defined('SCRIPT_DEBUG')) {
+                        common_debug("Child $c finished.");
+                    }
+
+                    $this->remove_ps($this->children, $c);
+                }
+
+                // Wait! We have too many damn kids.
+                if (sizeof($this->children) > MAXCHILDREN) {
+
+                    if (defined('SCRIPT_DEBUG')) {
+                        common_debug('Too many children. Waiting...');
+                    }
+
+                    if (($c = pcntl_wait($status, WUNTRACED)) > 0){
+
+                        if (defined('SCRIPT_DEBUG')) {
+                            common_debug("Finished waiting for $c");
+                        }
+
+                        $this->remove_ps($this->children, $c);
+                    }
+                }
+            }
+
+            // Remove all children from the process list before restarting
+            while(($c = pcntl_wait($status, WUNTRACED)) > 0) {
+
+                if (defined('SCRIPT_DEBUG')) {
+                    common_debug("Child $c finished.");
+                }
+
+                $this->remove_ps($this->children, $c);
+            }
+
+            // Rest for a bit before we fetch more statuses
+
+            if (defined('SCRIPT_DEBUG')) {
+                common_debug('Waiting ' . POLL_INTERVAL .
+                    ' secs before hitting Twitter again.');
+            }
+
+            if (POLL_INTERVAL > 0) {
+                sleep(POLL_INTERVAL);
+            }
+
+        } while (true);
+    }
+
+    function refreshFlinks() {
+
+        $flink = new Foreign_link();
+        $flink->service = 1; // Twitter
+        $flink->orderBy('last_noticesync');
+
+        $cnt = $flink->find();
+
+        if (defined('SCRIPT_DEBUG')) {
+            common_debug('Updating Twitter friends subscriptions' .
+                " for $cnt users.");
+        }
+
+        $flinks = array();
+
+        while ($flink->fetch()) {
+
+            if (($flink->noticesync & FOREIGN_NOTICE_RECV) ==
+                FOREIGN_NOTICE_RECV) {
+                $flinks[] = clone($flink);
+            }
+        }
+
+        $flink->free();
+        unset($flink);
+
+        return $flinks;
+    }
+
+    function remove_ps(&$plist, $ps){
+        for ($i = 0; $i < sizeof($plist); $i++) {
+            if ($plist[$i] == $ps) {
+                unset($plist[$i]);
+                $plist = array_values($plist);
+                break;
+            }
+        }
+    }
+
+    function getTimeline($flink)
+    {
+
+        if (empty($flink)) {
+            common_log(LOG_WARNING,
+                "Can't retrieve Foreign_link for foreign ID $fid");
+            return;
+        }
+
+        $fuser = $flink->getForeignUser();
+
+        if (empty($fuser)) {
+            common_log(LOG_WARNING, "Unmatched user for ID " .
+                $flink->user_id);
+            return;
+        }
+
+        if (defined('SCRIPT_DEBUG')) {
+            common_debug('Trying to get timeline for Twitter user ' .
+                "$fuser->nickname ($flink->foreign_id).");
+        }
+
+        // XXX: Biggest remaining issue - How do we know at which status
+        // to start importing?  How many statuses?  Right now I'm going
+        // with the default last 20.
+
+        $url = 'http://twitter.com/statuses/friends_timeline.json';
+
+        $timeline_json = get_twitter_data($url, $fuser->nickname,
+            $flink->credentials);
+
+        $timeline = json_decode($timeline_json);
+
+        if (empty($timeline)) {
+            common_log(LOG_WARNING, "Empty timeline.");
+            return;
+        }
+
+        // Reverse to preserve order
+        foreach (array_reverse($timeline) as $status) {
+
+            // Hacktastic: filter out stuff coming from this Laconica
+            $source = mb_strtolower(common_config('integration', 'source'));
+
+            if (preg_match("/$source/", mb_strtolower($status->source))) {
+                if (defined('SCRIPT_DEBUG')) {
+                    common_debug('Skipping import of status ' . $status->id .
+                        ' with source ' . $source);
+                }
+                continue;
+            }
+
+            $this->saveStatus($status, $flink);
+        }
+
+        // Okay, record the time we synced with Twitter for posterity
+        $flink->last_noticesync = common_sql_now();
+        $flink->update();
+    }
+
+    function saveStatus($status, $flink)
+    {
+        $id = $this->ensureProfile($status->user);
+        $profile = Profile::staticGet($id);
+
+        if (!$profile) {
+            common_log(LOG_ERR,
+                'Problem saving notice. No associated Profile.');
+            return null;
+        }
+
+        $uri = 'http://twitter.com/' . $status->user->screen_name .
+            '/status/' . $status->id;
+
+        $notice = Notice::staticGet('uri', $uri);
+
+        // check to see if we've already imported the status
+        if (!$notice) {
+
+            $notice = new Notice();
+            $notice->profile_id = $id;
+
+            $notice->query('BEGIN');
+
+            // XXX: figure out reply_to
+            $notice->reply_to = null;
+
+            // XXX: Should this be common_sql_now() instead of status create date?
+
+            $notice->created = strftime('%Y-%m-%d %H:%M:%S',
+                strtotime($status->created_at));
+            $notice->content = $status->text;
+            $notice->rendered = common_render_content($status->text, $notice);
+            $notice->source = 'twitter';
+            $notice->is_local = 0;
+            $notice->uri = $uri;
+
+            $notice_id = $notice->insert();
+
+            if (!$notice_id) {
+                common_log_db_error($notice, 'INSERT', __FILE__);
+                if (defined('SCRIPT_DEBUG')) {
+                    common_debug('Could not save notice!');
+                }
+            }
+
+            // XXX: Figure out a better way to link Twitter replies?
+            $notice->saveReplies();
+
+            // XXX: Do we want to pollute our tag cloud with
+            // hashtags from Twitter?
+            $notice->saveTags();
+            $notice->saveGroups();
+
+            $notice->query('COMMIT');
+
+            if (defined('SCRIPT_DEBUG')) {
+                common_debug("Saved status $status->id" .
+                    " as notice $notice->id.");
+            }
+        }
+
+        if (!Notice_inbox::staticGet('notice_id', $notice->id)) {
+
+            // Add to inbox
+            $inbox = new Notice_inbox();
+            $inbox->user_id = $flink->user_id;
+            $inbox->notice_id = $notice->id;
+            $inbox->created = common_sql_now();
+
+            $inbox->insert();
+        }
+    }
+
+    function ensureProfile($user)
+    {
+        // check to see if there's already a profile for this user
+        $profileurl = 'http://twitter.com/' . $user->screen_name;
+        $profile = Profile::staticGet('profileurl', $profileurl);
+
+        if ($profile) {
+            if (defined('SCRIPT_DEBUG')) {
+                common_debug("Profile for $profile->nickname found.");
+            }
+
+            // Check to see if the user's Avatar has changed
+            $this->checkAvatar($user, $profile);
+
+            return $profile->id;
+
+        } else {
+            if (defined('SCRIPT_DEBUG')) {
+                common_debug('Adding profile and remote profile ' .
+                    "for Twitter user: $profileurl");
+            }
+
+            $profile = new Profile();
+            $profile->query("BEGIN");
+
+            $profile->nickname = $user->screen_name;
+            $profile->fullname = $user->name;
+            $profile->homepage = $user->url;
+            $profile->bio = $user->description;
+            $profile->location = $user->location;
+            $profile->profileurl = $profileurl;
+            $profile->created = common_sql_now();
+
+            $id = $profile->insert();
+
+            if (empty($id)) {
+                common_log_db_error($profile, 'INSERT', __FILE__);
+                $profile->query("ROLLBACK");
+                return false;
+            }
+
+            // check for remote profile
+            $remote_pro = Remote_profile::staticGet('uri', $profileurl);
+
+            if (!$remote_pro) {
+
+                $remote_pro = new Remote_profile();
+
+                $remote_pro->id = $id;
+                $remote_pro->uri = $profileurl;
+                $remote_pro->created = common_sql_now();
+
+                $rid = $remote_pro->insert();
+
+                if (empty($rid)) {
+                    common_log_db_error($profile, 'INSERT', __FILE__);
+                    $profile->query("ROLLBACK");
+                    return false;
+                }
+            }
+
+            $profile->query("COMMIT");
+
+            $this->saveAvatars($user, $id);
+
+            return $id;
+        }
+    }
+
+    function checkAvatar($user, $profile)
+    {
+        global $config;
+
+        $path_parts = pathinfo($user->profile_image_url);
+        $newname = 'Twitter_' . $user->id . '_' .
+            $path_parts['basename'];
+
+        $oldname = $profile->getAvatar(48)->filename;
+
+        if ($newname != $oldname) {
+
+            if (defined('SCRIPT_DEBUG')) {
+                common_debug('Avatar for Twitter user ' .
+                    "$profile->nickname has changed.");
+                common_debug("old: $oldname new: $newname");
+            }
+
+            $img_root = substr($path_parts['basename'], 0, -11);
+            $ext = $path_parts['extension'];
+            $mediatype = $this->getMediatype($ext);
+
+            foreach (array('mini', 'normal', 'bigger') as $size) {
+                $url = $path_parts['dirname'] . '/' .
+                    $img_root . '_' . $size . ".$ext";
+                $filename = 'Twitter_' . $user->id . '_' .
+                    $img_root . "_$size.$ext";
+
+                if ($this->fetchAvatar($url, $filename)) {
+                    $this->updateAvatar($profile->id, $size, $mediatype, $filename);
+                }
+            }
+        }
+    }
+
+    function getMediatype($ext)
+    {
+        $mediatype = null;
+
+        switch (strtolower($ext)) {
+        case 'jpg':
+            $mediatype = 'image/jpg';
+            break;
+        case 'gif':
+            $mediatype = 'image/gif';
+            break;
+        default:
+            $mediatype = 'image/png';
+        }
+
+        return $mediatype;
+    }
+
+    function saveAvatars($user, $id)
+    {
+        global $config;
+
+        $path_parts = pathinfo($user->profile_image_url);
+        $ext = $path_parts['extension'];
+        $end = strlen('_normal' . $ext);
+        $img_root = substr($path_parts['basename'], 0, -($end+1));
+        $mediatype = $this->getMediatype($ext);
+
+        foreach (array('mini', 'normal', 'bigger') as $size) {
+            $url = $path_parts['dirname'] . '/' .
+                $img_root . '_' . $size . ".$ext";
+            $filename = 'Twitter_' . $user->id . '_' .
+                $img_root . "_$size.$ext";
+
+            if ($this->fetchAvatar($url, $filename)) {
+                $this->newAvatar($id, $size, $mediatype, $filename);
+            } else {
+                common_log(LOG_WARNING, "Problem fetching Avatar: $url", __FILE__);
+            }
+        }
+    }
+
+    function updateAvatar($profile_id, $size, $mediatype, $filename) {
+
+        if (defined('SCRIPT_DEBUG')) {
+            common_debug("Updating avatar: $size");
+        }
+
+        $profile = Profile::staticGet($profile_id);
+
+        if (!$profile) {
+            if (defined('SCRIPT_DEBUG')) {
+                common_debug("Couldn't get profile: $profile_id!");
+            }
+            return;
+        }
+
+        $sizes = array('mini' => 24, 'normal' => 48, 'bigger' => 73);
+        $avatar = $profile->getAvatar($sizes[$size]);
+
+        if ($avatar) {
+            if (defined('SCRIPT_DEBUG')) {
+                common_debug("Deleting $size avatar for $profile->nickname.");
+            }
+            @unlink(INSTALLDIR . '/avatar/' . $avatar->filename);
+            $avatar->delete();
+        }
+
+        $this->newAvatar($profile->id, $size, $mediatype, $filename);
+    }
+
+    function newAvatar($profile_id, $size, $mediatype, $filename)
+    {
+        global $config;
+
+        $avatar = new Avatar();
+        $avatar->profile_id = $profile_id;
+
+        switch($size) {
+        case 'mini':
+            $avatar->width  = 24;
+            $avatar->height = 24;
+            break;
+        case 'normal':
+            $avatar->width  = 48;
+            $avatar->height = 48;
+            break;
+        default:
+
+            // Note: Twitter's big avatars are a different size than
+            // Laconica's (Laconica's = 96)
+
+            $avatar->width  = 73;
+            $avatar->height = 73;
+        }
+
+        $avatar->original = 0; // we don't have the original
+        $avatar->mediatype = $mediatype;
+        $avatar->filename = $filename;
+        $avatar->url = Avatar::url($filename);
+
+        if (defined('SCRIPT_DEBUG')) {
+            common_debug("new filename: $avatar->url");
+        }
+
+        $avatar->created = common_sql_now();
+
+        $id = $avatar->insert();
+
+        if (!$id) {
+            common_log_db_error($avatar, 'INSERT', __FILE__);
+            return null;
+        }
+
+        if (defined('SCRIPT_DEBUG')) {
+            common_debug("Saved new $size avatar for $profile_id.");
+        }
+
+        return $id;
+    }
+
+    function fetchAvatar($url, $filename)
+    {
+        $avatar_dir = INSTALLDIR . '/avatar/';
+
+        $avatarfile = $avatar_dir . $filename;
+
+        $out = fopen($avatarfile, 'wb');
+        if (!$out) {
+            common_log(LOG_WARNING, "Couldn't open file $filename", __FILE__);
+            return false;
+        }
+
+        if (defined('SCRIPT_DEBUG')) {
+            common_debug("Fetching avatar: $url");
+        }
+
+        $ch = curl_init();
+        curl_setopt($ch, CURLOPT_URL, $url);
+        curl_setopt($ch, CURLOPT_FILE, $out);
+        curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
+        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0);
+        $result = curl_exec($ch);
+        curl_close($ch);
+
+        fclose($out);
+
+        return $result;
+    }
+}
+
+ini_set("max_execution_time", "0");
+ini_set("max_input_time", "0");
+set_time_limit(0);
+mb_internal_encoding('UTF-8');
+declare(ticks = 1);
+
+$fetcher = new TwitterStatusFetcher();
+$fetcher->runOnce();
+
index 2b8b085ce8ec568ba2f1fad4cd000e3816ab2b3c..7f39235fed8464f7e3847990c3976d75e7bd05b8 100755 (executable)
@@ -140,6 +140,13 @@ class XmppConfirmHandler extends XmppQueueHandler
     }
 }
 
+// Abort immediately if xmpp is not enabled, otherwise the daemon chews up
+// lots of CPU trying to connect to unconfigured servers
+if (common_config('xmpp','enabled')==false) {
+    print "Aborting daemon - xmpp is disabled\n";
+    exit();
+}
+
 ini_set("max_execution_time", "0");
 ini_set("max_input_time", "0");
 set_time_limit(0);
index ef3f8c63d862472d52d3af9e725e7c1262797ce9..b79fa1b3ba0145101e167ddafea80fdd67ca3fc8 100755 (executable)
@@ -152,11 +152,6 @@ class XMPPDaemon extends Daemon
             $body = preg_replace('/d[\ ]*('. $to .')[\ ]*/', '', $pl['body']);
             $this->add_direct($user, $body, $to, $from);
         } else {
-            $len = mb_strlen($pl['body']);
-            if($len > 140) {
-                $this->from_site($from, 'Message too long - maximum is 140 characters, you sent ' . $len);
-                return;
-            }
             $this->add_notice($user, $pl);
         }
 
@@ -255,15 +250,13 @@ class XMPPDaemon extends Daemon
     function add_notice(&$user, &$pl)
     {
         $body = trim($pl['body']);
-        $content_shortened = common_shorten_link($body);
+        $content_shortened = common_shorten_links($body);
         if (mb_strlen($content_shortened) > 140) {
-            $content = trim(mb_substr($body, 0, 140));
-            $content_shortened = common_shorten_link($content);
-        }
-        else {
-            $content = $body;
+          $from = jabber_normalize_jid($pl['from']);
+          $this->from_site($from, "Message too long - maximum is 140 characters, you sent ".mb_strlen($content_shortened));
+          return;
         }
-        $notice = Notice::saveNew($user->id, $content, 'xmpp');
+        $notice = Notice::saveNew($user->id, $content_shortened, 'xmpp');
         if (is_string($notice)) {
             $this->log(LOG_ERR, $notice);
             return;
@@ -321,6 +314,13 @@ class XMPPDaemon extends Daemon
     }
 }
 
+// Abort immediately if xmpp is not enabled, otherwise the daemon chews up
+// lots of CPU trying to connect to unconfigured servers
+if (common_config('xmpp','enabled')==false) {
+    print "Aborting daemon - xmpp is disabled\n";
+    exit();
+}
+
 ini_set("max_execution_time", "0");
 ini_set("max_input_time", "0");
 set_time_limit(0);
index a6fe8eb754f6c53e81636177e6132435a1830df7..5d2b5231c94a2549eed97ba396fc0bbd1f956391 100644 (file)
@@ -86,7 +86,7 @@ border:0;
 
 .error,
 .success {
-padding:4px 7px;
+padding:4px 1.55%;
 border-radius:4px;
 -moz-border-radius:4px;
 -webkit-border-radius:4px;
@@ -198,9 +198,11 @@ padding:0 7px;
 }
 
 
+.form_settings input.form_action-primary {
+padding:0;
+}
 .form_settings input.form_action-secondary {
 margin-left:29px;
-padding:0;
 }
 
 #form_search .submit {
@@ -248,10 +250,10 @@ display:none;
 }
 
 #site_notice {
-position:absolute;
-top:65px;
-right:18px;
-width:250px;
+float:right;
+clear:right;
+margin-top:7px;
+margin-right:18px;
 width:24%;
 }
 #page_notice {
@@ -397,6 +399,9 @@ border-radius:7px;
 border-style:solid;
 border-width:1px;
 }
+#shownotice #content {
+min-height:0;
+}
 
 #content_inner {
 position:relative;
@@ -426,6 +431,7 @@ line-height:1;
 #form_notice fieldset {
 border:0;
 padding:0;
+position:relative;
 }
 #form_notice legend {
 display:none;
@@ -480,12 +486,18 @@ margin-bottom:7px;
 margin-left:18px;
 float:left;
 }
-
+#form_notice .error {
+float:left;
+clear:both;
+width:96.9%;
+margin-bottom:0;
+line-height:1.618;
+}
 
 /* entity_profile */
 .entity_profile {
 position:relative;
-width:67.702%;
+width:74.702%;
 min-height:123px;
 float:left;
 margin-bottom:18px;
@@ -521,12 +533,15 @@ margin-bottom:4px;
 .entity_profile .entity_nickname {
 margin-left:11px;
 display:inline;
-font-weight:bold;
 }
 .entity_profile .entity_nickname {
 margin-left:0;
 }
-
+.entity_profile .fn,
+.entity_profile .nickname {
+font-size:1.1em;
+font-weight:bold;
+}
 .entity_profile .entity_fn dd:before {
 content: "(";
 font-weight:normal;
@@ -548,7 +563,7 @@ display:none;
 /*entity_actions*/
 .entity_actions {
 float:right;
-margin-left:4.35%;
+margin-left:2.35%;
 max-width:25%;
 }
 .entity_actions h2 {
@@ -626,6 +641,7 @@ margin-bottom:29px;
 clear:both;
 float:left;
 width:100%;
+list-style-position:inside;
 }
 .aside .section h2 {
 text-transform:uppercase;
@@ -649,6 +665,7 @@ list-style-type:none;
 float:left;
 margin-right:7px;
 margin-bottom:7px;
+display:inline;
 }
 .section .entities li .photo {
 margin-right:0;
@@ -730,6 +747,13 @@ border-radius:4px;
 -moz-border-radius:4px;
 -webkit-border-radius:4px;
 }
+.notices .notices {
+margin-top:7px;
+margin-left:3%;
+width:97%;
+float:left;
+}
+
 
 /* NOTICES */
 #notices_primary {
@@ -759,16 +783,14 @@ overflow:hidden;
 font-weight:bold;
 }
 
-.notice .author .photo {
-margin-bottom:0;
-}
-
 .vcard .photo {
 display:inline;
 margin-right:11px;
-margin-bottom:11px;
 float:left;
 }
+#shownotice .vcard .photo {
+margin-bottom:4px;
+}
 .vcard .url {
 text-decoration:none;
 }
@@ -805,9 +827,10 @@ clear:left;
 float:left;
 font-size:0.95em;
 margin-left:59px;
-width:70%;
+width:65%;
 }
-#showstream .notice div.entry-content {
+#showstream .notice div.entry-content,
+#shownotice .notice div.entry-content {
 margin-left:0;
 }
 
@@ -835,23 +858,6 @@ text-transform:lowercase;
 }
 
 
-
-.notice-data {
-position:absolute;
-top:18px;
-right:0;
-min-height:50px;
-margin-bottom:4px;
-}
-.notice .entry-content .notice-data dt {
-display:none;
-}
-
-.notice-data a {
-display:block;
-outline:none;
-}
-
 .notice-options {
 padding-left:2%;
 float:left;
@@ -1029,6 +1035,8 @@ padding-right:30px;
 .hentry .entry-content p {
 margin-bottom:18px;
 }
+.system_notice ul,
+.instructions ul,
 .hentry entry-content ol,
 .hentry .entry-content ul {
 list-style-position:inside;
@@ -1045,7 +1053,7 @@ margin-left:18px;
 
 /* TOP_POSTERS */
 .section tbody td {
-padding-right:11px;
+padding-right:18px;
 padding-bottom:11px;
 }
 .section .vcard .photo {
@@ -1162,6 +1170,17 @@ width:400px;
 margin-right:28px;
 }
 
+#settings_design_color .form_data li {
+width:33%;
+}
+#settings_design_color .form_data label {
+float:none;
+}
+#settings_design_color .form_data .swatch {
+padding:11px;
+margin-left:0;
+}
+
 .instructions ul {
 list-style-position:inside;
 }
diff --git a/theme/base/images/icons/clip-big.png b/theme/base/images/icons/clip-big.png
new file mode 100644 (file)
index 0000000..3945f56
Binary files /dev/null and b/theme/base/images/icons/clip-big.png differ
diff --git a/theme/base/images/icons/clip.png b/theme/base/images/icons/clip.png
new file mode 100644 (file)
index 0000000..3c5a17d
Binary files /dev/null and b/theme/base/images/icons/clip.png differ
diff --git a/theme/biz/css/base.css b/theme/biz/css/base.css
new file mode 100644 (file)
index 0000000..0e37a6e
--- /dev/null
@@ -0,0 +1,1173 @@
+/** theme: biz base
+ *
+ * @package   Laconica
+ * @author Sarven Capadisli <csarven@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+* { margin:0; padding:0; }
+img { display:block; border:0; }
+a abbr { cursor: pointer; border-bottom:0; }
+table { border-collapse:collapse; }
+ol { list-style-position:inside; }
+html { font-size: 87.5%; background-color:#fff; height:100%; }
+body {
+background-color:#fff;
+color:#000;
+font-family:sans-serif;
+font-size:1em;
+line-height:1.65;
+position:relative;
+}
+h1,h2,h3,h4,h5,h6 {
+margin-bottom:7px;
+overflow:hidden;
+}
+h1 {
+font-size:1.4em;
+margin-bottom:18px;
+}
+#showstream h1 { display:none; }
+h2 { font-size:1.3em; }
+h3 { font-size:1.2em; }
+h4 { font-size:1.1em; }
+h5 { font-size:1em; }
+h6 { font-size:0.9em; }
+
+caption {
+font-weight:bold;
+}
+legend {
+font-weight:bold;
+font-size:1.3em;
+}
+input, textarea, select, option {
+padding:4px;
+font-family:sans-serif;
+font-size:1em;
+}
+input, textarea, select {
+border-width:2px;
+border-style: solid;
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+
+input.submit {
+font-weight:bold;
+cursor:pointer;
+}
+textarea {
+overflow:auto;
+}
+option {
+padding-bottom:0;
+}
+fieldset {
+padding:0;
+border:0;
+}
+form ul li {
+list-style-type:none;
+margin:0 0 18px 0;
+}
+form label {
+font-weight:bold;
+}
+input.checkbox {
+position:relative;
+top:2px;
+left:0;
+border:0;
+}
+
+.error,
+.success {
+padding:4px 1.55%;
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+margin-bottom:18px;
+}
+form label.submit {
+display:none;
+}
+
+.form_settings {
+clear:both;
+}
+
+.form_settings fieldset {
+margin-bottom:29px;
+}
+.form_settings input.remove {
+margin-left:11px;
+}
+.form_settings .form_data li {
+width:100%;
+float:left;
+}
+.form_settings .form_data label {
+float:left;
+}
+.form_settings .form_data textarea,
+.form_settings .form_data select,
+.form_settings .form_data input {
+margin-left:11px;
+float:left;
+}
+.form_settings .form_data input.submit {
+margin-left:0;
+}
+
+.form_settings label {
+margin-top:2px;
+width:113px;
+}
+
+.form_actions label {
+display:none;
+}
+.form_guide {
+font-style:italic;
+}
+
+.form_settings #settings_autosubscribe label {
+display:inline;
+font-weight:bold;
+}
+
+#form_settings_profile legend,
+#form_login legend,
+#form_register legend,
+#form_password legend,
+#form_settings_avatar legend,
+#newgroup legend,
+#editgroup legend,
+#form_tag_user legend,
+#form_remote_subscribe legend,
+#form_openid_login legend,
+#form_search legend,
+#form_invite legend,
+#form_notice_delete legend,
+#form_password_recover legend,
+#form_password_change legend {
+display:none;
+}
+
+.form_settings .form_data p.form_guide {
+clear:both;
+margin-left:124px;
+margin-bottom:0;
+}
+
+.form_settings p {
+margin-bottom:11px;
+}
+
+.form_settings input.checkbox {
+margin-top:3px;
+margin-left:0;
+}
+.form_settings label.checkbox {
+font-weight:normal;
+margin-top:0;
+margin-right:0;
+margin-left:11px;
+float:left;
+width:90%;
+}
+
+
+#form_login p.form_guide,
+#form_register #settings_rememberme p.form_guide,
+#form_openid_login #settings_rememberme p.form_guide,
+#settings_twitter_remove p.form_guide,
+#form_search ul.form_data #q {
+margin-left:0;
+}
+
+.form_settings .form_note {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+padding:0 7px;
+}
+
+
+.form_settings input.form_action-primary {
+padding:0;
+}
+.form_settings input.form_action-secondary {
+margin-left:29px;
+}
+
+
+#form_search .submit {
+margin-left:11px;
+}
+
+address {
+float:left;
+margin-bottom:18px;
+margin-left:18px;
+}
+address.vcard img.logo {
+margin-right:0;
+}
+address .fn {
+font-weight:bold;
+}
+address img + .fn {
+display:none;
+}
+
+#header {
+width:100%;
+position:relative;
+float:left;
+padding-top:18px;
+margin-bottom:18px;
+}
+
+#site_nav_global_primary {
+float:left;
+margin-right:18px;
+margin-bottom:11px;
+width:50%;
+}
+#site_nav_global_primary ul li {
+display:inline;
+margin-right:11px;
+}
+
+.system_notice dt {
+font-weight:bold;
+text-transform:uppercase;
+display:none;
+}
+
+#site_notice {
+float:right;
+clear:right;
+margin-top:7px;
+margin-right:18px;
+width:24%;
+}
+#page_notice {
+clear:both;
+margin-bottom:18px;
+}
+
+
+#anon_notice {
+float:left;
+width:45.4%;
+/*
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+border-width:2px;
+border-style:solid;
+*/
+line-height:1.5;
+font-size:1.1em;
+font-weight:bold;
+}
+
+
+#footer {
+float:left;
+width:64%;
+padding:18px;
+}
+
+#site_nav_local_views {
+width:14.5%;
+float:left;
+}
+#site_nav_local_views dt {
+display:none;
+}
+#site_nav_local_views li {
+list-style-type:none;
+}
+#site_nav_local_views a {
+display:block;
+text-decoration:none;
+padding:4px 11px;
+-moz-border-radius-topleft:4px;
+-moz-border-radius-bottomleft:4px;
+-webkit-border-top-left-radius:4px;
+-webkit-border-bottom-left-radius:4px;
+border-width:1px;
+border-style:solid;
+border-right:0;
+text-shadow: 2px 2px 2px #ddd;
+font-weight:bold;
+}
+#site_nav_local_views .nav {
+float:left;
+width:100%;
+}
+
+#site_nav_global_primary dt,
+#site_nav_global_secondary dt {
+display:none;
+}
+
+#site_nav_global_secondary {
+margin-bottom:11px;
+}
+
+#site_nav_global_secondary ul li {
+display:inline;
+margin-right:11px;
+}
+#export_data li a {
+padding-left:20px;
+}
+#export_data li a.foaf {
+padding-left:30px;
+}
+#export_data li a.export_vcard {
+padding-left:28px;
+}
+
+#export_data ul {
+display:inline;
+}
+#export_data li {
+list-style-type:none;
+display:inline;
+margin-left:11px;
+}
+#export_data li:first-child {
+margin-left:0;
+}
+
+#licenses {
+font-size:0.9em;
+}
+
+#licenses dt {
+font-weight:bold;
+display:none;
+}
+#licenses dd {
+margin-bottom:11px;
+line-height:1.5;
+}
+
+#site_content_license_cc {
+margin-bottom:0;
+}
+#site_content_license_cc img {
+display:inline;
+vertical-align:top;
+margin-right:4px;
+}
+
+#wrap {
+margin:0 auto;
+width:100%;
+min-width:760px;
+max-width:1003px;
+overflow:hidden;
+}
+
+#core {
+position:relative;
+width:100%;
+float:left;
+margin-bottom:1em;
+}
+
+#content {
+width:51.009%;
+min-height:259px;
+padding:1.795%;
+float:left;
+border-radius:7px;
+-moz-border-radius:7px;
+-moz-border-radius-topleft:0;
+-webkit-border-radius:7px;
+-webkit-border-top-left-radius:0;
+border-style:solid;
+border-width:1px;
+}
+#shownotice #content {
+min-height:0;
+}
+
+#content_inner {
+position:relative;
+width:100%;
+float:left;
+}
+
+#aside_primary {
+width:29.917%;
+min-height:259px;
+float:left;
+margin-left:0.385%;
+}
+
+#form_notice {
+width:45.664%;
+float:left;
+position:relative;
+line-height:1;
+}
+#form_notice fieldset {
+border:0;
+padding:0;
+position:relative;
+}
+#form_notice legend {
+display:none;
+}
+#form_notice textarea {
+float:left;
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+width:80.789%;
+height:67px;
+line-height:1.5;
+padding:7px 7px 16px 7px;
+}
+#form_notice label {
+display:block;
+float:left;
+font-size:1.3em;
+margin-bottom:7px;
+}
+#form_notice #notice_submit label {
+display:none;
+}
+#form_notice .form_note {
+position:absolute;
+top:99px;
+right:98px;
+z-index:9;
+}
+#form_notice .form_note dt {
+font-weight:bold;
+display:none;
+}
+#notice_text-count {
+font-weight:bold;
+line-height:1.15;
+padding:1px 2px;
+}
+#form_notice #notice_action-submit {
+width:14%;
+height:47px;
+padding:0;
+position:absolute;
+bottom:0;
+right:0;
+}
+#form_notice label[for=to] {
+margin-top:7px;
+}
+#form_notice select[id=to] {
+margin-bottom:7px;
+margin-left:18px;
+float:left;
+}
+#form_notice .error {
+float:left;
+clear:both;
+width:96.9%;
+margin-bottom:0;
+line-height:1.618;
+}
+
+/* entity_profile */
+.entity_profile {
+position:relative;
+width:67.702%;
+min-height:123px;
+float:left;
+margin-bottom:18px;
+margin-left:0;
+overflow:hidden;
+}
+.entity_profile dt,
+#entity_statistics dt {
+font-weight:bold;
+}
+.entity_profile dd {
+display:inline;
+}
+
+.entity_profile .entity_depiction {
+float:left;
+width:96px;
+margin-right:18px;
+margin-bottom:18px;
+}
+
+.entity_profile .entity_fn,
+.entity_profile .entity_nickname,
+.entity_profile .entity_location,
+.entity_profile .entity_url,
+.entity_profile .entity_note,
+.entity_profile .entity_tags {
+margin-left:113px;
+margin-bottom:4px;
+}
+
+.entity_profile .entity_fn,
+.entity_profile .entity_nickname {
+margin-left:11px;
+display:inline;
+font-weight:bold;
+}
+.entity_profile .entity_nickname {
+margin-left:0;
+}
+
+.entity_profile .entity_fn dd:before {
+content: "(";
+font-weight:normal;
+}
+.entity_profile .entity_fn dd:after {
+content: ")";
+font-weight:normal;
+}
+
+.entity_profile dt {
+display:none;
+}
+.entity_profile h2 {
+display:none;
+}
+/* entity_profile */
+
+
+/*entity_actions*/
+.entity_actions {
+float:right;
+margin-left:4.35%;
+max-width:25%;
+}
+.entity_actions h2 {
+display:none;
+}
+.entity_actions ul {
+list-style-type:none;
+}
+.entity_actions li {
+margin-bottom:4px;
+}
+.entity_actions li:first-child {
+border-top:0;
+}
+.entity_actions fieldset {
+border:0;
+padding:0;
+}
+.entity_actions legend {
+display:none;
+}
+
+.entity_actions input.submit {
+display:block;
+text-align:left;
+width:100%;
+}
+.entity_actions a,
+.entity_nudge p,
+.entity_remote_subscribe {
+text-decoration:none;
+font-weight:bold;
+display:block;
+}
+
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_send-a-message a,
+.entity_edit a,
+.form_user_nudge input.submit,
+.entity_nudge p {
+border:0;
+padding-left:20px;
+}
+
+.entity_edit a,
+.entity_send-a-message a,
+.entity_nudge p {
+padding:4px 4px 4px 23px;
+}
+
+.entity_remote_subscribe {
+padding:4px;
+border-width:2px;
+border-style:solid;
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+.entity_actions .accept {
+margin-bottom:18px;
+}
+
+.entity_tags ul {
+list-style-type:none;
+display:inline;
+}
+.entity_tags li {
+display:inline;
+margin-right:4px;
+}
+
+.aside .section {
+margin-bottom:18px;
+clear:both;
+float:left;
+width:87.985%;
+padding:6%;
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+border-width:1px;
+border-style:solid;
+}
+.aside .section h2 {
+text-transform:uppercase;
+font-size:1em;
+}
+
+#entity_statistics dt,
+#entity_statistics dd {
+display:inline;
+}
+#entity_statistics dt:after {
+content: ":";
+}
+
+.section ul.entities {
+float:left;
+width:100%;
+}
+.section .entities li {
+list-style-type:none;
+float:left;
+margin-right:7px;
+margin-bottom:7px;
+}
+.section .entities li .photo {
+margin-right:0;
+margin-bottom:0;
+}
+.section .entities li .fn {
+display:none;
+}
+
+.aside .section p,
+.aside .section .more {
+clear:both;
+}
+
+.profile .entity_profile {
+margin-bottom:0;
+min-height:60px;
+}
+
+
+.profile .form_group_join legend,
+.profile .form_group_leave legend,
+.profile .form_user_subscribe legend,
+.profile .form_user_unsubscribe legend {
+display:none;
+}
+
+.profiles {
+list-style-type:none;
+}
+.profile .entity_profile .entity_location {
+width:auto;
+clear:none;
+margin-left:11px;
+}
+.profile .entity_profile dl,
+.profile .entity_profile dd {
+display:inline;
+float:none;
+}
+.profile .entity_profile .entity_note,
+.profile .entity_profile .entity_url,
+.profile .entity_profile .entity_tags,
+.profile .entity_profile .form_subscription_edit {
+margin-left:59px;
+clear:none;
+display:block;
+width:auto;
+}
+.profile .entity_profile .entity_tags dt {
+display:inline;
+margin-right:11px;
+}
+
+
+.profile .entity_profile .form_subscription_edit label {
+font-weight:normal;
+margin-right:11px;
+}
+
+
+/* NOTICE */
+.notice,
+.profile {
+position:relative;
+padding-top:11px;
+padding-bottom:11px;
+clear:both;
+float:left;
+width:100%;
+border-top-width:1px;
+border-top-style:dotted;
+}
+.notices li {
+list-style-type:none;
+}
+.notices li.hover {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+
+/* NOTICES */
+#notices_primary {
+float:left;
+width:100%;
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+}
+#notices_primary h2 {
+display:none;
+}
+.notice-data a span {
+display:block;
+padding-left:28px;
+}
+
+.notice .author {
+margin-right:11px;
+}
+
+.fn {
+overflow:hidden;
+}
+
+.notice .author .fn {
+font-weight:bold;
+}
+
+.vcard .photo {
+display:inline;
+margin-right:11px;
+float:left;
+}
+#shownotice .vcard .photo {
+margin-bottom:4px;
+}
+.vcard .url {
+text-decoration:none;
+}
+.vcard .url:hover {
+text-decoration:underline;
+}
+
+.notice .entry-title {
+float:left;
+width:100%;
+overflow:hidden;
+}
+#shownotice .notice .entry-title {
+font-size:2.2em;
+}
+
+.notice p.entry-content {
+display:inline;
+}
+
+#content .notice p.entry-content a:visited {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+.notice p.entry-content .vcard a {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+
+.notice div.entry-content {
+clear:left;
+float:left;
+font-size:0.95em;
+margin-left:59px;
+width:65%;
+}
+#showstream .notice div.entry-content,
+#shownotice .notice div.entry-content {
+margin-left:0;
+}
+
+.notice .notice-options a,
+.notice .notice-options input {
+float:left;
+font-size:1.025em;
+}
+
+.notice div.entry-content dl,
+.notice div.entry-content dt,
+.notice div.entry-content dd {
+display:inline;
+}
+
+.notice div.entry-content .timestamp dt,
+.notice div.entry-content .response dt {
+display:none;
+}
+.notice div.entry-content .timestamp a {
+display:inline-block;
+}
+.notice div.entry-content .device dt {
+text-transform:lowercase;
+}
+
+
+.notice-options {
+padding-left:2%;
+float:left;
+width:50%;
+position:relative;
+font-size:0.95em;
+width:12.5%;
+float:right;
+}
+
+.notice-options a {
+float:left;
+}
+.notice-options .notice_delete,
+.notice-options .notice_reply,
+.notice-options .form_favor,
+.notice-options .form_disfavor {
+position:absolute;
+top:0;
+}
+.notice-options .form_favor,
+.notice-options .form_disfavor {
+left:0;
+}
+.notice-options .notice_reply {
+left:29px;
+}
+.notice-options .notice_delete {
+right:0;
+}
+.notice-options .notice_reply dt {
+display:none;
+}
+
+.notice-options input,
+.notice-options a {
+text-indent:-9999px;
+outline:none;
+}
+
+.notice-options .notice_reply a,
+.notice-options input.submit {
+display:block;
+border:0;
+}
+.notice-options .notice_reply a,
+.notice-options .notice_delete a {
+text-decoration:none;
+padding-left:16px;
+}
+
+.notice-options form input.submit {
+width:16px;
+padding:2px 0;
+}
+
+.notice-options .notice_delete dt,
+.notice-options .form_favor legend,
+.notice-options .form_disfavor legend {
+display:none;
+}
+.notice-options .notice_delete fieldset,
+.notice-options .form_favor fieldset,
+.notice-options .form_disfavor fieldset {
+border:0;
+padding:0;
+}
+
+
+#usergroups #new_group {
+float: left;
+margin-right: 2em;
+}
+#new_group, #group_search {
+margin-bottom:18px;
+}
+#new_group a {
+padding-left:20px;
+}
+
+
+#filter_tags {
+margin-bottom:11px;
+float:left;
+}
+#filter_tags dt {
+display:none;
+}
+#filter_tags ul {
+list-style-type:none;
+}
+#filter_tags ul li {
+float:left;
+margin-left:7px;
+padding-left:7px;
+border-left-width:1px;
+border-left-style:solid;
+}
+#filter_tags ul li.child_1 {
+margin-left:0;
+border-left:0;
+padding-left:0;
+}
+#filter_tags ul li#filter_tags_all a {
+font-weight:bold;
+margin-top:7px;
+float:left;
+}
+
+#filter_tags ul li#filter_tags_item label {
+margin-right:7px;
+}
+#filter_tags ul li#filter_tags_item label,
+#filter_tags ul li#filter_tags_item select {
+display:inline;
+}
+#filter_tags ul li#filter_tags_item p {
+float:left;
+margin-left:38px;
+}
+#filter_tags ul li#filter_tags_item input {
+position:relative;
+top:3px;
+left:3px;
+}
+
+
+
+.pagination {
+float:left;
+clear:both;
+width:100%;
+margin-top:18px;
+}
+
+.pagination dt {
+font-weight:bold;
+display:none;
+}
+
+.pagination .nav {
+float:left;
+width:100%;
+list-style-type:none;
+}
+
+.pagination .nav_prev {
+float:left;
+}
+.pagination .nav_next {
+float:right;
+}
+
+.pagination a {
+display:block;
+text-decoration:none;
+font-weight:bold;
+padding:7px;
+border-width:1px;
+border-style:solid;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+border-radius:7px;
+}
+
+.pagination .nav_prev a {
+padding-left:30px;
+}
+.pagination .nav_next a {
+padding-right:30px;
+}
+/* END: NOTICE */
+
+
+.hentry .entry-content p {
+margin-bottom:18px;
+}
+.system_notice ul,
+.instructions ul,
+.hentry entry-content ol,
+.hentry .entry-content ul {
+list-style-position:inside;
+}
+.hentry .entry-content li {
+margin-bottom:18px;
+}
+.hentry .entry-content li li {
+margin-left:18px;
+}
+
+
+
+
+/* TOP_POSTERS */
+.section tbody td {
+padding-right:11px;
+padding-bottom:11px;
+}
+.section .vcard .photo {
+margin-right:7px;
+margin-bottom:0;
+}
+
+.section .notice {
+padding-top:7px;
+padding-bottom:7px;
+border-top:0;
+}
+
+.section .notice:first-child {
+padding-top:0;
+}
+
+.section .notice .author {
+margin-right:0;
+}
+.section .notice .author .fn {
+display:none;
+}
+
+
+/* tagcloud */
+.tag-cloud {
+list-style-type:none;
+text-align:center;
+}
+.aside .tag-cloud {
+font-size:0.8em;
+}
+.tag-cloud li {
+display:inline;
+margin-right:7px;
+line-height:1.25;
+}
+.aside .tag-cloud li {
+line-height:1.5;
+}
+.tag-cloud li a {
+text-decoration:none;
+}
+#tagcloud.section dt {
+text-transform:uppercase;
+font-weight:bold;
+}
+.tag-cloud-1 {
+font-size:1em;
+}
+.tag-cloud-2 {
+font-size:1.25em;
+}
+.tag-cloud-3 {
+font-size:1.75em;
+}
+.tag-cloud-4 {
+font-size:2em;
+}
+.tag-cloud-5 {
+font-size:2.25em;
+}
+.tag-cloud-6 {
+font-size:2.75em;
+}
+.tag-cloud-7 {
+font-size:3.25em;
+}
+
+#publictagcloud #tagcloud.section dt {
+display:none;
+}
+
+#form_settings_photo .form_data {
+clear:both;
+}
+
+#form_settings_avatar li {
+width:auto;
+}
+#form_settings_avatar input {
+margin-left:0;
+}
+#avatar_original,
+#avatar_preview {
+float:left;
+}
+#avatar_preview {
+margin-left:29px;
+}
+#avatar_preview_view {
+height:96px;
+width:96px;
+margin-bottom:18px;
+overflow:hidden;
+}
+
+#settings_attach,
+#form_settings_avatar .form_actions {
+clear:both;
+}
+
+#form_settings_avatar .form_actions {
+margin-bottom:0;
+}
+
+#form_settings_design #settings_design_color .form_data,
+#form_settings_design #color-picker {
+float:left;
+}
+#form_settings_design #settings_design_color .form_data {
+width:400px;
+margin-right:28px;
+}
+
+.instructions ul {
+list-style-position:inside;
+}
+.instructions p,
+.instructions ul {
+margin-bottom:18px;
+}
+.help dt {
+display:none;
+}
+.guide {
+clear:both;
+}
diff --git a/theme/biz/css/display.css b/theme/biz/css/display.css
new file mode 100644 (file)
index 0000000..14092d9
--- /dev/null
@@ -0,0 +1,252 @@
+/** theme: biz
+ *
+ * @package   Laconica
+ * @author Sarven Capadisli <csarven@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+@import url(base.css);
+
+html {
+background-color:#144A6E;
+}
+a:active {
+background-color:#F4F7E7;
+}
+body {
+font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
+font-size:1em;
+background:#144A6E url(../images/illustrations/illu_pattern-01.png) repeat-x;
+}
+
+address {
+margin-right:7.18%;
+}
+
+input, textarea, select, option {
+font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
+}
+input, textarea, select,
+.entity_remote_subscribe {
+border-color:#aaa;
+}
+#filter_tags ul li {
+border-color:#ddd;
+}
+
+.form_settings input.form_action-primary {
+background:none;
+}
+
+input.submit,
+#form_notice.warning #notice_text-count,
+.form_settings .form_note,
+.entity_remote_subscribe {
+background-color:#9BB43E;
+}
+
+input:focus, textarea:focus, select:focus,
+#form_notice.warning #notice_data-text {
+border-color:#9BB43E;
+}
+input.submit,
+.entity_remote_subscribe,
+#site_nav_local_views a {
+color:#fff;
+}
+
+a,
+#site_nav_local_views .current a,
+div.notice-options input,
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_send-a-message a,
+.form_user_nudge input.submit,
+.entity_nudge p,
+.form_settings input.form_action-primary {
+color:#002E6E;
+}
+
+#header a,
+#footer a {
+color:#87B4C8;
+}
+
+.notice,
+.profile {
+border-top-color:#CEE1E9;
+}
+.section .profile {
+border-top-color:#87B4C8;
+}
+
+#content .notice p.entry-content a:visited {
+background-color:#fcfcfc;
+}
+#content .notice p.entry-content .vcard a {
+background-color:#fcfffc;
+}
+
+.aside .section {
+background-color:#F1F5F8;
+background-position:100% 0;
+background-image:url(../images/illustrations/illu_pattern-02.png);
+background-repeat:no-repeat;
+}
+
+#notice_text-count {
+color:#333;
+}
+#form_notice.warning #notice_text-count {
+color:#000;
+}
+#form_notice.processing #notice_action-submit {
+background:#fff url(../../base/images/icons/icon_processing.gif) no-repeat 47% 47%;
+cursor:wait;
+text-indent:-9999px;
+}
+
+#content,
+#site_nav_local_views a,
+.aside .section {
+border-color:#fff;
+}
+#content,
+#site_nav_local_views .current a {
+background-color:#fff;
+}
+
+#site_nav_local_views a {
+background-color:rgba(135, 180, 200, 0.3);
+}
+#site_nav_local_views a:hover {
+background-color:rgba(255, 255, 255, 0.7);
+}
+
+.error {
+background-color:#F7E8E8;
+}
+.success {
+background-color:#EFF3DC;
+}
+
+#anon_notice {
+color:#fff;
+}
+
+#showstream #anon_notice {
+}
+
+#export_data li a {
+background-repeat:no-repeat;
+background-position:0 45%;
+}
+#export_data li a.rss {
+background-image:url(../../base/images/icons/icon_rss.png);
+}
+#export_data li a.atom {
+background-image:url(../../base/images/icons/icon_atom.png);
+}
+#export_data li a.foaf {
+background-image:url(../../base/images/icons/icon_foaf.gif);
+}
+
+.entity_edit a,
+.entity_send-a-message a,
+.form_user_nudge input.submit,
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_nudge p {
+background-position: 0 40%;
+background-repeat: no-repeat;
+background-color:transparent;
+}
+.form_group_join input.submit,
+.form_group_leave input.submit
+.form_user_subscribe input.submit,
+.form_user_unsubscribe input.submit {
+background-color:#9BB43E;
+color:#fff;
+}
+.form_user_unsubscribe input.submit,
+.form_group_leave input.submit,
+.form_user_authorization input.reject {
+background-color:#87B4C8;
+}
+
+.entity_edit a {
+background-image:url(../../base/images/icons/twotone/green/edit.gif);
+}
+.entity_send-a-message a {
+background-image:url(../../base/images/icons/twotone/green/quote.gif);
+}
+.entity_nudge p,
+.form_user_nudge input.submit {
+background-image:url(../../base/images/icons/twotone/green/mail.gif);
+}
+.form_user_block input.submit,
+.form_user_unblock input.submit {
+background-image:url(../../base/images/icons/twotone/green/shield.gif);
+}
+
+/* NOTICES */
+.notices li.over {
+background-color:#fcfcfc;
+}
+
+.notice-options .notice_reply a,
+.notice-options form input.submit {
+background-color:transparent;
+}
+.notice-options .notice_reply a {
+background:transparent url(../../base/images/icons/twotone/green/reply.gif) no-repeat 0 45%;
+}
+.notice-options form.form_favor input.submit {
+background:transparent url(../../base/images/icons/twotone/green/favourite.gif) no-repeat 0 45%;
+}
+.notice-options form.form_disfavor input.submit {
+background:transparent url(../../base/images/icons/twotone/green/disfavourite.gif) no-repeat 0 45%;
+}
+.notice-options .notice_delete a {
+background:transparent url(../../base/images/icons/twotone/green/trash.gif) no-repeat 0 45%;
+}
+
+.notices div.entry-content,
+.notices div.notice-options {
+opacity:0.4;
+}
+.notices li.hover div.entry-content,
+.notices li.hover div.notice-options {
+opacity:1;
+}
+div.entry-content {
+color:#333;
+}
+div.notice-options a,
+div.notice-options input {
+font-family:sans-serif;
+}
+.notices li.hover {
+background-color:#fcfcfc;
+}
+/*END: NOTICES */
+
+#new_group a {
+background:transparent url(../../base/images/icons/twotone/green/news.gif) no-repeat 0 45%;
+}
+
+.pagination .nav_prev a,
+.pagination .nav_next a {
+background-repeat:no-repeat;
+border-color:#CEE1E9;
+}
+.pagination .nav_prev a {
+background-image:url(../../base/images/icons/twotone/green/arrow-left.gif);
+background-position:10% 45%;
+}
+.pagination .nav_next a {
+background-image:url(../../base/images/icons/twotone/green/arrow-right.gif);
+background-position:90% 45%;
+}
diff --git a/theme/biz/css/ie.css b/theme/biz/css/ie.css
new file mode 100644 (file)
index 0000000..2f463bb
--- /dev/null
@@ -0,0 +1,9 @@
+/* IE specific styles */
+
+.notice-options input.submit {
+color:#fff;
+}
+
+#site_nav_local_views a {
+background-color:#D0DFE7;
+}
diff --git a/theme/biz/default-avatar-mini.png b/theme/biz/default-avatar-mini.png
new file mode 100644 (file)
index 0000000..38b8692
Binary files /dev/null and b/theme/biz/default-avatar-mini.png differ
diff --git a/theme/biz/default-avatar-profile.png b/theme/biz/default-avatar-profile.png
new file mode 100644 (file)
index 0000000..f8357d4
Binary files /dev/null and b/theme/biz/default-avatar-profile.png differ
diff --git a/theme/biz/default-avatar-stream.png b/theme/biz/default-avatar-stream.png
new file mode 100644 (file)
index 0000000..6b63baa
Binary files /dev/null and b/theme/biz/default-avatar-stream.png differ
diff --git a/theme/biz/images/illustrations/illu_pattern-01.png b/theme/biz/images/illustrations/illu_pattern-01.png
new file mode 100644 (file)
index 0000000..79bb46b
Binary files /dev/null and b/theme/biz/images/illustrations/illu_pattern-01.png differ
diff --git a/theme/biz/images/illustrations/illu_pattern-02.png b/theme/biz/images/illustrations/illu_pattern-02.png
new file mode 100644 (file)
index 0000000..4438b75
Binary files /dev/null and b/theme/biz/images/illustrations/illu_pattern-02.png differ
diff --git a/theme/biz/logo.png b/theme/biz/logo.png
new file mode 100644 (file)
index 0000000..fdead6c
Binary files /dev/null and b/theme/biz/logo.png differ
index b87722eec1f9bfc58a257416958b4aed5c0e13f1..12f186a5601c6daf1db55aafabaae3d40c9f8b4d 100644 (file)
@@ -12,7 +12,7 @@ img { display:block; border:0; }
 a abbr { cursor: pointer; border-bottom:0; }
 table { border-collapse:collapse; }
 ol { list-style-position:inside; }
-html { font-size: 100%; background-color:#fff; height:100%; }
+html { font-size: 100%; background-color:#fff; }
 body {
 background-color:#fff;
 color:#000;
@@ -126,7 +126,7 @@ margin-left:0;
 
 .form_settings label {
 margin-top:2px;
-width:145px;
+width:143px;
 }
 
 .form_actions label {
@@ -199,9 +199,11 @@ padding:0 7px;
 }
 
 
+.form_settings input.form_action-primary {
+padding:0;
+}
 .form_settings input.form_action-secondary {
 margin-left:29px;
-padding:0;
 }
 
 #form_search .submit {
@@ -1267,7 +1269,7 @@ border-color:#aaa;
 border-color:#ddd;
 }
 
-.form_settings input.form_action-secondary {
+.form_settings input.form_action-primary {
 background:none;
 }
 
@@ -1296,7 +1298,7 @@ div.notice-options input,
 .entity_send-a-message a,
 .form_user_nudge input.submit,
 .entity_nudge p,
-.form_settings input.form_action-secondary {
+.form_settings input.form_action-primary {
 color:#0084B4;
 }
 
index c0f1d411fdffd351fe16295c611f52d37be4fdf3..4fd8bd9e1951099c121ca843ef2d53f413b2a72f 100644 (file)
Binary files a/theme/cloudy/default-avatar-mini.png and b/theme/cloudy/default-avatar-mini.png differ
index 9f281f94f9c86c66a10641028057cdf5b72db774..eb08571d9979540d26ac4d6f549f5744f76d0c89 100644 (file)
Binary files a/theme/cloudy/default-avatar-profile.png and b/theme/cloudy/default-avatar-profile.png differ
index 8d505871ccd9d4896fd992a2b6561c4893302134..926b8a9ca2dc27ff5288551fd5b626d1acd433b2 100644 (file)
Binary files a/theme/cloudy/default-avatar-stream.png and b/theme/cloudy/default-avatar-stream.png differ
index 7c68b34f61008753d12394806c3cf46f7b04effd..fdead6c4a0a23614552329ee68bb0860b0200a0c 100644 (file)
Binary files a/theme/cloudy/logo.png and b/theme/cloudy/logo.png differ
index 0c8fae166ff14895d72f022056ddff3bdd92eb6a..16c9322a5d724dfd85c0fbc62089ca32b9e2b258 100644 (file)
@@ -12,7 +12,7 @@
 html,
 body,
 a:active {
-background-color:#97BFD1;
+background-color:#C3D6DF;
 }
 body {
 font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
@@ -30,10 +30,10 @@ input, textarea, select,
 border-color:#aaa;
 }
 #filter_tags ul li {
-border-color:#97BFD1;
+border-color:#C3D6DF;
 }
 
-.form_settings input.form_action-secondary {
+.form_settings input.form_action-primary {
 background:none;
 }
 
@@ -60,7 +60,7 @@ div.notice-options input,
 .entity_send-a-message a,
 .form_user_nudge input.submit,
 .entity_nudge p,
-.form_settings input.form_action-secondary {
+.form_settings input.form_action-primary {
 color:#002E6E;
 }
 
@@ -69,14 +69,7 @@ color:#002E6E;
 border-top-color:#D1D9E4;
 }
 .section .profile {
-border-top-color:#97BFD1;
-}
-
-#content .notice p.entry-content a:visited {
-background-color:#fcfcfc;
-}
-#content .notice p.entry-content .vcard a {
-background-color:#fcfffc;
+border-top-color:#C3D6DF;
 }
 
 #aside_primary {
@@ -120,7 +113,7 @@ background-color:#EFF3DC;
 }
 
 #anon_notice {
-background-color:#97BFD1;
+background-color:#C3D6DF;
 color:#fff;
 border-color:#fff;
 }
@@ -163,7 +156,7 @@ color:#fff;
 .form_user_unsubscribe input.submit,
 .form_group_leave input.submit,
 .form_user_authorization input.reject {
-background-color:#97BFD1;
+background-color:#C3D6DF;
 }
 
 .entity_edit a {
@@ -182,10 +175,6 @@ background-image:url(../../base/images/icons/twotone/green/shield.gif);
 }
 
 /* NOTICES */
-.notices li.over {
-background-color:#fcfcfc;
-}
-
 .notice-options .notice_reply a,
 .notice-options form input.submit {
 background-color:transparent;
@@ -221,6 +210,19 @@ font-family:sans-serif;
 .notices li.hover {
 background-color:#fcfcfc;
 }
+
+.notices .notices {
+background-color:rgba(200, 200, 200, 0.025);
+}
+.notices .notices .notices {
+background-color:rgba(200, 200, 200, 0.050);
+}
+.notices .notices .notices .notices {
+background-color:rgba(200, 200, 200, 0.075);
+}
+.notices .notices .notices .notices .notices {
+background-color:rgba(200, 200, 200, 0.100);
+}
 /*END: NOTICES */
 
 #new_group a {
diff --git a/theme/default/logo.png b/theme/default/logo.png
new file mode 100644 (file)
index 0000000..fdead6c
Binary files /dev/null and b/theme/default/logo.png differ
diff --git a/theme/h4ck3r/css/base.css b/theme/h4ck3r/css/base.css
new file mode 100644 (file)
index 0000000..41b3a77
--- /dev/null
@@ -0,0 +1,1139 @@
+/** theme: h4ck3r base
+ *
+ * @package   Laconica
+ * @author Sarven Capadisli <csarven@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+* { margin:0; padding:0; }
+img { display:block; border:0; }
+a abbr { cursor: pointer; border-bottom:0; }
+table { border-collapse:collapse; }
+ol { list-style-position:inside; }
+html { font-size: 100%; background-color:#fff; height:100%; }
+body {
+background-color:#fff;
+color:#000;
+font-family:sans-serif;
+font-size:1em;
+line-height:1.65;
+position:relative;
+}
+h1,h2,h3,h4,h5,h6 {
+margin-bottom:7px;
+overflow:hidden;
+}
+h1 {
+font-size:1.4em;
+margin-bottom:18px;
+}
+#showstream h1 { display:none; }
+h2 { font-size:1.3em; }
+h3 { font-size:1.2em; }
+h4 { font-size:1.1em; }
+h5 { font-size:1em; }
+h6 { font-size:0.9em; }
+
+caption {
+font-weight:bold;
+}
+legend {
+font-weight:bold;
+font-size:1.3em;
+}
+input, textarea, select, option {
+padding:4px;
+font-family:sans-serif;
+font-size:1em;
+}
+input, textarea, select {
+border-width:2px;
+border-style: solid;
+}
+
+input.submit {
+font-weight:bold;
+cursor:pointer;
+}
+textarea {
+overflow:auto;
+}
+option {
+padding-bottom:0;
+}
+fieldset {
+padding:0;
+border:0;
+}
+form ul li {
+list-style-type:none;
+margin:0 0 18px 0;
+}
+form label {
+font-weight:bold;
+}
+input.checkbox {
+position:relative;
+top:2px;
+left:0;
+border:0;
+}
+
+.error,
+.success {
+padding:4px 1.55%;
+margin-bottom:18px;
+}
+form label.submit {
+display:none;
+}
+
+.form_settings {
+clear:both;
+}
+
+.form_settings fieldset {
+margin-bottom:29px;
+}
+.form_settings input.remove {
+margin-left:11px;
+}
+.form_settings .form_data li {
+width:100%;
+float:left;
+}
+.form_settings .form_data label {
+float:left;
+}
+.form_settings .form_data textarea,
+.form_settings .form_data select,
+.form_settings .form_data input {
+margin-left:11px;
+float:left;
+}
+.form_settings .form_data input.submit {
+margin-left:0;
+}
+
+.form_settings label {
+margin-top:2px;
+width:152px;
+}
+
+.form_actions label {
+display:none;
+}
+.form_guide {
+font-style:italic;
+}
+
+.form_settings #settings_autosubscribe label {
+display:inline;
+font-weight:bold;
+}
+
+#form_settings_profile legend,
+#form_login legend,
+#form_register legend,
+#form_password legend,
+#form_settings_avatar legend,
+#newgroup legend,
+#editgroup legend,
+#form_tag_user legend,
+#form_remote_subscribe legend,
+#form_openid_login legend,
+#form_search legend,
+#form_invite legend,
+#form_notice_delete legend,
+#form_password_recover legend,
+#form_password_change legend {
+display:none;
+}
+
+.form_settings .form_data p.form_guide {
+clear:both;
+margin-left:163px;
+margin-bottom:0;
+}
+
+.form_settings p {
+margin-bottom:11px;
+}
+
+.form_settings input.checkbox {
+margin-top:3px;
+margin-left:0;
+}
+.form_settings label.checkbox {
+font-weight:normal;
+margin-top:0;
+margin-right:0;
+margin-left:11px;
+float:left;
+width:90%;
+}
+
+
+#form_login p.form_guide,
+#form_register #settings_rememberme p.form_guide,
+#form_openid_login #settings_rememberme p.form_guide,
+#settings_twitter_remove p.form_guide,
+#form_search ul.form_data #q {
+margin-left:0;
+}
+
+.form_settings .form_note {
+padding:0 7px;
+}
+
+
+.form_settings input.form_action-primary {
+padding:0;
+}
+.form_settings input.form_action-secondary {
+margin-left:29px;
+}
+
+#form_search .submit {
+margin-left:11px;
+}
+
+address {
+float:left;
+margin-bottom:18px;
+margin-left:18px;
+}
+address.vcard img.logo {
+margin-right:0;
+}
+address .fn {
+font-weight:bold;
+}
+address img + .fn {
+display:none;
+}
+
+#header {
+width:100%;
+position:relative;
+float:left;
+padding-top:18px;
+margin-bottom:29px;
+}
+
+#site_nav_global_primary {
+float:right;
+margin-right:18px;
+margin-bottom:11px;
+margin-left:18px;
+}
+#site_nav_global_primary ul li {
+display:inline;
+margin-left:11px;
+}
+
+.system_notice dt {
+font-weight:bold;
+text-transform:uppercase;
+display:none;
+}
+
+#site_notice {
+float:left;
+clear:right;
+margin-top:7px;
+margin-right:18px;
+width:31%;
+}
+#page_notice {
+clear:both;
+margin-bottom:18px;
+}
+
+
+#anon_notice {
+float:right;
+clear:right;
+width:41.2%;
+padding:1.1%;
+border-width:2px;
+border-style:dashed;
+line-height:1.5;
+font-size:1.1em;
+font-weight:bold;
+-moz-transform:skewX(-30deg) scale(0.85);
+-webkit-transform:skewX(-30deg) scale(0.85);
+}
+
+
+#footer {
+float:left;
+width:64%;
+padding:18px;
+}
+
+#site_nav_local_views {
+width:100%;
+float:right;
+}
+#site_nav_local_views dt {
+display:none;
+}
+#site_nav_local_views li {
+float:right;
+margin-left:11px;
+list-style-type:none;
+}
+#site_nav_local_views a {
+float:left;
+text-decoration:none;
+padding:4px 11px;
+border-width:1px;
+border-style:dashed;
+border-bottom:0;
+text-shadow: 2px 2px 2px #ddd;
+font-weight:bold;
+}
+#site_nav_local_views .nav {
+float:left;
+width:100%;
+}
+
+#site_nav_global_primary dt,
+#site_nav_global_secondary dt {
+display:none;
+}
+
+#site_nav_global_secondary {
+margin-bottom:11px;
+}
+
+#site_nav_global_secondary ul li {
+display:inline;
+margin-right:11px;
+}
+#export_data li a {
+padding-left:20px;
+}
+#export_data li a.foaf {
+padding-left:30px;
+}
+#export_data li a.export_vcard {
+padding-left:28px;
+}
+
+#export_data ul {
+display:inline;
+}
+#export_data li {
+list-style-type:none;
+display:inline;
+margin-left:11px;
+}
+#export_data li:first-child {
+margin-left:0;
+}
+
+#licenses {
+font-size:0.9em;
+}
+
+#licenses dt {
+font-weight:bold;
+display:none;
+}
+#licenses dd {
+margin-bottom:11px;
+line-height:1.5;
+}
+
+#site_content_license_cc {
+margin-bottom:0;
+}
+#site_content_license_cc img {
+display:inline;
+vertical-align:top;
+margin-right:4px;
+}
+
+#wrap {
+margin:0 auto;
+width:100%;
+min-width:760px;
+max-width:1003px;
+overflow:hidden;
+}
+
+#core {
+position:relative;
+width:100%;
+float:left;
+margin-bottom:1em;
+}
+
+#content {
+width:60.009%;
+min-height:259px;
+padding:1.795%;
+float:right;
+border-style:dashed;
+border-width:1px;
+}
+#shownotice #content {
+min-height:0;
+}
+
+#content_inner {
+position:relative;
+width:100%;
+float:left;
+}
+
+#aside_primary {
+width:27.917%;
+min-height:259px;
+float:right;
+margin-right:4.385%;
+padding:1.795%;
+border-width:1px;
+border-style:dashed;
+}
+
+#form_notice {
+width:43.664%;
+float:right;
+position:relative;
+line-height:1;
+}
+#form_notice fieldset {
+border:0;
+padding:0;
+position:relative;
+}
+#form_notice legend {
+display:none;
+}
+#form_notice textarea {
+float:left;
+width:80.789%;
+height:67px;
+line-height:1.5;
+padding:7px 7px 16px 7px;
+}
+#form_notice label {
+display:block;
+float:left;
+font-size:1.3em;
+margin-bottom:7px;
+}
+#form_notice #notice_submit label {
+display:none;
+}
+#form_notice .form_note {
+position:absolute;
+top:99px;
+right:98px;
+z-index:9;
+}
+#form_notice .form_note dt {
+font-weight:bold;
+display:none;
+}
+#notice_text-count {
+font-weight:bold;
+line-height:1.15;
+padding:1px 2px;
+}
+#form_notice #notice_action-submit {
+width:14%;
+height:47px;
+padding:0;
+position:absolute;
+bottom:0;
+right:0;
+}
+#form_notice label[for=to] {
+margin-top:7px;
+}
+#form_notice select[id=to] {
+margin-bottom:7px;
+margin-left:18px;
+float:left;
+}
+#form_notice .error {
+float:left;
+clear:both;
+width:96.9%;
+margin-bottom:0;
+line-height:1.618;
+}
+
+/* entity_profile */
+.entity_profile {
+position:relative;
+width:67.702%;
+min-height:123px;
+float:left;
+margin-bottom:18px;
+margin-left:0;
+overflow:hidden;
+}
+.entity_profile dt,
+#entity_statistics dt {
+font-weight:bold;
+}
+.entity_profile dd {
+display:inline;
+}
+
+.entity_profile .entity_depiction {
+float:left;
+width:96px;
+margin-right:18px;
+margin-bottom:18px;
+}
+
+.entity_profile .entity_fn,
+.entity_profile .entity_nickname,
+.entity_profile .entity_location,
+.entity_profile .entity_url,
+.entity_profile .entity_note,
+.entity_profile .entity_tags {
+margin-left:113px;
+margin-bottom:4px;
+}
+
+.entity_profile .entity_fn,
+.entity_profile .entity_nickname {
+margin-left:11px;
+display:inline;
+font-weight:bold;
+}
+.entity_profile .entity_nickname {
+margin-left:0;
+}
+
+.entity_profile .entity_fn dd:before {
+content: "(";
+font-weight:normal;
+}
+.entity_profile .entity_fn dd:after {
+content: ")";
+font-weight:normal;
+}
+
+.entity_profile dt {
+display:none;
+}
+.entity_profile h2 {
+display:none;
+}
+/* entity_profile */
+
+
+/*entity_actions*/
+.entity_actions {
+float:right;
+margin-left:4.35%;
+max-width:25%;
+}
+.entity_actions h2 {
+display:none;
+}
+.entity_actions ul {
+list-style-type:none;
+}
+.entity_actions li {
+margin-bottom:4px;
+}
+.entity_actions li:first-child {
+border-top:0;
+}
+.entity_actions fieldset {
+border:0;
+padding:0;
+}
+.entity_actions legend {
+display:none;
+}
+
+.entity_actions input.submit {
+display:block;
+text-align:left;
+width:100%;
+}
+.entity_actions a,
+.entity_nudge p,
+.entity_remote_subscribe {
+text-decoration:none;
+font-weight:bold;
+display:block;
+}
+
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_send-a-message a,
+.entity_edit a,
+.form_user_nudge input.submit,
+.entity_nudge p {
+border:0;
+padding-left:20px;
+}
+
+.entity_edit a,
+.entity_send-a-message a,
+.entity_nudge p {
+padding:4px 4px 4px 23px;
+}
+
+.entity_remote_subscribe {
+padding:4px;
+border-width:2px;
+border-style:solid;
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+.entity_actions .accept {
+margin-bottom:18px;
+}
+
+.entity_tags ul {
+list-style-type:none;
+display:inline;
+}
+.entity_tags li {
+display:inline;
+margin-right:4px;
+}
+
+.aside .section {
+margin-bottom:29px;
+clear:both;
+float:left;
+width:100%;
+}
+.aside .section h2 {
+text-transform:uppercase;
+font-size:1em;
+}
+
+#entity_statistics dt,
+#entity_statistics dd {
+display:inline;
+}
+#entity_statistics dt:after {
+content: ":";
+}
+
+.section ul.entities {
+float:left;
+width:100%;
+}
+.section .entities li {
+list-style-type:none;
+float:left;
+margin-right:7px;
+margin-bottom:7px;
+}
+.section .entities li .photo {
+margin-right:0;
+margin-bottom:0;
+}
+.section .entities li .fn {
+display:none;
+}
+
+.aside .section p,
+.aside .section .more {
+clear:both;
+}
+
+.profile .entity_profile {
+margin-bottom:0;
+min-height:60px;
+}
+
+
+.profile .form_group_join legend,
+.profile .form_group_leave legend,
+.profile .form_user_subscribe legend,
+.profile .form_user_unsubscribe legend {
+display:none;
+}
+
+.profiles {
+list-style-type:none;
+}
+.profile .entity_profile .entity_location {
+width:auto;
+clear:none;
+margin-left:11px;
+}
+.profile .entity_profile dl,
+.profile .entity_profile dd {
+display:inline;
+float:none;
+}
+.profile .entity_profile .entity_note,
+.profile .entity_profile .entity_url,
+.profile .entity_profile .entity_tags,
+.profile .entity_profile .form_subscription_edit {
+margin-left:59px;
+clear:none;
+display:block;
+width:auto;
+}
+.profile .entity_profile .entity_tags dt {
+display:inline;
+margin-right:11px;
+}
+
+
+.profile .entity_profile .form_subscription_edit label {
+font-weight:normal;
+margin-right:11px;
+}
+
+
+/* NOTICE */
+.notice,
+.profile {
+position:relative;
+padding-top:11px;
+padding-bottom:11px;
+clear:both;
+float:left;
+width:100%;
+border-top-width:1px;
+border-top-style:dashed;
+}
+.notices li {
+list-style-type:none;
+}
+
+
+/* NOTICES */
+#notices_primary {
+float:left;
+width:100%;
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+}
+#notices_primary h2 {
+display:none;
+}
+.notice-data a span {
+display:block;
+padding-left:28px;
+}
+
+.notice .author {
+margin-right:11px;
+}
+
+.fn {
+overflow:hidden;
+}
+
+.notice .author .fn {
+font-weight:bold;
+}
+
+.vcard .photo {
+display:inline;
+margin-right:11px;
+float:left;
+}
+#shownotice .vcard .photo {
+margin-bottom:4px;
+}
+.vcard .url {
+text-decoration:none;
+}
+.vcard .url:hover {
+text-decoration:underline;
+}
+
+.notice .entry-title {
+display:inline;
+width:100%;
+overflow:hidden;
+}
+#shownotice .notice .entry-title {
+font-size:2.2em;
+}
+
+.notice p.entry-content {
+display:inline;
+}
+
+#content .notice p.entry-content a:visited {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+.notice p.entry-content .vcard a {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+
+.notice div.entry-content {
+float:left;
+font-size:0.95em;
+width:65%;
+}
+
+.notice .notice-options a,
+.notice .notice-options input {
+float:left;
+font-size:1.025em;
+}
+
+.notice div.entry-content dl,
+.notice div.entry-content dt,
+.notice div.entry-content dd {
+display:inline;
+}
+
+.notice div.entry-content .timestamp dt,
+.notice div.entry-content .response dt {
+display:none;
+}
+.notice div.entry-content .timestamp a {
+display:inline-block;
+}
+.notice div.entry-content .device dt {
+text-transform:lowercase;
+}
+
+
+.notice-options {
+padding-left:2%;
+float:left;
+width:50%;
+position:relative;
+font-size:0.95em;
+width:12.5%;
+float:right;
+}
+
+.notice-options a {
+float:left;
+}
+.notice-options .notice_delete,
+.notice-options .notice_reply,
+.notice-options .form_favor,
+.notice-options .form_disfavor {
+position:absolute;
+top:0;
+}
+.notice-options .form_favor,
+.notice-options .form_disfavor {
+left:0;
+}
+.notice-options .notice_reply {
+left:29px;
+}
+.notice-options .notice_delete {
+right:0;
+}
+.notice-options .notice_reply dt {
+display:none;
+}
+
+.notice-options input,
+.notice-options a {
+text-indent:-9999px;
+outline:none;
+}
+
+.notice-options .notice_reply a,
+.notice-options input.submit {
+display:block;
+border:0;
+}
+.notice-options .notice_reply a,
+.notice-options .notice_delete a {
+text-decoration:none;
+padding-left:16px;
+}
+
+.notice-options form input.submit {
+width:16px;
+padding:2px 0;
+}
+
+.notice-options .notice_delete dt,
+.notice-options .form_favor legend,
+.notice-options .form_disfavor legend {
+display:none;
+}
+.notice-options .notice_delete fieldset,
+.notice-options .form_favor fieldset,
+.notice-options .form_disfavor fieldset {
+border:0;
+padding:0;
+}
+
+
+#usergroups #new_group {
+float: left;
+margin-right: 2em;
+}
+#new_group, #group_search {
+margin-bottom:18px;
+}
+#new_group a {
+padding-left:20px;
+}
+
+
+#filter_tags {
+margin-bottom:11px;
+float:left;
+}
+#filter_tags dt {
+display:none;
+}
+#filter_tags ul {
+list-style-type:none;
+}
+#filter_tags ul li {
+float:left;
+margin-left:7px;
+padding-left:7px;
+border-left-width:1px;
+border-left-style:solid;
+}
+#filter_tags ul li.child_1 {
+margin-left:0;
+border-left:0;
+padding-left:0;
+}
+#filter_tags ul li#filter_tags_all a {
+font-weight:bold;
+margin-top:7px;
+float:left;
+}
+
+#filter_tags ul li#filter_tags_item label {
+margin-right:7px;
+}
+#filter_tags ul li#filter_tags_item label,
+#filter_tags ul li#filter_tags_item select {
+display:inline;
+}
+#filter_tags ul li#filter_tags_item p {
+float:left;
+margin-left:38px;
+}
+#filter_tags ul li#filter_tags_item input {
+position:relative;
+top:3px;
+left:3px;
+}
+
+
+
+.pagination {
+float:left;
+clear:both;
+width:100%;
+margin-top:18px;
+}
+
+.pagination dt {
+font-weight:bold;
+display:none;
+}
+
+.pagination .nav {
+float:left;
+width:100%;
+list-style-type:none;
+}
+
+.pagination .nav_prev {
+float:left;
+}
+.pagination .nav_next {
+float:right;
+}
+
+.pagination a {
+display:block;
+text-decoration:none;
+font-weight:bold;
+padding:7px;
+border-width:1px;
+border-style:solid;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+border-radius:7px;
+}
+
+.pagination .nav_prev a {
+padding-left:30px;
+}
+.pagination .nav_next a {
+padding-right:30px;
+}
+/* END: NOTICE */
+
+
+.hentry .entry-content p {
+margin-bottom:18px;
+}
+.system_notice ul,
+.instructions ul,
+.hentry entry-content ol,
+.hentry .entry-content ul {
+list-style-position:inside;
+}
+.hentry .entry-content li {
+margin-bottom:18px;
+}
+.hentry .entry-content li li {
+margin-left:18px;
+}
+
+
+
+
+/* TOP_POSTERS */
+.section tbody td {
+padding-right:11px;
+padding-bottom:11px;
+}
+.section .vcard .photo {
+margin-right:7px;
+margin-bottom:0;
+}
+
+.section .notice {
+padding-top:7px;
+padding-bottom:7px;
+border-top:0;
+}
+
+.section .notice:first-child {
+padding-top:0;
+}
+
+.section .notice .author {
+margin-right:0;
+}
+.section .notice .author .fn {
+display:none;
+}
+
+
+/* tagcloud */
+.tag-cloud {
+list-style-type:none;
+text-align:center;
+}
+.aside .tag-cloud {
+font-size:0.8em;
+}
+.tag-cloud li {
+display:inline;
+margin-right:7px;
+line-height:1.25;
+}
+.aside .tag-cloud li {
+line-height:1.5;
+}
+.tag-cloud li a {
+text-decoration:none;
+}
+#tagcloud.section dt {
+text-transform:uppercase;
+font-weight:bold;
+}
+.tag-cloud-1 {
+font-size:1em;
+}
+.tag-cloud-2 {
+font-size:1.25em;
+}
+.tag-cloud-3 {
+font-size:1.75em;
+}
+.tag-cloud-4 {
+font-size:2em;
+}
+.tag-cloud-5 {
+font-size:2.25em;
+}
+.tag-cloud-6 {
+font-size:2.75em;
+}
+.tag-cloud-7 {
+font-size:3.25em;
+}
+
+#publictagcloud #tagcloud.section dt {
+display:none;
+}
+
+#form_settings_photo .form_data {
+clear:both;
+}
+
+#form_settings_avatar li {
+width:auto;
+}
+#form_settings_avatar input {
+margin-left:0;
+}
+#avatar_original,
+#avatar_preview {
+float:left;
+}
+#avatar_preview {
+margin-left:29px;
+}
+#avatar_preview_view {
+height:96px;
+width:96px;
+margin-bottom:18px;
+overflow:hidden;
+}
+
+#settings_attach,
+#form_settings_avatar .form_actions {
+clear:both;
+}
+
+#form_settings_avatar .form_actions {
+margin-bottom:0;
+}
+
+#form_settings_design #settings_design_color .form_data,
+#form_settings_design #color-picker {
+float:left;
+}
+#form_settings_design #settings_design_color .form_data {
+width:400px;
+margin-right:28px;
+}
+
+.instructions ul {
+list-style-position:inside;
+}
+.instructions p,
+.instructions ul {
+margin-bottom:18px;
+}
+.help dt {
+display:none;
+}
+.guide {
+clear:both;
+}
diff --git a/theme/h4ck3r/css/display.css b/theme/h4ck3r/css/display.css
new file mode 100644 (file)
index 0000000..31d49a5
--- /dev/null
@@ -0,0 +1,236 @@
+/** theme: h4ck3r
+ *
+ * @package   Laconica
+ * @author Sarven Capadisli <csarven@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+@import url(base.css);
+
+html,
+body,
+a:active {
+background-color:#000;
+}
+
+body {
+background-image:url(../images/illustrations/illu_h4x0r1ng.gif);
+font-family: monospace;
+font-size:1em;
+color:#647819;
+}
+address {
+margin-right:7.18%;
+}
+
+input, textarea, select, option {
+font-family:  monospace;
+}
+input, textarea, select,
+.entity_remote_subscribe {
+border-color:#aaa;
+background-color:#000;
+color:#ccc;
+}
+#filter_tags ul li {
+border-color:#ddd;
+}
+
+.form_settings input.form_action-primary {
+background:none;
+}
+
+input.submit,
+#form_notice.warning #notice_text-count,
+.form_settings .form_note,
+.entity_remote_subscribe {
+background-color:rgba(0, 255, 0, 0.5);
+}
+
+input:focus, textarea:focus, select:focus,
+#form_notice.warning #notice_data-text {
+border-color:#9BB43E;
+}
+input.submit,
+.entity_remote_subscribe {
+color:#fff;
+}
+
+a,
+div.notice-options input,
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_send-a-message a,
+.form_user_nudge input.submit,
+.entity_nudge p,
+.form_settings input.form_action-primary {
+color:#0f0;
+}
+
+.notice,
+.profile {
+border-top-color:#333;
+}
+.section .profile {
+border-top-color:#87B4C8;
+}
+
+#aside_primary {
+background-color:rgba(0,128,0,0.3);
+}
+
+#notice_text-count {
+color:#0f0;
+}
+#form_notice.warning #notice_text-count {
+color:#000;
+}
+#form_notice.processing #notice_action-submit {
+background:#ccc url(../../base/images/icons/icon_processing.gif) no-repeat 47% 47%;
+cursor:wait;
+text-indent:-9999px;
+}
+
+#content,
+#site_nav_local_views a,
+#aside_primary {
+border-color:#50964D;
+}
+#content,
+#site_nav_local_views .current a {
+background-color:rgba(0, 0, 0, 0.698);
+}
+
+#site_nav_local_views a {
+background-color:rgba(0, 200, 0, 0.3);
+}
+#site_nav_local_views a:hover {
+background-color:rgba(255, 255, 255, 0.4);
+}
+
+.error {
+background-color:#F7E8E8;
+}
+.success {
+background-color:#EFF3DC;
+}
+
+#anon_notice {
+color:#ccc;
+border-color:#50964D;
+}
+
+#showstream #anon_notice {
+}
+
+#export_data li a {
+background-repeat:no-repeat;
+background-position:0 45%;
+}
+#export_data li a.rss {
+background-image:url(../../base/images/icons/icon_rss.png);
+}
+#export_data li a.atom {
+background-image:url(../../base/images/icons/icon_atom.png);
+}
+#export_data li a.foaf {
+background-image:url(../../base/images/icons/icon_foaf.gif);
+}
+
+.entity_edit a,
+.entity_send-a-message a,
+.form_user_nudge input.submit,
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_nudge p {
+background-position: 0 40%;
+background-repeat: no-repeat;
+background-color:transparent;
+}
+.form_group_join input.submit,
+.form_group_leave input.submit
+.form_user_subscribe input.submit,
+.form_user_unsubscribe input.submit {
+background-color:#9BB43E;
+color:#ccc;
+}
+.form_user_unsubscribe input.submit,
+.form_group_leave input.submit,
+.form_user_authorization input.reject {
+background-color:#87B4C8;
+}
+
+.entity_edit a {
+background-image:url(../../base/images/icons/twotone/green/edit.gif);
+}
+.entity_send-a-message a {
+background-image:url(../../base/images/icons/twotone/green/quote.gif);
+}
+.entity_nudge p,
+.form_user_nudge input.submit {
+background-image:url(../../base/images/icons/twotone/green/mail.gif);
+}
+.form_user_block input.submit,
+.form_user_unblock input.submit {
+background-image:url(../../base/images/icons/twotone/green/shield.gif);
+}
+
+/* NOTICES */
+.notices li.over {
+background-color:#fcfcfc;
+}
+
+.notice-options .notice_reply a,
+.notice-options form input.submit {
+background-color:transparent;
+}
+.notice-options .notice_reply a {
+background:transparent url(../../base/images/icons/twotone/green/reply.gif) no-repeat 0 45%;
+}
+.notice-options form.form_favor input.submit {
+background:transparent url(../../base/images/icons/twotone/green/favourite.gif) no-repeat 0 45%;
+}
+.notice-options form.form_disfavor input.submit {
+background:transparent url(../../base/images/icons/twotone/green/disfavourite.gif) no-repeat 0 45%;
+}
+.notice-options .notice_delete a {
+background:transparent url(../../base/images/icons/twotone/green/trash.gif) no-repeat 0 45%;
+}
+
+.notices div.entry-content,
+.notices div.notice-options {
+opacity:0.4;
+}
+.notices li.hover div.entry-content,
+.notices li.hover div.notice-options {
+opacity:1;
+}
+div.entry-content {
+color:#ccc;
+}
+div.notice-options a,
+div.notice-options input {
+font-family:sans-serif;
+}
+
+/*END: NOTICES */
+
+#new_group a {
+background:transparent url(../../base/images/icons/twotone/green/news.gif) no-repeat 0 45%;
+}
+
+.pagination .nav_prev a,
+.pagination .nav_next a {
+background-repeat:no-repeat;
+border-color:#000;
+}
+.pagination .nav_prev a {
+background-image:url(../../base/images/icons/twotone/green/arrow-left.gif);
+background-position:10% 45%;
+}
+.pagination .nav_next a {
+background-image:url(../../base/images/icons/twotone/green/arrow-right.gif);
+background-position:90% 45%;
+}
diff --git a/theme/h4ck3r/css/ie.css b/theme/h4ck3r/css/ie.css
new file mode 100644 (file)
index 0000000..2f463bb
--- /dev/null
@@ -0,0 +1,9 @@
+/* IE specific styles */
+
+.notice-options input.submit {
+color:#fff;
+}
+
+#site_nav_local_views a {
+background-color:#D0DFE7;
+}
diff --git a/theme/h4ck3r/default-avatar-mini.png b/theme/h4ck3r/default-avatar-mini.png
new file mode 100644 (file)
index 0000000..38b8692
Binary files /dev/null and b/theme/h4ck3r/default-avatar-mini.png differ
diff --git a/theme/h4ck3r/default-avatar-profile.png b/theme/h4ck3r/default-avatar-profile.png
new file mode 100644 (file)
index 0000000..f8357d4
Binary files /dev/null and b/theme/h4ck3r/default-avatar-profile.png differ
diff --git a/theme/h4ck3r/default-avatar-stream.png b/theme/h4ck3r/default-avatar-stream.png
new file mode 100644 (file)
index 0000000..6b63baa
Binary files /dev/null and b/theme/h4ck3r/default-avatar-stream.png differ
diff --git a/theme/h4ck3r/images/illustrations/illu_h4x0r1ng.gif b/theme/h4ck3r/images/illustrations/illu_h4x0r1ng.gif
new file mode 100644 (file)
index 0000000..c233af3
Binary files /dev/null and b/theme/h4ck3r/images/illustrations/illu_h4x0r1ng.gif differ
diff --git a/theme/h4ck3r/logo.png b/theme/h4ck3r/logo.png
new file mode 100644 (file)
index 0000000..fdead6c
Binary files /dev/null and b/theme/h4ck3r/logo.png differ
index cc19da0f708ecab64658599e1e93c5047ce6def3..2fb123a20a26cbb1ac3ffde2529256c9782207d0 100644 (file)
@@ -33,7 +33,7 @@ border-color:#aaa;
 border-color:#ddd;
 }
 
-.form_settings input.form_action-secondary {
+.form_settings input.form_action-primary {
 background:none;
 }
 
@@ -60,7 +60,7 @@ div.notice-options input,
 .entity_send-a-message a,
 .form_user_nudge input.submit,
 .entity_nudge p,
-.form_settings input.form_action-secondary {
+.form_settings input.form_action-primary {
 color:#002E6E;
 }
 
@@ -72,13 +72,6 @@ border-top-color:#CEE1E9;
 border-top-color:#87B4C8;
 }
 
-#content .notice p.entry-content a:visited {
-background-color:#fcfcfc;
-}
-#content .notice p.entry-content .vcard a {
-background-color:#fcfffc;
-}
-
 #aside_primary {
 background-color:#CEE1E9;
 }
@@ -182,10 +175,6 @@ background-image:url(../../base/images/icons/twotone/green/shield.gif);
 }
 
 /* NOTICES */
-.notices li.over {
-background-color:#fcfcfc;
-}
-
 .notice-options .notice_reply a,
 .notice-options form input.submit {
 background-color:transparent;
@@ -221,6 +210,19 @@ font-family:sans-serif;
 .notices li.hover {
 background-color:#fcfcfc;
 }
+
+.notices .notices {
+background-color:rgba(200, 200, 200, 0.025);
+}
+.notices .notices .notices {
+background-color:rgba(200, 200, 200, 0.050);
+}
+.notices .notices .notices .notices {
+background-color:rgba(200, 200, 200, 0.075);
+}
+.notices .notices .notices .notices .notices {
+background-color:rgba(200, 200, 200, 0.100);
+}
 /*END: NOTICES */
 
 #new_group a {
diff --git a/theme/otalk/css/base.css b/theme/otalk/css/base.css
new file mode 100644 (file)
index 0000000..b399925
--- /dev/null
@@ -0,0 +1,1211 @@
+/** theme: otalk base
+ *
+ * @package   Laconica
+ * @author Sarven Capadisli <csarven@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+* { margin:0; padding:0; }
+img { display:block; border:0; }
+a abbr { cursor: pointer; border-bottom:0; }
+table { border-collapse:collapse; }
+ol { list-style-position:inside; }
+html { font-size: 87.5%; background-color:#fff; }
+body {
+background-color:#fff;
+color:#000;
+font-family:sans-serif;
+font-size:1em;
+line-height:1.65;
+position:relative;
+}
+h1,h2,h3,h4,h5,h6 {
+margin-bottom:7px;
+overflow:hidden;
+}
+h1 {
+font-size:1.4em;
+margin-bottom:18px;
+}
+#showstream h1 { display:none; }
+h2 { font-size:1.3em; }
+h3 { font-size:1.2em; }
+h4 { font-size:1.1em; }
+h5 { font-size:1em; }
+h6 { font-size:0.9em; }
+
+caption {
+font-weight:bold;
+}
+legend {
+font-weight:bold;
+font-size:1.3em;
+}
+input, textarea, select, option {
+padding:4px;
+font-family:sans-serif;
+font-size:1em;
+}
+input, textarea, select {
+border-width:2px;
+border-style: solid;
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+
+input.submit {
+font-weight:bold;
+cursor:pointer;
+}
+textarea {
+overflow:auto;
+}
+option {
+padding-bottom:0;
+}
+fieldset {
+padding:0;
+border:0;
+}
+form ul li {
+list-style-type:none;
+margin:0 0 18px 0;
+}
+form label {
+font-weight:bold;
+}
+input.checkbox {
+position:relative;
+top:2px;
+left:0;
+border:0;
+}
+
+.error,
+.success {
+padding:4px 7px;
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+margin-bottom:18px;
+}
+form label.submit {
+display:none;
+}
+
+.form_settings {
+clear:both;
+}
+
+.form_settings fieldset {
+margin-bottom:29px;
+}
+.form_settings input.remove {
+margin-left:11px;
+}
+.form_settings .form_data li {
+width:100%;
+float:left;
+}
+.form_settings .form_data label {
+float:left;
+}
+.form_settings .form_data textarea,
+.form_settings .form_data select,
+.form_settings .form_data input {
+margin-left:11px;
+float:left;
+}
+.form_settings .form_data input.submit {
+margin-left:0;
+}
+
+.form_settings label {
+margin-top:2px;
+width:152px;
+}
+
+.form_actions label {
+display:none;
+}
+.form_guide {
+font-style:italic;
+}
+
+.form_settings #settings_autosubscribe label {
+display:inline;
+font-weight:bold;
+}
+
+#form_settings_profile legend,
+#form_login legend,
+#form_register legend,
+#form_password legend,
+#form_settings_avatar legend,
+#newgroup legend,
+#editgroup legend,
+#form_tag_user legend,
+#form_remote_subscribe legend,
+#form_openid_login legend,
+#form_search legend,
+#form_invite legend,
+#form_notice_delete legend,
+#form_password_recover legend,
+#form_password_change legend {
+display:none;
+}
+
+.form_settings .form_data p.form_guide {
+clear:both;
+margin-left:163px;
+margin-bottom:0;
+}
+
+.form_settings p {
+margin-bottom:11px;
+}
+
+.form_settings input.checkbox {
+margin-top:3px;
+margin-left:0;
+}
+.form_settings label.checkbox {
+font-weight:normal;
+margin-top:0;
+margin-right:0;
+margin-left:11px;
+float:left;
+width:90%;
+}
+
+
+#form_login p.form_guide,
+#form_register #settings_rememberme p.form_guide,
+#form_openid_login #settings_rememberme p.form_guide,
+#settings_twitter_remove p.form_guide,
+#form_search ul.form_data #q {
+margin-left:0;
+}
+
+.form_settings .form_note {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+padding:0 7px;
+}
+
+
+.form_settings input.form_action-primary {
+padding:0;
+}
+.form_settings input.form_action-secondary {
+margin-left:29px;
+}
+
+#form_search .submit {
+margin-left:11px;
+}
+
+address {
+float:left;
+margin-bottom:18px;
+margin-left:18px;
+}
+address.vcard img.logo {
+margin-right:0;
+}
+address .fn {
+font-weight:bold;
+}
+address img + .fn {
+display:none;
+}
+
+#header {
+width:100%;
+position:relative;
+float:left;
+padding-top:18px;
+margin-bottom:29px;
+}
+
+#site_nav_global_primary {
+float:right;
+margin-right:18px;
+margin-bottom:11px;
+margin-left:18px;
+}
+#site_nav_global_primary ul li {
+display:inline;
+margin-left:11px;
+}
+
+.system_notice dt {
+font-weight:bold;
+text-transform:uppercase;
+display:none;
+}
+
+#site_notice {
+position:absolute;
+top:65px;
+right:18px;
+width:250px;
+width:24%;
+}
+#page_notice {
+clear:both;
+margin-bottom:18px;
+}
+
+
+#anon_notice {
+float:left;
+width:43.2%;
+padding:1.1%;
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+border-width:2px;
+border-style:solid;
+line-height:1.5;
+font-size:1.1em;
+font-weight:bold;
+}
+
+
+#footer {
+float:left;
+width:64%;
+padding:18px;
+}
+
+#site_nav_local_views {
+float:left;
+}
+#site_nav_local_views dt {
+display:none;
+}
+#site_nav_local_views li {
+float:left;
+margin-right:18px;
+list-style-type:none;
+}
+#site_nav_local_views a {
+float:left;
+text-decoration:none;
+padding:4px 11px;
+-moz-border-radius-topleft:4px;
+-moz-border-radius-topright:4px;
+-webkit-border-top-left-radius:4px;
+-webkit-border-top-right-radius:4px;
+border-width:0;
+border-style:solid;
+border-bottom:0;
+text-shadow: 2px 2px 2px #ddd;
+font-weight:bold;
+}
+#site_nav_local_views .nav {
+float:left;
+width:100%;
+border-bottom-width:1px;
+border-bottom-style:solid;
+}
+
+#site_nav_global_primary dt,
+#site_nav_global_secondary dt {
+display:none;
+}
+
+#site_nav_global_secondary {
+margin-bottom:11px;
+}
+
+#site_nav_global_secondary ul li {
+display:inline;
+margin-right:11px;
+}
+#export_data li a {
+padding-left:20px;
+}
+#export_data li a.foaf {
+padding-left:30px;
+}
+#export_data li a.export_vcard {
+padding-left:28px;
+}
+
+#export_data ul {
+display:inline;
+}
+#export_data li {
+list-style-type:none;
+display:inline;
+margin-left:11px;
+}
+#export_data li:first-child {
+margin-left:0;
+}
+
+#licenses {
+font-size:0.9em;
+}
+
+#licenses dt {
+font-weight:bold;
+display:none;
+}
+#licenses dd {
+margin-bottom:11px;
+line-height:1.5;
+}
+
+#site_content_license_cc {
+margin-bottom:0;
+}
+#site_content_license_cc img {
+display:inline;
+vertical-align:top;
+margin-right:4px;
+}
+
+#wrap {
+margin:0 auto;
+width:100%;
+min-width:760px;
+max-width:1003px;
+overflow:hidden;
+}
+
+#core {
+position:relative;
+width:100%;
+float:left;
+margin-bottom:1em;
+}
+
+#content {
+width:67.9%;
+min-height:259px;
+padding-top:1.795%;
+padding-bottom:1.795%;
+float:left;
+clear:left;
+border-radius:7px;
+-moz-border-radius:7px;
+-moz-border-radius-topleft:0;
+-webkit-border-radius:7px;
+-webkit-border-top-left-radius:0;
+border-style:solid;
+border-width:0;
+margin-bottom:18px;
+}
+
+#content_inner {
+position:relative;
+width:100%;
+float:left;
+}
+
+#aside_primary {
+width:27.917%;
+min-height:259px;
+float:left;
+padding:1.795%;
+margin-left:0.385%;
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+border-width:1px;
+border-style:solid;
+}
+
+#form_notice {
+width:45.664%;
+float:left;
+position:relative;
+line-height:1;
+}
+#form_notice fieldset {
+border:0;
+padding:0;
+}
+#form_notice legend {
+display:none;
+}
+#form_notice textarea {
+float:left;
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+width:80.789%;
+height:67px;
+line-height:1.5;
+padding:7px 7px 16px 7px;
+}
+#form_notice label {
+display:block;
+float:left;
+font-size:1.3em;
+margin-bottom:7px;
+}
+#form_notice #notice_submit label {
+display:none;
+}
+#form_notice .form_note {
+position:absolute;
+top:99px;
+right:98px;
+z-index:9;
+}
+#form_notice .form_note dt {
+font-weight:bold;
+display:none;
+}
+#notice_text-count {
+font-weight:bold;
+line-height:1.15;
+padding:1px 2px;
+}
+#form_notice #notice_action-submit {
+width:14%;
+height:47px;
+padding:0;
+position:absolute;
+bottom:0;
+right:0;
+}
+#form_notice label[for=to] {
+margin-top:7px;
+}
+#form_notice select[id=to] {
+margin-bottom:7px;
+margin-left:18px;
+float:left;
+}
+
+
+/* entity_profile */
+.entity_profile {
+position:relative;
+width:521px;
+min-height:123px;
+float:left;
+margin-bottom:18px;
+margin-left:0;
+overflow:hidden;
+}
+.entity_profile dt,
+#entity_statistics dt {
+font-weight:bold;
+}
+.entity_profile dd {
+display:inline;
+}
+
+.entity_profile .entity_depiction {
+float:left;
+width:96px;
+margin-right:18px;
+margin-bottom:18px;
+}
+
+.entity_profile .entity_fn,
+.entity_profile .entity_nickname,
+.entity_profile .entity_location,
+.entity_profile .entity_url,
+.entity_profile .entity_note,
+.entity_profile .entity_tags {
+margin-left:113px;
+margin-bottom:4px;
+}
+
+.entity_profile .entity_fn,
+.entity_profile .entity_nickname {
+margin-left:11px;
+display:inline;
+font-weight:bold;
+}
+.entity_profile .entity_nickname {
+margin-left:0;
+}
+
+.entity_profile .entity_fn dd:before {
+content: "(";
+font-weight:normal;
+}
+.entity_profile .entity_fn dd:after {
+content: ")";
+font-weight:normal;
+}
+
+.entity_profile dt {
+display:none;
+}
+.entity_profile h2 {
+display:none;
+}
+/* entity_profile */
+
+
+/*entity_actions*/
+.entity_actions {
+float:left;
+margin-left:4.35%;
+max-width:25%;
+}
+.entity_actions h2 {
+display:none;
+}
+.entity_actions ul {
+list-style-type:none;
+}
+.entity_actions li {
+margin-bottom:4px;
+}
+.entity_actions li:first-child {
+border-top:0;
+}
+.entity_actions fieldset {
+border:0;
+padding:0;
+}
+.entity_actions legend {
+display:none;
+}
+
+.entity_actions input.submit {
+display:block;
+text-align:left;
+width:100%;
+}
+.entity_actions a,
+.entity_nudge p,
+.entity_remote_subscribe {
+text-decoration:none;
+font-weight:bold;
+display:block;
+}
+
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_send-a-message a,
+.entity_edit a,
+.form_user_nudge input.submit,
+.entity_nudge p {
+border:0;
+padding-left:20px;
+}
+
+.entity_edit a,
+.entity_send-a-message a,
+.entity_nudge p {
+padding:4px 4px 4px 23px;
+}
+
+.entity_remote_subscribe {
+padding:4px;
+border-width:2px;
+border-style:solid;
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+.entity_actions .accept {
+margin-bottom:18px;
+}
+
+.entity_tags ul {
+list-style-type:none;
+display:inline;
+}
+.entity_tags li {
+display:inline;
+margin-right:4px;
+}
+
+.aside .section {
+margin-bottom:29px;
+clear:both;
+float:left;
+width:100%;
+}
+.aside .section h2 {
+text-transform:uppercase;
+font-size:1em;
+}
+
+#entity_statistics dt,
+#entity_statistics dd {
+display:inline;
+}
+#entity_statistics dt:after {
+content: ":";
+}
+
+.section ul.entities {
+float:left;
+width:100%;
+}
+.section .entities li {
+list-style-type:none;
+float:left;
+margin-right:7px;
+margin-bottom:7px;
+}
+.section .entities li .photo {
+margin-right:0;
+margin-bottom:0;
+}
+.section .entities li .fn {
+display:none;
+}
+
+.aside .section p,
+.aside .section .more {
+clear:both;
+}
+
+.profile .entity_profile {
+margin-bottom:0;
+min-height:60px;
+}
+
+
+.profile .form_group_join legend,
+.profile .form_group_leave legend,
+.profile .form_user_subscribe legend,
+.profile .form_user_unsubscribe legend {
+display:none;
+}
+
+.profiles {
+list-style-type:none;
+}
+.profile .entity_profile .entity_location {
+width:auto;
+clear:none;
+margin-left:11px;
+}
+.profile .entity_profile dl,
+.profile .entity_profile dd {
+display:inline;
+float:none;
+}
+.profile .entity_profile .entity_note,
+.profile .entity_profile .entity_url,
+.profile .entity_profile .entity_tags,
+.profile .entity_profile .form_subscription_edit {
+margin-left:59px;
+clear:none;
+display:block;
+width:auto;
+}
+.profile .entity_profile .entity_tags dt {
+display:inline;
+margin-right:11px;
+}
+
+
+.profile .entity_profile .form_subscription_edit label {
+font-weight:normal;
+margin-right:11px;
+}
+
+
+/* NOTICE */
+.notice,
+.profile {
+position:relative;
+clear:both;
+float:left;
+width:100%;
+border-width:0;
+border-style:solid;
+margin-bottom:29px;
+}
+.notices li {
+list-style-type:none;
+}
+
+#content .notice {
+width:37%;
+margin-left:17px;
+margin-bottom:47px;
+clear:none;
+overflow:hidden;
+padding: 0 0 0 65px;
+min-height:235px;
+}
+
+#aside_primary .notice {
+margin-bottom:18px;
+}
+
+#shownotice #content .notice {
+width:96%;
+}
+
+
+/* NOTICES */
+#notices_primary {
+float:left;
+width:100%;
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+}
+#notices_primary h2 {
+display:none;
+}
+.notice-data a span {
+display:block;
+padding-left:28px;
+}
+
+.notice .author {
+margin-right:11px;
+}
+
+#content .notice .author {
+/*overflow:hidden;*/
+}
+
+.fn {
+overflow:hidden;
+}
+
+.notice .author .fn {
+font-weight:bold;
+}
+
+.notice .author .photo {
+margin-bottom:0;
+}
+
+#content .notice .author .photo {
+margin-left:-83px;
+padding-right:17px;
+}
+
+
+.vcard .photo {
+display:inline;
+margin-right:11px;
+margin-bottom:11px;
+float:left;
+}
+.vcard .url {
+text-decoration:none;
+}
+.vcard .url:hover {
+text-decoration:underline;
+}
+
+.notice .entry-title {
+float:left;
+width:100%;
+overflow:hidden;
+}
+#content .notice .entry-title {
+overflow:visible;
+margin-bottom:11px;
+padding:18px;
+width:85%;
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+min-height:161px;
+}
+
+#shownotice .notice .entry-title {
+font-size:2.2em;
+}
+
+.notice p.entry-content {
+display:inline;
+}
+
+#content .notice p.entry-content
+overflow:hidden;
+}
+
+.notice p.entry-content .vcard a {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+
+.notice div.entry-content {
+clear:left;
+float:left;
+font-size:0.95em;
+}
+#showstream .notice div.entry-content {
+margin-left:0;
+}
+
+.notice .notice-options a,
+.notice .notice-options input {
+float:left;
+font-size:1.025em;
+}
+
+.notice div.entry-content dl,
+.notice div.entry-content dt,
+.notice div.entry-content dd {
+display:inline;
+}
+
+.notice div.entry-content .timestamp dt,
+.notice div.entry-content .response dt {
+display:none;
+}
+.notice div.entry-content .timestamp a {
+display:inline-block;
+}
+.notice div.entry-content .device dt {
+text-transform:lowercase;
+}
+
+
+
+.notice-data {
+position:absolute;
+top:18px;
+right:0;
+min-height:50px;
+margin-bottom:4px;
+}
+.notice .entry-content .notice-data dt {
+display:none;
+}
+
+.notice-data a {
+display:block;
+outline:none;
+}
+
+.notice-options {
+position:absolute;
+top:120px;
+left:30px;
+font-size:0.95em;
+}
+
+.notice-options a {
+float:left;
+}
+.notice-options .notice_delete,
+.notice-options .notice_reply,
+.notice-options .form_favor,
+.notice-options .form_disfavor {
+position:absolute;
+left:0;
+}
+.notice-options .form_favor,
+.notice-options .form_disfavor {
+top:0;
+}
+.notice-options .notice_reply {
+top:29px;
+}
+.notice-options .notice_delete {
+top:58px;
+}
+.notice-options .notice_reply dt {
+display:none;
+}
+
+.notice-options input,
+.notice-options a {
+text-indent:-9999px;
+outline:none;
+}
+
+.notice-options .notice_reply a,
+.notice-options input.submit {
+display:block;
+border:0;
+}
+.notice-options .notice_reply a,
+.notice-options .notice_delete a {
+text-decoration:none;
+padding-left:16px;
+}
+
+.notice-options form input.submit {
+width:16px;
+padding:2px 0;
+}
+
+.notice-options .notice_delete dt,
+.notice-options .form_favor legend,
+.notice-options .form_disfavor legend {
+display:none;
+}
+.notice-options .notice_delete fieldset,
+.notice-options .form_favor fieldset,
+.notice-options .form_disfavor fieldset {
+border:0;
+padding:0;
+}
+
+
+#usergroups #new_group {
+float: left;
+margin-right: 2em;
+}
+#new_group, #group_search {
+margin-bottom:18px;
+}
+#new_group a {
+padding-left:20px;
+}
+
+
+#filter_tags {
+margin-bottom:11px;
+float:left;
+}
+#filter_tags dt {
+display:none;
+}
+#filter_tags ul {
+list-style-type:none;
+}
+#filter_tags ul li {
+float:left;
+margin-left:7px;
+padding-left:7px;
+border-left-width:1px;
+border-left-style:solid;
+}
+#filter_tags ul li.child_1 {
+margin-left:0;
+border-left:0;
+padding-left:0;
+}
+#filter_tags ul li#filter_tags_all a {
+font-weight:bold;
+margin-top:7px;
+float:left;
+}
+
+#filter_tags ul li#filter_tags_item label {
+margin-right:7px;
+}
+#filter_tags ul li#filter_tags_item label,
+#filter_tags ul li#filter_tags_item select {
+display:inline;
+}
+#filter_tags ul li#filter_tags_item p {
+float:left;
+margin-left:38px;
+}
+#filter_tags ul li#filter_tags_item input {
+position:relative;
+top:3px;
+left:3px;
+}
+
+
+
+.pagination {
+float:left;
+clear:both;
+width:100%;
+margin-top:18px;
+}
+
+.pagination dt {
+font-weight:bold;
+display:none;
+}
+
+.pagination .nav {
+float:left;
+width:100%;
+list-style-type:none;
+}
+
+.pagination .nav_prev {
+float:left;
+}
+.pagination .nav_next {
+float:right;
+}
+
+.pagination a {
+display:block;
+text-decoration:none;
+font-weight:bold;
+padding:7px;
+border-width:1px;
+border-style:solid;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+border-radius:7px;
+}
+
+.pagination .nav_prev a {
+padding-left:30px;
+}
+.pagination .nav_next a {
+padding-right:30px;
+}
+/* END: NOTICE */
+
+
+.hentry .entry-content p {
+margin-bottom:18px;
+}
+.hentry entry-content ol,
+.hentry .entry-content ul {
+list-style-position:inside;
+}
+.hentry .entry-content li {
+margin-bottom:18px;
+}
+.hentry .entry-content li li {
+margin-left:18px;
+}
+
+
+
+
+/* TOP_POSTERS */
+.section tbody td {
+padding-right:11px;
+padding-bottom:11px;
+}
+.section .vcard .photo {
+margin-right:7px;
+margin-bottom:0;
+}
+
+.section .notice {
+padding-top:7px;
+padding-bottom:7px;
+border-top:0;
+}
+
+.section .notice:first-child {
+padding-top:0;
+}
+
+.section .notice .author {
+margin-right:0;
+}
+.section .notice .author .fn {
+display:none;
+}
+
+
+/* tagcloud */
+.tag-cloud {
+list-style-type:none;
+text-align:center;
+}
+.aside .tag-cloud {
+font-size:0.8em;
+}
+.tag-cloud li {
+display:inline;
+margin-right:7px;
+line-height:1.25;
+}
+.aside .tag-cloud li {
+line-height:1.5;
+}
+.tag-cloud li a {
+text-decoration:none;
+}
+#tagcloud.section dt {
+text-transform:uppercase;
+font-weight:bold;
+}
+.tag-cloud-1 {
+font-size:1em;
+}
+.tag-cloud-2 {
+font-size:1.25em;
+}
+.tag-cloud-3 {
+font-size:1.75em;
+}
+.tag-cloud-4 {
+font-size:2em;
+}
+.tag-cloud-5 {
+font-size:2.25em;
+}
+.tag-cloud-6 {
+font-size:2.75em;
+}
+.tag-cloud-7 {
+font-size:3.25em;
+}
+
+#publictagcloud #tagcloud.section dt {
+display:none;
+}
+
+#form_settings_photo .form_data {
+clear:both;
+}
+
+#form_settings_avatar li {
+width:auto;
+}
+#form_settings_avatar input {
+margin-left:0;
+}
+#avatar_original,
+#avatar_preview {
+float:left;
+}
+#avatar_preview {
+margin-left:29px;
+}
+#avatar_preview_view {
+height:96px;
+width:96px;
+margin-bottom:18px;
+overflow:hidden;
+}
+
+#settings_attach,
+#form_settings_avatar .form_actions {
+clear:both;
+}
+
+#form_settings_avatar .form_actions {
+margin-bottom:0;
+}
+
+#form_settings_design #settings_design_color .form_data,
+#form_settings_design #color-picker {
+float:left;
+}
+#form_settings_design #settings_design_color .form_data {
+width:400px;
+margin-right:28px;
+}
+
+.instructions ul {
+list-style-position:inside;
+}
+.instructions p,
+.instructions ul {
+margin-bottom:18px;
+}
+.help dt {
+display:none;
+}
+.guide {
+clear:both;
+}
diff --git a/theme/otalk/css/display.css b/theme/otalk/css/display.css
new file mode 100644 (file)
index 0000000..d2a4719
--- /dev/null
@@ -0,0 +1,292 @@
+/** theme: otalk
+ *
+ * @package   Laconica
+ * @author Sarven Capadisli <csarven@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+@import url(base.css);
+
+html {
+}
+
+html,
+body,
+a:active {
+}
+body {
+font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
+font-size:1em;
+background:#ddd url(../images/illustrations/illu_pattern-01.png) repeat 0 0;
+background-color:rgba(127, 127, 127, 0.1);
+}
+address {
+margin-right:7.18%;
+}
+
+input, textarea, select, option {
+font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
+}
+input, textarea, select,
+.entity_remote_subscribe {
+border-color:#aaa;
+}
+#filter_tags ul li {
+border-color:#ddd;
+}
+
+.form_settings input.form_action-primary {
+background:none;
+}
+
+input.submit,
+#form_notice.warning #notice_text-count,
+.form_settings .form_note,
+.entity_remote_subscribe {
+background-color:#9BB43E;
+}
+
+input:focus, textarea:focus, select:focus,
+#form_notice.warning #notice_data-text {
+border-color:#9BB43E;
+}
+input.submit,
+.entity_remote_subscribe {
+color:#fff;
+}
+
+a,
+div.notice-options input,
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_send-a-message a,
+.form_user_nudge input.submit,
+.entity_nudge p,
+.form_settings input.form_action-primary {
+color:#8F0000;
+}
+
+.notice,
+.profile {
+border-color:#CEE1E9;
+}
+#content .notice .entry-title,
+input, textarea, select, option,
+.pagination .nav_prev a,
+.pagination .nav_next a {
+background-color:rgba(255,255,255,0.8);
+}
+
+#content .notices li.hover .entry-title {
+background-color:rgba(255,255,255,0.9);
+}
+
+#content .notice:nth-child(1) .entry-title {
+background-color:rgba(255,255,255,0.95);
+}
+#content .notice:nth-child(2) .entry-title {
+background-color:rgba(255,255,255,0.9);
+}
+#content .notice:nth-child(3) .entry-title {
+background-color:rgba(255,255,255,0.8);
+}
+#content .notice:nth-child(4) .entry-title {
+background-color:rgba(255,255,255,0.7);
+}
+#content .notice:nth-child(5) .entry-title {
+background-color:rgba(255,255,255,0.6);
+}
+#content .notice:nth-child(6) .entry-title {
+background-color:rgba(255,255,255,0.5);
+}
+#content .notice:nth-child(7) .entry-title {
+background-color:rgba(255,255,255,0.4);
+}
+#content .notice:nth-child(8) .entry-title {
+background-color:rgba(255,255,255,0.3);
+}
+#content .notice:nth-child(9) .entry-title {
+background-color:rgba(255,255,255,0.2);
+}
+#content .notice:nth-child(10) {
+background-color:rgba(255,255,255,0.1);
+}
+
+
+#content .notice .author .photo {
+background:url(../images/illustrations/illu_arrow-left-01.gif) no-repeat 100% 0;
+}
+
+.section .profile {
+border-top-color:#87B4C8;
+}
+
+#aside_primary {
+background-color:rgba(206, 225, 233,0.5);
+}
+
+#notice_text-count {
+color:#333;
+}
+#form_notice.warning #notice_text-count {
+color:#000;
+}
+#form_notice.processing #notice_action-submit {
+background:#fff url(../../base/images/icons/icon_processing.gif) no-repeat 47% 47%;
+cursor:wait;
+text-indent:-9999px;
+}
+
+#content,
+#site_nav_local_views .nav,
+#site_nav_local_views a,
+#aside_primary {
+border-color:#fff;
+}
+#content,
+#site_nav_local_views .current a {
+background-color:transparent;
+/*background-color:red;*/
+}
+
+#site_nav_local_views .current a {
+background-color:transparent;
+}
+
+#site_nav_local_views a {
+background-color:rgba(127, 127, 127, 0.2);
+}
+#site_nav_local_views a:hover {
+background-color:rgba(255, 255, 255, 0.8);
+}
+
+.error {
+background-color:#F7E8E8;
+}
+.success {
+background-color:#EFF3DC;
+}
+
+#anon_notice {
+background-color:rgba(206, 225, 233, 0.7);
+color:#fff;
+border-color:#fff;
+}
+
+#showstream #anon_notice {
+background-color:rgba(155, 180, 62, 0.7);
+}
+
+#export_data li a {
+background-repeat:no-repeat;
+background-position:0 45%;
+}
+#export_data li a.rss {
+background-image:url(../../base/images/icons/icon_rss.png);
+}
+#export_data li a.atom {
+background-image:url(../../base/images/icons/icon_atom.png);
+}
+#export_data li a.foaf {
+background-image:url(../../base/images/icons/icon_foaf.gif);
+}
+
+.entity_edit a,
+.entity_send-a-message a,
+.form_user_nudge input.submit,
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_nudge p {
+background-position: 0 40%;
+background-repeat: no-repeat;
+background-color:transparent;
+}
+.form_group_join input.submit,
+.form_group_leave input.submit
+.form_user_subscribe input.submit,
+.form_user_unsubscribe input.submit {
+background-color:#9BB43E;
+color:#fff;
+}
+.form_user_unsubscribe input.submit,
+.form_group_leave input.submit,
+.form_user_authorization input.reject {
+background-color:#87B4C8;
+}
+
+.entity_edit a {
+background-image:url(../../base/images/icons/twotone/green/edit.gif);
+}
+.entity_send-a-message a {
+background-image:url(../../base/images/icons/twotone/green/quote.gif);
+}
+.entity_nudge p,
+.form_user_nudge input.submit {
+background-image:url(../../base/images/icons/twotone/green/mail.gif);
+}
+.form_user_block input.submit,
+.form_user_unblock input.submit {
+background-image:url(../../base/images/icons/twotone/green/shield.gif);
+}
+
+/* NOTICES */
+.notices li.over {
+background-color:#fcfcfc;
+}
+
+.notice-options .notice_reply a,
+.notice-options form input.submit {
+background-color:transparent;
+}
+.notice-options .notice_reply a {
+background:transparent url(../../base/images/icons/twotone/green/reply.gif) no-repeat 0 45%;
+}
+.notice-options form.form_favor input.submit {
+background:transparent url(../../base/images/icons/twotone/green/favourite.gif) no-repeat 0 45%;
+}
+.notice-options form.form_disfavor input.submit {
+background:transparent url(../../base/images/icons/twotone/green/disfavourite.gif) no-repeat 0 45%;
+}
+.notice-options .notice_delete a {
+background:transparent url(../../base/images/icons/twotone/green/trash.gif) no-repeat 0 45%;
+}
+
+.notices div.entry-content,
+.notices div.notice-options {
+opacity:0.4;
+}
+.notices li.hover div.entry-content,
+.notices li.hover div.notice-options {
+opacity:1;
+}
+div.entry-content {
+color:#333;
+}
+div.notice-options a,
+div.notice-options input {
+font-family:sans-serif;
+}
+.notices li.hover {
+/*background-color:#fcfcfc;*/
+}
+/*END: NOTICES */
+
+#new_group a {
+background:transparent url(../../base/images/icons/twotone/green/news.gif) no-repeat 0 45%;
+}
+
+.pagination .nav_prev a,
+.pagination .nav_next a {
+background-repeat:no-repeat;
+border-color:#CEE1E9;
+}
+.pagination .nav_prev a {
+background-image:url(../../base/images/icons/twotone/green/arrow-left.gif);
+background-position:10% 45%;
+}
+.pagination .nav_next a {
+background-image:url(../../base/images/icons/twotone/green/arrow-right.gif);
+background-position:90% 45%;
+}
diff --git a/theme/otalk/css/ie.css b/theme/otalk/css/ie.css
new file mode 100644 (file)
index 0000000..2f463bb
--- /dev/null
@@ -0,0 +1,9 @@
+/* IE specific styles */
+
+.notice-options input.submit {
+color:#fff;
+}
+
+#site_nav_local_views a {
+background-color:#D0DFE7;
+}
diff --git a/theme/otalk/default-avatar-mini.png b/theme/otalk/default-avatar-mini.png
new file mode 100644 (file)
index 0000000..38b8692
Binary files /dev/null and b/theme/otalk/default-avatar-mini.png differ
diff --git a/theme/otalk/default-avatar-profile.png b/theme/otalk/default-avatar-profile.png
new file mode 100644 (file)
index 0000000..f8357d4
Binary files /dev/null and b/theme/otalk/default-avatar-profile.png differ
diff --git a/theme/otalk/default-avatar-stream.png b/theme/otalk/default-avatar-stream.png
new file mode 100644 (file)
index 0000000..6b63baa
Binary files /dev/null and b/theme/otalk/default-avatar-stream.png differ
diff --git a/theme/otalk/images/illustrations/illu_arrow-left-01.gif b/theme/otalk/images/illustrations/illu_arrow-left-01.gif
new file mode 100644 (file)
index 0000000..1977759
Binary files /dev/null and b/theme/otalk/images/illustrations/illu_arrow-left-01.gif differ
diff --git a/theme/otalk/images/illustrations/illu_pattern-01.png b/theme/otalk/images/illustrations/illu_pattern-01.png
new file mode 100644 (file)
index 0000000..5a72eaf
Binary files /dev/null and b/theme/otalk/images/illustrations/illu_pattern-01.png differ
diff --git a/theme/otalk/logo.png b/theme/otalk/logo.png
new file mode 100644 (file)
index 0000000..fdead6c
Binary files /dev/null and b/theme/otalk/logo.png differ
diff --git a/theme/pigeonthoughts/css/base.css b/theme/pigeonthoughts/css/base.css
new file mode 100644 (file)
index 0000000..08427d3
--- /dev/null
@@ -0,0 +1,1155 @@
+/** theme: pigeonthoughts base
+ *
+ * @package   Laconica
+ * @author Sarven Capadisli <csarven@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+* { margin:0; padding:0; }
+img { display:block; border:0; }
+a abbr { cursor: pointer; border-bottom:0; }
+table { border-collapse:collapse; }
+ol { list-style-position:inside; }
+html { font-size: 87.5%; background-color:#fff; }
+body {
+background-color:#fff;
+color:#000;
+font-family:sans-serif;
+font-size:1em;
+line-height:1.65;
+position:relative;
+margin-left:183px;
+}
+h1,h2,h3,h4,h5,h6 {
+margin-bottom:7px;
+overflow:hidden;
+}
+h1 {
+font-size:1.4em;
+margin-bottom:18px;
+}
+#showstream h1 { display:none; }
+h2 { font-size:1.3em; }
+h3 { font-size:1.2em; }
+h4 { font-size:1.1em; }
+h5 { font-size:1em; }
+h6 { font-size:0.9em; }
+
+caption {
+font-weight:bold;
+}
+legend {
+font-weight:bold;
+font-size:1.3em;
+}
+input, textarea, select, option {
+padding:4px;
+font-family:sans-serif;
+font-size:1em;
+}
+input, textarea, select {
+border-width:2px;
+border-style: solid;
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+
+input.submit {
+font-weight:bold;
+cursor:pointer;
+}
+textarea {
+overflow:auto;
+}
+option {
+padding-bottom:0;
+}
+fieldset {
+padding:0;
+border:0;
+}
+form ul li {
+list-style-type:none;
+margin:0 0 18px 0;
+}
+form label {
+font-weight:bold;
+}
+input.checkbox {
+position:relative;
+top:2px;
+left:0;
+border:0;
+}
+
+.error,
+.success {
+padding:4px 1.55%;
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+margin-bottom:18px;
+}
+form label.submit {
+display:none;
+}
+
+.form_settings {
+clear:both;
+}
+
+.form_settings fieldset {
+margin-bottom:29px;
+}
+.form_settings input.remove {
+margin-left:11px;
+}
+.form_settings .form_data li {
+width:100%;
+float:left;
+}
+.form_settings .form_data label {
+float:left;
+}
+.form_settings .form_data textarea,
+.form_settings .form_data select,
+.form_settings .form_data input {
+margin-left:11px;
+float:left;
+}
+.form_settings .form_data input.submit {
+margin-left:0;
+}
+
+.form_settings label {
+margin-top:2px;
+width:152px;
+}
+
+.form_actions label {
+display:none;
+}
+.form_guide {
+font-style:italic;
+}
+
+.form_settings #settings_autosubscribe label {
+display:inline;
+font-weight:bold;
+}
+
+#form_settings_profile legend,
+#form_login legend,
+#form_register legend,
+#form_password legend,
+#form_settings_avatar legend,
+#newgroup legend,
+#editgroup legend,
+#form_tag_user legend,
+#form_remote_subscribe legend,
+#form_openid_login legend,
+#form_search legend,
+#form_invite legend,
+#form_notice_delete legend,
+#form_password_recover legend,
+#form_password_change legend {
+display:none;
+}
+
+.form_settings .form_data p.form_guide {
+clear:both;
+margin-left:163px;
+margin-bottom:0;
+}
+
+.form_settings p {
+margin-bottom:11px;
+}
+
+.form_settings input.checkbox {
+margin-top:3px;
+margin-left:0;
+}
+.form_settings label.checkbox {
+font-weight:normal;
+margin-top:0;
+margin-right:0;
+margin-left:11px;
+float:left;
+width:90%;
+}
+
+
+#form_login p.form_guide,
+#form_register #settings_rememberme p.form_guide,
+#form_openid_login #settings_rememberme p.form_guide,
+#settings_twitter_remove p.form_guide,
+#form_search ul.form_data #q {
+margin-left:0;
+}
+
+.form_settings .form_note {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+padding:0 7px;
+}
+
+
+.form_settings input.form_action-primary {
+padding:0;
+}
+.form_settings input.form_action-secondary {
+margin-left:29px;
+}
+
+#form_search .submit {
+margin-left:11px;
+}
+
+address {
+float:right;
+margin-bottom:18px;
+margin-right:18px;
+}
+address.vcard img.logo {
+margin-right:0;
+}
+address .fn {
+font-weight:bold;
+}
+address img + .fn {
+display:none;
+}
+
+#header {
+width:98.5%;
+position:relative;
+float:left;
+padding-top:18px;
+padding-left:18px;
+margin-bottom:29px;
+}
+
+#site_nav_global_primary {
+float:left;
+margin-right:18px;
+margin-bottom:11px;
+}
+#site_nav_global_primary ul li {
+display:inline;
+margin-right:11px;
+}
+
+.system_notice dt {
+font-weight:bold;
+text-transform:uppercase;
+display:none;
+}
+
+#site_notice {
+float:right;
+margin-top:7px;
+margin-right:18px;
+width:26%;
+}
+#page_notice {
+clear:both;
+margin-bottom:18px;
+}
+
+
+#anon_notice {
+float:left;
+width:50.2%;
+line-height:1.5;
+font-size:1.1em;
+font-weight:bold;
+}
+
+
+#footer {
+float:left;
+width:64%;
+padding:18px;
+}
+
+#site_nav_local_views {
+width:183px;
+float:left;
+margin-bottom:29px;
+position:fixed;
+top:179px;
+left:0;
+}
+#site_nav_local_views dt {
+display:none;
+}
+#site_nav_local_views li {
+list-style-type:none;
+}
+#site_nav_local_views a {
+text-decoration:none;
+padding:4px 11px;
+text-shadow: 1px 1px 1px #ddd;
+font-weight:bold;
+display:block;
+}
+#site_nav_local_views .nav {
+float:left;
+width:100%;
+}
+
+#site_nav_global_primary dt,
+#site_nav_global_secondary dt {
+display:none;
+}
+
+#site_nav_global_secondary {
+margin-bottom:11px;
+}
+
+#site_nav_global_secondary ul li {
+display:inline;
+margin-right:11px;
+}
+#export_data li a {
+padding-left:20px;
+}
+#export_data li a.foaf {
+padding-left:30px;
+}
+#export_data li a.export_vcard {
+padding-left:28px;
+}
+
+#export_data ul {
+display:inline;
+}
+#export_data li {
+list-style-type:none;
+display:inline;
+margin-left:11px;
+}
+#export_data li:first-child {
+margin-left:0;
+}
+
+#licenses {
+font-size:0.9em;
+}
+
+#licenses dt {
+font-weight:bold;
+display:none;
+}
+#licenses dd {
+margin-bottom:11px;
+line-height:1.5;
+}
+
+#site_content_license_cc {
+margin-bottom:0;
+}
+#site_content_license_cc img {
+display:inline;
+vertical-align:top;
+margin-right:4px;
+}
+
+#wrap {
+width:100%;
+min-width:760px;
+max-width:1003px;
+overflow:hidden;
+}
+
+#core {
+position:relative;
+width:100%;
+float:left;
+margin-bottom:1em;
+}
+
+#content {
+width:50.009%;
+min-height:259px;
+float:left;
+margin-left:18px;
+}
+#shownotice #content {
+min-height:0;
+}
+
+#content_inner {
+position:relative;
+width:100%;
+float:left;
+}
+
+#aside_primary {
+width:45.917%;
+min-height:259px;
+float:left;
+margin-left:1.385%;
+padding-bottom:47px;
+}
+
+#form_notice {
+width:45.664%;
+float:left;
+position:relative;
+line-height:1;
+}
+#form_notice fieldset {
+border:0;
+padding:0;
+position:relative;
+}
+#form_notice legend {
+display:none;
+}
+#form_notice textarea {
+float:left;
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+width:80.789%;
+height:46px;
+line-height:1.5;
+padding:7px 7px 16px 7px;
+}
+#form_notice label {
+display:block;
+float:left;
+font-size:1.3em;
+margin-bottom:7px;
+}
+#form_notice #notice_submit label {
+display:none;
+}
+#form_notice .form_note {
+position:absolute;
+top:76px;
+right:98px;
+z-index:9;
+}
+#form_notice .form_note dt {
+font-weight:bold;
+display:none;
+}
+#notice_text-count {
+font-weight:bold;
+line-height:1.15;
+padding:1px 2px;
+}
+#form_notice #notice_action-submit {
+width:14%;
+height:47px;
+padding:0;
+position:absolute;
+bottom:0;
+right:0;
+}
+#form_notice label[for=to] {
+margin-top:7px;
+}
+#form_notice select[id=to] {
+margin-bottom:7px;
+margin-left:18px;
+float:left;
+}
+#form_notice .error {
+float:left;
+clear:both;
+width:96.9%;
+margin-bottom:0;
+line-height:1.618;
+}
+
+/* entity_profile */
+.entity_profile {
+position:relative;
+width:67.702%;
+min-height:123px;
+float:left;
+margin-bottom:18px;
+margin-left:0;
+overflow:hidden;
+}
+.entity_profile dt,
+#entity_statistics dt {
+font-weight:bold;
+}
+.entity_profile dd {
+display:inline;
+}
+
+.entity_profile .entity_depiction {
+float:left;
+width:96px;
+margin-right:18px;
+margin-bottom:18px;
+}
+
+.entity_profile .entity_fn,
+.entity_profile .entity_nickname,
+.entity_profile .entity_location,
+.entity_profile .entity_url,
+.entity_profile .entity_note,
+.entity_profile .entity_tags {
+margin-left:113px;
+margin-bottom:4px;
+}
+
+.entity_profile .entity_fn,
+.entity_profile .entity_nickname {
+margin-left:11px;
+display:inline;
+font-weight:bold;
+}
+.entity_profile .entity_nickname {
+margin-left:0;
+}
+
+.entity_profile .entity_fn dd:before {
+content: "(";
+font-weight:normal;
+}
+.entity_profile .entity_fn dd:after {
+content: ")";
+font-weight:normal;
+}
+
+.entity_profile dt {
+display:none;
+}
+.entity_profile h2 {
+display:none;
+}
+/* entity_profile */
+
+
+/*entity_actions*/
+.entity_actions {
+float:right;
+margin-left:4.35%;
+max-width:25%;
+}
+.entity_actions h2 {
+display:none;
+}
+.entity_actions ul {
+list-style-type:none;
+}
+.entity_actions li {
+margin-bottom:4px;
+}
+.entity_actions li:first-child {
+border-top:0;
+}
+.entity_actions fieldset {
+border:0;
+padding:0;
+}
+.entity_actions legend {
+display:none;
+}
+
+.entity_actions input.submit {
+display:block;
+text-align:left;
+width:100%;
+}
+.entity_actions a,
+.entity_nudge p,
+.entity_remote_subscribe {
+text-decoration:none;
+font-weight:bold;
+display:block;
+}
+
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_send-a-message a,
+.entity_edit a,
+.form_user_nudge input.submit,
+.entity_nudge p {
+border:0;
+padding-left:20px;
+}
+
+.entity_edit a,
+.entity_send-a-message a,
+.entity_nudge p {
+padding:4px 4px 4px 23px;
+}
+
+.entity_remote_subscribe {
+padding:4px;
+border-width:2px;
+border-style:solid;
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+.entity_actions .accept {
+margin-bottom:18px;
+}
+
+.entity_tags ul {
+list-style-type:none;
+display:inline;
+}
+.entity_tags li {
+display:inline;
+margin-right:4px;
+}
+
+.aside .section {
+margin-bottom:29px;
+float:right;
+width:44%;
+padding:1%;
+border-width:1px;
+border-style:solid;
+margin-left:2.5%;
+}
+.aside .section h2 {
+text-transform:uppercase;
+font-size:1em;
+}
+
+#entity_statistics dt,
+#entity_statistics dd {
+display:inline;
+}
+#entity_statistics dt:after {
+content: ":";
+}
+
+.section ul.entities {
+float:left;
+width:100%;
+}
+.section .entities li {
+list-style-type:none;
+float:left;
+margin-right:7px;
+margin-bottom:7px;
+}
+.section .entities li .photo {
+margin-right:0;
+margin-bottom:0;
+}
+.section .entities li .fn {
+display:none;
+}
+
+.aside .section p,
+.aside .section .more {
+clear:both;
+}
+
+.profile .entity_profile {
+margin-bottom:0;
+min-height:60px;
+}
+
+
+.profile .form_group_join legend,
+.profile .form_group_leave legend,
+.profile .form_user_subscribe legend,
+.profile .form_user_unsubscribe legend {
+display:none;
+}
+
+.profiles {
+list-style-type:none;
+}
+.profile .entity_profile .entity_location {
+width:auto;
+clear:none;
+margin-left:11px;
+}
+.profile .entity_profile dl,
+.profile .entity_profile dd {
+display:inline;
+float:none;
+}
+.profile .entity_profile .entity_note,
+.profile .entity_profile .entity_url,
+.profile .entity_profile .entity_tags,
+.profile .entity_profile .form_subscription_edit {
+margin-left:59px;
+clear:none;
+display:block;
+width:auto;
+}
+.profile .entity_profile .entity_tags dt {
+display:inline;
+margin-right:11px;
+}
+
+
+.profile .entity_profile .form_subscription_edit label {
+font-weight:normal;
+margin-right:11px;
+}
+
+
+/* NOTICE */
+.notice,
+.profile {
+position:relative;
+padding-top:11px;
+padding-bottom:11px;
+clear:both;
+float:left;
+width:96.41%;
+border-width:1px;
+border-style:solid;
+padding:1.795%;
+margin-bottom:11px;
+}
+.notices li {
+list-style-type:none;
+}
+
+#aside_primary .notice,
+#aside_primary .profile {
+border:0;
+margin-bottom:11px;
+}
+
+/* NOTICES */
+#notices_primary {
+float:left;
+width:100%;
+border-radius:7px;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+}
+#notices_primary h2 {
+display:none;
+}
+.notice-data a span {
+display:block;
+padding-left:28px;
+}
+
+.notice .author {
+margin-right:11px;
+}
+
+.fn {
+overflow:hidden;
+}
+
+.notice .author .fn {
+font-weight:bold;
+}
+
+.vcard .photo {
+display:inline;
+margin-right:11px;
+float:left;
+}
+#shownotice .vcard .photo {
+margin-bottom:4px;
+}
+.vcard .url {
+text-decoration:none;
+}
+.vcard .url:hover {
+text-decoration:underline;
+}
+
+.notice .entry-title {
+float:left;
+width:100%;
+overflow:hidden;
+}
+#shownotice .notice .entry-title {
+font-size:2.2em;
+}
+
+.notice p.entry-content {
+display:inline;
+}
+
+#content .notice p.entry-content a:visited {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+.notice p.entry-content .vcard a {
+border-radius:4px;
+-moz-border-radius:4px;
+-webkit-border-radius:4px;
+}
+
+.notice div.entry-content {
+clear:left;
+float:left;
+font-size:0.95em;
+margin-left:59px;
+width:65%;
+}
+#showstream .notice div.entry-content,
+#shownotice .notice div.entry-content {
+margin-left:0;
+}
+
+.notice .notice-options a,
+.notice .notice-options input {
+float:left;
+font-size:1.025em;
+}
+
+.notice div.entry-content dl,
+.notice div.entry-content dt,
+.notice div.entry-content dd {
+display:inline;
+}
+
+.notice div.entry-content .timestamp dt,
+.notice div.entry-content .response dt {
+display:none;
+}
+.notice div.entry-content .timestamp a {
+display:inline-block;
+}
+.notice div.entry-content .device dt {
+text-transform:lowercase;
+}
+
+
+.notice-options {
+padding-left:2%;
+float:left;
+width:50%;
+position:relative;
+font-size:0.95em;
+width:12.5%;
+float:right;
+}
+
+.notice-options a {
+float:left;
+}
+.notice-options .notice_delete,
+.notice-options .notice_reply,
+.notice-options .form_favor,
+.notice-options .form_disfavor {
+position:absolute;
+top:0;
+}
+.notice-options .form_favor,
+.notice-options .form_disfavor {
+left:0;
+}
+.notice-options .notice_reply {
+left:29px;
+}
+.notice-options .notice_delete {
+right:0;
+}
+.notice-options .notice_reply dt {
+display:none;
+}
+
+.notice-options input,
+.notice-options a {
+text-indent:-9999px;
+outline:none;
+}
+
+.notice-options .notice_reply a,
+.notice-options input.submit {
+display:block;
+border:0;
+}
+.notice-options .notice_reply a,
+.notice-options .notice_delete a {
+text-decoration:none;
+padding-left:16px;
+}
+
+.notice-options form input.submit {
+width:16px;
+padding:2px 0;
+}
+
+.notice-options .notice_delete dt,
+.notice-options .form_favor legend,
+.notice-options .form_disfavor legend {
+display:none;
+}
+.notice-options .notice_delete fieldset,
+.notice-options .form_favor fieldset,
+.notice-options .form_disfavor fieldset {
+border:0;
+padding:0;
+}
+
+
+#usergroups #new_group {
+float: left;
+margin-right: 2em;
+}
+#new_group, #group_search {
+margin-bottom:18px;
+}
+#new_group a {
+padding-left:20px;
+}
+
+
+#filter_tags {
+margin-bottom:11px;
+float:left;
+}
+#filter_tags dt {
+display:none;
+}
+#filter_tags ul {
+list-style-type:none;
+}
+#filter_tags ul li {
+float:left;
+margin-left:7px;
+padding-left:7px;
+border-left-width:1px;
+border-left-style:solid;
+}
+#filter_tags ul li.child_1 {
+margin-left:0;
+border-left:0;
+padding-left:0;
+}
+#filter_tags ul li#filter_tags_all a {
+font-weight:bold;
+margin-top:7px;
+float:left;
+}
+
+#filter_tags ul li#filter_tags_item label {
+margin-right:7px;
+}
+#filter_tags ul li#filter_tags_item label,
+#filter_tags ul li#filter_tags_item select {
+display:inline;
+}
+#filter_tags ul li#filter_tags_item p {
+float:left;
+margin-left:38px;
+}
+#filter_tags ul li#filter_tags_item input {
+position:relative;
+top:3px;
+left:3px;
+}
+
+
+
+.pagination {
+float:left;
+clear:both;
+width:100%;
+margin-top:18px;
+}
+
+.pagination dt {
+font-weight:bold;
+display:none;
+}
+
+.pagination .nav {
+float:left;
+width:100%;
+list-style-type:none;
+}
+
+.pagination .nav_prev {
+float:left;
+}
+.pagination .nav_next {
+float:right;
+}
+
+.pagination a {
+display:block;
+text-decoration:none;
+font-weight:bold;
+padding:7px;
+border-width:1px;
+border-style:solid;
+-moz-border-radius:7px;
+-webkit-border-radius:7px;
+border-radius:7px;
+}
+
+.pagination .nav_prev a {
+padding-left:30px;
+}
+.pagination .nav_next a {
+padding-right:30px;
+}
+/* END: NOTICE */
+
+
+.hentry .entry-content p {
+margin-bottom:18px;
+}
+.system_notice ul,
+.instructions ul,
+.hentry entry-content ol,
+.hentry .entry-content ul {
+list-style-position:inside;
+}
+.hentry .entry-content li {
+margin-bottom:18px;
+}
+.hentry .entry-content li li {
+margin-left:18px;
+}
+
+
+
+
+/* TOP_POSTERS */
+.section tbody td {
+padding-right:11px;
+padding-bottom:11px;
+}
+.section .vcard .photo {
+margin-right:7px;
+margin-bottom:0;
+}
+
+.section .notice {
+padding-top:7px;
+padding-bottom:7px;
+border-top:0;
+}
+
+.section .notice:first-child {
+padding-top:0;
+}
+
+.section .notice .author {
+margin-right:0;
+}
+.section .notice .author .fn {
+display:none;
+}
+
+
+/* tagcloud */
+.tag-cloud {
+list-style-type:none;
+text-align:center;
+}
+.aside .tag-cloud {
+font-size:0.8em;
+}
+.tag-cloud li {
+display:inline;
+margin-right:7px;
+line-height:1.25;
+}
+.aside .tag-cloud li {
+line-height:1.5;
+}
+.tag-cloud li a {
+text-decoration:none;
+}
+#tagcloud.section dt {
+text-transform:uppercase;
+font-weight:bold;
+}
+.tag-cloud-1 {
+font-size:1em;
+}
+.tag-cloud-2 {
+font-size:1.25em;
+}
+.tag-cloud-3 {
+font-size:1.75em;
+}
+.tag-cloud-4 {
+font-size:2em;
+}
+.tag-cloud-5 {
+font-size:2.25em;
+}
+.tag-cloud-6 {
+font-size:2.75em;
+}
+.tag-cloud-7 {
+font-size:3.25em;
+}
+
+#publictagcloud #tagcloud.section dt {
+display:none;
+}
+
+#form_settings_photo .form_data {
+clear:both;
+}
+
+#form_settings_avatar li {
+width:auto;
+}
+#form_settings_avatar input {
+margin-left:0;
+}
+#avatar_original,
+#avatar_preview {
+float:left;
+}
+#avatar_preview {
+margin-left:29px;
+}
+#avatar_preview_view {
+height:96px;
+width:96px;
+margin-bottom:18px;
+overflow:hidden;
+}
+
+#settings_attach,
+#form_settings_avatar .form_actions {
+clear:both;
+}
+
+#form_settings_avatar .form_actions {
+margin-bottom:0;
+}
+
+#form_settings_design #settings_design_color .form_data,
+#form_settings_design #color-picker {
+float:left;
+}
+#form_settings_design #settings_design_color .form_data {
+width:400px;
+margin-right:28px;
+}
+
+.instructions ul {
+list-style-position:inside;
+}
+.instructions p,
+.instructions ul {
+margin-bottom:18px;
+}
+.help dt {
+display:none;
+}
+.guide {
+clear:both;
+}
diff --git a/theme/pigeonthoughts/css/display.css b/theme/pigeonthoughts/css/display.css
new file mode 100644 (file)
index 0000000..af31cf7
--- /dev/null
@@ -0,0 +1,295 @@
+/** theme: pigeonthoughts
+ *
+ * @package   Laconica
+ * @author Sarven Capadisli <csarven@controlyourself.ca>
+ * @copyright 2009 Control Yourself, Inc.
+ * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link      http://laconi.ca/
+ */
+
+@import url(base.css);
+
+html {
+background:#fff url(../images/illustrations/illu_pigeons-01.png) no-repeat 0 100%;
+}
+
+body,
+a:active {
+background-color:#AEA187;
+}
+body {
+font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
+font-size:1em;
+}
+address {
+margin-left:2%;
+}
+
+input, textarea, select, option {
+font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
+}
+input, textarea, select,
+.entity_remote_subscribe {
+border-color:#aaa;
+}
+#filter_tags ul li {
+border-color:#ddd;
+}
+
+.form_settings input.form_action-primary {
+background:none;
+}
+
+input.submit,
+#form_notice.warning #notice_text-count,
+.form_settings .form_note,
+.entity_remote_subscribe {
+background-color:#8F0000;
+}
+
+input:focus, textarea:focus, select:focus,
+#form_notice.warning #notice_data-text {
+border-color:#8F0000;
+}
+input.submit,
+.entity_remote_subscribe {
+color:#fff;
+}
+
+a,
+div.notice-options input,
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_send-a-message a,
+.form_user_nudge input.submit,
+.entity_nudge p,
+.form_settings input.form_action-primary {
+color:#000;
+}
+
+.notice,
+.profile {
+border-color:#000;
+}
+.notice a,
+.profile a {
+color:#fff;
+}
+
+.notice:nth-child(3n-1),
+.profile:nth-child(3n-1) {
+border-color:#fff;
+}
+.notice:nth-child(3n-1) a,
+.profile:nth-child(3n-1) a {
+color:#7F1114;
+}
+.notice:nth-child(3n),
+.profile:nth-child(3n) {
+border-color:#7F1114;
+}
+.notice:nth-child(3n) a,
+.profile:nth-child(3n) a {
+color:#000;
+}
+
+.aside .section .notice,
+.aside .section .profile,
+.aside .section .notice:nth-child(3n-1),
+.aside .section .profile:nth-child(3n-1),
+.aside .section .notice:nth-child(3n),
+.aside .section .profile:nth-child(3n) {
+background-color:transparent;
+color:#000;
+}
+
+
+.aside .section {
+border-color:#fff;
+background-color:#fff;
+color:#000;
+}
+
+.aside .section:nth-child(n) {
+border-color:#000;
+background-color:#000;
+color:#fff;
+}
+.aside .section:nth-child(3n-1) {
+border-color:#fff;
+background-color:#fff;
+color:#000;
+}
+.aside .section:nth-child(3n) {
+background-color:#7F1114;
+border-color:#7F1114;
+color:#000;
+}
+.aside .section a {
+color:#7F1114;
+}
+.aside .section:nth-child(3n-1) a {
+color:#7F1114;
+}
+.aside .section:nth-child(3n) a {
+color:#fff;
+}
+
+
+.section .profile {
+border-top-color:#87B4C8;
+}
+
+#aside_primary {
+background:url(../images/illustrations/illu_pigeons-02.png) no-repeat 10% 100%;
+}
+
+#notice_text-count {
+color:#333;
+}
+#form_notice.warning #notice_text-count {
+color:#000;
+}
+#form_notice.processing #notice_action-submit {
+background:#fff url(../../base/images/icons/icon_processing.gif) no-repeat 47% 47%;
+cursor:wait;
+text-indent:-9999px;
+}
+
+#content,
+#site_nav_local_views a {
+border-color:#fff;
+}
+#site_nav_local_views .current a {
+background-color:rgba(143, 0, 0, 0.8);
+color:#fff;
+}
+
+#site_nav_local_views a {
+background-color:rgba(255, 255, 255, 0.3);
+}
+#site_nav_local_views a:hover {
+background-color:#fff;
+color:#8F0000;
+}
+
+.error {
+background-color:#F7E8E8;
+}
+.success {
+background-color:#EFF3DC;
+}
+
+#anon_notice {
+color:#000;
+}
+
+
+#export_data li a {
+background-repeat:no-repeat;
+background-position:0 45%;
+}
+#export_data li a.rss {
+background-image:url(../../base/images/icons/icon_rss.png);
+}
+#export_data li a.atom {
+background-image:url(../../base/images/icons/icon_atom.png);
+}
+#export_data li a.foaf {
+background-image:url(../../base/images/icons/icon_foaf.gif);
+}
+
+.entity_edit a,
+.entity_send-a-message a,
+.form_user_nudge input.submit,
+.form_user_block input.submit,
+.form_user_unblock input.submit,
+.entity_nudge p {
+background-position: 0 40%;
+background-repeat: no-repeat;
+background-color:transparent;
+}
+.form_group_join input.submit,
+.form_group_leave input.submit
+.form_user_subscribe input.submit,
+.form_user_unsubscribe input.submit {
+background-color:#8F0000;
+color:#fff;
+}
+.form_user_unsubscribe input.submit,
+.form_group_leave input.submit,
+.form_user_authorization input.reject {
+background-color:#87B4C8;
+}
+
+.entity_edit a {
+background-image:url(../../base/images/icons/twotone/green/edit.gif);
+}
+.entity_send-a-message a {
+background-image:url(../../base/images/icons/twotone/green/quote.gif);
+}
+.entity_nudge p,
+.form_user_nudge input.submit {
+background-image:url(../../base/images/icons/twotone/green/mail.gif);
+}
+.form_user_block input.submit,
+.form_user_unblock input.submit {
+background-image:url(../../base/images/icons/twotone/green/shield.gif);
+}
+
+/* NOTICES */
+.notices li.over {
+background-color:#fcfcfc;
+}
+
+.notice-options .notice_reply a,
+.notice-options form input.submit {
+background-color:transparent;
+}
+.notice-options .notice_reply a {
+background:transparent url(../../base/images/icons/twotone/green/reply.gif) no-repeat 0 45%;
+}
+.notice-options form.form_favor input.submit {
+background:transparent url(../../base/images/icons/twotone/green/favourite.gif) no-repeat 0 45%;
+}
+.notice-options form.form_disfavor input.submit {
+background:transparent url(../../base/images/icons/twotone/green/disfavourite.gif) no-repeat 0 45%;
+}
+.notice-options .notice_delete a {
+background:transparent url(../../base/images/icons/twotone/green/trash.gif) no-repeat 0 45%;
+}
+
+.notices div.entry-content,
+.notices div.notice-options {
+opacity:0.4;
+}
+.notices li.hover div.entry-content,
+.notices li.hover div.notice-options {
+opacity:1;
+}
+div.entry-content {
+color:#333;
+}
+div.notice-options a,
+div.notice-options input {
+font-family:sans-serif;
+}
+/*END: NOTICES */
+
+#new_group a {
+background:transparent url(../../base/images/icons/twotone/green/news.gif) no-repeat 0 45%;
+}
+
+.pagination .nav_prev a,
+.pagination .nav_next a {
+background-repeat:no-repeat;
+border-color:#000;
+}
+.pagination .nav_prev a {
+background-image:url(../../base/images/icons/twotone/green/arrow-left.gif);
+background-position:10% 45%;
+}
+.pagination .nav_next a {
+background-image:url(../../base/images/icons/twotone/green/arrow-right.gif);
+background-position:90% 45%;
+}
diff --git a/theme/pigeonthoughts/css/ie.css b/theme/pigeonthoughts/css/ie.css
new file mode 100644 (file)
index 0000000..2f463bb
--- /dev/null
@@ -0,0 +1,9 @@
+/* IE specific styles */
+
+.notice-options input.submit {
+color:#fff;
+}
+
+#site_nav_local_views a {
+background-color:#D0DFE7;
+}
diff --git a/theme/pigeonthoughts/default-avatar-mini.png b/theme/pigeonthoughts/default-avatar-mini.png
new file mode 100644 (file)
index 0000000..38b8692
Binary files /dev/null and b/theme/pigeonthoughts/default-avatar-mini.png differ
diff --git a/theme/pigeonthoughts/default-avatar-profile.png b/theme/pigeonthoughts/default-avatar-profile.png
new file mode 100644 (file)
index 0000000..f8357d4
Binary files /dev/null and b/theme/pigeonthoughts/default-avatar-profile.png differ
diff --git a/theme/pigeonthoughts/default-avatar-stream.png b/theme/pigeonthoughts/default-avatar-stream.png
new file mode 100644 (file)
index 0000000..6b63baa
Binary files /dev/null and b/theme/pigeonthoughts/default-avatar-stream.png differ
diff --git a/theme/pigeonthoughts/images/illustrations/illu_pigeons-01.png b/theme/pigeonthoughts/images/illustrations/illu_pigeons-01.png
new file mode 100644 (file)
index 0000000..4fdaaeb
Binary files /dev/null and b/theme/pigeonthoughts/images/illustrations/illu_pigeons-01.png differ
diff --git a/theme/pigeonthoughts/images/illustrations/illu_pigeons-02.png b/theme/pigeonthoughts/images/illustrations/illu_pigeons-02.png
new file mode 100644 (file)
index 0000000..187c6c8
Binary files /dev/null and b/theme/pigeonthoughts/images/illustrations/illu_pigeons-02.png differ
diff --git a/theme/pigeonthoughts/logo.png b/theme/pigeonthoughts/logo.png
new file mode 100644 (file)
index 0000000..fdead6c
Binary files /dev/null and b/theme/pigeonthoughts/logo.png differ
index 4998b3c988b48b553c1fe4de0df8425f5a5ca5ba..83b5a61d0d6d9627518ad735516d30a863bd7cfa 100644 (file)
@@ -23,14 +23,16 @@ Only alter this file if you want to change the layout of the site. Please note t
 ./default/css/display.css contains only the background images and colour rules:
 This file is a good basis for creating your own theme.
 
+Let's create a theme:
 
-1. Copy over the default theme to start off (replace 'mytheme'):
-cp -r ./default ./mytheme
+1. To start off, copy over the default theme:
+cp -r default mytheme
 
 2. Edit your mytheme stylesheet:
-nano ./mytheme/css/display.css
+nano mytheme/css/display.css
 
-3. Search and replace a colour or a path to the background image of your choice.
+a) Search and replace your colours and background images, or
+b) Create your own layout either importing a separate stylesheet (e.g., change to @import url(base.css);) or simply place it before the rest of the rules.
 
 4. Set /config.php to load 'mytheme':
 $config['site']['theme'] = 'mytheme';