Merge branch '0.8.x' into stats

Conflicts:
	README
This commit is contained in:
Evan Prodromou 2009-05-25 22:47:23 -04:00
commit 76aa85fe5e
171 changed files with 16171 additions and 1178 deletions

3
.gitignore vendored
View File

@ -13,3 +13,6 @@ dataobject.ini
*.rej *.rej
.#* .#*
*.swp *.swp
.buildpath
.project
.settings

View File

@ -100,6 +100,20 @@ StartPublicGroupNav: Showing the public group nav menu
EndPublicGroupNav: At the end of the public group nav menu EndPublicGroupNav: At the end of the public group nav menu
- $action: the current action - $action: the current action
StartSubGroupNav: Showing the subscriptions group nav menu
- $action: the current action
EndSubGroupNav: At the end of the subscriptions group nav menu
- $action: the current action
RouterInitialized: After the router instance has been initialized RouterInitialized: After the router instance has been initialized
- $m: the Net_URL_Mapper that has just been set up - $m: the Net_URL_Mapper that has just been set up
StartLogout: Before logging out
- $action: the logout action
EndLogout: After logging out
- $action: the logout action
ArgsInitialized: After the argument array has been initialized
- $args: associative array of arguments, can be modified

3
README
View File

@ -1133,6 +1133,9 @@ welcome: nickname of a user account that sends welcome messages to new
busy servers it may be a good idea to keep that one just for busy servers it may be a good idea to keep that one just for
'urgent' messages. Default is null; no message. 'urgent' messages. Default is null; no message.
If either of these special user accounts are specified, the users should
be created before the configuration is updated.
snapshot snapshot
-------- --------

View File

@ -59,7 +59,7 @@ class AccesstokenAction extends Action
try { try {
common_debug('getting request from env variables', __FILE__); common_debug('getting request from env variables', __FILE__);
common_remove_magic_from_request(); common_remove_magic_from_request();
$req = OAuthRequest::from_request('POST', common_locale_url('accesstoken')); $req = OAuthRequest::from_request('POST', common_local_url('accesstoken'));
common_debug('getting a server', __FILE__); common_debug('getting a server', __FILE__);
$server = omb_oauth_server(); $server = omb_oauth_server();
common_debug('fetching the access token', __FILE__); common_debug('fetching the access token', __FILE__);

View File

@ -93,7 +93,7 @@ class AllAction extends ProfileAction
if (common_logged_in()) { if (common_logged_in()) {
$current_user = common_current_user(); $current_user = common_current_user();
if ($this->user->id === $current_user->id) { if ($this->user->id === $current_user->id) {
$message .= _('Try subscribing to more people, [join a group](%%action.groups) or post something yourself.'); $message .= _('Try subscribing to more people, [join a group](%%action.groups%%) or post something yourself.');
} else { } else {
$message .= sprintf(_('You can try to [nudge %s](../%s) from his profile or [post something to his or her attention](%%%%action.newnotice%%%%?status_textarea=%s).'), $this->user->nickname, $this->user->nickname, '@' . $this->user->nickname); $message .= sprintf(_('You can try to [nudge %s](../%s) from his profile or [post something to his or her attention](%%%%action.newnotice%%%%?status_textarea=%s).'), $this->user->nickname, $this->user->nickname, '@' . $this->user->nickname);
} }

View File

@ -130,6 +130,7 @@ class ApiAction extends Action
'statuses/friends_timeline', 'statuses/friends_timeline',
'statuses/friends', 'statuses/friends',
'statuses/replies', 'statuses/replies',
'statuses/mentions',
'statuses/followers', 'statuses/followers',
'favorites/favorites'); 'favorites/favorites');

209
actions/attachment.php Normal file
View File

@ -0,0 +1,209 @@
<?php
/**
* Laconica, the distributed open-source microblogging tool
*
* Show notice attachments
*
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Personal
* @package Laconica
* @author Evan Prodromou <evan@controlyourself.ca>
* @copyright 2008-2009 Control Yourself, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
if (!defined('LACONICA')) {
exit(1);
}
//require_once INSTALLDIR.'/lib/personalgroupnav.php';
//require_once INSTALLDIR.'/lib/feedlist.php';
require_once INSTALLDIR.'/lib/attachmentlist.php';
/**
* Show notice attachments
*
* @category Personal
* @package Laconica
* @author Evan Prodromou <evan@controlyourself.ca>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
class AttachmentAction extends Action
{
/**
* Attachment object to show
*/
var $attachment = null;
/**
* Load attributes based on database arguments
*
* Loads all the DB stuff
*
* @param array $args $_REQUEST array
*
* @return success flag
*/
function prepare($args)
{
parent::prepare($args);
$id = $this->arg('attachment');
$this->attachment = File::staticGet($id);
if (!$this->attachment) {
$this->clientError(_('No such attachment.'), 404);
return false;
}
return true;
}
/**
* Is this action read-only?
*
* @return boolean true
*/
function isReadOnly($args)
{
return true;
}
/**
* Title of the page
*
* @return string title of the page
*/
function title()
{
$a = new Attachment($this->attachment);
return $a->title();
}
/**
* Last-modified date for page
*
* When was the content of this page last modified? Based on notice,
* profile, avatar.
*
* @return int last-modified date as unix timestamp
*/
/*
function lastModified()
{
return max(strtotime($this->notice->created),
strtotime($this->profile->modified),
($this->avatar) ? strtotime($this->avatar->modified) : 0);
}
*/
/**
* An entity tag for this page
*
* Shows the ETag for the page, based on the notice ID and timestamps
* for the notice, profile, and avatar. It's weak, since we change
* the date text "one hour ago", etc.
*
* @return string etag
*/
/*
function etag()
{
$avtime = ($this->avatar) ?
strtotime($this->avatar->modified) : 0;
return 'W/"' . implode(':', array($this->arg('action'),
common_language(),
$this->notice->id,
strtotime($this->notice->created),
strtotime($this->profile->modified),
$avtime)) . '"';
}
*/
/**
* Handle input
*
* Only handles get, so just show the page.
*
* @param array $args $_REQUEST data (unused)
*
* @return void
*/
function handle($args)
{
parent::handle($args);
$this->showPage();
}
/**
* Don't show local navigation
*
* @return void
*/
function showLocalNavBlock()
{
}
/**
* Fill the content area of the page
*
* Shows a single notice list item.
*
* @return void
*/
function showContent()
{
$this->elementStart('ul', array('class' => 'attachments'));
$ali = new Attachment($this->attachment, $this);
$cnt = $ali->show();
$this->elementEnd('ul');
}
/**
* Don't show page notice
*
* @return void
*/
function showPageNoticeBlock()
{
}
/**
* Show aside: this attachments appears in what notices
*
* @return void
*/
function showSections() {
$ns = new AttachmentNoticeSection($this);
$ns->show();
$atcs = new AttachmentTagCloudSection($this);
$atcs->show();
}
}

141
actions/attachment_ajax.php Normal file
View File

@ -0,0 +1,141 @@
<?php
/**
* Laconica, the distributed open-source microblogging tool
*
* Show notice attachments
*
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Personal
* @package Laconica
* @author Evan Prodromou <evan@controlyourself.ca>
* @copyright 2008-2009 Control Yourself, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
if (!defined('LACONICA')) {
exit(1);
}
require_once INSTALLDIR.'/actions/attachment.php';
/**
* Show notice attachments
*
* @category Personal
* @package Laconica
* @author Evan Prodromou <evan@controlyourself.ca>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
class Attachment_ajaxAction extends AttachmentAction
{
/**
* Load attributes based on database arguments
*
* Loads all the DB stuff
*
* @param array $args $_REQUEST array
*
* @return success flag
*/
function prepare($args)
{
parent::prepare($args);
if (!$this->attachment) {
$this->clientError(_('No such attachment.'), 404);
return false;
}
return true;
}
/**
* Show page, a template method.
*
* @return nothing
*/
function showPage()
{
if (Event::handle('StartShowBody', array($this))) {
$this->showCore();
Event::handle('EndShowBody', array($this));
}
}
/**
* Show core.
*
* Shows local navigation, content block and aside.
*
* @return nothing
*/
function showCore()
{
$this->elementStart('div', array('id' => 'core'));
if (Event::handle('StartShowContentBlock', array($this))) {
$this->showContentBlock();
Event::handle('EndShowContentBlock', array($this));
}
$this->elementEnd('div');
}
/**
* Last-modified date for page
*
* When was the content of this page last modified? Based on notice,
* profile, avatar.
*
* @return int last-modified date as unix timestamp
*/
/*
function lastModified()
{
return max(strtotime($this->notice->created),
strtotime($this->profile->modified),
($this->avatar) ? strtotime($this->avatar->modified) : 0);
}
*/
/**
* An entity tag for this page
*
* Shows the ETag for the page, based on the notice ID and timestamps
* for the notice, profile, and avatar. It's weak, since we change
* the date text "one hour ago", etc.
*
* @return string etag
*/
/*
function etag()
{
$avtime = ($this->avatar) ?
strtotime($this->avatar->modified) : 0;
return 'W/"' . implode(':', array($this->arg('action'),
common_language(),
$this->notice->id,
strtotime($this->notice->created),
strtotime($this->profile->modified),
$avtime)) . '"';
}
*/
}

292
actions/attachments.php Normal file
View File

@ -0,0 +1,292 @@
<?php
/**
* Laconica, the distributed open-source microblogging tool
*
* Show notice attachments
*
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Personal
* @package Laconica
* @author Evan Prodromou <evan@controlyourself.ca>
* @copyright 2008-2009 Control Yourself, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
if (!defined('LACONICA')) {
exit(1);
}
//require_once INSTALLDIR.'/lib/personalgroupnav.php';
//require_once INSTALLDIR.'/lib/feedlist.php';
require_once INSTALLDIR.'/lib/attachmentlist.php';
/**
* Show notice attachments
*
* @category Personal
* @package Laconica
* @author Evan Prodromou <evan@controlyourself.ca>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
class AttachmentsAction extends Action
{
/**
* Notice object to show
*/
var $notice = null;
/**
* Profile of the notice object
*/
var $profile = null;
/**
* Avatar of the profile of the notice object
*/
var $avatar = null;
/**
* Is this action read-only?
*
* @return boolean true
*/
function isReadOnly($args)
{
return true;
}
/**
* Last-modified date for page
*
* When was the content of this page last modified? Based on notice,
* profile, avatar.
*
* @return int last-modified date as unix timestamp
*/
function lastModified()
{
return max(strtotime($this->notice->created),
strtotime($this->profile->modified),
($this->avatar) ? strtotime($this->avatar->modified) : 0);
}
/**
* An entity tag for this page
*
* Shows the ETag for the page, based on the notice ID and timestamps
* for the notice, profile, and avatar. It's weak, since we change
* the date text "one hour ago", etc.
*
* @return string etag
*/
function etag()
{
$avtime = ($this->avatar) ?
strtotime($this->avatar->modified) : 0;
return 'W/"' . implode(':', array($this->arg('action'),
common_language(),
$this->notice->id,
strtotime($this->notice->created),
strtotime($this->profile->modified),
$avtime)) . '"';
}
/**
* Title of the page
*
* @return string title of the page
*/
function title()
{
return sprintf(_('%1$s\'s status on %2$s'),
$this->profile->nickname,
common_exact_date($this->notice->created));
}
/**
* Load attributes based on database arguments
*
* Loads all the DB stuff
*
* @param array $args $_REQUEST array
*
* @return success flag
*/
function prepare($args)
{
parent::prepare($args);
$id = $this->arg('notice');
$this->notice = Notice::staticGet($id);
if (!$this->notice) {
$this->clientError(_('No such notice.'), 404);
return false;
}
/*
// STOP if there are no attachments
// maybe even redirect if there's a single one
// RYM FIXME TODO
$this->clientError(_('No such attachment.'), 404);
return false;
*/
$this->profile = $this->notice->getProfile();
if (!$this->profile) {
$this->serverError(_('Notice has no profile'), 500);
return false;
}
$this->avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE);
return true;
}
/**
* Handle input
*
* Only handles get, so just show the page.
*
* @param array $args $_REQUEST data (unused)
*
* @return void
*/
function handle($args)
{
parent::handle($args);
if ($this->notice->is_local == 0) {
if (!empty($this->notice->url)) {
common_redirect($this->notice->url, 301);
} else if (!empty($this->notice->uri) && preg_match('/^https?:/', $this->notice->uri)) {
common_redirect($this->notice->uri, 301);
}
} else {
$f2p = new File_to_post;
$f2p->post_id = $this->notice->id;
$file = new File;
$file->joinAdd($f2p);
$file->selectAdd();
$file->selectAdd('file.id as id');
$count = $file->find(true);
if (!$count) return;
if (1 === $count) {
common_redirect(common_local_url('attachment', array('attachment' => $file->id)), 301);
} else {
$this->showPage();
}
}
}
/**
* Don't show local navigation
*
* @return void
*/
function showLocalNavBlock()
{
}
/**
* Fill the content area of the page
*
* Shows a single notice list item.
*
* @return void
*/
function showContent()
{
$al = new AttachmentList($this->notice, $this);
$cnt = $al->show();
}
/**
* Don't show page notice
*
* @return void
*/
function showPageNoticeBlock()
{
}
/**
* Don't show aside
*
* @return void
*/
function showAside() {
}
/**
* Extra <head> content
*
* We show the microid(s) for the author, if any.
*
* @return void
*/
function extraHead()
{
$user = User::staticGet($this->profile->id);
if (!$user) {
return;
}
if ($user->emailmicroid && $user->email && $this->notice->uri) {
$id = new Microid('mailto:'. $user->email,
$this->notice->uri);
$this->element('meta', array('name' => 'microid',
'content' => $id->toString()));
}
if ($user->jabbermicroid && $user->jabber && $this->notice->uri) {
$id = new Microid('xmpp:', $user->jabber,
$this->notice->uri);
$this->element('meta', array('name' => 'microid',
'content' => $id->toString()));
}
}
}

View File

@ -0,0 +1,115 @@
<?php
/**
* Laconica, the distributed open-source microblogging tool
*
* Show notice attachments
*
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Personal
* @package Laconica
* @author Evan Prodromou <evan@controlyourself.ca>
* @copyright 2008-2009 Control Yourself, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
if (!defined('LACONICA')) {
exit(1);
}
//require_once INSTALLDIR.'/lib/personalgroupnav.php';
//require_once INSTALLDIR.'/lib/feedlist.php';
require_once INSTALLDIR.'/actions/attachments.php';
/**
* Show notice attachments
*
* @category Personal
* @package Laconica
* @author Evan Prodromou <evan@controlyourself.ca>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
class Attachments_ajaxAction extends AttachmentsAction
{
function showContent()
{
}
/**
* Fill the content area of the page
*
* Shows a single notice list item.
*
* @return void
*/
function showContentBlock()
{
$al = new AttachmentList($this->notice, $this);
$cnt = $al->show();
}
/**
* Extra <head> content
*
* We show the microid(s) for the author, if any.
*
* @return void
*/
function extraHead()
{
}
/**
* Show page, a template method.
*
* @return nothing
*/
function showPage()
{
if (Event::handle('StartShowBody', array($this))) {
$this->showCore();
Event::handle('EndShowBody', array($this));
}
}
/**
* Show core.
*
* Shows local navigation, content block and aside.
*
* @return nothing
*/
function showCore()
{
$this->elementStart('div', array('id' => 'core'));
if (Event::handle('StartShowContentBlock', array($this))) {
$this->showContentBlock();
Event::handle('EndShowContentBlock', array($this));
}
$this->elementEnd('div');
}
}

View File

@ -11,7 +11,7 @@
* @link http://laconi.ca/ * @link http://laconi.ca/
* *
* Laconica - a distributed open-source microblogging tool * Laconica - a distributed open-source microblogging tool
* Copyright (C) 2008, Controlez-Vous, Inc. * Copyright (C) 2009, Control Yourself, Inc.
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU Affero General Public License as published by
@ -31,7 +31,7 @@ if (!defined('LACONICA')) {
exit(1); exit(1);
} }
require_once(INSTALLDIR.'/lib/noticelist.php'); require_once INSTALLDIR.'/lib/noticelist.php';
/** /**
* Conversation tree in the browser * Conversation tree in the browser
@ -42,9 +42,10 @@ require_once(INSTALLDIR.'/lib/noticelist.php');
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://laconi.ca/ * @link http://laconi.ca/
*/ */
class ConversationAction extends Action class ConversationAction extends Action
{ {
var $id = null; var $id = null;
var $page = null; var $page = null;
/** /**
@ -69,24 +70,47 @@ class ConversationAction extends Action
return true; return true;
} }
/**
* Handle the action
*
* @param array $args Web and URL arguments
*
* @return void
*/
function handle($args) function handle($args)
{ {
parent::handle($args); parent::handle($args);
$this->showPage(); $this->showPage();
} }
/**
* Returns the page title
*
* @return string page title
*/
function title() function title()
{ {
return _("Conversation"); return _("Conversation");
} }
/**
* Show content.
*
* Display a hierarchical unordered list in the content area.
* Uses ConversationTree to do most of the heavy lifting.
*
* @return void
*/
function showContent() function showContent()
{ {
// FIXME this needs to be a tree, not a list // FIXME this needs to be a tree, not a list
$qry = 'SELECT * FROM notice WHERE conversation = %s '; $qry = 'SELECT * FROM notice WHERE conversation = %s ';
$offset = ($this->page-1)*NOTICES_PER_PAGE; $offset = ($this->page-1) * NOTICES_PER_PAGE;
$limit = NOTICES_PER_PAGE + 1; $limit = NOTICES_PER_PAGE + 1;
$txt = sprintf($qry, $this->id); $txt = sprintf($qry, $this->id);
@ -95,9 +119,9 @@ class ConversationAction extends Action
'notice:conversation:'.$this->id, 'notice:conversation:'.$this->id,
$offset, $limit); $offset, $limit);
$nl = new NoticeList($notices, $this); $ct = new ConversationTree($notices, $this);
$cnt = $nl->show(); $cnt = $ct->show();
$this->pagination($this->page > 1, $cnt > NOTICES_PER_PAGE, $this->pagination($this->page > 1, $cnt > NOTICES_PER_PAGE,
$this->page, 'conversation', array('id' => $this->id)); $this->page, 'conversation', array('id' => $this->id));
@ -105,3 +129,170 @@ class ConversationAction extends Action
} }
/**
* Conversation tree
*
* The widget class for displaying a hierarchical list of notices.
*
* @category Widget
* @package Laconica
* @author Evan Prodromou <evan@controlyourself.ca>
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://laconi.ca/
*/
class ConversationTree extends NoticeList
{
var $tree = null;
var $table = null;
/**
* Show the tree of notices
*
* @return void
*/
function show()
{
$cnt = 0;
$this->tree = array();
$this->table = array();
while ($this->notice->fetch()) {
$cnt++;
$id = $this->notice->id;
$notice = clone($this->notice);
$this->table[$id] = $notice;
if (is_null($notice->reply_to)) {
$this->tree['root'] = array($notice->id);
} else if (array_key_exists($notice->reply_to, $this->tree)) {
$this->tree[$notice->reply_to][] = $notice->id;
} else {
$this->tree[$notice->reply_to] = array($notice->id);
}
}
$this->out->elementStart('div', array('id' =>'notices_primary'));
$this->out->element('h2', null, _('Notices'));
$this->out->elementStart('ul', array('class' => 'notices'));
if (array_key_exists('root', $this->tree)) {
$rootid = $this->tree['root'][0];
$this->showNoticePlus($rootid);
}
$this->out->elementEnd('ul');
$this->out->elementEnd('div');
return $cnt;
}
/**
* Shows a notice plus its list of children.
*
* @param integer $id ID of the notice to show
*
* @return void
*/
function showNoticePlus($id)
{
$notice = $this->table[$id];
// We take responsibility for doing the li
$this->out->elementStart('li', array('class' => 'hentry notice',
'id' => 'notice-' . $this->notice->id));
$item = $this->newListItem($notice);
$item->show();
if (array_key_exists($id, $this->tree)) {
$children = $this->tree[$id];
$this->out->elementStart('ul', array('class' => 'notices'));
foreach ($children as $child) {
$this->showNoticePlus($child);
}
$this->out->elementEnd('ul');
}
$this->out->elementEnd('li');
}
/**
* Override parent class to return our preferred item.
*
* @param Notice $notice Notice to display
*
* @return NoticeListItem a list item to show
*/
function newListItem($notice)
{
return new ConversationTreeItem($notice, $this->out);
}
}
/**
* Conversation tree list item
*
* Special class of NoticeListItem for use inside conversation trees.
*
* @category Widget
* @package Laconica
* @author Evan Prodromou <evan@controlyourself.ca>
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://laconi.ca/
*/
class ConversationTreeItem extends NoticeListItem
{
/**
* start a single notice.
*
* The default creates the <li>; we skip, since the ConversationTree
* takes care of that.
*
* @return void
*/
function showStart()
{
return;
}
/**
* finish the notice
*
* The default closes the <li>; we skip, since the ConversationTree
* takes care of that.
*
* @return void
*/
function showEnd()
{
return;
}
/**
* show link to notice conversation page
*
* Since we're only used on the conversation page, we skip this
*
* @return void
*/
function showContext()
{
return;
}
}

View File

@ -112,8 +112,8 @@ class DeletenoticeAction extends DeleteAction
$this->hidden('token', common_session_token()); $this->hidden('token', common_session_token());
$this->hidden('notice', $this->trimmed('notice')); $this->hidden('notice', $this->trimmed('notice'));
$this->element('p', null, _('Are you sure you want to delete this notice?')); $this->element('p', null, _('Are you sure you want to delete this notice?'));
$this->submit('form_action-yes', _('Yes'), 'submit form_action-primary', 'yes'); $this->submit('form_action-no', _('No'), 'submit form_action-primary', 'no', _("Do not delete this notice"));
$this->submit('form_action-no', _('No'), 'submit form_action-secondary', 'no'); $this->submit('form_action-yes', _('Yes'), 'submit form_action-secondary', 'yes', _('Delete this notice'));
$this->elementEnd('fieldset'); $this->elementEnd('fieldset');
$this->elementEnd('form'); $this->elementEnd('form');
} }

View File

@ -76,14 +76,22 @@ class DesignsettingsAction extends AccountSettingsAction
'action' => 'action' =>
common_local_url('designsettings'))); common_local_url('designsettings')));
$this->elementStart('fieldset'); $this->elementStart('fieldset');
// $this->element('legend', null, _('Design settings'));
$this->hidden('token', common_session_token()); $this->hidden('token', common_session_token());
$this->elementStart('fieldset', array('id' => 'settings_design_background-image')); $this->elementStart('fieldset', array('id' => 'settings_design_background-image'));
$this->element('legend', null, _('Change background image')); $this->element('legend', null, _('Change background image'));
$this->elementStart('ul', 'form_data'); $this->elementStart('ul', 'form_data');
$this->elementStart('li'); $this->elementStart('li');
$this->element('p', null, _('Upload background image')); $this->element('label', array('for' => 'design_ background-image_file'),
_('Upload file'));
$this->element('input', array('name' => 'design_background-image_file',
'type' => 'file',
'id' => 'design_background-image_file'));
$this->element('p', 'form_guide', _('You can upload your personal background image. The maximum file size is 2Mb.'));
$this->element('input', array('name' => 'MAX_FILE_SIZE',
'type' => 'hidden',
'id' => 'MAX_FILE_SIZE',
'value' => ImageFile::maxFileSizeInt()));
$this->elementEnd('li'); $this->elementEnd('li');
$this->elementEnd('ul'); $this->elementEnd('ul');
$this->elementEnd('fieldset'); $this->elementEnd('fieldset');
@ -91,28 +99,59 @@ class DesignsettingsAction extends AccountSettingsAction
$this->elementStart('fieldset', array('id' => 'settings_design_color')); $this->elementStart('fieldset', array('id' => 'settings_design_color'));
$this->element('legend', null, _('Change colours')); $this->element('legend', null, _('Change colours'));
$this->elementStart('ul', 'form_data'); $this->elementStart('ul', 'form_data');
$this->elementStart('li');
$this->input('color-1', _('Background color'), '#F0F2F5', null); //This is a JSON object in the DB field. Here for testing. Remove later.
$this->elementEnd('li'); $userSwatch = '{"body":{"background-color":"#F0F2F5"},
$this->elementStart('li'); "#content":{"background-color":"#FFFFFF"},
$this->input('color-2', _('Content background color'), '#FFFFFF', null); "#aside_primary":{"background-color":"#CEE1E9"},
$this->elementEnd('li'); "html body":{"color":"#000000"},
$this->elementStart('li'); "a":{"color":"#002E6E"}}';
$this->input('color-3', _('Sidebar background color'), '#CEE1E9', null);
$this->elementEnd('li'); //Default theme swatch -- Where should this be stored?
$this->elementStart('li'); $defaultSwatch = array('body' => array('background-color' => '#F0F2F5'),
$this->input('color-4', _('Text color'), '#000000', null); '#content' => array('background-color' => '#FFFFFF'),
$this->elementEnd('li'); '#aside_primary' => array('background-color' => '#CEE1E9'),
$this->elementStart('li'); 'html body' => array('color' => '#000000'),
$this->input('color-5', _('Link color'), '#002E6E', null); 'a' => array('color' => '#002E6E'));
$this->elementEnd('li');
$userSwatch = ($userSwatch) ? json_decode($userSwatch, true) : $defaultSwatch;
$s = 0;
$labelSwatch = array('Background',
'Content',
'Sidebar',
'Text',
'Links');
foreach($userSwatch as $propertyvalue => $value) {
$foo = array_values($value);
$this->elementStart('li');
$this->element('label', array('for' => 'swatch-'.$s), _($labelSwatch[$s]));
$this->element('input', array('name' => 'swatch-'.$s, //prefer swatch[$s] ?
'type' => 'text',
'id' => 'swatch-'.$s,
'class' => 'swatch',
'maxlength' => '7',
'size' => '7',
'value' => $foo[0]));
$this->elementEnd('li');
$s++;
}
$this->elementEnd('ul'); $this->elementEnd('ul');
$this->element('div', array('id' => 'color-picker'));
$this->elementEnd('fieldset'); $this->elementEnd('fieldset');
$this->element('input', array('id' => 'settings_design_reset',
'type' => 'reset',
'value' => 'Reset',
'class' => 'submit form_action-primary',
'title' => _('Reset back to default')));
$this->submit('save', _('Save'), 'submit form_action-secondary', 'save', _('Save design'));
$this->submit('save', _('Save')); /*TODO: Check submitted form values:
json_encode(form values)
if submitted Swatch == DefaultSwatch, don't store in DB.
else store in BD
*/
$this->elementEnd('fieldset'); $this->elementEnd('fieldset');
$this->elementEnd('form'); $this->elementEnd('form');
@ -187,7 +226,7 @@ class DesignsettingsAction extends AccountSettingsAction
/** /**
* Add the jCrop stylesheet * Add the Farbtastic stylesheet
* *
* @return void * @return void
*/ */
@ -205,7 +244,7 @@ class DesignsettingsAction extends AccountSettingsAction
} }
/** /**
* Add the jCrop scripts * Add the Farbtastic scripts
* *
* @return void * @return void
*/ */
@ -214,14 +253,12 @@ class DesignsettingsAction extends AccountSettingsAction
{ {
parent::showScripts(); parent::showScripts();
// if ($this->mode == 'crop') { $farbtasticPack = common_path('js/farbtastic/farbtastic.js');
$farbtasticPack = common_path('js/farbtastic/farbtastic.js'); $farbtasticGo = common_path('js/farbtastic/farbtastic.go.js');
$farbtasticGo = common_path('js/farbtastic/farbtastic.go.js');
$this->element('script', array('type' => 'text/javascript', $this->element('script', array('type' => 'text/javascript',
'src' => $farbtasticPack)); 'src' => $farbtasticPack));
$this->element('script', array('type' => 'text/javascript', $this->element('script', array('type' => 'text/javascript',
'src' => $farbtasticGo)); 'src' => $farbtasticGo));
// }
} }
} }

