<?php
/**
* Base class for config modules
*/
abstract class ConfigModule
{
/**
* @var array list of known module types
*/
private static $moduleTypes = false;
private $moduleId = 0;
private $moduleArchive = false;
private $moduleTitle = false;
private $currentVersion = 0;
protected $moduleData = false;
/**
* Load all known config module types. This is done
* by including *.inc.php from inc/configmodule/. The
* files there should in turn call ConfigModule::registerModule()
* to register themselves.
*/
public static function loadDb()
{
if (self::$moduleTypes !== false)
return;
self::$moduleTypes = array();
foreach (glob(dirname(__FILE__) . '/configmodule/*.inc.php', GLOB_NOSORT) as $file) {
require_once $file;
}
}
/**
* Get all known config module types.
*
* @return array list of modules
*/
public static function getList()
{
self::loadDb();
return self::$moduleTypes;
}
/**
* Add a known configuration module. Every inc/configmodule/*.inc.php should call this.
*
* @param string $id Identifier for the module.
* The module class must be called ConfigModule_{$id}, the wizard start class {$id}_Start.
* The wizard's classes should be located in modules/sysconfig/addmodule_{$id_lowercase}.inc.php
* @param string $title Title of this module type
* @param string $description Description for this module type
* @param string $group Title for group this module type belongs to
* @param bool $unique Can only one such module be added to a config?
* @param int $sortOrder Lower comes first, alphabetical ordering otherwiese
*/
public static function registerModule($id, $title, $description, $group, $unique, $sortOrder = 0)
{
if (isset(self::$moduleTypes[$id]))
Util::traceError("Config Module $id already registered!");
$moduleClass = 'ConfigModule_' . $id;
$wizardClass = $id . '_Start';
if (!class_exists($moduleClass))
Util::traceError("Class $moduleClass does not exist!");
if (get_parent_class($moduleClass) !== 'ConfigModule')
Util::traceError("$moduleClass does not have ConfigModule as its parent!");
self::$moduleTypes[$id] = array(
'title' => $title,
'description' => $description,
'group' => $group,
'unique' => $unique,
'sortOrder' => $sortOrder,
'moduleClass' => $moduleClass,
'wizardClass' => $wizardClass
);
}
/**
* Get fresh instance of ConfigModule subclass for given module type.
*
* @param string $moduleType name of module type
* @return \ConfigModule module instance
*/
public static function getInstance($moduleType)
{
self::loadDb();
if (!isset(self::$moduleTypes[$moduleType]))
return false;
return new self::$moduleTypes[$moduleType]['moduleClass'];
}
/**
* Get module instance from id.
*
* @param int $moduleId module id to get
* @return ConfigModule The requested module from DB, or false on error
*/
public static function get($moduleId)
{
$ret = Database::queryFirst("SELECT title, moduletype, filepath, contents, version FROM configtgz_module "
. " WHERE moduleid = :moduleid LIMIT 1", array('moduleid' => $moduleId));
if ($ret === false)
return false;
$instance = self::getInstance($ret['moduletype']);
if ($instance === false)
return false;
$instance->currentVersion = $ret['version'];
$instance->moduleArchive = $ret['filepath'];
$instance->moduleData = json_decode($ret['contents'], true);
$instance->moduleId = $moduleId;
$instance->moduleTitle = $ret['title'];
return $instance;
}
/**
* Get module instances from module type.
*
* @param int $moduleType module type to get
* @return array The requested modules from DB, or false on error
*/
public static function getAll($moduleType = false)
{
if ($moduleType === false) {
$ret = Database::simpleQuery("SELECT moduleid, title, moduletype, filepath, contents, version FROM configtgz_module");
} else {
$ret = Database::simpleQuery("SELECT moduleid, title, moduletype, filepath, contents, version FROM configtgz_module "
. " WHERE moduletype = :moduletype", array('moduletype' => $moduleType));
}
if ($ret === false)
return false;
$list = array();
while ($row = $ret->fetch(PDO::FETCH_ASSOC)) {
$instance = self::getInstance($row['moduletype']);
if ($instance === false)
return false;
$instance->currentVersion = $row['version'];
$instance->moduleArchive = $row['filepath'];
$instance->moduleData = json_decode($row['contents'], true);
$instance->moduleId = $row['moduleid'];
$instance->moduleTitle = $row['title'];
$list[] = $instance;
}
return $list;
}
/**
* Get the module version.
*
* @return int module version
*/
protected abstract function moduleVersion();
/**
* Validate the module's configuration.
*
* @return boolean ok or not
*/
protected abstract function validateConfig();
/**
* Set module specific data.
*
* @param string $key key, name or id of data being set
* @param mixed $value Module specific data
* @return boolean true if data was successfully set, false otherwise (i.e. invalid data being set)
*/
public abstract function setData($key, $value);
/**
* Get module specific data.
* Can be overridden by modules.
*
* @param string $key key, name or id of data to get, or false to get the raw moduleData array
* @return mixed Module specific data
*/
public function getData($key)
{
if ($key === false)
return $this->moduleData;
if (!is_array($this->moduleData) || !isset($this->moduleData[$key]))
return false;
return $this->moduleData[$key];
}
/**
* Module specific version of generate.
*
* @param string $tgz File name of tgz module to write final output to
* @param string $parent Parent task of this task
* @return array|boolean true if generation is completed immediately,
* a task struct if some task needs to be run for generation,
* false on error
*/
protected abstract function generateInternal($tgz, $parent);
private final function createFileName()
{
return CONFIG_TGZ_LIST_DIR . '/modules/'
. $this->moduleType() . '_id-' . $this->moduleId . '__' . mt_rand() . '-' . time() . '.tgz';
}
/**
* Get module id (in db)
*
* @return int id
*/
public final function id()
{
return $this->moduleId;
}
/**
* Get module title.
*
* @return string
*/
public final function title()
{
return $this->moduleTitle;
}
/**
* Get module archive file name.
*
* @return string tgz file absolute path
*/
public final function archive()
{
return $this->moduleArchive;
}
/**
* Get the module type.
*
* @return string module type
*/
public final function moduleType()
{
$name = get_class($this);
if ($name === false)
Util::traceError('ConfigModule::moduleType: get_class($this) returned false!');
// ConfigModule_*
if (!preg_match('/^ConfigModule_(\w+)$/', $name, $out))
Util::traceError('ConfigModule::moduleType: get_class($this) returned "' . $name . '"');
return $out[1];
}
/**
* Insert this config module into DB. Only
* valid if the object was created using the creating constructor,
* not if the instance was created using a database entry (static get method).
*
* @param string $title display name of the module
* @return boolean true if inserted successfully, false if module config is invalid
*/
public final function insert($title)
{
if ($this->moduleId !== 0)
Util::traceError('ConfigModule::insert called when moduleId != 0');
if (!$this->validateConfig())
return false;
$this->moduleTitle = $title;
// Insert
Database::exec("INSERT INTO configtgz_module (title, moduletype, filepath, contents, version, status) "
. " VALUES (:title, :type, '', :contents, :version, :status)", array(
'title' => $title,
'type' => $this->moduleType(),
'contents' => json_encode($this->moduleData),
'version' => 0,
'status' => 'MISSING'
));
$this->moduleId = Database::lastInsertId();
if (!is_numeric($this->moduleId))
Util::traceError('Inserting new config module into DB did not yield a numeric insert id');
$this->moduleArchive = $this->createFileName();
Database::exec("UPDATE configtgz_module SET filepath = :path WHERE moduleid = :moduleid LIMIT 1", array(
'path' => $this->moduleArchive,
'moduleid' => $this->moduleId
));
return true;
}
/**
* Update the given module in database. This will not regenerate
* the module's tgz.
*
* @return boolean true on success, false otherwise
*/
public final function update($title)
{
if ($this->moduleId === 0)
Util::traceError('ConfigModule::update called when moduleId == 0');
if (empty($title))
$title = $this->moduleTitle;
if (!$this->validateConfig())
return false;
// Update
Database::exec("UPDATE configtgz_module SET title = :title, contents = :contents, status = :status "
. " WHERE moduleid = :moduleid LIMIT 1", array(
'moduleid' => $this->moduleId,
'title' => $title,
'contents' => json_encode($this->moduleData),
'status' => 'OUTDATED'
));
return true;
}
/**
* Generate the module's tgz, don't wait for completion.
* Updating the database etc. will happen later through a callback.
*
* @param boolean $deleteOnError if true, the db entry will be deleted if generation failed
* @param string $parent Parent task of this task
* @param int $timeoutMs maximum time in milliseconds we wait for completion
* @return string|boolean task id if deferred generation was started,
* true if generation succeeded (without using a task or within $timeoutMs)
* false on error
*/
public final function generate($deleteOnError, $parent = NULL, $timeoutMs = 0)
{
if ($this->moduleId === 0 || $this->moduleTitle === false)
Util::traceError('ConfigModule::generateAsync called on uninitialized/uninserted module!');
$tmpTgz = '/tmp/bwlp-id-' . $this->moduleId . '_' . mt_rand() . '_' . time() . '.tgz';
$ret = $this->generateInternal($tmpTgz, $parent);
// Wait for generation if requested
if ($timeoutMs > 0 && isset($ret['id']) && !Taskmanager::isFinished($ret))
$ret = Taskmanager::waitComplete($ret, $timeoutMs);
if ($ret === true || (isset($ret['statusCode']) && $ret['statusCode'] === TASK_FINISHED)) {
// Already Finished
if (file_exists($this->moduleArchive) && !file_exists($tmpTgz))
$tmpTgz = false; // If generateInternal succeeded and there's no tmpTgz, it means the file didn't have to be updated
return $this->markUpdated($tmpTgz);
}
if (!is_array($ret) || !isset($ret['id']) || Taskmanager::isFailed($ret)) {
if (is_array($ret)) // Failed
Taskmanager::addErrorMessage($ret);
if ($deleteOnError)
$this->delete();
else
$this->markFailed();
return false;
}
// Still running, add callback
TaskmanagerCallback::addCallback($ret, 'cbConfModCreated', array(
'moduleid' => $this->moduleId,
'deleteOnError' => $deleteOnError,
'tmpTgz' => $tmpTgz
));
return $ret['id'];
}
/**
* Delete the module.
*/
public final function delete()
{
if ($this->moduleId === 0)
Util::traceError('ConfigModule::delete called with invalid module id!');
$ret = Database::exec("DELETE FROM configtgz_module WHERE moduleid = :moduleid LIMIT 1", array(
'moduleid' => $this->moduleId
), true) !== false;
if ($ret !== false) {
if ($this->moduleArchive)
Taskmanager::submit('DeleteFile', array('file' => $this->moduleArchive), true);
$this->moduleId = 0;
$this->moduleData = false;
$this->moduleTitle = false;
$this->moduleArchive = false;
}
return $ret;
}
private final function markUpdated($tmpTgz)
{
if ($this->moduleId === 0)
Util::traceError('ConfigModule::markUpdated called with invalid module id!');
if ($this->moduleArchive === false)
$this->moduleArchive = $this->createFileName();
// Move file
if ($tmpTgz === false) {
if (!file_exists($this->moduleArchive)) {
EventLog::failure('ConfigModule::markUpdated for "' . $this->moduleTitle . '" called with no tmpTgz and no existing tgz!');
$this->markFailed();
return false;
}
} else {
$task = Taskmanager::submit('MoveFile', array(
'source' => $tmpTgz,
'destination' => $this->moduleArchive
));
$task = Taskmanager::waitComplete($task, 5000);
if (Taskmanager::isFailed($task) || !Taskmanager::isFinished($task)) {
if (!API && !AJAX)
Taskmanager::addErrorMessage($task);
else
EventLog::failure('Could not move ' . $tmpTgz . ' to ' . $this->moduleArchive . ' while generating "' . $this->moduleTitle . '"');
$this->markFailed();
return false;
}
}
// Update DB entry
$retval = Database::exec("UPDATE configtgz_module SET filepath = :filename, version = :version, status = 'OK' WHERE moduleid = :id LIMIT 1", array(
'id' => $this->moduleId,
'filename' => $this->moduleArchive,
'version' => $this->moduleVersion()
)) !== false;
// Update related config.tgzs
$configs = ConfigTgz::getAllForModule($this->moduleId);
foreach ($configs as $config) {
$config->markOutdated();
$config->generate();
}
return $retval;
}
private final function markFailed()
{
if ($this->moduleId === 0)
Util::traceError('ConfigModule::markFailed called with invalid module id!');
if ($this->moduleArchive === false)
$this->moduleArchive = $this->createFileName();
if (!file_exists($this->moduleArchive))
$status = 'MISSING';
else
$status = 'OUTDATED';
return Database::exec("UPDATE configtgz_module SET filepath = :filename, status = :status WHERE moduleid = :id LIMIT 1", array(
'id' => $this->moduleId,
'filename' => $this->moduleArchive,
'status' => $status
)) !== false;
}
################# Callbacks ##############
/**
* Event callback for when the server ip changed.
* Override this if you need to handle this, otherwise
* the base implementation does nothing.
*/
public function event_serverIpChanged()
{
// Do::Nothing()
}
##################### STATIC CALLBACKS #####################
/**
* Will be called if the server's IP address changes. The event will be propagated
* to all config module classes so action can be taken if appropriate.
*/
public static function serverIpChanged()
{
self::loadDb();
$list = self::getAll();
foreach ($list as $mod) {
$mod->event_serverIpChanged();
}
}
/**
* Called when (re)generating a config module failed, so we can
* update the status in the DB and add a server log entry.
*
* @param array $task
* @param array $args contains 'moduleid' and optionally 'deleteOnError' and 'tmpTgz'
*/
public static function generateFailed($task, $args)
{
if (!isset($args['moduleid']) || !is_numeric($args['moduleid'])) {
EventLog::warning('Ignoring generateFailed event as it has no moduleid assigned.');
return;
}
$module = self::get($args['moduleid']);
if ($module === false) {
EventLog::warning('generateFailed callback for module id ' . $args['moduleid'] . ', but no instance could be generated.');
return;
}
if (isset($task['data']['error']))
$error = $task['data']['error'];
elseif (isset($task['data']['messages']))
$error = $task['data']['messages'];
else
$error = '';
EventLog::failure("Generating module '" . $module->moduleTitle . "' failed.", $error);
if ($args['deleteOnError'])
$module->delete();
else
$module->markFailed();
}
/**
* (Re)generating a config module succeeded. Update db entry.
*
* @param array $args contains 'moduleid' and optionally 'deleteOnError' and 'tmpTgz'
*/
public static function generateSucceeded($args)
{
if (!isset($args['moduleid']) || !is_numeric($args['moduleid'])) {
EventLog::warning('Ignoring generateSucceeded event as it has no moduleid assigned.');
return;
}
$module = self::get($args['moduleid']);
if ($module === false) {
EventLog::warning('generateSucceeded callback for module id ' . $args['moduleid'] . ', but no instance could be generated.');
return;
}
if (isset($args['tmpTgz']))
$module->markUpdated($args['tmpTgz']);
else
$module->markUpdated(false);
}
}