summaryrefslogtreecommitdiffstats
path: root/modules-available
diff options
context:
space:
mode:
authorSimon Rettberg2016-05-11 19:00:30 +0200
committerSimon Rettberg2016-05-11 19:00:30 +0200
commit1cc1c2ed092c46eb35893c1d85accb24cf43d7f9 (patch)
tree95c1302f4a1ae441e174a1dca64133e2873f8297 /modules-available
parentAdd PhpStorm prefs (diff)
downloadslx-admin-1cc1c2ed092c46eb35893c1d85accb24cf43d7f9.tar.gz
slx-admin-1cc1c2ed092c46eb35893c1d85accb24cf43d7f9.tar.xz
slx-admin-1cc1c2ed092c46eb35893c1d85accb24cf43d7f9.zip
Still working in modularization cleanup and refinement
Diffstat (limited to 'modules-available')
-rw-r--r--modules-available/baseconfig/lang/de/template-tags.json2
-rw-r--r--modules-available/baseconfig/lang/en/template-tags.json2
-rw-r--r--modules-available/baseconfig/lang/pt/template-tags.json2
-rw-r--r--modules-available/main/lang/de/global-tags.json7
-rw-r--r--modules-available/main/lang/de/template-tags.json1
-rw-r--r--modules-available/main/lang/en/global-tags.json7
-rw-r--r--modules-available/main/lang/en/template-tags.json1
-rw-r--r--modules-available/main/lang/pt/global-tags.json4
-rw-r--r--modules-available/main/lang/pt/template-tags.json1
-rw-r--r--modules-available/main/templates/php-errors.html12
-rw-r--r--modules-available/sysconfig/addmodule.inc.php3
-rw-r--r--modules-available/sysconfig/addmodule_adauth.inc.php12
-rw-r--r--modules-available/sysconfig/inc/configmodule.inc.php515
-rw-r--r--modules-available/sysconfig/inc/configmodule/adauth.inc.php75
-rw-r--r--modules-available/sysconfig/inc/configmodule/branding.inc.php56
-rw-r--r--modules-available/sysconfig/inc/configmodule/customodule.inc.php56
-rw-r--r--modules-available/sysconfig/inc/configmodule/ldapauth.inc.php77
-rw-r--r--modules-available/sysconfig/inc/configmodule/sshconfig.inc.php63
-rw-r--r--modules-available/sysconfig/inc/configtgz.inc.php317
-rw-r--r--modules-available/sysconfig/lang/de/config-module.json16
-rw-r--r--modules-available/sysconfig/lang/en/config-module.json16
-rw-r--r--modules-available/sysconfig/lang/pt/config-module.json3
-rw-r--r--modules-available/sysconfig/page.inc.php12
-rw-r--r--modules-available/translation/lang/de/template-tags.json6
-rw-r--r--modules-available/translation/lang/en/template-tags.json5
-rw-r--r--modules-available/translation/page.inc.php14
-rw-r--r--modules-available/translation/templates/custom-list.html38
-rw-r--r--modules-available/translation/templates/edit.html6
-rw-r--r--modules-available/translation/templates/message-list.html54
-rw-r--r--modules-available/translation/templates/string-list.html38
-rw-r--r--modules-available/translation/templates/template-list.html48
-rw-r--r--modules-available/usermanagement/lang/de/module.json3
32 files changed, 1350 insertions, 122 deletions
diff --git a/modules-available/baseconfig/lang/de/template-tags.json b/modules-available/baseconfig/lang/de/template-tags.json
index 0f4819b4..de2007d9 100644
--- a/modules-available/baseconfig/lang/de/template-tags.json
+++ b/modules-available/baseconfig/lang/de/template-tags.json
@@ -2,10 +2,8 @@
"lang_basicConfiguration": "Basiskonfiguration",
"lang_catUser": "Benutzerverwaltung",
"lang_clientRelatedConfig": "Die Optionen auf dieser Seite beziehen sich auf das Verhalten der bwLehrpool-Clients.",
- "lang_close": "Schlie\u00dfen",
"lang_create": "Schaffen",
"lang_delete": "L\u00f6schen",
- "lang_help": "Hilfe",
"lang_newUser": "Neuer Benutzer",
"lang_partitionMountPoint": "Mount Point",
"lang_reset": "Zur\u00fccksetzen",
diff --git a/modules-available/baseconfig/lang/en/template-tags.json b/modules-available/baseconfig/lang/en/template-tags.json
index 3609f6fa..369fa081 100644
--- a/modules-available/baseconfig/lang/en/template-tags.json
+++ b/modules-available/baseconfig/lang/en/template-tags.json
@@ -3,11 +3,9 @@
"lang_catPartition": "Partition Managment",
"lang_catUser": "User Managment",
"lang_clientRelatedConfig": "The options on this page are related to the bwLehrpool client machines.",
- "lang_close": "Close",
"lang_confirm": "Would you like to save the settings on [ \/srv\/openslx\/www\/boot\/config ] ?",
"lang_create": "Create",
"lang_delete": "Delete",
- "lang_help": "Help",
"lang_helpId": "Partition Id",
"lang_helpMountPoint": "Must be a directory: \/example\/directory\/",
"lang_helpOptions": "Currently, only option 'bootable' is available",
diff --git a/modules-available/baseconfig/lang/pt/template-tags.json b/modules-available/baseconfig/lang/pt/template-tags.json
index 1ab41b16..f6037d65 100644
--- a/modules-available/baseconfig/lang/pt/template-tags.json
+++ b/modules-available/baseconfig/lang/pt/template-tags.json
@@ -2,11 +2,9 @@
"lang_basicConfiguration": "Configura\u00e7\u00e3o B\u00e1sica",
"lang_catPartition": "Gerenciamento de Parti\u00e7\u00f5es",
"lang_catUser": "Gerenciamente de Usu\u00e1rios",
- "lang_close": "Fechar",
"lang_confirm": "Voc\u00ea deseja salvar configura\u00e7\u00f5es em [ \/srv\/openslx\/www\/boot\/default\/config ] ?",
"lang_create": "Criar",
"lang_delete": "Deletar",
- "lang_help": "Ajuda",
"lang_helpId": "Id da parti\u00e7\u00e3o",
"lang_helpMountPoint": "Precisa ser um diret\u00f3rio: \/exemplo\/diret\u00f3rio\/",
"lang_helpOptions": "Atualmente, apenas a op\u00e7\u00e3o 'bootable' est\u00e1 dispon\u00edvel",
diff --git a/modules-available/main/lang/de/global-tags.json b/modules-available/main/lang/de/global-tags.json
index c36347fe..39236fd4 100644
--- a/modules-available/main/lang/de/global-tags.json
+++ b/modules-available/main/lang/de/global-tags.json
@@ -1,4 +1,11 @@
{
+ "lang_back": "Zur\u00fcck",
+ "lang_cancel": "Abbrechen",
+ "lang_close": "Schlie\u00dfen",
+ "lang_delete": "L\u00f6schen",
+ "lang_hint": "Hinweis",
+ "lang_next": "Weiter",
+ "lang_save": "Speichern",
"lang_today": "Heute",
"lang_yesterday": "Gestern"
} \ No newline at end of file
diff --git a/modules-available/main/lang/de/template-tags.json b/modules-available/main/lang/de/template-tags.json
index e9b829e8..8a3120d3 100644
--- a/modules-available/main/lang/de/template-tags.json
+++ b/modules-available/main/lang/de/template-tags.json
@@ -10,7 +10,6 @@
"lang_logout": "Abmelden",
"lang_minilinuxMissing": "Wichtige Dateien der MiniLinux-Installation fehlen.",
"lang_needsSetup": "Einrichtung unvollst\u00e4ndig",
- "lang_next": "Weiter",
"lang_noExistingAccount": "Es existiert noch kein Administrator-Zugang f\u00fcr diesen Satelliten-Server.",
"lang_numerOfImagesMarkedForDeletion": "Zur L\u00f6schung markierte Abbilder",
"lang_register": "Registrieren",
diff --git a/modules-available/main/lang/en/global-tags.json b/modules-available/main/lang/en/global-tags.json
index f5fbed61..10581ae2 100644
--- a/modules-available/main/lang/en/global-tags.json
+++ b/modules-available/main/lang/en/global-tags.json
@@ -1,4 +1,11 @@
{
+ "lang_back": "Back",
+ "lang_cancel": "Cancel",
+ "lang_close": "Close",
+ "lang_delete": "Delete",
+ "lang_hint": "Hint",
+ "lang_next": "Next",
+ "lang_save": "Save",
"lang_today": "Today",
"lang_yesterday": "Yesterday"
} \ No newline at end of file
diff --git a/modules-available/main/lang/en/template-tags.json b/modules-available/main/lang/en/template-tags.json
index 3483bcfe..aeebc998 100644
--- a/modules-available/main/lang/en/template-tags.json
+++ b/modules-available/main/lang/en/template-tags.json
@@ -10,7 +10,6 @@
"lang_logout": "Logout",
"lang_minilinuxMissing": "Important files from the mini Linux installation are missing.",
"lang_needsSetup": "Setup incomplete",
- "lang_next": "Next",
"lang_noExistingAccount": "No account has been created yet. Sign up to become the administrator.",
"lang_numerOfImagesMarkedForDeletion": "Images marked for deletion",
"lang_register": "Register",
diff --git a/modules-available/main/lang/pt/global-tags.json b/modules-available/main/lang/pt/global-tags.json
new file mode 100644
index 00000000..7c1311ec
--- /dev/null
+++ b/modules-available/main/lang/pt/global-tags.json
@@ -0,0 +1,4 @@
+{
+ "lang_close": "Fechar",
+ "lang_next": "Pr\u00f3ximo"
+} \ No newline at end of file
diff --git a/modules-available/main/lang/pt/template-tags.json b/modules-available/main/lang/pt/template-tags.json
index d9acf58b..79e0184e 100644
--- a/modules-available/main/lang/pt/template-tags.json
+++ b/modules-available/main/lang/pt/template-tags.json
@@ -5,7 +5,6 @@
"lang_login": "Entrar",
"lang_logout": "Sair",
"lang_needsSetup": "Instala\u00e7\u00e3o incompleta",
- "lang_next": "Pr\u00f3ximo",
"lang_noExistingAccount": "Nenhuma conta foi criada ainda. Registre-se para se tornar administrador.",
"lang_register": "Registrar",
"lang_translations": "Tradu\u00e7\u00f5es",
diff --git a/modules-available/main/templates/php-errors.html b/modules-available/main/templates/php-errors.html
new file mode 100644
index 00000000..a9932039
--- /dev/null
+++ b/modules-available/main/templates/php-errors.html
@@ -0,0 +1,12 @@
+<div class="alert alert-warning">
+ <div class="slx-bold">
+ <span class="glyphicon glyphicon-warning" aria-hidden="true"></span>
+ {{lang_warningDebug}}
+ </div>
+ <hr>
+ {{#errors}}
+ <div>
+ <b>{{errlevel}}</b>: {{errstr}}&ensp;–&ensp;{{errfile}}&thinsp;:&thinsp;{{errline}}
+ </div>
+ {{/errors}}
+</div>
diff --git a/modules-available/sysconfig/addmodule.inc.php b/modules-available/sysconfig/addmodule.inc.php
index 913a37c1..30ca1d2c 100644
--- a/modules-available/sysconfig/addmodule.inc.php
+++ b/modules-available/sysconfig/addmodule.inc.php
@@ -100,8 +100,9 @@ abstract class AddModule_Base
if (self::$instance === false) {
Util::traceError('No step instance yet');
}
- if (self::$instance->edit !== false)
+ if (self::$instance->edit !== false) {
Message::addInfo('replacing-module', self::$instance->edit->title());
+ }
self::$instance->renderInternal();
}
diff --git a/modules-available/sysconfig/addmodule_adauth.inc.php b/modules-available/sysconfig/addmodule_adauth.inc.php
index cb7bfb4e..1e76f108 100644
--- a/modules-available/sysconfig/addmodule_adauth.inc.php
+++ b/modules-available/sysconfig/addmodule_adauth.inc.php
@@ -25,7 +25,7 @@ class AdAuth_Start extends AddModule_Base
$data['server'] = $out[1];
}
$data['step'] = 'AdAuth_CheckConnection';
- Render::addDialog(Dictionary::translate('config-module', 'adAuth_title'), false, 'ad-start', $data);
+ Render::addDialog(Dictionary::translateFile('config-module', 'adAuth_title'), false, 'ad-start', $data);
}
}
@@ -88,7 +88,7 @@ class AdAuth_CheckConnection extends AddModule_Base
} else {
$data['next'] = 'AdAuth_CheckCredentials';
}
- Render::addDialog(Dictionary::translate('config-module', 'adAuth_title'), false, 'ad_ldap-checkconnection', $data);
+ Render::addDialog(Dictionary::translateFile('config-module', 'adAuth_title'), false, 'ad_ldap-checkconnection', $data);
}
}
@@ -165,7 +165,7 @@ class AdAuth_SelfSearch extends AddModule_Base
} else {
$data['next'] = 'AdAuth_CheckCredentials';
}
- Render::addDialog(Dictionary::translate('config-module', 'adAuth_title'), false, 'ad-selfsearch',
+ Render::addDialog(Dictionary::translateFile('config-module', 'adAuth_title'), false, 'ad-selfsearch',
array_merge($this->taskIds, $data));
}
@@ -218,7 +218,7 @@ class AdAuth_HomeAttrCheck extends AddModule_Base
protected function renderInternal()
{
- Render::addDialog(Dictionary::translate('config-module', 'adAuth_title'), false, 'ad-selfsearch', array_merge($this->taskIds, array(
+ Render::addDialog(Dictionary::translateFile('config-module', 'adAuth_title'), false, 'ad-selfsearch', array_merge($this->taskIds, array(
'edit' => Request::post('edit'),
'title' => Request::post('title'),
'server' => Request::post('server'),
@@ -289,7 +289,7 @@ class AdAuth_CheckCredentials extends AddModule_Base
protected function renderInternal()
{
- Render::addDialog(Dictionary::translate('config-module', 'adAuth_title'), false, 'ad_ldap-checkcredentials', array_merge($this->taskIds, array(
+ Render::addDialog(Dictionary::translateFile('config-module', 'adAuth_title'), false, 'ad_ldap-checkcredentials', array_merge($this->taskIds, array(
'edit' => Request::post('edit'),
'title' => Request::post('title'),
'server' => Request::post('server') . ':' . Request::post('port'),
@@ -407,7 +407,7 @@ class AdAuth_Finish extends AddModule_Base
protected function renderInternal()
{
- Render::addDialog(Dictionary::translate('config-module', 'adAuth_title'), false, 'ad-finish', $this->taskIds);
+ Render::addDialog(Dictionary::translateFile('config-module', 'adAuth_title'), false, 'ad-finish', $this->taskIds);
}
}
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 @@
+<?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);
+ }
+
+}
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 @@
+<?php
+
+ConfigModule::registerModule(
+ ConfigModule_AdAuth::MODID, // ID
+ Dictionary::translateFile('config-module', 'adAuth_title'), // Title
+ Dictionary::translateFile('config-module', 'adAuth_description'), // Description
+ Dictionary::translateFile('config-module', 'group_authentication'), // Group
+ true // Only one per config?
+);
+
+class ConfigModule_AdAuth extends ConfigModule
+{
+
+ const MODID = 'AdAuth';
+ const VERSION = 1;
+
+ private static $REQUIRED_FIELDS = array('server', 'searchbase', 'binddn');
+ private static $OPTIONAL_FIELDS = array('bindpw', 'home', 'ssl', 'fingerprint', 'certificate', 'homeattr');
+
+ protected function generateInternal($tgz, $parent)
+ {
+ Trigger::ldadp($this->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 @@
+<?php
+
+ConfigModule::registerModule(
+ ConfigModule_Branding::MODID, // ID
+ Dictionary::translateFile('config-module', 'branding_title'), // Title
+ Dictionary::translateFile('config-module', 'branding_description'), // Description
+ Dictionary::translateFile('config-module', 'group_branding'), // Group
+ true // Only one per config?
+);
+
+class ConfigModule_Branding extends ConfigModule
+{
+
+ const MODID = 'Branding';
+ const VERSION = 1;
+
+ private $tmpFile = false;
+
+ protected function generateInternal($tgz, $parent)
+ {
+ if (!$this->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 @@
+<?php
+
+ConfigModule::registerModule(
+ ConfigModule_CustomModule::MODID, // ID
+ Dictionary::translateFile('config-module', 'custom_title'), // Title
+ Dictionary::translateFile('config-module', 'custom_description'), // Description
+ Dictionary::translateFile('config-module', 'group_generic'), // Group
+ false, // Only one per config?
+ 100 // Sort order
+);
+
+class ConfigModule_CustomModule extends ConfigModule
+{
+ const MODID = 'CustomModule';
+ const VERSION = 1;
+
+ private $tmpFile = false;
+
+ protected function generateInternal($tgz, $parent)
+ {
+ if (!$this->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 @@
+<?php
+
+ConfigModule::registerModule(
+ ConfigModule_LdapAuth::MODID, // ID
+ Dictionary::translateFile('config-module', 'ldapAuth_title'), // Title
+ Dictionary::translateFile('config-module', 'ldapAuth_description'), // Description
+ Dictionary::translateFile('config-module', 'group_authentication'), // Group
+ true // Only one per config?
+);
+
+class ConfigModule_LdapAuth extends ConfigModule
+{
+
+ const MODID = 'LdapAuth';
+ const VERSION = 1;
+
+ private static $REQUIRED_FIELDS = array('server', 'searchbase');
+ private static $OPTIONAL_FIELDS = array('binddn', 'bindpw', 'home', 'ssl', 'fingerprint', 'certificate');
+
+ protected function generateInternal($tgz, $parent)
+ {
+ Trigger::ldadp($this->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 @@
+<?php
+
+ConfigModule::registerModule(
+ ConfigModule_SshConfig::MODID, // ID
+ Dictionary::translateFile('config-module', 'sshconfig_title'), // Title
+ Dictionary::translateFile('config-module', 'sshconfig_description'), // Description
+ Dictionary::translateFile('config-module', 'group_sshconfig'), // Group
+ true // Only one per config?
+);
+
+class ConfigModule_SshConfig extends ConfigModule
+{
+ const MODID = 'SshConfig';
+ const VERSION = 1;
+
+ protected function generateInternal($tgz, $parent)
+ {
+ if (!$this->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 @@
+<?php
+
+class ConfigTgz
+{
+
+ private $configId = 0;
+ private $configTitle = false;
+ private $file = false;
+ private $modules = array();
+
+ private function __construct()
+ {
+ ;
+ }
+
+ public function id()
+ {
+ return $this->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;
+ }
+
+}
diff --git a/modules-available/sysconfig/lang/de/config-module.json b/modules-available/sysconfig/lang/de/config-module.json
new file mode 100644
index 00000000..4e178b65
--- /dev/null
+++ b/modules-available/sysconfig/lang/de/config-module.json
@@ -0,0 +1,16 @@
+{
+ "adAuth_description": "Mit diesem Modul ist die Anmeldung an den Client-PCs mit den Benutzerkonten eines Active Directory m\u00f6glich. Je nach Konfiguration ist auch die Nutzung eines Benutzerverzeichnisses auf dem Client m\u00f6glich.",
+ "adAuth_title": "Active Directory Authentifizierung",
+ "branding_description": "Das Logo wird am Anmeldebildschirm und beim Starten des Systems angezeigt.",
+ "branding_title": "Einrichtungsspezifisches Logo",
+ "custom_description": "Mit einem generischen Modul ist es m\u00f6glich, beliebige Dateien zum Grundsystem hinzuzuf\u00fcgen.\r\nNutzen Sie dieses Modul, um z.B. spezielle Konfigurationsdateien auf den Client PCs zu verwenden, die sich nicht mit einem der anderen Wizards erstellen l\u00e4sst. Das Hinzuf\u00fcgen eines Erweiterten Moduls erfordert in der Regel zumindest grundlegende Systemkenntnisse im Linuxbereich.",
+ "custom_title": "Generisches Modul",
+ "group_authentication": "Authentifizierung",
+ "group_branding": "Einrichtungsspezifisches Logo",
+ "group_generic": "Generisch",
+ "group_sshconfig": "SSH",
+ "ldapAuth_description": "Mit diesem Modul l\u00e4sst sich eine generische LDAP-Authentifizierung einrichten.",
+ "ldapAuth_title": "LDAP Authentifizierung",
+ "sshconfig_description": "Mit diesem Modul l\u00e4sst sich steuern, ob und wie der sshd auf den gebooteten Clients startet, und welche Funktionen er zur Verf\u00fcgung stellt. Wenn Sie keinen sshd auf den Clients nutzen wollen, brauchen Sie kein solches Modul zu erstellen.",
+ "sshconfig_title": "SSH-D\u00e4mon"
+} \ No newline at end of file
diff --git a/modules-available/sysconfig/lang/en/config-module.json b/modules-available/sysconfig/lang/en/config-module.json
new file mode 100644
index 00000000..efe6f697
--- /dev/null
+++ b/modules-available/sysconfig/lang/en/config-module.json
@@ -0,0 +1,16 @@
+{
+ "adAuth_description": "With this module you can enable logging in on the clients with Active Directory accounts. It's optionally possible to specify a network share used as the user's personal directory.",
+ "adAuth_title": "Active Directory authentication",
+ "branding_description": "The logo will be displayed on the login screen and during system startup.",
+ "branding_title": "Institution's logo",
+ "custom_description": "Using a custom module it is possible to add or override any system file on the client.\r\nYou can use this to make special customizations that aren't possible using one of the configuration wizards. Some basic knowledge about Linux is probably required for this.",
+ "custom_title": "Custom module",
+ "group_authentication": "Authentication",
+ "group_branding": "Branding",
+ "group_generic": "Generic",
+ "group_sshconfig": "SSH",
+ "ldapAuth_description": "This module enables you to create a simple LDAP authentication module.",
+ "ldapAuth_title": "LDAP Authentication",
+ "sshconfig_description": "Here you can set whether the sshd on the clients will start, and what options it will use.",
+ "sshconfig_title": "SSH daemon"
+} \ No newline at end of file
diff --git a/modules-available/sysconfig/lang/pt/config-module.json b/modules-available/sysconfig/lang/pt/config-module.json
new file mode 100644
index 00000000..c44dc44f
--- /dev/null
+++ b/modules-available/sysconfig/lang/pt/config-module.json
@@ -0,0 +1,3 @@
+[
+
+] \ No newline at end of file
diff --git a/modules-available/sysconfig/page.inc.php b/modules-available/sysconfig/page.inc.php
index d4fe4a37..3f14a95d 100644
--- a/modules-available/sysconfig/page.inc.php
+++ b/modules-available/sysconfig/page.inc.php
@@ -54,8 +54,8 @@ class Page_SysConfig extends Page
$action = Request::any('action', 'list');
// Load all addmodule classes, as they populate the $moduleTypes array
- require_once 'modules/sysconfig/addmodule.inc.php';
- foreach (glob('modules/sysconfig/addmodule_*.inc.php') as $file) {
+ require_once Page::getModule()->getDir() . '/addmodule.inc.php';
+ foreach (glob(Page::getModule()->getDir() . '/addmodule_*.inc.php') as $file) {
require_once $file;
}
@@ -375,10 +375,10 @@ class Page_SysConfig extends Page
private function initAddModule()
{
ConfigModule::loadDb();
- require_once 'modules/sysconfig/addmodule.inc.php';
- $step = Request::any('step', 'AddModule_Start');
+ require_once Page::getModule()->getDir() . '/addmodule.inc.php';
+ $step = Request::any('step', 'AddModule_Start', 'string');
if (!class_exists($step) && preg_match('/^([a-zA-Z0-9]+)_/', $step, $out)) {
- require_once 'modules/sysconfig/addmodule_' . strtolower($out[1]) . '.inc.php';
+ require_once Page::getModule()->getDir() . '/addmodule_' . strtolower($out[1]) . '.inc.php';
}
AddModule_Base::setStep($step);
}
@@ -386,7 +386,7 @@ class Page_SysConfig extends Page
private function initAddConfig()
{
ConfigModule::loadDb();
- require_once 'modules/sysconfig/addconfig.inc.php';
+ require_once Page::getModule()->getDir() . '/addconfig.inc.php';
$step = Request::any('step', 0);
if ($step === 0)
$step = 'AddConfig_Start';
diff --git a/modules-available/translation/lang/de/template-tags.json b/modules-available/translation/lang/de/template-tags.json
index 9b4da6bd..83871e46 100644
--- a/modules-available/translation/lang/de/template-tags.json
+++ b/modules-available/translation/lang/de/template-tags.json
@@ -1,8 +1,6 @@
{
"lang_adminInfo": "Dies ist eine Liste aller Templates. Die \"Status\"-Spalte zeigt an, wenn f\u00fcr ein Template \u00fcbersetzungen fehlen, oder veraltete Tags definiert sind.",
- "lang_back": "Zur\u00fcck",
"lang_createTag": "Tag erstellen",
- "lang_deleteTAG": "L\u00f6schen",
"lang_editConfigModule": "Konfigurationsmodulbezeichnungsphrasen editieren",
"lang_editHardcoded": "Hardcoded-Texte bearbeiten",
"lang_editMessages": "Nachrichten bearbeiten",
@@ -11,7 +9,6 @@
"lang_editTemplates": "Template-Texte bearbeiten",
"lang_global": "Global",
"lang_globalTooltip": "Dieser Tag ist global verf\u00fcgbar und braucht normalerweise nicht explizit f\u00fcr dieses Modul \u00fcbersetzt zu werden",
- "lang_hint": "Hinweis",
"lang_langAdministration": "Templates",
"lang_mainHeading": "\u00dcbersetzungen verwalten",
"lang_messages": "Benachrichtigungen",
@@ -19,7 +16,6 @@
"lang_module": "Modul",
"lang_otherStrings": "Andere Texte",
"lang_sample": "Beispiel",
- "lang_save": "Speichern",
"lang_status": "Status",
"lang_tag": "Tag",
"lang_tags": "Tags",
@@ -29,5 +25,5 @@
"lang_translation": "\u00dcbersetzung",
"lang_translationHeading": "Verwalten der \u00dcbersetzungen",
"lang_unused": "Ungenutzt",
- "lang_unusedUnreliableHint": "Die Erkennung von ungenutzten Tags bezieht nur aktivierte Module mit ein. Es k\u00f6nnte sein, dass ein ungenutzt gemeldeter Tag in einem nicht aktivierten Modul verwendet wird."
+ "lang_unusedUnreliableHint": "Die Erkennung ungenutzter Tags bezieht nur aktivierte Module mit ein. Es k\u00f6nnte sein, dass ein ungenutzt gemeldeter Tag in einem nicht aktivierten Modul verwendet wird."
} \ No newline at end of file
diff --git a/modules-available/translation/lang/en/template-tags.json b/modules-available/translation/lang/en/template-tags.json
index 1b5e6dc8..75564bc3 100644
--- a/modules-available/translation/lang/en/template-tags.json
+++ b/modules-available/translation/lang/en/template-tags.json
@@ -1,8 +1,6 @@
{
"lang_adminInfo": "This is a list of all templates. The \"status\"-column tells if there are translations missing, or unused tags are defined.",
- "lang_back": "Back",
"lang_createTag": "Create TAG",
- "lang_deleteTAG": "Delete",
"lang_editConfigModule": "Edit config module strings",
"lang_editHardcoded": "Edit hardcoded strings",
"lang_editMessages": "Edit Messages",
@@ -11,7 +9,6 @@
"lang_editTemplates": "Edit template strings",
"lang_global": "Global",
"lang_globalTooltip": "This tag is global; usually there is no need to translate it explicitly for this module",
- "lang_hint": "Notice",
"lang_langAdministration": "Templates",
"lang_mainHeading": "Translation management",
"lang_messages": "Messages",
@@ -19,7 +16,6 @@
"lang_module": "Module",
"lang_otherStrings": "Other strings",
"lang_sample": "Sample",
- "lang_save": "Save",
"lang_status": "Status",
"lang_tag": "Tag",
"lang_tags": "Tags",
@@ -27,6 +23,7 @@
"lang_templateHint": "Hint: Yellow lines indicate a translation is missing and red lines indicate a tag is not being used by the template.",
"lang_templates": "Templates",
"lang_translation": "Translation",
+ "lang_translationHeading": "Manage translations",
"lang_unused": "Unused",
"lang_unusedUnreliableHint": "Detection of unused tags only includes currently activated modules. It's possible that a tag marked \"unused\" is actually refered to in a module not activated."
} \ No newline at end of file
diff --git a/modules-available/translation/page.inc.php b/modules-available/translation/page.inc.php
index f3474acd..1fc3a0fe 100644
--- a/modules-available/translation/page.inc.php
+++ b/modules-available/translation/page.inc.php
@@ -205,6 +205,7 @@ class Page_Translation extends Page
'module' => $this->module->getIdentifier(),
'moduleName' => $this->module->getDisplayName()
));
+ Render::openTag('div', array('class' => 'row'));
// Templates
$this->showModuleTemplates();
// Messages
@@ -213,6 +214,7 @@ class Page_Translation extends Page
$this->showModuleStrings();
// Module specific
$this->showModuleCustom();
+ Render::closeTag('div');
}
private function showModuleTemplates()
@@ -403,8 +405,13 @@ class Page_Translation extends Page
if ($module === false) {
$module = $this->module;
}
+ $allFiles = $this->getAllFiles('modules', '.php');
+ if ($module->getIdentifier() === 'main') {
+ $allFiles = array_merge($allFiles, $this->getAllFiles('apis', '.php'), $this->getAllFiles('inc', '.php'));
+ $allFiles[] = 'index.php';
+ }
$tags = $this->loadTagsFromPhp('/Message\s*::\s*add\w+\s*\(\s*[\'"](?<module>[^\'"\.]*)\.(?<tag>[^\'"]*)[\'"]\s*(?<data>\)|\,.*)/i',
- $this->getAllFiles('modules', '.php'));
+ $allFiles);
// Filter out tags that don't refer to this module
foreach (array_keys($tags) as $tag) {
// Figure out if this is a message from this module or not
@@ -869,6 +876,7 @@ class Page_Translation extends Page
}
Message::addError('invalid-section', $this->section);
$this->redirect(1);
+ return false;
}
/**
@@ -878,7 +886,7 @@ class Page_Translation extends Page
{
$this->ensureValidDestLanguage();
if ($this->module === false) {
- Message::addError('main.no-module-given');
+ Message::addError('no-module-given');
$this->redirect();
}
$file = $this->getJsonFile();
@@ -906,7 +914,7 @@ class Page_Translation extends Page
$translation = Request::post('new-text', array(), 'array');
foreach (Request::post('new-id', array(), 'array') as $k => $tag) {
- if (empty($translation[$k]))
+ if (empty($translation[$k]) || empty($tag))
continue;
$data[(string)$tag] = (string)$translation[$k];
}
diff --git a/modules-available/translation/templates/custom-list.html b/modules-available/translation/templates/custom-list.html
index 42ee98ad..ee9463f0 100644
--- a/modules-available/translation/templates/custom-list.html
+++ b/modules-available/translation/templates/custom-list.html
@@ -1,21 +1,23 @@
-<div class="panel panel-default">
- <div class="panel-heading">
- {{subsection}}
- </div>
- <div class="panel-body">
- <div class="row">
- <div class="col-sm-6">
- {{#langs}}
- <a href="?do=Translation&amp;module={{module}}&amp;section=custom&amp;subsection={{subsection}}&amp;destlang={{cc}}">{{name}} &raquo;</a>
- <ul>
- <li>{{lang_missing}}: {{missing}}</li>
- <li>{{lang_unused}}: {{unused}}</li>
- </ul>
- {{/langs}}
- </div>
- <div class="col-sm-6">
- {{lang_tags}}: {{tagcount}}
+<div class="col-lg-6">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ {{subsection}}
+ </div>
+ <div class="panel-body">
+ <div class="row">
+ <div class="col-sm-6">
+ {{#langs}}
+ <a href="?do=Translation&amp;module={{module}}&amp;section=custom&amp;subsection={{subsection}}&amp;destlang={{cc}}">{{name}} &raquo;</a>
+ <ul>
+ <li>{{lang_missing}}: {{missing}}</li>
+ <li>{{lang_unused}}: {{unused}}</li>
+ </ul>
+ {{/langs}}
+ </div>
+ <div class="col-sm-6">
+ {{lang_tags}}: {{tagcount}}
+ </div>
</div>
</div>
</div>
-</div>
+</div> \ No newline at end of file
diff --git a/modules-available/translation/templates/edit.html b/modules-available/translation/templates/edit.html
index af176c03..082e2cb5 100644
--- a/modules-available/translation/templates/edit.html
+++ b/modules-available/translation/templates/edit.html
@@ -14,7 +14,7 @@
<input type="hidden" name="subsection" value="{{subsection}}">
<input type="hidden" name="token" value="{{token}}">
<a class="btn btn-primary" href='?do=Translation' >{{lang_back}}</a>
- <button class="btn btn-primary" type="button" onclick="addTag()" >{{lang_createTag}}</button>
+ <button class="btn btn-primary" type="button" onclick="slxAddTag()" >{{lang_createTag}}</button>
<button type="submit" class="btn btn-primary" name="update" value="true">{{lang_save}}</button>
<div class="row">
<div class="col-xs-4 col-sm-3">{{lang_tag}}</div>
@@ -54,7 +54,7 @@
<div class="col-xs-2 col-sm-2 col-lg-1">
<button type="button" class="btn btn-danger btn-xs" onclick="slxDelTag({{tagid}})" tabindex="-1">
<span class="glyphicon glyphicon-remove"></span>
- <span class="hidden-xs">{{lang_deleteTAG}}</span>
+ <span class="hidden-xs">{{lang_delete}}</span>
</button>
</div>
</div>
@@ -81,7 +81,7 @@
' <div class="hidden-xs col-sm-4">' +
' </div>' +
' <div class="col-xs-2 col-sm-1">' +
- ' <button type="button" class="btn btn-danger btn-xs" onclick="slxDelNew(' + slxNewTagCounter + ')"><span class="glyphicon glyphicon-remove"></span> {{lang_deleteTAG}}</button>' +
+ ' <button type="button" class="btn btn-danger btn-xs" onclick="slxDelNew(' + slxNewTagCounter + ')" tabindex="-1"><span class="glyphicon glyphicon-remove"></span> {{lang_delete}}</button>' +
' </div>' +
'</div>'
);
diff --git a/modules-available/translation/templates/message-list.html b/modules-available/translation/templates/message-list.html
index 06cb4964..7e611a05 100644
--- a/modules-available/translation/templates/message-list.html
+++ b/modules-available/translation/templates/message-list.html
@@ -1,30 +1,32 @@
-<div class="panel panel-default">
- <div class="panel-heading">
- {{lang_messages}}
- </div>
- <div class="panel-body">
- <div class="row">
- <div class="col-sm-6">
- {{#langs}}
- <a href="?do=Translation&amp;module={{module}}&amp;section=messages&amp;destlang={{cc}}">{{name}} &raquo;</a>
- <ul>
- <li>{{lang_missing}}: {{missing}}</li>
- <li>{{lang_unused}}: {{unused}}</li>
- </ul>
- {{/langs}}
- </div>
- <div class="col-sm-6">
- <div>{{lang_messages}}: {{messagecount}}</div>
- <ul>
- {{#files}}
- <li>{{file}}</li>
- {{/files}}
- </ul>
- <div>
- <span class="label label-info">{{lang_hint}}</span>
- {{lang_unusedUnreliableHint}}
+<div class="col-lg-6">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ {{lang_messages}}
+ </div>
+ <div class="panel-body">
+ <div class="row">
+ <div class="col-sm-6">
+ {{#langs}}
+ <a href="?do=Translation&amp;module={{module}}&amp;section=messages&amp;destlang={{cc}}">{{name}} &raquo;</a>
+ <ul>
+ <li>{{lang_missing}}: {{missing}}</li>
+ <li>{{lang_unused}}: {{unused}}</li>
+ </ul>
+ {{/langs}}
</div>
+ <div class="col-sm-6 slx-textpreview-200">
+ <div>{{lang_messages}}: {{messagecount}}</div>
+ <ul>
+ {{#files}}
+ <li>{{file}}</li>
+ {{/files}}
+ </ul>
+ </div>
+ </div>
+ <div>
+ <span class="label label-info">{{lang_hint}}</span>
+ {{lang_unusedUnreliableHint}}
</div>
</div>
</div>
-</div>
+</div> \ No newline at end of file
diff --git a/modules-available/translation/templates/string-list.html b/modules-available/translation/templates/string-list.html
index f1301494..064e0ccf 100644
--- a/modules-available/translation/templates/string-list.html
+++ b/modules-available/translation/templates/string-list.html
@@ -1,21 +1,23 @@
-<div class="panel panel-default">
- <div class="panel-heading">
- {{lang_otherStrings}}
- </div>
- <div class="panel-body">
- <div class="row">
- <div class="col-sm-6">
- {{#langs}}
- <a href="?do=Translation&amp;module={{module}}&amp;section=module&amp;destlang={{cc}}">{{name}} &raquo;</a>
- <ul>
- <li>{{lang_missing}}: {{missing}}</li>
- <li>{{lang_unused}}: {{unused}}</li>
- </ul>
- {{/langs}}
- </div>
- <div class="col-sm-6">
- {{lang_tags}}: {{tagcount}}
+<div class="col-lg-6">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ {{lang_otherStrings}}
+ </div>
+ <div class="panel-body">
+ <div class="row">
+ <div class="col-sm-6">
+ {{#langs}}
+ <a href="?do=Translation&amp;module={{module}}&amp;section=module&amp;destlang={{cc}}">{{name}} &raquo;</a>
+ <ul>
+ <li>{{lang_missing}}: {{missing}}</li>
+ <li>{{lang_unused}}: {{unused}}</li>
+ </ul>
+ {{/langs}}
+ </div>
+ <div class="col-sm-6">
+ {{lang_tags}}: {{tagcount}}
+ </div>
</div>
</div>
</div>
-</div>
+</div> \ No newline at end of file
diff --git a/modules-available/translation/templates/template-list.html b/modules-available/translation/templates/template-list.html
index 5d3689ad..2de7c2f8 100644
--- a/modules-available/translation/templates/template-list.html
+++ b/modules-available/translation/templates/template-list.html
@@ -1,26 +1,28 @@
-<div class="panel panel-default">
- <div class="panel-heading">
- {{lang_templates}}
- </div>
- <div class="panel-body">
- <div class="row">
- <div class="col-sm-6">
- {{#langs}}
- <a href="?do=Translation&amp;module={{module}}&amp;section=template&amp;destlang={{cc}}">{{name}} &raquo;</a>
- <ul>
- <li>{{lang_missing}}: {{missing}}</li>
- <li>{{lang_unused}}: {{unused}}</li>
- </ul>
- {{/langs}}
- </div>
- <div class="col-sm-6">
- {{lang_tags}}: {{tagcount}}
- <ul>
- {{#templates}}
- <li>{{template}}</li>
- {{/templates}}
- </ul>
+<div class="col-lg-6">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ {{lang_templates}}
+ </div>
+ <div class="panel-body">
+ <div class="row">
+ <div class="col-sm-6">
+ {{#langs}}
+ <a href="?do=Translation&amp;module={{module}}&amp;section=template&amp;destlang={{cc}}">{{name}} &raquo;</a>
+ <ul>
+ <li>{{lang_missing}}: {{missing}}</li>
+ <li>{{lang_unused}}: {{unused}}</li>
+ </ul>
+ {{/langs}}
+ </div>
+ <div class="col-sm-6">
+ {{lang_tags}}: {{tagcount}}
+ <ul>
+ {{#templates}}
+ <li>{{template}}</li>
+ {{/templates}}
+ </ul>
+ </div>
</div>
</div>
</div>
-</div>
+</div> \ No newline at end of file
diff --git a/modules-available/usermanagement/lang/de/module.json b/modules-available/usermanagement/lang/de/module.json
new file mode 100644
index 00000000..49d72a69
--- /dev/null
+++ b/modules-available/usermanagement/lang/de/module.json
@@ -0,0 +1,3 @@
+{
+ "module_name": "Benutzerverwaltung"
+} \ No newline at end of file