Merge branch '0.8.x' into testing

Conflicts:
	actions/twitterauthorization.php
	lib/oauthclient.php
	lib/twitter.php
	lib/twitterapi.php
	lib/twitteroauthclient.php
	scripts/twitterstatusfetcher.php
This commit is contained in:
Evan Prodromou 2009-08-24 16:55:49 -04:00
commit ff87732053
139 changed files with 9612 additions and 1317 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 config section below for how to configure to use STOMP. As of this
writing, the software has been tested with ActiveMQ ( 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 * OAuth
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.
Optionally, there is a script (./scripts/synctwitterfriends.php), meant As of 0.8.1, OAuth is used to to access protected resources on Twitter
to be run periodically from a job scheduler (e.g.: cron under Unix), to instead of HTTP Basic Auth. To use Twitter bridging you will need
look for new additions to users' friends lists. Note that the friends to register your instance of Laconica as an application on Twitter
syncing only subscribes users to each other, it does not unsubscribe (http://twitter.com/apps), and update the following variables in your
users when they stop following each other on Twitter. 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 When registering your application with Twitter set the type to "Browser"
0,30 * * * * /path/to/php /path/to/laconica/scripts/synctwitterfriends.php>&/dev/null 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 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'. the service, *then* set this variable to 'true'.
inviteonly: If set to 'true', will only allow registration if the user inviteonly: If set to 'true', will only allow registration if the user
was invited by an existing 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 private: If set to 'true', anonymous users will be redirected to the
'login' page. Also, API methods that normally require no 'login' page. Also, API methods that normally require no
authentication will require it. Note that this does not turn authentication will require it. Note that this does not turn
@ -1167,6 +1197,14 @@ For configuring invites.
enabled: Whether to allow users to send invites. Default true. 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 tag
--- ---
@ -1228,6 +1266,30 @@ enabled: Set to true to enable. Default false.
server: a string with the hostname of the sphinx server. server: a string with the hostname of the sphinx server.
port: an integer with the port number 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 integration
----------- -----------

View File

@ -25,11 +25,31 @@ require_once INSTALLDIR.'/lib/feedlist.php';
class AllAction extends ProfileAction class AllAction extends ProfileAction
{ {
var $notice;
function isReadOnly($args) function isReadOnly($args)
{ {
return true; 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) function handle($args)
{ {
parent::handle($args); parent::handle($args);
@ -88,7 +108,9 @@ class AllAction extends ProfileAction
} }
} }
else { 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'); $this->elementStart('div', 'guide');
@ -98,15 +120,7 @@ class AllAction extends ProfileAction
function showContent() function showContent()
{ {
$cur = common_current_user(); $nl = new NoticeList($this->notice, $this);
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);
$cnt = $nl->show(); $cnt = $nl->show();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -122,7 +122,7 @@ class EmailsettingsAction extends AccountSettingsAction
} }
$this->elementEnd('fieldset'); $this->elementEnd('fieldset');
if ($user->email) { if (common_config('emailpost', 'enabled') && $user->email) {
$this->elementStart('fieldset', array('id' => 'settings_email_incoming')); $this->elementStart('fieldset', array('id' => 'settings_email_incoming'));
$this->element('legend',_('Incoming email')); $this->element('legend',_('Incoming email'));
if ($user->incomingemail) { if ($user->incomingemail) {
@ -173,11 +173,13 @@ class EmailsettingsAction extends AccountSettingsAction
_('Allow friends to nudge me and send me an email.'), _('Allow friends to nudge me and send me an email.'),
$user->emailnotifynudge); $user->emailnotifynudge);
$this->elementEnd('li'); $this->elementEnd('li');
if (common_config('emailpost', 'enabled')) {
$this->elementStart('li'); $this->elementStart('li');
$this->checkbox('emailpost', $this->checkbox('emailpost',
_('I want to post notices by email.'), _('I want to post notices by email.'),
$user->emailpost); $user->emailpost);
$this->elementEnd('li'); $this->elementEnd('li');
}
$this->elementStart('li'); $this->elementStart('li');
$this->checkbox('emailmicroid', $this->checkbox('emailmicroid',
_('Publish a MicroID for my email address.'), _('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.'); $message .= _('Be the first to add a notice to your favorites by clicking the fave button next to any notice you like.');
} }
else { 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'); $this->elementStart('div', 'guide');

View File

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

View File

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

View File

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

View File

@ -132,9 +132,10 @@ class groupRssAction extends Rss10Action
$c = array('url' => common_local_url('grouprss', $c = array('url' => common_local_url('grouprss',
array('nickname' => array('nickname' =>
$group->nickname)), $group->nickname)),
'title' => $group->nickname, 'title' => sprintf(_('%s timeline'), $group->nickname),
'link' => common_local_url('showgroup', array('nickname' => $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; 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.'); $message = _('If you can\'t find the group you\'re looking for, you can [create it](%%action.newgroup%%) yourself.');
} }
else { 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->elementStart('div', 'guide');
$this->raw(common_markup_to_html($message)); $this->raw(common_markup_to_html($message));

View File

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

View File

@ -235,7 +235,7 @@ class InviteAction extends CurrentUserDesignAction
common_root_url(), common_root_url(),
$personal, $personal,
common_local_url('showstream', array('nickname' => $user->nickname)), 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); 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. * 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 * @param array $args $_REQUEST data
* *
* @return void * @return void
@ -73,7 +75,9 @@ class LoginAction extends Action
function handle($args) function handle($args)
{ {
parent::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.')); $this->clientError(_('Already logged in.'));
} else if ($_SERVER['REQUEST_METHOD'] == 'POST') { } else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$this->checkLogin(); $this->checkLogin();
@ -247,11 +251,15 @@ class LoginAction extends Action
return _('For security reasons, please re-enter your ' . return _('For security reasons, please re-enter your ' .
'user name and password ' . 'user name and password ' .
'before changing your settings.'); 'before changing your settings.');
} else { } else if (common_config('openid', 'enabled')) {
return _('Login with your username and password. ' . return _('Login with your username and password. ' .
'Don\'t have a username yet? ' . 'Don\'t have a username yet? ' .
'[Register](%%action.register%%) a new account, or ' . '[Register](%%action.register%%) a new account, or ' .
'try [OpenID](%%action.openidlogin%%). '); '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)); $message = sprintf(_('Be the first to [post on this topic](%%%%action.newnotice%%%%?status_textarea=%s)!'), urlencode($q));
} }
else { 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'); $this->elementStart('div', 'guide');

View File

@ -86,9 +86,10 @@ class NoticesearchrssAction extends Rss10Action
{ {
$q = $this->trimmed('q'); $q = $this->trimmed('q');
$c = array('url' => common_local_url('noticesearchrss', array('q' => $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)), '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; return $c;
} }

View File

@ -31,8 +31,6 @@ if (!defined('LACONICA')) {
exit(1); exit(1);
} }
require_once INSTALLDIR.'/lib/twitterapi.php';
/** /**
* Oembed provider implementation * Oembed provider implementation
* *
@ -46,17 +44,13 @@ require_once INSTALLDIR.'/lib/twitterapi.php';
* @link http://laconi.ca/ * @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"); common_debug("in oembed api action");
$this->auth_user = $apidata['user'];
$url = $args['url']; $url = $args['url'];
if( substr(strtolower($url),0,strlen(common_root_url())) == strtolower(common_root_url()) ){ if( substr(strtolower($url),0,strlen(common_root_url())) == strtolower(common_root_url()) ){
$path = substr($url,strlen(common_root_url())); $path = substr($url,strlen(common_root_url()));
@ -131,8 +125,7 @@ class TwitapioembedAction extends TwitterapiAction
default: default:
$this->serverError(_("$path not supported for oembed requests"), 501); $this->serverError(_("$path not supported for oembed requests"), 501);
} }
switch($args['format']){
switch($apidata['content-type']){
case 'xml': case 'xml':
$this->init_document('xml'); $this->init_document('xml');
$this->elementStart('oembed'); $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_width']) $this->element('thumbnail_width',null,$oembed['thumbnail_width']);
if($oembed['thumbnail_height']) $this->element('thumbnail_height',null,$oembed['thumbnail_height']); if($oembed['thumbnail_height']) $this->element('thumbnail_height',null,$oembed['thumbnail_height']);
$this->elementEnd('oembed'); $this->elementEnd('oembed');
$this->end_document('xml'); $this->end_document('xml');
break; break;
case 'json': case 'json': case '':
$this->init_document('json'); $this->init_document('json');
print(json_encode($oembed)); print(json_encode($oembed));
$this->end_document('json'); $this->end_document('json');
@ -164,10 +156,51 @@ class TwitapioembedAction extends TwitterapiAction
default: default:
$this->serverError(_('content type ' . $apidata['content-type'] . ' not supported'), 501); $this->serverError(_('content type ' . $apidata['content-type'] . ' not supported'), 501);
} }
}else{ }else{
$this->serverError(_('Only ' . common_root_url() . ' urls over plain http please'), 404); $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) function handle($args)
{ {
parent::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.')); $this->clientError(_('Already logged in.'));
} else if ($_SERVER['REQUEST_METHOD'] == 'POST') { } else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$openid_url = $this->trimmed('openid_url'); $openid_url = $this->trimmed('openid_url');

View File

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

View File

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

View File

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

View File

@ -59,6 +59,7 @@ class PublicAction extends Action
*/ */
var $page = null; var $page = null;
var $notice;
function isReadOnly($args) function isReadOnly($args)
{ {
@ -84,6 +85,18 @@ class PublicAction extends Action
common_set_returnto($this->selfUrl()); 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; return true;
} }
@ -183,7 +196,8 @@ class PublicAction extends Action
} }
else { else {
if (! (common_config('site','closed') || common_config('site','inviteonly'))) { 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() function showContent()
{ {
$notice = Notice::publicStream(($this->page-1)*NOTICES_PER_PAGE, $nl = new NoticeList($this->notice, $this);
NOTICES_PER_PAGE + 1);
if (!$notice) {
$this->serverError(_('Could not retrieve public stream.'));
return;
}
$nl = new NoticeList($notice, $this);
$cnt = $nl->show(); $cnt = $nl->show();
@ -238,9 +244,11 @@ class PublicAction extends Action
function showAnonymousMessage() function showAnonymousMessage()
{ {
if (! (common_config('site','closed') || common_config('site','inviteonly'))) { 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. ' . '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 { } else {
$m = _('This is %%site.name%%, a [micro-blogging](http://en.wikipedia.org/wiki/Micro-blogging) service ' . $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.'); 'based on the Free Software [Laconica](http://laconi.ca/) tool.');

View File

@ -86,9 +86,9 @@ class PublicrssAction extends Rss10Action
{ {
$c = array( $c = array(
'url' => common_local_url('publicrss') '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') , '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; return $c;
} }

View File

@ -72,7 +72,8 @@ class PublictagcloudAction extends Action
$message .= _('Be the first to post one!'); $message .= _('Be the first to post one!');
} }
else { 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'); $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 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 * @param array $args $_REQUEST data
* *
* @return void * @return void
@ -127,6 +129,8 @@ class RegisterAction extends Action
if (common_config('site', 'closed')) { if (common_config('site', 'closed')) {
$this->clientError(_('Registration not allowed.')); $this->clientError(_('Registration not allowed.'));
} else if (common_config('site', 'openidonly')) {
common_redirect(common_local_url('openidlogin'));
} else if (common_logged_in()) { } else if (common_logged_in()) {
$this->clientError(_('Already logged in.')); $this->clientError(_('Already logged in.'));
} else if ($_SERVER['REQUEST_METHOD'] == 'POST') { } else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
@ -325,6 +329,7 @@ class RegisterAction extends Action
} else if ($this->error) { } else if ($this->error) {
$this->element('p', 'error', $this->error); $this->element('p', 'error', $this->error);
} else { } else {
if (common_config('openid', 'enabled')) {
$instr = $instr =
common_markup_to_html(_('With this form you can create '. common_markup_to_html(_('With this form you can create '.
' a new account. ' . ' a new account. ' .
@ -333,6 +338,13 @@ class RegisterAction extends Action
'(Have an [OpenID](http://openid.net/)? ' . '(Have an [OpenID](http://openid.net/)? ' .
'Try our [OpenID registration]'. 'Try our [OpenID registration]'.
'(%%action.openidlogin%%)!)')); '(%%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->elementStart('div', 'instructions');
$this->raw($instr); $this->raw($instr);

View File

@ -71,11 +71,13 @@ class RemotesubscribeAction extends Action
if ($this->err) { if ($this->err) {
$this->element('div', 'error', $this->err); $this->element('div', 'error', $this->err);
} else { } else {
$inst = _('To subscribe, you can [login](%%action.login%%),' . $inst = sprintf(_('To subscribe, you can [login](%%%%action.%s%%%%),' .
' or [register](%%action.register%%) a new ' . ' or [register](%%%%action.%s%%%%) a new ' .
' account. If you already have an account ' . ' account. If you already have an account ' .
' on a [compatible microblogging site](%%doc.openmublog%%), ' . ' 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); $output = common_markup_to_html($inst);
$this->elementStart('div', 'instructions'); $this->elementStart('div', 'instructions');
$this->raw($output); $this->raw($output);

View File

@ -48,6 +48,7 @@ require_once INSTALLDIR.'/lib/feedlist.php';
class RepliesAction extends OwnerDesignAction class RepliesAction extends OwnerDesignAction
{ {
var $page = null; var $page = null;
var $notice;
/** /**
* Prepare the object * Prepare the object
@ -84,6 +85,13 @@ class RepliesAction extends OwnerDesignAction
common_set_returnto($this->selfUrl()); 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; return true;
} }
@ -159,10 +167,7 @@ class RepliesAction extends OwnerDesignAction
function showContent() function showContent()
{ {
$notice = $this->user->getReplies(($this->page-1) * NOTICES_PER_PAGE, $nl = new NoticeList($this->notice, $this);
NOTICES_PER_PAGE + 1);
$nl = new NoticeList($notice, $this);
$cnt = $nl->show(); $cnt = $nl->show();
if (0 === $cnt) { if (0 === $cnt) {
@ -187,7 +192,9 @@ class RepliesAction extends OwnerDesignAction
} }
} }
else { 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'); $this->elementStart('div', 'guide');

View File

@ -68,7 +68,8 @@ class RepliesrssAction extends Rss10Action
'link' => common_local_url('replies', 'link' => common_local_url('replies',
array('nickname' => array('nickname' =>
$user->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; return $c;
} }

View File

@ -114,6 +114,29 @@ class ShowfavoritesAction extends OwnerDesignAction
common_set_returnto($this->selfUrl()); 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; return true;
} }
@ -173,7 +196,9 @@ class ShowfavoritesAction extends OwnerDesignAction
} }
} }
else { 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'); $this->elementStart('div', 'guide');
@ -191,26 +216,7 @@ class ShowfavoritesAction extends OwnerDesignAction
function showContent() function showContent()
{ {
$cur = common_current_user(); $nl = new NoticeList($this->notice, $this);
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);
$cnt = $nl->show(); $cnt = $nl->show();
if (0 == $cnt) { if (0 == $cnt) {

View File

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

View File

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

View File

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

View File

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

View File

@ -111,7 +111,9 @@ class SubscribersAction extends GalleryAction
} }
} }
else { 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'); $this->elementStart('div', 'guide');

View File

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

View File

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

View File

@ -61,7 +61,8 @@ class TagrssAction extends Rss10Action
$c = array('url' => common_local_url('tagrss', array('tag' => $tagname)), $c = array('url' => common_local_url('tagrss', array('tag' => $tagname)),
'title' => $tagname, 'title' => $tagname,
'link' => common_local_url('tagrss', array('tag' => $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; return $c;
} }

View File

@ -51,6 +51,103 @@ require_once INSTALLDIR.'/lib/twitterapi.php';
class TwitapigroupsAction extends TwitterapiAction 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) function show($args, $apidata)
{ {
parent::handle($args); parent::handle($args);

View File

@ -449,7 +449,8 @@ class TwitapistatusesAction extends TwitterapiAction
function friends($args, $apidata) function friends($args, $apidata)
{ {
parent::handle($args); 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) function friendsIDs($args, $apidata)
@ -461,7 +462,8 @@ class TwitapistatusesAction extends TwitterapiAction
function followers($args, $apidata) function followers($args, $apidata)
{ {
parent::handle($args); 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) function followersIDs($args, $apidata)
@ -470,7 +472,7 @@ class TwitapistatusesAction extends TwitterapiAction
return $this->subscriptions($apidata, 'subscriber', 'subscribed', true); 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']; $this->auth_user = $apidata['user'];
$user = $this->get_user($apidata['api_arg'], $apidata); $user = $this->get_user($apidata['api_arg'], $apidata);
@ -526,26 +528,26 @@ class TwitapistatusesAction extends TwitterapiAction
if ($onlyIDs) { if ($onlyIDs) {
$this->showIDs($others, $type); $this->showIDs($others, $type);
} else { } else {
$this->show_profiles($others, $type); $this->show_profiles($others, $type, $includeStatuses);
} }
$this->end_document($type); $this->end_document($type);
} }
function show_profiles($profiles, $type) function show_profiles($profiles, $type, $includeStatuses)
{ {
switch ($type) { switch ($type) {
case 'xml': case 'xml':
$this->elementStart('users', array('type' => 'array')); $this->elementStart('users', array('type' => 'array'));
foreach ($profiles as $profile) { foreach ($profiles as $profile) {
$this->show_profile($profile); $this->show_profile($profile,$type,null,$includeStatuses);
} }
$this->elementEnd('users'); $this->elementEnd('users');
break; break;
case 'json': case 'json':
$arrays = array(); $arrays = array();
foreach ($profiles as $profile) { foreach ($profiles as $profile) {
$arrays[] = $this->twitter_user_array($profile, true); $arrays[] = $this->twitter_user_array($profile, $includeStatuses);
} }
print json_encode($arrays); print json_encode($arrays);
break; break;

View File

@ -43,6 +43,13 @@ class TwitterauthorizationAction extends Action
return true; return true;
} }
/**
* Handler method
*
* @param array $args is ignored since it's now passed in in prepare()
*
* @return nothing
*/
function handle($args) function handle($args)
{ {
parent::handle($args); parent::handle($args);
@ -66,18 +73,32 @@ class TwitterauthorizationAction extends Action
// process // process
if (empty($this->oauth_token)) { 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 { try {
// Get a new request token and authorize it // Get a new request token and authorize it
$client = new TwitterOAuthClient(); $client = new TwitterOAuthClient();
$req_tok = $client->getRequestToken(); $req_tok =
$client->getRequestToken(TwitterOAuthClient::$requestTokenURL);
// Sock the request token away in the session temporarily // Sock the request token away in the session temporarily
$_SESSION['twitter_request_token'] = $req_tok->key; $_SESSION['twitter_request_token'] = $req_tok->key;
$_SESSION['twitter_request_token_secret'] = $req_tok->key; $_SESSION['twitter_request_token_secret'] = $req_tok->secret;
$auth_link = $client->getAuthorizeLink($req_tok); $auth_link = $client->getAuthorizeLink($req_tok);
@ -88,8 +109,16 @@ class TwitterauthorizationAction extends Action
} }
common_redirect($auth_link); common_redirect($auth_link);
}
} else { /**
* 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 // Check to make sure Twitter returned the same request
// token we sent them // token we sent them
@ -105,20 +134,42 @@ class TwitterauthorizationAction extends Action
// Exchange the request token for an access token // Exchange the request token for an access token
$atok = $client->getAccessToken(); $atok = $client->getAccessToken(TwitterOAuthClient::$accessTokenURL);
// Save the access token and Twitter user info // Test the access token and get the user's Twitter info
$client = new TwitterOAuthClient($atok->key, $atok->secret); $client = new TwitterOAuthClient($atok->key, $atok->secret);
$twitter_user = $client->verifyCredentials();
$twitter_user = $client->verify_credentials();
} catch (OAuthClientException $e) { } catch (OAuthClientException $e) {
$msg = sprintf('OAuth client cURL error - code: %1s, msg: %2s', $msg = sprintf('OAuth client cURL error - code: %1$s, msg: %2$s',
$e->getCode(), $e->getMessage()); $e->getCode(), $e->getMessage());
$this->serverError(_('Couldn\'t link your Twitter account.')); $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(); $user = common_current_user();
$flink = new Foreign_link(); $flink = new Foreign_link();
@ -126,10 +177,14 @@ class TwitterauthorizationAction extends Action
$flink->user_id = $user->id; $flink->user_id = $user->id;
$flink->foreign_id = $twitter_user->id; $flink->foreign_id = $twitter_user->id;
$flink->service = TWITTER_SERVICE; $flink->service = TWITTER_SERVICE;
$flink->token = $atok->key;
$flink->credentials = $atok->secret; $creds = TwitterOAuthClient::packToken($access_token);
$flink->credentials = $creds;
$flink->created = common_sql_now(); $flink->created = common_sql_now();
// Defaults: noticesync on, everything else off
$flink->set_flags(true, false, false, false); $flink->set_flags(true, false, false, false);
$flink_id = $flink->insert(); $flink_id = $flink->insert();
@ -140,14 +195,6 @@ class TwitterauthorizationAction extends Action
} }
save_twitter_user($twitter_user->id, $twitter_user->screen_name); save_twitter_user($twitter_user->id, $twitter_user->screen_name);
// 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'));
}
} }
} }

View File

@ -82,6 +82,12 @@ class TwittersettingsAction extends ConnectSettingsAction
function showContent() function showContent()
{ {
if (!common_config('twitter', 'enabled')) {
$this->element('div', array('class' => 'error'),
_('Twitter is not available.'));
return;
}
$user = common_current_user(); $user = common_current_user();
$profile = $user->getProfile(); $profile = $user->getProfile();

View File

@ -1,5 +1,16 @@
<?php <?php
/* /**
* Unsubscribe handler
*
* PHP version 5
*
* @category Action
* @package Laconica
* @author Evan Prodromou <evan@controlyourself.ca>
* @author Robin Millette <millette@controlyourself.ca>
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://laconi.ca/
*
* Laconica - a distributed open-source microblogging tool * Laconica - a distributed open-source microblogging tool
* Copyright (C) 2008, 2009, Control Yourself, Inc. * Copyright (C) 2008, 2009, Control Yourself, Inc.
* *
@ -17,6 +28,20 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
if (!defined('LACONICA')) {
exit(1);
}
/**
* Unsubscribe handler
*
* @category Action
* @package Laconica
* @author Evan Prodromou <evan@controlyourself.ca>
* @author Robin Millette <millette@controlyourself.ca>
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://laconi.ca/
*/
class UnsubscribeAction extends Action class UnsubscribeAction extends Action
{ {
@ -31,16 +56,18 @@ class UnsubscribeAction extends Action
$user = common_current_user(); $user = common_current_user();
if ($_SERVER['REQUEST_METHOD'] != 'POST') { if ($_SERVER['REQUEST_METHOD'] != 'POST') {
common_redirect(common_local_url('subscriptions', array('nickname' => $user->nickname))); common_redirect(common_local_url('subscriptions',
array('nickname' => $user->nickname)));
return; return;
} }
# CSRF protection /* Use a session token for CSRF protection. */
$token = $this->trimmed('token'); $token = $this->trimmed('token');
if (!$token || $token != common_session_token()) { if (!$token || $token != common_session_token()) {
$this->clientError(_('There was a problem with your session token. Try again, please.')); $this->clientError(_('There was a problem with your session token. ' .
'Try again, please.'));
return; return;
} }
@ -53,7 +80,7 @@ class UnsubscribeAction extends Action
$other = Profile::staticGet('id', $other_id); $other = Profile::staticGet('id', $other_id);
if (!$other_id) { if (!$other) {
$this->clientError(_('No profile with that id.')); $this->clientError(_('No profile with that id.'));
return; return;
} }
@ -76,8 +103,8 @@ class UnsubscribeAction extends Action
$this->elementEnd('body'); $this->elementEnd('body');
$this->elementEnd('html'); $this->elementEnd('html');
} else { } else {
common_redirect(common_local_url('subscriptions', array('nickname' => common_redirect(common_local_url('subscriptions',
$user->nickname)), array('nickname' => $user->nickname)),
303); 303);
} }
} }

View File

@ -79,7 +79,7 @@ class UpdateprofileAction extends Action
$nickname = $req->get_parameter('omb_listenee_nickname'); $nickname = $req->get_parameter('omb_listenee_nickname');
if ($nickname && !Validate::string($nickname, array('min_length' => 1, if ($nickname && !Validate::string($nickname, array('min_length' => 1,
'max_length' => 64, 'max_length' => 64,
'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) { 'format' => NICKNAME_FMT))) {
$this->clientError(_('Nickname must have only lowercase letters and numbers and no spaces.')); $this->clientError(_('Nickname must have only lowercase letters and numbers and no spaces.'));
return false; return false;
} }

View File

@ -47,7 +47,11 @@ class UserauthorizationAction extends Action
# Go log in, and then come back # Go log in, and then come back
common_set_returnto($_SERVER['REQUEST_URI']); common_set_returnto($_SERVER['REQUEST_URI']);
if (!common_config('site', 'openidonly')) {
common_redirect(common_local_url('login')); common_redirect(common_local_url('login'));
} else {
common_redirect(common_local_url('openidlogin'));
}
return; return;
} }
@ -481,7 +485,7 @@ class UserauthorizationAction extends Action
$nickname = $_GET['omb_listenee_nickname']; $nickname = $_GET['omb_listenee_nickname'];
if (!Validate::string($nickname, array('min_length' => 1, if (!Validate::string($nickname, array('min_length' => 1,
'max_length' => 64, 'max_length' => 64,
'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) { 'format' => NICKNAME_FMT))) {
throw new OAuthException('Nickname must have only letters and numbers and no spaces.'); throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
} }
$profile = $_GET['omb_listenee_profile']; $profile = $_GET['omb_listenee_profile'];

View File

@ -88,9 +88,10 @@ class UserrssAction extends Rss10Action
$c = array('url' => common_local_url('userrss', $c = array('url' => common_local_url('userrss',
array('nickname' => array('nickname' =>
$user->nickname)), $user->nickname)),
'title' => $user->nickname, 'title' => sprintf(_('%s timeline'), $user->nickname),
'link' => $profile->profileurl, '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; return $c;
} }

View File

@ -204,7 +204,10 @@ class Design extends Memcached_DataObject
'disposition'); 'disposition');
foreach ($attrs as $attr) { 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)) { if (empty($file_redir)) {
$redir_data = File_redirection::where($given_url); $redir_data = File_redirection::where($given_url);
$redir_url = $redir_data['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); $x = File::saveNew($redir_data, $given_url);
$file_id = $x->id; $file_id = $x->id;
} else { } else {

View File

@ -30,34 +30,38 @@ class Foreign_link extends Memcached_DataObject
/* the code above is auto generated do not remove the tag below */ /* the code above is auto generated do not remove the tag below */
###END_AUTOCODE ###END_AUTOCODE
// 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) static function getByUserID($user_id, $service)
{ {
if (empty($user_id) || empty($service)) {
return null;
}
$flink = new Foreign_link(); $flink = new Foreign_link();
$flink->service = $service; $flink->service = $service;
$flink->user_id = $user_id; $flink->user_id = $user_id;
$flink->limit(1); $flink->limit(1);
if ($flink->find(true)) { $result = $flink->find(true);
return $flink;
} return empty($result) ? null : $flink;
return null;
} }
static function getByForeignID($foreign_id, $service) static function getByForeignID($foreign_id, $service)
{ {
if (empty($foreign_id) || empty($service)) {
return null;
} else {
$flink = new Foreign_link(); $flink = new Foreign_link();
$flink->service = $service; $flink->service = $service;
$flink->foreign_id = $foreign_id; $flink->foreign_id = $foreign_id;
$flink->limit(1); $flink->limit(1);
if ($flink->find(true)) { $result = $flink->find(true);
return $flink;
}
return null; return empty($result) ? null : $flink;
}
} }
function set_flags($noticesend, $noticerecv, $replysync, $friendsync) 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_CACHE_WINDOW', 61);
define('NOTICE_LOCAL_PUBLIC', 1);
define('NOTICE_REMOTE_OMB', 0);
define('NOTICE_LOCAL_NONPUBLIC', -1);
define('MAX_BOXCARS', 128); define('MAX_BOXCARS', 128);
class Notice extends Memcached_DataObject class Notice extends Memcached_DataObject
@ -62,6 +58,10 @@ class Notice extends Memcached_DataObject
/* the code above is auto generated do not remove the tag below */ /* the code above is auto generated do not remove the tag below */
###END_AUTOCODE ###END_AUTOCODE
/* Notice types */
const LOCAL_PUBLIC = 1;
const REMOTE_OMB = 0;
const LOCAL_NONPUBLIC = -1;
const GATEWAY = -2; const GATEWAY = -2;
function getProfile() function getProfile()
@ -134,7 +134,7 @@ class Notice extends Memcached_DataObject
} }
static function saveNew($profile_id, $content, $source=null, 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); $profile = Profile::staticGet($profile_id);
@ -177,7 +177,7 @@ class Notice extends Memcached_DataObject
if (($blacklist && in_array($profile_id, $blacklist)) || if (($blacklist && in_array($profile_id, $blacklist)) ||
($source && $autosource && in_array($source, $autosource))) { ($source && $autosource && in_array($source, $autosource))) {
$notice->is_local = -1; $notice->is_local = Notice::LOCAL_NONPUBLIC;
} else { } else {
$notice->is_local = $is_local; $notice->is_local = $is_local;
} }
@ -509,7 +509,7 @@ class Notice extends Memcached_DataObject
function blowPublicCache($blowLast=false) function blowPublicCache($blowLast=false)
{ {
if ($this->is_local == 1) { if ($this->is_local == Notice::LOCAL_PUBLIC) {
$cache = common_memcache(); $cache = common_memcache();
if ($cache) { if ($cache) {
$cache->delete(common_cache_key('public')); $cache->delete(common_cache_key('public'));
@ -775,10 +775,11 @@ class Notice extends Memcached_DataObject
} }
if (common_config('public', 'localonly')) { if (common_config('public', 'localonly')) {
$notice->whereAdd('is_local = 1'); $notice->whereAdd('is_local = ' . Notice::LOCAL_PUBLIC);
} else { } else {
# -1 == blacklisted # -1 == blacklisted, -2 == gateway (i.e. Twitter)
$notice->whereAdd('is_local != -1'); $notice->whereAdd('is_local !='. Notice::LOCAL_NONPUBLIC);
$notice->whereAdd('is_local !='. Notice::GATEWAY);
} }
if ($since_id != 0) { if ($since_id != 0) {

View File

@ -297,4 +297,45 @@ class User_group extends Memcached_DataObject
return $ids; return $ids;
} }
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; // $config['site']['closed'] = true;
// Only allow registration for people invited by another user // Only allow registration for people invited by another user
// $config['site']['inviteonly'] = true; // $config['site']['inviteonly'] = true;
// Only allow registrations and logins through OpenID
// $config['site']['openidonly'] = true;
// Make the site invisible to non-logged-in users // Make the site invisible to non-logged-in users
// $config['site']['private'] = true; // $config['site']['private'] = true;
@ -97,6 +99,9 @@ $config['sphinx']['port'] = 3312;
// $config['xmpp']['public'][] = 'someindexer@example.net'; // $config['xmpp']['public'][] = 'someindexer@example.net';
// $config['xmpp']['debug'] = false; // $config['xmpp']['debug'] = false;
// Disable OpenID
// $config['openid']['enabled'] = false;
// Turn off invites // Turn off invites
// $config['invite']['enabled'] = false; // $config['invite']['enabled'] = false;
@ -164,6 +169,15 @@ $config['sphinx']['port'] = 3312;
// $config['memcached']['server'] = 'localhost'; // $config['memcached']['server'] = 'localhost';
// $config['memcached']['port'] = 11211; // $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 // Twitter integration source attribute. Note: default is Laconica
// $config['integration']['source'] = 'Laconica'; // $config['integration']['source'] = 'Laconica';
@ -173,6 +187,10 @@ $config['sphinx']['port'] = 3312;
// //
// $config['twitterbridge']['enabled'] = true; // $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 // 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 // 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. // 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']['user_quota'] = 50000000;
// $config['attachments']['monthly_quota'] = 15000000; // $config['attachments']['monthly_quota'] = 15000000;
// $config['attachments']['uploads'] = true; // $config['attachments']['uploads'] = true;
// $config['attachments']['path'] = "/file/";
// $config['oohembed']['endpoint'] = 'http://oohembed.com/oohembed/'; // $config['oohembed']['endpoint'] = 'http://oohembed.com/oohembed/';

View File

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

View File

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

View File

@ -0,0 +1,3 @@
Danilo Segan <danilo@kvota.net>
Nico Kaiser <nico@siriux.net> (contributed most changes between 1.0.2 and 1.0.3, bugfix for 1.0.5)
Steven Armstrong <sa@c-area.ch> (gettext.inc, leading to 1.0.6)

340
extlib/php-gettext/COPYING Normal file
View File

@ -0,0 +1,340 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Library General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Library General
Public License instead of this License.

View File

@ -0,0 +1,144 @@
2006-02-07 Danilo Šegan <danilo@gnome.org>
* examples/pigs_dropin.php: comment-out bind_textdomain_codeset
* gettext.inc (T_bind_textdomain_codeset): bind_textdomain_codeset
is available only in PHP 4.2.0+ (thanks to Jens A. Tkotz).
* Makefile: Include gettext.inc in DIST_FILES, VERSION up to
1.0.7.
2006-02-03 Danilo Šegan <danilo@gnome.org>
Added setlocale() emulation as well.
* examples/pigs_dropin.php: Use T_setlocale() and locale_emulation().
* examples/pigs_fallback.php: Use T_setlocale() and locale_emulation().
* gettext.inc: Added globals $EMULATEGETTEXT and $CURRENTLOCALE.
(locale_emulation): Whether emulation is active.
(_check_locale): Rewrite.
(_setlocale): Added emulated setlocale function.
(T_setlocale): Wrapper around _setlocale.
(_get_reader): Use variables and _setlocale.
2006-02-02 Danilo Šegan <danilo@gnome.org>
Fix bug #12192.
* examples/locale/sr_CS/LC_MESSAGES/messages.po: Correct grammar.
* examples/locale/sr_CS/LC_MESSAGES/messages.mo: Rebuild.
2006-02-02 Danilo Šegan <danilo@gnome.org>
Fix bug #15419.
* streams.php: Support for PHP 5.1.1 fread() which reads most 8kb.
(Fix by Piotr Szotkowski <shot@hot.pl>)
2006-02-02 Danilo Šegan <danilo@gnome.org>
Merge Steven Armstrong's changes, supporting standard gettext
interfaces:
* examples/*: Restructured examples.
* gettext.inc: Added.
* AUTHORS: Added Steven.
* Makefile (VERSION): Up to 1.0.6.
2006-01-28 Nico Kaiser <nico@siriux.net>
* gettext.php (select_string): Fix "true" <-> 1 difference of PHP
2005-07-29 Danilo Šegan <danilo@gnome.org>
* Makefile (VERSION): Up to 1.0.5.
2005-07-29 Danilo Šegan <danilo@gnome.org>
Fixes bug #13850.
* gettext.php (gettext_reader): check $Reader->error as well.
2005-07-29 Danilo Šegan <danilo@gnome.org>
* Makefile (VERSION): Up to 1.0.4.
2005-07-29 Danilo Šegan <danilo@gnome.org>
Fixes bug #13771.
* gettext.php (gettext_reader->get_plural_forms): Plural forms
header extraction regex change. Reported by Edgar Gonzales.
2005-02-28 Danilo Šegan <dsegan@gmx.net>
* AUTHORS: Added Nico to the list.
* Makefile (VERSION): Up to 1.0.3.
* README: Updated.
2005-02-28 Danilo Šegan <dsegan@gmx.net>
* gettext.php: Added pre-loading, code documentation, and many
code clean-ups by Nico Kaiser <nico@siriux.net>.
2005-02-28 Danilo Šegan <dsegan@gmx.net>
* streams.php (FileReader.read): Handle read($bytes = 0).
* examples/pigs.php: Prefix gettext function names with T or T_.
* examples/update: Use the same keywords T_ and T_ngettext.
* streams.php: Added CachedFileReader.
2003-11-11 Danilo Šegan <dsegan@gmx.net>
* gettext.php: Added hashing to find_string.
2003-11-01 Danilo Šegan <dsegan@gmx.net>
* Makefile (DIST_FILES): Replaced LICENSE with COPYING.
(VERSION): Up to 1.0.2.
* AUTHORS: Minor edits.
* README: Minor edits.
* COPYING: Removed LICENSE, added this file.
* gettext.php: Added copyright notice and disclaimer.
* streams.php: Same.
* examples/pigs.php: Same.
2003-10-23 Danilo Šegan <dsegan@gmx.net>
* Makefile: Upped version to 1.0.1.
* gettext.php (gettext_reader): Remove a call to set_total_plurals.
(set_total_plurals): Removed unused function for some better days.
2003-10-23 Danilo Šegan <dsegan@gmx.net>
* Makefile: Added, version 1.0.0.
* examples/*: Added an example of usage.
* README: Described all the crap.
2003-10-22 Danilo Šegan <dsegan@gmx.net>
* gettext.php: Plural forms implemented too.
* streams.php: Added FileReader for direct access to files (no
need to keep file in memory).
* gettext.php: It works, except for plural forms.
* streams.php: Created abstract class StreamReader.
Added StringReader class.
* gettext.php: Started writing gettext_reader.

189
extlib/php-gettext/README Normal file
View File

@ -0,0 +1,189 @@
PHP-gettext 1.0
Copyright 2003, 2006 -- Danilo "angry with PHP[1]" Segan
Licensed under GPLv2 (or any later version, see COPYING)
[1] PHP is actually cyrillic, and translates roughly to
"works-doesn't-work" (UTF-8: Ради-Не-Ради)
Introduction
How many times did you look for a good translation tool, and
found out that gettext is best for the job? Many times.
How many times did you try to use gettext in PHP, but failed
miserably, because either your hosting provider didn't support
it, or the server didn't have adequate locale? Many times.
Well, this is a solution to your needs. It allows using gettext
tools for managing translations, yet it doesn't require gettext
library at all. It parses generated MO files directly, and thus
might be a bit slower than the (maybe provided) gettext library.
PHP-gettext is a simple reader for GNU gettext MO files. Those
are binary containers for translations, produced by GNU msgfmt.
Why?
I got used to having gettext work even without gettext
library. It's there in my favourite language Python, so I was
surprised that I couldn't find it in PHP. I even Googled for it,
but to no avail.
So, I said, what the heck, I'm going to write it for this
disguisting language of PHP, because I'm often constrained to it.
Features
o Support for simple translations
Just define a simple alias for translate() function (suggested
use of _() or gettext(); see provided example).
o Support for ngettext calls (plural forms, see a note under bugs)
You may also use plural forms. Translations in MO files need to
provide this, and they must also provide "plural-forms" header.
Please see 'info gettext' for more details.
o Support for reading straight files, or strings (!!!)
Since I can imagine many different backends for reading in the MO
file data, I used imaginary abstract class StreamReader to do all
the input (check streams.php). For your convenience, I've already
provided two classes for reading files: FileReader and
StringReader (CachedFileReader is a combination of the two: it
loads entire file contents into a string, and then works on that).
See example below for usage. You can for instance use StringReader
when you read in data from a database, or you can create your own
derivative of StreamReader for anything you like.
Bugs
Plural-forms field in MO header (translation for empty string,
i.e. "") is treated according to PHP syntactic rules (it's
eval()ed). Since these should actually follow C syntax, there are
some problems.
For instance, I'm used to using this:
Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : \
n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;
but it fails with PHP (it sets $plural=2 instead of 0 for $n==1).
The fix is usually simple, but I'm lazy to go into the details of
PHP operator precedence, and maybe try to fix it. In here, I had
to put everything after the first ':' in parenthesis:
Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : \
(n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);
That works, and I'm satisfied.
Besides this one, there are probably a bunch of other bugs, since
I hate PHP (did I mention it already? no? strange), and don't
know it very well. So, feel free to fix any of those and report
them back to me at <danilo@kvota.net>.
Usage
Put files streams.php and gettext.php somewhere you can load them
from, and require 'em in where you want to use them.
Then, create one 'stream reader' (a class that provides functions
like read(), seekto(), currentpos() and length()) which will
provide data for the 'gettext_reader', with eg.
$streamer = new FileStream('data.mo');
Then, use that as a parameter to gettext_reader constructor:
$wohoo = new gettext_reader($streamer);
If you want to disable pre-loading of entire message catalog in
memory (if, for example, you have a multi-thousand message catalog
which you'll use only occasionally), use "false" for second
parameter to gettext_reader constructor:
$wohoo = new gettext_reader($streamer, false);
From now on, you have all the benefits of gettext data at your
disposal, so may run:
print $wohoo->translate("This is a test");
print $wohoo->ngettext("%d bird", "%d birds", $birds);
You might need to pass parameter "-k" to xgettext to make it
extract all the strings. In above example, try with
xgettext -ktranslate -kngettext:1,2 file.php
what should create messages.po which contains two messages for
translation.
I suggest creating simple aliases for these functions (see
example/pigs.php for how do I do it, which means it's probably a
bad way).
Usage with gettext.inc (standard gettext interfaces emulation)
Check example in examples/pig_dropin.php, basically you include
gettext.inc and use all the standard gettext interfaces as
documented on:
http://www.php.net/gettext
The only catch is that you can check return value of setlocale()
to see if your locale is system supported or not.
Example
See in examples/ subdirectory. There are a couple of files.
pigs.php is an example, serbian.po is a translation to Serbian
language, and serbian.mo is generated with
msgfmt -o serbian.mo serbian.po
There is also simple "update" script that can be used to generate
POT file and to update the translation using msgmerge.
Interesting TODO:
o Try to parse "plural-forms" header field, and to follow C syntax
rules. This won't be easy.
Boring TODO:
o Learn PHP and fix bugs, slowness and other stuff resulting from
my lack of knowledge (but *maybe*, it's not my knowledge that is
bad, but PHP itself ;-).
(This is mostly done thanks to Nico Kaiser.)
o Try to use hash tables in MO files: with pre-loading, would it
be useful at all?
Never-asked-questions:
o Why did you mark this as version 1.0 when this is the first code
release?
Well, it's quite simple. I consider that the first released thing
should be labeled "version 1" (first, right?). Zero is there to
indicate that there's zero improvement and/or change compared to
"version 1".
I plan to use version numbers 1.0.* for small bugfixes, and to
release 1.1 as "first stable release of version 1".
This may trick someone that this is actually useful software, but
as with any other free software, I take NO RESPONSIBILITY for
creating such a masterpiece that will smoke crack, trash your
hard disk, and make lasers in your CD device dance to the tune of
Mozart's 40th Symphony (there is one like that, right?).
o Can I...?
Yes, you can. This is free software (as in freedom, free speech),
and you might do whatever you wish with it, provided you do not
limit freedom of others (GPL).
I'm considering licensing this under LGPL, but I *do* want
*every* PHP-gettext user to contribute and respect ideas of free
software, so don't count on it happening anytime soon.
I'm sorry that I'm taking away your freedom of taking others'
freedom away, but I believe that's neglible as compared to what
freedoms you could take away. ;-)
Uhm, whatever.

View File

@ -0,0 +1,318 @@
<?php
/*
Copyright (c) 2005 Steven Armstrong <sa at c-area dot ch>
Drop in replacement for native gettext.
This file is part of PHP-gettext.
PHP-gettext is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
PHP-gettext 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with PHP-gettext; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/*
LC_CTYPE 0
LC_NUMERIC 1
LC_TIME 2
LC_COLLATE 3
LC_MONETARY 4
LC_MESSAGES 5
LC_ALL 6
*/
require('streams.php');
require('gettext.php');
// Variables
global $text_domains, $default_domain, $LC_CATEGORIES, $EMULATEGETTEXT, $CURRENTLOCALE;
$text_domains = array();
$default_domain = 'messages';
$LC_CATEGORIES = array('LC_CTYPE', 'LC_NUMERIC', 'LC_TIME', 'LC_COLLATE', 'LC_MONETARY', 'LC_MESSAGES', 'LC_ALL');
$EMULATEGETTEXT = 0;
$CURRENTLOCALE = '';
// Utility functions
/**
* Utility function to get a StreamReader for the given text domain.
*/
function _get_reader($domain=null, $category=5, $enable_cache=true) {
global $text_domains, $default_domain, $LC_CATEGORIES;
if (!isset($domain)) $domain = $default_domain;
if (!isset($text_domains[$domain]->l10n)) {
// get the current locale
$locale = _setlocale(LC_MESSAGES, 0);
$p = isset($text_domains[$domain]->path) ? $text_domains[$domain]->path : './';
$path = $p . "$locale/". $LC_CATEGORIES[$category] ."/$domain.mo";
if (file_exists($path)) {
$input = new FileReader($path);
}
else {
$input = null;
}
$text_domains[$domain]->l10n = new gettext_reader($input, $enable_cache);
}
return $text_domains[$domain]->l10n;
}
/**
* Returns whether we are using our emulated gettext API or PHP built-in one.
*/
function locale_emulation() {
global $EMULATEGETTEXT;
return $EMULATEGETTEXT;
}
/**
* Checks if the current locale is supported on this system.
*/
function _check_locale() {
global $EMULATEGETTEXT;
return !$EMULATEGETTEXT;
}
/**
* Get the codeset for the given domain.
*/
function _get_codeset($domain=null) {
global $text_domains, $default_domain, $LC_CATEGORIES;
if (!isset($domain)) $domain = $default_domain;
return (isset($text_domains[$domain]->codeset))? $text_domains[$domain]->codeset : ini_get('mbstring.internal_encoding');
}
/**
* Convert the given string to the encoding set by bind_textdomain_codeset.
*/
function _encode($text) {
$source_encoding = mb_detect_encoding($text);
$target_encoding = _get_codeset();
if ($source_encoding != $target_encoding) {
return mb_convert_encoding($text, $target_encoding, $source_encoding);
}
else {
return $text;
}
}
// Custom implementation of the standard gettext related functions
/**
* Sets a requested locale, if needed emulates it.
*/
function _setlocale($category, $locale) {
global $CURRENTLOCALE, $EMULATEGETTEXT;
if ($locale === 0) { // use === to differentiate between string "0"
if ($CURRENTLOCALE != '')
return $CURRENTLOCALE;
else
// obey LANG variable, maybe extend to support all of LC_* vars
// even if we tried to read locale without setting it first
return _setlocale($category, $CURRENTLOCALE);
} else {
$ret = 0;
if (function_exists('setlocale')) // I don't know if this ever happens ;)
$ret = setlocale($category, $locale);
if (($ret and $locale == '') or ($ret == $locale)) {
$EMULATEGETTEXT = 0;
$CURRENTLOCALE = $ret;
} else {
if ($locale == '') // emulate variable support
$CURRENTLOCALE = getenv('LANG');
else
$CURRENTLOCALE = $locale;
$EMULATEGETTEXT = 1;
}
return $CURRENTLOCALE;
}
}
/**
* Sets the path for a domain.
*/
function _bindtextdomain($domain, $path) {
global $text_domains;
// ensure $path ends with a slash
if ($path[strlen($path) - 1] != '/') $path .= '/';
elseif ($path[strlen($path) - 1] != '\\') $path .= '\\';
$text_domains[$domain]->path = $path;
}
/**
* Specify the character encoding in which the messages from the DOMAIN message catalog will be returned.
*/
function _bind_textdomain_codeset($domain, $codeset) {
global $text_domains;
$text_domains[$domain]->codeset = $codeset;
}
/**
* Sets the default domain.
*/
function _textdomain($domain) {
global $default_domain;
$default_domain = $domain;
}
/**
* Lookup a message in the current domain.
*/
function _gettext($msgid) {
$l10n = _get_reader();
//return $l10n->translate($msgid);
return _encode($l10n->translate($msgid));
}
/**
* Alias for gettext.
*/
function __($msgid) {
return _gettext($msgid);
}
/**
* Plural version of gettext.
*/
function _ngettext($single, $plural, $number) {
$l10n = _get_reader();
//return $l10n->ngettext($single, $plural, $number);
return _encode($l10n->ngettext($single, $plural, $number));
}
/**
* Override the current domain.
*/
function _dgettext($domain, $msgid) {
$l10n = _get_reader($domain);
//return $l10n->translate($msgid);
return _encode($l10n->translate($msgid));
}
/**
* Plural version of dgettext.
*/
function _dngettext($domain, $single, $plural, $number) {
$l10n = _get_reader($domain);
//return $l10n->ngettext($single, $plural, $number);
return _encode($l10n->ngettext($single, $plural, $number));
}
/**
* Overrides the domain and category for a single lookup.
*/
function _dcgettext($domain, $msgid, $category) {
$l10n = _get_reader($domain, $category);
//return $l10n->translate($msgid);
return _encode($l10n->translate($msgid));
}
/**
* Plural version of dcgettext.
*/
function _dcngettext($domain, $single, $plural, $number, $category) {
$l10n = _get_reader($domain, $category);
//return $l10n->ngettext($single, $plural, $number);
return _encode($l10n->ngettext($single, $plural, $number));
}
// Wrappers to use if the standard gettext functions are available, but the current locale is not supported by the system.
// Use the standard impl if the current locale is supported, use the custom impl otherwise.
function T_setlocale($category, $locale) {
return _setlocale($category, $locale);
}
function T_bindtextdomain($domain, $path) {
if (_check_locale()) return bindtextdomain($domain, $path);
else return _bindtextdomain($domain, $path);
}
function T_bind_textdomain_codeset($domain, $codeset) {
// bind_textdomain_codeset is available only in PHP 4.2.0+
if (_check_locale() and function_exists('bind_textdomain_codeset')) return bind_textdomain_codeset($domain, $codeset);
else return _bind_textdomain_codeset($domain, $codeset);
}
function T_textdomain($domain) {
if (_check_locale()) return textdomain($domain);
else return _textdomain($domain);
}
function T_gettext($msgid) {
if (_check_locale()) return gettext($msgid);
else return _gettext($msgid);
}
function T_($msgid) {
if (_check_locale()) return _($msgid);
return __($msgid);
}
function T_ngettext($single, $plural, $number) {
if (_check_locale()) return ngettext($single, $plural, $number);
else return _ngettext($single, $plural, $number);
}
function T_dgettext($domain, $msgid) {
if (_check_locale()) return dgettext($domain, $msgid);
else return _dgettext($domain, $msgid);
}
function T_dngettext($domain, $single, $plural, $number) {
if (_check_locale()) return dngettext($domain, $single, $plural, $number);
else return _dngettext($domain, $single, $plural, $number);
}
function T_dcgettext($domain, $msgid, $category) {
if (_check_locale()) return dcgettext($domain, $msgid, $category);
else return _dcgettext($domain, $msgid, $category);
}
function T_dcngettext($domain, $single, $plural, $number, $category) {
if (_check_locale()) return dcngettext($domain, $single, $plural, $number, $category);
else return _dcngettext($domain, $single, $plural, $number, $category);
}
// Wrappers used as a drop in replacement for the standard gettext functions
if (!function_exists('gettext')) {
function bindtextdomain($domain, $path) {
return _bindtextdomain($domain, $path);
}
function bind_textdomain_codeset($domain, $codeset) {
return _bind_textdomain_codeset($domain, $codeset);
}
function textdomain($domain) {
return _textdomain($domain);
}
function gettext($msgid) {
return _gettext($msgid);
}
function _($msgid) {
return __($msgid);
}
function ngettext($single, $plural, $number) {
return _ngettext($single, $plural, $number);
}
function dgettext($domain, $msgid) {
return _dgettext($domain, $msgid);
}
function dngettext($domain, $single, $plural, $number) {
return _dngettext($domain, $single, $plural, $number);
}
function dcgettext($domain, $msgid, $category) {
return _dcgettext($domain, $msgid, $category);
}
function dcngettext($domain, $single, $plural, $number, $category) {
return _dcngettext($domain, $single, $plural, $number, $category);
}
}
?>

View File

@ -0,0 +1,358 @@
<?php
/*
Copyright (c) 2003 Danilo Segan <danilo@kvota.net>.
Copyright (c) 2005 Nico Kaiser <nico@siriux.net>
This file is part of PHP-gettext.
PHP-gettext is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
PHP-gettext 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with PHP-gettext; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/**
* Provides a simple gettext replacement that works independently from
* the system's gettext abilities.
* It can read MO files and use them for translating strings.
* The files are passed to gettext_reader as a Stream (see streams.php)
*
* This version has the ability to cache all strings and translations to
* speed up the string lookup.
* While the cache is enabled by default, it can be switched off with the
* second parameter in the constructor (e.g. whenusing very large MO files
* that you don't want to keep in memory)
*/
class gettext_reader {
//public:
var $error = 0; // public variable that holds error code (0 if no error)
//private:
var $BYTEORDER = 0; // 0: low endian, 1: big endian
var $STREAM = NULL;
var $short_circuit = false;
var $enable_cache = false;
var $originals = NULL; // offset of original table
var $translations = NULL; // offset of translation table
var $pluralheader = NULL; // cache header field for plural forms
var $total = 0; // total string count
var $table_originals = NULL; // table for original strings (offsets)
var $table_translations = NULL; // table for translated strings (offsets)
var $cache_translations = NULL; // original -> translation mapping
/* Methods */
/**
* Reads a 32bit Integer from the Stream
*
* @access private
* @return Integer from the Stream
*/
function readint() {
if ($this->BYTEORDER == 0) {
// low endian
return array_shift(unpack('V', $this->STREAM->read(4)));
} else {
// big endian
return array_shift(unpack('N', $this->STREAM->read(4)));
}
}
/**
* Reads an array of Integers from the Stream
*
* @param int count How many elements should be read
* @return Array of Integers
*/
function readintarray($count) {
if ($this->BYTEORDER == 0) {
// low endian
return unpack('V'.$count, $this->STREAM->read(4 * $count));
} else {
// big endian
return unpack('N'.$count, $this->STREAM->read(4 * $count));
}
}
/**
* Constructor
*
* @param object Reader the StreamReader object
* @param boolean enable_cache Enable or disable caching of strings (default on)
*/
function gettext_reader($Reader, $enable_cache = true) {
// If there isn't a StreamReader, turn on short circuit mode.
if (! $Reader || isset($Reader->error) ) {
$this->short_circuit = true;
return;
}
// Caching can be turned off
$this->enable_cache = $enable_cache;
// $MAGIC1 = (int)0x950412de; //bug in PHP 5
$MAGIC1 = (int) - 1794895138;
// $MAGIC2 = (int)0xde120495; //bug
$MAGIC2 = (int) - 569244523;
$this->STREAM = $Reader;
$magic = $this->readint();
if ($magic == $MAGIC1) {
$this->BYTEORDER = 0;
} elseif ($magic == $MAGIC2) {
$this->BYTEORDER = 1;
} else {
$this->error = 1; // not MO file
return false;
}
// FIXME: Do we care about revision? We should.
$revision = $this->readint();
$this->total = $this->readint();
$this->originals = $this->readint();
$this->translations = $this->readint();
}
/**
* Loads the translation tables from the MO file into the cache
* If caching is enabled, also loads all strings into a cache
* to speed up translation lookups
*
* @access private
*/
function load_tables() {
if (is_array($this->cache_translations) &&
is_array($this->table_originals) &&
is_array($this->table_translations))
return;
/* get original and translations tables */
$this->STREAM->seekto($this->originals);
$this->table_originals = $this->readintarray($this->total * 2);
$this->STREAM->seekto($this->translations);
$this->table_translations = $this->readintarray($this->total * 2);
if ($this->enable_cache) {
$this->cache_translations = array ();
/* read all strings in the cache */
for ($i = 0; $i < $this->total; $i++) {
$this->STREAM->seekto($this->table_originals[$i * 2 + 2]);
$original = $this->STREAM->read($this->table_originals[$i * 2 + 1]);
$this->STREAM->seekto($this->table_translations[$i * 2 + 2]);
$translation = $this->STREAM->read($this->table_translations[$i * 2 + 1]);
$this->cache_translations[$original] = $translation;
}
}
}
/**
* Returns a string from the "originals" table
*
* @access private
* @param int num Offset number of original string
* @return string Requested string if found, otherwise ''
*/
function get_original_string($num) {
$length = $this->table_originals[$num * 2 + 1];
$offset = $this->table_originals[$num * 2 + 2];
if (! $length)
return '';
$this->STREAM->seekto($offset);
$data = $this->STREAM->read($length);
return (string)$data;
}
/**
* Returns a string from the "translations" table
*
* @access private
* @param int num Offset number of original string
* @return string Requested string if found, otherwise ''
*/
function get_translation_string($num) {
$length = $this->table_translations[$num * 2 + 1];
$offset = $this->table_translations[$num * 2 + 2];
if (! $length)
return '';
$this->STREAM->seekto($offset);
$data = $this->STREAM->read($length);
return (string)$data;
}
/**
* Binary search for string
*
* @access private
* @param string string
* @param int start (internally used in recursive function)
* @param int end (internally used in recursive function)
* @return int string number (offset in originals table)
*/
function find_string($string, $start = -1, $end = -1) {
if (($start == -1) or ($end == -1)) {
// find_string is called with only one parameter, set start end end
$start = 0;
$end = $this->total;
}
if (abs($start - $end) <= 1) {
// We're done, now we either found the string, or it doesn't exist
$txt = $this->get_original_string($start);
if ($string == $txt)
return $start;
else
return -1;
} else if ($start > $end) {
// start > end -> turn around and start over
return $this->find_string($string, $end, $start);
} else {
// Divide table in two parts
$half = (int)(($start + $end) / 2);
$cmp = strcmp($string, $this->get_original_string($half));
if ($cmp == 0)
// string is exactly in the middle => return it
return $half;
else if ($cmp < 0)
// The string is in the upper half
return $this->find_string($string, $start, $half);
else
// The string is in the lower half
return $this->find_string($string, $half, $end);
}
}
/**
* Translates a string
*
* @access public
* @param string string to be translated
* @return string translated string (or original, if not found)
*/
function translate($string) {
if ($this->short_circuit)
return $string;
$this->load_tables();
if ($this->enable_cache) {
// Caching enabled, get translated string from cache
if (array_key_exists($string, $this->cache_translations))
return $this->cache_translations[$string];
else
return $string;
} else {
// Caching not enabled, try to find string
$num = $this->find_string($string);
if ($num == -1)
return $string;
else
return $this->get_translation_string($num);
}
}
/**
* Get possible plural forms from MO header
*
* @access private
* @return string plural form header
*/
function get_plural_forms() {
// lets assume message number 0 is header
// this is true, right?
$this->load_tables();
// cache header field for plural forms
if (! is_string($this->pluralheader)) {
if ($this->enable_cache) {
$header = $this->cache_translations[""];
} else {
$header = $this->get_translation_string(0);
}
if (eregi("plural-forms: ([^\n]*)\n", $header, $regs))
$expr = $regs[1];
else
$expr = "nplurals=2; plural=n == 1 ? 0 : 1;";
$this->pluralheader = $expr;
}
return $this->pluralheader;
}
/**
* Detects which plural form to take
*
* @access private
* @param n count
* @return int array index of the right plural form
*/
function select_string($n) {
$string = $this->get_plural_forms();
$string = str_replace('nplurals',"\$total",$string);
$string = str_replace("n",$n,$string);
$string = str_replace('plural',"\$plural",$string);
$total = 0;
$plural = 0;
eval("$string");
if ($plural >= $total) $plural = $total - 1;
return $plural;
}
/**
* Plural version of gettext
*
* @access public
* @param string single
* @param string plural
* @param string number
* @return translated plural form
*/
function ngettext($single, $plural, $number) {
if ($this->short_circuit) {
if ($number != 1)
return $plural;
else
return $single;
}
// find out the appropriate form
$select = $this->select_string($number);
// this should contains all strings separated by NULLs
$key = $single.chr(0).$plural;
if ($this->enable_cache) {
if (! array_key_exists($key, $this->cache_translations)) {
return ($number != 1) ? $plural : $single;
} else {
$result = $this->cache_translations[$key];
$list = explode(chr(0), $result);
return $list[$select];
}
} else {
$num = $this->find_string($key);
if ($num == -1) {
return ($number != 1) ? $plural : $single;
} else {
$result = $this->get_translation_string($num);
$list = explode(chr(0), $result);
return $list[$select];
}
}
}
}
?>

View File

@ -0,0 +1,167 @@
<?php
/*
Copyright (c) 2003, 2005 Danilo Segan <danilo@kvota.net>.
This file is part of PHP-gettext.
PHP-gettext is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
PHP-gettext 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with PHP-gettext; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
// Simple class to wrap file streams, string streams, etc.
// seek is essential, and it should be byte stream
class StreamReader {
// should return a string [FIXME: perhaps return array of bytes?]
function read($bytes) {
return false;
}
// should return new position
function seekto($position) {
return false;
}
// returns current position
function currentpos() {
return false;
}
// returns length of entire stream (limit for seekto()s)
function length() {
return false;
}
}
class StringReader {
var $_pos;
var $_str;
function StringReader($str='') {
$this->_str = $str;
$this->_pos = 0;
}
function read($bytes) {
$data = substr($this->_str, $this->_pos, $bytes);
$this->_pos += $bytes;
if (strlen($this->_str)<$this->_pos)
$this->_pos = strlen($this->_str);
return $data;
}
function seekto($pos) {
$this->_pos = $pos;
if (strlen($this->_str)<$this->_pos)
$this->_pos = strlen($this->_str);
return $this->_pos;
}
function currentpos() {
return $this->_pos;
}
function length() {
return strlen($this->_str);
}
}
class FileReader {
var $_pos;
var $_fd;
var $_length;
function FileReader($filename) {
if (file_exists($filename)) {
$this->_length=filesize($filename);
$this->_pos = 0;
$this->_fd = fopen($filename,'rb');
if (!$this->_fd) {
$this->error = 3; // Cannot read file, probably permissions
return false;
}
} else {
$this->error = 2; // File doesn't exist
return false;
}
}
function read($bytes) {
if ($bytes) {
fseek($this->_fd, $this->_pos);
// PHP 5.1.1 does not read more than 8192 bytes in one fread()
// the discussions at PHP Bugs suggest it's the intended behaviour
$data = '';
while ($bytes > 0) {
$chunk = fread($this->_fd, $bytes);
$data .= $chunk;
$bytes -= strlen($chunk);
}
$this->_pos = ftell($this->_fd);
return $data;
} else return '';
}
function seekto($pos) {
fseek($this->_fd, $pos);
$this->_pos = ftell($this->_fd);
return $this->_pos;
}
function currentpos() {
return $this->_pos;
}
function length() {
return $this->_length;
}
function close() {
fclose($this->_fd);
}
}
// Preloads entire file in memory first, then creates a StringReader
// over it (it assumes knowledge of StringReader internals)
class CachedFileReader extends StringReader {
function CachedFileReader($filename) {
if (file_exists($filename)) {
$length=filesize($filename);
$fd = fopen($filename,'rb');
if (!$fd) {
$this->error = 3; // Cannot read file, probably permissions
return false;
}
$this->_str = fread($fd, $length);
fclose($fd);
} else {
$this->error = 2; // File doesn't exist
return false;
}
}
}
?>

View File

@ -182,13 +182,37 @@ function main()
// If the site is private, and they're not on one of the "public" // If the site is private, and they're not on one of the "public"
// parts of the site, redirect to login // parts of the site, redirect to login
if (!$user && common_config('site', 'private') && if (!$user && common_config('site', 'private')) {
!in_array($action, array('login', 'openidlogin', 'finishopenidlogin', $public_actions = array('openidlogin', 'finishopenidlogin',
'recoverpassword', 'api', 'doc', 'register')) && 'recoverpassword', 'api', 'doc',
'opensearch');
$login_action = 'openidlogin';
if (!common_config('site', 'openidonly')) {
$public_actions[] = 'login';
$public_actions[] = 'register';
$login_action = 'login';
}
if (!in_array($action, $public_actions) &&
!preg_match('/rss$/', $action)) { !preg_match('/rss$/', $action)) {
common_redirect(common_local_url('login'));
// set returnto
$rargs =& common_copy_args($args);
unset($rargs['action']);
if (common_config('site', 'fancy')) {
unset($rargs['p']);
}
if (array_key_exists('submit', $rargs)) {
unset($rargs['submit']);
}
foreach (array_keys($_COOKIE) as $cookie) {
unset($rargs[$cookie]);
}
common_set_returnto(common_local_url($action, $rargs));
common_redirect(common_local_url($login_action));
return; return;
} }
}
$action_class = ucfirst($action).'Action'; $action_class = ucfirst($action).'Action';

View File

@ -49,8 +49,7 @@ function checkPrereqs()
} }
$reqs = array('gd', 'curl', $reqs = array('gd', 'curl',
'xmlwriter', 'mbstring', 'xmlwriter', 'mbstring');
'gettext');
foreach ($reqs as $req) { foreach ($reqs as $req) {
if (!checkExtension($req)) { if (!checkExtension($req)) {
@ -180,6 +179,9 @@ function handlePost()
$password = $_POST['password']; $password = $_POST['password'];
$sitename = $_POST['sitename']; $sitename = $_POST['sitename'];
$fancy = !empty($_POST['fancy']); $fancy = !empty($_POST['fancy']);
$server = $_SERVER['HTTP_HOST'];
$path = substr(dirname($_SERVER['PHP_SELF']), 1);
?> ?>
<dl class="system_notice"> <dl class="system_notice">
<dt>Page notice</dt> <dt>Page notice</dt>
@ -219,20 +221,42 @@ function handlePost()
} }
switch($dbtype) { switch($dbtype) {
case 'mysql': mysql_db_installer($host, $database, $username, $password, $sitename, $fancy); case 'mysql':
$db = mysql_db_installer($host, $database, $username, $password);
break; break;
case 'pgsql': pgsql_db_installer($host, $database, $username, $password, $sitename, $fancy); case 'pgsql':
$db = pgsql_db_installer($host, $database, $username, $password);
break; break;
default: default:
} }
if ($path) $path .= '/';
updateStatus("You can visit your <a href='/$path'>new Laconica site</a>."); if (!$db) {
// database connection failed, do not move on to create config file.
return false;
}
updateStatus("Writing config file...");
$res = writeConf($sitename, $server, $path, $fancy, $db);
if (!$res) {
updateStatus("Can't write config file.", true);
showForm();
return;
}
/*
TODO https needs to be considered
*/
$link = "http://".$server.'/'.$path;
updateStatus("Laconica has been installed at $link");
updateStatus("You can visit your <a href='$link'>new Laconica site</a>.");
?> ?>
<?php <?php
} }
function pgsql_db_installer($host, $database, $username, $password, $sitename, $fancy) { function pgsql_db_installer($host, $database, $username, $password) {
$connstring = "dbname=$database host=$host user=$username"; $connstring = "dbname=$database host=$host user=$username";
//No password would mean trust authentication used. //No password would mean trust authentication used.
@ -265,7 +289,7 @@ function pgsql_db_installer($host, $database, $username, $password, $sitename, $
if ($res === false) { if ($res === false) {
updateStatus("Can't run database script.", true); updateStatus("Can't run database script.", true);
showForm(); showForm();
return; return false;
} }
foreach (array('sms_carrier' => 'SMS carrier', foreach (array('sms_carrier' => 'SMS carrier',
'notice_source' => 'notice source', 'notice_source' => 'notice source',
@ -276,29 +300,24 @@ function pgsql_db_installer($host, $database, $username, $password, $sitename, $
if ($res === false) { if ($res === false) {
updateStatus(sprintf("Can't run %d script.", $name), true); updateStatus(sprintf("Can't run %d script.", $name), true);
showForm(); showForm();
return; return false;
} }
} }
pg_query($conn, 'COMMIT'); pg_query($conn, 'COMMIT');
updateStatus("Writing config file...");
if (empty($password)) { if (empty($password)) {
$sqlUrl = "pgsql://$username@$host/$database"; $sqlUrl = "pgsql://$username@$host/$database";
} }
else { else {
$sqlUrl = "pgsql://$username:$password@$host/$database"; $sqlUrl = "pgsql://$username:$password@$host/$database";
} }
$res = writeConf($sitename, $sqlUrl, $fancy, 'pgsql');
if (!$res) {
updateStatus("Can't write config file.", true);
showForm();
return;
}
updateStatus("Done!");
$db = array('type' => 'pgsql', 'database' => $sqlUrl);
return $db;
} }
function mysql_db_installer($host, $database, $username, $password, $sitename, $fancy) { function mysql_db_installer($host, $database, $username, $password) {
updateStatus("Starting installation..."); updateStatus("Starting installation...");
updateStatus("Checking database..."); updateStatus("Checking database...");
@ -306,21 +325,21 @@ function mysql_db_installer($host, $database, $username, $password, $sitename, $
if (!$conn) { if (!$conn) {
updateStatus("Can't connect to server '$host' as '$username'.", true); updateStatus("Can't connect to server '$host' as '$username'.", true);
showForm(); showForm();
return; return false;
} }
updateStatus("Changing to database..."); updateStatus("Changing to database...");
$res = mysql_select_db($database, $conn); $res = mysql_select_db($database, $conn);
if (!$res) { if (!$res) {
updateStatus("Can't change to database.", true); updateStatus("Can't change to database.", true);
showForm(); showForm();
return; return false;
} }
updateStatus("Running database script..."); updateStatus("Running database script...");
$res = runDbScript(INSTALLDIR.'/db/laconica.sql', $conn); $res = runDbScript(INSTALLDIR.'/db/laconica.sql', $conn);
if ($res === false) { if ($res === false) {
updateStatus("Can't run database script.", true); updateStatus("Can't run database script.", true);
showForm(); showForm();
return; return false;
} }
foreach (array('sms_carrier' => 'SMS carrier', foreach (array('sms_carrier' => 'SMS carrier',
'notice_source' => 'notice source', 'notice_source' => 'notice source',
@ -331,35 +350,44 @@ function mysql_db_installer($host, $database, $username, $password, $sitename, $
if ($res === false) { if ($res === false) {
updateStatus(sprintf("Can't run %d script.", $name), true); updateStatus(sprintf("Can't run %d script.", $name), true);
showForm(); showForm();
return; return false;
} }
} }
updateStatus("Writing config file...");
$sqlUrl = "mysqli://$username:$password@$host/$database"; $sqlUrl = "mysqli://$username:$password@$host/$database";
$res = writeConf($sitename, $sqlUrl, $fancy); $db = array('type' => 'mysql', 'database' => $sqlUrl);
if (!$res) { return $db;
updateStatus("Can't write config file.", true); }
showForm();
return; function writeConf($sitename, $server, $path, $fancy, $db)
}
updateStatus("Done!");
}
function writeConf($sitename, $sqlUrl, $fancy, $type='mysql')
{ {
$res = file_put_contents(INSTALLDIR.'/config.php', // assemble configuration file in a string
"<?php\n". $cfg = "<?php\n".
"if (!defined('LACONICA')) { exit(1); }\n\n". "if (!defined('LACONICA')) { exit(1); }\n\n".
"\$config['site']['name'] = \"$sitename\";\n\n".
// site name
"\$config['site']['name'] = '$sitename';\n\n".
// site location
"\$config['site']['server'] = '$server';\n".
"\$config['site']['path'] = '$path'; \n\n".
// checks if fancy URLs are enabled
($fancy ? "\$config['site']['fancy'] = true;\n\n":''). ($fancy ? "\$config['site']['fancy'] = true;\n\n":'').
"\$config['db']['database'] = \"$sqlUrl\";\n\n".
($type == 'pgsql' ? "\$config['db']['quote_identifiers'] = true;\n\n" . // database
"\$config['db']['type'] = \"$type\";\n\n" : ''). "\$config['db']['database'] = '{$db['database']}';\n\n".
"?>"); ($db['type'] == 'pgsql' ? "\$config['db']['quote_identifiers'] = true;\n\n":'').
"\$config['db']['type'] = '{$db['type']}';\n\n".
"?>";
// write configuration file out to install directory
$res = file_put_contents(INSTALLDIR.'/config.php', $cfg);
return $res; return $res;
} }
function runDbScript($filename, $conn, $type='mysql') function runDbScript($filename, $conn, $type = 'mysql')
{ {
$sql = trim(file_get_contents($filename)); $sql = trim(file_get_contents($filename));
$stmts = explode(';', $sql); $stmts = explode(';', $sql);
@ -368,10 +396,15 @@ function runDbScript($filename, $conn, $type='mysql')
if (!mb_strlen($stmt)) { if (!mb_strlen($stmt)) {
continue; continue;
} }
if ($type == 'mysql') { switch ($type) {
case 'mysql':
$res = mysql_query($stmt, $conn); $res = mysql_query($stmt, $conn);
} elseif ($type=='pgsql') { break;
case 'pgsql':
$res = pg_query($conn, $stmt); $res = pg_query($conn, $stmt);
break;
default:
updateStatus("runDbScript() error: unknown database type ". $type ." provided.");
} }
if ($res === false) { if ($res === false) {
updateStatus("FAILED SQL: $stmt"); updateStatus("FAILED SQL: $stmt");
@ -383,9 +416,7 @@ function runDbScript($filename, $conn, $type='mysql')
?> ?>
<?php echo"<?"; ?> xml version="1.0" encoding="UTF-8" <?php echo "?>"; ?> <?php echo"<?"; ?> xml version="1.0" encoding="UTF-8" <?php echo "?>"; ?>
<!DOCTYPE html <!DOCTYPE html>
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en_US" lang="en_US"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en_US" lang="en_US">
<head> <head>
<title>Install Laconica</title> <title>Install Laconica</title>

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

@ -17,26 +17,51 @@
*/ */
$(document).ready(function(){ $(document).ready(function(){
var counterBlackout = false;
// count character on keyup // count character on keyup
function counter(event){ function counter(event){
var maxLength = 140; var maxLength = 140;
var currentLength = $("#notice_data-text").val().length; var currentLength = $("#notice_data-text").val().length;
var remaining = maxLength - currentLength; var remaining = maxLength - currentLength;
var counter = $("#notice_text-count"); var counter = $("#notice_text-count");
if (remaining.toString() != counter.text()) {
if (!counterBlackout || remaining == 0) {
if (counter.text() != String(remaining)) {
counter.text(remaining); counter.text(remaining);
}
if (remaining < 0) { if (remaining < 0) {
$("#form_notice").addClass("warning"); $("#form_notice").addClass("warning");
} else { } else {
$("#form_notice").removeClass("warning"); $("#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) { function submitonreturn(event) {
if (event.keyCode == 13) { if (event.keyCode == 13 || event.keyCode == 10) {
// iPhone sends \n not \r for 'return'
$("#form_notice").submit(); $("#form_notice").submit();
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
$("#notice_data-text").blur();
$("body").focus();
return false; return false;
} }
return true; return true;
@ -57,6 +82,10 @@ $(document).ready(function(){
// XXX: refactor this code // XXX: refactor this code
var favoptions = { dataType: 'xml', 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); success: function(xml) { var new_form = document._importNode($('form', xml).get(0), true);
var dis = new_form.id; var dis = new_form.id;
var fav = dis.replace('disfavor', 'favor'); var fav = dis.replace('disfavor', 'favor');
@ -66,6 +95,10 @@ $(document).ready(function(){
}; };
var disoptions = { dataType: 'xml', 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); success: function(xml) { var new_form = document._importNode($('form', xml).get(0), true);
var fav = new_form.id; var fav = new_form.id;
var dis = fav.replace('favor', 'disfavor'); var dis = fav.replace('favor', 'disfavor');
@ -255,10 +288,10 @@ function NoticeReply() {
function NoticeReplySet(nick,id) { function NoticeReplySet(nick,id) {
rgx_username = /^[0-9a-zA-Z\-_.]*$/; rgx_username = /^[0-9a-zA-Z\-_.]*$/;
if (nick.match(rgx_username)) { if (nick.match(rgx_username)) {
replyto = "@" + nick + " ";
var text = $("#notice_data-text"); var text = $("#notice_data-text");
if (text.length) { if (text.length) {
text.val(replyto + text.val()); replyto = "@" + nick + " ";
text.val(replyto + text.val().replace(RegExp(replyto, 'i'), ''));
$("#form_notice input#notice_in-reply-to").val(id); $("#form_notice input#notice_in-reply-to").val(id);
if (text.get(0).setSelectionRange) { if (text.get(0).setSelectionRange) {
var len = text.val().length; var len = text.val().length;

View File

@ -126,6 +126,10 @@ class AccountSettingsNav extends Widget
$this->action->elementStart('ul', array('class' => 'nav')); $this->action->elementStart('ul', array('class' => 'nav'));
foreach ($menu as $menuaction => $menudesc) { foreach ($menu as $menuaction => $menudesc) {
if ($menuaction == 'openidsettings' &&
!common_config('openid', 'enabled')) {
continue;
}
$this->action->menuItem(common_local_url($menuaction), $this->action->menuItem(common_local_url($menuaction),
$menudesc[0], $menudesc[0],
$menudesc[1], $menudesc[1],

View File

@ -193,21 +193,12 @@ class Action extends HTMLOutputter // lawsuit
if (Event::handle('StartShowStyles', array($this))) { if (Event::handle('StartShowStyles', array($this))) {
if (Event::handle('StartShowLaconicaStyles', array($this))) { if (Event::handle('StartShowLaconicaStyles', array($this))) {
$this->element('link', array('rel' => 'stylesheet', $this->cssLink('css/display.css',null,'screen, projection, tv');
'type' => 'text/css',
'href' => theme_path('css/display.css', null) . '?version=' . LACONICA_VERSION,
'media' => 'screen, projection, tv'));
if (common_config('site', 'mobile')) { 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 // 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', $this->cssLink('css/print.css','base','print');
'type' => 'text/css',
'href' => theme_path('css/print.css', 'base') . '?version=' . LACONICA_VERSION,
'media' => 'print'));
Event::handle('EndShowLaconicaStyles', array($this)); Event::handle('EndShowLaconicaStyles', array($this));
} }
@ -253,26 +244,14 @@ class Action extends HTMLOutputter // lawsuit
{ {
if (Event::handle('StartShowScripts', array($this))) { if (Event::handle('StartShowScripts', array($this))) {
if (Event::handle('StartShowJQueryScripts', array($this))) { if (Event::handle('StartShowJQueryScripts', array($this))) {
$this->element('script', array('type' => 'text/javascript', $this->script('js/jquery.min.js');
'src' => common_path('js/jquery.min.js')), $this->script('js/jquery.form.js');
' '); $this->script('js/jquery.joverlay.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')),
' ');
Event::handle('EndShowJQueryScripts', array($this)); Event::handle('EndShowJQueryScripts', array($this));
} }
if (Event::handle('StartShowLaconicaScripts', array($this))) { if (Event::handle('StartShowLaconicaScripts', array($this))) {
$this->element('script', array('type' => 'text/javascript', $this->script('js/xbImportNode.js');
'src' => common_path('js/xbImportNode.js')), $this->script('js/util.js');
' ');
$this->element('script', array('type' => 'text/javascript',
'src' => common_path('js/util.js?version='.LACONICA_VERSION)),
' ');
// Frame-busting code to avoid clickjacking attacks. // Frame-busting code to avoid clickjacking attacks.
$this->element('script', array('type' => 'text/javascript'), $this->element('script', array('type' => 'text/javascript'),
'if (window.top !== window.self) { window.top.location.href = window.self.location.href; }'); 'if (window.top !== window.self) { window.top.location.href = window.self.location.href; }');
@ -423,6 +402,14 @@ class Action extends HTMLOutputter // lawsuit
function showPrimaryNav() function showPrimaryNav()
{ {
$user = common_current_user(); $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->elementStart('dl', array('id' => 'site_nav_global_primary'));
$this->element('dt', null, _('Primary site navigation')); $this->element('dt', null, _('Primary site navigation'));
@ -434,12 +421,9 @@ class Action extends HTMLOutputter // lawsuit
_('Home'), _('Personal profile and friends timeline'), false, 'nav_home'); _('Home'), _('Personal profile and friends timeline'), false, 'nav_home');
$this->menuItem(common_local_url('profilesettings'), $this->menuItem(common_local_url('profilesettings'),
_('Account'), _('Change your email, avatar, password, profile'), false, 'nav_account'); _('Account'), _('Change your email, avatar, password, profile'), false, 'nav_account');
if (common_config('xmpp', 'enabled')) { if ($connect) {
$this->menuItem(common_local_url('imsettings'), $this->menuItem(common_local_url($connect),
_('Connect'), _('Connect to IM, SMS, Twitter'), false, 'nav_connect'); _('Connect'), _('Connect to services'), false, 'nav_connect');
} else {
$this->menuItem(common_local_url('smssettings'),
_('Connect'), _('Connect to SMS, Twitter'), false, 'nav_connect');
} }
if (common_config('invite', 'enabled')) { if (common_config('invite', 'enabled')) {
$this->menuItem(common_local_url('invite'), $this->menuItem(common_local_url('invite'),
@ -452,17 +436,24 @@ class Action extends HTMLOutputter // lawsuit
_('Logout'), _('Logout from the site'), false, 'nav_logout'); _('Logout'), _('Logout from the site'), false, 'nav_logout');
} }
else { else {
if (!common_config('site', 'openidonly')) {
if (!common_config('site', 'closed')) { if (!common_config('site', 'closed')) {
$this->menuItem(common_local_url('register'), $this->menuItem(common_local_url('register'),
_('Register'), _('Create an account'), false, 'nav_register'); _('Register'), _('Create an account'), false, 'nav_register');
} }
$this->menuItem(common_local_url('login'), $this->menuItem(common_local_url('login'),
_('Login'), _('Login to the site'), false, 'nav_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')), $this->menuItem(common_local_url('doc', array('title' => 'help')),
_('Help'), _('Help me!'), false, 'nav_help'); _('Help'), _('Help me!'), false, 'nav_help');
if ($user || !common_config('site', 'private')) {
$this->menuItem(common_local_url('peoplesearch'), $this->menuItem(common_local_url('peoplesearch'),
_('Search'), _('Search for people or text'), false, 'nav_search'); _('Search'), _('Search for people or text'), false, 'nav_search');
}
Event::handle('EndPrimaryNav', array($this)); Event::handle('EndPrimaryNav', array($this));
} }
$this->elementEnd('ul'); $this->elementEnd('ul');

View File

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

View File

@ -47,6 +47,9 @@ require_once('PEAR.php');
require_once('DB/DataObject.php'); require_once('DB/DataObject.php');
require_once('DB/DataObject/Cast.php'); # for dates require_once('DB/DataObject/Cast.php'); # for dates
if (!function_exists('gettext')) {
require_once("php-gettext/gettext.inc");
}
require_once(INSTALLDIR.'/lib/language.php'); require_once(INSTALLDIR.'/lib/language.php');
// This gets included before the config file, so that admin code and plugins // This gets included before the config file, so that admin code and plugins
@ -82,7 +85,7 @@ if (isset($server)) {
if (isset($path)) { if (isset($path)) {
$_path = $path; $_path = $path;
} else { } 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']) : _sn_to_path($_SERVER['SCRIPT_NAME']) :
null; null;
} }
@ -109,6 +112,7 @@ $config =
'broughtbyurl' => null, 'broughtbyurl' => null,
'closed' => false, 'closed' => false,
'inviteonly' => false, 'inviteonly' => false,
'openidonly' => false,
'private' => false, 'private' => false,
'ssl' => 'never', 'ssl' => 'never',
'sslserver' => null, 'sslserver' => null,
@ -169,6 +173,8 @@ $config =
'host' => null, # only set if != server 'host' => null, # only set if != server
'debug' => false, # print extra debug info 'debug' => false, # print extra debug info
'public' => array()), # JIDs of users who want to receive the public stream 'public' => array()), # JIDs of users who want to receive the public stream
'openid' =>
array('enabled' => true),
'invite' => 'invite' =>
array('enabled' => true), array('enabled' => true),
'sphinx' => 'sphinx' =>
@ -183,6 +189,12 @@ $config =
array('piddir' => '/var/run', array('piddir' => '/var/run',
'user' => false, 'user' => false,
'group' => false), 'group' => false),
'emailpost' =>
array('enabled' => true),
'sms' =>
array('enabled' => true),
'twitter' =>
array('enabled' => true),
'twitterbridge' => 'twitterbridge' =>
array('enabled' => false), array('enabled' => false),
'integration' => 'integration' =>
@ -364,6 +376,12 @@ if ($_db_name != 'laconica' && !array_key_exists('ini_'.$_db_name, $config['db']
$config['db']['ini_'.$_db_name] = INSTALLDIR.'/classes/laconica.ini'; $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? // XXX: how many of these could be auto-loaded on use?
require_once 'Validate.php'; require_once 'Validate.php';

View File

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

View File

@ -311,13 +311,7 @@ class DesignSettingsAction extends AccountSettingsAction
function showStylesheets() function showStylesheets()
{ {
parent::showStylesheets(); parent::showStylesheets();
$farbtasticStyle = $this->cssLink('css/farbtastic.css','base','screen, projection, tv');
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'));
} }
/** /**
@ -330,13 +324,8 @@ class DesignSettingsAction extends AccountSettingsAction
{ {
parent::showScripts(); parent::showScripts();
$farbtasticPack = common_path('js/farbtastic/farbtastic.js'); $this->script('js/farbtastic/farbtastic.js');
$userDesignGo = common_path('js/userdesign.go.js'); $this->script('js/farbtastic/farbtastic.go.js');
$this->element('script', array('type' => 'text/javascript',
'src' => $farbtasticPack));
$this->element('script', array('type' => 'text/javascript',
'src' => $userDesignGo));
} }
/** /**

View File

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

View File

@ -109,10 +109,11 @@ class HTMLOutputter extends XMLOutputter
header('Content-Type: '.$type); header('Content-Type: '.$type);
$this->extraHeaders(); $this->extraHeaders();
if( ! substr($type,0,strlen('text/html'))=='text/html' ){
$this->startXML('html', // Browsers don't like it when <?xml it output for non-xhtml documents
'-//W3C//DTD XHTML 1.0 Strict//EN', $this->xw->startDocument('1.0', 'UTF-8');
'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'); }
$this->xw->writeDTD('html');
$language = $this->getLanguage(); $language = $this->getLanguage();
@ -338,6 +339,52 @@ class HTMLOutputter extends XMLOutputter
'title' => $title)); '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 * output an HTML textarea and associated elements
* *

View File

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

View File

@ -72,14 +72,18 @@ class LoginGroupNav extends Widget
// action => array('prompt', 'title') // action => array('prompt', 'title')
$menu = array(); $menu = array();
if (!common_config('site','openidonly')) {
$menu['login'] = array(_('Login'), $menu['login'] = array(_('Login'),
_('Login with a username and password')); _('Login with a username and password'));
if (!(common_config('site','closed') || common_config('site','inviteonly'))) { if (!(common_config('site','closed') || common_config('site','inviteonly'))) {
$menu['register'] = array(_('Register'), $menu['register'] = array(_('Register'),
_('Sign up for a new account')); _('Sign up for a new account'));
} }
}
if (common_config('openid', 'enabled')) {
$menu['openidlogin'] = array(_('OpenID'), $menu['openidlogin'] = array(_('OpenID'),
_('Login or register with OpenID')); _('Login or register with OpenID'));
}
$action_name = $this->action->trimmed('action'); $action_name = $this->action->trimmed('action');
$this->action->elementStart('ul', array('class' => 'nav')); $this->action->elementStart('ul', array('class' => 'nav'));

View File

@ -597,6 +597,16 @@ function mail_notify_attn($user, $notice)
common_init_locale($user->language); 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); $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". $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" . "\t%3\$s\n\n" .
"It reads:\n\n". "It reads:\n\n".
"\t%4\$s\n\n" . "\t%4\$s\n\n" .
$conversationEmailText .
"You can reply back here:\n\n". "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" . "The list of all @-replies for you here:\n\n" .
"%6\$s\n\n" . "%7\$s\n\n" .
"Faithfully yours,\n" . "Faithfully yours,\n" .
"%2\$s\n\n" . "%2\$s\n\n" .
"P.S. You can turn off these email notifications here: %7\$s\n"), "P.S. You can turn off these email notifications here: %8\$s\n"),
$bestname, $bestname,//%1
common_config('site', 'name'), common_config('site', 'name'),//%2
common_local_url('shownotice', common_local_url('shownotice',
array('notice' => $notice->id)), array('notice' => $notice->id)),//%3
$notice->content, $notice->content,//%4
$conversationUrl,//%5
common_local_url('newnotice', common_local_url('newnotice',
array('replyto' => $sender->nickname)), array('replyto' => $sender->nickname)),//%6
common_local_url('replies', common_local_url('replies',
array('nickname' => $user->nickname)), array('nickname' => $user->nickname)),//%7
common_local_url('emailsettings')); common_local_url('emailsettings'));//%8
common_init_locale(); common_init_locale();
mail_to_user($user, $subject, $body); mail_to_user($user, $subject, $body);

View File

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

View File

@ -1,14 +1,80 @@
<?php <?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/
*/
require_once('OAuth.php'); if (!defined('LACONICA')) {
exit(1);
}
class OAuthClientCurlException extends Exception { } 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 class OAuthClient
{ {
var $consumer; var $consumer;
var $token; 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, function __construct($consumer_key, $consumer_secret,
$oauth_token = null, $oauth_token_secret = null) $oauth_token = null, $oauth_token_secret = null)
{ {
@ -21,34 +87,66 @@ class OAuthClient
} }
} }
function getRequestToken() /**
* 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(TwitterOAuthClient::$requestTokenURL); $response = $this->oAuthGet($url);
parse_str($response); parse_str($response);
$token = new OAuthToken($oauth_token, $oauth_token_secret); $token = new OAuthToken($oauth_token, $oauth_token_secret);
return $token; return $token;
} }
function getAuthorizeLink($request_token, $oauth_callback = null) /**
* 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)
{ {
$url = TwitterOAuthClient::$authorizeURL . '?oauth_token=' . $authorize_url = $url . '?oauth_token=' .
$request_token->key; $request_token->key;
if (isset($oauth_callback)) { if (isset($oauth_callback)) {
$url .= '&oauth_callback=' . urlencode($oauth_callback); $authorize_url .= '&oauth_callback=' . urlencode($oauth_callback);
} }
return $url; return $authorize_url;
} }
function getAccessToken() /**
* 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(TwitterOAuthClient::$accessTokenURL); $response = $this->oAuthPost($url);
parse_str($response); parse_str($response);
$token = new OAuthToken($oauth_token, $oauth_token_secret); $token = new OAuthToken($oauth_token, $oauth_token_secret);
return $token; return $token;
} }
/**
* Use HTTP GET to make a signed OAuth request
*
* @param string $url OAuth endpoint
*
* @return mixed the request
*/
function oAuthGet($url) function oAuthGet($url)
{ {
$request = OAuthRequest::from_consumer_and_token($this->consumer, $request = OAuthRequest::from_consumer_and_token($this->consumer,
@ -59,6 +157,14 @@ class OAuthClient
return $this->httpRequest($request->to_url()); 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) function oAuthPost($url, $params = null)
{ {
$request = OAuthRequest::from_consumer_and_token($this->consumer, $request = OAuthRequest::from_consumer_and_token($this->consumer,
@ -70,6 +176,14 @@ class OAuthClient
$request->to_postdata()); $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) function httpRequest($url, $params = null)
{ {
$options = array( $options = array(

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

@ -117,15 +117,8 @@ class Router
$m->connect('main/tagother/:id', array('action' => 'tagother')); $m->connect('main/tagother/:id', array('action' => 'tagother'));
$m->connect('main/oembed.xml', $m->connect('main/oembed',
array('action' => 'api', array('action' => 'oembed'));
'method' => 'oembed.xml',
'apiaction' => 'oembed'));
$m->connect('main/oembed.json',
array('action' => 'api',
'method' => 'oembed.json',
'apiaction' => 'oembed'));
// these take a code // these take a code
@ -413,6 +406,28 @@ class Router
'apiaction' => 'laconica')); 'apiaction' => 'laconica'));
// Groups // 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', $m->connect('api/laconica/groups/:method/:argument',
array('action' => 'api', array('action' => 'api',
'apiaction' => 'groups')); 'apiaction' => 'groups'));

View File

@ -120,7 +120,7 @@ class MySQLSearch extends SearchEngine
} else if ('identica_notices' === $this->table) { } else if ('identica_notices' === $this->table) {
// Don't show imported notices // 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) { if (strtolower($q) != $q) {
$this->target->whereAdd("( MATCH(content) AGAINST ('" . addslashes($q) . $this->target->whereAdd("( MATCH(content) AGAINST ('" . addslashes($q) .

View File

@ -17,83 +17,20 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * 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 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) function update_twitter_user($twitter_id, $screen_name)
{ {
$uri = 'http://twitter.com/' . $screen_name; $uri = 'http://twitter.com/' . $screen_name;
$fuser = new Foreign_user(); $fuser = new Foreign_user();
$fuser->query('BEGIN'); $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 // 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 // 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 = 'UPDATE foreign_user set uri = \'\' WHERE uri = ';
$qry .= '\'' . $uri . '\'' . ' AND service = ' . TWITTER_SERVICE; $qry .= '\'' . $uri . '\'' . ' AND service = ' . TWITTER_SERVICE;
$result = $fuser->query($qry); $fuser->query($qry);
if ($result) {
common_debug("Removed uri ($uri) from another foreign_user who was squatting on it.");
if (defined('SCRIPT_DEBUG')) {
print("Removed uri ($uri) from another Twitter user who was squatting on it.\n");
}
}
// Update the user // Update the user
$qry = 'UPDATE foreign_user SET nickname = '; $qry = 'UPDATE foreign_user SET nickname = ';
$qry .= '\'' . $screen_name . '\'' . ', uri = \'' . $uri . '\' '; $qry .= '\'' . $screen_name . '\'' . ', uri = \'' . $uri . '\' ';
$qry .= 'WHERE id = ' . $twitter_id . ' AND service = ' . TWITTER_SERVICE; $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->query('COMMIT');
$fuser->free(); $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 // 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 // This can happen when users move around or fakester accounts get
// repoed, and things like that. // repoed, and things like that.
$luser = new Foreign_user(); $luser = new Foreign_user();
$luser->uri = $new_uri; $luser->uri = $new_uri;
$luser->service = TWITTER_SERVICE; $luser->service = TWITTER_SERVICE;
$result = $luser->delete(); $result = $luser->delete();
if ($result) { if (empty($result)) {
common_log(LOG_WARNING, common_log(LOG_WARNING,
"Twitter bridge - removed invalid Twitter user squatting on uri: $new_uri"); "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(); $luser->free();
unset($luser); unset($luser);
// Otherwise, create a new Twitter user // Otherwise, create a new Twitter user
$fuser = new Foreign_user(); $fuser = new Foreign_user();
$fuser->nickname = $screen_name; $fuser->nickname = $screen_name;
@ -173,21 +88,12 @@ function add_twitter_user($twitter_id, $screen_name)
$fuser->created = common_sql_now(); $fuser->created = common_sql_now();
$result = $fuser->insert(); $result = $fuser->insert();
if (!$result) { if (empty($result)) {
common_log(LOG_WARNING, common_log(LOG_WARNING,
"Twitter bridge - failed to add new Twitter user: $twitter_id - $screen_name."); "Twitter bridge - failed to add new Twitter user: $twitter_id - $screen_name.");
common_log_db_error($fuser, 'INSERT', __FILE__); 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 { } else {
common_debug("Twitter bridge - Added new Twitter user: $screen_name ($twitter_id)."); 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; 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, // Check to see whether the Twitter user is already in the system,
// and update its screen name and uri if so. // and update its screen name and uri if so.
$fuser = Foreign_user::getForeignUser($twitter_id, TWITTER_SERVICE); $fuser = Foreign_user::getForeignUser($twitter_id, TWITTER_SERVICE);
if ($fuser) { if (!empty($fuser)) {
$result = true; $result = true;
// Only update if Twitter screen name has changed // Only update if Twitter screen name has changed
if ($fuser->nickname != $screen_name) { if ($fuser->nickname != $screen_name) {
$result = update_twitter_user($twitter_id, $screen_name); $result = update_twitter_user($twitter_id, $screen_name);
common_debug('Twitter bridge - Updated nickname (and URI) for Twitter user ' . common_debug('Twitter bridge - Updated nickname (and URI) for Twitter user ' .
"$fuser->id to $screen_name, was $fuser->nickname"); "$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; return $result;
@ -230,119 +133,6 @@ function save_twitter_user($twitter_id, $screen_name)
return true; 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) { function is_twitter_bound($notice, $flink) {
// Check to see if notice should go to Twitter // Check to see if notice should go to Twitter
@ -370,12 +160,14 @@ function broadcast_twitter($notice)
// XXX: Hack to get around PHP cURL's use of @ being a a meta character // XXX: Hack to get around PHP cURL's use of @ being a a meta character
$statustxt = preg_replace('/^@/', ' @', $notice->content); $statustxt = preg_replace('/^@/', ' @', $notice->content);
$client = new TwitterOAuthClient($flink->token, $flink->credentials); $token = TwitterOAuthClient::unpackToken($flink->credentials);
$client = new TwitterOAuthClient($token->key, $token->secret);
$status = null; $status = null;
try { try {
$status = $client->statuses_update($statustxt); $status = $client->statusesUpdate($statustxt);
} catch (OAuthClientCurlException $e) { } catch (OAuthClientCurlException $e) {
if ($e->getMessage() == 'The requested URL returned error: 401') { if ($e->getMessage() == 'The requested URL returned error: 401') {
@ -425,7 +217,6 @@ function broadcast_twitter($notice)
$msg = sprintf('Twitter bridge posted notice %s to Twitter.', $msg = sprintf('Twitter bridge posted notice %s to Twitter.',
$notice->id); $notice->id);
common_log(LOG_INFO, $msg); common_log(LOG_INFO, $msg);
} }
return true; return true;
@ -463,5 +254,65 @@ function remove_twitter_link($flink)
} }
} }
}
$result = mail_twitter_bridge_removed($user);
if (!$result) {
$msg = 'Unable to send email to notify ' .
"$user->nickname (user id: $user->id) " .
'that their Twitter bridge link was ' .
'removed!';
common_log(LOG_WARNING, $msg);
}
}
}
$result = mail_twitter_bridge_removed($user);
if (!$result) {
$msg = 'Unable to send email to notify ' .
"$user->nickname (user id: $user->id) " .
'that their Twitter bridge link was ' .
'removed!';
common_log(LOG_WARNING, $msg);
}
}
}
$result = mail_twitter_bridge_removed($user);
if (!$result) {
$msg = 'Unable to send email to notify ' .
"$user->nickname (user id: $user->id) " .
'that their Twitter bridge link was ' .
'removed!';
common_log(LOG_WARNING, $msg);
}
}
}
$result = mail_twitter_bridge_removed($user);
if (!$result) {
$msg = 'Unable to send email to notify ' .
"$user->nickname (user id: $user->id) " .
'that their Twitter bridge link was ' .
'removed!';
common_log(LOG_WARNING, $msg);
}
}
} }

View File

@ -188,7 +188,10 @@ class TwitterapiAction extends Action
// Enclosures // Enclosures
$attachments = $notice->attachments(); $attachments = $notice->attachments();
$enclosures = array();
if (!empty($attachments)) {
$twitter_status['attachments'] = array();
foreach ($attachments as $attachment) { foreach ($attachments as $attachment) {
if ($attachment->isEnclosure()) { if ($attachment->isEnclosure()) {
@ -196,12 +199,9 @@ class TwitterapiAction extends Action
$enclosure['url'] = $attachment->url; $enclosure['url'] = $attachment->url;
$enclosure['mimetype'] = $attachment->mimetype; $enclosure['mimetype'] = $attachment->mimetype;
$enclosure['size'] = $attachment->size; $enclosure['size'] = $attachment->size;
$enclosures[] = $enclosure; $twitter_status['attachments'][] = $enclosure;
} }
} }
if (!empty($enclosures)) {
$twitter_status['attachments'] = $enclosures;
} }
if ($include_user) { if ($include_user) {
@ -233,6 +233,24 @@ class TwitterapiAction extends Action
return $twitter_group; 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) function twitter_rss_entry_array($notice)
{ {
$profile = $notice->getProfile(); $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) function show_json_timeline($notice)
{ {
@ -668,6 +745,52 @@ class TwitterapiAction extends Action
$this->end_document('json'); $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) function show_single_json_group($group)
{ {
$this->init_document('json'); $this->init_document('json');
@ -844,9 +967,9 @@ class TwitterapiAction extends Action
$this->endXML(); $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) { switch ($content_type) {
case 'xml': case 'xml':
$this->show_twitter_xml_user($profile_array); $this->show_twitter_xml_user($profile_array);

View File

@ -1,11 +1,60 @@
<?php <?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 class TwitterOAuthClient extends OAuthClient
{ {
public static $requestTokenURL = 'https://twitter.com/oauth/request_token'; public static $requestTokenURL = 'https://twitter.com/oauth/request_token';
public static $authorizeURL = 'https://twitter.com/oauth/authorize'; public static $authorizeURL = 'https://twitter.com/oauth/authorize';
public static $accessTokenURL = 'https://twitter.com/oauth/access_token'; 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) function __construct($oauth_token = null, $oauth_token_secret = null)
{ {
$consumer_key = common_config('twitter', 'consumer_key'); $consumer_key = common_config('twitter', 'consumer_key');
@ -15,13 +64,43 @@ class TwitterOAuthClient extends OAuthClient
$oauth_token, $oauth_token_secret); $oauth_token, $oauth_token_secret);
} }
function getAuthorizeLink($request_token) { // XXX: the following two functions are to support the horrible hack
return parent::getAuthorizeLink($request_token, // of using the credentils field in Foreign_link to store both
common_local_url('twitterauthorization')); // 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));
} }
function verify_credentials() 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'; $url = 'https://twitter.com/account/verify_credentials.json';
$response = $this->oAuthGet($url); $response = $this->oAuthGet($url);
@ -29,7 +108,16 @@ class TwitterOAuthClient extends OAuthClient
return $twitter_user; return $twitter_user;
} }
function statuses_update($status, $in_reply_to_status_id = null) /**
* 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'; $url = 'https://twitter.com/statuses/update.json';
$params = array('status' => $status, $params = array('status' => $status,
@ -39,8 +127,19 @@ class TwitterOAuthClient extends OAuthClient
return $status; return $status;
} }
function statuses_friends_timeline($since_id = null, $max_id = null, /**
$cnt = null, $page = null) { * 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'; $url = 'https://twitter.com/statuses/friends_timeline.json';
$params = array('since_id' => $since_id, $params = array('since_id' => $since_id,
@ -58,4 +157,64 @@ class TwitterOAuthClient extends OAuthClient
return $statuses; 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) function _isLocal($notice)
{ {
return ($notice->is_local == NOTICE_LOCAL_PUBLIC || return ($notice->is_local == Notice::LOCAL_PUBLIC ||
$notice->is_local == NOTICE_LOCAL_NONPUBLIC); $notice->is_local == Notice::LOCAL_NONPUBLIC);
} }
} }

View File

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

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

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