View File

@ -107,7 +107,7 @@ class FavoritesrssAction extends Rss10Action
$c = array('url' => common_local_url('favoritesrss', $c = array('url' => common_local_url('favoritesrss',
array('nickname' => array('nickname' =>
$user->nickname)), $user->nickname)),
'title' => sprintf(_("%s favorite notices"), $user->nickname), 'title' => sprintf(_("%s's favorite notices"), $user->nickname),
'link' => common_local_url('showfavorites', 'link' => common_local_url('showfavorites',
array('nickname' => array('nickname' =>
$user->nickname)), $user->nickname)),

View File

@ -191,11 +191,28 @@ class FinishopenidloginAction extends Action
{ {
# FIXME: save invite code before redirect, and check here # FIXME: save invite code before redirect, and check here
if (common_config('site', 'closed') || common_config('site', 'inviteonly')) { if (common_config('site', 'closed')) {
$this->clientError(_('Registration not allowed.')); $this->clientError(_('Registration not allowed.'));
return; return;
} }
$invite = null;
if (common_config('site', 'inviteonly')) {
$code = $_SESSION['invitecode'];
if (empty($code)) {
$this->clientError(_('Registration not allowed.'));
return;
}
$invite = Invitation::staticGet($code);
if (empty($invite)) {
$this->clientError(_('Not a valid invitation code.'));
return;
}
}
$nickname = $this->trimmed('newname'); $nickname = $this->trimmed('newname');
if (!Validate::string($nickname, array('min_length' => 1, if (!Validate::string($nickname, array('min_length' => 1,
@ -257,10 +274,16 @@ class FinishopenidloginAction extends Action
# XXX: add language # XXX: add language
# XXX: add timezone # XXX: add timezone
$user = User::register(array('nickname' => $nickname, $args = array('nickname' => $nickname,
'email' => $email, 'email' => $email,
'fullname' => $fullname, 'fullname' => $fullname,
'location' => $location)); 'location' => $location);
if (!empty($invite)) {
$args['code'] = $invite->code;
}
$user = User::register($args);
$result = oid_link_user($user->id, $canonical, $display); $result = oid_link_user($user->id, $canonical, $display);

View File

@ -34,7 +34,7 @@ if (!defined('LACONICA')) {
require_once INSTALLDIR.'/lib/rssaction.php'; require_once INSTALLDIR.'/lib/rssaction.php';
define('MEMBERS_PER_SECTION', 81); define('MEMBERS_PER_SECTION', 27);
/** /**
* Group RSS feed * Group RSS feed

View File

@ -70,10 +70,20 @@ class LogoutAction extends Action
if (!common_logged_in()) { if (!common_logged_in()) {
$this->clientError(_('Not logged in.')); $this->clientError(_('Not logged in.'));
} else { } else {
common_set_user(null); if (Event::handle('StartLogout', array($this))) {
common_real_login(false); // not logged in $this->logout();
common_forgetme(); // don't log back in! }
Event::handle('EndLogout', array($this));
common_redirect(common_local_url('public'), 303); common_redirect(common_local_url('public'), 303);
} }
} }
function logout()
{
common_set_user(null);
common_real_login(false); // not logged in
common_forgetme(); // don't log back in!
}
} }

View File

@ -172,15 +172,54 @@ class NewmessageAction extends Action
$this->notify($user, $this->other, $message); $this->notify($user, $this->other, $message);
$url = common_local_url('outbox', array('nickname' => $user->nickname)); if ($this->boolean('ajax')) {
$this->startHTML('text/xml;charset=utf-8');
$this->elementStart('head');
$this->element('title', null, _('Message sent'));
$this->elementEnd('head');
$this->elementStart('body');
$this->element('p', array('id' => 'command_result'),
sprintf(_('Direct message to %s sent'),
$this->other->nickname));
$this->elementEnd('body');
$this->elementEnd('html');
} else {
$url = common_local_url('outbox',
array('nickname' => $user->nickname));
common_redirect($url, 303);
}
}
common_redirect($url, 303); /**
* Show an Ajax-y error message
*
* Goes back to the browser, where it's shown in a popup.
*
* @param string $msg Message to show
*
* @return void
*/
function ajaxErrorMsg($msg)
{
$this->startHTML('text/xml;charset=utf-8', true);
$this->elementStart('head');
$this->element('title', null, _('Ajax Error'));
$this->elementEnd('head');
$this->elementStart('body');
$this->element('p', array('id' => 'error'), $msg);
$this->elementEnd('body');
$this->elementEnd('html');
} }
function showForm($msg = null) function showForm($msg = null)
{ {
$this->msg = $msg; if ($msg && $this->boolean('ajax')) {
$this->ajaxErrorMsg($msg);
return;
}
$this->msg = $msg;
$this->showPage(); $this->showPage();
} }

View File

@ -158,7 +158,8 @@ class NewnoticeAction extends Action
$replyto = 'false'; $replyto = 'false';
} }
$notice = Notice::saveNew($user->id, $content, 'web', 1, // $notice = Notice::saveNew($user->id, $content_shortened, 'web', 1,
$notice = Notice::saveNew($user->id, $content_shortened, 'web', 1,
($replyto == 'false') ? null : $replyto); ($replyto == 'false') ? null : $replyto);
if (is_string($notice)) { if (is_string($notice)) {
@ -166,6 +167,8 @@ class NewnoticeAction extends Action
return; return;
} }
$this->saveUrls($notice);
common_broadcast_notice($notice); common_broadcast_notice($notice);
if ($this->boolean('ajax')) { if ($this->boolean('ajax')) {
@ -191,6 +194,24 @@ class NewnoticeAction extends Action
} }
} }
/** save all urls in the notice to the db
*
* follow redirects and save all available file information
* (mimetype, date, size, oembed, etc.)
*
* @param class $notice Notice to pull URLs from
*
* @return void
*/
function saveUrls($notice) {
common_replace_urls_callback($notice->content, array($this, 'saveUrl'), $notice->id);
}
function saveUrl($data) {
list($url, $notice_id) = $data;
$zzz = File::processNew($url, $notice_id);
}
/** /**
* Show an Ajax-y error message * Show an Ajax-y error message
* *

View File

@ -67,8 +67,8 @@ class OpenidsettingsAction extends AccountSettingsAction
function getInstructions() function getInstructions()
{ {
return _('[OpenID](%%doc.openid%%) lets you log into many sites ' . return _('[OpenID](%%doc.openid%%) lets you log into many sites' .
' with the same user account. '. ' with the same user account.'.
' Manage your associated OpenIDs from here.'); ' Manage your associated OpenIDs from here.');
} }

View File

@ -91,67 +91,68 @@ class ProfilesettingsAction extends AccountSettingsAction
$this->element('legend', null, _('Profile information')); $this->element('legend', null, _('Profile information'));
$this->hidden('token', common_session_token()); $this->hidden('token', common_session_token());
# too much common patterns here... abstractable? // too much common patterns here... abstractable?
$this->elementStart('ul', 'form_data'); $this->elementStart('ul', 'form_data');
$this->elementStart('li'); if (Event::handle('StartProfileFormData', array($this))) {
$this->input('nickname', _('Nickname'), $this->elementStart('li');
($this->arg('nickname')) ? $this->arg('nickname') : $profile->nickname, $this->input('nickname', _('Nickname'),
_('1-64 lowercase letters or numbers, no punctuation or spaces')); ($this->arg('nickname')) ? $this->arg('nickname') : $profile->nickname,
$this->elementEnd('li'); _('1-64 lowercase letters or numbers, no punctuation or spaces'));
$this->elementStart('li'); $this->elementEnd('li');
$this->input('fullname', _('Full name'), $this->elementStart('li');
($this->arg('fullname')) ? $this->arg('fullname') : $profile->fullname); $this->input('fullname', _('Full name'),
$this->elementEnd('li'); ($this->arg('fullname')) ? $this->arg('fullname') : $profile->fullname);
$this->elementStart('li'); $this->elementEnd('li');
$this->input('homepage', _('Homepage'), $this->elementStart('li');
($this->arg('homepage')) ? $this->arg('homepage') : $profile->homepage, $this->input('homepage', _('Homepage'),
_('URL of your homepage, blog, or profile on another site')); ($this->arg('homepage')) ? $this->arg('homepage') : $profile->homepage,
$this->elementEnd('li'); _('URL of your homepage, blog, or profile on another site'));
$this->elementStart('li'); $this->elementEnd('li');
$this->textarea('bio', _('Bio'), $this->elementStart('li');
($this->arg('bio')) ? $this->arg('bio') : $profile->bio, $this->textarea('bio', _('Bio'),
_('Describe yourself and your interests in 140 chars')); ($this->arg('bio')) ? $this->arg('bio') : $profile->bio,
$this->elementEnd('li'); _('Describe yourself and your interests in 140 chars'));
$this->elementStart('li'); $this->elementEnd('li');
$this->input('location', _('Location'), $this->elementStart('li');
($this->arg('location')) ? $this->arg('location') : $profile->location, $this->input('location', _('Location'),
_('Where you are, like "City, State (or Region), Country"')); ($this->arg('location')) ? $this->arg('location') : $profile->location,
$this->elementEnd('li'); _('Where you are, like "City, State (or Region), Country"'));
$this->elementStart('li'); $this->elementEnd('li');
$this->input('tags', _('Tags'), Event::handle('EndProfileFormData', array($this));
($this->arg('tags')) ? $this->arg('tags') : implode(' ', $user->getSelfTags()), $this->elementStart('li');
_('Tags for yourself (letters, numbers, -, ., and _), comma- or space- separated')); $this->input('tags', _('Tags'),
$this->elementEnd('li'); ($this->arg('tags')) ? $this->arg('tags') : implode(' ', $user->getSelfTags()),
$this->elementStart('li'); _('Tags for yourself (letters, numbers, -, ., and _), comma- or space- separated'));
$language = common_language(); $this->elementEnd('li');
$this->dropdown('language', _('Language'), $this->elementStart('li');
get_nice_language_list(), _('Preferred language'), $language = common_language();
true, $language); $this->dropdown('language', _('Language'),
$this->elementEnd('li'); get_nice_language_list(), _('Preferred language'),
$timezone = common_timezone(); false, $language);
$timezones = array(); $this->elementEnd('li');
foreach(DateTimeZone::listIdentifiers() as $k => $v) { $timezone = common_timezone();
$timezones[$v] = $v; $timezones = array();
foreach(DateTimeZone::listIdentifiers() as $k => $v) {
$timezones[$v] = $v;
}
$this->elementStart('li');
$this->dropdown('timezone', _('Timezone'),
$timezones, _('What timezone are you normally in?'),
true, $timezone);
$this->elementEnd('li');
$this->elementStart('li');
$this->checkbox('autosubscribe',
_('Automatically subscribe to whoever '.
'subscribes to me (best for non-humans)'),
($this->arg('autosubscribe')) ?
$this->boolean('autosubscribe') : $user->autosubscribe);
$this->elementEnd('li');
} }
$this->elementStart('li');
$this->dropdown('timezone', _('Timezone'),
$timezones, _('What timezone are you normally in?'),
true, $timezone);
$this->elementEnd('li');
$this->elementStart('li');
$this->checkbox('autosubscribe',
_('Automatically subscribe to whoever '.
'subscribes to me (best for non-humans)'),
($this->arg('autosubscribe')) ?
$this->boolean('autosubscribe') : $user->autosubscribe);
$this->elementEnd('li');
$this->elementEnd('ul'); $this->elementEnd('ul');
$this->submit('save', _('Save')); $this->submit('save', _('Save'));
$this->elementEnd('fieldset'); $this->elementEnd('fieldset');
$this->elementEnd('form'); $this->elementEnd('form');
} }
/** /**
@ -165,158 +166,158 @@ class ProfilesettingsAction extends AccountSettingsAction
function handlePost() function handlePost()
{ {
# CSRF protection // CSRF protection
$token = $this->trimmed('token'); $token = $this->trimmed('token');
if (!$token || $token != common_session_token()) { if (!$token || $token != common_session_token()) {
$this->showForm(_('There was a problem with your session token. '. $this->showForm(_('There was a problem with your session token. '.
'Try again, please.')); 'Try again, please.'));
return; return;
} }
$nickname = $this->trimmed('nickname'); if (Event::handle('StartProfileSaveForm', array($this))) {
$fullname = $this->trimmed('fullname');
$homepage = $this->trimmed('homepage');
$bio = $this->trimmed('bio');
$location = $this->trimmed('location');
$autosubscribe = $this->boolean('autosubscribe');
$language = $this->trimmed('language');
$timezone = $this->trimmed('timezone');
$tagstring = $this->trimmed('tags');
# Some validation $nickname = $this->trimmed('nickname');
$fullname = $this->trimmed('fullname');
$homepage = $this->trimmed('homepage');
$bio = $this->trimmed('bio');
$location = $this->trimmed('location');
$autosubscribe = $this->boolean('autosubscribe');
$language = $this->trimmed('language');
$timezone = $this->trimmed('timezone');
$tagstring = $this->trimmed('tags');
if (!Validate::string($nickname, array('min_length' => 1, // Some validation
'max_length' => 64, if (!Validate::string($nickname, array('min_length' => 1,
'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) { 'max_length' => 64,
$this->showForm(_('Nickname must have only lowercase letters and numbers and no spaces.')); 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
return; $this->showForm(_('Nickname must have only lowercase letters and numbers and no spaces.'));
} else if (!User::allowed_nickname($nickname)) { return;
$this->showForm(_('Not a valid nickname.')); } else if (!User::allowed_nickname($nickname)) {
return; $this->showForm(_('Not a valid nickname.'));
} else if (!is_null($homepage) && (strlen($homepage) > 0) && return;
!Validate::uri($homepage, array('allowed_schemes' => array('http', 'https')))) { } else if (!is_null($homepage) && (strlen($homepage) > 0) &&
$this->showForm(_('Homepage is not a valid URL.')); !Validate::uri($homepage, array('allowed_schemes' => array('http', 'https')))) {
return; $this->showForm(_('Homepage is not a valid URL.'));
} else if (!is_null($fullname) && mb_strlen($fullname) > 255) { return;
$this->showForm(_('Full name is too long (max 255 chars).')); } else if (!is_null($fullname) && mb_strlen($fullname) > 255) {
return; $this->showForm(_('Full name is too long (max 255 chars).'));
} else if (!is_null($bio) && mb_strlen($bio) > 140) { return;
$this->showForm(_('Bio is too long (max 140 chars).')); } else if (!is_null($bio) && mb_strlen($bio) > 140) {
return; $this->showForm(_('Bio is too long (max 140 chars).'));
} else if (!is_null($location) && mb_strlen($location) > 255) { return;
$this->showForm(_('Location is too long (max 255 chars).')); } else if (!is_null($location) && mb_strlen($location) > 255) {
return; $this->showForm(_('Location is too long (max 255 chars).'));
} else if (is_null($timezone) || !in_array($timezone, DateTimeZone::listIdentifiers())) { return;
$this->showForm(_('Timezone not selected.')); } else if (is_null($timezone) || !in_array($timezone, DateTimeZone::listIdentifiers())) {
return; $this->showForm(_('Timezone not selected.'));
} else if ($this->nicknameExists($nickname)) { return;
$this->showForm(_('Nickname already in use. Try another one.')); } else if ($this->nicknameExists($nickname)) {
return; $this->showForm(_('Nickname already in use. Try another one.'));
} else if (!is_null($language) && strlen($language) > 50) { return;
$this->showForm(_('Language is too long (max 50 chars).')); } else if (!is_null($language) && strlen($language) > 50) {
return; $this->showForm(_('Language is too long (max 50 chars).'));
}
if ($tagstring) {
$tags = array_map('common_canonical_tag', preg_split('/[\s,]+/', $tagstring));
} else {
$tags = array();
}
foreach ($tags as $tag) {
if (!common_valid_profile_tag($tag)) {
$this->showForm(sprintf(_('Invalid tag: "%s"'), $tag));
return; return;
} }
}
$user = common_current_user(); if ($tagstring) {
$tags = array_map('common_canonical_tag', preg_split('/[\s,]+/', $tagstring));
$user->query('BEGIN');
if ($user->nickname != $nickname ||
$user->language != $language ||
$user->timezone != $timezone) {
common_debug('Updating user nickname from ' . $user->nickname . ' to ' . $nickname,
__FILE__);
common_debug('Updating user language from ' . $user->language . ' to ' . $language,
__FILE__);
common_debug('Updating user timezone from ' . $user->timezone . ' to ' . $timezone,
__FILE__);
$original = clone($user);
$user->nickname = $nickname;
$user->language = $language;
$user->timezone = $timezone;
$result = $user->updateKeys($original);
if ($result === false) {
common_log_db_error($user, 'UPDATE', __FILE__);
$this->serverError(_('Couldn\'t update user.'));
return;
} else { } else {
# Re-initialize language environment if it changed $tags = array();
common_init_language();
} }
}
# XXX: XOR foreach ($tags as $tag) {
if (!common_valid_profile_tag($tag)) {
$this->showForm(sprintf(_('Invalid tag: "%s"'), $tag));
return;
}
}
if ($user->autosubscribe ^ $autosubscribe) { $user = common_current_user();
$original = clone($user); $user->query('BEGIN');
$user->autosubscribe = $autosubscribe; if ($user->nickname != $nickname ||
$user->language != $language ||
$user->timezone != $timezone) {
$result = $user->update($original); common_debug('Updating user nickname from ' . $user->nickname . ' to ' . $nickname,
__FILE__);
common_debug('Updating user language from ' . $user->language . ' to ' . $language,
__FILE__);
common_debug('Updating user timezone from ' . $user->timezone . ' to ' . $timezone,
__FILE__);
if ($result === false) { $original = clone($user);
common_log_db_error($user, 'UPDATE', __FILE__);
$this->serverError(_('Couldn\'t update user for autosubscribe.')); $user->nickname = $nickname;
$user->language = $language;
$user->timezone = $timezone;
$result = $user->updateKeys($original);
if ($result === false) {
common_log_db_error($user, 'UPDATE', __FILE__);
$this->serverError(_('Couldn\'t update user.'));
return;
} else {
// Re-initialize language environment if it changed
common_init_language();
}
}
// XXX: XOR
if ($user->autosubscribe ^ $autosubscribe) {
$original = clone($user);
$user->autosubscribe = $autosubscribe;
$result = $user->update($original);
if ($result === false) {
common_log_db_error($user, 'UPDATE', __FILE__);
$this->serverError(_('Couldn\'t update user for autosubscribe.'));
return;
}
}
$profile = $user->getProfile();
$orig_profile = clone($profile);
$profile->nickname = $user->nickname;
$profile->fullname = $fullname;
$profile->homepage = $homepage;
$profile->bio = $bio;
$profile->location = $location;
$profile->profileurl = common_profile_url($nickname);
common_debug('Old profile: ' . common_log_objstring($orig_profile), __FILE__);
common_debug('New profile: ' . common_log_objstring($profile), __FILE__);
$result = $profile->update($orig_profile);
if (!$result) {
common_log_db_error($profile, 'UPDATE', __FILE__);
$this->serverError(_('Couldn\'t save profile.'));
return; return;
} }
// Set the user tags
$result = $user->setSelfTags($tags);
if (!$result) {
$this->serverError(_('Couldn\'t save tags.'));
return;
}
$user->query('COMMIT');
Event::handle('EndProfileSaveForm', array($this));
common_broadcast_profile($profile);
$this->showForm(_('Settings saved.'), true);
} }
$profile = $user->getProfile();
$orig_profile = clone($profile);
$profile->nickname = $user->nickname;
$profile->fullname = $fullname;
$profile->homepage = $homepage;
$profile->bio = $bio;
$profile->location = $location;
$profile->profileurl = common_profile_url($nickname);
common_debug('Old profile: ' . common_log_objstring($orig_profile), __FILE__);
common_debug('New profile: ' . common_log_objstring($profile), __FILE__);
$result = $profile->update($orig_profile);
if (!$result) {
common_log_db_error($profile, 'UPDATE', __FILE__);
$this->serverError(_('Couldn\'t save profile.'));
return;
}
# Set the user tags
$result = $user->setSelfTags($tags);
if (!$result) {
$this->serverError(_('Couldn\'t save tags.'));
return;
}
$user->query('COMMIT');
common_broadcast_profile($profile);
$this->showForm(_('Settings saved.'), true);
} }
function nicknameExists($nickname) function nicknameExists($nickname)

View File

@ -151,11 +151,11 @@ class RecoverpasswordAction extends Action
$this->element('p', null, $this->element('p', null,
_('If you\'ve forgotten or lost your' . _('If you\'ve forgotten or lost your' .
' password, you can get a new one sent to' . ' password, you can get a new one sent to' .
' the email address you have stored ' . ' the email address you have stored' .
' in your account.')); ' in your account.'));
} else if ($this->mode == 'reset') { } else if ($this->mode == 'reset') {
$this->element('p', null, $this->element('p', null,
_('You\'ve been identified. Enter a ' . _('You\'ve been identified. Enter a' .
' new password below. ')); ' new password below. '));
} }
$this->elementEnd('div'); $this->elementEnd('div');

View File

@ -55,6 +55,45 @@ class RegisterAction extends Action
var $registered = false; var $registered = false;
/**
* Prepare page to run
*
*
* @param $args
* @return string title
*/
function prepare($args)
{
parent::prepare($args);
$this->code = $this->trimmed('code');
if (empty($this->code)) {
common_ensure_session();
if (array_key_exists('invitecode', $_SESSION)) {
$this->code = $_SESSION['invitecode'];
}
}
if (common_config('site', 'inviteonly') && empty($this->code)) {
$this->clientError(_('Sorry, only invited people can register.'));
return false;
}
if (!empty($this->code)) {
$this->invite = Invitation::staticGet('code', $this->code);
if (empty($this->invite)) {
$this->clientError(_('Sorry, invalid invitation code.'));
return false;
}
// Store this in case we need it
common_ensure_session();
$_SESSION['invitecode'] = $this->code;
}
return true;
}
/** /**
* Title of the page * Title of the page
* *
@ -108,109 +147,109 @@ class RegisterAction extends Action
function tryRegister() function tryRegister()
{ {
$token = $this->trimmed('token'); if (Event::handle('StartRegistrationTry', array($this))) {
if (!$token || $token != common_session_token()) { $token = $this->trimmed('token');
$this->showForm(_('There was a problem with your session token. '. if (!$token || $token != common_session_token()) {
'Try again, please.')); $this->showForm(_('There was a problem with your session token. '.
return; 'Try again, please.'));
} return;
}
$nickname = $this->trimmed('nickname'); $nickname = $this->trimmed('nickname');
$email = $this->trimmed('email'); $email = $this->trimmed('email');
$fullname = $this->trimmed('fullname'); $fullname = $this->trimmed('fullname');
$homepage = $this->trimmed('homepage'); $homepage = $this->trimmed('homepage');
$bio = $this->trimmed('bio'); $bio = $this->trimmed('bio');
$location = $this->trimmed('location'); $location = $this->trimmed('location');
// We don't trim these... whitespace is OK in a password! // We don't trim these... whitespace is OK in a password!
$password = $this->arg('password');
$confirm = $this->arg('confirm');
$password = $this->arg('password'); // invitation code, if any
$confirm = $this->arg('confirm'); $code = $this->trimmed('code');
// invitation code, if any if ($code) {
$invite = Invitation::staticGet($code);
}
$code = $this->trimmed('code'); if (common_config('site', 'inviteonly') && !($code && $invite)) {
$this->clientError(_('Sorry, only invited people can register.'));
return;
}
$invite = null; // Input scrubbing
$nickname = common_canonical_nickname($nickname);
$email = common_canonical_email($email);
if ($code) { if (!$this->boolean('license')) {
$invite = Invitation::staticGet($code); $this->showForm(_('You can\'t register if you don\'t '.
} 'agree to the license.'));
} else if ($email && !Validate::email($email, true)) {
$this->showForm(_('Not a valid email address.'));
} else if (!Validate::string($nickname, array('min_length' => 1,
'max_length' => 64,
'format' => NICKNAME_FMT))) {
$this->showForm(_('Nickname must have only lowercase letters '.
'and numbers and no spaces.'));
} else if ($this->nicknameExists($nickname)) {
$this->showForm(_('Nickname already in use. Try another one.'));
} else if (!User::allowed_nickname($nickname)) {
$this->showForm(_('Not a valid nickname.'));
} else if ($this->emailExists($email)) {
$this->showForm(_('Email address already exists.'));
} else if (!is_null($homepage) && (strlen($homepage) > 0) &&
!Validate::uri($homepage,
array('allowed_schemes' =>
array('http', 'https')))) {
$this->showForm(_('Homepage is not a valid URL.'));
return;
} else if (!is_null($fullname) && mb_strlen($fullname) > 255) {
$this->showForm(_('Full name is too long (max 255 chars).'));
return;
} else if (!is_null($bio) && mb_strlen($bio) > 140) {
$this->showForm(_('Bio is too long (max 140 chars).'));
return;
} else if (!is_null($location) && mb_strlen($location) > 255) {
$this->showForm(_('Location is too long (max 255 chars).'));
return;
} else if (strlen($password) < 6) {
$this->showForm(_('Password must be 6 or more characters.'));
return;
} else if ($password != $confirm) {
$this->showForm(_('Passwords don\'t match.'));
} else if ($user = User::register(array('nickname' => $nickname,
'password' => $password,
'email' => $email,
'fullname' => $fullname,
'homepage' => $homepage,
'bio' => $bio,
'location' => $location,
'code' => $code))) {
if (!$user) {
$this->showForm(_('Invalid username or password.'));
return;
}
// success!
if (!common_set_user($user)) {
$this->serverError(_('Error setting user.'));
return;
}
// this is a real login
common_real_login(true);
if ($this->boolean('rememberme')) {
common_debug('Adding rememberme cookie for ' . $nickname);
common_rememberme($user);
}
if (common_config('site', 'inviteonly') && !($code && !empty($invite))) { Event::handle('EndRegistrationTry', array($this));
$this->clientError(_('Sorry, only invited people can register.'));
return;
}
// Input scrubbing // Re-init language env in case it changed (not yet, but soon)
common_init_language();
$nickname = common_canonical_nickname($nickname); $this->showSuccess();
$email = common_canonical_email($email); } else {
if (!$this->boolean('license')) {
$this->showForm(_('You can\'t register if you don\'t '.
'agree to the license.'));
} else if ($email && !Validate::email($email, true)) {
$this->showForm(_('Not a valid email address.'));
} else if (!Validate::string($nickname, array('min_length' => 1,
'max_length' => 64,
'format' => NICKNAME_FMT))) {
$this->showForm(_('Nickname must have only lowercase letters '.
'and numbers and no spaces.'));
} else if ($this->nicknameExists($nickname)) {
$this->showForm(_('Nickname already in use. Try another one.'));
} else if (!User::allowed_nickname($nickname)) {
$this->showForm(_('Not a valid nickname.'));
} else if ($this->emailExists($email)) {
$this->showForm(_('Email address already exists.'));
} else if (!is_null($homepage) && (strlen($homepage) > 0) &&
!Validate::uri($homepage,
array('allowed_schemes' =>
array('http', 'https')))) {
$this->showForm(_('Homepage is not a valid URL.'));
return;
} else if (!is_null($fullname) && mb_strlen($fullname) > 255) {
$this->showForm(_('Full name is too long (max 255 chars).'));
return;
} else if (!is_null($bio) && mb_strlen($bio) > 140) {
$this->showForm(_('Bio is too long (max 140 chars).'));
return;
} else if (!is_null($location) && mb_strlen($location) > 255) {
$this->showForm(_('Location is too long (max 255 chars).'));
return;
} else if (strlen($password) < 6) {
$this->showForm(_('Password must be 6 or more characters.'));
return;
} else if ($password != $confirm) {
$this->showForm(_('Passwords don\'t match.'));
} else if ($user = User::register(array('nickname' => $nickname,
'password' => $password,
'email' => $email,
'fullname' => $fullname,
'homepage' => $homepage,
'bio' => $bio,
'location' => $location,
'code' => $code))) {
if (!$user) {
$this->showForm(_('Invalid username or password.')); $this->showForm(_('Invalid username or password.'));
return;
} }
// success!
if (!common_set_user($user)) {
$this->serverError(_('Error setting user.'));
return;
}
// this is a real login
common_real_login(true);
if ($this->boolean('rememberme')) {
common_debug('Adding rememberme cookie for ' . $nickname);
common_rememberme($user);
}
// Re-init language env in case it changed (not yet, but soon)
common_init_language();
$this->showSuccess();
} else {
$this->showForm(_('Invalid username or password.'));
} }
} }
@ -252,22 +291,24 @@ class RegisterAction extends Action
// overrrided to add entry-title class // overrrided to add entry-title class
function showPageTitle() { function showPageTitle() {
$this->element('h1', array('class' => 'entry-title'), $this->title()); if (Event::handle('StartShowPageTitle', array($this))) {
$this->element('h1', array('class' => 'entry-title'), $this->title());
}
} }
// overrided to add hentry, and content-inner class // overrided to add hentry, and content-inner class
function showContentBlock() function showContentBlock()
{ {
$this->elementStart('div', array('id' => 'content', 'class' => 'hentry')); $this->elementStart('div', array('id' => 'content', 'class' => 'hentry'));
$this->showPageTitle(); $this->showPageTitle();
$this->showPageNoticeBlock(); $this->showPageNoticeBlock();
$this->elementStart('div', array('id' => 'content_inner', $this->elementStart('div', array('id' => 'content_inner',
'class' => 'entry-content')); 'class' => 'entry-content'));
// show the actual content (forms, lists, whatever) // show the actual content (forms, lists, whatever)
$this->showContent(); $this->showContent();
$this->elementEnd('div'); $this->elementEnd('div');
$this->elementEnd('div'); $this->elementEnd('div');
} }
/** /**
* Instructions or a notice for the page * Instructions or a notice for the page
@ -362,82 +403,85 @@ class RegisterAction extends Action
$this->element('legend', null, 'Account settings'); $this->element('legend', null, 'Account settings');
$this->hidden('token', common_session_token()); $this->hidden('token', common_session_token());
if ($code) { if ($this->code) {
$this->hidden('code', $code); $this->hidden('code', $this->code);
} }
$this->elementStart('ul', 'form_data'); $this->elementStart('ul', 'form_data');
$this->elementStart('li'); if (Event::handle('StartRegistrationFormData', array($this))) {
$this->input('nickname', _('Nickname'), $this->trimmed('nickname'), $this->elementStart('li');
_('1-64 lowercase letters or numbers, '. $this->input('nickname', _('Nickname'), $this->trimmed('nickname'),
'no punctuation or spaces. Required.')); _('1-64 lowercase letters or numbers, '.
$this->elementEnd('li'); 'no punctuation or spaces. Required.'));
$this->elementStart('li'); $this->elementEnd('li');
$this->password('password', _('Password'), $this->elementStart('li');
_('6 or more characters. Required.')); $this->password('password', _('Password'),
$this->elementEnd('li'); _('6 or more characters. Required.'));
$this->elementStart('li'); $this->elementEnd('li');
$this->password('confirm', _('Confirm'), $this->elementStart('li');
_('Same as password above. Required.')); $this->password('confirm', _('Confirm'),
$this->elementEnd('li'); _('Same as password above. Required.'));
$this->elementStart('li'); $this->elementEnd('li');
if (!empty($invite) && $invite->address_type == 'email') { $this->elementStart('li');
$this->input('email', _('Email'), $invite->address, if ($this->invite && $this->invite->address_type == 'email') {
_('Used only for updates, announcements, '. $this->input('email', _('Email'), $this->invite->address,
'and password recovery')); _('Used only for updates, announcements, '.
} else { 'and password recovery'));
$this->input('email', _('Email'), $this->trimmed('email'), } else {
_('Used only for updates, announcements, '. $this->input('email', _('Email'), $this->trimmed('email'),
'and password recovery')); _('Used only for updates, announcements, '.
'and password recovery'));
}
$this->elementEnd('li');
$this->elementStart('li');
$this->input('fullname', _('Full name'),
$this->trimmed('fullname'),
_('Longer name, preferably your "real" name'));
$this->elementEnd('li');
$this->elementStart('li');
$this->input('homepage', _('Homepage'),
$this->trimmed('homepage'),
_('URL of your homepage, blog, '.
'or profile on another site'));
$this->elementEnd('li');
$this->elementStart('li');
$this->textarea('bio', _('Bio'),
$this->trimmed('bio'),
_('Describe yourself and your '.
'interests in 140 chars'));
$this->elementEnd('li');
$this->elementStart('li');
$this->input('location', _('Location'),
$this->trimmed('location'),
_('Where you are, like "City, '.
'State (or Region), Country"'));
$this->elementEnd('li');
Event::handle('EndRegistrationFormData', array($this));
$this->elementStart('li', array('id' => 'settings_rememberme'));
$this->checkbox('rememberme', _('Remember me'),
$this->boolean('rememberme'),
_('Automatically login in the future; '.
'not for shared computers!'));
$this->elementEnd('li');
$attrs = array('type' => 'checkbox',
'id' => 'license',
'class' => 'checkbox',
'name' => 'license',
'value' => 'true');
if ($this->boolean('license')) {
$attrs['checked'] = 'checked';
}
$this->elementStart('li');
$this->element('input', $attrs);
$this->elementStart('label', array('class' => 'checkbox', 'for' => 'license'));
$this->text(_('My text and files are available under '));
$this->element('a', array('href' => common_config('license', 'url')),
common_config('license', 'title'), _("Creative Commons Attribution 3.0"));
$this->text(_(' except this private data: password, '.
'email address, IM address, and phone number.'));
$this->elementEnd('label');
$this->elementEnd('li');
} }
$this->elementEnd('li');
$this->elementStart('li');
$this->input('fullname', _('Full name'),
$this->trimmed('fullname'),
_('Longer name, preferably your "real" name'));
$this->elementEnd('li');
$this->elementStart('li');
$this->input('homepage', _('Homepage'),
$this->trimmed('homepage'),
_('URL of your homepage, blog, '.
'or profile on another site'));
$this->elementEnd('li');
$this->elementStart('li');
$this->textarea('bio', _('Bio'),
$this->trimmed('bio'),
_('Describe yourself and your '.
'interests in 140 chars'));
$this->elementEnd('li');
$this->elementStart('li');
$this->input('location', _('Location'),
$this->trimmed('location'),
_('Where you are, like "City, '.
'State (or Region), Country"'));
$this->elementEnd('li');
$this->elementStart('li', array('id' => 'settings_rememberme'));
$this->checkbox('rememberme', _('Remember me'),
$this->boolean('rememberme'),
_('Automatically login in the future; '.
'not for shared computers!'));
$this->elementEnd('li');
$attrs = array('type' => 'checkbox',
'id' => 'license',
'class' => 'checkbox',
'name' => 'license',
'value' => 'true');
if ($this->boolean('license')) {
$attrs['checked'] = 'checked';
}
$this->elementStart('li');
$this->element('input', $attrs);
$this->elementStart('label', array('class' => 'checkbox', 'for' => 'license'));
$this->text(_('My text and files are available under '));
$this->element('a', array('href' => common_config('license', 'url')),
common_config('license', 'title'), _("Creative Commons Attribution 3.0"));
$this->text(_(' except this private data: password, '.
'email address, IM address, and phone number.'));
$this->elementEnd('label');
$this->elementEnd('li');
$this->elementEnd('ul'); $this->elementEnd('ul');
$this->submit('submit', _('Register')); $this->submit('submit', _('Register'));
$this->elementEnd('fieldset'); $this->elementEnd('fieldset');
@ -519,3 +563,4 @@ class RegisterAction extends Action
$nav->show(); $nav->show();
} }
} }

View File

@ -74,9 +74,9 @@ class ShowfavoritesAction extends Action
function title() function title()
{ {
if ($this->page == 1) { if ($this->page == 1) {
return sprintf(_("%s favorite notices"), $this->user->nickname); return sprintf(_("%s's favorite notices"), $this->user->nickname);
} else { } else {
return sprintf(_("%s favorite notices, page %d"), return sprintf(_("%s's favorite notices, page %d"),
$this->user->nickname, $this->user->nickname,
$this->page); $this->page);
} }

View File

@ -35,7 +35,7 @@ if (!defined('LACONICA')) {
require_once INSTALLDIR.'/lib/noticelist.php'; require_once INSTALLDIR.'/lib/noticelist.php';
require_once INSTALLDIR.'/lib/feedlist.php'; require_once INSTALLDIR.'/lib/feedlist.php';
define('MEMBERS_PER_SECTION', 81); define('MEMBERS_PER_SECTION', 27);
/** /**
* Group main page * Group main page
@ -361,7 +361,7 @@ class ShowgroupAction extends Action
$this->element('p', null, _('(None)')); $this->element('p', null, _('(None)'));
} }
if ($cnt == MEMBERS_PER_SECTION) { if ($cnt > MEMBERS_PER_SECTION) {
$this->element('a', array('href' => common_local_url('groupmembers', $this->element('a', array('href' => common_local_url('groupmembers',
array('nickname' => $this->group->nickname))), array('nickname' => $this->group->nickname))),
_('All members')); _('All members'));

View File

@ -68,6 +68,9 @@ class ShowstreamAction extends ProfileAction
} else { } else {
$base = $this->user->nickname; $base = $this->user->nickname;
} }
if (!empty($this->tag)) {
$base .= sprintf(_(' tagged %s'), $this->tag);
}
if ($this->page == 1) { if ($this->page == 1) {
return $base; return $base;
@ -110,6 +113,15 @@ class ShowstreamAction extends ProfileAction
function getFeeds() function getFeeds()
{ {
if (!empty($this->tag)) {
return array(new Feed(Feed::RSS1,
common_local_url('userrss',
array('nickname' => $this->user->nickname,
'tag' => $this->tag)),
sprintf(_('Notice feed for %s tagged %s (RSS 1.0)'),
$this->user->nickname, $this->tag)));
}
return array(new Feed(Feed::RSS1, return array(new Feed(Feed::RSS1,
common_local_url('userrss', common_local_url('userrss',
array('nickname' => $this->user->nickname)), array('nickname' => $this->user->nickname)),
@ -363,7 +375,9 @@ class ShowstreamAction extends ProfileAction
function showNotices() function showNotices()
{ {
$notice = $this->user->getNotices(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1); $notice = empty($this->tag)
? $this->user->getNotices(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1)
: $this->user->getTaggedNotices(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1, 0, 0, null, $this->tag);
$pnl = new ProfileNoticeList($notice, $this); $pnl = new ProfileNoticeList($notice, $this);
$cnt = $pnl->show(); $cnt = $pnl->show();

View File

@ -118,6 +118,16 @@ class SubscribersAction extends GalleryAction
$this->raw(common_markup_to_html($message)); $this->raw(common_markup_to_html($message));
$this->elementEnd('div'); $this->elementEnd('div');
} }
function showSections()
{
parent::showSections();
$cloud = new SubscribersPeopleTagCloudSection($this);
$cloud->show();
$cloud2 = new SubscribersPeopleSelfTagCloudSection($this);
$cloud2->show();
}
} }
class SubscribersList extends ProfileList class SubscribersList extends ProfileList

View File

@ -125,6 +125,16 @@ class SubscriptionsAction extends GalleryAction
$this->raw(common_markup_to_html($message)); $this->raw(common_markup_to_html($message));
$this->elementEnd('div'); $this->elementEnd('div');
} }
function showSections()
{
parent::showSections();
$cloud = new SubscriptionsPeopleTagCloudSection($this);
$cloud->show();
$cloud2 = new SubscriptionsPeopleSelfTagCloudSection($this);
$cloud2->show();
}
} }
class SubscriptionsList extends ProfileList class SubscriptionsList extends ProfileList

View File

@ -49,9 +49,10 @@ class TagAction extends Action
{ {
$pop = new PopularNoticeSection($this); $pop = new PopularNoticeSection($this);
$pop->show(); $pop->show();
$freqatt = new FrequentAttachmentSection($this);
$freqatt->show();
} }
function title() function title()
{ {
if ($this->page == 1) { if ($this->page == 1) {

View File

@ -133,11 +133,7 @@ class TwitapifriendshipsAction extends TwitterapiAction
return; return;
} }
if ($user_a->isSubscribed($user_b)) { $result = $user_a->isSubscribed($user_b);
$result = 'true';
} else {
$result = 'false';
}
switch ($apidata['content-type']) { switch ($apidata['content-type']) {
case 'xml': case 'xml':

View File

@ -144,10 +144,10 @@ class TwitapistatusesAction extends TwitterapiAction
break; break;
case 'atom': case 'atom':
if (isset($apidata['api_arg'])) { if (isset($apidata['api_arg'])) {
$selfuri = $selfuri = common_root_url() . $selfuri = common_root_url() .
'api/statuses/friends_timeline/' . $apidata['api_arg'] . '.atom'; 'api/statuses/friends_timeline/' . $apidata['api_arg'] . '.atom';
} else { } else {
$selfuri = $selfuri = common_root_url() . $selfuri = common_root_url() .
'api/statuses/friends_timeline.atom'; 'api/statuses/friends_timeline.atom';
} }
$this->show_atom_timeline($notice, $title, $id, $link, $subtitle, null, $selfuri); $this->show_atom_timeline($notice, $title, $id, $link, $subtitle, null, $selfuri);
@ -231,10 +231,10 @@ class TwitapistatusesAction extends TwitterapiAction
break; break;
case 'atom': case 'atom':
if (isset($apidata['api_arg'])) { if (isset($apidata['api_arg'])) {
$selfuri = $selfuri = common_root_url() . $selfuri = common_root_url() .
'api/statuses/user_timeline/' . $apidata['api_arg'] . '.atom'; 'api/statuses/user_timeline/' . $apidata['api_arg'] . '.atom';
} else { } else {
$selfuri = $selfuri = common_root_url() . $selfuri = common_root_url() .
'api/statuses/user_timeline.atom'; 'api/statuses/user_timeline.atom';
} }
$this->show_atom_timeline($notice, $title, $id, $link, $subtitle, $suplink, $selfuri); $this->show_atom_timeline($notice, $title, $id, $link, $subtitle, $suplink, $selfuri);
@ -344,7 +344,7 @@ class TwitapistatusesAction extends TwitterapiAction
$this->show($args, $apidata); $this->show($args, $apidata);
} }
function replies($args, $apidata) function mentions($args, $apidata)
{ {
parent::handle($args); parent::handle($args);
@ -360,11 +360,13 @@ class TwitapistatusesAction extends TwitterapiAction
$profile = $user->getProfile(); $profile = $user->getProfile();
$sitename = common_config('site', 'name'); $sitename = common_config('site', 'name');
$title = sprintf(_('%1$s / Updates replying to %2$s'), $sitename, $user->nickname); $title = sprintf(_('%1$s / Updates mentioning %2$s'),
$sitename, $user->nickname);
$taguribase = common_config('integration', 'taguri'); $taguribase = common_config('integration', 'taguri');
$id = "tag:$taguribase:Replies:".$user->id; $id = "tag:$taguribase:Mentions:".$user->id;
$link = common_local_url('replies', array('nickname' => $user->nickname)); $link = common_local_url('replies', array('nickname' => $user->nickname));
$subtitle = sprintf(_('%1$s updates that reply to updates from %2$s / %3$s.'), $sitename, $user->nickname, $profile->getBestName()); $subtitle = sprintf(_('%1$s updates that reply to updates from %2$s / %3$s.'),
$sitename, $user->nickname, $profile->getBestName());
if (!$page) { if (!$page) {
$page = 1; $page = 1;
@ -385,7 +387,8 @@ class TwitapistatusesAction extends TwitterapiAction
$since = strtotime($this->arg('since')); $since = strtotime($this->arg('since'));
$notice = $user->getReplies((($page-1)*20), $count, $since_id, $before_id, $since); $notice = $user->getReplies((($page-1)*20),
$count, $since_id, $before_id, $since);
$notices = array(); $notices = array();
while ($notice->fetch()) { while ($notice->fetch()) {
@ -400,14 +403,10 @@ class TwitapistatusesAction extends TwitterapiAction
$this->show_rss_timeline($notices, $title, $link, $subtitle); $this->show_rss_timeline($notices, $title, $link, $subtitle);
break; break;
case 'atom': case 'atom':
if (isset($apidata['api_arg'])) { $selfuri = common_root_url() .
$selfuri = $selfuri = common_root_url() . ltrim($_SERVER['QUERY_STRING'], 'p=');
'api/statuses/replies/' . $apidata['api_arg'] . '.atom'; $this->show_atom_timeline($notices, $title, $id, $link, $subtitle,
} else { null, $selfuri);
$selfuri = $selfuri = common_root_url() .
'api/statuses/replies.atom';
}
$this->show_atom_timeline($notices, $title, $id, $link, $subtitle, null, $selfuri);
break; break;
case 'json': case 'json':
$this->show_json_timeline($notices); $this->show_json_timeline($notices);
@ -418,6 +417,11 @@ class TwitapistatusesAction extends TwitterapiAction
} }
function replies($args, $apidata)
{
call_user_func(array($this, 'mentions'), $args, $apidata);
}
function show($args, $apidata) function show($args, $apidata)
{ {
parent::handle($args); parent::handle($args);

View File

@ -82,8 +82,8 @@ class TwitapiusersAction extends TwitterapiAction
$twitter_user['profile_text_color'] = ''; $twitter_user['profile_text_color'] = '';
$twitter_user['profile_link_color'] = ''; $twitter_user['profile_link_color'] = '';
$twitter_user['profile_sidebar_fill_color'] = ''; $twitter_user['profile_sidebar_fill_color'] = '';
$twitter_user['profile_sidebar_border_color'] = ''; $twitter_user['profile_sidebar_border_color'] = '';
$twitter_user['profile_background_tile'] = 'false'; $twitter_user['profile_background_tile'] = false;
$faves = DB_DataObject::factory('fave'); $faves = DB_DataObject::factory('fave');
$faves->user_id = $user->id; $faves->user_id = $user->id;
@ -103,24 +103,16 @@ class TwitapiusersAction extends TwitterapiAction
if (isset($apidata['user'])) { if (isset($apidata['user'])) {
if ($apidata['user']->isSubscribed($profile)) { $twitter_user['following'] = $apidata['user']->isSubscribed($profile);
$twitter_user['following'] = 'true';
} else { // Notifications on?
$twitter_user['following'] = 'false'; $sub = Subscription::pkeyGet(array('subscriber' =>
$apidata['user']->id, 'subscribed' => $profile->id));
if ($sub) {
$twitter_user['notifications'] = ($sub->jabber || $sub->sms);
} }
}
// Notifications on?
$sub = Subscription::pkeyGet(array('subscriber' =>
$apidata['user']->id, 'subscribed' => $profile->id));
if ($sub) {
if ($sub->jabber || $sub->sms) {
$twitter_user['notifications'] = 'true';
} else {
$twitter_user['notifications'] = 'false';
}
}
}
if ($apidata['content-type'] == 'xml') { if ($apidata['content-type'] == 'xml') {
$this->init_document('xml'); $this->init_document('xml');

View File

@ -138,7 +138,7 @@ class TwittersettingsAction extends ConnectSettingsAction
$this->elementStart('ul', 'form_data'); $this->elementStart('ul', 'form_data');
$this->elementStart('li'); $this->elementStart('li');
$this->checkbox('noticesync', $this->checkbox('noticesend',
_('Automatically send my notices to Twitter.'), _('Automatically send my notices to Twitter.'),
($flink) ? ($flink) ?
($flink->noticesync & FOREIGN_NOTICE_SEND) : ($flink->noticesync & FOREIGN_NOTICE_SEND) :
@ -158,6 +158,22 @@ class TwittersettingsAction extends ConnectSettingsAction
($flink->friendsync & FOREIGN_FRIEND_RECV) : ($flink->friendsync & FOREIGN_FRIEND_RECV) :
false); false);
$this->elementEnd('li'); $this->elementEnd('li');
if (common_config('twitterbridge','enabled')) {
$this->elementStart('li');
$this->checkbox('noticerecv',
_('Import my Friends Timeline.'),
($flink) ?
($flink->noticesync & FOREIGN_NOTICE_RECV) :
false);
$this->elementEnd('li');
} else {
// preserve setting even if bidrection bridge toggled off
if ($flink && ($flink->noticesync & FOREIGN_NOTICE_RECV)) {
$this->hidden('noticerecv', true, 'noticerecv');
}
}
$this->elementEnd('ul'); $this->elementEnd('ul');
if ($flink) { if ($flink) {
@ -261,7 +277,7 @@ class TwittersettingsAction extends ConnectSettingsAction
'alt' => ($other->fullname) ? 'alt' => ($other->fullname) ?
$other->fullname : $other->fullname :
$other->nickname)); $other->nickname));
$this->element('span', 'fn nickname', $other->nickname); $this->element('span', 'fn nickname', $other->nickname);
$this->elementEnd('a'); $this->elementEnd('a');
$this->elementEnd('li'); $this->elementEnd('li');
@ -320,7 +336,8 @@ class TwittersettingsAction extends ConnectSettingsAction
{ {
$screen_name = $this->trimmed('twitter_username'); $screen_name = $this->trimmed('twitter_username');
$password = $this->trimmed('twitter_password'); $password = $this->trimmed('twitter_password');
$noticesync = $this->boolean('noticesync'); $noticesend = $this->boolean('noticesend');
$noticerecv = $this->boolean('noticerecv');
$replysync = $this->boolean('replysync'); $replysync = $this->boolean('replysync');
$friendsync = $this->boolean('friendsync'); $friendsync = $this->boolean('friendsync');
@ -363,7 +380,7 @@ class TwittersettingsAction extends ConnectSettingsAction
$flink->credentials = $password; $flink->credentials = $password;
$flink->created = common_sql_now(); $flink->created = common_sql_now();
$flink->set_flags($noticesync, $replysync, $friendsync); $flink->set_flags($noticesend, $noticerecv, $replysync, $friendsync);
$flink_id = $flink->insert(); $flink_id = $flink->insert();
@ -375,6 +392,8 @@ class TwittersettingsAction extends ConnectSettingsAction
if ($friendsync) { if ($friendsync) {
save_twitter_friends($user, $twit_user->id, $screen_name, $password); save_twitter_friends($user, $twit_user->id, $screen_name, $password);
$flink->last_friendsync = common_sql_now();
$flink->update();
} }
$this->showForm(_('Twitter settings saved.'), true); $this->showForm(_('Twitter settings saved.'), true);
@ -419,7 +438,8 @@ class TwittersettingsAction extends ConnectSettingsAction
function savePreferences() function savePreferences()
{ {
$noticesync = $this->boolean('noticesync'); $noticesend = $this->boolean('noticesend');
$noticerecv = $this->boolean('noticerecv');
$friendsync = $this->boolean('friendsync'); $friendsync = $this->boolean('friendsync');
$replysync = $this->boolean('replysync'); $replysync = $this->boolean('replysync');
@ -448,7 +468,7 @@ class TwittersettingsAction extends ConnectSettingsAction
$original = clone($flink); $original = clone($flink);
$flink->set_flags($noticesync, $replysync, $friendsync); $flink->set_flags($noticesend, $noticerecv, $replysync, $friendsync);
$result = $flink->update($original); $result = $flink->update($original);

View File

@ -25,14 +25,15 @@ require_once(INSTALLDIR.'/lib/rssaction.php');
class UserrssAction extends Rss10Action class UserrssAction extends Rss10Action
{ {
var $user = null; var $user = null;
var $tag = null;
function prepare($args) function prepare($args)
{ {
parent::prepare($args); parent::prepare($args);
$nickname = $this->trimmed('nickname'); $nickname = $this->trimmed('nickname');
$this->user = User::staticGet('nickname', $nickname); $this->user = User::staticGet('nickname', $nickname);
$this->tag = $this->trimmed('tag');
if (!$this->user) { if (!$this->user) {
$this->clientError(_('No such user.')); $this->clientError(_('No such user.'));
@ -42,6 +43,25 @@ class UserrssAction extends Rss10Action
} }
} }
function getTaggedNotices($tag = null, $limit=0)
{
$user = $this->user;
if (is_null($user)) {
return null;
}
$notice = $user->getTaggedNotices(0, ($limit == 0) ? NOTICES_PER_PAGE : $limit, 0, 0, null, $tag);
$notices = array();
while ($notice->fetch()) {
$notices[] = clone($notice);
}
return $notices;
}
function getNotices($limit=0) function getNotices($limit=0)
{ {

View File

@ -4,7 +4,7 @@
*/ */
require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
class Fave extends Memcached_DataObject class Fave extends Memcached_DataObject
{ {
###START_AUTOCODE ###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */ /* the code below is auto generated do not remove the above tag */
@ -31,9 +31,58 @@ class Fave extends Memcached_DataObject
} }
return $fave; return $fave;
} }
function &pkeyGet($kv) function &pkeyGet($kv)
{ {
return Memcached_DataObject::pkeyGet('Fave', $kv); return Memcached_DataObject::pkeyGet('Fave', $kv);
} }
function stream($user_id, $offset=0, $limit=NOTICES_PER_PAGE)
{
$ids = Notice::stream(array('Fave', '_streamDirect'),
array($user_id),
'fave:ids_by_user:'.$user_id,
$offset, $limit);
return $ids;
}
function _streamDirect($user_id, $offset, $limit, $since_id, $before_id, $since)
{
$fav = new Fave();
$fav->user_id = $user_id;
$fav->selectAdd();
$fav->selectAdd('notice_id');
if ($since_id != 0) {
$fav->whereAdd('notice_id > ' . $since_id);
}
if ($before_id != 0) {
$fav->whereAdd('notice_id < ' . $before_id);
}
if (!is_null($since)) {
$fav->whereAdd('modified > \'' . date('Y-m-d H:i:s', $since) . '\'');
}
// NOTE: we sort by fave time, not by notice time!
$fav->orderBy('modified DESC');
if (!is_null($offset)) {
$fav->limit($offset, $limit);
}
$ids = array();
if ($fav->find()) {
while ($fav->fetch()) {
$ids[] = $fav->notice_id;
}
}
return $ids;
}
} }

123
classes/File.php Normal file
View File

@ -0,0 +1,123 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
* Copyright (C) 2008, Controlez-Vous, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
if (!defined('LACONICA')) { exit(1); }
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
require_once INSTALLDIR.'/classes/File_redirection.php';
require_once INSTALLDIR.'/classes/File_oembed.php';
require_once INSTALLDIR.'/classes/File_thumbnail.php';
require_once INSTALLDIR.'/classes/File_to_post.php';
//require_once INSTALLDIR.'/classes/File_redirection.php';
/**
* Table Definition for file
*/
class File extends Memcached_DataObject
{
###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */
public $__table = 'file'; // table name
public $id; // int(11) not_null primary_key group_by
public $url; // varchar(255) unique_key
public $mimetype; // varchar(50)
public $size; // int(11) group_by
public $title; // varchar(255)
public $date; // int(11) group_by
public $protected; // int(1) group_by
/* Static get */
function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('File',$k,$v); }
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
function isProtected($url) {
return 'http://www.facebook.com/login.php' === $url;
}
function getAttachments($post_id) {
$query = "select file.* from file join file_to_post on (file_id = file.id) join notice on (post_id = notice.id) where post_id = " . $this->escape($post_id);
$this->query($query);
$att = array();
while ($this->fetch()) {
$att[] = clone($this);
}
$this->free();
return $att;
}
function saveNew($redir_data, $given_url) {
$x = new File;
$x->url = $given_url;
if (!empty($redir_data['protected'])) $x->protected = $redir_data['protected'];
if (!empty($redir_data['title'])) $x->title = $redir_data['title'];
if (!empty($redir_data['type'])) $x->mimetype = $redir_data['type'];
if (!empty($redir_data['size'])) $x->size = intval($redir_data['size']);
if (isset($redir_data['time']) && $redir_data['time'] > 0) $x->date = intval($redir_data['time']);
$file_id = $x->insert();
if (isset($redir_data['type'])
&& ('text/html' === substr($redir_data['type'], 0, 9))
&& ($oembed_data = File_oembed::_getOembed($given_url))
&& isset($oembed_data['json'])) {
File_oembed::saveNew($oembed_data['json'], $file_id);
}
return $x;
}
function processNew($given_url, $notice_id) {
if (empty($given_url)) return -1; // error, no url to process
$given_url = File_redirection::_canonUrl($given_url);
if (empty($given_url)) return -1; // error, no url to process
$file = File::staticGet('url', $given_url);
if (empty($file->id)) {
$file_redir = File_redirection::staticGet('url', $given_url);
if (empty($file_redir->id)) {
$redir_data = File_redirection::where($given_url);
$redir_url = $redir_data['url'];
if ($redir_url === $given_url) {
$x = File::saveNew($redir_data, $given_url);
$file_id = $x->id;
} else {
$x = File::processNew($redir_url, $notice_id);
$file_id = $x->id;
File_redirection::saveNew($redir_data, $file_id, $given_url);
}
} else {
$file_id = $file_redir->file_id;
}
} else {
$file_id = $file->id;
$x = $file;
}
if (empty($x)) {
$x = File::staticGet($file_id);
if (empty($x)) die('Impossible!');
}
File_to_post::processNew($file_id, $notice_id);
return $x;
}
}

87
classes/File_oembed.php Normal file
View File

@ -0,0 +1,87 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
* Copyright (C) 2008, Controlez-Vous, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
if (!defined('LACONICA')) { exit(1); }
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
/**
* Table Definition for file_oembed
*/
class File_oembed extends Memcached_DataObject
{
###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */
public $__table = 'file_oembed'; // table name
public $id; // int(11) not_null primary_key group_by
public $file_id; // int(11) unique_key group_by
public $version; // varchar(20)
public $type; // varchar(20)
public $provider; // varchar(50)
public $provider_url; // varchar(255)
public $width; // int(11) group_by
public $height; // int(11) group_by
public $html; // blob(65535) blob
public $title; // varchar(255)
public $author_name; // varchar(50)
public $author_url; // varchar(255)
public $url; // varchar(255)
/* Static get */
function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('File_oembed',$k,$v); }
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
function _getOembed($url, $maxwidth = 500, $maxheight = 400, $format = 'json') {
$cmd = 'http://oohembed.com/oohembed/?url=' . urlencode($url);
if (is_int($maxwidth)) $cmd .= "&maxwidth=$maxwidth";
if (is_int($maxheight)) $cmd .= "&maxheight=$maxheight";
if (is_string($format)) $cmd .= "&format=$format";
$oe = @file_get_contents($cmd);
if (false === $oe) return false;
return array($format => (('json' === $format) ? json_decode($oe, true) : $oe));
}
function saveNew($data, $file_id) {
$file_oembed = new File_oembed;
$file_oembed->file_id = $file_id;
$file_oembed->version = $data['version'];
$file_oembed->type = $data['type'];
if (!empty($data['provider_name'])) $file_oembed->provider = $data['provider_name'];
if (!isset($file_oembed->provider) && !empty($data['provide'])) $file_oembed->provider = $data['provider'];
if (!empty($data['provide_url'])) $file_oembed->provider_url = $data['provider_url'];
if (!empty($data['width'])) $file_oembed->width = intval($data['width']);
if (!empty($data['height'])) $file_oembed->height = intval($data['height']);
if (!empty($data['html'])) $file_oembed->html = $data['html'];
if (!empty($data['title'])) $file_oembed->title = $data['title'];
if (!empty($data['author_name'])) $file_oembed->author_name = $data['author_name'];
if (!empty($data['author_url'])) $file_oembed->author_url = $data['author_url'];
if (!empty($data['url'])) $file_oembed->url = $data['url'];
$file_oembed->insert();
if (!empty($data['thumbnail_url'])) {
File_thumbnail::saveNew($data, $file_id);
}
}
}

