Merge branch '0.8.x' into 0.9.x

Conflicts:
	actions/updateprofile.php
	actions/userauthorization.php
	classes/User_group.php
	index.php
	install.php
	lib/accountsettingsaction.php
	lib/logingroupnav.php
This commit is contained in:
Evan Prodromou 2009-08-21 15:42:11 -04:00
commit b2664e1ae2
134 changed files with 8191 additions and 1632 deletions

92
README
View File

@ -553,25 +553,53 @@ our kind of hacky home-grown DB-based queue solution. See the "queues"
config section below for how to configure to use STOMP. As of this
writing, the software has been tested with ActiveMQ (
Twitter Friends Syncing
-----------------------
Twitter Bridge
--------------
As of Laconica 0.6.3, users may set a flag in their settings ("Subscribe
to my Twitter friends here" under the Twitter tab) to have Laconica
attempt to locate and subscribe to "friends" (people they "follow") on
Twitter who also have accounts on your Laconica system, and who have
previously set up a link for automatically posting notices to Twitter.
* OAuth
Optionally, there is a script (./scripts/synctwitterfriends.php), meant
to be run periodically from a job scheduler (e.g.: cron under Unix), to
look for new additions to users' friends lists. Note that the friends
syncing only subscribes users to each other, it does not unsubscribe
users when they stop following each other on Twitter.
As of 0.8.1, OAuth is used to to access protected resources on Twitter
instead of HTTP Basic Auth. To use Twitter bridging you will need
to register your instance of Laconica as an application on Twitter
(http://twitter.com/apps), and update the following variables in your
config.php with the consumer key and secret Twitter generates for you:
Sample cron job:
$config['twitter']['consumer_key'] = 'YOURKEY';
$config['twitter']['consumer_secret'] = 'YOURSECRET';
# Update Twitter friends subscriptions every half hour
0,30 * * * * /path/to/php /path/to/laconica/scripts/synctwitterfriends.php>&/dev/null
When registering your application with Twitter set the type to "Browser"
and your Callback URL to:
http://example.org/mublog/twitter/authorization
The default access type should be, "Read & Write".
* Importing statuses from Twitter
To allow your users to import their friends' Twitter statuses, you will
need to enable the bidirectional Twitter bridge in config.php:
$config['twitterbridge']['enabled'] = true;
and run the TwitterStatusFetcher daemon (scripts/twitterstatusfetcher.php).
Additionally, you will want to set the integration source variable,
which will keep notices posted to Twitter via Laconica from looping
back. The integration source should be set to the name of your
application, exactly as you specified it on the settings page for your
Laconica application on Twitter, e.g.:
$config['integration']['source'] = 'YourApp';
* Twitter Friends Syncing
Users may set a flag in their settings ("Subscribe to my Twitter friends
here" under the Twitter tab) to have Laconica attempt to locate and
subscribe to "friends" (people they "follow") on Twitter who also have
accounts on your Laconica system, and who have previously set up a link
for automatically posting notices to Twitter.
As of 0.8.0, this is no longer accomplished via a cron job. Instead you
must run the SyncTwitterFriends daemon (scripts/synctwitterfreinds.php).
Built-in Facebook Application
-----------------------------
@ -940,6 +968,8 @@ closed: If set to 'true', will disallow registration on your site.
the service, *then* set this variable to 'true'.
inviteonly: If set to 'true', will only allow registration if the user
was invited by an existing user.
openidonly: If set to 'true', will only allow registrations and logins
through OpenID.
private: If set to 'true', anonymous users will be redirected to the
'login' page. Also, API methods that normally require no
authentication will require it. Note that this does not turn
@ -1170,6 +1200,14 @@ For configuring invites.
enabled: Whether to allow users to send invites. Default true.
openid
------
For configuring OpenID.
enabled: Whether to allow users to register and login using OpenID. Default
true.
tag
---
@ -1231,6 +1269,30 @@ enabled: Set to true to enable. Default false.
server: a string with the hostname of the sphinx server.
port: an integer with the port number of the sphinx server.
emailpost
---------
For post-by-email.
enabled: Whether to enable post-by-email. Defaults to true. You will
also need to set up maildaemon.php.
sms
---
For SMS integration.
enabled: Whether to enable SMS integration. Defaults to true. Queues
should also be enabled.
twitter
-------
For Twitter integration
enabled: Whether to enable Twitter integration. Defaults to true.
Queues should also be enabled.
integration
-----------

View File

@ -25,11 +25,31 @@ require_once INSTALLDIR.'/lib/feedlist.php';
class AllAction extends ProfileAction
{
var $notice;
function isReadOnly($args)
{
return true;
}
function prepare($args)
{
parent::prepare($args);
$cur = common_current_user();
if (!empty($cur) && $cur->id == $this->user->id) {
$this->notice = $this->user->noticeInbox(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1);
} else {
$this->notice = $this->user->noticesWithFriends(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1);
}
if($this->page > 1 && $this->notice->N == 0){
$this->serverError(_('No such page'),$code=404);
}
return true;
}
function handle($args)
{
parent::handle($args);
@ -88,7 +108,9 @@ class AllAction extends ProfileAction
}
}
else {
$message .= sprintf(_('Why not [register an account](%%%%action.register%%%%) and then nudge %s or post a notice to his or her attention.'), $this->user->nickname);
$message .= sprintf(_('Why not [register an account](%%%%action.%s%%%%) and then nudge %s or post a notice to his or her attention.'),
(!common_config('site','openidonly')) ? 'register' : 'openidlogin',
$this->user->nickname);
}
$this->elementStart('div', 'guide');
@ -98,15 +120,7 @@ class AllAction extends ProfileAction
function showContent()
{
$cur = common_current_user();
if (!empty($cur) && $cur->id == $this->user->id) {
$notice = $this->user->noticeInbox(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1);
} else {
$notice = $this->user->noticesWithFriends(($this->page-1)*NOTICES_PER_PAGE, NOTICES_PER_PAGE + 1);
}
$nl = new NoticeList($notice, $this);
$nl = new NoticeList($this->notice, $this);
$cnt = $nl->show();

View File

@ -115,8 +115,8 @@ class AllrssAction extends Rss10Action
'link' => common_local_url('all',
array('nickname' =>
$user->nickname)),
'description' => sprintf(_('Feed for friends of %s'),
$user->nickname));
'description' => sprintf(_('Updates from %1$s and friends on %2$s!'),
$user->nickname, common_config('site', 'name')));
return $c;
}

View File

@ -131,6 +131,8 @@ class ApiAction extends Action
'tags/timeline',
'oembed/oembed',
'groups/show',
'groups/timeline',
'groups/list_all',
'groups/timeline');
static $bareauth = array('statuses/user_timeline',
@ -140,7 +142,8 @@ class ApiAction extends Action
'statuses/mentions',
'statuses/followers',
'favorites/favorites',
'friendships/show');
'friendships/show',
'groups/list_groups');
$fullname = "$this->api_action/$this->api_method";

View File

