$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(string $moduleType): ConfigModule { $ret = self::getInstanceOrNull($moduleType); if ($ret === null) { Message::addError('main.error-read', $moduleType . '.inc.php'); Util::redirect('?do=sysconfig'); } return $ret; } public static function getInstanceOrNull(string $moduleType): ?ConfigModule { self::loadDb(); if (!isset(self::$moduleTypes[$moduleType])) { error_log('Unknown module type: ' . $moduleType); return null; } return new self::$moduleTypes[$moduleType]['moduleClass']; } private static function instanceFromDbRow(array $dbRow): ?ConfigModule { $instance = self::getInstanceOrNull($dbRow['moduletype']); if ($instance === null) return null; $instance->currentVersion = $dbRow['version']; $instance->moduleArchive = $dbRow['filepath']; $instance->moduleData = json_decode($dbRow['contents'], true); $instance->moduleId = $dbRow['moduleid']; $instance->moduleTitle = $dbRow['title']; $instance->moduleStatus = $dbRow['status']; $instance->dateline = $dbRow['dateline']; if ($instance->moduleVersion() > $instance->currentVersion) { $instance->markFailed(); } return $instance; } /** * Get module instance from id. * * @param int $moduleId module id to get * @return ?ConfigModule The requested module from DB, or null on error */ public static function get(int $moduleId): ?ConfigModule { $ret = Database::queryFirst("SELECT moduleid, title, moduletype, filepath, contents, version, status, dateline FROM configtgz_module " . " WHERE moduleid = :moduleid LIMIT 1", array('moduleid' => $moduleId)); if ($ret === false) return null; return self::instanceFromDbRow($ret); } /** * Get module instances from module type. * * @param string $moduleType module type to get * @return ?ConfigModule[] The requested modules from DB, or null on error */ public static function getAll(string $moduleType = null): ?array { if ($moduleType === null) { $ret = Database::simpleQuery("SELECT moduleid, title, moduletype, filepath, contents, version, status, dateline FROM configtgz_module"); } else { $ret = Database::simpleQuery("SELECT moduleid, title, moduletype, filepath, contents, version, status, dateline FROM configtgz_module " . " WHERE moduletype = :moduletype", array('moduletype' => $moduleType)); } if ($ret === false) return null; $list = array(); foreach ($ret as $row) { $instance = self::instanceFromDbRow($row); if ($instance === null) continue; $list[] = $instance; } return $list; } /** * Get the module version. * * @return int module version */ protected abstract function moduleVersion(): int; /** * Validate the module's configuration. * * @return bool ok or not */ protected abstract function validateConfig(): bool; /** * Set module specific data. * * @param string $key key, name or id of data being set * @param mixed $value Module specific data * @return bool true if data was successfully set, false otherwise (i.e. invalid data being set) */ public abstract function setData(string $key, $value): bool; /** * Get module specific data. * Can be overridden by modules. * * @param ?string $key key, name or id of data to get, or null to get the raw moduleData array * @return mixed Module specific data */ public function getData(?string $key) { if ($key === null) return $this->moduleData; if (!is_array($this->moduleData) || !isset($this->moduleData[$key])) return null; return $this->moduleData[$key]; } /** * Module specific version of generate. * * @param string $tgz File name of tgz module to write final output to * @param string|null $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(string $tgz, ?string $parent); private function createFileName(): string { return CONFIG_TGZ_LIST_DIR . '/modules/' . $this->moduleType() . '_id-' . $this->moduleId . '__' . mt_rand() . '-' . time() . '.tgz'; } public function allowDownload(): bool { return false; } public function needRebuild(): bool { return $this->moduleStatus !== 'OK' || $this->currentVersion < $this->moduleVersion(); } /** * Get module id (in db) * * @return int id */ public final function id(): int { return $this->moduleId; } /** * Get module title. */ public final function title(): string { return $this->moduleTitle; } /** * Get module archive file name. * * @return string tgz file absolute path */ public final function archive(): string { return $this->moduleArchive; } public final function status(): string { return $this->moduleStatus; } public final function currentVersion(): int { return $this->currentVersion; } /** * Get the module type. * * @return string module type */ public final function moduleType(): string { // Yes, need to pass $this, otherwise we get ConfigModule, the base class this function is part of $name = get_class($this); // ConfigModule_* if (!preg_match('/^ConfigModule_(\w+)$/', $name, $out)) ErrorHandler::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(string $title): bool { if ($this->moduleId !== 0) ErrorHandler::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, dateline) " . " VALUES (:title, :type, '', :contents, :version, :status, :now)", array( 'title' => $title, 'type' => $this->moduleType(), 'contents' => json_encode($this->moduleData), 'version' => 0, 'status' => 'MISSING', 'now' => time(), )); $this->moduleId = Database::lastInsertId(); if (!is_numeric($this->moduleId)) ErrorHandler::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(string $title = ''): bool { if ($this->moduleId === 0) ErrorHandler::traceError('ConfigModule::update called when moduleId == 0'); if (!empty($title)) { $this->moduleTitle = $title; } if (!$this->validateConfig()) return false; // Update Database::exec("UPDATE configtgz_module SET title = :title, contents = :contents, status = :status, dateline = :now " . " WHERE moduleid = :moduleid LIMIT 1", array( 'moduleid' => $this->moduleId, 'title' => $this->moduleTitle, 'contents' => json_encode($this->moduleData), 'status' => 'OUTDATED', 'now' => time(), )); $this->moduleStatus = '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|null $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(bool $deleteOnError, string $parent = NULL, int $timeoutMs = 0) { if ($this->moduleId === 0 || empty($this->moduleTitle)) ErrorHandler::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'] === Taskmanager::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(): void { if ($this->moduleId === 0) ErrorHandler::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; } } /** * @param ?string $tmpTgz new tar archive to use for this module, or null if the old one is still valid */ private function markUpdated(?string $tmpTgz): bool { if ($this->moduleId === 0) ErrorHandler::traceError('ConfigModule::markUpdated called with invalid module id!'); if ($this->moduleArchive === null) $this->moduleArchive = $this->createFileName(); // Move file if ($tmpTgz === null) { if (!file_exists($this->moduleArchive)) { EventLog::failure('ConfigModule::markUpdated for "' . $this->moduleTitle . '" called with no tmpTgz and no existing tgz!'); $this->markFailed(); return false; } } elseif (!file_exists($tmpTgz)) { EventLog::warning('ConfigModule::markUpdated for tmpTgz="' . $this->moduleTitle . '" called which doesn\'t exist. Doing nothing.'); return true; } 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 . '"', print_r($task, true)); } $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 function markFailed(): void { if ($this->moduleId === 0) ErrorHandler::traceError('ConfigModule::markFailed called with invalid module id!'); if ($this->moduleArchive === '') { $this->moduleArchive = $this->createFileName(); } if (!file_exists($this->moduleArchive)) { $status = 'MISSING'; } else { $status = 'OUTDATED'; } Database::exec("UPDATE configtgz_module SET filepath = :filename, status = :status WHERE moduleid = :id LIMIT 1", array( 'id' => $this->moduleId, 'filename' => $this->moduleArchive, 'status' => $status )); } public function dateline_s(): string { return Util::prettyTime($this->dateline); } ################# 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(): void { // 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(): void { self::loadDb(); foreach (self::getAll() ?? [] 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 $args contains 'moduleid' and optionally 'deleteOnError' */ public static function generateFailed(array $task, array $args): void { 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 === null) { 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'] ?? false) { $module->delete(); } else { $module->markFailed(); } } /** * (Re)generating a config module succeeded. Update db entry. * * @param array $args contains 'moduleid' and optionally 'tmpTgz' */ public static function generateSucceeded(array $args): void { 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 === null) { EventLog::warning('generateSucceeded callback for module id ' . $args['moduleid'] . ', but no instance could be generated.'); return; } $module->markUpdated($args['tmpTgz'] ?? null); } }