From 1cc1c2ed092c46eb35893c1d85accb24cf43d7f9 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Wed, 11 May 2016 19:00:30 +0200 Subject: Still working in modularization cleanup and refinement --- .../sysconfig/inc/configmodule.inc.php | 515 +++++++++++++++++++++ .../sysconfig/inc/configmodule/adauth.inc.php | 75 +++ .../sysconfig/inc/configmodule/branding.inc.php | 56 +++ .../sysconfig/inc/configmodule/customodule.inc.php | 56 +++ .../sysconfig/inc/configmodule/ldapauth.inc.php | 77 +++ .../sysconfig/inc/configmodule/sshconfig.inc.php | 63 +++ modules-available/sysconfig/inc/configtgz.inc.php | 317 +++++++++++++ 7 files changed, 1159 insertions(+) create mode 100644 modules-available/sysconfig/inc/configmodule.inc.php create mode 100644 modules-available/sysconfig/inc/configmodule/adauth.inc.php create mode 100644 modules-available/sysconfig/inc/configmodule/branding.inc.php create mode 100644 modules-available/sysconfig/inc/configmodule/customodule.inc.php create mode 100644 modules-available/sysconfig/inc/configmodule/ldapauth.inc.php create mode 100644 modules-available/sysconfig/inc/configmodule/sshconfig.inc.php create mode 100644 modules-available/sysconfig/inc/configtgz.inc.php (limited to 'modules-available/sysconfig/inc') diff --git a/modules-available/sysconfig/inc/configmodule.inc.php b/modules-available/sysconfig/inc/configmodule.inc.php new file mode 100644 index 00000000..7b92ff89 --- /dev/null +++ b/modules-available/sysconfig/inc/configmodule.inc.php @@ -0,0 +1,515 @@ + $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); + } + +} diff --git a/modules-available/sysconfig/inc/configmodule/adauth.inc.php b/modules-available/sysconfig/inc/configmodule/adauth.inc.php new file mode 100644 index 00000000..a03be43c --- /dev/null +++ b/modules-available/sysconfig/inc/configmodule/adauth.inc.php @@ -0,0 +1,75 @@ +id(), $parent); + $config = $this->moduleData; + if (isset($config['certificate']) && !is_string($config['certificate'])) { + unset($config['certificate']); + } + if (preg_match('/^([^\:]+)\:(\d+)$/', $config['server'], $out)) { + $config['server'] = $out[1]; + $config['adport'] = $out[2]; + } else { + if (isset($config['certificate'])) { + $config['adport'] = 636; + } else { + $config['adport'] = 389; + } + } + $config['parentTask'] = $parent; + $config['failOnParentFail'] = false; + $config['proxyip'] = Property::getServerIp(); + $config['proxyport'] = 3100 + $this->id(); + $config['filename'] = $tgz; + $config['moduleid'] = $this->id(); + return Taskmanager::submit('CreateLdapConfig', $config); + } + + protected function moduleVersion() + { + return self::VERSION; + } + + protected function validateConfig() + { + // Check if required fields are filled + return Util::hasAllKeys($this->moduleData, self::$REQUIRED_FIELDS); + } + + public function setData($key, $value) + { + if (!in_array($key, self::$REQUIRED_FIELDS) && !in_array($key, self::$OPTIONAL_FIELDS)) + return false; + $this->moduleData[$key] = $value; + return true; + } + + // ############## Callbacks ############################# + + /** + * Server IP changed - rebuild all AD modules. + */ + public function event_serverIpChanged() + { + $this->generate(false); + } + +} diff --git a/modules-available/sysconfig/inc/configmodule/branding.inc.php b/modules-available/sysconfig/inc/configmodule/branding.inc.php new file mode 100644 index 00000000..479b406c --- /dev/null +++ b/modules-available/sysconfig/inc/configmodule/branding.inc.php @@ -0,0 +1,56 @@ +validateConfig()) { + return $this->archive() !== false && file_exists($this->archive()); // No new temp file given, old archive still exists, pretend it worked... + } + $task = Taskmanager::submit('MoveFile', array( + 'source' => $this->tmpFile, + 'destination' => $tgz, + 'parentTask' => $parent, + 'failOnParentFail' => false + )); + return $task; + } + + protected function moduleVersion() + { + return self::VERSION; + } + + protected function validateConfig() + { + return $this->tmpFile !== false && file_exists($this->tmpFile); + } + + public function setData($key, $value) + { + if ($key !== 'tmpFile' || !is_string($value) || !file_exists($value)) + return false; + $this->tmpFile = $value; + return true; + } + + public function getData($key) + { + return false; + } + +} diff --git a/modules-available/sysconfig/inc/configmodule/customodule.inc.php b/modules-available/sysconfig/inc/configmodule/customodule.inc.php new file mode 100644 index 00000000..09b621cc --- /dev/null +++ b/modules-available/sysconfig/inc/configmodule/customodule.inc.php @@ -0,0 +1,56 @@ +validateConfig()) { + return $this->archive() !== false && file_exists($this->archive()); // No new temp file given, old archive still exists, pretend it worked... + } + $task = Taskmanager::submit('MoveFile', array( + 'source' => $this->tmpFile, + 'destination' => $tgz, + 'parentTask' => $parent, + 'failOnParentFail' => false + )); + return $task; + } + + protected function moduleVersion() + { + return self::VERSION; + } + + protected function validateConfig() + { + return $this->tmpFile !== false && file_exists($this->tmpFile); + } + + public function setData($key, $value) + { + if ($key !== 'tmpFile' || !file_exists($value)) + return false; + $this->tmpFile = $value; + return true; + } + + public function getData($key) + { + return false; + } + +} diff --git a/modules-available/sysconfig/inc/configmodule/ldapauth.inc.php b/modules-available/sysconfig/inc/configmodule/ldapauth.inc.php new file mode 100644 index 00000000..0f386033 --- /dev/null +++ b/modules-available/sysconfig/inc/configmodule/ldapauth.inc.php @@ -0,0 +1,77 @@ +id(), $parent); + $config = $this->moduleData; + if (isset($config['certificate']) && !is_string($config['certificate'])) { + unset($config['certificate']); + } + if (preg_match('/^([^\:]+)\:(\d+)$/', $config['server'], $out)) { + $config['server'] = $out[1]; + $config['adport'] = $out[2]; // sic! + } else { + if (isset($config['certificate'])) { + $config['adport'] = 636; + } else { + $config['adport'] = 389; + } + } + $config['parentTask'] = $parent; + $config['failOnParentFail'] = false; + $config['proxyip'] = Property::getServerIp(); + $config['proxyport'] = 3100 + $this->id(); + $config['filename'] = $tgz; + $config['moduleid'] = $this->id(); + $config['plainldap'] = true; + return Taskmanager::submit('CreateLdapConfig', $config); + } + + protected function moduleVersion() + { + return self::VERSION; + } + + protected function validateConfig() + { + // Check if required fields are filled + return Util::hasAllKeys($this->moduleData, self::$REQUIRED_FIELDS); + } + + public function setData($key, $value) + { + if (!in_array($key, self::$REQUIRED_FIELDS) && !in_array($key, self::$OPTIONAL_FIELDS)) + return false; + $this->moduleData[$key] = $value; + return true; + } + + // ############## Callbacks ############################# + + /** + * Server IP changed - rebuild all LDAP modules. + */ + public function event_serverIpChanged() + { + error_log('Calling generate on ' . $this->title()); + $this->generate(false); + } + +} diff --git a/modules-available/sysconfig/inc/configmodule/sshconfig.inc.php b/modules-available/sysconfig/inc/configmodule/sshconfig.inc.php new file mode 100644 index 00000000..b1d58153 --- /dev/null +++ b/modules-available/sysconfig/inc/configmodule/sshconfig.inc.php @@ -0,0 +1,63 @@ +validateConfig()) + return false; + $config = $this->moduleData + array( + 'filename' => $tgz, + 'failOnParentFail' => false, + 'parent' => $parent + ); + // Create config module, which will also check if the pubkey is valid + return Taskmanager::submit('SshdConfigGenerator', $config); + } + + protected function moduleVersion() + { + return self::VERSION; + } + + protected function validateConfig() + { + return isset($this->moduleData['publicKey']) && isset($this->moduleData['allowPasswordLogin']) && isset($this->moduleData['listenPort']); + } + + public function setData($key, $value) + { + switch ($key) { + case 'publicKey': + break; + case 'allowPasswordLogin': + if ($value === true || $value === 'yes') + $value = 'yes'; + elseif ($value === false || $value === 'no') + $value = 'no'; + else + return false; + break; + case 'listenPort': + if (!is_numeric($value) || $value < 1 || $value > 65535) + return false; + break; + default: + return false; + } + $this->moduleData[$key] = $value; + return true; + } + +} diff --git a/modules-available/sysconfig/inc/configtgz.inc.php b/modules-available/sysconfig/inc/configtgz.inc.php new file mode 100644 index 00000000..b51d2787 --- /dev/null +++ b/modules-available/sysconfig/inc/configtgz.inc.php @@ -0,0 +1,317 @@ +configId; + } + + public function title() + { + return $this->configTitle; + } + + public function areAllModulesUpToDate() + { + if (!$this->configId > 0) + Util::traceError('ConfigTgz::areAllModulesUpToDate called on un-inserted config.tgz!'); + foreach ($this->modules as $module) { + if (!empty($module['filepath']) && file_exists($module['filepath'])) { + if ($module['status'] !== 'OK') + return false; + } else { + return false; + } + } + return true; + } + + public function isActive() + { + return readlink(CONFIG_HTTP_DIR . '/default/config.tgz') === $this->file; + } + + public function getModuleIds() + { + $ret = array(); + foreach ($this->modules as $module) { + $ret[] = $module['moduleid']; + } + return $ret; + } + + public function update($title, $moduleIds) + { + if (!is_array($moduleIds)) + return false; + $this->configTitle = $title; + $this->modules = array(); + // Get all modules to put in config + $idstr = '0'; // Passed directly in query. Make sure no SQL injection is possible + foreach ($moduleIds as $module) { + $idstr .= ',' . (int)$module; // Casting to int should make it safe + } + $res = Database::simpleQuery("SELECT moduleid, filepath, status FROM configtgz_module WHERE moduleid IN ($idstr)"); + // Delete old connections + Database::exec("DELETE FROM configtgz_x_module WHERE configid = :configid", array('configid' => $this->configId)); + // Make connection + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + Database::exec("INSERT INTO configtgz_x_module (configid, moduleid) VALUES (:configid, :moduleid)", array( + 'configid' => $this->configId, + 'moduleid' => $row['moduleid'] + )); + $this->modules[] = $row; + } + // Update name + Database::exec("UPDATE configtgz SET title = :title, status = :status WHERE configid = :configid LIMIT 1", array( + 'configid' => $this->configId, + 'title' => $title, + 'status' => 'OUTDATED' + )); + return true; + } + + public static function insert($title, $moduleIds) + { + if (!is_array($moduleIds)) + return false; + $instance = new ConfigTgz; + $instance->configTitle = $title; + // Create output file name (config.tgz) + do { + $instance->file = CONFIG_TGZ_LIST_DIR . '/config-' . Util::sanitizeFilename($instance->configTitle) . '-' . mt_rand() . '-' . time() . '.tgz'; + } while (file_exists($instance->file)); + Database::exec("INSERT INTO configtgz (title, filepath, status) VALUES (:title, :filepath, :status)", array( + 'title' => $instance->configTitle, + 'filepath' => $instance->file, + 'status' => 'MISSING' + )); + $instance->configId = Database::lastInsertId(); + $instance->modules = array(); + // Get all modules to put in config + $idstr = '0'; // Passed directly in query. Make sure no SQL injection is possible + foreach ($moduleIds as $module) { + $idstr .= ',' . (int)$module; // Casting to int should make it safe + } + $res = Database::simpleQuery("SELECT moduleid, filepath, status FROM configtgz_module WHERE moduleid IN ($idstr)"); + // Make connection + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + Database::exec("INSERT INTO configtgz_x_module (configid, moduleid) VALUES (:configid, :moduleid)", array( + 'configid' => $instance->configId, + 'moduleid' => $row['moduleid'] + )); + $instance->modules[] = $row; + } + return $instance; + } + + public static function get($configId) + { + $ret = Database::queryFirst("SELECT configid, title, filepath FROM configtgz WHERE configid = :configid", array( + 'configid' => $configId + )); + if ($ret === false) + return false; + $instance = new ConfigTgz; + $instance->configId = $ret['configid']; + $instance->configTitle = $ret['title']; + $instance->file = $ret['filepath']; + $ret = Database::simpleQuery("SELECT moduleid, filepath, status FROM configtgz_x_module " + . " INNER JOIN configtgz_module USING (moduleid) " + . " WHERE configid = :configid", array('configid' => $instance->configId)); + $instance->modules = array(); + while ($row = $ret->fetch(PDO::FETCH_ASSOC)) { + $instance->modules[] = $row; + } + return $instance; + } + + public static function getAllForModule($moduleId) + { + $res = Database::simpleQuery("SELECT configid, title, filepath FROM configtgz_x_module " + . " INNER JOIN configtgz USING (configid) " + . " WHERE moduleid = :moduleid", array( + 'moduleid' => $moduleId + )); + if ($res === false) + return false; + $list = array(); + while ($row = $res->fetch(PDO::FETCH_ASSOC)) { + $instance = new ConfigTgz; + $instance->configId = $row['configid']; + $instance->configTitle = $row['title']; + $instance->file = $row['filepath']; + $innerRes = Database::simpleQuery("SELECT moduleid, filepath, status FROM configtgz_x_module " + . " INNER JOIN configtgz_module USING (moduleid) " + . " WHERE configid = :configid", array('configid' => $instance->configId)); + $instance->modules = array(); + while ($innerRow = $innerRes->fetch(PDO::FETCH_ASSOC)) { + $instance->modules[] = $innerRow; + } + $list[] = $instance; + } + return $list; + } + + /** + * Called when (re)generating a config tgz failed, so we can + * update the status in the DB and add a server log entry. + * + * @param array $task + * @param array $args contains 'configid' and optionally 'deleteOnError' + */ + public static function generateFailed($task, $args) + { + if (!isset($args['configid']) || !is_numeric($args['configid'])) { + EventLog::warning('Ignoring generateFailed event as it has no configid assigned.'); + return; + } + $config = self::get($args['configid']); + if ($config === false) { + EventLog::warning('generateFailed callback for config id ' . $args['configid'] . ', 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 config.tgz '" . $config->configTitle . "' failed.", $error); + if ($args['deleteOnError']) + $config->delete(); + else + $config->markFailed(); + } + + /** + * (Re)generating a config tgz succeeded. Update db entry. + * + * @param array $args contains 'configid' and optionally 'deleteOnError' + */ + public static function generateSucceeded($args) + { + if (!isset($args['configid']) || !is_numeric($args['configid'])) { + EventLog::warning('Ignoring generateSucceeded event as it has no configid assigned.'); + return; + } + $config = self::get($args['configid']); + if ($config === false) { + EventLog::warning('generateSucceeded callback for config id ' . $args['configid'] . ', but no instance could be generated.'); + return; + } + $config->markUpdated(); + } + + /** + * + * @param type $deleteOnError + * @param type $timeoutMs + * @return string - OK (success) + * - OUTDATED (updating failed, but old version still exists) + * - MISSING (failed and no old version available) + */ + public function generate($deleteOnError = false, $timeoutMs = 0) + { + if (!($this->configId > 0) || !is_array($this->modules) || $this->file === false) + Util::traceError ('configId <= 0 or modules not array in ConfigTgz::rebuild()'); + $files = array(); + foreach ($this->modules as $module) { + if (!empty($module['filepath']) && file_exists($module['filepath'])) + $files[] = $module['filepath']; + } + // Hand over to tm + $task = Taskmanager::submit('RecompressArchive', array( + 'inputFiles' => $files, + 'outputFile' => $this->file + )); + // Wait for completion + if ($timeoutMs > 0 && !Taskmanager::isFailed($task) && !Taskmanager::isFinished($task)) + $task = Taskmanager::waitComplete($task, $timeoutMs); + if ($task === true || (isset($task['statusCode']) && $task['statusCode'] === TASK_FINISHED)) { + // Success! + $this->markUpdated(); + return true; + } + if (!is_array($task) || !isset($task['id']) || Taskmanager::isFailed($task)) { + // Failed... + Taskmanager::addErrorMessage($task); + if (!$deleteOnError) + $this->markFailed(); + else + $this->delete(); + return false; + } + // Still running, add callback + TaskmanagerCallback::addCallback($task, 'cbConfTgzCreated', array( + 'configid' => $this->configId, + 'deleteOnError' => $deleteOnError + )); + return $task['id']; + } + + public function delete() + { + if ($this->configId === 0) + Util::traceError('ConfigTgz::delete called with invalid config id!'); + $ret = Database::exec("DELETE FROM configtgz WHERE configid = :configid LIMIT 1", array( + 'configid' => $this->configId + ), true) !== false; + if ($ret !== false) { + if ($this->file !== false) + Taskmanager::submit('DeleteFile', array('file' => $this->file), true); + $this->configId = 0; + $this->modules = false; + $this->file = false; + } + return $ret; + } + + public function markOutdated() + { + if ($this->configId === 0) + Util::traceError('ConfigTgz::markOutdated called with invalid config id!'); + return $this->mark('OUTDATED'); + } + + private function markUpdated() + { + if ($this->configId === 0) + Util::traceError('ConfigTgz::markUpdated called with invalid config id!'); + Event::activeConfigChanged(); + if ($this->areAllModulesUpToDate()) + return $this->mark('OK'); + return $this->mark('OUTDATED'); + } + + private function markFailed() + { + if ($this->configId === 0) + Util::traceError('ConfigTgz::markFailed called with invalid config id!'); + if ($this->file === false || !file_exists($this->file)) + return $this->mark('MISSING'); + return $this->mark('OUTDATED'); + } + + private function mark($status) + { + Database::exec("UPDATE configtgz SET status = :status WHERE configid = :configid LIMIT 1", array( + 'configid' => $this->configId, + 'status' => $status + )); + return $status; + } + +} -- cgit v1.2.3-55-g7522