$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); } }