summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/action.php300
-rw-r--r--lib/channel.php237
-rw-r--r--lib/clienterroraction.php17
-rw-r--r--lib/clientexception.php56
-rw-r--r--lib/command.php419
-rw-r--r--lib/commandinterpreter.php197
-rw-r--r--lib/common.php76
-rw-r--r--lib/dberroraction.php73
-rw-r--r--lib/error.php3
-rw-r--r--lib/event.php113
-rw-r--r--lib/facebookaction.php232
-rw-r--r--lib/facebookutil.php108
-rw-r--r--lib/featureduserssection.php5
-rw-r--r--lib/feed.php110
-rw-r--r--lib/feedlist.php94
-rw-r--r--lib/grouplist.php10
-rw-r--r--lib/groupsbymemberssection.php2
-rw-r--r--lib/groupsbypostssection.php2
-rw-r--r--lib/grouptagcloudsection.php8
-rw-r--r--lib/htmloutputter.php25
-rw-r--r--lib/imagefile.php189
-rw-r--r--lib/jabber.php40
-rw-r--r--lib/language.php61
-rw-r--r--lib/mail.php61
-rw-r--r--lib/mailbox.php6
-rw-r--r--lib/messageform.php4
-rw-r--r--lib/noticelist.php45
-rw-r--r--lib/noticesection.php9
-rw-r--r--lib/oauthstore.php2
-rw-r--r--lib/omb.php10
-rw-r--r--lib/openid.php3
-rw-r--r--lib/peoplesearchresults.php75
-rw-r--r--lib/personal.php1
-rw-r--r--lib/personaltagcloudsection.php10
-rw-r--r--lib/plugin.php79
-rw-r--r--lib/popularnoticesection.php19
-rw-r--r--lib/profilelist.php25
-rw-r--r--lib/profileminilist.php6
-rw-r--r--lib/profilesection.php2
-rw-r--r--lib/publicgroupnav.php28
-rw-r--r--lib/router.php438
-rw-r--r--lib/rssaction.php38
-rw-r--r--lib/searchaction.php3
-rw-r--r--lib/section.php2
-rw-r--r--lib/servererroraction.php22
-rw-r--r--lib/serverexception.php55
-rw-r--r--lib/settingsaction.php7
-rw-r--r--lib/twitter.php97
-rw-r--r--lib/twitterapi.php43
-rw-r--r--lib/util.php607
-rw-r--r--lib/xmlstringer.php68
51 files changed, 3266 insertions, 876 deletions
diff --git a/lib/action.php b/lib/action.php
index 8f02b36bf..812df635e 100644
--- a/lib/action.php
+++ b/lib/action.php
@@ -73,7 +73,6 @@ class Action extends HTMLOutputter // lawsuit
parent::__construct($output, $indent);
}
-
/**
* For initializing members of the class.
*
@@ -94,7 +93,10 @@ class Action extends HTMLOutputter // lawsuit
*/
function showPage()
{
- $this->startHTML();
+ if (Event::handle('StartShowHTML', array($this))) {
+ $this->startHTML();
+ Event::handle('EndShowHTML', array($this));
+ }
$this->showHead();
$this->showBody();
$this->endHTML();
@@ -152,22 +154,49 @@ class Action extends HTMLOutputter // lawsuit
*/
function showStylesheets()
{
- $this->element('link', array('rel' => 'stylesheet',
- 'type' => 'text/css',
- 'href' => theme_path('css/display.css', 'base') . '?version=' . LACONICA_VERSION,
- 'media' => 'screen, projection, tv'));
- $this->element('link', array('rel' => 'stylesheet',
- 'type' => 'text/css',
- 'href' => theme_path('css/display.css', null) . '?version=' . LACONICA_VERSION,
- 'media' => 'screen, projection, tv'));
- $this->comment('[if IE]><link rel="stylesheet" type="text/css" '.
- 'href="'.theme_path('css/ie.css', 'base').'?version='.LACONICA_VERSION.'" /><![endif]');
- foreach (array(6,7) as $ver) {
- if (file_exists(theme_file('ie'.$ver.'.css'))) {
- // Yes, IE people should be put in jail.
- $this->comment('[if lte IE '.$ver.']><link rel="stylesheet" type="text/css" '.
- 'href="'.theme_path('css/ie'.$ver.'.css', 'base').'?version='.LACONICA_VERSION.'" /><![endif]');
+ if (Event::handle('StartShowStyles', array($this))) {
+ if (Event::handle('StartShowLaconicaStyles', array($this))) {
+
+ $this->element('link', array('rel' => 'stylesheet',
+ 'type' => 'text/css',
+ 'href' => theme_path('css/display.css', 'base') . '?version=' . LACONICA_VERSION,
+ 'media' => 'screen, projection, tv'));
+ $this->element('link', array('rel' => 'stylesheet',
+ 'type' => 'text/css',
+ 'href' => theme_path('css/modal.css', 'base') . '?version=' . LACONICA_VERSION,
+ 'media' => 'screen, projection, tv'));
+ $this->element('link', array('rel' => 'stylesheet',
+ 'type' => 'text/css',
+ 'href' => theme_path('css/display.css', null) . '?version=' . LACONICA_VERSION,
+ 'media' => 'screen, projection, tv'));
+ if (common_config('site', 'mobile')) {
+ $this->element('link', array('rel' => 'stylesheet',
+ 'type' => 'text/css',
+ 'href' => theme_path('css/mobile.css', 'base') . '?version=' . LACONICA_VERSION,
+ // TODO: "handheld" CSS for other mobile devices
+ 'media' => 'only screen and (max-device-width: 480px)')); // Mobile WebKit
+ }
+ $this->element('link', array('rel' => 'stylesheet',
+ 'type' => 'text/css',
+ 'href' => theme_path('css/print.css', 'base') . '?version=' . LACONICA_VERSION,
+ 'media' => 'print'));
+ Event::handle('EndShowLaconicaStyles', array($this));
}
+ if (Event::handle('StartShowUAStyles', array($this))) {
+ $this->comment('[if IE]><link rel="stylesheet" type="text/css" '.
+ 'href="'.theme_path('css/ie.css', 'base').'?version='.LACONICA_VERSION.'" /><![endif]');
+ foreach (array(6,7) as $ver) {
+ if (file_exists(theme_file('css/ie'.$ver.'.css', 'base'))) {
+ // Yes, IE people should be put in jail.
+ $this->comment('[if lte IE '.$ver.']><link rel="stylesheet" type="text/css" '.
+ 'href="'.theme_path('css/ie'.$ver.'.css', 'base').'?version='.LACONICA_VERSION.'" /><![endif]');
+ }
+ }
+ $this->comment('[if IE]><link rel="stylesheet" type="text/css" '.
+ 'href="'.theme_path('css/ie.css', null).'?version='.LACONICA_VERSION.'" /><![endif]');
+ Event::handle('EndShowUAStyles', array($this));
+ }
+ Event::handle('EndShowStyles', array($this));
}
}
@@ -178,18 +207,43 @@ class Action extends HTMLOutputter // lawsuit
*/
function showScripts()
{
- $this->element('script', array('type' => 'text/javascript',
- 'src' => common_path('js/jquery.min.js')),
- ' ');
- $this->element('script', array('type' => 'text/javascript',
- 'src' => common_path('js/jquery.form.js')),
- ' ');
- $this->element('script', array('type' => 'text/javascript',
- 'src' => common_path('js/xbImportNode.js')),
- ' ');
- $this->element('script', array('type' => 'text/javascript',
- 'src' => common_path('js/util.js?version='.LACONICA_VERSION)),
- ' ');
+ if (Event::handle('StartShowScripts', array($this))) {
+ if (Event::handle('StartShowJQueryScripts', array($this))) {
+ $this->element('script', array('type' => 'text/javascript',
+ 'src' => common_path('js/jquery.min.js')),
+ ' ');
+ $this->element('script', array('type' => 'text/javascript',
+ 'src' => common_path('js/jquery.form.js')),
+ ' ');
+
+ $this->element('script', array('type' => 'text/javascript',
+ 'src' => common_path('js/jquery.simplemodal-1.2.2.pack.js')),
+ ' ');
+
+ Event::handle('EndShowJQueryScripts', array($this));
+ }
+ if (Event::handle('StartShowLaconicaScripts', array($this))) {
+ $this->element('script', array('type' => 'text/javascript',
+ 'src' => common_path('js/xbImportNode.js')),
+ ' ');
+ $this->element('script', array('type' => 'text/javascript',
+ 'src' => common_path('js/util.js?version='.LACONICA_VERSION)),
+ ' ');
+ // Frame-busting code to avoid clickjacking attacks.
+ $this->element('script', array('type' => 'text/javascript'),
+ 'if (window.top !== window.self) { window.top.location.href = window.self.location.href; }');
+
+ $this->element('script', array('type' => 'text/javascript',
+ 'src' => common_path('js/flowplayer-3.0.5.min.js')),
+ ' ');
+
+ $this->element('script', array('type' => 'text/javascript',
+ 'src' => common_path('js/video.js')),
+ ' ');
+ Event::handle('EndShowLaconicaScripts', array($this));
+ }
+ Event::handle('EndShowScripts', array($this));
+ }
}
/**
@@ -215,9 +269,19 @@ class Action extends HTMLOutputter // lawsuit
*
* @return nothing
*/
+
function showFeeds()
{
- // does nothing by default
+ $feeds = $this->getFeeds();
+
+ if ($feeds) {
+ foreach ($feeds as $feed) {
+ $this->element('link', array('rel' => $feed->rel(),
+ 'href' => $feed->url,
+ 'type' => $feed->mimeType(),
+ 'title' => $feed->title));
+ }
+ }
}
/**
@@ -244,7 +308,6 @@ class Action extends HTMLOutputter // lawsuit
// does nothing by default
}
-
/**
* Show body.
*
@@ -255,10 +318,16 @@ class Action extends HTMLOutputter // lawsuit
function showBody()
{
$this->elementStart('body', array('id' => $this->trimmed('action')));
- $this->elementStart('div', 'wrap');
- $this->showHeader();
+ $this->elementStart('div', array('id' => 'wrap'));
+ if (Event::handle('StartShowHeader', array($this))) {
+ $this->showHeader();
+ Event::handle('EndShowHeader', array($this));
+ }
$this->showCore();
- $this->showFooter();
+ if (Event::handle('StartShowFooter', array($this))) {
+ $this->showFooter();
+ Event::handle('EndShowFooter', array($this));
+ }
$this->elementEnd('div');
$this->elementEnd('body');
}
@@ -312,41 +381,51 @@ class Action extends HTMLOutputter // lawsuit
*/
function showPrimaryNav()
{
+ $user = common_current_user();
+
$this->elementStart('dl', array('id' => 'site_nav_global_primary'));
$this->element('dt', null, _('Primary site navigation'));
$this->elementStart('dd');
- $user = common_current_user();
$this->elementStart('ul', array('class' => 'nav'));
- if ($user) {
- $this->menuItem(common_local_url('all', array('nickname' => $user->nickname)),
- _('Home'), _('Personal profile and friends timeline'), false, 'nav_home');
- }
- $this->menuItem(common_local_url('peoplesearch'),
- _('Search'), _('Search for people or text'), false, 'nav_search');
- if ($user) {
- $this->menuItem(common_local_url('profilesettings'),
- _('Account'), _('Change your email, avatar, password, profile'), false, 'nav_account');
- $this->menuItem(common_local_url('imsettings'),
- _('Connect'), _('Connect to IM, SMS, Twitter'), false, 'nav_connect');
- $this->menuItem(common_local_url('logout'),
- _('Logout'), _('Logout from the site'), false, 'nav_logout');
- } else {
- $this->menuItem(common_local_url('login'),
- _('Login'), _('Login to the site'), false, 'nav_login');
- if (!common_config('site', 'closed')) {
- $this->menuItem(common_local_url('register'),
- _('Register'), _('Create an account'), false, 'nav_register');
+ if (Event::handle('StartPrimaryNav', array($this))) {
+ if ($user) {
+ $this->menuItem(common_local_url('all', array('nickname' => $user->nickname)),
+ _('Home'), _('Personal profile and friends timeline'), false, 'nav_home');
+ }
+ $this->menuItem(common_local_url('peoplesearch'),
+ _('Search'), _('Search for people or text'), false, 'nav_search');
+ if ($user) {
+ $this->menuItem(common_local_url('profilesettings'),
+ _('Account'), _('Change your email, avatar, password, profile'), false, 'nav_account');
+
+ if (common_config('xmpp', 'enabled')) {
+ $this->menuItem(common_local_url('imsettings'),
+ _('Connect'), _('Connect to IM, SMS, Twitter'), false, 'nav_connect');
+ } else {
+ $this->menuItem(common_local_url('smssettings'),
+ _('Connect'), _('Connect to SMS, Twitter'), false, 'nav_connect');
+ }
+ $this->menuItem(common_local_url('logout'),
+ _('Logout'), _('Logout from the site'), false, 'nav_logout');
+ } else {
+ $this->menuItem(common_local_url('login'),
+ _('Login'), _('Login to the site'), false, 'nav_login');
+ if (!common_config('site', 'closed')) {
+ $this->menuItem(common_local_url('register'),
+ _('Register'), _('Create an account'), false, 'nav_register');
+ }
+ $this->menuItem(common_local_url('openidlogin'),
+ _('OpenID'), _('Login with OpenID'), false, 'nav_openid');
}
- $this->menuItem(common_local_url('openidlogin'),
- _('OpenID'), _('Login with OpenID'), false, 'nav_openid');
+ $this->menuItem(common_local_url('doc', array('title' => 'help')),
+ _('Help'), _('Help me!'), false, 'nav_help');
+ Event::handle('EndPrimaryNav', array($this));
}
- $this->menuItem(common_local_url('doc', array('title' => 'help')),
- _('Help'), _('Help me!'), false, 'nav_help');
$this->elementEnd('ul');
$this->elementEnd('dd');
$this->elementEnd('dl');
}
-
+
/**
* Show site notice.
*
@@ -360,7 +439,9 @@ class Action extends HTMLOutputter // lawsuit
$this->elementStart('dl', array('id' => 'site_notice',
'class' => 'system_notice'));
$this->element('dt', null, _('Site notice'));
- $this->element('dd', null, $text);
+ $this->elementStart('dd', null);
+ $this->raw($text);
+ $this->elementEnd('dd');
$this->elementEnd('dl');
}
}
@@ -377,7 +458,7 @@ class Action extends HTMLOutputter // lawsuit
$notice_form = new NoticeForm($this);
$notice_form->show();
}
-
+
/**
* Show anonymous message.
*
@@ -400,8 +481,14 @@ class Action extends HTMLOutputter // lawsuit
function showCore()
{
$this->elementStart('div', array('id' => 'core'));
- $this->showLocalNavBlock();
- $this->showContentBlock();
+ if (Event::handle('StartShowLocalNavBlock', array($this))) {
+ $this->showLocalNavBlock();
+ Event::handle('EndShowLocalNavBlock', array($this));
+ }
+ if (Event::handle('StartShowContentBlock', array($this))) {
+ $this->showContentBlock();
+ Event::handle('EndShowContentBlock', array($this));
+ }
$this->showAside();
$this->elementEnd('div');
}
@@ -503,27 +590,32 @@ class Action extends HTMLOutputter // lawsuit
*
* @return nothing
*/
+
function showAside()
{
$this->elementStart('div', array('id' => 'aside_primary',
'class' => 'aside'));
$this->showExportData();
- $this->showSections();
+ if (Event::handle('StartShowSections', array($this))) {
+ $this->showSections();
+ Event::handle('EndShowSections', array($this));
+ }
$this->elementEnd('div');
}
/**
* Show export data feeds.
*
- * MAY overload if there are feeds
- *
- * @return nothing
+ * @return void
*/
+
function showExportData()
{
- // is there structure to this?
- // list of (visible!) feed links
- // can we reuse list of feeds from showFeeds() ?
+ $feeds = $this->getFeeds();
+ if ($feeds) {
+ $fl = new FeedList($this);
+ $fl->show($feeds);
+ }
}
/**
@@ -562,18 +654,23 @@ class Action extends HTMLOutputter // lawsuit
$this->element('dt', null, _('Secondary site navigation'));
$this->elementStart('dd', null);
$this->elementStart('ul', array('class' => 'nav'));
- $this->menuItem(common_local_url('doc', array('title' => 'help')),
- _('Help'));
- $this->menuItem(common_local_url('doc', array('title' => 'about')),
- _('About'));
- $this->menuItem(common_local_url('doc', array('title' => 'faq')),
- _('FAQ'));
- $this->menuItem(common_local_url('doc', array('title' => 'privacy')),
- _('Privacy'));
- $this->menuItem(common_local_url('doc', array('title' => 'source')),
- _('Source'));
- $this->menuItem(common_local_url('doc', array('title' => 'contact')),
- _('Contact'));
+ if (Event::handle('StartSecondaryNav', array($this))) {
+ $this->menuItem(common_local_url('doc', array('title' => 'help')),
+ _('Help'));
+ $this->menuItem(common_local_url('doc', array('title' => 'about')),
+ _('About'));
+ $this->menuItem(common_local_url('doc', array('title' => 'faq')),
+ _('FAQ'));
+ $this->menuItem(common_local_url('doc', array('title' => 'privacy')),
+ _('Privacy'));
+ $this->menuItem(common_local_url('doc', array('title' => 'source')),
+ _('Source'));
+ $this->menuItem(common_local_url('doc', array('title' => 'contact')),
+ _('Contact'));
+ $this->menuItem(common_local_url('doc', array('title' => 'badge')),
+ _('Badge'));
+ Event::handle('EndSecondaryNav', array($this));
+ }
$this->elementEnd('ul');
$this->elementEnd('dd');
$this->elementEnd('dl');
@@ -726,8 +823,10 @@ class Action extends HTMLOutputter // lawsuit
if ($if_modified_since) {
$ims = strtotime($if_modified_since);
if ($lm <= $ims) {
- if (!$etag ||
- $this->_hasEtag($etag, $_SERVER['HTTP_IF_NONE_MATCH'])) {
+ $if_none_match = $_SERVER['HTTP_IF_NONE_MATCH'];
+ if (!$if_none_match ||
+ !$etag ||
+ $this->_hasEtag($etag, $if_none_match)) {
header('HTTP/1.1 304 Not Modified');
// Better way to do this?
exit(0);
@@ -745,15 +844,17 @@ class Action extends HTMLOutputter // lawsuit
*
* @return boolean
*/
+
function _hasEtag($etag, $if_none_match)
{
- return ($if_none_match) && in_array($etag, explode(',', $if_none_match));
+ $etags = explode(',', $if_none_match);
+ return in_array($etag, $etags) || in_array('*', $etags);
}
/**
* Boolean understands english (yes, no, true, false)
*
- * @param string $key query key we're interested in
+ * @param string $key query key we're interested in
* @param string $def default value
*
* @return boolean interprets yes/no strings as boolean
@@ -781,11 +882,12 @@ class Action extends HTMLOutputter // lawsuit
*
* @return nothing
*/
+
function serverError($msg, $code=500)
{
$action = $this->trimmed('action');
common_debug("Server error '$code' on '$action': $msg", __FILE__);
- common_server_error($msg, $code);
+ throw new ServerException($msg, $code);
}
/**
@@ -796,11 +898,12 @@ class Action extends HTMLOutputter // lawsuit
*
* @return nothing
*/
+
function clientError($msg, $code=400)
{
$action = $this->trimmed('action');
common_debug("User error '$code' on '$action': $msg", __FILE__);
- common_user_error($msg, $code);
+ throw new ClientException($msg, $code);
}
/**
@@ -873,17 +976,17 @@ class Action extends HTMLOutputter // lawsuit
}
if ($have_before) {
$pargs = array('page' => $page-1);
- $newargs = $args ? array_merge($args, $pargs) : $pargs;
$this->elementStart('li', array('class' => 'nav_prev'));
- $this->element('a', array('href' => common_local_url($action, $newargs), 'rel' => 'prev'),
+ $this->element('a', array('href' => common_local_url($action, $args, $pargs),
+ 'rel' => 'prev'),
_('After'));
$this->elementEnd('li');
}
if ($have_after) {
$pargs = array('page' => $page+1);
- $newargs = $args ? array_merge($args, $pargs) : $pargs;
$this->elementStart('li', array('class' => 'nav_next'));
- $this->element('a', array('href' => common_local_url($action, $newargs), 'rel' => 'next'),
+ $this->element('a', array('href' => common_local_url($action, $args, $pargs),
+ 'rel' => 'next'),
_('Before'));
$this->elementEnd('li');
}
@@ -894,4 +997,17 @@ class Action extends HTMLOutputter // lawsuit
$this->elementEnd('div');
}
}
+
+ /**
+ * An array of feeds for this action.
+ *
+ * Returns an array of potential feeds for this action.
+ *
+ * @return array Feed object to show in head and links
+ */
+
+ function getFeeds()
+ {
+ return null;
+ }
}
diff --git a/lib/channel.php b/lib/channel.php
new file mode 100644
index 000000000..f1e205546
--- /dev/null
+++ b/lib/channel.php
@@ -0,0 +1,237 @@
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('LACONICA')) { exit(1); }
+
+class Channel
+{
+ function on($user)
+ {
+ return false;
+ }
+
+ function off($user)
+ {
+ return false;
+ }
+
+ function output($user, $text)
+ {
+ return false;
+ }
+
+ function error($user, $text)
+ {
+ return false;
+ }
+
+ function source()
+ {
+ return null;
+ }
+}
+
+class XMPPChannel extends Channel
+{
+
+ var $conn = null;
+
+ function source()
+ {
+ return 'xmpp';
+ }
+
+ function __construct($conn)
+ {
+ $this->conn = $conn;
+ }
+
+ function on($user)
+ {
+ return $this->set_notify($user, 1);
+ }
+
+ function off($user)
+ {
+ return $this->set_notify($user, 0);
+ }
+
+ function output($user, $text)
+ {
+ $text = '['.common_config('site', 'name') . '] ' . $text;
+ jabber_send_message($user->jabber, $text);
+ }
+
+ function error($user, $text)
+ {
+ $text = '['.common_config('site', 'name') . '] ' . $text;
+ jabber_send_message($user->jabber, $text);
+ }
+
+ function set_notify(&$user, $notify)
+ {
+ $orig = clone($user);
+ $user->jabbernotify = $notify;
+ $result = $user->update($orig);
+ if (!$result) {
+ $last_error = &PEAR::getStaticProperty('DB_DataObject','lastError');
+ common_log(LOG_ERR,
+ 'Could not set notify flag to ' . $notify .
+ ' for user ' . common_log_objstring($user) .
+ ': ' . $last_error->message);
+ return false;
+ } else {
+ common_log(LOG_INFO,
+ 'User ' . $user->nickname . ' set notify flag to ' . $notify);
+ return true;
+ }
+ }
+}
+
+class WebChannel extends Channel
+{
+ var $out = null;
+
+ function __construct($out=null)
+ {
+ $this->out = $out;
+ }
+
+ function source()
+ {
+ return 'web';
+ }
+
+ function on($user)
+ {
+ return false;
+ }
+
+ function off($user)
+ {
+ return false;
+ }
+
+ function output($user, $text)
+ {
+ # XXX: buffer all output and send it at the end
+ # XXX: even better, redirect to appropriate page
+ # depending on what command was run
+ $this->out->startHTML();
+ $this->out->elementStart('head');
+ $this->out->element('title', null, _('Command results'));
+ $this->out->elementEnd('head');
+ $this->out->elementStart('body');
+ $this->out->element('p', array('id' => 'command_result'), $text);
+ $this->out->elementEnd('body');
+ $this->out->endHTML();
+ }
+
+ function error($user, $text)
+ {
+ common_user_error($text);
+ }
+}
+
+class AjaxWebChannel extends WebChannel
+{
+ function output($user, $text)
+ {
+ $this->out->startHTML('text/xml;charset=utf-8');
+ $this->out->elementStart('head');
+ $this->out->element('title', null, _('Command results'));
+ $this->out->elementEnd('head');
+ $this->out->elementStart('body');
+ $this->out->element('p', array('id' => 'command_result'), $text);
+ $this->out->elementEnd('body');
+ $this->out->endHTML();
+ }
+
+ function error($user, $text)
+ {
+ $this->out->startHTML('text/xml;charset=utf-8');
+ $this->out->elementStart('head');
+ $this->out->element('title', null, _('Ajax Error'));
+ $this->out->elementEnd('head');
+ $this->out->elementStart('body');
+ $this->out->element('p', array('id' => 'error'), $text);
+ $this->out->elementEnd('body');
+ $this->out->endHTML();
+ }
+}
+
+class MailChannel extends Channel
+{
+
+ var $addr = null;
+
+ function source()
+ {
+ return 'mail';
+ }
+
+ function __construct($addr=null)
+ {
+ $this->addr = $addr;
+ }
+
+ function on($user)
+ {
+ return $this->set_notify($user, 1);
+ }
+
+ function off($user)
+ {
+ return $this->set_notify($user, 0);
+ }
+
+ function output($user, $text)
+ {
+
+ $headers['From'] = $user->incomingemail;
+ $headers['To'] = $this->addr;
+
+ $headers['Subject'] = _('Command complete');
+
+ return mail_send(array($this->addr), $headers, $text);
+ }
+
+ function error($user, $text)
+ {
+
+ $headers['From'] = $user->incomingemail;
+ $headers['To'] = $this->addr;
+
+ $headers['Subject'] = _('Command failed');
+
+ return mail_send(array($this->addr), $headers, $text);
+ }
+
+ function set_notify($user, $value)
+ {
+ $orig = clone($user);
+ $user->smsnotify = $value;
+ $result = $user->update($orig);
+ if (!$result) {
+ common_log_db_error($user, 'UPDATE', __FILE__);
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/lib/clienterroraction.php b/lib/clienterroraction.php
index ef6fd51df..0c48414d5 100644
--- a/lib/clienterroraction.php
+++ b/lib/clienterroraction.php
@@ -49,7 +49,7 @@ class ClientErrorAction extends ErrorAction
function __construct($message='Error', $code=400)
{
parent::__construct($message, $code);
-
+
$this->status = array(400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
@@ -70,9 +70,9 @@ class ClientErrorAction extends ErrorAction
417 => 'Expectation Failed');
$this->default = 400;
}
-
+
// XXX: Should these error actions even be invokable via URI?
-
+
function handle($args)
{
parent::handle($args);
@@ -84,11 +84,16 @@ class ClientErrorAction extends ErrorAction
}
$this->message = $this->trimmed('message');
-
+
if (!$this->message) {
- $this->message = "Client Error $this->code";
- }
+ $this->message = "Client Error $this->code";
+ }
$this->showPage();
}
+
+ function title()
+ {
+ return $this->status[$this->code];
+ }
}
diff --git a/lib/clientexception.php b/lib/clientexception.php
new file mode 100644
index 000000000..3020d7f50
--- /dev/null
+++ b/lib/clientexception.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * class for a client exception (user error)
+ *
+ * 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 Exception
+ * @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);
+}
+
+/**
+ * Class for client exceptions
+ *
+ * Subclass of PHP Exception for user errors.
+ *
+ * @category Exception
+ * @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 ClientException extends Exception
+{
+ public function __construct($message = null, $code = 400) {
+ parent::__construct($message, $code);
+ }
+
+ // custom string representation of object
+ public function __toString() {
+ return __CLASS__ . ": [{$this->code}]: {$this->message}\n";
+ }
+}
diff --git a/lib/command.php b/lib/command.php
new file mode 100644
index 000000000..507990a0b
--- /dev/null
+++ b/lib/command.php
@@ -0,0 +1,419 @@
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('LACONICA')) { exit(1); }
+
+require_once(INSTALLDIR.'/lib/channel.php');
+
+class Command
+{
+
+ var $user = null;
+
+ function __construct($user=null)
+ {
+ $this->user = $user;
+ }
+
+ function execute($channel)
+ {
+ return false;
+ }
+}
+
+class UnimplementedCommand extends Command
+{
+ function execute($channel)
+ {
+ $channel->error($this->user, _("Sorry, this command is not yet implemented."));
+ }
+}
+
+class TrackingCommand extends UnimplementedCommand
+{
+}
+
+class TrackOffCommand extends UnimplementedCommand
+{
+}
+
+class TrackCommand extends UnimplementedCommand
+{
+ var $word = null;
+ function __construct($user, $word)
+ {
+ parent::__construct($user);
+ $this->word = $word;
+ }
+}
+
+class UntrackCommand extends UnimplementedCommand
+{
+ var $word = null;
+ function __construct($user, $word)
+ {
+ parent::__construct($user);
+ $this->word = $word;
+ }
+}
+
+class NudgeCommand extends UnimplementedCommand
+{
+ var $other = null;
+ function __construct($user, $other)
+ {
+ parent::__construct($user);
+ $this->other = $other;
+ }
+}
+
+class InviteCommand extends UnimplementedCommand
+{
+ var $other = null;
+ function __construct($user, $other)
+ {
+ parent::__construct($user);
+ $this->other = $other;
+ }
+}
+
+class StatsCommand extends Command
+{
+ function execute($channel)
+ {
+
+ $subs = new Subscription();
+ $subs->subscriber = $this->user->id;
+ $subs_count = (int) $subs->count() - 1;
+
+ $subbed = new Subscription();
+ $subbed->subscribed = $this->user->id;
+ $subbed_count = (int) $subbed->count() - 1;
+
+ $notices = new Notice();
+ $notices->profile_id = $this->user->id;
+ $notice_count = (int) $notices->count();
+
+ $channel->output($this->user, sprintf(_("Subscriptions: %1\$s\n".
+ "Subscribers: %2\$s\n".
+ "Notices: %3\$s"),
+ $subs_count,
+ $subbed_count,
+ $notice_count));
+ }
+}
+
+class FavCommand extends Command
+{
+
+ var $other = null;
+
+ function __construct($user, $other)
+ {
+ parent::__construct($user);
+ $this->other = $other;
+ }
+
+ function execute($channel)
+ {
+
+ $recipient =
+ common_relative_profile($this->user, common_canonical_nickname($this->other));
+
+ if (!$recipient) {
+ $channel->error($this->user, _('No such user.'));
+ return;
+ }
+ $notice = $recipient->getCurrentNotice();
+ if (!$notice) {
+ $channel->error($this->user, _('User has no last notice'));
+ return;
+ }
+
+ $fave = Fave::addNew($this->user, $notice);
+
+ if (!$fave) {
+ $channel->error($this->user, _('Could not create favorite.'));
+ return;
+ }
+
+ $other = User::staticGet('id', $recipient->id);
+
+ if ($other && $other->id != $user->id) {
+ if ($other->email && $other->emailnotifyfav) {
+ mail_notify_fave($other, $this->user, $notice);
+ }
+ }
+
+ $this->user->blowFavesCache();
+
+ $channel->output($this->user, _('Notice marked as fave.'));
+ }
+}
+
+class WhoisCommand extends Command
+{
+ var $other = null;
+ function __construct($user, $other)
+ {
+ parent::__construct($user);
+ $this->other = $other;
+ }
+
+ function execute($channel)
+ {
+ $recipient =
+ common_relative_profile($this->user, common_canonical_nickname($this->other));
+
+ if (!$recipient) {
+ $channel->error($this->user, _('No such user.'));
+ return;
+ }
+
+ $whois = sprintf(_("%1\$s (%2\$s)"), $recipient->nickname,
+ $recipient->profileurl);
+ if ($recipient->fullname) {
+ $whois .= "\n" . sprintf(_('Fullname: %s'), $recipient->fullname);
+ }
+ if ($recipient->location) {
+ $whois .= "\n" . sprintf(_('Location: %s'), $recipient->location);
+ }
+ if ($recipient->homepage) {
+ $whois .= "\n" . sprintf(_('Homepage: %s'), $recipient->homepage);
+ }
+ if ($recipient->bio) {
+ $whois .= "\n" . sprintf(_('About: %s'), $recipient->bio);
+ }
+ $channel->output($this->user, $whois);
+ }
+}
+
+class MessageCommand extends Command
+{
+ var $other = null;
+ var $text = null;
+ function __construct($user, $other, $text)
+ {
+ parent::__construct($user);
+ $this->other = $other;
+ $this->text = $text;
+ }
+
+ function execute($channel)
+ {
+ $other = User::staticGet('nickname', common_canonical_nickname($this->other));
+ $len = mb_strlen($this->text);
+ if ($len == 0) {
+ $channel->error($this->user, _('No content!'));
+ return;
+ } else if ($len > 140) {
+ $content = common_shorten_links($content);
+ if (mb_strlen($content) > 140) {
+ $channel->error($this->user, sprintf(_('Message too long - maximum is 140 characters, you sent %d'), $len));
+ return;
+ }
+ }
+
+ if (!$other) {
+ $channel->error($this->user, _('No such user.'));
+ return;
+ } else if (!$this->user->mutuallySubscribed($other)) {
+ $channel->error($this->user, _('You can\'t send a message to this user.'));
+ return;
+ } else if ($this->user->id == $other->id) {
+ $channel->error($this->user, _('Don\'t send a message to yourself; just say it to yourself quietly instead.'));
+ return;
+ }
+ $message = Message::saveNew($this->user->id, $other->id, $this->text, $channel->source());
+ if ($message) {
+ $channel->output($this->user, sprintf(_('Direct message to %s sent'), $this->other));
+ } else {
+ $channel->error($this->user, _('Error sending direct message.'));
+ }
+ }
+}
+
+class GetCommand extends Command
+{
+
+ var $other = null;
+
+ function __construct($user, $other)
+ {
+ parent::__construct($user);
+ $this->other = $other;
+ }
+
+ function execute($channel)
+ {
+ $target_nickname = common_canonical_nickname($this->other);
+
+ $target =
+ common_relative_profile($this->user, $target_nickname);
+
+ if (!$target) {
+ $channel->error($this->user, _('No such user.'));
+ return;
+ }
+ $notice = $target->getCurrentNotice();
+ if (!$notice) {
+ $channel->error($this->user, _('User has no last notice'));
+ return;
+ }
+ $notice_content = $notice->content;
+
+ $channel->output($this->user, $target_nickname . ": " . $notice_content);
+ }
+}
+
+class SubCommand extends Command
+{
+
+ var $other = null;
+
+ function __construct($user, $other)
+ {
+ parent::__construct($user);
+ $this->other = $other;
+ }
+
+ function execute($channel)
+ {
+
+ if (!$this->other) {
+ $channel->error($this->user, _('Specify the name of the user to subscribe to'));
+ return;
+ }
+
+ $result = subs_subscribe_user($this->user, $this->other);
+
+ if ($result == 'true') {
+ $channel->output($this->user, sprintf(_('Subscribed to %s'), $this->other));
+ } else {
+ $channel->error($this->user, $result);
+ }
+ }
+}
+
+class UnsubCommand extends Command
+{
+
+ var $other = null;
+
+ function __construct($user, $other)
+ {
+ parent::__construct($user);
+ $this->other = $other;
+ }
+
+ function execute($channel)
+ {
+ if(!$this->other) {
+ $channel->error($this->user, _('Specify the name of the user to unsubscribe from'));
+ return;
+ }
+
+ $result=subs_unsubscribe_user($this->user, $this->other);
+
+ if ($result) {
+ $channel->output($this->user, sprintf(_('Unsubscribed from %s'), $this->other));
+ } else {
+ $channel->error($this->user, $result);
+ }
+ }
+}
+
+class OffCommand extends Command
+{
+ var $other = null;
+ function __construct($user, $other=null)
+ {
+ parent::__construct($user);
+ $this->other = $other;
+ }
+ function execute($channel)
+ {
+ if ($other) {
+ $channel->error($this->user, _("Command not yet implemented."));
+ } else {
+ if ($channel->off($this->user)) {
+ $channel->output($this->user, _('Notification off.'));
+ } else {
+ $channel->error($this->user, _('Can\'t turn off notification.'));
+ }
+ }
+ }
+}
+
+class OnCommand extends Command
+{
+ var $other = null;
+ function __construct($user, $other=null)
+ {
+ parent::__construct($user);
+ $this->other = $other;
+ }
+
+ function execute($channel)
+ {
+ if ($other) {
+ $channel->error($this->user, _("Command not yet implemented."));
+ } else {
+ if ($channel->on($this->user)) {
+ $channel->output($this->user, _('Notification on.'));
+ } else {
+ $channel->error($this->user, _('Can\'t turn on notification.'));
+ }
+ }
+ }
+}
+
+class HelpCommand extends Command
+{
+ function execute($channel)
+ {
+ $channel->output($this->user,
+ _("Commands:\n".
+ "on - turn on notifications\n".
+ "off - turn off notifications\n".
+ "help - show this help\n".
+ "follow <nickname> - subscribe to user\n".
+ "leave <nickname> - unsubscribe from user\n".
+ "d <nickname> <text> - direct message to user\n".
+ "get <nickname> - get last notice from user\n".
+ "whois <nickname> - get profile info on user\n".
+ "fav <nickname> - add user's last notice as a 'fave'\n".
+ "stats - get your stats\n".
+ "stop - same as 'off'\n".
+ "quit - same as 'off'\n".
+ "sub <nickname> - same as 'follow'\n".
+ "unsub <nickname> - same as 'leave'\n".
+ "last <nickname> - same as 'get'\n".
+ "on <nickname> - not yet implemented.\n".
+ "off <nickname> - not yet implemented.\n".
+ "nudge <nickname> - not yet implemented.\n".
+ "invite <phone number> - not yet implemented.\n".
+ "track <word> - not yet implemented.\n".
+ "untrack <word> - not yet implemented.\n".
+ "track off - not yet implemented.\n".
+ "untrack all - not yet implemented.\n".
+ "tracks - not yet implemented.\n".
+ "tracking - not yet implemented.\n"));
+ }
+}
diff --git a/lib/commandinterpreter.php b/lib/commandinterpreter.php
new file mode 100644
index 000000000..49c733c03
--- /dev/null
+++ b/lib/commandinterpreter.php
@@ -0,0 +1,197 @@
+<?php
+/*
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('LACONICA')) { exit(1); }
+
+require_once INSTALLDIR.'/lib/command.php';
+
+class CommandInterpreter
+{
+ function handle_command($user, $text)
+ {
+ # XXX: localise
+
+ $text = preg_replace('/\s+/', ' ', trim($text));
+ list($cmd, $arg) = explode(' ', $text, 2);
+
+ # We try to support all the same commands as Twitter, see
+ # http://getsatisfaction.com/twitter/topics/what_are_the_twitter_commands
+ # There are a few compatibility commands from earlier versions of
+ # Laconica
+
+ switch(strtolower($cmd)) {
+ case 'help':
+ if ($arg) {
+ return null;
+ }
+ return new HelpCommand($user);
+ case 'on':
+ if ($arg) {
+ list($other, $extra) = explode(' ', $arg, 2);
+ if ($extra) {
+ return null;
+ } else {
+ return new OnCommand($user, $other);
+ }
+ } else {
+ return new OnCommand($user);
+ }
+ case 'off':
+ if ($arg) {
+ list($other, $extra) = explode(' ', $arg, 2);
+ if ($extra) {
+ return null;
+ } else {
+ return new OffCommand($user, $other);
+ }
+ } else {
+ return new OffCommand($user);
+ }
+ case 'stop':
+ case 'quit':
+ if ($arg) {
+ return null;
+ } else {
+ return new OffCommand($user);
+ }
+ case 'follow':
+ case 'sub':
+ if (!$arg) {
+ return null;
+ }
+ list($other, $extra) = explode(' ', $arg, 2);
+ if ($extra) {
+ return null;
+ } else {
+ return new SubCommand($user, $other);
+ }
+ case 'leave':
+ case 'unsub':
+ if (!$arg) {
+ return null;
+ }
+ list($other, $extra) = explode(' ', $arg, 2);
+ if ($extra) {
+ return null;
+ } else {
+ return new UnsubCommand($user, $other);
+ }
+ case 'get':
+ case 'last':
+ if (!$arg) {
+ return null;
+ }
+ list($other, $extra) = explode(' ', $arg, 2);
+ if ($extra) {
+ return null;
+ } else {
+ return new GetCommand($user, $other);
+ }
+ case 'd':
+ case 'dm':
+ if (!$arg) {
+ return null;
+ }
+ list($other, $extra) = explode(' ', $arg, 2);
+ if (!$extra) {
+ return null;
+ } else {
+ return new MessageCommand($user, $other, $extra);
+ }
+ case 'whois':
+ if (!$arg) {
+ return null;
+ }
+ list($other, $extra) = explode(' ', $arg, 2);
+ if ($extra) {
+ return null;
+ } else {
+ return new WhoisCommand($user, $other);
+ }
+ case 'fav':
+ if (!$arg) {
+ return null;
+ }
+ list($other, $extra) = explode(' ', $arg, 2);
+ if ($extra) {
+ return null;
+ } else {
+ return new FavCommand($user, $other);
+ }
+ case 'nudge':
+ if (!$arg) {
+ return null;
+ }
+ list($other, $extra) = explode(' ', $arg, 2);
+ if ($extra) {
+ return null;
+ } else {
+ return new NudgeCommand($user, $other);
+ }
+ case 'stats':
+ if ($arg) {
+ return null;
+ }
+ return new StatsCommand($user);
+ case 'invite':
+ if (!$arg) {
+ return null;
+ }
+ list($other, $extra) = explode(' ', $arg, 2);
+ if ($extra) {
+ return null;
+ } else {
+ return new InviteCommand($user, $other);
+ }
+ case 'track':
+ if (!$arg) {
+ return null;
+ }
+ list($word, $extra) = explode(' ', $arg, 2);
+ if ($extra) {
+ return null;
+ } else if ($word == 'off') {
+ return new TrackOffCommand($user);
+ } else {
+ return new TrackCommand($user, $word);
+ }
+ case 'untrack':
+ if (!$arg) {
+ return null;
+ }
+ list($word, $extra) = explode(' ', $arg, 2);
+ if ($extra) {
+ return null;
+ } else if ($word == 'all') {
+ return new TrackOffCommand($user);
+ } else {
+ return new UntrackCommand($user, $word);
+ }
+ case 'tracks':
+ case 'tracking':
+ if ($arg) {
+ return null;
+ }
+ return new TrackingCommand($user);
+ default:
+ return false;
+ }
+ }
+}
+
diff --git a/lib/common.php b/lib/common.php
index cc82f8bd7..0355d01e3 100644
--- a/lib/common.php
+++ b/lib/common.php
@@ -19,12 +19,11 @@
if (!defined('LACONICA')) { exit(1); }
-define('LACONICA_VERSION', '0.7.0');
+define('LACONICA_VERSION', '0.7.1');
define('AVATAR_PROFILE_SIZE', 96);
define('AVATAR_STREAM_SIZE', 48);
define('AVATAR_MINI_SIZE', 24);
-define('MAX_AVATAR_SIZE', 256 * 1024);
define('NOTICES_PER_PAGE', 20);
define('PROFILES_PER_PAGE', 20);
@@ -50,15 +49,31 @@ require_once('DB/DataObject/Cast.php'); # for dates
require_once(INSTALLDIR.'/lib/language.php');
+// This gets included before the config file, so that admin code and plugins
+// can use it
+
+require_once(INSTALLDIR.'/lib/event.php');
+require_once(INSTALLDIR.'/lib/plugin.php');
+
+// try to figure out where we are
+
+$_server = array_key_exists('SERVER_NAME', $_SERVER) ?
+ strtolower($_SERVER['SERVER_NAME']) :
+ null;
+$_path = array_key_exists('SCRIPT_NAME', $_SERVER) ?
+ substr($_SERVER['SCRIPT_NAME'], 1, strrpos($_SERVER['SCRIPT_NAME'], '/') - 1) :
+ null;
+
// default configuration, overwritten in config.php
$config =
array('site' =>
array('name' => 'Just another Laconica microblog',
- 'server' => 'localhost',
+ 'server' => $_server,
'theme' => 'default',
- 'path' => '/',
+ 'path' => $_path,
'logfile' => null,
+ 'logdebug' => false,
'fancy' => false,
'locale_path' => INSTALLDIR.'/locale',
'language' => 'en_US',
@@ -92,7 +107,8 @@ $config =
array('server' => null),
'public' =>
array('localonly' => true,
- 'blacklist' => array()),
+ 'blacklist' => array(),
+ 'autosource' => array()),
'theme' =>
array('server' => null),
'throttle' =>
@@ -152,7 +168,45 @@ if (function_exists('date_default_timezone_set')) {
date_default_timezone_set('UTC');
}
-require_once(INSTALLDIR.'/config.php');
+// From most general to most specific:
+// server-wide, then vhost-wide, then for a path,
+// finally for a dir (usually only need one of the last two).
+
+$_config_files = array('/etc/laconica/laconica.php',
+ '/etc/laconica/'.$_server.'.php');
+
+if (strlen($_path) > 0) {
+ $_config_files[] = '/etc/laconica/'.$_server.'_'.$_path.'.php';
+}
+
+$_config_files[] = INSTALLDIR.'/config.php';
+
+$_have_a_config = false;
+
+foreach ($_config_files as $_config_file) {
+ if (file_exists($_config_file)) {
+ include_once($_config_file);
+ $_have_a_config = true;
+ }
+}
+
+function _have_config()
+{
+ global $_have_a_config;
+ return $_have_a_config;
+}
+
+// XXX: Throw a conniption if database not installed
+
+// Fixup for laconica.ini
+
+$_db_name = substr($config['db']['database'], strrpos($config['db']['database'], '/') + 1);
+
+if ($_db_name != 'laconica' && !array_key_exists('ini_'.$_db_name, $config['db'])) {
+ $config['db']['ini_'.$_db_name] = INSTALLDIR.'/classes/laconica.ini';
+}
+
+// XXX: how many of these could be auto-loaded on use?
require_once('Validate.php');
require_once('markdown.php');
@@ -165,6 +219,9 @@ require_once(INSTALLDIR.'/lib/subs.php');
require_once(INSTALLDIR.'/lib/Shorturl_api.php');
require_once(INSTALLDIR.'/lib/twitter.php');
+require_once(INSTALLDIR.'/lib/clientexception.php');
+require_once(INSTALLDIR.'/lib/serverexception.php');
+
// XXX: other formats here
define('NICKNAME_FMT', VALIDATE_NUM.VALIDATE_ALPHA_LOWER);
@@ -177,5 +234,12 @@ function __autoload($class)
require_once(INSTALLDIR.'/classes/' . $class . '.php');
} else if (file_exists(INSTALLDIR.'/lib/' . strtolower($class) . '.php')) {
require_once(INSTALLDIR.'/lib/' . strtolower($class) . '.php');
+ } else if (mb_substr($class, -6) == 'Action' &&
+ file_exists(INSTALLDIR.'/actions/' . strtolower(mb_substr($class, 0, -6)) . '.php')) {
+ require_once(INSTALLDIR.'/actions/' . strtolower(mb_substr($class, 0, -6)) . '.php');
}
}
+
+// Give plugins a chance to initialize in a fully-prepared environment
+
+Event::handle('InitializePlugin');
diff --git a/lib/dberroraction.php b/lib/dberroraction.php
new file mode 100644
index 000000000..0dc92490c
--- /dev/null
+++ b/lib/dberroraction.php
@@ -0,0 +1,73 @@
+<?php
+/**
+ * DB error action.
+ *
+ * PHP version 5
+ *
+ * @category Action
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @author Zach Copley <zach@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://laconi.ca/
+ *
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR.'/lib/servererroraction.php';
+
+/**
+ * Class for displaying DB Errors
+ *
+ * This only occurs if there's been a DB_DataObject_Error that's
+ * reported through PEAR, so we try to avoid doing anything that connects
+ * to the DB, so we don't trigger it again.
+ *
+ * @category Action
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://laconi.ca/
+ */
+
+class DBErrorAction extends ServerErrorAction
+{
+ function __construct($message='Error', $code=500)
+ {
+ parent::__construct($message, $code);
+ }
+
+ function title()
+ {
+ return _('Database error');
+ }
+
+ function getLanguage()
+ {
+ // Don't try to figure out user's language; just show the page
+ return common_config('site', 'language');
+ }
+
+ function showPrimaryNav()
+ {
+ // don't show primary nav
+ }
+}
diff --git a/lib/error.php b/lib/error.php
index 9842053d8..526d9f81b 100644
--- a/lib/error.php
+++ b/lib/error.php
@@ -111,7 +111,7 @@ class ErrorAction extends Action
function showBody()
{
$this->elementStart('body', array('id' => 'error'));
- $this->elementStart('div', 'wrap');
+ $this->elementStart('div', array('id' => 'wrap'));
$this->showHeader();
$this->showCore();
$this->showFooter();
@@ -130,6 +130,7 @@ class ErrorAction extends Action
{
$this->elementStart('div', array('id' => 'header'));
$this->showLogo();
+ $this->showPrimaryNav();
$this->elementEnd('div');
}
diff --git a/lib/event.php b/lib/event.php
new file mode 100644
index 000000000..d815ae54b
--- /dev/null
+++ b/lib/event.php
@@ -0,0 +1,113 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * utilities for defining and running event handlers
+ *
+ * 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 Event
+ * @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);
+}
+
+/**
+ * Class for events
+ *
+ * This "class" two static functions for managing events in the Laconica code.
+ *
+ * @category Event
+ * @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/
+ *
+ * @todo Define a system for using Event instances
+ */
+
+class Event {
+
+ /* Global array of hooks, mapping eventname => array of callables */
+
+ protected static $_handlers = array();
+
+ /**
+ * Add an event handler
+ *
+ * To run some code at a particular point in Laconica processing.
+ * Named events include receiving an XMPP message, adding a new notice,
+ * or showing part of an HTML page.
+ *
+ * The arguments to the handler vary by the event. Handlers can return
+ * two possible values: false means that the event has been replaced by
+ * the handler completely, and no default processing should be done.
+ * Non-false means successful handling, and that the default processing
+ * should succeed. (Note that this only makes sense for some events.)
+ *
+ * Handlers can also abort processing by throwing an exception; these will
+ * be caught by the closest code and displayed as errors.
+ *
+ * @param string $name Name of the event
+ * @param callable $handler Code to run
+ *
+ * @return void
+ */
+
+ public static function addHandler($name, $handler) {
+ if (array_key_exists($name, Event::$_handlers)) {
+ Event::$_handlers[$name][] = $handler;
+ } else {
+ Event::$_handlers[$name] = array($handler);
+ }
+ }
+
+ /**
+ * Handle an event
+ *
+ * Events are any point in the code that we want to expose for admins
+ * or third-party developers to use.
+ *
+ * We pass in an array of arguments (including references, for stuff
+ * that can be changed), and each assigned handler gets run with those
+ * arguments. Exceptions can be thrown to indicate an error.
+ *
+ * @param string $name Name of the event that's happening
+ * @param array $args Arguments for handlers
+ *
+ * @return boolean flag saying whether to continue processing, based
+ * on results of handlers.
+ */
+
+ public static function handle($name, $args=array()) {
+ $result = null;
+ if (array_key_exists($name, Event::$_handlers)) {
+ foreach (Event::$_handlers[$name] as $handler) {
+ $result = call_user_func_array($handler, $args);
+ if ($result === false) {
+ break;
+ }
+ }
+ }
+ return ($result !== false);
+ }
+}
diff --git a/lib/facebookaction.php b/lib/facebookaction.php
index c781b86f4..043a078cd 100644
--- a/lib/facebookaction.php
+++ b/lib/facebookaction.php
@@ -122,15 +122,14 @@ class FacebookAction extends Action
// Add a timestamp to the file so Facebook cache wont ignore our changes
$ts = filemtime(INSTALLDIR.'/js/facebookapp.js');
- $this->element('script', array('type' => 'text/javascript',
- 'src' => common_path('js/facebookapp.js') . '?ts=' . $ts));
+ $this->element('script', array('src' => common_path('js/facebookapp.js') . '?ts=' . $ts));
}
/**
* Start an Facebook ready HTML document
*
* For Facebook we don't want to actually output any headers,
- * DTD info, etc.
+ * DTD info, etc. Just Stylesheet and JavaScript links.
*
* If $type isn't specified, will attempt to do content negotiation.
*
@@ -141,6 +140,9 @@ class FacebookAction extends Action
function startHTML($type=null)
{
+ $this->showStylesheets();
+ $this->showScripts();
+
$this->elementStart('div', array('class' => 'facebook-page'));
}
@@ -169,7 +171,7 @@ class FacebookAction extends Action
function showBody()
{
- $this->elementStart('div', 'wrap');
+ $this->elementStart('div', array('id' => 'wrap'));
$this->showHeader();
$this->showCore();
$this->showFooter();
@@ -182,8 +184,6 @@ class FacebookAction extends Action
function showHead($error, $success)
{
- $this->showStylesheets();
- $this->showScripts();
if ($error) {
$this->element("h1", null, $error);
@@ -205,7 +205,6 @@ class FacebookAction extends Action
// Make this into a widget later
function showLocalNav()
{
-
$this->elementStart('ul', array('class' => 'nav'));
$this->elementStart('li', array('class' =>
@@ -230,18 +229,7 @@ class FacebookAction extends Action
$this->elementEnd('li');
$this->elementEnd('ul');
-
}
-
- /**
- * Show primary navigation.
- *
- * @return nothing
- */
- function showPrimaryNav()
- {
- // we don't want to show anything for this
- }
/**
* Show header of the page.
@@ -255,7 +243,6 @@ class FacebookAction extends Action
$this->elementStart('div', array('id' => 'header'));
$this->showLogo();
$this->showNoticeForm();
- $this->showPrimaryNav();
$this->elementEnd('div');
}
@@ -276,12 +263,13 @@ class FacebookAction extends Action
function showInstructions()
{
+ $this->elementStart('div', array('class' => 'facebook_guide'));
+
$this->elementStart('dl', array('class' => 'system_notice'));
$this->element('dt', null, 'Page Notice');
$loginmsg_part1 = _('To use the %s Facebook Application you need to login ' .
'with your username and password. Don\'t have a username yet? ');
-
$loginmsg_part2 = _(' a new account.');
$this->elementStart('dd');
@@ -290,15 +278,18 @@ class FacebookAction extends Action
$this->element('a',
array('href' => common_local_url('register')), _('Register'));
$this->text($loginmsg_part2);
+ $this->elementEnd('p');
$this->elementEnd('dd');
+
$this->elementEnd('dl');
+ $this->elementEnd('div');
}
function showLoginForm($msg = null)
{
- $this->elementStart('div', array('class' => 'content'));
+ $this->elementStart('div', array('id' => 'content'));
$this->element('h1', null, _('Login'));
if ($msg) {
@@ -315,7 +306,6 @@ class FacebookAction extends Action
'action' => 'index.php'));
$this->elementStart('fieldset');
- $this->element('legend', null, _('Login to site'));
$this->elementStart('ul', array('class' => 'form_datas'));
$this->elementStart('li');
@@ -327,6 +317,7 @@ class FacebookAction extends Action
$this->elementEnd('ul');
$this->submit('submit', _('Login'));
+ $this->elementEnd('fieldset');
$this->elementEnd('form');
$this->elementStart('p');
@@ -335,6 +326,7 @@ class FacebookAction extends Action
$this->elementEnd('p');
$this->elementEnd('div');
+ $this->elementEnd('div');
}
@@ -344,42 +336,70 @@ class FacebookAction extends Action
// Need to include inline CSS for styling the Profile box
+ $app_props = $this->facebook->api_client->Admin_getAppProperties(array('icon_url'));
+ $icon_url = $app_props['icon_url'];
+
$style = '<style>
+ .entry-title *,
+ .entry-content * {
+ font-size:14px;
+ font-family:"Lucida Sans Unicode", "Lucida Grande", sans-serif;
+ }
+ .entry-title a,
+ .entry-content a {
+ color:#002E6E;
+ }
+
.entry-title .vcard .photo {
float:left;
display:inline;
+ margin-right:11px;
+ margin-bottom:11px
}
- .entry-title .vcard .nickname {
- margin-left:5px;
- }
-
+ .entry-title {
+ margin-bottom:11px;
+ }
.entry-title p.entry-content {
display:inline;
- margin-left:5px;
+ margin-left:5px;
}
+ div.entry-content {
+ clear:both;
+ }
div.entry-content dl,
div.entry-content dt,
div.entry-content dd {
display:inline;
+ text-transform:lowercase;
}
- div.entry-content dt,
- div.entry-content dd {
- display:inline;
- margin-left:5px;
+ div.entry-content dd,
+ div.entry-content .device dt {
+ margin-left:0;
+ margin-right:5px;
}
- div.entry-content dl.timestamp dt {
+ div.entry-content dl.timestamp dt,
+ div.entry-content dl.response dt {
display:none;
}
div.entry-content dd a {
display:inline-block;
}
+
+ #facebook_laconica_app {
+ text-indent:-9999px;
+ height:16px;
+ width:16px;
+ display:block;
+ background:url('.$icon_url.') no-repeat 0 0;
+ float:right;
+ }
</style>';
$this->xw->openMemory();
- $item = new FacebookNoticeListItem($notice, $this);
+ $item = new FacebookProfileBoxNotice($notice, $this);
$item->show();
$fbml = "<fb:wide>$style " . $this->xw->outputMemory(false) . "</fb:wide>";
@@ -438,6 +458,64 @@ class FacebookAction extends Action
}
}
+ function updateFacebookStatus($notice)
+ {
+ $prefix = $this->facebook->api_client->data_getUserPreference(FACEBOOK_NOTICE_PREFIX, $this->fbuid);
+ $content = "$prefix $notice->content";
+
+ if ($this->facebook->api_client->users_hasAppPermission('status_update', $this->fbuid)) {
+ $this->facebook->api_client->users_setStatus($content, $this->fbuid, false, true);
+ }
+ }
+
+ function saveNewNotice()
+ {
+
+ $user = $this->flink->getUser();
+
+ $content = $this->trimmed('status_textarea');
+
+ if (!$content) {
+ $this->showPage(_('No notice content!'));
+ return;
+ } else {
+ $content_shortened = common_shorten_links($content);
+
+ if (mb_strlen($content_shortened) > 140) {
+ $this->showPage(_('That\'s too long. Max notice size is 140 chars.'));
+ return;
+ }
+ }
+
+ $inter = new CommandInterpreter();
+
+ $cmd = $inter->handle_command($user, $content_shortened);
+
+ if ($cmd) {
+
+ // XXX fix this
+
+ $cmd->execute(new WebChannel());
+ return;
+ }
+
+ $replyto = $this->trimmed('inreplyto');
+
+ $notice = Notice::saveNew($user->id, $content,
+ 'Facebook', 1, ($replyto == 'false') ? null : $replyto);
+
+ if (is_string($notice)) {
+ $this->showPage($notice);
+ return;
+ }
+
+ common_broadcast_notice($notice);
+
+ // Also update the user's Facebook status
+ $this->updateFacebookStatus($notice);
+ $this->updateProfileBox($notice);
+
+ }
}
@@ -476,6 +554,18 @@ class FacebookNoticeForm extends NoticeForm
class FacebookNoticeList extends NoticeList
{
+
+ /**
+ * constructor
+ *
+ * @param Notice $notice stream of notices from DB_DataObject
+ */
+
+ function __construct($notice, $out=null)
+ {
+ parent::__construct($notice, $out);
+ }
+
/**
* show the list of notices
*
@@ -530,6 +620,20 @@ class FacebookNoticeList extends NoticeList
class FacebookNoticeListItem extends NoticeListItem
{
+
+ /**
+ * constructor
+ *
+ * Also initializes the profile attribute.
+ *
+ * @param Notice $notice The notice we'll display
+ */
+
+ function __construct($notice, $out=null)
+ {
+ parent::__construct($notice, $out);
+ }
+
/**
* recipe function for displaying a single notice in the Facebook App.
*
@@ -582,3 +686,65 @@ class FacebookNoticeListItem extends NoticeListItem
}
}
+
+
+class FacebookProfileBoxNotice extends FacebookNoticeListItem
+{
+
+ /**
+ * constructor
+ *
+ * Also initializes the profile attribute.
+ *
+ * @param Notice $notice The notice we'll display
+ */
+
+ function __construct($notice, $out=null)
+ {
+ parent::__construct($notice, $out);
+ }
+
+ /**
+ * Recipe function for displaying a single notice in the
+ * Facebook App's Profile
+ *
+ * @return void
+ */
+
+ function show()
+ {
+
+ $this->out->elementStart('div', 'entry-title');
+ $this->showAuthor();
+ $this->showContent();
+ $this->out->elementEnd('div');
+
+ $this->out->elementStart('div', 'entry-content');
+
+ $this->showNoticeLink();
+ $this->showNoticeSource();
+ $this->showReplyTo();
+ $this->out->elementEnd('div');
+
+ $this->showAppLink();
+
+ }
+
+ function showAppLink()
+ {
+
+ $this->facebook = getFacebook();
+
+ $app_props = $this->facebook->api_client->Admin_getAppProperties(
+ array('canvas_name', 'application_name'));
+
+ $this->app_uri = 'http://apps.facebook.com/' . $app_props['canvas_name'];
+ $this->app_name = $app_props['application_name'];
+
+ $this->out->elementStart('a', array('id' => 'facebook_laconica_app',
+ 'href' => $this->app_uri));
+ $this->out->text($this->app_name);
+ $this->out->elementEnd('a');
+ }
+
+}
diff --git a/lib/facebookutil.php b/lib/facebookutil.php
index 8454590d6..ec3987273 100644
--- a/lib/facebookutil.php
+++ b/lib/facebookutil.php
@@ -25,20 +25,6 @@ define("FACEBOOK_SERVICE", 2); // Facebook is foreign_service ID 2
define("FACEBOOK_NOTICE_PREFIX", 1);
define("FACEBOOK_PROMPTED_UPDATE_PREF", 2);
-// Gets all the notices from users with a Facebook link since a given ID
-function getFacebookNotices($since)
-{
- $qry = 'SELECT notice.* ' .
- 'FROM notice ' .
- 'JOIN foreign_link ' .
- 'WHERE notice.profile_id = foreign_link.user_id ' .
- 'AND foreign_link.service = 2 ' .
- 'ORDER BY notice.created DESC';
-
- // XXX: What should the limit be?
- return Notice::getStreamDirect($qry, 0, 100, 0, 0, null, $since);
-}
-
function getFacebook()
{
$apikey = common_config('facebook', 'apikey');
@@ -51,3 +37,97 @@ function updateProfileBox($facebook, $flink, $notice) {
$fbaction->updateProfileBox($notice);
}
+function isFacebookBound($notice, $flink) {
+
+ // If the user does not want to broadcast to Facebook, move along
+ if (!($flink->noticesync & FOREIGN_NOTICE_SEND == FOREIGN_NOTICE_SEND)) {
+ common_log(LOG_INFO, "Skipping notice $notice->id " .
+ 'because user has FOREIGN_NOTICE_SEND bit off.');
+ return false;
+ }
+
+ $success = false;
+
+ // If it's not a reply, or if the user WANTS to send @-replies...
+ if (!preg_match('/@[a-zA-Z0-9_]{1,15}\b/u', $notice->content) ||
+ ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY)) {
+
+ $success = true;
+
+ // The two condition below are deal breakers:
+
+ // Avoid a loop
+ if ($notice->source == 'Facebook') {
+ common_log(LOG_INFO, "Skipping notice $notice->id because its " .
+ 'source is Facebook.');
+ $success = false;
+ }
+
+ $facebook = getFacebook();
+ $fbuid = $flink->foreign_id;
+
+ try {
+
+ // Check to see if the user has given the FB app status update perms
+ $result = $facebook->api_client->
+ users_hasAppPermission('status_update', $fbuid);
+
+ if ($result != 1) {
+ $user = $flink->getUser();
+ $msg = "Can't send notice $notice->id to Facebook " .
+ "because user $user->nickname hasn't given the " .
+ 'Facebook app \'status_update\' permission.';
+ common_log(LOG_INFO, $msg);
+ $success = false;
+ }
+
+ } catch(FacebookRestClientException $e){
+ common_log(LOG_ERR, $e->getMessage());
+ $success = false;
+ }
+
+ }
+
+ return $success;
+
+}
+
+
+function facebookBroadcastNotice($notice)
+{
+ $facebook = getFacebook();
+ $flink = Foreign_link::getByUserID($notice->profile_id, FACEBOOK_SERVICE);
+ $fbuid = $flink->foreign_id;
+
+ if (isFacebookBound($notice, $flink)) {
+
+ $status = null;
+
+ // Get the status 'verb' (prefix) the user has set
+ try {
+ $prefix = $facebook->api_client->
+ data_getUserPreference(FACEBOOK_NOTICE_PREFIX, $fbuid);
+
+ $status = "$prefix $notice->content";
+
+ } catch(FacebookRestClientException $e) {
+ common_log(LOG_ERR, $e->getMessage());
+ return false;
+ }
+
+ // Okay, we're good to go!
+
+ try {
+ $facebook->api_client->users_setStatus($status, $fbuid, false, true);
+ updateProfileBox($facebook, $flink, $notice);
+ } catch(FacebookRestClientException $e) {
+ common_log(LOG_ERR, $e->getMessage());
+ return false;
+
+ // Should we remove flink if this fails?
+ }
+
+ }
+
+ return true;
+}
diff --git a/lib/featureduserssection.php b/lib/featureduserssection.php
index 2935d8363..aed94b1a5 100644
--- a/lib/featureduserssection.php
+++ b/lib/featureduserssection.php
@@ -86,4 +86,9 @@ class FeaturedUsersSection extends ProfileSection
{
return 'featured_users';
}
+
+ function moreUrl()
+ {
+ return common_local_url('featured');
+ }
}
diff --git a/lib/feed.php b/lib/feed.php
new file mode 100644
index 000000000..466926844
--- /dev/null
+++ b/lib/feed.php
@@ -0,0 +1,110 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Data structure for info about syndication feeds (RSS 1.0, RSS 2.0, Atom)
+ *
+ * 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 Feed
+ * @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);
+}
+
+/**
+ * Data structure for feeds
+ *
+ * This structure is a helpful container for shipping around information about syndication feeds.
+ *
+ * @category Feed
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @author Sarven Capadisli <csarven@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 Feed
+{
+ const RSS1 = 1;
+ const RSS2 = 2;
+ const ATOM = 3;
+ const FOAF = 4;
+
+ var $type = null;
+ var $url = null;
+ var $title = null;
+
+ function __construct($type, $url, $title)
+ {
+ $this->type = $type;
+ $this->url = $url;
+ $this->title = $title;
+ }
+
+ function mimeType()
+ {
+ switch ($this->type) {
+ case Feed::RSS1:
+ return 'application/rdf+xml';
+ case Feed::RSS2:
+ return 'application/rss+xml';
+ case Feed::ATOM:
+ return 'application/atom+xml';
+ case Feed::FOAF:
+ return 'application/rdf+xml';
+ default:
+ return null;
+ }
+ }
+
+ function typeName()
+ {
+ switch ($this->type) {
+ case Feed::RSS1:
+ return _('RSS 1.0');
+ case Feed::RSS2:
+ return _('RSS 2.0');
+ case Feed::ATOM:
+ return _('Atom');
+ case Feed::FOAF:
+ return _('FOAF');
+ default:
+ return null;
+ }
+ }
+
+ function rel()
+ {
+ switch ($this->type) {
+ case Feed::RSS1:
+ case Feed::RSS2:
+ case Feed::ATOM:
+ return 'alternate';
+ case Feed::FOAF:
+ return 'meta';
+ default:
+ return null;
+ }
+ }
+}
diff --git a/lib/feedlist.php b/lib/feedlist.php
index 47d909e96..927e43c33 100644
--- a/lib/feedlist.php
+++ b/lib/feedlist.php
@@ -50,7 +50,7 @@ if (!defined('LACONICA')) {
class FeedList extends Widget
{
var $action = null;
-
+
function __construct($action=null)
{
parent::__construct($action);
@@ -64,8 +64,8 @@ class FeedList extends Widget
$this->out->element('h2', null, _('Export data'));
$this->out->elementStart('ul', array('class' => 'xoxo'));
- foreach ($feeds as $key => $value) {
- $this->feedItem($feeds[$key]);
+ foreach ($feeds as $feed) {
+ $this->feedItem($feed);
}
$this->out->elementEnd('ul');
@@ -74,85 +74,27 @@ class FeedList extends Widget
function feedItem($feed)
{
- $nickname = $this->action->trimmed('nickname');
-
- switch($feed['item']) {
- case 'notices': default:
- $feed_classname = $feed['type'];
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = "$nickname's ".$feed['version']." notice feed";
- $feed['textContent'] = "RSS";
- break;
-
- case 'allrss':
- $feed_classname = $feed['type'];
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = $feed['version']." feed for $nickname and friends";
- $feed['textContent'] = "RSS";
- break;
-
- case 'repliesrss':
- $feed_classname = $feed['type'];
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = $feed['version']." feed for replies to $nickname";
- $feed['textContent'] = "RSS";
- break;
-
- case 'publicrss':
- $feed_classname = $feed['type'];
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = "Public timeline ".$feed['version']." feed";
- $feed['textContent'] = "RSS";
- break;
+ $classname = null;
- case 'publicatom':
- $feed_classname = "atom";
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = "Public timeline ".$feed['version']." feed";
- $feed['textContent'] = "Atom";
+ switch ($feed->type) {
+ case Feed::RSS1:
+ case Feed::RSS2:
+ $classname = 'rss';
break;
-
- case 'tagrss':
- $feed_classname = $feed['type'];
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = $feed['version']." feed for this tag";
- $feed['textContent'] = "RSS";
+ case Feed::ATOM:
+ $classname = 'atom';
break;
-
- case 'favoritedrss':
- $feed_classname = $feed['type'];
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = "Favorited ".$feed['version']." feed";
- $feed['textContent'] = "RSS";
- break;
-
- case 'foaf':
- $feed_classname = "foaf";
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = "$nickname's FOAF file";
- $feed['textContent'] = "FOAF";
- break;
-
- case 'favoritesrss':
- $feed_classname = "favorites";
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = "Feed for favorites of $nickname";
- $feed['textContent'] = "RSS";
- break;
-
- case 'usertimeline':
- $feed_classname = "atom";
- $feed_mimetype = "application/".$feed['type']."+xml";
- $feed_title = "$nickname's ".$feed['version']." notice feed";
- $feed['textContent'] = "Atom";
+ case Feed::FOAF:
+ $classname = 'foaf';
break;
}
+
$this->out->elementStart('li');
- $this->out->element('a', array('href' => $feed['href'],
- 'class' => $feed_classname,
- 'type' => $feed_mimetype,
- 'title' => $feed_title),
- $feed['textContent']);
+ $this->out->element('a', array('href' => $feed->url,
+ 'class' => $classname,
+ 'type' => $feed->mimeType(),
+ 'title' => $feed->title),
+ $feed->typeName());
$this->out->elementEnd('li');
}
}
diff --git a/lib/grouplist.php b/lib/grouplist.php
index 629bdd05d..1b8547499 100644
--- a/lib/grouplist.php
+++ b/lib/grouplist.php
@@ -124,7 +124,7 @@ class GroupList extends Widget
if ($this->group->location) {
$this->out->elementStart('dl', 'entity_location');
$this->out->element('dt', null, _('Location'));
- $this->out->elementStart('dd', 'location');
+ $this->out->elementStart('dd', 'label');
$this->out->raw($this->highlight($this->group->location));
$this->out->elementEnd('dd');
$this->out->elementEnd('dl');
@@ -151,13 +151,16 @@ class GroupList extends Widget
# If we're on a list with an owner (subscriptions or subscribers)...
- if ($user && $user->id == $this->owner->id) {
+ if (!empty($user) && !empty($this->owner) && $user->id == $this->owner->id) {
$this->showOwnerControls();
}
$this->out->elementEnd('div');
if ($user) {
+ $this->out->elementStart('div', 'entity_actions');
+ $this->out->elementStart('ul');
+ $this->out->elementStart('li', 'entity_subscribe');
# XXX: special-case for user looking at own
# subscriptions page
if ($user->isMember($this->group)) {
@@ -167,6 +170,9 @@ class GroupList extends Widget
$jf = new JoinForm($this->out, $this->group);
$jf->show();
}
+ $this->out->elementEnd('li');
+ $this->out->elementEnd('ul');
+ $this->out->elementEnd('div');
}
$this->out->elementEnd('li');
diff --git a/lib/groupsbymemberssection.php b/lib/groupsbymemberssection.php
index 4fa07a244..5f26c6626 100644
--- a/lib/groupsbymemberssection.php
+++ b/lib/groupsbymemberssection.php
@@ -45,7 +45,7 @@ class GroupsByMembersSection extends GroupSection
{
function getGroups()
{
- $qry = 'SELECT user_group.*, count(*) as value ' .
+ $qry = 'SELECT user_group.id, count(*) as value ' .
'FROM user_group JOIN group_member '.
'ON user_group.id = group_member.group_id ' .
'GROUP BY user_group.id ' .
diff --git a/lib/groupsbypostssection.php b/lib/groupsbypostssection.php
index a5e33a93d..1a60ddb4f 100644
--- a/lib/groupsbypostssection.php
+++ b/lib/groupsbypostssection.php
@@ -45,7 +45,7 @@ class GroupsByPostsSection extends GroupSection
{
function getGroups()
{
- $qry = 'SELECT user_group.*, count(*) as value ' .
+ $qry = 'SELECT user_group.id, count(*) as value ' .
'FROM user_group JOIN group_inbox '.
'ON user_group.id = group_inbox.group_id ' .
'GROUP BY user_group.id ' .
diff --git a/lib/grouptagcloudsection.php b/lib/grouptagcloudsection.php
index f05be85cb..5d68af28b 100644
--- a/lib/grouptagcloudsection.php
+++ b/lib/grouptagcloudsection.php
@@ -58,8 +58,14 @@ class GroupTagCloudSection extends TagCloudSection
function getTags()
{
+ if (common_config('db', 'type') == 'pgsql') {
+ $weightexpr='sum(exp(-extract(epoch from (now() - notice_tag.created)) / %s))';
+ } else {
+ $weightexpr='sum(exp(-(now() - notice_tag.created) / %s))';
+ }
+
$qry = 'SELECT notice_tag.tag, '.
- 'sum(exp(-(now() - notice_tag.created)/%s)) as weight ' .
+ $weightexpr . ' as weight ' .
'FROM notice_tag JOIN notice ' .
'ON notice_tag.notice_id = notice.id ' .
'JOIN group_inbox on group_inbox.notice_id = notice.id ' .
diff --git a/lib/htmloutputter.php b/lib/htmloutputter.php
index f9245414f..06603ac05 100644
--- a/lib/htmloutputter.php
+++ b/lib/htmloutputter.php
@@ -101,29 +101,32 @@ class HTMLOutputter extends XMLOutputter
$type = common_negotiate_type($cp, $sp);
if (!$type) {
- common_user_error(_('This page is not available in a '.
- 'media type you accept'), 406);
- exit(0);
+ throw new ClientException(_('This page is not available in a '.
+ 'media type you accept'), 406);
}
}
header('Content-Type: '.$type);
-
+
$this->extraHeaders();
$this->startXML('html',
'-//W3C//DTD XHTML 1.0 Strict//EN',
'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd');
- // FIXME: correct language for interface
-
- $language = common_language();
+ $language = $this->getLanguage();
$this->elementStart('html', array('xmlns' => 'http://www.w3.org/1999/xhtml',
'xml:lang' => $language,
'lang' => $language));
}
+ function getLanguage()
+ {
+ // FIXME: correct language for interface
+ return common_language();
+ }
+
/**
* Ends an HTML document
*
@@ -134,7 +137,7 @@ class HTMLOutputter extends XMLOutputter
$this->elementEnd('html');
$this->endXML();
}
-
+
/**
* To specify additional HTTP headers for the action
*
@@ -172,7 +175,7 @@ class HTMLOutputter extends XMLOutputter
'type' => 'text',
'id' => $id);
if ($value) {
- $attrs['value'] = htmlspecialchars($value);
+ $attrs['value'] = $value;
}
$this->element('input', $attrs);
if ($instructions) {
@@ -206,7 +209,7 @@ class HTMLOutputter extends XMLOutputter
'class' => 'checkbox',
'id' => $id);
if ($value) {
- $attrs['value'] = htmlspecialchars($value);
+ $attrs['value'] = $value;
}
if ($checked) {
$attrs['checked'] = 'checked';
@@ -255,7 +258,7 @@ class HTMLOutputter extends XMLOutputter
foreach ($content as $value => $option) {
if ($value == $selected) {
$this->element('option', array('value' => $value,
- 'selected' => $value),
+ 'selected' => 'selected'),
$option);
} else {
$this->element('option', array('value' => $value), $option);
diff --git a/lib/imagefile.php b/lib/imagefile.php
index 7f1db892c..0c93b257e 100644
--- a/lib/imagefile.php
+++ b/lib/imagefile.php
@@ -47,67 +47,196 @@ if (!defined('LACONICA')) {
class ImageFile
{
- var $filename = null;
- var $barename = null;
- var $type = null;
- var $height = null;
- var $width = null;
+ var $id;
+ var $filepath;
+ var $barename;
+ var $type;
+ var $height;
+ var $width;
- function __construct($filename=null, $type=null, $width=null, $height=null)
+ function __construct($id=null, $filepath=null, $type=null, $width=null, $height=null)
{
- $this->filename = $filename;
- $this->type = $type;
- $this->width = $type;
- $this->height = $type;
+ $this->id = $id;
+ $this->filepath = $filepath;
+
+ $info = @getimagesize($this->filepath);
+ $this->type = ($info) ? $info[2]:$type;
+ $this->width = ($info) ? $info[0]:$width;
+ $this->height = ($info) ? $info[1]:$height;
}
static function fromUpload($param='upload')
{
switch ($_FILES[$param]['error']) {
- case UPLOAD_ERR_OK: // success, jump out
+ case UPLOAD_ERR_OK: // success, jump out
break;
- case UPLOAD_ERR_INI_SIZE:
- case UPLOAD_ERR_FORM_SIZE:
- throw new Exception(_('That file is too big.'));
+ case UPLOAD_ERR_INI_SIZE:
+ case UPLOAD_ERR_FORM_SIZE:
+ throw new Exception(sprintf(_('That file is too big. The maximum file size is %d.'), $this->maxFileSize()));
return;
- case UPLOAD_ERR_PARTIAL:
+ case UPLOAD_ERR_PARTIAL:
@unlink($_FILES[$param]['tmp_name']);
throw new Exception(_('Partial upload.'));
return;
- default:
+ default:
throw new Exception(_('System error uploading file.'));
return;
}
- $imagefile = new ImageFile($_FILES[$param]['tmp_name']);
- $info = @getimagesize($imagefile->filename);
+ $info = @getimagesize($_FILES[$param]['tmp_name']);
if (!$info) {
- @unlink($imagefile->filename);
+ @unlink($_FILES[$param]['tmp_name']);
throw new Exception(_('Not an image or corrupt file.'));
return;
}
- $imagefile->width = $info[0];
- $imagefile->height = $info[1];
+ if ($info[2] !== IMAGETYPE_GIF &&
+ $info[2] !== IMAGETYPE_JPEG &&
+ $info[2] !== IMAGETYPE_PNG) {
- switch ($info[2]) {
- case IMAGETYPE_GIF:
- case IMAGETYPE_JPEG:
- case IMAGETYPE_PNG:
- $imagefile->type = $info[2];
- break;
- default:
- @unlink($imagefile->filename);
+ @unlink($_FILES[$param]['tmp_name']);
throw new Exception(_('Unsupported image file format.'));
return;
}
- return $imagefile;
+ return new ImageFile(null, $_FILES[$param]['tmp_name']);
+ }
+
+ function resize($size, $x = 0, $y = 0, $w = null, $h = null)
+ {
+ $w = ($w === null) ? $this->width:$w;
+ $h = ($h === null) ? $this->height:$h;
+
+ if (!file_exists($this->filepath)) {
+ throw new Exception(_('Lost our file.'));
+ return;
+ }
+
+ // Don't crop/scale if it isn't necessary
+ if ($size === $this->width
+ && $size === $this->height
+ && $x === 0
+ && $y === 0
+ && $w === $this->width
+ && $h === $this->height) {
+
+ $outname = Avatar::filename($this->id,
+ image_type_to_extension($this->type),
+ $size,
+ common_timestamp());
+ $outpath = Avatar::path($outname);
+ @copy($this->filepath, $outpath);
+ return $outname;
+ }
+
+ switch ($this->type) {
+ case IMAGETYPE_GIF:
+ $image_src = imagecreatefromgif($this->filepath);
+ break;
+ case IMAGETYPE_JPEG:
+ $image_src = imagecreatefromjpeg($this->filepath);
+ break;
+ case IMAGETYPE_PNG:
+ $image_src = imagecreatefrompng($this->filepath);
+ break;
+ default:
+ throw new Exception(_('Unknown file type'));
+ return;
+ }
+
+ $image_dest = imagecreatetruecolor($size, $size);
+
+ if ($this->type == IMAGETYPE_GIF || $this->type == IMAGETYPE_PNG) {
+
+ $transparent_idx = imagecolortransparent($image_src);
+
+ if ($transparent_idx >= 0) {
+
+ $transparent_color = imagecolorsforindex($image_src, $transparent_idx);
+ $transparent_idx = imagecolorallocate($image_dest, $transparent_color['red'], $transparent_color['green'], $transparent_color['blue']);
+ imagefill($image_dest, 0, 0, $transparent_idx);
+ imagecolortransparent($image_dest, $transparent_idx);
+
+ } elseif ($this->type == IMAGETYPE_PNG) {
+
+ imagealphablending($image_dest, false);
+ $transparent = imagecolorallocatealpha($image_dest, 0, 0, 0, 127);
+ imagefill($image_dest, 0, 0, $transparent);
+ imagesavealpha($image_dest, true);
+
+ }
+ }
+
+ imagecopyresampled($image_dest, $image_src, 0, 0, $x, $y, $size, $size, $w, $h);
+
+ $outname = Avatar::filename($this->id,
+ image_type_to_extension($this->type),
+ $size,
+ common_timestamp());
+
+ $outpath = Avatar::path($outname);
+
+ switch ($this->type) {
+ case IMAGETYPE_GIF:
+ imagegif($image_dest, $outpath);
+ break;
+ case IMAGETYPE_JPEG:
+ imagejpeg($image_dest, $outpath, 100);
+ break;
+ case IMAGETYPE_PNG:
+ imagepng($image_dest, $outpath);
+ break;
+ default:
+ throw new Exception(_('Unknown file type'));
+ return;
+ }
+
+ imagedestroy($image_src);
+ imagedestroy($image_dest);
+
+ return $outname;
}
function unlink()
{
@unlink($this->filename);
}
+
+ static function maxFileSize()
+ {
+ $value = ImageFile::maxFileSizeInt();
+
+ if ($value > 1024 * 1024) {
+ return ($value/(1024*1024)).'Mb';
+ } else if ($value > 1024) {
+ return ($value/(1024)).'kB';
+ } else {
+ return $value;
+ }
+ }
+
+ static function maxFileSizeInt()
+ {
+ return min(ImageFile::strToInt(ini_get('post_max_size')),
+ ImageFile::strToInt(ini_get('upload_max_filesize')),
+ ImageFile::strToInt(ini_get('memory_limit')));
+ }
+
+ static function strToInt($str)
+ {
+ $unit = substr($str, -1);
+ $num = substr($str, 0, -1);
+
+ switch(strtoupper($unit)){
+ case 'G':
+ $num *= 1024;
+ case 'M':
+ $num *= 1024;
+ case 'K':
+ $num *= 1024;
+ }
+
+ return $num;
+ }
} \ No newline at end of file
diff --git a/lib/jabber.php b/lib/jabber.php
index 84d2a562c..3fbb3e1ab 100644
--- a/lib/jabber.php
+++ b/lib/jabber.php
@@ -114,7 +114,7 @@ function jabber_connect($resource=null)
try {
$conn->connect(true); // true = persistent connection
} catch (XMPPHP_Exception $e) {
- common_log(LOG_ERROR, $e->getMessage());
+ common_log(LOG_ERR, $e->getMessage());
return false;
}
@@ -178,7 +178,7 @@ function jabber_format_entry($profile, $notice)
$entry .= "<link href='" . htmlspecialchars($profile->profileurl) . "'/>\n";
$entry .= "<link rel='self' type='application/rss+xml' href='" . $self_url . "'/>\n";
$entry .= "<author><name>" . $profile->nickname . "</name></author>\n";
- $entry .= "<icon>" . common_profile_avatar_url($profile, AVATAR_PROFILE_SIZE) . "</icon>\n";
+ $entry .= "<icon>" . $profile->avatarUrl(AVATAR_PROFILE_SIZE) . "</icon>\n";
$entry .= "</source>\n";
$entry .= "<title>" . htmlspecialchars($msg) . "</title>\n";
$entry .= "<summary>" . htmlspecialchars($msg) . "</summary>\n";
@@ -186,6 +186,11 @@ function jabber_format_entry($profile, $notice)
$entry .= "<id>". $notice->uri . "</id>\n";
$entry .= "<published>".common_date_w3dtf($notice->created)."</published>\n";
$entry .= "<updated>".common_date_w3dtf($notice->modified)."</updated>\n";
+ if ($notice->reply_to) {
+ $replyurl = common_local_url('shownotice',
+ array('notice' => $notice->reply_to));
+ $entry .= "<link rel='related' href='" . $replyurl . "'/>\n";
+ }
$entry .= "</entry>\n";
$html = "\n<html xmlns='http://jabber.org/protocol/xhtml-im'>\n";
@@ -354,12 +359,13 @@ function jabber_broadcast_notice($notice)
// First, get users to whom this is a direct reply
$user = new User();
- $user->query('SELECT user.id, user.jabber ' .
- 'FROM user JOIN reply ON user.id = reply.profile_id ' .
+ $UT = common_config('db','type')=='pgsql'?'"user"':'user';
+ $user->query("SELECT $UT.id, $UT.jabber " .
+ "FROM $UT JOIN reply ON $UT.id = reply.profile_id " .
'WHERE reply.notice_id = ' . $notice->id . ' ' .
- 'AND user.jabber is not null ' .
- 'AND user.jabbernotify = 1 ' .
- 'AND user.jabberreplies = 1 ');
+ "AND $UT.jabber is not null " .
+ "AND $UT.jabbernotify = 1 " .
+ "AND $UT.jabberreplies = 1 ");
while ($user->fetch()) {
common_log(LOG_INFO,
@@ -375,12 +381,12 @@ function jabber_broadcast_notice($notice)
// Now, get users subscribed to this profile
$user = new User();
- $user->query('SELECT user.id, user.jabber ' .
- 'FROM user JOIN subscription ' .
- 'ON user.id = subscription.subscriber ' .
+ $user->query("SELECT $UT.id, $UT.jabber " .
+ "FROM $UT JOIN subscription " .
+ "ON $UT.id = subscription.subscriber " .
'WHERE subscription.subscribed = ' . $notice->profile_id . ' ' .
- 'AND user.jabber is not null ' .
- 'AND user.jabbernotify = 1 ' .
+ "AND $UT.jabber is not null " .
+ "AND $UT.jabbernotify = 1 " .
'AND subscription.jabber = 1 ');
while ($user->fetch()) {
@@ -399,11 +405,13 @@ function jabber_broadcast_notice($notice)
// Now, get users who have it in their inbox because of groups
$user = new User();
- $user->query('SELECT user.id, user.jabber ' .
- 'FROM user JOIN notice_inbox ' .
- 'ON user.id = notice_inbox.user_id ' .
+ $user->query("SELECT $UT.id, $UT.jabber " .
+ "FROM $UT JOIN notice_inbox " .
+ "ON $UT.id = notice_inbox.user_id " .
'WHERE notice_inbox.notice_id = ' . $notice->id . ' ' .
- 'AND notice_inbox.source = 2 ');
+ 'AND notice_inbox.source = 2 ' .
+ 'AND user.jabber is not null ' .
+ 'AND user.jabbernotify = 1 ');
while ($user->fetch()) {
if (!array_key_exists($user->id, $sent_to)) {
diff --git a/lib/language.php b/lib/language.php
index a73b73f28..79e9030ae 100644
--- a/lib/language.php
+++ b/lib/language.php
@@ -94,40 +94,43 @@ function get_nice_language_list()
* Get a list of all languages that are enabled in the default config
*
* This should ONLY be called when setting up the default config in common.php.
- * Any other attempt to get a list of lanugages should instead call
+ * Any other attempt to get a list of languages should instead call
* common_config('site','languages')
*
* @return array mapping of language codes to language info
*/
function get_all_languages() {
return array(
- 'bg' => array('q' => 0.8, 'lang' => 'bg_BG', 'name' => 'Bulgarian', 'direction' => 'ltr'),
- 'ca' => array('q' => 0.5, 'lang' => 'ca_ES', 'name' => 'Catalan', 'direction' => 'ltr'),
- 'cs' => array('q' => 0.5, 'lang' => 'cs_CZ', 'name' => 'Czech', 'direction' => 'ltr'),
- 'de' => array('q' => 0.5, 'lang' => 'de_DE', 'name' => 'German', 'direction' => 'ltr'),
- 'el' => array('q' => 0.1, 'lang' => 'el', 'name' => 'Greek', 'direction' => 'ltr'),
- 'en-us' => array('q' => 1, 'lang' => 'en_US', 'name' => 'English (US)', 'direction' => 'ltr'),
- 'en-gb' => array('q' => 0.3, 'lang' => 'en_GB', 'name' => 'English (British)', 'direction' => 'ltr'),
- 'en' => array('q' => 1, 'lang' => 'en', 'name' => 'English', 'direction' => 'ltr'),
- 'es' => array('q' => 0.5, 'lang' => 'es', 'name' => 'Spanish', 'direction' => 'ltr'),
- 'fr-fr' => array('q' => 0.2, 'lang' => 'fr_FR', 'name' => 'French', 'direction' => 'ltr'),
- 'he' => array('q' => 0.5, 'lang' => 'he_IL', 'name' => 'Hebrew', 'direction' => 'ltr'),
- 'it' => array('q' => 0.9, 'lang' => 'it_IT', 'name' => 'Italian', 'direction' => 'rtl'),
- 'jp' => array('q' => 0.5, 'lang' => 'ja_JP', 'name' => 'Japanese', 'direction' => 'ltr'),
-# 'ko' => array('q' => 0, 'lang' => 'ko', 'name' => 'Korean', 'direction' => 'ltr'),
- 'mk' => array('q' => 0.5, 'lang' => 'mk_MK', 'name' => 'Macedonian', 'direction' => 'ltr'),
- 'nb' => array('q' => 0.1, 'lang' => 'nb_NO', 'name' => 'Norwegian (bokmal)', 'direction' => 'ltr'),
- 'nl' => array('q' => 0.5, 'lang' => 'nl_NL', 'name' => 'Dutch', 'direction' => 'ltr'),
- 'pl' => array('q' => 0.5, 'lang' => 'pl_PL', 'name' => 'Polish', 'direction' => 'ltr'),
-# 'pt' => array('q' => 0, 'lang' => 'pt', 'name' => 'Portuguese', 'direction' => 'ltr'),
- 'pt-br' => array('q' => 0.7, 'lang' => 'pt_BR', 'name' => 'Portuguese Brazil', 'direction' => 'ltr'),
- 'ru' => array('q' => 0.1, 'lang' => 'ru_RU', 'name' => 'Russian', 'direction' => 'ltr'),
- 'sv' => array('q' => 0.9, 'lang' => 'sv_SE', 'name' => 'Swedish', 'direction' => 'ltr'),
- 'te' => array('q' => 0.3, 'lang' => 'te_IN', 'name' => 'Telugu', 'direction' => 'ltr'),
- 'tr' => array('q' => 0.5, 'lang' => 'tr_TR', 'name' => 'Turkish', 'direction' => 'ltr'),
- 'uk' => array('q' => 0.7, 'lang' => 'uk_UA', 'name' => 'Ukrainian', 'direction' => 'ltr'),
- 'vi' => array('q' => 0.7, 'lang' => 'vi_VN', 'name' => 'Vietnamese', 'direction' => 'ltr'),
- 'zh-cn' => array('q' => 0.9, 'lang' => 'zh_CN', 'name' => 'Chinese (Simplified)', 'direction' => 'ltr'),
- 'zh-hant' => array('q' => 0.2, 'lang' => 'zh_hant', 'name' => 'Chinese (Taiwanese)', 'direction' => 'ltr'),
+ 'bg' => array('q' => 0.8, 'lang' => 'bg_BG', 'name' => 'Bulgarian', 'direction' => 'ltr'),
+ 'ca' => array('q' => 0.5, 'lang' => 'ca_ES', 'name' => 'Catalan', 'direction' => 'ltr'),
+ 'cs' => array('q' => 0.5, 'lang' => 'cs_CZ', 'name' => 'Czech', 'direction' => 'ltr'),
+ 'de' => array('q' => 0.5, 'lang' => 'de_DE', 'name' => 'German', 'direction' => 'ltr'),
+ 'el' => array('q' => 0.1, 'lang' => 'el', 'name' => 'Greek', 'direction' => 'ltr'),
+ 'en-us' => array('q' => 1, 'lang' => 'en_US', 'name' => 'English (US)', 'direction' => 'ltr'),
+ 'en-gb' => array('q' => 0.3, 'lang' => 'en_GB', 'name' => 'English (British)', 'direction' => 'ltr'),
+ 'en' => array('q' => 1, 'lang' => 'en', 'name' => 'English', 'direction' => 'ltr'),
+ 'es' => array('q' => 0.5, 'lang' => 'es', 'name' => 'Spanish', 'direction' => 'ltr'),
+ 'fi' => array('q' => 0.5, 'lang' => 'fi', 'name' => 'Finnish', 'direction' => 'ltr'),
+ 'fr-fr' => array('q' => 0.2, 'lang' => 'fr_FR', 'name' => 'French', 'direction' => 'ltr'),
+ 'he' => array('q' => 0.5, 'lang' => 'he_IL', 'name' => 'Hebrew', 'direction' => 'rtl'),
+ 'it' => array('q' => 0.9, 'lang' => 'it_IT', 'name' => 'Italian', 'direction' => 'ltr'),
+ 'jp' => array('q' => 0.5, 'lang' => 'ja_JP', 'name' => 'Japanese', 'direction' => 'ltr'),
+# 'ko' => array('q' => 0, 'lang' => 'ko', 'name' => 'Korean', 'direction' => 'ltr'),
+ 'mk' => array('q' => 0.5, 'lang' => 'mk_MK', 'name' => 'Macedonian', 'direction' => 'ltr'),
+ 'nb' => array('q' => 0.1, 'lang' => 'nb_NO', 'name' => 'Norwegian (Bokmål)', 'direction' => 'ltr'),
+ 'no' => array('q' => 0.1, 'lang' => 'nb_NO', 'name' => 'Norwegian (Bokmål)', 'direction' => 'ltr'),
+ 'nn' => array('q' => 0.1, 'lang' => 'nn_NO', 'name' => 'Norwegian (Nynorsk)', 'direction' => 'ltr'),
+ 'nl' => array('q' => 0.5, 'lang' => 'nl_NL', 'name' => 'Dutch', 'direction' => 'ltr'),
+ 'pl' => array('q' => 0.5, 'lang' => 'pl_PL', 'name' => 'Polish', 'direction' => 'ltr'),
+# 'pt' => array('q' => 0, 'lang' => 'pt', 'name' => 'Portuguese', 'direction' => 'ltr'),
+ 'pt-br' => array('q' => 0.7, 'lang' => 'pt_BR', 'name' => 'Portuguese Brazil', 'direction' => 'ltr'),
+ 'ru' => array('q' => 0.1, 'lang' => 'ru_RU', 'name' => 'Russian', 'direction' => 'ltr'),
+ 'sv' => array('q' => 0.9, 'lang' => 'sv_SE', 'name' => 'Swedish', 'direction' => 'ltr'),
+ 'te' => array('q' => 0.3, 'lang' => 'te_IN', 'name' => 'Telugu', 'direction' => 'ltr'),
+ 'tr' => array('q' => 0.5, 'lang' => 'tr_TR', 'name' => 'Turkish', 'direction' => 'ltr'),
+ 'uk' => array('q' => 0.7, 'lang' => 'uk_UA', 'name' => 'Ukrainian', 'direction' => 'ltr'),
+ 'vi' => array('q' => 0.7, 'lang' => 'vi_VN', 'name' => 'Vietnamese', 'direction' => 'ltr'),
+ 'zh-cn' => array('q' => 0.9, 'lang' => 'zh_CN', 'name' => 'Chinese (Simplified)', 'direction' => 'ltr'),
+ 'zh-hant' => array('q' => 0.2, 'lang' => 'zh_hant', 'name' => 'Chinese (Taiwanese)', 'direction' => 'ltr'),
);
}
diff --git a/lib/mail.php b/lib/mail.php
index 5638ae9bf..9fa86de5c 100644
--- a/lib/mail.php
+++ b/lib/mail.php
@@ -246,7 +246,7 @@ function mail_subscribe_notify_profile($listenee, $other)
"\n".'Faithfully yours,'."\n".'%7$s.'."\n\n".
"----\n".
"Change your email address or ".
- "notification options at %8$s\n"),
+ "notification options at ".'%8$s' ."\n"),
$long_name,
common_config('site', 'name'),
$other->profileurl,
@@ -331,12 +331,13 @@ function mail_broadcast_notice_sms($notice)
$user = new User();
+ $UT = common_config('db','type')=='pgsql'?'"user"':'user';
$user->query('SELECT nickname, smsemail, incomingemail ' .
- 'FROM user JOIN subscription ' .
- 'ON user.id = subscription.subscriber ' .
+ "FROM $UT JOIN subscription " .
+ "ON $UT.id = subscription.subscriber " .
'WHERE subscription.subscribed = ' . $notice->profile_id . ' ' .
- 'AND user.smsemail IS NOT null ' .
- 'AND user.smsnotify = 1 ' .
+ "AND $UT.smsemail IS NOT null " .
+ "AND $UT.smsnotify = 1 " .
'AND subscription.sms = 1 ');
while ($user->fetch()) {
@@ -572,3 +573,53 @@ function mail_notify_fave($other, $user, $notice)
common_init_locale();
mail_to_user($other, $subject, $body);
}
+
+/**
+ * notify a user that they have received an "attn:" message AKA "@-reply"
+ *
+ * @param User $user The user who recevied the notice
+ * @param Notice $notice The notice that was sent
+ *
+ * @return void
+ */
+
+function mail_notify_attn($user, $notice)
+{
+ if (!$user->email || !$user->emailnotifyattn) {
+ return;
+ }
+
+ $sender = $notice->getProfile();
+
+ $bestname = $sender->getBestName();
+
+ common_init_locale($user->language);
+
+ $subject = sprintf(_('%s sent a notice to your attention'), $bestname);
+
+ $body = sprintf(_("%1\$s just sent a notice to your attention (an '@-reply') on %2\$s.\n\n".
+ "The notice is here:\n\n".
+ "\t%3\$s\n\n" .
+ "It reads:\n\n".
+ "\t%4\$s\n\n" .
+ "You can reply back here:\n\n".
+ "\t%5\$s\n\n" .
+ "The list of all @-replies for you here:\n\n" .
+ "%6\$s\n\n" .
+ "Faithfully yours,\n" .
+ "%2\$s\n\n" .
+ "P.S. You can turn off these email notifications here: %7\$s\n"),
+ $bestname,
+ common_config('site', 'name'),
+ common_local_url('shownotice',
+ array('notice' => $notice->id)),
+ $notice->content,
+ common_local_url('newnotice',
+ array('replyto' => $sender->nickname)),
+ common_local_url('replies',
+ array('nickname' => $user->nickname)),
+ common_local_url('emailsettings'));
+
+ common_init_locale();
+ mail_to_user($user, $subject, $body);
+}
diff --git a/lib/mailbox.php b/lib/mailbox.php
index 8d5d44e49..d77234549 100644
--- a/lib/mailbox.php
+++ b/lib/mailbox.php
@@ -63,6 +63,8 @@ class MailboxAction extends PersonalAction
$this->page = 1;
}
+ common_set_returnto($this->selfUrl());
+
return true;
}
@@ -181,8 +183,8 @@ class MailboxAction extends PersonalAction
'class' => 'url'));
$avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
$this->element('img', array('src' => ($avatar) ?
- common_avatar_display_url($avatar) :
- common_default_avatar(AVATAR_STREAM_SIZE),
+ $avatar->displayUrl() :
+ Avatar::defaultImage(AVATAR_STREAM_SIZE),
'class' => 'photo avatar',
'width' => AVATAR_STREAM_SIZE,
'height' => AVATAR_STREAM_SIZE,
diff --git a/lib/messageform.php b/lib/messageform.php
index 61d3d75af..f41508305 100644
--- a/lib/messageform.php
+++ b/lib/messageform.php
@@ -99,7 +99,6 @@ class MessageForm extends Form
return common_local_url('newmessage');
}
-
/**
* Legend of the Form
*
@@ -110,7 +109,6 @@ class MessageForm extends Form
$this->out->element('legend', null, _('Send a direct notice'));
}
-
/**
* Data elements
*
@@ -137,7 +135,7 @@ class MessageForm extends Form
$this->out->elementStart('ul', 'form_data');
$this->out->elementStart('li', array('id' => 'notice_to'));
$this->out->dropdown('to', _('To'), $mutual, null, false,
- $this->to->id);
+ ($this->to) ? $this->to->id : null);
$this->out->elementEnd('li');
$this->out->elementStart('li', array('id' => 'notice_text'));
diff --git a/lib/noticelist.php b/lib/noticelist.php
index 20bf3c9f1..9fc0126b3 100644
--- a/lib/noticelist.php
+++ b/lib/noticelist.php
@@ -203,11 +203,14 @@ class NoticeListItem extends Widget
function showNoticeOptions()
{
- $this->out->elementStart('div', 'notice-options');
- $this->showFaveForm();
- $this->showReplyLink();
- $this->showDeleteLink();
- $this->out->elementEnd('div');
+ $user = common_current_user();
+ if ($user) {
+ $this->out->elementStart('div', 'notice-options');
+ $this->showFaveForm();
+ $this->showReplyLink();
+ $this->showDeleteLink();
+ $this->out->elementEnd('div');
+ }
}
/**
@@ -282,8 +285,8 @@ class NoticeListItem extends Widget
$avatar = $this->profile->getAvatar($avatar_size);
$this->out->element('img', array('src' => ($avatar) ?
- common_avatar_display_url($avatar) :
- common_default_avatar($avatar_size),
+ $avatar->displayUrl() :
+ Avatar::defaultImage($avatar_size),
'class' => 'avatar photo',
'width' => $avatar_size,
'height' => $avatar_size,
@@ -440,19 +443,21 @@ class NoticeListItem extends Widget
function showReplyLink()
{
- $reply_url = common_local_url('newnotice',
- array('replyto' => $this->profile->nickname));
-
- $this->out->elementStart('dl', 'notice_reply');
- $this->out->element('dt', null, _('Reply to this notice'));
- $this->out->elementStart('dd');
- $this->out->elementStart('a', array('href' => $reply_url,
- 'title' => _('Reply to this notice')));
- $this->out->text(_('Reply'));
- $this->out->element('span', 'notice_id', $this->notice->id);
- $this->out->elementEnd('a');
- $this->out->elementEnd('dd');
- $this->out->elementEnd('dl');
+ if (common_logged_in()) {
+ $reply_url = common_local_url('newnotice',
+ array('replyto' => $this->profile->nickname));
+
+ $this->out->elementStart('dl', 'notice_reply');
+ $this->out->element('dt', null, _('Reply to this notice'));
+ $this->out->elementStart('dd');
+ $this->out->elementStart('a', array('href' => $reply_url,
+ 'title' => _('Reply to this notice')));
+ $this->out->text(_('Reply'));
+ $this->out->element('span', 'notice_id', $this->notice->id);
+ $this->out->elementEnd('a');
+ $this->out->elementEnd('dd');
+ $this->out->elementEnd('dl');
+ }
}
/**
diff --git a/lib/noticesection.php b/lib/noticesection.php
index aa8e03229..94c2738ef 100644
--- a/lib/noticesection.php
+++ b/lib/noticesection.php
@@ -73,6 +73,11 @@ class NoticeSection extends Section
function showNotice($notice)
{
$profile = $notice->getProfile();
+ if (empty($profile)) {
+ common_log(LOG_WARNING, sprintf("Notice %d has no profile",
+ $notice->id));
+ return;
+ }
$this->out->elementStart('li', 'hentry notice');
$this->out->elementStart('div', 'entry-title');
$avatar = $profile->getAvatar(AVATAR_MINI_SIZE);
@@ -82,7 +87,7 @@ class NoticeSection extends Section
$profile->nickname,
'href' => $profile->profileurl,
'class' => 'url'));
- $this->out->element('img', array('src' => (($avatar) ? common_avatar_display_url($avatar) : common_default_avatar(AVATAR_MINI_SIZE)),
+ $this->out->element('img', array('src' => (($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_MINI_SIZE)),
'width' => AVATAR_MINI_SIZE,
'height' => AVATAR_MINI_SIZE,
'class' => 'avatar photo',
@@ -96,7 +101,7 @@ class NoticeSection extends Section
$this->out->elementStart('p', 'entry-content');
$this->out->raw($notice->rendered);
$this->out->elementEnd('p');
- if ($notice->value) {
+ if (!empty($notice->value)) {
$this->out->elementStart('p');
$this->out->text($notice->value);
$this->out->elementEnd('p');
diff --git a/lib/oauthstore.php b/lib/oauthstore.php
index 7ad3be20e..9af05ea2d 100644
--- a/lib/oauthstore.php
+++ b/lib/oauthstore.php
@@ -63,7 +63,7 @@ class LaconicaOAuthDataStore extends OAuthDataStore
if ($n->find(true)) {
return true;
} else {
- $n->timestamp = $timestamp;
+ $n->ts = $timestamp;
$n->created = DB_DataObject_Cast::dateTime();
$n->insert();
return false;
diff --git a/lib/omb.php b/lib/omb.php
index f2dbef5ba..befcf4666 100644
--- a/lib/omb.php
+++ b/lib/omb.php
@@ -206,7 +206,7 @@ function omb_post_notice_keys($notice, $postnoticeurl, $tk, $secret)
$result = $fetcher->post($req->get_normalized_http_url(),
$req->to_postdata(),
- array('User-Agent' => 'Laconica/' . LACONICA_VERSION));
+ array('User-Agent: Laconica/' . LACONICA_VERSION));
common_debug('Got HTTP result "'.print_r($result,true).'"', __FILE__);
@@ -239,7 +239,7 @@ function omb_broadcast_profile($profile)
while ($sub->fetch()) {
$rp = Remote_profile::staticGet('id', $sub->subscriber);
if ($rp) {
- if (!$updated[$rp->updateprofileurl]) {
+ if (!array_key_exists($rp->updateprofileurl, $updated)) {
if (omb_update_profile($profile, $rp, $sub)) {
$updated[$rp->updateprofileurl] = true;
}
@@ -291,11 +291,13 @@ function omb_update_profile($profile, $remote_profile, $subscription)
common_debug('postdata = '.$req->to_postdata(), __FILE__);
$result = $fetcher->post($req->get_normalized_http_url(),
$req->to_postdata(),
- array('User-Agent' => 'Laconica/' . LACONICA_VERSION));
+ array('User-Agent: Laconica/' . LACONICA_VERSION));
common_debug('Got HTTP result "'.print_r($result,true).'"', __FILE__);
- if ($result->status == 403) { # not authorized, don't send again
+ if (empty($result) || $result) {
+ common_debug("Unable to contact " . $req->get_normalized_http_url());
+ } else if ($result->status == 403) { # not authorized, don't send again
common_debug('403 result, deleting subscription', __FILE__);
$subscription->delete();
return false;
diff --git a/lib/openid.php b/lib/openid.php
index 860573702..5c3d460da 100644
--- a/lib/openid.php
+++ b/lib/openid.php
@@ -64,6 +64,9 @@ function oid_set_last($openid_url)
function oid_get_last()
{
+ if (empty($_COOKIE[OPENID_COOKIE_KEY])) {
+ return null;
+ }
$openid_url = $_COOKIE[OPENID_COOKIE_KEY];
if ($openid_url && strlen($openid_url) > 0) {
return $openid_url;
diff --git a/lib/peoplesearchresults.php b/lib/peoplesearchresults.php
new file mode 100644
index 000000000..f8ab7cf3b
--- /dev/null
+++ b/lib/peoplesearchresults.php
@@ -0,0 +1,75 @@
+<?php
+/**
+ * People search results class
+ *
+ * PHP version 5
+ *
+ * @category Widget
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @author Robin Millette <millette@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://laconi.ca/
+ *
+ * Laconica - a distributed open-source microblogging tool
+ * Copyright (C) 2008, Controlez-Vous, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+require_once INSTALLDIR.'/lib/profilelist.php';
+
+/**
+ * People search results class
+ *
+ * Derivative of ProfileList with specialization for highlighting search terms.
+ *
+ * @category Widget
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @author Robin Millette <millette@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://laconi.ca/
+ *
+ * @see PeoplesearchAction
+ */
+
+class PeopleSearchResults extends ProfileList
+{
+ var $terms = null;
+ var $pattern = null;
+
+ function __construct($profile, $terms, $action)
+ {
+ parent::__construct($profile, $terms, $action);
+ $this->terms = array_map('preg_quote',
+ array_map('htmlspecialchars', $terms));
+ $this->pattern = '/('.implode('|',$terms).')/i';
+ }
+
+ function highlight($text)
+ {
+ return preg_replace($this->pattern, '<strong>\\1</strong>', htmlspecialchars($text));
+ }
+
+ function isReadOnly()
+ {
+ return true;
+ }
+}
+
diff --git a/lib/personal.php b/lib/personal.php
index 900df0257..e46350c63 100644
--- a/lib/personal.php
+++ b/lib/personal.php
@@ -55,7 +55,6 @@ class PersonalAction extends Action
function handle($args)
{
parent::handle($args);
- common_set_returnto($this->selfUrl());
}
}
diff --git a/lib/personaltagcloudsection.php b/lib/personaltagcloudsection.php
index 0882822db..978153a84 100644
--- a/lib/personaltagcloudsection.php
+++ b/lib/personaltagcloudsection.php
@@ -58,8 +58,14 @@ class PersonalTagCloudSection extends TagCloudSection
function getTags()
{
- $qry = 'SELECT notice_tag.tag, '.
- 'sum(exp(-(now() - notice_tag.created)/%s)) as weight ' .
+ if (common_config('db', 'type') == 'pgsql') {
+ $weightexpr='sum(exp(-extract(epoch from (now() - notice_tag.created)) / %s))';
+ } else {
+ $weightexpr='sum(exp(-(now() - notice_tag.created) / %s))';
+ }
+
+ $qry = 'SELECT notice_tag.tag, '.
+ $weightexpr . ' as weight ' .
'FROM notice_tag JOIN notice ' .
'ON notice_tag.notice_id = notice.id ' .
'WHERE notice.profile_id = %d ' .
diff --git a/lib/plugin.php b/lib/plugin.php
new file mode 100644
index 000000000..7b2436e54
--- /dev/null
+++ b/lib/plugin.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Utility class for plugins
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 2008 Control Yourself, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+if (!defined('LACONICA')) {
+ exit(1);
+}
+
+/**
+ * Base class for plugins
+ *
+ * A base class for Laconica plugins. Mostly a light wrapper around
+ * the Event framework.
+ *
+ * Subclasses of Plugin will automatically handle an event if they define
+ * a method called "onEventName". (Well, OK -- only if they call parent::__construct()
+ * in their constructors.)
+ *
+ * They will also automatically handle the InitializePlugin and CleanupPlugin with the
+ * initialize() and cleanup() methods, respectively.
+ *
+ * @category Plugin
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ *
+ * @see Event
+ */
+
+class Plugin
+{
+ function __construct()
+ {
+ Event::addHandler('InitializePlugin', array($this, 'initialize'));
+ Event::addHandler('CleanupPlugin', array($this, 'cleanup'));
+
+ foreach (get_class_methods($this) as $method) {
+ if (mb_substr($method, 0, 2) == 'on') {
+ Event::addHandler(mb_substr($method, 2), array($this, $method));
+ }
+ }
+ }
+
+ function initialize()
+ {
+ return true;
+ }
+
+ function cleanup()
+ {
+ return true;
+ }
+}
diff --git a/lib/popularnoticesection.php b/lib/popularnoticesection.php
index 89daaa563..0505f0fa9 100644
--- a/lib/popularnoticesection.php
+++ b/lib/popularnoticesection.php
@@ -31,8 +31,6 @@ if (!defined('LACONICA')) {
exit(1);
}
-define('NOTICES_PER_SECTION', 6);
-
/**
* Base class for sections showing lists of notices
*
@@ -50,10 +48,18 @@ class PopularNoticeSection extends NoticeSection
{
function getNotices()
{
+ if (common_config('db', 'type') == 'pgsql') {
+ $weightexpr='sum(exp(-extract(epoch from (now() - fave.modified)) / %s))';
+ } else {
+ $weightexpr='sum(exp(-(now() - fave.modified) / %s))';
+ }
+
$qry = 'SELECT notice.*, '.
- 'sum(exp(-(now() - fave.modified) / %s)) as weight ' .
+ $weightexpr . ' as weight ' .
'FROM notice JOIN fave ON notice.id = fave.notice_id ' .
- 'GROUP BY fave.notice_id ' .
+ 'GROUP BY notice.id,notice.profile_id,notice.content,notice.uri,' .
+ 'notice.rendered,notice.url,notice.created,notice.modified,' .
+ 'notice.reply_to,notice.is_local,notice.source ' .
'ORDER BY weight DESC';
$offset = 0;
@@ -80,4 +86,9 @@ class PopularNoticeSection extends NoticeSection
{
return 'popular_notices';
}
+
+ function moreUrl()
+ {
+ return common_local_url('favorited');
+ }
}
diff --git a/lib/profilelist.php b/lib/profilelist.php
index 499d74f7b..c2040fbc2 100644
--- a/lib/profilelist.php
+++ b/lib/profilelist.php
@@ -34,8 +34,6 @@ if (!defined('LACONICA')) {
require_once INSTALLDIR.'/lib/widget.php';
-define('PROFILES_PER_PAGE', 20);
-
/**
* Widget to show a list of profiles
*
@@ -97,7 +95,7 @@ class ProfileList extends Widget
$avatar = $this->profile->getAvatar(AVATAR_STREAM_SIZE);
$this->out->elementStart('a', array('href' => $this->profile->profileurl,
'class' => 'url'));
- $this->out->element('img', array('src' => ($avatar) ? common_avatar_display_url($avatar) : common_default_avatar(AVATAR_STREAM_SIZE),
+ $this->out->element('img', array('src' => ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_STREAM_SIZE),
'class' => 'photo avatar',
'width' => AVATAR_STREAM_SIZE,
'height' => AVATAR_STREAM_SIZE,
@@ -123,7 +121,7 @@ class ProfileList extends Widget
if ($this->profile->location) {
$this->out->elementStart('dl', 'entity_location');
$this->out->element('dt', null, _('Location'));
- $this->out->elementStart('dd', 'location');
+ $this->out->elementStart('dd', 'label');
$this->out->raw($this->highlight($this->profile->location));
$this->out->elementEnd('dd');
$this->out->elementEnd('dl');
@@ -191,9 +189,14 @@ class ProfileList extends Widget
$this->out->elementEnd('div');
+ $this->out->elementStart('div', 'entity_actions');
+
+ $this->out->elementStart('ul');
+
if ($user && $user->id != $this->profile->id) {
# XXX: special-case for user looking at own
# subscriptions page
+ $this->out->elementStart('li', 'entity_subscribe');
if ($user->isSubscribed($this->profile)) {
$usf = new UnsubscribeForm($this->out, $this->profile);
$usf->show();
@@ -201,8 +204,18 @@ class ProfileList extends Widget
$sf = new SubscribeForm($this->out, $this->profile);
$sf->show();
}
+ $this->out->elementEnd('li');
+ $this->out->elementStart('li', 'entity_block');
+ if ($user && $user->id == $this->owner->id) {
+ $this->showBlockForm();
+ }
+ $this->out->elementEnd('li');
}
+ $this->out->elementEnd('ul');
+
+ $this->out->elementEnd('div');
+
$this->out->elementEnd('li');
}
@@ -217,4 +230,8 @@ class ProfileList extends Widget
{
return htmlspecialchars($text);
}
+
+ function showBlockForm()
+ {
+ }
}
diff --git a/lib/profileminilist.php b/lib/profileminilist.php
index 56b768419..0d466bba8 100644
--- a/lib/profileminilist.php
+++ b/lib/profileminilist.php
@@ -69,14 +69,12 @@ class ProfileMiniList extends ProfileList
function showProfile()
{
$this->out->elementStart('li', 'vcard');
- $this->out->elementStart('a', array('title' => ($this->profile->fullname) ?
- $this->profile->fullname :
- $this->profile->nickname,
+ $this->out->elementStart('a', array('title' => $this->profile->getBestName(),
'href' => $this->profile->profileurl,
'rel' => 'contact member',
'class' => 'url'));
$avatar = $this->profile->getAvatar(AVATAR_MINI_SIZE);
- $this->out->element('img', array('src' => (($avatar) ? common_avatar_display_url($avatar) : common_default_avatar(AVATAR_MINI_SIZE)),
+ $this->out->element('img', array('src' => (($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_MINI_SIZE)),
'width' => AVATAR_MINI_SIZE,
'height' => AVATAR_MINI_SIZE,
'class' => 'avatar photo',
diff --git a/lib/profilesection.php b/lib/profilesection.php
index 3642ae164..8ed290e03 100644
--- a/lib/profilesection.php
+++ b/lib/profilesection.php
@@ -86,7 +86,7 @@ class ProfileSection extends Section
'rel' => 'contact member',
'class' => 'url'));
$avatar = $profile->getAvatar(AVATAR_MINI_SIZE);
- $this->out->element('img', array('src' => (($avatar) ? common_avatar_display_url($avatar) : common_default_avatar(AVATAR_MINI_SIZE)),
+ $this->out->element('img', array('src' => (($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_MINI_SIZE)),
'width' => AVATAR_MINI_SIZE,
'height' => AVATAR_MINI_SIZE,
'class' => 'avatar photo',
diff --git a/lib/publicgroupnav.php b/lib/publicgroupnav.php
index d72475e20..485d25e20 100644
--- a/lib/publicgroupnav.php
+++ b/lib/publicgroupnav.php
@@ -39,6 +39,7 @@ require_once INSTALLDIR.'/lib/widget.php';
* @category Output
* @package Laconica
* @author Evan Prodromou <evan@controlyourself.ca>
+ * @author Sarven Capadisli <csarven@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/
*
@@ -73,23 +74,26 @@ class PublicGroupNav extends Widget
$this->action->elementStart('ul', array('class' => 'nav'));
- $this->out->menuItem(common_local_url('public'), _('Public'),
- _('Public timeline'), $action_name == 'public', 'nav_timeline_public');
+ if (Event::handle('StartPublicGroupNav', array($this))) {
+ $this->out->menuItem(common_local_url('public'), _('Public'),
+ _('Public timeline'), $action_name == 'public', 'nav_timeline_public');
- $this->out->menuItem(common_local_url('groups'), _('Groups'),
- _('User groups'), $action_name == 'groups', 'nav_groups');
+ $this->out->menuItem(common_local_url('groups'), _('Groups'),
+ _('User groups'), $action_name == 'groups', 'nav_groups');
- $this->out->menuItem(common_local_url('publictagcloud'), _('Recent tags'),
- _('Recent tags'), $action_name == 'publictagcloud', 'nav_recent-tags');
+ $this->out->menuItem(common_local_url('publictagcloud'), _('Recent tags'),
+ _('Recent tags'), $action_name == 'publictagcloud', 'nav_recent-tags');
- if (count(common_config('nickname', 'featured')) > 0) {
- $this->out->menuItem(common_local_url('featured'), _('Featured'),
- _('Featured users'), $action_name == 'featured', 'nav_featured');
- }
+ if (count(common_config('nickname', 'featured')) > 0) {
+ $this->out->menuItem(common_local_url('featured'), _('Featured'),
+ _('Featured users'), $action_name == 'featured', 'nav_featured');
+ }
- $this->out->menuItem(common_local_url('favorited'), _('Popular'),
- _("Popular notices"), $action_name == 'favorited', 'nav_timeline_favorited');
+ $this->out->menuItem(common_local_url('favorited'), _('Popular'),
+ _("Popular notices"), $action_name == 'favorited', 'nav_timeline_favorited');
+ Event::handle('EndPublicGroupNav', array($this));
+ }
$this->action->elementEnd('ul');
}
}
diff --git a/lib/router.php b/lib/router.php
new file mode 100644
index 000000000..41c376a72
--- /dev/null
+++ b/lib/router.php
@@ -0,0 +1,438 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * URL routing utilities
+ *
+ * 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 URL
+ * @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);
+}
+
+require_once 'Net/URL/Mapper.php';
+
+/**
+ * URL Router
+ *
+ * Cheap wrapper around Net_URL_Mapper
+ *
+ * @category URL
+ * @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 Router
+{
+ var $m = null;
+ static $inst = null;
+
+ static function get()
+ {
+ if (!Router::$inst) {
+ Router::$inst = new Router();
+ }
+ return Router::$inst;
+ }
+
+ function __construct()
+ {
+ if (!$this->m) {
+ $this->m = $this->initialize();
+ }
+ }
+
+ function initialize() {
+
+ $m = Net_URL_Mapper::getInstance();
+
+ // In the "root"
+
+ $m->connect('', array('action' => 'public'));
+ $m->connect('rss', array('action' => 'publicrss'));
+ $m->connect('xrds', array('action' => 'publicxrds'));
+ $m->connect('featuredrss', array('action' => 'featuredrss'));
+ $m->connect('favoritedrss', array('action' => 'favoritedrss'));
+ $m->connect('opensearch/people', array('action' => 'opensearch',
+ 'type' => 'people'));
+ $m->connect('opensearch/notice', array('action' => 'opensearch',
+ 'type' => 'notice'));
+
+ // docs
+
+ $m->connect('doc/:title', array('action' => 'doc'));
+
+ // facebook
+
+ $m->connect('facebook', array('action' => 'facebookhome'));
+ $m->connect('facebook/index.php', array('action' => 'facebookhome'));
+ $m->connect('facebook/settings.php', array('action' => 'facebooksettings'));
+ $m->connect('facebook/invite.php', array('action' => 'facebookinvite'));
+ $m->connect('facebook/remove', array('action' => 'facebookremove'));
+
+ // main stuff is repetitive
+
+ $main = array('login', 'logout', 'register', 'subscribe',
+ 'unsubscribe', 'confirmaddress', 'recoverpassword',
+ 'invite', 'favor', 'disfavor', 'sup',
+ 'block');
+
+ foreach ($main as $a) {
+ $m->connect('main/'.$a, array('action' => $a));
+ }
+
+ $m->connect('main/tagother/:id', array('action' => 'tagother'));
+
+ // these take a code
+
+ foreach (array('register', 'confirmaddress', 'recoverpassword') as $c) {
+ $m->connect('main/'.$c.'/:code', array('action' => $c));
+ }
+
+ // exceptional
+
+ $m->connect('main/openid', array('action' => 'openidlogin'));
+ $m->connect('main/remote', array('action' => 'remotesubscribe'));
+ $m->connect('main/remote?nickname=:nickname', array('action' => 'remotesubscribe'), array('nickname' => '[A-Za-z0-9_-]+'));
+
+ foreach (array('requesttoken', 'accesstoken', 'userauthorization',
+ 'postnotice', 'updateprofile', 'finishremotesubscribe') as $action) {
+ $m->connect('index.php?action=' . $action, array('action' => $action));
+ }
+
+ // settings
+
+ foreach (array('profile', 'avatar', 'password', 'openid', 'im',
+ 'email', 'sms', 'twitter', 'other') as $s) {
+ $m->connect('settings/'.$s, array('action' => $s.'settings'));
+ }
+
+ // search
+
+ foreach (array('group', 'people', 'notice') as $s) {
+ $m->connect('search/'.$s, array('action' => $s.'search'));
+ $m->connect('search/'.$s.'?q=:q', array('action' => $s.'search'), array('q' => '.+'));
+ }
+
+ $m->connect('search/notice/rss', array('action' => 'noticesearchrss'));
+
+ // notice
+
+ $m->connect('notice/new', array('action' => 'newnotice'));
+ $m->connect('notice/new?replyto=:replyto',
+ array('action' => 'newnotice'),
+ array('replyto' => '[A-Za-z0-9_-]+'));
+ $m->connect('notice/:notice',
+ array('action' => 'shownotice'),
+ array('notice' => '[0-9]+'));
+ $m->connect('notice/delete', array('action' => 'deletenotice'));
+ $m->connect('notice/delete/:notice',
+ array('action' => 'deletenotice'),
+ array('notice' => '[0-9]+'));
+
+ $m->connect('message/new', array('action' => 'newmessage'));
+ $m->connect('message/new?to=:to', array('action' => 'newmessage'), array('to' => '[A-Za-z0-9_-]'));
+ $m->connect('message/:message',
+ array('action' => 'showmessage'),
+ array('message' => '[0-9]+'));
+
+ $m->connect('user/:id',
+ array('action' => 'userbyid'),
+ array('id' => '[0-9]+'));
+
+ $m->connect('tags/', array('action' => 'publictagcloud'));
+ $m->connect('tag/', array('action' => 'publictagcloud'));
+ $m->connect('tags', array('action' => 'publictagcloud'));
+ $m->connect('tag', array('action' => 'publictagcloud'));
+ $m->connect('tag/:tag/rss',
+ array('action' => 'tagrss'),
+ array('tag' => '[a-zA-Z0-9]+'));
+ $m->connect('tag/:tag',
+ array('action' => 'tag'),
+ array('tag' => '[a-zA-Z0-9]+'));
+
+ $m->connect('peopletag/:tag',
+ array('action' => 'peopletag'),
+ array('tag' => '[a-zA-Z0-9]+'));
+
+ $m->connect('featured/', array('action' => 'featured'));
+ $m->connect('featured', array('action' => 'featured'));
+ $m->connect('favorited/', array('action' => 'favorited'));
+ $m->connect('favorited', array('action' => 'favorited'));
+
+ // groups
+
+ $m->connect('group/new', array('action' => 'newgroup'));
+
+ foreach (array('edit', 'join', 'leave') as $v) {
+ $m->connect('group/:nickname/'.$v,
+ array('action' => $v.'group'),
+ array('nickname' => '[a-zA-Z0-9]+'));
+ }
+
+ foreach (array('members', 'logo', 'rss') as $n) {
+ $m->connect('group/:nickname/'.$n,
+ array('action' => 'group'.$n),
+ array('nickname' => '[a-zA-Z0-9]+'));
+ }
+
+ $m->connect('group/:id/id',
+ array('action' => 'groupbyid'),
+ array('id' => '[0-9]+'));
+
+ $m->connect('group/:nickname',
+ array('action' => 'showgroup'),
+ array('nickname' => '[a-zA-Z0-9]+'));
+
+ $m->connect('group/', array('action' => 'groups'));
+ $m->connect('group', array('action' => 'groups'));
+ $m->connect('groups/', array('action' => 'groups'));
+ $m->connect('groups', array('action' => 'groups'));
+
+ // Twitter-compatible API
+
+ // statuses API
+
+ $m->connect('api/statuses/:method',
+ array('action' => 'api',
+ 'apiaction' => 'statuses'),
+ array('method' => '(public_timeline|friends_timeline|user_timeline|update|replies|friends|followers|featured)(\.(atom|rss|xml|json))?'));
+
+ $m->connect('api/statuses/:method/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'statuses'),
+ array('method' => '(user_timeline|friends_timeline|show|destroy|friends|followers)'));
+
+ // users
+
+ $m->connect('api/users/:method/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'users'),
+ array('method' => 'show(\.(xml|json))?'));
+
+ $m->connect('api/users/:method',
+ array('action' => 'api',
+ 'apiaction' => 'users'),
+ array('method' => 'show(\.(xml|json))?'));
+
+ // direct messages
+
+ foreach (array('xml', 'json') as $e) {
+ $m->connect('api/direct_messages/new.'.$e,
+ array('action' => 'api',
+ 'apiaction' => 'direct_messages',
+ 'method' => 'create.'.$e));
+ }
+
+ foreach (array('xml', 'json', 'rss', 'atom') as $e) {
+ $m->connect('api/direct_messages.'.$e,
+ array('action' => 'api',
+ 'apiaction' => 'direct_messages',
+ 'method' => 'direct_messages.'.$e));
+ }
+
+ foreach (array('xml', 'json', 'rss', 'atom') as $e) {
+ $m->connect('api/direct_message/sent.'.$e,
+ array('action' => 'api',
+ 'apiaction' => 'direct_messages',
+ 'method' => 'sent.'.$e));
+ }
+
+ $m->connect('api/direct_messages/destroy/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'direct_messages'));
+
+ // friendships
+
+ $m->connect('api/friendships/:method/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'friendships'),
+ array('method' => '(create|destroy)'));
+
+ $m->connect('api/friendships/:method',
+ array('action' => 'api',
+ 'apiaction' => 'friendships'),
+ array('method' => 'exists(\.(xml|json|rss|atom))'));
+
+
+ // Social graph
+
+ $m->connect('api/friends/ids/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'statuses',
+ 'method' => 'friendsIDs'));
+
+ foreach (array('xml', 'json') as $e) {
+ $m->connect('api/friends/ids.'.$e,
+ array('action' => 'api',
+ 'apiaction' => 'statuses',
+ 'method' => 'friendsIDs.'.$e));
+ }
+
+ $m->connect('api/followers/ids/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'statuses',
+ 'method' => 'followersIDs'));
+
+ foreach (array('xml', 'json') as $e) {
+ $m->connect('api/followers/ids.'.$e,
+ array('action' => 'api',
+ 'apiaction' => 'statuses',
+ 'method' => 'followersIDs.'.$e));
+ }
+
+ // account
+
+ $m->connect('api/account/:method',
+ array('action' => 'api',
+ 'apiaction' => 'account'));
+
+ // favorites
+
+ $m->connect('api/favorites/:method/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'favorites'));
+
+ $m->connect('api/favorites/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'favorites',
+ 'method' => 'favorites'));
+
+ foreach (array('xml', 'json', 'rss', 'atom') as $e) {
+ $m->connect('api/favorites.'.$e,
+ array('action' => 'api',
+ 'apiaction' => 'favorites',
+ 'method' => 'favorites.'.$e));
+ }
+
+ // notifications
+
+ $m->connect('api/notifications/:method/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'favorites'));
+
+ // blocks
+
+ $m->connect('api/blocks/:method/:argument',
+ array('action' => 'api',
+ 'apiaction' => 'blocks'));
+
+ // help
+
+ $m->connect('api/help/:method',
+ array('action' => 'api',
+ 'apiaction' => 'help'));
+
+ // laconica
+
+ $m->connect('api/laconica/:method',
+ array('action' => 'api',
+ 'apiaction' => 'laconica'));
+
+
+ // search
+
+ foreach (array('json', 'atom') as $e) {
+ $m->connect('api/search.'.$e,
+ array('action' => 'twitapisearch'));
+ }
+
+ $m->connect('api/trends.json', array('action' => 'twitapitrends'));
+
+ // user stuff
+
+ foreach (array('subscriptions', 'subscribers',
+ 'nudge', 'xrds', 'all', 'foaf',
+ 'replies', 'inbox', 'outbox', 'microsummary') as $a) {
+ $m->connect(':nickname/'.$a,
+ array('action' => $a),
+ array('nickname' => '[a-zA-Z0-9]{1,64}'));
+ }
+
+ foreach (array('subscriptions', 'subscribers') as $a) {
+ $m->connect(':nickname/'.$a.'/:tag',
+ array('action' => $a),
+ array('tag' => '[a-zA-Z0-9]+',
+ 'nickname' => '[a-zA-Z0-9]{1,64}'));
+ }
+
+ foreach (array('rss', 'groups') as $a) {
+ $m->connect(':nickname/'.$a,
+ array('action' => 'user'.$a),
+ array('nickname' => '[a-zA-Z0-9]{1,64}'));
+ }
+
+ foreach (array('all', 'replies', 'favorites') as $a) {
+ $m->connect(':nickname/'.$a.'/rss',
+ array('action' => $a.'rss'),
+ array('nickname' => '[a-zA-Z0-9]{1,64}'));
+ }
+
+ $m->connect(':nickname/favorites',
+ array('action' => 'showfavorites'),
+ array('nickname' => '[a-zA-Z0-9]{1,64}'));
+
+ $m->connect(':nickname/avatar/:size',
+ array('action' => 'avatarbynickname'),
+ array('size' => '(original|96|48|24)',
+ 'nickname' => '[a-zA-Z0-9]{1,64}'));
+
+ $m->connect(':nickname',
+ array('action' => 'showstream'),
+ array('nickname' => '[a-zA-Z0-9]{1,64}'));
+
+ Event::handle('RouterInitialized', array($m));
+
+ return $m;
+ }
+
+ function map($path)
+ {
+ try {
+ $match = $this->m->match($path);
+ } catch (Net_URL_Mapper_InvalidException $e) {
+ common_log(LOG_ERR, "Problem getting route for $path - " .
+ $e->getMessage());
+ $cac = new ClientErrorAction("Page not found.", 404);
+ $cac->showPage();
+ }
+
+ return $match;
+ }
+
+ function build($action, $args=null, $params=null, $fragment=null)
+ {
+ $action_arg = array('action' => $action);
+
+ if ($args) {
+ $args = array_merge($action_arg, $args);
+ } else {
+ $args = $action_arg;
+ }
+
+ return $this->m->generate($args, $params, $fragment);
+ }
+}
diff --git a/lib/rssaction.php b/lib/rssaction.php
index f19c8e1c5..66c2d9e8c 100644
--- a/lib/rssaction.php
+++ b/lib/rssaction.php
@@ -38,6 +38,7 @@ class Rss10Action extends Action
var $creators = array();
var $limit = DEFAULT_RSS_LIMIT;
+ var $notices = null;
/**
* Constructor
@@ -93,6 +94,9 @@ class Rss10Action extends Action
function handle($args)
{
+ // Get the list of notices
+ $this->notices = $this->getNotices();
+ // Parent handling, including cache check
parent::handle($args);
$this->showRss($this->limit);
}
@@ -178,11 +182,11 @@ class Rss10Action extends Action
$image = $this->getImage();
if ($image) {
$channel = $this->getChannel();
- common_element_start('image', array('rdf:about' => $image));
- common_element('title', null, $channel['title']);
- common_element('link', null, $channel['link']);
- common_element('url', null, $image);
- common_element_end('image');
+ $this->elementStart('image', array('rdf:about' => $image));
+ $this->element('title', null, $channel['title']);
+ $this->element('link', null, $channel['link']);
+ $this->element('url', null, $image);
+ $this->elementEnd('image');
}
}
@@ -199,7 +203,7 @@ class Rss10Action extends Action
$this->element('dc:date', null, common_date_w3dtf($notice->created));
$this->element('dc:creator', null, ($profile->fullname) ? $profile->fullname : $profile->nickname);
$this->element('sioc:has_creator', array('rdf:resource' => $creator_uri));
- $this->element('laconica:postIcon', array('rdf:resource' => common_profile_avatar_url($profile)));
+ $this->element('laconica:postIcon', array('rdf:resource' => $profile->avatarUrl()));
$this->element('cc:licence', array('rdf:resource' => common_config('license', 'url')));
$this->elementEnd('item');
$this->creators[$creator_uri] = $profile;
@@ -216,7 +220,7 @@ class Rss10Action extends Action
$this->element('foaf:name', null, $profile->fullname);
}
$this->element('sioc:id', null, $id);
- $avatar = common_profile_avatar_url($profile);
+ $avatar = $profile->avatarUrl();
$this->element('sioc:avatar', array('rdf:resource' => $avatar));
$this->elementEnd('sioc:User');
}
@@ -258,5 +262,25 @@ class Rss10Action extends Action
{
$this->elementEnd('rdf:RDF');
}
+
+ /**
+ * When was this page last modified?
+ *
+ */
+
+ function lastModified()
+ {
+ if (empty($this->notices)) {
+ return null;
+ }
+
+ if (count($this->notices) == 0) {
+ return null;
+ }
+
+ // FIXME: doesn't handle modified profiles, avatars, deleted notices
+
+ return strtotime($this->notices[0]->created);
+ }
}
diff --git a/lib/searchaction.php b/lib/searchaction.php
index fdfb8dc5a..df6876445 100644
--- a/lib/searchaction.php
+++ b/lib/searchaction.php
@@ -79,10 +79,11 @@ class SearchAction extends Action
function showTop($arr=null)
{
+ $error = null;
if ($arr) {
$error = $arr[1];
}
- if ($error) {
+ if (!empty($error)) {
$this->element('p', 'error', $error);
} else {
$instr = $this->getInstructions();
diff --git a/lib/section.php b/lib/section.php
index 0c32ddcf8..d14575086 100644
--- a/lib/section.php
+++ b/lib/section.php
@@ -103,6 +103,6 @@ class Section extends Widget
function moreTitle()
{
- return null;
+ return _('More...');
}
}
diff --git a/lib/servererroraction.php b/lib/servererroraction.php
index a39886591..595dcf147 100644
--- a/lib/servererroraction.php
+++ b/lib/servererroraction.php
@@ -42,7 +42,7 @@ require_once INSTALLDIR.'/lib/error.php';
* says that 500 errors should be treated similarly to 400 errors, and
* it's easier to give an HTML response. Maybe we can customize these
* to display some funny animal cartoons. If not, we can probably role
- * these classes up into a single class.
+ * these classes up into a single class.
*
* See: http://tools.ietf.org/html/rfc2616#section-10
*
@@ -57,19 +57,19 @@ class ServerErrorAction extends ErrorAction
function __construct($message='Error', $code=500)
{
parent::__construct($message, $code);
-
+
$this->status = array(500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported');
-
+
$this->default = 500;
}
-
+
// XXX: Should these error actions even be invokable via URI?
-
+
function handle($args)
{
parent::handle($args);
@@ -81,12 +81,16 @@ class ServerErrorAction extends ErrorAction
}
$this->message = $this->trimmed('message');
-
+
if (!$this->message) {
- $this->message = "Server Error $this->code";
- }
+ $this->message = "Server Error $this->code";
+ }
$this->showPage();
}
-
+
+ function title()
+ {
+ return $this->status[$this->code];
+ }
}
diff --git a/lib/serverexception.php b/lib/serverexception.php
new file mode 100644
index 000000000..b8ed6846e
--- /dev/null
+++ b/lib/serverexception.php
@@ -0,0 +1,55 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * class for a server exception (user error)
+ *
+ * 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 Exception
+ * @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);
+}
+
+/**
+ * Class for server exceptions
+ *
+ * Subclass of PHP Exception for server errors. The user typically can't fix these.
+ *
+ * @category Exception
+ * @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 ServerException extends Exception
+{
+ public function __construct($message = null, $code = 400) {
+ parent::__construct($message, $code);
+ }
+
+ public function __toString() {
+ return __CLASS__ . ": [{$this->code}]: {$this->message}\n";
+ }
+}
diff --git a/lib/settingsaction.php b/lib/settingsaction.php
index dfe1f114b..53c807c6f 100644
--- a/lib/settingsaction.php
+++ b/lib/settingsaction.php
@@ -76,7 +76,12 @@ class SettingsAction extends Action
// change important settings or see private info, and
// _all_ our settings are important
common_set_returnto($this->selfUrl());
- common_redirect(common_local_url('login'));
+ $user = common_current_user();
+ if ($user->hasOpenID()) {
+ common_redirect(common_local_url('openidlogin'));
+ } else {
+ common_redirect(common_local_url('login'));
+ }
} else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$this->handlePost();
} else {
diff --git a/lib/twitter.php b/lib/twitter.php
index 197298549..deb6fd276 100644
--- a/lib/twitter.php
+++ b/lib/twitter.php
@@ -19,6 +19,8 @@
if (!defined('LACONICA')) { exit(1); }
+define("TWITTER_SERVICE", 1); // Twitter is foreign_service ID 1
+
function get_twitter_data($uri, $screen_name, $password)
{
@@ -28,14 +30,13 @@ function get_twitter_data($uri, $screen_name, $password)
CURLOPT_FAILONERROR => true,
CURLOPT_HEADER => false,
CURLOPT_FOLLOWLOCATION => true,
- # CURLOPT_USERAGENT => "identi.ca",
+ CURLOPT_USERAGENT => "Laconica",
CURLOPT_CONNECTTIMEOUT => 120,
CURLOPT_TIMEOUT => 120,
# Twitter is strict about accepting invalid "Expect" headers
CURLOPT_HTTPHEADER => array('Expect:')
);
-
$ch = curl_init($uri);
curl_setopt_array($ch, $options);
$data = curl_exec($ch);
@@ -95,7 +96,7 @@ function add_twitter_user($twitter_id, $screen_name)
$fuser->nickname = $screen_name;
$fuser->uri = 'http://twitter.com/' . $screen_name;
$fuser->id = $twitter_id;
- $fuser->service = 1; // Twitter
+ $fuser->service = TWITTER_SERVICE; // Twitter
$fuser->created = common_sql_now();
$result = $fuser->insert();
@@ -206,3 +207,93 @@ function save_twitter_friends($user, $twitter_id, $screen_name, $password)
return true;
}
+function is_twitter_bound($notice, $flink) {
+
+ // Check to see if notice should go to Twitter
+ if (($flink->noticesync & FOREIGN_NOTICE_SEND)) {
+
+ // If it's not a Twitter-style reply, or if the user WANTS to send replies.
+ if (!preg_match('/^@[a-zA-Z0-9_]{1,15}\b/u', $notice->content) ||
+ ($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+function broadcast_twitter($notice)
+{
+ global $config;
+ $success = true;
+
+ $flink = Foreign_link::getByUserID($notice->profile_id,
+ TWITTER_SERVICE);
+
+ // XXX: Not sure WHERE to check whether a notice should go to
+ // Twitter. Should we even put in the queue if it shouldn't? --Zach
+ if (is_twitter_bound($notice, $flink)) {
+
+ $fuser = $flink->getForeignUser();
+ $twitter_user = $fuser->nickname;
+ $twitter_password = $flink->credentials;
+ $uri = 'http://www.twitter.com/statuses/update.json';
+
+ // XXX: Hack to get around PHP cURL's use of @ being a a meta character
+ $statustxt = preg_replace('/^@/', ' @', $notice->content);
+
+ $options = array(
+ CURLOPT_USERPWD => "$twitter_user:$twitter_password",
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS =>
+ array(
+ 'status' => $statustxt,
+ 'source' => $config['integration']['source']
+ ),
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_FAILONERROR => true,
+ CURLOPT_HEADER => false,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_USERAGENT => "Laconica",
+ CURLOPT_CONNECTTIMEOUT => 120, // XXX: How long should this be?
+ CURLOPT_TIMEOUT => 120,
+
+ # Twitter is strict about accepting invalid "Expect" headers
+ CURLOPT_HTTPHEADER => array('Expect:')
+ );
+
+ $ch = curl_init($uri);
+ curl_setopt_array($ch, $options);
+ $data = curl_exec($ch);
+ $errmsg = curl_error($ch);
+
+ if ($errmsg) {
+ common_debug("cURL error: $errmsg - " .
+ "trying to send notice for $twitter_user.",
+ __FILE__);
+ $success = false;
+ }
+
+ curl_close($ch);
+
+ if (!$data) {
+ common_debug("No data returned by Twitter's " .
+ "API trying to send update for $twitter_user",
+ __FILE__);
+ $success = false;
+ }
+
+ // Twitter should return a status
+ $status = json_decode($data);
+
+ if (!$status->id) {
+ common_debug("Unexpected data returned by Twitter " .
+ " API trying to send update for $twitter_user",
+ __FILE__);
+ $success = false;
+ }
+ }
+
+ return $success;
+}
+
diff --git a/lib/twitterapi.php b/lib/twitterapi.php
index da8b8b1e5..74f265cbb 100644
--- a/lib/twitterapi.php
+++ b/lib/twitterapi.php
@@ -43,7 +43,7 @@ class TwitterapiAction extends Action
$avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
- $twitter_user['profile_image_url'] = ($avatar) ? common_avatar_display_url($avatar) : common_default_avatar(AVATAR_STREAM_SIZE);
+ $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_STREAM_SIZE);
$twitter_user['protected'] = 'false'; # not supported by Laconica yet
$twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
@@ -60,20 +60,34 @@ class TwitterapiAction extends Action
function twitter_status_array($notice, $include_user=true)
{
-
$profile = $notice->getProfile();
$twitter_status = array();
$twitter_status['text'] = $notice->content;
$twitter_status['truncated'] = 'false'; # Not possible on Laconica
$twitter_status['created_at'] = $this->date_twitter($notice->created);
- $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ? intval($notice->reply_to) : null;
+ $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
+ intval($notice->reply_to) : null;
$twitter_status['source'] = $this->source_link($notice->source);
$twitter_status['id'] = intval($notice->id);
- $twitter_status['in_reply_to_user_id'] = ($notice->reply_to) ? $this->replier_by_reply(intval($notice->reply_to)) : null;
+
+ $replier_profile = null;
+
+ if ($notice->reply_to) {
+ $reply = Notice::staticGet(intval($notice->reply_to));
+ if ($reply) {
+ $replier_profile = $reply->getProfile();
+ }
+ }
+
+ $twitter_status['in_reply_to_user_id'] =
+ ($replier_profile) ? intval($replier_profile->id) : null;
+ $twitter_status['in_reply_to_screen_name'] =
+ ($replier_profile) ? $replier_profile->nickname : null;
if (isset($this->auth_user)) {
- $twitter_status['favorited'] = ($this->auth_user->hasFave($notice)) ? 'true' : 'false';
+ $twitter_status['favorited'] =
+ ($this->auth_user->hasFave($notice)) ? 'true' : 'false';
} else {
$twitter_status['favorited'] = 'false';
}
@@ -137,7 +151,6 @@ class TwitterapiAction extends Action
function twitter_dmsg_array($message)
{
-
$twitter_dm = array();
$from_profile = $message->getFrom();
@@ -386,23 +399,7 @@ class TwitterapiAction extends Action
$t = strtotime($dt);
return date("D M d G:i:s O Y", $t);
}
-
- function replier_by_reply($reply_id)
- {
- $notice = Notice::staticGet($reply_id);
- if ($notice) {
- $profile = $notice->getProfile();
- if ($profile) {
- return intval($profile->id);
- } else {
- common_debug('Can\'t find a profile for notice: ' . $notice->id, __FILE__);
- }
- } else {
- common_debug("Can't get notice: $reply_id", __FILE__);
- }
- return null;
- }
-
+
// XXX: Candidate for a general utility method somewhere?
function count_subscriptions($profile)
{
diff --git a/lib/util.php b/lib/util.php
index 419e21e82..9637dc506 100644
--- a/lib/util.php
+++ b/lib/util.php
@@ -81,7 +81,7 @@ function common_language()
// If there is a user logged in and they've set a language preference
// then return that one...
- if (common_logged_in()) {
+ if (_have_config() && common_logged_in()) {
$user = common_current_user();
$user_language = $user->language;
if ($user_language)
@@ -315,6 +315,10 @@ function common_current_user()
{
global $_cur;
+ if (!_have_config()) {
+ return null;
+ }
+
if ($_cur === false) {
if (isset($_REQUEST[session_name()]) || (isset($_SESSION['userid']) && $_SESSION['userid'])) {
@@ -370,8 +374,6 @@ function common_canonical_email($email)
return $email;
}
-define('URL_REGEX', '^|[ \t\r\n])((ftp|http|https|gopher|mailto|news|nntp|telnet|wais|file|prospero|aim|webcal):(([A-Za-z0-9$_.+!*(),;/?:@&~=-])|%[A-Fa-f0-9]{2}){2,}(#([a-zA-Z0-9][a-zA-Z0-9$_.+!*(),;/?:@&~=%-]*))?([A-Za-z0-9$_+!*();/?:~-]))');
-
function common_render_content($text, $notice)
{
$r = common_render_text($text);
@@ -388,45 +390,114 @@ function common_render_text($text)
$r = htmlspecialchars($text);
$r = preg_replace('/[\x{0}-\x{8}\x{b}-\x{c}\x{e}-\x{19}]/', '', $r);
- $r = preg_replace_callback('@https?://[^\]>\s]+@', 'common_render_uri_thingy', $r);
- $r = preg_replace('/(^|\s+)#([A-Za-z0-9_\-\.]{1,64})/e', "'\\1#'.common_tag_link('\\2')", $r);
+ $r = common_replace_urls_callback($r, 'common_linkify');
+ $r = preg_replace('/(^|\(|\[|\s+)#([A-Za-z0-9_\-\.]{1,64})/e', "'\\1#'.common_tag_link('\\2')", $r);
// XXX: machine tags
return $r;
}
-function common_render_uri_thingy($matches)
-{
- $uri = $matches[0];
- $trailer = '';
+function common_replace_urls_callback($text, $callback) {
+ // Start off with a regex
+ $regex = '#'.
+ '(?:'.
+ '(?:'.
+ '(?:https?|ftps?|mms|rtsp|gopher|news|nntp|telnet|wais|file|prospero|webcal|xmpp|irc)://'.
+ '|'.
+ '(?:mailto|aim|tel):'.
+ ')'.
+ '[^.\s]+\.[^\s]+'.
+ '|'.
+ '(?:[^.\s/:]+\.)+'.
+ '(?:museum|travel|[a-z]{2,4})'.
+ '(?:[:/][^\s]*)?'.
+ ')'.
+ '#ix';
+ preg_match_all($regex, $text, $matches);
+
+ // Then clean up what the regex left behind
+ $offset = 0;
+ foreach($matches[0] as $orig_url) {
+ $url = htmlspecialchars_decode($orig_url);
+
+ // Make sure we didn't pick up an email address
+ if (preg_match('#^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$#i', $url)) continue;
+
+ // Remove surrounding punctuation
+ $url = trim($url, '.?!,;:\'"`([<');
+
+ // Remove surrounding parens and the like
+ preg_match('/[)\]>]+$/', $url, $trailing);
+ if (isset($trailing[0])) {
+ preg_match_all('/[(\[<]/', $url, $opened);
+ preg_match_all('/[)\]>]/', $url, $closed);
+ $unopened = count($closed[0]) - count($opened[0]);
+
+ // Make sure not to take off more closing parens than there are at the end
+ $unopened = ($unopened > mb_strlen($trailing[0])) ? mb_strlen($trailing[0]):$unopened;
+
+ $url = ($unopened > 0) ? mb_substr($url, 0, $unopened * -1):$url;
+ }
- // Some heuristics for extracting URIs from surrounding punctuation
- // Strip from trailing text...
- if (preg_match('/^(.*)([,.:"\']+)$/', $uri, $matches)) {
- $uri = $matches[1];
- $trailer = $matches[2];
- }
+ // Remove trailing punctuation again (in case there were some inside parens)
+ $url = rtrim($url, '.?!,;:\'"`');
+
+ // Make sure we didn't capture part of the next sentence
+ preg_match('#((?:[^.\s/]+\.)+)(museum|travel|[a-z]{2,4})#i', $url, $url_parts);
- $pairs = array(
- ']' => '[', // technically disallowed in URIs, but used in Java docs
- ')' => '(', // far too frequent in Wikipedia and MSDN
- );
- $final = substr($uri, -1, 1);
- if (isset($pairs[$final])) {
- $openers = substr_count($uri, $pairs[$final]);
- $closers = substr_count($uri, $final);
- if ($closers > $openers) {
- // Assume the paren was opened outside the URI
- $uri = substr($uri, 0, -1);
- $trailer = $final . $trailer;
+ // Were the parts capitalized any?
+ $last_part = (mb_strtolower($url_parts[2]) !== $url_parts[2]) ? true:false;
+ $prev_part = (mb_strtolower($url_parts[1]) !== $url_parts[1]) ? true:false;
+
+ // If the first part wasn't cap'd but the last part was, we captured too much
+ if ((!$prev_part && $last_part)) {
+ $url = mb_substr($url, 0 , mb_strpos($url, '.'.$url_parts['2'], 0));
}
+
+ // Capture the new TLD
+ preg_match('#((?:[^.\s/]+\.)+)(museum|travel|[a-z]{2,4})#i', $url, $url_parts);
+
+ $tlds = array('ac', 'ad', 'ae', 'aero', 'af', 'ag', 'ai', 'al', 'am', 'an', 'ao', 'aq', 'ar', 'arpa', 'as', 'asia', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd', 'be', 'bf', 'bg', 'bh', 'bi', 'biz', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv', 'bw', 'by', 'bz', 'ca', 'cat', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm', 'cn', 'co', 'com', 'coop', 'cr', 'cu', 'cv', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'edu', 'ee', 'eg', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo', 'fr', 'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gov', 'gp', 'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu', 'id', 'ie', 'il', 'im', 'in', 'info', 'int', 'io', 'iq', 'ir', 'is', 'it', 'je', 'jm', 'jo', 'jobs', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mg', 'mh', 'mil', 'mk', 'ml', 'mm', 'mn', 'mo', 'mobi', 'mp', 'mq', 'mr', 'ms', 'mt', 'mu', 'museum', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'name', 'nc', 'ne', 'net', 'nf', 'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 'om', 'org', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl', 'pm', 'pn', 'pr', 'pro', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw', 'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn', 'so', 'sr', 'st', 'su', 'sv', 'sy', 'sz', 'tc', 'td', 'tel', 'tf', 'tg', 'th', 'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'tp', 'tr', 'travel', 'tt', 'tv', 'tw', 'tz', 'ua', 'ug', 'uk', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf', 'ws', 'ye', 'yt', 'yu', 'za', 'zm', 'zw');
+
+ if (!in_array($url_parts[2], $tlds)) continue;
+
+ // Make sure we didn't capture a hash tag
+ if (strpos($url, '#') === 0) continue;
+
+ // Put the url back the way we found it.
+ $url = (mb_strpos($orig_url, htmlspecialchars($url)) === FALSE) ? $url:htmlspecialchars($url);
+
+ // Call user specified func
+ $modified_url = $callback($url);
+
+ // Replace it!
+ $start = mb_strpos($text, $url, $offset);
+ $text = mb_substr($text, 0, $start).$modified_url.mb_substr($text, $start + mb_strlen($url), mb_strlen($text));
+ $offset = $start + mb_strlen($modified_url);
+ }
+
+ return $text;
+}
+
+function common_linkify($url) {
+ // It comes in special'd, so we unspecial it before passing to the stringifying
+ // functions
+ $ext = pathinfo($url, PATHINFO_EXTENSION);
+ $url = htmlspecialchars_decode($url);
+ $video_ext = array('mp4', 'flv', 'avi', 'mpg', 'mp3', 'ogg');
+ $display = $url;
+ $url = (!preg_match('#^([a-z]+://|(mailto|aim|tel):)#i', $url)) ? 'http://'.$url : $url;
+
+ $attrs = array('href' => $url, 'rel' => 'external');
+
+ if (in_array($ext, $video_ext)) {
+ $attrs['class'] = 'media';
}
- if ($longurl = common_longurl($uri)) {
- $longurl = htmlentities($longurl, ENT_QUOTES, 'UTF-8');
- $title = " title='$longurl'";
+
+ if ($longurl = common_longurl($url)) {
+ $attrs['title'] = $longurl;
}
- else $title = '';
- return '<a href="' . $uri . '"' . $title . ' class="extlink">' . $uri . '</a>' . $trailer;
+ return XMLStringer::estring('a', $attrs, $display);
}
function common_longurl($short_url)
@@ -450,7 +521,7 @@ function common_shorten_links($text)
static $cache = array();
if (isset($cache[$text])) return $cache[$text];
// \s = not a horizontal whitespace character (since PHP 5.2.4)
- return $cache[$text] = preg_replace('@https?://[^)\]>\s]+@e', "common_shorten_link('\\0')", $text);
+ return $cache[$text] = common_replace_urls_callback($text, 'common_shorten_link');;
}
function common_shorten_link($url, $reverse = false)
@@ -527,7 +598,13 @@ function common_tag_link($tag)
{
$canonical = common_canonical_tag($tag);
$url = common_local_url('tag', array('tag' => $canonical));
- return '<span class="tag"><a href="' . htmlspecialchars($url) . '" rel="tag">' . htmlspecialchars($tag) . '</a></span>';
+ $xs = new XMLStringer();
+ $xs->elementStart('span', 'tag');
+ $xs->element('a', array('href' => $url,
+ 'rel' => 'tag'),
+ $tag);
+ $xs->elementEnd('span');
+ return $xs->getString();
}
function common_canonical_tag($tag)
@@ -545,7 +622,20 @@ function common_at_link($sender_id, $nickname)
$sender = Profile::staticGet($sender_id);
$recipient = common_relative_profile($sender, common_canonical_nickname($nickname));
if ($recipient) {
- return '<span class="vcard"><a href="'.htmlspecialchars($recipient->profileurl).'" class="url"><span class="fn nickname">'.$nickname.'</span></a></span>';
+ $user = User::staticGet('id', $recipient->id);
+ if ($user) {
+ $url = common_local_url('userbyid', array('id' => $user->id));
+ } else {
+ $url = $recipient->profileurl;
+ }
+ $xs = new XMLStringer(false);
+ $xs->elementStart('span', 'vcard');
+ $xs->elementStart('a', array('href' => $url,
+ 'class' => 'url'));
+ $xs->element('span', 'fn nickname', $nickname);
+ $xs->elementEnd('a');
+ $xs->elementEnd('span');
+ return $xs->getString();
} else {
return $nickname;
}
@@ -556,7 +646,14 @@ function common_group_link($sender_id, $nickname)
$sender = Profile::staticGet($sender_id);
$group = User_group::staticGet('nickname', common_canonical_nickname($nickname));
if ($group && $sender->isMember($group)) {
- return '<span class="vcard"><a href="'.htmlspecialchars($group->permalink()).'" class="url"><span class="fn nickname">'.$nickname.'</span></a></span>';
+ $xs = new XMLStringer();
+ $xs->elementStart('span', 'vcard');
+ $xs->elementStart('a', array('href' => $group->permalink(),
+ 'class' => 'url'));
+ $xs->element('span', 'fn nickname', $nickname);
+ $xs->elementEnd('a');
+ $xs->elementEnd('span');
+ return $xs->getString();
} else {
return $nickname;
}
@@ -573,7 +670,13 @@ function common_at_hash_link($sender_id, $tag)
$url = common_local_url('subscriptions',
array('nickname' => $user->nickname,
'tag' => $tag));
- return '<span class="tag"><a href="'.htmlspecialchars($url).'" rel="tag">'.$tag.'</a></span>';
+ $xs = new XMLStringer();
+ $xs->elementStart('span', 'tag');
+ $xs->element('a', array('href' => $url,
+ 'rel' => $tag),
+ $tag);
+ $xs->elementEnd('span');
+ return $xs->getString();
} else {
return $tag;
}
@@ -615,315 +718,20 @@ function common_relative_profile($sender, $nickname, $dt=null)
return null;
}
-// where should the avatar go for this user?
-
-function common_avatar_filename($id, $extension, $size=null, $extra=null)
+function common_local_url($action, $args=null, $params=null, $fragment=null)
{
- global $config;
-
- if ($size) {
- return $id . '-' . $size . (($extra) ? ('-' . $extra) : '') . $extension;
- } else {
- return $id . '-original' . (($extra) ? ('-' . $extra) : '') . $extension;
+ $r = Router::get();
+ $path = $r->build($action, $args, $params, $fragment);
+ if ($path) {
}
-}
-
-function common_avatar_path($filename)
-{
- global $config;
- return INSTALLDIR . '/avatar/' . $filename;
-}
-
-function common_avatar_url($filename)
-{
- return common_path('avatar/'.$filename);
-}
-
-function common_avatar_display_url($avatar)
-{
- $server = common_config('avatar', 'server');
- if ($server) {
- return 'http://'.$server.'/'.$avatar->filename;
- } else {
- return $avatar->url;
- }
-}
-
-function common_default_avatar($size)
-{
- static $sizenames = array(AVATAR_PROFILE_SIZE => 'profile',
- AVATAR_STREAM_SIZE => 'stream',
- AVATAR_MINI_SIZE => 'mini');
- return theme_path('default-avatar-'.$sizenames[$size].'.png');
-}
-
-function common_local_url($action, $args=null, $fragment=null)
-{
- $url = null;
if (common_config('site','fancy')) {
- $url = common_fancy_url($action, $args);
+ $url = common_path(mb_substr($path, 1));
} else {
- $url = common_simple_url($action, $args);
- }
- if (!is_null($fragment)) {
- $url .= '#'.$fragment;
+ $url = common_path('index.php'.$path);
}
return $url;
}
-function common_fancy_url($action, $args=null)
-{
- switch (strtolower($action)) {
- case 'public':
- if ($args && isset($args['page'])) {
- return common_path('?page=' . $args['page']);
- } else {
- return common_path('');
- }
- case 'featured':
- if ($args && isset($args['page'])) {
- return common_path('featured?page=' . $args['page']);
- } else {
- return common_path('featured');
- }
- case 'favorited':
- if ($args && isset($args['page'])) {
- return common_path('favorited?page=' . $args['page']);
- } else {
- return common_path('favorited');
- }
- case 'publicrss':
- return common_path('rss');
- case 'publicatom':
- return common_path("api/statuses/public_timeline.atom");
- case 'publicxrds':
- return common_path('xrds');
- case 'featuredrss':
- return common_path('featuredrss');
- case 'favoritedrss':
- return common_path('favoritedrss');
- case 'opensearch':
- if ($args && $args['type']) {
- return common_path('opensearch/'.$args['type']);
- } else {
- return common_path('opensearch/people');
- }
- case 'doc':
- return common_path('doc/'.$args['title']);
- case 'block':
- case 'login':
- case 'logout':
- case 'subscribe':
- case 'unsubscribe':
- case 'invite':
- return common_path('main/'.$action);
- case 'tagother':
- return common_path('main/tagother?id='.$args['id']);
- case 'register':
- if ($args && $args['code']) {
- return common_path('main/register/'.$args['code']);
- } else {
- return common_path('main/register');
- }
- case 'remotesubscribe':
- if ($args && $args['nickname']) {
- return common_path('main/remote?nickname=' . $args['nickname']);
- } else {
- return common_path('main/remote');
- }
- case 'nudge':
- return common_path($args['nickname'].'/nudge');
- case 'openidlogin':
- return common_path('main/openid');
- case 'profilesettings':
- return common_path('settings/profile');
- case 'passwordsettings':
- return common_path('settings/password');
- case 'emailsettings':
- return common_path('settings/email');
- case 'openidsettings':
- return common_path('settings/openid');
- case 'smssettings':
- return common_path('settings/sms');
- case 'twittersettings':
- return common_path('settings/twitter');
- case 'othersettings':
- return common_path('settings/other');
- case 'deleteprofile':
- return common_path('settings/delete');
- case 'newnotice':
- if ($args && $args['replyto']) {
- return common_path('notice/new?replyto='.$args['replyto']);
- } else {
- return common_path('notice/new');
- }
- case 'shownotice':
- return common_path('notice/'.$args['notice']);
- case 'deletenotice':
- if ($args && $args['notice']) {
- return common_path('notice/delete/'.$args['notice']);
- } else {
- return common_path('notice/delete');
- }
- case 'microsummary':
- case 'xrds':
- case 'foaf':
- return common_path($args['nickname'].'/'.$action);
- case 'all':
- case 'replies':
- case 'inbox':
- case 'outbox':
- if ($args && isset($args['page'])) {
- return common_path($args['nickname'].'/'.$action.'?page=' . $args['page']);
- } else {
- return common_path($args['nickname'].'/'.$action);
- }
- case 'subscriptions':
- case 'subscribers':
- $nickname = $args['nickname'];
- unset($args['nickname']);
- if (isset($args['tag'])) {
- $tag = $args['tag'];
- unset($args['tag']);
- }
- $params = http_build_query($args);
- if ($params) {
- return common_path($nickname.'/'.$action . (($tag) ? '/' . $tag : '') . '?' . $params);
- } else {
- return common_path($nickname.'/'.$action . (($tag) ? '/' . $tag : ''));
- }
- case 'allrss':
- return common_path($args['nickname'].'/all/rss');
- case 'repliesrss':
- return common_path($args['nickname'].'/replies/rss');
- case 'userrss':
- if (isset($args['limit']))
- return common_path($args['nickname'].'/rss?limit=' . $args['limit']);
- return common_path($args['nickname'].'/rss');
- case 'showstream':
- if ($args && isset($args['page'])) {
- return common_path($args['nickname'].'?page=' . $args['page']);
- } else {
- return common_path($args['nickname']);
- }
-
- case 'usertimeline':
- return common_path("api/statuses/user_timeline/".$args['nickname'].".atom");
- case 'confirmaddress':
- return common_path('main/confirmaddress/'.$args['code']);
- case 'userbyid':
- return common_path('user/'.$args['id']);
- case 'recoverpassword':
- $path = 'main/recoverpassword';
- if ($args['code']) {
- $path .= '/' . $args['code'];
- }
- return common_path($path);
- case 'imsettings':
- return common_path('settings/im');
- case 'avatarsettings':
- return common_path('settings/avatar');
- case 'groupsearch':
- return common_path('search/group' . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'peoplesearch':
- return common_path('search/people' . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'noticesearch':
- return common_path('search/notice' . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'noticesearchrss':
- return common_path('search/notice/rss' . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'avatarbynickname':
- return common_path($args['nickname'].'/avatar/'.$args['size']);
- case 'tag':
- $path = 'tag/' . $args['tag'];
- unset($args['tag']);
- return common_path($path . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'publictagcloud':
- return common_path('tags');
- case 'peopletag':
- $path = 'peopletag/' . $args['tag'];
- unset($args['tag']);
- return common_path($path . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'tags':
- return common_path('tags' . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'favor':
- return common_path('main/favor');
- case 'disfavor':
- return common_path('main/disfavor');
- case 'showfavorites':
- if ($args && isset($args['page'])) {
- return common_path($args['nickname'].'/favorites?page=' . $args['page']);
- } else {
- return common_path($args['nickname'].'/favorites');
- }
- case 'favoritesrss':
- return common_path($args['nickname'].'/favorites/rss');
- case 'showmessage':
- return common_path('message/' . $args['message']);
- case 'newmessage':
- return common_path('message/new' . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'api':
- // XXX: do fancy URLs for all the API methods
- switch (strtolower($args['apiaction'])) {
- case 'statuses':
- switch (strtolower($args['method'])) {
- case 'user_timeline.rss':
- return common_path('api/statuses/user_timeline/'.$args['argument'].'.rss');
- case 'user_timeline.atom':
- return common_path('api/statuses/user_timeline/'.$args['argument'].'.atom');
- case 'user_timeline.json':
- return common_path('api/statuses/user_timeline/'.$args['argument'].'.json');
- case 'user_timeline.xml':
- return common_path('api/statuses/user_timeline/'.$args['argument'].'.xml');
- default: return common_simple_url($action, $args);
- }
- default: return common_simple_url($action, $args);
- }
- case 'sup':
- if ($args && isset($args['seconds'])) {
- return common_path('main/sup?seconds='.$args['seconds']);
- } else {
- return common_path('main/sup');
- }
- case 'newgroup':
- return common_path('group/new');
- case 'showgroup':
- return common_path('group/'.$args['nickname']);
- case 'editgroup':
- return common_path('group/'.$args['nickname'].'/edit');
- case 'joingroup':
- return common_path('group/'.$args['nickname'].'/join');
- case 'leavegroup':
- return common_path('group/'.$args['nickname'].'/leave');
- case 'groupbyid':
- return common_path('group/'.$args['id'].'/id');
- case 'grouprss':
- return common_path('group/'.$args['nickname'].'/rss');
- case 'groupmembers':
- return common_path('group/'.$args['nickname'].'/members');
- case 'grouplogo':
- return common_path('group/'.$args['nickname'].'/logo');
- case 'usergroups':
- return common_path($args['nickname'].'/groups' . (($args) ? ('?' . http_build_query($args)) : ''));
- case 'groups':
- return common_path('group' . (($args) ? ('?' . http_build_query($args)) : ''));
- default:
- return common_simple_url($action, $args);
- }
-}
-
-function common_simple_url($action, $args=null)
-{
- global $config;
- /* XXX: pretty URLs */
- $extra = '';
- if ($args) {
- foreach ($args as $key => $value) {
- $extra .= "&${key}=${value}";
- }
- }
- return common_path("index.php?action=${action}${extra}");
-}
-
function common_path($relative)
{
global $config;
@@ -1032,24 +840,6 @@ function common_redirect($url, $code=307)
function common_broadcast_notice($notice, $remote=false)
{
-
- // Check to see if notice should go to Twitter
- $flink = Foreign_link::getByUserID($notice->profile_id, 1); // 1 == Twitter
- if (($flink->noticesync & FOREIGN_NOTICE_SEND) == FOREIGN_NOTICE_SEND) {
-
- // If it's not a Twitter-style reply, or if the user WANTS to send replies...
-
- if (!preg_match('/^@[a-zA-Z0-9_]{1,15}\b/u', $notice->content) ||
- (($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY) == FOREIGN_NOTICE_SEND_REPLY)) {
-
- $result = common_twitter_broadcast($notice, $flink);
-
- if (!$result) {
- common_debug('Unable to send notice: ' . $notice->id . ' to Twitter.', __FILE__);
- }
- }
- }
-
if (common_config('queue', 'enabled')) {
// Do it later!
return common_enqueue_notice($notice);
@@ -1058,73 +848,11 @@ function common_broadcast_notice($notice, $remote=false)
}
}
-function common_twitter_broadcast($notice, $flink)
-{
- global $config;
- $success = true;
- $fuser = $flink->getForeignUser();
- $twitter_user = $fuser->nickname;
- $twitter_password = $flink->credentials;
- $uri = 'http://www.twitter.com/statuses/update.json';
-
- // XXX: Hack to get around PHP cURL's use of @ being a a meta character
- $statustxt = preg_replace('/^@/', ' @', $notice->content);
-
- $options = array(
- CURLOPT_USERPWD => "$twitter_user:$twitter_password",
- CURLOPT_POST => true,
- CURLOPT_POSTFIELDS => array(
- 'status' => $statustxt,
- 'source' => $config['integration']['source']
- ),
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_FAILONERROR => true,
- CURLOPT_HEADER => false,
- CURLOPT_FOLLOWLOCATION => true,
- CURLOPT_USERAGENT => "Laconica",
- CURLOPT_CONNECTTIMEOUT => 120, // XXX: Scary!!!! How long should this be?
- CURLOPT_TIMEOUT => 120,
-
- # Twitter is strict about accepting invalid "Expect" headers
- CURLOPT_HTTPHEADER => array('Expect:')
- );
-
- $ch = curl_init($uri);
- curl_setopt_array($ch, $options);
- $data = curl_exec($ch);
- $errmsg = curl_error($ch);
-
- if ($errmsg) {
- common_debug("cURL error: $errmsg - trying to send notice for $twitter_user.",
- __FILE__);
- $success = false;
- }
-
- curl_close($ch);
-
- if (!$data) {
- common_debug("No data returned by Twitter's API trying to send update for $twitter_user",
- __FILE__);
- $success = false;
- }
-
- // Twitter should return a status
- $status = json_decode($data);
-
- if (!$status->id) {
- common_debug("Unexpected data returned by Twitter API trying to send update for $twitter_user",
- __FILE__);
- $success = false;
- }
-
- return $success;
-}
-
// Stick the notice on the queue
function common_enqueue_notice($notice)
{
- foreach (array('jabber', 'omb', 'sms', 'public', 'ping') as $transport) {
+ foreach (array('jabber', 'omb', 'sms', 'public', 'twitter', 'facebook', 'ping') as $transport) {
$qi = new Queue_item();
$qi->notice_id = $notice->id;
$qi->transport = $transport;
@@ -1140,23 +868,6 @@ function common_enqueue_notice($notice)
return $result;
}
-function common_dequeue_notice($notice)
-{
- $qi = Queue_item::staticGet($notice->id);
- if ($qi) {
- $result = $qi->delete();
- if (!$result) {
- $last_error = &PEAR::getStaticProperty('DB_DataObject','lastError');
- common_log(LOG_ERR, 'DB error deleting queue item: ' . $last_error->message);
- return false;
- }
- common_log(LOG_DEBUG, 'complete dequeueing notice ID = ' . $notice->id);
- return $result;
- } else {
- return false;
- }
-}
-
function common_real_broadcast($notice, $remote=false)
{
$success = true;
@@ -1188,6 +899,15 @@ function common_real_broadcast($notice, $remote=false)
common_log(LOG_ERR, 'Error in public broadcast for notice ' . $notice->id);
}
}
+ if ($success) {
+ $success = broadcast_twitter($notice);
+ if (!$success) {
+ common_log(LOG_ERR, 'Error in Twitter broadcast for notice ' . $notice->id);
+ }
+ }
+
+ // XXX: Do a real-time FB broadcast here?
+
// XXX: broadcast notices to other IM
return $success;
}
@@ -1406,7 +1126,7 @@ function common_negotiate_type($cprefs, $sprefs)
}
$bestq = 0;
- $besttype = "text/html";
+ $besttype = 'text/html';
foreach(array_keys($combine) as $type) {
if($combine[$type] > $bestq) {
@@ -1415,6 +1135,9 @@ function common_negotiate_type($cprefs, $sprefs)
}
}
+ if ('text/html' === $besttype) {
+ return "text/html; charset=utf-8";
+ }
return $besttype;
}
@@ -1483,16 +1206,6 @@ function common_markup_to_html($c)
return Markdown($c);
}
-function common_profile_avatar_url($profile, $size=AVATAR_PROFILE_SIZE)
-{
- $avatar = $profile->getAvatar($size);
- if ($avatar) {
- return common_avatar_display_url($avatar);
- } else {
- return common_default_avatar($size);
- }
-}
-
function common_profile_uri($profile)
{
if (!$profile) {
diff --git a/lib/xmlstringer.php b/lib/xmlstringer.php
new file mode 100644
index 000000000..951b13b67
--- /dev/null
+++ b/lib/xmlstringer.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Generator for in-memory XML
+ *
+ * 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 Output
+ * @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);
+}
+
+/**
+ * Create in-memory XML
+ *
+ * @category Output
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ * @see Action
+ * @see HTMLOutputter
+ */
+
+class XMLStringer extends XMLOutputter
+{
+ function __construct($indent=false)
+ {
+ $this->xw = new XMLWriter();
+ $this->xw->openMemory();
+ $this->xw->setIndent($indent);
+ }
+
+ function getString()
+ {
+ return $this->xw->outputMemory();
+ }
+
+ // utility for quickly creating XML-strings
+
+ static function estring($tag, $attrs=null, $content=null)
+ {
+ $xs = new XMLStringer();
+ $xs->element($tag, $attrs, $content);
+ return $xs->getString();
+ }
+} \ No newline at end of file