View File

@ -0,0 +1,274 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
* Copyright (C) 2008, Controlez-Vous, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
if (!defined('LACONICA')) { exit(1); }
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
require_once INSTALLDIR.'/classes/File.php';
require_once INSTALLDIR.'/classes/File_oembed.php';
define('USER_AGENT', 'Laconica user agent / file probe');
/**
* Table Definition for file_redirection
*/
class File_redirection extends Memcached_DataObject
{
###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */
public $__table = 'file_redirection'; // table name
public $id; // int(11) not_null primary_key group_by
public $url; // varchar(255) unique_key
public $file_id; // int(11) group_by
public $redirections; // int(11) group_by
public $httpcode; // int(11) group_by
/* Static get */
function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('File_redirection',$k,$v); }
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
function _commonCurl($url, $redirs) {
$curlh = curl_init();
curl_setopt($curlh, CURLOPT_URL, $url);
curl_setopt($curlh, CURLOPT_AUTOREFERER, true); // # setup referer header when folowing redirects
curl_setopt($curlh, CURLOPT_CONNECTTIMEOUT, 10); // # seconds to wait
curl_setopt($curlh, CURLOPT_MAXREDIRS, $redirs); // # max number of http redirections to follow
curl_setopt($curlh, CURLOPT_USERAGENT, USER_AGENT);
curl_setopt($curlh, CURLOPT_FOLLOWLOCATION, true); // Follow redirects
curl_setopt($curlh, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curlh, CURLOPT_FILETIME, true);
curl_setopt($curlh, CURLOPT_HEADER, true); // Include header in output
return $curlh;
}
function _redirectWhere_imp($short_url, $redirs = 10, $protected = false) {
if ($redirs < 0) return false;
// let's see if we know this...
$a = File::staticGet('url', $short_url);
if (empty($a->id)) {
$b = File_redirection::staticGet('url', $short_url);
if (empty($b->id)) {
// we'll have to figure it out
} else {
// this is a redirect to $b->file_id
$a = File::staticGet($b->file_id);
$url = $a->url;
}
} else {
// this is a direct link to $a->url
$url = $a->url;
}
if (isset($url)) {
return $url;
}
$curlh = File_redirection::_commonCurl($short_url, $redirs);
// Don't include body in output
curl_setopt($curlh, CURLOPT_NOBODY, true);
curl_exec($curlh);
$info = curl_getinfo($curlh);
curl_close($curlh);
if (405 == $info['http_code']) {
$curlh = File_redirection::_commonCurl($short_url, $redirs);
curl_exec($curlh);
$info = curl_getinfo($curlh);
curl_close($curlh);
}
if (!empty($info['redirect_count']) && File::isProtected($info['url'])) {
return File_redirection::_redirectWhere_imp($short_url, $info['redirect_count'] - 1, true);
}
$ret = array('code' => $info['http_code']
, 'redirects' => $info['redirect_count']
, 'url' => $info['url']);
if (!empty($info['content_type'])) $ret['type'] = $info['content_type'];
if ($protected) $ret['protected'] = true;
if (!empty($info['download_content_length'])) $ret['size'] = $info['download_content_length'];
if (isset($info['filetime']) && ($info['filetime'] > 0)) $ret['time'] = $info['filetime'];
return $ret;
}
function where($in_url) {
$ret = File_redirection::_redirectWhere_imp($in_url);
return $ret;
}
function makeShort($long_url) {
$long_url = File_redirection::_canonUrl($long_url);
// do we already know this long_url and have a short redirection for it?
$file = new File;
$file_redir = new File_redirection;
$file->url = $long_url;
$file->joinAdd($file_redir);
$file->selectAdd('length(file_redirection.url) as len');
$file->limit(1);
$file->orderBy('len');
$file->find(true);
if (!empty($file->id)) {
return $file->url;
}
// if yet unknown, we must find a short url according to user settings
$short_url = File_redirection::_userMakeShort($long_url, common_current_user());
return $short_url;
}
function _userMakeShort($long_url, $user) {
if (empty($user)) {
// common current user does not find a user when called from the XMPP daemon
// therefore we'll set one here fix, so that XMPP given URLs may be shortened
$user->urlshorteningservice = 'ur1.ca';
}
$curlh = curl_init();
curl_setopt($curlh, CURLOPT_CONNECTTIMEOUT, 20); // # seconds to wait
curl_setopt($curlh, CURLOPT_USERAGENT, 'Laconica');
curl_setopt($curlh, CURLOPT_RETURNTRANSFER, true);
switch($user->urlshorteningservice) {
case 'ur1.ca':
require_once INSTALLDIR.'/lib/Shorturl_api.php';
$short_url_service = new LilUrl;
$short_url = $short_url_service->shorten($long_url);
break;
case '2tu.us':
$short_url_service = new TightUrl;
require_once INSTALLDIR.'/lib/Shorturl_api.php';
$short_url = $short_url_service->shorten($long_url);
break;
case 'ptiturl.com':
require_once INSTALLDIR.'/lib/Shorturl_api.php';
$short_url_service = new PtitUrl;
$short_url = $short_url_service->shorten($long_url);
break;
case 'bit.ly':
curl_setopt($curlh, CURLOPT_URL, 'http://bit.ly/api?method=shorten&long_url='.urlencode($long_url));
$short_url = current(json_decode(curl_exec($curlh))->results)->hashUrl;
break;
case 'is.gd':
curl_setopt($curlh, CURLOPT_URL, 'http://is.gd/api.php?longurl='.urlencode($long_url));
$short_url = curl_exec($curlh);
break;
case 'snipr.com':
curl_setopt($curlh, CURLOPT_URL, 'http://snipr.com/site/snip?r=simple&link='.urlencode($long_url));
$short_url = curl_exec($curlh);
break;
case 'metamark.net':
curl_setopt($curlh, CURLOPT_URL, 'http://metamark.net/api/rest/simple?long_url='.urlencode($long_url));
$short_url = curl_exec($curlh);
break;
case 'tinyurl.com':
curl_setopt($curlh, CURLOPT_URL, 'http://tinyurl.com/api-create.php?url='.urlencode($long_url));
$short_url = curl_exec($curlh);
break;
default:
$short_url = false;
}
curl_close($curlh);
if ($short_url) {
$short_url = (string)$short_url;
// store it
$file = File::staticGet('url', $long_url);
if (empty($file)) {
$redir_data = File_redirection::where($long_url);
$file = File::saveNew($redir_data, $long_url);
$file_id = $file->id;
if (!empty($redir_data['oembed']['json'])) {
File_oembed::saveNew($redir_data['oembed']['json'], $file_id);
}
} else {
$file_id = $file->id;
}
$file_redir = File_redirection::staticGet('url', $short_url);
if (empty($file_redir)) {
$file_redir = new File_redirection;
$file_redir->url = $short_url;
$file_redir->file_id = $file_id;
$file_redir->insert();
}
return $short_url;
}
return $long_url;
}
function _canonUrl($in_url, $default_scheme = 'http://') {
if (empty($in_url)) return false;
$out_url = $in_url;
$p = parse_url($out_url);
if (empty($p['host']) || empty($p['scheme'])) {
list($scheme) = explode(':', $in_url, 2);
switch ($scheme) {
case 'fax':
case 'tel':
$out_url = str_replace('.-()', '', $out_url);
break;
case 'mailto':
case 'aim':
case 'jabber':
case 'xmpp':
// don't touch anything
break;
default:
$out_url = $default_scheme . ltrim($out_url, '/');
$p = parse_url($out_url);
if (empty($p['scheme'])) return false;
break;
}
}
if (('ftp' == $p['scheme']) || ('http' == $p['scheme']) || ('https' == $p['scheme'])) {
if (empty($p['host'])) return false;
if (empty($p['path'])) {
$out_url .= '/';
}
}
return $out_url;
}
function saveNew($data, $file_id, $url) {
$file_redir = new File_redirection;
$file_redir->url = $url;
$file_redir->file_id = $file_id;
$file_redir->redirections = intval($data['redirects']);
$file_redir->httpcode = intval($data['code']);
$file_redir->insert();
}
}

