Commit c21f7b70 authored by Phil Höfer's avatar Phil Höfer
Browse files

Merge branch 'development' into '504-mobile-styles-optimieren-inkl-quicktips'

# Conflicts:
#   app/MetaGer.php
parents af96c7e4 183146e2
......@@ -6,4 +6,13 @@ Homestead.json
Homestead.yaml
.env
.orig
.vscode
\ No newline at end of file
.vscode
# The Files created by Gulp in the build process
/public/build
/public/js/lib.js
/public/js/quicktips.js
/public/js/scriptStartPage.js
/public/js/scriptResultPage.js
/public/css/beitritt.css
/public/css/themes/default.css
**/*.map
......@@ -27,6 +27,8 @@ update(144.76.113.134):
- chmod 777 config/sumas.xml config/sumasEn.xml database/metager.sqlite
- chmod -R 777 storage
- chmod -R 777 bootstrap/cache
- npm install
- ./gulpbuild.sh
- if [ -f ~/MetaGer/artisan ]; then php ~/MetaGer/artisan down;fi
- cd ~/
- while [ -d ~/MetaGer ]; do rm -rf ~/MetaGer;done
......@@ -64,6 +66,8 @@ update(metager2):
- chmod 777 config/sumas.xml config/sumasEn.xml database/metager.sqlite
- chmod -R 777 storage
- chmod -R 777 bootstrap/cache
- npm install
- ./gulpbuild.sh
- if [ -f ~/MetaGer/artisan ]; then php ~/MetaGer/artisan down;fi
- cd ~/
- while [ -d ~/MetaGer ]; do rm -rf ~/MetaGer;done
......@@ -101,6 +105,8 @@ update(metager3.de):
- chmod 777 config/sumas.xml config/sumasEn.xml database/metager.sqlite
- chmod -R 777 storage
- chmod -R 777 bootstrap/cache
- npm install
- ./gulpbuild.sh
- if [ -f ~/MetaGer/artisan ]; then php ~/MetaGer/artisan down;fi
- cd ~/
- while [ -d ~/MetaGer ]; do rm -rf ~/MetaGer;done
......
......@@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\LanguageObject;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
......@@ -87,11 +88,11 @@ class LanguageController extends Controller
$langTexts[$dir] += count($this->getValues([$key => $value]));
}
$filePath[basename($filename)] = preg_replace("/lang\/.*?\//si", "lang/$to/", substr($filename, strpos($filename, "lang")));
}
}
}
$langs = [];
$fn = "";
$t = [];
......@@ -99,10 +100,11 @@ class LanguageController extends Controller
if ($exclude !== "") {
try {
$ex = unserialize(base64_decode($exclude));
} catch (\ErrorException $e) {
} catch (ErrorException $e) {
$ex = ['files' => [], 'new' => 0];
}
}
foreach ($texts as $filename => $text) {
$has = false;
foreach ($ex['files'] as $file) {
......@@ -118,7 +120,6 @@ class LanguageController extends Controller
}
# Hier können wir später die bereits bearbeiteten Dateien ausschließen.
foreach ($text as $textname => $languages) {
if ($languages === "") {
continue;
}
......@@ -136,25 +137,111 @@ class LanguageController extends Controller
if (!isset($languages[$to])) {
$fn = $filePath[$filename];
$t = $text;
break;
break 2;
}
}
}
$t = $this->htmlEscape($t, $to);
$t = $this->createHints($t, $to);
return view('languages.edit')
->with('texts', $t)
->with('filename', $fn)
->with('title', trans('titles.languages.edit'))
->with('langs', $langs)
->with('to', $to)
->with('langTexts', $langTexts)
->with('sum', $sum)
->with('new', $ex["new"])
->with('email', $email);
->with('texts', $t) //Array mit vorhandenen Übersetzungen der Datei $fn in beiden Sprachen
->with('filename', $fn) //Pfad zur angezeigten Datei
->with('title', trans('titles.languages.edit'))
->with('langs', $langs) //Ausgangssprache (1 Element)
->with('to', $to) //zu bearbeitende Sprache
->with('langTexts', $langTexts) //Anzahl der vorhandenen Übersetzungen
->with('sum', $sum) //Alle vorhandenen Texte (in allen Dateien) in beiden Sprachen in einem Array
->with('new', $ex["new"]) //
->with('email', $email); //Email-Adresse des Benutzers
}
public function createSynopticEditPage(Request $request, $exclude = "")
{
$languageFilePath = resource_path() . "/lang/";
$languageFolders = scandir($languageFilePath);
#Enthält zu jeder Sprache ein Objekt mit allen Daten
$languageObjects = [];
$to = [];
#Dekodieren ausgeschlossener Dateien anhand des URL-Parameters
$ex = ['files' => [], 'new' => 0];
if ($exclude !== "") {
try {
$ex = unserialize(base64_decode($exclude));
} catch (\ErrorException $e) {
$ex = ['files' => [], 'new' => 0];
}
}
#Instanziiere LanguageObject
foreach ($languageFolders as $folder) {
if (is_dir($languageFilePath . $folder) && $folder !== "." && $folder !== "..") {
$languageObjects[$folder] = new LanguageObject($folder, $languageFilePath.$folder);
}
}
#Speichere Daten in LanguageObject, überspringe ausgeschlossene Dateien
foreach ($languageObjects as $folder => $languageObject) {
$to[] = $folder;
$di = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($languageObject->filePath));
foreach($di as $filename => $file) {
foreach($ex['files'] as $file) {
if($file === basename($filename)) {
continue 2;
}
}
if(!$this->endsWith($filename, ".")) {
$tmp = include $filename;
foreach ($tmp as $key => $value) {
$languageObject->saveData(basename($filename), $key, $value);
}
}
}
}
$fn = "";
#Wähle die erste, unbearbeitete Datei aus
foreach($languageObjects as $folder => $languageObject) {
foreach($languageObject->stringMap as $languageFileName => $languageFile) {
$fn = $languageFileName;
break 2;
}
}
if($fn === "") {
//Alles bearbeitet -> zeige entsprechende Nachricht
}
$snippets = [];
#Speichere den Inhalt der ausgewählten Datei in allen Sprachen in $snippets ab
foreach($languageObjects as $folder => $languageObject) {
foreach($languageObject->stringMap as $languageFileName => $languageFile) {
if($languageFileName === $fn) {
foreach($languageFile as $key => $value) {
$snippets[$key][$languageObject->language] = $value;
}
continue 2;
}
}
}
#Fülle $snippets auf mit leeren Einträgen für übrige Sprachen
foreach($to as $t) {
foreach($snippets as $key => $langArray) {
if(!isset($langArray[$t])) {
$snippets[$key][$t] = "";
}
}
}
return view('languages.synoptic')
->with('to', $to) #Alle vorhandenen Sprachen
->with('texts', $snippets) #Array mit Sprachsnippets
->with('filename', $fn) #Name der Datei
->with('title', trans('titles.languages.edit'));
}
private function htmlEscape($t, $to)
......
......@@ -9,6 +9,7 @@ use Illuminate\Http\Request;
use Illuminate\Http\Response;
use LaravelLocalization;
use Mail;
use ZipArchive;
class MailController extends Controller
{
......@@ -117,14 +118,54 @@ class MailController extends Controller
}
#Ueberprueft ob ein bereits vorhandener Eintrag bearbeitet worden ist
private function isEdited($k, $v, $filename)
{
try {
$temp = include resource_path()."/".$filename;
foreach ($temp as $key => $value) {
if($k === $key && $v !== $value) {
return true;
}
}
} catch (\ErrorException $e) {
#Datei existiert noch nicht
return true;
}
return false;
}
private function extractLanguage($key)
{
#Kürzt bspw. "_new_de_redirect bzw. "de_redirect" zu "de"
preg_match("/^(?:_new_)?([^_]*)/", $key, $matches);
foreach($matches as $dir) {
if(strlen($dir) == 2)
return $dir;
}
}
private function processKey($key)
{
$key = trim($key);
#Kürzt bspw. "_new_de_redirect bzw. "de_redirect" zu "redirect"
preg_match("/^(?:_new_)?(?:[^_]*)_(\w*.?\w*#?.?\w*)/", $key, $matches);
foreach($matches as $processedKey) {
if(strpos($processedKey, "_") === FALSE) {
return $processedKey;
}
}
return $key;
}
public function sendLanguageFile(Request $request, $from, $to, $exclude = "", $email ="")
{
$filename = $request->input('filename');
# Wir erstellen nun zunächst den Inhalt der Datei:
$data = [];
$new = 0;
$emailAddress = "";
$editedKeys = "";
foreach ($request->all() as $key => $value) {
if ($key === "filename" || $value === "") {
......@@ -138,7 +179,13 @@ class MailController extends Controller
if (strpos($key, "_new_") === 0 && $value !== "") {
$new++;
$key = substr($key, strpos($key, "_new_") + 5);
$editedKeys = $editedKeys."\n".$key;
} else if ($this->isEdited($key, $value, $filename)) {
$new++;
$editedKeys = $editedKeys."\n".$key;
}
$key = trim($key);
if (!strpos($key, "#")) {
$data[$key] = $value;
......@@ -151,7 +198,6 @@ class MailController extends Controller
$ref = &$ref[$key];
$ref = $value;
}
}
$output = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
......@@ -161,10 +207,8 @@ class MailController extends Controller
$output = "<?php\n\nreturn $output;\n";
$message = "Moin moin,\n\nein Benutzer hat eine Sprachdatei aktualisiert.\nSollten die Texte so in Ordnung sein, ersetzt, oder erstellt die Datei aus dem Anhang in folgendem Pfad:\n$filename\n\nFolgend zusätzlich der Inhalt der Datei:\n\n$output";
// echo $request->old('email');
//echo $request->input('email','test');
// die("");
$message = "Moin moin,\n\nein Benutzer hat eine Sprachdatei aktualisiert.\nBearbeitet wurden die Einträge: $editedKeys\n\nSollten die Texte so in Ordnung sein, ersetzt, oder erstellt die Datei aus dem Anhang in folgendem Pfad:\n$filename\n\nFolgend zusätzlich der Inhalt der Datei:\n\n$output";
# Wir haben nun eine Mail an uns geschickt, welche die entsprechende Datei beinhaltet.
# Nun müssen wir den Nutzer eigentlich nur noch zurück leiten und die Letzte bearbeitete Datei ausschließen:
$ex = [];
......@@ -189,8 +233,7 @@ class MailController extends Controller
if($emailAddress !== "") {
Mail::to("dev@suma-ev.de")
->send(new Sprachdatei($message, $output, basename($filename), $emailAddress));
}
else {
} else {
Mail::to("dev@suma-ev.de")
->send(new Sprachdatei($message, $output, basename($filename)));
}
......@@ -199,4 +242,131 @@ class MailController extends Controller
return redirect(url('languages/edit', ['from' => $from, 'to' => $to, 'exclude' => $ex, 'email' => $emailAddress]));
}
public function processSynopticPageInput(Request $request, $exclude = "") {
$filename = $request->input('filename');
#Identifizieren des gedrückten Buttons
if(isset($request['nextpage'])) {
#Leite weiter zur nächsten Seite
$ex = [];
if ($exclude !== "") {
try {
$ex = unserialize(base64_decode($exclude));
} catch (\ErrorException $e) {
$ex = [];
}
if (!isset($ex["files"])) {
$ex["files"] = [];
}
}
if (!isset($ex["new"])) {
$ex["new"] = 0;
}
$ex['files'][] = basename($filename);
$ex = base64_encode(serialize($ex));
return redirect(url('synoptic', ['exclude' => $ex]));
}
#Andernfalls auslesen, zippen und herunterladen der veränderten Dateien
$data = [];
$new = 0;
$editedFiles = [];
foreach ($request->all() as $key => $value) {
if ($key === "filename" || $value === "") {
continue;
}
$key = base64_decode($key);
#Pfad zur Datei anhand des Schlüsselnamens rekonstruieren (Schlüssel enthält Sprachkürzel)
$langdir = $this->extractLanguage($key);
$filepath = "lang/".$langdir."/".$filename;
if (strpos($key, "_new_") === 0 && $value !== "" || $this->isEdited($this->processKey($key), $value, $filepath)) {
$new++;
$editedFiles[$langdir] = $filepath;
}
}
#Erneute Iteration über Request, damit Dateien mitsamt vorherigen Einträgen abgespeichert werden
foreach($request->all() as $key => $value) {
if ($key === "filename" || $value === "") {
continue;
}
$key = base64_decode($key);
#Pfad zur Datei anhand des Schlüsselnamens rekonstruieren (Schlüssel enthält Sprachkürzel)
$langdir = $this->extractLanguage($key);
#Überspringe Datei, falls diese nicht bearbeitet worden ist
if(!isset($editedFiles[$langdir])) {
continue;
}
#Key kuerzen, sodass er nur den eigentlichen Keynamen enthält
$key = $this->processKey($key);
if (!strpos($key, "#")) {
$data[$langdir][$key] = $value;
#Aufdröseln von 2D-Arrays
} else {
$ref = &$data;
do {
$ref = &$ref[$langdir][substr($key, 0, strpos($key, "#"))];
$key = substr($key, strpos($key, "#") + 1);
} while (strpos($key, "#"));
$ref = &$ref[$key];
$ref = $value;
}
}
if(empty($data)) {
return redirect(url('synoptic', ['exclude' => $exclude]));
}
if(file_exists("langfiles.zip"))
unlink("langfiles.zip");
$zip = new ZipArchive();
if ($zip->open("langfiles.zip", ZipArchive::CREATE) !== TRUE) {
exit("Cannot open ".$filename);
}
try{
#Erstelle Ausgabedateien
foreach($data as $lang => $entries) {
$output = json_encode($entries, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$output = preg_replace("/\{/si", "[", $output);
$output = preg_replace("/\}/si", "]", $output);
$output = preg_replace("/\": ([\"\[])/si", "\"\t=>\t$1", $output);
$output = "<?php\n\nreturn $output;\n";
$zip->addEmptyDir($lang);
$zip->addFromString($lang."/".$filename, $output);
}
} catch(ErrorException $e) {
exit("Failed to write ".$filename);
}
$zip->close();
return response()->download("langfiles.zip", $filename.".zip");
}
}
......@@ -270,22 +270,11 @@ class MetaGer
if (LaravelLocalization::getCurrentLocale() === "en") {
$this->ads = [];
}
$this->validated = false;
if (isset($this->password)) {
# Wir bieten einen bezahlten API-Zugriff an, bei dem dementsprechend die Werbung ausgeblendet wurde:
# Aktuell ist es nur die Uni-Mainz. Deshalb überprüfen wir auch nur diese.
$password = getenv('mainz');
$passwordBerlin = getenv('berlin');
$eingabe = $this->eingabe;
$password = md5($eingabe . $password);
$passwordBerlin = md5($eingabe . $passwordBerlin);
if ($this->password === $password || $this->password === $passwordBerlin) {
$this->ads = [];
$this->products = [];
$this->validated = true;
$this->maps = false;
}
if ($this->validated) {
$this->ads = [];
$this->products = [];
$this->maps = false;
}
if (count($this->results) <= 0) {
......@@ -1031,7 +1020,21 @@ class MetaGer
}
$this->apiKey = $request->input('key', '');
$this->validated = false;
if (isset($this->password)) {
# Wir bieten einen bezahlten API-Zugriff an, bei dem dementsprechend die Werbung ausgeblendet wurde:
# Aktuell ist es nur die Uni-Mainz. Deshalb überprüfen wir auch nur diese.
$password = getenv('mainz');
$passwordBerlin = getenv('berlin');
$eingabe = $this->eingabe;
$password = md5($eingabe . $password);
$passwordBerlin = md5($eingabe . $passwordBerlin);
if ($this->password === $password || $this->password === $passwordBerlin) {
$this->validated = true;
}
}
$this->out = $request->input('out', "html");
# Standard output format html
if ($this->out !== "html" && $this->out !== "json" && $this->out !== "results" && $this->out !== "results-with-style" && $this->out !== "result-count" && $this->out !== "rss20" && $this->out !== "rich") {
......
<?php
namespace App\Models;
/*
* Hilfsklasse, welche zu je einer Sprache Angaben zum Pfad der jeweiligen Datei, sowie die vorhandenen Übersetzungen enthält
*/
class LanguageObject
{
public $language = "";
public $filePath = "";
#2D-Array der Form [$filename][$key]
public $stringMap = [];
public function __construct($lang, $path)
{
$this->language = $lang;
$this->filePath = $path;
}
#Speichert Daten in $stringMap, entdimensionalisiert ggbf. $value
public function saveData($filename, $key, $value)
{
if(is_array($value)) {
$this->deMultiDimensionalize($filename, $key, $value);
} else {
$this->stringMap[$filename][$key] = $value;
}
}
private function deMultiDimensionalize($filename, $key, $value)
{
foreach($value as $key2 => $value2) {
$this->saveData($filename, $key."#".$key2, $value2);
}
}
}
\ No newline at end of file
......@@ -24,7 +24,9 @@ elixir(function (mix) {
*/
mix.scripts(['lib/jquery.js', 'lib/jquery-ui.min.js', 'lib/bootstrap.js', 'lib/lightslider.js', 'lib/masonry.js', 'lib/imagesloaded.js', 'lib/openpgp.min.js', 'lib/iframeResizer.min.js', 'lib/md5.js'], 'public/js/lib.js')
mix.scripts(['lib/jquery.js', 'lib/iframeResizer.contentWindow.min.js'], 'public/js/quicktips.js')
mix.version(['css/themes/default.css', 'js/lib.js', 'js/quicktips.js'])
mix.scripts(['scriptStartPage.js', 'results.js'], 'public/js/scriptStartPage.js');
mix.scripts(['scriptResultPage.js', 'results.js'], 'public/js/scriptResultPage.js');
mix.version(['css/themes/default.css', 'js/lib.js', 'js/quicktips.js']);
mix.less('metager/beitritt.less', 'public/css/beitritt.css')
mix.version(['css/beitritt.css'])
mix.version(['js/widgets.js', 'js/editLanguage.js', 'js/kontakt.js', 'js/scriptResultPage.js', 'js/scriptStartPage.js', 'js/settings.js'])
......
{"version":3,"sources":["beitritt.less","beitritt.css"],"names":[],"mappings":"AAAA;EACI;IACI,uBAAA;IACA,YAAA;IACA,aAAA;GCCL;EDCC;IACI,yBAAA;GCCL;EDCC;IACI,yBAAA;GCCL;EDCC;IACI,yBAAA;GCCL;EDCC;IACI,yBAAA;GCCL;EDCC;IACI,0BAAA;GCCL;EDCC;IACI,0BAAA;GCCL;EDCC;;IACI,uBAAA;IACA,wBAAA;IACA,2BAAA;IACA,UAAA;GCEL;EDAC;IACI,uBAAA;IACA,wBAAA;IACA,8BAAA;GCEL;EDAC;IACI,iBAAA;GCEL;EDAC;IACI,iBAAA;GCEL;EDAC;IACI,yBAAA;IACA,0BAAA;GCEL;EACD,6DAA6D;EDA3D;IACI,sBAAA;IACA,2BAAA;IACA,0BAAA;IACA,8BAAA;IACA,yBAAA;IACA,uBAAA;IACA,wBAAA;IACA,2BAAA;IACA,4BAAA;GCEL;EDAC;ICEA,qBAAqB;IDAjB,8BAAA;GCEL;EDAC;ICEA,6BAA6B;IDAzB,8BAAA;GCEL;EDAC;ICEA,yBAAyB;IDArB,8BAAA;GCEL;EDAC;ICEA,2BAA2B;IDAvB,8BAAA;GCEL;EDAC;IACI,0BAAA;GCEL;CACF","file":"beitritt.css","sourcesContent":["@media print {\r\n .container {\r\n width: auto !important;\r\n margin: 0px;\r\n padding: 0px;\r\n }\r\n hr {\r\n display: none !important;\r\n }\r\n #spendenaufruf {\r\n display: none !important;\r\n }\r\n header {\r\n display: none !important;\r\n }\r\n footer {\r\n display: none !important;\r\n }\r\n .wrapper {\r\n padding-top: 0 !important;\r\n }\r\n * {\r\n font-size: 12px!important;\r\n }\r\n input[type=text], input[type=email] {\r\n margin: 0px !important;\r\n padding: 0px !important;\r\n height: initial !important;\r\n border: 0;\r\n }\r\n .beitritt-form-group {\r\n margin: 0px !important;\r\n padding: 0px !important;\r\n margin-bottom: 0px !important;\r\n }\r\n .sign {\r\n margin-top: 10px;\r\n }\r\n .donation-amount-input {\r\n padding-top: 5px;\r\n }\r\n h1 {\r\n margin-top: 0 !important;\r\n padding-top: 0 !important;\r\n }\r\n /* Skalierung für Firefox, absolut für die anderen Browser */\r\n input[type=radio] {\r\n transform: scale(0.5);\r\n -moz-transform: scale(0.5);\r\n -ms-transform: scale(0.5);\r\n -webkit-transform: scale(0.5);\r\n -o-transform: scale(0.5);\r\n width: 20px !important;\r\n height: 20px !important;\r\n margin-top: 0px !important;\r\n padding-top: 0px !important;\r\n }\r\n input::-webkit-input-placeholder {\r\n /* WebKit browsers */\r\n color: transparent !important;\r\n }\r\n input:-moz-placeholder {\r\n /* Mozilla Firefox 4 to 18 */\r\n color: transparent !important;\r\n }\r\n input::-moz-placeholder {\r\n /* Mozilla Firefox 19+ */\r\n color: transparent !important;\r\n }\r\n input:-ms-input-placeholder {\r\n /* Internet Explorer 10+ */\r\n color: transparent !important;\r\n }\r\n .pagebreak {\r\n page-break-before: always;\r\n }\r\n}","@media print {\n .container {\n width: auto !important;\n margin: 0px;\n padding: 0px;\n }\n hr {\n display: none !important;\n }\n #spendenaufruf {\n display: none !important;\n }\n header {\n display: none !important;\n }\n footer {\n display: none !important;\n }\n .wrapper {\n padding-top: 0 !important;\n }\n * {\n font-size: 12px!important;\n }\n input[type=text],\n input[type=email] {\n margin: 0px !important;\n padding: 0px !important;\n height: initial !important;\n border: 0;\n }\n .beitritt-form-group {\n margin: 0px !important;\n padding: 0px !important;\n margin-bottom: 0px !important;\n }\n .sign {\n margin-top: 10px;\n }\n .donation-amount-input {\n padding-top: 5px;\n }\n h1 {\n margin-top: 0 !important;\n padding-top: 0 !important;\n }\n /* Skalierung für Firefox, absolut für die anderen Browser */\n input[type=radio] {\n transform: scale(0.5);\n -moz-transform: scale(0.5);\n -ms-transform: scale(0.5);\n -webkit-transform: scale(0.5);\n -o-transform: scale(0.5);\n width: 20px !important;\n height: 20px !important;\n margin-top: 0px !important;\n padding-top: 0px !important;\n }\n input::-webkit-input-placeholder {\n /* WebKit browsers */\n color: transparent !important;\n }\n input:-moz-placeholder {\n /* Mozilla Firefox 4 to 18 */\n color: transparent !important;\n }\n input::-moz-placeholder {\n /* Mozilla Firefox 19+ */\n color: transparent !important;\n }\n input:-ms-input-placeholder {\n /* Internet Explorer 10+ */\n color: transparent !important;\n }\n .pagebreak {\n page-break-before: always;\n }\n}\n"]}
\ No newline at end of file
This diff is collapsed.
This source diff could not be displayed because it is too large. You can view the blob instead.
$(document).ready(function () {
$('.hint').tooltip();
$('.language-text-area').each(function () {
auto_grow(this);
});
$('.language-text-area').keyup(function () {
auto_grow(this);
});
});
function auto_grow (element) {
element.style.height = '5px';
element.style.height = (element.scrollHeight + 10) + 'px';
}
$(document).ready(function () {
switch (getLanguage()) {
case 'de':
$('.encrypt-btn').html('Verschlüsseln und senden');
break;
case 'en':
$('.encrypt-btn').html('encrypt and send');
break;
case 'es':
// $(".encrypt-btn").html(""); TODO
break;
}
$('.contact').submit(function () {
return encrypt(this);
});
});
// based on https://github.com/encrypt-to/secure.contactform.php
/* The MIT License (MIT)
Copyright (c) 2013 Jan Wiegelmann
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions: