diff options
Diffstat (limited to 'plugins/Sample')
-rw-r--r-- | plugins/Sample/SamplePlugin.php | 260 | ||||
-rw-r--r-- | plugins/Sample/User_greeting_count.php | 183 | ||||
-rw-r--r-- | plugins/Sample/hello.php | 166 |
3 files changed, 590 insertions, 19 deletions
diff --git a/plugins/Sample/SamplePlugin.php b/plugins/Sample/SamplePlugin.php index 6e361aafb..913741226 100644 --- a/plugins/Sample/SamplePlugin.php +++ b/plugins/Sample/SamplePlugin.php @@ -1,8 +1,12 @@ <?php -/* +/** * StatusNet - the distributed open-source microblogging tool * Copyright (C) 2009, StatusNet, Inc. * + * A sample module to show best practices for StatusNet plugins + * + * PHP version 5 + * * 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 @@ -15,44 +19,262 @@ * * 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 Sample + * @package StatusNet + * @author Brion Vibber <brionv@status.net> + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ */ -/** - * @package SamplePlugin - * @maintainer Your Name <you@example.com> - */ - -if (!defined('STATUSNET') && !defined('LACONICA')) { +if (!defined('STATUSNET')) { // This check helps protect against security problems; // your code file can't be executed directly from the web. exit(1); } +/** + * Sample plugin main class + * + * Each plugin requires a main class to interact with the StatusNet system. + * + * The main class usually extends the Plugin class that comes with StatusNet. + * + * The class has standard-named methods that will be called when certain events + * happen in the code base. These methods have names like 'onX' where X is an + * event name (see EVENTS.txt for the list of available events). Event handlers + * have pre-defined arguments, based on which event they're handling. A typical + * event handler: + * + * function onSomeEvent($paramA, &$paramB) + * { + * if ($paramA == 'jed') { + * throw new Exception(sprintf(_m("Invalid parameter %s"), $paramA)); + * } + * $paramB = 'spock'; + * return true; + * } + * + * Event handlers must return a boolean value. If they return false, all other + * event handlers for this event (in other plugins) will be skipped, and in some + * cases the default processing for that event would be skipped. This is great for + * replacing the default action of an event. + * + * If the handler returns true, processing of other event handlers and the default + * processing will continue. This is great for extending existing functionality. + * + * If the handler throws an exception, processing will stop, and the exception's + * error will be shown to the user. + * + * To install a plugin (like this one), site admins add the following code to + * their config.php file: + * + * addPlugin('Sample'); + * + * Plugins must be installed in one of the following directories: + * + * local/plugins/{$pluginclass}.php + * local/plugins/{$name}/{$pluginclass}.php + * local/{$pluginclass}.php + * local/{$name}/{$pluginclass}.php + * plugins/{$pluginclass}.php + * plugins/{$name}/{$pluginclass}.php + * + * Here, {$name} is the name of the plugin, like 'Sample', and {$pluginclass} is + * the name of the main class, like 'SamplePlugin'. Plugins that are part of the + * main StatusNet distribution go in 'plugins' and third-party or local ones go + * in 'local'. + * + * Simple plugins can be implemented as a single module. Others are more complex + * and require additional modules; these should use their own directory, like + * 'local/plugins/{$name}/'. All files related to the plugin, including images, + * JavaScript, CSS, external libraries or PHP modules should go in the plugin + * directory. + * + * @category Sample + * @package StatusNet + * @author Brion Vibber <brionv@status.net> + * @author Evan Prodromou <evan@status.net> + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + class SamplePlugin extends Plugin { - function onInitializePlugin() + /** + * Plugins are configured using public instance attributes. To set + * their values, site administrators use this syntax: + * + * addPlugin('Sample', array('attr1' => 'foo', 'attr2' => 'bar')); + * + * The same plugin class can be initialized multiple times with different + * arguments: + * + * addPlugin('EmailNotify', array('sendTo' => 'evan@status.net')); + * addPlugin('EmailNotify', array('sendTo' => 'brionv@status.net')); + * + */ + + public $attr1 = null; + public $attr2 = null; + + /** + * Initializer for this plugin + * + * Plugins overload this method to do any initialization they need, + * like connecting to remote servers or creating paths or so on. + * + * @return boolean hook value; true means continue processing, false means stop. + */ + + function initialize() + { + return true; + } + + /** + * Cleanup for this plugin + * + * Plugins overload this method to do any cleanup they need, + * like disconnecting from remote servers or deleting temp files or so on. + * + * @return boolean hook value; true means continue processing, false means stop. + */ + + function cleanup() { - // Event handlers normally return true to indicate that all is well. - // - // Returning false will cancel further processing of any other - // plugins or core code hooking the same event. return true; } /** - * Hook for RouterInitialized event. + * Database schema setup + * + * Plugins can add their own tables to the StatusNet database. Plugins + * should use StatusNet's schema interface to add or delete tables. The + * ensureTable() method provides an easy way to ensure a table's structure + * and availability. + * + * By default, the schema is checked every time StatusNet is run (say, when + * a Web page is hit). Admins can configure their systems to only check the + * schema when the checkschema.php script is run, greatly improving performance. + * However, they need to remember to run that script after installing or + * upgrading a plugin! + * + * @see Schema + * @see ColumnDef + * + * @return boolean hook value; true means continue processing, false means stop. + */ + + function onCheckSchema() + { + $schema = Schema::get(); + + // For storing user-submitted flags on profiles + + $schema->ensureTable('user_greeting_count', + array(new ColumnDef('user_id', 'integer', null, + true, 'PRI'), + new ColumnDef('greeting_count', 'integer'))); + + return true; + } + + /** + * Load related modules when needed + * + * Most non-trivial plugins will require extra modules to do their work. Typically + * these include data classes, action classes, widget classes, or external libraries. + * + * This method receives a class name and loads the PHP file related to that class. By + * tradition, action classes typically have files named for the action, all lower-case. + * Data classes are in files with the data class name, initial letter capitalized. + * + * Note that this method will be called for *all* overloaded classes, not just ones + * in this plugin! So, make sure to return true by default to let other plugins, and + * the core code, get a chance. + * + * @param string $cls Name of the class to be loaded + * + * @return boolean hook value; true means continue processing, false means stop. + */ + + function onAutoload($cls) + { + $dir = dirname(__FILE__); + + switch ($cls) + { + case 'HelloAction': + include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; + return false; + case 'User_greeting_count': + include_once $dir . '/'.$cls.'.php'; + return false; + default: + return true; + } + } + + /** + * Map URLs to actions + * + * This event handler lets the plugin map URLs on the site to actions (and + * thus an action handler class). Note that the action handler class for an + * action will be named 'FoobarAction', where action = 'foobar'. The class + * must be loaded in the onAutoload() method. * * @param Net_URL_Mapper $m path-to-action mapper - * @return boolean hook return + * + * @return boolean hook value; true means continue processing, false means stop. */ function onRouterInitialized($m) { - $m->connect(':nickname/samples', - array('action' => 'showsamples'), - array('feed' => '[A-Za-z0-9_-]+')); - $m->connect('settings/sample', - array('action' => 'samplesettings')); + $m->connect('main/hello', + array('action' => 'hello')); + return true; + } + + /** + * Modify the default menu to link to our custom action + * + * Using event handlers, it's possible to modify the default UI for pages + * almost without limit. In this method, we add a menu item to the default + * primary menu for the interface to link to our action. + * + * The Action class provides a rich set of events to hook, as well as output + * methods. + * + * @param Action $action The current action handler. Use this to + * do any output. + * + * @return boolean hook value; true means continue processing, false means stop. + * + * @see Action + */ + + function onEndPrimaryNav($action) + { + // common_local_url() gets the correct URL for the action name + // we provide + + $action->menuItem(common_local_url('hello'), + _m('Hello'), _m('A warm greeting'), false, 'nav_hello'); + return true; + } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'Sample', + 'version' => STATUSNET_VERSION, + 'author' => 'Brion Vibber, Evan Prodromou', + 'homepage' => 'http://status.net/wiki/Plugin:Sample', + 'rawdescription' => + _m('A sample plugin to show basics of development for new hackers.')); return true; } } diff --git a/plugins/Sample/User_greeting_count.php b/plugins/Sample/User_greeting_count.php new file mode 100644 index 000000000..d9a59770d --- /dev/null +++ b/plugins/Sample/User_greeting_count.php @@ -0,0 +1,183 @@ +<?php +/** + * Data class for counting greetings + * + * PHP version 5 + * + * @category Data + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2009, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +require_once INSTALLDIR . '/classes/Memcached_DataObject.php'; + +/** + * Data class for counting greetings + * + * We use the DB_DataObject framework for data classes in StatusNet. Each + * table maps to a particular data class, making it easier to manipulate + * data. + * + * Data classes should extend Memcached_DataObject, the (slightly misnamed) + * extension of DB_DataObject that provides caching, internationalization, + * and other bits of good functionality to StatusNet-specific data classes. + * + * @category Action + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see DB_DataObject + */ + +class User_greeting_count extends Memcached_DataObject +{ + public $__table = 'user_greeting_count'; // table name + public $user_id; // int(4) primary_key not_null + public $greeting_count; // int(4) + + /** + * Get an instance by key + * + * This is a utility method to get a single instance with a given key value. + * + * @param string $k Key to use to lookup (usually 'user_id' for this class) + * @param mixed $v Value to lookup + * + * @return User_greeting_count object found, or null for no hits + * + */ + + function staticGet($k, $v=null) + { + return Memcached_DataObject::staticGet('User_greeting_count', $k, $v); + } + + /** + * return table definition for DB_DataObject + * + * DB_DataObject needs to know something about the table to manipulate + * instances. This method provides all the DB_DataObject needs to know. + * + * @return array array of column definitions + */ + + function table() + { + return array('user_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'greeting_count' => DB_DATAOBJECT_INT); + } + + /** + * return key definitions for DB_DataObject + * + * DB_DataObject needs to know about keys that the table has; this function + * defines them. + * + * @return array key definitions + */ + + function keys() + { + return array('user_id' => 'K'); + } + + /** + * return key definitions for Memcached_DataObject + * + * Our caching system uses the same key definitions, but uses a different + * method to get them. + * + * @return array key definitions + */ + + function keyTypes() + { + return $this->keys(); + } + + /** + * Magic formula for non-autoincrementing integer primary keys + * + * If a table has a single integer column as its primary key, DB_DataObject + * assumes that the column is auto-incrementing and makes a sequence table + * to do this incrementation. Since we don't need this for our class, we + * overload this method and return the magic formula that DB_DataObject needs. + * + * @return array magic three-false array that stops auto-incrementing. + */ + + function sequenceKey() + { + return array(false, false, false); + } + + /** + * Increment a user's greeting count and return instance + * + * This method handles the ins and outs of creating a new greeting_count for a + * user or fetching the existing greeting count and incrementing its value. + * + * @param integer $user_id ID of the user to get a count for + * + * @return User_greeting_count instance for this user, with count already incremented. + */ + + static function inc($user_id) + { + $gc = User_greeting_count::staticGet('user_id', $user_id); + + if (empty($gc)) { + + $gc = new User_greeting_count(); + + $gc->user_id = $user_id; + $gc->greeting_count = 1; + + $result = $gc->insert(); + + if (!$result) { + throw Exception(sprintf(_m("Could not save new greeting count for %d"), + $user_id)); + } + + } else { + + $orig = clone($gc); + + $gc->greeting_count++; + + $result = $gc->update($orig); + + if (!$result) { + throw Exception(sprintf(_m("Could not increment greeting count for %d"), + $user_id)); + } + } + + return $gc; + } +} diff --git a/plugins/Sample/hello.php b/plugins/Sample/hello.php new file mode 100644 index 000000000..0cfd8a1c3 --- /dev/null +++ b/plugins/Sample/hello.php @@ -0,0 +1,166 @@ +<?php +/** + * Give a warm greeting to our friendly user + * + * PHP version 5 + * + * @category Sample + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2009, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Give a warm greeting to our friendly user + * + * This sample action shows some basic ways of doing output in an action + * class. + * + * Action classes have several output methods that they override from + * the parent class. + * + * @category Sample + * @package StatusNet + * @author Evan Prodromou <evan@status.net> + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + */ + +class HelloAction extends Action +{ + var $user = null; + var $gc = null; + + /** + * Take arguments for running + * + * This method is called first, and it lets the action class get + * all its arguments and validate them. It's also the time + * to fetch any relevant data from the database. + * + * Action classes should run parent::prepare($args) as the first + * line of this method to make sure the default argument-processing + * happens. + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + */ + + function prepare($args) + { + parent::prepare($args); + + $this->user = common_current_user(); + + if (!empty($this->user)) { + $this->gc = User_greeting_count::inc($this->user->id); + } + + return true; + } + + /** + * Handle request + * + * This is the main method for handling a request. Note that + * most preparation should be done in the prepare() method; + * by the time handle() is called the action should be + * more or less ready to go. + * + * @param array $args $_REQUEST args; handled in prepare() + * + * @return void + */ + + function handle($args) + { + parent::handle($args); + + $this->showPage(); + } + + /** + * Title of this page + * + * Override this method to show a custom title. + * + * @return string Title of the page + */ + + function title() + { + if (empty($this->user)) { + return _m('Hello'); + } else { + return sprintf(_m('Hello, %s'), $this->user->nickname); + } + } + + /** + * show content in the content area + * + * The default StatusNet page has a lot of decorations: menus, + * logos, tabs, all that jazz. This method is used to show + * content in the content area of the page; it's the main + * thing you want to overload. + * + * @return void + */ + + function showContent() + { + if (empty($this->user)) { + $this->element('p', array('class' => 'greeting'), + _m('Hello, stranger!')); + } else { + $this->element('p', array('class' => 'greeting'), + sprintf(_m('Hello, %s'), $this->user->nickname)); + $this->element('p', array('class' => 'greeting_count'), + sprintf(_m('I have greeted you %d time(s).'), + $this->gc->greeting_count)); + } + } + + /** + * Return true if read only. + * + * Some actions only read from the database; others read and write. + * The simple database load-balancer built into StatusNet will + * direct read-only actions to database mirrors (if they are configured), + * and read-write actions to the master database. + * + * This defaults to false to avoid data integrity issues, but you + * should make sure to overload it for performance gains. + * + * @param array $args other arguments, if RO/RW status depends on them. + * + * @return boolean is read only action? + */ + + function isReadOnly($args) + { + return false; + } +} |