View File

@ -0,0 +1,55 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
* Copyright (C) 2008, Controlez-Vous, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
if (!defined('LACONICA')) { exit(1); }
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
/**
* Table Definition for file_thumbnail
*/
class File_thumbnail extends Memcached_DataObject
{
###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */
public $__table = 'file_thumbnail'; // table name
public $id; // int(11) not_null primary_key group_by
public $file_id; // int(11) unique_key group_by
public $url; // varchar(255) unique_key
public $width; // int(11) group_by
public $height; // int(11) group_by
/* Static get */
function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('File_thumbnail',$k,$v); }
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
function saveNew($data, $file_id) {
$tn = new File_thumbnail;
$tn->file_id = $file_id;
$tn->url = $data['thumbnail_url'];
$tn->width = intval($data['thumbnail_width']);
$tn->height = intval($data['thumbnail_height']);
$tn->insert();
}
}

60
classes/File_to_post.php Normal file
View File

@ -0,0 +1,60 @@
<?php
/*
* Laconica - a distributed open-source microblogging tool
* Copyright (C) 2008, Controlez-Vous, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
if (!defined('LACONICA')) { exit(1); }
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
/**
* Table Definition for file_to_post
*/
class File_to_post extends Memcached_DataObject
{
###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */
public $__table = 'file_to_post'; // table name
public $id; // int(11) not_null primary_key group_by
public $file_id; // int(11) multiple_key group_by
public $post_id; // int(11) group_by
/* Static get */
function staticGet($k,$v=NULL) { return DB_DataObject::staticGet('File_to_post',$k,$v); }
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
function processNew($file_id, $notice_id) {
static $seen = array();
if (empty($seen[$notice_id]) || !in_array($file_id, $seen[$notice_id])) {
$f2p = new File_to_post;
$f2p->file_id = $file_id;
$f2p->post_id = $notice_id;
$f2p->insert();
if (empty($seen[$notice_id])) {
$seen[$notice_id] = array($file_id);
} else {
$seen[$notice_id][] = $file_id;
}
}
}
}

View File

@ -17,6 +17,8 @@ class Foreign_link extends Memcached_DataObject
public $noticesync; // tinyint(1) not_null default_1 public $noticesync; // tinyint(1) not_null default_1
public $friendsync; // tinyint(1) not_null default_2 public $friendsync; // tinyint(1) not_null default_2
public $profilesync; // tinyint(1) not_null default_1 public $profilesync; // tinyint(1) not_null default_1
public $last_noticesync; // datetime()
public $last_friendsync; // datetime()
public $created; // datetime() not_null public $created; // datetime() not_null
public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
@ -57,13 +59,19 @@ class Foreign_link extends Memcached_DataObject
return null; return null;
} }
function set_flags($noticesync, $replysync, $friendsync) function set_flags($noticesend, $noticerecv, $replysync, $friendsync)
{ {
if ($noticesync) { if ($noticesend) {
$this->noticesync |= FOREIGN_NOTICE_SEND; $this->noticesync |= FOREIGN_NOTICE_SEND;
} else { } else {
$this->noticesync &= ~FOREIGN_NOTICE_SEND; $this->noticesync &= ~FOREIGN_NOTICE_SEND;
} }
if ($noticerecv) {
$this->noticesync |= FOREIGN_NOTICE_RECV;
} else {
$this->noticesync &= ~FOREIGN_NOTICE_RECV;
}
if ($replysync) { if ($replysync) {
$this->noticesync |= FOREIGN_NOTICE_SEND_REPLY; $this->noticesync |= FOREIGN_NOTICE_SEND_REPLY;

0
classes/Group_inbox.php Executable file → Normal file
View File

0
classes/Group_member.php Executable file → Normal file
View File

View File

@ -124,8 +124,6 @@ class Notice extends Memcached_DataObject
$profile = Profile::staticGet($profile_id); $profile = Profile::staticGet($profile_id);
$final = common_shorten_links($content);
if (!$profile) { if (!$profile) {
common_log(LOG_ERR, 'Problem saving notice. Unknown user.'); common_log(LOG_ERR, 'Problem saving notice. Unknown user.');
return _('Problem saving notice. Unknown user.'); return _('Problem saving notice. Unknown user.');
@ -136,7 +134,7 @@ class Notice extends Memcached_DataObject
return _('Too many notices too fast; take a breather and post again in a few minutes.'); return _('Too many notices too fast; take a breather and post again in a few minutes.');
} }
if (common_config('site', 'dupelimit') > 0 && !Notice::checkDupes($profile_id, $final)) { if (common_config('site', 'dupelimit') > 0 && !Notice::checkDupes($profile_id, $content)) {
common_log(LOG_WARNING, 'Dupe posting by profile #' . $profile_id . '; throttled.'); common_log(LOG_WARNING, 'Dupe posting by profile #' . $profile_id . '; throttled.');
return _('Too many duplicate messages too quickly; take a breather and post again in a few minutes.'); return _('Too many duplicate messages too quickly; take a breather and post again in a few minutes.');
} }
@ -167,8 +165,8 @@ class Notice extends Memcached_DataObject
$notice->reply_to = $reply_to; $notice->reply_to = $reply_to;
$notice->created = common_sql_now(); $notice->created = common_sql_now();
$notice->content = $final; $notice->content = $content;
$notice->rendered = common_render_content($final, $notice); $notice->rendered = common_render_content($content, $notice);
$notice->source = $source; $notice->source = $source;
$notice->uri = $uri; $notice->uri = $uri;
@ -206,7 +204,12 @@ class Notice extends Memcached_DataObject
$notice->saveTags(); $notice->saveTags();
$notice->saveGroups(); $notice->saveGroups();
$notice->addToInboxes(); if (common_config('queue', 'enabled')) {
$notice->addToAuthorInbox();
} else {
$notice->addToInboxes();
}
$notice->query('COMMIT'); $notice->query('COMMIT');
Event::handle('EndNoticeSave', array($notice)); Event::handle('EndNoticeSave', array($notice));
@ -216,7 +219,11 @@ class Notice extends Memcached_DataObject
# XXX: someone clever could prepend instead of clearing the cache # XXX: someone clever could prepend instead of clearing the cache
if (common_config('memcached', 'enabled')) { if (common_config('memcached', 'enabled')) {
$notice->blowCaches(); if (common_config('queue', 'enabled')) {
$notice->blowAuthorCaches();
} else {
$notice->blowCaches();
}
} }
return $notice; return $notice;
@ -270,6 +277,16 @@ class Notice extends Memcached_DataObject
return true; return true;
} }
function hasAttachments() {
$post = clone $this;
$query = "select count(file_id) as n_attachments from file join file_to_post on (file_id = file.id) join notice on (post_id = notice.id) where post_id = " . $post->escape($post->id);
$post->query($query);
$post->fetch();
$n_attachments = intval($post->n_attachments);
$post->free();
return $n_attachments;
}
function blowCaches($blowLast=false) function blowCaches($blowLast=false)
{ {
$this->blowSubsCache($blowLast); $this->blowSubsCache($blowLast);
@ -280,6 +297,17 @@ class Notice extends Memcached_DataObject
$this->blowGroupCache($blowLast); $this->blowGroupCache($blowLast);
} }
function blowAuthorCaches($blowLast=false)
{
// Clear the user's cache
$cache = common_memcache();
if (!empty($cache)) {
$cache->delete(common_cache_key('notice_inbox:by_user:'.$this->profile_id));
}
$this->blowNoticeCache($blowLast);
$this->blowPublicCache($blowLast);
}
function blowGroupCache($blowLast=false) function blowGroupCache($blowLast=false)
{ {
$cache = common_memcache(); $cache = common_memcache();
@ -288,17 +316,17 @@ class Notice extends Memcached_DataObject
$group_inbox->notice_id = $this->id; $group_inbox->notice_id = $this->id;
if ($group_inbox->find()) { if ($group_inbox->find()) {
while ($group_inbox->fetch()) { while ($group_inbox->fetch()) {
$cache->delete(common_cache_key('group:notices:'.$group_inbox->group_id)); $cache->delete(common_cache_key('user_group:notice_ids:' . $group_inbox->group_id));
if ($blowLast) { if ($blowLast) {
$cache->delete(common_cache_key('group:notices:'.$group_inbox->group_id.';last')); $cache->delete(common_cache_key('user_group:notice_ids:' . $group_inbox->group_id.';last'));
} }
$member = new Group_member(); $member = new Group_member();
$member->group_id = $group_inbox->group_id; $member->group_id = $group_inbox->group_id;
if ($member->find()) { if ($member->find()) {
while ($member->fetch()) { while ($member->fetch()) {
$cache->delete(common_cache_key('user:notices_with_friends:' . $member->profile_id)); $cache->delete(common_cache_key('notice_inbox:by_user:' . $member->profile_id));
if ($blowLast) { if ($blowLast) {
$cache->delete(common_cache_key('user:notices_with_friends:' . $member->profile_id . ';last')); $cache->delete(common_cache_key('notice_inbox:by_user:' . $member->profile_id . ';last'));
} }
} }
} }
@ -317,10 +345,7 @@ class Notice extends Memcached_DataObject
$tag->notice_id = $this->id; $tag->notice_id = $this->id;
if ($tag->find()) { if ($tag->find()) {
while ($tag->fetch()) { while ($tag->fetch()) {
$cache->delete(common_cache_key('notice_tag:notice_stream:' . $tag->tag)); $tag->blowCache($blowLast);
if ($blowLast) {
$cache->delete(common_cache_key('notice_tag:notice_stream:' . $tag->tag . ';last'));
}
} }
} }
$tag->free(); $tag->free();
@ -341,9 +366,9 @@ class Notice extends Memcached_DataObject
'WHERE subscription.subscribed = ' . $this->profile_id); 'WHERE subscription.subscribed = ' . $this->profile_id);
while ($user->fetch()) { while ($user->fetch()) {
$cache->delete(common_cache_key('user:notices_with_friends:' . $user->id)); $cache->delete(common_cache_key('notice_inbox:by_user:'.$user->id));
if ($blowLast) { if ($blowLast) {
$cache->delete(common_cache_key('user:notices_with_friends:' . $user->id . ';last')); $cache->delete(common_cache_key('notice_inbox:by_user:'.$user->id.';last'));
} }
} }
$user->free(); $user->free();
@ -355,10 +380,10 @@ class Notice extends Memcached_DataObject
{ {
if ($this->is_local) { if ($this->is_local) {
$cache = common_memcache(); $cache = common_memcache();
if ($cache) { if (!empty($cache)) {
$cache->delete(common_cache_key('profile:notices:'.$this->profile_id)); $cache->delete(common_cache_key('profile:notice_ids:'.$this->profile_id));
if ($blowLast) { if ($blowLast) {
$cache->delete(common_cache_key('profile:notices:'.$this->profile_id.';last')); $cache->delete(common_cache_key('profile:notice_ids:'.$this->profile_id.';last'));
} }
} }
} }
@ -372,9 +397,9 @@ class Notice extends Memcached_DataObject
$reply->notice_id = $this->id; $reply->notice_id = $this->id;
if ($reply->find()) { if ($reply->find()) {
while ($reply->fetch()) { while ($reply->fetch()) {
$cache->delete(common_cache_key('user:replies:'.$reply->profile_id)); $cache->delete(common_cache_key('reply:stream:'.$reply->profile_id));
if ($blowLast) { if ($blowLast) {
$cache->delete(common_cache_key('user:replies:'.$reply->profile_id.';last')); $cache->delete(common_cache_key('reply:stream:'.$reply->profile_id.';last'));
} }
} }
} }
@ -404,9 +429,9 @@ class Notice extends Memcached_DataObject
$fave->notice_id = $this->id; $fave->notice_id = $this->id;
if ($fave->find()) { if ($fave->find()) {
while ($fave->fetch()) { while ($fave->fetch()) {
$cache->delete(common_cache_key('user:faves:'.$fave->user_id)); $cache->delete(common_cache_key('fave:ids_by_user:'.$fave->user_id));
if ($blowLast) { if ($blowLast) {
$cache->delete(common_cache_key('user:faves:'.$fave->user_id.';last')); $cache->delete(common_cache_key('fave:ids_by_user:'.$fave->user_id.';last'));
} }
} }
} }
@ -602,27 +627,80 @@ class Notice extends Memcached_DataObject
return $wrapper; return $wrapper;
} }
function getStreamByIds($ids)
{
$cache = common_memcache();
if (!empty($cache)) {
$notices = array();
foreach ($ids as $id) {
$notices[] = Notice::staticGet('id', $id);
}
return new ArrayWrapper($notices);
} else {
$notice = new Notice();
$notice->whereAdd('id in (' . implode(', ', $ids) . ')');
$notice->orderBy('id DESC');
$notice->find();
return $notice;
}
}
function publicStream($offset=0, $limit=20, $since_id=0, $before_id=0, $since=null) function publicStream($offset=0, $limit=20, $since_id=0, $before_id=0, $since=null)
{ {
$ids = Notice::stream(array('Notice', '_publicStreamDirect'),
array(),
'public',
$offset, $limit, $since_id, $before_id, $since);
$parts = array(); return Notice::getStreamByIds($ids);
}
$qry = 'SELECT * FROM notice '; function _publicStreamDirect($offset=0, $limit=20, $since_id=0, $before_id=0, $since=null)
{
$notice = new Notice();
$notice->selectAdd(); // clears it
$notice->selectAdd('id');
$notice->orderBy('id DESC');
if (!is_null($offset)) {
$notice->limit($offset, $limit);
}
if (common_config('public', 'localonly')) { if (common_config('public', 'localonly')) {
$parts[] = 'is_local = 1'; $notice->whereAdd('is_local = 1');
} else { } else {
# -1 == blacklisted # -1 == blacklisted
$parts[] = 'is_local != -1'; $notice->whereAdd('is_local != -1');
} }
if ($parts) { if ($since_id != 0) {
$qry .= ' WHERE ' . implode(' AND ', $parts); $notice->whereAdd('id > ' . $since_id);
} }
return Notice::getStream($qry, if ($before_id != 0) {
'public', $notice->whereAdd('id < ' . $before_id);
$offset, $limit, $since_id, $before_id, null, $since); }
if (!is_null($since)) {
$notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\'');
}
$ids = array();
if ($notice->find()) {
while ($notice->fetch()) {
$ids[] = $notice->id;
}
}
$notice->free();
$notice = NULL;
return $ids;
} }
function addToInboxes() function addToInboxes()
@ -648,6 +726,33 @@ class Notice extends Memcached_DataObject
return; return;
} }
function addToAuthorInbox()
{
$enabled = common_config('inboxes', 'enabled');
if ($enabled === true || $enabled === 'transitional') {
$user = User::staticGet('id', $this->profile_id);
if (empty($user)) {
return;
}
$inbox = new Notice_inbox();
$UT = common_config('db','type')=='pgsql'?'"user"':'user';
$qry = 'INSERT INTO notice_inbox (user_id, notice_id, created) ' .
"SELECT $UT.id, " . $this->id . ", '" . $this->created . "' " .
"FROM $UT " .
"WHERE $UT.id = " . $this->profile_id . ' ' .
'AND NOT EXISTS (SELECT user_id, notice_id ' .
'FROM notice_inbox ' .
"WHERE user_id = " . $this->profile_id . ' '.
'AND notice_id = ' . $this->id . ' )';
if ($enabled === 'transitional') {
$qry .= " AND $UT.inboxed = 1";
}
$inbox->query($qry);
}
return;
}
function saveGroups() function saveGroups()
{ {
$enabled = common_config('inboxes', 'enabled'); $enabled = common_config('inboxes', 'enabled');
@ -700,24 +805,29 @@ class Notice extends Memcached_DataObject
// FIXME: do this in an offline daemon // FIXME: do this in an offline daemon
$inbox = new Notice_inbox(); $this->addToGroupInboxes($group);
$UT = common_config('db','type')=='pgsql'?'"user"':'user';
$qry = 'INSERT INTO notice_inbox (user_id, notice_id, created, source) ' .
"SELECT $UT.id, " . $this->id . ", '" . $this->created . "', 2 " .
"FROM $UT JOIN group_member ON $UT.id = group_member.profile_id " .
'WHERE group_member.group_id = ' . $group->id . ' ' .
'AND NOT EXISTS (SELECT user_id, notice_id ' .
'FROM notice_inbox ' .
"WHERE user_id = $UT.id " .
'AND notice_id = ' . $this->id . ' )';
if ($enabled === 'transitional') {
$qry .= " AND $UT.inboxed = 1";
}
$result = $inbox->query($qry);
} }
} }
} }
function addToGroupInboxes($group)
{
$inbox = new Notice_inbox();
$UT = common_config('db','type')=='pgsql'?'"user"':'user';
$qry = 'INSERT INTO notice_inbox (user_id, notice_id, created, source) ' .
"SELECT $UT.id, " . $this->id . ", '" . $this->created . "', 2 " .
"FROM $UT JOIN group_member ON $UT.id = group_member.profile_id " .
'WHERE group_member.group_id = ' . $group->id . ' ' .
'AND NOT EXISTS (SELECT user_id, notice_id ' .
'FROM notice_inbox ' .
"WHERE user_id = $UT.id " .
'AND notice_id = ' . $this->id . ' )';
if ($enabled === 'transitional') {
$qry .= " AND $UT.inboxed = 1";
}
$result = $inbox->query($qry);
}
function saveReplies() function saveReplies()
{ {
// Alternative reply format // Alternative reply format
@ -913,4 +1023,59 @@ class Notice extends Memcached_DataObject
array('notice' => $this->id)); array('notice' => $this->id));
} }
} }
function stream($fn, $args, $cachekey, $offset=0, $limit=20, $since_id=0, $before_id=0, $since=null, $tag=null)
{
$cache = common_memcache();
if (empty($cache) ||
$since_id != 0 || $before_id != 0 || !is_null($since) ||
($offset + $limit) > NOTICE_CACHE_WINDOW) {
return call_user_func_array($fn, array_merge($args, array($offset, $limit, $since_id,
$before_id, $since, $tag)));
}
$idkey = common_cache_key($cachekey);
$idstr = $cache->get($idkey);
if (!empty($idstr)) {
// Cache hit! Woohoo!
$window = explode(',', $idstr);
$ids = array_slice($window, $offset, $limit);
return $ids;
}
$laststr = $cache->get($idkey.';last');
if (!empty($laststr)) {
$window = explode(',', $laststr);
$last_id = $window[0];
$new_ids = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW,
$last_id, 0, null, $tag)));
$new_window = array_merge($new_ids, $window);
$new_windowstr = implode(',', $new_window);
$result = $cache->set($idkey, $new_windowstr);
$result = $cache->set($idkey . ';last', $new_windowstr);
$ids = array_slice($new_window, $offset, $limit);
return $ids;
}
$window = call_user_func_array($fn, array_merge($args, array(0, NOTICE_CACHE_WINDOW,
0, 0, null, $tag)));
$windowstr = implode(',', $window);
$result = $cache->set($idkey, $windowstr);
$result = $cache->set($idkey . ';last', $windowstr);
$ids = array_slice($window, $offset, $limit);
return $ids;
}
} }

View File

@ -1,7 +1,7 @@
<?php <?php
/* /*
* Laconica - a distributed open-source microblogging tool * Laconica - a distributed open-source microblogging tool
* Copyright (C) 2008, Controlez-Vous, Inc. * Copyright (C) 2008, 2009, Control Yourself, Inc.
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU Affero General Public License as published by
@ -21,7 +21,11 @@ if (!defined('LACONICA')) { exit(1); }
require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
class Notice_inbox extends Memcached_DataObject // We keep 5 pages of inbox notices in memcache, +1 for pagination check
define('INBOX_CACHE_WINDOW', 101);
class Notice_inbox extends Memcached_DataObject
{ {
###START_AUTOCODE ###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */ /* the code below is auto generated do not remove the above tag */
@ -38,4 +42,47 @@ class Notice_inbox extends Memcached_DataObject
/* the code above is auto generated do not remove the tag below */ /* the code above is auto generated do not remove the tag below */
###END_AUTOCODE ###END_AUTOCODE
function stream($user_id, $offset, $limit, $since_id, $before_id, $since)
{
return Notice::stream(array('Notice_inbox', '_streamDirect'),
array($user_id),
'notice_inbox:by_user:'.$user_id,
$offset, $limit, $since_id, $before_id, $since);
}
function _streamDirect($user_id, $offset, $limit, $since_id, $before_id, $since)
{
$inbox = new Notice_inbox();
$inbox->user_id = $user_id;
if ($since_id != 0) {
$inbox->whereAdd('notice_id > ' . $since_id);
}
if ($before_id != 0) {
$inbox->whereAdd('notice_id < ' . $before_id);
}
if (!is_null($since)) {
$inbox->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\'');
}
$inbox->orderBy('notice_id DESC');
if (!is_null($offset)) {
$inbox->limit($offset, $limit);
}
$ids = array();
if ($inbox->find()) {
while ($inbox->fetch()) {
$ids[] = $inbox->notice_id;
}
}
return $ids;
}
} }

View File

@ -37,21 +37,62 @@ class Notice_tag extends Memcached_DataObject
###END_AUTOCODE ###END_AUTOCODE
static function getStream($tag, $offset=0, $limit=20) { static function getStream($tag, $offset=0, $limit=20) {
$qry =
'SELECT notice.* ' .
'FROM notice JOIN notice_tag ON notice.id = notice_tag.notice_id ' .
"WHERE notice_tag.tag = '%s' ";
return Notice::getStream(sprintf($qry, $tag), $ids = Notice::stream(array('Notice_tag', '_streamDirect'),
'notice_tag:notice_stream:' . common_keyize($tag), array($tag),
$offset, $limit); 'notice_tag:notice_ids:' . common_keyize($tag),
$offset, $limit);
return Notice::getStreamByIds($ids);
} }
function blowCache() function _streamDirect($tag, $offset, $limit, $since_id, $before_id, $since)
{
$nt = new Notice_tag();
$nt->tag = $tag;
$nt->selectAdd();
$nt->selectAdd('notice_id');
if ($since_id != 0) {
$nt->whereAdd('notice_id > ' . $since_id);
}
if ($before_id != 0) {
$nt->whereAdd('notice_id < ' . $before_id);
}
if (!is_null($since)) {
$nt->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\'');
}
$nt->orderBy('notice_id DESC');
if (!is_null($offset)) {
$nt->limit($offset, $limit);
}
$ids = array();
if ($nt->find()) {
while ($nt->fetch()) {
$ids[] = $nt->notice_id;
}
}
return $ids;
}
function blowCache($blowLast=false)
{ {
$cache = common_memcache(); $cache = common_memcache();
if ($cache) { if ($cache) {
$cache->delete(common_cache_key('notice_tag:notice_stream:' . $this->tag)); $idkey = common_cache_key('notice_tag:notice_ids:' . common_keyize($this->tag));
$cache->delete($idkey);
if ($blowLast) {
$cache->delete($idkey.';last');
}
} }
} }

View File

@ -153,16 +153,101 @@ class Profile extends Memcached_DataObject
return null; return null;
} }
function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0) function getTaggedNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null, $tag=null)
{ {
$qry = // XXX: I'm not sure this is going to be any faster. It probably isn't.
'SELECT * ' . $ids = Notice::stream(array($this, '_streamTaggedDirect'),
'FROM notice ' . array(),
'WHERE profile_id = %d '; 'profile:notice_ids:' . $this->id,
$offset, $limit, $since_id, $before_id, $since, $tag);
common_debug(print_r($ids, true));
return Notice::getStreamByIds($ids);
}
return Notice::getStream(sprintf($qry, $this->id), function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null)
'profile:notices:'.$this->id, {
$offset, $limit, $since_id, $before_id); // XXX: I'm not sure this is going to be any faster. It probably isn't.
$ids = Notice::stream(array($this, '_streamDirect'),
array(),
'profile:notice_ids:' . $this->id,
$offset, $limit, $since_id, $before_id, $since);
return Notice::getStreamByIds($ids);
}
function _streamTaggedDirect($offset, $limit, $since_id, $before_id, $since=null, $tag=null)
{
common_debug('_streamTaggedDirect()');
$notice = new Notice();
$notice->profile_id = $this->id;
$query = "select id from notice join notice_tag on id=notice_id where tag='" . $notice->escape($tag) . "' and profile_id=" . $notice->escape($notice->profile_id);
if ($since_id != 0) {
$query .= " and id > $since_id";
}
if ($before_id != 0) {
$query .= " and id < $before_id";
}
if (!is_null($since)) {
$query .= " and created > '" . date('Y-m-d H:i:s', $since) . "'";
}
$query .= ' order by id DESC';
if (!is_null($offset)) {
$query .= " limit $offset, $limit";
}
$notice->query($query);
$ids = array();
while ($notice->fetch()) {
common_debug(print_r($notice, true));
$ids[] = $notice->id;
}
return $ids;
}
function _streamDirect($offset, $limit, $since_id, $before_id, $since = null)
{
$notice = new Notice();
$notice->profile_id = $this->id;
$notice->selectAdd();
$notice->selectAdd('id');
if ($since_id != 0) {
$notice->whereAdd('id > ' . $since_id);
}
if ($before_id != 0) {
$notice->whereAdd('id < ' . $before_id);
}
if (!is_null($since)) {
$notice->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\'');
}
$notice->orderBy('id DESC');
if (!is_null($offset)) {
$notice->limit($offset, $limit);
}
$ids = array();
if ($notice->find()) {
while ($notice->fetch()) {
$ids[] = $notice->id;
}
}
return $ids;
} }
function isMember($group) function isMember($group)

0
classes/Related_group.php Executable file → Normal file
View File

View File

@ -4,7 +4,7 @@
*/ */
require_once INSTALLDIR.'/classes/Memcached_DataObject.php'; require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
class Reply extends Memcached_DataObject class Reply extends Memcached_DataObject
{ {
###START_AUTOCODE ###START_AUTOCODE
/* the code below is auto generated do not remove the above tag */ /* the code below is auto generated do not remove the above tag */
@ -13,7 +13,7 @@ class Reply extends Memcached_DataObject
public $notice_id; // int(4) primary_key not_null public $notice_id; // int(4) primary_key not_null
public $profile_id; // int(4) primary_key not_null public $profile_id; // int(4) primary_key not_null
public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP public $modified; // timestamp() not_null default_CURRENT_TIMESTAMP
public $replied_id; // int(4) public $replied_id; // int(4)
/* Static get */ /* Static get */
function staticGet($k,$v=null) function staticGet($k,$v=null)
@ -21,4 +21,47 @@ class Reply extends Memcached_DataObject
/* the code above is auto generated do not remove the tag below */ /* the code above is auto generated do not remove the tag below */
###END_AUTOCODE ###END_AUTOCODE
function stream($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null)
{
$ids = Notice::stream(array('Reply', '_streamDirect'),
array($user_id),
'reply:stream:' . $user_id,
$offset, $limit, $since_id, $before_id, $since);
return $ids;
}
function _streamDirect($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null)
{
$reply = new Reply();
$reply->profile_id = $user_id;
if ($since_id != 0) {
$reply->whereAdd('notice_id > ' . $since_id);
}
if ($before_id != 0) {
$reply->whereAdd('notice_id < ' . $before_id);
}
if (!is_null($since)) {
$reply->whereAdd('modified > \'' . date('Y-m-d H:i:s', $since) . '\'');
}
$reply->orderBy('notice_id DESC');
if (!is_null($offset)) {
$reply->limit($offset, $limit);
}
$ids = array();
if ($reply->find()) {
while ($reply->fetch()) {
$ids[] = $reply->notice_id;
}
}
return $ids;
}
} }

0
classes/Status_network.php Executable file → Normal file
View File

View File

@ -349,30 +349,31 @@ class User extends Memcached_DataObject
$cache = common_memcache(); $cache = common_memcache();
// XXX: Kind of a hack. // XXX: Kind of a hack.
if ($cache) { if ($cache) {
// This is the stream of favorite notices, in rev chron // This is the stream of favorite notices, in rev chron
// order. This forces it into cache. // order. This forces it into cache.
$faves = $this->favoriteNotices(0, NOTICE_CACHE_WINDOW);
$cnt = 0; $ids = Fave::stream($this->id, 0, NOTICE_CACHE_WINDOW);
while ($faves->fetch()) {
if ($faves->id < $notice->id) { // If it's in the list, then it's a fave
// If we passed it, it's not a fave
return false; if (in_array($notice->id, $ids)) {
} else if ($faves->id == $notice->id) { return true;
// If it matches a cached notice, then it's a fave
return true;
}
$cnt++;
} }
// If we're not past the end of the cache window, // If we're not past the end of the cache window,
// then the cache has all available faves, so this one // then the cache has all available faves, so this one
// is not a fave. // is not a fave.
if ($cnt < NOTICE_CACHE_WINDOW) {
if (count($ids) < NOTICE_CACHE_WINDOW) {
return false; return false;
} }
// Otherwise, cache doesn't have all faves; // Otherwise, cache doesn't have all faves;
// fall through to the default // fall through to the default
} }
$fave = Fave::pkeyGet(array('user_id' => $this->id, $fave = Fave::pkeyGet(array('user_id' => $this->id,
'notice_id' => $notice->id)); 'notice_id' => $notice->id));
return ((is_null($fave)) ? false : true); return ((is_null($fave)) ? false : true);
@ -401,13 +402,18 @@ class User extends Memcached_DataObject
function getReplies($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) function getReplies($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null)
{ {
$qry = $ids = Reply::stream($this->id, $offset, $limit, $since_id, $before_id, $since);
'SELECT notice.* ' . common_debug("Ids = " . implode(',', $ids));
'FROM notice JOIN reply ON notice.id = reply.notice_id ' . return Notice::getStreamByIds($ids);
'WHERE reply.profile_id = %d '; }
return Notice::getStream(sprintf($qry, $this->id),
'user:replies:'.$this->id, function getTaggedNotices($tag, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) {
$offset, $limit, $since_id, $before_id, null, $since); $profile = $this->getProfile();
if (!$profile) {
return null;
} else {
return $profile->getTaggedNotices($tag, $offset, $limit, $since_id, $before_id, $since);
}
} }
function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null)
@ -416,19 +422,14 @@ class User extends Memcached_DataObject
if (!$profile) { if (!$profile) {
return null; return null;
} else { } else {
return $profile->getNotices($offset, $limit, $since_id, $before_id); return $profile->getNotices($offset, $limit, $since_id, $before_id, $since);
} }
} }
function favoriteNotices($offset=0, $limit=NOTICES_PER_PAGE) function favoriteNotices($offset=0, $limit=NOTICES_PER_PAGE)
{ {
$qry = $ids = Fave::stream($this->id, $offset, $limit);
'SELECT notice.* ' . return Notice::getStreamByIds($ids);
'FROM notice JOIN fave ON notice.id = fave.notice_id ' .
'WHERE fave.user_id = %d ';
return Notice::getStream(sprintf($qry, $this->id),
'user:faves:'.$this->id,
$offset, $limit);
} }
function noticesWithFriends($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null) function noticesWithFriends($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $before_id=0, $since=null)
@ -444,21 +445,17 @@ class User extends Memcached_DataObject
'SELECT notice.* ' . 'SELECT notice.* ' .
'FROM notice JOIN subscription ON notice.profile_id = subscription.subscribed ' . 'FROM notice JOIN subscription ON notice.profile_id = subscription.subscribed ' .
'WHERE subscription.subscriber = %d '; 'WHERE subscription.subscriber = %d ';
$order = null; return Notice::getStream(sprintf($qry, $this->id),
'user:notices_with_friends:' . $this->id,
$offset, $limit, $since_id, $before_id,
$order, $since);
} else if ($enabled === true || } else if ($enabled === true ||
($enabled == 'transitional' && $this->inboxed == 1)) { ($enabled == 'transitional' && $this->inboxed == 1)) {
$qry = $ids = Notice_inbox::stream($this->id, $offset, $limit, $since_id, $before_id, $since);
'SELECT notice.* ' .
'FROM notice JOIN notice_inbox ON notice.id = notice_inbox.notice_id ' . return Notice::getStreamByIds($ids);
'WHERE notice_inbox.user_id = %d ';
// NOTE: we override ORDER
$order = null;
} }
return Notice::getStream(sprintf($qry, $this->id),
'user:notices_with_friends:' . $this->id,
$offset, $limit, $since_id, $before_id,
$order, $since);
} }
function blowFavesCache() function blowFavesCache()
@ -467,8 +464,8 @@ class User extends Memcached_DataObject
if ($cache) { if ($cache) {
// Faves don't happen chronologically, so we need to blow // Faves don't happen chronologically, so we need to blow
// ;last cache, too // ;last cache, too
$cache->delete(common_cache_key('user:faves:'.$this->id)); $cache->delete(common_cache_key('fave:ids_by_user:'.$this->id));
$cache->delete(common_cache_key('user:faves:'.$this->id).';last'); $cache->delete(common_cache_key('fave:ids_by_user:'.$this->id.';last'));
} }
} }

53
classes/User_group.php Executable file → Normal file
View File

