public
nobgit
read
NobMail
Based on mailcow: dockerized
Languages
Repository composition by tracked source files.
PHP
49%
JavaScript
35%
HTML
9%
CSS
4%
Shell
2%
Python
1%
Lua
0%
Perl
0%
Ruby
0%
SCSS
0%
Create file
Wiki Documentation
Clone
https://nobgit.com/orgs/nobgit/nobmail.git
ssh://[email protected]:2222/orgs/nobgit/nobmail.git
Commit
[Web] Customize app menu and logo; Fix #671
81775765
data/web/admin.php | 81 +++++++++++++-
data/web/css/admin.css | 4 +
data/web/css/mailcow.css | 9 ++
data/web/inc/footer.inc.php | 7 ++
data/web/inc/functions.customize.inc.php | 180 +++++++++++++++++++++++++++++++
data/web/inc/functions.mailbox.inc.php | 4 +-
data/web/inc/header.inc.php | 10 +-
data/web/inc/prerequisites.inc.php | 1 +
data/web/inc/sessions.inc.php | 1 +
data/web/inc/triggers.inc.php | 10 ++
data/web/index.php | 10 +-
data/web/js/admin.js | 20 +++-
data/web/js/api.js | 5 +-
data/web/js/bootstrap-filestyle.min.js | 10 ++
data/web/json_api.php | 42 ++++++++
data/web/lang/lang.de.php | 11 ++
data/web/lang/lang.en.php | 12 +++
17 files changed, 407 insertions(+), 10 deletions(-)
create mode 100644 data/web/inc/functions.customize.inc.php
create mode 100644 data/web/js/bootstrap-filestyle.min.js
Diff
diff --git a/data/web/admin.php b/data/web/admin.php
index 6347cd7e..a2ec3d94 100644
--- a/data/web/admin.php
+++ b/data/web/admin.php
@@ -129,6 +129,7 @@ $tfa_data = get_tfa();
<a href="#fwdhosts" class="list-group-item"><?=$lang['admin']['forwarding_hosts'];?></a>
<a href="#f2bparams" class="list-group-item"><?=$lang['admin']['f2b_parameters'];?></a>
<a href="#relayhosts" class="list-group-item">Relayhosts</a>
+ <a href="#customize" class="list-group-item"><?=$lang['admin']['customize'];?></a>
<a href="#top" class="list-group-item" style="border-top:1px dashed #dadada">↸ <?=$lang['admin']['to_top'];?></a>
</div>
</div>
@@ -260,9 +261,7 @@ $tfa_data = get_tfa();
</div>
<div class="form-group">
<label for="private_key_file"><?=$lang['admin']['private_key'];?>:</label>
- <textarea class="form-control" rows="5" name="private_key_file" id="private_key_file" required placeholder="-----BEGIN RSA PRIVATE KEY-----
-XYZ
------END RSA PRIVATE KEY-----"></textarea>
+ <textarea class="form-control" rows="5" name="private_key_file" id="private_key_file" required placeholder="-----BEGIN RSA KEY-----"></textarea>
</div>
<button class="btn btn-default" id="add_item" data-id="dkim_import" data-api-url='add/dkim_import' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['import'];?></button>
</form>
@@ -376,6 +375,82 @@ XYZ
</form>
</div>
</div>
+
+ <span class="anchor" id="customize"></span>
+ <div class="panel panel-default">
+ <div class="panel-heading"><?=$lang['admin']['customize'];?></div>
+ <div class="panel-body">
+ <legend><?=$lang['admin']['change_logo'];?></legend>
+ <p class="help-block"><?=$lang['admin']['logo_info'];?></p>
+ <form class="form-inline" role="form" method="post" enctype="multipart/form-data">
+ <p>
+ <input type="file" name="main_logo" class="filestyle" data-buttonName="btn-default" data-buttonText="Select" accept="image/gif, image/jpeg, image/pjpeg, image/x-png, image/png, image/svg+xml">
+ <button name="submit_main_logo" type="submit" class="btn btn-success"><span class="glyphicon glyphicon-cloud-upload"></span> <?=$lang['admin']['upload'];?></button>
+ </p>
+ </form>
+ <?php
+ if ($main_logo = customize('get', 'main_logo')):
+ $specs = customize('get', 'main_logo_specs');
+ ?>
+ <div class="row">
+ <div class="col-sm-3">
+ <div class="thumbnail">
+ <img class="img-thumbnail" src="<?=$main_logo;?>" alt="mailcow logo">
+ <div class="caption">
+ <span class="label label-info"><?=$specs['geometry']['width'];?>x<?=$specs['geometry']['height'];?> px</span>
+ <span class="label label-info"><?=$specs['mimetype'];?></span>
+ <span class="label label-info"><?=$specs['fileSize'];?></span>
+ </div>
+ </div>
+ <hr>
+ <form class="form-inline" role="form" method="post">
+ <p><button name="reset_main_logo" type="submit" class="btn btn-xs btn-default"><?=$lang['admin']['reset_default'];?></button></p>
+ </form>
+ </div>
+ </div>
+ <?php
+ endif;
+ ?>
+ <legend><?=$lang['admin']['app_links'];?></legend>
+ <p class="help-block"><?=$lang['admin']['merged_vars_hint'];?></p>
+ <form class="form-inline" data-id="app_links" role="form" method="post">
+ <table class="table table-condensed" style="width:1%;white-space: nowrap;" id="app_link_table">
+ <tr>
+ <th><?=$lang['admin']['app_name'];?></th>
+ <th><?=$lang['admin']['link'];?></th>
+ <th> </th>
+ </tr>
+ <?php
+ $app_links = customize('get', 'app_links');
+ foreach ($app_links as $row) {
+ foreach ($row as $key => $val):
+ ?>
+ <tr>
+ <td><input class="input-sm form-control" data-id="app_links" type="text" name="app" required value="<?=$key;?>"></td>
+ <td><input class="input-sm form-control" data-id="app_links" type="text" name="href" required value="<?=$val;?>"></td>
+ <td><a href="#" role="button" class="btn btn-xs btn-default" type="button"><?=$lang['admin']['remove_row'];?></a></td>
+ </tr>
+ <?php
+ endforeach;
+ }
+ foreach ($MAILCOW_APPS as $app):
+ ?>
+ <tr>
+ <td><input class="input-sm form-control" value="<?=htmlspecialchars($app['name']);?>" disabled></td>
+ <td><input class="input-sm form-control" value="<?=htmlspecialchars($app['link']);?>" disabled></td>
+ <td> </td>
+ </tr>
+ <?php
+ endforeach;
+ ?>
+ </table>
+ <div class="btn-group">
+ <button class="btn btn-success" id="edit_selected" data-item="admin" data-id="app_links" data-reload="no" data-api-url='edit/app_links' data-api-attr='{}' href="#"><?=$lang['admin']['save'];?></button>
+ <button class="btn btn-default" type="button" id="add_app_link_row"><?=$lang['admin']['add_row'];?></button>
+ </div>
+ </form>
+ </div>
+ </div>
</div>
</div>
</div>
diff --git a/data/web/css/admin.css b/data/web/css/admin.css
index aa07d7a3..eb6b64f8 100644
--- a/data/web/css/admin.css
+++ b/data/web/css/admin.css
@@ -53,3 +53,7 @@ body.modal-open {
top: 65px;
z-index: 1;
}
+.thumbnail img {
+ min-height:100px;
+ height:100px;
+}
\ No newline at end of file
diff --git a/data/web/css/mailcow.css b/data/web/css/mailcow.css
index e3356ee7..955b05fb 100644
--- a/data/web/css/mailcow.css
+++ b/data/web/css/mailcow.css
@@ -101,4 +101,13 @@ legend {
-ms-user-select: none
-o-user-select: none;
user-select: none;
+}
+.navbar .navbar-brand {
+ padding-top: 5px;
+}
+.navbar .navbar-brand img {
+ height: 40px;
+}
+.mailcow-logo img {
+ max-width: 250px;
}
\ No newline at end of file
diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php
index 55a8f88d..8740612c 100644
--- a/data/web/inc/footer.inc.php
+++ b/data/web/inc/footer.inc.php
@@ -6,10 +6,14 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/modals/footer.php';
<script src="/js/bootstrap-switch.min.js"></script>
<script src="/js/bootstrap-slider.min.js"></script>
<script src="/js/bootstrap-select.min.js"></script>
+<script src="/js/bootstrap-filestyle.min.js"></script>
<script src="/js/notifications.min.js"></script>
<script src="/js/u2f-api.js"></script>
<script src="/js/api.js"></script>
<script>
+$(window).scroll(function() {
+ sessionStorage.scrollTop = $(this).scrollTop();
+});
// Select language and reopen active URL without POST
function setLang(sel) {
$.post( "<?= $_SERVER['REQUEST_URI']; ?>", {lang: sel} );
@@ -192,6 +196,9 @@ $(document).ready(function() {
// CSRF
$('<input type="hidden" value="<?= $_SESSION['CSRF']['TOKEN']; ?>">').attr('id', 'csrf_token').attr('name', 'csrf_token').appendTo('form');
+ if (sessionStorage.scrollTop != "undefined") {
+ $(window).scrollTop(sessionStorage.scrollTop);
+ }
});
</script>
diff --git a/data/web/inc/functions.customize.inc.php b/data/web/inc/functions.customize.inc.php
new file mode 100644
index 00000000..231bc898
--- /dev/null
+++ b/data/web/inc/functions.customize.inc.php
@@ -0,0 +1,180 @@
+<?php
+
+function customize($_action, $_item, $_data = null) {
+ global $redis;
+ global $lang;
+ switch ($_action) {
+ case 'add':
+ if ($_SESSION['mailcow_cc_role'] != "admin") {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => sprintf($lang['danger']['access_denied'])
+ );
+ return false;
+ }
+ switch ($_item) {
+ case 'main_logo':
+ if (in_array($_data['main_logo']['type'], array('image/gif', 'image/jpeg', 'image/pjpeg', 'image/x-png', 'image/png', 'image/svg+xml'))) {
+ try {
+ if (file_exists($_data['main_logo']['tmp_name']) !== true) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'Cannot validate image file: Temporary file not found'
+ );
+ return false;
+ }
+ $image = new Imagick($_data['main_logo']['tmp_name']);
+ if ($image->valid() !== true) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'Cannot validate image file'
+ );
+ return false;
+ }
+ $image->destroy();
+ }
+ catch (ImagickException $e) {
+ $image->destroy();
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'Cannot validate image file'
+ );
+ return false;
+ }
+ }
+ else {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'Invalid mime type'
+ );
+ return false;
+ }
+ try {
+ $redis->Set('MAIN_LOGO', 'data:' . $_data['main_logo']['type'] . ';base64,' . base64_encode(file_get_contents($_data['main_logo']['tmp_name'])));
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'Redis: '.$e
+ );
+ return false;
+ }
+ $_SESSION['return'] = array(
+ 'type' => 'success',
+ 'msg' => 'File uploaded successfully'
+ );
+ break;
+ }
+ break;
+ case 'edit':
+ if ($_SESSION['mailcow_cc_role'] != "admin") {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => sprintf($lang['danger']['access_denied'])
+ );
+ return false;
+ }
+ switch ($_item) {
+ case 'app_links':
+ $apps = (array)$_data['app'];
+ $links = (array)$_data['href'];
+ $out = array();
+ if (count($apps) == count($links)) {;
+ for ($i = 0; $i < count($apps); $i++) {
+ $out[] = array($apps[$i] => $links[$i]);
+ }
+ try {
+ $redis->set('APP_LINKS', json_encode($out));
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'Redis: '.$e
+ );
+ return false;
+ }
+ }
+ $_SESSION['return'] = array(
+ 'type' => 'success',
+ 'msg' => 'Saved changes to app links'
+ );
+ break;
+ }
+ break;
+ case 'delete':
+ if ($_SESSION['mailcow_cc_role'] != "admin") {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => sprintf($lang['danger']['access_denied'])
+ );
+ return false;
+ }
+ switch ($_item) {
+ case 'main_logo':
+ try {
+ if ($redis->del('MAIN_LOGO')) {
+ $_SESSION['return'] = array(
+ 'type' => 'success',
+ 'msg' => 'Reset default logo'
+ );
+ return true;
+ }
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'Redis: '.$e
+ );
+ return false;
+ }
+ break;
+ }
+ break;
+ case 'get':
+ switch ($_item) {
+ case 'app_links':
+ try {
+ $app_links = json_decode($redis->get('APP_LINKS'), true);
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'Redis: '.$e
+ );
+ return false;
+ }
+ return ($app_links) ? $app_links : false;
+ break;
+ case 'main_logo':
+ try {
+ return $redis->get('MAIN_LOGO');
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'Redis: '.$e
+ );
+ return false;
+ }
+ break;
+ case 'main_logo_specs':
+ try {
+ $image = new Imagick();
+ $img_data = explode('base64,', customize('get', 'main_logo'));
+ if ($img_data[1]) {
+ $image->readImageBlob(base64_decode($img_data[1]));
+ }
+ return $image->identifyImage();
+ }
+ catch (ImagickException $e) {
+ $_SESSION['return'] = array(
+ 'type' => 'danger',
+ 'msg' => 'Error: Imagick exception while reading image'
+ );
+ return false;
+ }
+ break;
+ }
+ break;
+ }
+}
\ No newline at end of file
diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php
index 9deb6ef0..209b4acc 100644
--- a/data/web/inc/functions.mailbox.inc.php
+++ b/data/web/inc/functions.mailbox.inc.php
@@ -1291,11 +1291,11 @@ function mailbox($_action, $_type, $_data = null) {
$port1 = (!empty($_data['port1'])) ? $_data['port1'] : $is_now['port1'];
$password1 = (!empty($_data['password1'])) ? $_data['password1'] : $is_now['password1'];
$host1 = (!empty($_data['host1'])) ? $_data['host1'] : $is_now['host1'];
- $subfolder2 = (!empty($_data['subfolder2'])) ? $_data['subfolder2'] : '';
+ $subfolder2 = (isset($_data['subfolder2'])) ? $_data['subfolder2'] : $is_now['subfolder2'];
$enc1 = (!empty($_data['enc1'])) ? $_data['enc1'] : $is_now['enc1'];
$mins_interval = (!empty($_data['mins_interval'])) ? $_data['mins_interval'] : $is_now['mins_interval'];
$exclude = (!empty($_data['exclude'])) ? $_data['exclude'] : '';
- $maxage = (!empty($_data['maxage'])) ? $_data['maxage'] : $is_now['maxage'];
+ $maxage = (isset($_data['maxage']) && $_data['maxage'] != "") ? intval($_data['maxage']) : $is_now['maxage'];
}
else {
$_SESSION['return'] = array(
diff --git a/data/web/inc/header.inc.php b/data/web/inc/header.inc.php
index ccccc616..b64bd5a8 100644
--- a/data/web/inc/header.inc.php
+++ b/data/web/inc/header.inc.php
@@ -39,7 +39,7 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
- <a class="navbar-brand" href="/"><img height="32" alt="mailcow-logo" style="margin-top: -5px;" src="/img/cow_mailcow.svg"></a>
+ <a class="navbar-brand" href="/"><img alt="mailcow-logo" src="<?=($main_logo = customize('get', 'main_logo')) ? $main_logo : '/img/cow_mailcow.svg';?>"></a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
@@ -102,6 +102,14 @@
<li title="<?= htmlspecialchars($app['description']); ?>"><a href="<?= htmlspecialchars($app['link']); ?>"><?= htmlspecialchars($app['name']); ?></a></li>
<?php
endforeach;
+ $app_links = customize('get', 'app_links');
+ foreach ($app_links as $row) {
+ foreach ($row as $key => $val):
+ ?>
+ <li><a href="<?= htmlspecialchars($val); ?>"><?= htmlspecialchars($key); ?></a></li>
+ <?php
+ endforeach;
+ }
?>
</ul>
</li>
diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php
index 1f92e1fb..cdb88804 100644
--- a/data/web/inc/prerequisites.inc.php
+++ b/data/web/inc/prerequisites.inc.php
@@ -63,6 +63,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/lang/lang.en.php';
include $_SERVER['DOCUMENT_ROOT'] . '/lang/lang.'.$_SESSION['mailcow_locale'].'.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.customize.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.domain_admin.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.policy.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.dkim.inc.php';
diff --git a/data/web/inc/sessions.inc.php b/data/web/inc/sessions.inc.php
index 2dbc8fff..abbef40f 100644
--- a/data/web/inc/sessions.inc.php
+++ b/data/web/inc/sessions.inc.php
@@ -53,6 +53,7 @@ if (isset($_SESSION['mailcow_cc_role']) && session_check() === false) {
'msg' => 'Form token invalid or timed out'
);
$_POST = array();
+ $_FILES = array();
}
// Handle logouts
diff --git a/data/web/inc/triggers.inc.php b/data/web/inc/triggers.inc.php
index b91acd93..10fbe4cc 100644
--- a/data/web/inc/triggers.inc.php
+++ b/data/web/inc/triggers.inc.php
@@ -63,4 +63,14 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
unset_tfa_key($_POST);
}
}
+if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") {
+ if (isset($_POST["submit_main_logo"])) {
+ if ($_FILES['main_logo']['error'] == 0) {
+ customize('add', 'main_logo', $_FILES);
+ }
+ }
+ if (isset($_POST["reset_main_logo"])) {
+ customize('delete', 'main_logo');
+ }
+}
?>
diff --git a/data/web/index.php b/data/web/index.php
index 3202fb5b..6c5ba3c7 100644
--- a/data/web/index.php
+++ b/data/web/index.php
@@ -23,7 +23,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
<div class="panel panel-default">
<div class="panel-heading"><span class="glyphicon glyphicon-user" aria-hidden="true"></span> <?= $lang['login']['login']; ?></div>
<div class="panel-body">
- <div class="text-center"><img style="max-width: 250px;" src="/img/cow_mailcow.svg" alt="mailcow"></div>
+ <div class="text-center mailcow-logo"><img src="<?=($main_logo = customize('get', 'main_logo')) ? $main_logo : '/img/cow_mailcow.svg';?>" alt="mailcow"></div>
<legend>mailcow UI</legend>
<form method="post" autofill="off">
<div class="form-group">
@@ -72,6 +72,14 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
<a href="<?= htmlspecialchars($app['link']); ?>" role="button" title="<?= htmlspecialchars($app['description']); ?>" class="btn btn-lg btn-default"><?= htmlspecialchars($app['name']); ?></a>
<?php
endforeach;
+ $app_links = customize('get', 'app_links');
+ foreach ($app_links as $row) {
+ foreach ($row as $key => $val):
+ ?>
+ <a href="<?= htmlspecialchars($val); ?>" role="button" class="btn btn-lg btn-default"><?= htmlspecialchars($key); ?></a>
+ <?php
+ endforeach;
+ }
?>
</div>
</div>
diff --git a/data/web/js/admin.js b/data/web/js/admin.js
index c6b3b22c..83829ab7 100644
--- a/data/web/js/admin.js
+++ b/data/web/js/admin.js
@@ -90,7 +90,7 @@ var Base64 = {
return t
}
}
-
+
jQuery(function($){
// http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
var entityMap = {
@@ -730,6 +730,24 @@ jQuery(function($){
}
});
})
+
+ function add_table_row(table_id) {
+ var row = $('<tr />');
+ cols = '<td><input class="input-sm form-control" data-id="app_links" type="text" name="app" required></td>';
+ cols += '<td><input class="input-sm form-control" data-id="app_links" type="text" name="href" required></td>';
+ cols += '<td><a href="#" role="button" class="btn btn-xs btn-default" type="button">Remove row</a></td>';
+ row.append(cols);
+ table_id.append(row);
+ }
+
+ $('#app_link_table').on('click', 'tr a', function (e) {
+ e.preventDefault();
+ $(this).parents('tr').remove();
+ });
+
+ $('#add_app_link_row').click(function() {
+ add_table_row($('#app_link_table'));
+ });
});
$(window).load(function(){
diff --git a/data/web/js/api.js b/data/web/js/api.js
index a64ce04f..8935f856 100644
--- a/data/web/js/api.js
+++ b/data/web/js/api.js
@@ -14,7 +14,7 @@ $(document).ready(function() {
});
return o;
};
- // Collect values of input fields with name "multi_select" an same data-id to js array multi_data[data-id]
+ // Collect values of input fields with name "multi_select" and same data-id to js array multi_data[data-id]
var multi_data = [];
$(document).on('change', 'input[name=multi_select]:checkbox', function() {
if ($(this).is(':checked') && $(this).data('id')) {
@@ -105,7 +105,8 @@ $(document).ready(function() {
url: '/api/v1/' + api_url,
jsonp: false,
complete: function(data) {
- // var reponse = (JSON.parse(data.responseText));
+ var response = (data.responseText);
+ // alert(response);
// console.log(reponse.type);
// console.log(reponse.msg);
window.location = window.location.href.split("#")[0];
diff --git a/data/web/js/bootstrap-filestyle.min.js b/data/web/js/bootstrap-filestyle.min.js
new file mode 100644
index 00000000..9aa702c6
--- /dev/null
+++ b/data/web/js/bootstrap-filestyle.min.js
@@ -0,0 +1,10 @@
+/*
+ * bootstrap-filestyle
+ * doc: http://markusslima.github.io/bootstrap-filestyle/
+ * github: https://github.com/markusslima/bootstrap-filestyle
+ *
+ * Copyright (c) 2017 Markus Vinicius da Silva Lima
+ * Version 1.2.3
+ * Licensed under the MIT license.
+ */
+!function(t){"use strict";var e=0,i=function(e,i){this.options=i,this.$elementFilestyle=[],this.$element=t(e)};i.prototype={clear:function(){this.$element.val(""),this.$elementFilestyle.find(":text").val(""),this.$elementFilestyle.find(".badge").remove()},destroy:function(){this.$element.removeAttr("style").removeData("filestyle"),this.$elementFilestyle.remove()},disabled:function(t){if(!0===t)this.options.disabled||(this.$element.attr("disabled","true"),this.$elementFilestyle.find("label").attr("disabled","true"),this.options.disabled=!0);else{if(!1!==t)return this.options.disabled;this.options.disabled&&(this.$element.removeAttr("disabled"),this.$elementFilestyle.find("label").removeAttr("disabled"),this.options.disabled=!1)}},buttonBefore:function(t){if(!0===t)this.options.buttonBefore||(this.options.buttonBefore=!0,this.options.input&&(this.$elementFilestyle.remove(),this.constructor(),this.pushNameFiles()));else{if(!1!==t)return this.options.buttonBefore;this.options.buttonBefore&&(this.options.buttonBefore=!1,this.options.input&&(this.$elementFilestyle.remove(),this.constructor(),this.pushNameFiles()))}},icon:function(t){if(!0===t)this.options.icon||(this.options.icon=!0,this.$elementFilestyle.find("label").prepend(this.htmlIcon()));else{if(!1!==t)return this.options.icon;this.options.icon&&(this.options.icon=!1,this.$elementFilestyle.find(".icon-span-filestyle").remove())}},input:function(t){if(!0===t)this.options.input||(this.options.input=!0,this.options.buttonBefore?this.$elementFilestyle.append(this.htmlInput()):this.$elementFilestyle.prepend(this.htmlInput()),this.$elementFilestyle.find(".badge").remove(),this.pushNameFiles(),this.$elementFilestyle.find(".group-span-filestyle").addClass("input-group-btn"));else{if(!1!==t)return this.options.input;if(this.options.input){this.options.input=!1,this.$elementFilestyle.find(":text").remove();var e=this.pushNameFiles();e.length>0&&this.options.badge&&this.$elementFilestyle.find("label").append(' <span class="badge">'+e.length+"</span>"),this.$elementFilestyle.find(".group-span-filestyle").removeClass("input-group-btn")}}},size:function(t){if(void 0===t)return this.options.size;var e=this.$elementFilestyle.find("label"),i=this.$elementFilestyle.find("input");e.removeClass("btn-lg btn-sm"),i.removeClass("input-lg input-sm"),"nr"!=t&&(e.addClass("btn-"+t),i.addClass("input-"+t))},placeholder:function(t){if(void 0===t)return this.options.placeholder;this.options.placeholder=t,this.$elementFilestyle.find("input").attr("placeholder",t)},buttonText:function(t){if(void 0===t)return this.options.buttonText;this.options.buttonText=t,this.$elementFilestyle.find("label .buttonText").html(this.options.buttonText)},buttonName:function(t){if(void 0===t)return this.options.buttonName;this.options.buttonName=t,this.$elementFilestyle.find("label").attr({class:"btn "+this.options.buttonName})},iconName:function(t){if(void 0===t)return this.options.iconName;this.$elementFilestyle.find(".icon-span-filestyle").attr({class:"icon-span-filestyle "+this.options.iconName})},htmlIcon:function(){return this.options.icon?'<span style="margin-right: 3px;" class="icon-span-filestyle '+this.options.iconName+'"></span> ':""},htmlInput:function(){return this.options.input?'<input type="text" class="form-control '+("nr"==this.options.size?"":"input-"+this.options.size)+'" placeholder="'+this.options.placeholder+'" disabled> ':""},pushNameFiles:function(){var t="",e=[];void 0===this.$element[0].files?e[0]={name:this.$element[0]&&this.$element[0].value}:e=this.$element[0].files;for(var i=0;i<e.length;i++)t+=e[i].name.split("\\").pop()+", ";return""!==t?this.$elementFilestyle.find(":text").val(t.replace(/\, $/g,"")):this.$elementFilestyle.find(":text").val(""),e},constructor:function(){var i=this,n="",s=i.$element.attr("id"),l="";""!==s&&s||(s="filestyle-"+e,i.$element.attr({id:s}),e++),l='<span class="group-span-filestyle '+(i.options.input?"input-group-btn":"")+'"><label for="'+s+'" class="btn '+i.options.buttonName+" "+("nr"==i.options.size?"":"btn-"+i.options.size)+'" '+(i.options.disabled||i.$element.attr("disabled")?'disabled="true"':"")+">"+i.htmlIcon()+'<span class="buttonText">'+i.options.buttonText+"</span></label></span>",n=i.options.buttonBefore?l+i.htmlInput():i.htmlInput()+l,i.$elementFilestyle=t('<div class="bootstrap-filestyle input-group">'+n+"</div>"),i.$elementFilestyle.find(".group-span-filestyle").attr("tabindex","0").keypress(function(t){if(13===t.keyCode||32===t.charCode)return i.$elementFilestyle.find("label").click(),!1}),i.$element.css({position:"absolute",clip:"rect(0px 0px 0px 0px)"}).attr("tabindex","-1").after(i.$elementFilestyle),(i.options.disabled||i.$element.attr("disabled"))&&i.$element.attr("disabled","true"),i.$element.change(function(){var t=i.pushNameFiles();0==i.options.input&&i.options.badge?0==i.$elementFilestyle.find(".badge").length?i.$elementFilestyle.find("label").append(' <span class="badge">'+t.length+"</span>"):0==t.length?i.$elementFilestyle.find(".badge").remove():i.$elementFilestyle.find(".badge").html(t.length):i.$elementFilestyle.find(".badge").remove()}),window.navigator.userAgent.search(/firefox/i)>-1&&i.$elementFilestyle.find("label").click(function(){return i.$element.click(),!1})}};var n=t.fn.filestyle;t.fn.filestyle=function(e,n){var s="",l=this.each(function(){if("file"===t(this).attr("type")){var l=t(this),o=l.data("filestyle"),a=t.extend({},t.fn.filestyle.defaults,e,"object"==typeof e&&e);o||(l.data("filestyle",o=new i(this,a)),o.constructor()),"string"==typeof e&&(s=o[e](n))}});return void 0!==typeof s?s:l},t.fn.filestyle.defaults={buttonText:"Choose file",iconName:"glyphicon glyphicon-folder-open",buttonName:"btn-default",size:"nr",input:!0,badge:!0,icon:!0,buttonBefore:!1,disabled:!1,placeholder:""},t.fn.filestyle.noConflict=function(){return t.fn.filestyle=n,this},t(function(){t(".filestyle").each(function(){var e=t(this),i={input:"false"!==e.attr("data-input"),icon:"false"!==e.attr("data-icon"),buttonBefore:"true"===e.attr("data-buttonBefore"),disabled:"true"===e.attr("data-disabled"),size:e.attr("data-size"),buttonText:e.attr("data-buttonText"),buttonName:e.attr("data-buttonName"),iconName:e.attr("data-iconName"),badge:"false"!==e.attr("data-badge"),placeholder:e.attr("data-placeholder")};e.filestyle(i)})})}(window.jQuery);
\ No newline at end of file
diff --git a/data/web/json_api.php b/data/web/json_api.php
index 7673f294..0e4f9e87 100644
--- a/data/web/json_api.php
+++ b/data/web/json_api.php
@@ -1795,6 +1795,48 @@ if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_u
));
}
break;
+ case "app_links":
+ if (isset($_POST['attr'])) {
+ $attr = (array)json_decode($_POST['attr'], true);
+ if (is_array($attr)) {
+ if (customize('edit', 'app_links', $attr) === false) {
+ if (isset($_SESSION['return'])) {
+ echo json_encode($_SESSION['return']);
+ }
+ else {
+ echo json_encode(array(
+ 'type' => 'error',
+ 'msg' => 'Edit failed'
+ ));
+ }
+ exit();
+ }
+ else {
+ if (isset($_SESSION['return'])) {
+ echo json_encode($_SESSION['return']);
+ }
+ else {
+ echo json_encode(array(
+ 'type' => 'success',
+ 'msg' => 'Task completed'
+ ));
+ }
+ }
+ }
+ else {
+ echo json_encode(array(
+ 'type' => 'error',
+ 'msg' => 'Incomplete post data'
+ ));
+ }
+ }
+ else {
+ echo json_encode(array(
+ 'type' => 'error',
+ 'msg' => 'Incomplete post data'
+ ));
+ }
+ break;
case "relayhost":
if (isset($_POST['items']) && isset($_POST['attr'])) {
$items = (array)json_decode($_POST['items'], true);
diff --git a/data/web/lang/lang.de.php b/data/web/lang/lang.de.php
index 733d3782..681dca4e 100644
--- a/data/web/lang/lang.de.php
+++ b/data/web/lang/lang.de.php
@@ -512,3 +512,14 @@ $lang['success']['relayhost_removed'] = "Relayhost %s wurde entfernt";
$lang['success']['relayhost_added'] = "Relayhost %s wurde hinzugefügt";
$lang['admin']['relay_from'] = "Absenderadresse";
$lang['admin']['relay_run'] = "Test durchführen";
+$lang['admin']['customize'] = "Anpassung";
+$lang['admin']['change_logo'] = "Logo ändern";
+$lang['admin']['logo_info'] = "Die hochgeladene Grafik wird für die Navigationsleiste auf eine Höhe von 40px skaliert. Für die Startseite ist eine Skalierung auf eine maximale Breite von 250px programmiert. Eine frei skalierbare Grafik (etwa SVG) wird empfohlen.";
+$lang['admin']['upload'] = "Hochladen";
+$lang['admin']['app_links'] = "App Links";
+$lang['admin']['app_name'] = "App Name";
+$lang['admin']['link'] = "Link";
+$lang['admin']['remove_row'] = "Zeile entfernen";
+$lang['admin']['add_row'] = "Zeile hinzufügen";
+$lang['admin']['reset_default'] = "Auf Standard zurücksetzen";
+$lang['admin']['merged_vars_hint'] = 'Ausgegraute Zeilen wurden aus der Datei <code>vars.inc.(local.)php</code> gelesen und können nicht mittels UI verändert werden.';
diff --git a/data/web/lang/lang.en.php b/data/web/lang/lang.en.php
index aa41d1b0..527ded11 100644
--- a/data/web/lang/lang.en.php
+++ b/data/web/lang/lang.en.php
@@ -525,3 +525,15 @@ $lang['success']['relayhost_removed'] = "Relayhost %s has been removed";
$lang['success']['relayhost_added'] = "Relayhost %s has been added";
$lang['admin']['relay_from'] = '"From:" address';
$lang['admin']['relay_run'] = "Run test";
+
+$lang['admin']['customize'] = "Customize";
+$lang['admin']['change_logo'] = "Change logo";
+$lang['admin']['logo_info'] = "Your image will be scaled to a height of 40px for the top navigation bar and a max. width of 250px for the start page. A scalable graphic is highly recommended.";
+$lang['admin']['upload'] = "Upload";
+$lang['admin']['app_links'] = "App links";
+$lang['admin']['app_name'] = "App name";
+$lang['admin']['link'] = "Link";
+$lang['admin']['remove_row'] = "Remove row";
+$lang['admin']['add_row'] = "Add row";
+$lang['admin']['reset_default'] = "Reset to default";
+$lang['admin']['merged_vars_hint'] = 'Greyed out rows were merged from <code>vars.inc.(local.)php</code> and cannot be modified.';