summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README59
-rw-r--r--actions/api.php1
-rw-r--r--actions/attachment.php22
-rw-r--r--actions/conversation.php5
-rw-r--r--actions/finishopenidlogin.php2
-rw-r--r--actions/shownotice.php14
-rw-r--r--actions/twitapilaconica.php1
-rw-r--r--actions/twitapioembed.php173
-rw-r--r--classes/File.php5
-rw-r--r--classes/File_oembed.php55
-rw-r--r--classes/File_thumbnail.php6
-rw-r--r--classes/Notice.php3
-rw-r--r--classes/Notice_inbox.php41
-rw-r--r--classes/Profile.php4
-rw-r--r--db/laconica_pg.sql3
-rw-r--r--extlib/DB/DataObject.php243
-rw-r--r--extlib/DB/DataObject/Cast.php9
-rw-r--r--extlib/DB/DataObject/Error.php2
-rw-r--r--extlib/DB/DataObject/Generator.php68
-rw-r--r--[-rwxr-xr-x]extlib/DB/DataObject/createTables.php2
-rw-r--r--extlib/Services/oEmbed.php4
-rw-r--r--extlib/facebook/facebook.php6
-rwxr-xr-xextlib/facebook/facebookapi_php5_restlib.php354
-rw-r--r--install.php4
-rw-r--r--js/util.js2
-rw-r--r--lib/common.php2
-rw-r--r--lib/daemon.php11
-rw-r--r--lib/messageform.php6
-rw-r--r--lib/queuehandler.php11
-rw-r--r--lib/router.php9
-rw-r--r--lib/rssaction.php16
-rw-r--r--lib/twitterapi.php62
-rw-r--r--plugins/Comet/CometPlugin.php166
-rw-r--r--plugins/Comet/cometupdate.js30
-rw-r--r--plugins/FBConnect/FBConnectPlugin.php14
-rw-r--r--plugins/FBConnect/README77
-rw-r--r--plugins/Meteor/MeteorPlugin.php160
-rw-r--r--plugins/Meteor/json2.js478
-rw-r--r--plugins/Meteor/meteorupdater.js136
-rw-r--r--plugins/PiwikAnalyticsPlugin.php31
-rw-r--r--plugins/Realtime/RealtimePlugin.php229
-rw-r--r--plugins/Realtime/json2.js (renamed from plugins/Comet/json2.js)0
-rw-r--r--plugins/Realtime/realtimeupdate.js (renamed from plugins/Comet/updatetimeline.js)87
-rw-r--r--scripts/triminboxes.php40
-rwxr-xr-xscripts/twitterstatusfetcher.php31
-rw-r--r--theme/base/css/display.css6
46 files changed, 1436 insertions, 1254 deletions
diff --git a/README b/README
index 2c92a75da..b65a08d42 100644
--- a/README
+++ b/README
@@ -3,7 +3,7 @@ README
------
Laconica 0.8.0 ("Shiny Happy People")
-8 July 2009
+15 July 2009
This is the README file for Laconica, the Open Source microblogging
platform. It includes installation instructions, descriptions of
@@ -116,6 +116,16 @@ This is a major feature release since version 0.7.4, released May 31
as default TOS for Laconica sites.
- Better command-line handling for scripts, including standard options
and ability to set hostname and path from the command line.
+- An experimental plugin to use Meteor (http://www.meteorserver.org/)
+ for "real-time" updates.
+- A new framework for "real-time" updates, making it easier to develop
+ plugins for different browser-based update modes.
+- RSS 2.0 and Atom feeds for groups.
+- RSS 2.0 and Atom feeds for tags.
+- Attachments can be sent by email.
+- Attachments are encoded as enclosures in RSS 2.0 and Atom.
+- Notices with attachments display in Facebook as media inline.
+
- Many, many bug fixes.
Prerequisites
@@ -124,7 +134,7 @@ Prerequisites
The following software packages are *required* for this software to
run correctly.
-- PHP 5.2.x. It may be possible to run this software on earlier
+- PHP 5.2.3+. It may be possible to run this software on earlier
versions of PHP, but many of the functions used are only available
in PHP 5.2 or above.
- MySQL 5.x. The Laconica database is stored, by default, in a MySQL
@@ -1419,6 +1429,51 @@ notify third-party servers of updates.
notify: an array of URLs for ping endpoints. Default is the empty
array (no notification).
+Plugins
+=======
+
+Beginning with the 0.7.x branch, Laconica has supported a simple but
+powerful plugin architecture. Important events in the code are named,
+like 'StartNoticeSave', and other software can register interest
+in those events. When the events happen, the other software is called
+and has a choice of accepting or rejecting the events.
+
+In the simplest case, you can add a function to config.php and use the
+Event::addHandler() function to hook an event:
+
+ function AddGoogleLink($action)
+ {
+ $action->menuItem('http://www.google.com/', _('Google'), _('Search engine'));
+ return true;
+ }
+
+ Event::addHandler('EndPrimaryNav', 'AddGoogleLink');
+
+This adds a menu item to the end of the main navigation menu. You can
+see the list of existing events, and parameters that handlers must
+implement, in EVENTS.txt.
+
+The Plugin class in lib/plugin.php makes it easier to write more
+complex plugins. Sub-classes can just create methods named
+'onEventName', where 'EventName' is the name of the event (case
+matters!). These methods will be automatically registered as event
+handlers by the Plugin constructor (which you must call from your own
+class's constructor).
+
+Several example plugins are included in the plugins/ directory. You
+can enable a plugin with the following line in config.php:
+
+ addPlugin('Example', array('param1' => 'value1',
+ 'param2' => 'value2'));
+
+This will look for and load files named 'ExamplePlugin.php' or
+'Example/ExamplePlugin.php' either in the plugins/ directory (for
+plugins that ship with Laconica) or in the local/ directory (for
+plugins you write yourself or that you get from somewhere else) or
+local/plugins/.
+
+Plugins are documented in their own directories.
+
Troubleshooting
===============
diff --git a/actions/api.php b/actions/api.php
index 452ed8e82..8b92889f8 100644
--- a/actions/api.php
+++ b/actions/api.php
@@ -129,6 +129,7 @@ class ApiAction extends Action
'laconica/config',
'laconica/wadl',
'tags/timeline',
+ 'oembed/oembed',
'groups/timeline');
static $bareauth = array('statuses/user_timeline',
diff --git a/actions/attachment.php b/actions/attachment.php
index ee4cd9640..c6a5d0d52 100644
--- a/actions/attachment.php
+++ b/actions/attachment.php
@@ -98,6 +98,28 @@ class AttachmentAction extends Action
return $a->title();
}
+ function extraHead()
+ {
+ $this->element('link',array('rel'=>'alternate',
+ 'type'=>'application/json+oembed',
+ 'href'=>common_local_url(
+ 'api',
+ array('apiaction'=>'oembed','method'=>'oembed.json'),
+ array('url'=>
+ common_local_url('attachment',
+ array('attachment' => $this->attachment->id)))),
+ 'title'=>'oEmbed'),null);
+ $this->element('link',array('rel'=>'alternate',
+ 'type'=>'text/xml+oembed',
+ 'href'=>common_local_url(
+ 'api',
+ array('apiaction'=>'oembed','method'=>'oembed.xml'),
+ array('url'=>
+ common_local_url('attachment',
+ array('attachment' => $this->attachment->id)))),
+ 'title'=>'oEmbed'),null);
+ }
+
/**
* Handle input
*
diff --git a/actions/conversation.php b/actions/conversation.php
index 0eb0d86d6..c8755ba6e 100644
--- a/actions/conversation.php
+++ b/actions/conversation.php
@@ -116,6 +116,11 @@ class ConversationAction extends Action
$cnt = $ct->show();
}
+
+ function isReadOnly()
+ {
+ return true;
+ }
}
/**
diff --git a/actions/finishopenidlogin.php b/actions/finishopenidlogin.php
index e9f7c746b..ff0b35218 100644
--- a/actions/finishopenidlogin.php
+++ b/actions/finishopenidlogin.php
@@ -83,7 +83,7 @@ class FinishopenidloginAction extends Action
function showContent()
{
if (!empty($this->message_text)) {
- $this->element('p', null, $this->message);
+ $this->element('div', array('class' => 'error'), $this->message_text);
return;
}
diff --git a/actions/shownotice.php b/actions/shownotice.php
index 1ec38a76b..8f73dc824 100644
--- a/actions/shownotice.php
+++ b/actions/shownotice.php
@@ -275,6 +275,20 @@ class ShownoticeAction extends OwnerDesignAction
$this->element('meta', array('name' => 'microid',
'content' => $id->toString()));
}
+ $this->element('link',array('rel'=>'alternate',
+ 'type'=>'application/json+oembed',
+ 'href'=>common_local_url(
+ 'api',
+ array('apiaction'=>'oembed','method'=>'oembed.json'),
+ array('url'=>$this->notice->uri)),
+ 'title'=>'oEmbed'),null);
+ $this->element('link',array('rel'=>'alternate',
+ 'type'=>'text/xml+oembed',
+ 'href'=>common_local_url(
+ 'api',
+ array('apiaction'=>'oembed','method'=>'oembed.xml'),
+ array('url'=>$this->notice->uri)),
+ 'title'=>'oEmbed'),null);
}
}
diff --git a/actions/twitapilaconica.php b/actions/twitapilaconica.php
index 8cd7a64b9..442fdbcef 100644
--- a/actions/twitapilaconica.php
+++ b/actions/twitapilaconica.php
@@ -171,4 +171,5 @@ class TwitapilaconicaAction extends TwitterapiAction
parent::handle($args);
$this->serverError(_('API method under construction.'), 501);
}
+
}
diff --git a/actions/twitapioembed.php b/actions/twitapioembed.php
new file mode 100644
index 000000000..3019e5878
--- /dev/null
+++ b/actions/twitapioembed.php
@@ -0,0 +1,173 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Laconica-only extensions to the Twitter-like API
+ *
+ * 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 Twitter
+ * @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);
+}
+
+require_once INSTALLDIR.'/lib/twitterapi.php';
+
+/**
+ * Oembed provider implementation
+ *
+ * This class handles all /main/oembed(.xml|.json)/ requests.
+ *
+ * @category oEmbed
+ * @package Laconica
+ * @author Craig Andrews <candrews@integralblue.com>
+ * @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/
+ */
+
+class TwitapioembedAction extends TwitterapiAction
+{
+
+ function oembed($args, $apidata)
+ {
+ parent::handle($args);
+
+ common_debug("in oembed api action");
+
+ $this->auth_user = $apidata['user'];
+
+ $url = $args['url'];
+ if( substr(strtolower($url),0,strlen(common_root_url())) == strtolower(common_root_url()) ){
+ $path = substr($url,strlen(common_root_url()));
+
+ $r = Router::get();
+
+ $proxy_args = $r->map($path);
+
+ if (!$proxy_args) {
+ $this->serverError(_("$path not found"), 404);
+ }
+ $oembed=array();
+ $oembed['version']='1.0';
+ $oembed['provider_name']=common_config('site', 'name');
+ $oembed['provider_url']=common_root_url();
+ switch($proxy_args['action']){
+ case 'shownotice':
+ $oembed['type']='link';
+ $id = $proxy_args['notice'];
+ $notice = Notice::staticGet($id);
+ if(empty($notice)){
+ $this->serverError(_("notice $id not found"), 404);
+ }
+ $profile = $notice->getProfile();
+ if (empty($profile)) {
+ $this->serverError(_('Notice has no profile'), 500);
+ }
+ if (!empty($profile->fullname)) {
+ $authorname = $profile->fullname . ' (' . $profile->nickname . ')';
+ } else {
+ $authorname = $profile->nickname;
+ }
+ $oembed['title'] = sprintf(_('%1$s\'s status on %2$s'),
+ $authorname,
+ common_exact_date($notice->created));
+ $oembed['author_name']=$authorname;
+ $oembed['author_url']=$profile->profileurl;
+ $oembed['url']=($notice->url?$notice->url:$notice->uri);
+ $oembed['html']=$notice->rendered;
+ break;
+ case 'attachment':
+ $id = $proxy_args['attachment'];
+ $attachment = File::staticGet($id);
+ if(empty($attachment)){
+ $this->serverError(_("attachment $id not found"), 404);
+ }
+ if(empty($attachment->filename) && $file_oembed = File_oembed::staticGet('file_id', $attachment->id)){
+ // Proxy the existing oembed information
+ $oembed['type']=$file_oembed->type;
+ $oembed['provider']=$file_oembed->provider;
+ $oembed['provider_url']=$file_oembed->provider_url;
+ $oembed['width']=$file_oembed->width;
+ $oembed['height']=$file_oembed->height;
+ $oembed['html']=$file_oembed->html;
+ $oembed['title']=$file_oembed->title;
+ $oembed['author_name']=$file_oembed->author_name;
+ $oembed['author_url']=$file_oembed->author_url;
+ $oembed['url']=$file_oembed->url;
+ }else if(substr($attachment->mimetype,0,strlen('image/'))=='image/'){
+ $oembed['type']='photo';
+ //TODO set width and height
+ //$oembed['width']=
+ //$oembed['height']=
+ $oembed['url']=$attachment->url;
+ }else{
+ $oembed['type']='link';
+ $oembed['url']=common_local_url('attachment',
+ array('attachment' => $attachment->id));
+ }
+ if($attachment->title) $oembed['title']=$attachment->title;
+ break;
+ default:
+ $this->serverError(_("$path not supported for oembed requests"), 501);
+ }
+
+ switch($apidata['content-type']){
+ case 'xml':
+ $this->init_document('xml');
+ $this->elementStart('oembed');
+ $this->element('version',null,$oembed['version']);
+ $this->element('type',null,$oembed['type']);
+ if($oembed['provider_name']) $this->element('provider_name',null,$oembed['provider_name']);
+ if($oembed['provider_url']) $this->element('provider_url',null,$oembed['provider_url']);
+ if($oembed['title']) $this->element('title',null,$oembed['title']);
+ if($oembed['author_name']) $this->element('author_name',null,$oembed['author_name']);
+ if($oembed['author_url']) $this->element('author_url',null,$oembed['author_url']);
+ if($oembed['url']) $this->element('url',null,$oembed['url']);
+ if($oembed['html']) $this->element('html',null,$oembed['html']);
+ if($oembed['width']) $this->element('width',null,$oembed['width']);
+ if($oembed['height']) $this->element('height',null,$oembed['height']);
+ if($oembed['cache_age']) $this->element('cache_age',null,$oembed['cache_age']);
+ if($oembed['thumbnail_url']) $this->element('thumbnail_url',null,$oembed['thumbnail_url']);
+ if($oembed['thumbnail_width']) $this->element('thumbnail_width',null,$oembed['thumbnail_width']);
+ if($oembed['thumbnail_height']) $this->element('thumbnail_height',null,$oembed['thumbnail_height']);
+
+
+ $this->elementEnd('oembed');
+ $this->end_document('xml');
+ break;
+ case 'json':
+ $this->init_document('json');
+ print(json_encode($oembed));
+ $this->end_document('json');
+ break;
+ default:
+ $this->serverError(_('content type ' . $apidata['content-type'] . ' not supported'), 501);
+ }
+
+ }else{
+ $this->serverError(_('Only ' . common_root_url() . ' urls over plain http please'), 404);
+ }
+ }
+}
+
diff --git a/classes/File.php b/classes/File.php
index 56d9f9827..68d385d1e 100644
--- a/classes/File.php
+++ b/classes/File.php
@@ -79,9 +79,8 @@ class File extends Memcached_DataObject
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);
+ && ($oembed_data = File_oembed::_getOembed($given_url))) {
+ File_oembed::saveNew($oembed_data, $file_id);
}
return $x;
}
diff --git a/classes/File_oembed.php b/classes/File_oembed.php
index 69230e4a4..bbf112729 100644
--- a/classes/File_oembed.php
+++ b/classes/File_oembed.php
@@ -56,33 +56,46 @@ class File_oembed extends Memcached_DataObject
return array(false, false, false);
}
- function _getOembed($url, $maxwidth = 500, $maxheight = 400, $format = 'json') {
- $cmd = common_config('oohembed', 'endpoint') . '?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 _getOembed($url, $maxwidth = 500, $maxheight = 400) {
+ require_once INSTALLDIR.'/extlib/Services/oEmbed.php';
+ $parameters = array(
+ 'maxwidth'=>$maxwidth,
+ 'maxheight'=>$maxheight,
+ );
+ try{
+ $oEmbed = new Services_oEmbed($url);
+ $object = $oEmbed->getObject($parameters);
+ return $object;
+ }catch(Exception $e){
+ try{
+ $oEmbed = new Services_oEmbed($url, array(
+ Services_oEmbed::OPTION_API => common_config('oohembed', 'endpoint')
+ ));
+ $object = $oEmbed->getObject($parameters);
+ return $object;
+ }catch(Exception $ex){
+ return false;
+ }
+ }
}
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->version = $data->version;
+ $file_oembed->type = $data->type;
+ if (!empty($data->provider_name)) $file_oembed->provider = $data->provider_name;
+ if (!empty($data->provider)) $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'])) {
+ if (!empty($data->thumbnail_url)) {
File_thumbnail::saveNew($data, $file_id);
}
}
diff --git a/classes/File_thumbnail.php b/classes/File_thumbnail.php
index 44b92a2fa..0b09c6af8 100644
--- a/classes/File_thumbnail.php
+++ b/classes/File_thumbnail.php
@@ -51,9 +51,9 @@ class File_thumbnail extends Memcached_DataObject
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->url = $data->thumbnail_url;
+ $tn->width = intval($data->thumbnail_width);
+ $tn->height = intval($data->thumbnail_height);
$tn->insert();
}
}
diff --git a/classes/Notice.php b/classes/Notice.php
index 8974e22f8..101fadb67 100644
--- a/classes/Notice.php
+++ b/classes/Notice.php
@@ -875,6 +875,9 @@ class Notice extends Memcached_DataObject
}
$qry .= '('.$id.', '.$this->id.', '.$source.", '".$this->created. "') ";
$cnt++;
+ if (rand() % NOTICE_INBOX_SOFT_LIMIT == 0) {
+ Notice_inbox::gc($id);
+ }
if ($cnt >= MAX_BOXCARS) {
$inbox = new Notice_inbox();
$inbox->query($qry);
diff --git a/classes/Notice_inbox.php b/classes/Notice_inbox.php
index 940381f84..2af34b1a4 100644
--- a/classes/Notice_inbox.php
+++ b/classes/Notice_inbox.php
@@ -24,6 +24,10 @@ require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
// We keep 5 pages of inbox notices in memcache, +1 for pagination check
define('INBOX_CACHE_WINDOW', 101);
+define('NOTICE_INBOX_GC_BOXCAR', 128);
+define('NOTICE_INBOX_GC_MAX', 12800);
+define('NOTICE_INBOX_LIMIT', 1000);
+define('NOTICE_INBOX_SOFT_LIMIT', 1000);
define('NOTICE_INBOX_SOURCE_SUB', 1);
define('NOTICE_INBOX_SOURCE_GROUP', 2);
@@ -100,4 +104,41 @@ class Notice_inbox extends Memcached_DataObject
{
return Memcached_DataObject::pkeyGet('Notice_inbox', $kv);
}
+
+ static function gc($user_id)
+ {
+ $entry = new Notice_inbox();
+ $entry->user_id = $user_id;
+ $entry->orderBy('created DESC');
+ $entry->limit(NOTICE_INBOX_LIMIT - 1, NOTICE_INBOX_GC_MAX);
+
+ $total = $entry->find();
+
+ if ($total > 0) {
+ $notices = array();
+ $cnt = 0;
+ while ($entry->fetch()) {
+ $notices[] = $entry->notice_id;
+ $cnt++;
+ if ($cnt >= NOTICE_INBOX_GC_BOXCAR) {
+ self::deleteMatching($user_id, $notices);
+ $notices = array();
+ $cnt = 0;
+ }
+ }
+
+ if ($cnt > 0) {
+ self::deleteMatching($user_id, $notices);
+ $notices = array();
+ }
+ }
+ }
+
+ static function deleteMatching($user_id, $notices)
+ {
+ $entry = new Notice_inbox();
+ return $entry->query('DELETE FROM notice_inbox '.
+ 'WHERE user_id = ' . $user_id . ' ' .
+ 'AND notice_id in ('.implode(',', $notices).')');
+ }
}
diff --git a/classes/Profile.php b/classes/Profile.php
index 224b61bd2..372005cdd 100644
--- a/classes/Profile.php
+++ b/classes/Profile.php
@@ -360,7 +360,6 @@ class Profile extends Memcached_DataObject
$c->set(common_cache_key('profile:subscription_count:'.$this->id), $cnt);
}
- common_debug("subscriptionCount == $cnt");
return $cnt;
}
@@ -385,7 +384,6 @@ class Profile extends Memcached_DataObject
$c->set(common_cache_key('profile:subscriber_count:'.$this->id), $cnt);
}
- common_debug("subscriberCount == $cnt");
return $cnt;
}
@@ -407,7 +405,6 @@ class Profile extends Memcached_DataObject
$c->set(common_cache_key('profile:fave_count:'.$this->id), $cnt);
}
- common_debug("faveCount == $cnt");
return $cnt;
}
@@ -430,7 +427,6 @@ class Profile extends Memcached_DataObject
$c->set(common_cache_key('profile:notice_count:'.$this->id), $cnt);
}
- common_debug("noticeCount == $cnt");
return $cnt;
}
diff --git a/db/laconica_pg.sql b/db/laconica_pg.sql
index dae8b8faf..2d8286c60 100644
--- a/db/laconica_pg.sql
+++ b/db/laconica_pg.sql
@@ -1,3 +1,4 @@
+
/* local and remote users have profiles */
create sequence profile_seq;
@@ -184,7 +185,7 @@ create table token (
create table nonce (
consumer_key varchar(255) not null /* comment 'unique identifier, root URL' */,
- tok char(32) not null /* comment 'identifying value' */,
+ tok char(32) /* comment 'buggy old value, ignored' */,
nonce char(32) null /* comment 'buggy old value, ignored */,
ts integer not null /* comment 'timestamp sent' values are epoch, and only used internally */,
diff --git a/extlib/DB/DataObject.php b/extlib/DB/DataObject.php
index 0c6a13dc2..8e226b8fa 100644
--- a/extlib/DB/DataObject.php
+++ b/extlib/DB/DataObject.php
@@ -2,11 +2,11 @@
/**
* Object Based Database Query Builder and data store
*
- * PHP versions 4 and 5
+ * For PHP versions 4,5 and 6
*
- * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * LICENSE: This source file is subject to version 3.01 of the PHP license
* that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * http://www.php.net/license/3_01.txt. If you did not receive a copy of
* the PHP License and are unable to obtain it through the web, please
* send a note to license@php.net so we can mail you a copy immediately.
*
@@ -14,8 +14,8 @@
* @package DB_DataObject
* @author Alan Knowles <alan@akbkhome.com>
* @copyright 1997-2006 The PHP Group
- * @license http://www.php.net/license/3_0.txt PHP License 3.0
- * @version CVS: $Id: DataObject.php,v 1.439 2008/01/30 02:14:06 alan_k Exp $
+ * @license http://www.php.net/license/3_01.txt PHP License 3.01
+ * @version CVS: $Id: DataObject.php 284150 2009-07-15 23:27:59Z alan_k $
* @link http://pear.php.net/package/DB_DataObject
*/
@@ -235,7 +235,7 @@ class DB_DataObject extends DB_DataObject_Overload
* @access private
* @var string
*/
- var $_DB_DataObject_version = "1.8.8";
+ var $_DB_DataObject_version = "1.8.11";
/**
* The Database table (used by table extends)
@@ -1027,7 +1027,13 @@ class DB_DataObject extends DB_DataObject_Overload
if ($leftq || $useNative) {
$table = ($quoteIdentifiers ? $DB->quoteIdentifier($this->__table) : $this->__table);
- $r = $this->_query("INSERT INTO {$table} ($leftq) VALUES ($rightq) ");
+
+ if (($dbtype == 'pgsql') && empty($leftq)) {
+ $r = $this->_query("INSERT INTO {$table} DEFAULT VALUES");
+ } else {
+ $r = $this->_query("INSERT INTO {$table} ($leftq) VALUES ($rightq) ");
+ }
+
@@ -1339,7 +1345,7 @@ class DB_DataObject extends DB_DataObject_Overload
* build the condition only using the object parameters.
*
* @access public
- * @return mixed True on success, false on failure, 0 on no data affected
+ * @return mixed Int (No. of rows affected) on success, false on failure, 0 on no data affected
*/
function delete($useWhere = false)
{
@@ -1369,7 +1375,13 @@ class DB_DataObject extends DB_DataObject_Overload
if (($this->_query !== false) && $this->_query['condition']) {
$table = ($quoteIdentifiers ? $DB->quoteIdentifier($this->__table) : $this->__table);
- $sql = "DELETE FROM {$table} {$this->_query['condition']}{$extra_cond}";
+ $sql = "DELETE ";
+ // using a joined delete. - with useWhere..
+ $sql .= (!empty($this->_join) && $useWhere) ?
+ "{$table} FROM {$table} {$this->_join} " :
+ "FROM {$table} ";
+
+ $sql .= $this->_query['condition']. $extra_cond;
// add limit..
@@ -1521,15 +1533,15 @@ class DB_DataObject extends DB_DataObject_Overload
}
$keys = $this->keys();
- if (!$keys[0] && !is_string($countWhat)) {
+ if (empty($keys[0]) && (!is_string($countWhat) || (strtoupper($countWhat) == 'DISTINCT'))) {
$this->raiseError(
- "You cannot do run count without keys - use \$do->keys('id');",
+ "You cannot do run count without keys - use \$do->count('id'), or use \$do->count('distinct id')';",
DB_DATAOBJECT_ERROR_INVALIDARGS,PEAR_ERROR_DIE);
return false;
}
$table = ($quoteIdentifiers ? $DB->quoteIdentifier($this->__table) : $this->__table);
- $key_col = ($quoteIdentifiers ? $DB->quoteIdentifier($keys[0]) : $keys[0]);
+ $key_col = empty($keys[0]) ? '' : (($quoteIdentifiers ? $DB->quoteIdentifier($keys[0]) : $keys[0]));
$as = ($quoteIdentifiers ? $DB->quoteIdentifier('DATAOBJECT_NUM') : 'DATAOBJECT_NUM');
// support distinct on default keys.
@@ -2044,7 +2056,7 @@ class DB_DataObject extends DB_DataObject_Overload
// technically postgres native here...
// we need to get the new improved tabledata sorted out first.
- if ( in_array($dbtype , array('psql', 'mysql', 'mysqli', 'mssql', 'ifx')) &&
+ if ( in_array($dbtype , array('pgsql', 'mysql', 'mysqli', 'mssql', 'ifx')) &&
($table[$usekey] & DB_DATAOBJECT_INT) &&
isset($realkeys[$usekey]) && ($realkeys[$usekey] == 'N')
) {
@@ -2125,10 +2137,13 @@ class DB_DataObject extends DB_DataObject_Overload
$this->_loadConfig();
}
// Set database driver for reference
- $db_driver = empty($_DB_DATAOBJECT['CONFIG']['db_driver']) ? 'DB' : $_DB_DATAOBJECT['CONFIG']['db_driver'];
- // is it already connected ?
-
+ $db_driver = empty($_DB_DATAOBJECT['CONFIG']['db_driver']) ?
+ 'DB' : $_DB_DATAOBJECT['CONFIG']['db_driver'];
+
+ // is it already connected ?
if ($this->_database_dsn_md5 && !empty($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5])) {
+
+ // connection is an error...
if (PEAR::isError($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5])) {
return $this->raiseError(
$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->message,
@@ -2137,7 +2152,7 @@ class DB_DataObject extends DB_DataObject_Overload
}
- if (!$this->_database) {
+ if (empty($this->_database)) {
$this->_database = $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->dsn['database'];
$hasGetDatabase = method_exists($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5], 'getDatabase');
@@ -2166,6 +2181,7 @@ class DB_DataObject extends DB_DataObject_Overload
// try and work out what to use for the dsn !
$options= &$_DB_DATAOBJECT['CONFIG'];
+ // if the databse dsn dis defined in the object..
$dsn = isset($this->_database_dsn) ? $this->_database_dsn : null;
if (!$dsn) {
@@ -2173,14 +2189,14 @@ class DB_DataObject extends DB_DataObject_Overload
$this->_database = isset($options["table_{$this->__table}"]) ? $options["table_{$this->__table}"] : null;
}
if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
- $this->debug("Checking for database database_{$this->_database} in options","CONNECT");
+ $this->debug("Checking for database specific ini ('{$this->_database}') : database_{$this->_database} in options","CONNECT");
}
if ($this->_database && !empty($options["database_{$this->_database}"])) {
-
$dsn = $options["database_{$this->_database}"];
} else if (!empty($options['database'])) {
$dsn = $options['database'];
+
}
}
@@ -2205,6 +2221,9 @@ class DB_DataObject extends DB_DataObject_Overload
if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
$this->debug("USING CACHED CONNECTION", "CONNECT",3);
}
+
+
+
if (!$this->_database) {
$hasGetDatabase = method_exists($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5], 'getDatabase');
@@ -2221,7 +2240,7 @@ class DB_DataObject extends DB_DataObject_Overload
return true;
}
if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
- $this->debug("NEW CONNECTION", "CONNECT",3);
+ $this->debug("NEW CONNECTION TP DATABASE :" .$this->_database , "CONNECT",3);
/* actualy make a connection */
$this->debug(print_r($dsn,true) ." {$this->_database_dsn_md5}", "CONNECT",3);
}
@@ -2265,8 +2284,8 @@ class DB_DataObject extends DB_DataObject_Overload
);
}
-
- if (!$this->_database) {
+
+ if (empty($this->_database)) {
$hasGetDatabase = method_exists($_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5], 'getDatabase');
$this->_database = ($db_driver != 'DB' && $hasGetDatabase)
@@ -2357,38 +2376,38 @@ 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);
- } else {
- switch (strtolower(substr(trim($string),0,6))) {
+ for ($tries = 0;$tries < 3;$tries++) {
- case 'insert':
- case 'update':
- case 'delete':
- $result = $DB->exec($string);
- break;
-
- default:
- $result = $DB->query($string);
- break;
+ if ($_DB_driver == 'DB') {
+
+ $result = $DB->query($string);
+ } else {
+ switch (strtolower(substr(trim($string),0,6))) {
+
+ case 'insert':
+ case 'update':
+ case 'delete':
+ $result = $DB->exec($string);
+ break;
+
+ default:
+ $result = $DB->query($string);
+ break;
+ }
}
+
+ // see if we got a failure.. - try again a few times..
+ if (!is_a($result,'PEAR_Error')) {
+ break;
+ }
+ if ($result->getCode() != -14) { // *DB_ERROR_NODBSELECTED
+ break; // not a connection error..
+ }
+ sleep(1); // wait before retyring..
+ $DB->connect($DB->dsn);
}
-
- // 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'])) {
@@ -2556,11 +2575,13 @@ class DB_DataObject extends DB_DataObject_Overload
* use @ to silence it (if you are sure it is acceptable)
* eg. $do = @DB_DataObject::factory('person')
*
- * table name will eventually be databasename/table
+ * table name can bedatabasename/table
* - and allow modular dataobjects to be written..
* (this also helps proxy creation)
*
- *
+ * Experimental Support for Multi-Database factory eg. mydatabase.mytable
+ *
+ *
* @param string $table tablename (use blank to create a new instance of the same class.)
* @access private
* @return DataObject|PEAR_Error
@@ -2570,9 +2591,27 @@ class DB_DataObject extends DB_DataObject_Overload
function factory($table = '') {
global $_DB_DATAOBJECT;
+
+
+ // multi-database support.. - experimental.
+ $database = '';
+
+ if (strpos( $table,'/') !== false ) {
+ list($database,$table) = explode('.',$table, 2);
+
+ }
+
if (empty($_DB_DATAOBJECT['CONFIG'])) {
DB_DataObject::_loadConfig();
}
+ // no configuration available for database
+ if (!empty($database) && empty($_DB_DATAOBJECT['CONFIG']['database_'.$database])) {
+ return DB_DataObject::raiseError(
+ "unable to find database_{$database} in Configuration, It is required for factory with database"
+ , 0, PEAR_ERROR_DIE );
+ }
+
+
if ($table === '') {
if (is_a($this,'DB_DataObject') && strlen($this->__table)) {
@@ -2584,17 +2623,22 @@ class DB_DataObject extends DB_DataObject_Overload
}
}
-
+ // does this need multi db support??
$p = isset($_DB_DATAOBJECT['CONFIG']['class_prefix']) ?
$_DB_DATAOBJECT['CONFIG']['class_prefix'] : '';
$class = $p . preg_replace('/[^A-Z0-9]/i','_',ucfirst($table));
-
$ce = substr(phpversion(),0,1) > 4 ? class_exists($class,false) : class_exists($class);
+
$class = $ce ? $class : DB_DataObject::_autoloadClass($class);
// proxy = full|light
if (!$class && isset($_DB_DATAOBJECT['CONFIG']['proxy'])) {
+
+ DB_DataObject::debug("FAILED TO Autoload $database.$table - using proxy.","FACTORY",1);
+
+
$proxyMethod = 'getProxy'.$_DB_DATAOBJECT['CONFIG']['proxy'];
+ // if you have loaded (some other way) - dont try and load it again..
class_exists('DB_DataObject_Generator') ? '' :
require_once 'DB/DataObject/Generator.php';
@@ -2614,8 +2658,12 @@ class DB_DataObject extends DB_DataObject_Overload
"factory could not find class $class from $table",
DB_DATAOBJECT_ERROR_INVALIDCONFIG);
}
-
- return new $class;
+ $ret = new $class;
+ if (!empty($database)) {
+ DB_DataObject::debug("Setting database to $database","FACTORY",1);
+ $ret->database($database);
+ }
+ return $ret;
}
/**
* autoload Class
@@ -3079,7 +3127,7 @@ class DB_DataObject extends DB_DataObject_Overload
return;
}
-
+ //echo '<PRE>'; print_r(func_get_args());
$useWhereAsOn = false;
// support for 2nd argument as an array of options
if (is_array($joinType)) {
@@ -3119,8 +3167,39 @@ class DB_DataObject extends DB_DataObject_Overload
$DB = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5];
+ /// CHANGED 26 JUN 2009 - we prefer links from our local table over the remote one.
-
+ /* otherwise see if there are any links from this table to the obj. */
+ //print_r($this->links());
+ if (($ofield === false) && ($links = $this->links())) {
+ foreach ($links as $k => $v) {
+ /* link contains {this column} = {linked table}:{linked column} */
+ $ar = explode(':', $v);
+ // Feature Request #4266 - Allow joins with multiple keys
+ if (strpos($k, ',') !== false) {
+ $k = explode(',', $k);
+ }
+ if (strpos($ar[1], ',') !== false) {
+ $ar[1] = explode(',', $ar[1]);
+ }
+
+ if ($ar[0] == $obj->__table) {
+ if ($joinCol !== false) {
+ if ($k == $joinCol) {
+ $tfield = $k;
+ $ofield = $ar[1];
+ break;
+ } else {
+ continue;
+ }
+ } else {
+ $tfield = $k;
+ $ofield = $ar[1];
+ break;
+ }
+ }
+ }
+ }
/* look up the links for obj table */
//print_r($obj->links());
if (!$ofield && ($olinks = $obj->links())) {
@@ -3164,37 +3243,6 @@ class DB_DataObject extends DB_DataObject_Overload
}
}
- /* otherwise see if there are any links from this table to the obj. */
- //print_r($this->links());
- if (($ofield === false) && ($links = $this->links())) {
- foreach ($links as $k => $v) {
- /* link contains {this column} = {linked table}:{linked column} */
- $ar = explode(':', $v);
- // Feature Request #4266 - Allow joins with multiple keys
- if (strpos($k, ',') !== false) {
- $k = explode(',', $k);
- }
- if (strpos($ar[1], ',') !== false) {
- $ar[1] = explode(',', $ar[1]);
- }
-
- if ($ar[0] == $obj->__table) {
- if ($joinCol !== false) {
- if ($k == $joinCol) {
- $tfield = $k;
- $ofield = $ar[1];
- break;
- } else {
- continue;
- }
- } else {
- $tfield = $k;
- $ofield = $ar[1];
- break;
- }
- }
- }
- }
// finally if these two table have column names that match do a join by default on them
if (($ofield === false) && $joinCol) {
@@ -3383,22 +3431,25 @@ class DB_DataObject extends DB_DataObject_Overload
case 'RIGHT': // others??? .. cross, left outer, right outer, natural..?
// Feature Request #4266 - Allow joins with multiple keys
- $this->_join .= "\n {$joinType} JOIN {$objTable} {$fullJoinAs}";
+ $jadd = "\n {$joinType} JOIN {$objTable} {$fullJoinAs}";
+ //$this->_join .= "\n {$joinType} JOIN {$objTable} {$fullJoinAs}";
if (is_array($ofield)) {
$key_count = count($ofield);
for($i = 0; $i < $key_count; $i++) {
if ($i == 0) {
- $this->_join .= " ON ({$joinAs}.{$ofield[$i]}={$table}.{$tfield[$i]}) ";
+ $jadd .= " ON ({$joinAs}.{$ofield[$i]}={$table}.{$tfield[$i]}) ";
}
else {
- $this->_join .= " AND {$joinAs}.{$ofield[$i]}={$table}.{$tfield[$i]} ";
+ $jadd .= " AND {$joinAs}.{$ofield[$i]}={$table}.{$tfield[$i]} ";
}
}
- $this->_join .= ' ' . $appendJoin . ' ';
+ $jadd .= ' ' . $appendJoin . ' ';
} else {
- $this->_join .= " ON ({$joinAs}.{$ofield}={$table}.{$tfield}) {$appendJoin} ";
+ $jadd .= " ON ({$joinAs}.{$ofield}={$table}.{$tfield}) {$appendJoin} ";
}
-
+ // jadd avaliable for debugging join build.
+ //echo $jadd ."\n";
+ $this->_join .= $jadd;
break;
case '': // this is just a standard multitable select..
@@ -3459,7 +3510,7 @@ class DB_DataObject extends DB_DataObject_Overload
continue;
}
- if (empty($from[$k]) && $skipEmpty) {
+ if (empty($from[sprintf($format,$k)]) && $skipEmpty) {
continue;
}
diff --git a/extlib/DB/DataObject/Cast.php b/extlib/DB/DataObject/Cast.php
index 616abb55e..095d2a4d2 100644
--- a/extlib/DB/DataObject/Cast.php
+++ b/extlib/DB/DataObject/Cast.php
@@ -15,9 +15,9 @@
* @category Database
* @package DB_DataObject
* @author Alan Knowles <alan@akbkhome.com>
- * @copyright 1997-2006 The PHP Group
+ * @copyright 1997-2008 The PHP Group
* @license http://www.php.net/license/3_0.txt PHP License 3.0
- * @version CVS: $Id: Cast.php,v 1.15 2005/07/07 05:30:53 alan_k Exp $
+ * @version CVS: $Id: Cast.php 264148 2008-08-04 03:44:59Z alan_k $
* @link http://pear.php.net/package/DB_DataObject
*/
@@ -391,7 +391,10 @@ class DB_DataObject_Cast {
// this is funny - the parameter order is reversed ;)
return "'".mysqli_real_escape_string($db->connection, $this->value)."'";
-
+ case 'sqlite':
+ // this is funny - the parameter order is reversed ;)
+ return "'".sqlite_escape_string($this->value)."'";
+
default:
return PEAR::raiseError("DB_DataObject_Cast cant handle blobs for Database:{$db->dsn['phptype']} Yet");
diff --git a/extlib/DB/DataObject/Error.php b/extlib/DB/DataObject/Error.php
index 05a741408..382115453 100644
--- a/extlib/DB/DataObject/Error.php
+++ b/extlib/DB/DataObject/Error.php
@@ -18,7 +18,7 @@
* @author Alan Knowles <alan@akbkhome.com>
* @copyright 1997-2006 The PHP Group
* @license http://www.php.net/license/3_0.txt PHP License 3.0
- * @version CVS: $Id: Error.php,v 1.3 2005/03/23 02:35:35 alan_k Exp $
+ * @version CVS: $Id: Error.php 277015 2009-03-12 05:51:03Z alan_k $
* @link http://pear.php.net/package/DB_DataObject
*/
diff --git a/extlib/DB/DataObject/Generator.php b/extlib/DB/DataObject/Generator.php
index de16af692..ff6e42c7d 100644
--- a/extlib/DB/DataObject/Generator.php
+++ b/extlib/DB/DataObject/Generator.php
@@ -4,9 +4,9 @@
*
* PHP versions 4 and 5
*
- * LICENSE: This source file is subject to version 3.0 of the PHP license
+ * LICENSE: This source file is subject to version 3.01 of the PHP license
* that is available through the world-wide-web at the following URI:
- * http://www.php.net/license/3_0.txt. If you did not receive a copy of
+ * http://www.php.net/license/3_01.txt. If you did not receive a copy of
* the PHP License and are unable to obtain it through the web, please
* send a note to license@php.net so we can mail you a copy immediately.
*
@@ -14,8 +14,8 @@
* @package DB_DataObject
* @author Alan Knowles <alan@akbkhome.com>
* @copyright 1997-2006 The PHP Group
- * @license http://www.php.net/license/3_0.txt PHP License 3.0
- * @version CVS: $Id: Generator.php,v 1.141 2008/01/30 02:29:39 alan_k Exp $
+ * @license http://www.php.net/license/3_01.txt PHP License 3.01
+ * @version CVS: $Id: Generator.php 284150 2009-07-15 23:27:59Z alan_k $
* @link http://pear.php.net/package/DB_DataObject
*/
@@ -193,7 +193,11 @@ class DB_DataObject_Generator extends DB_DataObject
/**
* set portability and some modules to fetch the informations
*/
- $__DB->setOption('portability', MDB2_PORTABILITY_ALL ^ MDB2_PORTABILITY_FIX_CASE);
+ $db_options = PEAR::getStaticProperty('MDB2','options');
+ if (empty($db_options)) {
+ $__DB->setOption('portability', MDB2_PORTABILITY_ALL ^ MDB2_PORTABILITY_FIX_CASE);
+ }
+
$__DB->loadModule('Manager');
$__DB->loadModule('Reverse');
}
@@ -265,12 +269,7 @@ class DB_DataObject_Generator extends DB_DataObject
} else {
$defs = $__DB->reverse->tableInfo($quotedTable);
// rename the length value, so it matches db's return.
- foreach ($defs as $k => $v) {
- if (!isset($defs[$k]['length'])) {
- continue;
- }
- $defs[$k]['len'] = $defs[$k]['length'];
- }
+
}
if (is_a($defs,'PEAR_Error')) {
@@ -286,7 +285,10 @@ class DB_DataObject_Generator extends DB_DataObject
if (!is_array($def)) {
continue;
}
-
+ // rename the length value, so it matches db's return.
+ if (isset($def['length']) && !isset($def['len'])) {
+ $def['len'] = $def['length'];
+ }
$this->_definitions[$table][] = (object) $def;
}
@@ -391,7 +393,10 @@ class DB_DataObject_Generator extends DB_DataObject
$fk = array();
foreach($this->tables as $this->table) {
- $res =& $DB->query('SHOW CREATE TABLE ' . $this->table);
+ $quotedTable = !empty($options['quote_identifiers_tableinfo']) ? $DB->quoteIdentifier($table) : $this->table;
+
+ $res =& $DB->query('SHOW CREATE TABLE ' . $quotedTable );
+
if (PEAR::isError($res)) {
die($res->getMessage());
}
@@ -467,7 +472,7 @@ class DB_DataObject_Generator extends DB_DataObject
function _generateDefinitionsTable()
{
global $_DB_DATAOBJECT;
-
+ $options = PEAR::getStaticProperty('DB_DataObject','options');
$defs = $this->_definitions[$this->table];
$this->_newConfig .= "\n[{$this->table}]\n";
$keys_out = "\n[{$this->table}__keys]\n";
@@ -551,6 +556,9 @@ class DB_DataObject_Generator extends DB_DataObject
case 'ENUM':
case 'SET': // not really but oh well
+
+ case 'POINT': // mysql geometry stuff - not really string - but will do..
+
case 'TIMESTAMPTZ': // postgres
case 'BPCHAR': // postgres
case 'INTERVAL': // postgres (eg. '12 days')
@@ -594,14 +602,18 @@ class DB_DataObject_Generator extends DB_DataObject
DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME;
break;
-
- case 'TINYBLOB':
+
case 'BLOB': /// these should really be ignored!!!???
+ case 'TINYBLOB':
case 'MEDIUMBLOB':
case 'LONGBLOB':
+
+ case 'CLOB': // oracle character lob support
+
case 'BYTEA': // postgres blob support..
$type = DB_DATAOBJECT_STR + DB_DATAOBJECT_BLOB;
break;
+
default:
echo "*****************************************************************\n".
"** WARNING UNKNOWN TYPE **\n".
@@ -653,7 +665,9 @@ class DB_DataObject_Generator extends DB_DataObject
// only use primary key or nextval(), cause the setFrom blocks you setting all key items...
// if no keys exist fall back to using unique
//echo "\n{$t->name} => {$t->flags}\n";
- if (preg_match("/(auto_increment|nextval\()/i",rawurldecode($t->flags))
+ $secondary_key_match = isset($options['generator_secondary_key_match']) ? $options['generator_secondary_key_match'] : 'primary|unique';
+
+ if (preg_match('/(auto_increment|nextval\()/i',rawurldecode($t->flags))
|| (isset($t->autoincrement) && ($t->autoincrement === true))) {
// native sequences = 2
@@ -662,7 +676,7 @@ class DB_DataObject_Generator extends DB_DataObject
}
$ret_keys_primary[$t->name] = 'N';
- } else if (preg_match("/(primary|unique)/i",$t->flags)) {
+ } else if ($secondary_key_match && preg_match('/('.$secondary_key_match.')/i',$t->flags)) {
// keys.. = 1
$key_type = 'K';
if (!preg_match("/(primary)/i",$t->flags)) {
@@ -868,10 +882,13 @@ class DB_DataObject_Generator extends DB_DataObject
// then we should add var $_database = here
// as database names may not always match..
+ if (empty($GLOBALS['_DB_DATAOBJECT']['CONFIG'])) {
+ DB_DataObject::_loadConfig();
+ }
+
+ // Only include the $_database property if the omit_database_var is unset or false
-
-
- if (isset($options["database_{$this->_database}"])) {
+ if (isset($options["database_{$this->_database}"]) && empty($GLOBALS['_DB_DATAOBJECT']['CONFIG']['generator_omit_database_var'])) {
$body .= " {$var} \$_database = '{$this->_database}'; {$p}// database name (used with database_{*} config)\n";
}
@@ -904,9 +921,10 @@ class DB_DataObject_Generator extends DB_DataObject
$padding = (30 - strlen($t->name));
if ($padding < 2) $padding =2;
$p = str_repeat(' ',$padding) ;
-
- $body .=" {$var} \${$t->name}; {$p}// {$t->type}({$t->len}) {$t->flags}\n";
-
+
+ $length = empty($t->len) ? '' : '('.$t->len.')';
+ $body .=" {$var} \${$t->name}; {$p}// {$t->type}$length {$t->flags}\n";
+
// can not do set as PEAR::DB table info doesnt support it.
//if (substr($t->Type,0,3) == "set")
// $sets[$t->Field] = "array".substr($t->Type,3);
@@ -1185,7 +1203,7 @@ class DB_DataObject_Generator extends DB_DataObject
$__DB->loadModule('Manager');
$__DB->loadModule('Reverse');
}
- $quotedTable = !empty($options['quote_identifiers']) ?
+ $quotedTable = !empty($options['quote_identifiers_tableinfo']) ?
$__DB->quoteIdentifier($table) : $table;
if (!$is_MDB2) {
diff --git a/extlib/DB/DataObject/createTables.php b/extlib/DB/DataObject/createTables.php
index c0659574e..d54d28c24 100755..100644
--- a/extlib/DB/DataObject/createTables.php
+++ b/extlib/DB/DataObject/createTables.php
@@ -16,7 +16,7 @@
// | Author: Alan Knowles <alan@akbkhome.com>
// +----------------------------------------------------------------------+
//
-// $Id: createTables.php,v 1.24 2006/01/13 01:27:55 alan_k Exp $
+// $Id: createTables.php 277015 2009-03-12 05:51:03Z alan_k $
//
// since this version doesnt use overload,
diff --git a/extlib/Services/oEmbed.php b/extlib/Services/oEmbed.php
index 5d38ed883..7d507b6f6 100644
--- a/extlib/Services/oEmbed.php
+++ b/extlib/Services/oEmbed.php
@@ -162,7 +162,7 @@ class Services_oEmbed
}
if ($this->options[self::OPTION_API] === null) {
- $this->options[self::OPTION_API] = $this->discover();
+ $this->options[self::OPTION_API] = $this->discover($url);
}
}
@@ -319,7 +319,7 @@ class Services_oEmbed
}
}
- return (isset($ret['json']) ? $ret['json'] : array_pop($ret));
+ return (isset($ret['application/json']) ? $ret['application/json'] : array_pop($ret));
}
/**
diff --git a/extlib/facebook/facebook.php b/extlib/facebook/facebook.php
index fee1dd086..016e8e8e0 100644
--- a/extlib/facebook/facebook.php
+++ b/extlib/facebook/facebook.php
@@ -107,13 +107,13 @@ class Facebook {
* @param bool resolve_auth_token convert an auth token into a session
*/
public function validate_fb_params($resolve_auth_token=true) {
- $this->fb_params = $this->get_valid_fb_params($_POST, 48*3600, 'fb_sig');
+ $this->fb_params = $this->get_valid_fb_params($_POST, 48 * 3600, 'fb_sig');
// note that with preload FQL, it's possible to receive POST params in
// addition to GET, so use a different prefix to differentiate them
if (!$this->fb_params) {
- $fb_params = $this->get_valid_fb_params($_GET, 48*3600, 'fb_sig');
- $fb_post_params = $this->get_valid_fb_params($_POST, 48*3600, 'fb_post_sig');
+ $fb_params = $this->get_valid_fb_params($_GET, 48 * 3600, 'fb_sig');
+ $fb_post_params = $this->get_valid_fb_params($_POST, 48 * 3600, 'fb_post_sig');
$this->fb_params = array_merge($fb_params, $fb_post_params);
}
diff --git a/extlib/facebook/facebookapi_php5_restlib.php b/extlib/facebook/facebookapi_php5_restlib.php
index 3fec06e8a..55cb7fb86 100755
--- a/extlib/facebook/facebookapi_php5_restlib.php
+++ b/extlib/facebook/facebookapi_php5_restlib.php
@@ -55,6 +55,7 @@ class FacebookRestClient {
private $pending_batch;
private $call_as_apikey;
private $use_curl_if_available;
+ private $format = null;
const BATCH_MODE_DEFAULT = 0;
const BATCH_MODE_SERVER_PARALLEL = 0;
@@ -178,39 +179,32 @@ function toggleDisplay(id, type) {
private function execute_server_side_batch() {
$item_count = count($this->batch_queue);
$method_feed = array();
- foreach($this->batch_queue as $batch_item) {
+ foreach ($this->batch_queue as $batch_item) {
$method = $batch_item['m'];
$params = $batch_item['p'];
- $this->finalize_params($method, $params);
- $method_feed[] = $this->create_post_string($method, $params);
+ list($get, $post) = $this->finalize_params($method, $params);
+ $method_feed[] = $this->create_url_string(array_merge($post, $get));
}
- $method_feed_json = json_encode($method_feed);
-
$serial_only =
($this->batch_mode == FacebookRestClient::BATCH_MODE_SERIAL_ONLY);
- $params = array('method_feed' => $method_feed_json,
- 'serial_only' => $serial_only);
- if ($this->call_as_apikey) {
- $params['call_as_apikey'] = $this->call_as_apikey;
- }
-
- $xml = $this->post_request('batch.run', $params);
-
- $result = $this->convert_xml_to_result($xml, 'batch.run', $params);
+ $params = array('method_feed' => json_encode($method_feed),
+ 'serial_only' => $serial_only,
+ 'format' => $this->format);
+ $result = $this->call_method('facebook.batch.run', $params);
if (is_array($result) && isset($result['error_code'])) {
throw new FacebookRestClientException($result['error_msg'],
$result['error_code']);
}
- for($i = 0; $i < $item_count; $i++) {
+ for ($i = 0; $i < $item_count; $i++) {
$batch_item = $this->batch_queue[$i];
- $batch_item_result_xml = $result[$i];
- $batch_item_result = $this->convert_xml_to_result($batch_item_result_xml,
- $batch_item['m'],
- $batch_item['p']);
+ $batch_item['p']['format'] = $this->format;
+ $batch_item_result = $this->convert_result($result[$i],
+ $batch_item['m'],
+ $batch_item['p']);
if (is_array($batch_item_result) &&
isset($batch_item_result['error_code'])) {
@@ -516,12 +510,20 @@ function toggleDisplay(id, type) {
* behalf of app. Successful creation guarantees app will be admin.
*
* @param assoc array $event_info json encoded event information
+ * @param string $file (Optional) filename of picture to set
*
* @return int event id
*/
- public function &events_create($event_info) {
- return $this->call_method('facebook.events.create',
+ public function events_create($event_info, $file = null) {
+ if ($file) {
+ return $this->call_upload_method('facebook.events.create',
+ array('event_info' => $event_info),
+ $file,
+ Facebook::get_facebook_url('api-photo') . '/restserver.php');
+ } else {
+ return $this->call_method('facebook.events.create',
array('event_info' => $event_info));
+ }
}
/**
@@ -529,13 +531,21 @@ function toggleDisplay(id, type) {
*
* @param int $eid event id
* @param assoc array $event_info json encoded event information
+ * @param string $file (Optional) filename of new picture to set
*
* @return bool true if successful
*/
- public function &events_edit($eid, $event_info) {
- return $this->call_method('facebook.events.edit',
+ public function events_edit($eid, $event_info, $file = null) {
+ if ($file) {
+ return $this->call_upload_method('facebook.events.edit',
+ array('eid' => $eid, 'event_info' => $event_info),
+ $file,
+ Facebook::get_facebook_url('api-photo') . '/restserver.php');
+ } else {
+ return $this->call_method('facebook.events.edit',
array('eid' => $eid,
- 'event_info' => $event_info));
+ 'event_info' => $event_info));
+ }
}
/**
@@ -935,7 +945,7 @@ function toggleDisplay(id, type) {
/**
* Makes an FQL query. This is a generalized way of accessing all the data
* in the API, as an alternative to most of the other method calls. More
- * info at http://developers.facebook.com/documentation.php?v=1.0&doc=fql
+ * info at http://wiki.developers.facebook.com/index.php/FQL
*
* @param string $query the query to evaluate
*
@@ -947,6 +957,21 @@ function toggleDisplay(id, type) {
}
/**
+ * Makes a set of FQL queries in parallel. This method takes a dictionary
+ * of FQL queries where the keys are names for the queries. Results from
+ * one query can be used within another query to fetch additional data. More
+ * info about FQL queries at http://wiki.developers.facebook.com/index.php/FQL
+ *
+ * @param string $queries JSON-encoded dictionary of queries to evaluate
+ *
+ * @return array generalized array representing the results
+ */
+ public function &fql_multiquery($queries) {
+ return $this->call_method('facebook.fql.multiquery',
+ array('queries' => $queries));
+ }
+
+ /**
* Returns whether or not pairs of users are friends.
* Note that the Facebook friend relationship is symmetric.
*
@@ -995,6 +1020,23 @@ function toggleDisplay(id, type) {
}
/**
+ * Returns the mutual friends between the target uid and a source uid or
+ * the current session user.
+ *
+ * @param int $target_uid Target uid for which mutual friends will be found.
+ * @param int $source_uid (optional) Source uid for which mutual friends will
+ * be found. If no source_uid is specified,
+ * source_id will default to the session
+ * user.
+ * @return array An array of friend uids
+ */
+ public function &friends_getMutualFriends($target_uid, $source_uid = null) {
+ return $this->call_method('facebook.friends.getMutualFriends',
+ array("target_uid" => $target_uid,
+ "source_uid" => $source_uid));
+ }
+
+ /**
* Returns the set of friend lists for the current session user.
*
* @return array An array of friend list objects
@@ -1169,6 +1211,44 @@ function toggleDisplay(id, type) {
}
/**
+ * Payments Order API
+ */
+
+ /**
+ * Set Payments properties for an app.
+ *
+ * @param properties a map from property names to values
+ * @return true on success
+ */
+ public function payments_setProperties($properties) {
+ return $this->call_method ('facebook.payments.setProperties',
+ array('properties' => json_encode($properties)));
+ }
+
+ public function payments_getOrderDetails($order_id) {
+ return json_decode($this->call_method(
+ 'facebook.payments.getOrderDetails',
+ array('order_id' => $order_id)), true);
+ }
+
+ public function payments_updateOrder($order_id, $status,
+ $params) {
+ return $this->call_method('facebook.payments.updateOrder',
+ array('order_id' => $order_id,
+ 'status' => $status,
+ 'params' => json_encode($params)));
+ }
+
+ public function payments_getOrders($status, $start_time,
+ $end_time, $test_mode=false) {
+ return json_decode($this->call_method('facebook.payments.getOrders',
+ array('status' => $status,
+ 'start_time' => $start_time,
+ 'end_time' => $end_time,
+ 'test_mode' => $test_mode)), true);
+ }
+
+ /**
* Creates a note with the specified title and content.
*
* @param string $title Title of the note.
@@ -1233,7 +1313,6 @@ function toggleDisplay(id, type) {
* notes.
*/
public function &notes_get($uid, $note_ids = null) {
-
return $this->call_method('notes.get',
array('uid' => $uid,
'note_ids' => $note_ids));
@@ -1632,6 +1711,63 @@ function toggleDisplay(id, type) {
}
/**
+ * Gets the comments for a particular xid. This is essentially a wrapper
+ * around the comment FQL table.
+ *
+ * @param string $xid external id associated with the comments
+ *
+ * @return array of comment objects
+ */
+ public function &comments_get($xid) {
+ $args = array('xid' => $xid);
+ return $this->call_method('facebook.comments.get', $args);
+ }
+
+ /**
+ * Add a comment to a particular xid on behalf of a user. If called
+ * without an app_secret (with session secret), this will only work
+ * for the session user.
+ *
+ * @param string $xid external id associated with the comments
+ * @param string $text text of the comment
+ * @param int $uid user adding the comment (def: session user)
+ * @param string $title optional title for the stream story
+ * @param string $url optional url for the stream story
+ * @param bool $publish_to_stream publish a feed story about this comment?
+ * a link will be generated to title/url in the story
+ *
+ * @return string comment_id associated with the comment
+ */
+ public function &comments_add($xid, $text, $uid=0, $title='', $url='',
+ $publish_to_stream=false) {
+ $args = array(
+ 'xid' => $xid,
+ 'uid' => $this->get_uid($uid),
+ 'text' => $text,
+ 'title' => $title,
+ 'url' => $url,
+ 'publish_to_stream' => $publish_to_stream);
+
+ return $this->call_method('facebook.comments.add', $args);
+ }
+
+ /**
+ * Remove a particular comment.
+ *
+ * @param string $xid the external id associated with the comments
+ * @param string $comment_id id of the comment to remove (returned by
+ * comments.add and comments.get)
+ *
+ * @return boolean
+ */
+ public function &comments_remove($xid, $comment_id) {
+ $args = array(
+ 'xid' => $xid,
+ 'comment_id' => $comment_id);
+ return $this->call_method('facebook.comments.remove', $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.
@@ -1642,11 +1778,16 @@ function toggleDisplay(id, type) {
* @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
+ * @param array $metadata metadata to include with the return, allows
+ * requested metadata to be returned, such as
+ * profiles, albums, photo_tags
*
* @return array(
- * 'posts' => array of posts,
- * 'profiles' => array of profile metadata of users/pages in posts
- * 'albums' => array of album metadata in posts
+ * 'posts' => array of posts,
+ * // if requested, the following data may be returned
+ * 'profiles' => array of profile metadata of users/pages in posts
+ * 'albums' => array of album metadata in posts
+ * 'photo_tags' => array of photo_tags for photos in posts
* )
*/
public function &stream_get($viewer_id = null,
@@ -2849,6 +2990,7 @@ function toggleDisplay(id, type) {
array('uids' => $uids ? json_encode($uids) : null));
}
+
/* UTILITY FUNCTIONS */
/**
@@ -2862,18 +3004,15 @@ function toggleDisplay(id, type) {
* See: http://wiki.developers.facebook.com/index.php/Using_batching_API
*/
public function &call_method($method, $params = array()) {
+ if ($this->format) {
+ $params['format'] = $this->format;
+ }
if (!$this->pending_batch()) {
if ($this->call_as_apikey) {
$params['call_as_apikey'] = $this->call_as_apikey;
}
$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);
- }
-
+ $result = $this->convert_result($data, $method, $params);
if (is_array($result) && isset($result['error_code'])) {
throw new FacebookRestClientException($result['error_msg'],
$result['error_code']);
@@ -2888,6 +3027,32 @@ function toggleDisplay(id, type) {
return $result;
}
+ protected function convert_result($data, $method, $params) {
+ $is_xml = (empty($params['format']) ||
+ strtolower($params['format']) != 'json');
+ return ($is_xml) ? $this->convert_xml_to_result($data, $method, $params)
+ : json_decode($data, true);
+ }
+
+ /**
+ * Change the response format
+ *
+ * @param string $format The response format (json, xml)
+ */
+ public function setFormat($format) {
+ $this->format = $format;
+ return $this;
+ }
+
+ /**
+ * get the current response serialization format
+ *
+ * @return string 'xml', 'json', or null (which means 'xml')
+ */
+ public function getFormat() {
+ return $this->format;
+ }
+
/**
* Calls the specified file-upload POST method with the specified parameters
*
@@ -2906,8 +3071,14 @@ function toggleDisplay(id, type) {
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 ($this->format) {
+ $params['format'] = $this->format;
+ }
+ $data = $this->post_upload_request($method,
+ $params,
+ $file,
+ $server_addr);
+ $result = $this->convert_result($data, $method, $params);
if (is_array($result) && isset($result['error_code'])) {
throw new FacebookRestClientException($result['error_msg'],
@@ -2946,11 +3117,13 @@ function toggleDisplay(id, type) {
return $result;
}
- private function finalize_params($method, &$params) {
- $this->add_standard_params($method, $params);
+ protected function finalize_params($method, $params) {
+ list($get, $post) = $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);
+ $this->convert_array_values_to_json($post);
+ $post['sig'] = Facebook::generate_sig(array_merge($get, $post),
+ $this->secret);
+ return array($get, $post);
}
private function convert_array_values_to_json(&$params) {
@@ -2961,28 +3134,38 @@ function toggleDisplay(id, type) {
}
}
- private function add_standard_params($method, &$params) {
+ /**
+ * Add the generally required params to our request.
+ * Params method, api_key, and v should be sent over as get.
+ */
+ private function add_standard_params($method, $params) {
+ $post = $params;
+ $get = array();
if ($this->call_as_apikey) {
- $params['call_as_apikey'] = $this->call_as_apikey;
+ $get['call_as_apikey'] = $this->call_as_apikey;
}
- $params['method'] = $method;
- $params['session_key'] = $this->session_key;
- $params['api_key'] = $this->api_key;
- $params['call_id'] = microtime(true);
- if ($params['call_id'] <= $this->last_call_id) {
- $params['call_id'] = $this->last_call_id + 0.001;
+ $get['method'] = $method;
+ $get['session_key'] = $this->session_key;
+ $get['api_key'] = $this->api_key;
+ $post['call_id'] = microtime(true);
+ if ($post['call_id'] <= $this->last_call_id) {
+ $post['call_id'] = $this->last_call_id + 0.001;
}
- $this->last_call_id = $params['call_id'];
- if (!isset($params['v'])) {
- $params['v'] = '1.0';
+ $this->last_call_id = $post['call_id'];
+ if (isset($post['v'])) {
+ $get['v'] = $post['v'];
+ unset($post['v']);
+ } else {
+ $get['v'] = '1.0';
}
if (isset($this->use_ssl_resources) &&
$this->use_ssl_resources) {
- $params['return_ssl_resources'] = true;
+ $post['return_ssl_resources'] = true;
}
+ return array($get, $post);
}
- private function create_post_string($method, $params) {
+ private function create_url_string($params) {
$post_params = array();
foreach ($params as $key => &$val) {
$post_params[] = $key.'='.urlencode($val);
@@ -3022,48 +3205,64 @@ function toggleDisplay(id, type) {
}
public function post_request($method, $params) {
- $this->finalize_params($method, $params);
- $post_string = $this->create_post_string($method, $params);
+ list($get, $post) = $this->finalize_params($method, $params);
+ $post_string = $this->create_url_string($post);
+ $get_string = $this->create_url_string($get);
+ $url_with_get = $this->server_addr . '?' . $get_string;
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_URL, $url_with_get);
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);
+ $result = $this->curl_exec($ch);
curl_close($ch);
} else {
$content_type = 'application/x-www-form-urlencoded';
$content = $post_string;
$result = $this->run_http_post_transaction($content_type,
$content,
- $this->server_addr);
+ $url_with_get);
}
return $result;
}
+ /**
+ * execute a curl transaction -- this exists mostly so subclasses can add
+ * extra options and/or process the response, if they wish.
+ *
+ * @param resource $ch a curl handle
+ */
+ protected function curl_exec($ch) {
+ $result = curl_exec($ch);
+ 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);
+ list($get, $post) = $this->finalize_params($method, $params);
+ $get_string = $this->create_url_string($get);
+ $url_with_get = $server_addr . '?' . $get_string;
if ($this->use_curl_if_available && function_exists('curl_init')) {
// prepending '@' causes cURL to upload the file; the key is ignored.
- $params['_file'] = '@' . $file;
+ $post['_file'] = '@' . $file;
$useragent = 'Facebook API PHP5 Client 1.1 (curl) ' . phpversion();
$ch = curl_init();
- curl_setopt($ch, CURLOPT_URL, $server_addr);
+ curl_setopt($ch, CURLOPT_URL, $url_with_get);
// this has to come before the POSTFIELDS set!
- curl_setopt($ch, CURLOPT_POST, 1 );
+ 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_POSTFIELDS, $post);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERAGENT, $useragent);
- $result = curl_exec($ch);
+ $result = $this->curl_exec($ch);
curl_close($ch);
} else {
- $result = $this->run_multipart_http_transaction($method, $params, $file, $server_addr);
+ $result = $this->run_multipart_http_transaction($method, $post,
+ $file, $url_with_get);
}
return $result;
}
@@ -3110,7 +3309,7 @@ function toggleDisplay(id, type) {
}
}
- private function get_uid($uid) {
+ protected function get_uid($uid) {
return $uid ? $uid : $this->user;
}
}
@@ -3145,6 +3344,7 @@ class FacebookAPIErrorCodes {
const API_EC_DEPRECATED = 11;
const API_EC_VERSION = 12;
const API_EC_INTERNAL_FQL_ERROR = 13;
+ const API_EC_HOST_PUP = 14;
/*
* PARAMETER ERRORS
@@ -3179,6 +3379,7 @@ class FacebookAPIErrorCodes {
const API_EC_PERMISSION = 200;
const API_EC_PERMISSION_USER = 210;
const API_EC_PERMISSION_NO_DEVELOPERS = 211;
+ const API_EC_PERMISSION_OFFLINE_ACCESS = 212;
const API_EC_PERMISSION_ALBUM = 220;
const API_EC_PERMISSION_PHOTO = 221;
const API_EC_PERMISSION_MESSAGE = 230;
@@ -3267,6 +3468,7 @@ class FacebookAPIErrorCodes {
const FQL_EC_DEPRECATED_TABLE = 611;
const FQL_EC_EXTENDED_PERMISSION = 612;
const FQL_EC_RATE_LIMIT_EXCEEDED = 613;
+ const FQL_EC_UNRESOLVED_DEPENDENCY = 614;
const API_EC_REF_SET_FAILED = 700;
@@ -3319,6 +3521,21 @@ class FacebookAPIErrorCodes {
const API_EC_LIVEMESSAGE_MESSAGE_TOO_LONG = 1102;
/*
+ * PAYMENTS API ERRORS
+ */
+ const API_EC_PAYMENTS_UNKNOWN = 1150;
+ const API_EC_PAYMENTS_APP_INVALID = 1151;
+ const API_EC_PAYMENTS_DATABASE = 1152;
+ const API_EC_PAYMENTS_PERMISSION_DENIED = 1153;
+ const API_EC_PAYMENTS_APP_NO_RESPONSE = 1154;
+ const API_EC_PAYMENTS_APP_ERROR_RESPONSE = 1155;
+ const API_EC_PAYMENTS_INVALID_ORDER = 1156;
+ const API_EC_PAYMENTS_INVALID_PARAM = 1157;
+ const API_EC_PAYMENTS_INVALID_OPERATION = 1158;
+ const API_EC_PAYMENTS_PAYMENT_FAILED = 1159;
+ const API_EC_PAYMENTS_DISABLED = 1160;
+
+ /*
* CONNECT SESSION ERRORS
*/
const API_EC_CONNECT_FEED_DISABLED = 1300;
@@ -3347,6 +3564,7 @@ class FacebookAPIErrorCodes {
const API_EC_COMMENTS_INVALID_XID = 1703;
const API_EC_COMMENTS_INVALID_UID = 1704;
const API_EC_COMMENTS_INVALID_POST = 1705;
+ const API_EC_COMMENTS_INVALID_REMOVE = 1706;
/**
* This array is no longer maintained; to view the description of an error
diff --git a/install.php b/install.php
index 570b08edf..8f500259e 100644
--- a/install.php
+++ b/install.php
@@ -43,8 +43,8 @@ function checkPrereqs()
$pass = false;
}
- if (version_compare(PHP_VERSION, '5.0.0', '<')) {
- ?><p class="error">Require PHP version 5 or greater.</p><?php
+ if (version_compare(PHP_VERSION, '5.2.3', '<')) {
+ ?><p class="error">Require PHP version 5.2.3 or greater.</p><?php
$pass = false;
}
diff --git a/js/util.js b/js/util.js
index bbcbc0bbb..f3ed918cf 100644
--- a/js/util.js
+++ b/js/util.js
@@ -283,7 +283,7 @@ function NoticeAttachments() {
},
timeout : 0,
autoHide : true,
- css : {'max-width':'542px', 'top':'22.5%', 'left':'32.5%'}
+ css : {'max-width':'542px', 'top':'5%', 'left':'32.5%'}
};
$('#content .notice a.attachment').click(function() {
diff --git a/lib/common.php b/lib/common.php
index c47702779..9d7954fa9 100644
--- a/lib/common.php
+++ b/lib/common.php
@@ -19,7 +19,7 @@
if (!defined('LACONICA')) { exit(1); }
-define('LACONICA_VERSION', '0.8.0');
+define('LACONICA_VERSION', '0.8.1dev');
define('AVATAR_PROFILE_SIZE', 96);
define('AVATAR_STREAM_SIZE', 48);
diff --git a/lib/daemon.php b/lib/daemon.php
index 9d89c63e7..231f5414e 100644
--- a/lib/daemon.php
+++ b/lib/daemon.php
@@ -24,6 +24,7 @@ if (!defined('LACONICA')) {
class Daemon
{
var $daemonize = true;
+ var $_id = 'generic';
function __construct($daemonize = true)
{
@@ -35,6 +36,16 @@ class Daemon
return null;
}
+ function get_id()
+ {
+ return $this->_id;
+ }
+
+ function set_id($id)
+ {
+ $this->_id = $id;
+ }
+
function background()
{
$pid = pcntl_fork();
diff --git a/lib/messageform.php b/lib/messageform.php
index b8878ec1f..8ea2b36c2 100644
--- a/lib/messageform.php
+++ b/lib/messageform.php
@@ -140,6 +140,12 @@ class MessageForm extends Form
'rows' => 4,
'name' => 'content'),
($this->content) ? $this->content : '');
+ $this->out->elementStart('dl', 'form_note');
+ $this->out->element('dt', null, _('Available characters'));
+ $this->out->element('dd', array('id' => 'notice_text-count'),
+ '140');
+ $this->out->elementEnd('dl');
+
}
/**
diff --git a/lib/queuehandler.php b/lib/queuehandler.php
index c2ff10f32..f11e5bd90 100644
--- a/lib/queuehandler.php
+++ b/lib/queuehandler.php
@@ -29,7 +29,6 @@ define('QUEUE_HANDLER_HIT_IDLE', 0);
class QueueHandler extends Daemon
{
- var $_id = 'generic';
function __construct($id=null, $daemonize=true)
{
@@ -55,16 +54,6 @@ class QueueHandler extends Daemon
return strtolower($this->class_name().'.'.$this->get_id());
}
- function get_id()
- {
- return $this->_id;
- }
-
- function set_id($id)
- {
- $this->_id = $id;
- }
-
function transport()
{
return null;
diff --git a/lib/router.php b/lib/router.php
index e12138637..5e0fcfc94 100644
--- a/lib/router.php
+++ b/lib/router.php
@@ -129,6 +129,11 @@ class Router
$m->connect('index.php?action=' . $action, array('action' => $action));
}
+ $m->connect('main/:method',
+ array('action' => 'api',
+ 'method' => 'oembed(.xml|.json)?',
+ 'apiaction' => 'oembed'));
+
// settings
foreach (array('profile', 'avatar', 'password', 'openid', 'im',
@@ -394,6 +399,10 @@ class Router
array('action' => 'api',
'apiaction' => 'laconica'));
+ $m->connect('api/laconica/:method',
+ array('action' => 'api',
+ 'apiaction' => 'laconica'));
+
// Groups
$m->connect('api/laconica/groups/:method/:argument',
array('action' => 'api',
diff --git a/lib/rssaction.php b/lib/rssaction.php
index ffa1f9e99..901558943 100644
--- a/lib/rssaction.php
+++ b/lib/rssaction.php
@@ -39,6 +39,7 @@ class Rss10Action extends Action
var $creators = array();
var $limit = DEFAULT_RSS_LIMIT;
var $notices = null;
+ var $tags_already_output = array();
/**
* Constructor
@@ -234,6 +235,11 @@ class Rss10Action extends Action
$replyurl = common_local_url('shownotice', array('notice' => $notice->reply_to));
$this->element('sioc:reply_of', array('rdf:resource' => $replyurl));
}
+ if (!empty($notice->conversation)) {
+ $conversationurl = common_local_url('conversation',
+ array('id' => $notice->conversation));
+ $this->element('sioc:has_discussion', array('rdf:resource' => $conversationurl));
+ }
$attachments = $notice->attachments();
if($attachments){
foreach($attachments as $attachment){
@@ -268,6 +274,12 @@ class Rss10Action extends Action
foreach ($tags as $tag)
{
$tagpage = common_local_url('tag', array('tag' => $tag));
+
+ if ( in_array($tag, $this->tags_already_output) ) {
+ $this->element('ctag:tagged', array('rdf:resource'=>$tagpage.'#concept'));
+ continue;
+ }
+
$tagrss = common_local_url('tagrss', array('tag' => $tag));
$this->elementStart('ctag:tagged');
$this->elementStart('ctag:Tag', array('rdf:about'=>$tagpage.'#concept', 'ctag:label'=>$tag));
@@ -275,6 +287,8 @@ class Rss10Action extends Action
$this->element('rdfs:seeAlso', array('rdf:resource'=>$tagrss));
$this->elementEnd('ctag:Tag');
$this->elementEnd('ctag:tagged');
+
+ $this->tags_already_output[] = $tag;
}
}
$this->elementEnd('item');
@@ -320,6 +334,8 @@ class Rss10Action extends Action
'http://rdfs.org/sioc/ns#',
'xmlns:sioct' =>
'http://rdfs.org/sioc/types#',
+ 'xmlns:rdfs' =>
+ 'http://www.w3.org/2000/01/rdf-schema#',
'xmlns:laconica' =>
'http://laconi.ca/ont/',
'xmlns' => 'http://purl.org/rss/1.0/'));
diff --git a/lib/twitterapi.php b/lib/twitterapi.php
index 655b6c777..79da82a19 100644
--- a/lib/twitterapi.php
+++ b/lib/twitterapi.php
@@ -186,6 +186,24 @@ class TwitterapiAction extends Action
$twitter_status['favorited'] = false;
}
+ // Enclosures
+ $attachments = $notice->attachments();
+ $enclosures = array();
+
+ foreach ($attachments as $attachment) {
+ if ($attachment->isEnclosure()) {
+ $enclosure = array();
+ $enclosure['url'] = $attachment->url;
+ $enclosure['mimetype'] = $attachment->mimetype;
+ $enclosure['size'] = $attachment->size;
+ $enclosures[] = $enclosure;
+ }
+ }
+
+ if (!empty($enclosures)) {
+ $twitter_status['attachments'] = $enclosures;
+ }
+
if ($include_user) {
# Don't get notice (recursive!)
$twitter_user = $this->twitter_user_array($profile, false);
@@ -200,7 +218,7 @@ class TwitterapiAction extends Action
$profile = $notice->getProfile();
$entry = array();
- # We trim() to avoid extraneous whitespace in the output
+ // We trim() to avoid extraneous whitespace in the output
$entry['content'] = common_xml_safe_str(trim($notice->rendered));
$entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
@@ -213,7 +231,26 @@ class TwitterapiAction extends Action
$entry['updated'] = $entry['published'];
$entry['author'] = $profile->getBestName();
- # Enclosure
+ // Enclosures
+ $attachments = $notice->attachments();
+ $enclosures = array();
+
+ foreach ($attachments as $attachment) {
+ if ($attachment->isEnclosure()) {
+ $enclosure = array();
+ $enclosure['url'] = $attachment->url;
+ $enclosure['mimetype'] = $attachment->mimetype;
+ $enclosure['size'] = $attachment->size;
+ $enclosures[] = $enclosure;
+ }
+ }
+
+ if (!empty($enclosures)) {
+ $entry['enclosures'] = $enclosures;
+ }
+
+/*
+ // Enclosure
$attachments = $notice->attachments();
if($attachments){
$entry['enclosures']=array();
@@ -227,8 +264,8 @@ class TwitterapiAction extends Action
}
}
}
-
- # RSS Item specific
+*/
+ // RSS Item specific
$entry['description'] = $entry['content'];
$entry['pubDate'] = common_date_rfc2822($notice->created);
$entry['guid'] = $entry['link'];
@@ -354,6 +391,9 @@ class TwitterapiAction extends Action
case 'text':
$this->element($element, null, common_xml_safe_str($value));
break;
+ case 'attachments':
+ $this->show_xml_attachments($twitter_status['attachments']);
+ break;
default:
$this->element($element, null, $value);
}
@@ -374,6 +414,20 @@ class TwitterapiAction extends Action
$this->elementEnd($role);
}
+ function show_xml_attachments($attachments) {
+ if (!empty($attachments)) {
+ $this->elementStart('attachments', array('type' => 'array'));
+ foreach ($attachments as $attachment) {
+ $attrs = array();
+ $attrs['url'] = $attachment['url'];
+ $attrs['mimetype'] = $attachment['mimetype'];
+ $attrs['size'] = $attachment['size'];
+ $this->element('enclosure', $attrs, '');
+ }
+ $this->elementEnd('attachments');
+ }
+ }
+
function show_twitter_rss_item($entry)
{
$this->elementStart('item');
diff --git a/plugins/Comet/CometPlugin.php b/plugins/Comet/CometPlugin.php
index 45251c66f..1735d2b15 100644
--- a/plugins/Comet/CometPlugin.php
+++ b/plugins/Comet/CometPlugin.php
@@ -31,6 +31,8 @@ if (!defined('LACONICA')) {
exit(1);
}
+require_once INSTALLDIR.'/plugins/Realtime/RealtimePlugin.php';
+
/**
* Plugin to do realtime updates using Comet
*
@@ -41,165 +43,65 @@ if (!defined('LACONICA')) {
* @link http://laconi.ca/
*/
-class CometPlugin extends Plugin
+class CometPlugin extends RealtimePlugin
{
- var $server = null;
+ public $server = null;
+ public $username = null;
+ public $password = null;
+ public $prefix = null;
+ protected $bay = null;
- function __construct($server=null, $username=null, $password=null)
+ function __construct($server=null, $username=null, $password=null, $prefix=null)
{
$this->server = $server;
$this->username = $username;
$this->password = $password;
+ $this->prefix = $prefix;
parent::__construct();
}
- function onEndShowScripts($action)
+ function _getScripts()
{
- $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');
+ $scripts = parent::_getScripts();
- foreach ($scripts as $script) {
- $action->element('script', array('type' => 'text/javascript',
- 'src' => common_path('plugins/Comet/'.$script)),
- ' ');
- }
-
- $user = common_current_user();
+ $ours = array('jquery.comet.js', 'cometupdate.js');
- if (!empty($user->id)) {
- $user_id = $user->id;
- } else {
- $user_id = 0;
+ foreach ($ours as $script) {
+ $scripts[] = common_path('plugins/Comet/'.$script);
}
- $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;
+ return $scripts;
}
- function onEndNoticeSave($notice)
+ function _updateInitialize($timeline, $user_id)
{
- $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;
+ $script = parent::_updateInitialize($timeline, $user_id);
+ return $script." CometUpdate.init(\"$this->server\", \"$timeline\", $user_id, \"$this->replyurl\", \"$this->favorurl\", \"$this->deleteurl\");";
}
- function noticeAsJson($notice)
+ function _connect()
{
- // 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;
+ require_once INSTALLDIR.'/plugins/Comet/bayeux.class.inc.php';
+ // Bayeux? Comet? Huh? These terms confuse me
+ $this->bay = new Bayeux($this->server, $this->user, $this->password);
}
- function getNoticeTags($notice)
+ function _publish($timeline, $json)
{
- $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;
+ $this->bay->publish($timeline, $json);
}
- // Push this up to Plugin
+ function _disconnect()
+ {
+ unset($this->bay);
+ }
- function log($level, $msg)
+ function _pathToChannel($path)
{
- common_log($level, get_class($this) . ': '.$msg);
+ if (!empty($this->prefix)) {
+ array_unshift($path, $this->prefix);
+ }
+ return '/' . implode('/', $path);
}
}
diff --git a/plugins/Comet/cometupdate.js b/plugins/Comet/cometupdate.js
new file mode 100644
index 000000000..72cca004b
--- /dev/null
+++ b/plugins/Comet/cometupdate.js
@@ -0,0 +1,30 @@
+// update the local timeline from a Comet server
+//
+
+var CometUpdate = 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.init(server);
+ _server = server;
+ _timeline = timeline;
+ _userid = userid;
+ _favorurl = favorurl;
+ _replyurl = replyurl;
+ _deleteurl = deleteurl;
+ _cometd.subscribe(timeline, function(message) { RealtimeUpdate.receive(message.data) });
+ $(window).unload(function() { _cometd.disconnect(); } );
+ }
+ }
+}();
+
diff --git a/plugins/FBConnect/FBConnectPlugin.php b/plugins/FBConnect/FBConnectPlugin.php
index 65870a187..2e32ad198 100644
--- a/plugins/FBConnect/FBConnectPlugin.php
+++ b/plugins/FBConnect/FBConnectPlugin.php
@@ -122,7 +122,9 @@ class FBConnectPlugin extends Plugin
FB_RequireFeatures(
["XFBML"],
function() {
- FB.Facebook.init("%s", "../xd_receiver.html");
+ FB.init("%s", "../xd_receiver.html",
+ {"doNotUseCachedConnectState":true });
+
}
); }
@@ -220,11 +222,11 @@ class FBConnectPlugin extends Plugin
try {
$facebook = getFacebook();
- $fbuid = getFacebook()->get_loggedin_user();
+ $fbuid = $facebook->api_client->users_getLoggedInUser();
} catch (Exception $e) {
common_log(LOG_WARNING,
- 'Problem getting Facebook client: ' .
+ 'Problem getting Facebook user: ' .
$e->getMessage());
}
@@ -297,9 +299,9 @@ class FBConnectPlugin extends Plugin
$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);
+ $html = sprintf('<li id="nav_logout"><a href="#" title="%s" ' .
+ 'onclick="FB.Connect.logoutAndRedirect(\'%s\');">%s</a></li>',
+ $title, $logout_url, $text);
$action->raw($html);
diff --git a/plugins/FBConnect/README b/plugins/FBConnect/README
new file mode 100644
index 000000000..914b774cb
--- /dev/null
+++ b/plugins/FBConnect/README
@@ -0,0 +1,77 @@
+This plugin allows you to utilize Facebook Connect with Laconica.
+Supported Facebook Connect features:
+
+- Authenticate (register/login/logout -- works similar to OpenID)
+- Associate an existing Laconica account with a Facebook account
+- Disconnect a Facebook account from a Laconica account
+
+Future planned functionality:
+
+- Invite Facebook friends to use your Laconica installation
+- Auto-subscribe Facebook friends already using Laconica
+- Share Laconica favorite notices to your Facebook stream
+
+To use the plugin you will need to configure a Facebook application
+to point to your Laconica installation (see the Installation section
+below).
+
+Installation
+============
+
+If you don't already have the built-in Facebook application configured,
+you'll need to log into Facebook and create/configure a new application.
+Please follow the instructions in the section titled, "Setting Up Your
+Application and Getting an API Key," on the following page of the
+Facebook developer wiki:
+
+ http://wiki.developers.facebook.com/index.php/Connect/Setting_Up_Your_Site
+
+If you already are using the build-in Laconica Facebook application,
+you can modify your existing application's configuration using the
+Facebook Developer Application on Facebook. Use it to edit your
+application settings, and under the 'Connect' tab, change the 'Connect
+URL' to be the main URL for your Laconica site. E.g.:
+
+ http://SITE/PATH_TO_LACONICA/
+
+After you application is created and configured, you'll need to add its
+API key and secret to your Laconica config.php file:
+
+ $config['facebook']['apikey'] = 'APIKEY';
+ $config['facebook']['secret'] = 'SECRET';
+
+Finally, to enable the plugin, add the following stanza to your
+config.php:
+
+ require_once(INSTALLDIR.'/plugins/FBConnect/FBConnectPlugin.php');
+ $fbc = new FBConnectPlugin();
+
+To try out the plugin, fire up your browser and connect to:
+
+ http://SITE/PATH_TO_LACONICA/main/facebooklogin
+
+or, if you do not have fancy URLs turned on:
+
+ http://SITE/PATH_TO_LACONICA/index.php/main/facebooklogin
+
+You should see a page with a blue button that says: "Connect with
+Facebook".
+
+Connect/Disconnect existing account
+===================================
+
+If the Facebook Connect plugin is enabled, there will be a new Facebook
+Connect Settings tab under each user's Connect menu. Users can connect
+and disconnect to their Facebook accounts from it. Note: Before a user
+can disconnect from Facebook, she must set a normal Laconica password.
+Otherwise, she might not be able to login in to her account in the
+future. This is usually only required for users who have used Facebook
+Connect to register their Laconica account, and therefore haven't
+already set a local password.
+
+Helpful links
+=============
+
+Facebook Connect Homepage:
+http://developers.facebook.com/connect.php
+
diff --git a/plugins/Meteor/MeteorPlugin.php b/plugins/Meteor/MeteorPlugin.php
index 07285552c..d54d565bd 100644
--- a/plugins/Meteor/MeteorPlugin.php
+++ b/plugins/Meteor/MeteorPlugin.php
@@ -31,8 +31,10 @@ if (!defined('LACONICA')) {
exit(1);
}
+require_once INSTALLDIR.'/plugins/Realtime/RealtimePlugin.php';
+
/**
- * Plugin to do realtime updates using Comet
+ * Plugin to do realtime updates using Meteor
*
* @category Plugin
* @package Laconica
@@ -41,7 +43,7 @@ if (!defined('LACONICA')) {
* @link http://laconi.ca/
*/
-class MeteorPlugin extends Plugin
+class MeteorPlugin extends RealtimePlugin
{
public $webserver = null;
public $webport = null;
@@ -63,93 +65,21 @@ class MeteorPlugin extends Plugin
parent::__construct();
}
- function onEndShowScripts($action)
+ function _getScripts()
{
- $timeline = null;
-
- 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;
- }
-
- $action->element('script', array('type' => 'text/javascript',
- 'src' => 'http://'.$this->webserver.(($this->webport == 80) ? '':':'.$this->webport).'/meteor.js'),
- ' ');
-
- foreach (array('meteorupdater.js', 'json2.js') as $script) {
- $action->element('script', array('type' => 'text/javascript',
- 'src' => common_path('plugins/Meteor/'.$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() { MeteorUpdater.init(\"$this->webserver\", $this->webport, \"{$this->channelbase}{$timeline}\", $user_id, \"$replyurl\", \"$favorurl\", \"$deleteurl\"); });");
- $action->elementEnd('script');
-
- return true;
+ $scripts = parent::_getScripts();
+ $scripts[] = 'http://'.$this->webserver.(($this->webport == 80) ? '':':'.$this->webport).'/meteor.js';
+ $scripts[] = common_path('plugins/Meteor/meteorupdater.js');
+ return $scripts;
}
- function onEndNoticeSave($notice)
+ function _updateInitialize($timeline, $user_id)
{
- $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) {
-
- $json = json_encode($this->noticeAsJson($notice));
-
- $this->_connect();
-
- foreach ($timelines as $timeline) {
- $this->_addMessage($timeline, $json);
- }
-
- $this->_disconnect();
- }
-
- return true;
+ $script = parent::_updateInitialize($timeline, $user_id);
+ return $script." MeteorUpdater.init(\"$this->webserver\", $this->webport, \"{$timeline}\");";
}
- protected function _connect()
+ function _connect()
{
$controlserver = (empty($this->controlserver)) ? $this->webserver : $this->controlserver;
// May throw an exception.
@@ -159,10 +89,11 @@ class MeteorPlugin extends Plugin
}
}
- protected function _addMessage($channel, $message)
+ function _publish($channel, $message)
{
+ $message = json_encode($message);
$message = addslashes($message);
- $cmd = "ADDMESSAGE {$this->channelbase}{$channel} $message\n";
+ $cmd = "ADDMESSAGE $channel $message\n";
$cnt = fwrite($this->_socket, $cmd);
$result = fgets($this->_socket);
if (preg_match('/^ERR (.*)$/', $result, $matches)) {
@@ -171,66 +102,19 @@ class MeteorPlugin extends Plugin
// TODO: parse and deal with result
}
- protected function _disconnect()
+ function _disconnect()
{
$cnt = fwrite($this->_socket, "QUIT\n");
@fclose($this->_socket);
}
- 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;
+ // Meteord flips out with default '/' separator
- return $arr;
- }
-
- function getNoticeTags($notice)
+ function _pathToChannel($path)
{
- $tags = null;
-
- $nt = new Notice_tag();
- $nt->notice_id = $notice->id;
-
- if ($nt->find()) {
- $tags = array();
- while ($nt->fetch()) {
- $tags[] = $nt->tag;
- }
+ if (!empty($this->channelbase)) {
+ array_unshift($path, $this->channelbase);
}
-
- $nt->free();
- $nt = null;
-
- return $tags;
- }
-
- // Push this up to Plugin
-
- function log($level, $msg)
- {
- common_log($level, get_class($this) . ': '.$msg);
+ return implode('-', $path);
}
}
diff --git a/plugins/Meteor/json2.js b/plugins/Meteor/json2.js
deleted file mode 100644
index 7e27df518..000000000
--- a/plugins/Meteor/json2.js
+++ /dev/null
@@ -1,478 +0,0 @@
-/*
- 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/Meteor/meteorupdater.js b/plugins/Meteor/meteorupdater.js
index 60d2cc372..2e688336f 100644
--- a/plugins/Meteor/meteorupdater.js
+++ b/plugins/Meteor/meteorupdater.js
@@ -3,24 +3,12 @@
var MeteorUpdater = function()
{
- var _server;
- var _port;
- var _timeline;
- var _userid;
- var _replyurl;
- var _favorurl;
- var _deleteurl;
-
return {
- init: function(server, port, timeline, userid, replyurl, favorurl, deleteurl)
- {
- _userid = userid;
- _replyurl = replyurl;
- _favorurl = favorurl;
- _deleteurl = deleteurl;
+ init: function(server, port, timeline)
+ {
Meteor.callbacks["process"] = function(data) {
- receive(JSON.parse(data));
+ RealtimeUpdate.receive(JSON.parse(data));
};
Meteor.host = server;
@@ -29,123 +17,5 @@ var MeteorUpdater = function()
Meteor.connect();
}
}
-
- function receive(data)
- {
- id = data.id;
-
- // Don't add it if it already exists
- //
- if ($("#notice-"+id).length > 0) {
- return;
- }
-
- var noticeItem = makeNoticeItem(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/PiwikAnalyticsPlugin.php b/plugins/PiwikAnalyticsPlugin.php
index d2c52354e..dc3c7c37f 100644
--- a/plugins/PiwikAnalyticsPlugin.php
+++ b/plugins/PiwikAnalyticsPlugin.php
@@ -87,18 +87,25 @@ class PiwikAnalyticsPlugin extends Plugin
function onEndShowScripts($action)
{
- $js1 = 'var pkBaseURL = (("https:" == document.location.protocol) ? "https://'.
- $this->piwikroot.'" : "http://'.$this->piwikroot.
- '"); document.write(unescape("%3Cscript src=\'" + pkBaseURL + "piwik.js\''.
- ' type=\'text/javascript\'%3E%3C/script%3E"));';
- $js2 = 'piwik_action_name = ""; piwik_idsite = '.$this->piwikid.
- '; piwik_url = pkBaseURL + "piwik.php"; piwik_log(piwik_action_name, piwik_idsite, piwik_url);';
- $action->elementStart('script', array('type' => 'text/javascript'));
- $action->raw($js1);
- $action->elementEnd('script');
- $action->elementStart('script', array('type' => 'text/javascript'));
- $action->raw($js2);
- $action->elementEnd('script');
+ $piwikCode = <<<ENDOFPIWIK
+
+<!-- Piwik -->
+<script type="text/javascript">
+var pkBaseURL = (("https:" == document.location.protocol) ? "https://{$this->piwikroot}" : "http://{$this->piwikroot}");
+document.write(unescape("%3Cscript src='" + pkBaseURL + "piwik.js' type='text/javascript'%3E%3C/script%3E"));
+</script>
+<script type="text/javascript">
+try {
+ var piwikTracker = Piwik.getTracker(pkBaseURL + "piwik.php", 4);
+ piwikTracker.trackPageView();
+ piwikTracker.enableLinkTracking();
+} catch( err ) {}
+</script>
+<!-- End Piwik Tag -->
+
+ENDOFPIWIK;
+
+ $action->raw($piwikCode);
return true;
}
} \ No newline at end of file
diff --git a/plugins/Realtime/RealtimePlugin.php b/plugins/Realtime/RealtimePlugin.php
new file mode 100644
index 000000000..507f0194d
--- /dev/null
+++ b/plugins/Realtime/RealtimePlugin.php
@@ -0,0 +1,229 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Superclass for plugins that do "real time" updates of timelines using Ajax
+ *
+ * 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);
+}
+
+/**
+ * Superclass for plugin to do realtime updates
+ *
+ * Based on experience with the Comet and Meteor plugins,
+ * this superclass extracts out some of the common functionality
+ *
+ * @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 RealtimePlugin extends Plugin
+{
+ protected $replyurl = null;
+ protected $favorurl = null;
+ protected $deleteurl = null;
+
+ function onInitializePlugin()
+ {
+ $this->replyurl = common_local_url('newnotice');
+ $this->favorurl = common_local_url('favor');
+ // FIXME: need to find a better way to pass this pattern in
+ $this->deleteurl = common_local_url('deletenotice',
+ array('notice' => '0000000000'));
+ }
+
+ function onEndShowScripts($action)
+ {
+ $path = null;
+
+ switch ($action->trimmed('action')) {
+ case 'public':
+ $path = array('public');
+ break;
+ case 'tag':
+ $tag = $action->trimmed('tag');
+ if (!empty($tag)) {
+ $path = array('tag', $tag);
+ } else {
+ return true;
+ }
+ break;
+ default:
+ return true;
+ }
+
+ $timeline = $this->_pathToChannel($path);
+
+ $scripts = $this->_getScripts();
+
+ foreach ($scripts as $script) {
+ $action->element('script', array('type' => 'text/javascript',
+ 'src' => $script),
+ ' ');
+ }
+
+ $user = common_current_user();
+
+ if (!empty($user->id)) {
+ $user_id = $user->id;
+ } else {
+ $user_id = 0;
+ }
+
+ $action->elementStart('script', array('type' => 'text/javascript'));
+ $action->raw("$(document).ready(function() { ");
+ $action->raw($this->_updateInitialize($timeline, $user_id));
+ $action->raw(" });");
+ $action->elementEnd('script');
+
+ return true;
+ }
+
+ function onEndNoticeSave($notice)
+ {
+ $paths = array();
+
+ // XXX: Add other timelines; this is just for the public one
+
+ if ($notice->is_local ||
+ ($notice->is_local == 0 && !common_config('public', 'localonly'))) {
+ $paths[] = array('public');
+ }
+
+ $tags = $this->getNoticeTags($notice);
+
+ if (!empty($tags)) {
+ foreach ($tags as $tag) {
+ $paths[] = array('tag', $tag);
+ }
+ }
+
+ if (count($paths) > 0) {
+
+ $json = $this->noticeAsJson($notice);
+
+ $this->_connect();
+
+ foreach ($paths as $path) {
+ $timeline = $this->_pathToChannel($path);
+ $this->_publish($timeline, $json);
+ }
+
+ $this->_disconnect();
+ }
+
+ 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);
+ }
+
+ function _getScripts()
+ {
+ return array(common_path('plugins/Realtime/realtimeupdate.js'),
+ common_path('plugins/Realtime/json2.js'));
+ }
+
+ function _updateInitialize($timeline, $user_id)
+ {
+ return "RealtimeUpdate.init($user_id, \"$this->replyurl\", \"$this->favorurl\", \"$this->deleteurl\"); ";
+ }
+
+ function _connect()
+ {
+ }
+
+ function _publish($timeline, $json)
+ {
+ }
+
+ function _disconnect()
+ {
+ }
+
+ function _pathToChannel($path)
+ {
+ return '';
+ }
+}
diff --git a/plugins/Comet/json2.js b/plugins/Realtime/json2.js
index 7e27df518..7e27df518 100644
--- a/plugins/Comet/json2.js
+++ b/plugins/Realtime/json2.js
diff --git a/plugins/Comet/updatetimeline.js b/plugins/Realtime/realtimeupdate.js
index 170949e9b..d55db5859 100644
--- a/plugins/Comet/updatetimeline.js
+++ b/plugins/Realtime/realtimeupdate.js
@@ -1,41 +1,24 @@
-// update the local timeline from a Comet server
+// add a notice encoded as JSON into the current timeline
//
-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);
- }
- }
+RealtimeUpdate = {
+
+ _userid: 0,
+ _replyurl: '',
+ _favorurl: '',
+ _deleteurl: '',
- function leave()
+ init: function(userid, replyurl, favorurl, deleteurl)
{
- _cometd.disconnect();
- }
+ RealtimeUpdate._userid = userid;
+ RealtimeUpdate._replyurl = replyurl;
+ RealtimeUpdate._favorurl = favorurl;
+ RealtimeUpdate._deleteurl = deleteurl;
+ },
- function receive(message)
+ receive: function(data)
{
- id = message.data.id;
+ id = data.id;
// Don't add it if it already exists
@@ -43,15 +26,14 @@ var updater = function()
return;
}
- var noticeItem = makeNoticeItem(message.data);
+ var noticeItem = RealtimeUpdate.makeNoticeItem(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)
+ makeNoticeItem: function(data)
{
user = data['user'];
html = data['html'].replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&quot;/g,'"');
@@ -93,26 +75,26 @@ var updater = function()
ni = ni+"</div>"+
"<div class=\"notice-options\">";
- if (_userid != 0) {
+ if (RealtimeUpdate._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+RealtimeUpdate.makeFavoriteForm(data['id'], session_key);
+ ni = ni+RealtimeUpdate.makeReplyLink(data['id'], data['user']['screen_name']);
+ if (RealtimeUpdate._userid == data['user']['id']) {
+ ni = ni+RealtimeUpdate.makeDeleteLink(data['id']);
}
}
ni = ni+"</div>"+
"</li>";
return ni;
- }
+ },
- function makeFavoriteForm(id, session_key)
+ makeFavoriteForm: function(id, session_key)
{
var ff;
- ff = "<form id=\"favor-"+id+"\" class=\"form_favor\" method=\"post\" action=\""+_favorurl+"\">"+
+ ff = "<form id=\"favor-"+id+"\" class=\"form_favor\" method=\"post\" action=\""+RealtimeUpdate._favorurl+"\">"+
"<fieldset>"+
"<legend>Favor this notice</legend>"+ // XXX: i18n
"<input name=\"token-"+id+"\" type=\"hidden\" id=\"token-"+id+"\" value=\""+session_key+"\"/>"+
@@ -121,25 +103,25 @@ var updater = function()
"</fieldset>"+
"</form>";
return ff;
- }
+ },
- function makeReplyLink(id, nickname)
+ makeReplyLink: function(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 href=\""+RealtimeUpdate._replyurl+"?replyto="+nickname+"\" title=\"Reply to this notice\">Reply <span class=\"notice_id\">"+id+"</span>"+
"</a>"+
"</dd>"+
"</dl>";
return rl;
- }
+ },
- function makeDeleteLink(id)
+ makeDeleteLink: function(id)
{
var dl, delurl;
- delurl = _deleteurl.replace("0000000000", id);
+ delurl = RealtimeUpdate._deleteurl.replace("0000000000", id);
dl = "<dl class=\"notice_delete\">"+
"<dt>Delete this notice</dt>"+
@@ -149,6 +131,5 @@ var updater = function()
"</dl>";
return dl;
- }
-}();
-
+ },
+}
diff --git a/scripts/triminboxes.php b/scripts/triminboxes.php
index b2135d682..27e200fef 100644
--- a/scripts/triminboxes.php
+++ b/scripts/triminboxes.php
@@ -52,43 +52,5 @@ if (!empty($id)) {
$cnt = $user->find();
while ($user->fetch()) {
-
- $inbox_entry = new Notice_inbox();
- $inbox_entry->user_id = $user->id;
- $inbox_entry->orderBy('created DESC');
- $inbox_entry->limit(1000, 1);
-
- $id = null;
-
- if ($inbox_entry->find(true)) {
- $id = $inbox_entry->notice_id;
- }
-
- $inbox_entry->free();
- unset($inbox_entry);
-
- if (is_null($id)) {
- continue;
- }
-
- $start = microtime(true);
-
- $old_inbox = new Notice_inbox();
- $cnt = $old_inbox->query('DELETE from notice_inbox WHERE user_id = ' . $user->id . ' AND notice_id < ' . $id);
- $old_inbox->free();
- unset($old_inbox);
-
- print "Deleted $cnt notices for $user->nickname ($user->id).\n";
-
- $finish = microtime(true);
-
- $delay = 3.0 * ($finish - $start);
-
- print "Delaying $delay seconds...";
-
- // Wait to let slaves catch up
-
- usleep($delay * 1000000);
-
- print "DONE.\n";
+ Notice_inbox::gc($user->id);
}
diff --git a/scripts/twitterstatusfetcher.php b/scripts/twitterstatusfetcher.php
index 8b10bfbad..e2ba1d003 100755
--- a/scripts/twitterstatusfetcher.php
+++ b/scripts/twitterstatusfetcher.php
@@ -25,19 +25,18 @@ define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
define('MAXCHILDREN', 2);
define('POLL_INTERVAL', 60); // in seconds
-$shortoptions = 'i::';
-$longoptions = array('id::');
+$shortoptions = 'di::';
+$longoptions = array('id::', 'debug');
$helptext = <<<END_OF_TRIM_HELP
Batch script for retrieving Twitter messages from foreign service.
- -i --id Identity (default 'generic')
-
-END_OF_TRIM_HELP;
+ -i --id Identity (default 'generic')
+ -d --debug Debug (lots of log output)
-require_once INSTALLDIR.'/scripts/commandline.inc';
+END_OF_TRIM_HELP;
-require_once INSTALLDIR . '/lib/common.php';
+require_once INSTALLDIR .'/scripts/commandline.inc';
require_once INSTALLDIR . '/lib/daemon.php';
/**
@@ -61,6 +60,15 @@ class TwitterStatusFetcher extends Daemon
{
private $_children = array();
+ function __construct($id=null, $daemonize=true)
+ {
+ parent::__construct($daemonize);
+
+ if ($id) {
+ $this->set_id($id);
+ }
+ }
+
/**
* Name of this daemon
*
@@ -80,6 +88,11 @@ class TwitterStatusFetcher extends Daemon
function run()
{
+ if (defined('SCRIPT_DEBUG')) {
+ common_debug($this->name() .
+ ': debugging log output enabled.');
+ }
+
do {
$flinks = $this->refreshFlinks();
@@ -640,6 +653,10 @@ if (have_option('i')) {
$id = null;
}
+if (have_option('d') || have_option('debug')) {
+ define('SCRIPT_DEBUG', true);
+}
+
$fetcher = new TwitterStatusFetcher($id);
$fetcher->runOnce();
diff --git a/theme/base/css/display.css b/theme/base/css/display.css
index 3426e71c0..867dc0ef7 100644
--- a/theme/base/css/display.css
+++ b/theme/base/css/display.css
@@ -482,7 +482,7 @@ height:16px;
}
#form_notice .form_note {
position:absolute;
-top:99px;
+bottom:2px;
right:98px;
z-index:9;
}
@@ -863,7 +863,7 @@ clear:left;
float:left;
font-size:0.95em;
margin-left:59px;
-width:60%;
+width:50%;
}
#showstream .notice div.entry-content,
#shownotice .notice div.entry-content {
@@ -1028,7 +1028,7 @@ border-radius:7px;
-webkit-border-radius:7px;
}
#jOverlayLoading {
-top:22.5%;
+top:5%;
left:40%;
}
#attachment_view img {