diff options
Diffstat (limited to 'lib')
70 files changed, 3863 insertions, 870 deletions
diff --git a/lib/Shorturl_api.php b/lib/Shorturl_api.php index d1fc5eb6d..22d5b4cb5 100644 --- a/lib/Shorturl_api.php +++ b/lib/Shorturl_api.php @@ -1,7 +1,7 @@ <?php /* * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -22,6 +22,7 @@ if (!defined('LACONICA')) { exit(1); } class ShortUrlApi { protected $service_url; + protected $long_limit = 27; function __construct($service_url) { diff --git a/lib/accountsettingsaction.php b/lib/accountsettingsaction.php index 46090b8c1..4ab50abce 100644 --- a/lib/accountsettingsaction.php +++ b/lib/accountsettingsaction.php @@ -115,6 +115,9 @@ class AccountSettingsNav extends Widget 'openidsettings' => array(_('OpenID'), _('Add or remove OpenIDs')), + 'userdesignsettings' => + array(_('Design'), + _('Design your profile')), 'othersettings' => array(_('Other'), _('Other options'))); diff --git a/lib/action.php b/lib/action.php index 7c7c52c2c..95ee10c64 100644 --- a/lib/action.php +++ b/lib/action.php @@ -242,6 +242,11 @@ class Action extends HTMLOutputter // lawsuit $this->element('script', array('type' => 'text/javascript', 'src' => common_path('js/jquery.form.js')), ' '); + + $this->element('script', array('type' => 'text/javascript', + 'src' => common_path('js/jquery.joverlay.min.js')), + ' '); + Event::handle('EndShowJQueryScripts', array($this)); } if (Event::handle('StartShowLaconicaScripts', array($this))) { @@ -378,15 +383,18 @@ class Action extends HTMLOutputter // lawsuit { $this->elementStart('address', array('id' => 'site_contact', 'class' => 'vcard')); - $this->elementStart('a', array('class' => 'url home bookmark', - 'href' => common_local_url('public'))); - if (common_config('site', 'logo') || file_exists(theme_file('logo.png'))) { - $this->element('img', array('class' => 'logo photo', - 'src' => (common_config('site', 'logo')) ? common_config('site', 'logo') : theme_path('logo.png'), - 'alt' => common_config('site', 'name'))); + if (Event::handle('StartAddressData', array($this))) { + $this->elementStart('a', array('class' => 'url home bookmark', + 'href' => common_local_url('public'))); + if (common_config('site', 'logo') || file_exists(theme_file('logo.png'))) { + $this->element('img', array('class' => 'logo photo', + 'src' => (common_config('site', 'logo')) ? common_config('site', 'logo') : theme_path('logo.png'), + 'alt' => common_config('site', 'name'))); + } + $this->element('span', array('class' => 'fn org'), common_config('site', 'name')); + $this->elementEnd('a'); + Event::handle('EndAddressData', array($this)); } - $this->element('span', array('class' => 'fn org'), common_config('site', 'name')); - $this->elementEnd('a'); $this->elementEnd('address'); } @@ -416,11 +424,13 @@ class Action extends HTMLOutputter // lawsuit $this->menuItem(common_local_url('smssettings'), _('Connect'), _('Connect to SMS, Twitter'), false, 'nav_connect'); } - $this->menuItem(common_local_url('invite'), - _('Invite'), - sprintf(_('Invite friends and colleagues to join you on %s'), - common_config('site', 'name')), - false, 'nav_invitecontact'); + if (common_config('invite', 'enabled')) { + $this->menuItem(common_local_url('invite'), + _('Invite'), + sprintf(_('Invite friends and colleagues to join you on %s'), + common_config('site', 'name')), + false, 'nav_invitecontact'); + } $this->menuItem(common_local_url('logout'), _('Logout'), _('Logout from the site'), false, 'nav_logout'); } @@ -429,8 +439,6 @@ class Action extends HTMLOutputter // lawsuit $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('login'), _('Login'), _('Login to the site'), false, 'nav_login'); } @@ -569,20 +577,32 @@ class Action extends HTMLOutputter // lawsuit /** * Show page notice block. * + * Only show the block if a subclassed action has overrided + * Action::showPageNotice(), or an event handler is registered for + * the StartShowPageNotice event, in which case we assume the + * 'page_notice' definition list is desired. This is to prevent + * empty 'page_notice' definition lists from being output everywhere. + * * @return nothing */ function showPageNoticeBlock() { - $this->elementStart('dl', array('id' => 'page_notice', - 'class' => 'system_notice')); - $this->element('dt', null, _('Page notice')); - $this->elementStart('dd'); - if (Event::handle('StartShowPageNotice', array($this))) { - $this->showPageNotice(); - Event::handle('EndShowPageNotice', array($this)); + $rmethod = new ReflectionMethod($this, 'showPageNotice'); + $dclass = $rmethod->getDeclaringClass()->getName(); + + if ($dclass != 'Action' || Event::hasHandler('StartShowPageNotice')) { + + $this->elementStart('dl', array('id' => 'page_notice', + 'class' => 'system_notice')); + $this->element('dt', null, _('Page notice')); + $this->elementStart('dd'); + if (Event::handle('StartShowPageNotice', array($this))) { + $this->showPageNotice(); + Event::handle('EndShowPageNotice', array($this)); + } + $this->elementEnd('dd'); + $this->elementEnd('dl'); } - $this->elementEnd('dd'); - $this->elementEnd('dl'); } /** @@ -686,6 +706,11 @@ class Action extends HTMLOutputter // lawsuit _('About')); $this->menuItem(common_local_url('doc', array('title' => 'faq')), _('FAQ')); + $bb = common_config('site', 'broughtby'); + if (!empty($bb)) { + $this->menuItem(common_local_url('doc', array('title' => 'tos')), + _('TOS')); + } $this->menuItem(common_local_url('doc', array('title' => 'privacy')), _('Privacy')); $this->menuItem(common_local_url('doc', array('title' => 'source')), @@ -747,7 +772,9 @@ class Action extends HTMLOutputter // lawsuit $this->elementStart('p'); $this->element('img', array('id' => 'license_cc', 'src' => common_config('license', 'image'), - 'alt' => common_config('license', 'title'))); + 'alt' => common_config('license', 'title'), + 'width' => '80', + 'height' => '15')); //TODO: This is dirty: i18n $this->text(_('All '.common_config('site', 'name').' content and data are available under the ')); $this->element('a', array('class' => 'license', @@ -847,10 +874,12 @@ class Action extends HTMLOutputter // lawsuit } if ($lm) { header('Last-Modified: ' . date(DATE_RFC1123, $lm)); - if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { - $ims = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']); + if (array_key_exists('HTTP_IF_MODIFIED_SINCE', $_SERVER)) { + $if_modified_since = $_SERVER['HTTP_IF_MODIFIED_SINCE']; + $ims = strtotime($if_modified_since); if ($lm <= $ims) { - $if_none_match = $_SERVER['HTTP_IF_NONE_MATCH']; + $if_none_match = (array_key_exists('HTTP_IF_NONE_MATCH', $_SERVER)) ? + $_SERVER['HTTP_IF_NONE_MATCH'] : null; if (!$if_none_match || !$etag || $this->_hasEtag($etag, $if_none_match)) { @@ -944,12 +973,16 @@ class Action extends HTMLOutputter // lawsuit $action = $this->trimmed('action'); $args = $this->args; unset($args['action']); + if (common_config('site', 'fancy')) { + unset($args['p']); + } if (array_key_exists('submit', $args)) { unset($args['submit']); } foreach (array_keys($_COOKIE) as $cookie) { unset($args[$cookie]); } + return common_local_url($action, $args); } diff --git a/lib/arraywrapper.php b/lib/arraywrapper.php index ef0eeffa5..a8a12b3bb 100644 --- a/lib/arraywrapper.php +++ b/lib/arraywrapper.php @@ -1,7 +1,7 @@ <?php /* * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by diff --git a/lib/attachmentlist.php b/lib/attachmentlist.php new file mode 100644 index 000000000..f6a1b59d0 --- /dev/null +++ b/lib/attachmentlist.php @@ -0,0 +1,357 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * widget for displaying a list of notice attachments + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category UI + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @author Sarven Capadisli <csarven@controlyourself.ca> + * @copyright 2008 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +/** + * widget for displaying a list of notice attachments + * + * There are a number of actions that display a list of notices, in + * reverse chronological order. This widget abstracts out most of the + * code for UI for notice lists. It's overridden to hide some + * data for e.g. the profile page. + * + * @category UI + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + * @see Notice + * @see NoticeListItem + * @see ProfileNoticeList + */ + +class AttachmentList extends Widget +{ + /** the current stream of notices being displayed. */ + + var $notice = null; + + /** + * constructor + * + * @param Notice $notice stream of notices from DB_DataObject + */ + + function __construct($notice, $out=null) + { + parent::__construct($out); + $this->notice = $notice; + } + + /** + * show the list of notices + * + * "Uses up" the stream by looping through it. So, probably can't + * be called twice on the same list. + * + * @return int count of notices listed. + */ + + function show() + { + $atts = new File; + $att = $atts->getAttachments($this->notice->id); + if (empty($att)) return 0; + $this->out->elementStart('dl', array('id' =>'attachments', + 'class' => 'entry-content')); + $this->out->element('dt', null, _('Attachments')); + $this->out->elementStart('dd'); + $this->out->elementStart('ol', array('class' => 'attachments')); + + foreach ($att as $n=>$attachment) { + $item = $this->newListItem($attachment); + $item->show(); + } + + $this->out->elementEnd('dd'); + $this->out->elementEnd('ol'); + $this->out->elementEnd('dl'); + + return count($att); + } + + /** + * returns a new list item for the current notice + * + * Recipe (factory?) method; overridden by sub-classes to give + * a different list item class. + * + * @param Notice $notice the current notice + * + * @return NoticeListItem a list item for displaying the notice + */ + + function newListItem($attachment) + { + return new AttachmentListItem($attachment, $this->out); + } +} + +/** + * widget for displaying a single notice + * + * This widget has the core smarts for showing a single notice: what to display, + * where, and under which circumstances. Its key method is show(); this is a recipe + * that calls all the other show*() methods to build up a single notice. The + * ProfileNoticeListItem subclass, for example, overrides showAuthor() to skip + * author info (since that's implicit by the data in the page). + * + * @category UI + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + * @see NoticeList + * @see ProfileNoticeListItem + */ + +class AttachmentListItem extends Widget +{ + /** The attachment this item will show. */ + + var $attachment = null; + + var $oembed = null; + + /** + * constructor + * + * Also initializes the profile attribute. + * + * @param Notice $notice The notice we'll display + */ + + function __construct($attachment, $out=null) + { + parent::__construct($out); + $this->attachment = $attachment; + $this->oembed = File_oembed::staticGet('file_id', $this->attachment->id); + } + + function title() { + if (empty($this->attachment->title)) { + if (empty($this->oembed->title)) { + $title = $this->attachment->url; + } else { + $title = $this->oembed->title; + } + } else { + $title = $this->attachment->title; + } + + return $title; + } + + function linkTitle() { + return $this->title(); + } + + /** + * recipe function for displaying a single notice. + * + * This uses all the other methods to correctly display a notice. Override + * it or one of the others to fine-tune the output. + * + * @return void + */ + + function show() + { + $this->showStart(); + $this->showNoticeAttachment(); + $this->showEnd(); + } + + function linkAttr() { + return array('class' => 'attachment', 'href' => $this->attachment->url, 'id' => 'attachment-' . $this->attachment->id); + } + + function showLink() { + $this->out->elementStart('a', $this->linkAttr()); + $this->out->element('span', null, $this->linkTitle()); + $this->showRepresentation(); + $this->out->elementEnd('a'); + } + + function showNoticeAttachment() + { + $this->showLink(); + } + + function showRepresentation() { + $thumbnail = File_thumbnail::staticGet('file_id', $this->attachment->id); + if (!empty($thumbnail)) { + $this->out->element('img', array('alt' => '', 'src' => $thumbnail->url, 'width' => $thumbnail->width, 'height' => $thumbnail->height)); + } + } + + /** + * start a single notice. + * + * @return void + */ + + function showStart() + { + // XXX: RDFa + // TODO: add notice_type class e.g., notice_video, notice_image + $this->out->elementStart('li'); + } + + /** + * finish the notice + * + * Close the last elements in the notice list item + * + * @return void + */ + + function showEnd() + { + $this->out->elementEnd('li'); + } +} + +class Attachment extends AttachmentListItem +{ + function showLink() { + $this->out->elementStart('div', array('id' => 'attachment_view', + 'class' => 'hentry')); + $this->out->elementStart('div', 'entry-title'); + $this->out->elementStart('a', $this->linkAttr()); + $this->out->element('span', null, $this->linkTitle()); + $this->out->elementEnd('a'); + $this->out->elementEnd('div'); + + $this->out->elementStart('div', 'entry-content'); + $this->showRepresentation(); + $this->out->elementEnd('div'); + + if (!empty($this->oembed->author_name) || !empty($this->oembed->provider)) { + $this->out->elementStart('div', array('id' => 'oembed_info', + 'class' => 'entry-content')); + if (!empty($this->oembed->author_name)) { + $this->out->elementStart('dl', 'vcard author'); + $this->out->element('dt', null, _('Author')); + $this->out->elementStart('dd', 'fn'); + if (empty($this->oembed->author_url)) { + $this->out->text($this->oembed->author_name); + } else { + $this->out->element('a', array('href' => $this->oembed->author_url, + 'class' => 'url'), $this->oembed->author_name); + } + $this->out->elementEnd('dd'); + $this->out->elementEnd('dl'); + } + if (!empty($this->oembed->provider)) { + $this->out->elementStart('dl', 'vcard'); + $this->out->element('dt', null, _('Provider')); + $this->out->elementStart('dd', 'fn'); + if (empty($this->oembed->provider_url)) { + $this->out->text($this->oembed->provider); + } else { + $this->out->element('a', array('href' => $this->oembed->provider_url, + 'class' => 'url'), $this->oembed->provider); + } + $this->out->elementEnd('dd'); + $this->out->elementEnd('dl'); + } + $this->out->elementEnd('div'); + } + $this->out->elementEnd('div'); + } + + function show() { + $this->showNoticeAttachment(); + } + + function linkAttr() { + return array('class' => 'external', 'href' => $this->attachment->url); + } + + function linkTitle() { + return $this->attachment->url; + } + + function showRepresentation() { + if (empty($this->oembed->type)) { + if (empty($this->attachment->mimetype)) { + $this->out->element('pre', null, 'oh well... not sure how to handle the following: ' . print_r($this->attachment, true)); + } else { + switch ($this->attachment->mimetype) { + case 'image/gif': + case 'image/png': + case 'image/jpg': + case 'image/jpeg': + $this->out->element('img', array('src' => $this->attachment->url, 'alt' => 'alt')); + break; + + case 'application/ogg': + case 'audio/x-speex': + case 'video/mpeg': + case 'audio/mpeg': + case 'video/mp4': + case 'video/quicktime': + $arr = array('type' => $this->attachment->mimetype, + 'data' => $this->attachment->url, + 'width' => 320, + 'height' => 240 + ); + $this->out->elementStart('object', $arr); + $this->out->element('param', array('name' => 'src', 'value' => $this->attachment->url)); + $this->out->element('param', array('name' => 'autoStart', 'value' => 1)); + $this->out->elementEnd('object'); + break; + } + } + } else { + switch ($this->oembed->type) { + case 'rich': + case 'video': + case 'link': + if (!empty($this->oembed->html)) { + $this->out->raw($this->oembed->html); + } + break; + + case 'photo': + $this->out->element('img', array('src' => $this->oembed->url, 'width' => $this->oembed->width, 'height' => $this->oembed->height, 'alt' => 'alt')); + break; + + default: + $this->out->element('pre', null, 'oh well... not sure how to handle the following oembed: ' . print_r($this->oembed, true)); + } + } + } +} + diff --git a/lib/personal.php b/lib/attachmentnoticesection.php index f92732375..eb3176376 100644 --- a/lib/personal.php +++ b/lib/attachmentnoticesection.php @@ -2,7 +2,7 @@ /** * Laconica, the distributed open-source microblogging tool * - * User profile page + * FIXME * * PHP version 5 * @@ -19,11 +19,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * - * @category Personal + * @category Widget * @package Laconica * @author Evan Prodromou <evan@controlyourself.ca> - * @author Sarven Capadisli <csarven@controlyourself.ca> - * @copyright 2008-2009 Control Yourself, Inc. + * @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/ */ @@ -33,28 +32,44 @@ if (!defined('LACONICA')) { } /** - * Base class for user profile page + * FIXME * - * @category Personal + * These are the widgets that show interesting data about a person * group, or site. + * + * @category Widget * @package Laconica * @author Evan Prodromou <evan@controlyourself.ca> * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://laconi.ca/ */ -class PersonalAction extends Action +class AttachmentNoticeSection extends NoticeSection { + function showContent() { + parent::showContent(); + return false; + } - var $user = null; - - function isReadOnly($args) + function getNotices() { - return true; + $notice = new Notice; + $f2p = new File_to_post; + $f2p->file_id = $this->out->attachment->id; + $notice->joinAdd($f2p); + $notice->orderBy('created desc'); + $notice->selectAdd('post_id as id'); + $notice->find(); + return $notice; } - function handle($args) + function title() { - parent::handle($args); + return _('Notices where this attachment appears'); } + function divId() + { + return 'popular_notices'; + } } + diff --git a/lib/attachmenttagcloudsection.php b/lib/attachmenttagcloudsection.php new file mode 100644 index 000000000..50bfceccb --- /dev/null +++ b/lib/attachmenttagcloudsection.php @@ -0,0 +1,83 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Attachment tag cloud section + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Widget + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +/** + * Attachment tag cloud section + * + * @category Widget + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +class AttachmentTagCloudSection extends TagCloudSection +{ + function title() + { + return _('Tags for this attachment'); + } + + function showTag($tag, $weight, $relative) + { + if ($relative > 0.5) { + $rel = 'tag-cloud-7'; + } else if ($relative > 0.4) { + $rel = 'tag-cloud-6'; + } else if ($relative > 0.3) { + $rel = 'tag-cloud-5'; + } else if ($relative > 0.2) { + $rel = 'tag-cloud-4'; + } else if ($relative > 0.1) { + $rel = 'tag-cloud-3'; + } else if ($relative > 0.05) { + $rel = 'tag-cloud-2'; + } else { + $rel = 'tag-cloud-1'; + } + + $this->out->elementStart('li', $rel); + $this->out->element('a', array('href' => $this->tagUrl($tag)), + $tag); + $this->out->elementEnd('li'); + } + + function getTags() + { + $notice_tag = new Notice_tag; + $query = 'select tag,count(tag) as weight from notice_tag join file_to_post on (notice_tag.notice_id=post_id) join notice on notice_id = notice.id where file_id=' . $notice_tag->escape($this->out->attachment->id) . ' group by tag order by weight desc'; + $notice_tag->query($query); + return $notice_tag; + } +} + diff --git a/lib/channel.php b/lib/channel.php index f1e205546..38c1d4d67 100644 --- a/lib/channel.php +++ b/lib/channel.php @@ -1,7 +1,7 @@ <?php /* * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by diff --git a/lib/clienterroraction.php b/lib/clienterroraction.php index 0c48414d5..7ddc35eb6 100644 --- a/lib/clienterroraction.php +++ b/lib/clienterroraction.php @@ -13,7 +13,7 @@ * @link http://laconi.ca/ * * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by diff --git a/lib/command.php b/lib/command.php index 507990a0b..4e2280bc8 100644 --- a/lib/command.php +++ b/lib/command.php @@ -1,7 +1,7 @@ <?php /* * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -97,18 +97,11 @@ class StatsCommand extends Command { function execute($channel) { + $profile = $this->user->getProfile(); - $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(); + $subs_count = $profile->subscriptionCount(); + $subbed_count = $profile->subscriberCount(); + $notice_count = $profile->noticeCount(); $channel->output($this->user, sprintf(_("Subscriptions: %1\$s\n". "Subscribers: %2\$s\n". diff --git a/lib/commandinterpreter.php b/lib/commandinterpreter.php index 49c733c03..6538d4442 100644 --- a/lib/commandinterpreter.php +++ b/lib/commandinterpreter.php @@ -1,7 +1,7 @@ <?php /* * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by diff --git a/lib/common.php b/lib/common.php index 151b31d80..c47702779 100644 --- a/lib/common.php +++ b/lib/common.php @@ -1,7 +1,7 @@ <?php /* * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -19,7 +19,7 @@ if (!defined('LACONICA')) { exit(1); } -define('LACONICA_VERSION', '0.7.4'); +define('LACONICA_VERSION', '0.8.0'); define('AVATAR_PROFILE_SIZE', 96); define('AVATAR_STREAM_SIZE', 48); @@ -55,14 +55,37 @@ require_once(INSTALLDIR.'/lib/language.php'); require_once(INSTALLDIR.'/lib/event.php'); require_once(INSTALLDIR.'/lib/plugin.php'); -// try to figure out where we are +function _sn_to_path($sn) +{ + $past_root = substr($sn, 1); + $last_slash = strrpos($past_root, '/'); + if ($last_slash > 0) { + $p = substr($past_root, 0, $last_slash); + } else { + $p = ''; + } + return $p; +} + +// try to figure out where we are. $server and $path +// can be set by including module, else we guess based +// on HTTP info. + +if (isset($server)) { + $_server = $server; +} else { + $_server = array_key_exists('SERVER_NAME', $_SERVER) ? + strtolower($_SERVER['SERVER_NAME']) : + null; +} -$_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; +if (isset($path)) { + $_path = $path; +} else { + $_path = array_key_exists('SCRIPT_NAME', $_SERVER) ? + _sn_to_path($_SERVER['SCRIPT_NAME']) : + null; +} // default configuration, overwritten in config.php @@ -71,6 +94,14 @@ $config = array('name' => 'Just another Laconica microblog', 'server' => $_server, 'theme' => 'default', + 'design' => + array('backgroundcolor' => '#CEE1E9', + 'contentcolor' => '#FFFFFF', + 'sidebarcolor' => '#C8D1D5', + 'textcolor' => '#000000', + 'linkcolor' => '#002E6E', + 'backgroundimage' => null, + 'disposition' => 1), 'path' => $_path, 'logfile' => null, 'logo' => null, @@ -93,9 +124,16 @@ $config = 'dupelimit' => 60), # default for same person saying the same thing 'syslog' => array('appname' => 'laconica', # for syslog - 'priority' => 'debug'), # XXX: currently ignored + 'priority' => 'debug', # XXX: currently ignored + 'facility' => LOG_USER), 'queue' => - array('enabled' => false), + array('enabled' => false, + 'subsystem' => 'db', # default to database, or 'stomp' + 'stomp_server' => null, + 'queue_basename' => 'laconica', + 'stomp_username' => null, + 'stomp_password' => null, + ), 'license' => array('url' => 'http://creativecommons.org/licenses/by/3.0/', 'title' => 'Creative Commons Attribution 3.0', @@ -109,13 +147,21 @@ $config = 'profile' => array('banned' => array()), 'avatar' => - array('server' => null), + array('server' => null, + 'dir' => INSTALLDIR . '/avatar/', + 'path' => $_path . '/avatar/'), + 'background' => + array('server' => null, + 'dir' => INSTALLDIR . '/background/', + 'path' => $_path . '/background/'), 'public' => array('localonly' => true, 'blacklist' => array(), 'autosource' => array()), 'theme' => - array('server' => null), + array('server' => null, + 'dir' => null, + 'path'=> null), 'throttle' => array('enabled' => false, // whether to throttle edits; false by default 'count' => 20, // number of allowed messages in timespan @@ -131,6 +177,8 @@ $config = 'host' => null, # only set if != server 'debug' => false, # print extra debug info 'public' => array()), # JIDs of users who want to receive the public stream + 'invite' => + array('enabled' => true), 'sphinx' => array('enabled' => false, 'server' => 'localhost', @@ -143,20 +191,76 @@ $config = array('piddir' => '/var/run', 'user' => false, 'group' => false), + 'twitterbridge' => + array('enabled' => false), 'integration' => array('source' => 'Laconica', # source attribute for Twitter 'taguri' => $_server.',2009'), # base for tag URIs 'memcached' => array('enabled' => false, 'server' => 'localhost', + 'base' => null, 'port' => 11211), 'ping' => array('notify' => array()), 'inboxes' => array('enabled' => true), # on by default for new sites 'newuser' => - array('subscribe' => null, + array('default' => null, 'welcome' => null), + 'snapshot' => + array('run' => 'web', + 'frequency' => 10000, + 'reporturl' => 'http://laconi.ca/stats/report'), + 'attachments' => + array('server' => null, + 'dir' => INSTALLDIR . '/file/', + 'path' => $_path . '/file/', + 'supported' => array('image/png', + 'image/jpeg', + 'image/gif', + 'image/svg+xml', + 'audio/mpeg', + 'audio/x-speex', + 'application/ogg', + 'application/pdf', + 'application/vnd.oasis.opendocument.text', + 'application/vnd.oasis.opendocument.text-template', + 'application/vnd.oasis.opendocument.graphics', + 'application/vnd.oasis.opendocument.graphics-template', + 'application/vnd.oasis.opendocument.presentation', + 'application/vnd.oasis.opendocument.presentation-template', + 'application/vnd.oasis.opendocument.spreadsheet', + 'application/vnd.oasis.opendocument.spreadsheet-template', + 'application/vnd.oasis.opendocument.chart', + 'application/vnd.oasis.opendocument.chart-template', + 'application/vnd.oasis.opendocument.image', + 'application/vnd.oasis.opendocument.image-template', + 'application/vnd.oasis.opendocument.formula', + 'application/vnd.oasis.opendocument.formula-template', + 'application/vnd.oasis.opendocument.text-master', + 'application/vnd.oasis.opendocument.text-web', + 'application/x-zip', + 'application/zip', + 'text/plain', + 'video/mpeg', + 'video/mp4', + 'video/quicktime', + 'video/mpeg'), + 'file_quota' => 5000000, + 'user_quota' => 50000000, + 'monthly_quota' => 15000000, + 'uploads' => true, + 'filecommand' => '/usr/bin/file', + ), + 'group' => + array('maxaliases' => 3), + 'oohembed' => array('endpoint' => 'http://oohembed.com/oohembed/'), + 'search' => + array('type' => 'fulltext'), + 'sessions' => + array('handle' => false, // whether to handle sessions ourselves + 'debug' => false), // debugging output for sessions ); $config['db'] = &PEAR::getStaticProperty('DB_DataObject','options'); @@ -178,18 +282,55 @@ if (function_exists('date_default_timezone_set')) { date_default_timezone_set('UTC'); } +function addPlugin($name, $attrs = null) +{ + $name = ucfirst($name); + $pluginclass = "{$name}Plugin"; + + if (!class_exists($pluginclass)) { + + $files = array("local/plugins/{$pluginclass}.php", + "local/plugins/{$name}/{$pluginclass}.php", + "local/{$pluginclass}.php", + "local/{$name}/{$pluginclass}.php", + "plugins/{$pluginclass}.php", + "plugins/{$name}/{$pluginclass}.php"); + + foreach ($files as $file) { + $fullpath = INSTALLDIR.'/'.$file; + if (@file_exists($fullpath)) { + include_once($fullpath); + break; + } + } + } + + $inst = new $pluginclass(); + + if (!empty($attrs)) { + foreach ($attrs as $aname => $avalue) { + $inst->$aname = $avalue; + } + } + return $inst; +} + // 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 (isset($conffile)) { + $_config_files = array($conffile); +} else { + $_config_files = array('/etc/laconica/laconica.php', + '/etc/laconica/'.$_server.'.php'); -if (strlen($_path) > 0) { - $_config_files[] = '/etc/laconica/'.$_server.'_'.$_path.'.php'; -} + if (strlen($_path) > 0) { + $_config_files[] = '/etc/laconica/'.$_server.'_'.$_path.'.php'; + } -$_config_files[] = INSTALLDIR.'/config.php'; + $_config_files[] = INSTALLDIR.'/config.php'; +} $_have_a_config = false; @@ -218,19 +359,19 @@ if ($_db_name != 'laconica' && !array_key_exists('ini_'.$_db_name, $config['db'] // XXX: how many of these could be auto-loaded on use? -require_once('Validate.php'); -require_once('markdown.php'); +require_once 'Validate.php'; +require_once 'markdown.php'; -require_once(INSTALLDIR.'/lib/util.php'); -require_once(INSTALLDIR.'/lib/action.php'); -require_once(INSTALLDIR.'/lib/theme.php'); -require_once(INSTALLDIR.'/lib/mail.php'); -require_once(INSTALLDIR.'/lib/subs.php'); -require_once(INSTALLDIR.'/lib/Shorturl_api.php'); -require_once(INSTALLDIR.'/lib/twitter.php'); +require_once INSTALLDIR.'/lib/util.php'; +require_once INSTALLDIR.'/lib/action.php'; +require_once INSTALLDIR.'/lib/theme.php'; +require_once INSTALLDIR.'/lib/mail.php'; +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'); +require_once INSTALLDIR.'/lib/clientexception.php'; +require_once INSTALLDIR.'/lib/serverexception.php'; // XXX: other formats here diff --git a/lib/currentuserdesignaction.php b/lib/currentuserdesignaction.php new file mode 100644 index 000000000..4c7e15a8b --- /dev/null +++ b/lib/currentuserdesignaction.php @@ -0,0 +1,92 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Base class for actions that use the current user's design + * + * 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 Action + * @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); +} + +/** + * Base class for actions that use the current user's design + * + * Some pages (settings in particular) use the current user's chosen + * design. This superclass returns that design. + * + * @category Action + * @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 CurrentUserDesignAction extends Action +{ + + /** + * Show the user's design stylesheet + * + * @return nothing + */ + + function showStylesheets() + { + parent::showStylesheets(); + + $user = common_current_user(); + + if (empty($user) || $user->viewdesigns) { + $design = $this->getDesign(); + + if (!empty($design)) { + $design->showCSS($this); + } + } + } + + /** + * A design for this action + * + * if the user attribute has been set, returns that user's + * design. + * + * @return Design a design object to use + */ + + function getDesign() + { + $cur = common_current_user(); + + if (empty($cur)) { + return null; + } + + return $cur->getDesign(); + } + +} diff --git a/lib/daemon.php b/lib/daemon.php index 9c1ae50a0..9d89c63e7 100644 --- a/lib/daemon.php +++ b/lib/daemon.php @@ -1,7 +1,7 @@ <?php /** * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -23,6 +23,13 @@ if (!defined('LACONICA')) { class Daemon { + var $daemonize = true; + + function __construct($daemonize = true) + { + $this->daemonize = $daemonize; + } + function name() { return null; @@ -129,12 +136,16 @@ class Daemon common_log(LOG_INFO, $this->name() . ' already running. Exiting.'); exit(0); } - if ($this->background()) { - $this->writePidFile(); - $this->changeUser(); - $this->run(); - $this->clearPidFile(); + + if ($this->daemonize) { + common_log(LOG_INFO, 'Backgrounding daemon "'.$this->name().'"'); + $this->background(); } + + $this->writePidFile(); + $this->changeUser(); + $this->run(); + $this->clearPidFile(); } function run() diff --git a/lib/dberroraction.php b/lib/dberroraction.php index 0dc92490c..a04e5f74f 100644 --- a/lib/dberroraction.php +++ b/lib/dberroraction.php @@ -12,7 +12,7 @@ * @link http://laconi.ca/ * * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by diff --git a/lib/dbqueuemanager.php b/lib/dbqueuemanager.php new file mode 100644 index 000000000..6e7172de0 --- /dev/null +++ b/lib/dbqueuemanager.php @@ -0,0 +1,166 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Simple-minded queue manager for storing items in the database + * + * 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 QueueManager + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @author Sarven Capadisli <csarven@controlyourself.ca> + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +class DBQueueManager extends QueueManager +{ + var $qis = array(); + + function enqueue($object, $queue) + { + $notice = $object; + + $qi = new Queue_item(); + + $qi->notice_id = $notice->id; + $qi->transport = $queue; + $qi->created = $notice->created; + $result = $qi->insert(); + + if (!$result) { + common_log_db_error($qi, 'INSERT', __FILE__); + throw new ServerException('DB error inserting queue item'); + } + + return true; + } + + function service($queue, $handler) + { + while (true) { + $this->_log(LOG_DEBUG, 'Checking for notices...'); + $notice = $this->_nextItem($queue, null); + if (empty($notice)) { + $this->_log(LOG_DEBUG, 'No notices waiting; idling.'); + // Nothing in the queue. Do you + // have other tasks, like servicing your + // XMPP connection, to do? + $handler->idle(QUEUE_HANDLER_MISS_IDLE); + } else { + $this->_log(LOG_INFO, 'Got notice '. $notice->id); + // Yay! Got one! + if ($handler->handle_notice($notice)) { + $this->_log(LOG_INFO, 'Successfully handled notice '. $notice->id); + $this->_done($notice, $queue); + } else { + $this->_log(LOG_INFO, 'Failed to handle notice '. $notice->id); + $this->_fail($notice, $queue); + } + // Chance to e.g. service your XMPP connection + $this->_log(LOG_DEBUG, 'Idling after success.'); + $handler->idle(QUEUE_HANDLER_HIT_IDLE); + } + // XXX: when do we give up? + } + } + + function _nextItem($queue, $timeout=null) + { + $start = time(); + $result = null; + + do { + $qi = Queue_item::top($queue); + if (!empty($qi)) { + $notice = Notice::staticGet('id', $qi->notice_id); + if (!empty($notice)) { + $result = $notice; + } else { + $this->_log(LOG_INFO, 'dequeued non-existent notice ' . $notice->id); + $qi->delete(); + $qi->free(); + $qi = null; + } + } + } while (empty($result) && (is_null($timeout) || (time() - $start) < $timeout)); + + return $result; + } + + function _done($object, $queue) + { + // XXX: right now, we only handle notices + + $notice = $object; + + $qi = Queue_item::pkeyGet(array('notice_id' => $notice->id, + 'transport' => $queue)); + + if (empty($qi)) { + $this->_log(LOG_INFO, 'Cannot find queue item for notice '.$notice->id.', queue '.$queue); + } else { + if (empty($qi->claimed)) { + $this->_log(LOG_WARNING, 'Reluctantly releasing unclaimed queue item '. + 'for '.$notice->id.', queue '.$queue); + } + $qi->delete(); + $qi->free(); + $qi = null; + } + + $this->_log(LOG_INFO, 'done with notice ID = ' . $notice->id); + + $notice->free(); + $notice = null; + } + + function _fail($object, $queue) + { + // XXX: right now, we only handle notices + + $notice = $object; + + $qi = Queue_item::pkeyGet(array('notice_id' => $notice->id, + 'transport' => $queue)); + + if (empty($qi)) { + $this->_log(LOG_INFO, 'Cannot find queue item for notice '.$notice->id.', queue '.$queue); + } else { + if (empty($qi->claimed)) { + $this->_log(LOG_WARNING, 'Ignoring failure for unclaimed queue item '. + 'for '.$notice->id.', queue '.$queue); + } else { + $orig = clone($qi); + $qi->claimed = null; + $qi->update($orig); + $qi = null; + } + } + + $this->_log(LOG_INFO, 'done with notice ID = ' . $notice->id); + + $notice->free(); + $notice = null; + } + + function _log($level, $msg) + { + common_log($level, 'DBQueueManager: '.$msg); + } +} diff --git a/lib/designsettings.php b/lib/designsettings.php new file mode 100644 index 000000000..fbffdb208 --- /dev/null +++ b/lib/designsettings.php @@ -0,0 +1,477 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Change user password + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Settings + * @package Laconica + * @author Sarven Capadisli <csarven@controlyourself.ca> + * @author Zach Copley <zach@controlyourself.ca> + * @copyright 2008-2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/accountsettingsaction.php'; +require_once INSTALLDIR . '/lib/webcolor.php'; + +/** + * Base class for setting a user or group design + * + * Shows the design setting form and also handles some things like saving + * background images, and fetching a default design + * + * @category Settings + * @package Laconica + * @author Zach Copley <zach@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 DesignSettingsAction extends AccountSettingsAction +{ + + var $submitaction = null; + + /** + * Title of the page + * + * @return string Title of the page + */ + + function title() + { + return _('Profile design'); + } + + /** + * Instructions for use + * + * @return instructions for use + */ + + function getInstructions() + { + return _('Customize the way your profile looks ' . + 'with a background image and a colour palette of your choice.'); + } + + /** + * Shows the design settings form + * + * @param Design $design a working design to show + * + * @return nothing + */ + + function showDesignForm($design) + { + + $this->elementStart('form', array('method' => 'post', + 'enctype' => 'multipart/form-data', + 'id' => 'form_settings_design', + 'class' => 'form_settings', + 'action' => $this->submitaction)); + $this->elementStart('fieldset'); + $this->hidden('token', common_session_token()); + + $this->elementStart('fieldset', array('id' => + 'settings_design_background-image')); + $this->element('legend', null, _('Change background image')); + $this->elementStart('ul', 'form_data'); + $this->elementStart('li'); + $this->element('label', array('for' => 'design_background-image_file'), + _('Upload file')); + $this->element('input', array('name' => 'design_background-image_file', + 'type' => 'file', + 'id' => 'design_background-image_file')); + $this->element('p', 'form_guide', _('You can upload your personal ' . + 'background image. The maximum file size is 2Mb.')); + $this->element('input', array('name' => 'MAX_FILE_SIZE', + 'type' => 'hidden', + 'id' => 'MAX_FILE_SIZE', + 'value' => ImageFile::maxFileSizeInt())); + $this->elementEnd('li'); + + if (!empty($design->backgroundimage)) { + + $this->elementStart('li', array('id' => + 'design_background-image_onoff')); + + $this->element('img', array('src' => + Design::url($design->backgroundimage))); + + $attrs = array('name' => 'design_background-image_onoff', + 'type' => 'radio', + 'id' => 'design_background-image_on', + 'class' => 'radio', + 'value' => 'on'); + + if ($design->disposition & BACKGROUND_ON) { + $attrs['checked'] = 'checked'; + } + + $this->element('input', $attrs); + + $this->element('label', array('for' => 'design_background-image_on', + 'class' => 'radio'), + _('On')); + + $attrs = array('name' => 'design_background-image_onoff', + 'type' => 'radio', + 'id' => 'design_background-image_off', + 'class' => 'radio', + 'value' => 'off'); + + if ($design->disposition & BACKGROUND_OFF) { + $attrs['checked'] = 'checked'; + } + + $this->element('input', $attrs); + + $this->element('label', array('for' => 'design_background-image_off', + 'class' => 'radio'), + _('Off')); + $this->element('p', 'form_guide', _('Turn background image on or off.')); + $this->elementEnd('li'); + + $this->elementStart('li'); + $this->checkbox('design_background-image_repeat', + _('Tile background image'), + ($design->disposition & BACKGROUND_TILE) ? true : false); + $this->elementEnd('li'); + } + + $this->elementEnd('ul'); + $this->elementEnd('fieldset'); + + $this->elementStart('fieldset', array('id' => 'settings_design_color')); + $this->element('legend', null, _('Change colours')); + $this->elementStart('ul', 'form_data'); + + try { + + $bgcolor = new WebColor($design->backgroundcolor); + + $this->elementStart('li'); + $this->element('label', array('for' => 'swatch-1'), _('Background')); + $this->element('input', array('name' => 'design_background', + 'type' => 'text', + 'id' => 'swatch-1', + 'class' => 'swatch', + 'maxlength' => '7', + 'size' => '7', + 'value' => '#' . $bgcolor->hexValue())); + $this->elementEnd('li'); + + $ccolor = new WebColor($design->contentcolor); + + $this->elementStart('li'); + $this->element('label', array('for' => 'swatch-2'), _('Content')); + $this->element('input', array('name' => 'design_content', + 'type' => 'text', + 'id' => 'swatch-2', + 'class' => 'swatch', + 'maxlength' => '7', + 'size' => '7', + 'value' => '#' . $ccolor->hexValue())); + $this->elementEnd('li'); + + $sbcolor = new WebColor($design->sidebarcolor); + + $this->elementStart('li'); + $this->element('label', array('for' => 'swatch-3'), _('Sidebar')); + $this->element('input', array('name' => 'design_sidebar', + 'type' => 'text', + 'id' => 'swatch-3', + 'class' => 'swatch', + 'maxlength' => '7', + 'size' => '7', + 'value' => '#' . $sbcolor->hexValue())); + $this->elementEnd('li'); + + $tcolor = new WebColor($design->textcolor); + + $this->elementStart('li'); + $this->element('label', array('for' => 'swatch-4'), _('Text')); + $this->element('input', array('name' => 'design_text', + 'type' => 'text', + 'id' => 'swatch-4', + 'class' => 'swatch', + 'maxlength' => '7', + 'size' => '7', + 'value' => '#' . $tcolor->hexValue())); + $this->elementEnd('li'); + + $lcolor = new WebColor($design->linkcolor); + + $this->elementStart('li'); + $this->element('label', array('for' => 'swatch-5'), _('Links')); + $this->element('input', array('name' => 'design_links', + 'type' => 'text', + 'id' => 'swatch-5', + 'class' => 'swatch', + 'maxlength' => '7', + 'size' => '7', + 'value' => '#' . $lcolor->hexValue())); + $this->elementEnd('li'); + + } catch (WebColorException $e) { + common_log(LOG_ERR, 'Bad color values in design ID: ' .$design->id); + } + + $this->elementEnd('ul'); + $this->elementEnd('fieldset'); + + $this->submit('defaults', _('Use defaults'), 'submit form_action-default', + 'defaults', _('Restore default designs')); + + $this->element('input', array('id' => 'settings_design_reset', + 'type' => 'reset', + 'value' => 'Reset', + 'class' => 'submit form_action-primary', + 'title' => _('Reset back to default'))); + + $this->submit('save', _('Save'), 'submit form_action-secondary', + 'save', _('Save design')); + + $this->elementEnd('fieldset'); + $this->elementEnd('form'); + } + + /** + * Handle a post + * + * Validate input and save changes. Reload the form with a success + * or error message. + * + * @return void + */ + + function handlePost() + { + // XXX: Robin's workaround for a bug in PHP where $_POST + // and $_FILE are empty in the case that the uploaded + // file is bigger than PHP is configured to handle. + + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + if (empty($_POST) && $_SERVER['CONTENT_LENGTH']) { + + $msg = _('The server was unable to handle that much POST ' . + 'data (%s bytes) due to its current configuration.'); + + $this->showForm(sprintf($msg, $_SERVER['CONTENT_LENGTH'])); + } + } + + // CSRF protection + $token = $this->trimmed('token'); + if (!$token || $token != common_session_token()) { + $this->showForm(_('There was a problem with your session token. '. + 'Try again, please.')); + return; + } + + if ($this->arg('save')) { + $this->saveDesign(); + } else if ($this->arg('defaults')) { + $this->restoreDefaults(); + } else { + $this->showForm(_('Unexpected form submission.')); + } + } + + /** + * Add the Farbtastic stylesheet + * + * @return void + */ + + function showStylesheets() + { + parent::showStylesheets(); + $farbtasticStyle = + common_path('theme/base/css/farbtastic.css?version='.LACONICA_VERSION); + + $this->element('link', array('rel' => 'stylesheet', + 'type' => 'text/css', + 'href' => $farbtasticStyle, + 'media' => 'screen, projection, tv')); + } + + /** + * Add the Farbtastic scripts + * + * @return void + */ + + function showScripts() + { + parent::showScripts(); + + $farbtasticPack = common_path('js/farbtastic/farbtastic.js'); + $userDesignGo = common_path('js/userdesign.go.js'); + + $this->element('script', array('type' => 'text/javascript', + 'src' => $farbtasticPack)); + $this->element('script', array('type' => 'text/javascript', + 'src' => $userDesignGo)); + } + + /** + * Get a default design + * + * @return Design design + */ + + function defaultDesign() + { + $defaults = common_config('site', 'design'); + + $design = new Design(); + + try { + + $color = new WebColor(); + + $color->parseColor($defaults['backgroundcolor']); + $design->backgroundcolor = $color->intValue(); + + $color->parseColor($defaults['contentcolor']); + $design->contentcolor = $color->intValue(); + + $color->parseColor($defaults['sidebarcolor']); + $design->sidebarcolor = $color->intValue(); + + $color->parseColor($defaults['textcolor']); + $design->textcolor = $color->intValue(); + + $color->parseColor($defaults['linkcolor']); + $design->linkcolor = $color->intValue(); + + $design->backgroundimage = $defaults['backgroundimage']; + + $design->disposition = $defaults['disposition']; + + } catch (WebColorException $e) { + common_log(LOG_ERR, _('Bad default color settings: ' . + $e->getMessage())); + } + + return $design; + } + + /** + * Save the background image, if any, and set its disposition + * + * @param Design $design a working design to attach the img to + * + * @return nothing + */ + + function saveBackgroundImage($design) + { + + // Now that we have a Design ID we can add a file to the design. + // XXX: This is an additional DB hit, but figured having the image + // associated with the Design rather than the User was worth + // it. -- Zach + + if ($_FILES['design_background-image_file']['error'] == + UPLOAD_ERR_OK) { + + $filepath = null; + + try { + $imagefile = + ImageFile::fromUpload('design_background-image_file'); + } catch (Exception $e) { + $this->showForm($e->getMessage()); + return; + } + + $filename = Design::filename($design->id, + image_type_to_extension($imagefile->type), + common_timestamp()); + + $filepath = Design::path($filename); + + move_uploaded_file($imagefile->filepath, $filepath); + + // delete any old backround img laying around + + if (isset($design->backgroundimage)) { + @unlink(Design::path($design->backgroundimage)); + } + + $original = clone($design); + + $design->backgroundimage = $filename; + + // default to on, no tile + + $design->setDisposition(true, false, false); + + $result = $design->update($original); + + if ($result === false) { + common_log_db_error($design, 'UPDATE', __FILE__); + $this->showForm(_('Couldn\'t update your design.')); + return; + } + } + } + + /** + * Restore the user or group design to system defaults + * + * @return nothing + */ + + function restoreDefaults() + { + $design = $this->getWorkingDesign(); + $default = $this->defaultDesign(); + $original = clone($design); + + $design->backgroundcolor = $default->backgroundcolor; + $design->contentcolor = $default->contentcolor; + $design->sidebarcolor = $default->sidebarcolor; + $design->textcolor = $default->textcolor; + $design->linkcolor = $default->linkcolor; + + $design->setDisposition(false, true, false); + + $result = $design->update($original); + + if ($result === false) { + common_log_db_error($design, 'UPDATE', __FILE__); + $this->showForm(_('Couldn\'t update your design.')); + return; + } + + $this->showForm(_('Design defaults restored.'), true); + } + +} diff --git a/lib/error.php b/lib/error.php index 282682133..bbf9987cf 100644 --- a/lib/error.php +++ b/lib/error.php @@ -13,7 +13,7 @@ * @link http://laconi.ca/ * * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by diff --git a/lib/event.php b/lib/event.php index d815ae54b..4ccee17e6 100644 --- a/lib/event.php +++ b/lib/event.php @@ -110,4 +110,32 @@ class Event { } return ($result !== false); } + + /** + * Check to see if an event handler exists + * + * Look to see if there's any handler for a given event, or narrow + * by providing the name of a specific plugin class. + * + * @param string $name Name of the event to look for + * @param string $plugin Optional name of the plugin class to look for + * + * @return boolean flag saying whether such a handler exists + * + */ + + public static function hasHandler($name, $plugin=null) { + if (array_key_exists($name, Event::$_handlers)) { + if (isset($plugin)) { + foreach (Event::$_handlers[$name] as $handler) { + if (get_class($handler[0]) == $plugin) { + return true; + } + } + } else { + return true; + } + } + return false; + } } diff --git a/lib/facebookaction.php b/lib/facebookaction.php index 043a078cd..1ae90d53b 100644 --- a/lib/facebookaction.php +++ b/lib/facebookaction.php @@ -38,14 +38,14 @@ require_once INSTALLDIR.'/lib/noticeform.php'; class FacebookAction extends Action { - + var $facebook = null; var $fbuid = null; var $flink = null; var $action = null; var $app_uri = null; var $app_name = null; - + /** * Constructor * @@ -60,71 +60,71 @@ class FacebookAction extends Action function __construct($output='php://output', $indent=true, $facebook=null, $flink=null) { parent::__construct($output, $indent); - + $this->facebook = $facebook; $this->flink = $flink; - + if ($this->flink) { - $this->fbuid = $flink->foreign_id; + $this->fbuid = $flink->foreign_id; $this->user = $flink->getUser(); } - + $this->args = array(); } - + function prepare($argarray) - { + { parent::prepare($argarray); - + $this->facebook = getFacebook(); $this->fbuid = $this->facebook->require_login(); - + $this->action = $this->trimmed('action'); - + $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->flink = Foreign_link::getByForeignID($this->fbuid, FACEBOOK_SERVICE); - + return true; - + } - + function showStylesheets() { // Add a timestamp to the file so Facebook cache wont ignore our changes $ts = filemtime(INSTALLDIR.'/theme/base/css/display.css'); - - $this->element('link', array('rel' => 'stylesheet', - 'type' => 'text/css', - 'href' => theme_path('css/display.css', 'base') . '?ts=' . $ts)); - + + $this->element('link', array('rel' => 'stylesheet', + 'type' => 'text/css', + 'href' => theme_path('css/display.css', 'base') . '?ts=' . $ts)); + $theme = common_config('site', 'theme'); - + $ts = filemtime(INSTALLDIR. '/theme/' . $theme .'/css/display.css'); - + $this->element('link', array('rel' => 'stylesheet', 'type' => 'text/css', 'href' => theme_path('css/display.css', null) . '?ts=' . $ts)); - + $ts = filemtime(INSTALLDIR.'/theme/base/css/facebookapp.css'); - + $this->element('link', array('rel' => 'stylesheet', 'type' => 'text/css', 'href' => theme_path('css/facebookapp.css', 'base') . '?ts=' . $ts)); } - + function showScripts() { // Add a timestamp to the file so Facebook cache wont ignore our changes $ts = filemtime(INSTALLDIR.'/js/facebookapp.js'); - + $this->element('script', array('src' => common_path('js/facebookapp.js') . '?ts=' . $ts)); } - + /** * Start an Facebook ready HTML document * @@ -138,11 +138,11 @@ class FacebookAction extends Action * @return void */ - function startHTML($type=null) - { + function startHTML($type=null) + { $this->showStylesheets(); $this->showScripts(); - + $this->elementStart('div', array('class' => 'facebook-page')); } @@ -177,18 +177,18 @@ class FacebookAction extends Action $this->showFooter(); $this->elementEnd('div'); } - + function showAside() { } function showHead($error, $success) { - + if ($error) { $this->element("h1", null, $error); } - + if ($success) { $this->element("h1", null, $success); } @@ -198,10 +198,10 @@ class FacebookAction extends Action $this->element('fb:add-section-button', array('section' => 'profile')); $this->elementEnd('span'); $this->elementEnd('fb:if-section-not-added'); - + } - + // Make this into a widget later function showLocalNav() { @@ -213,12 +213,14 @@ class FacebookAction extends Action array('href' => 'index.php', 'title' => _('Home')), _('Home')); $this->elementEnd('li'); - $this->elementStart('li', - array('class' => - ($this->action == 'facebookinvite') ? 'current' : 'facebook_invite')); - $this->element('a', - array('href' => 'invite.php', 'title' => _('Invite')), _('Invite')); - $this->elementEnd('li'); + if (common_config('invite', 'enabled')) { + $this->elementStart('li', + array('class' => + ($this->action == 'facebookinvite') ? 'current' : 'facebook_invite')); + $this->element('a', + array('href' => 'invite.php', 'title' => _('Invite')), _('Invite')); + $this->elementEnd('li'); + } $this->elementStart('li', array('class' => @@ -229,8 +231,8 @@ class FacebookAction extends Action $this->elementEnd('li'); $this->elementEnd('ul'); - } - + } + /** * Show header of the page. * @@ -245,7 +247,7 @@ class FacebookAction extends Action $this->showNoticeForm(); $this->elementEnd('div'); } - + /** * Show page, a template method. * @@ -258,7 +260,7 @@ class FacebookAction extends Action $this->showBody(); $this->endHTML(); } - + function showInstructions() { @@ -278,7 +280,7 @@ class FacebookAction extends Action $this->element('a', array('href' => common_local_url('register')), _('Register')); $this->text($loginmsg_part2); - $this->elementEnd('p'); + $this->elementEnd('p'); $this->elementEnd('dd'); $this->elementEnd('dl'); @@ -317,7 +319,7 @@ class FacebookAction extends Action $this->elementEnd('ul'); $this->submit('submit', _('Login')); - $this->elementEnd('fieldset'); + $this->elementEnd('fieldset'); $this->elementEnd('form'); $this->elementStart('p'); @@ -329,73 +331,73 @@ class FacebookAction extends Action $this->elementEnd('div'); } - - + + function updateProfileBox($notice) { // 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']; + $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 *, + .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 + margin-right:11px; + margin-bottom:11px } - .entry-title { - margin-bottom:11px; - } + .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 { + clear:both; + } div.entry-content dl, div.entry-content dt, div.entry-content dd { display:inline; - text-transform:lowercase; + text-transform:lowercase; } div.entry-content dd, - div.entry-content .device dt { - margin-left:0; - margin-right:5px; + div.entry-content .device dt { + margin-left:0; + margin-right:5px; } div.entry-content dl.timestamp dt, - div.entry-content dl.response 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>'; + #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(); @@ -407,12 +409,12 @@ class FacebookAction extends Action $fbml_main = "<fb:narrow>$style " . $this->xw->outputMemory(false) . "</fb:narrow>"; - $this->facebook->api_client->profile_setFBML(null, $this->fbuid, $fbml, null, null, $fbml_main); + $this->facebook->api_client->profile_setFBML(null, $this->fbuid, $fbml, null, null, $fbml_main); $this->xw->openURI('php://output'); } - - + + /** * Generate pagination links * @@ -457,24 +459,24 @@ class FacebookAction extends Action $this->elementEnd('div'); } } - - function updateFacebookStatus($notice) + + 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; @@ -492,9 +494,9 @@ class FacebookAction extends Action $cmd = $inter->handle_command($user, $content_shortened); if ($cmd) { - + // XXX fix this - + $cmd->execute(new WebChannel()); return; } @@ -510,20 +512,20 @@ class FacebookAction extends Action } common_broadcast_notice($notice); - + // Also update the user's Facebook status $this->updateFacebookStatus($notice); $this->updateProfileBox($notice); - + } } -class FacebookNoticeForm extends NoticeForm +class FacebookNoticeForm extends NoticeForm { - + var $post_action = null; - + /** * Constructor * @@ -532,13 +534,13 @@ class FacebookNoticeForm extends NoticeForm * @param string $content content to pre-fill */ - function __construct($out=null, $action=null, $content=null, + function __construct($out=null, $action=null, $content=null, $post_action=null, $user=null) { parent::__construct($out, $action, $content, $user); $this->post_action = $post_action; } - + /** * Action of the form * @@ -554,7 +556,7 @@ class FacebookNoticeForm extends NoticeForm class FacebookNoticeList extends NoticeList { - + /** * constructor * @@ -565,7 +567,7 @@ class FacebookNoticeList extends NoticeList { parent::__construct($notice, $out); } - + /** * show the list of notices * @@ -619,7 +621,7 @@ class FacebookNoticeList extends NoticeList } class FacebookNoticeListItem extends NoticeListItem -{ +{ /** * constructor @@ -646,51 +648,19 @@ class FacebookNoticeListItem extends NoticeListItem function show() { $this->showStart(); + $this->showNotice(); + $this->showNoticeInfo(); - $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'); + // XXX: Need to update to show attachements and controls $this->showEnd(); } - function showNoticeLink() - { - $noticeurl = common_local_url('shownotice', - array('notice' => $this->notice->id)); - // XXX: we need to figure this out better. Is this right? - if (strcmp($this->notice->uri, $noticeurl) != 0 && - preg_match('/^http/', $this->notice->uri)) { - $noticeurl = $this->notice->uri; - } - - $this->out->elementStart('dl', 'timestamp'); - $this->out->element('dt', null, _('Published')); - $this->out->elementStart('dd', null); - $this->out->elementStart('a', array('rel' => 'bookmark', - 'href' => $noticeurl)); - $dt = common_date_iso8601($this->notice->created); - $this->out->element('abbr', array('class' => 'published', - 'title' => $dt), - common_date_string($this->notice->created)); - $this->out->elementEnd('a'); - $this->out->elementEnd('dd'); - $this->out->elementEnd('dl'); - } - } - class FacebookProfileBoxNotice extends FacebookNoticeListItem -{ - +{ + /** * constructor * @@ -703,36 +673,24 @@ class FacebookProfileBoxNotice extends FacebookNoticeListItem { parent::__construct($notice, $out); } - + /** - * Recipe function for displaying a single notice in the - * Facebook App's Profile + * Recipe function for displaying a single notice in the + * Facebook App profile notice box * * @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->showNotice(); + $this->showNoticeInfo(); $this->showAppLink(); - } - function showAppLink() + function showAppLink() { - + $this->facebook = getFacebook(); $app_props = $this->facebook->api_client->Admin_getAppProperties( @@ -740,7 +698,7 @@ class FacebookProfileBoxNotice extends FacebookNoticeListItem $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); diff --git a/lib/facebookutil.php b/lib/facebookutil.php index 242d2e06f..632ec4bad 100644 --- a/lib/facebookutil.php +++ b/lib/facebookutil.php @@ -1,7 +1,7 @@ <?php /* * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -51,6 +51,10 @@ function updateProfileBox($facebook, $flink, $notice) { function isFacebookBound($notice, $flink) { + if (empty($flink)) { + return false; + } + // 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 " . @@ -86,10 +90,10 @@ function isFacebookBound($notice, $flink) { if ($result != 1) { $user = $flink->getUser(); - $msg = "Can't send notice $notice->id to Facebook " . + $msg = "Not sending notice $notice->id to Facebook " . "because user $user->nickname hasn't given the " . 'Facebook app \'status_update\' permission.'; - common_log(LOG_INFO, $msg); + common_debug($msg); $success = false; } @@ -108,13 +112,16 @@ function facebookBroadcastNotice($notice) { $facebook = getFacebook(); $flink = Foreign_link::getByUserID($notice->profile_id, FACEBOOK_SERVICE); - $fbuid = $flink->foreign_id; if (isFacebookBound($notice, $flink)) { $status = null; + $fbuid = $flink->foreign_id; + + $user = $flink->getUser(); // Get the status 'verb' (prefix) the user has set + try { $prefix = $facebook->api_client-> data_getUserPreference(FACEBOOK_NOTICE_PREFIX, $fbuid); @@ -122,23 +129,79 @@ function facebookBroadcastNotice($notice) $status = "$prefix $notice->content"; } catch(FacebookRestClientException $e) { - common_log(LOG_ERR, $e->getMessage()); - return false; + common_log(LOG_WARNING, $e->getMessage()); + common_log(LOG_WARNING, + 'Unable to get the status verb setting from Facebook ' . + "for $user->nickname (user id: $user->id)."); } - // Okay, we're good to go! + // Okay, we're good to go, update the FB status 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; + common_log(LOG_ERR, + 'Unable to update Facebook status for ' . + "$user->nickname (user id: $user->id)!"); + + $code = $e->getCode(); - // Should we remove flink if this fails? + if ($code >= 200) { + + // 200 The application does not have permission to operate on the passed in uid parameter. + // 250 Updating status requires the extended permission status_update. + // see: http://wiki.developers.facebook.com/index.php/Users.setStatus#Example_Return_XML + + remove_facebook_app($flink); + } + + } + + // Now try to update the profile box + + try { + updateProfileBox($facebook, $flink, $notice); + } catch(FacebookRestClientException $e) { + common_log(LOG_WARNING, $e->getMessage()); + common_log(LOG_WARNING, + 'Unable to update Facebook profile box for ' . + "$user->nickname (user id: $user->id)."); } } return true; } + +function remove_facebook_app($flink) +{ + + $user = $flink->getUser(); + + common_log(LOG_INFO, 'Removing Facebook App Foreign link for ' . + "user $user->nickname (user id: $user->id)."); + + $result = $flink->delete(); + + if (empty($result)) { + common_log(LOG_ERR, 'Could not remove Facebook App ' . + "Foreign_link for $user->nickname (user id: $user->id)!"); + common_log_db_error($flink, 'DELETE', __FILE__); + } + + // Notify the user that we are removing their FB app access + + $result = mail_facebook_app_removed($user); + + if (!$result) { + + $msg = 'Unable to send email to notify ' . + "$user->nickname (user id: $user->id) " . + 'that their Facebook app link was ' . + 'removed!'; + + common_log(LOG_WARNING, $msg); + } + +} diff --git a/lib/form.php b/lib/form.php index 5317df471..f872aef0b 100644 --- a/lib/form.php +++ b/lib/form.php @@ -52,6 +52,8 @@ require_once INSTALLDIR.'/lib/widget.php'; class Form extends Widget { + var $enctype = null; + /** * Show the form * @@ -63,11 +65,15 @@ class Form extends Widget function show() { - $this->out->elementStart('form', - array('id' => $this->id(), - 'class' => $this->formClass(), - 'method' => 'post', - 'action' => $this->action())); + $attributes = array('id' => $this->id(), + 'class' => $this->formClass(), + 'method' => 'post', + 'action' => $this->action()); + + if (!empty($this->enctype)) { + $attributes['enctype'] = $this->enctype; + } + $this->out->elementStart('form', $attributes); $this->out->elementStart('fieldset'); $this->formLegend(); $this->sessionToken(); diff --git a/lib/galleryaction.php b/lib/galleryaction.php index 8fa11a756..b389fc00f 100644 --- a/lib/galleryaction.php +++ b/lib/galleryaction.php @@ -1,7 +1,7 @@ <?php /** * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -27,10 +27,9 @@ require_once INSTALLDIR.'/lib/profilelist.php'; define('AVATARS_PER_PAGE', 80); -class GalleryAction extends Action +class GalleryAction extends OwnerDesignAction { var $profile = null; - var $user = null; var $page = null; var $tag = null; diff --git a/lib/groupdesignaction.php b/lib/groupdesignaction.php new file mode 100644 index 000000000..58777c283 --- /dev/null +++ b/lib/groupdesignaction.php @@ -0,0 +1,89 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Base class for actions that use the current user's design + * + * 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 Action + * @package Laconica + * @author Zach Copley <zach@controlyourself.ca> + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +/** + * Base class for actions that use a group's design + * + * Pages related to groups can be themed with a design. + * This superclass returns that design. + * + * @category Action + * @package Laconica + * @author Zach Copley <zach@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + * + */ +class GroupDesignAction extends Action { + + /** The group in question */ + var $group = null; + + /** + * Show the groups's design stylesheet + * + * @return nothing + */ + function showStylesheets() + { + parent::showStylesheets(); + + $user = common_current_user(); + + if (empty($user) || $user->viewdesigns) { + $design = $this->getDesign(); + + if (!empty($design)) { + $design->showCSS($this); + } + } + } + + /** + * A design for this action + * + * if the group attribute has been set, returns that group's + * design. + * + * @return Design a design object to use + */ + + function getDesign() + { + if (empty($this->group)) { + return null; + } + + return $this->group->getDesign(); + } +} diff --git a/lib/groupeditform.php b/lib/groupeditform.php index ca674f3c8..fbb39129b 100644 --- a/lib/groupeditform.php +++ b/lib/groupeditform.php @@ -111,7 +111,6 @@ class GroupEditForm extends Form } } - /** * Name of the form * @@ -131,32 +130,58 @@ class GroupEditForm extends Form function formData() { + if ($this->group) { + $id = $this->group->id; + $nickname = $this->group->nickname; + $fullname = $this->group->fullname; + $homepage = $this->group->homepage; + $description = $this->group->description; + $location = $this->group->location; + } else { + $id = ''; + $nickname = ''; + $fullname = ''; + $homepage = ''; + $description = ''; + $location = ''; + } + $this->out->elementStart('ul', 'form_data'); $this->out->elementStart('li'); - $this->out->hidden('groupid', $this->group->id); + $this->out->hidden('groupid', $id); $this->out->input('nickname', _('Nickname'), - ($this->out->arg('nickname')) ? $this->out->arg('nickname') : $this->group->nickname, + ($this->out->arg('nickname')) ? $this->out->arg('nickname') : $nickname, _('1-64 lowercase letters or numbers, no punctuation or spaces')); $this->out->elementEnd('li'); $this->out->elementStart('li'); $this->out->input('fullname', _('Full name'), - ($this->out->arg('fullname')) ? $this->out->arg('fullname') : $this->group->fullname); + ($this->out->arg('fullname')) ? $this->out->arg('fullname') : $fullname); $this->out->elementEnd('li'); $this->out->elementStart('li'); $this->out->input('homepage', _('Homepage'), - ($this->out->arg('homepage')) ? $this->out->arg('homepage') : $this->group->homepage, + ($this->out->arg('homepage')) ? $this->out->arg('homepage') : $homepage, _('URL of the homepage or blog of the group or topic')); $this->out->elementEnd('li'); $this->out->elementStart('li'); $this->out->textarea('description', _('Description'), - ($this->out->arg('description')) ? $this->out->arg('description') : $this->group->description, + ($this->out->arg('description')) ? $this->out->arg('description') : $description, _('Describe the group or topic in 140 chars')); $this->out->elementEnd('li'); $this->out->elementStart('li'); $this->out->input('location', _('Location'), - ($this->out->arg('location')) ? $this->out->arg('location') : $this->group->location, + ($this->out->arg('location')) ? $this->out->arg('location') : $location, _('Location for the group, if any, like "City, State (or Region), Country"')); $this->out->elementEnd('li'); + if (common_config('group', 'maxaliases') > 0) { + $aliases = (empty($this->group)) ? array() : $this->group->getAliases(); + $this->out->elementStart('li'); + $this->out->input('aliases', _('Aliases'), + ($this->out->arg('aliases')) ? $this->out->arg('aliases') : + (!empty($aliases)) ? implode(' ', $aliases) : '', + sprintf(_('Extra nicknames for the group, comma- or space- separated, max %d'), + common_config('group', 'maxaliases')));; + $this->out->elementEnd('li'); + } $this->out->elementEnd('ul'); } diff --git a/lib/grouplist.php b/lib/grouplist.php index 1b8547499..1ded5160b 100644 --- a/lib/grouplist.php +++ b/lib/grouplist.php @@ -166,7 +166,7 @@ class GroupList extends Widget if ($user->isMember($this->group)) { $lf = new LeaveForm($this->out, $this->group); $lf->show(); - } else { + } else if (!Group_block::isBlocked($this->group, $user->getProfile())) { $jf = new JoinForm($this->out, $this->group); $jf->show(); } diff --git a/lib/groupnav.php b/lib/groupnav.php index 90bdc1014..9e530c447 100644 --- a/lib/groupnav.php +++ b/lib/groupnav.php @@ -95,6 +95,12 @@ class GroupNav extends Widget $cur = common_current_user(); if ($cur && $cur->isAdmin($this->group)) { + $this->out->menuItem(common_local_url('blockedfromgroup', array('nickname' => + $nickname)), + _('Blocked'), + sprintf(_('%s blocked users'), $nickname), + $action_name == 'blockedfromgroup', + 'nav_group_blocked'); $this->out->menuItem(common_local_url('editgroup', array('nickname' => $nickname)), _('Admin'), @@ -107,6 +113,12 @@ class GroupNav extends Widget sprintf(_('Add or edit %s logo'), $nickname), $action_name == 'grouplogo', 'nav_group_logo'); + $this->out->menuItem(common_local_url('groupdesignsettings', array('nickname' => + $nickname)), + _('Design'), + sprintf(_('Add or edit %s design'), $nickname), + $action_name == 'groupdesignsettings', + 'nav_group_design'); } $this->out->elementEnd('ul'); } diff --git a/lib/grouptagcloudsection.php b/lib/grouptagcloudsection.php index 5d68af28b..9b7a10f6b 100644 --- a/lib/grouptagcloudsection.php +++ b/lib/grouptagcloudsection.php @@ -32,7 +32,7 @@ if (!defined('LACONICA')) { } /** - * Personal tag cloud section + * Group tag cloud section * * @category Widget * @package Laconica @@ -64,12 +64,27 @@ class GroupTagCloudSection extends TagCloudSection $weightexpr='sum(exp(-(now() - notice_tag.created) / %s))'; } + $names = $this->group->getAliases(); + + $names = array_merge(array($this->group->nickname), $names); + + // XXX This is dumb. + + $quoted = array(); + + foreach ($names as $name) { + $quoted[] = "\"$name\""; + } + + $namestring = implode(',', $quoted); + $qry = 'SELECT notice_tag.tag, '. $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 ' . 'WHERE group_inbox.group_id = %d ' . + 'AND notice_tag.tag not in (%s) '. 'GROUP BY notice_tag.tag ' . 'ORDER BY weight DESC '; @@ -85,9 +100,9 @@ class GroupTagCloudSection extends TagCloudSection $tag = Memcached_DataObject::cachedQuery('Notice_tag', sprintf($qry, common_config('tag', 'dropoff'), - $this->group->id), + $this->group->id, + $namestring), 3600); return $tag; } - } diff --git a/lib/imagefile.php b/lib/imagefile.php index 0c93b257e..52e4c4b22 100644 --- a/lib/imagefile.php +++ b/lib/imagefile.php @@ -72,7 +72,8 @@ class ImageFile break; 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())); + throw new Exception(sprintf(_('That file is too big. The maximum file size is %d.'), + ImageFile::maxFileSize())); return; case UPLOAD_ERR_PARTIAL: @unlink($_FILES[$param]['tmp_name']); diff --git a/lib/jabber.php b/lib/jabber.php index 7d584ad01..e15076160 100644 --- a/lib/jabber.php +++ b/lib/jabber.php @@ -77,6 +77,14 @@ function jabber_daemon_address() return common_config('xmpp', 'user') . '@' . common_config('xmpp', 'server'); } +class Sharing_XMPP extends XMPPHP_XMPP +{ + function getSocket() + { + return $this->socket; + } +} + /** * connect the configured Jabber account to the configured server * @@ -89,7 +97,7 @@ function jabber_connect($resource=null) { static $conn = null; if (!$conn) { - $conn = new XMPPHP_XMPP(common_config('xmpp', 'host') ? + $conn = new Sharing_XMPP(common_config('xmpp', 'host') ? common_config('xmpp', 'host') : common_config('xmpp', 'server'), common_config('xmpp', 'port'), diff --git a/lib/jsonsearchresultslist.php b/lib/jsonsearchresultslist.php index f786c20a8..7beea9328 100644 --- a/lib/jsonsearchresultslist.php +++ b/lib/jsonsearchresultslist.php @@ -89,6 +89,7 @@ class JSONSearchResultsList function show() { $cnt = 0; + $this->max_id = 0; $time_start = microtime(true); diff --git a/lib/language.php b/lib/language.php index cd6498d30..3ea3dd2aa 100644 --- a/lib/language.php +++ b/lib/language.php @@ -108,7 +108,7 @@ function get_all_languages() { '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' => 1, 'lang' => 'en_GB', 'name' => 'English (British)', 'direction' => 'ltr'), - 'en' => array('q' => 1, 'lang' => 'en', 'name' => 'English', 'direction' => 'ltr'), + 'en' => array('q' => 1, 'lang' => 'en_US', 'name' => 'English (US)', 'direction' => 'ltr'), 'es' => array('q' => 1, 'lang' => 'es', 'name' => 'Spanish', 'direction' => 'ltr'), 'fi' => array('q' => 1, 'lang' => 'fi', 'name' => 'Finnish', 'direction' => 'ltr'), 'fr-fr' => array('q' => 1, 'lang' => 'fr_FR', 'name' => 'French', 'direction' => 'ltr'), diff --git a/lib/mail.php b/lib/mail.php index 4e1f1dbb1..90ee3c992 100644 --- a/lib/mail.php +++ b/lib/mail.php @@ -625,3 +625,75 @@ function mail_notify_attn($user, $notice) common_init_locale(); mail_to_user($user, $subject, $body); } + +/** + * Send a mail message to notify a user that her Twitter bridge link + * has stopped working, and therefore has been removed. This can + * happen when the user changes her Twitter password, or otherwise + * revokes access. + * + * @param User $user user whose Twitter bridge link has been removed + * + * @return boolean success flag + */ + +function mail_twitter_bridge_removed($user) +{ + common_init_locale($user->language); + + $profile = $user->getProfile(); + + $subject = sprintf(_('Your Twitter bridge has been disabled.')); + + $body = sprintf(_("Hi, %1\$s. We're sorry to inform you that your " . + 'link to Twitter has been disabled. Your Twitter credentials ' . + 'have either changed (did you recently change your Twitter ' . + 'password?) or you have otherwise revoked our access to your ' . + "Twitter account.\n\n" . + 'You can re-enable your Twitter bridge by visiting your ' . + "Twitter settings page:\n\n\t%2\$s\n\n" . + "Regards,\n%3\$s\n"), + $profile->getBestName(), + common_local_url('twittersettings'), + common_config('site', 'name')); + + common_init_locale(); + return mail_to_user($user, $subject, $body); +} + +/** + * Send a mail message to notify a user that her Facebook Application + * access has been removed. + * + * @param User $user user whose Facebook app link has been removed + * + * @return boolean success flag + */ + +function mail_facebook_app_removed($user) +{ + common_init_locale($user->language); + + $profile = $user->getProfile(); + + $site_name = common_config('site', 'name'); + + $subject = sprintf( + _('Your %s Facebook application access has been disabled.', + $site_name)); + + $body = sprintf(_("Hi, %1\$s. We're sorry to inform you that we are " . + 'unable to update your Facebook status from %s, and have disabled ' . + 'the Facebook application for your account. This may be because ' . + 'you have removed the Facebook application\'s authorization, or ' . + 'have deleted your Facebook account. You can re-enable the ' . + 'Facebook application and automatic status updating by ' . + "re-installing the %1\$s Facebook application.\n\nRegards,\n\n%1\$s"), + $site_name); + + common_init_locale(); + return mail_to_user($user, $subject, $body); + +} + + diff --git a/lib/mailbox.php b/lib/mailbox.php index 01bbf5721..f1f6e98c1 100644 --- a/lib/mailbox.php +++ b/lib/mailbox.php @@ -31,8 +31,6 @@ if (!defined('LACONICA')) { exit(1); } -require_once INSTALLDIR.'/lib/personal.php'; - define('MESSAGES_PER_PAGE', 20); /** @@ -47,11 +45,11 @@ define('MESSAGES_PER_PAGE', 20); * @see OutboxAction */ -class MailboxAction extends PersonalAction +class MailboxAction extends CurrentUserDesignAction { var $page = null; - function prepare($args) + function prepare($args) { parent::prepare($args); @@ -265,12 +263,12 @@ class MailboxAction extends PersonalAction * Returns either the name (and link) of the API client that posted the notice, * or one of other other channels. * - * @param string $source the source of the message + * @param string $source the source of the message * * @return void */ - function showSource($source) + function showSource($source) { $source_name = _($source); switch ($source) { @@ -297,4 +295,17 @@ class MailboxAction extends PersonalAction return; } + /** + * Mailbox actions are read only + * + * @param array $args other arguments + * + * @return boolean + */ + + function isReadOnly($args) + { + return true; + } + } diff --git a/lib/noticeform.php b/lib/noticeform.php index 606b5d028..4e2a2edd6 100644 --- a/lib/noticeform.php +++ b/lib/noticeform.php @@ -89,7 +89,10 @@ class NoticeForm extends Form } else { $this->user = common_current_user(); } - + + if (common_config('attachments', 'uploads')) { + $this->enctype = 'multipart/form-data'; + } } /** @@ -142,13 +145,19 @@ class NoticeForm extends Form 'rows' => 4, 'name' => 'status_textarea'), ($this->content) ? $this->content : ''); - $this->out->elementStart('dl', 'form_note'); $this->out->element('dt', null, _('Available characters')); $this->out->element('dd', array('id' => 'notice_text-count'), '140'); $this->out->elementEnd('dl'); - + if (common_config('attachments', 'uploads')) { + $this->out->element('label', array('for' => 'notice_data-attach'),_('Attach')); + $this->out->element('input', array('id' => 'notice_data-attach', + 'type' => 'file', + 'name' => 'attach', + 'title' => _('Attach a file'))); + $this->out->hidden('MAX_FILE_SIZE', common_config('attachments', 'file_quota')); + } if ($this->action) { $this->out->hidden('notice_return-to', $this->action, 'returnto'); } diff --git a/lib/noticelist.php b/lib/noticelist.php index 4182d8808..44726a17b 100644 --- a/lib/noticelist.php +++ b/lib/noticelist.php @@ -34,6 +34,7 @@ if (!defined('LACONICA')) { require_once INSTALLDIR.'/lib/favorform.php'; require_once INSTALLDIR.'/lib/disfavorform.php'; +require_once INSTALLDIR.'/lib/attachmentlist.php'; /** * widget for displaying a list of notices @@ -49,7 +50,6 @@ require_once INSTALLDIR.'/lib/disfavorform.php'; * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://laconi.ca/ * @see Notice - * @see StreamAction * @see NoticeListItem * @see ProfileNoticeList */ @@ -85,7 +85,7 @@ class NoticeList extends Widget { $this->out->elementStart('div', array('id' =>'notices_primary')); $this->out->element('h2', null, _('Notices')); - $this->out->elementStart('ul', array('class' => 'notices')); + $this->out->elementStart('ol', array('class' => 'notices xoxo')); $cnt = 0; @@ -100,7 +100,7 @@ class NoticeList extends Widget $item->show(); } - $this->out->elementEnd('ul'); + $this->out->elementEnd('ol'); $this->out->elementEnd('div'); return $cnt; @@ -197,7 +197,7 @@ class NoticeListItem extends Widget $this->out->elementStart('div', 'entry-content'); $this->showNoticeLink(); $this->showNoticeSource(); - $this->showReplyTo(); + $this->showContext(); $this->out->elementEnd('div'); } @@ -366,6 +366,7 @@ class NoticeListItem extends Widget $this->out->element('abbr', array('class' => 'published', 'title' => $dt), common_date_string($this->notice->created)); + $this->out->elementEnd('a'); $this->out->elementEnd('dd'); $this->out->elementEnd('dl'); @@ -421,17 +422,18 @@ class NoticeListItem extends Widget * @return void */ - function showReplyTo() + function showContext() { - if ($this->notice->reply_to) { - $replyurl = common_local_url('shownotice', - array('notice' => $this->notice->reply_to)); + // XXX: also show context if there are replies to this notice + if (!empty($this->notice->conversation) + && $this->notice->conversation != $this->notice->id) { + $convurl = common_local_url('conversation', + array('id' => $this->notice->conversation)); $this->out->elementStart('dl', 'response'); $this->out->element('dt', null, _('To')); $this->out->elementStart('dd'); - $this->out->element('a', array('href' => $replyurl, - 'rel' => 'in-reply-to'), - _('in reply to')); + $this->out->element('a', array('href' => $convurl.'#notice-'.$this->notice->id), + _('in context')); $this->out->elementEnd('dd'); $this->out->elementEnd('dl'); } diff --git a/lib/noticesection.php b/lib/noticesection.php index 94c2738ef..ca1432686 100644 --- a/lib/noticesection.php +++ b/lib/noticesection.php @@ -51,17 +51,13 @@ class NoticeSection extends Section function showContent() { $notices = $this->getNotices(); - $cnt = 0; - - $this->out->elementStart('ul', 'notices'); - + $this->out->elementStart('ol', 'notices xoxo'); while ($notices->fetch() && ++$cnt <= NOTICES_PER_SECTION) { $this->showNotice($notices); } - $this->out->elementEnd('ul'); - + $this->out->elementEnd('ol'); return ($cnt > NOTICES_PER_SECTION); } @@ -100,6 +96,37 @@ class NoticeSection extends Section $this->out->elementStart('p', 'entry-content'); $this->out->raw($notice->rendered); + + $notice_link_cfg = common_config('site', 'notice_link'); + if ('direct' === $notice_link_cfg) { + $this->out->text(' ('); + $this->out->element('a', array('href' => $notice->uri), 'see'); + $this->out->text(')'); + } elseif ('attachment' === $notice_link_cfg) { + if ($count = $notice->hasAttachments()) { + // link to attachment(s) pages + if (1 === $count) { + $f2p = File_to_post::staticGet('post_id', $notice->id); + $href = common_local_url('attachment', array('attachment' => $f2p->file_id)); + $att_class = 'attachment'; + } else { + $href = common_local_url('attachments', array('notice' => $notice->id)); + $att_class = 'attachments'; + } + + $clip = theme_path('images/icons/clip.png', 'base'); + $this->out->elementStart('a', array('class' => $att_class, 'style' => "font-style: italic;", 'href' => $href, 'title' => "# of attachments: $count")); + $this->out->raw(" ($count "); + $this->out->element('img', array('style' => 'display: inline', 'align' => 'top', 'width' => 20, 'height' => 20, 'src' => $clip, 'alt' => 'alt')); + $this->out->text(')'); + $this->out->elementEnd('a'); + } else { + $this->out->text(' ('); + $this->out->element('a', array('href' => $notice->uri), 'see'); + $this->out->text(')'); + } + } + $this->out->elementEnd('p'); if (!empty($notice->value)) { $this->out->elementStart('p'); diff --git a/lib/oauthstore.php b/lib/oauthstore.php index 183164e17..f224c6c22 100644 --- a/lib/oauthstore.php +++ b/lib/oauthstore.php @@ -1,7 +1,7 @@ <?php /* * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by diff --git a/lib/omb.php b/lib/omb.php index 40cb847df..4f6a96095 100644 --- a/lib/omb.php +++ b/lib/omb.php @@ -1,7 +1,7 @@ <?php /* * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by diff --git a/lib/openid.php b/lib/openid.php index 3af7a39cf..0b7633284 100644 --- a/lib/openid.php +++ b/lib/openid.php @@ -1,7 +1,7 @@ <?php /* * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by diff --git a/lib/ownerdesignaction.php b/lib/ownerdesignaction.php new file mode 100644 index 000000000..785b8a93d --- /dev/null +++ b/lib/ownerdesignaction.php @@ -0,0 +1,92 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Base class for actions that use the page owner's design + * + * 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 Action + * @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); +} + +/** + * Base class for actions that use the page owner's design + * + * Some pages have a clear "owner" -- like the profile page, subscriptions + * pages, etc. This superclass uses that owner's chosen design for the page + * design. + * + * @category Action + * @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 OwnerDesignAction extends Action { + + /** The user for this page. */ + + var $user = null; + + /** + * Show the owner's design stylesheet + * + * @return nothing + */ + function showStylesheets() + { + parent::showStylesheets(); + + $user = common_current_user(); + + if (empty($user) || $user->viewdesigns) { + $design = $this->getDesign(); + + if (!empty($design)) { + $design->showCSS($this); + } + } + } + + /** + * A design for this action + * + * if the user attribute has been set, returns that user's + * design. + * + * @return Design a design object to use + */ + + function getDesign() + { + if (empty($this->user)) { + return null; + } + + return $this->user->getDesign(); + } +} diff --git a/lib/peoplesearchresults.php b/lib/peoplesearchresults.php deleted file mode 100644 index d3f840852..000000000 --- a/lib/peoplesearchresults.php +++ /dev/null @@ -1,75 +0,0 @@ -<?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($args) - { - return true; - } -} - diff --git a/lib/ping.php b/lib/ping.php index 3de541e9a..d26c73417 100644 --- a/lib/ping.php +++ b/lib/ping.php @@ -59,7 +59,7 @@ function ping_broadcast_notice($notice) { $response = xmlrpc_decode($file); - if (xmlrpc_is_fault($response)) { + if (is_array($response) && xmlrpc_is_fault($response)) { common_log(LOG_WARNING, "XML-RPC error for ping ($notify_url, $notice->id) ". "$response[faultString] ($response[faultCode])"); diff --git a/lib/popularnoticesection.php b/lib/popularnoticesection.php index a8d47ef54..e47c9b385 100644 --- a/lib/popularnoticesection.php +++ b/lib/popularnoticesection.php @@ -51,7 +51,7 @@ class PopularNoticeSection extends NoticeSection if (common_config('db', 'type') == 'pgsql') { $weightexpr='sum(exp(-extract(epoch from (now() - fave.modified)) / %s))'; if (!empty($this->out->tag)) { - $tag = pg_escape_string($this->tag); + $tag = pg_escape_string($this->out->tag); } } else { $weightexpr='sum(exp(-(now() - fave.modified) / %s))'; @@ -68,7 +68,7 @@ class PopularNoticeSection extends NoticeSection } $qry .= ' 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 ' . + 'notice.reply_to,notice.is_local,notice.source,notice.conversation ' . 'ORDER BY weight DESC'; $offset = 0; diff --git a/lib/profileaction.php b/lib/profileaction.php index 1f2e30994..9e9c79c78 100644 --- a/lib/profileaction.php +++ b/lib/profileaction.php @@ -47,18 +47,18 @@ require_once INSTALLDIR.'/lib/groupminilist.php'; * @link http://laconi.ca/ */ -class ProfileAction extends Action +class ProfileAction extends OwnerDesignAction { - var $user = null; - var $page = null; + var $page = null; var $profile = null; + var $tag = null; function prepare($args) { parent::prepare($args); $nickname_arg = $this->arg('nickname'); - $nickname = common_canonical_nickname($nickname_arg); + $nickname = common_canonical_nickname($nickname_arg); // Permanent redirect on non-canonical nickname @@ -85,10 +85,9 @@ class ProfileAction extends Action return false; } + $this->tag = $this->trimmed('tag'); $this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1; - common_set_returnto($this->selfUrl()); - return true; } @@ -109,8 +108,10 @@ class ProfileAction extends Action $this->element('h2', null, _('Subscriptions')); - if ($profile) { - $pml = new ProfileMiniList($profile, $this->user, $this); + $cnt = 0; + + if (!empty($profile)) { + $pml = new ProfileMiniList($profile, $this); $cnt = $pml->show(); if ($cnt == 0) { $this->element('p', null, _('(None)')); @@ -138,8 +139,10 @@ class ProfileAction extends Action $this->element('h2', null, _('Subscribers')); - if ($profile) { - $pml = new ProfileMiniList($profile, $this->user, $this); + $cnt = 0; + + if (!empty($profile)) { + $pml = new ProfileMiniList($profile, $this); $cnt = $pml->show(); if ($cnt == 0) { $this->element('p', null, _('(None)')); @@ -160,18 +163,9 @@ class ProfileAction extends Action function showStatistics() { - // XXX: WORM cache this - $subs = new Subscription(); - $subs->subscriber = $this->profile->id; - $subs_count = (int) $subs->count() - 1; - - $subbed = new Subscription(); - $subbed->subscribed = $this->profile->id; - $subbed_count = (int) $subbed->count() - 1; - - $notices = new Notice(); - $notices->profile_id = $this->profile->id; - $notice_count = (int) $notices->count(); + $subs_count = $this->profile->subscriptionCount(); + $subbed_count = $this->profile->subscriberCount(); + $notice_count = $this->profile->noticeCount(); $this->elementStart('div', array('id' => 'entity_statistics', 'class' => 'section')); @@ -196,7 +190,7 @@ class ProfileAction extends Action array('nickname' => $this->profile->nickname))), _('Subscriptions')); $this->elementEnd('dt'); - $this->element('dd', null, (is_int($subs_count)) ? $subs_count : '0'); + $this->element('dd', null, $subs_count); $this->elementEnd('dl'); $this->elementStart('dl', 'entity_subscribers'); @@ -205,12 +199,12 @@ class ProfileAction extends Action array('nickname' => $this->profile->nickname))), _('Subscribers')); $this->elementEnd('dt'); - $this->element('dd', 'subscribers', (is_int($subbed_count)) ? $subbed_count : '0'); + $this->element('dd', 'subscribers', $subbed_count); $this->elementEnd('dl'); $this->elementStart('dl', 'entity_notices'); $this->element('dt', null, _('Notices')); - $this->element('dd', null, (is_int($notice_count)) ? $notice_count : '0'); + $this->element('dd', null, $notice_count); $this->elementEnd('dl'); $this->elementEnd('div'); @@ -244,4 +238,5 @@ class ProfileAction extends Action $this->elementEnd('div'); } -}
\ No newline at end of file +} + diff --git a/lib/profilelist.php b/lib/profilelist.php index a4cc23555..774538a4b 100644 --- a/lib/profilelist.php +++ b/lib/profilelist.php @@ -49,25 +49,37 @@ class ProfileList extends Widget { /** Current profile, profile query. */ var $profile = null; - /** Owner of this list */ - var $owner = null; /** Action object using us. */ var $action = null; - function __construct($profile, $owner=null, $action=null) + function __construct($profile, $action=null) { parent::__construct($action); $this->profile = $profile; - $this->owner = $owner; $this->action = $action; } function show() { + $this->startList(); + $cnt = $this->showProfiles(); + $this->endList(); + return $cnt; + } + function startList() + { $this->out->elementStart('ul', 'profiles'); + } + + function endList() + { + $this->out->elementEnd('ul'); + } + function showProfiles() + { $cnt = 0; while ($this->profile->fetch()) { @@ -75,24 +87,66 @@ class ProfileList extends Widget if($cnt > PROFILES_PER_PAGE) { break; } - $this->showProfile(); + $pli = $this->newListItem($this->profile); + $pli->show(); } - $this->out->elementEnd('ul'); - return $cnt; } - function showProfile() + function newListItem($profile) + { + return new ProfileListItem($this->profile, $this->action); + } +} + +class ProfileListItem extends Widget +{ + /** Current profile. */ + var $profile = null; + /** Action object using us. */ + var $action = null; + + function __construct($profile, $action) + { + parent::__construct($action); + + $this->profile = $profile; + $this->action = $action; + } + + function show() + { + $this->startItem(); + $this->showProfile(); + $this->showActions(); + $this->endItem(); + } + + function startItem() { $this->out->elementStart('li', array('class' => 'profile', 'id' => 'profile-' . $this->profile->id)); + } - $user = common_current_user(); - $is_own = !is_null($user) && isset($this->owner) && ($user->id === $this->owner->id); + function showProfile() + { + $this->startProfile(); + $this->showAvatar(); + $this->showFullName(); + $this->showLocation(); + $this->showHomepage(); + $this->showBio(); + $this->endProfile(); + } + function startProfile() + { $this->out->elementStart('div', 'entity_profile vcard'); + } + function showAvatar() + { $avatar = $this->profile->getAvatar(AVATAR_STREAM_SIZE); $this->out->elementStart('a', array('href' => $this->profile->profileurl, 'class' => 'url')); @@ -108,7 +162,10 @@ class ProfileList extends Widget $this->out->raw($this->highlight($this->profile->nickname)); $this->out->elementEnd('span'); $this->out->elementEnd('a'); + } + function showFullName() + { if (!empty($this->profile->fullname)) { $this->out->elementStart('dl', 'entity_fn'); $this->out->element('dt', null, 'Full name'); @@ -119,6 +176,10 @@ class ProfileList extends Widget $this->out->elementEnd('dd'); $this->out->elementEnd('dl'); } + } + + function showLocation() + { if (!empty($this->profile->location)) { $this->out->elementStart('dl', 'entity_location'); $this->out->element('dt', null, _('Location')); @@ -127,6 +188,10 @@ class ProfileList extends Widget $this->out->elementEnd('dd'); $this->out->elementEnd('dl'); } + } + + function showHomepage() + { if (!empty($this->profile->homepage)) { $this->out->elementStart('dl', 'entity_url'); $this->out->element('dt', null, _('URL')); @@ -138,6 +203,10 @@ class ProfileList extends Widget $this->out->elementEnd('dd'); $this->out->elementEnd('dl'); } + } + + function showBio() + { if (!empty($this->profile->bio)) { $this->out->elementStart('dl', 'entity_note'); $this->out->element('dt', null, _('Note')); @@ -146,94 +215,64 @@ class ProfileList extends Widget $this->out->elementEnd('dd'); $this->out->elementEnd('dl'); } + } - # If we're on a list with an owner (subscriptions or subscribers)... - - if ($this->owner) { - # Get tags - $tags = Profile_tag::getTags($this->owner->id, $this->profile->id); - - $this->out->elementStart('dl', 'entity_tags'); - $this->out->elementStart('dt'); - if ($is_own) { - $this->out->element('a', array('href' => common_local_url('tagother', - array('id' => $this->profile->id))), - _('Tags')); - } else { - $this->out->text(_('Tags')); - } - $this->out->elementEnd('dt'); - $this->out->elementStart('dd'); - if ($tags) { - $this->out->elementStart('ul', 'tags xoxo'); - foreach ($tags as $tag) { - $this->out->elementStart('li'); - $this->out->element('span', 'mark_hash', '#'); - $this->out->element('a', array('rel' => 'tag', - 'href' => common_local_url($this->action->trimmed('action'), - array('nickname' => $this->owner->nickname, - 'tag' => $tag))), - $tag); - $this->out->elementEnd('li'); - } - $this->out->elementEnd('ul'); - } else { - $this->out->text(_('(none)')); - } - $this->out->elementEnd('dd'); - $this->out->elementEnd('dl'); - } - - if ($is_own) { - $this->showOwnerControls($this->profile); - } - + function endProfile() + { $this->out->elementEnd('div'); + } - $this->out->elementStart('div', 'entity_actions'); + function showActions() + { + $this->startActions(); + $this->showSubscribeButton(); + $this->endActions(); + } + function startActions() + { + $this->out->elementStart('div', 'entity_actions'); $this->out->elementStart('ul'); + } + function showSubscribeButton() + { // Is this a logged-in user, looking at someone else's // profile? + $user = common_current_user(); + if (!empty($user) && $this->profile->id != $user->id) { $this->out->elementStart('li', 'entity_subscribe'); if ($user->isSubscribed($this->profile)) { $usf = new UnsubscribeForm($this->out, $this->profile); $usf->show(); } else { - $sf = new SubscribeForm($this->out, $this->profile); - $sf->show(); - } - $this->out->elementEnd('li'); - $this->out->elementStart('li', 'entity_block'); - if ($user->id == $this->owner->id) { - $this->showBlockForm(); + // Is it a local user? can't remote sub from a list + // XXX: make that possible! + $other = User::staticGet('id', $this->profile->id); + if (!empty($other)) { + $sf = new SubscribeForm($this->out, $this->profile); + $sf->show(); + } } $this->out->elementEnd('li'); } + } + function endActions() + { $this->out->elementEnd('ul'); - $this->out->elementEnd('div'); - - $this->out->elementEnd('li'); } - /* Override this in subclasses. */ - - function showOwnerControls($profile) + function endItem() { - return; + $this->out->elementEnd('li'); } function highlight($text) { return htmlspecialchars($text); } - - function showBlockForm() - { - } } diff --git a/lib/profileminilist.php b/lib/profileminilist.php index 57496d0e9..357b4a2db 100644 --- a/lib/profileminilist.php +++ b/lib/profileminilist.php @@ -47,26 +47,38 @@ define('PROFILES_PER_MINILIST', 27); class ProfileMiniList extends ProfileList { - function show() + + function startList() { $this->out->elementStart('ul', 'entities users xoxo'); + } + + function newListItem($profile) + { + return new ProfileMiniListItem($profile, $this->action); + } + function showProfiles() + { $cnt = 0; while ($this->profile->fetch()) { $cnt++; - if($cnt > PROFILES_PER_MINILIST) { + if ($cnt > PROFILES_PER_MINILIST) { break; } - $this->showProfile(); + $pli = $this->newListItem($this->profile); + $pli->show(); } - $this->out->elementEnd('ul'); - return $cnt; } - function showProfile() +} + +class ProfileMiniListItem extends ProfileListItem +{ + function show() { $this->out->elementStart('li', 'vcard'); $this->out->elementStart('a', array('title' => $this->profile->getBestName(), diff --git a/lib/profilesection.php b/lib/profilesection.php index 8ed290e03..9ff243fb5 100644 --- a/lib/profilesection.php +++ b/lib/profilesection.php @@ -94,8 +94,8 @@ class ProfileSection extends Section $profile->fullname : $profile->nickname)); $this->out->element('span', 'fn nickname', $profile->nickname); - $this->out->elementEnd('span'); $this->out->elementEnd('a'); + $this->out->elementEnd('span'); $this->out->elementEnd('td'); if ($profile->value) { $this->out->element('td', 'value', $profile->value); diff --git a/lib/queuehandler.php b/lib/queuehandler.php index fde650d9e..c2ff10f32 100644 --- a/lib/queuehandler.php +++ b/lib/queuehandler.php @@ -1,7 +1,7 @@ <?php /* * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -17,26 +17,34 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -define('CLAIM_TIMEOUT', 1200); - if (!defined('LACONICA')) { exit(1); } require_once(INSTALLDIR.'/lib/daemon.php'); require_once(INSTALLDIR.'/classes/Queue_item.php'); require_once(INSTALLDIR.'/classes/Notice.php'); +define('CLAIM_TIMEOUT', 1200); +define('QUEUE_HANDLER_MISS_IDLE', 10); +define('QUEUE_HANDLER_HIT_IDLE', 0); + class QueueHandler extends Daemon { - var $_id = 'generic'; - function QueueHandler($id=null) + function __construct($id=null, $daemonize=true) { + parent::__construct($daemonize); + if ($id) { $this->set_id($id); } } + function timeout() + { + return 60; + } + function class_name() { return ucfirst($this->transport()) . 'Handler'; @@ -80,41 +88,16 @@ class QueueHandler extends Daemon if (!$this->start()) { return false; } - $transport = $this->transport(); - $this->log(LOG_INFO, 'checking for queued notices for "' . $transport . '"'); - do { - $qi = Queue_item::top($transport); - if ($qi) { - $this->log(LOG_INFO, 'Got item enqueued '.common_exact_date($qi->created)); - $notice = Notice::staticGet($qi->notice_id); - if ($notice) { - $this->log(LOG_INFO, 'broadcasting notice ID = ' . $notice->id); - # XXX: what to do if broadcast fails? - $result = $this->handle_notice($notice); - if (!$result) { - $this->log(LOG_WARNING, 'Failed broadcast for notice ID = ' . $notice->id); - $orig = $qi; - $qi->claimed = null; - $qi->update($orig); - $this->log(LOG_WARNING, 'Abandoned claim for notice ID = ' . $notice->id); - continue; - } - $this->log(LOG_INFO, 'finished broadcasting notice ID = ' . $notice->id); - $notice->free(); - unset($notice); - $notice = null; - } else { - $this->log(LOG_WARNING, 'queue item for notice that does not exist'); - } - $qi->delete(); - $qi->free(); - unset($qi); - $this->idle(0); - } else { - $this->clear_old_claims(); - $this->idle(5); - } - } while (true); + + $this->log(LOG_INFO, 'checking for queued notices'); + + $queue = $this->transport(); + $timeout = $this->timeout(); + + $qm = QueueManager::get(); + + $qm->service($queue, $this); + if (!$this->finish()) { return false; } @@ -123,23 +106,19 @@ class QueueHandler extends Daemon function idle($timeout=0) { - if ($timeout>0) { + if ($timeout > 0) { sleep($timeout); } } - function clear_old_claims() + function log($level, $msg) { - $qi = new Queue_item(); - $qi->transport = $this->transport(); - $qi->whereAdd('now() - claimed > '.CLAIM_TIMEOUT); - $qi->update(DB_DATAOBJECT_WHEREADD_ONLY); - $qi->free(); - unset($qi); + common_log($level, $this->class_name() . ' ('. $this->get_id() .'): '.$msg); } - function log($level, $msg) + function getSockets() { - common_log($level, $this->class_name() . ' ('. $this->get_id() .'): '.$msg); + return array(); } } + diff --git a/lib/queuemanager.php b/lib/queuemanager.php new file mode 100644 index 000000000..582c24790 --- /dev/null +++ b/lib/queuemanager.php @@ -0,0 +1,74 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Abstract class for queue managers + * + * 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 QueueManager + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @author Sarven Capadisli <csarven@controlyourself.ca> + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +class QueueManager +{ + static $qm = null; + + static function get() + { + if (empty(self::$qm)) { + + if (Event::handle('StartNewQueueManager', array(&self::$qm))) { + + $enabled = common_config('queue', 'enabled'); + $type = common_config('queue', 'subsystem'); + + if (!$enabled) { + // does everything immediately + self::$qm = new UnQueueManager(); + } else { + switch ($type) { + case 'db': + self::$qm = new DBQueueManager(); + break; + case 'stomp': + self::$qm = new StompQueueManager(); + break; + default: + throw new ServerException("No queue manager class for type '$type'"); + } + } + } + } + + return self::$qm; + } + + function enqueue($object, $queue) + { + throw ServerException("Unimplemented function 'enqueue' called"); + } + + function service($queue, $handler) + { + throw ServerException("Unimplemented function 'service' called"); + } +} diff --git a/lib/router.php b/lib/router.php index 748966567..75e72f932 100644 --- a/lib/router.php +++ b/lib/router.php @@ -101,7 +101,8 @@ class Router $main = array('login', 'logout', 'register', 'subscribe', 'unsubscribe', 'confirmaddress', 'recoverpassword', 'invite', 'favor', 'disfavor', 'sup', - 'block', 'unblock', 'subedit'); + 'block', 'unblock', 'subedit', + 'groupblock', 'groupunblock'); foreach ($main as $a) { $m->connect('main/'.$a, array('action' => $a)); @@ -131,7 +132,7 @@ class Router // settings foreach (array('profile', 'avatar', 'password', 'openid', 'im', - 'email', 'sms', 'twitter', 'other') as $s) { + 'email', 'sms', 'twitter', 'userdesign', 'other') as $s) { $m->connect('settings/'.$s, array('action' => $s.'settings')); } @@ -151,12 +152,27 @@ class Router $m->connect('search/notice/rss?q=:q', array('action' => 'noticesearchrss'), array('q' => '.+')); - // notice + $m->connect('attachment/:attachment', + array('action' => 'attachment'), + array('attachment' => '[0-9]+')); + + $m->connect('attachment/:attachment/ajax', + array('action' => 'attachment_ajax'), + array('attachment' => '[0-9]+')); + + $m->connect('attachment/:attachment/thumbnail', + array('action' => 'attachment_thumbnail'), + array('attachment' => '[0-9]+')); $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/file', + array('action' => 'file'), + array('notice' => '[0-9]+')); + $m->connect('notice/:notice', array('action' => 'shownotice'), array('notice' => '[0-9]+')); @@ -165,6 +181,12 @@ class Router array('action' => 'deletenotice'), array('notice' => '[0-9]+')); + // conversation + + $m->connect('conversation/:id', + array('action' => 'conversation'), + array('id' => '[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', @@ -205,12 +227,20 @@ class Router array('nickname' => '[a-zA-Z0-9]+')); } - foreach (array('members', 'logo', 'rss') as $n) { + foreach (array('members', 'logo', 'rss', 'designsettings') as $n) { $m->connect('group/:nickname/'.$n, array('action' => 'group'.$n), array('nickname' => '[a-zA-Z0-9]+')); } + $m->connect('group/:nickname/blocked', + array('action' => 'blockedfromgroup'), + array('nickname' => '[a-zA-Z0-9]+')); + + $m->connect('group/:nickname/makeadmin', + array('action' => 'makeadmin'), + array('nickname' => '[a-zA-Z0-9]+')); + $m->connect('group/:id/id', array('action' => 'groupbyid'), array('id' => '[0-9]+')); @@ -231,7 +261,7 @@ class Router $m->connect('api/statuses/:method', array('action' => 'api', 'apiaction' => 'statuses'), - array('method' => '(public_timeline|friends_timeline|user_timeline|update|replies|mentions|friends|followers|featured)(\.(atom|rss|xml|json))?')); + array('method' => '(public_timeline|friends_timeline|user_timeline|update|replies|mentions|show|friends|followers|featured)(\.(atom|rss|xml|json))?')); $m->connect('api/statuses/:method/:argument', array('action' => 'api', @@ -287,7 +317,7 @@ class Router $m->connect('api/friendships/:method', array('action' => 'api', 'apiaction' => 'friendships'), - array('method' => 'exists(\.(xml|json))')); + array('method' => '(show|exists)(\.(xml|json))')); // Social graph @@ -325,7 +355,8 @@ class Router $m->connect('api/favorites/:method/:argument', array('action' => 'api', - 'apiaction' => 'favorites')); + 'apiaction' => 'favorites', + array('method' => '(create|destroy)'))); $m->connect('api/favorites/:argument', array('action' => 'api', @@ -406,6 +437,16 @@ class Router array('size' => '(original|96|48|24)', 'nickname' => '[a-zA-Z0-9]{1,64}')); + $m->connect(':nickname/tag/:tag/rss', + array('action' => 'userrss'), + array('nickname' => '[a-zA-Z0-9]{1,64}'), + array('tag' => '[a-zA-Z0-9]+')); + + $m->connect(':nickname/tag/:tag', + array('action' => 'showstream'), + array('nickname' => '[a-zA-Z0-9]{1,64}'), + array('tag' => '[a-zA-Z0-9]+')); + $m->connect(':nickname', array('action' => 'showstream'), array('nickname' => '[a-zA-Z0-9]{1,64}')); diff --git a/lib/rssaction.php b/lib/rssaction.php index ddba862dc..fe3fd6f4a 100644 --- a/lib/rssaction.php +++ b/lib/rssaction.php @@ -97,7 +97,11 @@ class Rss10Action extends Action // Parent handling, including cache check parent::handle($args); // Get the list of notices - $this->notices = $this->getNotices($this->limit); + if (empty($this->tag)) { + $this->notices = $this->getNotices($this->limit); + } else { + $this->notices = $this->getTaggedNotices($this->tag, $this->limit); + } $this->showRss(); } @@ -166,7 +170,7 @@ class Rss10Action extends Action $this->elementStart('rdf:Seq'); foreach ($this->notices as $notice) { - $this->element('sioct:MicroblogPost', array('rdf:resource' => $notice->uri)); + $this->element('rdf:li', array('rdf:resource' => $notice->uri)); } $this->elementEnd('rdf:Seq'); @@ -193,16 +197,32 @@ class Rss10Action extends Action $profile = Profile::staticGet($notice->profile_id); $nurl = common_local_url('shownotice', array('notice' => $notice->id)); $creator_uri = common_profile_uri($profile); - $this->elementStart('item', array('rdf:about' => $notice->uri)); + $this->elementStart('item', array('rdf:about' => $notice->uri, + 'rdf:type' => 'http://rdfs.org/sioc/types#MicroblogPost')); $title = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content)); $this->element('title', null, $title); $this->element('link', null, $nurl); $this->element('description', null, $profile->nickname."'s status on ".common_exact_date($notice->created)); + if ($notice->rendered) { + $this->element('content:encoded', null, common_xml_safe_str($notice->rendered)); + } $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('foaf:maker', array('rdf:resource' => $creator_uri)); + $this->element('sioc:has_creator', array('rdf:resource' => $creator_uri.'#acct')); $this->element('laconica:postIcon', array('rdf:resource' => $profile->avatarUrl())); $this->element('cc:licence', array('rdf:resource' => common_config('license', 'url'))); + if ($notice->reply_to) { + $replyurl = common_local_url('shownotice', array('notice' => $notice->reply_to)); + $this->element('sioc:reply_of', array('rdf:resource' => $replyurl)); + } + $attachments = $notice->attachments(); + if($attachments){ + foreach($attachments as $attachment){ + $this->element('enc:enclosure', array('rdf:resource'=>$attachment->url,'enc:type'=>$attachment->mimetype,'enc:length'=>$attachment->size), null); + } + } + $this->elementEnd('item'); $this->creators[$creator_uri] = $profile; } @@ -212,15 +232,15 @@ class Rss10Action extends Action foreach ($this->creators as $uri => $profile) { $id = $profile->id; $nickname = $profile->nickname; - $this->elementStart('sioc:User', array('rdf:about' => $uri)); + $this->elementStart('foaf:Agent', array('rdf:about' => $uri)); $this->element('foaf:nick', null, $nickname); if ($profile->fullname) { $this->element('foaf:name', null, $profile->fullname); } - $this->element('sioc:id', null, $id); + $this->element('foaf:holdsAccount', array('rdf:resource' => $uri.'#acct')); $avatar = $profile->avatarUrl(); - $this->element('sioc:avatar', array('rdf:resource' => $avatar)); - $this->elementEnd('sioc:User'); + $this->element('foaf:depiction', array('rdf:resource' => $avatar)); + $this->elementEnd('foaf:Agent'); } } @@ -235,9 +255,11 @@ class Rss10Action extends Action 'xmlns:dc' => 'http://purl.org/dc/elements/1.1/', 'xmlns:cc' => - 'http://web.resource.org/cc/', + 'http://creativecommons.org/ns#', 'xmlns:content' => 'http://purl.org/rss/1.0/modules/content/', + 'xmlns:enc' => + 'http://purl.oclc.org/net/rss_2.0/enc#', 'xmlns:foaf' => 'http://xmlns.com/foaf/0.1/', 'xmlns:sioc' => @@ -249,10 +271,10 @@ class Rss10Action extends Action 'xmlns' => 'http://purl.org/rss/1.0/')); $this->elementStart('sioc:Site', array('rdf:about' => common_root_url())); $this->element('sioc:name', null, common_config('site', 'name')); - $this->elementStart('sioc:container_of'); + $this->elementStart('sioc:space_of'); $this->element('sioc:Container', array('rdf:about' => $channel['url'])); - $this->elementEnd('sioc:container_of'); + $this->elementEnd('sioc:space_of'); $this->elementEnd('sioc:Site'); } diff --git a/lib/search_engines.php b/lib/search_engines.php index 7b9dbb618..772f41883 100644 --- a/lib/search_engines.php +++ b/lib/search_engines.php @@ -1,7 +1,7 @@ <?php /* * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -118,12 +118,20 @@ class MySQLSearch extends SearchEngine } return true; } else if ('identica_notices' === $this->table) { - $this->target->whereAdd('MATCH(content) ' . - 'AGAINST (\''.addslashes($q).'\' IN BOOLEAN MODE)'); + + // Don't show imported notices + $this->target->whereAdd('notice.is_local != ' . NOTICE_GATEWAY); + if (strtolower($q) != $q) { + $this->target->whereAdd("( MATCH(content) AGAINST ('" . addslashes($q) . + "' IN BOOLEAN MODE)) OR ( MATCH(content) " . + "AGAINST ('" . addslashes(strtolower($q)) . + "' IN BOOLEAN MODE))"); + } else { $this->target->whereAdd('MATCH(content) ' . - 'AGAINST (\''.addslashes(strtolower($q)).'\' IN BOOLEAN MODE)', 'OR'); + 'AGAINST (\''.addslashes($q).'\' IN BOOLEAN MODE)'); } + return true; } else { throw new ServerException('Unknown table: ' . $this->table); @@ -131,6 +139,28 @@ class MySQLSearch extends SearchEngine } } +class MySQLLikeSearch extends SearchEngine +{ + function query($q) + { + if ('identica_people' === $this->table) { + $qry = sprintf('(nickname LIKE "%%%1$s%%" OR '. + ' fullname LIKE "%%%1$s%%" OR '. + ' location LIKE "%%%1$s%%" OR '. + ' bio LIKE "%%%1$s%%" OR '. + ' homepage LIKE "%%%1$s%%")', addslashes($q)); + } else if ('identica_notices' === $this->table) { + $qry = sprintf('content LIKE "%%%1$s%%"', addslashes($q)); + } else { + throw new ServerException('Unknown table: ' . $this->table); + } + + $this->target->whereAdd($qry); + + return true; + } +} + class PGSearch extends SearchEngine { function query($q) @@ -138,6 +168,9 @@ class PGSearch extends SearchEngine if ('identica_people' === $this->table) { return $this->target->whereAdd('textsearch @@ plainto_tsquery(\''.addslashes($q).'\')'); } else if ('identica_notices' === $this->table) { + + // XXX: We need to filter out gateway notices (notice.is_local = -2) --Zach + return $this->target->whereAdd('to_tsvector(\'english\', content) @@ plainto_tsquery(\''.addslashes($q).'\')'); } else { throw new ServerException('Unknown table: ' . $this->table); diff --git a/lib/searchaction.php b/lib/searchaction.php index e74450e11..34fe9373f 100644 --- a/lib/searchaction.php +++ b/lib/searchaction.php @@ -12,7 +12,7 @@ * @link http://laconi.ca/ * * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -148,10 +148,10 @@ You can also try your search on other engines: * [Tweet scan](http://www.tweetscan.com/indexi.php?s=%s) * [Google](http://www.google.com/search?q=site%%3A%%%%site.server%%%%+%s) * [Yahoo](http://search.yahoo.com/search?p=site%%3A%%%%site.server%%%%+%s) - +* [Collecta](http://collecta.com/#q=%s) E_O_T -), $qe, $qe, $qe, $qe); +), $qe, $qe, $qe, $qe, $qe); $this->elementStart('dl', array('id' => 'help_search', 'class' => 'help')); $this->element('dt', null, _('Search help')); $this->elementStart('dd', 'instructions'); diff --git a/lib/servererroraction.php b/lib/servererroraction.php index 595dcf147..db7352166 100644 --- a/lib/servererroraction.php +++ b/lib/servererroraction.php @@ -13,7 +13,7 @@ * @link http://laconi.ca/ * * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by diff --git a/lib/settingsaction.php b/lib/settingsaction.php index db20c5804..17d3a2f64 100644 --- a/lib/settingsaction.php +++ b/lib/settingsaction.php @@ -43,7 +43,7 @@ if (!defined('LACONICA')) { * @see Widget */ -class SettingsAction extends Action +class SettingsAction extends CurrentUserDesignAction { /** * A message for the user. diff --git a/lib/snapshot.php b/lib/snapshot.php new file mode 100644 index 000000000..4b05b502d --- /dev/null +++ b/lib/snapshot.php @@ -0,0 +1,227 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * A snapshot of site stats that can report itself to headquarters + * + * 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 Stats + * @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); +} + +/** + * A snapshot of site stats that can report itself to headquarters + * + * This class will collect statistics on the site and report them to + * a statistics server of the admin's choice. (Default is the big one + * at laconi.ca.) + * + * It can either be called from a cron job, or run occasionally by the + * Web site. + * + * @category Stats + * @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 Snapshot +{ + var $stats = null; + + /** + * Constructor for a snapshot + */ + + function __construct() + { + } + + /** + * Static function for reporting statistics + * + * This function checks whether it should report statistics, based on + * the current configuation settings. If it should, it creates a new + * Snapshot object, takes a snapshot, and reports it to headquarters. + * + * @return void + */ + + static function check() + { + switch (common_config('snapshot', 'run')) { + case 'web': + // skip if we're not running on the Web. + if (!isset($_SERVER) || !array_key_exists('REQUEST_METHOD', $_SERVER)) { + break; + } + // Run once every frequency hits + // XXX: do frequency by time (once a week, etc.) rather than + // hits + if (rand() % common_config('snapshot', 'frequency') == 0) { + $snapshot = new Snapshot(); + $snapshot->take(); + $snapshot->report(); + } + break; + case 'cron': + // skip if we're running on the Web + if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) { + break; + } + common_log(LOG_INFO, 'Running snapshot from cron job'); + // We're running from the command line; assume + + $snapshot = new Snapshot(); + $snapshot->take(); + common_log(LOG_INFO, count($snapshot->stats) . " statistics being uploaded."); + $snapshot->report(); + + break; + case 'never': + break; + default: + common_log(LOG_WARNING, "Unrecognized value for snapshot run config."); + } + } + + /** + * Take a snapshot of the server + * + * Builds an array of statistical and configuration data based + * on the local database and config files. We avoid grabbing any + * information that could be personal or private. + * + * @return void + */ + + function take() + { + $this->stats = array(); + + // Some basic identification stuff + + $this->stats['version'] = LACONICA_VERSION; + $this->stats['phpversion'] = phpversion(); + $this->stats['name'] = common_config('site', 'name'); + $this->stats['root'] = common_root_url(); + + // non-identifying stats on various tables. Primary + // interest is size and rate of activity of service. + + $tables = array('user', + 'notice', + 'subscription', + 'remote_profile', + 'user_group'); + + foreach ($tables as $table) { + $this->tableStats($table); + } + + // stats on some important config options + + $this->stats['theme'] = common_config('site', 'theme'); + $this->stats['dbtype'] = common_config('db', 'type'); + $this->stats['xmpp'] = common_config('xmpp', 'enabled'); + $this->stats['inboxes'] = common_config('inboxes', 'enabled'); + $this->stats['queue'] = common_config('queue', 'enabled'); + $this->stats['license'] = common_config('license', 'url'); + $this->stats['fancy'] = common_config('site', 'fancy'); + $this->stats['private'] = common_config('site', 'private'); + $this->stats['closed'] = common_config('site', 'closed'); + $this->stats['memcached'] = common_config('memcached', 'enabled'); + $this->stats['language'] = common_config('site', 'language'); + $this->stats['timezone'] = common_config('site', 'timezone'); + + } + + /** + * Reports statistics to headquarters + * + * Posts statistics to a reporting server. + * + * @return void + */ + + function report() + { + // XXX: Use OICU2 and OAuth to make authorized requests + + $postdata = http_build_query($this->stats); + + $opts = + array('http' => + array( + 'method' => 'POST', + 'header' => 'Content-type: '. + 'application/x-www-form-urlencoded', + 'content' => $postdata, + 'user_agent' => 'Laconica/'.LACONICA_VERSION + ) + ); + + $context = stream_context_create($opts); + + $reporturl = common_config('snapshot', 'reporturl'); + + $result = @file_get_contents($reporturl, false, $context); + + return $result; + } + + /** + * Updates statistics for a single table + * + * Determines the size of a table and its oldest and newest rows. + * Goal here is to see how active a site is. Note that it + * fills up the instance stats variable. + * + * @param string $table name of table to check + * + * @return void + */ + + function tableStats($table) + { + $inst = DB_DataObject::factory($table); + + $inst->selectAdd(); + $inst->selectAdd('count(*) as cnt, '. + 'min(created) as first, '. + 'max(created) as last'); + + if ($inst->find(true)) { + $this->stats[$table.'count'] = $inst->cnt; + $this->stats[$table.'first'] = $inst->first; + $this->stats[$table.'last'] = $inst->last; + } + + $inst->free(); + unset($inst); + } +} diff --git a/lib/stompqueuemanager.php b/lib/stompqueuemanager.php new file mode 100644 index 000000000..46baeb5c7 --- /dev/null +++ b/lib/stompqueuemanager.php @@ -0,0 +1,169 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Abstract class for queue managers + * + * 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 QueueManager + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @author Sarven Capadisli <csarven@controlyourself.ca> + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +require_once 'Stomp.php'; + +class LiberalStomp extends Stomp +{ + function getSocket() + { + return $this->_socket; + } +} + +class StompQueueManager +{ + var $server = null; + var $username = null; + var $password = null; + var $base = null; + var $con = null; + + function __construct() + { + $this->server = common_config('queue', 'stomp_server'); + $this->username = common_config('queue', 'stomp_username'); + $this->password = common_config('queue', 'stomp_password'); + $this->base = common_config('queue', 'queue_basename'); + } + + function _connect() + { + if (empty($this->con)) { + $this->_log(LOG_INFO, "Connecting to '$this->server' as '$this->username'..."); + $this->con = new LiberalStomp($this->server); + + if ($this->con->connect($this->username, $this->password)) { + $this->_log(LOG_INFO, "Connected."); + } else { + $this->_log(LOG_ERR, 'Failed to connect to queue server'); + throw new ServerException('Failed to connect to queue server'); + } + } + } + + function enqueue($object, $queue) + { + $notice = $object; + + $this->_connect(); + + // XXX: serialize and send entire notice + + $result = $this->con->send($this->_queueName($queue), + $notice->id, // BODY of the message + array ('created' => $notice->created)); + + if (!$result) { + common_log(LOG_ERR, 'Error sending to '.$queue.' queue'); + return false; + } + + common_log(LOG_DEBUG, 'complete remote queueing notice ID = ' + . $notice->id . ' for ' . $queue); + } + + function service($queue, $handler) + { + $result = null; + + $this->_connect(); + + $this->con->setReadTimeout($handler->timeout()); + + $this->con->subscribe($this->_queueName($queue)); + + while (true) { + + // Wait for something on one of our sockets + + $stompsock = $this->con->getSocket(); + + $handsocks = $handler->getSockets(); + + $socks = array_merge(array($stompsock), $handsocks); + + $read = $socks; + $write = array(); + $except = array(); + + $ready = stream_select($read, $write, $except, $handler->timeout(), 0); + + if ($ready === false) { + $this->_log(LOG_ERR, "Error selecting on sockets"); + } else if ($ready > 0) { + if (in_array($stompsock, $read)) { + $this->_handleNotice($queue, $handler); + } + $handler->idle(QUEUE_HANDLER_HIT_IDLE); + } + } + + $this->con->unsubscribe($this->_queueName($queue)); + } + + function _handleNotice($queue, $handler) + { + $frame = $this->con->readFrame(); + + if (!empty($frame)) { + $notice = Notice::staticGet('id', $frame->body); + + if (empty($notice)) { + $this->_log(LOG_WARNING, 'Got ID '. $frame->body .' for non-existent notice in queue '. $queue); + $this->con->ack($frame); + } else { + if ($handler->handle_notice($notice)) { + $this->_log(LOG_INFO, 'Successfully handled notice '. $notice->id .' posted at ' . $frame->headers['created'] . ' in queue '. $queue); + $this->con->ack($frame); + } else { + $this->_log(LOG_WARNING, 'Failed handling notice '. $notice->id .' posted at ' . $frame->headers['created'] . ' in queue '. $queue); + // FIXME we probably shouldn't have to do + // this kind of queue management ourselves + $this->con->ack($frame); + $this->enqueue($notice, $queue); + } + unset($notice); + } + + unset($frame); + } + } + + function _queueName($queue) + { + return common_config('queue', 'queue_basename') . $queue; + } + + function _log($level, $msg) + { + common_log($level, 'StompQueueManager: '.$msg); + } +} diff --git a/lib/stream.php b/lib/stream.php deleted file mode 100644 index 0cb9e0bf4..000000000 --- a/lib/stream.php +++ /dev/null @@ -1,32 +0,0 @@ -<?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/personal.php'); -require_once(INSTALLDIR.'/lib/noticelist.php'); - -class StreamAction extends PersonalAction -{ - function show_notice_list($notice) - { - $nl = new NoticeList($notice); - return $nl->show(); - } -} diff --git a/lib/subgroupnav.php b/lib/subgroupnav.php index 4a9b36ae8..520991923 100644 --- a/lib/subgroupnav.php +++ b/lib/subgroupnav.php @@ -100,7 +100,7 @@ class SubGroupNav extends Widget $this->user->nickname), $action == 'usergroups', 'nav_usergroups'); - if (!is_null($cur) && $this->user->id === $cur->id) { + if (common_config('invite', 'enabled') && !is_null($cur) && $this->user->id === $cur->id) { $this->out->menuItem(common_local_url('invite'), _('Invite'), sprintf(_('Invite friends and colleagues to join you on %s'), diff --git a/lib/subs.php b/lib/subs.php index 0e7b9ded5..e76023752 100644 --- a/lib/subs.php +++ b/lib/subs.php @@ -1,7 +1,7 @@ <?php /* * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -44,7 +44,6 @@ function subs_subscribe_user($user, $other_nickname) function subs_subscribe_to($user, $other) { - if ($user->isSubscribed($other)) { return _('Already subscribed!.'); } @@ -60,12 +59,16 @@ function subs_subscribe_to($user, $other) subs_notify($other, $user); - $cache = common_memcache(); + $cache = common_memcache(); if ($cache) { $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id)); } + $profile = $user->getProfile(); + + $profile->blowSubscriptionsCount(); + $other->blowSubscribersCount(); if ($other->autosubscribe && !$other->isSubscribed($user) && !$user->hasBlocked($other)) { if (!$other->subscribeTo($user)) { @@ -117,7 +120,6 @@ function subs_unsubscribe_user($user, $other_nickname) function subs_unsubscribe_to($user, $other) { - if (!$user->isSubscribed($other)) return _('Not subscribed!.'); @@ -139,6 +141,11 @@ function subs_unsubscribe_to($user, $other) $cache->delete(common_cache_key('user:notices_with_friends:' . $user->id)); } + $profile = $user->getProfile(); + + $profile->blowSubscriptionsCount(); + $other->blowSubscribersCount(); + return true; } diff --git a/lib/subscriptionlist.php b/lib/subscriptionlist.php new file mode 100644 index 000000000..23da64cca --- /dev/null +++ b/lib/subscriptionlist.php @@ -0,0 +1,131 @@ +<?php + +/** + * Laconica, the distributed open-source microblogging tool + * + * Widget to show a list of profiles + * + * 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 Public + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @copyright 2008-2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +require_once INSTALLDIR.'/lib/profilelist.php'; + +/** + * Widget to show a list of subscriptions + * + * @category Public + * @package Laconica + * @author Zach Copley <zach@controlyourself.ca> + * @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 SubscriptionList extends ProfileList +{ + /** Owner of this list */ + var $owner = null; + + function __construct($profile, $owner=null, $action=null) + { + parent::__construct($profile, $action); + + $this->owner = $owner; + } + + function newListItem($profile) + { + return new SubscriptionListItem($profile, $this->owner, $this->action); + } +} + +class SubscriptionListItem extends ProfileListItem +{ + /** Owner of this list */ + var $owner = null; + + function __construct($profile, $owner, $action) + { + parent::__construct($profile, $action); + + $this->owner = $owner; + } + + function showProfile() + { + $this->startProfile(); + $this->showAvatar(); + $this->showFullName(); + $this->showLocation(); + $this->showHomepage(); + $this->showBio(); + // Relevant portion! + $this->showTags(); + $this->endProfile(); + } + + function isOwn() + { + $user = common_current_user(); + return (!empty($user) && ($this->owner->id == $user->id)); + } + + function showTags() + { + $tags = Profile_tag::getTags($this->owner->id, $this->profile->id); + + $this->out->elementStart('dl', 'entity_tags'); + $this->out->elementStart('dt'); + if ($this->isOwn()) { + $this->out->element('a', array('href' => common_local_url('tagother', + array('id' => $this->profile->id))), + _('Tags')); + } else { + $this->out->text(_('Tags')); + } + $this->out->elementEnd('dt'); + $this->out->elementStart('dd'); + if ($tags) { + $this->out->elementStart('ul', 'tags xoxo'); + foreach ($tags as $tag) { + $this->out->elementStart('li'); + $this->out->element('span', 'mark_hash', '#'); + $this->out->element('a', array('rel' => 'tag', + 'href' => common_local_url($this->action->trimmed('action'), + array('nickname' => $this->owner->nickname, + 'tag' => $tag))), + $tag); + $this->out->elementEnd('li'); + } + $this->out->elementEnd('ul'); + } else { + $this->out->text(_('(none)')); + } + $this->out->elementEnd('dd'); + $this->out->elementEnd('dl'); + } +} diff --git a/lib/tagcloudsection.php b/lib/tagcloudsection.php index ff2aca6d6..62f7d8961 100644 --- a/lib/tagcloudsection.php +++ b/lib/tagcloudsection.php @@ -114,7 +114,11 @@ class TagCloudSection extends Section function tagUrl($tag) { - return common_local_url('tag', array('tag' => $tag)); + if ('showstream' === $this->out->trimmed('action')) { + return common_local_url('showstream', array('nickname' => $this->out->profile->nickname, 'tag' => $tag)); + } else { + return common_local_url('tag', array('tag' => $tag)); + } } function divId() diff --git a/lib/theme.php b/lib/theme.php index 95030affe..2fe6ab69b 100644 --- a/lib/theme.php +++ b/lib/theme.php @@ -43,10 +43,14 @@ if (!defined('LACONICA')) { function theme_file($relative, $theme=null) { - if (!$theme) { + if (empty($theme)) { $theme = common_config('site', 'theme'); } - return INSTALLDIR.'/theme/'.$theme.'/'.$relative; + $dir = common_config('theme', 'dir'); + if (empty($dir)) { + $dir = INSTALLDIR.'/theme'; + } + return $dir.'/'.$theme.'/'.$relative; } /** @@ -60,13 +64,31 @@ function theme_file($relative, $theme=null) function theme_path($relative, $theme=null) { - if (!$theme) { + if (empty($theme)) { $theme = common_config('site', 'theme'); } + + $path = common_config('theme', 'path'); + + if (empty($path)) { + $path = common_config('site', 'path') . '/theme/'; + } + + if ($path[strlen($path)-1] != '/') { + $path .= '/'; + } + + if ($path[0] != '/') { + $path = '/'.$path; + } + $server = common_config('theme', 'server'); - if ($server) { - return 'http://'.$server.'/'.$theme.'/'.$relative; - } else { - return common_path('theme/'.$theme.'/'.$relative); + + if (empty($server)) { + $server = common_config('site', 'server'); } -}
\ No newline at end of file + + // XXX: protocol + + return 'http://'.$server.$path.$theme.'/'.$relative; +} diff --git a/lib/twitter.php b/lib/twitter.php index ccc6c93ca..47af32e61 100644 --- a/lib/twitter.php +++ b/lib/twitter.php @@ -1,7 +1,7 @@ <?php /* * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -346,7 +346,7 @@ function save_twitter_friends($user, $twitter_id, $screen_name, $password) function is_twitter_bound($notice, $flink) { // Check to see if notice should go to Twitter - if ($flink->noticesync & FOREIGN_NOTICE_SEND) { + if (!empty($flink) && ($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) || @@ -360,14 +360,11 @@ function is_twitter_bound($notice, $flink) { function broadcast_twitter($notice) { - $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_null($flink) && is_twitter_bound($notice, $flink)) { + if (is_twitter_bound($notice, $flink)) { $fuser = $flink->getForeignUser(); $twitter_user = $fuser->nickname; @@ -401,33 +398,99 @@ function broadcast_twitter($notice) curl_setopt_array($ch, $options); $data = curl_exec($ch); $errmsg = curl_error($ch); + $errno = curl_errno($ch); - if ($errmsg) { - common_debug("cURL error: $errmsg - " . + if (!empty($errmsg)) { + common_debug("cURL error ($errno): $errmsg - " . "trying to send notice for $twitter_user.", __FILE__); - $success = false; + + $user = $flink->getUser(); + + if ($errmsg == 'The requested URL returned error: 401') { + common_debug(sprintf('User %s (user id: %s) ' . + 'has bad Twitter credentials!', + $user->nickname, $user->id)); + + // Bad credentials we need to delete the foreign_link + // to Twitter and inform the user. + + remove_twitter_link($flink); + + return true; + + } else { + + // Some other error happened, so we should try to + // send again later + + return false; + } + } curl_close($ch); - if (!$data) { + if (empty($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); + // XXX: Not sure this represents a failure to send, but it + // probably does - if (!$status->id) { - common_debug("Unexpected data returned by Twitter " . - " API trying to send update for $twitter_user", - __FILE__); - $success = false; + return false; + + } else { + + // Twitter should return a status + $status = json_decode($data); + + if (empty($status)) { + common_debug("Unexpected data returned by Twitter " . + " API trying to send update for $twitter_user", + __FILE__); + + // XXX: Again, this could represent a failure posting + // or the Twitter API might just be behaving flakey. + // We're treating it as a failure to post. + + return false; + } } } - return $success; + return true; +} + +function remove_twitter_link($flink) +{ + $user = $flink->getUser(); + + common_log(LOG_INFO, 'Removing Twitter bridge Foreign link for ' . + "user $user->nickname (user id: $user->id)."); + + $result = $flink->delete(); + + if (empty($result)) { + common_log(LOG_ERR, 'Could not remove Twitter bridge ' . + "Foreign_link for $user->nickname (user id: $user->id)!"); + common_log_db_error($flink, 'DELETE', __FILE__); + } + + // Notify the user that her Twitter bridge is down + + $result = mail_twitter_bridge_removed($user); + + if (!$result) { + + $msg = 'Unable to send email to notify ' . + "$user->nickname (user id: $user->id) " . + 'that their Twitter bridge link was ' . + 'removed!'; + + common_log(LOG_WARNING, $msg); + } + } + diff --git a/lib/twitterapi.php b/lib/twitterapi.php index ca8b03cdc..f48513e67 100644 --- a/lib/twitterapi.php +++ b/lib/twitterapi.php @@ -1,7 +1,7 @@ <?php /* * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -17,7 +17,9 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -if (!defined('LACONICA')) { exit(1); } +if (!defined('LACONICA')) { + exit(1); +} class TwitterapiAction extends Action { @@ -87,7 +89,7 @@ class TwitterapiAction extends Action $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null; $twitter_user['protected'] = false; # not supported by Laconica yet - $twitter_user['followers_count'] = $this->count_subscriptions($profile); + $twitter_user['followers_count'] = $profile->subscriberCount(); // To be supported soon... $twitter_user['profile_background_color'] = ''; @@ -96,17 +98,11 @@ class TwitterapiAction extends Action $twitter_user['profile_sidebar_fill_color'] = ''; $twitter_user['profile_sidebar_border_color'] = ''; - $subbed = DB_DataObject::factory('subscription'); - $subbed->subscriber = $profile->id; - $subbed_count = (int) $subbed->count() - 1; - $twitter_user['friends_count'] = (is_int($subbed_count)) ? $subbed_count : 0; + $twitter_user['friends_count'] = $profile->subscriptionCount(); $twitter_user['created_at'] = $this->date_twitter($profile->created); - $faves = DB_DataObject::factory('fave'); - $faves->user_id = $user->id; - $faves_count = (int) $faves->count(); - $twitter_user['favourites_count'] = $faves_count; // British spelling! + $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling! // Need to pull up the user for some of this $user = User::staticGet($profile->id); @@ -127,11 +123,7 @@ class TwitterapiAction extends Action $twitter_user['profile_background_image_url'] = ''; $twitter_user['profile_background_tile'] = false; - $notices = DB_DataObject::factory('notice'); - $notices->profile_id = $profile->id; - $notice_count = (int) $notices->count(); - - $twitter_user['statuses_count'] = (is_int($notice_count)) ? $notice_count : 0; + $twitter_user['statuses_count'] = $profile->noticeCount(); // Is the requesting user following this user? $twitter_user['following'] = false; @@ -205,7 +197,6 @@ class TwitterapiAction extends Action function twitter_rss_entry_array($notice) { - $profile = $notice->getProfile(); $entry = array(); @@ -222,6 +213,19 @@ class TwitterapiAction extends Action $entry['updated'] = $entry['published']; $entry['author'] = $profile->getBestName(); + # Enclosure + $attachments = $notice->attachments(); + if($attachments){ + $entry['enclosures']=array(); + foreach($attachments as $attachment){ + $enclosure=array(); + $enclosure['url']=$attachment->url; + $enclosure['mimetype']=$attachment->mimetype; + $enclosure['size']=$attachment->size; + $entry['enclosures'][]=$enclosure; + } + } + # RSS Item specific $entry['description'] = $entry['content']; $entry['pubDate'] = common_date_rfc2822($notice->created); @@ -276,6 +280,67 @@ class TwitterapiAction extends Action return $twitter_dm; } + function twitter_relationship_array($source, $target) + { + $relationship = array(); + + $relationship['source'] = + $this->relationship_details_array($source, $target); + $relationship['target'] = + $this->relationship_details_array($target, $source); + + return array('relationship' => $relationship); + } + + function relationship_details_array($source, $target) + { + $details = array(); + + $details['screen_name'] = $source->nickname; + $details['followed_by'] = $target->isSubscribed($source); + $details['following'] = $source->isSubscribed($target); + + $notifications = false; + + if ($source->isSubscribed($target)) { + + $sub = Subscription::pkeyGet(array('subscriber' => + $source->id, 'subscribed' => $target->id)); + + if (!empty($sub)) { + $notifications = ($sub->jabber || $sub->sms); + } + } + + $details['notifications_enabled'] = $notifications; + $details['blocking'] = $source->hasBlocked($target); + $details['id'] = $source->id; + + return $details; + } + + function show_twitter_xml_relationship($relationship) + { + $this->elementStart('relationship'); + + foreach($relationship as $element => $value) { + if ($element == 'source' || $element == 'target') { + $this->elementStart($element); + $this->show_xml_relationship_details($value); + $this->elementEnd($element); + } + } + + $this->elementEnd('relationship'); + } + + function show_xml_relationship_details($details) + { + foreach($details as $element => $value) { + $this->element($element, null, $value); + } + } + function show_twitter_xml_status($twitter_status) { $this->elementStart('status'); @@ -315,6 +380,13 @@ class TwitterapiAction extends Action $this->element('pubDate', null, $entry['pubDate']); $this->element('guid', null, $entry['guid']); $this->element('link', null, $entry['link']); + + # RSS only supports 1 enclosure per item + if($entry['enclosures']){ + $enclosure = $entry['enclosures'][0]; + $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null); + } + $this->elementEnd('item'); } @@ -521,11 +593,11 @@ class TwitterapiAction extends Action function init_document($type='xml') { switch ($type) { - case 'xml': + case 'xml': header('Content-Type: application/xml; charset=utf-8'); $this->startXML(); break; - case 'json': + case 'json': header('Content-Type: application/json; charset=utf-8'); // Check for JSONP callback @@ -534,16 +606,16 @@ class TwitterapiAction extends Action print $callback . '('; } break; - case 'rss': + case 'rss': header("Content-Type: application/rss+xml; charset=utf-8"); $this->init_twitter_rss(); break; - case 'atom': + case 'atom': header('Content-Type: application/atom+xml; charset=utf-8'); $this->init_twitter_atom(); break; - default: - $this->client_error(_('Not a supported data format.')); + default: + $this->clientError(_('Not a supported data format.')); break; } @@ -553,10 +625,10 @@ class TwitterapiAction extends Action function end_document($type='xml') { switch ($type) { - case 'xml': + case 'xml': $this->endXML(); break; - case 'json': + case 'json': // Check for JSONP callback $callback = $this->arg('callback'); @@ -564,20 +636,20 @@ class TwitterapiAction extends Action print ')'; } break; - case 'rss': + case 'rss': $this->end_twitter_rss(); break; - case 'atom': + case 'atom': $this->end_twitter_rss(); break; - default: - $this->client_error(_('Not a supported data format.')); + default: + $this->clientError(_('Not a supported data format.')); break; } return; } - function client_error($msg, $code = 400, $content_type = 'json') + function clientError($msg, $code = 400, $content_type = 'json') { static $status = array(400 => 'Bad Request', @@ -657,14 +729,14 @@ class TwitterapiAction extends Action { $profile_array = $this->twitter_user_array($profile, true); switch ($content_type) { - case 'xml': + case 'xml': $this->show_twitter_xml_user($profile_array); break; - case 'json': + case 'json': $this->show_json_objects($profile_array); break; - default: - $this->client_error(_('Not a supported data format.')); + default: + $this->clientError(_('Not a supported data format.')); return; } return; @@ -672,8 +744,8 @@ class TwitterapiAction extends Action function get_user($id, $apidata=null) { - if (!$id) { - + if (empty($id)) { + // Twitter supports these other ways of passing the user ID if (is_numeric($this->arg('id'))) { return User::staticGet($this->arg('id')); @@ -681,7 +753,7 @@ class TwitterapiAction extends Action $nickname = common_canonical_nickname($this->arg('id')); return User::staticGet('nickname', $nickname); } else if ($this->arg('user_id')) { - // This is to ensure that a non-numeric user_id still + // This is to ensure that a non-numeric user_id still // overrides screen_name even if it doesn't get used if (is_numeric($this->arg('user_id'))) { return User::staticGet('id', $this->arg('user_id')); @@ -693,7 +765,7 @@ class TwitterapiAction extends Action // Fall back to trying the currently authenticated user return $apidata['user']; } - + } else if (is_numeric($id)) { return User::staticGet($id); } else { @@ -720,13 +792,13 @@ class TwitterapiAction extends Action { $source_name = _($source); switch ($source) { - case 'web': - case 'xmpp': - case 'mail': - case 'omb': - case 'api': + case 'web': + case 'xmpp': + case 'mail': + case 'omb': + case 'api': break; - default: + default: $ns = Notice_source::staticGet($source); if ($ns) { $source_name = '<a href="' . $ns->url . '">' . $ns->name . '</a>'; @@ -736,4 +808,49 @@ class TwitterapiAction extends Action return $source_name; } + /** + * Returns query argument or default value if not found. Certain + * parameters used throughout the API are lightly scrubbed and + * bounds checked. This overrides Action::arg(). + * + * @param string $key requested argument + * @param string $def default value to return if $key is not provided + * + * @return var $var + */ + function arg($key, $def=null) + { + + // XXX: Do even more input validation/scrubbing? + + if (array_key_exists($key, $this->args)) { + switch($key) { + case 'page': + $page = (int)$this->args['page']; + return ($page < 1) ? 1 : $page; + case 'count': + $count = (int)$this->args['count']; + if ($count < 1) { + return 20; + } elseif ($count > 200) { + return 200; + } else { + return $count; + } + case 'since_id': + $since_id = (int)$this->args['since_id']; + return ($since_id < 1) ? 0 : $since_id; + case 'max_id': + $max_id = (int)$this->args['max_id']; + return ($max_id < 1) ? 0 : $max_id; + case 'since': + return strtotime($this->args['since']); + default: + return parent::arg($key, $def); + } + } else { + return $def; + } + } + } diff --git a/lib/unqueuemanager.php b/lib/unqueuemanager.php new file mode 100644 index 000000000..515461072 --- /dev/null +++ b/lib/unqueuemanager.php @@ -0,0 +1,85 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * A queue manager interface for just doing things immediately + * + * 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 QueueManager + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @author Sarven Capadisli <csarven@controlyourself.ca> + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +class UnQueueManager +{ + function enqueue($object, $queue) + { + $notice = $object; + + switch ($queue) + { + case 'omb': + if ($this->_isLocal($notice)) { + require_once(INSTALLDIR.'/lib/omb.php'); + omb_broadcast_remote_subscribers($notice); + } + break; + case 'public': + if ($this->_isLocal($notice)) { + require_once(INSTALLDIR.'/lib/jabber.php'); + jabber_public_notice($notice); + } + break; + case 'twitter': + if ($this->_isLocal($notice)) { + broadcast_twitter($notice); + } + break; + case 'facebook': + if ($this->_isLocal($notice)) { + require_once INSTALLDIR . '/lib/facebookutil.php'; + return facebookBroadcastNotice($notice); + } + break; + case 'ping': + if ($this->_isLocal($notice)) { + require_once INSTALLDIR . '/lib/ping.php'; + return ping_broadcast_notice($notice); + } + case 'sms': + require_once(INSTALLDIR.'/lib/mail.php'); + mail_broadcast_notice_sms($notice); + break; + case 'jabber': + require_once(INSTALLDIR.'/lib/jabber.php'); + jabber_broadcast_notice($notice); + break; + default: + throw ServerException("UnQueueManager: Unknown queue: $type"); + } + } + + function _isLocal($notice) + { + return ($notice->is_local == NOTICE_LOCAL_PUBLIC || + $notice->is_local == NOTICE_LOCAL_NONPUBLIC); + } +}
\ No newline at end of file diff --git a/lib/util.php b/lib/util.php index 22308f432..9e8ec41d2 100644 --- a/lib/util.php +++ b/lib/util.php @@ -1,7 +1,7 @@ <?php /* * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -114,7 +114,7 @@ function common_check_user($nickname, $password) return false; } $user = User::staticGet('nickname', $nickname); - if (is_null($user)) { + if (is_null($user) || $user === false) { return false; } else { if (0 == strcmp(common_munge_password($password, $user->id), @@ -139,8 +139,22 @@ function common_have_session() function common_ensure_session() { + $c = null; + if (array_key_exists(session_name, $_COOKIE)) { + $c = $_COOKIE[session_name()]; + } if (!common_have_session()) { + if (common_config('sessions', 'handle')) { + Session::setSaveHandler(); + } @session_start(); + if (!isset($_SESSION['started'])) { + $_SESSION['started'] = time(); + if (!empty($c)) { + common_log(LOG_WARNING, 'Session cookie "' . $_COOKIE[session_name()] . '" ' . + ' is set but started value is null'); + } + } } } @@ -395,7 +409,7 @@ function common_render_text($text) return $r; } -function common_replace_urls_callback($text, $callback) { +function common_replace_urls_callback($text, $callback, $notice_id = null) { // Start off with a regex $regex = '#'. '(?:'. @@ -466,7 +480,11 @@ function common_replace_urls_callback($text, $callback) { $url = (mb_strpos($orig_url, htmlspecialchars($url)) === FALSE) ? $url:htmlspecialchars($url); // Call user specified func - $modified_url = call_user_func($callback, $url); + if (empty($notice_id)) { + $modified_url = call_user_func($callback, $url); + } else { + $modified_url = call_user_func($callback, array($url, $notice_id)); + } // Replace it! $start = mb_strpos($text, $url, $offset); @@ -481,107 +499,79 @@ function common_linkify($url) { // It comes in special'd, so we unspecial it before passing to the stringifying // functions $url = htmlspecialchars_decode($url); - $display = $url; - $url = (!preg_match('#^([a-z]+://|(mailto|aim|tel):)#i', $url)) ? 'http://'.$url : $url; - $attrs = array('href' => $url, 'rel' => 'external'); + $canon = File_redirection::_canonUrl($url); - if ($longurl = common_longurl($url)) { - $attrs['title'] = $longurl; + $longurl_data = File_redirection::where($url); + if (is_array($longurl_data)) { + $longurl = $longurl_data['url']; + } elseif (is_string($longurl_data)) { + $longurl = $longurl_data; + } else { + throw new ServerException("Can't linkify url '$url'"); } - return XMLStringer::estring('a', $attrs, $display); -} + $attrs = array('href' => $canon, 'rel' => 'external'); -function common_longurl($short_url) -{ - $long_url = common_shorten_link($short_url, true); - if ($long_url === $short_url) return false; - return $long_url; -} + $is_attachment = false; + $attachment_id = null; + $has_thumb = false; -function common_longurl2($uri) -{ - $uri_e = urlencode($uri); - $longurl = unserialize(file_get_contents("http://api.longurl.org/v1/expand?format=php&url=$uri_e")); - if (empty($longurl['long_url']) || $uri === $longurl['long_url']) return false; - return stripslashes($longurl['long_url']); -} - -function common_shorten_links($text) -{ - if (mb_strlen($text) <= 140) return $text; - static $cache = array(); - if (isset($cache[$text])) return $cache[$text]; - // \s = not a horizontal whitespace character (since PHP 5.2.4) - return $cache[$text] = common_replace_urls_callback($text, 'common_shorten_link');; -} - -function common_shorten_link($url, $reverse = false) -{ + // Check to see whether there's a filename associated with this URL. + // If there is, it's an upload and qualifies as an attachment - static $url_cache = array(); - if ($reverse) return isset($url_cache[$url]) ? $url_cache[$url] : $url; + $localfile = File::staticGet('url', $longurl); - $user = common_current_user(); - if (!isset($user)) { - // common current user does not find a user when called from the XMPP daemon - // therefore we'll set one here fix, so that XMPP given URLs may be shortened - $user->urlshorteningservice = 'ur1.ca'; + if (!empty($localfile)) { + if (isset($localfile->filename)) { + $is_attachment = true; + $attachment_id = $localfile->id; + } } - $curlh = curl_init(); - curl_setopt($curlh, CURLOPT_CONNECTTIMEOUT, 20); // # seconds to wait - curl_setopt($curlh, CURLOPT_USERAGENT, 'Laconica'); - curl_setopt($curlh, CURLOPT_RETURNTRANSFER, true); - switch($user->urlshorteningservice) { - case 'ur1.ca': - $short_url_service = new LilUrl; - $short_url = $short_url_service->shorten($url); - break; + // if this URL is an attachment, then we set class='attachment' and id='attahcment-ID' + // where ID is the id of the attachment for the given URL. + // + // we need a better test telling what can be shown as an attachment + // we're currently picking up oembeds only. + // I think the best option is another file_view table in the db + // and associated dbobject. - case '2tu.us': - $short_url_service = new TightUrl; - $short_url = $short_url_service->shorten($url); - break; + $query = "select file_oembed.file_id as file_id from file join file_oembed on file.id = file_oembed.file_id where file.url='$longurl'"; + $file = new File; + $file->query($query); + $file->fetch(); - case 'ptiturl.com': - $short_url_service = new PtitUrl; - $short_url = $short_url_service->shorten($url); - break; + if (!empty($file->file_id)) { + $is_attachment = true; + $attachment_id = $file->file_id; - case 'bit.ly': - curl_setopt($curlh, CURLOPT_URL, 'http://bit.ly/api?method=shorten&long_url='.urlencode($url)); - $short_url = current(json_decode(curl_exec($curlh))->results)->hashUrl; - break; + $query = "select file_thumbnail.file_id as file_id from file join file_thumbnail on file.id = file_thumbnail.file_id where file.url='$longurl'"; + $file2 = new File; + $file2->query($query); + $file2->fetch(); - case 'is.gd': - curl_setopt($curlh, CURLOPT_URL, 'http://is.gd/api.php?longurl='.urlencode($url)); - $short_url = curl_exec($curlh); - break; - case 'snipr.com': - curl_setopt($curlh, CURLOPT_URL, 'http://snipr.com/site/snip?r=simple&link='.urlencode($url)); - $short_url = curl_exec($curlh); - break; - case 'metamark.net': - curl_setopt($curlh, CURLOPT_URL, 'http://metamark.net/api/rest/simple?long_url='.urlencode($url)); - $short_url = curl_exec($curlh); - break; - case 'tinyurl.com': - curl_setopt($curlh, CURLOPT_URL, 'http://tinyurl.com/api-create.php?url='.urlencode($url)); - $short_url = curl_exec($curlh); - break; - default: - $short_url = false; + if (!empty($file2)) { + $has_thumb = true; + } } - curl_close($curlh); - - if ($short_url) { - $url_cache[(string)$short_url] = $url; - return (string)$short_url; + // Add clippy + if ($is_attachment) { + $attrs['class'] = 'attachment'; + if ($has_thumb) { + $attrs['class'] = 'attachment thumbnail'; + } + $attrs['id'] = "attachment-{$attachment_id}"; } - return $url; + + return XMLStringer::estring('a', $attrs, $url); +} + +function common_shorten_links($text) +{ + if (mb_strlen($text) <= 140) return $text; + return common_replace_urls_callback($text, array('File_redirection', 'makeShort')); } function common_xml_safe_str($str) @@ -644,7 +634,7 @@ function common_at_link($sender_id, $nickname) function common_group_link($sender_id, $nickname) { $sender = Profile::staticGet($sender_id); - $group = User_group::staticGet('nickname', common_canonical_nickname($nickname)); + $group = User_group::getForNickname($nickname); if ($group && $sender->isMember($group)) { $attrs = array('href' => $group->permalink(), 'class' => 'url'); @@ -843,7 +833,12 @@ function common_date_iso8601($dt) function common_sql_now() { - return strftime('%Y-%m-%d %H:%M:%S', time()); + return common_sql_date(time()); +} + +function common_sql_date($datetime) +{ + return strftime('%Y-%m-%d %H:%M:%S', $datetime); } function common_redirect($url, $code=307) @@ -867,101 +862,43 @@ function common_redirect($url, $code=307) function common_broadcast_notice($notice, $remote=false) { - if (common_config('queue', 'enabled')) { - // Do it later! - return common_enqueue_notice($notice); - } else { - return common_real_broadcast($notice, $remote); - } + return common_enqueue_notice($notice); } // Stick the notice on the queue function common_enqueue_notice($notice) { - $transports = array('twitter', 'facebook', 'ping'); + static $localTransports = array('omb', + 'twitter', + 'facebook', + 'ping'); + static $allTransports = array('sms'); - // If inboxes are enabled, wait till inboxes are filled - // before doing inbox-dependent broadcasts + $transports = $allTransports; - $transports = array_merge($transports, common_post_inbox_transports()); + $xmpp = common_config('xmpp', 'enabled'); - foreach ($transports as $transport) { - common_enqueue_notice_transport($notice, $transport); + if ($xmpp) { + $transports[] = 'jabber'; } - return $result; -} - -function common_post_inbox_transports() -{ - $transports = array('omb', 'sms'); - - if (common_config('xmpp', 'enabled')) { - $transports = array_merge($transports, array('jabber', 'public')); + if ($notice->is_local == NOTICE_LOCAL_PUBLIC || + $notice->is_local == NOTICE_LOCAL_NONPUBLIC) { + $transports = array_merge($transports, $localTransports); + if ($xmpp) { + $transports[] = 'public'; + } } - return $transports; -} + $qm = QueueManager::get(); -function common_enqueue_notice_transport($notice, $transport) -{ - $qi = new Queue_item(); - $qi->notice_id = $notice->id; - $qi->transport = $transport; - $qi->created = $notice->created; - $result = $qi->insert(); - if (!$result) { - $last_error = &PEAR::getStaticProperty('DB_DataObject','lastError'); - common_log(LOG_ERR, 'DB error inserting queue item: ' . $last_error->message); - throw new ServerException('DB error inserting queue item: ' . $last_error->message); + foreach ($transports as $transport) + { + $qm->enqueue($notice, $transport); } - common_log(LOG_DEBUG, 'complete queueing notice ID = ' . $notice->id . ' for ' . $transport); - return true; -} -function common_real_broadcast($notice, $remote=false) -{ - $success = true; - if (!$remote) { - // Make sure we have the OMB stuff - require_once(INSTALLDIR.'/lib/omb.php'); - $success = omb_broadcast_remote_subscribers($notice); - if (!$success) { - common_log(LOG_ERR, 'Error in OMB broadcast for notice ' . $notice->id); - } - } - if ($success) { - require_once(INSTALLDIR.'/lib/jabber.php'); - $success = jabber_broadcast_notice($notice); - if (!$success) { - common_log(LOG_ERR, 'Error in jabber broadcast for notice ' . $notice->id); - } - } - if ($success) { - require_once(INSTALLDIR.'/lib/mail.php'); - $success = mail_broadcast_notice_sms($notice); - if (!$success) { - common_log(LOG_ERR, 'Error in sms broadcast for notice ' . $notice->id); - } - } - if ($success) { - $success = jabber_public_notice($notice); - if (!$success) { - 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; + return true; } function common_broadcast_profile($profile) @@ -1041,20 +978,26 @@ function common_ensure_syslog() { static $initialized = false; if (!$initialized) { - openlog(common_config('syslog', 'appname'), 0, LOG_USER); + openlog(common_config('syslog', 'appname'), 0, + common_config('syslog', 'facility')); $initialized = true; } } +function common_log_line($priority, $msg) +{ + static $syslog_priorities = array('LOG_EMERG', 'LOG_ALERT', 'LOG_CRIT', 'LOG_ERR', + 'LOG_WARNING', 'LOG_NOTICE', 'LOG_INFO', 'LOG_DEBUG'); + return date('Y-m-d H:i:s') . ' ' . $syslog_priorities[$priority] . ': ' . $msg . "\n"; +} + function common_log($priority, $msg, $filename=null) { $logfile = common_config('site', 'logfile'); if ($logfile) { $log = fopen($logfile, "a"); if ($log) { - static $syslog_priorities = array('LOG_EMERG', 'LOG_ALERT', 'LOG_CRIT', 'LOG_ERR', - 'LOG_WARNING', 'LOG_NOTICE', 'LOG_INFO', 'LOG_DEBUG'); - $output = date('Y-m-d H:i:s') . ' ' . $syslog_priorities[$priority] . ': ' . $msg . "\n"; + $output = common_log_line($priority, $msg); fwrite($log, $output); fclose($log); } @@ -1085,6 +1028,9 @@ function common_log_objstring(&$object) if (is_null($object)) { return "null"; } + if (!($object instanceof DB_DataObject)) { + return "(unknown)"; + } $arr = $object->toArray(); $fields = array(); foreach ($arr as $k => $v) { @@ -1124,7 +1070,7 @@ function common_accept_to_prefs($accept, $def = '*/*') foreach($parts as $part) { // FIXME: doesn't deal with params like 'text/html; level=1' - @list($value, $qpart) = explode(';', $part); + @list($value, $qpart) = explode(';', trim($part)); $match = array(); if(!isset($qpart)) { $prefs[$value] = 1; @@ -1285,18 +1231,39 @@ function common_canonical_sms($sms) function common_error_handler($errno, $errstr, $errfile, $errline, $errcontext) { switch ($errno) { + + case E_ERROR: + case E_COMPILE_ERROR: + case E_CORE_ERROR: case E_USER_ERROR: - common_log(LOG_ERR, "[$errno] $errstr ($errfile:$errline)"); - exit(1); + case E_PARSE: + case E_RECOVERABLE_ERROR: + common_log(LOG_ERR, "[$errno] $errstr ($errfile:$errline) [ABORT]"); + die(); break; + case E_WARNING: + case E_COMPILE_WARNING: + case E_CORE_WARNING: case E_USER_WARNING: common_log(LOG_WARNING, "[$errno] $errstr ($errfile:$errline)"); break; + case E_NOTICE: case E_USER_NOTICE: common_log(LOG_NOTICE, "[$errno] $errstr ($errfile:$errline)"); break; + + case E_STRICT: + case E_DEPRECATED: + case E_USER_DEPRECATED: + // XXX: config variable to log this stuff, too + break; + + default: + common_log(LOG_ERR, "[$errno] $errstr ($errfile:$errline) [UNKNOWN LEVEL, die()'ing]"); + die(); + break; } // FIXME: show error page if we're on the Web @@ -1315,7 +1282,13 @@ function common_session_token() function common_cache_key($extra) { - return 'laconica:' . common_keyize(common_config('site', 'name')) . ':' . $extra; + $base_key = common_config('memcached', 'base'); + + if (empty($base_key)) { + $base_key = common_keyize(common_config('site', 'name')); + } + + return 'laconica:' . $base_key . ':' . $extra; } function common_keyize($str) @@ -1364,3 +1337,92 @@ function common_database_tablename($tablename) //table prefixes could be added here later return $tablename; } + +function common_shorten_url($long_url) +{ + $user = common_current_user(); + if (empty($user)) { + // common current user does not find a user when called from the XMPP daemon + // therefore we'll set one here fix, so that XMPP given URLs may be shortened + $svc = 'ur1.ca'; + } else { + $svc = $user->urlshorteningservice; + } + + $curlh = curl_init(); + curl_setopt($curlh, CURLOPT_CONNECTTIMEOUT, 20); // # seconds to wait + curl_setopt($curlh, CURLOPT_USERAGENT, 'Laconica'); + curl_setopt($curlh, CURLOPT_RETURNTRANSFER, true); + + switch($svc) { + case 'ur1.ca': + require_once INSTALLDIR.'/lib/Shorturl_api.php'; + $short_url_service = new LilUrl; + $short_url = $short_url_service->shorten($long_url); + break; + + case '2tu.us': + $short_url_service = new TightUrl; + require_once INSTALLDIR.'/lib/Shorturl_api.php'; + $short_url = $short_url_service->shorten($long_url); + break; + + case 'ptiturl.com': + require_once INSTALLDIR.'/lib/Shorturl_api.php'; + $short_url_service = new PtitUrl; + $short_url = $short_url_service->shorten($long_url); + break; + + case 'bit.ly': + curl_setopt($curlh, CURLOPT_URL, 'http://bit.ly/api?method=shorten&long_url='.urlencode($long_url)); + $short_url = current(json_decode(curl_exec($curlh))->results)->hashUrl; + break; + + case 'is.gd': + curl_setopt($curlh, CURLOPT_URL, 'http://is.gd/api.php?longurl='.urlencode($long_url)); + $short_url = curl_exec($curlh); + break; + case 'snipr.com': + curl_setopt($curlh, CURLOPT_URL, 'http://snipr.com/site/snip?r=simple&link='.urlencode($long_url)); + $short_url = curl_exec($curlh); + break; + case 'metamark.net': + curl_setopt($curlh, CURLOPT_URL, 'http://metamark.net/api/rest/simple?long_url='.urlencode($long_url)); + $short_url = curl_exec($curlh); + break; + case 'tinyurl.com': + curl_setopt($curlh, CURLOPT_URL, 'http://tinyurl.com/api-create.php?url='.urlencode($long_url)); + $short_url = curl_exec($curlh); + break; + default: + $short_url = false; + } + + curl_close($curlh); + + return $short_url; +} + +function common_client_ip() +{ + if (!isset($_SERVER) || !array_key_exists('REQUEST_METHOD', $_SERVER)) { + return null; + } + + if ($_SERVER['HTTP_X_FORWARDED_FOR']) { + if ($_SERVER['HTTP_CLIENT_IP']) { + $proxy = $_SERVER['HTTP_CLIENT_IP']; + } else { + $proxy = $_SERVER['REMOTE_ADDR']; + } + $ip = $_SERVER['HTTP_X_FORWARDED_FOR']; + } else { + if ($_SERVER['HTTP_CLIENT_IP']) { + $ip = $_SERVER['HTTP_CLIENT_IP']; + } else { + $ip = $_SERVER['REMOTE_ADDR']; + } + } + + return array($ip, $proxy); +} diff --git a/lib/webcolor.php b/lib/webcolor.php new file mode 100644 index 000000000..f3ca6e94a --- /dev/null +++ b/lib/webcolor.php @@ -0,0 +1,192 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Base class for deleting things + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Personal + * @package Laconica + * @author Zach Copley <zach@controlyourself.ca> + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +class WebColor { + + // XXX: Maybe make getters and setters for r,g,b values and tuples, + // e.g.: to support this kinda CSS representation: rgb(255,0,0) + // http://www.w3.org/TR/CSS21/syndata.html#color-units + + var $red = 0; + var $green = 0; + var $blue = 0; + + /** + * Constructor + * + * @return nothing + */ + + function __construct($color = null) + { + if (isset($color)) { + $this->parseColor($color); + } + } + + /** + * Parses input to and tries to determine whether the color + * is being specified via an integer or hex tuple and sets + * the RGB instance variables accordingly. + * + * XXX: Maybe support (r,g,b) style, and array? + * + * @param mixed $color + * + * @return nothing + */ + + function parseColor($color) { + + if (is_numeric($color)) { + $this->setIntColor($color); + } else { + + // XXX named colors + + // XXX: probably should do even more validation + + if (preg_match('/(#([0-9A-Fa-f]{3,6})\b)/u', $color) > 0) { + $this->setHexColor($color); + } else { + $errmsg = _('%s is not a valid color!'); + throw new WebColorException(sprintf($errmsg, $color)); + } + } + } + + /** + * @param string $name + * + * @return nothing + */ + + function setNamedColor($name) + { + // XXX Implement this + } + + + /** + * Sets the RGB color values from a a hex tuple + * + * @param string $hexcolor + * + * @return nothing + */ + + function setHexColor($hexcolor) { + + if ($hexcolor[0] == '#') { + $hexcolor = substr($hexcolor, 1); + } + + if (strlen($hexcolor) == 6) { + list($r, $g, $b) = array($hexcolor[0].$hexcolor[1], + $hexcolor[2].$hexcolor[3], + $hexcolor[4].$hexcolor[5]); + } elseif (strlen($hexcolor) == 3) { + list($r, $g, $b) = array($hexcolor[0].$hexcolor[0], + $hexcolor[1].$hexcolor[1], + $hexcolor[2].$hexcolor[2]); + } else { + $errmsg = _('%s is not a valid color! Use 3 or 6 hex chars.'); + throw new WebColorException(sprintf($errmsg, $hexcolor)); + } + + $this->red = hexdec($r); + $this->green = hexdec($g); + $this->blue = hexdec($b); + + } + + /** + * Sets the RGB color values from a 24-bit integer + * + * @param int $intcolor + * + * @return nothing + */ + + function setIntColor($intcolor) + { + // We could do 32 bit and have an alpha channel because + // Sarven wants one real bad, but nah. + + $this->red = $intcolor >> 16; + $this->green = $intcolor >> 8 & 0xFF; + $this->blue = $intcolor & 0xFF; + + } + + /** + * Returns a hex tuple of the RGB color useful for output in HTML + * + * @return string + */ + + function hexValue() { + + $hexcolor = (strlen(dechex($this->red)) < 2 ? '0' : '' ) . + dechex($this->red); + $hexcolor .= (strlen(dechex($this->green)) < 2 ? '0' : '') . + dechex($this->green); + $hexcolor .= (strlen(dechex($this->blue)) < 2 ? '0' : '') . + dechex($this->blue); + + return strtoupper($hexcolor); + + } + + /** + * Returns a 24-bit packed integer representation of the RGB color + * for convenient storage in the DB + * + * XXX: probably could just use hexdec() instead + * + * @return int + */ + + function intValue() + { + $intcolor = 256 * 256 * $this->red + 256 * $this->green + $this->blue; + return $intcolor; + } + +} + +class WebColorException extends Exception +{ +} + +?>
\ No newline at end of file diff --git a/lib/xmppqueuehandler.php b/lib/xmppqueuehandler.php index 91015fd45..77d476c30 100644 --- a/lib/xmppqueuehandler.php +++ b/lib/xmppqueuehandler.php @@ -1,7 +1,7 @@ <?php /* * Laconica - a distributed open-source microblogging tool - * Copyright (C) 2008, Controlez-Vous, Inc. + * Copyright (C) 2008, 2009, Control Yourself, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -21,8 +21,10 @@ if (!defined('LACONICA')) { exit(1); } require_once(INSTALLDIR.'/lib/queuehandler.php'); +define('PING_INTERVAL', 120); + /** - * Common superclass for all XMPP-using queue handlers. They all need to + * Common superclass for all XMPP-using queue handlers. They all need to * service their message queues on idle, and forward any incoming messages * to the XMPP listener connection. So, we abstract out common code to a * superclass. @@ -30,12 +32,14 @@ require_once(INSTALLDIR.'/lib/queuehandler.php'); class XmppQueueHandler extends QueueHandler { - + var $pingid = 0; + var $lastping = null; + function start() { # Low priority; we don't want to receive messages $this->log(LOG_INFO, "INITIALIZE"); - $this->conn = jabber_connect($this->_id); + $this->conn = jabber_connect($this->_id.$this->transport()); if ($this->conn) { $this->conn->addEventHandler('message', 'forward_message', $this); $this->conn->addEventHandler('reconnect', 'handle_reconnect', $this); @@ -44,7 +48,12 @@ class XmppQueueHandler extends QueueHandler } return !is_null($this->conn); } - + + function timeout() + { + return 10; + } + function handle_reconnect(&$pl) { $this->conn->processUntil('session_start'); @@ -56,14 +65,36 @@ class XmppQueueHandler extends QueueHandler # Process the queue for as long as needed try { if ($this->conn) { + $this->log(LOG_DEBUG, "Servicing the XMPP queue."); $this->conn->processTime($timeout); + $now = time(); + if (empty($this->lastping) || $now - $this->lastping > PING_INTERVAL) { + $this->sendPing(); + $this->lastping = $now; + } } } catch (XMPPHP_Exception $e) { $this->log(LOG_ERR, "Got an XMPPHP_Exception: " . $e->getMessage()); die($e->getMessage()); } } - + + function sendPing() + { + $jid = jabber_daemon_address().'/'.$this->_id.$this->transport(); + $server = common_config('xmpp', 'server'); + + if (!isset($this->pingid)) { + $this->pingid = 0; + } else { + $this->pingid++; + } + + $this->log(LOG_DEBUG, "Sending ping #{$this->pingid}"); + + $this->conn->send("<iq from='{$jid}' to='{$server}' id='ping_{$this->pingid}' type='get'><ping xmlns='urn:xmpp:ping'/></iq>"); + } + function forward_message(&$pl) { if ($pl['type'] != 'chat') { @@ -92,7 +123,12 @@ class XmppQueueHandler extends QueueHandler if (common_config('xmpp', 'listener')) { return common_config('xmpp', 'listener'); } else { - return jabber_daemon_address() . '/' . common_config('xmpp','resource') . '-listener'; + return jabber_daemon_address() . '/' . common_config('xmpp','resource') . 'daemon'; } } + + function getSockets() + { + return array($this->conn->getSocket()); + } } |