summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--EVENTS.txt41
-rw-r--r--actions/showstream.php52
-rw-r--r--config.php.sample4
-rw-r--r--db/laconica.sql10
-rw-r--r--index.php41
-rw-r--r--lib/action.php134
-rw-r--r--lib/clientexception.php56
-rw-r--r--lib/common.php18
-rw-r--r--lib/event.php113
-rw-r--r--lib/imagefile.php50
-rw-r--r--lib/plugin.php79
-rw-r--r--lib/serverexception.php55
-rw-r--r--lib/util.php2
-rw-r--r--plugins/GoogleAnalyticsPlugin.php77
15 files changed, 624 insertions, 109 deletions
diff --git a/.gitignore b/.gitignore
index f5a3e0212..f2e96d3eb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,3 +9,4 @@ dataobject.ini
*.bak
*.orig
*.rej
+.#*
diff --git a/EVENTS.txt b/EVENTS.txt
new file mode 100644
index 000000000..d9634325d
--- /dev/null
+++ b/EVENTS.txt
@@ -0,0 +1,41 @@
+InitializePlugin: a chance to initialize a plugin in a complete
+ environment
+
+CleanupPlugin: a chance to cleanup a plugin at the end of a program
+
+StartPrimaryNav: Showing the primary nav menu
+- $action: the current action
+
+EndPrimaryNav: At the end of the primary nav menu
+- $action: the current action
+
+StartSecondaryNav: Showing the secondary nav menu
+- $action: the current action
+
+EndSecondaryNav: At the end of the secondary nav menu
+- $action: the current action
+
+StartShowScripts: Showing JavaScript links
+- $action: the current action
+
+EndShowScripts: End showing JavaScript links; good place to add custom
+ links like Google Analytics
+- $action: the current action
+
+StartShowJQueryScripts: Showing JQuery script links (use this to link to e.g. Google mirrors)
+- $action: the current action
+
+EndShowJQueryScripts: End showing JQuery script links
+- $action: the current action
+
+StartShowLaconicaScripts: Showing Laconica script links (use this to link to a CDN or something)
+- $action: the current action
+
+EndShowLaconicaScripts: End showing Laconica script links
+- $action: the current action
+
+StartShowSections: Start the list of sections in the sidebar
+- $action: the current action
+
+EndShowSections: End the list of sections in the sidebar
+- $action: the current action
diff --git a/actions/showstream.php b/actions/showstream.php
index 28bb8453f..962f4b452 100644
--- a/actions/showstream.php
+++ b/actions/showstream.php
@@ -111,7 +111,7 @@ class ShowstreamAction extends Action
$this->page = ($this->arg('page')) ? ($this->arg('page')+0) : 1;
common_set_returnto($this->selfUrl());
-
+
return true;
}
@@ -178,21 +178,21 @@ class ShowstreamAction extends Action
function showFeeds()
{
$this->element('link', array('rel' => 'alternate',
- 'type' => 'application/rss+xml',
- 'href' => common_local_url('userrss',
- array('nickname' => $this->user->nickname)),
- 'title' => sprintf(_('Notice feed for %s (RSS)'),
- $this->user->nickname)));
-
- $this->element('link',
- array('rel' => 'alternate',
- 'href' => common_local_url('api',
- array('apiaction' => 'statuses',
- 'method' => 'user_timeline.atom',
- 'argument' => $this->user->nickname)),
- 'type' => 'application/atom+xml',
- 'title' => sprintf(_('Notice feed for %s (Atom)'),
- $this->user->nickname)));
+ 'type' => 'application/rss+xml',
+ 'href' => common_local_url('userrss',
+ array('nickname' => $this->user->nickname)),
+ 'title' => sprintf(_('Notice feed for %s (RSS)'),
+ $this->user->nickname)));
+
+ $this->element('link',
+ array('rel' => 'alternate',
+ 'href' => common_local_url('api',
+ array('apiaction' => 'statuses',
+ 'method' => 'user_timeline.atom',
+ 'argument' => $this->user->nickname)),
+ 'type' => 'application/atom+xml',
+ 'title' => sprintf(_('Notice feed for %s (Atom)'),
+ $this->user->nickname)));
}
function extraHead()
@@ -206,7 +206,7 @@ class ShowstreamAction extends Action
// for remote subscriptions etc.
$this->element('meta', array('http-equiv' => 'X-XRDS-Location',
'content' => common_local_url('xrds', array('nickname' =>
- $this->user->nickname))));
+ $this->user->nickname))));
if ($this->profile->bio) {
$this->element('meta', array('name' => 'description',
@@ -248,6 +248,15 @@ class ShowstreamAction extends Action
'height' => AVATAR_PROFILE_SIZE,
'alt' => $this->profile->nickname));
$this->elementEnd('dd');
+
+ $user = User::staticGet('id', $this->profile->id);
+ $cur = common_current_user();
+ if ($cur && $cur->id == $user->id) {
+ $this->elementStart('dd');
+ $this->element('a', array('href' => common_local_url('avatarsettings')), _('Edit Avatar'));
+ $this->elementEnd('dd');
+ }
+
$this->elementEnd('dl');
$this->elementStart('dl', 'entity_nickname');
@@ -256,7 +265,7 @@ class ShowstreamAction extends Action
$hasFN = ($this->profile->fullname) ? 'nickname url uid' : 'fn nickname url uid';
$this->element('a', array('href' => $this->profile->profileurl,
'rel' => 'me', 'class' => $hasFN),
- $this->profile->nickname);
+ $this->profile->nickname);
$this->elementEnd('dd');
$this->elementEnd('dl');
@@ -324,7 +333,7 @@ class ShowstreamAction extends Action
$this->elementStart('li', 'entity_edit');
$this->element('a', array('href' => common_local_url('profilesettings'),
'title' => _('Edit profile settings')),
- _('Edit'));
+ _('Edit'));
$this->elementEnd('li');
}
@@ -346,9 +355,8 @@ class ShowstreamAction extends Action
$this->elementEnd('li');
}
- $user = User::staticGet('id', $this->profile->id);
if ($cur && $cur->id != $user->id && $cur->mutuallySubscribed($user)) {
- $this->elementStart('li', 'entity_send-a-message');
+ $this->elementStart('li', 'entity_send-a-message');
$this->element('a', array('href' => common_local_url('newmessage', array('to' => $user->id)),
'title' => _('Send a direct message to this user')),
_('Message'));
@@ -490,7 +498,7 @@ class ShowstreamAction extends Action
$this->elementStart('dl', 'entity_member-since');
$this->element('dt', null, _('Member since'));
$this->element('dd', null, date('j M Y',
- strtotime($this->profile->created)));
+ strtotime($this->profile->created)));
$this->elementEnd('dl');
$this->elementStart('dl', 'entity_subscriptions');
diff --git a/config.php.sample b/config.php.sample
index db1a21663..a2c5801f4 100644
--- a/config.php.sample
+++ b/config.php.sample
@@ -142,3 +142,7 @@ $config['sphinx']['port'] = 3312;
# config section for the built-in Facebook application
#$config['facebook']['apikey'] = 'APIKEY';
#$config['facebook']['secret'] = 'SECRET';
+
+# Add Google Analytics
+# require_once('plugins/GoogleAnalyticsPlugin.php');
+# $ga = new GoogleAnalyticsPlugin('your secret code');
diff --git a/db/laconica.sql b/db/laconica.sql
index 012270b51..16f482134 100644
--- a/db/laconica.sql
+++ b/db/laconica.sql
@@ -258,7 +258,8 @@ create table notice_tag (
created datetime not null comment 'date this record was created',
constraint primary key (tag, notice_id),
- index notice_tag_created_idx (created)
+ index notice_tag_created_idx (created),
+ index notice_tag_notice_id_idx (notice_id)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
/* Synching with foreign services */
@@ -356,7 +357,8 @@ create table profile_tag (
constraint primary key (tagger, tagged, tag),
index profile_tag_modified_idx (modified),
- index profile_tag_tagger_tag_idx (tagger, tag)
+ index profile_tag_tagger_tag_idx (tagger, tag),
+ index profile_tag_tagged_idx (tagged)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
create table profile_block (
@@ -400,7 +402,9 @@ create table group_member (
created datetime not null comment 'date this record was created',
modified timestamp comment 'date this record was modified',
- constraint primary key (group_id, profile_id)
+ constraint primary key (group_id, profile_id),
+ index group_member_profile_id_idx (profile_id),
+ index group_member_created_idx (created)
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin;
diff --git a/index.php b/index.php
index 387b642e2..e62d9469a 100644
--- a/index.php
+++ b/index.php
@@ -22,6 +22,8 @@ define('LACONICA', true);
require_once INSTALLDIR . '/lib/common.php';
+// XXX: we need a little more structure in this script
+
// get and cache current user
$user = common_current_user();
@@ -45,16 +47,16 @@ if (!$user && common_config('site', 'private') &&
common_redirect(common_local_url('login'));
}
-$actionfile = INSTALLDIR."/actions/$action.php";
-
-if (file_exists($actionfile)) {
-
- include_once $actionfile;
-
- $action_class = ucfirst($action).'Action';
+$action_class = ucfirst($action).'Action';
+if (!class_exists($action_class)) {
+ $cac = new ClientErrorAction(_('Unknown action'), 404);
+ $cac->showPage();
+} else {
$action_obj = new $action_class();
+ // XXX: find somewhere for this little block to live
+
if ($config['db']['mirror'] && $action_obj->isReadOnly()) {
if (is_array($config['db']['mirror'])) {
// "load balancing", ha ha
@@ -66,9 +68,24 @@ if (file_exists($actionfile)) {
}
$config['db']['database'] = $mirror;
}
- if (call_user_func(array($action_obj, 'prepare'), $_REQUEST)) {
- call_user_func(array($action_obj, 'handle'), $_REQUEST);
+
+ try {
+ if ($action_obj->prepare($_REQUEST)) {
+ $action_obj->handle($_REQUEST);
+ }
+ } catch (ClientException $cex) {
+ $cac = new ClientErrorAction($cex->getMessage(), $cex->getCode());
+ $cac->showPage();
+ } catch (ServerException $sex) { // snort snort guffaw
+ $sac = new ServerErrorAction($sex->getMessage(), $sex->getCode());
+ $sac->showPage();
+ } catch (Exception $ex) {
+ $sac = new ServerErrorAction($ex->getMessage());
+ $sac->showPage();
}
-} else {
- common_user_error(_('Unknown action'));
-} \ No newline at end of file
+}
+
+// XXX: cleanup exit() calls or add an exit handler so
+// this always gets called
+
+Event::handle('CleanupPlugin');
diff --git a/lib/action.php b/lib/action.php
index c4172ada1..ce92addf5 100644
--- a/lib/action.php
+++ b/lib/action.php
@@ -179,18 +179,27 @@ class Action extends HTMLOutputter // lawsuit
*/
function showScripts()
{
- $this->element('script', array('type' => 'text/javascript',
- 'src' => common_path('js/jquery.min.js')),
- ' ');
- $this->element('script', array('type' => 'text/javascript',
- 'src' => common_path('js/jquery.form.js')),
- ' ');
- $this->element('script', array('type' => 'text/javascript',
- 'src' => common_path('js/xbImportNode.js')),
- ' ');
- $this->element('script', array('type' => 'text/javascript',
- 'src' => common_path('js/util.js?version='.LACONICA_VERSION)),
- ' ');
+ if (Event::handle('StartShowScripts', array($this))) {
+ if (Event::handle('StartShowJQueryScripts', array($this))) {
+ $this->element('script', array('type' => 'text/javascript',
+ 'src' => common_path('js/jquery.min.js')),
+ ' ');
+ $this->element('script', array('type' => 'text/javascript',
+ 'src' => common_path('js/jquery.form.js')),
+ ' ');
+ Event::handle('EndShowJQueryScripts', array($this));
+ }
+ if (Event::handle('StartShowLaconicaScripts', array($this))) {
+ $this->element('script', array('type' => 'text/javascript',
+ 'src' => common_path('js/xbImportNode.js')),
+ ' ');
+ $this->element('script', array('type' => 'text/javascript',
+ 'src' => common_path('js/util.js?version='.LACONICA_VERSION)),
+ ' ');
+ Event::handle('EndShowLaconicaScripts', array($this));
+ }
+ Event::handle('EndShowScripts', array($this));
+ }
}
/**
@@ -312,42 +321,46 @@ class Action extends HTMLOutputter // lawsuit
*/
function showPrimaryNav()
{
+ $user = common_current_user();
+
$this->elementStart('dl', array('id' => 'site_nav_global_primary'));
$this->element('dt', null, _('Primary site navigation'));
$this->elementStart('dd');
- $user = common_current_user();
$this->elementStart('ul', array('class' => 'nav'));
- if ($user) {
- $this->menuItem(common_local_url('all', array('nickname' => $user->nickname)),
- _('Home'), _('Personal profile and friends timeline'), false, 'nav_home');
- }
- $this->menuItem(common_local_url('peoplesearch'),
- _('Search'), _('Search for people or text'), false, 'nav_search');
- if ($user) {
- $this->menuItem(common_local_url('profilesettings'),
- _('Account'), _('Change your email, avatar, password, profile'), false, 'nav_account');
-
- if (common_config('xmpp', 'enabled')) {
- $this->menuItem(common_local_url('imsettings'),
- _('Connect'), _('Connect to IM, SMS, Twitter'), false, 'nav_connect');
- } else {
- $this->menuItem(common_local_url('smssettings'),
- _('Connect'), _('Connect to SMS, Twitter'), false, 'nav_connect');
+ if (Event::handle('StartPrimaryNav', array($this))) {
+ if ($user) {
+ $this->menuItem(common_local_url('all', array('nickname' => $user->nickname)),
+ _('Home'), _('Personal profile and friends timeline'), false, 'nav_home');
}
- $this->menuItem(common_local_url('logout'),
- _('Logout'), _('Logout from the site'), false, 'nav_logout');
- } else {
- $this->menuItem(common_local_url('login'),
- _('Login'), _('Login to the site'), false, 'nav_login');
- if (!common_config('site', 'closed')) {
- $this->menuItem(common_local_url('register'),
- _('Register'), _('Create an account'), false, 'nav_register');
+ $this->menuItem(common_local_url('peoplesearch'),
+ _('Search'), _('Search for people or text'), false, 'nav_search');
+ if ($user) {
+ $this->menuItem(common_local_url('profilesettings'),
+ _('Account'), _('Change your email, avatar, password, profile'), false, 'nav_account');
+
+ if (common_config('xmpp', 'enabled')) {
+ $this->menuItem(common_local_url('imsettings'),
+ _('Connect'), _('Connect to IM, SMS, Twitter'), false, 'nav_connect');
+ } else {
+ $this->menuItem(common_local_url('smssettings'),
+ _('Connect'), _('Connect to SMS, Twitter'), false, 'nav_connect');
+ }
+ $this->menuItem(common_local_url('logout'),
+ _('Logout'), _('Logout from the site'), false, 'nav_logout');
+ } else {
+ $this->menuItem(common_local_url('login'),
+ _('Login'), _('Login to the site'), false, 'nav_login');
+ if (!common_config('site', 'closed')) {
+ $this->menuItem(common_local_url('register'),
+ _('Register'), _('Create an account'), false, 'nav_register');
+ }
+ $this->menuItem(common_local_url('openidlogin'),
+ _('OpenID'), _('Login with OpenID'), false, 'nav_openid');
}
- $this->menuItem(common_local_url('openidlogin'),
- _('OpenID'), _('Login with OpenID'), false, 'nav_openid');
+ $this->menuItem(common_local_url('doc', array('title' => 'help')),
+ _('Help'), _('Help me!'), false, 'nav_help');
+ Event::handle('EndPrimaryNav', array($this));
}
- $this->menuItem(common_local_url('doc', array('title' => 'help')),
- _('Help'), _('Help me!'), false, 'nav_help');
$this->elementEnd('ul');
$this->elementEnd('dd');
$this->elementEnd('dl');
@@ -511,12 +524,16 @@ class Action extends HTMLOutputter // lawsuit
*
* @return nothing
*/
+
function showAside()
{
$this->elementStart('div', array('id' => 'aside_primary',
'class' => 'aside'));
$this->showExportData();
- $this->showSections();
+ if (Event::handle('StartShowSections', array($this))) {
+ $this->showSections();
+ Event::handle('EndShowSections', array($this));
+ }
$this->elementEnd('div');
}
@@ -570,18 +587,21 @@ class Action extends HTMLOutputter // lawsuit
$this->element('dt', null, _('Secondary site navigation'));
$this->elementStart('dd', null);
$this->elementStart('ul', array('class' => 'nav'));
- $this->menuItem(common_local_url('doc', array('title' => 'help')),
- _('Help'));
- $this->menuItem(common_local_url('doc', array('title' => 'about')),
- _('About'));
- $this->menuItem(common_local_url('doc', array('title' => 'faq')),
- _('FAQ'));
- $this->menuItem(common_local_url('doc', array('title' => 'privacy')),
- _('Privacy'));
- $this->menuItem(common_local_url('doc', array('title' => 'source')),
- _('Source'));
- $this->menuItem(common_local_url('doc', array('title' => 'contact')),
- _('Contact'));
+ if (Event::handle('StartSecondaryNav', array($this))) {
+ $this->menuItem(common_local_url('doc', array('title' => 'help')),
+ _('Help'));
+ $this->menuItem(common_local_url('doc', array('title' => 'about')),
+ _('About'));
+ $this->menuItem(common_local_url('doc', array('title' => 'faq')),
+ _('FAQ'));
+ $this->menuItem(common_local_url('doc', array('title' => 'privacy')),
+ _('Privacy'));
+ $this->menuItem(common_local_url('doc', array('title' => 'source')),
+ _('Source'));
+ $this->menuItem(common_local_url('doc', array('title' => 'contact')),
+ _('Contact'));
+ Event::handle('EndSecondaryNav', array($this));
+ }
$this->elementEnd('ul');
$this->elementEnd('dd');
$this->elementEnd('dl');
@@ -789,11 +809,12 @@ class Action extends HTMLOutputter // lawsuit
*
* @return nothing
*/
+
function serverError($msg, $code=500)
{
$action = $this->trimmed('action');
common_debug("Server error '$code' on '$action': $msg", __FILE__);
- common_server_error($msg, $code);
+ throw new ServerException($msg, $code);
}
/**
@@ -804,11 +825,12 @@ class Action extends HTMLOutputter // lawsuit
*
* @return nothing
*/
+
function clientError($msg, $code=400)
{
$action = $this->trimmed('action');
common_debug("User error '$code' on '$action': $msg", __FILE__);
- common_user_error($msg, $code);
+ throw new ClientException($msg, $code);
}
/**
diff --git a/lib/clientexception.php b/lib/clientexception.php
new file mode 100644
index 000000000..3020d7f50
--- /dev/null
+++ b/lib/clientexception.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * class for a client exception (user error)
+ *
+ * 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 Exception
+ * @package Laconica
+ * @author Evan Prodromou <evan@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 client exceptions
+ *
+ * Subclass of PHP Exception for user errors.
+ *
+ * @category Exception
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+class ClientException extends Exception
+{
+ public function __construct($message = null, $code = 400) {
+ parent::__construct($message, $code);
+ }
+
+ // custom string representation of object
+ public function __toString() {
+ return __CLASS__ . ": [{$this->code}]: {$this->message}\n";
+ }
+}
diff --git a/lib/common.php b/lib/common.php
index 5b4e3c40c..7bfd14c42 100644
--- a/lib/common.php
+++ b/lib/common.php
@@ -49,6 +49,12 @@ require_once('DB/DataObject/Cast.php'); # for dates
require_once(INSTALLDIR.'/lib/language.php');
+// This gets included before the config file, so that admin code and plugins
+// can use it
+
+require_once(INSTALLDIR.'/lib/event.php');
+require_once(INSTALLDIR.'/lib/plugin.php');
+
// try to figure out where we are
$_server = array_key_exists('SERVER_NAME', $_SERVER) ?
@@ -177,6 +183,8 @@ foreach ($_config_files as $_config_file) {
}
}
+// XXX: how many of these could be auto-loaded on use?
+
require_once('Validate.php');
require_once('markdown.php');
@@ -188,6 +196,9 @@ require_once(INSTALLDIR.'/lib/subs.php');
require_once(INSTALLDIR.'/lib/Shorturl_api.php');
require_once(INSTALLDIR.'/lib/twitter.php');
+require_once(INSTALLDIR.'/lib/clientexception.php');
+require_once(INSTALLDIR.'/lib/serverexception.php');
+
// XXX: other formats here
define('NICKNAME_FMT', VALIDATE_NUM.VALIDATE_ALPHA_LOWER);
@@ -200,5 +211,12 @@ function __autoload($class)
require_once(INSTALLDIR.'/classes/' . $class . '.php');
} else if (file_exists(INSTALLDIR.'/lib/' . strtolower($class) . '.php')) {
require_once(INSTALLDIR.'/lib/' . strtolower($class) . '.php');
+ } else if (mb_substr($class, -6) == 'Action' &&
+ file_exists(INSTALLDIR.'/actions/' . strtolower(mb_substr($class, 0, -6)) . '.php')) {
+ require_once(INSTALLDIR.'/actions/' . strtolower(mb_substr($class, 0, -6)) . '.php');
}
}
+
+// Give plugins a chance to initialize in a fully-prepared environment
+
+Event::handle('InitializePlugin');
diff --git a/lib/event.php b/lib/event.php
new file mode 100644
index 000000000..d815ae54b
--- /dev/null
+++ b/lib/event.php
@@ -0,0 +1,113 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * utilities for defining and running event handlers
+ *
+ * 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 Event
+ * @package Laconica
+ * @author Evan Prodromou <evan@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 events
+ *
+ * This "class" two static functions for managing events in the Laconica code.
+ *
+ * @category Event
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ *
+ * @todo Define a system for using Event instances
+ */
+
+class Event {
+
+ /* Global array of hooks, mapping eventname => array of callables */
+
+ protected static $_handlers = array();
+
+ /**
+ * Add an event handler
+ *
+ * To run some code at a particular point in Laconica processing.
+ * Named events include receiving an XMPP message, adding a new notice,
+ * or showing part of an HTML page.
+ *
+ * The arguments to the handler vary by the event. Handlers can return
+ * two possible values: false means that the event has been replaced by
+ * the handler completely, and no default processing should be done.
+ * Non-false means successful handling, and that the default processing
+ * should succeed. (Note that this only makes sense for some events.)
+ *
+ * Handlers can also abort processing by throwing an exception; these will
+ * be caught by the closest code and displayed as errors.
+ *
+ * @param string $name Name of the event
+ * @param callable $handler Code to run
+ *
+ * @return void
+ */
+
+ public static function addHandler($name, $handler) {
+ if (array_key_exists($name, Event::$_handlers)) {
+ Event::$_handlers[$name][] = $handler;
+ } else {
+ Event::$_handlers[$name] = array($handler);
+ }
+ }
+
+ /**
+ * Handle an event
+ *
+ * Events are any point in the code that we want to expose for admins
+ * or third-party developers to use.
+ *
+ * We pass in an array of arguments (including references, for stuff
+ * that can be changed), and each assigned handler gets run with those
+ * arguments. Exceptions can be thrown to indicate an error.
+ *
+ * @param string $name Name of the event that's happening
+ * @param array $args Arguments for handlers
+ *
+ * @return boolean flag saying whether to continue processing, based
+ * on results of handlers.
+ */
+
+ public static function handle($name, $args=array()) {
+ $result = null;
+ if (array_key_exists($name, Event::$_handlers)) {
+ foreach (Event::$_handlers[$name] as $handler) {
+ $result = call_user_func_array($handler, $args);
+ if ($result === false) {
+ break;
+ }
+ }
+ }
+ return ($result !== false);
+ }
+}
diff --git a/lib/imagefile.php b/lib/imagefile.php
index db344db8f..0c93b257e 100644
--- a/lib/imagefile.php
+++ b/lib/imagefile.php
@@ -68,17 +68,17 @@ class ImageFile
static function fromUpload($param='upload')
{
switch ($_FILES[$param]['error']) {
- case UPLOAD_ERR_OK: // success, jump out
+ case UPLOAD_ERR_OK: // success, jump out
break;
- case UPLOAD_ERR_INI_SIZE:
- case UPLOAD_ERR_FORM_SIZE:
+ case UPLOAD_ERR_INI_SIZE:
+ case UPLOAD_ERR_FORM_SIZE:
throw new Exception(sprintf(_('That file is too big. The maximum file size is %d.'), $this->maxFileSize()));
return;
- case UPLOAD_ERR_PARTIAL:
+ case UPLOAD_ERR_PARTIAL:
@unlink($_FILES[$param]['tmp_name']);
throw new Exception(_('Partial upload.'));
return;
- default:
+ default:
throw new Exception(_('System error uploading file.'));
return;
}
@@ -113,6 +113,23 @@ class ImageFile
return;
}
+ // Don't crop/scale if it isn't necessary
+ if ($size === $this->width
+ && $size === $this->height
+ && $x === 0
+ && $y === 0
+ && $w === $this->width
+ && $h === $this->height) {
+
+ $outname = Avatar::filename($this->id,
+ image_type_to_extension($this->type),
+ $size,
+ common_timestamp());
+ $outpath = Avatar::path($outname);
+ @copy($this->filepath, $outpath);
+ return $outname;
+ }
+
switch ($this->type) {
case IMAGETYPE_GIF:
$image_src = imagecreatefromgif($this->filepath);
@@ -154,9 +171,9 @@ class ImageFile
imagecopyresampled($image_dest, $image_src, 0, 0, $x, $y, $size, $size, $w, $h);
$outname = Avatar::filename($this->id,
- image_type_to_extension($this->type),
- $size,
- common_timestamp());
+ image_type_to_extension($this->type),
+ $size,
+ common_timestamp());
$outpath = Avatar::path($outname);
@@ -165,7 +182,7 @@ class ImageFile
imagegif($image_dest, $outpath);
break;
case IMAGETYPE_JPEG:
- imagejpeg($image_dest, $outpath);
+ imagejpeg($image_dest, $outpath, 100);
break;
case IMAGETYPE_PNG:
imagepng($image_dest, $outpath);
@@ -175,6 +192,9 @@ class ImageFile
return;
}
+ imagedestroy($image_src);
+ imagedestroy($image_dest);
+
return $outname;
}
@@ -209,12 +229,12 @@ class ImageFile
$num = substr($str, 0, -1);
switch(strtoupper($unit)){
- case 'G':
- $num *= 1024;
- case 'M':
- $num *= 1024;
- case 'K':
- $num *= 1024;
+ case 'G':
+ $num *= 1024;
+ case 'M':
+ $num *= 1024;
+ case 'K':
+ $num *= 1024;
}
return $num;
diff --git a/lib/plugin.php b/lib/plugin.php
new file mode 100644
index 000000000..7b2436e54
--- /dev/null
+++ b/lib/plugin.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Utility class for plugins
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 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);
+}
+
+/**
+ * Base class for plugins
+ *
+ * A base class for Laconica plugins. Mostly a light wrapper around
+ * the Event framework.
+ *
+ * Subclasses of Plugin will automatically handle an event if they define
+ * a method called "onEventName". (Well, OK -- only if they call parent::__construct()
+ * in their constructors.)
+ *
+ * They will also automatically handle the InitializePlugin and CleanupPlugin with the
+ * initialize() and cleanup() methods, respectively.
+ *
+ * @category Plugin
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ *
+ * @see Event
+ */
+
+class Plugin
+{
+ function __construct()
+ {
+ Event::addHandler('InitializePlugin', array($this, 'initialize'));
+ Event::addHandler('CleanupPlugin', array($this, 'cleanup'));
+
+ foreach (get_class_methods($this) as $method) {
+ if (mb_substr($method, 0, 2) == 'on') {
+ Event::addHandler(mb_substr($method, 2), array($this, $method));
+ }
+ }
+ }
+
+ function initialize()
+ {
+ return true;
+ }
+
+ function cleanup()
+ {
+ return true;
+ }
+}
diff --git a/lib/serverexception.php b/lib/serverexception.php
new file mode 100644
index 000000000..b8ed6846e
--- /dev/null
+++ b/lib/serverexception.php
@@ -0,0 +1,55 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * class for a server exception (user error)
+ *
+ * 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 Exception
+ * @package Laconica
+ * @author Evan Prodromou <evan@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 server exceptions
+ *
+ * Subclass of PHP Exception for server errors. The user typically can't fix these.
+ *
+ * @category Exception
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ */
+
+class ServerException extends Exception
+{
+ public function __construct($message = null, $code = 400) {
+ parent::__construct($message, $code);
+ }
+
+ public function __toString() {
+ return __CLASS__ . ": [{$this->code}]: {$this->message}\n";
+ }
+}
diff --git a/lib/util.php b/lib/util.php
index 7ce4e229e..c5a092f63 100644
--- a/lib/util.php
+++ b/lib/util.php
@@ -478,7 +478,7 @@ function common_linkify($url) {
}
else $title = '';
- return "<a href=\"$url\" $title class=\"extlink\">$display</a>";
+ return "<a href=\"$url\" $title rel=\"external\">$display</a>";
}
function common_longurl($short_url)
diff --git a/plugins/GoogleAnalyticsPlugin.php b/plugins/GoogleAnalyticsPlugin.php
new file mode 100644
index 000000000..87a70e31e
--- /dev/null
+++ b/plugins/GoogleAnalyticsPlugin.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * Laconica, the distributed open-source microblogging tool
+ *
+ * Plugin to use Google Analytics
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @category Plugin
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @copyright 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);
+}
+
+/**
+ * Plugin to use Google Analytics
+ *
+ * This plugin will spoot out the correct JavaScript spell to invoke Google Analytics on a page.
+ *
+ * Note that Google Analytics is not compatible with the Franklin Street Statement; consider using
+ * Pikiw (http://www.pikiw.org/) instead!
+ *
+ * @category Plugin
+ * @package Laconica
+ * @author Evan Prodromou <evan@controlyourself.ca>
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://laconi.ca/
+ *
+ * @see Event
+ */
+
+class GoogleAnalyticsPlugin extends Plugin
+{
+ var $code = null;
+
+ function __construct($code=null)
+ {
+ $this->code = $code;
+ parent::__construct();
+ }
+
+ function onEndShowScripts($action)
+ {
+ $js1 = 'var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");'.
+ 'document.write(unescape("%3Cscript src=\'" + gaJsHost + "google-analytics.com/ga.js\' type=\'text/javascript\'%3E%3C/script%3E"));';
+ $js2 = sprintf('try{'.
+ 'var pageTracker = _gat._getTracker("%s");'.
+ 'pageTracker._trackPageview();'.
+ '} catch(err) {}',
+ $this->code);
+ $action->elementStart('script', array('type' => 'text/javascript'));
+ $action->raw($js1);
+ $action->elementEnd('script');
+ $action->elementStart('script', array('type' => 'text/javascript'));
+ $action->raw($js2);
+ $action->elementEnd('script');
+ }
+}