@ -50,13 +50,50 @@ class User_group extends Memcached_DataObject
function getNotices($offset, $limit) function getNotices($offset, $limit)
{ {
$qry = $ids = Notice::stream(array($this, '_streamDirect'),
'SELECT notice.* ' . array(),
'FROM notice JOIN group_inbox ON notice.id = group_inbox.notice_id ' . 'user_group:notice_ids:' . $this->id,
'WHERE group_inbox.group_id = %d '; $offset, $limit);
return Notice::getStream(sprintf($qry, $this->id),
'group:notices:'.$this->id, return Notice::getStreamByIds($ids);
$offset, $limit); }
function _streamDirect($offset, $limit, $since_id, $before_id, $since)
{
$inbox = new Group_inbox();
$inbox->group_id = $this->id;
$inbox->selectAdd();
$inbox->selectAdd('notice_id');
if ($since_id != 0) {
$inbox->whereAdd('notice_id > ' . $since_id);
}
if ($before_id != 0) {
$inbox->whereAdd('notice_id < ' . $before_id);
}
if (!is_null($since)) {
$inbox->whereAdd('created > \'' . date('Y-m-d H:i:s', $since) . '\'');
}
$inbox->orderBy('notice_id DESC');
if (!is_null($offset)) {
$inbox->limit($offset, $limit);
}
$ids = array();
if ($inbox->find()) {
while ($inbox->fetch()) {
$ids[] = $inbox->notice_id;
}
}
return $ids;
} }
function allowedNickname($nickname) function allowedNickname($nickname)
@ -91,7 +128,7 @@ class User_group extends Memcached_DataObject
function setOriginal($filename) function setOriginal($filename)
{ {
$imagefile = new ImageFile($this->id, Avatar::path($filename)); $imagefile = new ImageFile($this->id, Avatar::path($filename));
$orig = clone($this); $orig = clone($this);
$this->original_logo = Avatar::url($filename); $this->original_logo = Avatar::url($filename);
$this->homepage_logo = Avatar::url($imagefile->resize(AVATAR_PROFILE_SIZE)); $this->homepage_logo = Avatar::url($imagefile->resize(AVATAR_PROFILE_SIZE));

61
classes/laconica.ini Executable file → Normal file
View File

@ -1,4 +1,3 @@
[avatar] [avatar]
profile_id = 129 profile_id = 129
original = 17 original = 17
@ -47,6 +46,64 @@ modified = 384
notice_id = K notice_id = K
user_id = K user_id = K
[file]
id = 129
url = 2
mimetype = 2
size = 1
title = 2
date = 1
protected = 1
[file__keys]
id = N
[file_oembed]
id = 129
file_id = 1
version = 2
type = 2
provider = 2
provider_url = 2
width = 1
height = 1
html = 34
title = 2
author_name = 2
author_url = 2
url = 2
[file_oembed__keys]
id = N
[file_redirection]
id = 129
url = 2
file_id = 1
redirections = 1
httpcode = 1
[file_redirection__keys]
id = N
[file_thumbnail]
id = 129
file_id = 1
url = 2
width = 1
height = 1
[file_thumbnail__keys]
id = N
[file_to_post]
id = 129
file_id = 1
post_id = 1
[file_to_post__keys]
id = N
[foreign_link] [foreign_link]
user_id = 129 user_id = 129
foreign_id = 129 foreign_id = 129
@ -55,6 +112,8 @@ credentials = 2
noticesync = 145 noticesync = 145
friendsync = 145 friendsync = 145
profilesync = 145 profilesync = 145
last_noticesync = 14
last_friendsync = 14
created = 142 created = 142
modified = 384 modified = 384

View File

@ -41,3 +41,17 @@ subscribed = profile:id
[fave] [fave]
notice_id = notice:id notice_id = notice:id
user_id = user:id user_id = user:id
[file_oembed]
file_id = file:id
[file_redirection]
file_id = file:id
[file_thumbnail]
file_id = file:id
[file_to_post]
file_id = file:id
post_id = notice:id

View File

@ -2,4 +2,5 @@ insert into foreign_service
(id, name, description, created) (id, name, description, created)
values values
('1','Twitter', 'Twitter Micro-blogging service', now()), ('1','Twitter', 'Twitter Micro-blogging service', now()),
('2','Facebook', 'Facebook', now()); ('2','Facebook', 'Facebook', now()),
('3','FacebookConnect', 'Facebook Connect', now());

View File

@ -119,6 +119,7 @@ create table notice (
index notice_profile_id_idx (profile_id), index notice_profile_id_idx (profile_id),
index notice_conversation_idx (conversation), index notice_conversation_idx (conversation),
index notice_created_idx (created), index notice_created_idx (created),
index notice_replyto_idx (reply_to),
FULLTEXT(content) FULLTEXT(content)
) ENGINE=MyISAM CHARACTER SET utf8 COLLATE utf8_general_ci; ) ENGINE=MyISAM CHARACTER SET utf8 COLLATE utf8_general_ci;
@ -290,6 +291,8 @@ create table foreign_link (
noticesync tinyint not null default 1 comment 'notice synchronization, bit 1 = sync outgoing, bit 2 = sync incoming, bit 3 = filter local replies', noticesync tinyint not null default 1 comment 'notice synchronization, bit 1 = sync outgoing, bit 2 = sync incoming, bit 3 = filter local replies',
friendsync tinyint not null default 2 comment 'friend synchronization, bit 1 = sync outgoing, bit 2 = sync incoming', friendsync tinyint not null default 2 comment 'friend synchronization, bit 1 = sync outgoing, bit 2 = sync incoming',
profilesync tinyint not null default 1 comment 'profile synchronization, bit 1 = sync outgoing, bit 2 = sync incoming', profilesync tinyint not null default 1 comment 'profile synchronization, bit 1 = sync outgoing, bit 2 = sync incoming',
last_noticesync datetime default null comment 'last time notices were imported',
last_friendsync datetime default null comment 'last time friends were imported',
created datetime not null comment 'date this record was created', created datetime not null comment 'date this record was created',
modified timestamp comment 'date this record was modified', modified timestamp comment 'date this record was modified',
@ -422,3 +425,60 @@ create table group_inbox (
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin; ) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
create table file (
id integer primary key auto_increment,
url varchar(255), mimetype varchar(50),
size integer,
title varchar(255),
date integer(11),
protected integer(1),
unique(url)
) ENGINE=MyISAM CHARACTER SET utf8 COLLATE utf8_general_ci;
create table file_oembed (
id integer primary key auto_increment,
file_id integer,
version varchar(20),
type varchar(20),
provider varchar(50),
provider_url varchar(255),
width integer,
height integer,
html text,
title varchar(255),
author_name varchar(50),
author_url varchar(255),
url varchar(255),
unique(file_id)
) ENGINE=MyISAM CHARACTER SET utf8 COLLATE utf8_general_ci;
create table file_redirection (
id integer primary key auto_increment,
url varchar(255),
file_id integer,
redirections integer,
httpcode integer,
unique(url)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
create table file_thumbnail (
id integer primary key auto_increment,
file_id integer,
url varchar(255),
width integer,
height integer,
unique(file_id),
unique(url)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
create table file_to_post (
id integer primary key auto_increment,
file_id integer,
post_id integer,
unique(file_id, post_id)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;

View File

@ -291,6 +291,8 @@ create table foreign_link (
noticesync int not null default 1 /* comment 'notice synchronisation, bit 1 = sync outgoing, bit 2 = sync incoming, bit 3 = filter local replies' */, noticesync int not null default 1 /* comment 'notice synchronisation, bit 1 = sync outgoing, bit 2 = sync incoming, bit 3 = filter local replies' */,
friendsync int not null default 2 /* comment 'friend synchronisation, bit 1 = sync outgoing, bit 2 = sync incoming */, friendsync int not null default 2 /* comment 'friend synchronisation, bit 1 = sync outgoing, bit 2 = sync incoming */,
profilesync int not null default 1 /* comment 'profile synchronization, bit 1 = sync outgoing, bit 2 = sync incoming' */, profilesync int not null default 1 /* comment 'profile synchronization, bit 1 = sync outgoing, bit 2 = sync incoming' */,
last_noticesync timestamp default null /* comment 'last time notices were imported' */,
last_friendsync timestamp default null /* comment 'last time friends were imported' */,
created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */, created timestamp not null default CURRENT_TIMESTAMP /* comment 'date this record was created' */,
modified timestamp /* comment 'date this record was modified' */, modified timestamp /* comment 'date this record was modified' */,
@ -425,6 +427,64 @@ create table group_inbox (
); );
create index group_inbox_created_idx on group_inbox using btree(created); create index group_inbox_created_idx on group_inbox using btree(created);
/*attachments and URLs stuff */
create sequence file_seq;
create table file (
id bigint default nextval('file_seq') primary key /* comment 'unique identifier' */,
url varchar(255) unique,
mimetype varchar(50),
size integer,
title varchar(255),
date integer(11),
protected integer(1)
);
create sequence file_oembed_seq;
create table file_oembed (
id bigint default nextval('file_oembed_seq') primary key /* comment 'unique identifier' */,
file_id bigint unique,
version varchar(20),
type varchar(20),
provider varchar(50),
provider_url varchar(255),
width integer,
height integer,
html text,
title varchar(255),
author_name varchar(50),
author_url varchar(255),
url varchar(255),
);
create sequence file_redirection_seq;
create table file_redirection (
id bigint default nextval('file_redirection_seq') primary key /* comment 'unique identifier' */,
url varchar(255) unique,
file_id bigint,
redirections integer,
httpcode integer
);
create sequence file_thumbnail_seq;
create table file_thumbnail (
id bigint default nextval('file_thumbnail_seq') primary key /* comment 'unique identifier' */,
file_id bigint unique,
url varchar(255) unique,
width integer,
height integer
);
create sequence file_to_post_seq;
create table file_to_post (
id bigint default nextval('file_to_post_seq') primary key /* comment 'unique identifier' */,
file_id bigint,
post_id bigint,
unique(file_id, post_id)
);
/* Textsearch stuff */ /* Textsearch stuff */
create index textsearch_idx on profile using gist(textsearch); create index textsearch_idx on profile using gist(textsearch);

View File

@ -8,6 +8,7 @@ VALUES
('deskbar','Deskbar-Applet','http://www.gnome.org/projects/deskbar-applet/', now()), ('deskbar','Deskbar-Applet','http://www.gnome.org/projects/deskbar-applet/', now()),
('Do','Gnome Do','http://do.davebsd.com/wiki/index.php?title=Microblog_Plugin', now()), ('Do','Gnome Do','http://do.davebsd.com/wiki/index.php?title=Microblog_Plugin', now()),
('Facebook','Facebook','http://apps.facebook.com/identica/', now()), ('Facebook','Facebook','http://apps.facebook.com/identica/', now()),
('feed2omb','feed2omb','http://projects.ciarang.com/p/feed2omb/', now()),
('Gwibber','Gwibber','http://launchpad.net/gwibber', now()), ('Gwibber','Gwibber','http://launchpad.net/gwibber', now()),
('HelloTxt','HelloTxt','http://hellotxt.com/', now()), ('HelloTxt','HelloTxt','http://hellotxt.com/', now()),
('identicatools','Laconica Tools','http://bitbucketlabs.net/laconica-tools/', now()), ('identicatools','Laconica Tools','http://bitbucketlabs.net/laconica-tools/', now()),
@ -23,6 +24,7 @@ VALUES
('peoplebrowsr', 'PeopleBrowsr', 'http://www.peoplebrowsr.com/', now()), ('peoplebrowsr', 'PeopleBrowsr', 'http://www.peoplebrowsr.com/', now()),
('Pikchur','Pikchur','http://www.pikchur.com/', now()), ('Pikchur','Pikchur','http://www.pikchur.com/', now()),
('Ping.fm','Ping.fm','http://ping.fm/', now()), ('Ping.fm','Ping.fm','http://ping.fm/', now()),
('pingvine','PingVine','http://pingvine.com/', now()),
('pocketwit','PockeTwit','http://code.google.com/p/pocketwit/', now()), ('pocketwit','PockeTwit','http://code.google.com/p/pocketwit/', now()),
('posty','Posty','http://spreadingfunkyness.com/posty/', now()), ('posty','Posty','http://spreadingfunkyness.com/posty/', now()),
('royalewithcheese','Royale With Cheese','http://p.hellyeah.org/', now()), ('royalewithcheese','Royale With Cheese','http://p.hellyeah.org/', now()),
@ -42,6 +44,7 @@ VALUES
('twidge','Twidge','http://software.complete.org/twidge', now()), ('twidge','Twidge','http://software.complete.org/twidge', now()),
('twidroid','twidroid','http://www.twidroid.com/', now()), ('twidroid','twidroid','http://www.twidroid.com/', now()),
('twittelator','Twittelator','http://www.stone.com/iPhone/Twittelator/', now()), ('twittelator','Twittelator','http://www.stone.com/iPhone/Twittelator/', now()),
('twitter','Twitter','http://twitter.com/', now()),
('twitterfeed','twitterfeed','http://twitterfeed.com/', now()), ('twitterfeed','twitterfeed','http://twitterfeed.com/', now()),
('twitterphoto','TwitterPhoto','http://richfish.org/twitterphoto/', now()), ('twitterphoto','TwitterPhoto','http://richfish.org/twitterphoto/', now()),
('twitterpm','Net::Twitter','http://search.cpan.org/dist/Net-Twitter/', now()), ('twitterpm','Net::Twitter','http://search.cpan.org/dist/Net-Twitter/', now()),

View File

@ -2357,6 +2357,8 @@ class DB_DataObject extends DB_DataObject_Overload
$t= explode(' ',microtime()); $t= explode(' ',microtime());
$_DB_DATAOBJECT['QUERYENDTIME'] = $time = $t[0]+$t[1]; $_DB_DATAOBJECT['QUERYENDTIME'] = $time = $t[0]+$t[1];
do {
if ($_DB_driver == 'DB') { if ($_DB_driver == 'DB') {
$result = $DB->query($string); $result = $DB->query($string);
@ -2374,8 +2376,19 @@ class DB_DataObject extends DB_DataObject_Overload
break; break;
} }
} }
// try to reconnect, at most 3 times
$again = false;
if (is_a($result, 'PEAR_Error')
AND $result->getCode() == DB_ERROR_NODBSELECTED
AND $cpt++<3) {
$DB->disconnect();
sleep(1);
$DB->connect($DB->dsn);
$again = true;
}
} while ($again);
if (is_a($result,'PEAR_Error')) { if (is_a($result,'PEAR_Error')) {
if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) { if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {

View File

@ -1,5 +1,5 @@
<?php <?php
// Copyright 2004-2008 Facebook. All Rights Reserved. // Copyright 2004-2009 Facebook. All Rights Reserved.
// //
// +---------------------------------------------------------------------------+ // +---------------------------------------------------------------------------+
// | Facebook Platform PHP5 client | // | Facebook Platform PHP5 client |
@ -30,13 +30,12 @@
// +---------------------------------------------------------------------------+ // +---------------------------------------------------------------------------+
// | For help with this library, contact developers-help@facebook.com | // | For help with this library, contact developers-help@facebook.com |
// +---------------------------------------------------------------------------+ // +---------------------------------------------------------------------------+
//
include_once 'facebookapi_php5_restlib.php'; include_once 'facebookapi_php5_restlib.php';
define('FACEBOOK_API_VALIDATION_ERROR', 1); define('FACEBOOK_API_VALIDATION_ERROR', 1);
class Facebook { class Facebook {
public $api_client; public $api_client;
public $api_key; public $api_key;
public $secret; public $secret;
public $generate_session_secret; public $generate_session_secret;
@ -213,28 +212,55 @@ class Facebook {
} }
} }
// Invalidate the session currently being used, and clear any state associated with it // Invalidate the session currently being used, and clear any state associated
// with it. Note that the user will still remain logged into Facebook.
public function expire_session() { public function expire_session() {
if ($this->api_client->auth_expireSession()) { if ($this->api_client->auth_expireSession()) {
if (!$this->in_fb_canvas() && isset($_COOKIE[$this->api_key . '_user'])) { $this->clear_cookie_state();
$cookies = array('user', 'session_key', 'expires', 'ss');
foreach ($cookies as $name) {
setcookie($this->api_key . '_' . $name, false, time() - 3600);
unset($_COOKIE[$this->api_key . '_' . $name]);
}
setcookie($this->api_key, false, time() - 3600);
unset($_COOKIE[$this->api_key]);
}
// now, clear the rest of the stored state
$this->user = 0;
$this->api_client->session_key = 0;
return true; return true;
} else { } else {
return false; return false;
} }
} }
/** Logs the user out of all temporary application sessions as well as their
* Facebook session. Note this will only work if the user has a valid current
* session with the application.
*
* @param string $next URL to redirect to upon logging out
*
*/
public function logout($next) {
$logout_url = $this->get_logout_url($next);
// Clear any stored state
$this->clear_cookie_state();
$this->redirect($logout_url);
}
/**
* Clears any persistent state stored about the user, including
* cookies and information related to the current session in the
* client.
*
*/
public function clear_cookie_state() {
if (!$this->in_fb_canvas() && isset($_COOKIE[$this->api_key . '_user'])) {
$cookies = array('user', 'session_key', 'expires', 'ss');
foreach ($cookies as $name) {
setcookie($this->api_key . '_' . $name, false, time() - 3600);
unset($_COOKIE[$this->api_key . '_' . $name]);
}
setcookie($this->api_key, false, time() - 3600);
unset($_COOKIE[$this->api_key]);
}
// now, clear the rest of the stored state
$this->user = 0;
$this->api_client->session_key = 0;
}
public function redirect($url) { public function redirect($url) {
if ($this->in_fb_canvas()) { if ($this->in_fb_canvas()) {
echo '<fb:redirect url="' . $url . '"/>'; echo '<fb:redirect url="' . $url . '"/>';
@ -249,7 +275,8 @@ class Facebook {
} }
public function in_frame() { public function in_frame() {
return isset($this->fb_params['in_canvas']) || isset($this->fb_params['in_iframe']); return isset($this->fb_params['in_canvas'])
|| isset($this->fb_params['in_iframe']);
} }
public function in_fb_canvas() { public function in_fb_canvas() {
return isset($this->fb_params['in_canvas']); return isset($this->fb_params['in_canvas']);
@ -296,14 +323,42 @@ class Facebook {
} }
public function get_add_url($next=null) { public function get_add_url($next=null) {
return self::get_facebook_url().'/add.php?api_key='.$this->api_key . $page = self::get_facebook_url().'/add.php';
($next ? '&next=' . urlencode($next) : ''); $params = array('api_key' => $this->api_key);
if ($next) {
$params['next'] = $next;
}
return $page . '?' . http_build_query($params);
} }
public function get_login_url($next, $canvas) { public function get_login_url($next, $canvas) {
return self::get_facebook_url().'/login.php?v=1.0&api_key=' . $this->api_key . $page = self::get_facebook_url().'/login.php';
($next ? '&next=' . urlencode($next) : '') . $params = array('api_key' => $this->api_key,
($canvas ? '&canvas' : ''); 'v' => '1.0');
if ($next) {
$params['next'] = $next;
}
if ($canvas) {
$params['canvas'] = '1';
}
return $page . '?' . http_build_query($params);
}
public function get_logout_url($next) {
$page = self::get_facebook_url().'/logout.php';
$params = array('app_key' => $this->api_key,
'session_key' => $this->api_client->session_key);
if ($next) {
$params['connect_next'] = 1;
$params['next'] = $next;
}
return $page . '?' . http_build_query($params);
} }
public function set_user($user, $session_key, $expires=null, $session_secret=null) { public function set_user($user, $session_key, $expires=null, $session_secret=null) {
@ -410,7 +465,20 @@ class Facebook {
return $fb_params; return $fb_params;
} }
/* /**
* Validates the account that a user was trying to set up an
* independent account through Facebook Connect.
*
* @param user The user attempting to set up an independent account.
* @param hash The hash passed to the reclamation URL used.
* @return bool True if the user is the one that selected the
* reclamation link.
*/
public function verify_account_reclamation($user, $hash) {
return $hash == md5($user . $this->secret);
}
/**
* Validates that a given set of parameters match their signature. * Validates that a given set of parameters match their signature.
* Parameters all match a given input prefix, such as "fb_sig". * Parameters all match a given input prefix, such as "fb_sig".
* *
@ -422,6 +490,37 @@ class Facebook {
return self::generate_sig($fb_params, $this->secret) == $expected_sig; return self::generate_sig($fb_params, $this->secret) == $expected_sig;
} }
/**
* Validate the given signed public session data structure with
* public key of the app that
* the session proof belongs to.
*
* @param $signed_data the session info that is passed by another app
* @param string $public_key Optional public key of the app. If this
* is not passed, function will make an API call to get it.
* return true if the session proof passed verification.
*/
public function verify_signed_public_session_data($signed_data,
$public_key = null) {
// If public key is not already provided, we need to get it through API
if (!$public_key) {
$public_key = $this->api_client->auth_getAppPublicKey(
$signed_data['api_key']);
}
// Create data to verify
$data_to_serialize = $signed_data;
unset($data_to_serialize['sig']);
$serialized_data = implode('_', $data_to_serialize);
// Decode signature
$signature = base64_decode($signed_data['sig']);
$result = openssl_verify($serialized_data, $signature, $public_key,
OPENSSL_ALGO_SHA1);
return $result == 1;
}
/* /*
* Generate a signature using the application secret key. * Generate a signature using the application secret key.
* *

View File

@ -1,5 +1,5 @@
<?php <?php
// Copyright 2004-2008 Facebook. All Rights Reserved. // Copyright 2004-2009 Facebook. All Rights Reserved.
// //
// +---------------------------------------------------------------------------+ // +---------------------------------------------------------------------------+
// | Facebook Platform PHP5 client | // | Facebook Platform PHP5 client |

1026
extlib/facebook/facebookapi_php5_restlib.php Normal file → Executable file

File diff suppressed because it is too large Load Diff

View File

@ -63,6 +63,10 @@ function handleError($error)
function main() function main()
{ {
// quick check for fancy URL auto-detection support in installer.
if (isset($_SERVER['REDIRECT_URL']) && ('/check-fancy' === $_SERVER['REDIRECT_URL'])) {
die("Fancy URL support detection succeeded. We suggest you enable this to get fancy (pretty) URLs.");
}
global $user, $action, $config; global $user, $action, $config;
Snapshot::check(); Snapshot::check();
@ -103,6 +107,8 @@ function main()
$args = array_merge($args, $_REQUEST); $args = array_merge($args, $_REQUEST);
Event::handle('ArgsInitialize', array(&$args));
$action = $args['action']; $action = $args['action'];
if (!$action || !preg_match('/^[a-zA-Z0-9_-]*$/', $action)) { if (!$action || !preg_match('/^[a-zA-Z0-9_-]*$/', $action)) {

View File

@ -35,15 +35,17 @@ function main()
function checkPrereqs() function checkPrereqs()
{ {
$pass = true;
if (file_exists(INSTALLDIR.'/config.php')) { if (file_exists(INSTALLDIR.'/config.php')) {
?><p class="error">Config file &quot;config.php&quot; already exists.</p> ?><p class="error">Config file &quot;config.php&quot; already exists.</p>
<?php <?php
return false; $pass = false;
} }
if (version_compare(PHP_VERSION, '5.0.0', '<')) { if (version_compare(PHP_VERSION, '5.0.0', '<')) {
?><p class="error">Require PHP version 5 or greater.</p><?php ?><p class="error">Require PHP version 5 or greater.</p><?php
return false; $pass = false;
} }
$reqs = array('gd', 'mysql', 'curl', $reqs = array('gd', 'mysql', 'curl',
@ -52,28 +54,26 @@ function checkPrereqs()
foreach ($reqs as $req) { foreach ($reqs as $req) {
if (!checkExtension($req)) { if (!checkExtension($req)) {
?><p class="error">Cannot load required extension &quot;<?php echo $req; ?>&quot;.</p><?php ?><p class="error">Cannot load required extension: <code><?php echo $req; ?></code></p><?php
return false; $pass = false;
} }
} }
if (!is_writable(INSTALLDIR)) { if (!is_writable(INSTALLDIR)) {
?><p class="error">Cannot write config file to &quot;<?php echo INSTALLDIR; ?>&quot;.</p> ?><p class="error">Cannot write config file to: <code><?php echo INSTALLDIR; ?></code></p>
<p>On your server, try this command:</p> <p>On your server, try this command: <code>chmod a+w <?php echo INSTALLDIR; ?></code>
<blockquote>chmod a+w <?php echo INSTALLDIR; ?></blockquote>
<?php <?php
return false; $pass = false;
} }
if (!is_writable(INSTALLDIR.'/avatar/')) { if (!is_writable(INSTALLDIR.'/avatar/')) {
?><p class="error">Cannot write avatar directory &quot;<?php echo INSTALLDIR; ?>/avatar/&quot;.</p> ?><p class="error">Cannot write avatar directory: <code><?php echo INSTALLDIR; ?>/avatar/</code></p>
<p>On your server, try this command:</p> <p>On your server, try this command: <code>chmod a+w <?php echo INSTALLDIR; ?>/avatar/</code></p>
<blockquote>chmod a+w <?php echo INSTALLDIR; ?>/avatar/</blockquote>
<? <?
return false; $pass = false;
} }
return true; return $pass;
} }
function checkExtension($name) function checkExtension($name)
@ -88,96 +88,125 @@ function checkExtension($name)
function showForm() function showForm()
{ {
?> $config_path = htmlentities(trim(dirname($_SERVER['REQUEST_URI']), '/'));
<p>Enter your database connection information below to initialize the database.</p> echo<<<E_O_T
<form method='post' action='install.php'> </ul>
<fieldset> </dd>
<ul class='form_data'> </dl>
<li> <dl id="page_notice" class="system_notice">
<label for='sitename'>Site name</label> <dt>Page notice</dt>
<input type='text' id='sitename' name='sitename' /> <dd>
<p>The name of your site</p> <div class="instructions">
</li> <p>Enter your database connection information below to initialize the database.</p>
<li> </div>
<li> </dd>
<label for='host'>Hostname</label> </dl>
<input type='text' id='host' name='host' /> <form method="post" action="install.php" class="form_settings" id="form_install">
<p>Database hostname</p> <fieldset>
</li> <legend>Connection settings</legend>
<li> <ul class="form_data">
<label for='host'>Database</label> <li>
<input type='text' id='database' name='database' /> <label for="sitename">Site name</label>
<p>Database name</p> <input type="text" id="sitename" name="sitename" />
</li> <p class="form_guide">The name of your site</p>
<li> </li>
<label for='username'>Username</label> <li>
<input type='text' id='username' name='username' /> <label for="fancy-enable">Fancy URLs</label>
<p>Database username</p> <input type="radio" name="fancy" id="fancy-enable" value="enable" checked='checked' /> enable<br />
</li> <input type="radio" name="fancy" id="fancy-disable" value="" /> disable<br />
<li> <p class="form_guide" id='fancy-form_guide'>Enable fancy (pretty) URLs. Auto-detection failed, it depends on Javascript.</p>
<label for='password'>Password</label> </li>
<input type='password' id='password' name='password' /> <li>
<p>Database password</p> <label for="host">Hostname</label>
</li> <input type="text" id="host" name="host" />
</ul> <p class="form_guide">Database hostname</p>
<input type='submit' name='submit' value='Submit'> </li>
</fieldset> <li>
<label for="host">Site path</label>
<input type="text" id="path" name="path" value="$config_path" />
<p class="form_guide">Site path, following the "/" after the domain name in the URL. Empty is fine. Field should be filled automatically.</p>
</li>
<li>
<label for="host">Database</label>
<input type="text" id="database" name="database" />
<p class="form_guide">Database name</p>
</li>
<li>
<label for="username">Username</label>
<input type="text" id="username" name="username" />
<p class="form_guide">Database username</p>
</li>
<li>
<label for="password">Password</label>
<input type="password" id="password" name="password" />
<p class="form_guide">Database password</p>
</li>
</ul>
<input type="submit" name="submit" class="submit" value="Submit" />
</fieldset>
</form> </form>
<?
E_O_T;
} }
function updateStatus($status, $error=false) function updateStatus($status, $error=false)
{ {
?> ?>
<li> <li <?php echo ($error) ? 'class="error"': ''; ?>><?print $status;?></li>
<?
print $status; <?php
?>
</li>
<?
} }
function handlePost() function handlePost()
{ {
?> ?>
<ul>
<? <?php
$host = $_POST['host']; $host = $_POST['host'];
$database = $_POST['database']; $database = $_POST['database'];
$username = $_POST['username']; $username = $_POST['username'];
$password = $_POST['password']; $password = $_POST['password'];
$sitename = $_POST['sitename']; $sitename = $_POST['sitename'];
$path = $_POST['path'];
$fancy = !empty($_POST['fancy']);
?>
<dl class="system_notice">
<dt>Page notice</dt>
<dd>
<ul>
<?php
$fail = false;
if (empty($host)) { if (empty($host)) {
updateStatus("No hostname specified.", true); updateStatus("No hostname specified.", true);
showForm(); $fail = true;
return;
} }
if (empty($database)) { if (empty($database)) {
updateStatus("No database specified.", true); updateStatus("No database specified.", true);
showForm(); $fail = true;
return;
} }
if (empty($username)) { if (empty($username)) {
updateStatus("No username specified.", true); updateStatus("No username specified.", true);
showForm(); $fail = true;
return;
} }
if (empty($password)) { if (empty($password)) {
updateStatus("No password specified.", true); updateStatus("No password specified.", true);
showForm(); $fail = true;
return;
} }
if (empty($sitename)) { if (empty($sitename)) {
updateStatus("No sitename specified.", true); updateStatus("No sitename specified.", true);
showForm(); $fail = true;
return;
} }
if($fail){
showForm();
return;
}
updateStatus("Starting installation..."); updateStatus("Starting installation...");
updateStatus("Checking database..."); updateStatus("Checking database...");
$conn = mysql_connect($host, $username, $password); $conn = mysql_connect($host, $username, $password);
@ -214,24 +243,30 @@ function handlePost()
} }
updateStatus("Writing config file..."); updateStatus("Writing config file...");
$sqlUrl = "mysqli://$username:$password@$host/$database"; $sqlUrl = "mysqli://$username:$password@$host/$database";
$res = writeConf($sitename, $sqlUrl); $res = writeConf($sitename, $sqlUrl, $fancy, $path);
if (!$res) { if (!$res) {
updateStatus("Can't write config file.", true); updateStatus("Can't write config file.", true);
showForm(); showForm();
return; return;
} }
updateStatus("Done!"); updateStatus("Done!");
if ($path) $path .= '/';
updateStatus("You can visit your <a href='/$path'>new Laconica site</a>.");
?> ?>
</ul>
<? <?php
} }
function writeConf($sitename, $sqlUrl) function writeConf($sitename, $sqlUrl, $fancy, $path)
{ {
$res = file_put_contents(INSTALLDIR.'/config.php', $res = file_put_contents(INSTALLDIR.'/config.php',
"<?php\n". "<?php\n".
"if (!defined('LACONICA')) { exit(1); }\n\n".
"\$config['site']['name'] = \"$sitename\";\n\n". "\$config['site']['name'] = \"$sitename\";\n\n".
"\$config['db']['database'] = \"$sqlUrl\";\n\n"); ($fancy ? "\$config['site']['fancy'] = true;\n\n":'').
"\$config['site']['path'] = \"$path\";\n\n".
"\$config['db']['database'] = \"$sqlUrl\";\n\n".
"?>");
return $res; return $res;
} }
@ -253,21 +288,37 @@ function runDbScript($filename, $conn)
} }
?> ?>
<html> <?php echo"<?"; ?> xml version="1.0" encoding="UTF-8" <?php echo "?>"; ?>
<head> <!DOCTYPE html
<title>Install Laconica</title> PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
<link rel="stylesheet" type="text/css" href="theme/base/css/display.css?version=0.7.1" media="screen, projection, tv"/> "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<link rel="stylesheet" type="text/css" href="theme/base/css/modal.css?version=0.7.1" media="screen, projection, tv"/> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en_US" lang="en_US">
<link rel="stylesheet" type="text/css" href="theme/default/css/display.css?version=0.7.1" media="screen, projection, tv"/> <head>
</head> <title>Install Laconica</title>
<body> <link rel="stylesheet" type="text/css" href="theme/base/css/display.css?version=0.8" media="screen, projection, tv"/>
<div id="wrap"> <link rel="stylesheet" type="text/css" href="theme/default/css/display.css?version=0.8" media="screen, projection, tv"/>
<div id="core"> <!--[if IE]><link rel="stylesheet" type="text/css" href="theme/base/css/ie.css?version=0.8" /><![endif]-->
<div id="content"> <!--[if lte IE 6]><link rel="stylesheet" type="text/css" theme/base/css/ie6.css?version=0.8" /><![endif]-->
<h1>Install Laconica</h1> <!--[if IE]><link rel="stylesheet" type="text/css" href="theme/earthy/css/ie.css?version=0.8" /><![endif]-->
<script src='js/jquery.min.js'></script>
<script src='js/install.js'></script>
</head>
<body id="install">
<div id="wrap">
<div id="header">
<address id="site_contact" class="vcard">
<a class="url home bookmark" href=".">
<img class="logo photo" src="theme/default/logo.png" alt="Laconica"/>
<span class="fn org">Laconica</span>
</a>
</address>
</div>
<div id="core">
<div id="content">
<h1>Install Laconica</h1>
<?php main(); ?> <?php main(); ?>
</div> </div>
</div> </div>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,10 +1,85 @@
/** Init for Farbtastic library and page setup
*
* @package Laconica
* @author Sarven Capadisli <csarven@controlyourself.ca>
* @copyright 2009 Control Yourself, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
$(document).ready(function() { $(document).ready(function() {
var f = $.farbtastic('#color-picker'); function UpdateColors(S) {
var colors = $('#settings_design_color input'); C = $(S).val();
switch (parseInt(S.id.slice(-1))) {
case 0: default:
$('body').css({'background-color':C});
break;
case 1:
$('#content').css({'background-color':C});
break;
case 2:
$('#aside_primary').css({'background-color':C});
break;
case 3:
$('body').css({'color':C});
break;
case 4:
$('a').css({'color':C});
break;
}
}
colors function UpdateFarbtastic(e) {
.each(function () { f.linkTo(this); }) f.linked = e;
.focus(function() { f.setColor(e.value);
f.linkTo(this); }
});
function UpdateSwatch(e) {
$(e).css({"background-color": e.value,
"color": f.hsl[2] > 0.5 ? "#000": "#fff"});
}
function SynchColors(e) {
var S = f.linked;
var C = f.color;
if (S && S.value && S.value != C) {
S.value = C;
UpdateSwatch(S);
UpdateColors(S);
}
}
function Init() {
$('#settings_design_color').append('<div id="color-picker"></div>');
$('#color-picker').hide();
f = $.farbtastic('#color-picker', SynchColors);
swatches = $('#settings_design_color .swatch');
swatches
.each(SynchColors)
.blur(function() {
$(this).val($(this).val().toUpperCase());
})
.focus(function() {
$('#color-picker').show();
UpdateFarbtastic(this);
})
.change(function() {
UpdateFarbtastic(this);
UpdateSwatch(this);
UpdateColors(this);
}).change();
}
var f, swatches;
Init();
$('#form_settings_design').bind('reset', function(){
setTimeout(function(){
swatches.each(function(){UpdateColors(this);});
$('#color-picker').remove();
swatches.unbind();
Init();
},10);
});
}); });

View File

@ -128,7 +128,7 @@
var a = document.createElement('A'); var a = document.createElement('A');
a.innerHTML = 'get this'; a.innerHTML = 'get this';
a.target = '_blank'; a.target = '_blank';
a.href = 'http://identica/doc/badge'; a.href = 'http://identi.ca/doc/badge';
$.s.f.appendChild(a); $.s.f.appendChild(a);
$.s.appendChild($.s.f); $.s.appendChild($.s.f);
$.f.getUser(); $.f.getUser();

18
js/install.js Normal file
View File

@ -0,0 +1,18 @@
$(document).ready(function(){
$.ajax({url:'check-fancy',
type:'GET',
success:function(data, textStatus) {
$('#fancy-enable').attr('checked', true);
$('#fancy-disable').attr('checked', false);
$('#fancy-form_guide').text(data);
},
error:function(XMLHttpRequest, textStatus, errorThrown) {
$('#fancy-enable').attr('checked', false);
$('#fancy-disable').attr('checked', true);
$('#fancy-enable').attr('disabled', true);
$('#fancy-disable').attr('disabled', true);
$('#fancy-form_guide').text("Fancy URL support detection failed, disabling this option. Make sure you renamed htaccess.sample to .htaccess.");
}
});
});

View File

@ -1,39 +1,48 @@
$(function(){ /** Init for Jcrop library and page setup
var x = ($('#avatar_crop_x').val()) ? $('#avatar_crop_x').val() : 0; *
var y = ($('#avatar_crop_y').val()) ? $('#avatar_crop_y').val() : 0; * @package Laconica
var w = ($('#avatar_crop_w').val()) ? $('#avatar_crop_w').val() : $("#avatar_original img").attr("width"); * @author Sarven Capadisli <csarven@controlyourself.ca>
var h = ($('#avatar_crop_h').val()) ? $('#avatar_crop_h').val() : $("#avatar_original img").attr("height"); * @copyright 2009 Control Yourself, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
jQuery("#avatar_original img").Jcrop({ $(function(){
onChange: showPreview, var x = ($('#avatar_crop_x').val()) ? $('#avatar_crop_x').val() : 0;
setSelect: [ x, y, w, h ], var y = ($('#avatar_crop_y').val()) ? $('#avatar_crop_y').val() : 0;
onSelect: updateCoords, var w = ($('#avatar_crop_w').val()) ? $('#avatar_crop_w').val() : $("#avatar_original img").attr("width");
aspectRatio: 1, var h = ($('#avatar_crop_h').val()) ? $('#avatar_crop_h').val() : $("#avatar_original img").attr("height");
boxWidth: 480,
boxHeight: 480,
bgColor: '#000',
bgOpacity: .4
});
});
function showPreview(coords) { jQuery("#avatar_original img").Jcrop({
var rx = 96 / coords.w; onChange: showPreview,
var ry = 96 / coords.h; setSelect: [ x, y, w, h ],
onSelect: updateCoords,
aspectRatio: 1,
boxWidth: 480,
boxHeight: 480,
bgColor: '#000',
bgOpacity: .4
});
});
var img_width = $("#avatar_original img").attr("width"); function showPreview(coords) {
var img_height = $("#avatar_original img").attr("height"); var rx = 96 / coords.w;
var ry = 96 / coords.h;
$('#avatar_preview img').css({ var img_width = $("#avatar_original img").attr("width");
width: Math.round(rx *img_width) + 'px', var img_height = $("#avatar_original img").attr("height");
height: Math.round(ry * img_height) + 'px',
marginLeft: '-' + Math.round(rx * coords.x) + 'px',
marginTop: '-' + Math.round(ry * coords.y) + 'px'
});
};
function updateCoords(c) { $('#avatar_preview img').css({
$('#avatar_crop_x').val(c.x); width: Math.round(rx *img_width) + 'px',
$('#avatar_crop_y').val(c.y); height: Math.round(ry * img_height) + 'px',
$('#avatar_crop_w').val(c.w); marginLeft: '-' + Math.round(rx * coords.x) + 'px',
$('#avatar_crop_h').val(c.h); marginTop: '-' + Math.round(ry * coords.y) + 'px'
}; });
};
function updateCoords(c) {
$('#avatar_crop_x').val(c.x);
$('#avatar_crop_y').val(c.y);
$('#avatar_crop_w').val(c.w);
$('#avatar_crop_h').val(c.h);
};

6
js/jquery.joverlay.min.js vendored Normal file
View File

@ -0,0 +1,6 @@
/* Copyright (c) 2009 Alvaro A. Lima Jr http://alvarojunior.com/jquery/joverlay.html
* Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
* Version: 0.6 (Abr 23, 2009)
* Requires: jQuery 1.3+
*/
(function($){var f=$.browser.msie&&$.browser.version==6.0;var g=null;$.fn.jOverlay=function(b){var b=$.extend({},$.fn.jOverlay.options,b);if(g!=null){clearTimeout(g)}var c=this.is('*')?this:'#jOverlayContent';var d=f?'absolute':'fixed';var e=b.imgLoading?"<img id='jOverlayLoading' src='"+b.imgLoading+"' style='position:"+d+"; z-index:"+(b.zIndex+9)+";'/>":'';$('body').prepend(e+"<div id='jOverlay' />"+"<div id='jOverlayContent' style='position:"+d+"; z-index:"+(b.zIndex+5)+"; display:none;'/>");$('#jOverlayLoading').load(function(){if(b.center){$.center(this)}});if(f){$("select").hide();$("#jOverlayContent select").show()}$('#jOverlay').css({backgroundColor:b.color,position:d,top:'0px',left:'0px',filter:'alpha(opacity='+(b.opacity*100)+')',opacity:b.opacity,zIndex:b.zIndex,width:!f?'100%':$(window).width()+'px',height:!f?'100%':$(document).height()+'px'}).show();if(this.is('*')){$('#jOverlayContent').html(this.addClass('jOverlayChildren').show()).show();if(b.center){$.center('#jOverlayContent')}if(!b.url&&$.isFunction(b.success)){b.success(this.html())}}if(b.url){$.ajax({type:b.method,data:b.data,url:b.url,success:function(a){$('#jOverlayLoading').fadeOut(600);$(c).html(a).show();if(b.center){$.center('#jOverlayContent')}if($.isFunction(b.success)){b.success(a)}}})}if(f){$(window).scroll(function(){if(b.center){$.center('#jOverlayContent')}});$(window).resize(function(){$('#jOverlay').css({width:$(window).width()+'px',height:$(document).height()+'px'});if(b.center){$.center('#jOverlayContent')}})}$(document).keydown(function(a){if(a.keyCode==27){$.closeOverlay()}});if(b.bgClickToClose){$('#jOverlay').click($.closeOverlay)}if(Number(b.timeout)>0){g=setTimeout($.closeOverlay,Number(b.timeout))}};$.center=function(a){var a=$(a);var b=a.height();var c=a.width();a.css({width:c+'px',marginLeft:'-'+(c/2)+'px',marginTop:'-'+b/2+'px',height:'auto',top:!f?'50%':$(window).scrollTop()+($(window).height()/2)+"px",left:'50%'})};$.fn.jOverlay.options={method:'GET',data:'',url:'',color:'#000',opacity:'0.6',zIndex:9999,center:true,imgLoading:'',bgClickToClose:true,success:null,timeout:0};$.closeOverlay=function(){if(f){$("select").show()}$('#jOverlayContent .jOverlayChildren').hide().prependTo($('body'));$('#jOverlayLoading, #jOverlayContent, #jOverlay').remove()}})(jQuery);

View File

@ -17,6 +17,10 @@
*/ */
$(document).ready(function(){ $(document).ready(function(){
$('.attachments').click(function() {$().jOverlay({zIndex:999, success:function(html) {$('.attachment').click(function() {$().jOverlay({url:$(this).attr('href') + '/ajax'}); return false; });
}, url:$(this).attr('href') + '/ajax'}); return false; });
$('.attachment').click(function() {$().jOverlay({url:$(this).attr('href') + '/ajax'}); return false; });
// count character on keyup // count character on keyup
function counter(event){ function counter(event){
var maxLength = 140; var maxLength = 140;
@ -166,19 +170,45 @@ $(document).ready(function(){
$("#notice_action-submit").addClass("disabled"); $("#notice_action-submit").addClass("disabled");
return true; return true;
}, },
success: function(xml) { if ($("#error", xml).length > 0 || $("#command_result", xml).length > 0) { timeout: '60000',
error: function (xhr, textStatus, errorThrown) { $("#form_notice").removeClass("processing");
$("#notice_action-submit").removeAttr("disabled");
$("#notice_action-submit").removeClass("disabled");
if (textStatus == "timeout") {
alert ("Sorry! We had trouble sending your notice. The servers are overloaded. Please try again, and contact the site administrator if this problem persists");
}
else {
if ($(".error", xhr.responseXML).length > 0) {
$('#form_notice').append(document._importNode($(".error", xhr.responseXML).get(0), true));
}
else {
alert("Sorry! We had trouble sending your notice ("+xhr.status+" "+xhr.statusText+"). Please report the problem to the site administrator if this happens again.");
}
}
},
success: function(xml) { if ($("#error", xml).length > 0) {
var result = document._importNode($("p", xml).get(0), true); var result = document._importNode($("p", xml).get(0), true);
result = result.textContent || result.innerHTML; result = result.textContent || result.innerHTML;
alert(result); alert(result);
} }
else { else {
$("#notices_primary .notices").prepend(document._importNode($("li", xml).get(0), true)); if ($("#command_result", xml).length > 0) {
var result = document._importNode($("p", xml).get(0), true);
result = result.textContent || result.innerHTML;
alert(result);
}
else {
li = $("li", xml).get(0);
if ($("#"+li.id).length == 0) {
$("#notices_primary .notices").prepend(document._importNode(li, true));
$("#notices_primary .notice:first").css({display:"none"});
$("#notices_primary .notice:first").fadeIn(2500);
NoticeHover();
NoticeReply();
}
}
$("#notice_data-text").val(""); $("#notice_data-text").val("");
counter(); counter();
$("#notices_primary .notice:first").css({display:"none"});
$("#notices_primary .notice:first").fadeIn(2500);
NoticeHover();
NoticeReply();
} }
$("#form_notice").removeClass("processing"); $("#form_notice").removeClass("processing");
$("#notice_action-submit").removeAttr("disabled"); $("#notice_action-submit").removeAttr("disabled");
@ -187,7 +217,6 @@ $(document).ready(function(){
}; };
$("#form_notice").ajaxForm(PostNotice); $("#form_notice").ajaxForm(PostNotice);
$("#form_notice").each(addAjaxHidden); $("#form_notice").each(addAjaxHidden);
NoticeHover(); NoticeHover();
NoticeReply(); NoticeReply();
}); });

View File

@ -22,6 +22,7 @@ if (!defined('LACONICA')) { exit(1); }
class ShortUrlApi class ShortUrlApi
{ {
protected $service_url; protected $service_url;
protected $long_limit = 27;
function __construct($service_url) function __construct($service_url)
{ {
@ -39,7 +40,7 @@ class ShortUrlApi
} }
private function is_long($url) { private function is_long($url) {
return strlen($url) >= 30; return strlen($url) >= $this->long_limit;
} }
protected function http_post($data) { protected function http_post($data) {

View File

@ -98,15 +98,15 @@ class Action extends HTMLOutputter // lawsuit
Event::handle('EndShowHTML', array($this)); Event::handle('EndShowHTML', array($this));
} }
if (Event::handle('StartShowHead', array($this))) { if (Event::handle('StartShowHead', array($this))) {
$this->showHead(); $this->showHead();
Event::handle('EndShowHead', array($this)); Event::handle('EndShowHead', array($this));
} }
if (Event::handle('StartShowBody', array($this))) { if (Event::handle('StartShowBody', array($this))) {
$this->showBody(); $this->showBody();
Event::handle('EndShowBody', array($this)); Event::handle('EndShowBody', array($this));
} }
if (Event::handle('StartEndHTML', array($this))) { if (Event::handle('StartEndHTML', array($this))) {
$this->endHTML(); $this->endHTML();
Event::handle('EndEndHTML', array($this)); Event::handle('EndEndHTML', array($this));
} }
} }
@ -243,6 +243,12 @@ class Action extends HTMLOutputter // lawsuit
$this->element('script', array('type' => 'text/javascript', $this->element('script', array('type' => 'text/javascript',
'src' => common_path('js/jquery.form.js')), 'src' => common_path('js/jquery.form.js')),
' '); ' ');
$this->element('script', array('type' => 'text/javascript',
'src' => common_path('js/jquery.joverlay.min.js')),
' ');
Event::handle('EndShowJQueryScripts', array($this)); Event::handle('EndShowJQueryScripts', array($this));
} }
if (Event::handle('StartShowLaconicaScripts', array($this))) { if (Event::handle('StartShowLaconicaScripts', array($this))) {
@ -347,7 +353,7 @@ class Action extends HTMLOutputter // lawsuit
{ {
$this->elementStart('body', (common_current_user()) ? array('id' => $this->trimmed('action'), $this->elementStart('body', (common_current_user()) ? array('id' => $this->trimmed('action'),
'class' => 'user_in') 'class' => 'user_in')
: array('id' => $this->trimmed('action'))); : array('id' => $this->trimmed('action')));
$this->elementStart('div', array('id' => 'wrap')); $this->elementStart('div', array('id' => 'wrap'));
if (Event::handle('StartShowHeader', array($this))) { if (Event::handle('StartShowHeader', array($this))) {
$this->showHeader(); $this->showHeader();
@ -431,10 +437,10 @@ class Action extends HTMLOutputter // lawsuit
_('Connect'), _('Connect to SMS, Twitter'), false, 'nav_connect'); _('Connect'), _('Connect to SMS, Twitter'), false, 'nav_connect');
} }
$this->menuItem(common_local_url('invite'), $this->menuItem(common_local_url('invite'),
_('Invite'), _('Invite'),
sprintf(_('Invite friends and colleagues to join you on %s'), sprintf(_('Invite friends and colleagues to join you on %s'),
common_config('site', 'name')), common_config('site', 'name')),
false, 'nav_invitecontact'); false, 'nav_invitecontact');
$this->menuItem(common_local_url('logout'), $this->menuItem(common_local_url('logout'),
_('Logout'), _('Logout from the site'), false, 'nav_logout'); _('Logout'), _('Logout from the site'), false, 'nav_logout');
} }
@ -591,7 +597,10 @@ class Action extends HTMLOutputter // lawsuit
'class' => 'system_notice')); 'class' => 'system_notice'));
$this->element('dt', null, _('Page notice')); $this->element('dt', null, _('Page notice'));
$this->elementStart('dd'); $this->elementStart('dd');
$this->showPageNotice(); if (Event::handle('StartShowPageNotice', array($this))) {
$this->showPageNotice();
Event::handle('EndShowPageNotice', array($this));
}
$this->elementEnd('dd'); $this->elementEnd('dd');
$this->elementEnd('dl'); $this->elementEnd('dl');
} }
@ -629,7 +638,7 @@ class Action extends HTMLOutputter // lawsuit
$this->elementStart('div', array('id' => 'aside_primary', $this->elementStart('div', array('id' => 'aside_primary',
'class' => 'aside')); 'class' => 'aside'));
if (Event::handle('StartShowExportData', array($this))) { if (Event::handle('StartShowExportData', array($this))) {
$this->showExportData(); $this->showExportData();
Event::handle('EndShowExportData', array($this)); Event::handle('EndShowExportData', array($this));
} }
if (Event::handle('StartShowSections', array($this))) { if (Event::handle('StartShowSections', array($this))) {

300
lib/attachmentlist.php Normal file
View File

@ -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));
}
}
}
}

View File

@ -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';
}
}

