1
0
Fork 0
mirror of https://github.com/Yetangitu/ampache synced 2025-10-03 09:49:30 +02:00

Plugin work. Plugins are now pluggable: no plugin-specific code in the main Ampache

code.  Plugins are now updatable, if configuration changes are needed for a new
version.
This commit is contained in:
Paul 'flowerysong' Arthur 2010-06-22 19:00:45 +00:00
parent 91eab20868
commit a66bf4c5f6
11 changed files with 285 additions and 155 deletions

View file

@ -123,7 +123,19 @@ switch ($_REQUEST['action']) {
show_confirmation($title,$body,$url);
break;
case 'upgrade_plugin':
/* Verify that this plugin exists */
$plugins = Plugin::get_plugins();
if (!array_key_exists($_REQUEST['plugin'],$plugins)) {
debug_event('plugins','Error: Invalid Plugin: ' . $_REQUEST['plugin'] . ' selected','1');
break;
}
$plugin = new Plugin($_REQUEST['plugin']);
$plugin->upgrade();
User::rebuild_all_preferences();
$url = Config::get('web_path') . '/admin/modules.php?action=show_plugins';
$title = _('Plugin Upgraded');
$body = '';
show_confirmation($title, $body, $url);
break;
case 'show_plugins':
$plugins = Plugin::get_plugins();

View file

@ -4,6 +4,7 @@
--------------------------------------------------------------------------
v.3.6-Alpha1
- New plugin architecture
- Fixed display charset issue with catalog add/update
- Fixed handling of temporary playlists with >100 items
- Changed Browse from a singleton to multiple instances

25
docs/PLUGINS Normal file
View file

@ -0,0 +1,25 @@
Plugins are placed in modules/plugins; the name of the file must be
<Name>.plugin.php, e.g. Dummy.plugin.php. The file must declare a
corresponding class and the name of the class must be prefixed with
Ampache, e.g. AmpacheDummy.
The following public variables must be declared:
name (string)
description (string)
version (int) - This plugin's version
min_ampache (int) - Minimum Ampache DB version required
max_ampache (int) - Maximum Ampache DB version supported
The following public methods must be implemented:
install
uninstall
load
The following public methods may be implemented:
upgrade
Finally, for the plugin to actually be useful one or more of the following hooks
should be implemented as a public method:
save_rating(Rating $rating, int $new_value)
save_songplay(Song $song)

View file

@ -71,7 +71,7 @@ class Plugin {
* get_plugins
* This returns an array of plugin names
*/
public static function get_plugins() {
public static function get_plugins($type='') {
$results = array();
@ -87,11 +87,24 @@ class Plugin {
// Ignore non-plugin files
if (substr($file,-10,10) != 'plugin.php') { continue; }
if (is_dir($file)) { continue; }
// It's a plugin record it
$plugin_name = basename($file,'.plugin.php');
if ($type != '') {
$plugin = new Plugin($plugin_name);
if (! Plugin::is_installed($plugin->_plugin->name)) {
debug_event('Plugins', 'Plugin ' . $plugin->_plugin->name . ' is not installed, skipping', 5);
continue;
}
if (! $plugin->is_valid()) {
debug_event('Plugins', 'Plugin ' . $plugin_name . ' is not valid, skipping', 5);
continue;
}
if (! method_exists($plugin->_plugin, $type)) {
debug_event('Plugins', 'Plugin ' . $plugin_name . ' does not support ' . $type . ', skipping', 5);
continue;
}
}
// It's a plugin record it
$results[$plugin_name] = $plugin_name;
} // end while
// Little stupid but hey
@ -104,9 +117,10 @@ class Plugin {
/**
* is_valid
* This checks to make sure the plugin has the required functions and
* settings, Ampache requires Name/Description/Version (Int) and a
* install & uninstall method and Ampache must be within the min/max
* version specifications
* settings. Ampache requires public variables name, description, and
* version (as an int), and methods install, uninstall, and load. We
* also check that Ampache's database version falls within the min/max
* version specified by the plugin.
*/
function is_valid() {
@ -130,6 +144,10 @@ class Plugin {
return false;
}
if (!method_exists($this->_plugin,'load')) {
return false;
}
/* Make sure it's within the version confines */
$db_version = $this->get_ampache_db_version();
@ -141,15 +159,15 @@ class Plugin {
return false;
}
/* We've passed all of the tests its good */
// We've passed all of the tests
return true;
} // is_valid
/**
* is_installed
* This checks to see if the specified plugin is currently installed in the
* database, it doesn't check the files for integrity
* This checks to see if the specified plugin is currently installed in
* the database, it doesn't check the files for integrity
*/
public static function is_installed($plugin_name) {
@ -160,27 +178,22 @@ class Plugin {
/**
* install
* This runs the install function of the plugin (must be called install)
* at the end it inserts a row into the update_info table to indicate
* That it's installed
* This runs the install function of the plugin and inserts a row into
* the update_info table to indicate that it's installed.
*/
public function install() {
$installed = $this->_plugin->install();
$version = $this->set_plugin_version($this->_plugin->version);
if (!$installed OR !$version) { return false; }
if ($this->_plugin->install() &&
$this->set_plugin_version($this->_plugin->version)) {
return true;
}
return false;
} // install
/**
* uninstall
* This runs the uninstall function of the plugin (must be called uninstall)
* at the end it removes the row from the update_info table to indicate
* that it isn't installed
* This runs the uninstall function of the plugin and removes the row
* from the update_info table to indicate that it isn't installed.
*/
public function uninstall() {
@ -190,6 +203,28 @@ class Plugin {
} // uninstall
/**
* upgrade
* This runs the upgrade function of the plugin (if it exists) and
* updates the database to indicate our new version.
*/
public function upgrade() {
if (method_exists($this->_plugin, 'upgrade')) {
if($this->_plugin->upgrade()) {
$this->set_plugin_version($this->_plugin->version);
}
}
} // upgrade
/**
* load
* This calls the plugin's load function
*/
public function load() {
$GLOBALS['user']->set_preferences();
return $this->_plugin->load();
}
/**
* get_plugin_version
* This returns the version of the specified plugin
@ -201,9 +236,11 @@ class Plugin {
$sql = "SELECT * FROM `update_info` WHERE `key`='$name'";
$db_results = Dba::read($sql);
$results = Dba::fetch_assoc($db_results);
if ($results = Dba::fetch_assoc($db_results)) {
return $results['value'];
}
return false;
} // get_plugin_version
@ -231,7 +268,7 @@ class Plugin {
$name = Dba::escape('Plugin_' . $this->_plugin->name);
$version = Dba::escape($version);
$sql = "INSERT INTO `update_info` SET `key`='$name', `value`='$version'";
$sql = "REPLACE INTO `update_info` SET `key`='$name', `value`='$version'";
$db_results = Dba::read($sql);
return true;

View file

@ -288,6 +288,18 @@ class Preference {
} // delete
/**
* rename
* This renames a preference in the database
*/
public static function rename($old, $new) {
$old = Dba::escape($old);
$new = Dba::escape($new);
$sql = "UPDATE `preference` SET `name`='$new' WHERE `name`='$old'";
$db_results = Dba::write($sql);
}
/**
* rebuild_preferences
* This removes any garbage and then adds back in anything missing preferences wise

View file

@ -202,6 +202,13 @@ class Rating extends database_object {
parent::add_to_cache('rating_' . $type . '_user' . $user_id, $id, $rating);
foreach (Plugin::get_plugins('save_rating') as $plugin_name) {
$plugin = new Plugin($plugin_name);
if ($plugin->load()) {
$plugin->_plugin->save_rating($this, $score);
}
}
return true;
} // set_rating

View file

@ -564,31 +564,22 @@ class User extends database_object {
if (!strlen($song_info->file)) { return false; }
// Make sure we didn't just play this song
$data = Stats::get_last_song($this->id);
// Make sure we've played a significant chunk of the song
$data = Stats::get_last_song($user);
$last_song = new Song($data['object_id']);
if ($data['date'] + ($song_info->time / 2) >= time()) {
debug_event('Stats','Not collecting stats less then 50% of song has elapsed','3');
debug_event('Stats','Not collecting stats less than 50% of song has elapsed','3');
return false;
}
$this->set_preferences();
// Check if lastfm is loaded, if so run the update
if (Plugin::is_installed('Last.FM')) {
$lastfm = new Plugin('Lastfm');
if ($lastfm->_plugin->load($this->prefs,$this->id)) {
$lastfm->_plugin->submit($song_info,$this->id);
foreach (Plugin::get_plugins('save_songplay') as $plugin_name) {
$plugin = new Plugin($plugin_name);
if ($plugin->load()) {
$plugin->_plugin->save_songplay($song_info);
}
} // end if is_installed
// Check and see if librefm is loaded and run scrobblizing
if (Plugin::is_installed('Libre.FM')) {
$librefm = new Plugin('Librefm');
if ($librefm->_plugin->load($this->prefs,$this->id)) {
$librefm->_plugin->submit($song_info,$this->id);
}
} // end if is_installed
// Do this last so the 'last played checks are correct'
Stats::insert('song',$song_id,$user);

View file

@ -58,17 +58,17 @@ function update_preferences($pref_id=0) {
case 'sample_rate':
$value = Stream::validate_bitrate($value);
break;
/* MD5 the LastFM & MyStrands so it's not plainTXT */
case 'librefm_pass':
case 'lastfm_pass':
/* If it's our default blanking thing then don't use it */
if ($value == '******') { unset($_REQUEST[$name]); break; }
$value = md5($value);
break;
default:
break;
}
if (preg_match('/_pass$/', $name)) {
if ($value == '******') { unset($_REQUEST[$name]); }
else if (preg_match('/md5_pass$/', $name)) {
$value = md5($value);
}
}
/* Run the update for this preference only if it's set */
if (isset($_REQUEST[$name])) {
Preference::update($id,$pref_id,$value,$_REQUEST[$apply_to_all]);
@ -247,10 +247,6 @@ function create_preference_input($name,$value) {
} // foreach themes
echo "</select>\n";
break;
case 'lastfm_pass':
case 'librefm_pass':
echo "<input type=\"password\" size=\"16\" name=\"$name\" value=\"******\" />";
break;
case 'playlist_method':
${$value} = ' selected="selected"';
echo "<select name=\"$name\">\n";
@ -293,7 +289,12 @@ function create_preference_input($name,$value) {
echo "</select>\n";
break;
default:
echo "<input type=\"text\" size=\"$len\" name=\"$name\" value=\"$value\" />";
if (preg_match('/_pass$/', $name)) {
echo '<input type="password" size="16" name="' . $name . '" value="******" />';
}
else {
echo '<input type="text" size="' . $len . '" name="' . $name . '" value="' . $value .'" />';
}
break;
}

View file

@ -25,18 +25,19 @@ class AmpacheLastfm {
public $name ='Last.FM';
public $description ='Records your played songs to your Last.FM Account';
public $url ='';
public $version ='000003';
public $min_ampache ='340007';
public $max_ampache ='340008';
public $version ='000004';
public $min_ampache ='360003';
public $max_ampache ='999999';
// These are internal settings used by this class, run this->load to
// fill em out
// fill them out
private $username;
private $password;
private $hostname;
private $port;
private $path;
private $challenge;
private $user_id;
/**
* Constructor
@ -46,11 +47,11 @@ class AmpacheLastfm {
return true;
} // PluginLastfm
} // constructor
/**
* install
* This is a required plugin function it inserts the required preferences
* This is a required plugin function. It inserts our preferences
* into Ampache
*/
public function install() {
@ -59,7 +60,7 @@ class AmpacheLastfm {
if (Preference::exists('lastfm_user')) { return false; }
Preference::insert('lastfm_user','Last.FM Username','','25','string','plugins');
Preference::insert('lastfm_pass','Last.FM Password','','25','string','plugins');
Preference::insert('lastfm_md5_pass','Last.FM Password','','25','string','plugins');
Preference::insert('lastfm_port','Last.FM Submit Port','','25','string','internal');
Preference::insert('lastfm_host','Last.FM Submit Host','','25','string','internal');
Preference::insert('lastfm_url','Last.FM Submit URL','','25','string','internal');
@ -71,12 +72,12 @@ class AmpacheLastfm {
/**
* uninstall
* This is a required plugin function it removes the required preferences from
* the database returning it to its origional form
* This is a required plugin function. It removes our preferences from
* the database returning it to its original form
*/
public function uninstall() {
Preference::delete('lastfm_pass');
Preference::delete('lastfm_md5_pass');
Preference::delete('lastfm_user');
Preference::delete('lastfm_url');
Preference::delete('lastfm_host');
@ -86,30 +87,44 @@ class AmpacheLastfm {
} // uninstall
/**
* submit
* This takes care of queueing and then submiting the tracks eventually this will make sure
* that you've haven't
* upgrade
* This is a recommended plugin function
*/
public function submit($song,$user_id) {
public function upgrade() {
$from_version = Plugin::get_plugin_version($this->name);
if ($from_version < 4) {
Preference::rename('lastfm_pass', 'lastfm_md5_pass');
}
return true;
} // upgrade
// Before we start let's pull the last song submited by this user
$previous = Stats::get_last_song($user_id);
/**
* save_songplay
* This takes care of queueing and then submitting the tracks.
*/
public function save_songplay($song) {
// Let's pull the last song submitted by this user
$previous = Stats::get_last_song($this->user_id);
$diff = time() - $previous['date'];
// Make sure it wasn't within the last min
if ($diff < 60) {
debug_event('LastFM','Last song played within ' . $diff . ' seconds, not recording stats','3');
debug_event($this->name,'Last song played within ' . $diff . ' seconds, not recording stats','3');
return false;
}
if ($song->time < 30) {
debug_event('LastFM','Song less then 30 seconds not queueing','3');
debug_event($this->name,'Song less then 30 seconds not queueing','3');
return false;
}
// Make sure there's actually a username and password before we keep going
if (!$this->username || !$this->password) { return false; }
if (!$this->username || !$this->password) {
debug_event($this->name,'Username or password missing','3');
return false;
}
// Create our scrobbler with everything this time and then queue it
$scrobbler = new scrobbler($this->username,$this->password,$this->hostname,$this->port,$this->path,$this->challenge);
@ -122,10 +137,10 @@ class AmpacheLastfm {
// Go ahead and submit it now
if (!$scrobbler->submit_tracks()) {
debug_event('LastFM','Error Submit Failed: ' . $scrobbler->error_msg,'3');
debug_event($this->name,'Error Submit Failed: ' . $scrobbler->error_msg,'3');
if ($scrobbler->reset_handshake) {
debug_event('LastFM','Re-running Handshake due to error','3');
$this->set_handshake($user_id);
debug_event($this->name,'Re-running Handshake due to error','3');
$this->set_handshake($this->user_id);
// Try try again
if ($scrobbler->submit_tracks()) {
return true;
@ -134,7 +149,7 @@ class AmpacheLastfm {
return false;
}
debug_event('LastFM','Submission Successful','5');
debug_event($this->name,'Submission Successful','5');
return true;
@ -142,9 +157,9 @@ class AmpacheLastfm {
/**
* set_handshake
* This runs a handshake and properly updates the preferences as needed, it returns the data
* as an array so we don't have to requery the db. This requires a userid so it knows who's
* crap to update
* This runs a handshake and properly updates the preferences as needed.
* It returns the data as an array so we don't have to requery the db.
* This requires a userid so it knows whose crap to update.
*/
public function set_handshake($user_id) {
@ -152,7 +167,7 @@ class AmpacheLastfm {
$data = $scrobbler->handshake();
if (!$data) {
debug_event('LastFM','Handshake Failed: ' . $scrobbler->error_msg,'3');
debug_event($this->name,'Handshake Failed: ' . $scrobbler->error_msg,'3');
return false;
}
@ -173,31 +188,36 @@ class AmpacheLastfm {
/**
* load
* This loads up the data we need into this object, this stuff comes from the preferences
* it's passed as a key'd array
* This loads up the data we need into this object, this stuff comes
* from the preferences.
*/
public function load($data,$user_id) {
public function load() {
$GLOBALS['user']->set_preferences();
$data = $GLOBALS['user']->prefs;
if (strlen(trim($data['lastfm_user']))) {
$this->username = trim($data['lastfm_user']);
}
else {
debug_event('LastFM','No Username, not scrobbling','3');
debug_event($this->name,'No Username, not scrobbling','3');
return false;
}
if (strlen(trim($data['lastfm_pass']))) {
$this->password = trim($data['lastfm_pass']);
if (strlen(trim($data['lastfm_md5_pass']))) {
$this->password = trim($data['lastfm_md5_pass']);
}
else {
debug_event('LastFM','No Password, not scrobbling','3');
debug_event($this->name,'No Password, not scrobbling','3');
return false;
}
$this->user_id = $GLOBALS['user']->id;
// If we don't have the other stuff try to get it before giving up
if (!$data['lastfm_host'] || !$data['lastfm_port'] || !$data['lastfm_url'] || !$data['lastfm_challenge']) {
debug_event('LastFM','Running Handshake, missing information','3');
if (!$this->set_handshake($user_id)) {
debug_event('LastFM','Handshake failed, you lose','3');
debug_event($this->name,'Running Handshake, missing information','3');
if (!$this->set_handshake($this->user_id)) {
debug_event($this->name,'Handshake failed, you lose','3');
return false;
}
}
@ -208,11 +228,9 @@ class AmpacheLastfm {
$this->challenge = $data['lastfm_challenge'];
}
return true;
} // load
} // end AmpacheLastfm
?>

View file

@ -25,18 +25,19 @@ class Ampachelibrefm {
public $name ='Libre.FM';
public $description ='Records your played songs to your Libre.FM Account';
public $url ='';
public $version ='000001';
public $min_ampache ='350001';
public $max_ampache ='360008';
public $version ='000002';
public $min_ampache ='360003';
public $max_ampache ='999999';
// These are internal settings used by this class, run this->load to
// fill em out
// fill them out
private $username;
private $password;
private $hostname;
private $port;
private $path;
private $challenge;
private $user_id;
/**
* Constructor
@ -46,11 +47,11 @@ class Ampachelibrefm {
return true;
} // Pluginlibrefm
} // constructor
/**
* install
* This is a required plugin function it inserts the required preferences
* This is a required plugin function. It inserts our preferences
* into Ampache
*/
public function install() {
@ -59,7 +60,7 @@ class Ampachelibrefm {
if (Preference::exists('librefm_user')) { return false; }
Preference::insert('librefm_user','Libre.FM Username','','25','string','plugins');
Preference::insert('librefm_pass','Libre.FM Password','','25','string','plugins');
Preference::insert('librefm_md5_pass','Libre.FM Password','','25','string','plugins');
Preference::insert('librefm_port','Libre.FM Submit Port','','25','string','internal');
Preference::insert('librefm_host','Libre.FM Submit Host','','25','string','internal');
Preference::insert('librefm_url','Libre.FM Submit URL','','25','string','internal');
@ -71,12 +72,12 @@ class Ampachelibrefm {
/**
* uninstall
* This is a required plugin function it removes the required preferences from
* the database returning it to its origional form
* This is a required plugin function. It removes our preferences from
* the database returning it to its original form
*/
public function uninstall() {
Preference::delete('librefm_pass');
Preference::delete('librefm_md5_pass');
Preference::delete('librefm_user');
Preference::delete('librefm_url');
Preference::delete('librefm_host');
@ -86,30 +87,44 @@ class Ampachelibrefm {
} // uninstall
/**
* submit
* This takes care of queueing and then submiting the tracks eventually this will make sure
* that you've haven't
* upgrade
* This is a recommended plugin function
*/
public function submit($song,$user_id) {
public function upgrade() {
$from_version = Plugin::get_plugin_version($this->name);
if ($from_version < 2) {
Preference::rename('librefm_pass', 'librefm_md5_pass');
}
return true;
} // upgrade
// Before we start let's pull the last song submited by this user
$previous = Stats::get_last_song($user_id);
/**
* save_songplay
* This takes care of queueing and then submitting the tracks.
*/
public function save_songplay($song) {
// Before we start let's pull the last song submitted by this user
$previous = Stats::get_last_song($this->user_id);
$diff = time() - $previous['date'];
// Make sure it wasn't within the last min
if ($diff < 60) {
debug_event('librefm','Last song played within ' . $diff . ' seconds, not recording stats','3');
debug_event($this->name,'Last song played within ' . $diff . ' seconds, not recording stats','3');
return false;
}
if ($song->time < 30) {
debug_event('librefm','Song less then 30 seconds not queueing','3');
debug_event($this->name,'Song less then 30 seconds not queueing','3');
return false;
}
// Make sure there's actually a username and password before we keep going
if (!$this->username || !$this->password) { return false; }
if (!$this->username || !$this->password) {
debug_event($this->name,'Username or password missing','3');
return false;
}
// Create our scrobbler with everything this time and then queue it
$scrobbler = new scrobbler($this->username,$this->password,$this->hostname,$this->port,$this->path,$this->challenge,'turtle.libre.fm');
@ -122,10 +137,10 @@ class Ampachelibrefm {
// Go ahead and submit it now
if (!$scrobbler->submit_tracks()) {
debug_event('librefm','Error Submit Failed: ' . $scrobbler->error_msg,'3');
debug_event($this->name,'Error Submit Failed: ' . $scrobbler->error_msg,'3');
if ($scrobbler->reset_handshake) {
debug_event('librefm','Re-running Handshake due to error','3');
$this->set_handshake($user_id);
debug_event($this->name,'Re-running Handshake due to error','3');
$this->set_handshake($this->user_id);
// Try try again
if ($scrobbler->submit_tracks()) {
return true;
@ -134,7 +149,7 @@ class Ampachelibrefm {
return false;
}
debug_event('librefm','Submission Successful','5');
debug_event($this->name,'Submission Successful','5');
return true;
@ -142,9 +157,9 @@ class Ampachelibrefm {
/**
* set_handshake
* This runs a handshake and properly updates the preferences as needed, it returns the data
* as an array so we don't have to requery the db. This requires a userid so it knows who's
* crap to update
* This runs a handshake and properly updates the preferences as needed.
* It returns the data as an array so we don't have to requery the db.
* This requires a userid so it knows whose crap to update.
*/
public function set_handshake($user_id) {
@ -152,7 +167,7 @@ class Ampachelibrefm {
$data = $scrobbler->handshake();
if (!$data) {
debug_event('librefm','Handshake Failed: ' . $scrobbler->error_msg,'3');
debug_event($this->name,'Handshake Failed: ' . $scrobbler->error_msg,'3');
return false;
}
@ -173,31 +188,36 @@ class Ampachelibrefm {
/**
* load
* This loads up the data we need into this object, this stuff comes from the preferences
* it's passed as a key'd array
* This loads up the data we need into this object, this stuff comes
* from the preferences.
*/
public function load($data,$user_id) {
public function load() {
$GLOBALS['user']->set_preferences();
$data = $GLOBALS['user']->prefs;
if (strlen(trim($data['librefm_user']))) {
$this->username = trim($data['librefm_user']);
}
else {
debug_event('librefm','No Username, not scrobbling','3');
debug_event($this->name,'No Username, not scrobbling','3');
return false;
}
if (strlen(trim($data['librefm_pass']))) {
$this->password = trim($data['librefm_pass']);
if (strlen(trim($data['librefm_md5_pass']))) {
$this->password = trim($data['librefm_md5_pass']);
}
else {
debug_event('librefm','No Password, not scrobbling','3');
debug_event($this->name,'No Password, not scrobbling','3');
return false;
}
$this->user_id = $GLOBALS['user']->id;
// If we don't have the other stuff try to get it before giving up
if (!$data['librefm_host'] || !$data['librefm_port'] || !$data['librefm_url'] || !$data['librefm_challenge']) {
debug_event('librefm','Running Handshake, missing information','3');
if (!$this->set_handshake($user_id)) {
debug_event('librefm','Handshake failed, you lose','3');
debug_event($this->name,'Running Handshake, missing information','3');
if (!$this->set_handshake($this->user_id)) {
debug_event($this->name,'Handshake failed, you lose','3');
return false;
}
}
@ -208,11 +228,9 @@ class Ampachelibrefm {
$this->challenge = $data['librefm_challenge'];
}
return true;
} // load
} // end Ampachelibrefm
?>

View file

@ -33,24 +33,32 @@ $web_path = Config::get('web_path');
<th class="cel_name"><?php echo _('Name'); ?></th>
<th class="cel_description"><?php echo _('Description'); ?></th>
<th class="cel_version"><?php echo _('Version'); ?></th>
<th class="cel_iversion"><?php echo _('Installed Version'); ?></th>
<th class="cel_action"><?php echo _('Action'); ?></th>
</tr>
<?php
foreach ($plugins as $plugin_name) {
$plugin = new Plugin($plugin_name);
if (!Plugin::is_installed($plugin->_plugin->name)) {
$installed_version = Plugin::get_plugin_version($plugin->_plugin->name);
if (! $installed_version) {
$action = "<a href=\"" . $web_path . "/admin/modules.php?action=install_plugin&amp;plugin=" . scrub_out($plugin_name) . "\">" .
_('Activate') . "</a>";
}
else {
$action = "<a href=\"" . $web_path . "/admin/modules.php?action=confirm_uninstall_plugin&amp;plugin=" . scrub_out($plugin_name) . "\">" .
_('Deactivate') . "</a>";
if ($installed_version < $plugin->_plugin->version) {
$action .= '&nbsp;&nbsp;<a href="' . $web_path .
'/admin/modules.php?action=upgrade_plugin&amp;plugin=' .
scrub_out($plugin_name) . '">' . _('Upgrade') . '</a>';
}
}
?>
<tr class="<?php echo flip_class(); ?>">
<td class="cel_name"><?php echo scrub_out($plugin->_plugin->name); ?></td>
<td class="cel_description"><?php echo scrub_out($plugin->_plugin->description); ?></td>
<td class="cel_version"><?php echo scrub_out($plugin->_plugin->version); ?></td>
<td class="cel_iversion"><?php echo scrub_out($installed_version); ?></td>
<td class="cel_action"><?php echo $action; ?></td>
</tr>
<?php } if (!count($plugins)) { ?>