Newer
Older
<?php
require_once(INCLUDE_DIR.'/class.config.php');
class PluginConfig extends Config {
var $table = CONFIG_TABLE;
var $form;
function __construct($name) {
// Use parent constructor to place configurable information into the
// central config table in a namespace of "plugin.<id>"
parent::__construct("plugin.$name");
foreach ($this->getOptions() as $name => $field) {
if ($this->exists($name))
$this->config[$name]->value = $field->to_php($this->get($name));
elseif ($default = $field->get('default'))
$this->defaults[$name] = $default;
}
}
/* abstract */
function getOptions() {
return array();
}
function hasCustomConfig() {
return $this instanceof PluginCustomConfig;
}
/**
* Retreive a Form instance for the configurable options offered in
* ::getOptions
*/
function getForm() {
if (!isset($this->form)) {
$this->form = new SimpleForm($this->getOptions());
if ($_SERVER['REQUEST_METHOD'] != 'POST')
$this->form->data($this->getInfo());
}
return $this->form;
}
/**
* commit
*
* Used in the POST request of the configuration process. The
* ::getForm() method should be used to retrieve a configuration form
* for this plugin. That form should be submitted via a POST request,
* and this method should be called in that request. The data from the
* POST request will be interpreted and will adjust the configuration of
* this field
*
* Parameters:
* errors - (OUT array) receives validation errors of the parsed
* configuration form
*
* Returns:
* (bool) true if the configuration was updated, false if there were
* errors. If false, the errors were written into the received errors
* array.
*/
function commit(&$errors=array()) {
global $msg;
if ($this->hasCustomConfig())
return $this->saveCustomConfig($errors);
return $this->commitForm($errors);
}
function commitForm(&$errors=array()) {
if ($f->isValid()) {
$config = $f->getClean();
$commit = $this->pre_save($config, $errors);
if ($commit && count($errors) === 0) {
$dbready = array();
foreach ($config as $name => $val) {
$field = $f->getField($name);
try {
$dbready[$name] = $field->to_database($val);
}
catch (FieldUnchanged $e) {
// Don't save the field value
continue;
}
if ($this->updateAll($dbready)) {
if (!$msg)
$msg = 'Successfully updated configuration';
return true;
}
return false;
}
/**
* Pre-save hook to check configuration for errors (other than obvious
* validation errors) prior to saving. Add an error to the errors list
* or return boolean FALSE if the config commit should be aborted.
}
/**
* Remove all configuration for this plugin -- used when the plugin is
* uninstalled
*/
function purge() {
$sql = 'DELETE FROM '.$this->table
.' WHERE `namespace`='.db_input($this->getNamespace());
return (db_query($sql) && db_affected_rows());
}
}
/**
* Interface: PluginCustomConfig
*
* Allows a plugin to specify custom configuration pages. If the
* configuration cannot be suited by a single page, single form, then
* the plugin can use the ::renderCustomConfig() method to trigger
* rendering the page, and use ::saveCustomConfig() to trigger
* validating and saving the custom configuration.
*/
interface PluginCustomConfig {
function renderCustomConfig();
function saveCustomConfig();
}
class PluginManager {
static private $plugin_info = array();
static private $plugin_list = array();
/**
* boostrap
*
* Used to bootstrap the plugin subsystem and initialize all the plugins
* currently enabled.
*/
function bootstrap() {
foreach ($this->allActive() as $p)
$p->bootstrap();
}
/**
* allActive
*
* Scans the plugin registry to find all installed and active plugins.
* Those plugins are included, instanciated, and cached in a list.
*
* Returns:
* Array<Plugin> a cached list of instanciated plugins for all installed
* and active plugins
*/
static function allInstalled() {
if (static::$plugin_list)
return static::$plugin_list;
$sql = 'SELECT * FROM '.PLUGIN_TABLE;
if (!($res = db_query($sql)))
return static::$plugin_list;
while ($ht = db_fetch_array($res)) {
// XXX: Only read active plugins here. allInfos() will
// read all plugins
$info = static::getInfoForPath(
INCLUDE_DIR . $ht['install_path'], $ht['isphar']);
list($path, $class) = explode(':', $info['plugin']);
if (!$class)
$class = $path;
elseif ($ht['isphar'])
@include_once('phar://' . INCLUDE_DIR . $ht['install_path']
@include_once(INCLUDE_DIR . $ht['install_path']
if (!class_exists($class)) {
$class = 'DefunctPlugin';
$ht['isactive'] = false;
$info = array('name' => $ht['name'] . ' '. __('(defunct — missing)'));
}
if ($ht['isactive']) {
static::$plugin_list[$ht['install_path']]
= new $class($ht['id']);
}
else {
// Get instance without calling the constructor. Thanks
// http://stackoverflow.com/a/2556089
$a = unserialize(
sprintf(
'O:%d:"%s":0:{}',
strlen($class), $class
)
);
// Simulate __construct() and load()
$a->id = $ht['id'];
$a->ht = $ht;
$a->info = $info;
static::$plugin_list[$ht['install_path']] = &$a;
unset($a);
return static::$plugin_list;
}
static function allActive() {
$plugins = array();
foreach (static::allInstalled() as $p)
if ($p instanceof Plugin && $p->isActive())
$plugins[] = $p;
return $plugins;
}
function throwException($errno, $errstr) {
throw new RuntimeException($errstr);
}
/**
* allInfos
*
* Scans the plugin folders for installed plugins. For each one, the
* plugin.php file is included and the info array returned in added to
* the list returned.
*
* Returns:
* Information about all available plugins. The registry will have to be
* queried to determine if the plugin is installed
*/
static function allInfos() {
foreach (glob(INCLUDE_DIR . 'plugins/*',
GLOB_NOSORT|GLOB_BRACE) as $p) {
$is_phar = false;
if (substr($p, strlen($p) - 5) == '.phar'
&& Phar::isValidPharFilename($p)) {
try {
// When public key is invalid, openssl throws a
// 'supplied key param cannot be coerced into a public key' warning
// and phar ignores sig verification.
// We need to protect from that by catching the warning
// Thanks, https://github.com/koto/phar-util
set_error_handler(array('self', 'throwException'));
$ph = new Phar($p);
restore_error_handler();
// Verify the signature
$ph->getSignature();
$p = 'phar://' . $p;
$is_phar = true;
} catch (UnexpectedValueException $e) {
// Cannot find signature file
} catch (RuntimeException $e) {
// Invalid signature file
}
if (!is_file($p . '/plugin.php'))
// Invalid plugin -- must define "/plugin.php"
continue;
// Cache the info into static::$plugin_info
static::getInfoForPath($p, $is_phar);
static function getInfoForPath($path, $is_phar=false) {
static $defaults = array(
'include' => 'include/',
'stream' => false,
);
$install_path = str_replace(INCLUDE_DIR, '', $path);
$install_path = str_replace('phar://', '', $install_path);
if ($is_phar && substr($path, 0, 7) != 'phar://')
$path = 'phar://' . $path;
if (!isset(static::$plugin_info[$install_path])) {
// plugin.php is require to return an array of informaiton about
// the plugin.
if (!file_exists($path . '/plugin.php'))
return false;
$info = array_merge($defaults, (@include $path . '/plugin.php'));
$info['install_path'] = $install_path;
// XXX: Ensure 'id' key isset
static::$plugin_info[$install_path] = $info;
}
return static::$plugin_info[$install_path];
}
function getInstance($path) {
static $instances = array();
if (!isset($instances[$path])
&& ($ps = static::allInstalled())
&& ($ht = $ps[$path])) {
$info = static::getInfoForPath($path);
// $ht may be the plugin instance
if ($ht instanceof Plugin)
return $ht;
// Usually this happens when the plugin is being enabled
list($path, $class) = explode(':', $info['plugin']);
if (!$class)
$class = $path;
else
require_once(INCLUDE_DIR . $info['install_path'] . '/' . $path);
$instances[$path] = new $class($ht['id']);
}
return $instances[$path];
}
/**
* install
*
* Used to install a plugin that is in-place on the filesystem, but not
* registered in the plugin registry -- the %plugin table.
*/
function install($path) {
$is_phar = substr($path, strlen($path) - 5) == '.phar';
if (!($info = $this->getInfoForPath(INCLUDE_DIR . $path, $is_phar)))
$sql='INSERT INTO '.PLUGIN_TABLE.' SET installed=NOW() '
.', name='.db_input($info['name'])
.', isphar='.db_input($is_phar);
if (!db_query($sql) || !db_affected_rows())
return false;
static::$plugin_list = array();
}
}
/**
* Class: Plugin (abstract)
*
* Base class for plugins. Plugins should inherit from this class and define
* the useful pieces of the
*/
/**
* Configuration manager for the plugin. Should be the name of a class
* that inherits from PluginConfig. This is abstract and must be defined
* by the plugin subclass.
*/
var $config_class = null;
var $id;
var $info;
const VERIFIED = 1; // Thumbs up
const VERIFY_EXT_MISSING = 2; // PHP extension missing
const VERIFY_FAILED = 3; // Bad signature data
const VERIFY_ERROR = 4; // Unable to verify (unexpected error)
const VERIFY_NO_KEY = 5; // Public key missing
const VERIFY_DNS_PASS = 6; // DNS check passes, cannot verify sig
static $verify_domain = 'updates.osticket.com';
$this->id = $id;
$this->load();
}
function load() {
$sql = 'SELECT * FROM '.PLUGIN_TABLE.' WHERE
`id`='.db_input($this->id);
if (($res = db_query($sql)) && ($ht=db_fetch_array($res)))
$this->ht = $ht;
$this->info = PluginManager::getInfoForPath($this->ht['install_path'],
$this->isPhar());
}
function getId() { return $this->id; }
function getName() { return $this->__($this->info['name']); }
function isActive() { return $this->ht['isactive']; }
function isPhar() { return $this->ht['isphar']; }
function getInstallDate() { return $this->ht['installed']; }
function getInstallPath() { return $this->ht['install_path']; }
function getIncludePath() {
return realpath(INCLUDE_DIR . $this->info['install_path'] . '/'
. $this->info['include_path']) . '/';
}
/**
* Main interface for plugins. Called at the beginning of every request
* for each installed plugin. Plugins should register functionality and
* connect to signals, etc.
*/
abstract function bootstrap();
/**
* uninstall
*
* Removes the plugin from the plugin registry. The files remain on the
* filesystem which would allow the plugin to be reinstalled. The
* configuration for the plugin is also removed. If the plugin is
* reinstalled, it will have to be reconfigured.
*/
function uninstall(&$errors) {
if ($this->pre_uninstall($errors) === false)
$sql = 'DELETE FROM '.PLUGIN_TABLE
.' WHERE id='.db_input($this->getId());
if (!db_query($sql) || !db_affected_rows())
return false;
if ($config = $this->getConfig())
$config->purge();
/**
* pre_uninstall
*
* Hook function to veto the uninstallation request. Return boolean
* FALSE if the uninstall operation should be aborted.
*/
function pre_uninstall(&$errors) {
function enable() {
$sql = 'UPDATE '.PLUGIN_TABLE
.' SET isactive=1 WHERE id='.db_input($this->getId());
return (db_query($sql) && db_affected_rows());
}
function disable() {
$sql = 'UPDATE '.PLUGIN_TABLE
.' SET isactive=0 WHERE id='.db_input($this->getId());
return (db_query($sql) && db_affected_rows());
}
/**
* upgrade
*
* Upgrade the plugin. This is used to migrate the database pieces of
* the plugin using the database migration stream packaged with the
* plugin.
*/
function upgrade() {
}
function getConfig() {
static $config = null;
if ($config === null && $this->config_class)
$config = new $this->config_class($this->getId());
return $config;
}
function source($what) {
$what = str_replace('\\', '/', $what);
if ($what && $what[0] != '/')
$what = $this->getIncludePath() . $what;
include_once $what;
}
static function lookup($id) { //Assuming local ID is the only lookup used!
$path = false;
if ($id && is_numeric($id)) {
$sql = 'SELECT install_path FROM '.PLUGIN_TABLE
.' WHERE id='.db_input($id);
$path = db_result(db_query($sql));
}
if ($path)
return PluginManager::getInstance($path);
}
/**
* Function: isVerified
*
* This will help verify the content, integrity, oversight, and origin
* of plugins, language packs and other modules distributed for
* osTicket.
*
* This idea is that the signature of the PHAR file will be registered
* in DNS, for instance,
* `7afc8bf80b0555bed88823306744258d6030f0d9.updates.osticket.com`, for
* a PHAR file with a SHA1 signature of
* `7afc8bf80b0555bed88823306744258d6030f0d9 `, which will resolve to a
* string like the following:
* ```
* "v=1; i=storage:s3; s=MEUCIFw6A489eX4Oq17BflxCZ8+MH6miNjtcpScUoKDjmb
* lsAiEAjiBo9FzYtV3WQtW6sbhPlJXcoPpDfYyQB+BFVBMps4c=; V=0.1;"
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
* ```
* Which is a simple semicolon separated key-value pair string with the
* following keys
*
* Key | Description
* :----|:---------------------------------------------------
* v | Algorithm version
* i | Plugin 'id' registered in plugin.php['id']
* V | Plugin 'version' registered in plugin.php['version']
* s | OpenSSL signature of the PHAR SHA1 signature using a
* | private key (specified on the command line)
*
* The public key, which will be distributed with osTicket, can be used
* to verify the signature of the PHAR file from the data received from
* DNS.
*
* Parameters:
* $phar - (string) filename of phar file to verify
*
* Returns:
* (int) -
* Plugin::VERIFIED upon success
* Plugin::VERIFY_DNS_PASS if found in DNS but cannot verify sig
* Plugin::VERIFY_NO_KEY if public key not found in include/plugins
* Plugin::VERIFY_FAILED if the plugin fails validation
* Plugin::VERIFY_EXT_MISSING if a PHP extension is required
* Plugin::VERIFY_ERROR if an unexpected error occurred
*/
static function isVerified($phar) {
static $pubkey = null;
if (!class_exists('Phar') || !extension_loaded('openssl'))
return self::VERIFY_EXT_MISSING;
elseif (!file_exists(INCLUDE_DIR . '/plugins/updates.pem'))
return self::VERIFY_NO_KEY;
if (!isset($pubkey)) {
$pubkey = openssl_pkey_get_public(
file_get_contents(INCLUDE_DIR . 'plugins/updates.pem'));
}
if (!$pubkey) {
return self::VERIFY_ERROR;
}
$P = new Phar($phar);
$sig = $P->getSignature();
$info = array();
$ignored = null;
if ($r = dns_get_record($sig['hash'].'.'.self::$verify_domain.'.',
DNS_TXT, $ignored, $ignored, true)
) {
foreach ($r as $rec) {
foreach (explode(';', $rec['txt']) as $kv) {
list($k, $v) = explode('=', trim($kv));
$info[$k] = trim($v);
if ($info['v'] && $info['s'])
break;
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
}
}
if (is_array($info) && isset($info['v'])) {
switch ($info['v']) {
case '1':
if (!($signature = base64_decode($info['s'])))
return self::VERIFY_FAILED;
elseif (!function_exists('openssl_verify'))
return self::VERIFY_DNS_PASS;
$codes = array(
-1 => self::VERIFY_ERROR,
0 => self::VERIFY_FAILED,
1 => self::VERIFIED,
);
$result = openssl_verify($sig['hash'], $signature, $pubkey,
OPENSSL_ALGO_SHA1);
return $codes[$result];
}
}
return self::VERIFY_FAILED;
}
static function showVerificationBadge($phar) {
switch (self::isVerified($phar)) {
case self::VERIFIED:
$show_lock = true;
case self::VERIFY_DNS_PASS: ?>
<span class="label label-verified" title="<?php
if ($show_lock) echo sprintf(__('Verified by %s'), self::$verify_domain);
?>"> <?php
if ($show_lock) echo '<i class="icon icon-lock"></i>'; ?>
<?php echo $show_lock ? __('Verified') : __('Registered'); ?></span>
<?php break;
case self::VERIFY_FAILED: ?>
<span class="label label-danger" title="<?php
echo __('The originator of this extension cannot be verified');
?>"><i class="icon icon-warning-sign"></i></span>
<?php break;
}
}
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
/**
* Function: __
*
* Translate a single string (without plural alternatives) from the
* langauge pack installed in this plugin. The domain is auto-configured
* and detected from the plugin install path.
*/
function __($msgid) {
if (!isset($this->translation)) {
// Detect the domain from the plugin install-path
$groups = array();
preg_match('`plugins/(\w+)(?:.phar)?`', $this->getInstallPath(), $groups);
$domain = $groups[1];
if (!$domain)
return $msgid;
$this->translation = self::translate($domain);
}
list($__, $_N) = $this->translation;
return $__($msgid);
}
// Domain-specific translations (plugins)
/**
* Function: translate
*
* Convenience function to setup translation functions for other
* domains. This is of greatest benefit for plugins. This will return
* two functions to perform the translations. The first will translate a
* single string, the second will translate a plural string.
*
* Parameters:
* $domain - (string) text domain. The location of the MO.php file
* will be (path)/LC_MESSAGES/(locale)/(domain).mo.php. The (path)
* can be set via the $options parameter
* $options - (array<string:mixed>) Extra options for the setup
* "path" - (string) path to the folder containing the LC_MESSAGES
* folder. The (locale) setting is set externally respective to
* the user. If this is not set, the directory of the caller is
* assumed, plus '/i18n'. This is geared for plugins to be
* built with i18n content inside the '/i18n/' folder.
*
* Returns:
* Translation utility functions which mimic the __() and _N()
* functions. Note that two functions are returned. Capture them with a
* PHP list() construct.
*
* Caveats:
* When desiging plugins which might be installed in versions of
* osTicket which don't provide this function, use this compatibility
* interface:
*
* // Provide compatibility function for versions of osTicket prior to
* // translation support (v1.9.4)
* function translate($domain) {
* if (!method_exists('Plugin', 'translate')) {
* return array(
* function($x) { return $x; },
* function($x, $y, $n) { return $n != 1 ? $y : $x; },
* );
* }
* return Plugin::translate($domain);
* }
*/
static function translate($domain, $options=array()) {
// Configure the path for the domain. If no
$path = @$options['path'];
if (!$path) {
# Fetch the working path of the caller
$bt = debug_backtrace(false);
$path = dirname($bt[0]["file"]) . '/i18n';
}
$path = rtrim($path, '/') . '/';
$D = TextDomain::lookup($domain);
$D->setPath($path);
$trans = $D->getTranslation();
return array(
// __()
function($msgid) use ($trans) {
return $trans->translate($msgid);
},
// _N()
function($singular, $plural, $n) use ($trans) {
return $trans->ngettext($singular, $plural, $n);
},
);
}
class DefunctPlugin extends Plugin {
function bootstrap() {}
function enable() {
return false;
}
}