80
lib/attachmentsection.php Normal file
View File

@ -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');
}
}

View File

@ -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;
}
}

View File

@ -143,6 +143,8 @@ $config =
array('piddir' => '/var/run', array('piddir' => '/var/run',
'user' => false, 'user' => false,
'group' => false), 'group' => false),
'twitterbridge' =>
array('enabled' => false),
'integration' => 'integration' =>
array('source' => 'Laconica', # source attribute for Twitter array('source' => 'Laconica', # source attribute for Twitter
'taguri' => $_server.',2009'), # base for tag URIs 'taguri' => $_server.',2009'), # base for tag URIs
@ -197,7 +199,7 @@ $_config_files[] = INSTALLDIR.'/config.php';
$_have_a_config = false; $_have_a_config = false;
foreach ($_config_files as $_config_file) { foreach ($_config_files as $_config_file) {
if (file_exists($_config_file)) { if (@file_exists($_config_file)) {
include_once($_config_file); include_once($_config_file);
$_have_a_config = true; $_have_a_config = true;
} }

View File

@ -27,9 +27,21 @@ define("FACEBOOK_PROMPTED_UPDATE_PREF", 2);
function getFacebook() function getFacebook()
{ {
static $facebook = null;
$apikey = common_config('facebook', 'apikey'); $apikey = common_config('facebook', 'apikey');
$secret = common_config('facebook', 'secret'); $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) { function updateProfileBox($facebook, $flink, $notice) {
@ -92,7 +104,6 @@ function isFacebookBound($notice, $flink) {
} }
function facebookBroadcastNotice($notice) function facebookBroadcastNotice($notice)
{ {
$facebook = getFacebook(); $facebook = getFacebook();

View File

@ -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';
}
}

View File

@ -134,9 +134,11 @@ class GalleryAction extends Action
$this->elementStart('li', array('id'=>'filter_tags_item')); $this->elementStart('li', array('id'=>'filter_tags_item'));
$this->elementStart('form', array('name' => 'bytag', $this->elementStart('form', array('name' => 'bytag',
'id' => 'bytag', 'id' => 'bytag',
'action' => common_path('?action=' . $this->trimmed('action')),
'method' => 'post')); 'method' => 'post'));
$this->dropdown('tag', _('Tag'), $content, $this->dropdown('tag', _('Tag'), $content,
_('Choose a tag to narrow list'), false, $tag); _('Choose a tag to narrow list'), false, $tag);
$this->hidden('nickname', $this->user->nickname);
$this->submit('submit', _('Go')); $this->submit('submit', _('Go'));
$this->elementEnd('form'); $this->elementEnd('form');
$this->elementEnd('li'); $this->elementEnd('li');
@ -169,4 +171,4 @@ class GalleryAction extends Action
{ {
return array(); return array();
} }
} }

View File

@ -179,6 +179,7 @@ class NoticeListItem extends Widget
{ {
$this->showStart(); $this->showStart();
$this->showNotice(); $this->showNotice();
$this->showNoticeAttachments();
$this->showNoticeInfo(); $this->showNoticeInfo();
$this->showNoticeOptions(); $this->showNoticeOptions();
$this->showEnd(); $this->showEnd();
@ -192,6 +193,48 @@ class NoticeListItem extends Widget
$this->out->elementEnd('div'); $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() function showNoticeInfo()
{ {
$this->out->elementStart('div', 'entry-content'); $this->out->elementStart('div', 'entry-content');

View File

@ -51,17 +51,13 @@ class NoticeSection extends Section
function showContent() function showContent()
{ {
$notices = $this->getNotices(); $notices = $this->getNotices();
$cnt = 0; $cnt = 0;
$this->out->elementStart('ul', 'notices'); $this->out->elementStart('ul', 'notices');
while ($notices->fetch() && ++$cnt <= NOTICES_PER_SECTION) { while ($notices->fetch() && ++$cnt <= NOTICES_PER_SECTION) {
$this->showNotice($notices); $this->showNotice($notices);
} }
$this->out->elementEnd('ul'); $this->out->elementEnd('ul');
return ($cnt > NOTICES_PER_SECTION); return ($cnt > NOTICES_PER_SECTION);
} }
@ -100,6 +96,37 @@ class NoticeSection extends Section
$this->out->elementStart('p', 'entry-content'); $this->out->elementStart('p', 'entry-content');
$this->out->raw($notice->rendered); $this->out->raw($notice->rendered);
$notice_link_cfg = common_config('site', 'notice_link');
if ('direct' === $notice_link_cfg) {
$this->out->text(' (');
$this->out->element('a', array('href' => $notice->uri), 'see');
$this->out->text(')');
} elseif ('attachment' === $notice_link_cfg) {
if ($count = $notice->hasAttachments()) {
// link to attachment(s) pages
if (1 === $count) {
$f2p = File_to_post::staticGet('post_id', $notice->id);
$href = common_local_url('attachment', array('attachment' => $f2p->file_id));
$att_class = 'attachment';
} else {
$href = common_local_url('attachments', array('notice' => $notice->id));
$att_class = 'attachments';
}
$clip = theme_path('images/icons/clip.png', 'base');
$this->out->elementStart('a', array('class' => $att_class, 'style' => "font-style: italic;", 'href' => $href, 'title' => "# of attachments: $count"));
$this->out->raw(" ($count&nbsp");
$this->out->element('img', array('style' => 'display: inline', 'align' => 'top', 'width' => 20, 'height' => 20, 'src' => $clip, 'alt' => 'alt'));
$this->out->text(')');
$this->out->elementEnd('a');
} else {
$this->out->text(' (');
$this->out->element('a', array('href' => $notice->uri), 'see');
$this->out->text(')');
}
}
$this->out->elementEnd('p'); $this->out->elementEnd('p');
if (!empty($notice->value)) { if (!empty($notice->value)) {
$this->out->elementStart('p'); $this->out->elementStart('p');

View File

@ -93,43 +93,45 @@ class PersonalGroupNav extends Widget
$this->out->elementStart('ul', array('class' => 'nav')); $this->out->elementStart('ul', array('class' => 'nav'));
$this->out->menuItem(common_local_url('all', array('nickname' => if (Event::handle('StartPersonalGroupNav', array($this))) {
$nickname)), $this->out->menuItem(common_local_url('all', array('nickname' =>
_('Personal'), $nickname)),
sprintf(_('%s and friends'), (($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname)), _('Personal'),
$action == 'all', 'nav_timeline_personal'); sprintf(_('%s and friends'), (($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname)),
$this->out->menuItem(common_local_url('replies', array('nickname' => $action == 'all', 'nav_timeline_personal');
$nickname)), $this->out->menuItem(common_local_url('replies', array('nickname' =>
_('Replies'), $nickname)),
sprintf(_('Replies to %s'), (($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname)), _('Replies'),
$action == 'replies', 'nav_timeline_replies'); sprintf(_('Replies to %s'), (($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname)),
$this->out->menuItem(common_local_url('showstream', array('nickname' => $action == 'replies', 'nav_timeline_replies');
$nickname)), $this->out->menuItem(common_local_url('showstream', array('nickname' =>
_('Profile'), $nickname)),
($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname, _('Profile'),
$action == 'showstream', 'nav_profile'); ($user_profile && $user_profile->fullname) ? $user_profile->fullname : $nickname,
$this->out->menuItem(common_local_url('showfavorites', array('nickname' => $action == 'showstream', 'nav_profile');
$nickname)), $this->out->menuItem(common_local_url('showfavorites', array('nickname' =>
_('Favorites'), $nickname)),
sprintf(_('%s\'s favorite notices'), ($user_profile) ? $user_profile->getBestName() : _('User')), _('Favorites'),
$action == 'showfavorites', 'nav_timeline_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' => $this->out->menuItem(common_local_url('inbox', array('nickname' =>
$nickname)), $nickname)),
_('Inbox'), _('Inbox'),
_('Your incoming messages'), _('Your incoming messages'),
$action == 'inbox'); $action == 'inbox');
$this->out->menuItem(common_local_url('outbox', array('nickname' => $this->out->menuItem(common_local_url('outbox', array('nickname' =>
$nickname)), $nickname)),
_('Outbox'), _('Outbox'),
_('Your sent messages'), _('Your sent messages'),
$action == 'outbox'); $action == 'outbox');
}
Event::handle('EndPersonalGroupNav', array($this));
} }
$this->out->elementEnd('ul'); $this->out->elementEnd('ul');
} }
} }

View File

@ -51,7 +51,7 @@ class PopularNoticeSection extends NoticeSection
if (common_config('db', 'type') == 'pgsql') { if (common_config('db', 'type') == 'pgsql') {
$weightexpr='sum(exp(-extract(epoch from (now() - fave.modified)) / %s))'; $weightexpr='sum(exp(-extract(epoch from (now() - fave.modified)) / %s))';
if (!empty($this->out->tag)) { if (!empty($this->out->tag)) {
$tag = pg_escape_string($this->tag); $tag = pg_escape_string($this->out->tag);
} }
} else { } else {
$weightexpr='sum(exp(-(now() - fave.modified) / %s))'; $weightexpr='sum(exp(-(now() - fave.modified) / %s))';

View File

@ -49,16 +49,17 @@ require_once INSTALLDIR.'/lib/groupminilist.php';
class ProfileAction extends Action class ProfileAction extends Action
{ {
var $user = null; var $user = null;
var $page = null; var $page = null;
var $profile = null; var $profile = null;
var $tag = null;
function prepare($args) function prepare($args)
{ {
parent::prepare($args); parent::prepare($args);
$nickname_arg = $this->arg('nickname'); $nickname_arg = $this->arg('nickname');
$nickname = common_canonical_nickname($nickname_arg); $nickname = common_canonical_nickname($nickname_arg);
// Permanent redirect on non-canonical nickname // Permanent redirect on non-canonical nickname
@ -85,10 +86,9 @@ class ProfileAction extends Action
return false; return false;
} }
$this->tag = $this->trimmed('tag');
$this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1; $this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1;
common_set_returnto($this->selfUrl()); common_set_returnto($this->selfUrl());
return true; return true;
} }
@ -244,4 +244,5 @@ class ProfileAction extends Action
$this->elementEnd('div'); $this->elementEnd('div');
} }
} }

View File

@ -151,12 +151,26 @@ class Router
$m->connect('search/notice/rss?q=:q', array('action' => 'noticesearchrss'), $m->connect('search/notice/rss?q=:q', array('action' => 'noticesearchrss'),
array('q' => '.+')); 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 // notice
$m->connect('notice/new', array('action' => 'newnotice')); $m->connect('notice/new', array('action' => 'newnotice'));
$m->connect('notice/new?replyto=:replyto', $m->connect('notice/new?replyto=:replyto',
array('action' => 'newnotice'), array('action' => 'newnotice'),
array('replyto' => '[A-Za-z0-9_-]+')); 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', $m->connect('notice/:notice',
array('action' => 'shownotice'), array('action' => 'shownotice'),
array('notice' => '[0-9]+')); array('notice' => '[0-9]+'));
@ -237,12 +251,12 @@ class Router
$m->connect('api/statuses/:method', $m->connect('api/statuses/:method',
array('action' => 'api', array('action' => 'api',
'apiaction' => 'statuses'), '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', $m->connect('api/statuses/:method/:argument',
array('action' => 'api', array('action' => 'api',
'apiaction' => 'statuses'), 'apiaction' => 'statuses'),
array('method' => '(user_timeline|friends_timeline|replies|show|destroy|friends|followers)')); array('method' => '(user_timeline|friends_timeline|replies|mentions|show|destroy|friends|followers)'));
// users // users
@ -412,6 +426,16 @@ class Router
array('size' => '(original|96|48|24)', array('size' => '(original|96|48|24)',
'nickname' => '[a-zA-Z0-9]{1,64}')); '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', $m->connect(':nickname',
array('action' => 'showstream'), array('action' => 'showstream'),
array('nickname' => '[a-zA-Z0-9]{1,64}')); array('nickname' => '[a-zA-Z0-9]{1,64}'));

View File

@ -97,7 +97,11 @@ class Rss10Action extends Action
// Parent handling, including cache check // Parent handling, including cache check
parent::handle($args); parent::handle($args);
// Get the list of notices // Get the list of notices
$this->notices = $this->getNotices($this->limit); if (empty($this->tag)) {
$this->notices = $this->getNotices($this->limit);
} else {
$this->notices = $this->getTaggedNotices($this->tag, $this->limit);
}
$this->showRss(); $this->showRss();
} }

View File

@ -74,38 +74,44 @@ class SubGroupNav extends Widget
$this->out->elementStart('ul', array('class' => 'nav')); $this->out->elementStart('ul', array('class' => 'nav'));
$this->out->menuItem(common_local_url('subscriptions', if (Event::handle('StartSubGroupNav', array($this))) {
array('nickname' =>
$this->user->nickname)), $this->out->menuItem(common_local_url('subscriptions',
_('Subscriptions'), array('nickname' =>
sprintf(_('People %s subscribes to'), $this->user->nickname)),
$this->user->nickname), _('Subscriptions'),
$action == 'subscriptions', sprintf(_('People %s subscribes to'),
'nav_subscriptions'); $this->user->nickname),
$this->out->menuItem(common_local_url('subscribers', $action == 'subscriptions',
array('nickname' => 'nav_subscriptions');
$this->user->nickname)), $this->out->menuItem(common_local_url('subscribers',
_('Subscribers'), array('nickname' =>
sprintf(_('People subscribed to %s'), $this->user->nickname)),
$this->user->nickname), _('Subscribers'),
$action == 'subscribers', sprintf(_('People subscribed to %s'),
'nav_subscribers'); $this->user->nickname),
$this->out->menuItem(common_local_url('usergroups', $action == 'subscribers',
array('nickname' => 'nav_subscribers');
$this->user->nickname)), $this->out->menuItem(common_local_url('usergroups',
_('Groups'), array('nickname' =>
sprintf(_('Groups %s is a member of'), $this->user->nickname)),
$this->user->nickname), _('Groups'),
$action == 'usergroups', sprintf(_('Groups %s is a member of'),
'nav_usergroups'); $this->user->nickname),
if (!is_null($cur) && $this->user->id === $cur->id) { $action == 'usergroups',
$this->out->menuItem(common_local_url('invite'), 'nav_usergroups');
_('Invite'), if (!is_null($cur) && $this->user->id === $cur->id) {
sprintf(_('Invite friends and colleagues to join you on %s'), $this->out->menuItem(common_local_url('invite'),
common_config('site', 'name')), _('Invite'),
$action == 'invite', sprintf(_('Invite friends and colleagues to join you on %s'),
'nav_invite'); common_config('site', 'name')),
$action == 'invite',
'nav_invite');
}
Event::handle('EndSubGroupNav', array($this));
} }
$this->out->elementEnd('ul'); $this->out->elementEnd('ul');
} }
} }