@ -103,18 +103,18 @@ class AttachmentAction extends Action
$this->element('link',array('rel'=>'alternate',
'type'=>'application/json+oembed',
'href'=>common_local_url(
'api',
array('apiaction'=>'oembed','method'=>'oembed.json'),
array('url'=>
'oembed',
array(),
array('format'=>'json', 'url'=>
common_local_url('attachment',
array('attachment' => $this->attachment->id)))),
'title'=>'oEmbed'),null);
$this->element('link',array('rel'=>'alternate',
'type'=>'text/xml+oembed',
'href'=>common_local_url(
'api',
array('apiaction'=>'oembed','method'=>'oembed.xml'),
array('url'=>
'oembed',
array(),
array('format'=>'xml','url'=>
common_local_url('attachment',
array('attachment' => $this->attachment->id)))),
'title'=>'oEmbed'),null);

View File

@ -382,13 +382,7 @@ class AvatarsettingsAction extends AccountSettingsAction
function showStylesheets()
{
parent::showStylesheets();
$jcropStyle =
common_path('theme/base/css/jquery.Jcrop.css?version='.LACONICA_VERSION);
$this->element('link', array('rel' => 'stylesheet',
'type' => 'text/css',
'href' => $jcropStyle,
'media' => 'screen, projection, tv'));
$this->cssLink('css/jquery.Jcrop.css','base','screen, projection, tv');
}
/**
@ -402,13 +396,8 @@ class AvatarsettingsAction extends AccountSettingsAction
parent::showScripts();
if ($this->mode == 'crop') {
$jcropPack = common_path('js/jcrop/jquery.Jcrop.pack.js');
$jcropGo = common_path('js/jcrop/jquery.Jcrop.go.js');
$this->element('script', array('type' => 'text/javascript',
'src' => $jcropPack));
$this->element('script', array('type' => 'text/javascript',
'src' => $jcropGo));
$this->script('js/jcrop/jquery.Jcrop.min.js');
$this->script('js/jcrop/jquery.Jcrop.go.js');
}
}
}

View File

@ -67,7 +67,11 @@ class ConfirmaddressAction extends Action
parent::handle($args);
if (!common_logged_in()) {
common_set_returnto($this->selfUrl());
if (!common_config('site', 'openidonly')) {
common_redirect(common_local_url('login'));
} else {
common_redirect(common_local_url('openidlogin'));
}
return;
}
$code = $this->trimmed('code');

View File

@ -122,7 +122,7 @@ class EmailsettingsAction extends AccountSettingsAction
}
$this->elementEnd('fieldset');
if ($user->email) {
if (common_config('emailpost', 'enabled') && $user->email) {
$this->elementStart('fieldset', array('id' => 'settings_email_incoming'));
$this->element('legend',_('Incoming email'));
if ($user->incomingemail) {
@ -173,11 +173,13 @@ class EmailsettingsAction extends AccountSettingsAction
_('Allow friends to nudge me and send me an email.'),
$user->emailnotifynudge);
$this->elementEnd('li');
if (common_config('emailpost', 'enabled')) {
$this->elementStart('li');
$this->checkbox('emailpost',
_('I want to post notices by email.'),
$user->emailpost);
$this->elementEnd('li');
}
$this->elementStart('li');
$this->checkbox('emailmicroid',
_('Publish a MicroID for my email address.'),

View File

@ -153,7 +153,8 @@ class FavoritedAction extends Action
$message .= _('Be the first to add a notice to your favorites by clicking the fave button next to any notice you like.');
}
else {
$message .= _('Why not [register an account](%%action.register%%) and be the first to add a notice to your favorites!');
$message .= sprintf(_('Why not [register an account](%%%%action.%s%%%%) and be the first to add a notice to your favorites!'),
(!common_config('site','openidonly')) ? 'register' : 'openidlogin');
}
$this->elementStart('div', 'guide');

View File

@ -111,8 +111,8 @@ class FavoritesrssAction extends Rss10Action
'link' => common_local_url('showfavorites',
array('nickname' =>
$user->nickname)),
'description' => sprintf(_('Feed of favorite notices of %s'),
$user->nickname));
'description' => sprintf(_('Updates favored by %1$s on %2$s!'),
$user->nickname, common_config('site', 'name')));
return $c;
}

View File

@ -30,7 +30,9 @@ class FinishopenidloginAction extends Action
function handle($args)
{
parent::handle($args);
if (common_is_real_login()) {
if (!common_config('openid', 'enabled')) {
common_redirect(common_local_url('login'));
} else if (common_is_real_login()) {
$this->clientError(_('Already logged in.'));
} else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$token = $this->trimmed('token');
@ -217,7 +219,7 @@ class FinishopenidloginAction extends Action
if (!Validate::string($nickname, array('min_length' => 1,
'max_length' => 64,
'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
'format' => NICKNAME_FMT))) {
$this->showForm(_('Nickname must have only lowercase letters and numbers and no spaces.'));
return;
}
@ -389,7 +391,7 @@ class FinishopenidloginAction extends Action
{
if (!Validate::string($str, array('min_length' => 1,
'max_length' => 64,
'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
'format' => NICKNAME_FMT))) {
return false;
}
if (!User::allowed_nickname($str)) {

View File

@ -428,13 +428,7 @@ class GrouplogoAction extends GroupDesignAction
function showStylesheets()
{
parent::showStylesheets();
$jcropStyle =
common_path('theme/base/css/jquery.Jcrop.css?version='.LACONICA_VERSION);
$this->element('link', array('rel' => 'stylesheet',
'type' => 'text/css',
'href' => $jcropStyle,
'media' => 'screen, projection, tv'));
$this->cssLink('css/jquery.Jcrop.css','base','screen, projection, tv');
}
/**
@ -448,13 +442,8 @@ class GrouplogoAction extends GroupDesignAction
parent::showScripts();
if ($this->mode == 'crop') {
$jcropPack = common_path('js/jcrop/jquery.Jcrop.pack.js');
$jcropGo = common_path('js/jcrop/jquery.Jcrop.go.js');
$this->element('script', array('type' => 'text/javascript',
'src' => $jcropPack));
$this->element('script', array('type' => 'text/javascript',
'src' => $jcropGo));
$this->script('js/jcrop/jquery.Jcrop.min.js');
$this->script('js/jcrop/jquery.Jcrop.go.js');
}
}

View File

@ -132,9 +132,10 @@ class groupRssAction extends Rss10Action
$c = array('url' => common_local_url('grouprss',
array('nickname' =>
$group->nickname)),
'title' => $group->nickname,
'title' => sprintf(_('%s timeline'), $group->nickname),
'link' => common_local_url('showgroup', array('nickname' => $group->nickname)),
'description' => sprintf(_('Microblog by %s group'), $group->nickname));
'description' => sprintf(_('Updates from members of %1$s on %2$s!'),
$group->nickname, common_config('site', 'name')));
return $c;
}

View File

@ -82,7 +82,8 @@ class GroupsearchAction extends SearchAction
$message = _('If you can\'t find the group you\'re looking for, you can [create it](%%action.newgroup%%) yourself.');
}
else {
$message = _('Why not [register an account](%%action.register%%) and [create the group](%%action.newgroup%%) yourself!');
$message = sprintf(_('Why not [register an account](%%%%action.%s%%%%) and [create the group](%%%%action.newgroup%%%%) yourself!'),
(!common_config('site','openidonly')) ? 'register' : 'openidlogin');
}
$this->elementStart('div', 'guide');
$this->raw(common_markup_to_html($message));

View File

@ -84,6 +84,12 @@ class ImsettingsAction extends ConnectSettingsAction
function showContent()
{
if (!common_config('xmpp', 'enabled')) {
$this->element('div', array('class' => 'error'),
_('IM is not available.'));
return;
}
$user = common_current_user();
$this->elementStart('form', array('method' => 'post',
'id' => 'form_settings_im',

View File

@ -235,7 +235,7 @@ class InviteAction extends CurrentUserDesignAction
common_root_url(),
$personal,
common_local_url('showstream', array('nickname' => $user->nickname)),
common_local_url('register', array('code' => $invite->code)));
common_local_url((!common_config('site', 'openidonly')) ? 'register' : 'openidlogin', array('code' => $invite->code)));
mail_send($recipients, $headers, $body);
}

View File

@ -65,6 +65,8 @@ class LoginAction extends Action
*
* Switches on request method; either shows the form or handles its input.
*
* Checks if only OpenID is allowed and redirects to openidlogin if so.
*
* @param array $args $_REQUEST data
*
* @return void
@ -73,7 +75,9 @@ class LoginAction extends Action
function handle($args)
{
parent::handle($args);
if (common_is_real_login()) {
if (common_config('site', 'openidonly')) {
common_redirect(common_local_url('openidlogin'));
} else if (common_is_real_login()) {
$this->clientError(_('Already logged in.'));
} else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$this->checkLogin();
@ -247,11 +251,15 @@ class LoginAction extends Action
return _('For security reasons, please re-enter your ' .
'user name and password ' .
'before changing your settings.');
} else {
} else if (common_config('openid', 'enabled')) {
return _('Login with your username and password. ' .
'Don\'t have a username yet? ' .
'[Register](%%action.register%%) a new account, or ' .
'try [OpenID](%%action.openidlogin%%). ');
} else {
return _('Login with your username and password. ' .
'Don\'t have a username yet? ' .
'[Register](%%action.register%%) a new account.');
}
}

View File

@ -121,7 +121,9 @@ class NoticesearchAction extends SearchAction
$message = sprintf(_('Be the first to [post on this topic](%%%%action.newnotice%%%%?status_textarea=%s)!'), urlencode($q));
}
else {
$message = sprintf(_('Why not [register an account](%%%%action.register%%%%) and be the first to [post on this topic](%%%%action.newnotice%%%%?status_textarea=%s)!'), urlencode($q));
$message = sprintf(_('Why not [register an account](%%%%action.%s%%%%) and be the first to [post on this topic](%%%%action.newnotice%%%%?status_textarea=%s)!'),
(!common_config('site','openidonly')) ? 'register' : 'openidlogin',
urlencode($q));
}
$this->elementStart('div', 'guide');

View File

@ -86,9 +86,10 @@ class NoticesearchrssAction extends Rss10Action
{
$q = $this->trimmed('q');
$c = array('url' => common_local_url('noticesearchrss', array('q' => $q)),
'title' => common_config('site', 'name') . sprintf(_(' Search Stream for "%s"'), $q),
'title' => sprintf(_('Updates with "%s"'), $q),
'link' => common_local_url('noticesearch', array('q' => $q)),
'description' => sprintf(_('All updates matching search term "%s"'), $q));
'description' => sprintf(_('Updates matching search term "%1$s" on %2$s!'),
$q, common_config('site', 'name')));
return $c;
}

View File

@ -31,8 +31,6 @@ if (!defined('LACONICA')) {
exit(1);
}
require_once INSTALLDIR.'/lib/twitterapi.php';
/**
* Oembed provider implementation
*
@ -46,17 +44,13 @@ require_once INSTALLDIR.'/lib/twitterapi.php';
* @link http://laconi.ca/
*/
class TwitapioembedAction extends TwitterapiAction
class OembedAction extends Action
{
function oembed($args, $apidata)
function handle($args)
{
parent::handle($args);
common_debug("in oembed api action");
$this->auth_user = $apidata['user'];
$url = $args['url'];
if( substr(strtolower($url),0,strlen(common_root_url())) == strtolower(common_root_url()) ){
$path = substr($url,strlen(common_root_url()));
@ -131,8 +125,7 @@ class TwitapioembedAction extends TwitterapiAction
default:
$this->serverError(_("$path not supported for oembed requests"), 501);
}
switch($apidata['content-type']){
switch($args['format']){
case 'xml':
$this->init_document('xml');
$this->elementStart('oembed');
@ -152,11 +145,10 @@ class TwitapioembedAction extends TwitterapiAction
if($oembed['thumbnail_width']) $this->element('thumbnail_width',null,$oembed['thumbnail_width']);
if($oembed['thumbnail_height']) $this->element('thumbnail_height',null,$oembed['thumbnail_height']);
$this->elementEnd('oembed');
$this->end_document('xml');
break;
case 'json':
case 'json': case '':
$this->init_document('json');
print(json_encode($oembed));
$this->end_document('json');
@ -164,10 +156,51 @@ class TwitapioembedAction extends TwitterapiAction
default:
$this->serverError(_('content type ' . $apidata['content-type'] . ' not supported'), 501);
}
}else{
$this->serverError(_('Only ' . common_root_url() . ' urls over plain http please'), 404);
}
}
function init_document($type)
{
switch ($type) {
case 'xml':
header('Content-Type: application/xml; charset=utf-8');
$this->startXML();
break;
case 'json':
header('Content-Type: application/json; charset=utf-8');
// Check for JSONP callback
$callback = $this->arg('callback');
if ($callback) {
print $callback . '(';
}
break;
default:
$this->serverError(_('Not a supported data format.'), 501);
break;
}
}
function end_document($type='xml')
{
switch ($type) {
case 'xml':
$this->endXML();
break;
case 'json':
// Check for JSONP callback
$callback = $this->arg('callback');
if ($callback) {
print ')';
}
break;
default:
$this->serverError(_('Not a supported data format.'), 501);
break;
}
return;
}
}

View File

@ -26,7 +26,9 @@ class OpenidloginAction extends Action
function handle($args)
{
parent::handle($args);
if (common_is_real_login()) {
if (!common_config('openid', 'enabled')) {
common_redirect(common_local_url('login'));
} else if (common_is_real_login()) {
$this->clientError(_('Already logged in.'));
} else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$openid_url = $this->trimmed('openid_url');

View File

@ -82,6 +82,12 @@ class OpenidsettingsAction extends AccountSettingsAction
function showContent()
{
if (!common_config('openid', 'enabled')) {
$this->element('div', array('class' => 'error'),
_('OpenID is not available.'));
return;
}
$user = common_current_user();
$this->elementStart('form', array('method' => 'post',

View File

@ -66,7 +66,7 @@ class OpensearchAction extends Action
$type = 'noticesearch';
$short_name = _('Notice Search');
}
header('Content-Type: text/html');
header('Content-Type: application/opensearchdescription+xml');
$this->startXML();
$this->elementStart('OpenSearchDescription', array('xmlns' => 'http://a9.com/-/spec/opensearch/1.1/'));
$short_name = common_config('site', 'name').' '.$short_name;

View File

@ -196,7 +196,7 @@ class ProfilesettingsAction extends AccountSettingsAction
// Some validation
if (!Validate::string($nickname, array('min_length' => 1,
'max_length' => 64,
'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
'format' => NICKNAME_FMT))) {
$this->showForm(_('Nickname must have only lowercase letters and numbers and no spaces.'));
return;
} else if (!User::allowed_nickname($nickname)) {

View File

@ -59,6 +59,7 @@ class PublicAction extends Action
*/
var $page = null;
var $notice;
function isReadOnly($args)
{
@ -84,6 +85,18 @@ class PublicAction extends Action
common_set_returnto($this->selfUrl());
$this->notice = Notice::publicStream(($this->page-1)*NOTICES_PER_PAGE,
NOTICES_PER_PAGE + 1);
if (!$this->notice) {
$this->serverError(_('Could not retrieve public stream.'));
return;
}
if($this->page > 1 && $this->notice->N == 0){
$this->serverError(_('No such page'),$code=404);
}
return true;
}
@ -183,7 +196,8 @@ class PublicAction extends Action
}
else {
if (! (common_config('site','closed') || common_config('site','inviteonly'))) {
$message .= _('Why not [register an account](%%action.register%%) and be the first to post!');
$message .= sprintf(_('Why not [register an account](%%%%action.%s%%%%) and be the first to post!'),
(!common_config('site','openidonly')) ? 'register' : 'openidlogin');
}
}
@ -203,15 +217,7 @@ class PublicAction extends Action
function showContent()
{
$notice = Notice::publicStream(($this->page-1)*NOTICES_PER_PAGE,
NOTICES_PER_PAGE + 1);
if (!$notice) {
$this->serverError(_('Could not retrieve public stream.'));
return;
}
$nl = new NoticeList($notice, $this);
$nl = new NoticeList($this->notice, $this);
$cnt = $nl->show();
@ -238,9 +244,11 @@ class PublicAction extends Action
function showAnonymousMessage()
{
if (! (common_config('site','closed') || common_config('site','inviteonly'))) {
$m = _('This is %%site.name%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' .
$m = sprintf(_('This is %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' .
'based on the Free Software [Laconica](http://laconi.ca/) tool. ' .
'[Join now](%%action.register%%) to share notices about yourself with friends, family, and colleagues! ([Read more](%%doc.help%%))');
'[Join now](%%%%action.%s%%%%) to share notices about yourself with friends, family, and colleagues! ' .
'([Read more](%%%%doc.help%%%%))'),
(!common_config('site','openidonly')) ? 'register' : 'openidlogin');
} else {
$m = _('This is %%site.name%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' .
'based on the Free Software [Laconica](http://laconi.ca/) tool.');

View File

@ -86,9 +86,9 @@ class PublicrssAction extends Rss10Action
{
$c = array(
'url' => common_local_url('publicrss')
, 'title' => sprintf(_('%s Public Stream'), common_config('site', 'name'))
, 'title' => sprintf(_('%s public timeline'), common_config('site', 'name'))
, 'link' => common_local_url('public')
, 'description' => sprintf(_('All updates for %s'), common_config('site', 'name')));
, 'description' => sprintf(_('%s updates from everyone!'), common_config('site', 'name')));
return $c;
}

View File

@ -72,7 +72,8 @@ class PublictagcloudAction extends Action
$message .= _('Be the first to post one!');
}
else {
$message .= _('Why not [register an account](%%action.register%%) and be the first to post one!');
$message .= sprintf(_('Why not [register an account](%%%%action.%s%%%%) and be the first to post one!'),
(!common_config('site','openidonly')) ? 'register' : 'openidlogin');
}
$this->elementStart('div', 'guide');

View File

@ -116,6 +116,8 @@ class RegisterAction extends Action
*
* Checks if registration is closed and shows an error if so.
*
* Checks if only OpenID is allowed and redirects to openidlogin if so.
*
* @param array $args $_REQUEST data
*
* @return void
@ -127,6 +129,8 @@ class RegisterAction extends Action
if (common_config('site', 'closed')) {
$this->clientError(_('Registration not allowed.'));
} else if (common_config('site', 'openidonly')) {
common_redirect(common_local_url('openidlogin'));
} else if (common_logged_in()) {
$this->clientError(_('Already logged in.'));
} else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
@ -326,6 +330,7 @@ class RegisterAction extends Action
} else if ($this->error) {
$this->element('p', 'error', $this->error);
} else {
if (common_config('openid', 'enabled')) {
$instr =
common_markup_to_html(_('With this form you can create '.
' a new account. ' .
@ -334,6 +339,13 @@ class RegisterAction extends Action
'(Have an [OpenID](http://openid.net/)? ' .
'Try our [OpenID registration]'.
'(%%action.openidlogin%%)!)'));
} else {
$instr =
common_markup_to_html(_('With this form you can create '.
' a new account. ' .
'You can then post notices and '.
'link up to friends and colleagues.'));
}
$this->elementStart('div', 'instructions');
$this->raw($instr);

View File

@ -97,11 +97,13 @@ class RemotesubscribeAction extends Action
if ($this->err) {
$this->element('div', 'error', $this->err);
} else {
$inst = _('To subscribe, you can [login](%%action.login%%),' .
' or [register](%%action.register%%) a new ' .
$inst = sprintf(_('To subscribe, you can [login](%%%%action.%s%%%%),' .
' or [register](%%%%action.%s%%%%) a new ' .
' account. If you already have an account ' .
' on a [compatible microblogging site](%%doc.openmublog%%), ' .
' enter your profile URL below.');
' enter your profile URL below.'),
(!common_config('site','openidonly')) ? 'login' : 'openidlogin',
(!common_config('site','openidonly')) ? 'register' : 'openidlogin');
$output = common_markup_to_html($inst);
$this->elementStart('div', 'instructions');
$this->raw($output);

View File

@ -48,6 +48,7 @@ require_once INSTALLDIR.'/lib/feedlist.php';
class RepliesAction extends OwnerDesignAction
{
var $page = null;
var $notice;
/**
* Prepare the object
@ -84,6 +85,13 @@ class RepliesAction extends OwnerDesignAction
common_set_returnto($this->selfUrl());
$this->notice = $this->user->getReplies(($this->page-1) * NOTICES_PER_PAGE,
NOTICES_PER_PAGE + 1);
if($this->page > 1 && $this->notice->N == 0){
$this->serverError(_('No such page'),$code=404);
}
return true;
}
@ -159,10 +167,7 @@ class RepliesAction extends OwnerDesignAction
function showContent()
{
$notice = $this->user->getReplies(($this->page-1) * NOTICES_PER_PAGE,
NOTICES_PER_PAGE + 1);
$nl = new NoticeList($notice, $this);
$nl = new NoticeList($this->notice, $this);
$cnt = $nl->show();
if (0 === $cnt) {
@ -187,7 +192,9 @@ class RepliesAction extends OwnerDesignAction
}
}
else {
$message .= sprintf(_('Why not [register an account](%%%%action.register%%%%) and then nudge %s or post a notice to his or her attention.'), $this->user->nickname);
$message .= sprintf(_('Why not [register an account](%%%%action.%s%%%%) and then nudge %s or post a notice to his or her attention.'),
(!common_config('site','openidonly')) ? 'register' : 'openidlogin',
$this->user->nickname);
}
$this->elementStart('div', 'guide');

View File

@ -68,7 +68,8 @@ class RepliesrssAction extends Rss10Action
'link' => common_local_url('replies',
array('nickname' =>
$user->nickname)),
'description' => sprintf(_('Feed for replies to %s'), $user->nickname));
'description' => sprintf(_('Replies to %1$s on %2$s!'),
$user->nickname, common_config('site', 'name')));
return $c;
}

View File

@ -114,6 +114,29 @@ class ShowfavoritesAction extends OwnerDesignAction
common_set_returnto($this->selfUrl());
$cur = common_current_user();
if (!empty($cur) && $cur->id == $this->user->id) {
// Show imported/gateway notices as well as local if
// the user is looking at his own favorites
$this->notice = $this->user->favoriteNotices(($this->page-1)*NOTICES_PER_PAGE,
NOTICES_PER_PAGE + 1, true);
} else {
$this->notice = $this->user->favoriteNotices(($this->page-1)*NOTICES_PER_PAGE,
NOTICES_PER_PAGE + 1, false);
}
if (empty($this->notice)) {
$this->serverError(_('Could not retrieve favorite notices.'));
return;
}
if($this->page > 1 && $this->notice->N == 0){
$this->serverError(_('No such page'),$code=404);
}
return true;
}
@ -173,7 +196,9 @@ class ShowfavoritesAction extends OwnerDesignAction
}
}
else {
$message = sprintf(_('%s hasn\'t added any notices to his favorites yet. Why not [register an account](%%%%action.register%%%%) and then post something interesting they would add to thier favorites :)'), $this->user->nickname);
$message = sprintf(_('%s hasn\'t added any notices to his favorites yet. Why not [register an account](%%%%action.%s%%%%) and then post something interesting they would add to their favorites :)'),
$this->user->nickname,
(!common_config('site','openidonly')) ? 'register' : 'openidlogin');
}
$this->elementStart('div', 'guide');
@ -191,26 +216,7 @@ class ShowfavoritesAction extends OwnerDesignAction
function showContent()
{
$cur = common_current_user();
if (!empty($cur) && $cur->id == $this->user->id) {
// Show imported/gateway notices as well as local if
// the user is looking at his own favorites
$notice = $this->user->favoriteNotices(($this->page-1)*NOTICES_PER_PAGE,
NOTICES_PER_PAGE + 1, true);
} else {
$notice = $this->user->favoriteNotices(($this->page-1)*NOTICES_PER_PAGE,
NOTICES_PER_PAGE + 1, false);
}
if (empty($notice)) {
$this->serverError(_('Could not retrieve favorite notices.'));
return;
}
$nl = new NoticeList($notice, $this);
$nl = new NoticeList($this->notice, $this);
$cnt = $nl->show();
if (0 == $cnt) {

View File

@ -130,9 +130,19 @@ class ShowgroupAction extends GroupDesignAction
$this->group = User_group::staticGet('nickname', $nickname);
if (!$this->group) {
$alias = Group_alias::staticGet('alias', $nickname);
if ($alias) {
$args = array('id' => $alias->group_id);
if ($this->page != 1) {
$args['page'] = $this->page;
}
common_redirect(common_local_url('groupbyid', $args), 301);
return false;
} else {
$this->clientError(_('No such group'), 404);
return false;
}
}
common_set_returnto($this->selfUrl());
@ -440,8 +450,9 @@ class ShowgroupAction extends GroupDesignAction
$m = sprintf(_('**%s** is a user group on %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' .
'based on the Free Software [Laconica](http://laconi.ca/) tool. Its members share ' .
'short messages about their life and interests. '.
'[Join now](%%%%action.register%%%%) to become part of this group and many more! ([Read more](%%%%doc.help%%%%))'),
$this->group->nickname);
'[Join now](%%%%action.%s%%%%) to become part of this group and many more! ([Read more](%%%%doc.help%%%%))'),
$this->group->nickname,
(!common_config('site','openidonly')) ? 'register' : 'openidlogin');
} else {
$m = sprintf(_('**%s** is a user group on %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' .
'based on the Free Software [Laconica](http://laconi.ca/) tool. Its members share ' .

View File

@ -103,8 +103,8 @@ class ShownoticeAction extends OwnerDesignAction
$this->user = User::staticGet('id', $this->profile->id);
if (empty($this->user)) {
$this->serverError(_('Not a local notice'), 500);
if (! $this->notice->is_local) {
common_redirect($this->notice->uri);
return false;
}
@ -196,7 +196,7 @@ class ShownoticeAction extends OwnerDesignAction
{
parent::handle($args);
if ($this->notice->is_local == 0) {
if ($this->notice->is_local == Notice::REMOTE_OMB) {
if (!empty($this->notice->url)) {
common_redirect($this->notice->url, 301);
} else if (!empty($this->notice->uri) && preg_match('/^https?:/', $this->notice->uri)) {
@ -284,16 +284,16 @@ class ShownoticeAction extends OwnerDesignAction
$this->element('link',array('rel'=>'alternate',
'type'=>'application/json+oembed',
'href'=>common_local_url(
'api',
array('apiaction'=>'oembed','method'=>'oembed.json'),
array('url'=>$this->notice->uri)),
'oembed',
array(),
array('format'=>'json','url'=>$this->notice->uri)),
'title'=>'oEmbed'),null);
$this->element('link',array('rel'=>'alternate',
'type'=>'text/xml+oembed',
'href'=>common_local_url(
'api',
array('apiaction'=>'oembed','method'=>'oembed.xml'),
array('url'=>$this->notice->uri)),
'oembed',
array(),
array('format'=>'xml','url'=>$this->notice->uri)),
'title'=>'oEmbed'),null);
}
}

View File

@ -358,7 +358,9 @@ class ShowstreamAction extends ProfileAction
}
}
else {
$message .= sprintf(_('Why not [register an account](%%%%action.register%%%%) and then nudge %s or post a notice to his or her attention.'), $this->user->nickname);
$message .= sprintf(_('Why not [register an account](%%%%action.%s%%%%) and then nudge %s or post a notice to his or her attention.'),
(!common_config('site','openidonly')) ? 'register' : 'openidlogin',
$this->user->nickname);
}
$this->elementStart('div', 'guide');
@ -387,8 +389,10 @@ class ShowstreamAction extends ProfileAction
if (!(common_config('site','closed') || common_config('site','inviteonly'))) {
$m = sprintf(_('**%s** has an account on %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' .
'based on the Free Software [Laconica](http://laconi.ca/) tool. ' .
'[Join now](%%%%action.register%%%%) to follow **%s**\'s notices and many more! ([Read more](%%%%doc.help%%%%))'),
$this->user->nickname, $this->user->nickname);
'[Join now](%%%%action.%s%%%%) to follow **%s**\'s notices and many more! ([Read more](%%%%doc.help%%%%))'),
$this->user->nickname,
(!common_config('site','openidonly')) ? 'register' : 'openidlogin',
$this->user->nickname);
} else {
$m = sprintf(_('**%s** has an account on %%%%site.name%%%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' .
'based on the Free Software [Laconica](http://laconi.ca/) tool. '),

View File

@ -80,6 +80,12 @@ class SmssettingsAction extends ConnectSettingsAction
function showContent()
{
if (!common_config('sms', 'enabled')) {
$this->element('div', array('class' => 'error'),
_('SMS is not available.'));
return;
}
$user = common_current_user();
$this->elementStart('form', array('method' => 'post',

View File

@ -111,7 +111,9 @@ class SubscribersAction extends GalleryAction
}
}
else {
$message = sprintf(_('%s has no subscribers. Why not [register an account](%%%%action.register%%%%) and be the first?'), $this->user->nickname);
$message = sprintf(_('%s has no subscribers. Why not [register an account](%%%%action.%s%%%%) and be the first?'),
$this->user->nickname,
(!common_config('site','openidonly')) ? 'register' : 'openidlogin');
}
$this->elementStart('div', 'guide');

View File

@ -174,14 +174,26 @@ class SubscriptionsListItem extends SubscriptionListItem
return;
}
if (!common_config('xmpp', 'enabled') && !common_config('sms', 'enabled')) {
return;
}
$this->out->elementStart('form', array('id' => 'subedit-' . $this->profile->id,
'method' => 'post',
'class' => 'form_subscription_edit',
'action' => common_local_url('subedit')));
$this->out->hidden('token', common_session_token());
$this->out->hidden('profile', $this->profile->id);
if (common_config('xmpp', 'enabled')) {
$this->out->checkbox('jabber', _('Jabber'), $sub->jabber);
} else {
$this->out->hidden('jabber', $sub->jabber);
}
if (common_config('sms', 'enabled')) {
$this->out->checkbox('sms', _('SMS'), $sub->sms);
} else {
$this->out->hidden('sms', $sub->sms);
}
$this->out->submit('save', _('Save'));
$this->out->elementEnd('form');
return;

View File

@ -21,6 +21,9 @@ if (!defined('LACONICA')) { exit(1); }
class TagAction extends Action
{
var $notice;
function prepare($args)
{
parent::prepare($args);
@ -42,6 +45,12 @@ class TagAction extends Action
common_set_returnto($this->selfUrl());
$this->notice = Notice_tag::getStream($this->tag, (($this->page-1)*NOTICES_PER_PAGE), NOTICES_PER_PAGE + 1);
if($this->page > 1 && $this->notice->N == 0){
$this->serverError(_('No such page'),$code=404);
}
return true;
}
@ -94,9 +103,7 @@ class TagAction extends Action
function showContent()
{
$notice = Notice_tag::getStream($this->tag, (($this->page-1)*NOTICES_PER_PAGE), NOTICES_PER_PAGE + 1);
$nl = new NoticeList($notice, $this);
$nl = new NoticeList($this->notice, $this);
$cnt = $nl->show();

View File

@ -61,7 +61,8 @@ class TagrssAction extends Rss10Action
$c = array('url' => common_local_url('tagrss', array('tag' => $tagname)),
'title' => $tagname,
'link' => common_local_url('tagrss', array('tag' => $tagname)),
'description' => sprintf(_('Microblog tagged with %s'), $tagname));
'description' => sprintf(_('Updates tagged with %1$s on %2$s!'),
$tagname, common_config('site', 'name')));
return $c;
}

View File

@ -51,6 +51,103 @@ require_once INSTALLDIR.'/lib/twitterapi.php';
class TwitapigroupsAction extends TwitterapiAction
{
function list_groups($args, $apidata)
{
parent::handle($args);
common_debug("in groups api action");
$this->auth_user = $apidata['user'];
$user = $this->get_user($apidata['api_arg'], $apidata);
if (empty($user)) {
$this->clientError('Not Found', 404, $apidata['content-type']);
return;
}
$page = (int)$this->arg('page', 1);
$count = (int)$this->arg('count', 20);
$max_id = (int)$this->arg('max_id', 0);
$since_id = (int)$this->arg('since_id', 0);
$since = $this->arg('since');
$group = $user->getGroups(($page-1)*$count,
$count, $since_id, $max_id, $since);
$sitename = common_config('site', 'name');
$title = sprintf(_("%s's groups"), $user->nickname);
$taguribase = common_config('integration', 'taguri');
$id = "tag:$taguribase:Groups";
$link = common_root_url();
$subtitle = sprintf(_("groups %s is a member of on %s"), $user->nickname, $sitename);
switch($apidata['content-type']) {
case 'xml':
$this->show_xml_groups($group);
break;
case 'rss':
$this->show_rss_groups($group, $title, $link, $subtitle);
break;
case 'atom':
$selfuri = common_root_url() . 'api/laconica/groups/list/' . $user->id . '.atom';
$this->show_atom_groups($group, $title, $id, $link,
$subtitle, $selfuri);
break;
case 'json':
$this->show_json_groups($group);
break;
default:
$this->clientError(_('API method not found!'), $code = 404);
break;
}
}
function list_all($args, $apidata)
{
parent::handle($args);
common_debug("in groups api action");
$page = (int)$this->arg('page', 1);
$count = (int)$this->arg('count', 20);
$max_id = (int)$this->arg('max_id', 0);
$since_id = (int)$this->arg('since_id', 0);
$since = $this->arg('since');
/* TODO:
Use the $page, $count, $max_id, $since_id, and $since parameters
*/
$group = new User_group();
$group->orderBy('created DESC');
$group->find();
$sitename = common_config('site', 'name');
$title = sprintf(_("%s groups"), $sitename);
$taguribase = common_config('integration', 'taguri');
$id = "tag:$taguribase:Groups";
$link = common_root_url();
$subtitle = sprintf(_("groups on %s"), $sitename);
switch($apidata['content-type']) {
case 'xml':
$this->show_xml_groups($group);
break;
case 'rss':
$this->show_rss_groups($group, $title, $link, $subtitle);
break;
case 'atom':
$selfuri = common_root_url() . 'api/laconica/groups/list_all.atom';
$this->show_atom_groups($group, $title, $id, $link,
$subtitle, $selfuri);
break;
case 'json':
$this->show_json_groups($group);
break;
default:
$this->clientError(_('API method not found!'), $code = 404);
break;
}
}
function show($args, $apidata)
{
parent::handle($args);

View File

@ -456,7 +456,8 @@ class TwitapistatusesAction extends TwitterapiAction
function friends($args, $apidata)
{
parent::handle($args);
return $this->subscriptions($apidata, 'subscribed', 'subscriber');
$includeStatuses=! (boolean) $args['lite'];
return $this->subscriptions($apidata, 'subscribed', 'subscriber', false, $includeStatuses);
}
function friendsIDs($args, $apidata)
@ -468,7 +469,8 @@ class TwitapistatusesAction extends TwitterapiAction
function followers($args, $apidata)
{
parent::handle($args);
return $this->subscriptions($apidata, 'subscriber', 'subscribed');
$includeStatuses=! (boolean) $args['lite'];
return $this->subscriptions($apidata, 'subscriber', 'subscribed', false, $includeStatuses);
}
function followersIDs($args, $apidata)
@ -477,7 +479,7 @@ class TwitapistatusesAction extends TwitterapiAction
return $this->subscriptions($apidata, 'subscriber', 'subscribed', true);
}
function subscriptions($apidata, $other_attr, $user_attr, $onlyIDs=false)
function subscriptions($apidata, $other_attr, $user_attr, $onlyIDs=false, $includeStatuses=true)
{
$this->auth_user = $apidata['user'];
$user = $this->get_user($apidata['api_arg'], $apidata);
@ -533,26 +535,26 @@ class TwitapistatusesAction extends TwitterapiAction
if ($onlyIDs) {
$this->showIDs($others, $type);
} else {
$this->show_profiles($others, $type);
$this->show_profiles($others, $type, $includeStatuses);
}
$this->end_document($type);
}
function show_profiles($profiles, $type)
function show_profiles($profiles, $type, $includeStatuses)
{
switch ($type) {
case 'xml':
$this->elementStart('users', array('type' => 'array'));
foreach ($profiles as $profile) {
$this->show_profile($profile);
$this->show_profile($profile,$type,null,$includeStatuses);
}
$this->elementEnd('users');
break;
case 'json':
$arrays = array();
foreach ($profiles as $profile) {
$arrays[] = $this->twitter_user_array($profile, true);
$arrays[] = $this->twitter_user_array($profile, $includeStatuses);
}
print json_encode($arrays);
break;

View File

@ -0,0 +1,222 @@
<?php
/**
* Laconica, the distributed open-source microblogging tool
*
* Class for doing OAuth authentication against Twitter
*
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Twitter
* @package Laconica
* @author Zach Copely <zach@controlyourself.ca>
* @copyright 2009 Control Yourself, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
if (!defined('LACONICA')) {
exit(1);
}
/**
* Class for doing OAuth authentication against Twitter
*
* Peforms the OAuth "dance" between Laconica and Twitter -- requests a token,
* authorizes it, and exchanges it for an access token. It also creates a link
* (Foreign_link) between the Laconica user and Twitter user and stores the
* access token and secret in the link.
*
* @category Twitter
* @package Laconica
* @author Zach Copley <zach@controlyourself.ca>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*
*/
class TwitterauthorizationAction extends Action
{
/**
* Initialize class members. Looks for 'oauth_token' parameter.
*
* @param array $args misc. arguments
*
* @return boolean true
*/
function prepare($args)
{
parent::prepare($args);
$this->oauth_token = $this->arg('oauth_token');
return true;
}
/**
* Handler method
*
* @param array $args is ignored since it's now passed in in prepare()
*
* @return nothing
*/
function handle($args)
{
parent::handle($args);
if (!common_logged_in()) {
$this->clientError(_('Not logged in.'), 403);
}
$user = common_current_user();
$flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);
// If there's already a foreign link record, it means we already
// have an access token, and this is unecessary. So go back.
if (isset($flink)) {
common_redirect(common_local_url('twittersettings'));
}
// $this->oauth_token is only populated once Twitter authorizes our
// request token. If it's empty we're at the beginning of the auth
// process
if (empty($this->oauth_token)) {
$this->authorizeRequestToken();
} else {
$this->saveAccessToken();
}
}
/**
* Asks Twitter for a request token, and then redirects to Twitter
* to authorize it.
*
* @return nothing
*/
function authorizeRequestToken()
{
try {
// Get a new request token and authorize it
$client = new TwitterOAuthClient();
$req_tok =
$client->getRequestToken(TwitterOAuthClient::$requestTokenURL);
// Sock the request token away in the session temporarily
$_SESSION['twitter_request_token'] = $req_tok->key;
$_SESSION['twitter_request_token_secret'] = $req_tok->secret;
$auth_link = $client->getAuthorizeLink($req_tok);
} catch (TwitterOAuthClientException $e) {
$msg = sprintf('OAuth client cURL error - code: %1s, msg: %2s',
$e->getCode(), $e->getMessage());
$this->serverError(_('Couldn\'t link your Twitter account.'));
}
common_redirect($auth_link);
}
/**
* Called when Twitter returns an authorized request token. Exchanges
* it for an access token and stores it.
*
* @return nothing
*/
function saveAccessToken()
{
// Check to make sure Twitter returned the same request
// token we sent them
if ($_SESSION['twitter_request_token'] != $this->oauth_token) {
$this->serverError(_('Couldn\'t link your Twitter account.'));
}
try {
$client = new TwitterOAuthClient($_SESSION['twitter_request_token'],
$_SESSION['twitter_request_token_secret']);
// Exchange the request token for an access token
$atok = $client->getAccessToken(TwitterOAuthClient::$accessTokenURL);
// Test the access token and get the user's Twitter info
$client = new TwitterOAuthClient($atok->key, $atok->secret);
$twitter_user = $client->verifyCredentials();
} catch (OAuthClientException $e) {
$msg = sprintf('OAuth client cURL error - code: %1$s, msg: %2$s',
$e->getCode(), $e->getMessage());
$this->serverError(_('Couldn\'t link your Twitter account.'));
}
// Save the access token and Twitter user info
$this->saveForeignLink($atok, $twitter_user);
// Clean up the the mess we made in the session
unset($_SESSION['twitter_request_token']);
unset($_SESSION['twitter_request_token_secret']);
common_redirect(common_local_url('twittersettings'));
}
/**
* Saves a Foreign_link between Twitter user and local user,
* which includes the access token and secret.
*
* @param OAuthToken $access_token the access token to save
* @param mixed $twitter_user twitter API user object
*
* @return nothing
*/
function saveForeignLink($access_token, $twitter_user)
{
$user = common_current_user();
$flink = new Foreign_link();
$flink->user_id = $user->id;
$flink->foreign_id = $twitter_user->id;
$flink->service = TWITTER_SERVICE;
$creds = TwitterOAuthClient::packToken($access_token);
$flink->credentials = $creds;
$flink->created = common_sql_now();
// Defaults: noticesync on, everything else off
$flink->set_flags(true, false, false, false);
$flink_id = $flink->insert();
if (empty($flink_id)) {
common_log_db_error($flink, 'INSERT', __FILE__);
$this->serverError(_('Couldn\'t link your Twitter account.'));
}
save_twitter_user($twitter_user->id, $twitter_user->screen_name);
}
}

View File

@ -34,8 +34,6 @@ if (!defined('LACONICA')) {
require_once INSTALLDIR.'/lib/connectsettingsaction.php';
require_once INSTALLDIR.'/lib/twitter.php';
define('SUBSCRIPTIONS', 80);
/**
* Settings for Twitter integration
*
@ -69,9 +67,8 @@ class TwittersettingsAction extends ConnectSettingsAction
function getInstructions()
{
return _('Add your Twitter account to automatically send '.
' your notices to Twitter, ' .
'and subscribe to Twitter friends already here.');
return _('Connect your Twitter account to share your updates ' .
'with your Twitter friends and vice-versa.');
}
/**
@ -85,6 +82,12 @@ class TwittersettingsAction extends ConnectSettingsAction
function showContent()
{
if (!common_config('twitter', 'enabled')) {
$this->element('div', array('class' => 'error'),
_('Twitter is not available.'));
return;
}
$user = common_current_user();
$profile = $user->getProfile();
@ -93,7 +96,7 @@ class TwittersettingsAction extends ConnectSettingsAction
$flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);
if ($flink) {
if (!empty($flink)) {
$fuser = $flink->getForeignUser();
}
@ -102,40 +105,35 @@ class TwittersettingsAction extends ConnectSettingsAction
'class' => 'form_settings',
'action' =>
common_local_url('twittersettings')));
$this->elementStart('fieldset', array('id' => 'settings_twitter_account'));
$this->element('legend', null, _('Twitter Account'));
$this->hidden('token', common_session_token());
if ($fuser) {
$this->elementStart('fieldset', array('id' => 'settings_twitter_account'));
if (empty($fuser)) {
$this->elementStart('ul', 'form_data');
$this->elementStart('li', array('id' => 'settings_twitter_remove'));
$this->element('span', 'twitter_user', $fuser->nickname);
$this->element('a', array('href' => $fuser->uri), $fuser->uri);
$this->element('p', 'form_note',
_('Current verified Twitter account.'));
$this->hidden('flink_foreign_id', $flink->foreign_id);
$this->elementStart('li', array('id' => 'settings_twitter_login_button'));
$this->element('a', array('href' => common_local_url('twitterauthorization')),
'Connect my Twitter account');
$this->elementEnd('li');
$this->elementEnd('ul');
$this->submit('remove', _('Remove'));
$this->elementEnd('fieldset');
} else {
$this->elementStart('ul', 'form_data');
$this->elementStart('li', array('id' => 'settings_twitter_login'));
$this->input('twitter_username', _('Twitter user name'),
($this->arg('twitter_username')) ?
$this->arg('twitter_username') :
$profile->nickname,
_('No spaces, please.')); // hey, it's what Twitter says
$this->elementEnd('li');
$this->elementStart('li');
$this->password('twitter_password', _('Twitter password'));
$this->elementend('li');
$this->elementEnd('ul');
}
$this->element('legend', null, _('Twitter account'));
$this->elementStart('p', array('id' => 'form_confirmed'));
$this->element('a', array('href' => $fuser->uri), $fuser->nickname);
$this->elementEnd('p');
$this->element('p', 'form_note',
_('Connected Twitter account'));
$this->submit('remove', _('Remove'));
$this->elementEnd('fieldset');
$this->elementStart('fieldset',
array('id' => 'settings_twitter_preferences'));
$this->element('legend', null, _('Preferences'));
$this->elementStart('fieldset', array('id' => 'settings_twitter_preferences'));
$this->element('legend', null, _('Preferences'));
$this->elementStart('ul', 'form_data');
$this->elementStart('li');
$this->checkbox('noticesend',
@ -167,8 +165,9 @@ class TwittersettingsAction extends ConnectSettingsAction
($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');
}
@ -181,115 +180,13 @@ class TwittersettingsAction extends ConnectSettingsAction
} else {
$this->submit('add', _('Add'));
}
$this->elementEnd('fieldset');
$this->showTwitterSubscriptions();
$this->elementEnd('fieldset');
}
$this->elementEnd('form');
}
/**
* Gets some of the user's Twitter friends
*
* Gets the number of Twitter friends that are on this
* instance of Laconica.
*
* @return array array of User objects
*/
function subscribedTwitterUsers()
{
$current_user = common_current_user();
$qry = 'SELECT "user".* ' .
'FROM subscription ' .
'JOIN "user" ON subscription.subscribed = "user".id ' .
'JOIN foreign_link ON foreign_link.user_id = "user".id ' .
'WHERE subscriber = %d ' .
'ORDER BY "user".nickname';
$user = new User();
$user->query(sprintf($qry, $current_user->id));
$users = array();
while ($user->fetch()) {
// Don't include the user's own self-subscription
if ($user->id != $current_user->id) {
$users[] = clone($user);
}
}
return $users;
}
/**
* Show user's Twitter friends
*
* Gets the number of Twitter friends that are on this
* instance of Laconica, and shows their mini-avatars.
*
* @return void
*/
function showTwitterSubscriptions()
{
$friends = $this->subscribedTwitterUsers();
$friends_count = count($friends);
if ($friends_count > 0) {
$this->elementStart('div', array('id' => 'entity_subscriptions',
'class' => 'section'));
$this->element('h2', null, _('Twitter Friends'));
$this->elementStart('ul', 'entities users xoxo');
for ($i = 0; $i < min($friends_count, SUBSCRIPTIONS); $i++) {
$other = Profile::staticGet($friends[$i]->id);
if (!$other) {
common_log_db_error($subs, 'SELECT', __FILE__);
continue;
}
$this->elementStart('li', 'vcard');
$this->elementStart('a', array('title' => ($other->fullname) ?
$other->fullname :
$other->nickname,
'href' => $other->profileurl,
'class' => 'url'));
$avatar = $other->getAvatar(AVATAR_MINI_SIZE);
$avatar_url = ($avatar) ?
$avatar->displayUrl() :
Avatar::defaultImage(AVATAR_MINI_SIZE);
$this->element('img', array('src' => $avatar_url,
'width' => AVATAR_MINI_SIZE,
'height' => AVATAR_MINI_SIZE,
'class' => 'avatar photo',
'alt' => ($other->fullname) ?
$other->fullname :
$other->nickname));
$this->element('span', 'fn nickname', $other->nickname);
$this->elementEnd('a');
$this->elementEnd('li');
}
$this->elementEnd('ul');
$this->elementEnd('div');
}
}
/**
* Handle posts to this form
*
@ -303,7 +200,6 @@ class TwittersettingsAction extends ConnectSettingsAction
function handlePost()
{
// CSRF protection
$token = $this->trimmed('token');
if (!$token || $token != common_session_token()) {
@ -314,8 +210,6 @@ class TwittersettingsAction extends ConnectSettingsAction
if ($this->arg('save')) {
$this->savePreferences();
} else if ($this->arg('add')) {
$this->addTwitterAccount();
} else if ($this->arg('remove')) {
$this->removeTwitterAccount();
} else {
@ -323,82 +217,6 @@ class TwittersettingsAction extends ConnectSettingsAction
}
}
/**
* Associate a Twitter account with the user's account
*
* Validates post input; verifies it against Twitter; and if
* successful stores in the database.
*
* @return void
*/
function addTwitterAccount()
{
$screen_name = $this->trimmed('twitter_username');
$password = $this->trimmed('twitter_password');
$noticesend = $this->boolean('noticesend');
$noticerecv = $this->boolean('noticerecv');
$replysync = $this->boolean('replysync');
$friendsync = $this->boolean('friendsync');
if (!Validate::string($screen_name,
array('min_length' => 1,
'max_length' => 15,
'format' => VALIDATE_NUM.VALIDATE_ALPHA.'_'))) {
$this->showForm(_('Username must have only numbers, '.
'upper- and lowercase letters, '.
'and underscore (_). 15 chars max.'));
return;
}
if (!$this->verifyCredentials($screen_name, $password)) {
$this->showForm(_('Could not verify your Twitter credentials!'));
return;
}
$twit_user = twitter_user_info($screen_name, $password);
if (!$twit_user) {
$this->showForm(sprintf(_('Unable to retrieve account information '.
'For "%s" from Twitter.'),
$screen_name));
return;
}
if (!save_twitter_user($twit_user->id, $screen_name)) {
$this->showForm(_('Unable to save your Twitter settings!'));
return;
}
$user = common_current_user();
$flink = new Foreign_link();
$flink->user_id = $user->id;
$flink->foreign_id = $twit_user->id;
$flink->service = TWITTER_SERVICE;
$flink->credentials = $password;
$flink->created = common_sql_now();
$flink->set_flags($noticesend, $noticerecv, $replysync, $friendsync);
$flink_id = $flink->insert();
if (!$flink_id) {
common_log_db_error($flink, 'INSERT', __FILE__);
$this->showForm(_('Unable to save your Twitter settings!'));
return;
}
if ($friendsync) {
save_twitter_friends($user, $twit_user->id, $screen_name, $password);
$flink->last_friendsync = common_sql_now();
$flink->update();
}
$this->showForm(_('Twitter settings saved.'), true);
}
/**
* Disassociate an existing Twitter account from this account
*
@ -408,20 +226,11 @@ class TwittersettingsAction extends ConnectSettingsAction
function removeTwitterAccount()
{
$user = common_current_user();
$flink = Foreign_link::getByUserID($user->id, 1);
$flink_foreign_id = $this->arg('flink_foreign_id');
// Maybe an old tab open...?
if ($flink->foreign_id != $flink_foreign_id) {
$this->showForm(_('That is not your Twitter account.'));
return;
}
$flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);
$result = $flink->delete();
if (!$result) {
if (empty($result)) {
common_log_db_error($flink, 'DELETE', __FILE__);
$this->serverError(_('Couldn\'t remove Twitter user.'));
return;
@ -444,32 +253,16 @@ class TwittersettingsAction extends ConnectSettingsAction
$replysync = $this->boolean('replysync');
$user = common_current_user();
$flink = Foreign_link::getByUserID($user->id, TWITTER_SERVICE);
$flink = Foreign_link::getByUserID($user->id, 1);
if (!$flink) {
if (empty($flink)) {
common_log_db_error($flink, 'SELECT', __FILE__);
$this->showForm(_('Couldn\'t save Twitter preferences.'));
return;
}
$twitter_id = $flink->foreign_id;
$password = $flink->credentials;
$fuser = $flink->getForeignUser();
if (!$fuser) {
common_log_db_error($fuser, 'SELECT', __FILE__);
$this->showForm(_('Couldn\'t save Twitter preferences.'));
return;
}
$screen_name = $fuser->nickname;
$original = clone($flink);
$flink->set_flags($noticesend, $noticerecv, $replysync, $friendsync);
$result = $flink->update($original);
if ($result === false) {
@ -478,45 +271,7 @@ class TwittersettingsAction extends ConnectSettingsAction
return;
}
if ($friendsync) {
save_twitter_friends($user, $flink->foreign_id, $screen_name, $password);
}
$this->showForm(_('Twitter preferences saved.'), true);
}
/**
* Verifies a username and password against Twitter's API
*
* @param string $screen_name Twitter user name
* @param string $password Twitter password
*
* @return boolean success flag
*/
function verifyCredentials($screen_name, $password)
{
$uri = 'http://twitter.com/account/verify_credentials.json';
$data = get_twitter_data($uri, $screen_name, $password);
if (!$data) {
return false;
}
$user = json_decode($data);
if (!$user) {
return false;
}
$twitter_id = $user->id;
if ($twitter_id) {
return $twitter_id;
}
return false;
}
}

View File

@ -57,13 +57,46 @@ class UpdateprofileAction extends Action
*/
function prepare($argarray)
{
parent::prepare($argarray);
$license = $_POST['omb_listenee_license'];
$site_license = common_config('license', 'url');
if (!common_compatible_license($license, $site_license)) {
$this->clientError(sprintf(_('Listenee stream license %s is not '.
'compatible with site license %s.'),
$license, $site_license);
$version = $req->get_parameter('omb_version');
if ($version != OMB_VERSION_01) {
$this->clientError(_('Unsupported OMB version'), 400);
return false;
}
# First, check to see if listenee exists
$listenee = $req->get_parameter('omb_listenee');
$remote = Remote_profile::staticGet('uri', $listenee);
if (!$remote) {
$this->clientError(_('Profile unknown'), 404);
return false;
}
# Second, check to see if they should be able to post updates!
# We see if there are any subscriptions to that remote user with
# the given token.
$sub = new Subscription();
$sub->subscribed = $remote->id;
$sub->token = $token->key;
if (!$sub->find(true)) {
$this->clientError(_('You did not send us that profile'), 403);
return false;
}
$profile = Profile::staticGet('id', $remote->id);
if (!$profile) {
# This one is our fault
$this->serverError(_('Remote profile with no matching profile'), 500);
return false;
}
$nickname = $req->get_parameter('omb_listenee_nickname');
if ($nickname && !Validate::string($nickname, array('min_length' => 1,
'max_length' => 64,
'format' => NICKNAME_FMT))) {
$this->clientError(_('Nickname must have only lowercase letters and numbers and no spaces.'));
return false;
}
$license = $req->get_parameter('omb_listenee_license');
if ($license && !common_valid_http_url($license)) {
$this->clientError(sprintf(_("Invalid license URL '%s'"), $license));
return false;
}
return true;
@ -83,4 +116,3 @@ class UpdateprofileAction extends Action
}
}
}
?>

View File

@ -63,7 +63,11 @@ class UserauthorizationAction extends Action
/* Go log in, and then come back. */
common_set_returnto($_SERVER['REQUEST_URI']);
if (!common_config('site', 'openidonly')) {
common_redirect(common_local_url('login'));
} else {
common_redirect(common_local_url('openidlogin'));
}
return;
}
@ -353,4 +357,3 @@ class UserauthorizationAction extends Action
}
}
}
?>

View File

@ -88,9 +88,10 @@ class UserrssAction extends Rss10Action
$c = array('url' => common_local_url('userrss',
array('nickname' =>
$user->nickname)),
'title' => $user->nickname,
'title' => sprintf(_('%s timeline'), $user->nickname),
'link' => $profile->profileurl,
'description' => sprintf(_('Microblog by %s'), $user->nickname));
'description' => sprintf(_('Updates from %1$s on %2$s!'),
$user->nickname, common_config('site', 'name')));
return $c;
}

View File

@ -107,7 +107,7 @@ class Design extends Memcached_DataObject
static function toWebColor($color)
{
if (is_null($color)) {
if ($color == null) {
return null;
}
@ -204,7 +204,10 @@ class Design extends Memcached_DataObject
'disposition');
foreach ($attrs as $attr) {
$siteDesign->$attr = common_config('design', $attr);
$val = common_config('design', $attr);
if ($val !== false) {
$siteDesign->$attr = $val;
}
}
}

View File

@ -95,7 +95,8 @@ class File extends Memcached_DataObject
if (empty($file_redir)) {
$redir_data = File_redirection::where($given_url);
$redir_url = $redir_data['url'];
if ($redir_url === $given_url) {
// TODO: max field length
if ($redir_url === $given_url || strlen($redir_url) > 255) {
$x = File::saveNew($redir_data, $given_url);
$file_id = $x->id;
} else {

View File

@ -29,34 +29,38 @@ class Foreign_link extends Memcached_DataObject
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
// XXX: This only returns a 1->1 single obj mapping. Change? Or make
// a getForeignUsers() that returns more than one? --Zach
static function getByUserID($user_id, $service)
{
if (empty($user_id) || empty($service)) {
return null;
}
$flink = new Foreign_link();
$flink->service = $service;
$flink->user_id = $user_id;
$flink->limit(1);
if ($flink->find(true)) {
return $flink;
}
$result = $flink->find(true);
return empty($result) ? null : $flink;
return null;
}
static function getByForeignID($foreign_id, $service)
{
if (empty($foreign_id) || empty($service)) {
return null;
} else {
$flink = new Foreign_link();
$flink->service = $service;
$flink->foreign_id = $foreign_id;
$flink->limit(1);
if ($flink->find(true)) {
return $flink;
}
$result = $flink->find(true);
return null;
return empty($result) ? null : $flink;
}
}
function set_flags($noticesend, $noticerecv, $replysync, $friendsync)

View File

@ -29,10 +29,6 @@ require_once INSTALLDIR.'/classes/Memcached_DataObject.php';
define('NOTICE_CACHE_WINDOW', 61);
define('NOTICE_LOCAL_PUBLIC', 1);
define('NOTICE_REMOTE_OMB', 0);
define('NOTICE_LOCAL_NONPUBLIC', -1);
define('MAX_BOXCARS', 128);
class Notice extends Memcached_DataObject
@ -60,6 +56,10 @@ class Notice extends Memcached_DataObject
/* the code above is auto generated do not remove the tag below */
###END_AUTOCODE
/* Notice types */
const LOCAL_PUBLIC = 1;
const REMOTE_OMB = 0;
const LOCAL_NONPUBLIC = -1;
const GATEWAY = -2;
function getProfile()
@ -146,7 +146,7 @@ class Notice extends Memcached_DataObject
}
static function saveNew($profile_id, $content, $source=null,
$is_local=1, $reply_to=null, $uri=null, $created=null) {
$is_local=Notice::LOCAL_PUBLIC, $reply_to=null, $uri=null, $created=null) {
$profile = Profile::staticGet($profile_id);
@ -189,7 +189,7 @@ class Notice extends Memcached_DataObject
if (($blacklist && in_array($profile_id, $blacklist)) ||
($source && $autosource && in_array($source, $autosource))) {
$notice->is_local = -1;
$notice->is_local = Notice::LOCAL_NONPUBLIC;
} else {
$notice->is_local = $is_local;
}
@ -521,7 +521,7 @@ class Notice extends Memcached_DataObject
function blowPublicCache($blowLast=false)
{
if ($this->is_local == 1) {
if ($this->is_local == Notice::LOCAL_PUBLIC) {
$cache = common_memcache();
if ($cache) {
$cache->delete(common_cache_key('public'));
@ -787,10 +787,11 @@ class Notice extends Memcached_DataObject
}
if (common_config('public', 'localonly')) {
$notice->whereAdd('is_local = 1');
$notice->whereAdd('is_local = ' . Notice::LOCAL_PUBLIC);
} else {
# -1 == blacklisted
$notice->whereAdd('is_local != -1');
# -1 == blacklisted, -2 == gateway (i.e. Twitter)
$notice->whereAdd('is_local !='. Notice::LOCAL_NONPUBLIC);
$notice->whereAdd('is_local !='. Notice::GATEWAY);
}
if ($since_id != 0) {

View File

@ -313,4 +313,45 @@ class User_group extends Memcached_DataObject
$desclimit = self::maxDescription();
return ($desclimit > 0 && !empty($desc) && (mb_strlen($desc) > $desclimit));
}
function asAtomEntry($namespace=false, $source=false)
{
$xs = new XMLStringer(true);
if ($namespace) {
$attrs = array('xmlns' => 'http://www.w3.org/2005/Atom',
'xmlns:thr' => 'http://purl.org/syndication/thread/1.0');
} else {
$attrs = array();
}
$xs->elementStart('entry', $attrs);
if ($source) {
$xs->elementStart('source');
$xs->element('title', null, $profile->nickname . " - " . common_config('site', 'name'));
$xs->element('link', array('href' => $this->permalink()));
}
if ($source) {
$xs->elementEnd('source');
}
$xs->element('title', null, $this->nickname);
$xs->element('summary', null, $this->description);
$xs->element('link', array('rel' => 'alternate',
'href' => $this->permalink()));
$xs->element('id', null, $this->permalink());
$xs->element('published', null, common_date_w3dtf($this->created));
$xs->element('updated', null, common_date_w3dtf($this->modified));
$xs->element('content', array('type' => 'html'), $this->description);
$xs->elementEnd('entry');
return $xs->getString();
}
}

View File

@ -38,6 +38,8 @@ $config['site']['path'] = 'laconica';
// $config['site']['closed'] = true;
// Only allow registration for people invited by another user
// $config['site']['inviteonly'] = true;
// Only allow registrations and logins through OpenID
// $config['site']['openidonly'] = true;
// Make the site invisible to non-logged-in users
// $config['site']['private'] = true;
@ -97,6 +99,9 @@ $config['sphinx']['port'] = 3312;
// $config['xmpp']['public'][] = 'someindexer@example.net';
// $config['xmpp']['debug'] = false;
// Disable OpenID
// $config['openid']['enabled'] = false;
// Turn off invites
// $config['invite']['enabled'] = false;
@ -164,6 +169,15 @@ $config['sphinx']['port'] = 3312;
// $config['memcached']['server'] = 'localhost';
// $config['memcached']['port'] = 11211;
// Disable post-by-email
// $config['emailpost']['enabled'] = false;
// Disable SMS
// $config['sms']['enabled'] = false;
// Disable Twitter integration
// $config['twitter']['enabled'] = false;
// Twitter integration source attribute. Note: default is Laconica
// $config['integration']['source'] = 'Laconica';
@ -173,6 +187,10 @@ $config['sphinx']['port'] = 3312;
//
// $config['twitterbridge']['enabled'] = true;
// Twitter OAuth settings
// $config['twitter']['consumer_key'] = 'YOURKEY';
// $config['twitter']['consumer_secret'] = 'YOURSECRET';
// Edit throttling. Off by default. If turned on, you can only post 20 notices
// every 10 minutes. Admins may want to play with the settings to minimize inconvenience for
// real users without getting uncontrollable floods from spammers or runaway bots.
@ -243,5 +261,6 @@ $config['sphinx']['port'] = 3312;
// $config['attachments']['user_quota'] = 50000000;
// $config['attachments']['monthly_quota'] = 15000000;
// $config['attachments']['uploads'] = true;
// $config['attachments']['path'] = "/file/";
// $config['oohembed']['endpoint'] = 'http://oohembed.com/oohembed/';

View File

@ -22,6 +22,8 @@ VALUES
('IdentiFox','IdentiFox','http://www.bitbucket.org/uncryptic/identifox/', now()),
('identitwitch','IdentiTwitch','http://richfish.org/identitwitch/', now()),
('LaTwit','LaTwit','http://latwit.mac65.com/', now()),
('LiveTweeter', 'LiveTweeter', 'http://addons.songbirdnest.com/addon/1204', now()),
('livetweeter', 'livetweeter', 'http://addons.songbirdnest.com/addon/1204', now()),
('maisha', 'Maisha', 'http://maisha.grango.org/', now()),
('mbpidgin','mbpidgin','http://code.google.com/p/microblog-purple/', now()),
('Mobidentica', 'Mobidentica', 'http://www.substanceofcode.com/software/mobidentica/', now()),
@ -34,6 +36,7 @@ VALUES
('pocketwit','PockeTwit','http://code.google.com/p/pocketwit/', now()),
('posty','Posty','http://spreadingfunkyness.com/posty/', now()),
('qtwitter','qTwitter','http://qtwitter.ayoy.net/', now()),
('qwit', 'Qwit', 'http://code.google.com/p/qwit/', now()),
('royalewithcheese','Royale With Cheese','http://p.hellyeah.org/', now()),
('rssdent','rssdent','http://github.com/zcopley/rssdent/tree/master', now()),
('rygh.no','rygh.no','http://rygh.no/', now()),

View File

@ -32,4 +32,15 @@ currently-implemented commands:
you subscribe to.
* **off**: Turn off notifications. You'll no longer receive Jabber
notifications.
* **stop**: Same as 'off'
* **quit**: Same as 'off'
* **help**: Show this help. List available Jabber/XMPP commands
* **follow &lt;nickname&gt;**: Subscribe to &lt;nickname&gt;
* **sub &lt;nickname&gt;**: Same as follow
* **leave &lt;nickname&gt;**: Subscribe to &lt;nickname&gt;
* **unsub &lt;nickname&gt;**: Same as leave
* **d &lt;nickname&gt; &lt;text&gt;**: Send direct message to &lt;nickname&gt; with message body &lt;text&gt;
* **get &lt;nickname&gt;**: Get last notice from &lt;nickname&gt;
* **last &lt;nickname&gt;**: Same as 'get'
* **whois &lt;nickname&gt;**: Get Profile info on &lt;nickname&gt;
* **fav &lt;nickname&gt;**: Add user's last notice as a favorite

View File

@ -44,24 +44,24 @@ You can use the following commands with %%site.name%%.
* on - turn on notifications
* off - turn off notifications
* help - show this help
* follow <nickname> - subscribe to user
* leave <nickname> - unsubscribe from user
* d <nickname> <text> - direct message to user
* get <nickname> - get last notice from user
* whois <nickname> - get profile info on user
* fav <nickname> - add user's last notice as a 'fave'
* follow &lt;nickname&gt; - subscribe to user
* leave &lt;nickname&gt; - unsubscribe from user
* d &lt;nickname&gt; &lt;text&gt; - direct message to user
* get &lt;nickname&gt; - get last notice from user
* whois &lt;nickname&gt; - get profile info on user
* fav &lt;nickname&gt; - add user's last notice as a 'fave'
* stats - get your stats
* stop - same as 'off'
* quit - same as 'off'
* sub <nickname> - same as 'follow'
* unsub <nickname> - same as 'leave'
* last <nickname> - same as 'get'
* on <nickname> - not yet implemented.
* off <nickname> - not yet implemented.
* nudge <nickname> - not yet implemented.
* invite <phone number> - not yet implemented.
* track <word> - not yet implemented.
* untrack <word> - not yet implemented.
* sub &lt;nickname&gt; - same as 'follow'
* unsub &lt;nickname&gt; - same as 'leave'
* last &lt;nickname&gt; - same as 'get'
* on &lt;nickname&gt; - not yet implemented.
* off &lt;nickname&gt; - not yet implemented.
* nudge &lt;nickname&gt; - not yet implemented.
* invite &lt;phone number&gt; - not yet implemented.
* track &lt;word&gt; - not yet implemented.
* untrack &lt;word&gt; - not yet implemented.
* track off - not yet implemented.
* untrack all - not yet implemented.
* tracks - not yet implemented.

View File

@ -73,7 +73,7 @@ function handleError($error)
exit(-1);
}
function checkMirror($action_obj)
function checkMirror($action_obj, $args)
{
global $config;
@ -121,6 +121,25 @@ function isLoginAction($action)
function main()
{
// fake HTTP redirects using lighttpd's 404 redirects
if (strpos($_SERVER['SERVER_SOFTWARE'], 'lighttpd') !== false) {
$_lighty_url = $base_url.$_SERVER['REQUEST_URI'];
$_lighty_url = @parse_url($_lighty_url);
if ($_lighty_url['path'] != '/index.php' && $_lighty_url['path'] != '/') {
$_lighty_path = preg_replace('/^'.preg_quote(common_config('site','path')).'\//', '', substr($_lighty_url['path'], 1));
$_SERVER['QUERY_STRING'] = 'p='.$_lighty_path;
if ($_lighty_url['query'])
$_SERVER['QUERY_STRING'] .= '&'.$_lighty_url['query'];
parse_str($_lighty_url['query'], $_lighty_query);
foreach ($_lighty_query as $key => $val) {
$_GET[$key] = $_REQUEST[$key] = $val;
}
$_GET['p'] = $_REQUEST['p'] = $_lighty_path;
}
}
$_SERVER['REDIRECT_URL'] = preg_replace("/\?.+$/", "", $_SERVER['REQUEST_URI']);
// quick check for fancy URL auto-detection support in installer.
if (isset($_SERVER['REDIRECT_URL']) && (preg_replace("/^\/$/","",(dirname($_SERVER['REQUEST_URI']))) . '/check-fancy') === $_SERVER['REDIRECT_URL']) {
die("Fancy URL support detection succeeded. We suggest you enable this to get fancy (pretty) URLs.");
@ -192,7 +211,7 @@ function main()
} else {
$action_obj = new $action_class();
checkMirror($action_obj);
checkMirror($action_obj, $args);
try {
if ($action_obj->prepare($args)) {

View File

@ -163,7 +163,7 @@ E_O_T;
function updateStatus($status, $error=false)
{
?>
<li <?php echo ($error) ? 'class="error"': ''; ?>><?print $status;?></li>
<li <?php echo ($error) ? 'class="error"': ''; ?>><?php echo $status;?></li>
<?php
}

163
js/jcrop/jquery.Jcrop.min.js vendored Normal file
View File

@ -0,0 +1,163 @@
/**
* Jcrop v.0.9.8 (minimized)
* (c) 2008 Kelly Hallman and DeepLiquid.com
* More information: http://deepliquid.com/content/Jcrop.html
* Released under MIT License - this header must remain with code
*/
(function($){$.Jcrop=function(obj,opt)
{var obj=obj,opt=opt;if(typeof(obj)!=='object')obj=$(obj)[0];if(typeof(opt)!=='object')opt={};if(!('trackDocument'in opt))
{opt.trackDocument=$.browser.msie?false:true;if($.browser.msie&&$.browser.version.split('.')[0]=='8')
opt.trackDocument=true;}
if(!('keySupport'in opt))
opt.keySupport=$.browser.msie?false:true;var defaults={trackDocument:false,baseClass:'jcrop',addClass:null,bgColor:'black',bgOpacity:.6,borderOpacity:.4,handleOpacity:.5,handlePad:5,handleSize:9,handleOffset:5,edgeMargin:14,aspectRatio:0,keySupport:true,cornerHandles:true,sideHandles:true,drawBorders:true,dragEdges:true,boxWidth:0,boxHeight:0,boundary:8,animationDelay:20,swingSpeed:3,allowSelect:true,allowMove:true,allowResize:true,minSelect:[0,0],maxSize:[0,0],minSize:[0,0],onChange:function(){},onSelect:function(){}};var options=defaults;setOptions(opt);var $origimg=$(obj);var $img=$origimg.clone().removeAttr('id').css({position:'absolute'});$img.width($origimg.width());$img.height($origimg.height());$origimg.after($img).hide();presize($img,options.boxWidth,options.boxHeight);var boundx=$img.width(),boundy=$img.height(),$div=$('<div />').width(boundx).height(boundy).addClass(cssClass('holder')).css({position:'relative',backgroundColor:options.bgColor}).insertAfter($origimg).append($img);;if(options.addClass)$div.addClass(options.addClass);var $img2=$('<img />').attr('src',$img.attr('src')).css('position','absolute').width(boundx).height(boundy);var $img_holder=$('<div />').width(pct(100)).height(pct(100)).css({zIndex:310,position:'absolute',overflow:'hidden'}).append($img2);var $hdl_holder=$('<div />').width(pct(100)).height(pct(100)).css('zIndex',320);var $sel=$('<div />').css({position:'absolute',zIndex:300}).insertBefore($img).append($img_holder,$hdl_holder);var bound=options.boundary;var $trk=newTracker().width(boundx+(bound*2)).height(boundy+(bound*2)).css({position:'absolute',top:px(-bound),left:px(-bound),zIndex:290}).mousedown(newSelection);var xlimit,ylimit,xmin,ymin;var xscale,yscale,enabled=true;var docOffset=getPos($img),btndown,lastcurs,dimmed,animating,shift_down;var Coords=function()
{var x1=0,y1=0,x2=0,y2=0,ox,oy;function setPressed(pos)
{var pos=rebound(pos);x2=x1=pos[0];y2=y1=pos[1];};function setCurrent(pos)
{var pos=rebound(pos);ox=pos[0]-x2;oy=pos[1]-y2;x2=pos[0];y2=pos[1];};function getOffset()
{return[ox,oy];};function moveOffset(offset)
{var ox=offset[0],oy=offset[1];if(0>x1+ox)ox-=ox+x1;if(0>y1+oy)oy-=oy+y1;if(boundy<y2+oy)oy+=boundy-(y2+oy);if(boundx<x2+ox)ox+=boundx-(x2+ox);x1+=ox;x2+=ox;y1+=oy;y2+=oy;};function getCorner(ord)
{var c=getFixed();switch(ord)
{case'ne':return[c.x2,c.y];case'nw':return[c.x,c.y];case'se':return[c.x2,c.y2];case'sw':return[c.x,c.y2];}};function getFixed()
{if(!options.aspectRatio)return getRect();var aspect=options.aspectRatio,min_x=options.minSize[0]/xscale,min_y=options.minSize[1]/yscale,max_x=options.maxSize[0]/xscale,max_y=options.maxSize[1]/yscale,rw=x2-x1,rh=y2-y1,rwa=Math.abs(rw),rha=Math.abs(rh),real_ratio=rwa/rha,xx,yy;if(max_x==0){max_x=boundx*10}
if(max_y==0){max_y=boundy*10}
if(real_ratio<aspect)
{yy=y2;w=rha*aspect;xx=rw<0?x1-w:w+x1;if(xx<0)
{xx=0;h=Math.abs((xx-x1)/aspect);yy=rh<0?y1-h:h+y1;}
else if(xx>boundx)
{xx=boundx;h=Math.abs((xx-x1)/aspect);yy=rh<0?y1-h:h+y1;}}
else
{xx=x2;h=rwa/aspect;yy=rh<0?y1-h:y1+h;if(yy<0)
{yy=0;w=Math.abs((yy-y1)*aspect);xx=rw<0?x1-w:w+x1;}
else if(yy>boundy)
{yy=boundy;w=Math.abs(yy-y1)*aspect;xx=rw<0?x1-w:w+x1;}}
if(xx>x1){if(xx-x1<min_x){xx=x1+min_x;}else if(xx-x1>max_x){xx=x1+max_x;}
if(yy>y1){yy=y1+(xx-x1)/aspect;}else{yy=y1-(xx-x1)/aspect;}}else if(xx<x1){if(x1-xx<min_x){xx=x1-min_x}else if(x1-xx>max_x){xx=x1-max_x;}
if(yy>y1){yy=y1+(x1-xx)/aspect;}else{yy=y1-(x1-xx)/aspect;}}
if(xx<0){x1-=xx;xx=0;}else if(xx>boundx){x1-=xx-boundx;xx=boundx;}
if(yy<0){y1-=yy;yy=0;}else if(yy>boundy){y1-=yy-boundy;yy=boundy;}
return last=makeObj(flipCoords(x1,y1,xx,yy));};function rebound(p)
{if(p[0]<0)p[0]=0;if(p[1]<0)p[1]=0;if(p[0]>boundx)p[0]=boundx;if(p[1]>boundy)p[1]=boundy;return[p[0],p[1]];};function flipCoords(x1,y1,x2,y2)
{var xa=x1,xb=x2,ya=y1,yb=y2;if(x2<x1)
{xa=x2;xb=x1;}
if(y2<y1)
{ya=y2;yb=y1;}
return[Math.round(xa),Math.round(ya),Math.round(xb),Math.round(yb)];};function getRect()
{var xsize=x2-x1;var ysize=y2-y1;if(xlimit&&(Math.abs(xsize)>xlimit))
x2=(xsize>0)?(x1+xlimit):(x1-xlimit);if(ylimit&&(Math.abs(ysize)>ylimit))
y2=(ysize>0)?(y1+ylimit):(y1-ylimit);if(ymin&&(Math.abs(ysize)<ymin))
y2=(ysize>0)?(y1+ymin):(y1-ymin);if(xmin&&(Math.abs(xsize)<xmin))
x2=(xsize>0)?(x1+xmin):(x1-xmin);if(x1<0){x2-=x1;x1-=x1;}
if(y1<0){y2-=y1;y1-=y1;}
if(x2<0){x1-=x2;x2-=x2;}
if(y2<0){y1-=y2;y2-=y2;}
if(x2>boundx){var delta=x2-boundx;x1-=delta;x2-=delta;}
if(y2>boundy){var delta=y2-boundy;y1-=delta;y2-=delta;}
if(x1>boundx){var delta=x1-boundy;y2-=delta;y1-=delta;}
if(y1>boundy){var delta=y1-boundy;y2-=delta;y1-=delta;}
return makeObj(flipCoords(x1,y1,x2,y2));};function makeObj(a)
{return{x:a[0],y:a[1],x2:a[2],y2:a[3],w:a[2]-a[0],h:a[3]-a[1]};};return{flipCoords:flipCoords,setPressed:setPressed,setCurrent:setCurrent,getOffset:getOffset,moveOffset:moveOffset,getCorner:getCorner,getFixed:getFixed};}();var Selection=function()
{var start,end,dragmode,awake,hdep=370;var borders={};var handle={};var seehandles=false;var hhs=options.handleOffset;if(options.drawBorders){borders={top:insertBorder('hline').css('top',$.browser.msie?px(-1):px(0)),bottom:insertBorder('hline'),left:insertBorder('vline'),right:insertBorder('vline')};}
if(options.dragEdges){handle.t=insertDragbar('n');handle.b=insertDragbar('s');handle.r=insertDragbar('e');handle.l=insertDragbar('w');}
options.sideHandles&&createHandles(['n','s','e','w']);options.cornerHandles&&createHandles(['sw','nw','ne','se']);function insertBorder(type)
{var jq=$('<div />').css({position:'absolute',opacity:options.borderOpacity}).addClass(cssClass(type));$img_holder.append(jq);return jq;};function dragDiv(ord,zi)
{var jq=$('<div />').mousedown(createDragger(ord)).css({cursor:ord+'-resize',position:'absolute',zIndex:zi});$hdl_holder.append(jq);return jq;};function insertHandle(ord)
{return dragDiv(ord,hdep++).css({top:px(-hhs+1),left:px(-hhs+1),opacity:options.handleOpacity}).addClass(cssClass('handle'));};function insertDragbar(ord)
{var s=options.handleSize,o=hhs,h=s,w=s,t=o,l=o;switch(ord)
{case'n':case's':w=pct(100);break;case'e':case'w':h=pct(100);break;}
return dragDiv(ord,hdep++).width(w).height(h).css({top:px(-t+1),left:px(-l+1)});};function createHandles(li)
{for(i in li)handle[li[i]]=insertHandle(li[i]);};function moveHandles(c)
{var midvert=Math.round((c.h/2)-hhs),midhoriz=Math.round((c.w/2)-hhs),north=west=-hhs+1,east=c.w-hhs,south=c.h-hhs,x,y;'e'in handle&&handle.e.css({top:px(midvert),left:px(east)})&&handle.w.css({top:px(midvert)})&&handle.s.css({top:px(south),left:px(midhoriz)})&&handle.n.css({left:px(midhoriz)});'ne'in handle&&handle.ne.css({left:px(east)})&&handle.se.css({top:px(south),left:px(east)})&&handle.sw.css({top:px(south)});'b'in handle&&handle.b.css({top:px(south)})&&handle.r.css({left:px(east)});};function moveto(x,y)
{$img2.css({top:px(-y),left:px(-x)});$sel.css({top:px(y),left:px(x)});};function resize(w,h)
{$sel.width(w).height(h);};function refresh()
{var c=Coords.getFixed();Coords.setPressed([c.x,c.y]);Coords.setCurrent([c.x2,c.y2]);updateVisible();};function updateVisible()
{if(awake)return update();};function update()
{var c=Coords.getFixed();resize(c.w,c.h);moveto(c.x,c.y);options.drawBorders&&borders['right'].css({left:px(c.w-1)})&&borders['bottom'].css({top:px(c.h-1)});seehandles&&moveHandles(c);awake||show();options.onChange(unscale(c));};function show()
{$sel.show();$img.css('opacity',options.bgOpacity);awake=true;};function release()
{disableHandles();$sel.hide();$img.css('opacity',1);awake=false;};function showHandles()
{if(seehandles)
{moveHandles(Coords.getFixed());$hdl_holder.show();}};function enableHandles()
{seehandles=true;if(options.allowResize)
{moveHandles(Coords.getFixed());$hdl_holder.show();return true;}};function disableHandles()
{seehandles=false;$hdl_holder.hide();};function animMode(v)
{(animating=v)?disableHandles():enableHandles();};function done()
{animMode(false);refresh();};var $track=newTracker().mousedown(createDragger('move')).css({cursor:'move',position:'absolute',zIndex:360})
$img_holder.append($track);disableHandles();return{updateVisible:updateVisible,update:update,release:release,refresh:refresh,setCursor:function(cursor){$track.css('cursor',cursor);},enableHandles:enableHandles,enableOnly:function(){seehandles=true;},showHandles:showHandles,disableHandles:disableHandles,animMode:animMode,done:done};}();var Tracker=function()
{var onMove=function(){},onDone=function(){},trackDoc=options.trackDocument;if(!trackDoc)
{$trk.mousemove(trackMove).mouseup(trackUp).mouseout(trackUp);}
function toFront()
{$trk.css({zIndex:450});if(trackDoc)
{$(document).mousemove(trackMove).mouseup(trackUp);}}
function toBack()
{$trk.css({zIndex:290});if(trackDoc)
{$(document).unbind('mousemove',trackMove).unbind('mouseup',trackUp);}}
function trackMove(e)
{onMove(mouseAbs(e));};function trackUp(e)
{e.preventDefault();e.stopPropagation();if(btndown)
{btndown=false;onDone(mouseAbs(e));options.onSelect(unscale(Coords.getFixed()));toBack();onMove=function(){};onDone=function(){};}
return false;};function activateHandlers(move,done)
{btndown=true;onMove=move;onDone=done;toFront();return false;};function setCursor(t){$trk.css('cursor',t);};$img.before($trk);return{activateHandlers:activateHandlers,setCursor:setCursor};}();var KeyManager=function()
{var $keymgr=$('<input type="radio" />').css({position:'absolute',left:'-30px'}).keypress(parseKey).blur(onBlur),$keywrap=$('<div />').css({position:'absolute',overflow:'hidden'}).append($keymgr);function watchKeys()
{if(options.keySupport)
{$keymgr.show();$keymgr.focus();}};function onBlur(e)
{$keymgr.hide();};function doNudge(e,x,y)
{if(options.allowMove){Coords.moveOffset([x,y]);Selection.updateVisible();};e.preventDefault();e.stopPropagation();};function parseKey(e)
{if(e.ctrlKey)return true;shift_down=e.shiftKey?true:false;var nudge=shift_down?10:1;switch(e.keyCode)
{case 37:doNudge(e,-nudge,0);break;case 39:doNudge(e,nudge,0);break;case 38:doNudge(e,0,-nudge);break;case 40:doNudge(e,0,nudge);break;case 27:Selection.release();break;case 9:return true;}
return nothing(e);};if(options.keySupport)$keywrap.insertBefore($img);return{watchKeys:watchKeys};}();function px(n){return''+parseInt(n)+'px';};function pct(n){return''+parseInt(n)+'%';};function cssClass(cl){return options.baseClass+'-'+cl;};function getPos(obj)
{var pos=$(obj).offset();return[pos.left,pos.top];};function mouseAbs(e)
{return[(e.pageX-docOffset[0]),(e.pageY-docOffset[1])];};function myCursor(type)
{if(type!=lastcurs)
{Tracker.setCursor(type);lastcurs=type;}};function startDragMode(mode,pos)
{docOffset=getPos($img);Tracker.setCursor(mode=='move'?mode:mode+'-resize');if(mode=='move')
return Tracker.activateHandlers(createMover(pos),doneSelect);var fc=Coords.getFixed();var opp=oppLockCorner(mode);var opc=Coords.getCorner(oppLockCorner(opp));Coords.setPressed(Coords.getCorner(opp));Coords.setCurrent(opc);Tracker.activateHandlers(dragmodeHandler(mode,fc),doneSelect);};function dragmodeHandler(mode,f)
{return function(pos){if(!options.aspectRatio)switch(mode)
{case'e':pos[1]=f.y2;break;case'w':pos[1]=f.y2;break;case'n':pos[0]=f.x2;break;case's':pos[0]=f.x2;break;}
else switch(mode)
{case'e':pos[1]=f.y+1;break;case'w':pos[1]=f.y+1;break;case'n':pos[0]=f.x+1;break;case's':pos[0]=f.x+1;break;}
Coords.setCurrent(pos);Selection.update();};};function createMover(pos)
{var lloc=pos;KeyManager.watchKeys();return function(pos)
{Coords.moveOffset([pos[0]-lloc[0],pos[1]-lloc[1]]);lloc=pos;Selection.update();};};function oppLockCorner(ord)
{switch(ord)
{case'n':return'sw';case's':return'nw';case'e':return'nw';case'w':return'ne';case'ne':return'sw';case'nw':return'se';case'se':return'nw';case'sw':return'ne';};};function createDragger(ord)
{return function(e){if(options.disabled)return false;if((ord=='move')&&!options.allowMove)return false;btndown=true;startDragMode(ord,mouseAbs(e));e.stopPropagation();e.preventDefault();return false;};};function presize($obj,w,h)
{var nw=$obj.width(),nh=$obj.height();if((nw>w)&&w>0)
{nw=w;nh=(w/$obj.width())*$obj.height();}
if((nh>h)&&h>0)
{nh=h;nw=(h/$obj.height())*$obj.width();}
xscale=$obj.width()/nw;yscale=$obj.height()/nh;$obj.width(nw).height(nh);};function unscale(c)
{return{x:parseInt(c.x*xscale),y:parseInt(c.y*yscale),x2:parseInt(c.x2*xscale),y2:parseInt(c.y2*yscale),w:parseInt(c.w*xscale),h:parseInt(c.h*yscale)};};function doneSelect(pos)
{var c=Coords.getFixed();if(c.w>options.minSelect[0]&&c.h>options.minSelect[1])
{Selection.enableHandles();Selection.done();}
else
{Selection.release();}
Tracker.setCursor(options.allowSelect?'crosshair':'default');};function newSelection(e)
{if(options.disabled)return false;if(!options.allowSelect)return false;btndown=true;docOffset=getPos($img);Selection.disableHandles();myCursor('crosshair');var pos=mouseAbs(e);Coords.setPressed(pos);Tracker.activateHandlers(selectDrag,doneSelect);KeyManager.watchKeys();Selection.update();e.stopPropagation();e.preventDefault();return false;};function selectDrag(pos)
{Coords.setCurrent(pos);Selection.update();};function newTracker()
{var trk=$('<div></div>').addClass(cssClass('tracker'));$.browser.msie&&trk.css({opacity:0,backgroundColor:'white'});return trk;};function animateTo(a)
{var x1=a[0]/xscale,y1=a[1]/yscale,x2=a[2]/xscale,y2=a[3]/yscale;if(animating)return;var animto=Coords.flipCoords(x1,y1,x2,y2);var c=Coords.getFixed();var animat=initcr=[c.x,c.y,c.x2,c.y2];var interv=options.animationDelay;var x=animat[0];var y=animat[1];var x2=animat[2];var y2=animat[3];var ix1=animto[0]-initcr[0];var iy1=animto[1]-initcr[1];var ix2=animto[2]-initcr[2];var iy2=animto[3]-initcr[3];var pcent=0;var velocity=options.swingSpeed;Selection.animMode(true);var animator=function()
{return function()
{pcent+=(100-pcent)/velocity;animat[0]=x+((pcent/100)*ix1);animat[1]=y+((pcent/100)*iy1);animat[2]=x2+((pcent/100)*ix2);animat[3]=y2+((pcent/100)*iy2);if(pcent<100)animateStart();else Selection.done();if(pcent>=99.8)pcent=100;setSelectRaw(animat);};}();function animateStart()
{window.setTimeout(animator,interv);};animateStart();};function setSelect(rect)
{setSelectRaw([rect[0]/xscale,rect[1]/yscale,rect[2]/xscale,rect[3]/yscale]);};function setSelectRaw(l)
{Coords.setPressed([l[0],l[1]]);Coords.setCurrent([l[2],l[3]]);Selection.update();};function setOptions(opt)
{if(typeof(opt)!='object')opt={};options=$.extend(options,opt);if(typeof(options.onChange)!=='function')
options.onChange=function(){};if(typeof(options.onSelect)!=='function')
options.onSelect=function(){};};function tellSelect()
{return unscale(Coords.getFixed());};function tellScaled()
{return Coords.getFixed();};function setOptionsNew(opt)
{setOptions(opt);interfaceUpdate();};function disableCrop()
{options.disabled=true;Selection.disableHandles();Selection.setCursor('default');Tracker.setCursor('default');};function enableCrop()
{options.disabled=false;interfaceUpdate();};function cancelCrop()
{Selection.done();Tracker.activateHandlers(null,null);};function destroy()
{$div.remove();$origimg.show();};function interfaceUpdate(alt)
{options.allowResize?alt?Selection.enableOnly():Selection.enableHandles():Selection.disableHandles();Tracker.setCursor(options.allowSelect?'crosshair':'default');Selection.setCursor(options.allowMove?'move':'default');$div.css('backgroundColor',options.bgColor);if('setSelect'in options){setSelect(opt.setSelect);Selection.done();delete(options.setSelect);}
if('trueSize'in options){xscale=options.trueSize[0]/boundx;yscale=options.trueSize[1]/boundy;}
xlimit=options.maxSize[0]||0;ylimit=options.maxSize[1]||0;xmin=options.minSize[0]||0;ymin=options.minSize[1]||0;if('outerImage'in options)
{$img.attr('src',options.outerImage);delete(options.outerImage);}
Selection.refresh();};$hdl_holder.hide();interfaceUpdate(true);var api={animateTo:animateTo,setSelect:setSelect,setOptions:setOptionsNew,tellSelect:tellSelect,tellScaled:tellScaled,disable:disableCrop,enable:enableCrop,cancel:cancelCrop,focus:KeyManager.watchKeys,getBounds:function(){return[boundx*xscale,boundy*yscale];},getWidgetSize:function(){return[boundx,boundy];},release:Selection.release,destroy:destroy};$origimg.data('Jcrop',api);return api;};$.fn.Jcrop=function(options)
{function attachWhenDone(from)
{var loadsrc=options.useImg||from.src;var img=new Image();img.onload=function(){$.Jcrop(from,options);};img.src=loadsrc;};if(typeof(options)!=='object')options={};this.each(function()
{if($(this).data('Jcrop'))
{if(options=='api')return $(this).data('Jcrop');else $(this).data('Jcrop').setOptions(options);}
else attachWhenDone(this);});return this;};})(jQuery);

File diff suppressed because one or more lines are too long

View File

@ -27,13 +27,14 @@ $(document).ready(function() {
}
}
/* rgb2hex written by R0bb13 <robertorebollo@gmail.com> */
function rgb2hex(rgb) {
rgb = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
function hex(x) {
hexDigits = new Array("0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F");
return isNaN(x) ? "00" : hexDigits[(x - x % 16) / 16] + hexDigits[x % 16];
return '#' + dec2hex(rgb[1]) + dec2hex(rgb[2]) + dec2hex(rgb[3]);
}
return "#" + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]);
function dec2hex(x) {
hexDigits = new Array('0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F');
return isNaN(x) ? '00' : hexDigits[(x - x % 16) / 16] + hexDigits[x % 16];
}
function UpdateColors(S) {

View File

@ -17,6 +17,8 @@
*/
$(document).ready(function(){
var counterBlackout = false;
// count character on keyup
function counter(event){
if (maxLength <= 0) {
@ -25,20 +27,43 @@ $(document).ready(function(){
var currentLength = $("#notice_data-text").val().length;
var remaining = maxLength - currentLength;
var counter = $("#notice_text-count");
counter.text(remaining);
if (remaining <= 0) {
if (remaining.toString() != counter.text()) {
if (!counterBlackout || remaining == 0) {
if (counter.text() != String(remaining)) {
counter.text(remaining);
}
if (remaining < 0) {
$("#form_notice").addClass("warning");
} else {
$("#form_notice").removeClass("warning");
}
// Skip updates for the next 500ms.
// On slower hardware, updating on every keypress is unpleasant.
if (!counterBlackout) {
counterBlackout = true;
window.setTimeout(clearCounterBlackout, 500);
}
}
}
}
function clearCounterBlackout() {
// Allow keyup events to poke the counter again
counterBlackout = false;
// Check if the string changed since we last looked
counter(null);
}
function submitonreturn(event) {
if (event.keyCode == 13) {
if (event.keyCode == 13 || event.keyCode == 10) {
// iPhone sends \n not \r for 'return'
$("#form_notice").submit();
event.preventDefault();
event.stopPropagation();
$("#notice_data-text").blur();
$("body").focus();
return false;
}
return true;
@ -67,6 +92,10 @@ $(document).ready(function(){
// XXX: refactor this code
var favoptions = { dataType: 'xml',
beforeSubmit: function(data, target, options) {
$(target).addClass('processing');
return true;
},
success: function(xml) { var new_form = document._importNode($('form', xml).get(0), true);
var dis = new_form.id;
var fav = dis.replace('disfavor', 'favor');
@ -76,6 +105,10 @@ $(document).ready(function(){
};
var disoptions = { dataType: 'xml',
beforeSubmit: function(data, target, options) {
$(target).addClass('processing');
return true;
},
success: function(xml) { var new_form = document._importNode($('form', xml).get(0), true);
var fav = new_form.id;
var dis = fav.replace('favor', 'disfavor');
@ -258,7 +291,7 @@ function NoticeReply() {
$('#content .notice').each(function() {
var notice = $(this)[0];
$($('.notice_reply', notice)[0]).click(function() {
var nickname = ($('.author .nickname', notice).length > 0) ? $($('.author .nickname', notice)[0]) : $('.author .nickname');
var nickname = ($('.author .nickname', notice).length > 0) ? $($('.author .nickname', notice)[0]) : $('.author .nickname.uid');
NoticeReplySet(nickname.text(), $($('.notice_id', notice)[0]).text());
return false;
});
@ -269,11 +302,16 @@ function NoticeReply() {
function NoticeReplySet(nick,id) {
rgx_username = /^[0-9a-zA-Z\-_.]*$/;
if (nick.match(rgx_username)) {
var text = $("#notice_data-text");
if (text.length) {
replyto = "@" + nick + " ";
if ($("#notice_data-text").length) {
$("#notice_data-text").val(replyto);
text.val(replyto + text.val().replace(RegExp(replyto, 'i'), ''));
$("#form_notice input#notice_in-reply-to").val(id);
$("#notice_data-text").focus();
if (text.get(0).setSelectionRange) {
var len = text.val().length;
text.get(0).setSelectionRange(len,len);
text.get(0).focus();
}
return false;
}
}

View File

@ -196,21 +196,12 @@ class Action extends HTMLOutputter // lawsuit
if (Event::handle('StartShowStyles', array($this))) {
if (Event::handle('StartShowLaconicaStyles', array($this))) {
$this->element('link', array('rel' => 'stylesheet',
'type' => 'text/css',
'href' => theme_path('css/display.css', null) . '?version=' . LACONICA_VERSION,
'media' => 'screen, projection, tv'));
$this->cssLink('css/display.css',null,'screen, projection, tv');
if (common_config('site', 'mobile')) {
$this->element('link', array('rel' => 'stylesheet',
'type' => 'text/css',
'href' => theme_path('css/mobile.css', 'base') . '?version=' . LACONICA_VERSION,
// TODO: "handheld" CSS for other mobile devices
'media' => 'only screen and (max-device-width: 480px)')); // Mobile WebKit
$this->cssLink('css/mobile.css','base','only screen and (max-device-width: 480px)'); // Mobile WebKit
}
$this->element('link', array('rel' => 'stylesheet',
'type' => 'text/css',
'href' => theme_path('css/print.css', 'base') . '?version=' . LACONICA_VERSION,
'media' => 'print'));
$this->cssLink('css/print.css','base','print');
Event::handle('EndShowLaconicaStyles', array($this));
}
@ -256,26 +247,14 @@ class Action extends HTMLOutputter // lawsuit
{
if (Event::handle('StartShowScripts', array($this))) {
if (Event::handle('StartShowJQueryScripts', array($this))) {
$this->element('script', array('type' => 'text/javascript',
'src' => common_path('js/jquery.min.js')),
' ');
$this->element('script', array('type' => 'text/javascript',
'src' => common_path('js/jquery.form.js')),
' ');
$this->element('script', array('type' => 'text/javascript',
'src' => common_path('js/jquery.joverlay.min.js')),
' ');
$this->script('js/jquery.min.js');
$this->script('js/jquery.form.js');
$this->script('js/jquery.joverlay.min.js');
Event::handle('EndShowJQueryScripts', array($this));
}
if (Event::handle('StartShowLaconicaScripts', array($this))) {
$this->element('script', array('type' => 'text/javascript',
'src' => common_path('js/xbImportNode.js')),
' ');
$this->element('script', array('type' => 'text/javascript',
'src' => common_path('js/util.js?version='.LACONICA_VERSION)),
' ');
$this->script('js/xbImportNode.js');
$this->script('js/util.js');
// Frame-busting code to avoid clickjacking attacks.
$this->element('script', array('type' => 'text/javascript'),
'if (window.top !== window.self) { window.top.location.href = window.self.location.href; }');
@ -426,6 +405,14 @@ class Action extends HTMLOutputter // lawsuit
function showPrimaryNav()
{
$user = common_current_user();
$connect = '';
if (common_config('xmpp', 'enabled')) {
$connect = 'imsettings';
} else if (common_config('sms', 'enabled')) {
$connect = 'smssettings';
} else if (common_config('twitter', 'enabled')) {
$connect = 'twittersettings';
}
$this->elementStart('dl', array('id' => 'site_nav_global_primary'));
$this->element('dt', null, _('Primary site navigation'));
@ -437,12 +424,9 @@ class Action extends HTMLOutputter // lawsuit
_('Home'), _('Personal profile and friends timeline'), false, 'nav_home');
$this->menuItem(common_local_url('profilesettings'),
_('Account'), _('Change your email, avatar, password, profile'), false, 'nav_account');
if (common_config('xmpp', 'enabled')) {
$this->menuItem(common_local_url('imsettings'),
_('Connect'), _('Connect to IM, SMS, Twitter'), false, 'nav_connect');
} else {
$this->menuItem(common_local_url('smssettings'),
_('Connect'), _('Connect to SMS, Twitter'), false, 'nav_connect');
if ($connect) {
$this->menuItem(common_local_url($connect),
_('Connect'), _('Connect to services'), false, 'nav_connect');
}
if (common_config('invite', 'enabled')) {
$this->menuItem(common_local_url('invite'),
@ -455,17 +439,24 @@ class Action extends HTMLOutputter // lawsuit
_('Logout'), _('Logout from the site'), false, 'nav_logout');
}
else {
if (!common_config('site', 'openidonly')) {
if (!common_config('site', 'closed')) {
$this->menuItem(common_local_url('register'),
_('Register'), _('Create an account'), false, 'nav_register');
}
$this->menuItem(common_local_url('login'),
_('Login'), _('Login to the site'), false, 'nav_login');
} else {
$this->menuItem(common_local_url('openidlogin'),
_('OpenID'), _('Login with OpenID'), false, 'nav_openid');
}
}
$this->menuItem(common_local_url('doc', array('title' => 'help')),
_('Help'), _('Help me!'), false, 'nav_help');
if ($user || !common_config('site', 'private')) {
$this->menuItem(common_local_url('peoplesearch'),
_('Search'), _('Search for people or text'), false, 'nav_search');
}
Event::handle('EndPrimaryNav', array($this));
}
$this->elementEnd('ul');

View File

@ -25,12 +25,14 @@ class ArrayWrapper
{
var $_items = null;
var $_count = 0;
var $N = 0;
var $_i = -1;
function __construct($items)
{
$this->_items = $items;
$this->_count = count($this->_items);
$this->N = $this->_count;
}
function fetch()

View File

@ -82,7 +82,7 @@ if (isset($server)) {
if (isset($path)) {
$_path = $path;
} else {
$_path = array_key_exists('SCRIPT_NAME', $_SERVER) ?
$_path = (array_key_exists('SERVER_NAME', $_SERVER) && array_key_exists('SCRIPT_NAME', $_SERVER)) ?
_sn_to_path($_SERVER['SCRIPT_NAME']) :
null;
}
@ -109,6 +109,7 @@ $config =
'broughtbyurl' => null,
'closed' => false,
'inviteonly' => false,
'openidonly' => false,
'private' => false,
'ssl' => 'never',
'sslserver' => null,
@ -172,6 +173,8 @@ $config =
'host' => null, # only set if != server
'debug' => false, # print extra debug info
'public' => array()), # JIDs of users who want to receive the public stream
'openid' =>
array('enabled' => true),
'invite' =>
array('enabled' => true),
'sphinx' =>
@ -186,11 +189,20 @@ $config =
array('piddir' => '/var/run',
'user' => false,
'group' => false),
'emailpost' =>
array('enabled' => true),
'sms' =>
array('enabled' => true),
'twitter' =>
array('enabled' => true),
'twitterbridge' =>
array('enabled' => false),
'integration' =>
array('source' => 'Laconica', # source attribute for Twitter
'taguri' => $_server.',2009'), # base for tag URIs
'twitter' =>
array('consumer_key' => null,
'consumer_secret' => null),
'memcached' =>
array('enabled' => false,
'server' => 'localhost',
@ -369,6 +381,12 @@ if ($_db_name != 'laconica' && !array_key_exists('ini_'.$_db_name, $config['db']
$config['db']['ini_'.$_db_name] = INSTALLDIR.'/classes/laconica.ini';
}
// Ignore openidonly if OpenID is disabled
if (!$config['openid']['enabled']) {
$config['site']['openidonly'] = false;
}
// XXX: how many of these could be auto-loaded on use?
require_once 'Validate.php';

View File

@ -99,25 +99,27 @@ class ConnectSettingsNav extends Widget
function show()
{
# action => array('prompt', 'title')
$menu =
array('imsettings' =>
$menu = array();
if (common_config('xmpp', 'enabled')) {
$menu['imsettings'] =
array(_('IM'),
_('Updates by instant messenger (IM)')),
'smssettings' =>
_('Updates by instant messenger (IM)'));
}
if (common_config('sms', 'enabled')) {
$menu['smssettings'] =
array(_('SMS'),
_('Updates by SMS')),
'twittersettings' =>
_('Updates by SMS'));
}
if (common_config('twitter', 'enabled')) {
$menu['twittersettings'] =
array(_('Twitter'),
_('Twitter integration options')));
_('Twitter integration options'));
}
$action_name = $this->action->trimmed('action');
$this->action->elementStart('ul', array('class' => 'nav'));
foreach ($menu as $menuaction => $menudesc) {
if ($menuaction == 'imsettings' &&
!common_config('xmpp', 'enabled')) {
continue;
}
$this->action->menuItem(common_local_url($menuaction),
$menudesc[0],
$menudesc[1],

View File

@ -311,13 +311,7 @@ class DesignSettingsAction extends AccountSettingsAction
function showStylesheets()
{
parent::showStylesheets();
$farbtasticStyle =
common_path('theme/base/css/farbtastic.css?version='.LACONICA_VERSION);
$this->element('link', array('rel' => 'stylesheet',
'type' => 'text/css',
'href' => $farbtasticStyle,
'media' => 'screen, projection, tv'));
$this->cssLink('css/farbtastic.css','base','screen, projection, tv');
}
/**
@ -330,13 +324,8 @@ class DesignSettingsAction extends AccountSettingsAction
{
parent::showScripts();
$farbtasticPack = common_path('js/farbtastic/farbtastic.js');
$userDesignGo = common_path('js/userdesign.go.js');
$this->element('script', array('type' => 'text/javascript',
'src' => $farbtasticPack));
$this->element('script', array('type' => 'text/javascript',
'src' => $userDesignGo));
$this->script('js/farbtastic/farbtastic.js');
$this->script('js/farbtastic/farbtastic.go.js');
}
/**

View File

@ -94,34 +94,13 @@ class FacebookAction extends Action
function showStylesheets()
{
// Add a timestamp to the file so Facebook cache wont ignore our changes
$ts = filemtime(INSTALLDIR.'/theme/base/css/display.css');
$this->element('link', array('rel' => 'stylesheet',
'type' => 'text/css',
'href' => theme_path('css/display.css', 'base') . '?ts=' . $ts));
$theme = common_config('site', 'theme');
$ts = filemtime(INSTALLDIR. '/theme/' . $theme .'/css/display.css');
$this->element('link', array('rel' => 'stylesheet',
'type' => 'text/css',
'href' => theme_path('css/display.css', null) . '?ts=' . $ts));
$ts = filemtime(INSTALLDIR.'/theme/base/css/facebookapp.css');
$this->element('link', array('rel' => 'stylesheet',
'type' => 'text/css',
'href' => theme_path('css/facebookapp.css', 'base') . '?ts=' . $ts));
$this->cssLink('css/display.css', 'base');
$this->cssLink('css/facebookapp.css', 'base');
}
function showScripts()
{
// Add a timestamp to the file so Facebook cache wont ignore our changes
$ts = filemtime(INSTALLDIR.'/js/facebookapp.js');
$this->element('script', array('src' => common_path('js/facebookapp.js') . '?ts=' . $ts));
$this->script('js/facebookapp.js');
}
/**
@ -274,8 +253,13 @@ class FacebookAction extends Action
$this->elementStart('dd');
$this->elementStart('p');
$this->text(sprintf($loginmsg_part1, common_config('site', 'name')));
if (!common_config('site', 'openidonly')) {
$this->element('a',
array('href' => common_local_url('register')), _('Register'));
} else {
$this->element('a',
array('href' => common_local_url('openidlogin')), _('Register'));
}
$this->text($loginmsg_part2);
$this->elementEnd('p');
$this->elementEnd('dd');

View File

@ -36,7 +36,7 @@ function getFacebook()
$facebook = new Facebook($apikey, $secret);
}
if (!$facebook) {
if (empty($facebook)) {
common_log(LOG_ERR, 'Could not make new Facebook client obj!',
__FILE__);
}
@ -44,71 +44,37 @@ function getFacebook()
return $facebook;
}
function updateProfileBox($facebook, $flink, $notice) {
$fbaction = new FacebookAction($output='php://output', $indent=true, $facebook, $flink);
$fbaction->updateProfileBox($notice);
}
function isFacebookBound($notice, $flink) {
if (empty($flink)) {
return false;
}
// Avoid a loop
if ($notice->source == 'Facebook') {
common_log(LOG_INFO, "Skipping notice $notice->id because its " .
'source is Facebook.');
return false;
}
// If the user does not want to broadcast to Facebook, move along
if (!($flink->noticesync & FOREIGN_NOTICE_SEND == FOREIGN_NOTICE_SEND)) {
common_log(LOG_INFO, "Skipping notice $notice->id " .
'because user has FOREIGN_NOTICE_SEND bit off.');
return false;
}
$success = false;
// If it's not a reply, or if the user WANTS to send @-replies,
// then, yeah, it can go to Facebook.
// If it's not a reply, or if the user WANTS to send @-replies...
if (!preg_match('/@[a-zA-Z0-9_]{1,15}\b/u', $notice->content) ||
($flink->noticesync & FOREIGN_NOTICE_SEND_REPLY)) {
$success = true;
// The two condition below are deal breakers:
// Avoid a loop
if ($notice->source == 'Facebook') {
common_log(LOG_INFO, "Skipping notice $notice->id because its " .
'source is Facebook.');
$success = false;
return true;
}
$facebook = getFacebook();
$fbuid = $flink->foreign_id;
try {
// Check to see if the user has given the FB app status update perms
$result = $facebook->api_client->
users_hasAppPermission('publish_stream', $fbuid);
if ($result != 1) {
$result = $facebook->api_client->
users_hasAppPermission('status_update', $fbuid);
}
if ($result != 1) {
$user = $flink->getUser();
$msg = "Not sending notice $notice->id to Facebook " .
"because user $user->nickname hasn't given the " .
'Facebook app \'status_update\' or \'publish_stream\' permission.';
common_debug($msg);
$success = false;
}
} catch(FacebookRestClientException $e){
common_log(LOG_ERR, $e->getMessage());
$success = false;
}
}
return $success;
return false;
}
@ -119,86 +85,63 @@ function facebookBroadcastNotice($notice)
if (isFacebookBound($notice, $flink)) {
// Okay, we're good to go, update the FB status
$status = null;
$fbuid = $flink->foreign_id;
$user = $flink->getUser();
$attachments = $notice->attachments();
try {
// Get the status 'verb' (prefix) the user has set
try {
$prefix = $facebook->api_client->
data_getUserPreference(FACEBOOK_NOTICE_PREFIX, $fbuid);
// XXX: Does this call count against our per user FB request limit?
// If so we should consider storing verb elsewhere or not storing
$prefix = $facebook->api_client->data_getUserPreference(FACEBOOK_NOTICE_PREFIX,
$fbuid);
$status = "$prefix $notice->content";
} catch(FacebookRestClientException $e) {
common_log(LOG_WARNING, $e->getMessage());
common_log(LOG_WARNING,
'Unable to get the status verb setting from Facebook ' .
"for $user->nickname (user id: $user->id).");
}
$can_publish = $facebook->api_client->users_hasAppPermission('publish_stream',
$fbuid);
// Okay, we're good to go, update the FB status
$can_update = $facebook->api_client->users_hasAppPermission('status_update',
$fbuid);
try {
$result = $facebook->api_client->
users_hasAppPermission('publish_stream', $fbuid);
if($result == 1){
// authorized to use the stream api, so use it
$fbattachment = null;
$attachments = $notice->attachments();
if($attachments){
$fbattachment=array();
$fbattachment['media']=array();
//facebook only supports one attachment per item
$attachment = $attachments[0];
$fbmedia=array();
if(strncmp($attachment->mimetype,'image/',strlen('image/'))==0){
$fbmedia['type']='image';
$fbmedia['src']=$attachment->url;
$fbmedia['href']=$attachment->url;
$fbattachment['media'][]=$fbmedia;
/* Video doesn't seem to work. The notice never makes it to facebook, and no error is reported.
}else if(strncmp($attachment->mimetype,'video/',strlen('image/'))==0 || $attachment->mimetype="application/ogg"){
$fbmedia['type']='video';
$fbmedia['video_src']=$attachment->url;
// http://wiki.developers.facebook.com/index.php/Attachment_%28Streams%29
// says that preview_img is required... but we have no value to put in it
// $fbmedia['preview_img']=$attachment->url;
if($attachment->title){
$fbmedia['video_title']=$attachment->title;
}
$fbmedia['video_type']=$attachment->mimetype;
$fbattachment['media'][]=$fbmedia;
*/
}else if($attachment->mimetype=='audio/mpeg'){
$fbmedia['type']='mp3';
$fbmedia['src']=$attachment->url;
$fbattachment['media'][]=$fbmedia;
}else if($attachment->mimetype=='application/x-shockwave-flash'){
$fbmedia['type']='flash';
// http://wiki.developers.facebook.com/index.php/Attachment_%28Streams%29
// says that imgsrc is required... but we have no value to put in it
// $fbmedia['imgsrc']='';
$fbmedia['swfsrc']=$attachment->url;
$fbattachment['media'][]=$fbmedia;
}else{
$fbattachment['name']=($attachment->title?$attachment->title:$attachment->url);
$fbattachment['href']=$attachment->url;
}
}
$facebook->api_client->stream_publish($status, $fbattachment, null, null, $fbuid);
}else{
if (!empty($attachments) && $can_publish == 1) {
$fbattachment = format_attachments($attachments);
$facebook->api_client->stream_publish($status, $fbattachment,
null, null, $fbuid);
common_log(LOG_INFO,
"Posted notice $notice->id w/attachment " .
"to Facebook user's stream (fbuid = $fbuid).");
} elseif ($can_update == 1 || $can_publish == 1) {
$facebook->api_client->users_setStatus($status, $fbuid, false, true);
common_log(LOG_INFO,
"Posted notice $notice->id to Facebook " .
"as a status update (fbuid = $fbuid).");
} else {
$msg = "Not sending notice $notice->id to Facebook " .
"because user $user->nickname hasn't given the " .
'Facebook app \'status_update\' or \'publish_stream\' permission.';
common_log(LOG_WARNING, $msg);
}
// Finally, attempt to update the user's profile box
if ($can_publish == 1 || $can_update == 1) {
updateProfileBox($facebook, $flink, $notice);
}
} catch (FacebookRestClientException $e) {
$code = $e->getCode();
common_log(LOG_ERR, 'Facebook returned error code ' .
common_log(LOG_WARNING, 'Facebook returned error code ' .
$code . ': ' . $e->getMessage());
common_log(LOG_ERR,
common_log(LOG_WARNING,
'Unable to update Facebook status for ' .
"$user->nickname (user id: $user->id)!");
@ -209,25 +152,62 @@ function facebookBroadcastNotice($notice)
// see: http://wiki.developers.facebook.com/index.php/Users.setStatus#Example_Return_XML
remove_facebook_app($flink);
} else {
// Try sending again later.
return false;
}
}
// Now try to update the profile box
try {
updateProfileBox($facebook, $flink, $notice);
} catch(FacebookRestClientException $e) {
common_log(LOG_ERR, 'Facebook returned error code ' .
$e->getCode() . ': ' . $e->getMessage());
common_log(LOG_WARNING,
'Unable to update Facebook profile box for ' .
"$user->nickname (user id: $user->id).");
}
}
return true;
}
function updateProfileBox($facebook, $flink, $notice) {
$fbaction = new FacebookAction($output = 'php://output',
$indent = true, $facebook, $flink);
$fbaction->updateProfileBox($notice);
}
function format_attachments($attachments)
{
$fbattachment = array();
$fbattachment['media'] = array();
// Facebook only supports one attachment per item
$attachment = $attachments[0];
$fbmedia = array();
if (strncmp($attachment->mimetype, 'image/', strlen('image/')) == 0) {
$fbmedia['type'] = 'image';
$fbmedia['src'] = $attachment->url;
$fbmedia['href'] = $attachment->url;
$fbattachment['media'][] = $fbmedia;
} else if ($attachment->mimetype == 'audio/mpeg') {
$fbmedia['type'] = 'mp3';
$fbmedia['src'] = $attachment->url;
$fbattachment['media'][] = $fbmedia;
}else if ($attachment->mimetype == 'application/x-shockwave-flash') {
$fbmedia['type'] = 'flash';
// http://wiki.developers.facebook.com/index.php/Attachment_%28Streams%29
// says that imgsrc is required... but we have no value to put in it
// $fbmedia['imgsrc']='';
$fbmedia['swfsrc'] = $attachment->url;
$fbattachment['media'][] = $fbmedia;
}else{
$fbattachment['name'] = ($attachment->title ?
$attachment->title : $attachment->url);
$fbattachment['href'] = $attachment->url;
}
return $fbattachment;
}
function remove_facebook_app($flink)

View File

@ -109,10 +109,11 @@ class HTMLOutputter extends XMLOutputter
header('Content-Type: '.$type);
$this->extraHeaders();
$this->startXML('html',
'-//W3C//DTD XHTML 1.0 Strict//EN',
'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd');
if( ! substr($type,0,strlen('text/html'))=='text/html' ){
// Browsers don't like it when <?xml it output for non-xhtml documents
$this->xw->startDocument('1.0', 'UTF-8');
}
$this->xw->writeDTD('html');
$language = $this->getLanguage();
@ -338,6 +339,52 @@ class HTMLOutputter extends XMLOutputter
'title' => $title));
}
/**
* output a script (almost always javascript) tag
*
* @param string $src relative or absolute script path
* @param string $type 'type' attribute value of the tag
*
* @return void
*/
function script($src, $type='text/javascript')
{
$url = parse_url($src);
if( empty($url->scheme) && empty($url->host) && empty($url->query) && empty($url->fragment))
{
$src = common_path($src) . '?version=' . LACONICA_VERSION;
}
$this->element('script', array('type' => $type,
'src' => $src),
' ');
}
/**
* output a css link
*
* @param string $src relative path within the theme directory, or an absolute path
* @param string $theme 'theme' that contains the stylesheet
* @param string media 'media' attribute of the tag
*
* @return void
*/
function cssLink($src,$theme=null,$media=null)
{
$url = parse_url($src);
if( empty($url->scheme) && empty($url->host) && empty($url->query) && empty($url->fragment))
{
if(file_exists(theme_file($src,$theme))){
$src = theme_path($src, $theme) . '?version=' . LACONICA_VERSION;
}else{
$src = common_path($src);
}
}
$this->element('link', array('rel' => 'stylesheet',
'type' => 'text/css',
'href' => $src,
'media' => $media));
}
/**
* output an HTML textarea and associated elements
*

View File

@ -207,7 +207,7 @@ class ResultItem
$replier_profile = null;
if ($this->notice->reply_to) {
$reply = Notice::staticGet(intval($notice->reply_to));
$reply = Notice::staticGet(intval($this->notice->reply_to));
if ($reply) {
$replier_profile = $reply->getProfile();
}
@ -224,7 +224,7 @@ class ResultItem
$user = User::staticGet('id', $this->profile->id);
$this->iso_language_code = $this->user->language;
$this->iso_language_code = $user->language;
$this->source = $this->getSourceLink($this->notice->source);

View File

@ -597,6 +597,16 @@ function mail_notify_attn($user, $notice)
common_init_locale($user->language);
if ($notice->conversation != $notice->id) {
$conversationEmailText = "The full conversation can be read here:\n\n".
"\t%5\$s\n\n ";
$conversationUrl = common_local_url('conversation',
array('id' => $notice->conversation)).'#notice-'.$notice->id;
} else {
$conversationEmailText = "%5\$s";
$conversationUrl = null;
}
$subject = sprintf(_('%s sent a notice to your attention'), $bestname);
$body = sprintf(_("%1\$s just sent a notice to your attention (an '@-reply') on %2\$s.\n\n".
@ -604,23 +614,25 @@ function mail_notify_attn($user, $notice)
"\t%3\$s\n\n" .
"It reads:\n\n".
"\t%4\$s\n\n" .
$conversationEmailText .
"You can reply back here:\n\n".
"\t%5\$s\n\n" .
"\t%6\$s\n\n" .
"The list of all @-replies for you here:\n\n" .
"%6\$s\n\n" .
"%7\$s\n\n" .
"Faithfully yours,\n" .
"%2\$s\n\n" .
"P.S. You can turn off these email notifications here: %7\$s\n"),
$bestname,
common_config('site', 'name'),
"P.S. You can turn off these email notifications here: %8\$s\n"),
$bestname,//%1
common_config('site', 'name'),//%2
common_local_url('shownotice',
array('notice' => $notice->id)),
$notice->content,
array('notice' => $notice->id)),//%3
$notice->content,//%4
$conversationUrl,//%5
common_local_url('newnotice',
array('replyto' => $sender->nickname)),
array('replyto' => $sender->nickname)),//%6
common_local_url('replies',
array('nickname' => $user->nickname)),
common_local_url('emailsettings'));
array('nickname' => $user->nickname)),//%7
common_local_url('emailsettings'));//%8
common_init_locale();
mail_to_user($user, $subject, $body);
@ -645,11 +657,12 @@ function mail_twitter_bridge_removed($user)
$subject = sprintf(_('Your Twitter bridge has been disabled.'));
$body = sprintf(_("Hi, %1\$s. We're sorry to inform you that your " .
'link to Twitter has been disabled. Your Twitter credentials ' .
'have either changed (did you recently change your Twitter ' .
'password?) or you have otherwise revoked our access to your ' .
"Twitter account.\n\n" .
$site_name = common_config('site', 'name');
$body = sprintf(_('Hi, %1$s. We\'re sorry to inform you that your ' .
'link to Twitter has been disabled. We no longer seem to have ' .
'permission to update your Twitter status. (Did you revoke ' .
'%3$s\'s access?)' . "\n\n" .
'You can re-enable your Twitter bridge by visiting your ' .
"Twitter settings page:\n\n\t%2\$s\n\n" .
"Regards,\n%3\$s\n"),

View File

@ -350,11 +350,10 @@ class NoticeListItem extends Widget
function showNoticeLink()
{
if($this->notice->is_local){
$noticeurl = common_local_url('shownotice',
array('notice' => $this->notice->id));
// XXX: we need to figure this out better. Is this right?
if (strcmp($this->notice->uri, $noticeurl) != 0 &&
preg_match('/^http/', $this->notice->uri)) {
}else{
$noticeurl = $this->notice->uri;
}
$this->out->elementStart('a', array('rel' => 'bookmark',

225
lib/oauthclient.php Normal file
View File

@ -0,0 +1,225 @@
<?php
/**
* Laconica, the distributed open-source microblogging tool
*
* Base class for doing OAuth calls as a consumer
*
* PHP version 5
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Action
* @package Laconica
* @author Zach Copley <zach@controlyourself.ca>
* @copyright 2008 Control Yourself, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
if (!defined('LACONICA')) {
exit(1);
}
require_once 'OAuth.php';
/**
* Exception wrapper for cURL errors
*
* @category Integration
* @package Laconica
* @author Zach Copley <zach@controlyourself.ca>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*
*/
class OAuthClientCurlException extends Exception
{
}
/**
* Base class for doing OAuth calls as a consumer
*
* @category Integration
* @package Laconica
* @author Zach Copley <zach@controlyourself.ca>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*
*/
class OAuthClient
{
var $consumer;
var $token;
/**
* Constructor
*
* Can be initialized with just consumer key and secret for requesting new
* tokens or with additional request token or access token
*
* @param string $consumer_key consumer key
* @param string $consumer_secret consumer secret
* @param string $oauth_token user's token
* @param string $oauth_token_secret user's secret
*
* @return nothing
*/
function __construct($consumer_key, $consumer_secret,
$oauth_token = null, $oauth_token_secret = null)
{
$this->sha1_method = new OAuthSignatureMethod_HMAC_SHA1();
$this->consumer = new OAuthConsumer($consumer_key, $consumer_secret);
$this->token = null;
if (isset($oauth_token) && isset($oauth_token_secret)) {
$this->token = new OAuthToken($oauth_token, $oauth_token_secret);
}
}
/**
* Gets a request token from the given url
*
* @param string $url OAuth endpoint for grabbing request tokens
*
* @return OAuthToken $token the request token
*/
function getRequestToken($url)
{
$response = $this->oAuthGet($url);
parse_str($response);
$token = new OAuthToken($oauth_token, $oauth_token_secret);
return $token;
}
/**
* Builds a link that can be redirected to in order to
* authorize a request token.
*
* @param string $url endpoint for authorizing request tokens
* @param OAuthToken $request_token the request token to be authorized
* @param string $oauth_callback optional callback url
*
* @return string $authorize_url the url to redirect to
*/
function getAuthorizeLink($url, $request_token, $oauth_callback = null)
{
$authorize_url = $url . '?oauth_token=' .
$request_token->key;
if (isset($oauth_callback)) {
$authorize_url .= '&oauth_callback=' . urlencode($oauth_callback);
}
return $authorize_url;
}
/**
* Fetches an access token
*
* @param string $url OAuth endpoint for exchanging authorized request tokens
* for access tokens
*
* @return OAuthToken $token the access token
*/
function getAccessToken($url)
{
$response = $this->oAuthPost($url);
parse_str($response);
$token = new OAuthToken($oauth_token, $oauth_token_secret);
return $token;
}
/**
* Use HTTP GET to make a signed OAuth request
*
* @param string $url OAuth endpoint
*
* @return mixed the request
*/
function oAuthGet($url)
{
$request = OAuthRequest::from_consumer_and_token($this->consumer,
$this->token, 'GET', $url, null);
$request->sign_request($this->sha1_method,
$this->consumer, $this->token);
return $this->httpRequest($request->to_url());
}
/**
* Use HTTP POST to make a signed OAuth request
*
* @param string $url OAuth endpoint
* @param array $params additional post parameters
*
* @return mixed the request
*/
function oAuthPost($url, $params = null)
{
$request = OAuthRequest::from_consumer_and_token($this->consumer,
$this->token, 'POST', $url, $params);
$request->sign_request($this->sha1_method,
$this->consumer, $this->token);
return $this->httpRequest($request->get_normalized_http_url(),
$request->to_postdata());
}
/**
* Make a HTTP request using cURL.
*
* @param string $url Where to make the
* @param array $params post parameters
*
* @return mixed the request
*/
function httpRequest($url, $params = null)
{
$options = array(
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FAILONERROR => true,
CURLOPT_HEADER => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_USERAGENT => 'Laconica',
CURLOPT_CONNECTTIMEOUT => 120,
CURLOPT_TIMEOUT => 120,
CURLOPT_HTTPAUTH => CURLAUTH_ANY,
CURLOPT_SSL_VERIFYPEER => false,
// Twitter is strict about accepting invalid "Expect" headers
CURLOPT_HTTPHEADER => array('Expect:')
);
if (isset($params)) {
$options[CURLOPT_POST] = true;
$options[CURLOPT_POSTFIELDS] = $params;
}
$ch = curl_init($url);
curl_setopt_array($ch, $options);
$response = curl_exec($ch);
if ($response === false) {
$msg = curl_error($ch);
$code = curl_errno($ch);
throw new OAuthClientCurlException($msg, $code);
}
curl_close($ch);
return $response;
}
}

229
lib/parallelizingdaemon.php Normal file
View File

@ -0,0 +1,229 @@
<?php
/**
* Laconica, the distributed open-source microblogging tool
*
* Base class for making daemons that can do several tasks in parallel.
*
* 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 Daemon
* @package Laconica
* @author Zach Copley <zach@controlyourself.ca>
* @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);
}
declare(ticks = 1);
/**
* Daemon able to spawn multiple child processes to do work in parallel
*
* @category Daemon
* @package Laconica
* @author Zach Copley <zach@controlyourself.ca>
* @author Evan Prodromou <evan@controlyourself.ca>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
class ParallelizingDaemon extends Daemon
{
private $_children = array();
private $_interval = 0; // seconds
private $_max_children = 0; // maximum number of children
private $_debug = false;
/**
* Constructor
*
* @param string $id the name/id of this daemon
* @param int $interval sleep this long before doing everything again
* @param int $max_children maximum number of child processes at a time
* @param boolean $debug debug output flag
*
* @return void
*
**/
function __construct($id = null, $interval = 60, $max_children = 2,
$debug = null)
{
parent::__construct(true); // daemonize
$this->_interval = $interval;
$this->_max_children = $max_children;
$this->_debug = $debug;
if (isset($id)) {
$this->set_id($id);
}
}
/**
* Run the daemon
*
* @return void
*/
function run()
{
if (isset($this->_debug)) {
echo $this->name() . " - Debugging output enabled.\n";
}
do {
$objects = $this->getObjects();
foreach ($objects as $o) {
// Fork a child for each object
$pid = pcntl_fork();
if ($pid == -1) {
die ($this->name() . ' - Couldn\'t fork!');
}
if ($pid) {
// Parent
if (isset($this->_debug)) {
echo $this->name() .
" - Forked new child - pid $pid.\n";
}
$this->_children[] = $pid;
} else {
// Child
// Do something with each object
$this->childTask($o);
exit();
}
// Remove child from ps list as it finishes
while (($c = pcntl_wait($status, WNOHANG OR WUNTRACED)) > 0) {
if (isset($this->_debug)) {
echo $this->name() . " - Child $c finished.\n";
}
$this->removePs($this->_children, $c);
}
// Wait! We have too many damn kids.
if (sizeof($this->_children) >= $this->_max_children) {
if (isset($this->_debug)) {
echo $this->name() . " - Too many children. Waiting...\n";
}
if (($c = pcntl_wait($status, WUNTRACED)) > 0) {
if (isset($this->_debug)) {
echo $this->name() .
" - Finished waiting for child $c.\n";
}
$this->removePs($this->_children, $c);
}
}
}
// Remove all children from the process list before restarting
while (($c = pcntl_wait($status, WUNTRACED)) > 0) {
if (isset($this->_debug)) {
echo $this->name() . " - Child $c finished.\n";
}
$this->removePs($this->_children, $c);
}
// Rest for a bit
if (isset($this->_debug)) {
echo $this->name() . ' - Waiting ' . $this->_interval .
" secs before running again.\n";
}
if ($this->_interval > 0) {
sleep($this->_interval);
}
} while (true);
}
/**
* Remove a child process from the list of children
*
* @param array &$plist array of processes
* @param int $ps process id
*
* @return void
*/
function removePs(&$plist, $ps)
{
for ($i = 0; $i < sizeof($plist); $i++) {
if ($plist[$i] == $ps) {
unset($plist[$i]);
$plist = array_values($plist);
break;
}
}
}
/**
* Get a list of objects to work on in parallel
*
* @return array An array of objects to work on
*/
function getObjects()
{
die('Implement ParallelizingDaemon::getObjects().');
}
/**
* Do something with each object in parallel
*
* @param mixed $object data to work on
*
* @return void
*/
function childTask($object)
{
die("Implement ParallelizingDaemon::childTask($object).");
}
}

View File

@ -97,7 +97,7 @@ class ProfileSection extends Section
$this->out->elementEnd('a');
$this->out->elementEnd('span');
$this->out->elementEnd('td');
if ($profile->value) {
if (isset($profile->value)) {
$this->out->element('td', 'value', $profile->value);
}

View File

@ -88,6 +88,10 @@ class Router
$m->connect('doc/:title', array('action' => 'doc'));
// Twitter
$m->connect('twitter/authorization', array('action' => 'twitterauthorization'));
// facebook
$m->connect('facebook', array('action' => 'facebookhome'));
@ -113,15 +117,8 @@ class Router
$m->connect('main/tagother/:id', array('action' => 'tagother'));
$m->connect('main/oembed.xml',
array('action' => 'api',
'method' => 'oembed.xml',
'apiaction' => 'oembed'));
$m->connect('main/oembed.json',
array('action' => 'api',
'method' => 'oembed.json',
'apiaction' => 'oembed'));
$m->connect('main/oembed',
array('action' => 'oembed'));
// these take a code
@ -409,6 +406,28 @@ class Router
'apiaction' => 'laconica'));
// Groups
//'list' has to be handled differently, as php will not allow a method to be named 'list'
$m->connect('api/laconica/groups/list/:argument',
array('action' => 'api',
'method' => 'list_groups',
'apiaction' => 'groups'));
foreach (array('xml', 'json', 'rss', 'atom') as $e) {
$m->connect('api/laconica/groups/list.' . $e,
array('action' => 'api',
'method' => 'list_groups.' . $e,
'apiaction' => 'groups'));
}
$m->connect('api/laconica/groups/:method',
array('action' => 'api',
'apiaction' => 'statuses'),
array('method' => '(list_all|)(\.(atom|rss|xml|json))?'));
$m->connect('api/statuses/:method/:argument',
array('action' => 'api',
'apiaction' => 'statuses'),
array('method' => '(|user_timeline|friends_timeline|replies|mentions|show|destroy|friends|followers)'));
$m->connect('api/laconica/groups/:method/:argument',
array('action' => 'api',
'apiaction' => 'groups'));

View File

@ -120,7 +120,7 @@ class MySQLSearch extends SearchEngine
} else if ('identica_notices' === $this->table) {
// Don't show imported notices
$this->target->whereAdd('notice.is_local != ' . NOTICE_GATEWAY);
$this->target->whereAdd('notice.is_local != ' . Notice::GATEWAY);
if (strtolower($q) != $q) {
$this->target->whereAdd("( MATCH(content) AGAINST ('" . addslashes($q) .

View File

@ -52,6 +52,7 @@ require_once INSTALLDIR.'/lib/error.php';
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://laconi.ca/
*/
class ServerErrorAction extends ErrorAction
{
function __construct($message='Error', $code=500)
@ -66,6 +67,10 @@ class ServerErrorAction extends ErrorAction
505 => 'HTTP Version Not Supported');
$this->default = 500;
// Server errors must be logged.
common_log(LOG_ERR, "ServerErrorAction: $code $message");
}
// XXX: Should these error actions even be invokable via URI?

View File

@ -17,83 +17,20 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
if (!defined('LACONICA')) { exit(1); }
if (!defined('LACONICA')) {
exit(1);
}
define('TWITTER_SERVICE', 1); // Twitter is foreign_service ID 1
function get_twitter_data($uri, $screen_name, $password)
{
$options = array(
CURLOPT_USERPWD => sprintf("%s:%s", $screen_name, $password),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FAILONERROR => true,
CURLOPT_HEADER => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_USERAGENT => "Laconica",
CURLOPT_CONNECTTIMEOUT => 120,
CURLOPT_TIMEOUT => 120,
# Twitter is strict about accepting invalid "Expect" headers
CURLOPT_HTTPHEADER => array('Expect:')
);
$ch = curl_init($uri);
curl_setopt_array($ch, $options);
$data = curl_exec($ch);
$errmsg = curl_error($ch);
if ($errmsg) {
common_debug("Twitter bridge - cURL error: $errmsg - trying to load: $uri with user $screen_name.",
__FILE__);
if (defined('SCRIPT_DEBUG')) {
print "cURL error: $errmsg - trying to load: $uri with user $screen_name.\n";
}
}
curl_close($ch);
return $data;
}
function twitter_json_data($uri, $screen_name, $password)
{
$json_data = get_twitter_data($uri, $screen_name, $password);
if (!$json_data) {
return false;
}
$data = json_decode($json_data);
if (!$data) {
return false;
}
return $data;
}
function twitter_user_info($screen_name, $password)
{
$uri = "http://twitter.com/users/show/$screen_name.json";
return twitter_json_data($uri, $screen_name, $password);
}
function twitter_friends_ids($screen_name, $password)
{
$uri = "http://twitter.com/friends/ids/$screen_name.json";
return twitter_json_data($uri, $screen_name, $password);
}
function update_twitter_user($twitter_id, $screen_name)
{
$uri = 'http://twitter.com/' . $screen_name;
$fuser = new Foreign_user();
$fuser->query('BEGIN');
// Dropping down to SQL because regular db_object udpate stuff doesn't seem
// Dropping down to SQL because regular DB_DataObject udpate stuff doesn't seem
// to work so good with tables that have multiple column primary keys
// Any time we update the uri for a forein user we have to make sure there
@ -102,35 +39,14 @@ function update_twitter_user($twitter_id, $screen_name)
$qry = 'UPDATE foreign_user set uri = \'\' WHERE uri = ';
$qry .= '\'' . $uri . '\'' . ' AND service = ' . TWITTER_SERVICE;
$result = $fuser->query($qry);
if ($result) {
common_debug("Removed uri ($uri) from another foreign_user who was squatting on it.");
if (defined('SCRIPT_DEBUG')) {
print("Removed uri ($uri) from another Twitter user who was squatting on it.\n");
}
}
$fuser->query($qry);
// Update the user
$qry = 'UPDATE foreign_user SET nickname = ';
$qry .= '\'' . $screen_name . '\'' . ', uri = \'' . $uri . '\' ';
$qry .= 'WHERE id = ' . $twitter_id . ' AND service = ' . TWITTER_SERVICE;
$result = $fuser->query($qry);
if (!$result) {
common_log(LOG_WARNING,
"Couldn't update foreign_user data for Twitter user: $screen_name");
common_log_db_error($fuser, 'UPDATE', __FILE__);
if (defined('SCRIPT_DEBUG')) {
print "UPDATE failed: for Twitter user: $twitter_id - $screen_name. - ";
print common_log_objstring($fuser) . "\n";
$error = &PEAR::getStaticProperty('DB_DataObject','lastError');
print "DB_DataObject Error: " . $error->getMessage() . "\n";
}
return false;
}
$fuser->query('COMMIT');
$fuser->free();
@ -147,23 +63,22 @@ function add_twitter_user($twitter_id, $screen_name)
// Clear out any bad old foreign_users with the new user's legit URL
// This can happen when users move around or fakester accounts get
// repoed, and things like that.
$luser = new Foreign_user();
$luser->uri = $new_uri;
$luser->service = TWITTER_SERVICE;
$result = $luser->delete();
if ($result) {
if (empty($result)) {
common_log(LOG_WARNING,
"Twitter bridge - removed invalid Twitter user squatting on uri: $new_uri");
if (defined('SCRIPT_DEBUG')) {
print "Removed invalid Twitter user squatting on uri: $new_uri\n";
}
}
$luser->free();
unset($luser);
// Otherwise, create a new Twitter user
$fuser = new Foreign_user();
$fuser->nickname = $screen_name;
@ -173,21 +88,12 @@ function add_twitter_user($twitter_id, $screen_name)
$fuser->created = common_sql_now();
$result = $fuser->insert();
if (!$result) {
if (empty($result)) {
common_log(LOG_WARNING,
"Twitter bridge - failed to add new Twitter user: $twitter_id - $screen_name.");
common_log_db_error($fuser, 'INSERT', __FILE__);
if (defined('SCRIPT_DEBUG')) {
print "INSERT failed: could not add new Twitter user: $twitter_id - $screen_name. - ";
print common_log_objstring($fuser) . "\n";
$error = &PEAR::getStaticProperty('DB_DataObject','lastError');
print "DB_DataObject Error: " . $error->getMessage() . "\n";
}
} else {
common_debug("Twitter bridge - Added new Twitter user: $screen_name ($twitter_id).");
if (defined('SCRIPT_DEBUG')) {
print "Added new Twitter user: $screen_name ($twitter_id).\n";
}
}
return $result;
@ -199,23 +105,20 @@ function save_twitter_user($twitter_id, $screen_name)
// Check to see whether the Twitter user is already in the system,
// and update its screen name and uri if so.
$fuser = Foreign_user::getForeignUser($twitter_id, TWITTER_SERVICE);
if ($fuser) {
if (!empty($fuser)) {
$result = true;
// Only update if Twitter screen name has changed
if ($fuser->nickname != $screen_name) {
$result = update_twitter_user($twitter_id, $screen_name);
common_debug('Twitter bridge - Updated nickname (and URI) for Twitter user ' .
"$fuser->id to $screen_name, was $fuser->nickname");
if (defined('SCRIPT_DEBUG')) {
print 'Updated nickname (and URI) for Twitter user ' .
"$fuser->id to $screen_name, was $fuser->nickname\n";
}
}
return $result;
@ -230,119 +133,6 @@ function save_twitter_user($twitter_id, $screen_name)
return true;
}
function retreive_twitter_friends($twitter_id, $screen_name, $password)
{
$friends = array();
$uri = "http://twitter.com/statuses/friends/$twitter_id.json?page=";
$friends_ids = twitter_friends_ids($screen_name, $password);
if (!$friends_ids) {
return $friends;
}
if (defined('SCRIPT_DEBUG')) {
print "Twitter 'social graph' ids method says $screen_name has " .
count($friends_ids) . " friends.\n";
}
// Calculate how many pages to get...
$pages = ceil(count($friends_ids) / 100);
if ($pages == 0) {
common_log(LOG_WARNING,
"Twitter bridge - $screen_name seems to have no friends.");
if (defined('SCRIPT_DEBUG')) {
print "$screen_name seems to have no friends.\n";
}
}
for ($i = 1; $i <= $pages; $i++) {
$data = get_twitter_data($uri . $i, $screen_name, $password);
if (!$data) {
common_log(LOG_WARNING,
"Twitter bridge - Couldn't retrieve page $i of $screen_name's friends.");
if (defined('SCRIPT_DEBUG')) {
print "Couldn't retrieve page $i of $screen_name's friends.\n";
}
continue;
}
$more_friends = json_decode($data);
if (!$more_friends) {
common_log(LOG_WARNING,
"Twitter bridge - No data for page $i of $screen_name's friends.");
if (defined('SCRIPT_DEBUG')) {
print "No data for page $i of $screen_name's friends.\n";
}
continue;
}
$friends = array_merge($friends, $more_friends);
}
return $friends;
}
function save_twitter_friends($user, $twitter_id, $screen_name, $password)
{
$friends = retreive_twitter_friends($twitter_id, $screen_name, $password);
if (empty($friends)) {
common_debug("Twitter bridge - Couldn't get friends data from Twitter for $screen_name.");
if (defined('SCRIPT_DEBUG')) {
print "Couldn't get friends data from Twitter for $screen_name.\n";
}
return false;
}
foreach ($friends as $friend) {
$friend_name = $friend->screen_name;
$friend_id = (int) $friend->id;
// Update or create the Foreign_user record
if (!save_twitter_user($friend_id, $friend_name)) {
common_log(LOG_WARNING,
"Twitter bridge - couldn't save $screen_name's friend, $friend_name.");
if (defined('SCRIPT_DEBUG')) {
print "Couldn't save $screen_name's friend, $friend_name.\n";
}
continue;
}
// Check to see if there's a related local user
$flink = Foreign_link::getByForeignID($friend_id, 1);
if ($flink) {
// Get associated user and subscribe her
$friend_user = User::staticGet('id', $flink->user_id);
if (!empty($friend_user)) {
$result = subs_subscribe_to($user, $friend_user);
if ($result === true) {
common_debug("Twitter bridge - subscribed $friend_user->nickname to $user->nickname.");
if (defined('SCRIPT_DEBUG')) {
print("Subscribed $friend_user->nickname to $user->nickname.\n");
}
} else {
if (defined('SCRIPT_DEBUG')) {
print "$result ($friend_user->nickname to $user->nickname)\n";
}
}
}
}
}
return true;
}
function is_twitter_bound($notice, $flink) {
// Check to see if notice should go to Twitter
@ -360,104 +150,73 @@ function is_twitter_bound($notice, $flink) {
function broadcast_twitter($notice)
{
$flink = Foreign_link::getByUserID($notice->profile_id,
TWITTER_SERVICE);
if (is_twitter_bound($notice, $flink)) {
$fuser = $flink->getForeignUser();
$twitter_user = $fuser->nickname;
$twitter_password = $flink->credentials;
$uri = 'http://www.twitter.com/statuses/update.json';
$user = $flink->getUser();
// XXX: Hack to get around PHP cURL's use of @ being a a meta character
$statustxt = preg_replace('/^@/', ' @', $notice->content);
$options = array(
CURLOPT_USERPWD => "$twitter_user:$twitter_password",
CURLOPT_POST => true,
CURLOPT_POSTFIELDS =>
array(
'status' => $statustxt,
'source' => common_config('integration', 'source')
),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FAILONERROR => true,
CURLOPT_HEADER => false,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_USERAGENT => "Laconica",
CURLOPT_CONNECTTIMEOUT => 120, // XXX: How long should this be?
CURLOPT_TIMEOUT => 120,
$token = TwitterOAuthClient::unpackToken($flink->credentials);
# Twitter is strict about accepting invalid "Expect" headers
CURLOPT_HTTPHEADER => array('Expect:')
);
$client = new TwitterOAuthClient($token->key, $token->secret);
$ch = curl_init($uri);
curl_setopt_array($ch, $options);
$data = curl_exec($ch);
$errmsg = curl_error($ch);
$errno = curl_errno($ch);
$status = null;
if (!empty($errmsg)) {
common_debug("cURL error ($errno): $errmsg - " .
"trying to send notice for $twitter_user.",
__FILE__);
try {
$status = $client->statusesUpdate($statustxt);
} catch (OAuthClientCurlException $e) {
$user = $flink->getUser();
if ($e->getMessage() == 'The requested URL returned error: 401') {
if ($errmsg == 'The requested URL returned error: 401') {
common_debug(sprintf('User %s (user id: %s) ' .
'has bad Twitter credentials!',
$user->nickname, $user->id));
$errmsg = sprintf('User %1$s (user id: %2$s) has an invalid ' .
'Twitter OAuth access token.',
$user->nickname, $user->id);
common_log(LOG_WARNING, $errmsg);
// Bad credentials we need to delete the foreign_link
// Bad auth token! We need to delete the foreign_link
// to Twitter and inform the user.
remove_twitter_link($flink);
return true;
} else {
// Some other error happened, so we should try to
// send again later
// Some other error happened, so we should probably
// try to send again later.
$errmsg = sprintf('cURL error trying to send notice to Twitter ' .
'for user %1$s (user id: %2$s) - ' .
'code: %3$s message: $4$s.',
$user->nickname, $user->id,
$e->getCode(), $e->getMessage());
common_log(LOG_WARNING, $errmsg);
return false;
}
}
curl_close($ch);
if (empty($data)) {
common_debug("No data returned by Twitter's " .
"API trying to send update for $twitter_user",
__FILE__);
// XXX: Not sure this represents a failure to send, but it
// probably does
return false;
} else {
// Twitter should return a status
$status = json_decode($data);
if (empty($status)) {
common_debug("Unexpected data returned by Twitter " .
" API trying to send update for $twitter_user",
__FILE__);
// XXX: Again, this could represent a failure posting
// This could represent a failure posting,
// or the Twitter API might just be behaving flakey.
// We're treating it as a failure to post.
$errmsg = sprint('No data returned by Twitter API when ' .
'trying to send update for %1$s (user id %2$s).',
$user->nickname, $user->id);
common_log(LOG_WARNING, $errmsg);
return false;
}
}
// Notice crossed the great divide
$msg = sprintf('Twitter bridge posted notice %s to Twitter.',
$notice->id);
common_log(LOG_INFO, $msg);
}
return true;
@ -480,6 +239,8 @@ function remove_twitter_link($flink)
// Notify the user that her Twitter bridge is down
if (isset($user->email)) {
$result = mail_twitter_bridge_removed($user);
if (!$result) {
@ -491,6 +252,7 @@ function remove_twitter_link($flink)
common_log(LOG_WARNING, $msg);
}
}
}

View File

@ -188,7 +188,10 @@ class TwitterapiAction extends Action
// Enclosures
$attachments = $notice->attachments();
$enclosures = array();
if (!empty($attachments)) {
$twitter_status['attachments'] = array();
foreach ($attachments as $attachment) {
if ($attachment->isEnclosure()) {
@ -196,12 +199,9 @@ class TwitterapiAction extends Action
$enclosure['url'] = $attachment->url;
$enclosure['mimetype'] = $attachment->mimetype;
$enclosure['size'] = $attachment->size;
$enclosures[] = $enclosure;
$twitter_status['attachments'][] = $enclosure;
}
}
if (!empty($enclosures)) {
$twitter_status['attachments'] = $enclosures;
}
if ($include_user) {
@ -233,6 +233,24 @@ class TwitterapiAction extends Action
return $twitter_group;
}
function twitter_rss_group_array($group)
{
$entry = array();
$entry['content']=$group->description;
$entry['title']=$group->nickname;
$entry['link']=$group->permalink();
$entry['published']=common_date_iso8601($group->created);
$entry['updated']==common_date_iso8601($group->modified);
$taguribase = common_config('integration', 'groupuri');
$entry['id'] = "group:$groupuribase:$entry[link]";
$entry['description'] = $entry['content'];
$entry['pubDate'] = common_date_rfc2822($group->created);
$entry['guid'] = $entry['link'];
return $entry;
}
function twitter_rss_entry_array($notice)
{
$profile = $notice->getProfile();
@ -644,6 +662,65 @@ class TwitterapiAction extends Action
}
function show_rss_groups($group, $title, $link, $subtitle)
{
$this->init_document('rss');
$this->elementStart('channel');
$this->element('title', null, $title);
$this->element('link', null, $link);
$this->element('description', null, $subtitle);
$this->element('language', null, 'en-us');
$this->element('ttl', null, '40');
if (is_array($group)) {
foreach ($group as $g) {
$twitter_group = $this->twitter_rss_group_array($g);
$this->show_twitter_rss_item($twitter_group);
}
} else {
while ($group->fetch()) {
$twitter_group = $this->twitter_rss_group_array($group);
$this->show_twitter_rss_item($twitter_group);
}
}
$this->elementEnd('channel');
$this->end_twitter_rss();
}
function show_atom_groups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
{
$this->init_document('atom');
$this->element('title', null, $title);
$this->element('id', null, $id);
$this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
if (!is_null($selfuri)) {
$this->element('link', array('href' => $selfuri,
'rel' => 'self', 'type' => 'application/atom+xml'), null);
}
$this->element('updated', null, common_date_iso8601('now'));
$this->element('subtitle', null, $subtitle);
if (is_array($group)) {
foreach ($group as $g) {
$this->raw($g->asAtomEntry());
}
} else {
while ($group->fetch()) {
$this->raw($group->asAtomEntry());
}
}
$this->end_document('atom');
}
function show_json_timeline($notice)
{
@ -668,6 +745,52 @@ class TwitterapiAction extends Action
$this->end_document('json');
}
function show_json_groups($group)
{
$this->init_document('json');
$groups = array();
if (is_array($group)) {
foreach ($group as $g) {
$twitter_group = $this->twitter_group_array($g);
array_push($groups, $twitter_group);
}
} else {
while ($group->fetch()) {
$twitter_group = $this->twitter_group_array($group);
array_push($groups, $twitter_group);
}
}
$this->show_json_objects($groups);
$this->end_document('json');
}
function show_xml_groups($group)
{
$this->init_document('xml');
$this->elementStart('groups', array('type' => 'array'));
if (is_array($group)) {
foreach ($group as $g) {
$twitter_group = $this->twitter_group_array($g);
$this->show_twitter_xml_group($twitter_group);
}
} else {
while ($group->fetch()) {
$twitter_group = $this->twitter_group_array($group);
$this->show_twitter_xml_group($twitter_group);
}
}
$this->elementEnd('groups');
$this->end_document('xml');
}
function show_single_json_group($group)
{
$this->init_document('json');
@ -844,9 +967,9 @@ class TwitterapiAction extends Action
$this->endXML();
}
function show_profile($profile, $content_type='xml', $notice=null)
function show_profile($profile, $content_type='xml', $notice=null, $includeStatuses=true)
{
$profile_array = $this->twitter_user_array($profile, true);
$profile_array = $this->twitter_user_array($profile, $includeStatuses);
switch ($content_type) {
case 'xml':
$this->show_twitter_xml_user($profile_array);

220
lib/twitteroauthclient.php Normal file
View File

@ -0,0 +1,220 @@
<?php
/**
* Laconica, the distributed open-source microblogging tool
*
* Class for doing OAuth calls against Twitter
*
* 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 Integration
* @package Laconica
* @author Zach Copley <zach@controlyourself.ca>
* @copyright 2008 Control Yourself, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
if (!defined('LACONICA')) {
exit(1);
}
/**
* Class for talking to the Twitter API with OAuth.
*
* @category Integration
* @package Laconica
* @author Zach Copley <zach@controlyourself.ca>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*
*/
class TwitterOAuthClient extends OAuthClient
{
public static $requestTokenURL = 'https://twitter.com/oauth/request_token';
public static $authorizeURL = 'https://twitter.com/oauth/authorize';
public static $accessTokenURL = 'https://twitter.com/oauth/access_token';
/**
* Constructor
*
* @param string $oauth_token the user's token
* @param string $oauth_token_secret the user's token secret
*
* @return nothing
*/
function __construct($oauth_token = null, $oauth_token_secret = null)
{
$consumer_key = common_config('twitter', 'consumer_key');
$consumer_secret = common_config('twitter', 'consumer_secret');
parent::__construct($consumer_key, $consumer_secret,
$oauth_token, $oauth_token_secret);
}
// XXX: the following two functions are to support the horrible hack
// of using the credentils field in Foreign_link to store both
// the access token and token secret. This hack should go away with
// 0.9, in which we can make DB changes and add a new column for the
// token itself.
static function packToken($token)
{
return implode(chr(0), array($token->key, $token->secret));
}
static function unpackToken($str)
{
$vals = explode(chr(0), $str);
return new OAuthToken($vals[0], $vals[1]);
}
/**
* Builds a link to Twitter's endpoint for authorizing a request token
*
* @param OAuthToken $request_token token to authorize
*
* @return the link
*/
function getAuthorizeLink($request_token)
{
return parent::getAuthorizeLink(self::$authorizeURL,
$request_token,
common_local_url('twitterauthorization'));
}
/**
* Calls Twitter's /account/verify_credentials API method
*
* @return mixed the Twitter user
*/
function verifyCredentials()
{
$url = 'https://twitter.com/account/verify_credentials.json';
$response = $this->oAuthGet($url);
$twitter_user = json_decode($response);
return $twitter_user;
}
/**
* Calls Twitter's /stutuses/update API method
*
* @param string $status text of the status
* @param int $in_reply_to_status_id optional id of the status it's
* a reply to
*
* @return mixed the status
*/
function statusesUpdate($status, $in_reply_to_status_id = null)
{
$url = 'https://twitter.com/statuses/update.json';
$params = array('status' => $status,
'in_reply_to_status_id' => $in_reply_to_status_id);
$response = $this->oAuthPost($url, $params);
$status = json_decode($response);
return $status;
}
/**
* Calls Twitter's /stutuses/friends_timeline API method
*
* @param int $since_id show statuses after this id
* @param int $max_id show statuses before this id
* @param int $cnt number of statuses to show
* @param int $page page number
*
* @return mixed an array of statuses
*/
function statusesFriendsTimeline($since_id = null, $max_id = null,
$cnt = null, $page = null)
{
$url = 'https://twitter.com/statuses/friends_timeline.json';
$params = array('since_id' => $since_id,
'max_id' => $max_id,
'count' => $cnt,
'page' => $page);
$qry = http_build_query($params);
if (!empty($qry)) {
$url .= "?$qry";
}
$response = $this->oAuthGet($url);
$statuses = json_decode($response);
return $statuses;
}
/**
* Calls Twitter's /stutuses/friends API method
*
* @param int $id id of the user whom you wish to see friends of
* @param int $user_id numerical user id
* @param int $screen_name screen name
* @param int $page page number
*
* @return mixed an array of twitter users and their latest status
*/
function statusesFriends($id = null, $user_id = null, $screen_name = null,
$page = null)
{
$url = "https://twitter.com/statuses/friends.json";
$params = array('id' => $id,
'user_id' => $user_id,
'screen_name' => $screen_name,
'page' => $page);
$qry = http_build_query($params);
if (!empty($qry)) {
$url .= "?$qry";
}
$response = $this->oAuthGet($url);
$friends = json_decode($response);
return $friends;
}
/**
* Calls Twitter's /stutuses/friends/ids API method
*
* @param int $id id of the user whom you wish to see friends of
* @param int $user_id numerical user id
* @param int $screen_name screen name
* @param int $page page number
*
* @return mixed a list of ids, 100 per page
*/
function friendsIds($id = null, $user_id = null, $screen_name = null,
$page = null)
{
$url = "https://twitter.com/friends/ids.json";
$params = array('id' => $id,
'user_id' => $user_id,
'screen_name' => $screen_name,
'page' => $page);
$qry = http_build_query($params);
if (!empty($qry)) {
$url .= "?$qry";
}
$response = $this->oAuthGet($url);
$ids = json_decode($response);
return $ids;
}
}

View File

@ -79,7 +79,7 @@ class UnQueueManager
function _isLocal($notice)
{
return ($notice->is_local == NOTICE_LOCAL_PUBLIC ||
$notice->is_local == NOTICE_LOCAL_NONPUBLIC);
return ($notice->is_local == Notice::LOCAL_PUBLIC ||
$notice->is_local == Notice::LOCAL_NONPUBLIC);
}
}

View File

@ -412,73 +412,43 @@ function common_render_text($text)
function common_replace_urls_callback($text, $callback, $notice_id = null) {
// Start off with a regex
$regex = '#'.
'(?:'.
'(?:^|\s+)('.
'(?:'.
'(?:https?|ftps?|mms|rtsp|gopher|news|nntp|telnet|wais|file|prospero|webcal|irc)://'.
'|'.
'(?:mailto|aim|tel|xmpp):'.
')?'.
'(?:'.
'(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)'. //IPv4
'|(?:'.
'(?:[0-9a-f]{1,4}:){1,1}(?::[0-9a-f]{1,4}){1,6}|'. //IPv6
'(?:[0-9a-f]{1,4}:){1,2}(?::[0-9a-f]{1,4}){1,5}|'.
'(?:[0-9a-f]{1,4}:){1,3}(?::[0-9a-f]{1,4}){1,4}|'.
'(?:[0-9a-f]{1,4}:){1,4}(?::[0-9a-f]{1,4}){1,3}|'.
'(?:[0-9a-f]{1,4}:){1,5}(?::[0-9a-f]{1,4}){1,2}|'.
'(?:[0-9a-f]{1,4}:){1,6}(?::[0-9a-f]{1,4}){1,1}|'.
'(?:(?:[0-9a-f]{1,4}:){1,7}|:):|'.
':(?::[0-9a-f]{1,4}){1,7}|'.
'(?:(?:(?:[0-9a-f]{1,4}:){6})(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})|'.
'(?:(?:[0-9a-f]{1,4}:){5}[0-9a-f]{1,4}:(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3})|'.
'(?:[0-9a-f]{1,4}:){5}:[0-9a-f]{1,4}:(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}|'.
'(?:[0-9a-f]{1,4}:){1,1}(?::[0-9a-f]{1,4}){1,4}:(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}|'.
'(?:[0-9a-f]{1,4}:){1,2}(?::[0-9a-f]{1,4}){1,3}:(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}|'.
'(?:[0-9a-f]{1,4}:){1,3}(?::[0-9a-f]{1,4}){1,2}:(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}|'.
'(?:[0-9a-f]{1,4}:){1,4}(?::[0-9a-f]{1,4}){1,1}:(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}|'.
'(?:(?:[0-9a-f]{1,4}:){1,5}|:):(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}|'.
':(?::[0-9a-f]{1,4}){1,5}:(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}'.
')|'.
'(?:[^.\s/:]+\.)+'. //DNS
'(?:museum|travel|onion|[a-z]{2,4})'.
')'.
'[^.\s]+\.[^\s]+'.
'|'.
'(?:[^.\s/:]+\.)+'.
'(?:museum|travel|[a-z]{2,4})'.
'(?:[:/][^\s]*)?'.
')'.
'#ix';
preg_match_all($regex, $text, $matches);
// Then clean up what the regex left behind
$offset = 0;
foreach($matches[0] as $orig_url) {
$url = htmlspecialchars_decode($orig_url);
// Make sure we didn't pick up an email address
if (preg_match('#^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$#i', $url)) continue;
// Remove surrounding punctuation
$url = trim($url, '.?!,;:\'"`([<');
// Remove surrounding parens and the like
preg_match('/[)\]>]+$/', $url, $trailing);
if (isset($trailing[0])) {
preg_match_all('/[(\[<]/', $url, $opened);
preg_match_all('/[)\]>]/', $url, $closed);
$unopened = count($closed[0]) - count($opened[0]);
// Make sure not to take off more closing parens than there are at the end
$unopened = ($unopened > mb_strlen($trailing[0])) ? mb_strlen($trailing[0]):$unopened;
$url = ($unopened > 0) ? mb_substr($url, 0, $unopened * -1):$url;
}
// Remove trailing punctuation again (in case there were some inside parens)
$url = rtrim($url, '.?!,;:\'"`');
// Make sure we didn't capture part of the next sentence
preg_match('#((?:[^.\s/]+\.)+)(museum|travel|[a-z]{2,4})#i', $url, $url_parts);
// Were the parts capitalized any?
$last_part = (mb_strtolower($url_parts[2]) !== $url_parts[2]) ? true:false;
$prev_part = (mb_strtolower($url_parts[1]) !== $url_parts[1]) ? true:false;
// If the first part wasn't cap'd but the last part was, we captured too much
if ((!$prev_part && $last_part)) {
$url = mb_substr($url, 0 , mb_strpos($url, '.'.$url_parts['2'], 0));
}
// Capture the new TLD
preg_match('#((?:[^.\s/]+\.)+)(museum|travel|[a-z]{2,4})#i', $url, $url_parts);
$tlds = array('ac', 'ad', 'ae', 'aero', 'af', 'ag', 'ai', 'al', 'am', 'an', 'ao', 'aq', 'ar', 'arpa', 'as', 'asia', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd', 'be', 'bf', 'bg', 'bh', 'bi', 'biz', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv', 'bw', 'by', 'bz', 'ca', 'cat', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm', 'cn', 'co', 'com', 'coop', 'cr', 'cu', 'cv', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'edu', 'ee', 'eg', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo', 'fr', 'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gov', 'gp', 'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu', 'id', 'ie', 'il', 'im', 'in', 'info', 'int', 'io', 'iq', 'ir', 'is', 'it', 'je', 'jm', 'jo', 'jobs', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mg', 'mh', 'mil', 'mk', 'ml', 'mm', 'mn', 'mo', 'mobi', 'mp', 'mq', 'mr', 'ms', 'mt', 'mu', 'museum', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'name', 'nc', 'ne', 'net', 'nf', 'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 'om', 'org', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl', 'pm', 'pn', 'pr', 'pro', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw', 'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn', 'so', 'sr', 'st', 'su', 'sv', 'sy', 'sz', 'tc', 'td', 'tel', 'tf', 'tg', 'th', 'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'tp', 'tr', 'travel', 'tt', 'tv', 'tw', 'tz', 'ua', 'ug', 'uk', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf', 'ws', 'ye', 'yt', 'yu', 'za', 'zm', 'zw');
if (!in_array($url_parts[2], $tlds)) continue;
// Make sure we didn't capture a hash tag
if (strpos($url, '#') === 0) continue;
// Put the url back the way we found it.
$url = (mb_strpos($orig_url, htmlspecialchars($url)) === FALSE) ? $url:htmlspecialchars($url);
foreach($matches[1] as $url) {
// Call user specified func
if (empty($notice_id)) {
$modified_url = call_user_func($callback, $url);
@ -895,8 +865,8 @@ function common_enqueue_notice($notice)
$transports[] = 'jabber';
}
if ($notice->is_local == NOTICE_LOCAL_PUBLIC ||
$notice->is_local == NOTICE_LOCAL_NONPUBLIC) {
if ($notice->is_local == Notice::LOCAL_PUBLIC ||
$notice->is_local == Notice::LOCAL_NONPUBLIC) {
$transports = array_merge($transports, $localTransports);
if ($xmpp) {
$transports[] = 'public';

2
lighttpd.conf.example Normal file
View File

@ -0,0 +1,2 @@
# Add this line to lighttpd.conf to enable pseudo-rewrites using 404s
server.error-handler-404 = "/index.php"

View File

@ -0,0 +1,38 @@
$(document).ready(function(){
$.getJSON($('address .url')[0].href+'/api/statuses/friends.json?user_id=' + current_user['id'] + '&lite=true&callback=?',
function(friends){
$('#notice_data-text').autocomplete(friends, {
multiple: true,
multipleSeparator: " ",
minChars: 1,
formatItem: function(row, i, max){
return '@' + row.screen_name + ' (' + row.name + ')';
},
formatMatch: function(row, i, max){
return '@' + row.screen_name;
},
formatResult: function(row){
return '@' + row.screen_name;
}
});
}
);
$.getJSON($('address .url')[0].href+'/api/laconica/groups/list.json?user_id=' + current_user['id'] + '&callback=?',
function(groups){
$('#notice_data-text').autocomplete(groups, {
multiple: true,
multipleSeparator: " ",
minChars: 1,
formatItem: function(row, i, max){
return '!' + row.nickname + ' (' + row.fullname + ')';
},
formatMatch: function(row, i, max){
return '!' + row.nickname;
},
formatResult: function(row){
return '!' + row.nickname;
}
});
}
);
});

View File

@ -0,0 +1,63 @@
<?php
/**
* Laconica, the distributed open-source microblogging tool
*
* Plugin to enable nickname completion in the enter status box
*
* 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 Craig Andrews <candrews@integralblue.com>
* @copyright 2009 Craig Andrews http://candrews.integralblue.com
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://laconi.ca/
*/
if (!defined('LACONICA')) {
exit(1);
}
class AutocompletePlugin extends Plugin
{
function __construct()
{
parent::__construct();
}
function onEndShowScripts($action){
if (common_logged_in()) {
$current_user = common_current_user();
$js_string = <<<EOT
<script type="text/javascript">
var current_user = { id: '$current_user->id' };
</script>
EOT;
$action->raw($js_string);
$action->script('plugins/Autocomplete/jquery-autocomplete/jquery.autocomplete.pack.js');
$action->script('plugins/Autocomplete/Autocomplete.js');
}
}
function onEndShowLaconicaStyles($action)
{
if (common_logged_in()) {
$action->cssLink('plugins/Autocomplete/jquery-autocomplete/jquery.autocomplete.css');
}
}
}
?>

View File

@ -0,0 +1,20 @@
1.0.2
-----
* Fixed missing semicolon
1.0.1
-----
* Fixed element creation (<ul> to <ul/> and <li> to </li>)
* Fixed ac_even class (was ac_event)
* Fixed bgiframe usage: now its really optional
* Removed the blur-on-return workaround, added a less obtrusive one only for Opera
* Fixed hold cursor keys: Opera needs keypress, everyone else keydown to scroll through result list when holding cursor key
* Updated package to jQuery 1.2.5, removing dimensions
* Fixed multiple-mustMatch: Remove only the last term when no match is found
* Fixed multiple without mustMatch: Don't select the last active when no match is found (on tab/return)
* Fixed multiple cursor position: Put cursor at end of input after selecting a value
1.0
---
* First release.

View File

@ -0,0 +1,48 @@
.ac_results {
padding: 0px;
border: 1px solid black;
background-color: white;
overflow: hidden;
z-index: 99999;
}
.ac_results ul {
width: 100%;
list-style-position: outside;
list-style: none;
padding: 0;
margin: 0;
}
.ac_results li {
margin: 0px;
padding: 2px 5px;
cursor: default;
display: block;
/*
if width will be 100% horizontal scrollbar will apear
when scroll mode will be used
*/
/*width: 100%;*/
font: menu;
font-size: 12px;
/*
it is very important, if line-height not setted or setted
in relative units scroll will be broken in firefox
*/
line-height: 16px;
overflow: hidden;
}
.ac_loading {
background: white url('indicator.gif') right center no-repeat;
}
.ac_odd {
background-color: #eee;
}
.ac_over {
background-color: #0A246A;
color: white;
}

View File

@ -0,0 +1,759 @@
/*
* Autocomplete - jQuery plugin 1.0.2
*
* Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
*
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*
* Revision: $Id: jquery.autocomplete.js 5747 2008-06-25 18:30:55Z joern.zaefferer $
*
*/
;(function($) {
$.fn.extend({
autocomplete: function(urlOrData, options) {
var isUrl = typeof urlOrData == "string";
options = $.extend({}, $.Autocompleter.defaults, {
url: isUrl ? urlOrData : null,
data: isUrl ? null : urlOrData,
delay: isUrl ? $.Autocompleter.defaults.delay : 10,
max: options && !options.scroll ? 10 : 150
}, options);
// if highlight is set to false, replace it with a do-nothing function
options.highlight = options.highlight || function(value) { return value; };
// if the formatMatch option is not specified, then use formatItem for backwards compatibility
options.formatMatch = options.formatMatch || options.formatItem;
return this.each(function() {
new $.Autocompleter(this, options);
});
},
result: function(handler) {
return this.bind("result", handler);
},
search: function(handler) {
return this.trigger("search", [handler]);
},
flushCache: function() {
return this.trigger("flushCache");
},
setOptions: function(options){
return this.trigger("setOptions", [options]);
},
unautocomplete: function() {
return this.trigger("unautocomplete");
}
});
$.Autocompleter = function(input, options) {
var KEY = {
UP: 38,
DOWN: 40,
DEL: 46,
TAB: 9,
RETURN: 13,
ESC: 27,
COMMA: 188,
PAGEUP: 33,
PAGEDOWN: 34,
BACKSPACE: 8
};
// Create $ object for input element
var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);
var timeout;
var previousValue = "";
var cache = $.Autocompleter.Cache(options);
var hasFocus = 0;
var lastKeyPressCode;
var config = {
mouseDownOnSelect: false
};
var select = $.Autocompleter.Select(options, input, selectCurrent, config);
var blockSubmit;
// prevent form submit in opera when selecting with return key
$.browser.opera && $(input.form).bind("submit.autocomplete", function() {
if (blockSubmit) {
blockSubmit = false;
return false;
}
});
// only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all
$input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) {
// track last key pressed
lastKeyPressCode = event.keyCode;
switch(event.keyCode) {
case KEY.UP:
event.preventDefault();
if ( select.visible() ) {
select.prev();
} else {
onChange(0, true);
}
break;
case KEY.DOWN:
event.preventDefault();
if ( select.visible() ) {
select.next();
} else {
onChange(0, true);
}
break;
case KEY.PAGEUP:
event.preventDefault();
if ( select.visible() ) {
select.pageUp();
} else {
onChange(0, true);
}
break;
case KEY.PAGEDOWN:
event.preventDefault();
if ( select.visible() ) {
select.pageDown();
} else {
onChange(0, true);
}
break;
// matches also semicolon
case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
case KEY.TAB:
case KEY.RETURN:
if( selectCurrent() ) {
// stop default to prevent a form submit, Opera needs special handling
event.preventDefault();
blockSubmit = true;
return false;
}
break;
case KEY.ESC:
select.hide();
break;
default:
clearTimeout(timeout);
timeout = setTimeout(onChange, options.delay);
break;
}
}).focus(function(){
// track whether the field has focus, we shouldn't process any
// results if the field no longer has focus
hasFocus++;
}).blur(function() {
hasFocus = 0;
if (!config.mouseDownOnSelect) {
hideResults();
}
}).click(function() {
// show select when clicking in a focused field
if ( hasFocus++ > 1 && !select.visible() ) {
onChange(0, true);
}
}).bind("search", function() {
// TODO why not just specifying both arguments?
var fn = (arguments.length > 1) ? arguments[1] : null;
function findValueCallback(q, data) {
var result;
if( data && data.length ) {
for (var i=0; i < data.length; i++) {
if( data[i].result.toLowerCase() == q.toLowerCase() ) {
result = data[i];
break;
}
}
}
if( typeof fn == "function" ) fn(result);
else $input.trigger("result", result && [result.data, result.value]);
}
$.each(trimWords($input.val()), function(i, value) {
request(value, findValueCallback, findValueCallback);
});
}).bind("flushCache", function() {
cache.flush();
}).bind("setOptions", function() {
$.extend(options, arguments[1]);
// if we've updated the data, repopulate
if ( "data" in arguments[1] )
cache.populate();
}).bind("unautocomplete", function() {
select.unbind();
$input.unbind();
$(input.form).unbind(".autocomplete");
});
function selectCurrent() {
var selected = select.selected();
if( !selected )
return false;
var v = selected.result;
previousValue = v;
if ( options.multiple ) {
var words = trimWords($input.val());
if ( words.length > 1 ) {
v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v;
}
v += options.multipleSeparator;
}
$input.val(v);
hideResultsNow();
$input.trigger("result", [selected.data, selected.value]);
return true;
}
function onChange(crap, skipPrevCheck) {
if( lastKeyPressCode == KEY.DEL ) {
select.hide();
return;
}
var currentValue = $input.val();
if ( !skipPrevCheck && currentValue == previousValue )
return;
previousValue = currentValue;
currentValue = lastWord(currentValue);
if ( currentValue.length >= options.minChars) {
$input.addClass(options.loadingClass);
if (!options.matchCase)
currentValue = currentValue.toLowerCase();
request(currentValue, receiveData, hideResultsNow);
} else {
stopLoading();
select.hide();
}
};
function trimWords(value) {
if ( !value ) {
return [""];
}
var words = value.split( options.multipleSeparator );
var result = [];
$.each(words, function(i, value) {
if ( $.trim(value) )
result[i] = $.trim(value);
});
return result;
}
function lastWord(value) {
if ( !options.multiple )
return value;
var words = trimWords(value);
return words[words.length - 1];
}
// fills in the input box w/the first match (assumed to be the best match)
// q: the term entered
// sValue: the first matching result
function autoFill(q, sValue){
// autofill in the complete box w/the first match as long as the user hasn't entered in more data
// if the last user key pressed was backspace, don't autofill
if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) {
// fill in the value (keep the case the user has typed)
$input.val($input.val() + sValue.substring(lastWord(previousValue).length));
// select the portion of the value not typed by the user (so the next character will erase)
$.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length);
}
};
function hideResults() {
clearTimeout(timeout);
timeout = setTimeout(hideResultsNow, 200);
};
function hideResultsNow() {
var wasVisible = select.visible();
select.hide();
clearTimeout(timeout);
stopLoading();
if (options.mustMatch) {
// call search and run callback
$input.search(
function (result){
// if no value found, clear the input box
if( !result ) {
if (options.multiple) {
var words = trimWords($input.val()).slice(0, -1);
$input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") );
}
else
$input.val( "" );
}
}
);
}
if (wasVisible)
// position cursor at end of input field
$.Autocompleter.Selection(input, input.value.length, input.value.length);
};
function receiveData(q, data) {
if ( data && data.length && hasFocus ) {
stopLoading();
select.display(data, q);
autoFill(q, data[0].value);
select.show();
} else {
hideResultsNow();
}
};
function request(term, success, failure) {
if (!options.matchCase)
term = term.toLowerCase();
var data = cache.load(term);
// recieve the cached data
if (data && data.length) {
success(term, data);
// if an AJAX url has been supplied, try loading the data now
} else if( (typeof options.url == "string") && (options.url.length > 0) ){
var extraParams = {
timestamp: +new Date()
};
$.each(options.extraParams, function(key, param) {
extraParams[key] = typeof param == "function" ? param() : param;
});
$.ajax({
// try to leverage ajaxQueue plugin to abort previous requests
mode: "abort",
// limit abortion to this input
port: "autocomplete" + input.name,
dataType: options.dataType,
url: options.url,
data: $.extend({
q: lastWord(term),
limit: options.max
}, extraParams),
success: function(data) {
var parsed = options.parse && options.parse(data) || parse(data);
cache.add(term, parsed);
success(term, parsed);
}
});
} else {
// if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match
select.emptyList();
failure(term);
}
};
function parse(data) {
var parsed = [];
var rows = data.split("\n");
for (var i=0; i < rows.length; i++) {
var row = $.trim(rows[i]);
if (row) {
row = row.split("|");
parsed[parsed.length] = {
data: row,
value: row[0],
result: options.formatResult && options.formatResult(row, row[0]) || row[0]
};
}
}
return parsed;
};
function stopLoading() {
$input.removeClass(options.loadingClass);
};
};
$.Autocompleter.defaults = {
inputClass: "ac_input",
resultsClass: "ac_results",
loadingClass: "ac_loading",
minChars: 1,
delay: 400,
matchCase: false,
matchSubset: true,
matchContains: false,
cacheLength: 10,
max: 100,
mustMatch: false,
extraParams: {},
selectFirst: true,
formatItem: function(row) { return row[0]; },
formatMatch: null,
autoFill: false,
width: 0,
multiple: false,
multipleSeparator: ", ",
highlight: function(value, term) {
return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
},
scroll: true,
scrollHeight: 180
};
$.Autocompleter.Cache = function(options) {
var data = {};
var length = 0;
function matchSubset(s, sub) {
if (!options.matchCase)
s = s.toLowerCase();
var i = s.indexOf(sub);
if (i == -1) return false;
return i == 0 || options.matchContains;
};
function add(q, value) {
if (length > options.cacheLength){
flush();
}
if (!data[q]){
length++;
}
data[q] = value;
}
function populate(){
if( !options.data ) return false;
// track the matches
var stMatchSets = {},
nullData = 0;
// no url was specified, we need to adjust the cache length to make sure it fits the local data store
if( !options.url ) options.cacheLength = 1;
// track all options for minChars = 0
stMatchSets[""] = [];
// loop through the array and create a lookup structure
for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
var rawValue = options.data[i];
// if rawValue is a string, make an array otherwise just reference the array
rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;
var value = options.formatMatch(rawValue, i+1, options.data.length);
if ( value === false )
continue;
var firstChar = value.charAt(0).toLowerCase();
// if no lookup array for this character exists, look it up now
if( !stMatchSets[firstChar] )
stMatchSets[firstChar] = [];
// if the match is a string
var row = {
value: value,
data: rawValue,
result: options.formatResult && options.formatResult(rawValue) || value
};
// push the current match into the set list
stMatchSets[firstChar].push(row);
// keep track of minChars zero items
if ( nullData++ < options.max ) {
stMatchSets[""].push(row);
}
};
// add the data items to the cache
$.each(stMatchSets, function(i, value) {
// increase the cache size
options.cacheLength++;
// add to the cache
add(i, value);
});
}
// populate any existing data
setTimeout(populate, 25);
function flush(){
data = {};
length = 0;
}
return {
flush: flush,
add: add,
populate: populate,
load: function(q) {
if (!options.cacheLength || !length)
return null;
/*
* if dealing w/local data and matchContains than we must make sure
* to loop through all the data collections looking for matches
*/
if( !options.url && options.matchContains ){
// track all matches
var csub = [];
// loop through all the data grids for matches
for( var k in data ){
// don't search through the stMatchSets[""] (minChars: 0) cache
// this prevents duplicates
if( k.length > 0 ){
var c = data[k];
$.each(c, function(i, x) {
// if we've got a match, add it to the array
if (matchSubset(x.value, q)) {
csub.push(x);
}
});
}
}
return csub;
} else
// if the exact item exists, use it
if (data[q]){
return data[q];
} else
if (options.matchSubset) {
for (var i = q.length - 1; i >= options.minChars; i--) {
var c = data[q.substr(0, i)];
if (c) {
var csub = [];
$.each(c, function(i, x) {
if (matchSubset(x.value, q)) {
csub[csub.length] = x;
}
});
return csub;
}
}
}
return null;
}
};
};
$.Autocompleter.Select = function (options, input, select, config) {
var CLASSES = {
ACTIVE: "ac_over"
};
var listItems,
active = -1,
data,
term = "",
needsInit = true,
element,
list;
// Create results
function init() {
if (!needsInit)
return;
element = $("<div/>")
.hide()
.addClass(options.resultsClass)
.css("position", "absolute")
.appendTo(document.body);
list = $("<ul/>").appendTo(element).mouseover( function(event) {
if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event));
$(target(event)).addClass(CLASSES.ACTIVE);
}
}).click(function(event) {
$(target(event)).addClass(CLASSES.ACTIVE);
select();
// TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus
input.focus();
return false;
}).mousedown(function() {
config.mouseDownOnSelect = true;
}).mouseup(function() {
config.mouseDownOnSelect = false;
});
if( options.width > 0 )
element.css("width", options.width);
needsInit = false;
}
function target(event) {
var element = event.target;
while(element && element.tagName != "LI")
element = element.parentNode;
// more fun with IE, sometimes event.target is empty, just ignore it then
if(!element)
return [];
return element;
}
function moveSelect(step) {
listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE);
movePosition(step);
var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
if(options.scroll) {
var offset = 0;
listItems.slice(0, active).each(function() {
offset += this.offsetHeight;
});
if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
} else if(offset < list.scrollTop()) {
list.scrollTop(offset);
}
}
};
function movePosition(step) {
active += step;
if (active < 0) {
active = listItems.size() - 1;
} else if (active >= listItems.size()) {
active = 0;
}
}
function limitNumberOfItems(available) {
return options.max && options.max < available
? options.max
: available;
}
function fillList() {
list.empty();
var max = limitNumberOfItems(data.length);
for (var i=0; i < max; i++) {
if (!data[i])
continue;
var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
if ( formatted === false )
continue;
var li = $("<li/>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0];
$.data(li, "ac_data", data[i]);
}
listItems = list.find("li");
if ( options.selectFirst ) {
listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
active = 0;
}
// apply bgiframe if available
if ( $.fn.bgiframe )
list.bgiframe();
}
return {
display: function(d, q) {
init();
data = d;
term = q;
fillList();
},
next: function() {
moveSelect(1);
},
prev: function() {
moveSelect(-1);
},
pageUp: function() {
if (active != 0 && active - 8 < 0) {
moveSelect( -active );
} else {
moveSelect(-8);
}
},
pageDown: function() {
if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
moveSelect( listItems.size() - 1 - active );
} else {
moveSelect(8);
}
},
hide: function() {
element && element.hide();
listItems && listItems.removeClass(CLASSES.ACTIVE);
active = -1;
},
visible : function() {
return element && element.is(":visible");
},
current: function() {
return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
},
show: function() {
var offset = $(input).offset();
element.css({
width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
top: offset.top + input.offsetHeight,
left: offset.left
}).show();
if(options.scroll) {
list.scrollTop(0);
list.css({
maxHeight: options.scrollHeight,
overflow: 'auto'
});
if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
var listHeight = 0;
listItems.each(function() {
listHeight += this.offsetHeight;
});
var scrollbarsVisible = listHeight > options.scrollHeight;
list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
if (!scrollbarsVisible) {
// IE doesn't recalculate width when scrollbar disappears
listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
}
}
}
},
selected: function() {
var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
return selected && selected.length && $.data(selected[0], "ac_data");
},
emptyList: function (){
list && list.empty();
},
unbind: function() {
element && element.remove();
}
};
};
$.Autocompleter.Selection = function(field, start, end) {
if( field.createTextRange ){
var selRange = field.createTextRange();
selRange.collapse(true);
selRange.moveStart("character", start);
selRange.moveEnd("character", end);
selRange.select();
} else if( field.setSelectionRange ){
field.setSelectionRange(start, end);
} else {
if( field.selectionStart ){
field.selectionStart = start;
field.selectionEnd = end;
}
}
field.focus();
};
})(jQuery);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,116 @@
/**
* Ajax Queue Plugin
*
* Homepage: http://jquery.com/plugins/project/ajaxqueue
* Documentation: http://docs.jquery.com/AjaxQueue
*/
/**
<script>
$(function(){
jQuery.ajaxQueue({
url: "test.php",
success: function(html){ jQuery("ul").append(html); }
});
jQuery.ajaxQueue({
url: "test.php",
success: function(html){ jQuery("ul").append(html); }
});
jQuery.ajaxSync({
url: "test.php",
success: function(html){ jQuery("ul").append("<b>"+html+"</b>"); }
});
jQuery.ajaxSync({
url: "test.php",
success: function(html){ jQuery("ul").append("<b>"+html+"</b>"); }
});
});
</script>
<ul style="position: absolute; top: 5px; right: 5px;"></ul>
*/
/*
* Queued Ajax requests.
* A new Ajax request won't be started until the previous queued
* request has finished.
*/
/*
* Synced Ajax requests.
* The Ajax request will happen as soon as you call this method, but
* the callbacks (success/error/complete) won't fire until all previous
* synced requests have been completed.
*/
(function($) {
var ajax = $.ajax;
var pendingRequests = {};
var synced = [];
var syncedData = [];
$.ajax = function(settings) {
// create settings for compatibility with ajaxSetup
settings = jQuery.extend(settings, jQuery.extend({}, jQuery.ajaxSettings, settings));
var port = settings.port;
switch(settings.mode) {
case "abort":
if ( pendingRequests[port] ) {
pendingRequests[port].abort();
}
return pendingRequests[port] = ajax.apply(this, arguments);
case "queue":
var _old = settings.complete;
settings.complete = function(){
if ( _old )
_old.apply( this, arguments );
jQuery([ajax]).dequeue("ajax" + port );;
};
jQuery([ ajax ]).queue("ajax" + port, function(){
ajax( settings );
});
return;
case "sync":
var pos = synced.length;
synced[ pos ] = {
error: settings.error,
success: settings.success,
complete: settings.complete,
done: false
};
syncedData[ pos ] = {
error: [],
success: [],
complete: []
};
settings.error = function(){ syncedData[ pos ].error = arguments; };
settings.success = function(){ syncedData[ pos ].success = arguments; };
settings.complete = function(){
syncedData[ pos ].complete = arguments;
synced[ pos ].done = true;
if ( pos == 0 || !synced[ pos-1 ] )
for ( var i = pos; i < synced.length && synced[i].done; i++ ) {
if ( synced[i].error ) synced[i].error.apply( jQuery, syncedData[i].error );
if ( synced[i].success ) synced[i].success.apply( jQuery, syncedData[i].success );
if ( synced[i].complete ) synced[i].complete.apply( jQuery, syncedData[i].complete );
synced[i] = null;
syncedData[i] = null;
}
};
}
return ajax.apply(this, arguments);
};
})(jQuery);

View File

@ -0,0 +1,10 @@
/* Copyright (c) 2006 Brandon Aaron (http://brandonaaron.net)
* Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
* and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
*
* $LastChangedDate: 2007-07-22 01:45:56 +0200 (Son, 22 Jul 2007) $
* $Rev: 2447 $
*
* Version 2.1.1
*/
(function($){$.fn.bgIframe=$.fn.bgiframe=function(s){if($.browser.msie&&/6.0/.test(navigator.userAgent)){s=$.extend({top:'auto',left:'auto',width:'auto',height:'auto',opacity:true,src:'javascript:false;'},s||{});var prop=function(n){return n&&n.constructor==Number?n+'px':n;},html='<iframe class="bgiframe"frameborder="0"tabindex="-1"src="'+s.src+'"'+'style="display:block;position:absolute;z-index:-1;'+(s.opacity!==false?'filter:Alpha(Opacity=\'0\');':'')+'top:'+(s.top=='auto'?'expression(((parseInt(this.parentNode.currentStyle.borderTopWidth)||0)*-1)+\'px\')':prop(s.top))+';'+'left:'+(s.left=='auto'?'expression(((parseInt(this.parentNode.currentStyle.borderLeftWidth)||0)*-1)+\'px\')':prop(s.left))+';'+'width:'+(s.width=='auto'?'expression(this.parentNode.offsetWidth+\'px\')':prop(s.width))+';'+'height:'+(s.height=='auto'?'expression(this.parentNode.offsetHeight+\'px\')':prop(s.height))+';'+'"/>';return this.each(function(){if($('> iframe.bgiframe',this).length==0)this.insertBefore(document.createElement(html),this.firstChild);});}return this;};})(jQuery);

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,163 @@
/* ----------------------------------------------------------------------------------------------------------------*/
/* ---------->>> global settings needed for thickbox <<<-----------------------------------------------------------*/
/* ----------------------------------------------------------------------------------------------------------------*/
*{padding: 0; margin: 0;}
/* ----------------------------------------------------------------------------------------------------------------*/
/* ---------->>> thickbox specific link and font settings <<<------------------------------------------------------*/
/* ----------------------------------------------------------------------------------------------------------------*/
#TB_window {
font: 12px Arial, Helvetica, sans-serif;
color: #333333;
}
#TB_secondLine {
font: 10px Arial, Helvetica, sans-serif;
color:#666666;
}
#TB_window a:link {color: #666666;}
#TB_window a:visited {color: #666666;}
#TB_window a:hover {color: #000;}
#TB_window a:active {color: #666666;}
#TB_window a:focus{color: #666666;}
/* ----------------------------------------------------------------------------------------------------------------*/
/* ---------->>> thickbox settings <<<-----------------------------------------------------------------------------*/
/* ----------------------------------------------------------------------------------------------------------------*/
#TB_overlay {
position: fixed;
z-index:100;
top: 0px;
left: 0px;
height:100%;
width:100%;
}
.TB_overlayMacFFBGHack {background: url(macFFBgHack.png) repeat;}
.TB_overlayBG {
background-color:#000;
filter:alpha(opacity=75);
-moz-opacity: 0.75;
opacity: 0.75;
}
* html #TB_overlay { /* ie6 hack */
position: absolute;
height: expression(document.body.scrollHeight > document.body.offsetHeight ? document.body.scrollHeight : document.body.offsetHeight + 'px');
}
#TB_window {
position: fixed;
background: #ffffff;
z-index: 102;
color:#000000;
display:none;
border: 4px solid #525252;
text-align:left;
top:50%;
left:50%;
}
* html #TB_window { /* ie6 hack */
position: absolute;
margin-top: expression(0 - parseInt(this.offsetHeight / 2) + (TBWindowMargin = document.documentElement && document.documentElement.scrollTop || document.body.scrollTop) + 'px');
}
#TB_window img#TB_Image {
display:block;
margin: 15px 0 0 15px;
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
border-top: 1px solid #666;
border-left: 1px solid #666;
}
#TB_caption{
height:25px;
padding:7px 30px 10px 25px;
float:left;
}
#TB_closeWindow{
height:25px;
padding:11px 25px 10px 0;
float:right;
}
#TB_closeAjaxWindow{
padding:7px 10px 5px 0;
margin-bottom:1px;
text-align:right;
float:right;
}
#TB_ajaxWindowTitle{
float:left;
padding:7px 0 5px 10px;
margin-bottom:1px;
}
#TB_title{
background-color:#e8e8e8;
height:27px;
}
#TB_ajaxContent{
clear:both;
padding:2px 15px 15px 15px;
overflow:auto;
text-align:left;
line-height:1.4em;
}
#TB_ajaxContent.TB_modal{
padding:15px;
}
#TB_ajaxContent p{
padding:5px 0px 5px 0px;
}
#TB_load{
position: fixed;
display:none;
height:13px;
width:208px;
z-index:103;
top: 50%;
left: 50%;
margin: -6px 0 0 -104px; /* -height/2 0 0 -width/2 */
}
* html #TB_load { /* ie6 hack */
position: absolute;
margin-top: expression(0 - parseInt(this.offsetHeight / 2) + (TBWindowMargin = document.documentElement && document.documentElement.scrollTop || document.body.scrollTop) + 'px');
}
#TB_HideSelect{
z-index:99;
position:fixed;
top: 0;
left: 0;
background-color:#fff;
border:none;
filter:alpha(opacity=0);
-moz-opacity: 0;
opacity: 0;
height:100%;
width:100%;
}
* html #TB_HideSelect { /* ie6 hack */
position: absolute;
height: expression(document.body.scrollHeight > document.body.offsetHeight ? document.body.scrollHeight : document.body.offsetHeight + 'px');
}
#TB_iframeContent{
clear:both;
border:none;
margin-bottom:-1px;
margin-top:1px;
_margin-bottom:1px;
}

View File

@ -0,0 +1,166 @@
TODO
- test formatItem implementation that returns (clickable) anchors
- bug: handle del key; eg. type a letter, remove it using del, type same letter again: nothing happens
- handle up/down keys in textarea (prevent default while select is open?)
- docs: max:0 works, too, "removing" it(??)
- fix ac_loading/options.loadingClass
- support/enable request urls like foo/bar/10 instead of foo/q=10
- urlencode request term before passing to $.ajax/data; evaluate why $.ajax doesn't handle that itself, if at all; try with umlauts, russian/danish/chinese characeters (see validate)
- test what happens when an element gets focused programmatically (maybe even before then autcomplete is applied)
- check if blur on selecting can be removed
- fix keyhandling to ignore metakeys, eg. shift; especially important for chinese characters that need more then one key
- enhance mustMatch: provide event/callback when a value gets deleted
- handle tab key different then enter, eg. don't blur field or prevent default, just let it move on; in any case, no need to blur the field when selecting a value via tab, unlike return
- prevent redundant requests on
- superstring returned no result, no need to query again for substring, eg. pete returned nothing, peter won't either
- previous query mustn't be requested again, eg. pete returns 10 lines, peter nothing, backspace to pete should get the 10 lines from cache (may need TimeToLive setting for cache to invalidate it)
- incorporate improvements and suggestions by Hector: http://beta.winserver.com/public/test/MultiSuggestTest.wct
- json support: An optional JSON format, that assumes a certain JSON format as default and just looks for a dataType "json" to be activated; [records], where each record is { id:String, label:String, moreOptionalValues... }
- accept callback as first argument to let users implement their own dynamic data (no caching) - consider async API
- allow users to keep their incomplete value when pressing tab, just mimic the default-browser-autocomplete: tab doesn't select any proposed value -> tab closes the select and works normal otherwise
- small bug in your autocomplete, When setting autoFill:true I would expect formatResult to be called on autofill, it seems not to be the case.
- add a callback to allow decoding the response
- allow modification of not-last value in multiple-fields
@option Number size Limit the number of items to show at once. Default:
@option Function parse - TEST AND DOCUMENT ME
- add option to display selectbox on focus
$input.bind("show", function() {
if ( !select.visible() ) {
onChange(0, true);
}
});
- reference: http://capxous.com/
- add "try ..." hints to demo
- check out demos
- reference: http://createwebapp.com/demo/
- add option to hide selectbox when no match is found - see comment by Ian on plugin page (14. Juli 2007 04:31)
- add example for reinitializing an autocomplete using unbind()
- Add option to pass through additional arguments to $.ajax, like type to use POST instead of GET
- I found out that the problem with UTF-8 not being correctly sent can be solved on the server side by applying (PHP) rawurldecode() function, which decodes the Unicode characters sent by GET method and therefore URL-encoded.
-> add that hint to docs and examples
But I am trying this with these three values: “foo bar”, “foo foo”, and “foo far”, and if I enter “b” (or “ba”) nothing matches, if I enter “f” all three do match, and if I enter “fa” the last one matches.
The problem seems to be that the cache is implemented with a first-character hashtable, so only after matching the first character, the latter ones are searched for.
xml example:
<script type="text/javascript">
function parseXML(data) {
var results = [];
var branches = $(data).find('item');
$(branches).each(function() {
var text = $.trim($(this).find('text').text());
var value = $.trim($(this).find('value').text());
//console.log(text);
//console.log(value);
results[results.length] = {'data': this, 'result': value, 'value': text};
});
$(results).each(function() {
//console.log('value', this.value);
//console.log('text', this.text);
});
//console.log(results);
return results;
};
$(YourOojHere).autocomplete(SERVER_AJAX_URL, {parse: parseXML});
</script>
<?xml version="1.0"?>
<ajaxresponse>
<item>
<text>
<![CDATA[<b>FreeNode:</b> irc.freenode.net:6667]]>
</text>
<value><![CDATA[irc.freenode.net:6667]]></value>
</item><item>
<text>
<![CDATA[<b>irc.oftc.net</b>:6667]]>
</text>
<value><![CDATA[irc.oftc.net:6667]]></value>
</item><item>
<text>
<![CDATA[<b>irc.undernet.org</b>:6667]]>
</text>
<value><![CDATA[irc.undernet.org:6667]]></value>
</item>
</ajaxresponse>
Hi all,
I use Autocomplete 1.0 Alpha mostly for form inputs bound to foreign
key columns. For instance I have a user_position table with two
columns: user_id and position_id. On new appointment form I have two
autocomplete text inputs with the following code:
<input type="text" id="user_id" class="ac_input" tabindex="1" />
<input type="text" id="position_id" class="ac_input" tabindex="2" />
As you can see the inputs do not have a name attribute, and when the
form is submitted their values are not sent, which is all right since
they will contain strings like:
'John Doe'
'Sales Manager'
whereas our backend expects something like:
23
14
which are the user_id for John Doe and position_id for Sales Manager.
To send these values I have two hidden inputs in the form like this:
<input type="hidden" name="user_id" value="">
<input type="hidden" name="position_id" value="">
Also I have the following code in the $().ready function:
$("#user_id").result(function(event, data, formatted) {
$("input[@name=user_id]").val(data[1]);
});
$("#position_id").result(function(event, data, formatted) {
$("input[@name=position_id]").val(data[1]);
});
As could be seen these functions stuff user_id and position_id values
(in our example 23 and 14) into the hidden inputs, and when the form
is submitted these values are sent:
user_id = 23
position_id = 14
The backend script then takes care of adding a record to our
user_position table containing those values.
I wonder how could the plugin code be modified to simplify the setup
by taking care of adding hidden inputs and updating the value of
hidden inputs as default behavior. I have successfully attempted a
simpler solution - writing a wrapper to perform these additional tasks
and invoke autocomplete as well. I hope my intention is clear enough,
if not, this is exactly the expected outcome:
Before:
<script type="text/javascript"
src="jquery.autocomplete-modified.js"></script>
<input type="text" name="user_id" class="ac_input" tabindex="1" />
After:
<input type="text" id="user_id" class="ac_input" tabindex="1" />
<input type="hidden" name="user_id" value="23">
Last word, I know this looks like a tall order, and I do not hope
someone will make a complete working mod for me, but rather would very
much appreciate helpful advise and directions.
Many thanks in advance
Majid

View File

@ -0,0 +1,6 @@
Autocomplete allows users to autocomplete screen names in @ replies. When an "@" is typed into the notice text area, an autocomplete box is displayed populated with the user's friends' screen names.
Installation
============
Add "addPlugin('Autocomplete');" to the bottom of your config.php
That's it!

View File

@ -47,9 +47,7 @@ class FBC_XDReceiverAction extends Action
header('Expires:');
header('Pragma:');
$this->startXML('html',
'-//W3C//DTD XHTML 1.0 Strict//EN',
'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd');
$this->startXML('html');
$language = $this->getLanguage();
@ -58,10 +56,7 @@ class FBC_XDReceiverAction extends Action
'lang' => $language));
$this->elementStart('head');
$this->element('title', null, 'cross domain receiver page');
$this->element('script',
array('src' =>
'http://static.ak.connect.facebook.com/js/api_lib/v0.4/XdCommReceiver.debug.js',
'type' => 'text/javascript'), '');
$this->script('http://static.ak.connect.facebook.com/js/api_lib/v0.4/XdCommReceiver.debug.js');
$this->elementEnd('head');
$this->elementStart('body');
$this->elementEnd('body');

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