diff options
Diffstat (limited to 'lib')
61 files changed, 3032 insertions, 701 deletions
diff --git a/lib/Shorturl_api.php b/lib/Shorturl_api.php index fe106cb83..924aa93a8 100644 --- a/lib/Shorturl_api.php +++ b/lib/Shorturl_api.php @@ -22,6 +22,7 @@ if (!defined('LACONICA')) { exit(1); } class ShortUrlApi { protected $service_url; + protected $long_limit = 27; function __construct($service_url) { @@ -39,7 +40,7 @@ class ShortUrlApi } private function is_long($url) { - return strlen($url) >= 30; + return strlen($url) >= $this->long_limit; } protected function http_post($data) { diff --git a/lib/accountsettingsaction.php b/lib/accountsettingsaction.php index 46090b8c1..86800d2a3 100644 --- a/lib/accountsettingsaction.php +++ b/lib/accountsettingsaction.php @@ -115,6 +115,9 @@ class AccountSettingsNav extends Widget 'openidsettings' => array(_('OpenID'), _('Add or remove OpenIDs')), + 'designsettings' => + array(_('Design'), + _('Design your profile')), 'othersettings' => array(_('Other'), _('Other options'))); diff --git a/lib/action.php b/lib/action.php index 79e8c9547..6a69d2651 100644 --- a/lib/action.php +++ b/lib/action.php @@ -93,10 +93,22 @@ class Action extends HTMLOutputter // lawsuit */ function showPage() { - $this->startHTML(); - $this->showHead(); - $this->showBody(); - $this->endHTML(); + if (Event::handle('StartShowHTML', array($this))) { + $this->startHTML(); + Event::handle('EndShowHTML', array($this)); + } + if (Event::handle('StartShowHead', array($this))) { + $this->showHead(); + Event::handle('EndShowHead', array($this)); + } + if (Event::handle('StartShowBody', array($this))) { + $this->showBody(); + Event::handle('EndShowBody', array($this)); + } + if (Event::handle('StartEndHTML', array($this))) { + $this->endHTML(); + Event::handle('EndEndHTML', array($this)); + } } /** @@ -109,8 +121,10 @@ class Action extends HTMLOutputter // lawsuit // XXX: attributes (profile?) $this->elementStart('head'); $this->showTitle(); + $this->showShortcutIcon(); $this->showStylesheets(); $this->showScripts(); + $this->showRelationshipLinks(); $this->showOpenSearch(); $this->showFeeds(); $this->showDescription(); @@ -145,6 +159,32 @@ class Action extends HTMLOutputter // lawsuit } /** + * Show themed shortcut icon + * + * @return nothing + */ + function showShortcutIcon() + { + if (is_readable(INSTALLDIR . '/theme/' . common_config('site', 'theme') . '/favicon.ico')) { + $this->element('link', array('rel' => 'shortcut icon', + 'href' => theme_path('favicon.ico'))); + } else { + $this->element('link', array('rel' => 'shortcut icon', + 'href' => common_path('favicon.ico'))); + } + + if (common_config('site', 'mobile')) { + if (is_readable(INSTALLDIR . '/theme/' . common_config('site', 'theme') . '/apple-touch-icon.png')) { + $this->element('link', array('rel' => 'apple-touch-icon', + 'href' => theme_path('apple-touch-icon.png'))); + } else { + $this->element('link', array('rel' => 'apple-touch-icon', + 'href' => common_path('apple-touch-icon.png'))); + } + } + } + + /** * Show stylesheets * * @return nothing @@ -153,21 +193,21 @@ class Action extends HTMLOutputter // lawsuit { if (Event::handle('StartShowStyles', array($this))) { if (Event::handle('StartShowLaconicaStyles', array($this))) { - $this->element('link', array('rel' => 'stylesheet', 'type' => 'text/css', - 'href' => theme_path('css/display.css', 'base') . '?version=' . LACONICA_VERSION, - 'media' => 'screen, projection, tv')); - - - $this->element('link', array('rel' => 'stylesheet', - 'type' => 'text/css', - 'href' => theme_path('css/modal.css', 'base') . '?version=' . LACONICA_VERSION, + 'href' => theme_path('css/display.css', null) . '?version=' . LACONICA_VERSION, 'media' => 'screen, projection, tv')); + if (common_config('site', 'mobile')) { + $this->element('link', array('rel' => 'stylesheet', + 'type' => 'text/css', + 'href' => theme_path('css/mobile.css', 'base') . '?version=' . LACONICA_VERSION, + // TODO: "handheld" CSS for other mobile devices + 'media' => 'only screen and (max-device-width: 480px)')); // Mobile WebKit + } $this->element('link', array('rel' => 'stylesheet', 'type' => 'text/css', - 'href' => theme_path('css/display.css', null) . '?version=' . LACONICA_VERSION, - 'media' => 'screen, projection, tv')); + 'href' => theme_path('css/print.css', 'base') . '?version=' . LACONICA_VERSION, + 'media' => 'print')); Event::handle('EndShowLaconicaStyles', array($this)); } if (Event::handle('StartShowUAStyles', array($this))) { @@ -204,9 +244,8 @@ class Action extends HTMLOutputter // lawsuit 'src' => common_path('js/jquery.form.js')), ' '); - $this->element('script', array('type' => 'text/javascript', - 'src' => common_path('js/jquery.simplemodal-1.2.2.pack.js')), + 'src' => common_path('js/jquery.joverlay.min.js')), ' '); @@ -219,18 +258,9 @@ class Action extends HTMLOutputter // lawsuit $this->element('script', array('type' => 'text/javascript', 'src' => common_path('js/util.js?version='.LACONICA_VERSION)), ' '); - - - $this->element('script', array('type' => 'text/javascript', - 'src' => common_path('js/flowplayer-3.0.5.min.js')), - ' '); - - $this->element('script', array('type' => 'text/javascript', - 'src' => common_path('js/video.js')), - ' '); - - - + // Frame-busting code to avoid clickjacking attacks. + $this->element('script', array('type' => 'text/javascript'), + 'if (window.top !== window.self) { window.top.location.href = window.self.location.href; }'); Event::handle('EndShowLaconicaScripts', array($this)); } Event::handle('EndShowScripts', array($this)); @@ -238,6 +268,19 @@ class Action extends HTMLOutputter // lawsuit } /** + * Show document relationship links + * + * SHOULD overload + * + * @return nothing + */ + function showRelationshipLinks() + { + // output <link> elements with appropriate HTML4.01 link types: + // http://www.w3.org/TR/html401/types.html#type-links + } + + /** * Show OpenSearch headers * * @return nothing @@ -308,7 +351,9 @@ class Action extends HTMLOutputter // lawsuit */ function showBody() { - $this->elementStart('body', array('id' => $this->trimmed('action'))); + $this->elementStart('body', (common_current_user()) ? array('id' => $this->trimmed('action'), + 'class' => 'user_in') + : array('id' => $this->trimmed('action'))); $this->elementStart('div', array('id' => 'wrap')); if (Event::handle('StartShowHeader', array($this))) { $this->showHeader(); @@ -382,13 +427,8 @@ class Action extends HTMLOutputter // lawsuit if ($user) { $this->menuItem(common_local_url('all', array('nickname' => $user->nickname)), _('Home'), _('Personal profile and friends timeline'), false, 'nav_home'); - } - $this->menuItem(common_local_url('peoplesearch'), - _('Search'), _('Search for people or text'), false, 'nav_search'); - if ($user) { $this->menuItem(common_local_url('profilesettings'), _('Account'), _('Change your email, avatar, password, profile'), false, 'nav_account'); - if (common_config('xmpp', 'enabled')) { $this->menuItem(common_local_url('imsettings'), _('Connect'), _('Connect to IM, SMS, Twitter'), false, 'nav_connect'); @@ -396,20 +436,28 @@ 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'); $this->menuItem(common_local_url('logout'), _('Logout'), _('Logout from the site'), false, 'nav_logout'); - } else { - $this->menuItem(common_local_url('login'), - _('Login'), _('Login to the site'), false, 'nav_login'); + } + else { if (!common_config('site', 'closed')) { $this->menuItem(common_local_url('register'), _('Register'), _('Create an account'), false, 'nav_register'); } $this->menuItem(common_local_url('openidlogin'), _('OpenID'), _('Login with OpenID'), false, 'nav_openid'); + $this->menuItem(common_local_url('login'), + _('Login'), _('Login to the site'), false, 'nav_login'); } $this->menuItem(common_local_url('doc', array('title' => 'help')), _('Help'), _('Help me!'), false, 'nav_help'); + $this->menuItem(common_local_url('peoplesearch'), + _('Search'), _('Search for people or text'), false, 'nav_search'); Event::handle('EndPrimaryNav', array($this)); } $this->elementEnd('ul'); @@ -472,7 +520,10 @@ class Action extends HTMLOutputter // lawsuit function showCore() { $this->elementStart('div', array('id' => 'core')); - $this->showLocalNavBlock(); + if (Event::handle('StartShowLocalNavBlock', array($this))) { + $this->showLocalNavBlock(); + Event::handle('EndShowLocalNavBlock', array($this)); + } if (Event::handle('StartShowContentBlock', array($this))) { $this->showContentBlock(); Event::handle('EndShowContentBlock', array($this)); @@ -546,7 +597,10 @@ class Action extends HTMLOutputter // lawsuit 'class' => 'system_notice')); $this->element('dt', null, _('Page notice')); $this->elementStart('dd'); - $this->showPageNotice(); + if (Event::handle('StartShowPageNotice', array($this))) { + $this->showPageNotice(); + Event::handle('EndShowPageNotice', array($this)); + } $this->elementEnd('dd'); $this->elementEnd('dl'); } @@ -583,7 +637,10 @@ class Action extends HTMLOutputter // lawsuit { $this->elementStart('div', array('id' => 'aside_primary', 'class' => 'aside')); - $this->showExportData(); + if (Event::handle('StartShowExportData', array($this))) { + $this->showExportData(); + Event::handle('EndShowExportData', array($this)); + } if (Event::handle('StartShowSections', array($this))) { $this->showSections(); Event::handle('EndShowSections', array($this)); @@ -655,6 +712,8 @@ class Action extends HTMLOutputter // lawsuit _('Source')); $this->menuItem(common_local_url('doc', array('title' => 'contact')), _('Contact')); + $this->menuItem(common_local_url('doc', array('title' => 'badge')), + _('Badge')); Event::handle('EndSecondaryNav', array($this)); } $this->elementEnd('ul'); @@ -751,9 +810,12 @@ class Action extends HTMLOutputter // lawsuit * * MAY override * + * @param array $args other arguments + * * @return boolean is read only action? */ - function isReadOnly() + + function isReadOnly($args) { return false; } @@ -805,12 +867,15 @@ class Action extends HTMLOutputter // lawsuit } if ($lm) { header('Last-Modified: ' . date(DATE_RFC1123, $lm)); - $if_modified_since = $_SERVER['HTTP_IF_MODIFIED_SINCE']; - if ($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 (!$etag || - $this->_hasEtag($etag, $_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)) { header('HTTP/1.1 304 Not Modified'); // Better way to do this? exit(0); @@ -828,9 +893,11 @@ class Action extends HTMLOutputter // lawsuit * * @return boolean */ + function _hasEtag($etag, $if_none_match) { - return ($if_none_match) && in_array($etag, explode(',', $if_none_match)); + $etags = explode(',', $if_none_match); + return in_array($etag, $etags) || in_array('*', $etags); } /** @@ -893,11 +960,15 @@ class Action extends HTMLOutputter // lawsuit * * @return string current URL */ + function selfUrl() { $action = $this->trimmed('action'); $args = $this->args; unset($args['action']); + if (array_key_exists('submit', $args)) { + unset($args['submit']); + } foreach (array_keys($_COOKIE) as $cookie) { unset($args[$cookie]); } @@ -958,17 +1029,17 @@ class Action extends HTMLOutputter // lawsuit } if ($have_before) { $pargs = array('page' => $page-1); - $newargs = $args ? array_merge($args, $pargs) : $pargs; $this->elementStart('li', array('class' => 'nav_prev')); - $this->element('a', array('href' => common_local_url($action, $newargs), 'rel' => 'prev'), + $this->element('a', array('href' => common_local_url($action, $args, $pargs), + 'rel' => 'prev'), _('After')); $this->elementEnd('li'); } if ($have_after) { $pargs = array('page' => $page+1); - $newargs = $args ? array_merge($args, $pargs) : $pargs; $this->elementStart('li', array('class' => 'nav_next')); - $this->element('a', array('href' => common_local_url($action, $newargs), 'rel' => 'next'), + $this->element('a', array('href' => common_local_url($action, $args, $pargs), + 'rel' => 'next'), _('Before')); $this->elementEnd('li'); } @@ -992,4 +1063,36 @@ class Action extends HTMLOutputter // lawsuit { return null; } + + /** + * Generate document metadata for sequential navigation + * + * @param boolean $have_before is there something before? + * @param boolean $have_after is there something after? + * @param integer $page current page + * @param string $action current action + * @param array $args rest of query arguments + * + * @return nothing + */ + function sequenceRelationships($have_next, $have_previous, $page, $action, $args=null) + { + // Outputs machine-readable pagination in <link> elements. + // Pattern taken from $this->pagination() method. + + // "next" is equivalent to "after" + if ($have_next) { + $pargs = array('page' => $page-1); + $this->element('link', array('rel' => 'next', + 'href' => common_local_url($action, $args, $pargs), + 'title' => _('Next'))); + } + // "previous" is equivalent to "before" + if ($have_previous=true) { // FIXME + $pargs = array('page' => $page+1); + $this->element('link', array('rel' => 'prev', + 'href' => common_local_url($action, $args, $pargs), + 'title' => _('Previous'))); + } + } } diff --git a/lib/attachmentlist.php b/lib/attachmentlist.php new file mode 100644 index 000000000..9485fe3d6 --- /dev/null +++ b/lib/attachmentlist.php @@ -0,0 +1,300 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * widget for displaying a list of notice attachments + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category UI + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @author Sarven Capadisli <csarven@controlyourself.ca> + * @copyright 2008 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +/** + * widget for displaying a list of notice attachments + * + * There are a number of actions that display a list of notices, in + * reverse chronological order. This widget abstracts out most of the + * code for UI for notice lists. It's overridden to hide some + * data for e.g. the profile page. + * + * @category UI + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + * @see Notice + * @see StreamAction + * @see NoticeListItem + * @see ProfileNoticeList + */ + +class AttachmentList extends Widget +{ + /** the current stream of notices being displayed. */ + + var $notice = null; + + /** + * constructor + * + * @param Notice $notice stream of notices from DB_DataObject + */ + + function __construct($notice, $out=null) + { + parent::__construct($out); + $this->notice = $notice; + } + + /** + * show the list of notices + * + * "Uses up" the stream by looping through it. So, probably can't + * be called twice on the same list. + * + * @return int count of notices listed. + */ + + function show() + { +// $this->out->elementStart('div', array('id' =>'attachments_primary')); + $this->out->elementStart('div', array('id' =>'content')); + $this->out->element('h2', null, _('Attachments')); + $this->out->elementStart('ul', array('class' => 'attachments')); + + $atts = new File; + $att = $atts->getAttachments($this->notice->id); + foreach ($att as $n=>$attachment) { + $item = $this->newListItem($attachment); + $item->show(); + } + + $this->out->elementEnd('ul'); + $this->out->elementEnd('div'); + + return count($att); + } + + /** + * returns a new list item for the current notice + * + * Recipe (factory?) method; overridden by sub-classes to give + * a different list item class. + * + * @param Notice $notice the current notice + * + * @return NoticeListItem a list item for displaying the notice + */ + + function newListItem($attachment) + { + return new AttachmentListItem($attachment, $this->out); + } +} + +/** + * widget for displaying a single notice + * + * This widget has the core smarts for showing a single notice: what to display, + * where, and under which circumstances. Its key method is show(); this is a recipe + * that calls all the other show*() methods to build up a single notice. The + * ProfileNoticeListItem subclass, for example, overrides showAuthor() to skip + * author info (since that's implicit by the data in the page). + * + * @category UI + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + * @see NoticeList + * @see ProfileNoticeListItem + */ + +class AttachmentListItem extends Widget +{ + /** The attachment this item will show. */ + + var $attachment = null; + + var $oembed = null; + + /** + * constructor + * + * Also initializes the profile attribute. + * + * @param Notice $notice The notice we'll display + */ + + function __construct($attachment, $out=null) + { + parent::__construct($out); + $this->attachment = $attachment; + $this->oembed = File_oembed::staticGet('file_id', $this->attachment->id); + } + + function title() { + if (empty($this->attachment->title)) { + if (empty($this->oembed->title)) { + $title = $this->attachment->url; + } else { + $title = $this->oembed->title; + } + } else { + $title = $this->attachment->title; + } + + return $title; + } + + function linkTitle() { + return 'Our page for ' . $this->title(); + } + + /** + * recipe function for displaying a single notice. + * + * This uses all the other methods to correctly display a notice. Override + * it or one of the others to fine-tune the output. + * + * @return void + */ + + function show() + { + $this->showStart(); + $this->showNoticeAttachment(); + $this->showEnd(); + } + + function linkAttr() { + return array('class' => 'attachment', 'href' => common_local_url('attachment', array('attachment' => $this->attachment->id))); + } + + function showLink() { + $attr = $this->linkAttr(); + $text = $this->linkTitle(); + $this->out->elementStart('h4'); + $this->out->element('a', $attr, $text); + + if ($this->attachment->url !== $this->title()) + $this->out->element('span', null, " ({$this->attachment->url})"); + + + $this->out->elementEnd('h4'); + } + + function showNoticeAttachment() + { + $this->showLink(); + $this->showRepresentation(); + } + + function showRepresentation() { + $thumbnail = File_thumbnail::staticGet('file_id', $this->attachment->id); + if (!empty($thumbnail)) { + $this->out->elementStart('a', $this->linkAttr()/*'href' => $this->linkTo()*/); + $this->out->element('img', array('alt' => 'nothing to say', 'src' => $thumbnail->url, 'width' => $thumbnail->width, 'height' => $thumbnail->height)); + $this->out->elementEnd('a'); + } + } + + /** + * start a single notice. + * + * @return void + */ + + function showStart() + { + // XXX: RDFa + // TODO: add notice_type class e.g., notice_video, notice_image + $this->out->elementStart('li'); + } + + /** + * finish the notice + * + * Close the last elements in the notice list item + * + * @return void + */ + + function showEnd() + { + $this->out->elementEnd('li'); + } +} + +class Attachment extends AttachmentListItem +{ + function show() { + $this->showNoticeAttachment(); + } + + function linkAttr() { + return array('class' => 'external', 'href' => $this->attachment->url); + } + + function linkTitle() { + return 'Direct link to ' . $this->title(); + } + + function showRepresentation() { + if (empty($this->oembed->type)) { + if (empty($this->attachment->mimetype)) { + $this->out->element('pre', null, 'oh well... not sure how to handle the following: ' . print_r($this->attachment, true)); + } else { + switch ($this->attachment->mimetype) { + case 'image/gif': + case 'image/png': + case 'image/jpg': + case 'image/jpeg': + $this->out->element('img', array('src' => $this->attachment->url, 'alt' => 'alt')); + break; + } + } + } else { + switch ($this->oembed->type) { + case 'rich': + case 'video': + case 'link': + if (!empty($this->oembed->html)) { + $this->out->raw($this->oembed->html); + } + break; + + case 'photo': + $this->out->element('img', array('src' => $this->oembed->url, 'width' => $this->oembed->width, 'height' => $this->oembed->height, 'alt' => 'alt')); + break; + + default: + $this->out->element('pre', null, 'oh well... not sure how to handle the following oembed: ' . print_r($this->oembed, true)); + } + } + } +} + diff --git a/lib/attachmentnoticesection.php b/lib/attachmentnoticesection.php new file mode 100644 index 000000000..eb3176376 --- /dev/null +++ b/lib/attachmentnoticesection.php @@ -0,0 +1,75 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * FIXME + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Widget + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +/** + * FIXME + * + * These are the widgets that show interesting data about a person * group, or site. + * + * @category Widget + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +class AttachmentNoticeSection extends NoticeSection +{ + function showContent() { + parent::showContent(); + return false; + } + + function getNotices() + { + $notice = new Notice; + $f2p = new File_to_post; + $f2p->file_id = $this->out->attachment->id; + $notice->joinAdd($f2p); + $notice->orderBy('created desc'); + $notice->selectAdd('post_id as id'); + $notice->find(); + return $notice; + } + + function title() + { + return _('Notices where this attachment appears'); + } + + function divId() + { + return 'popular_notices'; + } +} + diff --git a/lib/attachmentsection.php b/lib/attachmentsection.php new file mode 100644 index 000000000..20e620b9b --- /dev/null +++ b/lib/attachmentsection.php @@ -0,0 +1,80 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Base class for sections showing lists of attachments + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Widget + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +define('ATTACHMENTS_PER_SECTION', 6); + +/** + * Base class for sections showing lists of attachments + * + * These are the widgets that show interesting data about a person + * group, or site. + * + * @category Widget + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +class AttachmentSection extends Section +{ + function showContent() + { + $attachments = $this->getAttachments(); + + $cnt = 0; + + $this->out->elementStart('ul', 'attachments'); + + while ($attachments->fetch() && ++$cnt <= ATTACHMENTS_PER_SECTION) { + $this->showAttachment($attachments); + } + + $this->out->elementEnd('ul'); + + return ($cnt > ATTACHMENTS_PER_SECTION); + } + + function getAttachments() + { + return null; + } + + function showAttachment($attachment) + { + $this->out->elementStart('li'); + $this->out->element('a', array('class' => 'attachment', 'href' => common_local_url('attachment', array('attachment' => $attachment->file_id))), "Attachment tagged {$attachment->c} times"); + $this->out->elementEnd('li'); + } +} + diff --git a/lib/attachmenttagcloudsection.php b/lib/attachmenttagcloudsection.php new file mode 100644 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/clienterroraction.php b/lib/clienterroraction.php index 5019dc06d..0c48414d5 100644 --- a/lib/clienterroraction.php +++ b/lib/clienterroraction.php @@ -49,7 +49,7 @@ class ClientErrorAction extends ErrorAction function __construct($message='Error', $code=400) { parent::__construct($message, $code); - + $this->status = array(400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', @@ -72,7 +72,7 @@ class ClientErrorAction extends ErrorAction } // XXX: Should these error actions even be invokable via URI? - + function handle($args) { parent::handle($args); @@ -84,11 +84,16 @@ class ClientErrorAction extends ErrorAction } $this->message = $this->trimmed('message'); - + if (!$this->message) { - $this->message = "Client Error $this->code"; - } + $this->message = "Client Error $this->code"; + } $this->showPage(); } + + function title() + { + return $this->status[$this->code]; + } } diff --git a/lib/common.php b/lib/common.php index 4fc749ca0..abdc22c0e 100644 --- a/lib/common.php +++ b/lib/common.php @@ -19,7 +19,7 @@ if (!defined('LACONICA')) { exit(1); } -define('LACONICA_VERSION', '0.7.1'); +define('LACONICA_VERSION', '0.7.3'); define('AVATAR_PROFILE_SIZE', 96); define('AVATAR_STREAM_SIZE', 48); @@ -71,8 +71,11 @@ $config = array('name' => 'Just another Laconica microblog', 'server' => $_server, 'theme' => 'default', + 'skin' => 'default', 'path' => $_path, 'logfile' => null, + 'logo' => null, + 'logdebug' => false, 'fancy' => false, 'locale_path' => INSTALLDIR.'/locale', 'language' => 'en_US', @@ -84,7 +87,10 @@ $config = 'broughtbyurl' => null, 'closed' => false, 'inviteonly' => false, - 'private' => false), + 'private' => false, + 'ssl' => 'never', + 'sslserver' => null, + 'dupelimit' => 60), # default for same person saying the same thing 'syslog' => array('appname' => 'laconica', # for syslog 'priority' => 'debug'), # XXX: currently ignored @@ -137,14 +143,22 @@ $config = array('piddir' => '/var/run', 'user' => false, 'group' => false), + 'twitterbridge' => + array('enabled' => false), 'integration' => - array('source' => 'Laconica'), # source attribute for Twitter + array('source' => 'Laconica', # source attribute for Twitter + 'taguri' => $_server.',2009'), # base for tag URIs 'memcached' => array('enabled' => false, 'server' => 'localhost', 'port' => 11211), + 'ping' => + array('notify' => array()), 'inboxes' => array('enabled' => true), # on by default for new sites + 'newuser' => + array('subscribe' => null, + 'welcome' => null), ); $config['db'] = &PEAR::getStaticProperty('DB_DataObject','options'); @@ -178,12 +192,31 @@ if (strlen($_path) > 0) { $_config_files[] = INSTALLDIR.'/config.php'; +$_have_a_config = false; + foreach ($_config_files as $_config_file) { - if (file_exists($_config_file)) { + if (@file_exists($_config_file)) { include_once($_config_file); + $_have_a_config = true; } } +function _have_config() +{ + global $_have_a_config; + return $_have_a_config; +} + +// XXX: Throw a conniption if database not installed + +// Fixup for laconica.ini + +$_db_name = substr($config['db']['database'], strrpos($config['db']['database'], '/') + 1); + +if ($_db_name != 'laconica' && !array_key_exists('ini_'.$_db_name, $config['db'])) { + $config['db']['ini_'.$_db_name] = INSTALLDIR.'/classes/laconica.ini'; +} + // XXX: how many of these could be auto-loaded on use? require_once('Validate.php'); diff --git a/lib/error.php b/lib/error.php index 526d9f81b..282682133 100644 --- a/lib/error.php +++ b/lib/error.php @@ -93,7 +93,7 @@ class ErrorAction extends Action return $this->message; } - function isReadOnly() + function isReadOnly($args) { return true; } diff --git a/lib/facebookutil.php b/lib/facebookutil.php index ec3987273..242d2e06f 100644 --- a/lib/facebookutil.php +++ b/lib/facebookutil.php @@ -27,9 +27,21 @@ define("FACEBOOK_PROMPTED_UPDATE_PREF", 2); function getFacebook() { + static $facebook = null; + $apikey = common_config('facebook', 'apikey'); $secret = common_config('facebook', 'secret'); - return new Facebook($apikey, $secret); + + if ($facebook === null) { + $facebook = new Facebook($apikey, $secret); + } + + if (!$facebook) { + common_log(LOG_ERR, 'Could not make new Facebook client obj!', + __FILE__); + } + + return $facebook; } function updateProfileBox($facebook, $flink, $notice) { @@ -92,7 +104,6 @@ function isFacebookBound($notice, $flink) { } - function facebookBroadcastNotice($notice) { $facebook = getFacebook(); diff --git a/lib/featureduserssection.php b/lib/featureduserssection.php index aed94b1a5..4b9238d47 100644 --- a/lib/featureduserssection.php +++ b/lib/featureduserssection.php @@ -57,9 +57,14 @@ class FeaturedUsersSection extends ProfileSection $quoted[] = "'$nick'"; } + $table = "user"; + if(common_config('db','quote_identifiers')) { + $table = '"' . $table . '"'; + } + $qry = 'SELECT profile.* ' . - 'FROM profile JOIN user on profile.id = user.id ' . - 'WHERE user.nickname in (' . implode(',', $quoted) . ') ' . + 'FROM profile JOIN '. $table .' on profile.id = '. $table .'.id ' . + 'WHERE '. $table .'.nickname in (' . implode(',', $quoted) . ') ' . 'ORDER BY profile.created DESC '; $limit = PROFILES_PER_SECTION + 1; diff --git a/lib/frequentattachmentsection.php b/lib/frequentattachmentsection.php new file mode 100644 index 000000000..0ce0d1871 --- /dev/null +++ b/lib/frequentattachmentsection.php @@ -0,0 +1,66 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * FIXME + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Widget + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +/** + * FIXME + * + * These are the widgets that show interesting data about a person + * group, or site. + * + * @category Widget + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +class FrequentAttachmentSection extends AttachmentSection +{ + function getAttachments() { + $notice_tag = new Notice_tag; + $query = 'select file_id, count(file_id) as c from notice_tag join file_to_post on post_id = notice_id where tag="' . $notice_tag->escape($this->out->tag) . '" group by file_id order by c desc'; + $notice_tag->query($query); + return $notice_tag; + } + + function title() + { + return sprintf(_('Attachments frequently tagged with %s'), $this->out->tag); + } + + function divId() + { + return 'frequent_attachments'; + } +} + diff --git a/lib/galleryaction.php b/lib/galleryaction.php index 25a5e3fd5..8fa11a756 100644 --- a/lib/galleryaction.php +++ b/lib/galleryaction.php @@ -50,7 +50,7 @@ class GalleryAction extends Action if ($this->arg('page') && $this->arg('page') != 1) { $args['page'] = $this->arg['page']; } - common_redirect(common_local_url('subscriptions', $args), 301); + common_redirect(common_local_url($this->trimmed('action'), $args), 301); return false; } @@ -71,11 +71,12 @@ class GalleryAction extends Action $this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1; $this->tag = $this->trimmed('tag'); + $this->q = $this->trimmed('q'); return true; } - function isReadOnly() + function isReadOnly($args) { return true; } @@ -87,7 +88,7 @@ class GalleryAction extends Action # Post from the tag dropdown; redirect to a GET if ($_SERVER['REQUEST_METHOD'] == 'POST') { - common_redirect($this->selfUrl(), 307); + common_redirect($this->selfUrl(), 303); return; } @@ -133,10 +134,12 @@ class GalleryAction extends Action $this->elementStart('li', array('id'=>'filter_tags_item')); $this->elementStart('form', array('name' => 'bytag', 'id' => 'bytag', + 'action' => common_path('?action=' . $this->trimmed('action')), 'method' => 'post')); $this->dropdown('tag', _('Tag'), $content, _('Choose a tag to narrow list'), false, $tag); - $this->submit('go', _('Go')); + $this->hidden('nickname', $this->user->nickname); + $this->submit('submit', _('Go')); $this->elementEnd('form'); $this->elementEnd('li'); $this->elementEnd('ul'); @@ -168,4 +171,4 @@ class GalleryAction extends Action { return array(); } -}
\ No newline at end of file +} diff --git a/lib/grouplist.php b/lib/grouplist.php index 6801ab426..1b8547499 100644 --- a/lib/grouplist.php +++ b/lib/grouplist.php @@ -151,7 +151,7 @@ class GroupList extends Widget # If we're on a list with an owner (subscriptions or subscribers)... - if ($user && $user->id == $this->owner->id) { + if (!empty($user) && !empty($this->owner) && $user->id == $this->owner->id) { $this->showOwnerControls(); } diff --git a/lib/groupminilist.php b/lib/groupminilist.php index fe38d0340..ae2d237f1 100644 --- a/lib/groupminilist.php +++ b/lib/groupminilist.php @@ -33,7 +33,7 @@ if (!defined('LACONICA')) { require_once INSTALLDIR.'/lib/grouplist.php'; -define('GROUPS_PER_MINILIST', 80); +define('GROUPS_PER_MINILIST', 27); /** * Widget to show a list of groups, good for sidebar @@ -75,8 +75,9 @@ class GroupMiniList extends GroupList 'href' => $this->group->homeUrl(), 'rel' => 'contact group', 'class' => 'url')); - $logo = ($this->group->stream_logo) ? - $this->group->stream_logo : User_group::defaultLogo(AVATAR_STREAM_SIZE); + + $logo = ($this->group->mini_logo) ? + $this->group->mini_logo : User_group::defaultLogo(AVATAR_MINI_SIZE); $this->out->element('img', array('src' => $logo, 'width' => AVATAR_MINI_SIZE, diff --git a/lib/groupsbymemberssection.php b/lib/groupsbymemberssection.php index 4fa07a244..963e21f15 100644 --- a/lib/groupsbymemberssection.php +++ b/lib/groupsbymemberssection.php @@ -48,7 +48,7 @@ class GroupsByMembersSection extends GroupSection $qry = 'SELECT user_group.*, count(*) as value ' . 'FROM user_group JOIN group_member '. 'ON user_group.id = group_member.group_id ' . - 'GROUP BY user_group.id ' . + 'GROUP BY user_group.id,user_group.nickname,user_group.fullname,user_group.homepage,user_group.description,user_group.location,user_group.original_logo,user_group.homepage_logo,user_group.stream_logo,user_group.mini_logo,user_group.created,user_group.modified ' . 'ORDER BY value DESC '; $limit = GROUPS_PER_SECTION; diff --git a/lib/groupsbypostssection.php b/lib/groupsbypostssection.php index a5e33a93d..325b4033f 100644 --- a/lib/groupsbypostssection.php +++ b/lib/groupsbypostssection.php @@ -48,7 +48,7 @@ class GroupsByPostsSection extends GroupSection $qry = 'SELECT user_group.*, count(*) as value ' . 'FROM user_group JOIN group_inbox '. 'ON user_group.id = group_inbox.group_id ' . - 'GROUP BY user_group.id ' . + 'GROUP BY user_group.id,user_group.nickname,user_group.fullname,user_group.homepage,user_group.description,user_group.location,user_group.original_logo,user_group.homepage_logo,user_group.stream_logo,user_group.mini_logo,user_group.created,user_group.modified ' . 'ORDER BY value DESC '; $limit = GROUPS_PER_SECTION; diff --git a/lib/grouptagcloudsection.php b/lib/grouptagcloudsection.php index f05be85cb..5d68af28b 100644 --- a/lib/grouptagcloudsection.php +++ b/lib/grouptagcloudsection.php @@ -58,8 +58,14 @@ class GroupTagCloudSection extends TagCloudSection function getTags() { + if (common_config('db', 'type') == 'pgsql') { + $weightexpr='sum(exp(-extract(epoch from (now() - notice_tag.created)) / %s))'; + } else { + $weightexpr='sum(exp(-(now() - notice_tag.created) / %s))'; + } + $qry = 'SELECT notice_tag.tag, '. - 'sum(exp(-(now() - notice_tag.created)/%s)) as weight ' . + $weightexpr . ' as weight ' . 'FROM notice_tag JOIN notice ' . 'ON notice_tag.notice_id = notice.id ' . 'JOIN group_inbox on group_inbox.notice_id = notice.id ' . diff --git a/lib/jabber.php b/lib/jabber.php index 3fbb3e1ab..7d584ad01 100644 --- a/lib/jabber.php +++ b/lib/jabber.php @@ -163,50 +163,25 @@ function jabber_send_notice($to, $notice) function jabber_format_entry($profile, $notice) { - // FIXME: notice url might be remote - - $noticeurl = common_local_url('shownotice', - array('notice' => $notice->id)); - - $msg = jabber_format_notice($profile, $notice); - - $self_url = common_local_url('userrss', array('nickname' => $profile->nickname)); - - $entry = "\n<entry xmlns='http://www.w3.org/2005/Atom'>\n"; - $entry .= "<source>\n"; - $entry .= "<title>" . $profile->nickname . " - " . common_config('site', 'name') . "</title>\n"; - $entry .= "<link href='" . htmlspecialchars($profile->profileurl) . "'/>\n"; - $entry .= "<link rel='self' type='application/rss+xml' href='" . $self_url . "'/>\n"; - $entry .= "<author><name>" . $profile->nickname . "</name></author>\n"; - $entry .= "<icon>" . $profile->avatarUrl(AVATAR_PROFILE_SIZE) . "</icon>\n"; - $entry .= "</source>\n"; - $entry .= "<title>" . htmlspecialchars($msg) . "</title>\n"; - $entry .= "<summary>" . htmlspecialchars($msg) . "</summary>\n"; - $entry .= "<link rel='alternate' href='" . $noticeurl . "' />\n"; - $entry .= "<id>". $notice->uri . "</id>\n"; - $entry .= "<published>".common_date_w3dtf($notice->created)."</published>\n"; - $entry .= "<updated>".common_date_w3dtf($notice->modified)."</updated>\n"; - if ($notice->reply_to) { - $replyurl = common_local_url('shownotice', - array('notice' => $notice->reply_to)); - $entry .= "<link rel='related' href='" . $replyurl . "'/>\n"; + $entry = $notice->asAtomEntry(true, true); + + $xs = new XMLStringer(); + $xs->elementStart('html', array('xmlns' => 'http://jabber.org/protocol/xhtml-im')); + $xs->elementStart('body', array('xmlns' => 'http://www.w3.org/1999/xhtml')); + $xs->element('a', array('href' => $profile->profileurl), + $profile->nickname); + $xs->text(": "); + if (!empty($notice->rendered)) { + $xs->raw($notice->rendered); + } else { + $xs->raw(common_render_content($notice->content, $notice)); } - $entry .= "</entry>\n"; - - $html = "\n<html xmlns='http://jabber.org/protocol/xhtml-im'>\n"; - $html .= "<body xmlns='http://www.w3.org/1999/xhtml'>\n"; - $html .= "<a href='".htmlspecialchars($profile->profileurl)."'>".$profile->nickname."</a>: "; - $html .= ($notice->rendered) ? $notice->rendered : common_render_content($notice->content, $notice); - $html .= "\n</body>\n"; - $html .= "\n</html>\n"; + $xs->elementEnd('body'); + $xs->elementEnd('html'); - $address = "<addresses xmlns='http://jabber.org/protocol/address'>\n"; - $address .= "<address type='replyto' jid='" . jabber_daemon_address() . "' />\n"; - $address .= "</addresses>\n"; + $html = $xs->getString(); - // FIXME: include a pubsub event, too. - - return $html . $entry . $address; + return $html . ' ' . $entry; } /** @@ -410,8 +385,8 @@ function jabber_broadcast_notice($notice) "ON $UT.id = notice_inbox.user_id " . 'WHERE notice_inbox.notice_id = ' . $notice->id . ' ' . 'AND notice_inbox.source = 2 ' . - 'AND user.jabber is not null ' . - 'AND user.jabbernotify = 1 '); + "AND $UT.jabber is not null " . + "AND $UT.jabbernotify = 1 "); while ($user->fetch()) { if (!array_key_exists($user->id, $sent_to)) { diff --git a/lib/jsonsearchresultslist.php b/lib/jsonsearchresultslist.php new file mode 100644 index 000000000..f786c20a8 --- /dev/null +++ b/lib/jsonsearchresultslist.php @@ -0,0 +1,270 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * widget for displaying a list of notices + * + * 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 Search + * @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); +} + +/** + * widget-like class for showing JSON search results + * + * @category Search + * @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 JSONSearchResultsList +{ + protected $notice; // protected attrs invisible to json_encode() + protected $rpp; + + // The below attributes are carefully named so the JSON output from + // this obj matches the output from search.twitter.com + + var $results; + var $since_id; + var $max_id; + var $refresh_url; + var $results_per_page; + var $completed_in; + var $page; + var $query; + + /** + * constructor + * + * @param Notice $notice stream of notices from DB_DataObject + * @param string $query the original search query + * @param int $rpp the number of results to display per page + * @param int $page a page offset + * @param int $since_id only display notices newer than this + */ + + function __construct($notice, $query, $rpp, $page, $since_id = 0) + { + $this->notice = $notice; + $this->query = urlencode($query); + $this->results_per_page = $rpp; + $this->rpp = $rpp; + $this->page = $page; + $this->since_id = $since_id; + $this->results = array(); + } + + /** + * show the list of search results + * + * @return int $count of the search results listed. + */ + + function show() + { + $cnt = 0; + + $time_start = microtime(true); + + while ($this->notice->fetch() && $cnt <= $this->rpp) { + $cnt++; + + // XXX: Hmmm. this depends on desc sort order + if (!$this->max_id) { + $this->max_id = (int)$this->notice->id; + } + + if ($cnt > $this->rpp) { + break; + } + + $item = new ResultItem($this->notice); + array_push($this->results, $item); + } + + $time_end = microtime(true); + $this->completed_in = $time_end - $time_start; + + // Set other attrs + + $this->refresh_url = '?since_id=' . $this->max_id . + '&q=' . $this->query; + + // pagination stuff + + if ($cnt > $this->rpp) { + $this->next_page = '?page=' . ($this->page + 1) . + '&max_id=' . $this->max_id; + if ($this->rpp != 15) { + $this->next_page .= '&rpp=' . $this->rpp; + } + $this->next_page .= '&q=' . $this->query; + } + + if ($this->page > 1) { + $this->previous_page = '?page=' . ($this->page - 1) . + '&max_id=' . $this->max_id; + if ($this->rpp != 15) { + $this->previous_page .= '&rpp=' . $this->rpp; + } + $this->previous_page .= '&q=' . $this->query; + } + + print json_encode($this); + + return $cnt; + } +} + +/** + * widget for displaying a single JSON search result + * + * @category UI + * @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/ + * @see JSONSearchResultsList + */ + +class ResultItem +{ + /** The notice this item is based on. */ + + protected $notice; // protected attrs invisible to json_encode() + + /** The profile associated with the notice. */ + + protected $profile; + + // The below attributes are carefully named so the JSON output from + // this obj matches the output from search.twitter.com + + var $text; + var $to_user_id; + var $to_user; + var $from_user; + var $id; + var $from_user_id; + var $iso_language_code; + var $source; + var $profile_image_url; + var $created_at; + + /** + * constructor + * + * Also initializes the profile attribute. + * + * @param Notice $notice The notice we'll display + */ + + function __construct($notice) + { + $this->notice = $notice; + $this->profile = $notice->getProfile(); + $this->buildResult(); + } + + /** + * Build a search result object + * + * This populates the the result in preparation for JSON encoding. + * + * @return void + */ + + function buildResult() + { + $this->text = $this->notice->content; + $replier_profile = null; + + if ($this->notice->reply_to) { + $reply = Notice::staticGet(intval($notice->reply_to)); + if ($reply) { + $replier_profile = $reply->getProfile(); + } + } + + $this->to_user_id = ($replier_profile) ? + intval($replier_profile->id) : null; + $this->to_user = ($replier_profile) ? + $replier_profile->nickname : null; + + $this->from_user = $this->profile->nickname; + $this->id = $this->notice->id; + $this->from_user_id = $this->profile->id; + + $user = User::staticGet('id', $this->profile->id); + + $this->iso_language_code = $this->user->language; + + $this->source = $this->getSourceLink($this->notice->source); + + $avatar = $this->profile->getAvatar(AVATAR_STREAM_SIZE); + + $this->profile_image_url = ($avatar) ? + $avatar->displayUrl() : Avatar::defaultImage(AVATAR_STREAM_SIZE); + + $this->created_at = common_date_rfc2822($this->notice->created); + } + + /** + * Show the source of the notice + * + * 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 Notice + * + * @return string a fully rendered source of the Notice + */ + + function getSourceLink($source) + { + $source_name = _($source); + switch ($source) { + case 'web': + case 'xmpp': + case 'mail': + case 'omb': + case 'api': + break; + default: + $ns = Notice_source::staticGet($source); + if ($ns) { + $source_name = '<a href="' . $ns->url . '">' . $ns->name . '</a>'; + } + break; + } + + return $source_name; + } + +} diff --git a/lib/language.php b/lib/language.php index a73b73f28..cd6498d30 100644 --- a/lib/language.php +++ b/lib/language.php @@ -94,40 +94,43 @@ function get_nice_language_list() * Get a list of all languages that are enabled in the default config * * This should ONLY be called when setting up the default config in common.php. - * Any other attempt to get a list of lanugages should instead call + * Any other attempt to get a list of languages should instead call * common_config('site','languages') * * @return array mapping of language codes to language info */ function get_all_languages() { return array( - 'bg' => array('q' => 0.8, 'lang' => 'bg_BG', 'name' => 'Bulgarian', 'direction' => 'ltr'), - 'ca' => array('q' => 0.5, 'lang' => 'ca_ES', 'name' => 'Catalan', 'direction' => 'ltr'), - 'cs' => array('q' => 0.5, 'lang' => 'cs_CZ', 'name' => 'Czech', 'direction' => 'ltr'), - 'de' => array('q' => 0.5, 'lang' => 'de_DE', 'name' => 'German', 'direction' => 'ltr'), - 'el' => array('q' => 0.1, 'lang' => 'el', 'name' => 'Greek', 'direction' => 'ltr'), - 'en-us' => array('q' => 1, 'lang' => 'en_US', 'name' => 'English (US)', 'direction' => 'ltr'), - 'en-gb' => array('q' => 0.3, 'lang' => 'en_GB', 'name' => 'English (British)', 'direction' => 'ltr'), - 'en' => array('q' => 1, 'lang' => 'en', 'name' => 'English', 'direction' => 'ltr'), - 'es' => array('q' => 0.5, 'lang' => 'es', 'name' => 'Spanish', 'direction' => 'ltr'), - 'fr-fr' => array('q' => 0.2, 'lang' => 'fr_FR', 'name' => 'French', 'direction' => 'ltr'), - 'he' => array('q' => 0.5, 'lang' => 'he_IL', 'name' => 'Hebrew', 'direction' => 'ltr'), - 'it' => array('q' => 0.9, 'lang' => 'it_IT', 'name' => 'Italian', 'direction' => 'rtl'), - 'jp' => array('q' => 0.5, 'lang' => 'ja_JP', 'name' => 'Japanese', 'direction' => 'ltr'), -# 'ko' => array('q' => 0, 'lang' => 'ko', 'name' => 'Korean', 'direction' => 'ltr'), - 'mk' => array('q' => 0.5, 'lang' => 'mk_MK', 'name' => 'Macedonian', 'direction' => 'ltr'), - 'nb' => array('q' => 0.1, 'lang' => 'nb_NO', 'name' => 'Norwegian (bokmal)', 'direction' => 'ltr'), - 'nl' => array('q' => 0.5, 'lang' => 'nl_NL', 'name' => 'Dutch', 'direction' => 'ltr'), - 'pl' => array('q' => 0.5, 'lang' => 'pl_PL', 'name' => 'Polish', 'direction' => 'ltr'), -# 'pt' => array('q' => 0, 'lang' => 'pt', 'name' => 'Portuguese', 'direction' => 'ltr'), - 'pt-br' => array('q' => 0.7, 'lang' => 'pt_BR', 'name' => 'Portuguese Brazil', 'direction' => 'ltr'), - 'ru' => array('q' => 0.1, 'lang' => 'ru_RU', 'name' => 'Russian', 'direction' => 'ltr'), - 'sv' => array('q' => 0.9, 'lang' => 'sv_SE', 'name' => 'Swedish', 'direction' => 'ltr'), - 'te' => array('q' => 0.3, 'lang' => 'te_IN', 'name' => 'Telugu', 'direction' => 'ltr'), - 'tr' => array('q' => 0.5, 'lang' => 'tr_TR', 'name' => 'Turkish', 'direction' => 'ltr'), - 'uk' => array('q' => 0.7, 'lang' => 'uk_UA', 'name' => 'Ukrainian', 'direction' => 'ltr'), - 'vi' => array('q' => 0.7, 'lang' => 'vi_VN', 'name' => 'Vietnamese', 'direction' => 'ltr'), - 'zh-cn' => array('q' => 0.9, 'lang' => 'zh_CN', 'name' => 'Chinese (Simplified)', 'direction' => 'ltr'), - 'zh-hant' => array('q' => 0.2, 'lang' => 'zh_hant', 'name' => 'Chinese (Taiwanese)', 'direction' => 'ltr'), + 'bg' => array('q' => 0.8, 'lang' => 'bg_BG', 'name' => 'Bulgarian', 'direction' => 'ltr'), + 'ca' => array('q' => 0.5, 'lang' => 'ca_ES', 'name' => 'Catalan', 'direction' => 'ltr'), + 'cs' => array('q' => 0.5, 'lang' => 'cs_CZ', 'name' => 'Czech', 'direction' => 'ltr'), + 'de' => array('q' => 0.8, 'lang' => 'de_DE', 'name' => 'German', 'direction' => 'ltr'), + 'el' => array('q' => 0.1, 'lang' => 'el', 'name' => 'Greek', 'direction' => 'ltr'), + 'en-us' => array('q' => 1, 'lang' => 'en_US', 'name' => 'English (US)', 'direction' => 'ltr'), + 'en-gb' => array('q' => 1, 'lang' => 'en_GB', 'name' => 'English (British)', 'direction' => 'ltr'), + 'en' => array('q' => 1, 'lang' => 'en', 'name' => 'English', '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'), + 'he' => array('q' => 0.5, 'lang' => 'he_IL', 'name' => 'Hebrew', 'direction' => 'rtl'), + 'it' => array('q' => 1, 'lang' => 'it_IT', 'name' => 'Italian', 'direction' => 'ltr'), + 'jp' => array('q' => 0.5, 'lang' => 'ja_JP', 'name' => 'Japanese', 'direction' => 'ltr'), + 'ko' => array('q' => 0.9, 'lang' => 'ko_KR', 'name' => 'Korean', 'direction' => 'ltr'), + 'mk' => array('q' => 0.5, 'lang' => 'mk_MK', 'name' => 'Macedonian', 'direction' => 'ltr'), + 'nb' => array('q' => 0.1, 'lang' => 'nb_NO', 'name' => 'Norwegian (BokmÃ¥l)', 'direction' => 'ltr'), + 'no' => array('q' => 0.1, 'lang' => 'nb_NO', 'name' => 'Norwegian (BokmÃ¥l)', 'direction' => 'ltr'), + 'nn' => array('q' => 1, 'lang' => 'nn_NO', 'name' => 'Norwegian (Nynorsk)', 'direction' => 'ltr'), + 'nl' => array('q' => 0.5, 'lang' => 'nl_NL', 'name' => 'Dutch', 'direction' => 'ltr'), + 'pl' => array('q' => 0.5, 'lang' => 'pl_PL', 'name' => 'Polish', 'direction' => 'ltr'), + 'pt' => array('q' => 0.1, 'lang' => 'pt', 'name' => 'Portuguese', 'direction' => 'ltr'), + 'pt-br' => array('q' => 0.9, 'lang' => 'pt_BR', 'name' => 'Portuguese Brazil', 'direction' => 'ltr'), + 'ru' => array('q' => 0.9, 'lang' => 'ru_RU', 'name' => 'Russian', 'direction' => 'ltr'), + 'sv' => array('q' => 0.8, 'lang' => 'sv_SE', 'name' => 'Swedish', 'direction' => 'ltr'), + 'te' => array('q' => 0.3, 'lang' => 'te_IN', 'name' => 'Telugu', 'direction' => 'ltr'), + 'tr' => array('q' => 0.5, 'lang' => 'tr_TR', 'name' => 'Turkish', 'direction' => 'ltr'), + 'uk' => array('q' => 1, 'lang' => 'uk_UA', 'name' => 'Ukrainian', 'direction' => 'ltr'), + 'vi' => array('q' => 0.8, 'lang' => 'vi_VN', 'name' => 'Vietnamese', 'direction' => 'ltr'), + 'zh-cn' => array('q' => 0.9, 'lang' => 'zh_CN', 'name' => 'Chinese (Simplified)', 'direction' => 'ltr'), + 'zh-hant' => array('q' => 0.2, 'lang' => 'zh_TW', 'name' => 'Chinese (Taiwanese)', 'direction' => 'ltr'), ); } diff --git a/lib/logingroupnav.php b/lib/logingroupnav.php index fd909581f..f23985f3a 100644 --- a/lib/logingroupnav.php +++ b/lib/logingroupnav.php @@ -70,16 +70,16 @@ class LoginGroupNav extends Widget function show() { // action => array('prompt', 'title') - $menu = - array('login' => - array(_('Login'), - _('Login with a username and password')), - 'register' => - array(_('Register'), - _('Sign up for a new account')), - 'openidlogin' => - array(_('OpenID'), - _('Login or register with OpenID'))); + $menu = array(); + + $menu['login'] = array(_('Login'), + _('Login with a username and password')); + if (!(common_config('site','closed') || common_config('site','inviteonly'))) { + $menu['register'] = array(_('Register'), + _('Sign up for a new account')); + } + $menu['openidlogin'] = array(_('OpenID'), + _('Login or register with OpenID')); $action_name = $this->action->trimmed('action'); $this->action->elementStart('ul', array('class' => 'nav')); diff --git a/lib/mail.php b/lib/mail.php index 9fa86de5c..27a1d99dc 100644 --- a/lib/mail.php +++ b/lib/mail.php @@ -50,10 +50,9 @@ function mail_backend() static $backend = null; if (!$backend) { - global $config; - $backend = Mail::factory($config['mail']['backend'], - ($config['mail']['params']) ? - $config['mail']['params'] : + $backend = Mail::factory(common_config('mail', 'backend'), + (common_config('mail', 'params')) ? + common_config('mail', 'params') : array()); if (PEAR::isError($backend)) { common_server_error($backend->getMessage(), 500); @@ -555,17 +554,19 @@ function mail_notify_fave($other, $user, $notice) $body = sprintf(_("%1\$s just added your notice from %2\$s". " as one of their favorites.\n\n" . - "In case you forgot, you can see the text". - " of your notice here:\n\n" . + "The URL of your notice is:\n\n" . "%3\$s\n\n" . - "You can see the list of %1\$s's favorites here:\n\n" . + "The text of your notice is:\n\n" . "%4\$s\n\n" . + "You can see the list of %1\$s's favorites here:\n\n" . + "%5\$s\n\n" . "Faithfully yours,\n" . - "%5\$s\n"), + "%6\$s\n"), $bestname, common_exact_date($notice->created), common_local_url('shownotice', array('notice' => $notice->id)), + $notice->content, common_local_url('showfavorites', array('nickname' => $user->nickname)), common_config('site', 'name')); diff --git a/lib/mailbox.php b/lib/mailbox.php index d77234549..01bbf5721 100644 --- a/lib/mailbox.php +++ b/lib/mailbox.php @@ -137,6 +137,9 @@ class MailboxAction extends PersonalAction $message->free(); unset($message); } + else { + $this->element('p', 'guide', _('You have no private messages. You can send private message to engage other users in conversation. People can send you messages for your eyes only.')); + } } function getMessages() diff --git a/lib/messageform.php b/lib/messageform.php index f41508305..b8878ec1f 100644 --- a/lib/messageform.php +++ b/lib/messageform.php @@ -132,20 +132,14 @@ class MessageForm extends Form $mutual_users->free(); unset($mutual_users); - $this->out->elementStart('ul', 'form_data'); - $this->out->elementStart('li', array('id' => 'notice_to')); $this->out->dropdown('to', _('To'), $mutual, null, false, ($this->to) ? $this->to->id : null); - $this->out->elementEnd('li'); - $this->out->elementStart('li', array('id' => 'notice_text')); $this->out->element('textarea', array('id' => 'notice_data-text', 'cols' => 35, 'rows' => 4, 'name' => 'content'), ($this->content) ? $this->content : ''); - $this->out->elementEnd('li'); - $this->out->elementEnd('ul'); } /** @@ -156,14 +150,10 @@ class MessageForm extends Form function formActions() { - $this->out->elementStart('ul', 'form_actions'); - $this->out->elementStart('li', array('id' => 'notice_submit')); $this->out->element('input', array('id' => 'notice_action-submit', 'class' => 'submit', 'name' => 'message_send', 'type' => 'submit', 'value' => _('Send'))); - $this->out->elementEnd('li'); - $this->out->elementEnd('ul'); } } diff --git a/lib/noticeform.php b/lib/noticeform.php index 0c991c969..606b5d028 100644 --- a/lib/noticeform.php +++ b/lib/noticeform.php @@ -134,9 +134,6 @@ class NoticeForm extends Form function formData() { - - $this->out->elementStart('ul', 'form_data'); - $this->out->elementStart('li', array('id' => 'notice_text')); $this->out->element('label', array('for' => 'notice_data-text'), sprintf(_('What\'s up, %s?'), $this->user->nickname)); // XXX: vary by defined max size @@ -145,8 +142,6 @@ class NoticeForm extends Form 'rows' => 4, 'name' => 'status_textarea'), ($this->content) ? $this->content : ''); - $this->out->elementEnd('li'); - $this->out->elementEnd('ul'); $this->out->elementStart('dl', 'form_note'); $this->out->element('dt', null, _('Available characters')); @@ -168,14 +163,10 @@ class NoticeForm extends Form function formActions() { - $this->out->elementStart('ul', 'form_actions'); - $this->out->elementStart('li', array('id' => 'notice_submit')); $this->out->element('input', array('id' => 'notice_action-submit', 'class' => 'submit', 'name' => 'status_submit', 'type' => 'submit', 'value' => _('Send'))); - $this->out->elementEnd('li'); - $this->out->elementEnd('ul'); } } diff --git a/lib/noticelist.php b/lib/noticelist.php index 9fc0126b3..004905056 100644 --- a/lib/noticelist.php +++ b/lib/noticelist.php @@ -179,25 +179,89 @@ class NoticeListItem extends Widget { $this->showStart(); $this->showNotice(); - $this->showNoticeInfo(); + $this->showNoticeAttachments(); $this->showNoticeOptions(); + $this->showNoticeInfo(); $this->showEnd(); } function showNotice() { - $this->out->elementStart('div', 'entry-title'); +if(0) + $this->out->elementStart('entry-title'); +else + + if ('shownotice' === $this->out->args['action']) { + $width = '85%'; + } else { + $width = '90%'; + } + + + $this->out->elementStart('div', array('class' => 'entry-title', 'style' => "float: left; width: $width;")); $this->showAuthor(); $this->showContent(); $this->out->elementEnd('div'); } + function showNoticeAttachments() + { + $f2p = new File_to_post; + $f2p->post_id = $this->notice->id; + $file = new File; + $file->joinAdd($f2p); + $file->selectAdd(); + $file->selectAdd('file.id as id'); + $count = $file->find(true); + if (!$count) return; + if (1 === $count) { + $href = common_local_url('attachment', array('attachment' => $file->id)); + $att_class = 'attachment'; + } else { + $href = common_local_url('attachments', array('notice' => $this->notice->id)); + $att_class = 'attachments'; + } + + $clip = theme_path('images/icons/clip', 'base'); + if ('shownotice' === $this->out->args['action']) { + $height = '96px'; + $width = '83%'; + $width_att = '15%'; + $clip .= '-big.png'; + $top = '70px'; + } else { + $height = '48px'; + $width = '90%'; + $width_att = '8%'; + $clip .= '.png'; + $top = '20px'; + } +if(0) + $this->out->elementStart('div', 'entry-attachments'); +else + $this->out->elementStart('p', array('class' => 'entry-attachments', 'style' => "float: right; width: $width_att; background: url($clip) no-repeat; text-align: right; height: $height;")); + $this->out->element('a', array('class' => $att_class, 'style' => "text-decoration: none; padding-top: $top; display: block; height: $height;", 'href' => $href, 'title' => "# of attachments: $count"), $count === 1 ? '' : $count); + + + $this->out->elementEnd('p'); + } + function showNoticeInfo() { +if(0) $this->out->elementStart('div', 'entry-content'); +else + + if ('shownotice' === $this->out->args['action']) { + $width = '85%'; + } else { + $width = '90%'; + } + + $this->out->elementStart('div', array('class' => 'entry-content', 'style' => "float: left; width: $width;")); $this->showNoticeLink(); $this->showNoticeSource(); - $this->showReplyTo(); + $this->showContext(); $this->out->elementEnd('div'); } @@ -205,7 +269,10 @@ class NoticeListItem extends Widget { $user = common_current_user(); if ($user) { +if(0) $this->out->elementStart('div', 'notice-options'); +else + $this->out->elementStart('div', array('class' => 'notice-options', 'style' => 'float: right; width: 16%;')); $this->showFaveForm(); $this->showReplyLink(); $this->showDeleteLink(); @@ -258,8 +325,12 @@ class NoticeListItem extends Widget function showAuthor() { $this->out->elementStart('span', 'vcard author'); - $this->out->elementStart('a', array('href' => $this->profile->profileurl, - 'class' => 'url')); + $attrs = array('href' => $this->profile->profileurl, + 'class' => 'url'); + if (!empty($this->profile->fullname)) { + $attrs['title'] = $this->profile->fullname . ' (' . $this->profile->nickname . ') '; + } + $this->out->elementStart('a', $attrs); $this->showAvatar(); $this->showNickname(); $this->out->elementEnd('a'); @@ -387,6 +458,7 @@ class NoticeListItem extends Widget case 'xmpp': case 'mail': case 'omb': + case 'system': case 'api': $this->out->element('dd', null, $source_name); break; @@ -416,17 +488,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), + _('in context')); $this->out->elementEnd('dd'); $this->out->elementEnd('dl'); } diff --git a/lib/noticesection.php b/lib/noticesection.php index 97b517529..37aafdaf6 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'); - while ($notices->fetch() && ++$cnt <= NOTICES_PER_SECTION) { $this->showNotice($notices); } $this->out->elementEnd('ul'); - return ($cnt > NOTICES_PER_SECTION); } @@ -73,6 +69,11 @@ class NoticeSection extends Section function showNotice($notice) { $profile = $notice->getProfile(); + if (empty($profile)) { + common_log(LOG_WARNING, sprintf("Notice %d has no profile", + $notice->id)); + return; + } $this->out->elementStart('li', 'hentry notice'); $this->out->elementStart('div', 'entry-title'); $avatar = $profile->getAvatar(AVATAR_MINI_SIZE); @@ -95,8 +96,39 @@ 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 ($notice->value) { + if (!empty($notice->value)) { $this->out->elementStart('p'); $this->out->text($notice->value); $this->out->elementEnd('p'); diff --git a/lib/oauthstore.php b/lib/oauthstore.php index 7ad3be20e..183164e17 100644 --- a/lib/oauthstore.php +++ b/lib/oauthstore.php @@ -54,16 +54,21 @@ class LaconicaOAuthDataStore extends OAuthDataStore } } + // http://oauth.net/core/1.0/#nonce + // "The Consumer SHALL then generate a Nonce value that is unique for + // all requests with that timestamp." + + // XXX: It's not clear why the token is here + function lookup_nonce($consumer, $token, $nonce, $timestamp) { $n = new Nonce(); $n->consumer_key = $consumer->key; - $n->tok = $token->key; + $n->ts = $timestamp; $n->nonce = $nonce; if ($n->find(true)) { return true; } else { - $n->timestamp = $timestamp; $n->created = DB_DataObject_Cast::dateTime(); $n->insert(); return false; diff --git a/lib/omb.php b/lib/omb.php index f2dbef5ba..e8e1acc41 100644 --- a/lib/omb.php +++ b/lib/omb.php @@ -206,7 +206,7 @@ function omb_post_notice_keys($notice, $postnoticeurl, $tk, $secret) $result = $fetcher->post($req->get_normalized_http_url(), $req->to_postdata(), - array('User-Agent' => 'Laconica/' . LACONICA_VERSION)); + array('User-Agent: Laconica/' . LACONICA_VERSION)); common_debug('Got HTTP result "'.print_r($result,true).'"', __FILE__); @@ -239,7 +239,7 @@ function omb_broadcast_profile($profile) while ($sub->fetch()) { $rp = Remote_profile::staticGet('id', $sub->subscriber); if ($rp) { - if (!$updated[$rp->updateprofileurl]) { + if (!array_key_exists($rp->updateprofileurl, $updated)) { if (omb_update_profile($profile, $rp, $sub)) { $updated[$rp->updateprofileurl] = true; } @@ -251,7 +251,6 @@ function omb_broadcast_profile($profile) function omb_update_profile($profile, $remote_profile, $subscription) { - global $config; # for license URL $user = User::staticGet($profile->id); $con = omb_oauth_consumer(); $token = new OAuthToken($subscription->token, $subscription->secret); @@ -291,11 +290,13 @@ function omb_update_profile($profile, $remote_profile, $subscription) common_debug('postdata = '.$req->to_postdata(), __FILE__); $result = $fetcher->post($req->get_normalized_http_url(), $req->to_postdata(), - array('User-Agent' => 'Laconica/' . LACONICA_VERSION)); + array('User-Agent: Laconica/' . LACONICA_VERSION)); common_debug('Got HTTP result "'.print_r($result,true).'"', __FILE__); - if ($result->status == 403) { # not authorized, don't send again + if (empty($result) || !$result) { + common_debug("Unable to contact " . $req->get_normalized_http_url()); + } else if ($result->status == 403) { # not authorized, don't send again common_debug('403 result, deleting subscription', __FILE__); $subscription->delete(); return false; @@ -304,7 +305,7 @@ function omb_update_profile($profile, $remote_profile, $subscription) return false; } else { # success! parse_str($result->body, $return); - if ($return['omb_version'] == OMB_VERSION_01) { + if (isset($return['omb_version']) && $return['omb_version'] === OMB_VERSION_01) { return true; } else { return false; diff --git a/lib/openid.php b/lib/openid.php index 860573702..3af7a39cf 100644 --- a/lib/openid.php +++ b/lib/openid.php @@ -64,6 +64,9 @@ function oid_set_last($openid_url) function oid_get_last() { + if (empty($_COOKIE[OPENID_COOKIE_KEY])) { + return null; + } $openid_url = $_COOKIE[OPENID_COOKIE_KEY]; if ($openid_url && strlen($openid_url) > 0) { return $openid_url; @@ -157,7 +160,7 @@ function oid_authenticate($openid_url, $returnto, $immediate=false) $auth_request->addExtension($sreg_request); } - $trust_root = common_local_url('public'); + $trust_root = common_root_url(true); $process_url = common_local_url($returnto); if ($auth_request->shouldSendRedirect()) { @@ -168,7 +171,7 @@ function oid_authenticate($openid_url, $returnto, $immediate=false) } else if (Auth_OpenID::isFailure($redirect_url)) { return sprintf(_('Could not redirect to server: %s'), $redirect_url->message); } else { - common_redirect($redirect_url); + common_redirect($redirect_url, 303); } } else { // Generate form markup and render it. diff --git a/lib/peoplesearchresults.php b/lib/peoplesearchresults.php new file mode 100644 index 000000000..d3f840852 --- /dev/null +++ b/lib/peoplesearchresults.php @@ -0,0 +1,75 @@ +<?php +/** + * People search results class + * + * PHP version 5 + * + * @category Widget + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @author Robin Millette <millette@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://laconi.ca/ + * + * Laconica - a distributed open-source microblogging tool + * Copyright (C) 2008, Controlez-Vous, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +if (!defined('LACONICA')) { + exit(1); +} + +require_once INSTALLDIR.'/lib/profilelist.php'; + +/** + * People search results class + * + * Derivative of ProfileList with specialization for highlighting search terms. + * + * @category Widget + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @author Robin Millette <millette@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://laconi.ca/ + * + * @see PeoplesearchAction + */ + +class PeopleSearchResults extends ProfileList +{ + var $terms = null; + var $pattern = null; + + function __construct($profile, $terms, $action) + { + parent::__construct($profile, $terms, $action); + $this->terms = array_map('preg_quote', + array_map('htmlspecialchars', $terms)); + $this->pattern = '/('.implode('|',$terms).')/i'; + } + + function highlight($text) + { + return preg_replace($this->pattern, '<strong>\\1</strong>', htmlspecialchars($text)); + } + + function isReadOnly($args) + { + return true; + } +} + diff --git a/lib/personal.php b/lib/personal.php index e46350c63..f92732375 100644 --- a/lib/personal.php +++ b/lib/personal.php @@ -47,7 +47,7 @@ class PersonalAction extends Action var $user = null; - function isReadOnly() + function isReadOnly($args) { return true; } diff --git a/lib/personalgroupnav.php b/lib/personalgroupnav.php index 63e6138df..acc033667 100644 --- a/lib/personalgroupnav.php +++ b/lib/personalgroupnav.php @@ -93,43 +93,45 @@ class PersonalGroupNav extends Widget $this->out->elementStart('ul', array('class' => 'nav')); - $this->out->menuItem(common_local_url('all', array('nickname' => - $nickname)), - _('Personal'), - sprintf(_('%s and friends'), (($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname)), - $action == 'all', 'nav_timeline_personal'); - $this->out->menuItem(common_local_url('replies', array('nickname' => - $nickname)), - _('Replies'), - sprintf(_('Replies to %s'), (($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname)), - $action == 'replies', 'nav_timeline_replies'); - $this->out->menuItem(common_local_url('showstream', array('nickname' => - $nickname)), - _('Profile'), - ($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname, - $action == 'showstream', 'nav_profile'); - $this->out->menuItem(common_local_url('showfavorites', array('nickname' => - $nickname)), - _('Favorites'), - sprintf(_('%s\'s favorite notices'), ($user_profile) ? $user_profile->getBestName() : _('User')), - $action == 'showfavorites', 'nav_timeline_favorites'); + if (Event::handle('StartPersonalGroupNav', array($this))) { + $this->out->menuItem(common_local_url('all', array('nickname' => + $nickname)), + _('Personal'), + sprintf(_('%s and friends'), (($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname)), + $action == 'all', 'nav_timeline_personal'); + $this->out->menuItem(common_local_url('replies', array('nickname' => + $nickname)), + _('Replies'), + sprintf(_('Replies to %s'), (($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname)), + $action == 'replies', 'nav_timeline_replies'); + $this->out->menuItem(common_local_url('showstream', array('nickname' => + $nickname)), + _('Profile'), + ($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname, + $action == 'showstream', 'nav_profile'); + $this->out->menuItem(common_local_url('showfavorites', array('nickname' => + $nickname)), + _('Favorites'), + sprintf(_('%s\'s favorite notices'), ($user_profile) ? $user_profile->getBestName() : _('User')), + $action == 'showfavorites', 'nav_timeline_favorites'); - $cur = common_current_user(); + $cur = common_current_user(); - if ($cur && $cur->id == $user->id) { + if ($cur && $cur->id == $user->id) { - $this->out->menuItem(common_local_url('inbox', array('nickname' => - $nickname)), - _('Inbox'), - _('Your incoming messages'), - $action == 'inbox'); - $this->out->menuItem(common_local_url('outbox', array('nickname' => - $nickname)), - _('Outbox'), - _('Your sent messages'), - $action == 'outbox'); + $this->out->menuItem(common_local_url('inbox', array('nickname' => + $nickname)), + _('Inbox'), + _('Your incoming messages'), + $action == 'inbox'); + $this->out->menuItem(common_local_url('outbox', array('nickname' => + $nickname)), + _('Outbox'), + _('Your sent messages'), + $action == 'outbox'); + } + Event::handle('EndPersonalGroupNav', array($this)); } - $this->out->elementEnd('ul'); } } diff --git a/lib/personaltagcloudsection.php b/lib/personaltagcloudsection.php index 0882822db..978153a84 100644 --- a/lib/personaltagcloudsection.php +++ b/lib/personaltagcloudsection.php @@ -58,8 +58,14 @@ class PersonalTagCloudSection extends TagCloudSection function getTags() { - $qry = 'SELECT notice_tag.tag, '. - 'sum(exp(-(now() - notice_tag.created)/%s)) as weight ' . + if (common_config('db', 'type') == 'pgsql') { + $weightexpr='sum(exp(-extract(epoch from (now() - notice_tag.created)) / %s))'; + } else { + $weightexpr='sum(exp(-(now() - notice_tag.created) / %s))'; + } + + $qry = 'SELECT notice_tag.tag, '. + $weightexpr . ' as weight ' . 'FROM notice_tag JOIN notice ' . 'ON notice_tag.notice_id = notice.id ' . 'WHERE notice.profile_id = %d ' . diff --git a/lib/ping.php b/lib/ping.php new file mode 100644 index 000000000..3de541e9a --- /dev/null +++ b/lib/ping.php @@ -0,0 +1,122 @@ +<?php +/* + * Laconica - a distributed open-source microblogging tool + * Copyright (C) 2009, Control Yourself, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * 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); } + +function ping_broadcast_notice($notice) { + + if (!$notice->is_local) { + return true; + } + + # Array of servers, URL => type + $notify = common_config('ping', 'notify'); + $profile = $notice->getProfile(); + $tags = ping_notice_tags($notice); + + foreach ($notify as $notify_url => $type) { + switch ($type) { + case 'xmlrpc': + case 'extended': + $req = xmlrpc_encode_request('weblogUpdates.ping', + array($profile->nickname, # site name + common_local_url('showstream', + array('nickname' => $profile->nickname)), + common_local_url('shownotice', + array('notice' => $notice->id)), + common_local_url('userrss', + array('nickname' => $profile->nickname)), + $tags)); + + $context = stream_context_create(array('http' => array('method' => "POST", + 'header' => + "Content-Type: text/xml\r\n". + "User-Agent: Laconica/".LACONICA_VERSION."\r\n", + 'content' => $req))); + $file = file_get_contents($notify_url, false, $context); + + if ($file === false || mb_strlen($file) == 0) { + common_log(LOG_WARNING, + "XML-RPC empty results for ping ($notify_url, $notice->id) "); + continue; + } + + $response = xmlrpc_decode($file); + + if (xmlrpc_is_fault($response)) { + common_log(LOG_WARNING, + "XML-RPC error for ping ($notify_url, $notice->id) ". + "$response[faultString] ($response[faultCode])"); + } else { + common_log(LOG_INFO, + "Ping success for $notify_url $notice->id"); + } + break; + + case 'get': + case 'post': + $args = array('name' => $profile->nickname, + 'url' => common_local_url('showstream', + array('nickname' => $profile->nickname)), + 'changesURL' => common_local_url('userrss', + array('nickname' => $profile->nickname))); + + $fetcher = Auth_Yadis_Yadis::getHTTPFetcher(); + + if ($type === 'get') { + $result = $fetcher->get($notify_url . '?' . http_build_query($args), + array('User-Agent: Laconica/'.LACONICA_VERSION)); + } else { + $result = $fetcher->post($notify_url, + http_build_query($args), + array('User-Agent: Laconica/'.LACONICA_VERSION)); + } + if ($result->status != '200') { + common_log(LOG_WARNING, + "Ping error for '$notify_url' ($notice->id): ". + "$result->body"); + } else { + common_log(LOG_INFO, + "Ping success for '$notify_url' ($notice->id): ". + "'$result->body'"); + } + break; + + default: + common_log(LOG_WARNING, 'Unknown notify type for ' . $notify_url . ': ' . $type); + } + } + + return true; +} + +function ping_notice_tags($notice) { + $tag = new Notice_tag(); + $tag->notice_id = $notice->id; + $tags = array(); + if ($tag->find()) { + while ($tag->fetch()) { + $tags[] = $tag->tag; + } + $tag->free(); + unset($tag); + return implode('|', $tags); + } + return NULL; +}
\ No newline at end of file diff --git a/lib/popularnoticesection.php b/lib/popularnoticesection.php index 5380563b9..375d5538b 100644 --- a/lib/popularnoticesection.php +++ b/lib/popularnoticesection.php @@ -31,8 +31,6 @@ if (!defined('LACONICA')) { exit(1); } -define('NOTICES_PER_SECTION', 5); - /** * Base class for sections showing lists of notices * @@ -50,11 +48,28 @@ class PopularNoticeSection extends NoticeSection { function getNotices() { - $qry = 'SELECT notice.*, '. - 'sum(exp(-(now() - fave.modified) / %s)) as weight ' . - 'FROM notice JOIN fave ON notice.id = fave.notice_id ' . - 'GROUP BY fave.notice_id ' . - 'ORDER BY weight DESC'; + 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->out->tag); + } + } else { + $weightexpr='sum(exp(-(now() - fave.modified) / %s))'; + if (!empty($this->out->tag)) { + $tag = mysql_escape_string($this->out->tag); + } + } + $qry = "SELECT notice.*, $weightexpr as weight "; + if(isset($tag)) { + $qry .= 'FROM notice_tag, notice JOIN fave ON notice.id = fave.notice_id ' . + "WHERE notice.id = notice_tag.notice_id and '$tag' = notice_tag.tag"; + } else { + $qry .= 'FROM notice JOIN fave ON notice.id = fave.notice_id'; + } + $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 ' . + 'ORDER BY weight DESC'; $offset = 0; $limit = NOTICES_PER_SECTION + 1; diff --git a/lib/profileaction.php b/lib/profileaction.php new file mode 100644 index 000000000..a3437ff4d --- /dev/null +++ b/lib/profileaction.php @@ -0,0 +1,248 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Common parent of Personal and Profile actions + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Personal + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @author Sarven Capadisli <csarven@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/profileminilist.php'; +require_once INSTALLDIR.'/lib/groupminilist.php'; + +/** + * Profile action common superclass + * + * Abstracts out common code from profile and personal tabs + * + * @category Personal + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +class ProfileAction extends Action +{ + var $user = null; + var $page = null; + var $profile = null; + var $tag = null; + + function prepare($args) + { + parent::prepare($args); + + $nickname_arg = $this->arg('nickname'); + $nickname = common_canonical_nickname($nickname_arg); + + // Permanent redirect on non-canonical nickname + + if ($nickname_arg != $nickname) { + $args = array('nickname' => $nickname); + if ($this->arg('page') && $this->arg('page') != 1) { + $args['page'] = $this->arg['page']; + } + common_redirect(common_local_url($this->trimmed('action'), $args), 301); + return false; + } + + $this->user = User::staticGet('nickname', $nickname); + + if (!$this->user) { + $this->clientError(_('No such user.'), 404); + return false; + } + + $this->profile = $this->user->getProfile(); + + if (!$this->profile) { + $this->serverError(_('User has no profile.')); + return false; + } + + $this->tag = $this->trimmed('tag'); + $this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1; + common_set_returnto($this->selfUrl()); + return true; + } + + function showSections() + { + $this->showSubscriptions(); + $this->showSubscribers(); + $this->showGroups(); + $this->showStatistics(); + } + + function showSubscriptions() + { + $profile = $this->user->getSubscriptions(0, PROFILES_PER_MINILIST + 1); + + $this->elementStart('div', array('id' => 'entity_subscriptions', + 'class' => 'section')); + + $this->element('h2', null, _('Subscriptions')); + + if ($profile) { + $pml = new ProfileMiniList($profile, $this->user, $this); + $cnt = $pml->show(); + if ($cnt == 0) { + $this->element('p', null, _('(None)')); + } + } + + if ($cnt > PROFILES_PER_MINILIST) { + $this->elementStart('p'); + $this->element('a', array('href' => common_local_url('subscriptions', + array('nickname' => $this->profile->nickname)), + 'class' => 'more'), + _('All subscriptions')); + $this->elementEnd('p'); + } + + $this->elementEnd('div'); + } + + function showSubscribers() + { + $profile = $this->user->getSubscribers(0, PROFILES_PER_MINILIST + 1); + + $this->elementStart('div', array('id' => 'entity_subscribers', + 'class' => 'section')); + + $this->element('h2', null, _('Subscribers')); + + if ($profile) { + $pml = new ProfileMiniList($profile, $this->user, $this); + $cnt = $pml->show(); + if ($cnt == 0) { + $this->element('p', null, _('(None)')); + } + } + + if ($cnt > PROFILES_PER_MINILIST) { + $this->elementStart('p'); + $this->element('a', array('href' => common_local_url('subscribers', + array('nickname' => $this->profile->nickname)), + 'class' => 'more'), + _('All subscribers')); + $this->elementEnd('p'); + } + + $this->elementEnd('div'); + } + + 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(); + + $this->elementStart('div', array('id' => 'entity_statistics', + 'class' => 'section')); + + $this->element('h2', null, _('Statistics')); + + // Other stats...? + $this->elementStart('dl', 'entity_user-id'); + $this->element('dt', null, _('User ID')); + $this->element('dd', null, $this->profile->id); + $this->elementEnd('dl'); + + $this->elementStart('dl', 'entity_member-since'); + $this->element('dt', null, _('Member since')); + $this->element('dd', null, date('j M Y', + strtotime($this->profile->created))); + $this->elementEnd('dl'); + + $this->elementStart('dl', 'entity_subscriptions'); + $this->elementStart('dt'); + $this->element('a', array('href' => common_local_url('subscriptions', + array('nickname' => $this->profile->nickname))), + _('Subscriptions')); + $this->elementEnd('dt'); + $this->element('dd', null, (is_int($subs_count)) ? $subs_count : '0'); + $this->elementEnd('dl'); + + $this->elementStart('dl', 'entity_subscribers'); + $this->elementStart('dt'); + $this->element('a', array('href' => common_local_url('subscribers', + array('nickname' => $this->profile->nickname))), + _('Subscribers')); + $this->elementEnd('dt'); + $this->element('dd', 'subscribers', (is_int($subbed_count)) ? $subbed_count : '0'); + $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->elementEnd('dl'); + + $this->elementEnd('div'); + } + + function showGroups() + { + $groups = $this->user->getGroups(0, GROUPS_PER_MINILIST + 1); + + $this->elementStart('div', array('id' => 'entity_groups', + 'class' => 'section')); + + $this->element('h2', null, _('Groups')); + + if ($groups) { + $gml = new GroupMiniList($groups, $this->user, $this); + $cnt = $gml->show(); + if ($cnt == 0) { + $this->element('p', null, _('(None)')); + } + } + + if ($cnt > GROUPS_PER_MINILIST) { + $this->elementStart('p'); + $this->element('a', array('href' => common_local_url('usergroups', + array('nickname' => $this->profile->nickname)), + 'class' => 'more'), + _('All groups')); + $this->elementEnd('p'); + } + + $this->elementEnd('div'); + } +} + diff --git a/lib/profilelist.php b/lib/profilelist.php index 8bef49dce..a4cc23555 100644 --- a/lib/profilelist.php +++ b/lib/profilelist.php @@ -34,8 +34,6 @@ if (!defined('LACONICA')) { require_once INSTALLDIR.'/lib/widget.php'; -define('PROFILES_PER_PAGE', 20); - /** * Widget to show a list of profiles * @@ -91,6 +89,7 @@ class ProfileList extends Widget 'id' => 'profile-' . $this->profile->id)); $user = common_current_user(); + $is_own = !is_null($user) && isset($this->owner) && ($user->id === $this->owner->id); $this->out->elementStart('div', 'entity_profile vcard'); @@ -104,13 +103,13 @@ class ProfileList extends Widget 'alt' => ($this->profile->fullname) ? $this->profile->fullname : $this->profile->nickname)); - $hasFN = ($this->profile->fullname) ? 'nickname' : 'fn nickname'; + $hasFN = ($this->profile->fullname !== '') ? 'nickname' : 'fn nickname'; $this->out->elementStart('span', $hasFN); $this->out->raw($this->highlight($this->profile->nickname)); $this->out->elementEnd('span'); $this->out->elementEnd('a'); - if ($this->profile->fullname) { + if (!empty($this->profile->fullname)) { $this->out->elementStart('dl', 'entity_fn'); $this->out->element('dt', null, 'Full name'); $this->out->elementStart('dd'); @@ -120,7 +119,7 @@ class ProfileList extends Widget $this->out->elementEnd('dd'); $this->out->elementEnd('dl'); } - if ($this->profile->location) { + if (!empty($this->profile->location)) { $this->out->elementStart('dl', 'entity_location'); $this->out->element('dt', null, _('Location')); $this->out->elementStart('dd', 'label'); @@ -128,7 +127,7 @@ class ProfileList extends Widget $this->out->elementEnd('dd'); $this->out->elementEnd('dl'); } - if ($this->profile->homepage) { + if (!empty($this->profile->homepage)) { $this->out->elementStart('dl', 'entity_url'); $this->out->element('dt', null, _('URL')); $this->out->elementStart('dd'); @@ -139,7 +138,7 @@ class ProfileList extends Widget $this->out->elementEnd('dd'); $this->out->elementEnd('dl'); } - if ($this->profile->bio) { + if (!empty($this->profile->bio)) { $this->out->elementStart('dl', 'entity_note'); $this->out->element('dt', null, _('Note')); $this->out->elementStart('dd', 'note'); @@ -156,7 +155,7 @@ class ProfileList extends Widget $this->out->elementStart('dl', 'entity_tags'); $this->out->elementStart('dt'); - if ($user->id == $this->owner->id) { + if ($is_own) { $this->out->element('a', array('href' => common_local_url('tagother', array('id' => $this->profile->id))), _('Tags')); @@ -185,7 +184,7 @@ class ProfileList extends Widget $this->out->elementEnd('dl'); } - if ($user && $user->id == $this->owner->id) { + if ($is_own) { $this->showOwnerControls($this->profile); } @@ -195,9 +194,10 @@ class ProfileList extends Widget $this->out->elementStart('ul'); - if ($user && $user->id != $this->profile->id) { - # XXX: special-case for user looking at own - # subscriptions page + // Is this a logged-in user, looking at someone else's + // profile? + + 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); @@ -208,7 +208,7 @@ class ProfileList extends Widget } $this->out->elementEnd('li'); $this->out->elementStart('li', 'entity_block'); - if ($user && $user->id == $this->owner->id) { + if ($user->id == $this->owner->id) { $this->showBlockForm(); } $this->out->elementEnd('li'); diff --git a/lib/profileminilist.php b/lib/profileminilist.php index 0d466bba8..57496d0e9 100644 --- a/lib/profileminilist.php +++ b/lib/profileminilist.php @@ -33,7 +33,7 @@ if (!defined('LACONICA')) { require_once INSTALLDIR.'/lib/profilelist.php'; -define('PROFILES_PER_MINILIST', 80); +define('PROFILES_PER_MINILIST', 27); /** * Widget to show a list of profiles, good for sidebar diff --git a/lib/publicgroupnav.php b/lib/publicgroupnav.php index d72475e20..485d25e20 100644 --- a/lib/publicgroupnav.php +++ b/lib/publicgroupnav.php @@ -39,6 +39,7 @@ require_once INSTALLDIR.'/lib/widget.php'; * @category Output * @package Laconica * @author Evan Prodromou <evan@controlyourself.ca> + * @author Sarven Capadisli <csarven@controlyourself.ca> * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://laconi.ca/ * @@ -73,23 +74,26 @@ class PublicGroupNav extends Widget $this->action->elementStart('ul', array('class' => 'nav')); - $this->out->menuItem(common_local_url('public'), _('Public'), - _('Public timeline'), $action_name == 'public', 'nav_timeline_public'); + if (Event::handle('StartPublicGroupNav', array($this))) { + $this->out->menuItem(common_local_url('public'), _('Public'), + _('Public timeline'), $action_name == 'public', 'nav_timeline_public'); - $this->out->menuItem(common_local_url('groups'), _('Groups'), - _('User groups'), $action_name == 'groups', 'nav_groups'); + $this->out->menuItem(common_local_url('groups'), _('Groups'), + _('User groups'), $action_name == 'groups', 'nav_groups'); - $this->out->menuItem(common_local_url('publictagcloud'), _('Recent tags'), - _('Recent tags'), $action_name == 'publictagcloud', 'nav_recent-tags'); + $this->out->menuItem(common_local_url('publictagcloud'), _('Recent tags'), + _('Recent tags'), $action_name == 'publictagcloud', 'nav_recent-tags'); - if (count(common_config('nickname', 'featured')) > 0) { - $this->out->menuItem(common_local_url('featured'), _('Featured'), - _('Featured users'), $action_name == 'featured', 'nav_featured'); - } + if (count(common_config('nickname', 'featured')) > 0) { + $this->out->menuItem(common_local_url('featured'), _('Featured'), + _('Featured users'), $action_name == 'featured', 'nav_featured'); + } - $this->out->menuItem(common_local_url('favorited'), _('Popular'), - _("Popular notices"), $action_name == 'favorited', 'nav_timeline_favorited'); + $this->out->menuItem(common_local_url('favorited'), _('Popular'), + _("Popular notices"), $action_name == 'favorited', 'nav_timeline_favorited'); + Event::handle('EndPublicGroupNav', array($this)); + } $this->action->elementEnd('ul'); } } diff --git a/lib/queuehandler.php b/lib/queuehandler.php index 9ce9e32b3..f76f16e07 100644 --- a/lib/queuehandler.php +++ b/lib/queuehandler.php @@ -36,7 +36,7 @@ class QueueHandler extends Daemon $this->set_id($id); } } - + function class_name() { return ucfirst($this->transport()) . 'Handler'; @@ -46,7 +46,7 @@ class QueueHandler extends Daemon { return strtolower($this->class_name().'.'.$this->get_id()); } - + function get_id() { return $this->_id; @@ -56,16 +56,16 @@ class QueueHandler extends Daemon { $this->_id = $id; } - + function transport() { return null; } - + function start() { } - + function finish() { } @@ -74,16 +74,10 @@ class QueueHandler extends Daemon { return true; } - - function run() - { - if (!$this->start()) { - return false; - } - $this->log(LOG_INFO, 'checking for queued notices'); - $transport = $this->transport(); + + function db_dispatch() { do { - $qi = Queue_item::top($transport); + $qi = Queue_item::top($this->transport()); if ($qi) { $this->log(LOG_INFO, 'Got item enqueued '.common_exact_date($qi->created)); $notice = Notice::staticGet($qi->notice_id); @@ -113,8 +107,69 @@ class QueueHandler extends Daemon } else { $this->clear_old_claims(); $this->idle(5); - } + } } while (true); + } + + function stomp_dispatch() { + require("Stomp.php"); + $con = new Stomp(common_config('queue','stomp_server')); + if (!$con->connect()) { + $this->log(LOG_ERR, 'Failed to connect to queue server'); + return false; + } + $queue_basename = common_config('queue','queue_basename'); + // subscribe to the relevant queue (format: basename-transport) + $con->subscribe('/queue/'.$queue_basename.'-'.$this->transport()); + + do { + $frame = $con->readFrame(); + if ($frame) { + $this->log(LOG_INFO, 'Got item enqueued '.common_exact_date($frame->headers['created'])); + + // XXX: Now the queue handler receives only the ID of the + // notice, and it has to get it from the DB + // A massive improvement would be avoid DB query by transmitting + // all the notice details via queue server... + $notice = Notice::staticGet($frame->body); + + if ($notice) { + $this->log(LOG_INFO, 'broadcasting notice ID = ' . $notice->id); + $result = $this->handle_notice($notice); + if ($result) { + // if the msg has been handled positively, ack it + // and the queue server will remove it from the queue + $con->ack($frame); + $this->log(LOG_INFO, 'finished broadcasting notice ID = ' . $notice->id); + } + else { + // no ack + $this->log(LOG_WARNING, 'Failed broadcast for notice ID = ' . $notice->id); + } + $notice->free(); + unset($notice); + $notice = null; + } else { + $this->log(LOG_WARNING, 'queue item for notice that does not exist'); + } + } + } while (true); + + $con->disconnect(); + } + + function run() + { + if (!$this->start()) { + return false; + } + $this->log(LOG_INFO, 'checking for queued notices'); + if (common_config('queue','subsystem') == 'stomp') { + $this->stomp_dispatch(); + } + else { + $this->db_dispatch(); + } if (!$this->finish()) { return false; } @@ -127,7 +182,7 @@ class QueueHandler extends Daemon sleep($timeout); } } - + function clear_old_claims() { $qi = new Queue_item(); @@ -137,10 +192,10 @@ class QueueHandler extends Daemon $qi->free(); unset($qi); } - + function log($level, $msg) { common_log($level, $this->class_name() . ' ('. $this->get_id() .'): '.$msg); } } -
\ No newline at end of file + diff --git a/lib/router.php b/lib/router.php index d47ad7118..70ee0f3fb 100644 --- a/lib/router.php +++ b/lib/router.php @@ -47,8 +47,11 @@ require_once 'Net/URL/Mapper.php'; class Router { - static $m = null; + var $m = null; static $inst = null; + static $bare = array('requesttoken', 'accesstoken', 'userauthorization', + 'postnotice', 'updateprofile', 'finishremotesubscribe', + 'finishopenidlogin', 'finishaddopenid'); static function get() { @@ -65,8 +68,8 @@ class Router } } - function initialize() { - + function initialize() + { $m = Net_URL_Mapper::getInstance(); // In the "root" @@ -98,12 +101,17 @@ class Router $main = array('login', 'logout', 'register', 'subscribe', 'unsubscribe', 'confirmaddress', 'recoverpassword', 'invite', 'favor', 'disfavor', 'sup', - 'tagother', 'block'); + 'block', 'subedit'); foreach ($main as $a) { $m->connect('main/'.$a, array('action' => $a)); } + $m->connect('main/sup/:seconds', array('action' => 'sup'), + array('seconds' => '[0-9]+')); + + $m->connect('main/tagother/:id', array('action' => 'tagother')); + // these take a code foreach (array('register', 'confirmaddress', 'recoverpassword') as $c) { @@ -114,11 +122,16 @@ class Router $m->connect('main/openid', array('action' => 'openidlogin')); $m->connect('main/remote', array('action' => 'remotesubscribe')); + $m->connect('main/remote?nickname=:nickname', array('action' => 'remotesubscribe'), array('nickname' => '[A-Za-z0-9_-]+')); + + foreach (Router::$bare as $action) { + $m->connect('index.php?action=' . $action, array('action' => $action)); + } // settings foreach (array('profile', 'avatar', 'password', 'openid', 'im', - 'email', 'sms', 'twitter', 'other') as $s) { + 'email', 'sms', 'twitter', 'design', 'other') as $s) { $m->connect('settings/'.$s, array('action' => $s.'settings')); } @@ -126,13 +139,38 @@ class Router foreach (array('group', 'people', 'notice') as $s) { $m->connect('search/'.$s, array('action' => $s.'search')); + $m->connect('search/'.$s.'?q=:q', + array('action' => $s.'search'), + array('q' => '.+')); } + // The second of these is needed to make the link work correctly + // when inserted into the page. The first is needed to match the + // route on the way in. Seems to be another Net_URL_Mapper bug to me. $m->connect('search/notice/rss', array('action' => 'noticesearchrss')); + $m->connect('search/notice/rss?q=:q', array('action' => 'noticesearchrss'), + array('q' => '.+')); + + $m->connect('attachment/:attachment/ajax', + array('action' => 'attachment_ajax'), + array('notice' => '[0-9]+')); + + $m->connect('attachment/:attachment', + array('action' => 'attachment'), + array('notice' => '[0-9]+')); // notice $m->connect('notice/new', array('action' => 'newnotice')); + $m->connect('notice/new?replyto=:replyto', + array('action' => 'newnotice'), + array('replyto' => '[A-Za-z0-9_-]+')); + $m->connect('notice/:notice/attachments/ajax', + array('action' => 'attachments_ajax'), + array('notice' => '[0-9]+')); + $m->connect('notice/:notice/attachments', + array('action' => 'attachments'), + array('notice' => '[0-9]+')); $m->connect('notice/:notice', array('action' => 'shownotice'), array('notice' => '[0-9]+')); @@ -141,7 +179,14 @@ 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', array('action' => 'showmessage'), array('message' => '[0-9]+')); @@ -206,40 +251,52 @@ class Router $m->connect('api/statuses/:method', array('action' => 'api', 'apiaction' => 'statuses'), - array('method' => '(public_timeline|friends_timeline|user_timeline|update|replies|friends|followers|featured)(\.(atom|rss|xml|json))?')); + array('method' => '(public_timeline|friends_timeline|user_timeline|update|replies|mentions|friends|followers|featured)(\.(atom|rss|xml|json))?')); $m->connect('api/statuses/:method/:argument', array('action' => 'api', 'apiaction' => 'statuses'), - array('method' => '(user_timeline|show|destroy|friends|followers)')); + array('method' => '(user_timeline|friends_timeline|replies|mentions|show|destroy|friends|followers)')); // users - $m->connect('api/users/show/:argument', + $m->connect('api/users/:method/:argument', array('action' => 'api', - 'apiaction' => 'users')); + 'apiaction' => 'users'), + array('method' => 'show(\.(xml|json))?')); $m->connect('api/users/:method', array('action' => 'api', 'apiaction' => 'users'), - array('method' => 'show(\.(xml|json|atom|rss))?')); + array('method' => 'show(\.(xml|json))?')); // direct messages - $m->connect('api/direct_messages/:method', - array('action' => 'api', - 'apiaction' => 'direct_messages'), - array('method' => '(sent|new)(\.(xml|json|atom|rss))?')); + foreach (array('xml', 'json') as $e) { + $m->connect('api/direct_messages/new.'.$e, + array('action' => 'api', + 'apiaction' => 'direct_messages', + 'method' => 'create.'.$e)); + } + + foreach (array('xml', 'json', 'rss', 'atom') as $e) { + $m->connect('api/direct_messages.'.$e, + array('action' => 'api', + 'apiaction' => 'direct_messages', + 'method' => 'direct_messages.'.$e)); + } + + foreach (array('xml', 'json', 'rss', 'atom') as $e) { + $m->connect('api/direct_messages/sent.'.$e, + array('action' => 'api', + 'apiaction' => 'direct_messages', + 'method' => 'sent.'.$e)); + } $m->connect('api/direct_messages/destroy/:argument', array('action' => 'api', 'apiaction' => 'direct_messages')); - $m->connect('api/:method', - array('action' => 'api', - 'apiaction' => 'direct_messages'), - array('method' => 'direct_messages(\.(xml|json|atom|rss))?')); - // friendships $m->connect('api/friendships/:method/:argument', @@ -250,7 +307,33 @@ class Router $m->connect('api/friendships/:method', array('action' => 'api', 'apiaction' => 'friendships'), - array('method' => 'exists(\.(xml|json|rss|atom))')); + array('method' => 'exists(\.(xml|json))')); + + // Social graph + + $m->connect('api/friends/ids/:argument', + array('action' => 'api', + 'apiaction' => 'statuses', + 'method' => 'friendsIDs')); + + foreach (array('xml', 'json') as $e) { + $m->connect('api/friends/ids.'.$e, + array('action' => 'api', + 'apiaction' => 'statuses', + 'method' => 'friendsIDs.'.$e)); + } + + $m->connect('api/followers/ids/:argument', + array('action' => 'api', + 'apiaction' => 'statuses', + 'method' => 'followersIDs')); + + foreach (array('xml', 'json') as $e) { + $m->connect('api/followers/ids.'.$e, + array('action' => 'api', + 'apiaction' => 'statuses', + 'method' => 'followersIDs.'.$e)); + } // account @@ -269,10 +352,12 @@ class Router 'apiaction' => 'favorites', 'method' => 'favorites')); - $m->connect('api/:method', - array('action' => 'api', - 'apiaction' => 'favorites'), - array('method' => 'favorites(\.(xml|json|rss|atom))?')); + foreach (array('xml', 'json', 'rss', 'atom') as $e) { + $m->connect('api/favorites.'.$e, + array('action' => 'api', + 'apiaction' => 'favorites', + 'method' => 'favorites.'.$e)); + } // notifications @@ -298,6 +383,11 @@ class Router array('action' => 'api', 'apiaction' => 'laconica')); + // search + $m->connect('api/search.atom', array('action' => 'twitapisearchatom')); + $m->connect('api/search.json', array('action' => 'twitapisearchjson')); + $m->connect('api/trends.json', array('action' => 'twitapitrends')); + // user stuff foreach (array('subscriptions', 'subscribers', @@ -336,19 +426,40 @@ 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}')); + Event::handle('RouterInitialized', array($m)); + return $m; } function map($path) { - return $this->m->match($path); + try { + $match = $this->m->match($path); + } catch (Net_URL_Mapper_InvalidException $e) { + common_log(LOG_ERR, "Problem getting route for $path - " . + $e->getMessage()); + $cac = new ClientErrorAction("Page not found.", 404); + $cac->showPage(); + } + + return $match; } - function build($action, $args=null, $fragment=null) + function build($action, $args=null, $params=null, $fragment=null) { $action_arg = array('action' => $action); @@ -358,6 +469,17 @@ class Router $args = $action_arg; } - return $this->m->generate($args, null, $fragment); + $url = $this->m->generate($args, $params, $fragment); + + // Due to a bug in the Net_URL_Mapper code, the returned URL may + // contain a malformed query of the form ?p1=v1?p2=v2?p3=v3. We + // repair that here rather than modifying the upstream code... + + $qpos = strpos($url, '?'); + if ($qpos !== false) { + $url = substr($url, 0, $qpos+1) . + str_replace('?', '&', substr($url, $qpos+1)); + } + return $url; } -}
\ No newline at end of file +} diff --git a/lib/rssaction.php b/lib/rssaction.php index 131e8ac65..2f25ed7e4 100644 --- a/lib/rssaction.php +++ b/lib/rssaction.php @@ -38,6 +38,7 @@ class Rss10Action extends Action var $creators = array(); var $limit = DEFAULT_RSS_LIMIT; + var $notices = null; /** * Constructor @@ -93,8 +94,15 @@ class Rss10Action extends Action function handle($args) { + // Parent handling, including cache check parent::handle($args); - $this->showRss($this->limit); + // Get the list of notices + if (empty($this->tag)) { + $this->notices = $this->getNotices($this->limit); + } else { + $this->notices = $this->getTaggedNotices($this->tag, $this->limit); + } + $this->showRss(); } /** @@ -128,15 +136,13 @@ class Rss10Action extends Action return null; } - function showRss($limit=0) + function showRss() { - $notices = $this->getNotices($limit); - $this->initRss(); - $this->showChannel($notices); + $this->showChannel(); $this->showImage(); - foreach ($notices as $n) { + foreach ($this->notices as $n) { $this->showItem($n); } @@ -144,7 +150,7 @@ class Rss10Action extends Action $this->endRss(); } - function showChannel($notices) + function showChannel() { $channel = $this->getChannel(); @@ -163,7 +169,7 @@ class Rss10Action extends Action $this->elementStart('items'); $this->elementStart('rdf:Seq'); - foreach ($notices as $notice) { + foreach ($this->notices as $notice) { $this->element('sioct:MicroblogPost', array('rdf:resource' => $notice->uri)); } @@ -258,5 +264,25 @@ class Rss10Action extends Action { $this->elementEnd('rdf:RDF'); } + + /** + * When was this page last modified? + * + */ + + function lastModified() + { + if (empty($this->notices)) { + return null; + } + + if (count($this->notices) == 0) { + return null; + } + + // FIXME: doesn't handle modified profiles, avatars, deleted notices + + return strtotime($this->notices[0]->created); + } } diff --git a/lib/search_engines.php b/lib/search_engines.php index 559107910..7b9dbb618 100644 --- a/lib/search_engines.php +++ b/lib/search_engines.php @@ -74,7 +74,7 @@ class SphinxSearch extends SearchEngine { //FIXME without LARGEST_POSSIBLE, the most recent results aren't returned // this probably has a large impact on performance - $LARGEST_POSSIBLE = 1e6; + $LARGEST_POSSIBLE = 1e6; if ($rss) { $this->sphinx->setLimits($offset, $count, $count, $LARGEST_POSSIBLE); @@ -109,12 +109,25 @@ class MySQLSearch extends SearchEngine { function query($q) { - if ('identica_people' === $this->table) - return $this->target->whereAdd('MATCH(nickname, fullname, location, bio, homepage) ' . - 'against (\''.addslashes($q).'\')'); - if ('identica_notices' === $this->table) - return $this->target->whereAdd('MATCH(content) ' . - 'against (\''.addslashes($q).'\')'); + if ('identica_people' === $this->table) { + $this->target->whereAdd('MATCH(nickname, fullname, location, bio, homepage) ' . + 'AGAINST (\''.addslashes($q).'\' IN BOOLEAN MODE)'); + if (strtolower($q) != $q) { + $this->target->whereAdd('MATCH(nickname, fullname, location, bio, homepage) ' . + 'AGAINST (\''.addslashes(strtolower($q)).'\' IN BOOLEAN MODE)', 'OR'); + } + return true; + } else if ('identica_notices' === $this->table) { + $this->target->whereAdd('MATCH(content) ' . + 'AGAINST (\''.addslashes($q).'\' IN BOOLEAN MODE)'); + if (strtolower($q) != $q) { + $this->target->whereAdd('MATCH(content) ' . + 'AGAINST (\''.addslashes(strtolower($q)).'\' IN BOOLEAN MODE)', 'OR'); + } + return true; + } else { + throw new ServerException('Unknown table: ' . $this->table); + } } } @@ -122,10 +135,13 @@ class PGSearch extends SearchEngine { function query($q) { - if ('identica_people' === $this->table) + if ('identica_people' === $this->table) { return $this->target->whereAdd('textsearch @@ plainto_tsquery(\''.addslashes($q).'\')'); - if ('identica_notices' === $this->table) + } else if ('identica_notices' === $this->table) { 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 fdfb8dc5a..e7ad4affd 100644 --- a/lib/searchaction.php +++ b/lib/searchaction.php @@ -79,10 +79,11 @@ class SearchAction extends Action function showTop($arr=null) { + $error = null; if ($arr) { $error = $arr[1]; } - if ($error) { + if (!empty($error)) { $this->element('p', 'error', $error); } else { $instr = $this->getInstructions(); @@ -109,8 +110,6 @@ class SearchAction extends Action function showForm($error=null) { - global $config; - $q = $this->trimmed('q'); $page = $this->trimmed('page', 1); $this->elementStart('form', array('method' => 'get', @@ -121,7 +120,7 @@ class SearchAction extends Action $this->element('legend', null, _('Search site')); $this->elementStart('ul', 'form_data'); $this->elementStart('li'); - if (!isset($config['site']['fancy']) || !$config['site']['fancy']) { + if (!common_config('site', 'fancy')) { $this->hidden('action', $this->trimmed('action')); } $this->input('q', 'Keyword(s)', $q); @@ -134,5 +133,31 @@ class SearchAction extends Action $this->showResults($q, $page); } } + + function searchSuggestions($q) { + $qe = urlencode($q); + $message = sprintf(_(<<<E_O_T +* Make sure all words are spelled correctly. +* Try different keywords. +* Try more general keywords. +* Try fewer keywords. + +You can also try your search on other engines: + +* [Twingly](http://www.twingly.com/search?q=%s&content=microblog&site=identi.ca) +* [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) + + +E_O_T +), $qe, $qe, $qe, $qe); + $this->elementStart('dl', array('id' => 'help_search', 'class' => 'help')); + $this->element('dt', null, _('Search help')); + $this->elementStart('dd', 'instructions'); + $this->raw(common_markup_to_html($message)); + $this->elementEnd('dd'); + $this->elementEnd('div'); + } } diff --git a/lib/servererroraction.php b/lib/servererroraction.php index 80a3fdd7b..595dcf147 100644 --- a/lib/servererroraction.php +++ b/lib/servererroraction.php @@ -42,7 +42,7 @@ require_once INSTALLDIR.'/lib/error.php'; * says that 500 errors should be treated similarly to 400 errors, and * it's easier to give an HTML response. Maybe we can customize these * to display some funny animal cartoons. If not, we can probably role - * these classes up into a single class. + * these classes up into a single class. * * See: http://tools.ietf.org/html/rfc2616#section-10 * @@ -57,19 +57,19 @@ class ServerErrorAction extends ErrorAction function __construct($message='Error', $code=500) { parent::__construct($message, $code); - + $this->status = array(500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported'); - + $this->default = 500; } // XXX: Should these error actions even be invokable via URI? - + function handle($args) { parent::handle($args); @@ -81,12 +81,16 @@ class ServerErrorAction extends ErrorAction } $this->message = $this->trimmed('message'); - + if (!$this->message) { - $this->message = "Server Error $this->code"; - } + $this->message = "Server Error $this->code"; + } $this->showPage(); } - + + function title() + { + return $this->status[$this->code]; + } } diff --git a/lib/settingsaction.php b/lib/settingsaction.php index 53c807c6f..db20c5804 100644 --- a/lib/settingsaction.php +++ b/lib/settingsaction.php @@ -78,9 +78,9 @@ class SettingsAction extends Action common_set_returnto($this->selfUrl()); $user = common_current_user(); if ($user->hasOpenID()) { - common_redirect(common_local_url('openidlogin')); + common_redirect(common_local_url('openidlogin'), 303); } else { - common_redirect(common_local_url('login')); + common_redirect(common_local_url('login'), 303); } } else if ($_SERVER['REQUEST_METHOD'] == 'POST') { $this->handlePost(); diff --git a/lib/subgroupnav.php b/lib/subgroupnav.php index 5fd8a72a2..4a9b36ae8 100644 --- a/lib/subgroupnav.php +++ b/lib/subgroupnav.php @@ -74,38 +74,44 @@ class SubGroupNav extends Widget $this->out->elementStart('ul', array('class' => 'nav')); - $this->out->menuItem(common_local_url('subscriptions', - array('nickname' => - $this->user->nickname)), - _('Subscriptions'), - sprintf(_('People %s subscribes to'), - $this->user->nickname), - $action == 'subscriptions', - 'nav_subscriptions'); - $this->out->menuItem(common_local_url('subscribers', - array('nickname' => - $this->user->nickname)), - _('Subscribers'), - sprintf(_('People subscribed to %s'), - $this->user->nickname), - $action == 'subscribers', - 'nav_subscribers'); - $this->out->menuItem(common_local_url('usergroups', - array('nickname' => - $this->user->nickname)), - _('Groups'), - sprintf(_('Groups %s is a member of'), - $this->user->nickname), - $action == 'usergroups', - 'nav_usergroups'); - if ($this->user->id == $cur->id) { - $this->out->menuItem(common_local_url('invite'), - _('Invite'), - sprintf(_('Invite friends and colleagues to join you on %s'), - common_config('site', 'name')), - $action == 'invite', - 'nav_invite'); + if (Event::handle('StartSubGroupNav', array($this))) { + + $this->out->menuItem(common_local_url('subscriptions', + array('nickname' => + $this->user->nickname)), + _('Subscriptions'), + sprintf(_('People %s subscribes to'), + $this->user->nickname), + $action == 'subscriptions', + 'nav_subscriptions'); + $this->out->menuItem(common_local_url('subscribers', + array('nickname' => + $this->user->nickname)), + _('Subscribers'), + sprintf(_('People subscribed to %s'), + $this->user->nickname), + $action == 'subscribers', + 'nav_subscribers'); + $this->out->menuItem(common_local_url('usergroups', + array('nickname' => + $this->user->nickname)), + _('Groups'), + sprintf(_('Groups %s is a member of'), + $this->user->nickname), + $action == 'usergroups', + 'nav_usergroups'); + if (!is_null($cur) && $this->user->id === $cur->id) { + $this->out->menuItem(common_local_url('invite'), + _('Invite'), + sprintf(_('Invite friends and colleagues to join you on %s'), + common_config('site', 'name')), + $action == 'invite', + 'nav_invite'); + } + + Event::handle('EndSubGroupNav', array($this)); } + $this->out->elementEnd('ul'); } } diff --git a/lib/subpeopletagcloudsection.php b/lib/subpeopletagcloudsection.php new file mode 100644 index 000000000..9f6948dc9 --- /dev/null +++ b/lib/subpeopletagcloudsection.php @@ -0,0 +1,77 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Personal tag cloud section + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Widget + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +/** + * Personal tag cloud section + * + * @category Widget + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +class SubPeopleTagCloudSection extends TagCloudSection +{ + function getTags() + { + $qry = $this->query(); + $limit = TAGS_PER_SECTION; + $offset = 0; + + if (common_config('db','type') == 'pgsql') { + $qry .= ' LIMIT ' . $limit . ' OFFSET ' . $offset; + } else { + $qry .= ' LIMIT ' . $offset . ', ' . $limit; + } + + $profile_tag = Memcached_DataObject::cachedQuery('Profile_tag', + sprintf($qry, + $this->out->user->id)); + return $profile_tag; + } + + function tagUrl($tag) { + return common_local_url('peopletag', array('tag' => $tag)); + } + + function showTag($tag, $weight, $relative) { + $rel = 'tag-cloud-'; + $rel .= 1+intval(7 * $relative * $weight - 0.01); + + $this->out->elementStart('li', $rel); + $this->out->element('a', array('href' => $this->tagUrl($tag)), $tag); + $this->out->elementEnd('li'); + } +} + diff --git a/lib/subscriberspeopleselftagcloudsection.php b/lib/subscriberspeopleselftagcloudsection.php new file mode 100644 index 000000000..115241a53 --- /dev/null +++ b/lib/subscriberspeopleselftagcloudsection.php @@ -0,0 +1,62 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Personal tag cloud section + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Widget + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +/** + * Personal tag cloud section + * + * @category Widget + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +class SubscribersPeopleSelfTagCloudSection extends SubPeopleTagCloudSection +{ + function title() + { + return _('People Tagcloud as self-tagged'); + } + + function query() { +// return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscriber where subscribed=%d and subscribed != subscriber and tagger = tagged group by tag order by weight desc'; + + + return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscriber where subscribed=%d and subscribed != subscriber and tagger = tagged and tag is not null group by tag order by weight desc'; + +// return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscribed where subscriber=%d and subscribed != subscriber and tagger = tagged and tag is not null group by tag order by weight desc'; + + + } +} + diff --git a/lib/subscriberspeopletagcloudsection.php b/lib/subscriberspeopletagcloudsection.php new file mode 100644 index 000000000..4dafbc1c7 --- /dev/null +++ b/lib/subscriberspeopletagcloudsection.php @@ -0,0 +1,61 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Personal tag cloud section + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Widget + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +/** + * Personal tag cloud section + * + * @category Widget + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +class SubscribersPeopleTagCloudSection extends SubPeopleTagCloudSection +{ + function title() + { + return _('People Tagcloud as tagged'); + } + + function tagUrl($tag) { + $nickname = $this->out->profile->nickname; + return common_local_url('subscribers', array('nickname' => $nickname, 'tag' => $tag)); + } + + function query() { +// return 'select tag, count(tag) as weight from subscription left join profile_tag on subscriber=tagged and subscribed=tagger where subscribed=%d and subscriber != subscribed group by tag order by weight desc'; + return 'select tag, count(tag) as weight from subscription left join profile_tag on subscriber=tagged and subscribed=tagger where subscribed=%d and subscriber != subscribed and tag is not null group by tag order by weight desc'; + } +} + diff --git a/lib/subscriptionspeopleselftagcloudsection.php b/lib/subscriptionspeopleselftagcloudsection.php new file mode 100644 index 000000000..3896294d2 --- /dev/null +++ b/lib/subscriptionspeopleselftagcloudsection.php @@ -0,0 +1,61 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Personal tag cloud section + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Widget + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +/** + * Personal tag cloud section + * + * @category Widget + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +class SubscriptionsPeopleSelfTagCloudSection extends SubPeopleTagCloudSection +{ + function title() + { + return _('People Tagcloud as self-tagged'); + } + + function query() { +// return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscriber where subscribed=%d and subscriber != subscribed and tagger = tagged group by tag order by weight desc'; + + + + return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscribed where subscriber=%d and subscribed != subscriber and tagger = tagged and tag is not null group by tag order by weight desc'; + +// return 'select tag, count(tag) as weight from subscription left join profile_tag on tagger = subscriber where subscribed=%d and subscribed != subscriber and tagger = tagged and tag is not null group by tag order by weight desc'; + } +} + diff --git a/lib/subscriptionspeopletagcloudsection.php b/lib/subscriptionspeopletagcloudsection.php new file mode 100644 index 000000000..f9c8672e3 --- /dev/null +++ b/lib/subscriptionspeopletagcloudsection.php @@ -0,0 +1,61 @@ +<?php +/** + * Laconica, the distributed open-source microblogging tool + * + * Personal tag cloud section + * + * PHP version 5 + * + * LICENCE: This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * @category Widget + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @copyright 2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +/** + * Personal tag cloud section + * + * @category Widget + * @package Laconica + * @author Evan Prodromou <evan@controlyourself.ca> + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +class SubscriptionsPeopleTagCloudSection extends SubPeopleTagCloudSection +{ + function title() + { + return _('People Tagcloud as tagged'); + } + + function tagUrl($tag) { + $nickname = $this->out->profile->nickname; + return common_local_url('subscriptions', array('nickname' => $nickname, 'tag' => $tag)); + } + + function query() { +// return 'select tag, count(tag) as weight from subscription left join profile_tag on subscriber=tagger and subscribed=tagged where subscriber=%d and subscriber != subscribed group by tag order by weight desc'; + return 'select tag, count(tag) as weight from subscription left join profile_tag on subscriber=tagger and subscribed=tagged where subscriber=%d and subscriber != subscribed and tag is not null group by tag order by weight desc'; + } +} + 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..bef660cbf 100644 --- a/lib/theme.php +++ b/lib/theme.php @@ -69,4 +69,31 @@ function theme_path($relative, $theme=null) } else { return common_path('theme/'.$theme.'/'.$relative); } -}
\ No newline at end of file +} + +/** + * Gets the full URL of a file in a skin dir based on its relative name + * + * @param string $relative relative path within the theme, skin directory + * @param string $theme name of the theme; defaults to current theme + * @param string $skin name of the skin; defaults to current theme + * + * @return string URL of the file + */ + +function skin_path($relative, $theme=null, $skin=null) +{ + if (!$theme) { + $theme = common_config('site', 'theme'); + } + if (!$skin) { + $skin = common_config('site', 'skin'); + } + $server = common_config('theme', 'server'); + if ($server) { + return 'http://'.$server.'/'.$theme.'/skin/'.$skin.'/'.$relative; + } else { + return common_path('theme/'.$theme.'/skin/'.$skin.'/'.$relative); + } +} + diff --git a/lib/topposterssection.php b/lib/topposterssection.php index 4bd59ac79..1a2ce0014 100644 --- a/lib/topposterssection.php +++ b/lib/topposterssection.php @@ -51,7 +51,7 @@ class TopPostersSection extends ProfileSection $qry = 'SELECT profile.*, count(*) as value ' . 'FROM profile JOIN notice ON profile.id = notice.profile_id ' . (common_config('public', 'localonly') ? 'WHERE is_local = 1 ' : '') . - 'GROUP BY profile.id ' . + 'GROUP BY profile.id,nickname,fullname,profileurl,homepage,bio,location,profile.created,profile.modified,textsearch ' . 'ORDER BY value DESC '; $limit = PROFILES_PER_SECTION; diff --git a/lib/twitter.php b/lib/twitter.php index deb6fd276..c1d0dc254 100644 --- a/lib/twitter.php +++ b/lib/twitter.php @@ -19,7 +19,7 @@ if (!defined('LACONICA')) { exit(1); } -define("TWITTER_SERVICE", 1); // Twitter is foreign_service ID 1 +define('TWITTER_SERVICE', 1); // Twitter is foreign_service ID 1 function get_twitter_data($uri, $screen_name, $password) { @@ -45,6 +45,10 @@ function get_twitter_data($uri, $screen_name, $password) if ($errmsg) { common_debug("Twitter bridge - cURL error: $errmsg - trying to load: $uri with user $screen_name.", __FILE__); + + if (defined('SCRIPT_DEBUG')) { + print "cURL error: $errmsg - trying to load: $uri with user $screen_name.\n"; + } } curl_close($ch); @@ -52,63 +56,141 @@ function get_twitter_data($uri, $screen_name, $password) return $data; } -function twitter_user_info($screen_name, $password) +function twitter_json_data($uri, $screen_name, $password) { + $json_data = get_twitter_data($uri, $screen_name, $password); - $uri = "http://twitter.com/users/show/$screen_name.json"; - $data = get_twitter_data($uri, $screen_name, $password); - - if (!$data) { + if (!$json_data) { return false; } - $twit_user = json_decode($data); + $data = json_decode($json_data); - if (!$twit_user) { + if (!$data) { return false; } - return $twit_user; + return $data; } -function update_twitter_user($fuser, $twitter_id, $screen_name) +function twitter_user_info($screen_name, $password) { + $uri = "http://twitter.com/users/show/$screen_name.json"; + return twitter_json_data($uri, $screen_name, $password); +} - $original = clone($fuser); - $fuser->nickname = $screen_name; - $fuser->uri = 'http://twitter.com/' . $screen_name; - $result = $fuser->updateKeys($original); +function twitter_friends_ids($screen_name, $password) +{ + $uri = "http://twitter.com/friends/ids/$screen_name.json"; + return twitter_json_data($uri, $screen_name, $password); +} + +function update_twitter_user($twitter_id, $screen_name) +{ + $uri = 'http://twitter.com/' . $screen_name; + + $fuser = new Foreign_user(); + + $fuser->query('BEGIN'); + + // Dropping down to SQL because regular db_object udpate stuff doesn't seem + // to work so good with tables that have multiple column primary keys + + // Any time we update the uri for a forein user we have to make sure there + // are no dupe entries first -- unique constraint on the uri column + + $qry = 'UPDATE foreign_user set uri = \'\' WHERE uri = '; + $qry .= '\'' . $uri . '\'' . ' AND service = ' . TWITTER_SERVICE; + + $result = $fuser->query($qry); + + if ($result) { + common_debug("Removed uri ($uri) from another foreign_user who was squatting on it."); + if (defined('SCRIPT_DEBUG')) { + print("Removed uri ($uri) from another Twitter user who was squatting on it.\n"); + } + } + + // Update the user + $qry = 'UPDATE foreign_user SET nickname = '; + $qry .= '\'' . $screen_name . '\'' . ', uri = \'' . $uri . '\' '; + $qry .= 'WHERE id = ' . $twitter_id . ' AND service = ' . TWITTER_SERVICE; + + $result = $fuser->query($qry); if (!$result) { + common_log(LOG_WARNING, + "Couldn't update foreign_user data for Twitter user: $screen_name"); common_log_db_error($fuser, 'UPDATE', __FILE__); + if (defined('SCRIPT_DEBUG')) { + print "UPDATE failed: for Twitter user: $twitter_id - $screen_name. - "; + print common_log_objstring($fuser) . "\n"; + $error = &PEAR::getStaticProperty('DB_DataObject','lastError'); + print "DB_DataObject Error: " . $error->getMessage() . "\n"; + } return false; } + $fuser->query('COMMIT'); + + $fuser->free(); + unset($fuser); + return true; } function add_twitter_user($twitter_id, $screen_name) { + $new_uri = 'http://twitter.com/' . $screen_name; + + // Clear out any bad old foreign_users with the new user's legit URL + // This can happen when users move around or fakester accounts get + // repoed, and things like that. + $luser = new Foreign_user(); + $luser->uri = $new_uri; + $luser->service = TWITTER_SERVICE; + $result = $luser->delete(); + + if ($result) { + common_log(LOG_WARNING, + "Twitter bridge - removed invalid Twitter user squatting on uri: $new_uri"); + if (defined('SCRIPT_DEBUG')) { + print "Removed invalid Twitter user squatting on uri: $new_uri\n"; + } + } + + $luser->free(); + unset($luser); + // Otherwise, create a new Twitter user - $fuser = DB_DataObject::factory('foreign_user'); + $fuser = new Foreign_user(); $fuser->nickname = $screen_name; $fuser->uri = 'http://twitter.com/' . $screen_name; $fuser->id = $twitter_id; - $fuser->service = TWITTER_SERVICE; // Twitter + $fuser->service = TWITTER_SERVICE; $fuser->created = common_sql_now(); $result = $fuser->insert(); if (!$result) { - common_debug("Twitter bridge - failed to add new Twitter user: $twitter_id - $screen_name."); + common_log(LOG_WARNING, + "Twitter bridge - failed to add new Twitter user: $twitter_id - $screen_name."); common_log_db_error($fuser, 'INSERT', __FILE__); - return false; + if (defined('SCRIPT_DEBUG')) { + print "INSERT failed: could not add new Twitter user: $twitter_id - $screen_name. - "; + print common_log_objstring($fuser) . "\n"; + $error = &PEAR::getStaticProperty('DB_DataObject','lastError'); + print "DB_DataObject Error: " . $error->getMessage() . "\n"; + } + } else { + common_debug("Twitter bridge - Added new Twitter user: $screen_name ($twitter_id)."); + if (defined('SCRIPT_DEBUG')) { + print "Added new Twitter user: $screen_name ($twitter_id).\n"; + } } - common_debug("Twitter bridge - Added new Twitter user: $screen_name ($twitter_id)."); - - return true; + return $result; } // Creates or Updates a Twitter user @@ -117,53 +199,87 @@ function save_twitter_user($twitter_id, $screen_name) // Check to see whether the Twitter user is already in the system, // and update its screen name and uri if so. - $fuser = Foreign_user::getForeignUser($twitter_id, 1); + $fuser = Foreign_user::getForeignUser($twitter_id, TWITTER_SERVICE); if ($fuser) { + $result = true; + // Only update if Twitter screen name has changed if ($fuser->nickname != $screen_name) { + $result = update_twitter_user($twitter_id, $screen_name); common_debug('Twitter bridge - Updated nickname (and URI) for Twitter user ' . "$fuser->id to $screen_name, was $fuser->nickname"); - return update_twitter_user($fuser, $twitter_id, $screen_name); + if (defined('SCRIPT_DEBUG')) { + print 'Updated nickname (and URI) for Twitter user ' . + "$fuser->id to $screen_name, was $fuser->nickname\n"; + } } + return $result; + } else { return add_twitter_user($twitter_id, $screen_name); } + $fuser->free(); + unset($fuser); + return true; } function retreive_twitter_friends($twitter_id, $screen_name, $password) { + $friends = array(); $uri = "http://twitter.com/statuses/friends/$twitter_id.json?page="; - $twitter_user = twitter_user_info($screen_name, $password); + $friends_ids = twitter_friends_ids($screen_name, $password); + + if (!$friends_ids) { + return $friends; + } + + if (defined('SCRIPT_DEBUG')) { + print "Twitter 'social graph' ids method says $screen_name has " . + count($friends_ids) . " friends.\n"; + } // Calculate how many pages to get... - $pages = ceil($twitter_user->friends_count / 100); + $pages = ceil(count($friends_ids) / 100); if ($pages == 0) { - common_debug("Twitter bridge - Twitter user $screen_name has no friends! Lame."); + common_log(LOG_WARNING, + "Twitter bridge - $screen_name seems to have no friends."); + if (defined('SCRIPT_DEBUG')) { + print "$screen_name seems to have no friends.\n"; + } } - $friends = array(); - for ($i = 1; $i <= $pages; $i++) { $data = get_twitter_data($uri . $i, $screen_name, $password); if (!$data) { - return null; + common_log(LOG_WARNING, + "Twitter bridge - Couldn't retrieve page $i of $screen_name's friends."); + if (defined('SCRIPT_DEBUG')) { + print "Couldn't retrieve page $i of $screen_name's friends.\n"; + } + continue; } $more_friends = json_decode($data); if (!$more_friends) { - return null; + + common_log(LOG_WARNING, + "Twitter bridge - No data for page $i of $screen_name's friends."); + if (defined('SCRIPT_DEBUG')) { + print "No data for page $i of $screen_name's friends.\n"; + } + continue; } $friends = array_merge($friends, $more_friends); @@ -177,19 +293,27 @@ function save_twitter_friends($user, $twitter_id, $screen_name, $password) $friends = retreive_twitter_friends($twitter_id, $screen_name, $password); - if (is_null($friends)) { - common_debug("Twitter bridge - Couldn't get friends data from Twitter."); + if (empty($friends)) { + common_debug("Twitter bridge - Couldn't get friends data from Twitter for $screen_name."); + if (defined('SCRIPT_DEBUG')) { + print "Couldn't get friends data from Twitter for $screen_name.\n"; + } return false; } foreach ($friends as $friend) { $friend_name = $friend->screen_name; - $friend_id = $friend->id; + $friend_id = (int) $friend->id; // Update or create the Foreign_user record if (!save_twitter_user($friend_id, $friend_name)) { - return false; + common_log(LOG_WARNING, + "Twitter bridge - couldn't save $screen_name's friend, $friend_name."); + if (defined('SCRIPT_DEBUG')) { + print "Couldn't save $screen_name's friend, $friend_name.\n"; + } + continue; } // Check to see if there's a related local user @@ -199,8 +323,20 @@ function save_twitter_friends($user, $twitter_id, $screen_name, $password) // Get associated user and subscribe her $friend_user = User::staticGet('id', $flink->user_id); - subs_subscribe_to($user, $friend_user); - common_debug("Twitter bridge - subscribed $friend_user->nickname to $user->nickname."); + if (!empty($friend_user)) { + $result = subs_subscribe_to($user, $friend_user); + + if ($result === true) { + common_debug("Twitter bridge - subscribed $friend_user->nickname to $user->nickname."); + if (defined('SCRIPT_DEBUG')) { + print("Subscribed $friend_user->nickname to $user->nickname.\n"); + } + } else { + if (defined('SCRIPT_DEBUG')) { + print "$result ($friend_user->nickname to $user->nickname)\n"; + } + } + } } } @@ -210,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) || @@ -218,21 +354,20 @@ function is_twitter_bound($notice, $flink) { return true; } } - + return false; } function broadcast_twitter($notice) { - global $config; $success = true; - $flink = Foreign_link::getByUserID($notice->profile_id, + $flink = Foreign_link::getByUserID($notice->profile_id, TWITTER_SERVICE); - - // XXX: Not sure WHERE to check whether a notice should go to + + // XXX: Not sure WHERE to check whether a notice should go to // Twitter. Should we even put in the queue if it shouldn't? --Zach - if (is_twitter_bound($notice, $flink)) { + if (!is_null($flink) && is_twitter_bound($notice, $flink)) { $fuser = $flink->getForeignUser(); $twitter_user = $fuser->nickname; @@ -245,10 +380,10 @@ function broadcast_twitter($notice) $options = array( CURLOPT_USERPWD => "$twitter_user:$twitter_password", CURLOPT_POST => true, - CURLOPT_POSTFIELDS => + CURLOPT_POSTFIELDS => array( 'status' => $statustxt, - 'source' => $config['integration']['source'] + 'source' => common_config('integration', 'source') ), CURLOPT_RETURNTRANSFER => true, CURLOPT_FAILONERROR => true, @@ -293,7 +428,6 @@ function broadcast_twitter($notice) $success = false; } } - + return $success; } - diff --git a/lib/twitterapi.php b/lib/twitterapi.php index a4d183fcd..caf8c0716 100644 --- a/lib/twitterapi.php +++ b/lib/twitterapi.php @@ -24,10 +24,52 @@ class TwitterapiAction extends Action var $auth_user; + /** + * Initialization. + * + * @param array $args Web and URL arguments + * + * @return boolean false if user doesn't exist + */ + + function prepare($args) + { + parent::prepare($args); + return true; + } + + /** + * Handle a request + * + * @param array $args Arguments from $_REQUEST + * + * @return void + */ + function handle($args) { parent::handle($args); } + + /** + * Overrides XMLOutputter::element to write booleans as strings (true|false). + * See that method's documentation for more info. + * + * @param string $tag Element type or tagname + * @param array $attrs Array of element attributes, as + * key-value pairs + * @param string $content string content of the element + * + * @return void + */ + function element($tag, $attrs=null, $content=null) + { + if (is_bool($content)) { + $content = ($content ? 'true' : 'false'); + } + + return parent::element($tag, $attrs, $content); + } function twitter_user_array($profile, $get_notice=false) { @@ -44,7 +86,7 @@ class TwitterapiAction extends Action $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE); $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(AVATAR_STREAM_SIZE); - $twitter_user['protected'] = 'false'; # not supported by Laconica yet + $twitter_user['protected'] = false; # not supported by Laconica yet $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null; if ($get_notice) { @@ -60,22 +102,35 @@ class TwitterapiAction extends Action function twitter_status_array($notice, $include_user=true) { - $profile = $notice->getProfile(); $twitter_status = array(); $twitter_status['text'] = $notice->content; - $twitter_status['truncated'] = 'false'; # Not possible on Laconica + $twitter_status['truncated'] = false; # Not possible on Laconica $twitter_status['created_at'] = $this->date_twitter($notice->created); - $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ? intval($notice->reply_to) : null; + $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ? + intval($notice->reply_to) : null; $twitter_status['source'] = $this->source_link($notice->source); $twitter_status['id'] = intval($notice->id); - $twitter_status['in_reply_to_user_id'] = ($notice->reply_to) ? $this->replier_by_reply(intval($notice->reply_to)) : null; + + $replier_profile = null; + + if ($notice->reply_to) { + $reply = Notice::staticGet(intval($notice->reply_to)); + if ($reply) { + $replier_profile = $reply->getProfile(); + } + } + + $twitter_status['in_reply_to_user_id'] = + ($replier_profile) ? intval($replier_profile->id) : null; + $twitter_status['in_reply_to_screen_name'] = + ($replier_profile) ? $replier_profile->nickname : null; if (isset($this->auth_user)) { - $twitter_status['favorited'] = ($this->auth_user->hasFave($notice)) ? 'true' : 'false'; + $twitter_status['favorited'] = $this->auth_user->hasFave($notice); } else { - $twitter_status['favorited'] = 'false'; + $twitter_status['favorited'] = false; } if ($include_user) { @@ -91,8 +146,6 @@ class TwitterapiAction extends Action { $profile = $notice->getProfile(); - - $server = common_config('site', 'server'); $entry = array(); # We trim() to avoid extraneous whitespace in the output @@ -101,8 +154,12 @@ class TwitterapiAction extends Action $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content)); $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id)); $entry['published'] = common_date_iso8601($notice->created); - $entry['id'] = "tag:$server,2008:$entry[link]"; + + $taguribase = common_config('integration', 'taguri'); + $entry['id'] = "tag:$taguribase:$entry[link]"; + $entry['updated'] = $entry['published']; + $entry['author'] = $profile->getBestName(); # RSS Item specific $entry['description'] = $entry['content']; @@ -115,7 +172,6 @@ class TwitterapiAction extends Action function twitter_rss_dmsg_array($message) { - $server = common_config('site', 'server'); $entry = array(); $entry['title'] = sprintf('Message from %s to %s', @@ -124,8 +180,12 @@ class TwitterapiAction extends Action $entry['content'] = common_xml_safe_str(trim($message->content)); $entry['link'] = common_local_url('showmessage', array('message' => $message->id)); $entry['published'] = common_date_iso8601($message->created); - $entry['id'] = "tag:$server,2008:$entry[link]"; + + $taguribase = common_config('integration', 'taguri'); + + $entry['id'] = "tag:$taguribase,:$entry[link]"; $entry['updated'] = $entry['published']; + $entry['author'] = $message->getFrom()->getBestName(); # RSS Item specific $entry['description'] = $entry['content']; @@ -137,7 +197,6 @@ class TwitterapiAction extends Action function twitter_dmsg_array($message) { - $twitter_dm = array(); $from_profile = $message->getFrom(); @@ -198,18 +257,6 @@ class TwitterapiAction extends Action $this->elementEnd('item'); } - function show_twitter_atom_entry($entry) - { - $this->elementStart('entry'); - $this->element('title', null, $entry['title']); - $this->element('content', array('type' => 'html'), $entry['content']); - $this->element('id', null, $entry['id']); - $this->element('published', null, $entry['published']); - $this->element('updated', null, $entry['updated']); - $this->element('link', array('href' => $entry['link'], 'rel' => 'alternate', 'type' => 'text/html'), null); - $this->elementEnd('entry'); - } - function show_json_objects($objects) { print(json_encode($objects)); @@ -323,7 +370,7 @@ class TwitterapiAction extends Action $this->end_twitter_rss(); } - function show_atom_timeline($notice, $title, $id, $link, $subtitle=null, $suplink=null) + function show_atom_timeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null) { $this->init_document('atom'); @@ -331,23 +378,29 @@ class TwitterapiAction extends Action $this->element('title', null, $title); $this->element('id', null, $id); $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null); + if (!is_null($suplink)) { # For FriendFeed's SUP protocol $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup', 'href' => $suplink, 'type' => 'application/json')); } + + if (!is_null($selfuri)) { + $this->element('link', array('href' => $selfuri, + 'rel' => 'self', 'type' => 'application/atom+xml'), null); + } + + $this->element('updated', null, common_date_iso8601('now')); $this->element('subtitle', null, $subtitle); if (is_array($notice)) { foreach ($notice as $n) { - $entry = $this->twitter_rss_entry_array($n); - $this->show_twitter_atom_entry($entry); + $this->raw($n->asAtomEntry()); } } else { while ($notice->fetch()) { - $entry = $this->twitter_rss_entry_array($notice); - $this->show_twitter_atom_entry($entry); + $this->raw($notice->asAtomEntry()); } } @@ -384,23 +437,7 @@ class TwitterapiAction extends Action function date_twitter($dt) { $t = strtotime($dt); - return date("D M d G:i:s O Y", $t); - } - - function replier_by_reply($reply_id) - { - $notice = Notice::staticGet($reply_id); - if ($notice) { - $profile = $notice->getProfile(); - if ($profile) { - return intval($profile->id); - } else { - common_debug('Can\'t find a profile for notice: ' . $notice->id, __FILE__); - } - } else { - common_debug("Can't get notice: $reply_id", __FILE__); - } - return null; + return date("D M d H:i:s O Y", $t); } // XXX: Candidate for a general utility method somewhere? @@ -543,13 +580,16 @@ class TwitterapiAction extends Action function init_twitter_atom() { $this->startXML(); - $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom', 'xml:lang' => 'en-US')); + // FIXME: don't hardcode the language here! + $this->elementStart('feed', array('xmlns' => 'http://www.w3.org/2005/Atom', + 'xml:lang' => 'en-US', + 'xmlns:thr' => 'http://purl.org/syndication/thread/1.0')); } function end_twitter_atom() { - $this->endXML(); $this->elementEnd('feed'); + $this->endXML(); } function show_profile($profile, $content_type='xml', $notice=null) @@ -615,79 +655,4 @@ class TwitterapiAction extends Action return $source_name; } - function show_extended_profile($user, $apidata) - { - - $this->auth_user = $apidata['user']; - - $profile = $user->getProfile(); - - if (!$profile) { - common_server_error(_('User has no profile.')); - return; - } - - $twitter_user = $this->twitter_user_array($profile, true); - - // Add in extended user fields offered up by this method - $twitter_user['created_at'] = $this->date_twitter($profile->created); - - $subbed = DB_DataObject::factory('subscription'); - $subbed->subscriber = $profile->id; - $subbed_count = (int) $subbed->count() - 1; - - $notices = DB_DataObject::factory('notice'); - $notices->profile_id = $profile->id; - $notice_count = (int) $notices->count(); - - $twitter_user['friends_count'] = (is_int($subbed_count)) ? $subbed_count : 0; - $twitter_user['statuses_count'] = (is_int($notice_count)) ? $notice_count : 0; - - // Other fields Twitter sends... - $twitter_user['profile_background_color'] = ''; - $twitter_user['profile_text_color'] = ''; - $twitter_user['profile_link_color'] = ''; - $twitter_user['profile_sidebar_fill_color'] = ''; - - $faves = DB_DataObject::factory('fave'); - $faves->user_id = $user->id; - $faves_count = (int) $faves->count(); - $twitter_user['favourites_count'] = $faves_count; - - $timezone = 'UTC'; - - if ($user->timezone) { - $timezone = $user->timezone; - } - - $t = new DateTime; - $t->setTimezone(new DateTimeZone($timezone)); - $twitter_user['utc_offset'] = $t->format('Z'); - $twitter_user['time_zone'] = $timezone; - - $following = 'false'; - - if (isset($this->auth_user)) { - if ($this->auth_user->isSubscribed($profile)) { - $following = 'true'; - } - - // Not implemented yet - $twitter_user['notifications'] = 'false'; - } - - $twitter_user['following'] = $following; - - if ($apidata['content-type'] == 'xml') { - $this->init_document('xml'); - $this->show_twitter_xml_user($twitter_user); - $this->end_document('xml'); - } elseif ($apidata['content-type'] == 'json') { - $this->init_document('json'); - $this->show_json_objects($twitter_user); - $this->end_document('json'); - } - - } - } diff --git a/lib/util.php b/lib/util.php index 46aa7b9df..fbef8764a 100644 --- a/lib/util.php +++ b/lib/util.php @@ -72,8 +72,7 @@ function common_timezone() } } - global $config; - return $config['site']['timezone']; + return common_config('site', 'timezone'); } function common_language() @@ -81,7 +80,7 @@ function common_language() // If there is a user logged in and they've set a language preference // then return that one... - if (common_logged_in()) { + if (_have_config() && common_logged_in()) { $user = common_current_user(); $user_language = $user->language; if ($user_language) @@ -315,6 +314,10 @@ function common_current_user() { global $_cur; + if (!_have_config()) { + return null; + } + if ($_cur === false) { if (isset($_REQUEST[session_name()]) || (isset($_SESSION['userid']) && $_SESSION['userid'])) { @@ -392,7 +395,7 @@ function common_render_text($text) return $r; } -function common_replace_urls_callback($text, $callback) { +function common_replace_urls_callback($text, $callback, $notice_id = null) { // Start off with a regex $regex = '#'. '(?:'. @@ -456,11 +459,18 @@ function common_replace_urls_callback($text, $callback) { if (!in_array($url_parts[2], $tlds)) continue; + // Make sure we didn't capture a hash tag + if (strpos($url, '#') === 0) continue; + // Put the url back the way we found it. $url = (mb_strpos($orig_url, htmlspecialchars($url)) === FALSE) ? $url:htmlspecialchars($url); // Call user specified func - $modified_url = $callback($url); + 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); @@ -474,117 +484,31 @@ function common_replace_urls_callback($text, $callback) { function common_linkify($url) { // It comes in special'd, so we unspecial it before passing to the stringifying // functions - $ext = pathinfo($url, PATHINFO_EXTENSION); $url = htmlspecialchars_decode($url); - $video_ext = array('mp4', 'flv', 'avi', 'mpg', 'mp3', 'ogg'); - $display = $url; - $url = (!preg_match('#^([a-z]+://|(mailto|aim|tel):)#i', $url)) ? 'http://'.$url : $url; - - $attrs = array('href' => $url, 'rel' => 'external'); - - if (in_array($ext, $video_ext)) { - $attrs['class'] = 'media'; - } - - if ($longurl = common_longurl($url)) { - $attrs['title'] = $longurl; + $display = File_redirection::_canonUrl($url); + $longurl_data = File_redirection::where($url); + if (is_array($longurl_data)) { + $longurl = $longurl_data['url']; + } elseif (is_string($longurl_data)) { + $longurl = $longurl_data; + } else { + die('impossible to linkify'); } + $attrs = array('href' => $longurl, 'rel' => 'external'); return XMLStringer::estring('a', $attrs, $display); } -function common_longurl($short_url) -{ - $long_url = common_shorten_link($short_url, true); - if ($long_url === $short_url) return false; - return $long_url; -} - -function common_longurl2($uri) -{ - $uri_e = urlencode($uri); - $longurl = unserialize(file_get_contents("http://api.longurl.org/v1/expand?format=php&url=$uri_e")); - if (empty($longurl['long_url']) || $uri === $longurl['long_url']) return false; - return stripslashes($longurl['long_url']); -} - function common_shorten_links($text) { if (mb_strlen($text) <= 140) return $text; - static $cache = array(); - if (isset($cache[$text])) return $cache[$text]; - // \s = not a horizontal whitespace character (since PHP 5.2.4) - return $cache[$text] = common_replace_urls_callback($text, 'common_shorten_link');; -} - -function common_shorten_link($url, $reverse = false) -{ - static $url_cache = array(); - if ($reverse) return isset($url_cache[$url]) ? $url_cache[$url] : $url; - - $user = common_current_user(); - - $curlh = curl_init(); - curl_setopt($curlh, CURLOPT_CONNECTTIMEOUT, 20); // # seconds to wait - curl_setopt($curlh, CURLOPT_USERAGENT, 'Laconica'); - curl_setopt($curlh, CURLOPT_RETURNTRANSFER, true); - - switch($user->urlshorteningservice) { - case 'ur1.ca': - $short_url_service = new LilUrl; - $short_url = $short_url_service->shorten($url); - break; - - case '2tu.us': - $short_url_service = new TightUrl; - $short_url = $short_url_service->shorten($url); - break; - - case 'ptiturl.com': - $short_url_service = new PtitUrl; - $short_url = $short_url_service->shorten($url); - break; - - case 'bit.ly': - curl_setopt($curlh, CURLOPT_URL, 'http://bit.ly/api?method=shorten&long_url='.urlencode($url)); - $short_url = current(json_decode(curl_exec($curlh))->results)->hashUrl; - break; - - case 'is.gd': - curl_setopt($curlh, CURLOPT_URL, 'http://is.gd/api.php?longurl='.urlencode($url)); - $short_url = curl_exec($curlh); - break; - case 'snipr.com': - curl_setopt($curlh, CURLOPT_URL, 'http://snipr.com/site/snip?r=simple&link='.urlencode($url)); - $short_url = curl_exec($curlh); - break; - case 'metamark.net': - curl_setopt($curlh, CURLOPT_URL, 'http://metamark.net/api/rest/simple?long_url='.urlencode($url)); - $short_url = curl_exec($curlh); - break; - case 'tinyurl.com': - curl_setopt($curlh, CURLOPT_URL, 'http://tinyurl.com/api-create.php?url='.urlencode($url)); - $short_url = curl_exec($curlh); - break; - default: - $short_url = false; - } - - curl_close($curlh); - - if ($short_url) { - $url_cache[(string)$short_url] = $url; - return (string)$short_url; - } - return $url; + return common_replace_urls_callback($text, array('File_redirection', 'makeShort')); } function common_xml_safe_str($str) { - $xmlStr = htmlentities(iconv('UTF-8', 'UTF-8//IGNORE', $str), ENT_NOQUOTES, 'UTF-8'); - - // Replace control, formatting, and surrogate characters with '*', ala Twitter - return preg_replace('/[\p{Cc}\p{Cf}\p{Cs}]/u', '*', $str); + // Neutralize control codes and surrogates + return preg_replace('/[\p{Cc}\p{Cs}]/u', '*', $str); } function common_tag_link($tag) @@ -615,10 +539,20 @@ function common_at_link($sender_id, $nickname) $sender = Profile::staticGet($sender_id); $recipient = common_relative_profile($sender, common_canonical_nickname($nickname)); if ($recipient) { + $user = User::staticGet('id', $recipient->id); + if ($user) { + $url = common_local_url('userbyid', array('id' => $user->id)); + } else { + $url = $recipient->profileurl; + } $xs = new XMLStringer(false); + $attrs = array('href' => $url, + 'class' => 'url'); + if (!empty($recipient->fullname)) { + $attrs['title'] = $recipient->fullname . ' (' . $recipient->nickname . ')'; + } $xs->elementStart('span', 'vcard'); - $xs->elementStart('a', array('href' => $recipient->profileurl, - 'class' => 'url')); + $xs->elementStart('a', $attrs); $xs->element('span', 'fn nickname', $nickname); $xs->elementEnd('a'); $xs->elementEnd('span'); @@ -633,10 +567,14 @@ function common_group_link($sender_id, $nickname) $sender = Profile::staticGet($sender_id); $group = User_group::staticGet('nickname', common_canonical_nickname($nickname)); if ($group && $sender->isMember($group)) { + $attrs = array('href' => $group->permalink(), + 'class' => 'url'); + if (!empty($group->fullname)) { + $attrs['title'] = $group->fullname . ' (' . $group->nickname . ')'; + } $xs = new XMLStringer(); $xs->elementStart('span', 'vcard'); - $xs->elementStart('a', array('href' => $group->permalink(), - 'class' => 'url')); + $xs->elementStart('a', $attrs); $xs->element('span', 'fn nickname', $nickname); $xs->elementEnd('a'); $xs->elementEnd('span'); @@ -675,7 +613,7 @@ function common_relative_profile($sender, $nickname, $dt=null) $recipient = new Profile(); // XXX: use a join instead of a subquery $recipient->whereAdd('EXISTS (SELECT subscribed from subscription where subscriber = '.$sender->id.' and subscribed = id)', 'AND'); - $recipient->whereAdd('nickname = "' . trim($nickname) . '"', 'AND'); + $recipient->whereAdd("nickname = '" . trim($nickname) . "'", 'AND'); if ($recipient->find(true)) { // XXX: should probably differentiate between profiles with // the same name by date of most recent update @@ -685,7 +623,7 @@ function common_relative_profile($sender, $nickname, $dt=null) $recipient = new Profile(); // XXX: use a join instead of a subquery $recipient->whereAdd('EXISTS (SELECT subscriber from subscription where subscribed = '.$sender->id.' and subscriber = id)', 'AND'); - $recipient->whereAdd('nickname = "' . trim($nickname) . '"', 'AND'); + $recipient->whereAdd("nickname = '" . trim($nickname) . "'", 'AND'); if ($recipient->find(true)) { // XXX: should probably differentiate between profiles with // the same name by date of most recent update @@ -705,25 +643,48 @@ function common_relative_profile($sender, $nickname, $dt=null) return null; } -function common_local_url($action, $args=null, $fragment=null) +function common_local_url($action, $args=null, $params=null, $fragment=null) { + static $sensitive = array('login', 'register', 'passwordsettings', + 'twittersettings', 'finishopenidlogin', + 'finishaddopenid', 'api'); + $r = Router::get(); - $path = $r->build($action, $args, $fragment); - if ($path) { - } + $path = $r->build($action, $args, $params, $fragment); + + $ssl = in_array($action, $sensitive); + if (common_config('site','fancy')) { - $url = common_path(mb_substr($path, 1)); + $url = common_path(mb_substr($path, 1), $ssl); } else { - $url = common_path('index.php'.$path); + if (mb_strpos($path, '/index.php') === 0) { + $url = common_path(mb_substr($path, 1), $ssl); + } else { + $url = common_path('index.php'.$path, $ssl); + } } return $url; } -function common_path($relative) +function common_path($relative, $ssl=false) { - global $config; - $pathpart = ($config['site']['path']) ? $config['site']['path']."/" : ''; - return "http://".$config['site']['server'].'/'.$pathpart.$relative; + $pathpart = (common_config('site', 'path')) ? common_config('site', 'path')."/" : ''; + + if (($ssl && (common_config('site', 'ssl') === 'sometimes')) + || common_config('site', 'ssl') === 'always') { + $proto = 'https'; + if (is_string(common_config('site', 'sslserver')) && + mb_strlen(common_config('site', 'sslserver')) > 0) { + $serverpart = common_config('site', 'sslserver'); + } else { + $serverpart = common_config('site', 'server'); + } + } else { + $proto = 'http'; + $serverpart = common_config('site', 'server'); + } + + return $proto.'://'.$serverpart.'/'.$pathpart.$relative; } function common_date_string($dt) @@ -813,7 +774,7 @@ function common_redirect($url, $code=307) 303 => "See Other", 307 => "Temporary Redirect"); - header("Status: ${code} $status[$code]"); + header('HTTP/1.1 '.$code.' '.$status[$code]); header("Location: $url"); $xo = new XMLOutputter(); @@ -839,18 +800,76 @@ function common_broadcast_notice($notice, $remote=false) function common_enqueue_notice($notice) { - foreach (array('jabber', 'omb', 'sms', 'public', 'twitter', 'facebook') as $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); - return false; + if (common_config('queue','subsystem') == 'stomp') { + // use an external message queue system via STOMP + require_once("Stomp.php"); + $con = new Stomp(common_config('queue','stomp_server')); + if (!$con->connect()) { + common_log(LOG_ERR, 'Failed to connect to queue server'); + return false; + } + $queue_basename = common_config('queue','queue_basename'); + foreach (array('jabber', 'omb', 'sms', 'public', 'twitter', 'facebook', 'ping') as $transport) { + if (!$con->send( + '/queue/'.$queue_basename.'-'.$transport, // QUEUE + $notice->id, // BODY of the message + array ( // HEADERS of the msg + 'created' => $notice->created + ))) { + common_log(LOG_ERR, 'Error sending to '.$transport.' queue'); + return false; + } + common_log(LOG_DEBUG, 'complete remote queueing notice ID = ' . $notice->id . ' for ' . $transport); + } + + //send tags as headers, so they can be used as JMS selectors + common_log(LOG_DEBUG, 'searching for tags ' . $notice->id); + $tags = array(); + $tag = new Notice_tag(); + $tag->notice_id = $notice->id; + if ($tag->find()) { + while ($tag->fetch()) { + common_log(LOG_DEBUG, 'tag found = ' . $tag->tag); + array_push($tags,$tag->tag); + } + } + $tag->free(); + + $con->send('/topic/laconica.'.$notice->profile_id, + $notice->content, + array( + 'profile_id' => $notice->profile_id, + 'created' => $notice->created, + 'tags' => implode($tags,' - ') + ) + ); + common_log(LOG_DEBUG, 'sent to personal topic ' . $notice->id); + $con->send('/topic/laconica.allusers', + $notice->content, + array( + 'profile_id' => $notice->profile_id, + 'created' => $notice->created, + 'tags' => implode($tags,' - ') + ) + ); + common_log(LOG_DEBUG, 'sent to catch-all topic ' . $notice->id); + $result = true; + } + else { + // in any other case, 'internal' + foreach (array('jabber', 'omb', 'sms', 'public', 'twitter', 'facebook', 'ping') as $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); + return false; + } + common_log(LOG_DEBUG, 'complete queueing notice ID = ' . $notice->id . ' for ' . $transport); } - common_log(LOG_DEBUG, 'complete queueing notice ID = ' . $notice->id . ' for ' . $transport); } return $result; } @@ -915,9 +934,9 @@ function common_profile_url($nickname) // Should make up a reasonable root URL -function common_root_url() +function common_root_url($ssl=false) { - return common_path(''); + return common_path('', $ssl); } // returns $bytes bytes of random data as a hexadecimal string @@ -926,7 +945,7 @@ function common_root_url() function common_good_rand($bytes) { // XXX: use random.org...? - if (file_exists('/dev/urandom')) { + if (@file_exists('/dev/urandom')) { return common_urandom($bytes); } else { // FIXME: this is probably not good enough return common_mtrand($bytes); @@ -976,8 +995,7 @@ function common_ensure_syslog() { static $initialized = false; if (!$initialized) { - global $config; - openlog($config['syslog']['appname'], 0, LOG_USER); + openlog(common_config('syslog', 'appname'), 0, LOG_USER); $initialized = true; } } @@ -1060,7 +1078,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; @@ -1287,3 +1305,16 @@ function common_compatible_license($from, $to) // XXX: better compatibility check needed here! return ($from == $to); } + +/** + * returns a quoted table name, if required according to config + */ +function common_database_tablename($tablename) +{ + + if(common_config('db','quote_identifiers')) { + $tablename = '"'. $tablename .'"'; + } + //table prefixes could be added here later + return $tablename; +} |