View File

@ -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');
}
}

View File

@ -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';
}
}

View File

@ -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';
}
}

View File

@ -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';
}
}

View File

@ -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';
}
}

View File

@ -114,7 +114,11 @@ class TagCloudSection extends Section
function tagUrl($tag) 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() function divId()

View File

@ -51,6 +51,26 @@ class TwitterapiAction extends Action
parent::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) function twitter_user_array($profile, $get_notice=false)
{ {
@ -66,7 +86,7 @@ class TwitterapiAction extends Action
$avatar = $profile->getAvatar(AVATAR_STREAM_SIZE); $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
$twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() : Avatar::defaultImage(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; $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
if ($get_notice) { if ($get_notice) {
@ -86,7 +106,7 @@ class TwitterapiAction extends Action
$twitter_status = array(); $twitter_status = array();
$twitter_status['text'] = $notice->content; $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['created_at'] = $this->date_twitter($notice->created);
$twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ? $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
intval($notice->reply_to) : null; intval($notice->reply_to) : null;
@ -108,10 +128,9 @@ class TwitterapiAction extends Action
($replier_profile) ? $replier_profile->nickname : null; ($replier_profile) ? $replier_profile->nickname : null;
if (isset($this->auth_user)) { if (isset($this->auth_user)) {
$twitter_status['favorited'] = $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
($this->auth_user->hasFave($notice)) ? 'true' : 'false';
} else { } else {
$twitter_status['favorited'] = 'false'; $twitter_status['favorited'] = false;
} }
if ($include_user) { if ($include_user) {
@ -418,7 +437,7 @@ class TwitterapiAction extends Action
function date_twitter($dt) function date_twitter($dt)
{ {
$t = strtotime($dt); $t = strtotime($dt);
return date("D M d G:i:s O Y", $t); return date("D M d H:i:s O Y", $t);
} }
// XXX: Candidate for a general utility method somewhere? // XXX: Candidate for a general utility method somewhere?

View File

@ -395,7 +395,7 @@ function common_render_text($text)
return $r; return $r;
} }
function common_replace_urls_callback($text, $callback) { function common_replace_urls_callback($text, $callback, $notice_id = null) {
// Start off with a regex // Start off with a regex
$regex = '#'. $regex = '#'.
'(?:'. '(?:'.
@ -466,7 +466,11 @@ function common_replace_urls_callback($text, $callback) {
$url = (mb_strpos($orig_url, htmlspecialchars($url)) === FALSE) ? $url:htmlspecialchars($url); $url = (mb_strpos($orig_url, htmlspecialchars($url)) === FALSE) ? $url:htmlspecialchars($url);
// Call user specified func // Call user specified func
$modified_url = call_user_func($callback, $url); if (empty($notice_id)) {
$modified_url = call_user_func($callback, $url);
} else {
$modified_url = call_user_func($callback, array($url, $notice_id));
}
// Replace it! // Replace it!
$start = mb_strpos($text, $url, $offset); $start = mb_strpos($text, $url, $offset);
@ -481,102 +485,24 @@ function common_linkify($url) {
// It comes in special'd, so we unspecial it before passing to the stringifying // It comes in special'd, so we unspecial it before passing to the stringifying
// functions // functions
$url = htmlspecialchars_decode($url); $url = htmlspecialchars_decode($url);
$display = $url; $display = File_redirection::_canonUrl($url);
$url = (!preg_match('#^([a-z]+://|(mailto|aim|tel):)#i', $url)) ? 'http://'.$url : $url; $longurl_data = File_redirection::where($url);
if (is_array($longurl_data)) {
$attrs = array('href' => $url, 'rel' => 'external'); $longurl = $longurl_data['url'];
} elseif (is_string($longurl_data)) {
if ($longurl = common_longurl($url)) { $longurl = $longurl_data;
$attrs['title'] = $longurl; } else {
die('impossible to linkify');
} }
$attrs = array('href' => $longurl, 'rel' => 'external');
return XMLStringer::estring('a', $attrs, $display); 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) function common_shorten_links($text)
{ {
if (mb_strlen($text) <= 140) return $text; if (mb_strlen($text) <= 140) return $text;
static $cache = array(); return common_replace_urls_callback($text, array('File_redirection', 'makeShort'));
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;
} }
function common_xml_safe_str($str) function common_xml_safe_str($str)
@ -1019,7 +945,7 @@ function common_root_url($ssl=false)
function common_good_rand($bytes) function common_good_rand($bytes)
{ {
// XXX: use random.org...? // XXX: use random.org...?
if (file_exists('/dev/urandom')) { if (@file_exists('/dev/urandom')) {
return common_urandom($bytes); return common_urandom($bytes);
} else { // FIXME: this is probably not good enough } else { // FIXME: this is probably not good enough
return common_mtrand($bytes); return common_mtrand($bytes);
@ -1385,7 +1311,7 @@ function common_compatible_license($from, $to)
*/ */
function common_database_tablename($tablename) function common_database_tablename($tablename)
{ {
if(common_config('db','quote_identifiers')) { if(common_config('db','quote_identifiers')) {
$tablename = '"'. $tablename .'"'; $tablename = '"'. $tablename .'"';
} }

View File

@ -0,0 +1,205 @@
<?php
/**
* Laconica, the distributed open-source microblogging tool
*
* Plugin to do "real time" updates using Comet/Bayeux
*
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Plugin
* @package Laconica
* @author Evan Prodromou <evan@controlyourself.ca>
* @copyright 2009 Control Yourself, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
if (!defined('LACONICA')) {
exit(1);
}
/**
* Plugin to do realtime updates using Comet
*
* @category Plugin
* @package Laconica
* @author Evan Prodromou <evan@controlyourself.ca>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
class CometPlugin extends Plugin
{
var $server = null;
function __construct($server=null, $username=null, $password=null)
{
$this->server = $server;
$this->username = $username;
$this->password = $password;
parent::__construct();
}
function onEndShowScripts($action)
{
$timeline = null;
$this->log(LOG_DEBUG, 'got action ' . $action->trimmed('action'));
switch ($action->trimmed('action')) {
case 'public':
$timeline = '/timelines/public';
break;
case 'tag':
$tag = $action->trimmed('tag');
if (!empty($tag)) {
$timeline = '/timelines/tag/'.$tag;
} else {
return true;
}
break;
default:
return true;
}
$scripts = array('jquery.comet.js', 'json2.js', 'updatetimeline.js');
foreach ($scripts as $script) {
$action->element('script', array('type' => 'text/javascript',
'src' => common_path('plugins/Comet/'.$script)),
' ');
}
$user = common_current_user();
if (!empty($user->id)) {
$user_id = $user->id;
} else {
$user_id = 0;
}
$replyurl = common_local_url('newnotice');
$favorurl = common_local_url('favor');
// FIXME: need to find a better way to pass this pattern in
$deleteurl = common_local_url('deletenotice',
array('notice' => '0000000000'));
$action->elementStart('script', array('type' => 'text/javascript'));
$action->raw("$(document).ready(function() { updater.init(\"$this->server\", \"$timeline\", $user_id, \"$replyurl\", \"$favorurl\", \"$deleteurl\"); });");
$action->elementEnd('script');
return true;
}
function onEndNoticeSave($notice)
{
$this->log(LOG_INFO, "Called for save notice.");
$timelines = array();
// XXX: Add other timelines; this is just for the public one
if ($notice->is_local ||
($notice->is_local == 0 && !common_config('public', 'localonly'))) {
$timelines[] = '/timelines/public';
}
$tags = $this->getNoticeTags($notice);
if (!empty($tags)) {
foreach ($tags as $tag) {
$timelines[] = '/timelines/tag/' . $tag;
}
}
if (count($timelines) > 0) {
// Require this, since we need it
require_once(INSTALLDIR.'/plugins/Comet/bayeux.class.inc.php');
$json = $this->noticeAsJson($notice);
// Bayeux? Comet? Huh? These terms confuse me
$bay = new Bayeux($this->server, $this->user, $this->password);
foreach ($timelines as $timeline) {
$this->log(LOG_INFO, "Posting notice $notice->id to '$timeline'.");
$bay->publish($timeline, $json);
}
$bay = NULL;
}
return true;
}
function noticeAsJson($notice)
{
// FIXME: this code should be abstracted to a neutral third
// party, like Notice::asJson(). I'm not sure of the ethics
// of refactoring from within a plugin, so I'm just abusing
// the TwitterApiAction method. Don't do this unless you're me!
require_once(INSTALLDIR.'/lib/twitterapi.php');
$act = new TwitterApiAction('/dev/null');
$arr = $act->twitter_status_array($notice, true);
$arr['url'] = $notice->bestUrl();
$arr['html'] = htmlspecialchars($notice->rendered);
$arr['source'] = htmlspecialchars($arr['source']);
if (!empty($notice->reply_to)) {
$reply_to = Notice::staticGet('id', $notice->reply_to);
if (!empty($reply_to)) {
$arr['in_reply_to_status_url'] = $reply_to->bestUrl();
}
$reply_to = null;
}
$profile = $notice->getProfile();
$arr['user']['profile_url'] = $profile->profileurl;
return $arr;
}
function getNoticeTags($notice)
{
$tags = null;
$nt = new Notice_tag();
$nt->notice_id = $notice->id;
if ($nt->find()) {
$tags = array();
while ($nt->fetch()) {
$tags[] = $nt->tag;
}
}
$nt->free();
$nt = null;
return $tags;
}
// Push this up to Plugin
function log($level, $msg)
{
common_log($level, get_class($this) . ': '.$msg);
}
}

26
plugins/Comet/README Normal file
View File

@ -0,0 +1,26 @@
This is a plugin to automatically load notices in the browser no
matter who creates them -- the kind of thing we see with
search.twitter.com, rejaw.com, or FriendFeed's "real time" news.
NOTE: this is an insecure version; don't roll it out on a production
server.
It requires a cometd server. I've only had the cometd-java server work
correctly; something's wiggy with the Twisted-based server.
After you have a cometd server installed, just add this code to your
config.php:
require_once(INSTALLDIR.'/plugins/Comet/CometPlugin.php');
$cp = new CometPlugin('http://example.com:8080/cometd/');
Change 'example.com:8080' to the name and port of the server you
installed cometd on.
TODO:
* Needs to be tested with Ajax submission. Probably messes everything
up.
* Add more timelines: personal inbox and tags would be great.
* Add security. In particular, only let the PHP code publish notices
to the cometd server. Currently, it doesn't try to authenticate.

View File

@ -0,0 +1,134 @@
<?php
/*
*
* Phomet: a php comet client
*
* Copyright (C) 2008 Morgan 'ARR!' Allen <morganrallen@gmail.com> http://morglog.alleycatracing.com
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
class Bayeux
{
private $oCurl = '';
private $nNextId = 0;
private $sUser = '';
private $sPassword = '';
public $sUrl = '';
function __construct($sUrl, $sUser='', $sPassword='')
{
$this->sUrl = $sUrl;
$this->oCurl = curl_init();
$aHeaders = array();
$aHeaders[] = 'Connection: Keep-Alive';
curl_setopt($this->oCurl, CURLOPT_URL, $sUrl);
curl_setopt($this->oCurl, CURLOPT_HTTPHEADER, $aHeaders);
curl_setopt($this->oCurl, CURLOPT_HEADER, 0);
curl_setopt($this->oCurl, CURLOPT_POST, 1);
curl_setopt($this->oCurl, CURLOPT_RETURNTRANSFER,1);
if (!is_null($sUser) && mb_strlen($sUser) > 0) {
curl_setopt($this->oCurl, CURLOPT_USERPWD,"$sUser:$sPassword");
}
$this->handShake();
}
function __destruct()
{
$this->disconnect();
}
function handShake()
{
$msgHandshake = array();
$msgHandshake['channel'] = '/meta/handshake';
$msgHandshake['version'] = "1.0";
$msgHandshake['minimumVersion'] = "0.9";
$msgHandshake['supportedConnectionTypes'] = array('long-polling');
$msgHandshake['id'] = $this->nNextId++;
curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($msgHandshake)))));
$data = curl_exec($this->oCurl);
if(curl_errno($this->oCurl))
die("Error: " . curl_error($this->oCurl));
$oReturn = json_decode($data);
if (is_array($oReturn)) {
$oReturn = $oReturn[0];
}
$bSuccessful = ($oReturn->successful) ? true : false;
if($bSuccessful)
{
$this->clientId = $oReturn->clientId;
$this->connect();
}
}
public function connect()
{
$aMsg['channel'] = '/meta/connect';
$aMsg['id'] = $this->nNextId++;
$aMsg['clientId'] = $this->clientId;
$aMsg['connectionType'] = 'long-polling';
curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($aMsg)))));
$data = curl_exec($this->oCurl);
}
function disconnect()
{
$msgHandshake = array();
$msgHandshake['channel'] = '/meta/disconnect';
$msgHandshake['id'] = $this->nNextId++;
$msgHandshake['clientId'] = $this->clientId;
curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($msgHandshake)))));
curl_exec($this->oCurl);
}
public function publish($sChannel, $oData)
{
if(!$sChannel || !$oData)
return;
$aMsg = array();
$aMsg['channel'] = $sChannel;
$aMsg['id'] = $this->nNextId++;
$aMsg['data'] = $oData;
$aMsg['clientId'] = $this->clientId;
curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($aMsg)))));
$data = curl_exec($this->oCurl);
// var_dump($data);
}
}

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More