Project import generated by Copybara.

GitOrigin-RevId: 63746295f1a5ab5a619056791995793d65529e62
diff --git a/src/addcalendar.php b/src/addcalendar.php
new file mode 100644
index 0000000..fff0536
--- /dev/null
+++ b/src/addcalendar.php
@@ -0,0 +1,132 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!isset($_GET["id"])) {
+  security::go("calendars.php");
+}
+
+$category = $_GET["id"];
+
+$categoryd = categories::get($category);
+
+if ($categoryd === false && $category != -1) {
+  security::go("calendars.php");
+}
+
+if ($category == -1) {
+  $categoryd = array("name" => "Calendario por defecto");
+}
+
+// These checks are just to know whether the user filled in the form or not.
+// Thus, I added true as the third parameter to not display debug information when they fail.
+$newCalendar = security::checkParams("GET", [
+  ["begins", security::PARAM_ISDATE],
+  ["ends", security::PARAM_ISDATE]
+], true);
+
+$importCalendar = security::checkParams("POST", [
+  ["import", security::PARAM_NEMPTY]
+], true);
+
+$calendarEditor = ($newCalendar || $importCalendar);
+
+if ($importCalendar) {
+  $calendar = json_decode($_POST["import"], true);
+
+  if (json_last_error() !== JSON_ERROR_NONE || !isset($calendar["begins"]) || !isset($calendar["ends"]) || !isset($calendar["calendar"])) {
+    security::go("addcalendar.php?id=".(int)$category."&msg=jsoninvalid");
+  }
+}
+
+$mdHeaderRowBefore = visual::backBtn(($calendarEditor ? "addcalendar.php?id=".(int)$category : "calendars.php"));
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+  <link rel="stylesheet" href="css/calendar.css">
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Añadir calendario a &ldquo;<?=security::htmlsafe($categoryd["name"])?>&rdquo;</h2>
+          <?php
+          if ($calendarEditor) {
+            if ($newCalendar) {
+              $current = new DateTime($_GET["begins"]);
+              $ends = new DateTime($_GET["ends"]);
+            } else {
+              $current = new DateTime();
+              $current->setTimestamp((int)$calendar["begins"]);
+              $ends = new DateTime();
+              $ends->setTimestamp((int)$calendar["ends"]);
+            }
+
+            if ($current->diff($ends)->invert === 1) {
+              security::go("addcalendar.php?id=".(int)$category."&msg=inverted");
+            }
+
+            if (calendars::checkOverlap($category, $current->getTimestamp(), $ends->getTimestamp())) {
+              security::go("addcalendar.php?id=".(int)$category."&msg=overlap");
+            }
+
+            if ($importCalendar) {
+              echo "<p>Este es el calendario que has importado. Ahora puedes hacer las modificaciones que creas oportunas y añadirlo.</p>";
+            }
+            ?>
+            <form action="doaddcalendar.php" method="POST">
+              <input type="hidden" name="id" value="<?=(int)$category?>">
+              <input type="hidden" name="begins" value="<?=security::htmlsafe(($newCalendar ? $_GET["begins"] : $current->format("Y-m-d")))?>">
+              <input type="hidden" name="ends" value="<?=security::htmlsafe(($newCalendar ? $_GET["ends"] : $ends->format("Y-m-d")))?>">
+              <?php
+              calendarsView::renderCalendar($current, $ends, ($newCalendar ? function ($timestamp, $id, $dow, $dom, $extra) {
+                return (($dow >= 6 && $id == calendars::TYPE_FESTIU) || $dow < 6 && $id == calendars::TYPE_LECTIU);
+              } : function ($timestamp, $id, $dow, $dom, $extra) {
+                return ($extra[$timestamp] == $id);
+              }), false, ($newCalendar ? false : $calendar["calendar"]));
+              ?>
+              <button class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent">Añadir</button>
+            </form>
+            <?php
+          } else {
+            ?>
+            <p>Introduce las fechas de inicio y fin del calendario que quieres configurar:</p>
+            <form action="addcalendar.php" method="GET">
+              <input type="hidden" name="id" value="<?=(int)$category?>">
+              <p><label for="begins">Fecha inicio:</label> <input type="date" id="begins" name="begins" required> <label for="ends">Fecha fin:</label> <input type="date" id="ends" name="ends" required></p>
+              <p><button class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent">Empezar a configurar el calendario</button></p>
+            </form>
+            <hr>
+            <p>Alternativamente, puedes importar un calendario para usarlo como plantilla y editarlo antes de crearlo:</p>
+            <form action="addcalendar.php?id=<?=(int)$category?>" method="POST">
+              <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+                <textarea class="mdl-textfield__input" name="import" id="import" rows="3" data-required></textarea>
+                <label class="mdl-textfield__label" for="import">Código JSON del calendario original</label>
+              </div>
+              <p><button class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent">Importar calendario</button></p>
+            </form>
+            <?php
+          }
+          ?>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <script src="js/calendar.js"></script>
+
+  <?php
+  visual::smartSnackbar([
+    ["inverted", "La fecha de inicio debe ser anterior a la fecha de fin."],
+    ["overlap", "El calendario que intentabas añadir se solapa con uno ya existente."],
+    ["jsoninvalid", "El código JSON del calendario que estás importando es incorrecto."]
+  ]);
+  ?>
+</body>
+</html>
diff --git a/src/ajax/addpersontocompany.php b/src/ajax/addpersontocompany.php
new file mode 100644
index 0000000..fb5d3c3
--- /dev/null
+++ b/src/ajax/addpersontocompany.php
@@ -0,0 +1,19 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!security::checkParams("POST", [
+  ["person", security::PARAM_NEMPTY],
+  ["company", security::PARAM_NEMPTY]
+])) {
+  api::error();
+}
+
+$person = (int)$_POST["person"];
+$company = (int)$_POST["company"];
+
+if (people::addToCompany($person, $company)) {
+  echo "OK\n";
+} else {
+  api::error();
+}
diff --git a/src/ajax/addsecuritykey.php b/src/ajax/addsecuritykey.php
new file mode 100644
index 0000000..9a98fd0
--- /dev/null
+++ b/src/ajax/addsecuritykey.php
@@ -0,0 +1,16 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::WORKER, security::METHOD_NOTFOUND);
+secondFactor::checkAvailability();
+
+if ($_SERVER['REQUEST_METHOD'] !== "POST") {
+  api::error('This method should be called with POST.');
+}
+
+try {
+  $result = secondFactor::createRegistrationChallenge();
+} catch (Throwable $e) {
+  api::error('An unexpected error occurred: ' . $e->getMessage());
+}
+
+api::write($result);
diff --git a/src/ajax/addsecuritykey2.php b/src/ajax/addsecuritykey2.php
new file mode 100644
index 0000000..7818a11
--- /dev/null
+++ b/src/ajax/addsecuritykey2.php
@@ -0,0 +1,22 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::WORKER, security::METHOD_NOTFOUND);
+secondFactor::checkAvailability();
+
+if ($_SERVER['REQUEST_METHOD'] !== "POST") {
+  api::error('This method should be called with POST.');
+}
+
+$input = api::inputJson();
+if ($input === false || !isset($input["clientDataJSON"]) || !isset($input["attestationObject"]) || !isset($input["name"])) api::error();
+$clientDataJSON = (string)$input["clientDataJSON"];
+$attestationObject = (string)$input["attestationObject"];
+$name = (string)$input["name"];
+
+try {
+  $result = secondFactor::completeRegistrationChallenge($clientDataJSON, $attestationObject, $name);
+} catch (Throwable $e) {
+  api::error('An unexpected error occurred: ' . $e->getMessage());
+}
+
+api::write($result);
diff --git a/src/ajax/completewebauthnauthentication.php b/src/ajax/completewebauthnauthentication.php
new file mode 100644
index 0000000..8996d42
--- /dev/null
+++ b/src/ajax/completewebauthnauthentication.php
@@ -0,0 +1,21 @@
+<?php
+require_once(__DIR__."/../core.php");
+
+if (!secondFactor::isAvailable() || security::userType() !== security::UNKNOWN || !isset($_SESSION["firstfactorid"]) || !secondFactor::isEnabled($_SESSION["firstfactorid"]) || !secondFactor::hasSecurityKeys($_SESSION["firstfactorid"]) || $_SERVER['REQUEST_METHOD'] !== "POST") {
+  api::error();
+}
+
+$input = api::inputJson();
+if ($input === false || !isset($input["id"]) || !isset($input["clientDataJSON"]) || !isset($input["authenticatorData"]) || !isset($input["signature"])) api::error();
+$id = (string)$input["id"];
+$clientDataJSON = (string)$input["clientDataJSON"];
+$authenticatorData = (string)$input["authenticatorData"];
+$signature = (string)$input["signature"];
+
+try {
+  $result = secondFactor::completeValidationChallenge($id, $clientDataJSON, $authenticatorData, $signature);
+} catch (Throwable $e) {
+  api::error($conf['debug'] ? $e->getMessage() : null);
+}
+
+api::write($result);
diff --git a/src/ajax/startwebauthnauthentication.php b/src/ajax/startwebauthnauthentication.php
new file mode 100644
index 0000000..7dd504e
--- /dev/null
+++ b/src/ajax/startwebauthnauthentication.php
@@ -0,0 +1,16 @@
+<?php
+require_once(__DIR__."/../core.php");
+
+if (!secondFactor::isAvailable() || security::userType() !== security::UNKNOWN || !isset($_SESSION["firstfactorid"]) || !secondFactor::isEnabled($_SESSION["firstfactorid"]) || !secondFactor::hasSecurityKeys($_SESSION["firstfactorid"]) || $_SERVER['REQUEST_METHOD'] !== "POST") {
+  api::error();
+}
+
+try {
+  $result = secondFactor::createValidationChallenge();
+} catch (Throwable $e) {
+  api::error($conf['debug'] ? $e->getMessage() : null);
+}
+
+if (isset($result->publicKey)) $result->publicKey->rpId = ($conf["secondFactor"]["origin"] ?? null);
+
+api::write($result);
diff --git a/src/ajax/verifysecuritycode.php b/src/ajax/verifysecuritycode.php
new file mode 100644
index 0000000..9e5899e
--- /dev/null
+++ b/src/ajax/verifysecuritycode.php
@@ -0,0 +1,17 @@
+<?php
+require_once(__DIR__."/../core.php");
+
+if (!secondFactor::isAvailable() || security::userType() !== security::UNKNOWN || !isset($_SESSION["firstfactorid"]) || !secondFactor::isEnabled($_SESSION["firstfactorid"])) {
+  api::error();
+}
+
+$input = api::inputJson();
+if ($input === false || !isset($input["code"])) api::error();
+
+$code = (string)$input["code"];
+
+if (secondFactor::completeCodeChallenge($code)) {
+  api::write(["status" => "ok"]);
+} else {
+  api::write(["status" => "wrongCode"]);
+}
diff --git a/src/backupdb.php b/src/backupdb.php
new file mode 100644
index 0000000..1d8bcb0
--- /dev/null
+++ b/src/backupdb.php
@@ -0,0 +1,31 @@
+<?php
+require_once("core.php");
+security::checkType(security::HYPERADMIN);
+
+$mdHeaderRowBefore = visual::backBtn("powertools.php");
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Exportar base de datos</h2>
+
+          <form action="dobackupdb.php" method="POST">
+            <input type="hidden" name="format" value="<?=(int)db::EXPORT_DB_FORMAT_SQL?>">
+            <button class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent">Exportar base de datos</button>
+          </form>
+        </div>
+      </div>
+    </main>
+  </div>
+</body>
+</html>
diff --git a/src/calendars.php b/src/calendars.php
new file mode 100644
index 0000000..bc4e908
--- /dev/null
+++ b/src/calendars.php
@@ -0,0 +1,111 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+$mdHeaderRowBefore = visual::backBtn("settings.php");
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+
+  <style>
+  .add-calendar, .category {
+    vertical-align: middle;
+  }
+  </style>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Calendarios</h2>
+          <?php
+          $calendars_response = calendars::getAll();
+
+          foreach ($calendars_response as $ccs) {
+            ?>
+            <h4><span class="category"><?=security::htmlsafe($ccs["category"])?></span> <a href="addcalendar.php?id=<?=(int)$ccs["categoryid"]?>" class="mdl-button mdl-js-button mdl-button--icon mdl-button--accent add-calendar" id="cat<?=(int)$ccs["categoryid"]?>"><i class="material-icons">add</i></a></h4>
+            <?php visual::addTooltip("cat".(int)$ccs["categoryid"], "Añadir un calendario a esta categoría"); ?>
+            <?php
+            if (count($ccs["calendars"])) {
+              ?>
+              <div class="overflow-wrapper overflow-wrapper--for-table">
+                <table class="mdl-data-table mdl-js-data-table mdl-shadow--2dp datatable">
+                  <thead>
+                    <tr>
+                      <?php
+                      if ($conf["debug"]) {
+                        ?>
+                        <th class="extra">ID</th>
+                        <?php
+                      }
+                      ?>
+                      <th class="mdl-data-table__cell--non-numeric">Fecha inicio</th>
+                      <th class="mdl-data-table__cell--non-numeric">Fecha fin</th>
+                      <th class="mdl-data-table__cell--non-numeric"></th>
+                    </tr>
+                  </thead>
+                  <tbody>
+                    <?php
+                    foreach ($ccs["calendars"] as $c) {
+                      ?>
+                      <tr>
+                        <?php
+                        if ($conf["debug"]) {
+                          ?>
+                          <td class="extra"><?=(int)$c["id"]?></td>
+                          <?php
+                        }
+                        ?>
+                        <td class="mdl-data-table__cell--non-numeric"><?=security::htmlsafe(date("d/m/Y", $c["begins"]))?></td>
+                        <td class="mdl-data-table__cell--non-numeric"><?=security::htmlsafe(date("d/m/Y", $c["ends"]))?></td>
+                        <td class='mdl-data-table__cell--non-numeric'>
+                          <a href='editcalendar.php?id=<?=(int)$c['id']?>&view=1' title='Ver calendario'><i class='material-icons icon'>open_in_new</i></a>
+                          <a href='editcalendar.php?id=<?=(int)$c['id']?>' title='Editar calendario'><i class='material-icons icon'>edit</i></a>
+                          <a href='dynamic/deletecalendar.php?id=<?=(int)$c['id']?>' data-dyndialog-href='dynamic/deletecalendar.php?id=<?=(int)$c['id']?>' title='Eliminar calendario'><i class='material-icons icon'>delete</i></a>
+                          <a href='dynamic/exportcalendar.php?id=<?=(int)$c['id']?>' data-dyndialog-href='dynamic/exportcalendar.php?id=<?=(int)$c['id']?>' title='Exportar calendario'><i class='material-icons icon'>code</i></a>
+                        </td>
+                      </tr>
+                      <?php
+                    }
+                    ?>
+                  </tbody>
+                </table>
+              </div>
+              <?php
+            } else {
+              echo "<p>No se ha configurado ningún calendario todavía.</p>";
+            }
+          }
+          ?>
+
+          <?php visual::printDebug("calendars::getAll()", $calendars_response); ?>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <div class="mdl-snackbar mdl-js-snackbar">
+    <div class="mdl-snackbar__text"></div>
+    <button type="button" class="mdl-snackbar__action"></button>
+  </div>
+
+  <?php
+  visual::renderTooltips();
+
+  visual::smartSnackbar([
+    ["added", "Se ha añadido el calendario correctamente."],
+    ["modified", "Se ha modificado el calendario correctamente."],
+    ["deleted", "Se ha eliminado el calendario correctamente."],
+    ["empty", "Faltan datos por introducir en el formulario o el correo electrónico es incorrecto."],
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."],
+    ["overlap", "El calendario que intentabas añadir se solapa con uno ya existente, así que no se ha añadido."]
+  ], 10000, false);
+  ?>
+</body>
+</html>
diff --git a/src/categories.php b/src/categories.php
new file mode 100644
index 0000000..2ee8818
--- /dev/null
+++ b/src/categories.php
@@ -0,0 +1,131 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+$mdHeaderRowBefore = visual::backBtn("settings.php");
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+
+  <style>
+  .addcategory {
+    position: fixed;
+    bottom: 16px;
+    right: 16px;
+    z-index: 1000;
+  }
+
+  @media (max-width: 655px) {
+    .extra {
+      display: none;
+    }
+  }
+  </style>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <button class="addcategory mdl-button md-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--accent"><i class="material-icons">add</i><span class="mdl-ripple"></span></button>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Categorías</h2>
+          <?php
+          $categories = categories::getAll(false);
+          if (count($categories)) {
+            ?>
+            <div class="overflow-wrapper overflow-wrapper--for-table">
+              <table class="mdl-data-table mdl-js-data-table mdl-shadow--2dp">
+                <thead>
+                  <tr>
+                    <th class="extra">ID</th>
+                    <th class="mdl-data-table__cell--non-numeric">Categoría</th>
+                    <th class="mdl-data-table__cell--non-numeric extra">Emails responsables <i id="tt_emails" class="material-icons help">help</i></th>
+                    <th class="mdl-data-table__cell--non-numeric"></th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <?php
+                  foreach ($categories as $c) {
+                    $emails = categories::readableEmails($c["emails"]);
+                    ?>
+                    <tr>
+                      <td class="extra"><?=(int)$c["id"]?></td>
+                      <td class="mdl-data-table__cell--non-numeric"><?=security::htmlsafe($c["name"]).($c["parent"] == 0 ? "" : "<br><span class='mdl-color-text--grey-600'>Padre: ".security::htmlsafe($c["parentname"])."</span>")?></td>
+                      <td class="mdl-data-table__cell--non-numeric extra"><?=security::htmlsafe((empty($emails) ? "-" : $emails))?></td>
+                      <td class='mdl-data-table__cell--non-numeric'><a href='dynamic/editcategory.php?id=<?=(int)$c["id"]?>' data-dyndialog-href='dynamic/editcategory.php?id=<?=(int)$c["id"]?>' title='Editar categoría'><i class='material-icons icon'>edit</i></a></td>
+                    </tr>
+                    <?php
+                  }
+                  ?>
+                </tbody>
+              </table>
+            </div>
+            <?php visual::addTooltip("tt_emails", "Cuando un tipo de incidencia tenga activada las notificaciones a los responsables de categoría, se notificará de las incidencias nuevas a estos correos."); ?>
+            <?php
+          } else {
+            ?>
+            <p>Todavía no hay definida ninguna categoría para los trabajadores.</p>
+            <p>Puedes añadir una haciendo clic en el botón de la esquina inferior derecha de la página.</p>
+            <?php
+          }
+          ?>
+
+          <?php visual::printDebug("categories::getAll()", $categories); ?>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <dialog class="mdl-dialog" id="addcategory">
+    <form action="doaddcategory.php" method="POST" autocomplete="off">
+      <h4 class="mdl-dialog__title">Añade una categoría</h4>
+      <div class="mdl-dialog__content">
+        <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+          <input class="mdl-textfield__input" type="text" name="name" id="name" autocomplete="off" data-required>
+          <label class="mdl-textfield__label" for="name">Nombre de la categoría</label>
+        </div>
+        <br>
+        <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+          <select name="parent" id="parent" class="mdlext-selectfield__select">
+            <option value="0"></option>
+            <?php
+            foreach ($categories as $category) {
+              if ($category["parent"] == 0) echo '<option value="'.$category["id"].'">'.$category["name"].'</option>';
+            }
+            ?>
+          </select>
+          <label for="parent" class="mdlext-selectfield__label">Categoría padre</label>
+        </div>
+        <br>
+        <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+          <textarea class="mdl-textfield__input" name="emails" id="emails"></textarea>
+          <label class="mdl-textfield__label" for="emails">Correos electrónicos de los responsables</label>
+        </div>
+        <span style="font-size: 12px;">Introduce los correos separados por comas.</span>
+      </div>
+      <div class="mdl-dialog__actions">
+        <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Añadir</button>
+        <button onclick="event.preventDefault(); document.querySelector('#addcategory').close();" class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+      </div>
+    </form>
+  </dialog>
+
+  <?php
+  visual::renderTooltips();
+
+  visual::smartSnackbar([
+    ["added", "Se ha añadido la categoría correctamente."],
+    ["modified", "Se ha modificado la categoría correctamente."],
+    ["empty", "Faltan datos por introducir en el formulario."],
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."]
+  ]);
+  ?>
+
+  <script src="js/categories.js"></script>
+</body>
+</html>
diff --git a/src/changepassword.php b/src/changepassword.php
new file mode 100644
index 0000000..fd1d1ee
--- /dev/null
+++ b/src/changepassword.php
@@ -0,0 +1,47 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER);
+security::checkWorkerUIEnabled();
+
+if (secondFactor::isAvailable()) $mdHeaderRowBefore = visual::backBtn("security.php");
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Cambiar contraseña</h2>
+        	<form action="dochangepassword.php" method="POST">
+            <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+              <input class="mdl-textfield__input" type="password" name="oldpassword" id="oldpassword" data-required>
+              <label class="mdl-textfield__label" for="oldpassword">Contraseña actual</label>
+            </div>
+            <br>
+            <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+              <input class="mdl-textfield__input" type="password" name="newpassword" id="newpassword" data-required>
+              <label class="mdl-textfield__label" for="newpassword">Nueva contraseña</label>
+            </div>
+            <p class="mdl-color-text--grey-600"><?=security::htmlsafe(security::$passwordHelperText)?></p>
+
+            <p><button class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent">Cambiar contraseña</button></p>
+          </form>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <?php
+  visual::smartSnackbar([
+    ["wrong", "Ha ocurrido un error intentando cambiar la contraseña. Asegúrate de que has introducido correctamente la contraseña actual."]
+  ]);
+  ?>
+</body>
+</html>
diff --git a/src/companies.php b/src/companies.php
new file mode 100644
index 0000000..787f665
--- /dev/null
+++ b/src/companies.php
@@ -0,0 +1,114 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+$mdHeaderRowBefore = visual::backBtn("settings.php");
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+
+  <style>
+  .addcompany {
+    position: fixed;
+    bottom: 16px;
+    right: 16px;
+    z-index: 1000;
+  }
+
+  @media (max-width: 655px) {
+    .extra {
+      display: none;
+    }
+  }
+  </style>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <button class="addcompany mdl-button md-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--accent"><i class="material-icons">add</i><span class="mdl-ripple"></span></button>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Empresas</h2>
+          <?php
+          $companies = companies::getAll(false);
+          if (count($companies)) {
+            ?>
+            <div class="overflow-wrapper overflow-wrapper--for-table">
+              <table class="mdl-data-table mdl-js-data-table mdl-shadow--2dp">
+                <thead>
+                  <tr>
+                    <th class="extra">ID</th>
+                    <th class="mdl-data-table__cell--non-numeric">Empresa</th>
+                    <th class="mdl-data-table__cell--non-numeric">CIF</th>
+                    <th class="mdl-data-table__cell--non-numeric"></th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <?php
+                  foreach ($companies as $c) {
+                    ?>
+                    <tr>
+                      <td class="extra"><?=(int)$c["id"]?></td>
+                      <td class="mdl-data-table__cell--non-numeric"><?=security::htmlsafe($c["name"])?></td>
+                      <td class="mdl-data-table__cell--non-numeric"><?=(empty($c["cif"]) ? "-" : security::htmlsafe($c["cif"]))?></td>
+                      <td class='mdl-data-table__cell--non-numeric'><a href='dynamic/editcompany.php?id=<?=(int)$c["id"]?>' data-dyndialog-href='dynamic/editcompany.php?id=<?=(int)$c["id"]?>' title='Editar empresa'><i class='material-icons icon'>edit</i></a></td>
+                    </tr>
+                    <?php
+                  }
+                  ?>
+                </tbody>
+              </table>
+            </div>
+            <?php
+          } else {
+            ?>
+            <p>Todavía no hay definida ninguna empresa.</p>
+            <p>Puedes añadir una haciendo clic en el botón de la esquina inferior derecha de la página.</p>
+            <?php
+          }
+          ?>
+
+          <?php visual::printDebug("companies::getAll()", $companies); ?>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <dialog class="mdl-dialog" id="addcompany">
+    <form action="doaddcompany.php" method="POST" autocomplete="off">
+      <h4 class="mdl-dialog__title">Añade una empresa</h4>
+      <div class="mdl-dialog__content">
+        <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+          <input class="mdl-textfield__input" type="text" name="name" id="name" autocomplete="off" data-required>
+          <label class="mdl-textfield__label" for="name">Nombre de la empresa</label>
+        </div>
+        <br>
+        <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+          <input class="mdl-textfield__input" type="text" name="cif" id="cif" autocomplete="off">
+          <label class="mdl-textfield__label" for="cif">CIF (opcional)</label>
+        </div>
+      </div>
+      <div class="mdl-dialog__actions">
+        <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Añadir</button>
+        <button onclick="event.preventDefault(); document.querySelector('#addcompany').close();" class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+      </div>
+    </form>
+  </dialog>
+
+  <?php
+  visual::smartSnackbar([
+    ["added", "Se ha añadido la empresa correctamente."],
+    ["modified", "Se ha modificado la empresa correctamente."],
+    ["empty", "Faltan datos por introducir en el formulario."],
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."]
+  ]);
+  ?>
+
+  <script src="js/companies.js"></script>
+</body>
+</html>
diff --git a/src/config.default.php b/src/config.default.php
new file mode 100644
index 0000000..0c52ac6
--- /dev/null
+++ b/src/config.default.php
@@ -0,0 +1,62 @@
+<?php
+$conf = [];
+$conf["db"] = [];
+$conf["db"]["user"] = ""; // Enter the MySQL username
+$conf["db"]["password"] = ""; // Enter the MySQL password
+$conf["db"]["database"] = ""; // Enter the MySQL database name
+$conf["db"]["host"] = ""; // Enter the MySQL host
+
+$conf["path"] = ""; // Enter the absolute path to the website
+$conf["fullPath"] = ""; // Enter the URI pointing to the website
+$conf["appName"] = "Registro horario"; // Enter the name of the website
+$conf["passwordLen"] = 10; // Password length for the automatically generated passwords
+$conf["enableWorkerUI"] = true; // Allows workers to sign in in order to view their incidents and schedule, and enter new incidents (which will go to the moderation queue)
+$conf["attachmentsFolder"] = ""; // Folder where incident attachments will be saved, with a slash at the end (ex: "/home/user/files/")
+$conf["backgroundColor"] = "green"; // Background color of the website (hex or mdl color)
+$conf["backgroundColorIsDark"] = false; // Whether the background color is dark
+$conf["logo"] = ""; // Optional, set it to a URL of a logo which will be displayed in the nav bar.
+$conf["enableRecovery"] = false; // Sets whether users can recover passwords.
+$conf["debug"] = false; // Sets whether the app shows debug information useful to the developer. WARNING: DO NOT ENABLE IN PRODUCTION AS IT MAY SHOW SENSITIVE INFORMATION
+$conf["superdebug"] = false; // Sets whether to enable super debug mode (for now it only disables redirects and displays verbose errors when calling security::checkParams())
+
+$conf["pdfs"] = [];
+$conf["pdfs"]["workersAlwaysHaveBreakfastAndLunch"] = false; // Sets up whether when an incident overlaps breakfast or lunch, the length of the incident should subtract lunch and breakfast time (false) or not because workers will have lunch or breakfast at another time inside of the work schedule (true). WARNING: SETTING THIS TO TRUE MAY LEAD TO UNEXPECTED RESULTS IN THE SUMMARY SHOWN AT THE BOTTOM OF DETAILED PDFs.
+$conf["pdfs"]["showExactTimeForBreakfastAndLunch"] = true; // Whether time for breakfast and lunch should be indicated with the start and end time (true) or with a summary of how many hours it consisted of (false).
+
+$conf["validation"] = [];
+$conf["validation"]["defaultMethod"] = validations::METHOD_SIMPLE; // Validation method which will be shown by default to a worker to validate incidents/records.
+$conf["validation"]["allowedMethods"] = [validations::METHOD_SIMPLE]; // Validation methods which are allowed to be used by a worker.
+$conf["validation"]["gracePeriod"] = 3; // Grace period for considering a validation as pending by some parts of the script (more info at https://avm99963.github.io/hores-external/administradores/instalacion-y-configuracion/configuracion/#notificacion-de-validaciones-pendientes)
+
+$conf["secondFactor"] = [];
+$conf["secondFactor"]["enabled"] = false; // Whether the second factor is allowed to be used
+$conf["secondFactor"]["origin"] = ""; // Domain name where the application will be hosted (for instance, "example.org")
+
+$conf["mail"] = []; // SMTP details for the email account which sends email notifications
+$conf["mail"]["enabled"] = false; // Whether the app will send email notifications
+$conf["mail"]["smtpauth"] = true; // Whether the SMTP server should be contacted securely
+$conf["mail"]["host"] = ""; // SMTP server host
+$conf["mail"]["port"] = 587; // SMTP server port
+$conf["mail"]["username"] = ""; // Username of the email account
+$conf["mail"]["password"] = ""; // Password of the email account
+$conf["mail"]["remitent"] = ""; // Email address set as the remitent of the emails sent
+$conf["mail"]["remitentName"] = ""; // Name of the remitent
+$conf["mail"]["subjectPrefix"] = "[Registro horario]"; // Prefix which will be added to the subject of all emails
+$conf["mail"]["adminEmail"] = ""; // Email address of the site administrator
+
+$conf["mail"]["capabilities"] = []; // Individual switches for certain email notifications
+$conf["mail"]["capabilities"]["notifyOnWorkerIncidentCreation"] = true; // Notify the site administrator when a worker creates an incident
+$conf["mail"]["capabilities"]["notifyOnAdminIncidentCreation"] = true; // Notify the site administrator when an admin creates an incident
+$conf["mail"]["capabilities"]["notifyCategoryResponsiblesOnIncidentCreation"] = true; // Notify category responsibles when an incident is created either by an admin or a worker (only if the incident type is configured with the 'notify' option)
+$conf["mail"]["capabilities"]["notifyWorkerOnIncidentDecision"] = true; // Notify a worker when their incident is verified to let them know if it was approved or not
+$conf["mail"]["capabilities"]["sendPendingValidationsReminder"] = true; // Notify a worker monthly if they have pending incidents or records to validate
+
+$conf["signinThrottling"] = []; // Settings for the security feature of the app which disables login when it detects unusual behaviour
+$conf["signinThrottling"]["attemptCountLimit"] = []; // Sets limits for the number of sign-in attempts made in the last 10 seconds
+$conf["signinThrottling"]["attemptCountLimit"]["global"] = 300; // Global limit for all sign-in attempts
+$conf["signinThrottling"]["attemptCountLimit"]["ip"] = 25; // Limit for the sign-in attempts made by a single IP address
+$conf["signinThrottling"]["attemptCountLimit"]["ipBlock"] = 100; // Limit for the sign-in attempts made by an IP address block
+$conf["signinThrottling"]["attemptCountLimit"]["ipBlocksPerUsername"] = 3; // Limit for the number of different ip blocks allowed to make sign-in attempts to a single user account
+$conf["signinThrottling"]["attemptCountLimit"]["username"] = 5; // Limit for the sign-in attempts willing to sign in to a single user account
+
+$conf["signinThrottling"]["retentionDays"] = 30; // Sets for how many days we should keep the records in the signinattempts table
diff --git a/src/core.php b/src/core.php
new file mode 100644
index 0000000..1e280d0
--- /dev/null
+++ b/src/core.php
@@ -0,0 +1,40 @@
+<?php
+// Core of the application
+
+const INTERNAL_CLASS_NAMESPACE = "Internal\\";
+
+// Classes autoload
+spl_autoload_register(function($className) {
+  if ($className == "lbuchs\WebAuthn\Binary\ByteBuffer") {
+    include_once(__DIR__."/lib/WebAuthn/Binary/ByteBuffer.php");
+    return;
+  }
+
+
+  include_once(__DIR__."/inc/".$className.".php");
+});
+
+// Getting configuration
+require_once(__DIR__."/config.php");
+
+// Setting timezone and locale accordingly
+date_default_timezone_set("Europe/Madrid");
+setlocale(LC_TIME, 'es_ES.UTF-8', 'es_ES', 'es');
+
+// Database settings
+$con = @mysqli_connect($conf["db"]["server"], $conf["db"]["user"], $conf["db"]["password"], $conf["db"]["database"]) or die("There was an error connecting to the database.\n");
+mysqli_set_charset($con, "utf8mb4");
+
+// Session settings
+session_set_cookie_params([
+  "lifetime" => 0,
+  "path" => $conf["path"],
+  "httponly" => true
+]);
+session_start();
+
+// Check if app has been installed
+if (db::needsSetUp()) {
+  security::logout();
+  die("Please, run install.php from the command line to install the app before using it.");
+}
diff --git a/src/cron/cleansigninattempts.php b/src/cron/cleansigninattempts.php
new file mode 100644
index 0000000..946af49
--- /dev/null
+++ b/src/cron/cleansigninattempts.php
@@ -0,0 +1,17 @@
+<?php
+require_once(__DIR__."/../core.php");
+
+if (php_sapi_name() != "cli") {
+  security::notFound();
+  exit();
+}
+
+echo "=======================\n";
+echo "cleansigninattempts.php\n";
+echo "=======================\n\n";
+
+if (security::cleanSignInAttempts()) {
+  echo "[info] The action was performed successfully.\n";
+} else {
+  echo "[error] An error occurred.\n";
+}
diff --git a/src/cron/generateregistry.php b/src/cron/generateregistry.php
new file mode 100644
index 0000000..b97d13b
--- /dev/null
+++ b/src/cron/generateregistry.php
@@ -0,0 +1,23 @@
+<?php
+require_once(__DIR__."/../core.php");
+
+if (php_sapi_name() != "cli") {
+  security::notFound();
+  exit();
+}
+
+echo "====================\n";
+echo "generateregistry.php\n";
+echo "====================\n\n";
+
+if (!isset($argc)) {
+  echo "[error] An unexpected error occurred (\$argc is not set).\n";
+  exit();
+}
+
+$time = ($argc > 1 ? (int)$argv[1] : time());
+
+$logId = -1;
+registry::generateNow($time, $logId, false);
+
+echo "[info] Log ID: ".$logId."\n";
diff --git a/src/cron/pendingvalidationsreminder.php b/src/cron/pendingvalidationsreminder.php
new file mode 100644
index 0000000..aeec7cb
--- /dev/null
+++ b/src/cron/pendingvalidationsreminder.php
@@ -0,0 +1,15 @@
+<?php
+require_once(__DIR__."/../core.php");
+
+if (php_sapi_name() != "cli") {
+  security::notFound();
+  exit();
+}
+
+echo "==============================\n";
+echo "pendingvalidationsreminder.php\n";
+echo "==============================\n\n";
+
+validations::sendPendingValidationsReminder();
+
+//echo "[info] Log ID: ".$logId."\n";
diff --git a/src/css/calendar.css b/src/css/calendar.css
new file mode 100644
index 0000000..a6d979b
--- /dev/null
+++ b/src/css/calendar.css
@@ -0,0 +1,47 @@
+.calendar {
+  margin: 10px auto 40px auto;
+  border-collapse: collapse;
+}
+
+.calendar td {
+  padding: 8px;
+  min-width: 75px;
+}
+
+.calendar .day {
+  border: solid 1px black;
+}
+
+.calendar .day .date {
+  font-weight: bold;
+}
+
+select {
+  border-radius: 3px;
+}
+
+select[data-value="0"] {
+  background: #FF1744;
+  color: white;
+}
+
+select[data-value="1"] {
+  background: #00E5FF;
+  color: black;
+}
+
+select[data-value="2"] {
+  background: #00E676;
+  color: black;
+}
+
+select option {
+  background: white;
+  color: black;
+}
+
+.month {
+  font-weight: light;
+  font-size: 18px;
+  text-align: center;
+}
diff --git a/src/css/dashboard.css b/src/css/dashboard.css
new file mode 100644
index 0000000..7bec14f
--- /dev/null
+++ b/src/css/dashboard.css
@@ -0,0 +1,320 @@
+:root {
+  --material-green: rgb(76,175,80);
+}
+
+body {
+  font-family: "Roboto", "Helvetica", "Arial", sans-serif;
+}
+
+.main {
+  position: relative;
+  display: block;
+  width: Calc(100% - 64px);
+  max-width: 1024px;
+  margin: 20px auto;
+  padding: 16px 16px 32px 16px;
+  border-radius: 2px;
+  background: #FFFFFF;
+}
+
+.main.withfab {
+  padding: 16px 16px 64px 16px!important;
+}
+
+.delete, .icon {
+  color: black!important;
+  vertical-align: middle;
+}
+
+.icon-no-black {
+  vertical-align: middle;
+}
+
+a.backbtn {
+  text-decoration: none;
+  color: #424242;
+}
+
+a.backbtn .material-icons {
+  vertical-align: middle;
+}
+
+.mdl-tabs__panel {
+  padding-top: 16px;
+}
+
+.mdl-navigation__link span {
+  vertical-align: middle;
+}
+
+dynscript {
+  display: none;
+}
+
+details.debug summary {
+  color: rgb(255, 64, 129);
+  font-size: 12px;
+  cursor: pointer;
+}
+
+.overflow-wrapper {
+  max-width: 100%;
+  overflow-x: auto;
+  -webkit-overflow-scrolling: touch;
+}
+
+.overflow-wrapper.overflow-wrapper--for-table {
+  padding: 4px;
+  margin: -4px;
+}
+
+details.debug pre {
+  margin: 0;
+  max-width: 100%;
+  overflow: auto;
+}
+
+.margintop {
+  margin-top: 16px;
+}
+
+i.help {
+  vertical-align: middle;
+  cursor: help;
+}
+
+.mdl-data-table__cell--centered {
+  text-align: center!important;
+}
+
+.mdl-dialog .mdl-dialog__content, .copyto {
+  overflow-y: auto;
+  background: linear-gradient(white 30%, rgba(255, 255, 255, 0)), linear-gradient(rgba(255, 255, 255, 0), white 70%) 0 100%, radial-gradient(farthest-side at 50% 0, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)), radial-gradient(farthest-side at 50% 100%, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0)) 0 100%;
+  background-repeat: no-repeat;
+  background-color: white;
+  background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px;
+  background-attachment: local, local, scroll, scroll;
+}
+
+.mdl-dialog .mdl-dialog__content {
+  max-height: Calc(100vh - 232px);
+}
+
+.copyto {
+  max-height: 300px;
+}
+
+.mdl-dialog .mdl-dialog__title {
+  margin-bottom: 4px; /* Otherwise sometimes the title is not entirely shown */
+}
+
+a.mdl-js-ripple-effect {
+  position: relative;
+}
+
+.always-focused {
+  font-size: 12px;
+  top: 4px;
+  visibility: visible;
+}
+
+.clicky-container {
+  text-decoration: none!important;
+  color: black!important;
+}
+
+.clicky {
+  position: relative;
+  display: block;
+  width: Calc(100% - 10px);
+  margin-bottom: 5px;
+  overflow-x: hidden;
+  overflow-y: hidden;
+  padding: 5px;
+  border-left: 4px solid #212121;
+}
+
+.clicky:hover {
+  background-color: #EEE;
+}
+
+.clicky .icon {
+  float: left;
+  margin-right: 8px;
+}
+
+.clicky .material-icons {
+  font-size: 40px;
+}
+
+.clicky .title {
+  font-weight: bold;
+}
+
+.clicky .description {
+  color: rgba(0, 0, 0, .54);
+  font-weight: 400;
+}
+
+main .actions {
+  margin: 16px 0 16px 16px;
+  float: right;
+}
+
+.mdl-menu a {
+  text-decoration: none;
+}
+
+.oneline {
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+
+.icon-text {
+  font-size: 14px!important;
+  vertical-align: middle;
+}
+
+.pagination {
+  margin: 16px 0;
+}
+
+.pagination .page {
+  padding: 0 4px;
+}
+
+.mdl-button .material-icons {
+  vertical-align: middle;
+}
+
+[data-dyndialog-href] {
+  cursor: pointer;
+}
+
+.mdl-dialog__actions .mdl-button.mdl-button--icon {
+  height: 32px!important;
+  margin-top: 2px;
+}
+
+
+.mdl-layout__drawer>span.mdl-layout-title {
+    line-height: 20px!important;
+    margin-top: 22px;
+}
+
+body.dark-background header, body.dark-background header .material-icons {
+  color: white!important;
+}
+
+.mdl-layout__drawer .logo {
+  max-height: 32px;
+}
+
+option:disabled {
+  color: graytext!important;
+}
+
+.subtitle {
+  display: inline-block;
+  padding-top: 8px;
+  padding-left: 40px;
+  padding-right: 40px;
+}
+
+.help-btn--top-right {
+  float: right;
+  margin: 16px 0;
+}
+
+.help-btn--top-right.help-btn--top-right-margin {
+  margin: 16px;
+}
+
+.mdl-button .material-icons, .mdl-button span {
+  vertical-align: middle;
+}
+
+/* Multiselect custom element stylings */
+.mdl-custom-multiselect__item {
+  line-height: normal;
+  height: auto;
+  padding: 0;
+}
+
+.mdl-custom-multiselect__item .mdl-checkbox {
+  height: auto;
+  padding: 4px 16px 4px 40px;
+}
+
+.mdl-custom-multiselect__item .mdl-checkbox__focus-helper, .mdl-custom-multiselect__item .mdl-checkbox__box-outline {
+  top: Calc(3px + 4px);
+  left: 16px;
+}
+
+.mdl-custom-multiselect__item {
+  line-height: normal;
+  height: auto;
+  padding: 0;
+}
+
+.mdl-custom-multiselect__item, .mdl-custom-multiselect__item .mdl-checkbox {
+  cursor: pointer!important;
+}
+
+.mdl-custom-selectfield__select {
+  user-select: none;
+}
+
+/* mdlext-selectfield color fix */
+.mdlext-selectfield__label::after {
+  background-color: var(--material-green);
+}
+
+.mdlext-selectfield.is-focused .mdlext-selectfield__label {
+  color: var(--material-green)!important;
+}
+
+/* Date-picker polyfill fixes */
+dialog {
+  z-index: 10;
+}
+
+/* Limit change */
+.limit-change-container {
+  margin: 4px 0;
+  padding: 4px 0 4px 8px;
+  border-left: 2px solid #212121;
+}
+
+/* Left actions above table */
+.left-actions {
+  position: sticky;
+  z-index: 10;
+  background: white;
+  top: 0;
+  padding: 8px 0;
+  border-top: 1px solid rgba(0,0,0,.12);
+  border-bottom: 1px solid rgba(0,0,0,.12);
+}
+
+.highlighted {
+  font-weight: 500;
+}
+
+@media (max-width: 700px) {
+  .main {
+    width: Calc(100% - 40px);
+  }
+}
+
+@media screen and (max-width: 1024px) {
+  .mdl-layout__drawer>span.mdl-layout-title {
+    margin-top: 18px;
+  }
+
+  .subtitle {
+    padding-left: 16px;
+    padding-right: 16px;
+  }
+}
diff --git a/src/css/incidents.css b/src/css/incidents.css
new file mode 100644
index 0000000..2104748
--- /dev/null
+++ b/src/css/incidents.css
@@ -0,0 +1,75 @@
+.incidents-wrapper {
+  display: inline-block;
+}
+
+.incidents-wrapper--scrollable {
+  max-height: 400px;
+  overflow-y: auto;
+}
+
+.incidents {
+  border: solid 1px rgba(0,0,0,.12);
+  border-collapse: collapse;
+  white-space: nowrap;
+}
+
+.incidents tr.artificial-height {
+  height: 32px;
+}
+
+.incidents tr:not(:last-child) {
+  border-bottom: solid 1px rgba(0,0,0,.24);
+}
+
+.incidents td, .incidents th {
+  padding: 1px 8px;
+}
+
+.incidents .centered {
+  text-align: center;
+}
+
+.incidents .icon-cell {
+  user-select: none;
+}
+
+.incidents .has-checkbox {
+  padding-left: 14px;
+  padding-right: 0;
+}
+
+.incidents .has-checkbox .mdl-checkbox__ripple-container {
+  height: 34px!important;
+  width: 34px!important;
+  top: -5px!important;
+  left: -9px!important;
+}
+
+.incidents .icon-cell .material-icons {
+  cursor: pointer;
+}
+
+.incidents .material-icons {
+  vertical-align: middle;
+}
+
+.incidents .verification-actions {
+  display: inline-block;
+}
+
+.incidents .more {
+  cursor: pointer;
+}
+
+.incidents-wrapper .mdl-menu__container {
+  top: 0;
+  right: 0;
+}
+
+.notvisible {
+  display: none;
+}
+
+.incidents tr.line-through td.can-strike {
+  text-decoration: line-through;
+}
diff --git a/src/css/index.css b/src/css/index.css
new file mode 100644
index 0000000..e6b4c36
--- /dev/null
+++ b/src/css/index.css
@@ -0,0 +1,13 @@
+.login {
+  display: block;
+  max-width: 400px;
+  margin: 20px auto;
+  padding: 16px;
+  border-radius: 2px;
+  background: #FFFFFF;
+}
+
+#about {
+  max-width: Calc(100% - 64px);
+  max-width: 700px;
+}
diff --git a/src/css/schedule.css b/src/css/schedule.css
new file mode 100644
index 0000000..d655252
--- /dev/null
+++ b/src/css/schedule.css
@@ -0,0 +1,130 @@
+.schedule {
+  display: inline-flex;
+  flex: none;
+  border: 1px solid rgba(0,0,0,.12);
+
+  --hour-height: 30px;
+}
+
+.schedule .sidetime {
+  padding-top: 36px;
+  width: 40px;
+}
+
+.schedule .sidetime .hour {
+  position: relative;
+  height: var(--hour-height);
+}
+
+.schedule .sidetime .hour .hour--text {
+  position: absolute;
+  top: -9px;
+  right: 7px;
+  font-size: 11px;
+}
+
+.schedule .day {
+  flex: 1 0 auto;
+  width: 100px;
+  min-width: 100px;
+}
+
+.schedule .day .day--header {
+  text-align: center;
+  box-sizing: content-box;
+  height: 20px;
+  font-size: 18px;
+  font-weight: 300;
+  padding: 8px 0;
+  border-bottom: solid 1px #ccc;
+}
+
+.schedule .day .day--content {
+  position: relative;
+}
+
+.schedule .day .day--content .hour {
+  height: Calc(var(--hour-height) - 1px);
+}
+
+.schedule .day .day--content .hour:not(:last-child) {
+  border-bottom: solid 1px #ddd;
+}
+
+.schedule .day .day--content .hour:last-child, .schedule .sidetime .hour:last-child {
+  height: 13px;
+}
+
+.schedule .day .day--content .work-event, .schedule .day .day--content .inline-event {
+  position: absolute;
+  border-radius: 3px;
+  overflow-y: auto;
+}
+
+.schedule .day .day--content .event--actions {
+  position: absolute;
+  top: 4px;
+  right: 4px;
+  line-height: 14px;
+  background: #00000077;
+  border-radius: 5px;
+  z-index: 2;
+}
+
+.schedule .day .day--content .event--actions .material-icons {
+  color: white;
+  font-size: 20px;
+}
+
+.schedule .day .day--content .event--header, .schedule .day .day--content .event--body {
+  text-align: left;
+  font-size: 11px;
+}
+
+.schedule .day .day--content .work-event {
+  left: 4px;
+  width: 92px;
+  background: #2E7D32;
+}
+
+.schedule .day .day--content .work-event .event--header, .schedule .day .day--content .work-event .event--body {
+  line-height: 14px;
+  color: white;
+}
+
+.schedule .day .day--content .work-event .event--header {
+  margin: 5px 5px 0 5px;
+  font-weight: bold;
+}
+
+.schedule .day .day--content .work-event .event--body {
+  margin: 0 5px;
+}
+
+.schedule .day .day--content .inline-event {
+  left: 3px;
+  width: 86px;
+  background: #81C784;
+}
+
+.schedule .day .day--content .inline-event .event--header {
+  font-weight: 500;
+}
+
+.schedule .day .day--content .inline-event .event--header, .schedule .day .day--content .inline-event .event--body {
+  line-height: 14px;
+  margin: 0 5px;
+  color: black;
+}
+
+@media (hover: hover) {
+  .schedule .day .day--content .event--actions {
+    opacity: 0;
+    transition: opacity 0.15s ease-in;
+    background: #00000099;
+  }
+
+  .schedule .day .day--content .work-event:hover .event--actions {
+    opacity: 1;
+  }
+}
diff --git a/src/csvimport.php b/src/csvimport.php
new file mode 100644
index 0000000..a34d190
--- /dev/null
+++ b/src/csvimport.php
@@ -0,0 +1,64 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!isset($_FILES["file"]) || $_FILES["file"]["error"] == UPLOAD_ERR_NO_FILE) {
+  security::go("users.php?msg=empty");
+}
+
+if ($_FILES["file"]["error"] !== UPLOAD_ERR_OK) {
+  security::go("users.php?msg=unexpected");
+}
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Resultado de la importación de usuarios</h2>
+
+          <?php
+          $workers = csv::csv2array($_FILES["file"]["tmp_name"]);
+
+          if ($workers === false) {
+            echo "<p class='mdl-color-text--red'>El formato del documento no es correcto (la cabecera debería ser: <code>".security::htmlsafe(implode(";", csv::$fields))."</code>).</p>";
+          } else {
+            foreach ($workers as $worker) {
+              $username = explode("@", $worker["email"])[0];
+              $passwordHash = password_hash($worker["dni"], PASSWORD_DEFAULT);
+
+              $status = people::add($username, $worker["name"], $worker["dni"], $worker["email"], $worker["category"], $passwordHash, security::WORKER, 0);
+
+              echo "<p class='mdl-color-text--".($status ? "green" : "red")."'>&ldquo;".security::htmlsafe($worker["name"])."&rdquo; ".($status ? "se ha importado correctamente" : " no se ha podido importar correctamente").".</p>";
+
+              if ($status) {
+                $personid = mysqli_insert_id($con);
+
+                $companies = explode(",", $worker["companies"]);
+
+                foreach ($companies as $company) {
+                  $status2 = people::addToCompany($personid, $company);
+
+                  echo "<p class='mdl-color-text--".($status2 ? "green" : "red")."'>".($status2 ? "Se ha" : "No se ha")." podido añadir &ldquo;".security::htmlsafe($worker["name"])."&rdquo; a la empresa con número de identificación ".(int)$company.".</p>";
+                }
+              }
+            }
+          }
+          ?>
+
+          <a href="users.php" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent">Ir al listado de personas</a>
+        </div>
+      </div>
+    </main>
+  </div>
+</body>
+</html>
diff --git a/src/doactiveschedule.php b/src/doactiveschedule.php
new file mode 100644
index 0000000..326dff8
--- /dev/null
+++ b/src/doactiveschedule.php
@@ -0,0 +1,19 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_NEMPTY],
+  ["value", security::PARAM_ISINT]
+])) {
+  security::go("users.php?msg=unexpected");
+}
+
+$id = (int)$_POST["id"];
+$value = (int)$_POST["value"];
+
+if (schedules::switchActive($id, $value)) {
+  security::go("schedule.php?id=".(int)$id."&msg=activeswitched".$value);
+} else {
+  security::go("schedule.php?id=".(int)$id."&msg=unexpected");
+}
diff --git a/src/doaddcalendar.php b/src/doaddcalendar.php
new file mode 100644
index 0000000..085ebab
--- /dev/null
+++ b/src/doaddcalendar.php
@@ -0,0 +1,37 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_NEMPTY],
+  ["type", security::PARAM_ISSET],
+  ["begins", security::PARAM_ISDATE],
+  ["ends", security::PARAM_ISDATE]
+])) {
+  security::go("calendars.php?msg=unexpected");
+}
+
+$id = (int)$_POST["id"];
+
+if (!categories::exists($id)) {
+  security::go("calendars.php?msg=unexpected");
+}
+
+$calendar_response = calendars::parseFormCalendar($_POST["type"], $_POST["begins"], $_POST["ends"]);
+
+if ($calendar_response === false) {
+  security::go("calendars.php?msg=unexpected");
+}
+
+$return = calendars::add($id, $calendar_response["begins"], $calendar_response["ends"], $calendar_response["calendar"]);
+
+switch ($return) {
+  case 0:
+  security::go("calendars.php?msg=added");
+
+  case -1:
+  security::go("calendars.php?msg=overlap");
+
+  default:
+  security::go("calendars.php?msg=unexpected");
+}
diff --git a/src/doaddcategory.php b/src/doaddcategory.php
new file mode 100644
index 0000000..2f706d3
--- /dev/null
+++ b/src/doaddcategory.php
@@ -0,0 +1,21 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["name", security::PARAM_NEMPTY],
+  ["emails", security::PARAM_ISSET],
+  ["parent", security::PARAM_ISSET]
+])) {
+  security::go("categories.php?msg=empty");
+}
+
+$name = $_POST["name"];
+$emails = $_POST["emails"];
+$parent = (int)$_POST["parent"];
+
+if (categories::add($name, $emails, $parent)) {
+  security::go("categories.php?msg=added");
+} else {
+  security::go("categories.php?msg=unexpected");
+}
diff --git a/src/doaddcompany.php b/src/doaddcompany.php
new file mode 100644
index 0000000..9765898
--- /dev/null
+++ b/src/doaddcompany.php
@@ -0,0 +1,19 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["name", security::PARAM_NEMPTY],
+  ["cif", security::PARAM_ISSET]
+])) {
+  security::go("companies.php?msg=empty");
+}
+
+$name = $_POST["name"];
+$cif = $_POST["cif"];
+
+if (companies::add($name, $cif)) {
+  security::go("companies.php?msg=added");
+} else {
+  security::go("companies.php?msg=unexpected");
+}
diff --git a/src/doadddayschedule.php b/src/doadddayschedule.php
new file mode 100644
index 0000000..aeae3ae
--- /dev/null
+++ b/src/doadddayschedule.php
@@ -0,0 +1,64 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_NEMPTY]
+])) {
+  security::go("users.php?msg=unexpected");
+}
+
+$id = (int)$_POST["id"];
+
+if (!security::checkParams("POST", [
+  ["day", security::PARAM_ISARRAY],
+  ["type", security::PARAM_ISARRAY]
+])) {
+  security::go("schedule.php?id=".$id."&msg=empty");
+}
+
+$dates = ["beginswork", "endswork", "beginsbreakfast", "endsbreakfast", "beginslunch", "endslunch"];
+$time = [];
+foreach ($dates as $date) {
+  if (isset($_POST[$date]) && !empty($_POST[$date])) {
+    if (!security::checkParam($_POST[$date], security::PARAM_ISTIME)) {
+      security::go("schedule.php?id=".$id."&msg=unexpected");
+    }
+    $time[$date] = schedules::time2sec($_POST[$date]);
+  } else {
+    $time[$date] = 0;
+  }
+}
+
+$status = schedules::checkAddDayGeneric($time["beginswork"], $time["endswork"], $time["beginsbreakfast"], $time["endsbreakfast"], $time["beginslunch"], $time["endslunch"]);
+
+if ($status != 0) {
+  security::go("schedule.php?id=".$id."&msg=errorcheck".(int)$status);
+}
+
+$flag = false;
+
+foreach ($_POST["day"] as $rawday) {
+  $day = (int)$rawday;
+  if ($day < 0 || $day > 6) continue;
+
+  foreach ($_POST["type"] as $rawtype) {
+    $type = (int)$rawtype;
+    if (!in_array($type, array_keys(calendars::$types))) continue;
+
+    if (!schedules::checkAddDay2ScheduleParticular($id, $day, $type)) {
+      $flag = true;
+      continue;
+    }
+
+    if (!schedules::addDay2Schedule($id, $day, $type, $time["beginswork"], $time["endswork"], $time["beginsbreakfast"], $time["endsbreakfast"], $time["beginslunch"], $time["endslunch"])) {
+      security::go("schedule.php?id=".$id."&msg=unexpected");
+    }
+  }
+}
+
+if ($flag) {
+  security::go("schedule.php?id=".$id."&msg=existing");
+} else {
+  security::go("schedule.php?id=".$id."&msg=added");
+}
diff --git a/src/doadddayscheduletemplate.php b/src/doadddayscheduletemplate.php
new file mode 100644
index 0000000..c0c8097
--- /dev/null
+++ b/src/doadddayscheduletemplate.php
@@ -0,0 +1,64 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_NEMPTY]
+])) {
+  security::go("scheduletemplates.php?msg=unexpected");
+}
+
+$id = (int)$_POST["id"];
+
+if (!security::checkParams("POST", [
+  ["day", security::PARAM_ISARRAY],
+  ["type", security::PARAM_ISARRAY]
+])) {
+  security::go("scheduletemplate.php?id=".$id."&msg=empty");
+}
+
+$dates = ["beginswork", "endswork", "beginsbreakfast", "endsbreakfast", "beginslunch", "endslunch"];
+$time = [];
+foreach ($dates as $date) {
+  if (isset($_POST[$date]) && !empty($_POST[$date])) {
+    if (!security::checkParam($_POST[$date], security::PARAM_ISTIME)) {
+      security::go("scheduletemplate.php?id=".$id."&msg=unexpected");
+    }
+    $time[$date] = schedules::time2sec($_POST[$date]);
+  } else {
+    $time[$date] = 0;
+  }
+}
+
+$status = schedules::checkAddDayGeneric($time["beginswork"], $time["endswork"], $time["beginsbreakfast"], $time["endsbreakfast"], $time["beginslunch"], $time["endslunch"]);
+
+if ($status != 0) {
+  security::go("scheduletemplate.php?id=".$id."&msg=errorcheck".(int)$status);
+}
+
+$flag = false;
+
+foreach ($_POST["day"] as $rawday) {
+  $day = (int)$rawday;
+  if ($day < 0 || $day > 6) continue;
+
+  foreach ($_POST["type"] as $rawtype) {
+    $type = (int)$rawtype;
+    if (!in_array($type, array_keys(calendars::$types))) continue;
+
+    if (!schedules::checkAddDay2TemplateParticular($id, $day, $type)) {
+      $flag = true;
+      continue;
+    }
+
+    if (!schedules::addDay2Template($id, $day, $type, $time["beginswork"], $time["endswork"], $time["beginsbreakfast"], $time["endsbreakfast"], $time["beginslunch"], $time["endslunch"])) {
+      security::go("scheduletemplate.php?id=".$id."&msg=unexpected");
+    }
+  }
+}
+
+if ($flag) {
+  security::go("scheduletemplate.php?id=".$id."&msg=existing");
+} else {
+  security::go("scheduletemplate.php?id=".$id."&msg=added");
+}
diff --git a/src/doaddincident.php b/src/doaddincident.php
new file mode 100644
index 0000000..645a9f9
--- /dev/null
+++ b/src/doaddincident.php
@@ -0,0 +1,65 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER);
+
+$url = (security::isAllowed(security::ADMIN) ? "incidents.php" : "userincidents.php?id=".(int)$_SESSION["id"]);
+
+if (!security::checkParams("POST", [
+  ["type", security::PARAM_ISINT],
+  ["worker", security::PARAM_ISINT],
+  ["day", security::PARAM_ISDATE]
+])) {
+  security::go(visual::getContinueUrl($url, "empty", "POST"));
+}
+
+$type = (int)$_POST["type"];
+$worker = (int)$_POST["worker"];
+$day = $_POST["day"];
+$details = ((isset($_POST["details"]) && is_string($_POST["details"])) ? $_POST["details"] : "");
+
+if (isset($_POST["allday"]) && $_POST["allday"] == 1) {
+  $begins = incidents::STARTOFDAY;
+  $ends = incidents::ENDOFDAY;
+} else {
+  if (!security::checkParams("POST", [
+    ["begins", security::PARAM_ISTIME],
+    ["ends", security::PARAM_ISTIME]
+  ])) {
+    security::go(visual::getContinueUrl($url, "empty", "POST"));
+  }
+
+  $begins = schedules::time2sec($_POST["begins"]);
+  $ends = schedules::time2sec($_POST["ends"]);
+}
+
+$verified = ((isset($_POST["autoverify"]) && $_POST["autoverify"] == 1 && security::isAllowed(security::ADMIN)) ? 1 : 0);
+$isAdminView = security::isAdminView();
+
+$incidentPerson = people::workerData("id", $worker);
+
+$status = incidents::add($worker, $type, $details, $day, $begins, $ends, "ME", $verified, false, !$isAdminView, true, ($incidentPerson == people::userData("id")));
+
+switch ($status) {
+  case 0:
+  security::go(visual::getContinueUrl($url, "added", "POST"));
+  break;
+
+  case 2:
+  security::go(visual::getContinueUrl($url, "overlap", "POST"));
+  break;
+
+  case 3:
+  security::go(visual::getContinueUrl($url, "order", "POST"));
+  break;
+
+  case 4:
+  security::go(visual::getContinueUrl($url, "addedemailnotsent", "POST"));
+  break;
+
+  case 5:
+  security::go(visual::getContinueUrl($url, "addednotautovalidated", "POST"));
+  break;
+
+  default:
+  security::go(visual::getContinueUrl($url, "unexpected", "POST"));
+}
diff --git a/src/doaddincidentattachment.php b/src/doaddincidentattachment.php
new file mode 100644
index 0000000..9e70a83
--- /dev/null
+++ b/src/doaddincidentattachment.php
@@ -0,0 +1,46 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER, security::METHOD_NOTFOUND);
+security::checkWorkerUIEnabled();
+
+$isAdmin = security::isAllowed(security::ADMIN);
+$url = ($isAdmin ? "incidents.php" : "userincidents.php?id=".$_SESSION["id"]);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_ISINT]
+])) {
+  security::go(visual::getContinueUrl($url, "unexpected", "POST"));
+}
+
+if (!isset($_FILES["file"]) || $_FILES["file"]["error"] == UPLOAD_ERR_NO_FILE) {
+  security::go(visual::getContinueUrl($url, "empty", "POST"));
+}
+
+$id = (int)$_POST["id"];
+
+$incident = incidents::get($id, true);
+if ($incident === false) security::go(visual::getContinueUrl($url, "unexpected", "POST"));
+
+$status = incidents::getStatus($incident);
+
+if (in_array($status, incidents::$cannotEditCommentsStates)) security::notFound();
+if (!$isAdmin) incidents::checkIncidentIsFromPerson($incident["id"]);
+
+$status = incidents::addAttachment($id, $_FILES["file"]);
+
+switch ($status) {
+  case 0:
+  security::go(visual::getContinueUrl($url, "attachmentadded", "POST"));
+  break;
+
+  case 2:
+  security::go(visual::getContinueUrl($url, "filesize", "POST"));
+  break;
+
+  case 3:
+  security::go(visual::getContinueUrl($url, "filetype", "POST"));
+  break;
+
+  default:
+  security::go(visual::getContinueUrl($url, "unexpected", "POST"));
+}
diff --git a/src/doaddincidentbulk.php b/src/doaddincidentbulk.php
new file mode 100644
index 0000000..349e979
--- /dev/null
+++ b/src/doaddincidentbulk.php
@@ -0,0 +1,83 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["type", security::PARAM_ISINT],
+  ["workers", security::PARAM_ISARRAY],
+  ["day", security::PARAM_ISDATE]
+])) {
+  security::go("incidents.php?msg=empty");
+}
+
+$type = (int)$_POST["type"];
+$day = $_POST["day"];
+$details = ((isset($_POST["details"]) && is_string($_POST["details"])) ? $_POST["details"] : "");
+
+if (isset($_POST["allday"]) && $_POST["allday"] == 1) {
+  $begins = 0;
+  $ends = incidents::ENDOFDAY;
+} else {
+  if (!security::checkParams("POST", [
+    ["begins", security::PARAM_ISTIME],
+    ["ends", security::PARAM_ISTIME]
+  ])) {
+    security::go("incidents.php?msg=empty");
+  }
+
+  $begins = schedules::time2sec($_POST["begins"]);
+  $ends = schedules::time2sec($_POST["ends"]);
+}
+
+$mdHeaderRowBefore = visual::backBtn("workers.php");
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Resultado de la creación de incidencias</h2>
+
+          <?php
+          foreach ($_POST["workers"] as $workerid) {
+            $worker = workers::get($workerid);
+            if ($worker === false) continue;
+
+            $status = incidents::add($worker["id"], $type, $details, $day, $begins, $ends);
+
+            $person = "&ldquo;".security::htmlsafe($worker["name"])."&rdquo; (".security::htmlsafe($worker["companyname"]).")";
+
+            switch ($status) {
+              case 0:
+              echo "<p class='mdl-color-text--green'>Incidencia añadida correctamente a $person.";
+              break;
+
+              case 2:
+              echo "<p class='mdl-color-text--orange'>La incidencia que se intentaba añadir se solapa con otra incidencia de $person, así que no se ha añadido a este trabajador.";
+              break;
+
+              case 3:
+              echo "<p class='mdl-color-text--red'>No se ha podido añadir la incidencia a $person porque la fecha de inicio debe ser anterior a la de fin.";
+              break;
+
+              default:
+              echo "<p class='mdl-color-text--red'>Ha ocurrido un error inesperado copiando la plantilla a $person.";
+            }
+            echo "</p>";
+          }
+          ?>
+        </div>
+      </div>
+    </main>
+  </div>
+</body>
+</html>
diff --git a/src/doaddincidenttype.php b/src/doaddincidenttype.php
new file mode 100644
index 0000000..fdef005
--- /dev/null
+++ b/src/doaddincidenttype.php
@@ -0,0 +1,23 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["name", security::PARAM_NEMPTY]
+])) {
+  security::go("incidenttypes.php?msg=empty");
+}
+
+$name = $_POST["name"];
+$present = ((isset($_POST["present"]) && $_POST["present"] == 1) ? 1 : 0);
+$paid = ((isset($_POST["paid"]) && $_POST["paid"] == 1) ? 1 : 0);
+$workerfill = ((isset($_POST["workerfill"]) && $_POST["workerfill"] == 1) ? 1 : 0);
+$notifies = ((isset($_POST["notifies"]) && $_POST["notifies"] == 1) ? 1 : 0);
+$autovalidates = ((isset($_POST["autovalidates"]) && $_POST["autovalidates"] == 1) ? 1 : 0);
+$hidden = ((isset($_POST["hidden"]) && $_POST["hidden"] == 1) ? 1 : 0);
+
+if (incidents::addType($name, $present, $paid, $workerfill, $notifies, $autovalidates, $hidden)) {
+  security::go("incidenttypes.php?msg=added");
+} else {
+  security::go("incidenttypes.php?msg=unexpected");
+}
diff --git a/src/doaddrecurringincident.php b/src/doaddrecurringincident.php
new file mode 100644
index 0000000..deded14
--- /dev/null
+++ b/src/doaddrecurringincident.php
@@ -0,0 +1,40 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["type", security::PARAM_ISINT],
+  ["worker", security::PARAM_ISINT],
+  ["firstday", security::PARAM_ISDATE],
+  ["lastday", security::PARAM_ISDATE],
+  ["day", security::PARAM_ISARRAY],
+  ["daytype", security::PARAM_ISARRAY]
+])) {
+  security::go(visual::getContinueUrl("incidents.php", "empty", "POST"));
+}
+
+$type = (int)$_POST["type"];
+$worker = (int)$_POST["worker"];
+$details = ((isset($_POST["details"]) && is_string($_POST["details"])) ? $_POST["details"] : "");
+$firstday = $_POST["firstday"];
+$lastday = $_POST["lastday"];
+$days = $_POST["day"];
+$typeDays = $_POST["daytype"];
+
+if (isset($_POST["allday"]) && $_POST["allday"] == 1) {
+  $begins = incidents::STARTOFDAY;
+  $ends = incidents::ENDOFDAY;
+} else {
+  if (!security::checkParams("POST", [
+    ["begins", security::PARAM_ISTIME],
+    ["ends", security::PARAM_ISTIME]
+  ])) {
+    security::go(visual::getContinueUrl("incidents.php", "empty", "POST"));
+  }
+
+  $begins = schedules::time2sec($_POST["begins"]);
+  $ends = schedules::time2sec($_POST["ends"]);
+}
+
+if (recurringIncidents::add($worker, $type, $details, $firstday, $lastday, $begins, $ends, "ME", $typeDays, $days)) security::go(visual::getContinueUrl("incidents.php", "addedrecurring", "POST"));
+else security::go(visual::getContinueUrl("incidents.php", "unexpectedrecurring", "POST"));
diff --git a/src/doaddschedule.php b/src/doaddschedule.php
new file mode 100644
index 0000000..d91254f
--- /dev/null
+++ b/src/doaddschedule.php
@@ -0,0 +1,46 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["worker", security::PARAM_NEMPTY]
+])) {
+  security::go("users.php?msg=unexpected");
+}
+
+$w = workers::get((int)$_POST["worker"]);
+
+if ($w === false) {
+  security::go("users.php?msg=unexpected");
+}
+
+if (!security::checkParams("POST", [
+  ["begins", security::PARAM_ISDATE],
+  ["ends", security::PARAM_ISDATE]
+])) {
+  security::go("userschedule.php?id=".(int)$w["person"]."&msg=empty");
+}
+
+$begins = $_POST["begins"];
+$ends = $_POST["ends"];
+
+$status = schedules::add($w["id"], $begins, $ends);
+
+switch ($status) {
+  case 0:
+  $id = mysqli_insert_id($con);
+  security::go("schedule.php?id=".(int)$id."&msg=added");
+  break;
+
+  case 1:
+  security::go("userschedule.php?id=".(int)$w["person"]."&msg=overlaps");
+  break;
+
+  case 3:
+  security::go("userschedule.php?id=".(int)$w["person"]."&msg=order");
+  break;
+
+  default:
+  security::go("userschedule.php?id=".(int)$w["person"]."&msg=unexpected");
+  break;
+}
diff --git a/src/doaddscheduletemplate.php b/src/doaddscheduletemplate.php
new file mode 100644
index 0000000..ab6df65
--- /dev/null
+++ b/src/doaddscheduletemplate.php
@@ -0,0 +1,30 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["name", security::PARAM_NEMPTY],
+  ["begins", security::PARAM_ISDATE],
+  ["ends", security::PARAM_ISDATE]
+])) {
+  security::go("scheduletemplates.php?msg=empty");
+}
+
+$name = $_POST["name"];
+$begins = $_POST["begins"];
+$ends = $_POST["ends"];
+
+$status = schedules::addTemplate($name, $begins, $ends);
+switch ($status) {
+  case 0:
+  $id = mysqli_insert_id($con);
+  security::go("scheduletemplate.php?id=".(int)$id."&msg=added");
+  break;
+
+  case 2:
+  security::go("scheduletemplates.php?msg=order");
+  break;
+
+  default:
+  security::go("scheduletemplates.php?msg=unexpected");
+}
diff --git a/src/doadduser.php b/src/doadduser.php
new file mode 100644
index 0000000..af0951c
--- /dev/null
+++ b/src/doadduser.php
@@ -0,0 +1,33 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["username", security::PARAM_NEMPTY],
+  ["name", security::PARAM_NEMPTY],
+  ["dni", security::PARAM_ISSET],
+  ["email", security::PARAM_ISEMAILOREMPTY],
+  ["category", security::PARAM_NEMPTY],
+  ["password", security::PARAM_NEMPTY],
+  ["type", security::PARAM_ISSET]
+])) {
+  security::go("users.php?msg=empty");
+}
+
+if (!security::passwordIsGoodEnough($_POST["password"])) security::go("users.php?msg=weakpassword");
+
+$username = $_POST["username"];
+$name = $_POST["name"];
+$dni = $_POST["dni"];
+$email = $_POST["email"];
+$category = (int)$_POST["category"];
+$password_hash = password_hash($_POST["password"], PASSWORD_DEFAULT);
+$type = (int)$_POST["type"];
+
+if (!security::isAllowed($type)) security::go("users.php?msg=unexpected");
+
+if (people::add($username, $name, $dni, $email, $category, $password_hash, $type)) {
+  security::go("users.php?msg=added");
+} else {
+  security::go("users.php?msg=unexpected");
+}
diff --git a/src/doaddworkhistoryitem.php b/src/doaddworkhistoryitem.php
new file mode 100644
index 0000000..64df048
--- /dev/null
+++ b/src/doaddworkhistoryitem.php
@@ -0,0 +1,19 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_NEMPTY],
+  ["day", security::PARAM_ISSET],
+  ["status", security::PARAM_ISSET]
+])) {
+  security::go("workers.php?msg=empty");
+}
+
+$id = $_POST["id"];
+$date = new DateTime($_POST["day"]);
+$day = $date->getTimestamp();
+$status = $_POST["status"];
+
+if (workers::addWorkHistoryItem($id, $day, $status)) security::go("workers.php?openWorkerHistory=".(int)$id);
+else security::go("workers.php?msg=unexpected");
diff --git a/src/dobackupdb.php b/src/dobackupdb.php
new file mode 100644
index 0000000..1953cd3
--- /dev/null
+++ b/src/dobackupdb.php
@@ -0,0 +1,15 @@
+<?php
+require_once("core.php");
+security::checkType(security::HYPERADMIN);
+
+if (!security::checkParams("POST", [
+  ["format", security::PARAM_ISINT]
+])) security::notFound();
+
+switch ($_POST["format"]) {
+  default:
+  header("Content-Type: application/sql");
+  header("Content-Disposition: filename=\"registrohorario_backup_".(int)date("Ymd").".sql\"");
+  header("Cache-control: private");
+  passthru("mysqldump ".escapeshellarg($conf["db"]["database"])." --host=".escapeshellarg($conf["db"]["host"])." --user=".escapeshellarg($conf["db"]["user"])." --password=".escapeshellarg($conf["db"]["password"]));
+}
diff --git a/src/dochangepassword.php b/src/dochangepassword.php
new file mode 100644
index 0000000..179b0e8
--- /dev/null
+++ b/src/dochangepassword.php
@@ -0,0 +1,20 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER);
+security::checkWorkerUIEnabled();
+
+if (!security::checkParams("POST", [
+  ["oldpassword", security::PARAM_NEMPTY],
+  ["newpassword", security::PARAM_NEMPTY]
+])) {
+  security::go("users.php?msg=empty");
+}
+
+$oldpassword = $_POST["oldpassword"];
+$newpassword = $_POST["newpassword"];
+
+if (people::workerViewChangePassword($oldpassword, $newpassword)) {
+  header("Location: workerhome.php?msg=passwordchanged");
+} else {
+  header("Location: changepassword.php?msg=wrong");
+}
diff --git a/src/docopytemplate.php b/src/docopytemplate.php
new file mode 100644
index 0000000..9ff412a
--- /dev/null
+++ b/src/docopytemplate.php
@@ -0,0 +1,69 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["template", security::PARAM_ISINT],
+  ["workers", security::PARAM_ISARRAY]
+])) {
+  security::go("workers.php?msg=unexpected");
+}
+
+$template = (int)$_POST["template"];
+$active = ((isset($_POST["active"]) && $_POST["active"] == 1) ? 1 : 0);
+
+$mdHeaderRowBefore = visual::backBtn("workers.php");
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Resultado de la copia de plantilla</h2>
+
+          <?php
+          foreach ($_POST["workers"] as $workerid) {
+            $worker = workers::get($workerid);
+            if ($worker === false) continue;
+
+            $status = schedules::copyTemplate($template, $worker["id"], $active);
+            $person = "&ldquo;".security::htmlsafe($worker["name"])."&rdquo; (".security::htmlsafe($worker["companyname"]).")";
+
+            switch ($status) {
+              case 0:
+              echo "<p class='mdl-color-text--green'>Plantilla copiada correctamente a $person.";
+              break;
+
+              case 2:
+              echo "<p class='mdl-color-text--orange'>El horario de la plantilla se solapa con uno de los horarios de $person, así que no se ha copiado al trabajador.";
+              break;
+
+              case 1:
+              echo "<p class='mdl-color-text--red'>No se ha podido copiar la plantilla a $person porque la plantilla no existe.";
+              break;
+
+              case -1:
+              echo "<p class='mdl-color-text--red'>Se ha empezado a copiar la plantilla a $person pero no se han podido copiar todos los horarios de cada día correctamente por algún error desconocido.";
+              break;
+
+              default:
+              echo "<p class='mdl-color-text--red'>Ha ocurrido un error inesperado copiando la plantilla a $person.";
+            }
+            echo "</p>";
+          }
+          ?>
+        </div>
+      </div>
+    </main>
+  </div>
+</body>
+</html>
diff --git a/src/dodeleteattachment.php b/src/dodeleteattachment.php
new file mode 100644
index 0000000..0f79981
--- /dev/null
+++ b/src/dodeleteattachment.php
@@ -0,0 +1,24 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER, security::METHOD_NOTFOUND);
+security::checkWorkerUIEnabled();
+
+$url = (security::isAllowed(security::ADMIN) ? "incidents.php" : "userincidents.php?id=".(int)$_SESSION["id"]);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_ISINT],
+  ["name", security::PARAM_NEMPTY]
+])) {
+  security::go(visual::getContinueUrl($url, "unexpected", "POST"));
+
+}
+
+$id = (int)$_POST["id"];
+$name = $_POST["name"];
+
+$incident = incidents::get($id, true);
+if ($incident === false) security::go(visual::getContinueUrl($url, "unexpected", "POST"));
+
+if (!security::isAllowed(security::ADMIN)) incidents::checkIncidentIsFromPerson($incident["id"]);
+
+security::go(visual::getContinueUrl($url, (incidents::deleteAttachment($id, $name) ? "attachmentdeleted" : "unexpected"), "POST"));
diff --git a/src/dodeletecalendar.php b/src/dodeletecalendar.php
new file mode 100644
index 0000000..ad0c673
--- /dev/null
+++ b/src/dodeletecalendar.php
@@ -0,0 +1,21 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_NEMPTY]
+])) {
+  security::go("calendars.php?msg=unexpected");
+}
+
+$id = (int)$_POST["id"];
+
+if (!calendars::exists($id)) {
+  security::go("calendars.php?msg=unexpected");
+}
+
+if (calendars::remove($id)) {
+  security::go("calendars.php?msg=deleted");
+} else {
+  security::go("calendars.php?msg=unexpected");
+}
diff --git a/src/dodeleteday.php b/src/dodeleteday.php
new file mode 100644
index 0000000..4328a42
--- /dev/null
+++ b/src/dodeleteday.php
@@ -0,0 +1,23 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_NEMPTY]
+])) {
+  security::go("users.php?msg=unexpected");
+}
+
+$id = (int)$_POST["id"];
+
+$day = schedules::getDay($id);
+
+if ($day === false) {
+  security::go("users.php?msg=unexpected");
+}
+
+if (schedules::removeDay($id)) {
+  security::go("schedule.php?id=".(int)$day["schedule"]."&msg=deleted");
+} else {
+  security::go("schedule.php?id=".(int)$day["schedule"]."&msg=unexpected");
+}
diff --git a/src/dodeleteincident.php b/src/dodeleteincident.php
new file mode 100644
index 0000000..d9750f4
--- /dev/null
+++ b/src/dodeleteincident.php
@@ -0,0 +1,29 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER, security::METHOD_NOTFOUND);
+security::checkWorkerUIEnabled();
+
+$isAdmin = security::isAdminView();
+$url = ($isAdmin ? "incidents.php?" : "userincidents.php?id=".$_SESSION["id"]."&");
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_ISINT]
+])) {
+  security::go(visual::getContinueUrl($url, "unexpected", "POST"));
+}
+
+$id = (int)$_POST["id"];
+
+$incident = incidents::get($id, true);
+if ($incident === false) security::go(visual::getContinueUrl($url, "unexpected", "POST"));
+
+$istatus = incidents::getStatus($incident);
+
+if (($isAdmin && !in_array($istatus, incidents::$canRemoveStates)) || (!$isAdmin && !in_array($istatus, incidents::$workerCanRemoveStates))) security::notFound();
+if (!$isAdmin) incidents::checkIncidentIsFromPerson($incident["id"]);
+
+if (incidents::remove($id)) {
+  security::go(visual::getContinueUrl($url, "removed", "POST"));
+} else {
+  security::go(visual::getContinueUrl($url, "unexpected", "POST"));
+}
diff --git a/src/dodeleteincidentsbulk.php b/src/dodeleteincidentsbulk.php
new file mode 100644
index 0000000..07855cb
--- /dev/null
+++ b/src/dodeleteincidentsbulk.php
@@ -0,0 +1,25 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["incidents", security::PARAM_ISARRAY]
+])) {
+  security::go("incidents.php?msg=empty");
+}
+
+$allOk = true;
+foreach ($_POST["incidents"] as $id) {
+  $incident = incidents::get($id, true);
+  if ($incident === false) security::go($returnURL."msg=unexpected");
+
+  $istatus = incidents::getStatus($incident);
+
+  if (in_array($istatus, incidents::$canRemoveStates)) {
+    if (!incidents::remove($id)) $allOk = false;
+  } elseif (in_array($istatus, incidents::$canInvalidateStates)) {
+    if (!incidents::invalidate($id)) $allOk = false;
+  } else $allOk = false;
+}
+
+security::go("incidents.php?msg=deleteincidentsbulk".($allOk ? "success" : "partialsuccess"));
diff --git a/src/dodeleteschedule.php b/src/dodeleteschedule.php
new file mode 100644
index 0000000..358a8d7
--- /dev/null
+++ b/src/dodeleteschedule.php
@@ -0,0 +1,29 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_NEMPTY]
+])) {
+  security::go("users.php?msg=unexpected");
+}
+
+$id = (int)$_POST["id"];
+
+$s = schedules::get($id);
+
+if ($s === false) {
+  security::go("users.php?msg=unexpected");
+}
+
+$w = workers::get($s["worker"]);
+
+if ($w === false) {
+  security::go("users.php?msg=unexpected");
+}
+
+if (schedules::remove($id)) {
+  security::go("userschedule.php?id=".(int)$w["person"]."&msg=deleted");
+} else {
+  security::go("userschedule.php?id=".(int)$w["person"]."&msg=unexpected");
+}
diff --git a/src/dodeletescheduletemplate.php b/src/dodeletescheduletemplate.php
new file mode 100644
index 0000000..f8104e7
--- /dev/null
+++ b/src/dodeletescheduletemplate.php
@@ -0,0 +1,21 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_NEMPTY]
+])) {
+  security::go("scheduletemplates.php?msg=unexpected");
+}
+
+$id = (int)$_POST["id"];
+
+if (!schedules::templateExists($id)) {
+  security::go("scheduletemplates.php?msg=unexpected");
+}
+
+if (schedules::removeTemplate($id)) {
+  security::go("scheduletemplates.php?msg=deleted");
+} else {
+  security::go("scheduletemplates.php?msg=unexpected");
+}
diff --git a/src/dodeletesecuritykey.php b/src/dodeletesecuritykey.php
new file mode 100644
index 0000000..1252d7c
--- /dev/null
+++ b/src/dodeletesecuritykey.php
@@ -0,0 +1,22 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER);
+security::checkWorkerUIEnabled();
+secondFactor::checkAvailability();
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_NEMPTY]
+])) {
+  security::go("securitykeys.php?msg=unexpected");
+}
+
+$id = (int)$_POST["id"];
+
+$s = secondFactor::getSecurityKeyById($id);
+if ($s === false || people::userData("id") != $s["person"]) security::go("securitykeys.php?msg=unexpected");
+
+if (secondFactor::removeSecurityKey($id)) {
+  security::go("securitykeys.php?msg=securitykeydeleted");
+} else {
+  security::go("securitykeys.php?msg=unexpected");
+}
diff --git a/src/dodeletetemplateday.php b/src/dodeletetemplateday.php
new file mode 100644
index 0000000..b4a19e4
--- /dev/null
+++ b/src/dodeletetemplateday.php
@@ -0,0 +1,23 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_NEMPTY]
+])) {
+  security::go("scheduletemplates.php?msg=unexpected");
+}
+
+$id = (int)$_POST["id"];
+
+$day = schedules::getTemplateDay($id);
+
+if ($day === false) {
+  security::go("scheduletemplates.php?msg=unexpected");
+}
+
+if (schedules::removeTemplateDay($id)) {
+  security::go("scheduletemplate.php?id=".(int)$day["template"]."&msg=deleted");
+} else {
+  security::go("scheduletemplate.php?id=".(int)$day["template"]."&msg=unexpected");
+}
diff --git a/src/dodeleteworkhistoryitem.php b/src/dodeleteworkhistoryitem.php
new file mode 100644
index 0000000..c5116dd
--- /dev/null
+++ b/src/dodeleteworkhistoryitem.php
@@ -0,0 +1,17 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_NEMPTY]
+])) {
+  security::go("workers.php?msg=empty");
+}
+
+$id = $_POST["id"];
+
+$item = workers::getWorkHistoryItem($id);
+if ($item === false) return false;
+
+if (workers::deleteWorkHistoryItem($id)) security::go("workers.php?openWorkerHistory=".(int)$item["worker"]);
+else security::go("workers.php?msg=unexpected");
diff --git a/src/dodisablesecondfactor.php b/src/dodisablesecondfactor.php
new file mode 100644
index 0000000..7ee3460
--- /dev/null
+++ b/src/dodisablesecondfactor.php
@@ -0,0 +1,39 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER, security::METHOD_NOTFOUND);
+security::checkWorkerUIEnabled();
+secondFactor::checkAvailability();
+
+if (!secondFactor::isEnabled()) {
+  security::notFound();
+}
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_ISSET]
+])) {
+  security::go("security.php?msg=empty");
+}
+
+$id = (int)$_POST["id"];
+
+$url = ((security::isAllowed(security::ADMIN) && $id != people::userData("id")) ? "users.php" : "security.php");
+
+if (!security::isAllowed(security::ADMIN)) {
+  if ($id != people::userData("id")) security::notFound();
+
+  if (!security::checkParams("POST", [
+    ["password", security::PARAM_ISSET]
+  ])) {
+    security::go($url."?msg=empty");
+  }
+
+  $password = (string)$_POST["password"];
+
+  if (!security::isUserPassword(false, $password)) security::go($url."?msg=wrongpassword");
+}
+
+if (secondFactor::disable($id)) {
+  security::go($url."?msg=disabledsecondfactor");
+} else {
+  security::go($url."?msg=unexpected");
+}
diff --git a/src/doeditcalendar.php b/src/doeditcalendar.php
new file mode 100644
index 0000000..b58d207
--- /dev/null
+++ b/src/doeditcalendar.php
@@ -0,0 +1,30 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_NEMPTY],
+  ["type", security::PARAM_ISSET]
+])) {
+  security::go("calendars.php?msg=unexpected");
+}
+
+$id = (int)$_POST["id"];
+
+$c = calendars::get($id);
+
+if ($c === false) {
+  security::go("calendars.php?msg=unexpected");
+}
+
+$calendar_response = calendars::parseFormCalendar($_POST["type"], $c["begins"], $c["ends"], true);
+
+if ($calendar_response === false) {
+  security::go("calendars.php?msg=unexpected");
+}
+
+if (calendars::edit($id, $calendar_response["calendar"])) {
+  security::go("calendars.php?msg=modified");
+} else {
+  security::go("calendars.php?msg=unexpected");
+}
diff --git a/src/doeditcategory.php b/src/doeditcategory.php
new file mode 100644
index 0000000..3d68e02
--- /dev/null
+++ b/src/doeditcategory.php
@@ -0,0 +1,23 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_NEMPTY],
+  ["name", security::PARAM_NEMPTY],
+  ["emails", security::PARAM_ISSET],
+  ["parent", security::PARAM_ISSET]
+])) {
+  security::go("categories.php?msg=empty");
+}
+
+$id = (int)$_POST["id"];
+$name = $_POST["name"];
+$emails = $_POST["emails"];
+$parent = (int)$_POST["parent"];
+
+if (categories::edit($id, $name, $emails, $parent)) {
+  security::go("categories.php?msg=modified");
+} else {
+  security::go("categories.php?msg=unexpected");
+}
diff --git a/src/doeditcompany.php b/src/doeditcompany.php
new file mode 100644
index 0000000..9129658
--- /dev/null
+++ b/src/doeditcompany.php
@@ -0,0 +1,21 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_NEMPTY],
+  ["name", security::PARAM_NEMPTY],
+  ["cif", security::PARAM_ISSET]
+])) {
+  security::go("companies.php?msg=empty");
+}
+
+$id = (int)$_POST["id"];
+$name = $_POST["name"];
+$cif = $_POST["cif"];
+
+if (companies::edit($id, $name, $cif)) {
+  security::go("companies.php?msg=modified");
+} else {
+  security::go("companies.php?msg=unexpected");
+}
diff --git a/src/doeditdayschedule.php b/src/doeditdayschedule.php
new file mode 100644
index 0000000..0cf775b
--- /dev/null
+++ b/src/doeditdayschedule.php
@@ -0,0 +1,42 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_NEMPTY]
+])) {
+  security::go("users.php?msg=unexpected");
+
+}
+$id = (int)$_POST["id"];
+
+$day = schedules::getDay($id);
+
+if ($day === false) {
+  security::go("users.php?msg=unexpected");
+}
+
+$dates = ["beginswork", "endswork", "beginsbreakfast", "endsbreakfast", "beginslunch", "endslunch"];
+$time = [];
+foreach ($dates as $date) {
+  if (isset($_POST[$date]) && !empty($_POST[$date])) {
+    if (!security::checkParam($_POST[$date], security::PARAM_ISTIME)) {
+      security::go("schedule.php?id=".$day["schedule"]."&msg=unexpected");
+    }
+    $time[$date] = schedules::time2sec($_POST[$date]);
+  } else {
+    $time[$date] = 0;
+  }
+}
+
+$status = schedules::checkAddDayGeneric($time["beginswork"], $time["endswork"], $time["beginsbreakfast"], $time["endsbreakfast"], $time["beginslunch"], $time["endslunch"]);
+
+if ($status != 0) {
+  security::go("schedule.php?id=".$day["schedule"]."&msg=errorcheck".(int)$status);
+}
+
+if (schedules::editDay($id, $time["beginswork"], $time["endswork"], $time["beginsbreakfast"], $time["endsbreakfast"], $time["beginslunch"], $time["endslunch"])) {
+  security::go("schedule.php?id=".$day["schedule"]."&msg=modified");
+} else {
+  security::go("schedule.php?id=".$day["schedule"]."&msg=unexpected");
+}
diff --git a/src/doeditdayscheduletemplate.php b/src/doeditdayscheduletemplate.php
new file mode 100644
index 0000000..36ecc4c
--- /dev/null
+++ b/src/doeditdayscheduletemplate.php
@@ -0,0 +1,42 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_NEMPTY]
+])) {
+  security::go("scheduletemplates.php?msg=unexpected");
+}
+
+$id = (int)$_POST["id"];
+
+$day = schedules::getTemplateDay($id);
+
+if ($day === false) {
+  security::go("scheduletemplates.php?msg=unexpected");
+}
+
+$dates = ["beginswork", "endswork", "beginsbreakfast", "endsbreakfast", "beginslunch", "endslunch"];
+$time = [];
+foreach ($dates as $date) {
+  if (isset($_POST[$date]) && !empty($_POST[$date])) {
+    if (!security::checkParam($_POST[$date], security::PARAM_ISTIME)) {
+      security::go("scheduletemplate.php?id=".$day["template"]."&msg=unexpected");
+    }
+    $time[$date] = schedules::time2sec($_POST[$date]);
+  } else {
+    $time[$date] = 0;
+  }
+}
+
+$status = schedules::checkAddDayGeneric($time["beginswork"], $time["endswork"], $time["beginsbreakfast"], $time["endsbreakfast"], $time["beginslunch"], $time["endslunch"]);
+
+if ($status != 0) {
+  security::go("scheduletemplate.php?id=".$day["template"]."&msg=errorcheck".(int)$status);
+}
+
+if (schedules::editTemplateDay($id, $time["beginswork"], $time["endswork"], $time["beginsbreakfast"], $time["endsbreakfast"], $time["beginslunch"], $time["endslunch"])) {
+  security::go("scheduletemplate.php?id=".$day["template"]."&msg=modified");
+} else {
+  security::go("scheduletemplate.php?id=".$day["template"]."&msg=unexpected");
+}
diff --git a/src/doeditincident.php b/src/doeditincident.php
new file mode 100644
index 0000000..428079c
--- /dev/null
+++ b/src/doeditincident.php
@@ -0,0 +1,61 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER, security::METHOD_NOTFOUND);
+security::checkWorkerUIEnabled();
+
+$isAdmin = security::isAdminView();
+$defaultUrl = ($isAdmin ? "incidents.php" : "userincidents.php?id=".$_SESSION["id"]);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_ISINT],
+  ["type", security::PARAM_ISINT],
+  ["day", security::PARAM_ISDATE]
+])) {
+  security::go(visual::getContinueUrl($url, "unexpected", "POST"));
+}
+
+$id = (int)$_POST["id"];
+$type = (int)$_POST["type"];
+$day = $_POST["day"];
+
+$incident = incidents::get($id, true);
+if ($incident === false) security::go(visual::getContinueUrl($url, "unexpected", "POST"));
+
+$istatus = incidents::getStatus($incident);
+
+if (($isAdmin && in_array($istatus, incidents::$cannotEditStates)) || (!$isAdmin && !in_array($istatus, incidents::$workerCanEditStates))) security::notFound();
+if (!$isAdmin) incidents::checkIncidentIsFromPerson($incident["id"]);
+
+if (isset($_POST["allday"]) && $_POST["allday"] == 1) {
+  $begins = 0;
+  $ends = incidents::ENDOFDAY;
+} else {
+  if (!security::checkParams("POST", [
+    ["begins", security::PARAM_ISTIME],
+    ["ends", security::PARAM_ISTIME]
+  ])) {
+    security::go(visual::getContinueUrl($url, "empty", "POST"));
+  }
+
+  $begins = schedules::time2sec($_POST["begins"]);
+  $ends = schedules::time2sec($_POST["ends"]);
+}
+
+$status = incidents::edit($id, $type, $day, $begins, $ends);
+
+switch ($status) {
+  case 0:
+  security::go(visual::getContinueUrl($url, "modified", "POST"));
+  break;
+
+  case 2:
+  security::go(visual::getContinueUrl($url, "overlap", "POST"));
+  break;
+
+  case 3:
+  security::go(visual::getContinueUrl($url, "order", "POST"));
+  break;
+
+  default:
+  security::go(visual::getContinueUrl($url, "unexpected", "POST"));
+}
diff --git a/src/doeditincidentcomment.php b/src/doeditincidentcomment.php
new file mode 100644
index 0000000..bc42d4d
--- /dev/null
+++ b/src/doeditincidentcomment.php
@@ -0,0 +1,26 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_ISINT]
+])) {
+  security::go("incidents.php?msg=unexpected");
+}
+
+$id = (int)$_POST["id"];
+$details = ((isset($_POST["details"]) && is_string($_POST["details"])) ? $_POST["details"] : "");
+
+$status = incidents::editDetails($id, $details);
+switch ($status) {
+  case 0:
+  security::go("incidents.php?msg=modified");
+  break;
+
+  case 1:
+  security::go("incidents.php?msg=cannotmodify");
+  break;
+
+  default:
+  security::go("incidents.php?msg=unexpected");
+}
diff --git a/src/doeditincidenttype.php b/src/doeditincidenttype.php
new file mode 100644
index 0000000..ca8ba35
--- /dev/null
+++ b/src/doeditincidenttype.php
@@ -0,0 +1,26 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_NEMPTY],
+  ["name", security::PARAM_NEMPTY]
+])) {
+  security::go("incidenttypes.php?msg=empty");
+}
+
+$id = (int)$_POST["id"];
+$name = $_POST["name"];
+$present = ((isset($_POST["present"]) && $_POST["present"] == 1) ? 1 : 0);
+$paid = ((isset($_POST["paid"]) && $_POST["paid"] == 1) ? 1 : 0);
+$workerfill = ((isset($_POST["workerfill"]) && $_POST["workerfill"] == 1) ? 1 : 0);
+$notifies = ((isset($_POST["notifies"]) && $_POST["notifies"] == 1) ? 1 : 0);
+$autovalidates = ((isset($_POST["autovalidates"]) && $_POST["autovalidates"] == 1) ? 1 : 0);
+$hidden = ((isset($_POST["hidden"]) && $_POST["hidden"] == 1) ? 1 : 0);
+
+if (incidents::editType($id, $name, $present, $paid, $workerfill, $notifies, $autovalidates, $hidden)) {
+  security::go("incidenttypes.php?msg=modified");
+} else {
+  security::go("incidenttypes.php?msg=unexpected");
+}
diff --git a/src/doeditschedule.php b/src/doeditschedule.php
new file mode 100644
index 0000000..d0706e4
--- /dev/null
+++ b/src/doeditschedule.php
@@ -0,0 +1,34 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_ISINT],
+  ["begins", security::PARAM_ISDATE],
+  ["ends", security::PARAM_ISDATE]
+])) {
+  security::go((isset($_POST["id"]) ? "schedule.php?id=".(int)$_POST["id"]."msg=empty" : "users.php"));
+}
+
+$id = $_POST["id"];
+$begins = $_POST["begins"];
+$ends = $_POST["ends"];
+
+$status = schedules::edit($id, $begins, $ends);
+switch ($status) {
+  case 0:
+  security::go("schedule.php?id=".(int)$id."&msg=modified");
+  break;
+
+  case 1:
+  security::go("schedule.php?id=".(int)$id."&msg=overlaps");
+  break;
+
+  case 3:
+  security::go("schedule.php?id=".(int)$id."&msg=order");
+  break;
+
+  default:
+  security::go("schedule.php?id=".(int)$id."&msg=unexpected");
+  break;
+}
diff --git a/src/doeditscheduletemplate.php b/src/doeditscheduletemplate.php
new file mode 100644
index 0000000..a08077a
--- /dev/null
+++ b/src/doeditscheduletemplate.php
@@ -0,0 +1,31 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_ISINT],
+  ["name", security::PARAM_NEMPTY],
+  ["begins", security::PARAM_ISDATE],
+  ["ends", security::PARAM_ISDATE]
+])) {
+  security::go((isset($_POST["id"]) ? "scheduletemplate.php?id=".(int)$_POST["id"]."msg=empty" : "scheduletemplates.php?msg=empty"));
+}
+
+$id = $_POST["id"];
+$name = $_POST["name"];
+$begins = $_POST["begins"];
+$ends = $_POST["ends"];
+
+$status = schedules::editTemplate($id, $name, $begins, $ends);
+switch ($status) {
+  case 0:
+  security::go("scheduletemplate.php?id=".(int)$id."&msg=modified");
+  break;
+
+  case 2:
+  security::go("scheduletemplate.php?id=".(int)$id."&msg=order");
+  break;
+
+  default:
+  security::go("scheduletemplate.php?id=".(int)$id."&msg=unexpected");
+}
diff --git a/src/doedituser.php b/src/doedituser.php
new file mode 100644
index 0000000..bf62372
--- /dev/null
+++ b/src/doedituser.php
@@ -0,0 +1,43 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_NEMPTY],
+  ["username", security::PARAM_NEMPTY],
+  ["name", security::PARAM_NEMPTY],
+  ["dni", security::PARAM_ISSET],
+  ["email", security::PARAM_ISEMAILOREMPTY],
+  ["category", security::PARAM_NEMPTY],
+  ["type", security::PARAM_ISSET]
+])) {
+  security::go("users.php?msg=empty");
+}
+
+$id = (int)$_POST["id"];
+$username = $_POST["username"];
+$name = $_POST["name"];
+$dni = $_POST["dni"];
+$email = $_POST["email"];
+$category = (int)$_POST["category"];
+$type = (int)$_POST["type"];
+
+$p = people::get($id);
+if ($p === false) security::go("users.php?msg=unexpected");
+
+if (!security::isAllowed($type) || !security::isAllowed($p["type"]) || !categories::exists($category) || !security::existsType($type)) security::go("users.php?msg=unexpected");
+
+if (people::edit($id, $username, $name, $dni, $email, $category, $type)) {
+  if (security::checkParams("POST", [["password", security::PARAM_NEMPTY]])) {
+    if (!security::passwordIsGoodEnough($_POST["password"])) security::go("users.php?msg=weakpassword");
+
+    $password_hash = password_hash($_POST["password"], PASSWORD_DEFAULT);
+    if (!people::updatePassword($id, $password_hash)) {
+      security::go("users.php?msg=couldntupdatepassword");
+    }
+  }
+} else {
+  security::go("users.php?msg=unexpected");
+}
+
+security::go("users.php?msg=modified");
diff --git a/src/doeditworkhistoryitem.php b/src/doeditworkhistoryitem.php
new file mode 100644
index 0000000..042f528
--- /dev/null
+++ b/src/doeditworkhistoryitem.php
@@ -0,0 +1,22 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_NEMPTY],
+  ["day", security::PARAM_ISSET],
+  ["status", security::PARAM_ISSET]
+])) {
+  security::go("workers.php?msg=empty");
+}
+
+$id = $_POST["id"];
+$date = new DateTime($_POST["day"]);
+$day = $date->getTimestamp();
+$status = $_POST["status"];
+
+$item = workers::getWorkHistoryItem($id);
+if ($item === false) return false;
+
+if (workers::editWorkHistoryItem($id, $day, $status)) security::go("workers.php?openWorkerHistory=".(int)$item["worker"]);
+else security::go("workers.php?msg=unexpected");
diff --git a/src/doenablesecondfactor.php b/src/doenablesecondfactor.php
new file mode 100644
index 0000000..47e1f6f
--- /dev/null
+++ b/src/doenablesecondfactor.php
@@ -0,0 +1,29 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER, security::METHOD_NOTFOUND);
+security::checkWorkerUIEnabled();
+secondFactor::checkAvailability();
+
+if (secondFactor::isEnabled()) {
+  security::notFound();
+}
+
+if (!security::checkParams("POST", [
+  ["secret", security::PARAM_ISSET],
+  ["code", security::PARAM_ISINT]
+])) {
+  security::go("security.php?msg=empty");
+}
+
+$secret = (string)$_POST["secret"];
+$code = (string)$_POST["code"];
+
+if (!secondFactor::checkCode($secret, $code)) {
+  security::go("security.php?msg=wrongcode");
+}
+
+if (secondFactor::enable($secret)) {
+  security::go("security.php?msg=enabledsecondfactor");
+} else {
+  security::go("security.php?msg=unexpected");
+}
diff --git a/src/doexport.php b/src/doexport.php
new file mode 100644
index 0000000..ae9589e
--- /dev/null
+++ b/src/doexport.php
@@ -0,0 +1,370 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER);
+security::checkWorkerUIEnabled();
+
+$isAdmin = security::isAllowed(security::ADMIN);
+
+$mainURL = (security::isAdminView() ? "export.php" : "export4worker.php");
+
+if (!security::checkParams("GET", [
+  ["begins", security::PARAM_ISDATE],
+  ["ends", security::PARAM_ISDATE],
+  ["workers", security::PARAM_ISARRAY],
+  ["format", security::PARAM_ISINT]
+])) {
+  security::go($mainURL."?msg=empty");
+}
+
+$ignoreempty = (isset($_GET["ignoreempty"]) && $_GET["ignoreempty"] == 1);
+$labelinvalid = (isset($_GET["labelinvalid"]) && $_GET["labelinvalid"] == 1);
+$showvalidated = (isset($_GET["showvalidated"]) && $_GET["showvalidated"] == 1);
+$shownotvalidated = (isset($_GET["shownotvalidated"]) && $_GET["shownotvalidated"] == 1);
+
+$begins = new DateTime($_GET["begins"]);
+$ends = new DateTime($_GET["ends"]);
+$interval = new DateInterval("P1D");
+
+if (!intervals::wellFormed([$begins, $ends])) {
+  security::go($mainURL."?msg=inverted");
+}
+
+$date = new DateTime(date("Y-m-d")."T00:00:00");
+$interval = new DateInterval("P1D");
+$date->sub($interval);
+
+if ($ends->diff($date)->invert !== 0) {
+  security::go($mainURL."?msg=forecastingthefutureisimpossible");
+}
+
+$companies = companies::getAll(false, true);
+if ($companies === false) {
+  security::go($mainURL."?msg=unexpected");
+}
+
+$filenameDate = $begins->format("Ymd")."_".$ends->format("Ymd");
+
+if (!$isAdmin && !in_array($_GET["format"], export::$workerFormats)) {
+  echo "Todavía no implementado.\n";
+  exit();
+}
+
+switch ($_GET["format"]) {
+  case export::FORMAT_PDF:
+  case export::FORMAT_DETAILEDPDF:
+  require_once("lib/fpdf/fpdf.php");
+  require_once("lib/fpdf-easytable/exfpdf.php");
+  require_once("lib/fpdf-easytable/easyTable.php");
+
+  $actualTime = time();
+
+  class PDF extends EXFPDF {
+    function Footer() {
+      global $actualTime;
+
+      $this->SetFont('Arial','I',10);
+      $this->SetY(-20);
+      $this->Cell(0, 10, export::convert("Generado: ".strftime("%d %b %Y %T", $actualTime)), 0, 0, 'L');
+      $this->SetY(-20);
+      $this->Cell(0, 10, $this->PageNo().'/{nb}', 0, 0, 'R');
+    }
+  }
+
+  $pdf = new PDF();
+  $pdf->SetCompression(true);
+  $pdf->SetCreator(export::convert($conf["appName"]));
+  $pdf->SetTitle(export::convert("Registro".($showvalidated && !$shownotvalidated ? " (validados)" : "").(!$showvalidated && $shownotvalidated ? " (no validados)" : "")));
+  $pdf->SetAutoPageBreak(false, 22);
+  $pdf->AliasNbPages();
+  $pdf->SetFont('Arial','',12);
+
+  $headerCells = ["Fecha", "Inicio jornada", "Fin jornada", "Desayuno", "Comida", ""];
+  $headerCells = array_map(function($value) {
+    return export::convert($value);
+  }, $headerCells);
+  $spacingDayRow = "%{18, 18, 18, 19, 19, 8}";
+
+  foreach ($_GET["workers"] as $workerid) {
+    $worker = workers::get($workerid);
+    if ($worker === false || (!$isAdmin && $worker["person"] != $_SESSION["id"])) continue;
+
+    $days = export::getDays($worker["id"], $begins->getTimestamp(), $ends->getTimestamp(), $showvalidated, $shownotvalidated);
+
+    if (count($days) || !$ignoreempty) {
+      $pdf->AddPage();
+      $pdf->SetMargins(17, 17);
+      $pdf->SetY(17);
+
+      $header = new easyTable($pdf, 1, 'border-width: 0.01; border: 0; align: L;');
+      $header->easyCell(export::convert($worker["name"].(!empty($worker["dni"]) ? " (".$worker["dni"].")" : "")), 'font-style:B;');
+      $header->printRow();
+      $header->easyCell(export::convert($worker["companyname"].(!empty($companies[$worker["company"]]["cif"]) ? " (CIF: ".$companies[$worker["company"]]["cif"].")" : "")));
+      $header->printRow();
+      $header->endTable(6);
+    }
+
+    if (count($days)) {
+      $totalEffectiveTime = 0;
+      $totalWorkTime = 0;
+      $incidentsTime = [];
+
+      $table = new easyTable($pdf, $spacingDayRow, 'border: "TB"; border-width: 0.15;');
+      foreach ($headerCells as $i => $cell) {
+        $table->easyCell($cell, 'font-style: B;'.($i == 0 ? "" : " align: C;"));
+      }
+      $table->printRow(true);
+      $table->endTable(0);
+
+      foreach ($days as $timestamp => $day) {
+        $issetSchedule = isset($day["schedule"]);
+        $showSchedule = $issetSchedule;
+        if ($showSchedule && isset($day["incidents"])) {
+          foreach ($day["incidents"] as $incident) {
+            if ($incident["allday"] == true && $incident["typepresent"] == 0) {
+              $showSchedule = false;
+              break;
+            }
+          }
+        }
+
+        $workInt = ($issetSchedule ? [$day["schedule"]["beginswork"], $day["schedule"]["endswork"]] : [incidents::STARTOFDAY, incidents::STARTOFDAY]);
+        $notWorkIntA = ($issetSchedule ? [incidents::STARTOFDAY, $day["schedule"]["beginswork"]] : [incidents::STARTOFDAY, incidents::ENDOFDAY]);
+        $notWorkIntB = ($issetSchedule ? [$day["schedule"]["endswork"], incidents::ENDOFDAY] : [incidents::ENDOFDAY, incidents::ENDOFDAY]);
+        $breakfastInt = ($issetSchedule ? [$day["schedule"]["beginsbreakfast"], $day["schedule"]["endsbreakfast"]] : [incidents::STARTOFDAY, incidents::STARTOFDAY]);
+        $lunchInt = ($issetSchedule ? [$day["schedule"]["beginslunch"], $day["schedule"]["endslunch"]] : [incidents::STARTOFDAY, incidents::STARTOFDAY]);
+
+        $effectiveTime = intervals::measure($workInt) - (intervals::measure($breakfastInt) + intervals::measure($lunchInt));
+
+        $totalWorkTime += $effectiveTime;
+
+        if (isset($day["incidents"])) {
+          foreach ($day["incidents"] as &$incident) {
+            $incidentInt = [$incident["begins"], $incident["ends"]];
+
+            $incidentTime = 0;
+
+            if ($incident["typepresent"] == 1) {
+              $incidentTime = intervals::measureIntersection($incidentInt, $notWorkIntA) + intervals::measureIntersection($incidentInt, $notWorkIntB) + intervals::measureIntersection($incidentInt, $breakfastInt) + intervals::measureIntersection($incidentInt, $lunchInt);
+              $effectiveTime = $effectiveTime + $incidentTime;
+
+              $incidentTime = -$incidentTime;
+            } else {
+              $incidentTime = intervals::measureIntersection($incidentInt, $workInt) - ($conf["pdfs"]["workersAlwaysHaveBreakfastAndLunch"] ? 0 : (intervals::measureIntersection($incidentInt, $breakfastInt) + intervals::measureIntersection($incidentInt, $lunchInt)));
+              $effectiveTime = $effectiveTime - $incidentTime;
+            }
+
+            $incidentsTime[$incident["typename"]] = (isset($incidentsTime[$incident["typename"]]) ? $incidentsTime[$incident["typename"]] : 0) + $incidentTime;
+          }
+        }
+
+        $effectiveTime = max(incidents::STARTOFDAY, min($effectiveTime, incidents::ENDOFDAY));
+        $totalEffectiveTime += $effectiveTime;
+
+        $labelAsInvalid = ($shownotvalidated && isset($day["schedule"]) && $day["schedule"]["state"] === registry::STATE_REGISTERED);
+
+        $table = new easyTable($pdf, $spacingDayRow, 'border: "BT";'.(isset($day["incidents"]) ? ' border-width: 0.07; border-color: #555;' : ' border-width: 0.15;').($labelAsInvalid && $labelinvalid ? ' font-color: #F00;' : ''));
+        $table->easyCell(export::convert(date("d/m/Y", $timestamp).($labelAsInvalid ? " (nv)" : "")));
+        $table->easyCell(export::convert(($showSchedule ? schedules::sec2time($day["schedule"]["beginswork"]) : "-")), 'align: C;');
+        $table->easyCell(export::convert(($showSchedule ? schedules::sec2time($day["schedule"]["endswork"]) : "-")), 'align: C;');
+        $table->easyCell(export::convert(($showSchedule ? (intervals::measure($breakfastInt) == 0 ? "-" : (!$conf["pdfs"]["showExactTimeForBreakfastAndLunch"] ? export::sec2hours(intervals::measure($breakfastInt)) : schedules::sec2time($day["schedule"]["beginsbreakfast"])." - ".schedules::sec2time($day["schedule"]["endsbreakfast"]))) : "-")), 'align: C;');
+        $table->easyCell(export::convert(($showSchedule ? (intervals::measure($lunchInt) == 0 ? "-" : (!$conf["pdfs"]["showExactTimeForBreakfastAndLunch"] ? export::sec2hours(intervals::measure($lunchInt)) : schedules::sec2time($day["schedule"]["beginslunch"])." - ".schedules::sec2time($day["schedule"]["endslunch"]))) : "-")), 'align: C;');
+        $table->easyCell(export::convert(export::sec2hours($effectiveTime)), 'font-style: B; align: R;');
+        $table->printRow();
+        $table->endTable(0);
+
+        if (isset($day["incidents"])) {
+          $incidentstable = new easyTable($pdf, "%{7, 15, 20, 20, 38}", 'border: "BT"; border-width: 0.15; font-color: #555;');
+          foreach ($day["incidents"] as &$incident) {
+            $labelAsInvalid = ($incident["state"] === incidents::STATE_REGISTERED);
+
+            if ($labelAsInvalid && $labelinvalid) $incidentstable->rowStyle('font-color: #F00;');
+            $incidentstable->easyCell("");
+            $incidentstable->easyCell(export::convert("Incidencia:"), 'font-style: B;');
+            $incidentstable->easyCell(export::convert($incident["typename"]), 'align: C; font-size: 11;');
+            $incidentstable->easyCell(export::convert("(".(($incident["begins"] == 0 && $incident["ends"] == incidents::ENDOFDAY) ? "todo el día" : schedules::sec2time($incident["begins"])." - ".schedules::sec2time($incident["ends"])).")"), 'align: C;');
+            $incidentstable->easyCell(export::convert((!empty($incident["details"]) ? "Obs.: ".$incident["details"] : "").($labelAsInvalid ? "\n(No validada)" : "")), 'font-size: 10;');
+            $incidentstable->printRow();
+          }
+          $incidentstable->endTable(0);
+        }
+      }
+
+      if ($_GET["format"] == export::FORMAT_DETAILEDPDF) {
+        $pdf->Ln();
+        $pdf->Ln();
+        $table = new easyTable($pdf, "%{77, 23}", 'align: R; width: 100; border: "BT"; border-width: 0.15;');
+
+        $table->easyCell(export::convert("Tiempo de trabajo programado"));
+        $table->easyCell(export::convert(export::sec2hours($totalWorkTime)), 'align: R;');
+        $table->printRow();
+
+        foreach ($incidentsTime as $incidentName => $incidentTime) {
+          $table->easyCell(export::convert($incidentName));
+          $table->easyCell(export::convert(($incidentTime <= 0 ? "+" : "\u{2013}")." ".export::sec2hours(abs($incidentTime))), 'align: R;');
+          $table->printRow();
+        }
+
+        $table->easyCell(export::convert("Tiempo trabajado"), 'font-style: B;');
+        $table->easyCell(export::convert(export::sec2hours($totalEffectiveTime)), 'align: R; font-style: B;');
+        $table->printRow();
+
+        $table->endTable(0);
+      }
+    } elseif (!$ignoreempty) {
+      $pdf->Cell(0, 0, "No hay datos para este trabajador.", 0, 0, 'C');
+    }
+  }
+
+  $pdf->Output("I", "registrohorario_".$filenameDate.".pdf");
+  break;
+
+  case export::FORMAT_CSV_SCHEDULES:
+  case export::FORMAT_CSV_INCIDENTS:
+  $isSchedules = ($_GET["format"] == export::FORMAT_CSV_SCHEDULES);
+  $field = ($isSchedules ? "schedule" : "incidents");
+  header("Content-Type: text/csv");
+  header("Content-Disposition: attachment;filename=".($isSchedules ? "schedules_".$filenameDate.".csv" : "incidents_".$filenameDate.".csv"));
+
+  $array = [];
+
+  $array[] = ($isSchedules ? export::$schedulesFields : export::$incidentsFields);
+
+  foreach ($_GET["workers"] as $workerid) {
+    $worker = workers::get($workerid);
+    if ($worker === false || (!$isAdmin && $worker["person"] != $_SESSION["id"])) continue;
+
+    $days = export::getDays($worker["id"], $begins->getTimestamp(), $ends->getTimestamp(), true, true);
+
+    foreach ($days as &$day) {
+      if (isset($day[$field])) {
+        if ($isSchedules) {
+          $schedule = [];
+          foreach (export::$schedulesFields as $i => $key) {
+            $types = ["breakfast", "lunch"];
+            $typesNotDefined = [];
+            foreach ($types as $type) {
+              if (intervals::measure([$day[$field]["begins".$type], $day[$field]["ends".$type]]) == 0) {
+                $typesNotDefined[] = "begins".$type;
+                $typesNotDefined[] = "ends".$type;
+              }
+            }
+
+            switch ($key) {
+              case "worker":
+              $schedule[$i] = $worker["name"];
+              break;
+
+              case "company":
+              $schedule[$i] = $worker["companyname"];
+              break;
+
+              case "workerid":
+              $schedule[$i] = $worker["id"];
+              break;
+
+              case "dni":
+              $schedule[$i] = $worker["dni"];
+              break;
+
+              case "day":
+              $schedule[$i] = date("d/m/Y", $day[$field][$key]);
+              break;
+
+              case "state":
+              $schedule[$i] = (registry::$stateTooltips[$day[$field][$key]]);
+              break;
+
+              case "beginswork":
+              case "endswork":
+              case "beginsbreakfast":
+              case "endsbreakfast":
+              case "beginslunch":
+              case "endslunch":
+              $schedule[$i] = (in_array($key, $typesNotDefined) ? "-" : schedules::sec2time($day[$field][$key]));
+              break;
+
+              default:
+              $schedule[$i] = $day[$field][$key];
+            }
+          }
+          $array[] = $schedule;
+        } else {
+          foreach ($day[$field] as &$incident) {
+            $convIncident = [];
+            foreach (export::$incidentsFields as $i => $key) {
+              switch ($key) {
+                case "worker":
+                $convIncident[$i] = $worker["name"];
+                break;
+
+                case "company":
+                $convIncident[$i] = $worker["companyname"];
+                break;
+
+                case "workerid":
+                $convIncident[$i] = $worker["id"];
+                break;
+
+                case "dni":
+                $convIncident[$i] = $worker["dni"];
+                break;
+
+                case "creator":
+                case "updatedby":
+                case "confirmedby":
+                $convIncident[$i] = (($key == "updatedby" && $incident["updatestate"] != 1) ? "-" : people::userData("name", $incident[$key]));
+                break;
+
+                case "day":
+                $convIncident[$i] = date("d/m/Y", $incident[$key]);
+                break;
+
+                case "begins":
+                case "ends":
+                $convIncident[$i] = ($incident["allday"] == 1 ? "-" : schedules::sec2time($incident[$key]));
+                break;
+
+                case "updated":
+                case "verified":
+                case "typepresent":
+                case "typepaid":
+                if ($key == "updated") $key = "updatestate";
+                $convIncident[$i] = ($incident[$key] == -1 ? "-" : ($incident[$key] == 1 ? visual::YES : visual::NO));
+                break;
+
+                case "allday":
+                $convIncident[$i] = ($incident[$key] == 1 ? visual::YES : visual::NO);
+                break;
+
+                case "state":
+                $convIncident[$i] = (incidents::$stateTooltips[$incident[$key]]);
+                break;
+
+                case "type":
+                $convIncident[$i] = $incident["typename"];
+                break;
+
+                default:
+                $convIncident[$i] = $incident[$key];
+              }
+            }
+            $array[] = $convIncident;
+          }
+        }
+      }
+    }
+  }
+
+  if (!count($array)) exit();
+
+  $df = fopen("php://output", 'w');
+
+  foreach ($array as $row) fputcsv($df, $row);
+
+  fclose($df);
+  break;
+
+  default:
+  header("Content-Type: text/plain;");
+  echo "Todavía no implementado.\n";
+}
diff --git a/src/doinvalidatebulkrecords.php b/src/doinvalidatebulkrecords.php
new file mode 100644
index 0000000..a657efc
--- /dev/null
+++ b/src/doinvalidatebulkrecords.php
@@ -0,0 +1,26 @@
+<?php
+require_once("core.php");
+security::checkType(security::HYPERADMIN);
+
+if (!security::checkParams("POST", [
+  ["begins", security::PARAM_ISDATE],
+  ["ends", security::PARAM_ISDATE],
+  ["workers", security::PARAM_ISARRAY]
+])) {
+  security::go("invalidatebulkrecords.php?msg=empty");
+}
+
+$begins = $_POST["begins"];
+$ends = $_POST["ends"];
+
+if (!intervals::wellFormed([$begins, $ends])) {
+  security::go("invalidatebulkrecords.php?msg=inverted");
+}
+
+$flag = true;
+
+foreach ($_POST["workers"] as $workerid) {
+  if (!registry::invalidateAll($workerid, $begins, $ends)) $flag = false;
+}
+
+security::go("invalidatebulkrecords.php?msg=".($flag ? "success" : "partialortotalfailure"));
diff --git a/src/doinvalidateincident.php b/src/doinvalidateincident.php
new file mode 100644
index 0000000..3a8ee5b
--- /dev/null
+++ b/src/doinvalidateincident.php
@@ -0,0 +1,17 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_ISINT]
+])) {
+  security::go(visual::getContinueUrl("incidents.php", "unexpected", "POST"));
+}
+
+$id = (int)$_POST["id"];
+
+if (incidents::invalidate($id)) {
+  security::go(visual::getContinueUrl("incidents.php", "invalidated", "POST"));
+} else {
+  security::go(visual::getContinueUrl("incidents.php", "unexpected", "POST"));
+}
diff --git a/src/doinvalidaterecord.php b/src/doinvalidaterecord.php
new file mode 100644
index 0000000..2799922
--- /dev/null
+++ b/src/doinvalidaterecord.php
@@ -0,0 +1,18 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER, security::METHOD_NOTFOUND);
+security::checkWorkerUIEnabled();
+
+if (!isset($_POST["id"])) {
+  security::notFound();
+}
+
+$id = (int)$_POST["id"];
+
+$record = registry::get($id);
+if ($record === false || $record["invalidated"] != 0) security::notFound();
+
+$isAdmin = security::isAllowed(security::ADMIN);
+if (!$isAdmin) registry::checkRecordIsFromPerson($record["id"]);
+
+security::go((security::isAdminView() ? "registry.php?msg=" : "userregistry.php?id=".$_SESSION["id"]).(registry::invalidate($id) ? "invalidated" : "unexpected"));
diff --git a/src/domanuallygenerateregistry.php b/src/domanuallygenerateregistry.php
new file mode 100644
index 0000000..e7482ab
--- /dev/null
+++ b/src/domanuallygenerateregistry.php
@@ -0,0 +1,44 @@
+<?php
+require_once("core.php");
+security::checkType(security::HYPERADMIN);
+
+$advancedMode = (isset($_POST["advanced"]) && $_POST["advanced"] == "1");
+
+if (!$advancedMode) {
+  if (!security::checkParams("POST", [
+    ["day", security::PARAM_ISDATE],
+    ["workers", security::PARAM_ISARRAY]
+  ])) {
+    security::go("manuallygenerateregistry.php?msg=empty");
+  }
+
+  $day = new DateTime($_POST["day"]);
+  $time = $day->getTimestamp();
+
+  $logId = -1;
+  $status = registry::generateNow($time, $logId, true, people::userData("id"), $_POST["workers"]);
+
+  security::go("manuallygenerateregistry.php?".($logId == -1 ? "msg=generatederr" : "")."&logId=".$logId);
+} else {
+  if (!security::checkParams("POST", [
+    ["begins", security::PARAM_ISDATE],
+    ["ends", security::PARAM_ISDATE],
+    ["workers", security::PARAM_ISARRAY]
+  ])) {
+    security::go("manuallygenerateregistry.php?msg=empty");
+  }
+
+  $executedBy = people::userData("id");
+
+  $current = new DateTime($_POST["begins"]);
+  $ends = new DateTime($_POST["ends"]);
+  $interval = new DateInterval("P1D");
+  while ($current->diff($ends)->invert === 0) {
+    $logId = 0;
+    registry::generateNow($current->getTimestamp(), $logId, true, $executedBy, $_POST["workers"]);
+
+    $current->add($interval);
+  }
+
+  security::go("manuallygenerateregistry.php?msg=done");
+}
diff --git a/src/dorecovery.php b/src/dorecovery.php
new file mode 100644
index 0000000..7d57275
--- /dev/null
+++ b/src/dorecovery.php
@@ -0,0 +1,18 @@
+<?php
+require_once("core.php");
+
+if (!security::checkParams("POST", [
+  ["token", security::PARAM_NEMPTY],
+  ["password", security::PARAM_NEMPTY]
+])) {
+  security::go("index.php?msg=unexpected");
+}
+
+$token = $_POST["token"];
+$password = $_POST["password"];
+
+if (!security::passwordIsGoodEnough($password)) security::go("recovery.php?token=".$token."&msg=weakpassword");
+
+$status = recovery::finishRecovery($token, $password);
+
+security::go("index.php?msg=".($status ? "recoverycompleted" : "recovery2failed"));
diff --git a/src/dosendbulkpasswords.php b/src/dosendbulkpasswords.php
new file mode 100644
index 0000000..724fdb8
--- /dev/null
+++ b/src/dosendbulkpasswords.php
@@ -0,0 +1,51 @@
+<?php
+require_once("core.php");
+security::checkType(security::HYPERADMIN);
+
+if (!security::checkParams("POST", [
+  ["people", security::PARAM_ISARRAY]
+])) {
+  security::go("sendbulkpasswords.php?msg=empty");
+}
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Resultado del envío de enlaces</h2>
+
+          <?php
+          foreach ($_POST["people"] as $id) {
+            $person = people::get($id);
+            if ($person === false) continue;
+
+            $status = recovery::recover($person["id"], recovery::EMAIL_TYPE_WELCOME);
+
+            $personText = "&ldquo;".security::htmlsafe($person["name"])."&rdquo;";
+
+            if ($status) {
+              echo "<p class='mdl-color-text--green'>Enlace enviado correctamente a $personText.";
+            } elseif ($status === recovery::EMAIL_NOT_SET) {
+              echo "<p class='mdl-color-text--orange'>No se ha podido enviar el correo a $personText porque no tiene asociada ninguna dirección de correo electrónico.";
+            } else {
+              echo "<p class='mdl-color-text--red'>Ha ocurrido un error generando el enlace o enviando el correo a $personText.";
+            }
+            echo "</p>";
+          }
+          ?>
+        </div>
+      </div>
+    </main>
+  </div>
+</body>
+</html>
diff --git a/src/dosethelpresource.php b/src/dosethelpresource.php
new file mode 100644
index 0000000..2b7295b
--- /dev/null
+++ b/src/dosethelpresource.php
@@ -0,0 +1,24 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["place", security::PARAM_ISINT],
+  ["url", security::PARAM_ISSET]
+])) {
+  security::go("help.php?msg=empty");
+}
+
+$status = help::set($_POST["place"], $_POST["url"]);
+switch ($status) {
+  case 0:
+  security::go("help.php?msg=success");
+  break;
+
+  case 1:
+  security::go("help.php?msg=invalidurl");
+  break;
+
+  default:
+  security::go("help.php?msg=unexpected");
+}
diff --git a/src/dostartrecovery.php b/src/dostartrecovery.php
new file mode 100644
index 0000000..b7060bc
--- /dev/null
+++ b/src/dostartrecovery.php
@@ -0,0 +1,22 @@
+<?php
+require_once("core.php");
+
+if (!$conf["enableRecovery"] || !$conf["mail"]["enabled"]) security::notFound();
+
+if (!security::checkParams("POST", [
+  ["email", security::PARAM_ISEMAIL],
+  ["dni", security::PARAM_NEMPTY]
+])) {
+  security::go("index.php?msg=unexpected");
+}
+
+$email = $_POST["email"];
+$dni = $_POST["dni"];
+
+$user = recovery::getUser($email, $dni);
+if ($user === false) {
+  sleep(3);
+  security::go("index.php?msg=recovery");
+}
+
+security::go("index.php?msg=".(recovery::recover($user) ? "recovery" : "unexpected"));
diff --git a/src/dovalidate.php b/src/dovalidate.php
new file mode 100644
index 0000000..3f5084c
--- /dev/null
+++ b/src/dovalidate.php
@@ -0,0 +1,27 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER);
+
+if (!security::checkParams("POST", [
+  ["incidents", security::PARAM_ISSET],
+  ["records", security::PARAM_ISSET],
+  ["method", security::PARAM_ISINT]
+])) {
+  security::go("validations.php?msg=unexpected");
+}
+
+$method = (int)$_POST["method"];
+
+$status = validations::validate($method, $_POST["incidents"], $_POST["records"]);
+switch ($status) {
+  case 0:
+  security::go("validations.php?msg=success");
+  break;
+
+  case 1:
+  security::go("validations.php?msg=partialsuccess");
+  break;
+
+  default:
+  security::go("validations.php?msg=unexpected");
+}
diff --git a/src/doverifyincident.php b/src/doverifyincident.php
new file mode 100644
index 0000000..c6e58bf
--- /dev/null
+++ b/src/doverifyincident.php
@@ -0,0 +1,19 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("POST", [
+  ["id", security::PARAM_ISINT],
+  ["value", security::PARAM_ISINT]
+])) {
+  security::go(visual::getContinueUrl("incidents.php", "unexpected", "POST"));
+}
+
+$id = (int)$_POST["id"];
+$value = ($_POST["value"] == 1 ? 1 : 0);
+
+if (incidents::verify($id, $value)) {
+  security::go(visual::getContinueUrl("incidents.php", "verified".$value, "POST"));
+} else {
+  security::go(visual::getContinueUrl("incidents.php", "unexpected", "POST"));
+}
diff --git a/src/dynamic/addincidentbulk.php b/src/dynamic/addincidentbulk.php
new file mode 100644
index 0000000..afb04ba
--- /dev/null
+++ b/src/dynamic/addincidentbulk.php
@@ -0,0 +1,86 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!security::checkParams("GET", [
+  ["workers", security::PARAM_ISARRAY]
+])) {
+  security::notFound();
+}
+?>
+
+<style>
+.notvisible {
+  display: none;
+}
+</style>
+
+<dynscript>
+document.getElementById("allday").addEventListener("change", e => {
+  var partialtime = document.getElementById("partialtime");
+  if (e.target.checked) {
+    partialtime.classList.add("notvisible");
+  } else {
+    partialtime.classList.remove("notvisible");
+  }
+});
+</dynscript>
+
+<form action="doaddincidentbulk.php" method="POST" autocomplete="off">
+  <h4 class="mdl-dialog__title">Añade una incidencia</h4>
+  <div class="mdl-dialog__content">
+    <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+      <select name="type" id="type" class="mdlext-selectfield__select" data-required>
+        <option></option>
+        <?php
+        foreach (incidents::getTypesForm() as $i) {
+          echo '<option value="'.(int)$i["id"].'">'.security::htmlsafe($i["name"]).'</option>';
+        }
+        ?>
+      </select>
+      <label for="type" class="mdlext-selectfield__label">Tipo</label>
+    </div>
+
+    <h5>Afectación</h5>
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="date" name="day" id="day" autocomplete="off" data-required>
+      <label class="mdl-textfield__label always-focused" for="day">Día</label>
+    </div>
+    <br>
+    <p>
+      <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="allday">
+        <input type="checkbox" id="allday" name="allday" value="1" class="mdl-switch__input">
+        <span class="mdl-switch__label">Día entero</span>
+      </label>
+    </p>
+    <div id="partialtime">De <input type="time" name="begins"> a <input type="time" name="ends"></div>
+
+    <h5>Detalles adicionales</h5>
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <textarea class="mdl-textfield__input" name="details" id="details"></textarea>
+      <label class="mdl-textfield__label" for="details">Observaciones (opcional)</label>
+    </div>
+    <p>Las observaciones aparecerán en los PDFs que se exporten.</p>
+    <p>Después de crear la incidencia podrás añadir archivos adjuntos haciendo clic en el botón <i class="material-icons" style="vertical-align: middle;">attach_file</i>.</p>
+
+    <b>Trabajadores:</b>
+    <div class="copyto">
+      <ul>
+        <?php
+        foreach ($_GET["workers"] as $workerid) {
+          $worker = workers::get($workerid);
+          if ($worker === false) {
+            die("Error: Uno de los trabajadores seleccionados ya no existe");
+          }
+
+          echo "<li><input type='hidden' name='workers[]' value='".(int)$worker["id"]."'> ".security::htmlsafe($worker["name"])." (".security::htmlsafe($worker["companyname"]).")</li>";
+        }
+        ?>
+      </ul>
+    </div>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Añadir</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/addworkhistoryitem.php b/src/dynamic/addworkhistoryitem.php
new file mode 100644
index 0000000..712c3f6
--- /dev/null
+++ b/src/dynamic/addworkhistoryitem.php
@@ -0,0 +1,50 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$id = (int)$_GET["id"];
+
+$worker = workers::get($id);
+if ($worker === false) security::notFound();
+?>
+
+<dynscript>
+document.getElementById("cancel").addEventListener("click", e => {
+  e.preventDefault();
+  dynDialog.load("dynamic/workhistory.php?id="+parseInt(document.getElementById("cancel").getAttribute("data-worker-id")));
+});
+</dynscript>
+
+<form action="doaddworkhistoryitem.php" method="POST" autocomplete="off">
+  <input type="hidden" name="id" value="<?=(int)$worker["id"]?>">
+  <h4 class="mdl-dialog__title">Añadir alta/baja</h4>
+  <div class="mdl-dialog__content">
+    <p><b>Persona:</b> <?=security::htmlsafe($worker["name"])?><br>
+    <b>Empresa:</b> <?=security::htmlsafe($worker["companyname"])?></p>
+
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="date" name="day" id="day" autocomplete="off" data-required>
+      <label class="mdl-textfield__label" for="day">Fecha</label>
+    </div>
+    <br>
+    <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+      <select name="status" id="status" class="mdlext-selectfield__select" data-required>
+        <option></option>
+        <?php
+        foreach (workers::$affiliationStatusesManual as $status) {
+          echo '<option value="'.(int)$status.'">'.security::htmlsafe(workers::affiliationStatusHelper($status)).'</option>';
+        }
+        ?>
+      </select>
+      <label for="status" class="mdlext-selectfield__label">Tipo</label>
+    </div>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Añadir</button>
+    <button id="cancel" class="mdl-button mdl-js-button mdl-js-ripple-effect" data-worker-id="<?=(int)$worker["id"]?>">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/authorsincident.php b/src/dynamic/authorsincident.php
new file mode 100644
index 0000000..25194f3
--- /dev/null
+++ b/src/dynamic/authorsincident.php
@@ -0,0 +1,43 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::WORKER, security::METHOD_NOTFOUND);
+security::checkWorkerUIEnabled();
+
+$isAdmin = security::isAllowed(security::ADMIN);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$id = (int)$_GET["id"];
+
+$incident = incidents::get($id, true);
+if ($incident === false) security::notFound();
+
+if (!$isAdmin) incidents::checkIncidentIsFromPerson($incident["id"]);
+?>
+
+<style>
+#dynDialog {
+  max-width: 380px;
+  width: auto;
+}
+</style>
+
+<h4 class="mdl-dialog__title">Autoría de la incidencia</h4>
+<div class="mdl-dialog__content">
+  <ul>
+    <?php if ($incident["creator"] != -1) { ?><li><b>Creador:</b> <?=security::htmlsafe(people::userData("name", $incident["creator"]))?></li><?php } ?>
+    <?php if ($incident["updatedby"] != -1) { ?><li><b>Última modificación por:</b> <?=security::htmlsafe(people::userData("name", $incident["updatedby"]))?></li><?php } ?>
+    <?php if ($incident["confirmedby"] != -1) { ?><li><b>Revisor:</b> <?=security::htmlsafe(people::userData("name", $incident["confirmedby"]))?></li><?php } ?>
+    <?php
+    if ($incident["state"] == incidents::STATE_VALIDATED_BY_WORKER) {
+      $validation = json_decode($incident["workervalidation"], true);
+      validationsView::renderValidationInfo($validation);
+    }
+    ?>
+  </ul>
+</div>
+<div class="mdl-dialog__actions">
+  <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Cerrar</button>
+</div>
diff --git a/src/dynamic/authorsrecord.php b/src/dynamic/authorsrecord.php
new file mode 100644
index 0000000..8888af3
--- /dev/null
+++ b/src/dynamic/authorsrecord.php
@@ -0,0 +1,43 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::WORKER, security::METHOD_NOTFOUND);
+security::checkWorkerUIEnabled();
+
+$isAdmin = security::isAllowed(security::ADMIN);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$id = (int)$_GET["id"];
+
+$record = registry::get($id, true);
+if ($record === false) security::notFound();
+
+if (!$isAdmin) registry::checkRecordIsFromPerson($record["id"]);
+?>
+
+<style>
+#dynDialog {
+  max-width: 380px;
+  width: auto;
+}
+</style>
+
+<h4 class="mdl-dialog__title">Autoría del elemento del registro</h4>
+<div class="mdl-dialog__content">
+  <ul>
+    <li><b>Creador:</b> <?=($record["creator"] == -1 ? "<span style='font-family: monospace;'>cron</span>" : security::htmlsafe(people::userData("name", $record["creator"])))?></li>
+    <li><b>Fecha de creación:</b> <?=date("d/m/Y H:i", $record["created"])?></li>
+    <?php if ($record["invalidatedby"] != -1) { ?><li><b>Invalidado por:</b> <?=security::htmlsafe(people::userData("name", $record["invalidatedby"]))?></li><?php } ?>
+    <?php
+    if ($record["state"] == registry::STATE_VALIDATED_BY_WORKER) {
+      $validation = json_decode($record["workervalidation"], true);
+      validationsView::renderValidationInfo($validation);
+    }
+    ?>
+  </ul>
+</div>
+<div class="mdl-dialog__actions">
+  <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Cerrar</button>
+</div>
diff --git a/src/dynamic/companyuser.php b/src/dynamic/companyuser.php
new file mode 100644
index 0000000..0acfac3
--- /dev/null
+++ b/src/dynamic/companyuser.php
@@ -0,0 +1,102 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$p = people::get($_GET["id"]);
+
+if ($p === false) {
+  security::notFound();
+}
+
+$companies = companies::getAll();
+?>
+
+<style>
+.mdl-dialog__content {
+  color: rgba(0,0,0,.87)!important;
+}
+
+#dynDialog {
+  max-width: 300px;
+  width: auto;
+}
+</style>
+
+<dynscript>
+var person = <?=(int)$p["id"]?>;
+
+document.querySelectorAll("button[data-company-id]").forEach(btn => {
+  btn.addEventListener("click", e => {
+    var id = e.currentTarget.getAttribute("data-company-id");
+    fetch("ajax/addpersontocompany.php", {
+      method: "post",
+      body: "person="+parseInt(person)+"&company="+parseInt(id),
+      headers: {
+        "Content-Type": "application/x-www-form-urlencoded"
+      }
+    }).then(response => {
+      response.text().then(text => console.log);
+      dynDialog.reload();
+    }).catch(error => {
+      alert("Ha habido un error dando de alta a este trabajador de esta empresa: "+error);
+    });
+  });
+});
+
+document.querySelectorAll("button[data-worker-id]").forEach(btn => {
+  btn.addEventListener("click", e => {
+    var id = e.currentTarget.getAttribute("data-worker-id");
+    dynDialog.load("dynamic/workhistory.php?id="+parseInt(id));
+  });
+});
+</dynscript>
+
+<h4 class="mdl-dialog__title"><?=security::htmlsafe($p["name"])?></h4>
+<div class="mdl-dialog__content">
+<?php
+$list = [];
+$list["visible"] = "";
+$list["hidden"] = "";
+
+$workers = workers::getPersonWorkers($p["id"]);
+foreach ($workers as $w) {
+  $list[($w["hidden"] ? "hidden" : "visible")] .= '<li>'.
+    security::htmlsafe($companies[$w["company"]]).'
+    <button class="mdl-button mdl-js-button mdl-button--icon" title="Acceder al historial de altas y bajas" data-worker-id="'.(int)$w["id"].'">
+      <i class="material-icons">history</i>
+    </button>
+    <br>
+    <span class="mdl-color-text--grey-600">'.($w["hidden"] ? "Dada de baja" : "Dada de alta").' el '.date("d/m/Y", $w["lastupdated"]).'</span></li>';
+}
+?>
+  <p><b>Dada de alta en:</b></p>
+  <?php
+  if (!empty($list["visible"])) {
+    echo "<ul>".$list["visible"]."</ul>";
+  }
+  ?>
+  <p><b>Dada de baja en:</b></p>
+  <?php
+  if (!empty($list["hidden"])) {
+    echo "<ul>".$list["hidden"]."</ul>";
+  }
+  ?>
+  <p><b>No dada de alta en:</b></p>
+  <ul>
+    <?php
+    foreach ($companies as $id => $name) {
+      if (in_array($id, $p["companies"])) continue;
+      ?>
+      <li><?=security::htmlsafe($name)?> <button class="mdl-button mdl-js-button mdl-button--icon mdl-color-text--green" title="Dar de alta en esta empresa" data-company-id="<?=(int)$id?>"><i class="material-icons">add</i></button></li>
+      <?php
+    }
+    ?>
+  </ul>
+</div>
+<div class="mdl-dialog__actions">
+  <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cerrar</button>
+</div>
diff --git a/src/dynamic/copytemplate.php b/src/dynamic/copytemplate.php
new file mode 100644
index 0000000..0f127ba
--- /dev/null
+++ b/src/dynamic/copytemplate.php
@@ -0,0 +1,52 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!security::checkParams("GET", [
+  ["workers", security::PARAM_ISARRAY]
+])) {
+  security::notFound();
+}
+?>
+
+<form action="docopytemplate.php" method="POST" autocomplete="off">
+  <h4 class="mdl-dialog__title">Copiar plantilla a trabajadores</h4>
+  <div class="mdl-dialog__content">
+    <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+      <select name="template" id="template" class="mdlext-selectfield__select" data-required>
+        <?php
+        $templates = schedules::getTemplates();
+        foreach ($templates as $t) {
+          echo '<option value="'.(int)$t["id"].'">'.security::htmlsafe($t["name"]).'</option>';
+        }
+        ?>
+      </select>
+      <label for="template" class="mdlext-selectfield__label">Plantilla</label>
+    </div>
+    <br>
+    <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="active">
+      <input type="checkbox" id="active" name="active" value="1" class="mdl-switch__input">
+      <span class="mdl-switch__label">Activar horario</span>
+    </label>
+    <br><br>
+    <b>Copiar a:</b>
+    <div class="copyto">
+      <ul>
+        <?php
+        foreach ($_GET["workers"] as $workerid) {
+          $worker = workers::get($workerid);
+          if ($worker === false) {
+            die("Error: Uno de los trabajadores seleccionados ya no existe");
+          }
+
+          echo "<li><input type='hidden' name='workers[]' value='".(int)$worker["id"]."'> ".security::htmlsafe($worker["name"])." (".security::htmlsafe($worker["companyname"]).")</li>";
+        }
+        ?>
+      </ul>
+    </div>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Copiar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/deleteattachment.php b/src/dynamic/deleteattachment.php
new file mode 100644
index 0000000..66b6b1f
--- /dev/null
+++ b/src/dynamic/deleteattachment.php
@@ -0,0 +1,49 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::WORKER, security::METHOD_NOTFOUND);
+security::checkWorkerUIEnabled();
+
+if (!security::checkParams("GET", [
+  ["id", security::PARAM_ISINT],
+  ["name", security::PARAM_NEMPTY]
+])) {
+  security::notFound();
+}
+
+$id = (int)$_GET["id"];
+$name = $_GET["name"];
+
+$incident = incidents::get($id, true);
+if ($incident === false) security::notFound();
+
+if (!security::isAllowed(security::ADMIN)) incidents::checkIncidentIsFromPerson($incident["id"]);
+
+$attachments = incidents::getAttachmentsFromIncident($incident);
+
+if ($attachments === false || !count($attachments)) security::notFound();
+
+$flag = false;
+
+foreach ($attachments as $attachment) {
+  if ($attachment == $name) {
+    $flag = true;
+    ?>
+    <form action="dodeleteattachment.php" method="POST" autocomplete="off">
+      <input type="hidden" name="id" value="<?=(int)$id?>">
+      <?php visual::addContinueInput(); ?>
+      <input type="hidden" name="name" value="<?=security::htmlsafe($name)?>">
+      <h4 class="mdl-dialog__title">Eliminar archivo adjunto</h4>
+      <div class="mdl-dialog__content">
+        <p>¿Estás seguro que quieres eliminar el archivo adjunto <code><?=security::htmlsafe($name)?></code>? <span style="color:#EF5350;font-weight:bold;">Esta acción es irreversible</span></p>
+      </div>
+      <div class="mdl-dialog__actions">
+        <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Eliminar</button>
+        <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+      </div>
+    </form>
+    <?php
+    break;
+  }
+}
+
+if ($flag === false) security::notFound();
diff --git a/src/dynamic/deletecalendar.php b/src/dynamic/deletecalendar.php
new file mode 100644
index 0000000..83f19cc
--- /dev/null
+++ b/src/dynamic/deletecalendar.php
@@ -0,0 +1,26 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$id = (int)$_GET["id"];
+
+if (!calendars::exists($id)) {
+  security::notFound();
+}
+?>
+
+<form action="dodeletecalendar.php" method="POST" autocomplete="off">
+  <input type="hidden" name="id" value="<?=(int)$id?>">
+  <h4 class="mdl-dialog__title">Eliminar calendario</h4>
+  <div class="mdl-dialog__content">
+    <p>¿Estás seguro que quieres eliminar el calendario? <span style="color:#EF5350;font-weight:bold;">Esta acción es irreversible</span></p>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Eliminar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/deleteday.php b/src/dynamic/deleteday.php
new file mode 100644
index 0000000..d22206a
--- /dev/null
+++ b/src/dynamic/deleteday.php
@@ -0,0 +1,26 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$id = (int)$_GET["id"];
+
+if (!schedules::dayExists($id)) {
+  security::notFound();
+}
+?>
+
+<form action="dodeleteday.php" method="POST" autocomplete="off">
+  <input type="hidden" name="id" value="<?=(int)$id?>">
+  <h4 class="mdl-dialog__title">Eliminar horario</h4>
+  <div class="mdl-dialog__content">
+    <p>¿Estás seguro que quieres eliminar este horario diario? <span style="color:#EF5350;font-weight:bold;">Esta acción es irreversible</span></p>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Eliminar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/deleteincident.php b/src/dynamic/deleteincident.php
new file mode 100644
index 0000000..8ba455c
--- /dev/null
+++ b/src/dynamic/deleteincident.php
@@ -0,0 +1,31 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::WORKER, security::METHOD_NOTFOUND);
+security::checkWorkerUIEnabled();
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$id = (int)$_GET["id"];
+
+$incident = incidents::get($id);
+if ($incident === false) security::notFound();
+
+$isAdmin = security::isAdminView();
+$status = incidents::getStatus($incident);
+if (($isAdmin && !in_array($status, incidents::$canRemoveStates)) || (!$isAdmin && !in_array($status, incidents::$workerCanRemoveStates))) security::notFound();
+?>
+
+<form action="dodeleteincident.php" method="POST" autocomplete="off">
+  <input type="hidden" name="id" value="<?=(int)$id?>">
+  <?php visual::addContinueInput(); ?>
+  <h4 class="mdl-dialog__title">Eliminar incidencia</h4>
+  <div class="mdl-dialog__content">
+    <p>¿Estás seguro que quieres eliminar esta incidencia? <span style="color:#EF5350;font-weight:bold;">Esta acción es irreversible</span></p>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Eliminar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/deleteincidentsbulk.php b/src/dynamic/deleteincidentsbulk.php
new file mode 100644
index 0000000..b3a397c
--- /dev/null
+++ b/src/dynamic/deleteincidentsbulk.php
@@ -0,0 +1,33 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!security::checkParams("GET", [
+  ["incidents", security::PARAM_ISARRAY]
+])) {
+  security::notFound();
+}
+?>
+
+<style>
+#dynDialog, #dynDialog .mdl-dialog__content {
+  background: #FFCDD2;
+}
+</style>
+
+<form action="dodeleteincidentsbulk.php" method="POST" autocomplete="off">
+  <?php
+  foreach ($_GET["incidents"] as $incident) {
+    echo "<input type='hidden' name='incidents[]' value='".(int)$incident."'></li>";
+  }
+  ?>
+  <h4 class="mdl-dialog__title">Eliminar/invalidar incidencias</h4>
+  <div class="mdl-dialog__content">
+    <p>¿Estás seguro que quieres eliminar/invalidar estas incidencias? <span style="color:#EF5350;font-weight:bold;">Esta acción es irreversible</span></p>
+    <p>Dependiendo del estado de cada incidencia, esta se eliminará o se invalidará.</p>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Eliminar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/deleteschedule.php b/src/dynamic/deleteschedule.php
new file mode 100644
index 0000000..52c14f9
--- /dev/null
+++ b/src/dynamic/deleteschedule.php
@@ -0,0 +1,26 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$id = (int)$_GET["id"];
+
+if (!schedules::exists($id)) {
+  security::notFound();
+}
+?>
+
+<form action="dodeleteschedule.php" method="POST" autocomplete="off">
+  <input type="hidden" name="id" value="<?=(int)$id?>">
+  <h4 class="mdl-dialog__title">Eliminar horario</h4>
+  <div class="mdl-dialog__content">
+    <p>¿Estás seguro que quieres eliminar este horario? <span style="color:#EF5350;font-weight:bold;">Esta acción es irreversible</span></p>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Eliminar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/deletescheduletemplate.php b/src/dynamic/deletescheduletemplate.php
new file mode 100644
index 0000000..ad3a1ae
--- /dev/null
+++ b/src/dynamic/deletescheduletemplate.php
@@ -0,0 +1,26 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$id = (int)$_GET["id"];
+
+if (!schedules::templateExists($id)) {
+  security::notFound();
+}
+?>
+
+<form action="dodeletescheduletemplate.php" method="POST" autocomplete="off">
+  <input type="hidden" name="id" value="<?=(int)$id?>">
+  <h4 class="mdl-dialog__title">Eliminar plantilla</h4>
+  <div class="mdl-dialog__content">
+    <p>¿Estás seguro que quieres eliminar esta plantilla? <span style="color:#EF5350;font-weight:bold;">Esta acción es irreversible</span></p>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Eliminar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/deletesecuritykey.php b/src/dynamic/deletesecuritykey.php
new file mode 100644
index 0000000..8b62609
--- /dev/null
+++ b/src/dynamic/deletesecuritykey.php
@@ -0,0 +1,24 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::WORKER, security::METHOD_NOTFOUND);
+security::checkWorkerUIEnabled();
+secondFactor::checkAvailability();
+
+if (!isset($_GET["id"])) security::notFound();
+$id = (int)$_GET["id"];
+
+$s = secondFactor::getSecurityKeyById($id);
+if ($s === false || people::userData("id") != $s["person"]) security::notFound();
+?>
+
+<form action="dodeletesecuritykey.php" method="POST" autocomplete="off">
+  <input type="hidden" name="id" value="<?=(int)$s["id"]?>">
+  <h4 class="mdl-dialog__title">Eliminar llave de seguridad</h4>
+  <div class="mdl-dialog__content">
+    <p>¿Estás seguro que quieres eliminar la llave de seguridad <b><?=security::htmlsafe($s["name"])?></b>? <span style="color:#EF5350;font-weight:bold;">Una vez la elimines no tendrás la opción de escogerla como segundo factor.</span></p>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Eliminar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/deletetemplateday.php b/src/dynamic/deletetemplateday.php
new file mode 100644
index 0000000..63e0cb8
--- /dev/null
+++ b/src/dynamic/deletetemplateday.php
@@ -0,0 +1,26 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$id = (int)$_GET["id"];
+
+if (!schedules::templateDayExists($id)) {
+  security::notFound();
+}
+?>
+
+<form action="dodeletetemplateday.php" method="POST" autocomplete="off">
+  <input type="hidden" name="id" value="<?=(int)$id?>">
+  <h4 class="mdl-dialog__title">Eliminar horario</h4>
+  <div class="mdl-dialog__content">
+    <p>¿Estás seguro que quieres eliminar este horario de la plantilla? <span style="color:#EF5350;font-weight:bold;">Esta acción es irreversible</span></p>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Eliminar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/deleteworkhistoryitem.php b/src/dynamic/deleteworkhistoryitem.php
new file mode 100644
index 0000000..0dd5613
--- /dev/null
+++ b/src/dynamic/deleteworkhistoryitem.php
@@ -0,0 +1,44 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$id = (int)$_GET["id"];
+
+$item = workers::getWorkHistoryItem($id);
+if ($item === false) security::notFound();
+
+$isHidden = workers::isHidden($item["status"]);
+
+$worker = workers::get($item["worker"]);
+if ($worker === false) security::notFound();
+
+$helper = security::htmlsafe(strtolower(workers::affiliationStatusHelper($item["status"])));
+?>
+
+<dynscript>
+document.getElementById("cancel").addEventListener("click", e => {
+  e.preventDefault();
+  dynDialog.load("dynamic/workhistory.php?id="+parseInt(document.getElementById("cancel").getAttribute("data-worker-id")));
+});
+</dynscript>
+
+<form action="dodeleteworkhistoryitem.php" method="POST" autocomplete="off">
+  <input type="hidden" name="id" value="<?=(int)$id?>">
+  <h4 class="mdl-dialog__title">Eliminar registro de <?=security::htmlsafe($helper)?></h4>
+
+  <div class="mdl-dialog__content">
+    <p><b>Persona:</b> <?=security::htmlsafe($worker["name"])?><br>
+    <b>Empresa:</b> <?=security::htmlsafe($worker["companyname"])?><br>
+    <b>Día:</b> <?=security::htmlsafe(date("d/m/Y", $item["day"]))?></p>
+
+    <p>¿Estás seguro que quieres eliminar este registro de <?=security::htmlsafe($helper)?>? <span style="color:#EF5350;font-weight:bold;">Esta acción es irreversible</span></p>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Eliminar</button>
+    <button id="cancel" class="mdl-button mdl-js-button mdl-js-ripple-effect" data-worker-id="<?=(int)$worker["id"]?>">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/disablesecondfactor.php b/src/dynamic/disablesecondfactor.php
new file mode 100644
index 0000000..ad9913e
--- /dev/null
+++ b/src/dynamic/disablesecondfactor.php
@@ -0,0 +1,55 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::WORKER, security::METHOD_NOTFOUND);
+security::checkWorkerUIEnabled();
+secondFactor::checkAvailability();
+
+if (!isset($_GET["id"])) security::notFound();
+$id = (int)$_GET["id"];
+
+if (!secondFactor::isEnabled($id)) {
+  security::notFound();
+}
+
+if (!security::isAllowed(security::ADMIN) && $id != people::userData("id")) security::notFound();
+
+if ($id == people::userData("id")) {
+?>
+<style>
+#dynDialog {
+  max-width: 500px;
+  width: auto;
+}
+</style>
+<?php
+}
+?>
+
+<form action="dodisablesecondfactor.php" method="POST" autocomplete="off">
+  <input type="hidden" name="id" value="<?=(int)$id?>">
+  <h4 class="mdl-dialog__title">Desactivar la verificación en dos pasos</h4>
+  <div class="mdl-dialog__content">
+    <?php
+    if ($id != people::userData("id")) {
+      ?>
+      <p>¿Estás seguro que quieres desactivar la verificación en dos pasos para <b><?=security::htmlsafe(people::userData("name", $id))?></b>?</p>
+      <p>Esta acción solo debe tomarse cuando el trabajador no puede acceder a su cuenta, puesto que <span style="color:#EF5350;font-weight:bold;">esta acción solo la puede revertir el trabajador reactivando de nuevo la verificación en dos pasos</span></p>
+      <?php
+    } else {
+      ?>
+      <p>¿Estás seguro que quieres desactivar la verificación en dos pasos?</p>
+      <p>La verificación en 2 pasos ofrece seguridad extra a tu cuenta. Si inhabilitas la verificación en 2 pasos todas tus llaves de seguridad se desvincularán de esta cuenta y no te pediremos ningún código de verificación al iniciar sesión.</p>
+      <p>Si aun así sigues quiriendo desactivarla, introduce tu contraseña y haz clic en el botón Desactivar.</p>
+      <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+        <input class="mdl-textfield__input" type="password" name="password" id="password" data-required>
+        <label class="mdl-textfield__label" for="password">Contraseña actual</label>
+      </div>
+      <?php
+    }
+    ?>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Desactivar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/editcategory.php b/src/dynamic/editcategory.php
new file mode 100644
index 0000000..1057df1
--- /dev/null
+++ b/src/dynamic/editcategory.php
@@ -0,0 +1,53 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$c = categories::get($_GET["id"]);
+
+if ($c === false) {
+  security::notFound();
+}
+?>
+
+<form action="doeditcategory.php" method="POST" autocomplete="off">
+  <h4 class="mdl-dialog__title">Editar categoría</h4>
+  <div class="mdl-dialog__content">
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="text" name="id" id="edit_id" value="<?=security::htmlsafe($c['id'])?>" readonly="readonly" autocomplete="off">
+      <label class="mdl-textfield__label" for="edit_nombre">ID</label>
+    </div>
+    <br>
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="text" name="name" id="edit_name"  value="<?=security::htmlsafe($c['name'])?>" autocomplete="off" data-required>
+      <label class="mdl-textfield__label" for="edit_name">Nombre de la categoría</label>
+    </div>
+    <br>
+    <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+      <select name="parent" id="parent" class="mdlext-selectfield__select">
+        <option value="0"></option>
+        <?php
+        foreach (categories::getAll(false) as $category) {
+          if ($category["parent"] == 0 && $category["id"] != $c["id"]) {
+            echo '<option value="'.$category["id"].'"'.($c["parent"] == $category["id"] ? "selected" : "").'>'.$category["name"].'</option>';
+          }
+        }
+        ?>
+      </select>
+      <label for="parent" class="mdlext-selectfield__label">Categoría padre</label>
+    </div>
+    <br>
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <textarea class="mdl-textfield__input" name="emails" id="emails"><?=security::htmlsafe(categories::readableEmails($c["emails"]))?></textarea>
+      <label class="mdl-textfield__label" for="emails">Correos electrónicos de los responsables</label>
+    </div>
+    <span style="font-size: 12px;">Introduce los correos separados por comas.</span>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Confirmar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/editcompany.php b/src/dynamic/editcompany.php
new file mode 100644
index 0000000..1af315e
--- /dev/null
+++ b/src/dynamic/editcompany.php
@@ -0,0 +1,38 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$c = companies::get($_GET["id"]);
+
+if ($c === false) {
+  security::notFound();
+}
+?>
+
+<form action="doeditcompany.php" method="POST" autocomplete="off">
+  <h4 class="mdl-dialog__title">Editar empresa</h4>
+  <div class="mdl-dialog__content">
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="text" name="id" id="edit_id" value="<?=security::htmlsafe($c['id'])?>" readonly="readonly" autocomplete="off">
+      <label class="mdl-textfield__label" for="edit_nombre">ID</label>
+    </div>
+    <br>
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="text" name="name" id="edit_name"  value="<?=security::htmlsafe($c['name'])?>" autocomplete="off" data-required>
+      <label class="mdl-textfield__label" for="edit_name">Nombre de la empresa</label>
+    </div>
+    <br>
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="text" name="cif" id="edit_cif"  value="<?=security::htmlsafe($c['cif'])?>" autocomplete="off">
+      <label class="mdl-textfield__label" for="edit_cif">CIF (opcional)</label>
+    </div>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Confirmar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/editday.php b/src/dynamic/editday.php
new file mode 100644
index 0000000..3829b47
--- /dev/null
+++ b/src/dynamic/editday.php
@@ -0,0 +1,65 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$id = (int)$_GET["id"];
+
+$day = schedules::getDay($id);
+
+if ($day === false) {
+  security::notFound();
+}
+
+$empty = [];
+
+foreach (schedules::$allEvents as $date) {
+  $empty[$date] = (intervals::measure([$day["begins".$date], $day["ends".$date]]) == 0);
+}
+?>
+
+<form action="doeditdayschedule.php" method="POST" autocomplete="off">
+  <input type="hidden" name="id" value="<?=(int)$day["id"]?>">
+  <h4 class="mdl-dialog__title">Modificar horario</h4>
+  <div class="mdl-dialog__content">
+    <h5>Día</h5>
+    <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+      <select id="edit_day" class="mdlext-selectfield__select" disabled>
+        <?php
+        foreach (calendars::$days as $id => $tday) {
+          echo '<option value="'.(int)$id.'"'.($day["day"] == $id ? " selected" : "").'>'.security::htmlsafe($tday).'</option>';
+        }
+        ?>
+      </select>
+      <label for="edit_day" class="mdlext-selectfield__label">Día de la semana</label>
+    </div>
+    <br>
+    <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+      <select id="edit_type" class="mdlext-selectfield__select" disabled>
+        <?php
+        foreach (calendars::$types as $id => $type) {
+          if ($id == calendars::TYPE_FESTIU) continue;
+          echo '<option value="'.(int)$id.'"'.($day["typeday"] == $id ? " selected" : "").'>'.security::htmlsafe($type).'</option>';
+        }
+        ?>
+      </select>
+      <label for="edit_type" class="mdlext-selectfield__label">Tipo de día</label>
+    </div>
+
+    <h5>Jornada laboral</h5>
+    <p>De <input type="time" name="beginswork" <?=(!$empty["work"] ? " value='".schedules::sec2time($day["beginswork"])."'" : "")?> required> a <input type="time" name="endswork" <?=(!$empty["work"] ? " value='".schedules::sec2time($day["endswork"])."'" : "")?> required></p>
+
+    <h5>Desayuno</h5>
+    <p>De <input type="time" name="beginsbreakfast" <?=(!$empty["breakfast"] ? " value='".schedules::sec2time($day["beginsbreakfast"])."'" : "")?>> a <input type="time" name="endsbreakfast" <?=(!$empty["breakfast"] ? " value='".schedules::sec2time($day["endsbreakfast"])."'" : "")?>></p>
+
+    <h5>Comida</h5>
+    <p>De <input type="time" name="beginslunch" <?=(!$empty["lunch"] ? " value='".schedules::sec2time($day["beginslunch"])."'" : "")?>> a <input type="time" name="endslunch" <?=(!$empty["lunch"] ? " value='".schedules::sec2time($day["endslunch"])."'" : "")?>></p>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--primary">Modificar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/editincident.php b/src/dynamic/editincident.php
new file mode 100644
index 0000000..808070e
--- /dev/null
+++ b/src/dynamic/editincident.php
@@ -0,0 +1,68 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::WORKER, security::METHOD_NOTFOUND);
+security::checkWorkerUIEnabled();
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$id = (int)$_GET["id"];
+
+$incident = incidents::get($id, true);
+if ($incident === false) security::notFound();
+
+$isAdmin = security::isAdminView();
+$status = incidents::getStatus($incident);
+
+if (($isAdmin && in_array($status, incidents::$cannotEditStates)) || (!$isAdmin && !in_array($status, incidents::$workerCanEditStates))) security::notFound();
+if (!$isAdmin) incidents::checkIncidentIsFromPerson($incident["id"]);
+?>
+
+<dynscript>
+document.getElementById("edit_allday").addEventListener("change", e => {
+  var partialtime = document.getElementById("edit_partialtime");
+  if (e.target.checked) {
+    partialtime.classList.add("notvisible");
+  } else {
+    partialtime.classList.remove("notvisible");
+  }
+});
+</dynscript>
+
+<form action="doeditincident.php" method="POST" autocomplete="off">
+  <input type="hidden" name="id" value="<?=(int)$id?>">
+  <?php visual::addContinueInput(); ?>
+  <h4 class="mdl-dialog__title">Editar incidencia</h4>
+  <div class="mdl-dialog__content">
+    <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+      <select name="type" id="edit_type" class="mdlext-selectfield__select" data-required>
+        <option></option>
+        <?php
+        foreach (incidents::getTypesForm() as $i) {
+          echo '<option value="'.(int)$i["id"].'"'.($i["id"] == $incident["type"] ? " selected" : "").'>'.security::htmlsafe($i["name"]).'</option>';
+        }
+        ?>
+      </select>
+      <label for="edit_type" class="mdlext-selectfield__label">Tipo</label>
+    </div>
+
+    <h5>Afectación</h5>
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="date" name="day" id="edit_day" autocomplete="off" value="<?=security::htmlsafe(date("Y-m-d", $incident["day"]))?>" data-required>
+      <label class="mdl-textfield__label always-focused" for="edit_day">Día</label>
+    </div>
+    <br>
+    <p>
+      <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="edit_allday">
+        <input type="checkbox" id="edit_allday" name="allday" value="1" class="mdl-switch__input"<?=($incident["allday"] ? " checked" : "")?>>
+        <span class="mdl-switch__label">Día entero</span>
+      </label>
+    </p>
+    <div id="edit_partialtime"<?=($incident["allday"] ? ' class="notvisible"' : '')?>>De <input type="time" name="begins"<?=($incident["allday"] ? '' : ' value="'.schedules::sec2time($incident["begins"]).'"')?>> a <input type="time" name="ends"<?=($incident["allday"] ? '' : ' value="'.schedules::sec2time($incident["ends"]).'"')?>></div>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Editar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/editincidentcomment.php b/src/dynamic/editincidentcomment.php
new file mode 100644
index 0000000..8fd9dd6
--- /dev/null
+++ b/src/dynamic/editincidentcomment.php
@@ -0,0 +1,40 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::WORKER, security::METHOD_NOTFOUND);
+security::checkWorkerUIEnabled();
+
+$isAdmin = security::isAllowed(security::ADMIN);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$id = (int)$_GET["id"];
+
+$incident = incidents::get($id);
+if ($incident === false) security::notFound();
+
+if (!$isAdmin) incidents::checkIncidentIsFromPerson($incident["id"]);
+
+$status = incidents::getStatus($incident);
+$cantedit = (in_array($status, incidents::$cannotEditCommentsStates) || !$isAdmin);
+?>
+
+<form action="<?=(!$isAdmin ? "doeditworkerincidentcomment.php" : "doeditincidentcomment.php")?>" method="POST" autocomplete="off">
+  <input type="hidden" name="id" value="<?=(int)$incident["id"]?>">
+  <h4 class="mdl-dialog__title">Observaciones incidencia</h4>
+  <div class="mdl-dialog__content">
+    <h5>Observaciones</h5>
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <textarea class="mdl-textfield__input" name="details" id="details"<?=($cantedit ? " disabled" : "")?>><?=security::htmlsafe($incident["details"])?></textarea>
+      <label class="mdl-textfield__label" for="details">Observaciones (opcional)</label>
+    </div>
+
+    <h5>Observaciones del trabajador</h5>
+    <p><?=security::htmlsafe((!empty($incident["workerdetails"]) ? $incident["workerdetails"] : "-"))?></p>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--primary"<?=($cantedit ? " disabled" : "")?>>Modificar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/editincidenttype.php b/src/dynamic/editincidenttype.php
new file mode 100644
index 0000000..2901a4f
--- /dev/null
+++ b/src/dynamic/editincidenttype.php
@@ -0,0 +1,74 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$t = incidents::getType($_GET["id"]);
+
+if ($t === false) {
+  security::notFound();
+}
+?>
+
+<form action="doeditincidenttype.php" method="POST" autocomplete="off">
+  <h4 class="mdl-dialog__title">Edita tipo de incidencia</h4>
+  <div class="mdl-dialog__content">
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="text" name="id" id="edit_id" value="<?=security::htmlsafe($t['id'])?>" readonly="readonly" autocomplete="off">
+      <label class="mdl-textfield__label" for="edit_id">ID</label>
+    </div>
+    <br>
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="text" name="name" id="e_name" value="<?=security::htmlsafe($t['name'])?>" autocomplete="off" data-required>
+      <label class="mdl-textfield__label" for="e_name">Nombre del tipo de incidencia</label>
+    </div>
+    <p>
+      <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="e_present">
+        <input type="checkbox" id="e_present" name="present" value="1" class="mdl-switch__input" <?=($t["present"] ? " checked" : "")?>>
+        <span class="mdl-switch__label">Presente <i class="material-icons help" id="edit_present">help</i></span>
+      </label>
+      <div class="mdl-tooltip" for="edit_present">Márquese si el trabajador está físicamente presente en el espacio de trabajo durante la incidencia.</div>
+    </p>
+    <p>
+      <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="e_paid">
+        <input type="checkbox" id="e_paid" name="paid" value="1" class="mdl-switch__input" <?=($t["paid"] ? " checked" : "")?>>
+        <span class="mdl-switch__label">Remunerada <i class="material-icons help" id="edit_paid">help</i></span>
+      </label>
+      <div class="mdl-tooltip" for="edit_paid">Márquese si el trabajador es remunerado las horas que dura la incidencia.</div>
+    </p>
+    <p>
+      <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="e_workerfill">
+        <input type="checkbox" id="e_workerfill" name="workerfill" value="1" class="mdl-switch__input"<?=($t["workerfill"] ? " checked" : "")?>>
+        <span class="mdl-switch__label">Puede autorrellenarse <i class="material-icons help" id="edit_workerfill">help</i></span>
+      </label>
+    </p>
+    <div class="mdl-tooltip" for="edit_workerfill">Márquese si se permite que el trabajador pueda rellenar una incidencia de este tipo él mismo (con la posterior verificación por parte de un administrador).</div>
+    <p>
+      <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="e_notifies">
+        <input type="checkbox" id="e_notifies" name="notifies" value="1" class="mdl-switch__input"<?=($t["notifies"] ? " checked" : "")?>>
+        <span class="mdl-switch__label">Notifica <i class="material-icons help" id="edit_notifies">help</i></span>
+      </label>
+      <div class="mdl-tooltip" for="edit_notifies">Márquese si la introducción de una incidencia de este tipo notifica por correo electrónico a las personas especificadas en la categoría del trabajador.</div>
+    </p>
+    <p>
+      <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="e_autovalidates">
+        <input type="checkbox" id="e_autovalidates" name="autovalidates" value="1" class="mdl-switch__input"<?=($t["autovalidates"] ? " checked" : "")?>>
+        <span class="mdl-switch__label">Se autovalida <i class="material-icons help" id="edit_autovalidates">help</i></span>
+      </label>
+      <div class="mdl-tooltip" for="edit_autovalidates">Márquese si al introducir una incidencia de este tipo se quiere que se autovalide sin necesidad de ser validada posteriormente por el trabajador.</div>
+    </p>
+    <p>
+      <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="e_hidden">
+        <input type="checkbox" id="e_hidden" name="hidden" value="1" class="mdl-switch__input"<?=($t["hidden"] ? " checked" : "")?>>
+        <span class="mdl-switch__label">Oculto</span>
+      </label>
+    </p>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Confirmar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/editschedule.php b/src/dynamic/editschedule.php
new file mode 100644
index 0000000..c8d0b6a
--- /dev/null
+++ b/src/dynamic/editschedule.php
@@ -0,0 +1,34 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$s = schedules::get($_GET["id"]);
+
+if ($s === false) {
+  security::notFound();
+}
+?>
+
+<form action="doeditschedule.php" method="POST" autocomplete="off">
+  <input type="hidden" name="id" value="<?=(int)$s["id"]?>">
+  <h4 class="mdl-dialog__title">Editar horario</h4>
+  <div class="mdl-dialog__content">
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="date" name="begins" id="begins" autocomplete="off"  value="<?=security::htmlsafe(date("Y-m-d", $s["begins"]))?>" data-required>
+      <label class="mdl-textfield__label always-focused" for="begins">Fecha inicio de validez del horario</label>
+    </div>
+    <br>
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="date" name="ends" id="ends" autocomplete="off" value="<?=security::htmlsafe(date("Y-m-d", $s["ends"]))?>" data-required>
+      <label class="mdl-textfield__label always-focused" for="ends">Fecha fin de validez del horario</label>
+    </div>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Confirmar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/editscheduletemplate.php b/src/dynamic/editscheduletemplate.php
new file mode 100644
index 0000000..3c67d74
--- /dev/null
+++ b/src/dynamic/editscheduletemplate.php
@@ -0,0 +1,39 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$t = schedules::getTemplate($_GET["id"]);
+
+if ($t === false) {
+  security::notFound();
+}
+?>
+
+<form action="doeditscheduletemplate.php" method="POST" autocomplete="off">
+  <input type="hidden" name="id" value="<?=(int)$t["id"]?>">
+  <h4 class="mdl-dialog__title">Editar plantilla</h4>
+  <div class="mdl-dialog__content">
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="text" name="name" id="name" autocomplete="off" value="<?=security::htmlsafe($t["name"])?>" data-required>
+      <label class="mdl-textfield__label" for="name">Nombre de la plantilla</label>
+    </div>
+    <br>
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="date" name="begins" id="begins" autocomplete="off"  value="<?=security::htmlsafe(date("Y-m-d", $t["begins"]))?>" data-required>
+      <label class="mdl-textfield__label always-focused" for="begins">Fecha inicio de validez del horario</label>
+    </div>
+    <br>
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="date" name="ends" id="ends" autocomplete="off" value="<?=security::htmlsafe(date("Y-m-d", $t["ends"]))?>" data-required>
+      <label class="mdl-textfield__label always-focused" for="ends">Fecha fin de validez del horario</label>
+    </div>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Confirmar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/edittemplateday.php b/src/dynamic/edittemplateday.php
new file mode 100644
index 0000000..136ff1a
--- /dev/null
+++ b/src/dynamic/edittemplateday.php
@@ -0,0 +1,65 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$id = (int)$_GET["id"];
+
+$day = schedules::getTemplateDay($id);
+
+if ($day === false) {
+  security::notFound();
+}
+
+$empty = [];
+
+foreach (schedules::$allEvents as $date) {
+  $empty[$date] = (intervals::measure([$day["begins".$date], $day["ends".$date]]) == 0);
+}
+?>
+
+<form action="doeditdayscheduletemplate.php" method="POST" autocomplete="off">
+  <input type="hidden" name="id" value="<?=(int)$day["id"]?>">
+  <h4 class="mdl-dialog__title">Modificar horario</h4>
+  <div class="mdl-dialog__content">
+    <h5>Día</h5>
+    <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+      <select id="edit_day" class="mdlext-selectfield__select" disabled>
+        <?php
+        foreach (calendars::$days as $id => $tday) {
+          echo '<option value="'.(int)$id.'"'.($day["day"] == $id ? " selected" : "").'>'.security::htmlsafe($tday).'</option>';
+        }
+        ?>
+      </select>
+      <label for="edit_day" class="mdlext-selectfield__label">Día de la semana</label>
+    </div>
+    <br>
+    <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+      <select id="edit_type" class="mdlext-selectfield__select" disabled>
+        <?php
+        foreach (calendars::$types as $id => $type) {
+          if ($id == calendars::TYPE_FESTIU) continue;
+          echo '<option value="'.(int)$id.'"'.($day["typeday"] == $id ? " selected" : "").'>'.security::htmlsafe($type).'</option>';
+        }
+        ?>
+      </select>
+      <label for="edit_type" class="mdlext-selectfield__label">Tipo de día</label>
+    </div>
+
+    <h5>Jornada laboral</h5>
+    <p>De <input type="time" name="beginswork" <?=(!$empty["work"] ? " value='".schedules::sec2time($day["beginswork"])."'" : "")?> required> a <input type="time" name="endswork" <?=(!$empty["work"] ? " value='".schedules::sec2time($day["endswork"])."'" : "")?> required></p>
+
+    <h5>Desayuno</h5>
+    <p>De <input type="time" name="beginsbreakfast" <?=(!$empty["breakfast"] ? " value='".schedules::sec2time($day["beginsbreakfast"])."'" : "")?>> a <input type="time" name="endsbreakfast" <?=(!$empty["breakfast"] ? " value='".schedules::sec2time($day["endsbreakfast"])."'" : "")?>></p>
+
+    <h5>Comida</h5>
+    <p>De <input type="time" name="beginslunch" <?=(!$empty["lunch"] ? " value='".schedules::sec2time($day["beginslunch"])."'" : "")?>> a <input type="time" name="endslunch" <?=(!$empty["lunch"] ? " value='".schedules::sec2time($day["endslunch"])."'" : "")?>></p>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--primary">Modificar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/edituser.php b/src/dynamic/edituser.php
new file mode 100644
index 0000000..36fb51c
--- /dev/null
+++ b/src/dynamic/edituser.php
@@ -0,0 +1,79 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$p = people::get($_GET["id"]);
+
+if ($p === false) {
+  security::notFound();
+}
+?>
+
+<form action="doedituser.php" method="POST" autocomplete="off">
+  <h4 class="mdl-dialog__title">Edita persona</h4>
+  <div class="mdl-dialog__content">
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="text" name="id" id="edit_id" value="<?=security::htmlsafe($p['id'])?>" readonly="readonly" autocomplete="off">
+      <label class="mdl-textfield__label" for="edit_nombre">ID</label>
+    </div>
+    <br>
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="text" name="username" id="edit_username" value="<?=security::htmlsafe($p['username'])?>" autocomplete="off" data-required>
+      <label class="mdl-textfield__label" for="edit_username">Nombre de usuario</label>
+    </div>
+    <br>
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="text" name="name" id="edit_name"  value="<?=security::htmlsafe($p['name'])?>" autocomplete="off" data-required>
+      <label class="mdl-textfield__label" for="edit_name">Nombre</label>
+    </div>
+    <br>
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="text" name="dni" id="edit_dni"  value="<?=security::htmlsafe($p['dni'])?>" autocomplete="off">
+      <label class="mdl-textfield__label" for="edit_dni">DNI (opcional)</label>
+    </div>
+    <br>
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="email" name="email" id="edit_email"  value="<?=security::htmlsafe($p['email'])?>" autocomplete="off">
+      <label class="mdl-textfield__label" for="edit_email">Correo electrónico (opcional)</label>
+    </div>
+    <br>
+    <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+      <select name="category" id="edit_category" class="mdlext-selectfield__select">
+        <option value="-1"></option>
+        <?php
+        $categories = categories::getAll();
+        foreach ($categories as $id => $category) {
+          $selected = ($id == $p["categoryid"] ? " selected" : "");
+          echo '<option value="'.(int)$id.'"'.$selected.'>'.security::htmlsafe($category).'</option>';
+        }
+        ?>
+      </select>
+      <label for="edit_category" class="mdlext-selectfield__label">Categoría (opcional)</label>
+    </div>
+    <br>
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="password" name="password" id="edit_password" autocomplete="off">
+      <label class="mdl-textfield__label" for="edit_password">Contraseña</label>
+    </div>
+    <p><?=security::htmlsafe(security::$passwordHelperText)?></p>
+    <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+      <select name="type" id="edit_type" class="mdlext-selectfield__select" data-required>
+        <?php
+        foreach (security::$types as $i => $type) {
+          $selected = ($i == $p["type"] ? " selected" : "");
+          echo '<option value="'.(int)$i.'"'.$selected.(security::isAllowed($i) ? "" : " disabled").'>'.security::htmlsafe($type).'</option>';
+        }
+        ?>
+      </select>
+      <label for="edit_type" class="mdlext-selectfield__label">Tipo</label>
+    </div>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Confirmar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/editworkhistoryitem.php b/src/dynamic/editworkhistoryitem.php
new file mode 100644
index 0000000..14fd04c
--- /dev/null
+++ b/src/dynamic/editworkhistoryitem.php
@@ -0,0 +1,56 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$id = (int)$_GET["id"];
+
+$item = workers::getWorkHistoryItem($id);
+if ($item === false) security::notFound();
+
+$isHidden = workers::isHidden($item["status"]);
+
+$worker = workers::get($item["worker"]);
+if ($worker === false) security::notFound();
+?>
+
+<dynscript>
+document.getElementById("cancel").addEventListener("click", e => {
+  e.preventDefault();
+  dynDialog.load("dynamic/workhistory.php?id="+parseInt(document.getElementById("cancel").getAttribute("data-worker-id")));
+});
+</dynscript>
+
+<form action="doeditworkhistoryitem.php" method="POST" autocomplete="off">
+  <input type="hidden" name="id" value="<?=(int)$id?>">
+  <h4 class="mdl-dialog__title">Editar <?=security::htmlsafe(strtolower(workers::affiliationStatusHelper($item["status"])))?></h4>
+  <div class="mdl-dialog__content">
+    <p><b>Persona:</b> <?=security::htmlsafe($worker["name"])?><br>
+    <b>Empresa:</b> <?=security::htmlsafe($worker["companyname"])?></p>
+
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="date" name="day" id="day" autocomplete="off" data-required value="<?=security::htmlsafe(date("Y-m-d", $item["day"]))?>">
+      <label class="mdl-textfield__label" for="day">Fecha</label>
+    </div>
+    <br>
+    <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+      <select name="status" id="status" class="mdlext-selectfield__select" data-required>
+        <option></option>
+        <?php
+        foreach (workers::$affiliationStatusesManual as $status) {
+          $currentIsHidden = workers::isHidden($status);
+          echo '<option value="'.(int)$status.'"'.((($isHidden && $currentIsHidden) || (!$isHidden && !$currentIsHidden)) ? ' selected' : '').'>'.security::htmlsafe(workers::affiliationStatusHelper($status)).'</option>';
+        }
+        ?>
+      </select>
+      <label for="status" class="mdlext-selectfield__label">Tipo</label>
+    </div>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Editar</button>
+    <button id="cancel" class="mdl-button mdl-js-button mdl-js-ripple-effect" data-worker-id="<?=(int)$worker["id"]?>">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/enablesecondfactor.php b/src/dynamic/enablesecondfactor.php
new file mode 100644
index 0000000..633d36e
--- /dev/null
+++ b/src/dynamic/enablesecondfactor.php
@@ -0,0 +1,102 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::WORKER, security::METHOD_NOTFOUND);
+security::checkWorkerUIEnabled();
+secondFactor::checkAvailability();
+
+if (secondFactor::isEnabled()) {
+  security::notFound();
+}
+
+$secret = secondFactor::generateSecret();
+$url = "otpauth://totp/".str_replace("+", "%20", urlencode($conf["appName"])).":".urlencode(people::userData('username'))."?secret=".urlencode($secret)."&issuer=".str_replace("+", "%20", urlencode($conf["appName"]));
+?>
+
+<style>
+#dynDialog {
+  max-width: 500px;
+  width: auto;
+}
+
+.step {
+  padding: 10px 0;
+  border-bottom: 1px solid #ebebeb;
+}
+
+.step .number {
+  display: inline-block;
+  vertical-align: middle;
+  font-family: "Arial", sans-serif;
+  font-size: 36px;
+  font-weight: bold;
+  color: green;
+  margin: 0;
+  margin-right: 15px;
+  padding: 0;
+  line-height: normal;
+}
+
+.step .text {
+  display: inline-block;
+  vertical-align: middle;
+  margin: 0;
+  padding: 0;
+  width: Calc(100% - 40px);
+}
+
+.step .icon_container {
+  float: right;
+  height: 24px;
+  padding-top: 9px;
+  padding-right: 9px;
+}
+
+#qrcode {
+  margin: 8px 0;
+}
+
+#qrcode img, #qrcode canvas {
+  margin: auto;
+}
+</style>
+
+<dynscript>
+new QRCode(document.getElementById("qrcode"), {
+  text: "<?=security::htmlsafe($url)?>",
+  width: 200,
+	height: 200
+});
+</dynscript>
+
+<form action="doenablesecondfactor.php" method="POST" autocomplete="off">
+  <input type="hidden" name="secret" value="<?=security::htmlsafe($secret)?>">
+  <h4 class="mdl-dialog__title">Activa la verificación en dos pasos</h4>
+  <div class="mdl-dialog__content">
+    <p>Para activar la verificación en dos pasos, sigue los siguientes pasos:</p>
+
+    <div class="step">
+      <div class="number">1</div>
+      <div class="text"><b>Instala la aplicación Google Authenticator en tu <a href="http://appstore.com/googleauthenticator" target="_blank" rel="noopener noreferrer">iPhone</a> o <a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" target="_blank" rel="noopener noreferrer">Android</a>.</b><br>También puedes usar otra aplicación si lo prefieres.</div>
+    </div>
+    <div class="step">
+      <div class="number">2</div><div class="text"><b>Configura tu cuenta en la app Google Authenticator escaneando el siguiente código QR:</b></div>
+    </div>
+
+    <div id="qrcode"></div>
+
+    <div class="step" style="border-top: 1px solid #ebebeb;">
+        <div class="number">3</div><div class="text"><b>¿No puedes escanear el código QR? Introduce manualmente la siguiente clave secreta:</b><br><?=security::htmlsafe(secondFactorView::renderSecret($secret))?></div>
+    </div>
+    <div class="step" style="margin-bottom: 5px;">
+        <div class="number">4</div><div class="text"><b>Introduce el código de verificación de 6 dígitos:</b></div>
+    </div>
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="text" name="code" id="code" autocomplete="off" pattern="[0-9]{6}" data-required>
+      <label class="mdl-textfield__label" for="code">Código de verificación</label>
+    </div>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--primary">Activar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/exportcalendar.php b/src/dynamic/exportcalendar.php
new file mode 100644
index 0000000..47b88fa
--- /dev/null
+++ b/src/dynamic/exportcalendar.php
@@ -0,0 +1,59 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$id = (int)$_GET["id"];
+
+$c = calendars::get($id);
+
+if ($c === false) {
+  security::notFound();
+}
+
+$details = json_decode($c["details"], true);
+$export = array(
+  "begins" => $c["begins"],
+  "ends" => $c["ends"],
+  "calendar" => $details
+);
+?>
+
+<style>
+textarea.code {
+  width: 100%;
+  height: 100px;
+}
+</style>
+
+<dynscript>
+document.querySelector("textarea.code").select();
+
+document.getElementById("copy").addEventListener("click", _ => {
+  navigator.clipboard.writeText(document.querySelector("textarea.code").value).then(_ => {
+    document.querySelector(".mdl-js-snackbar").MaterialSnackbar.showSnackbar({
+      message: "Se ha copiado el texto correctamente.",
+      timeout: 5000
+    });
+  }).catch(error => {
+    document.querySelector(".mdl-js-snackbar").MaterialSnackbar.showSnackbar({
+      message: "Ha ocurrido un error copiando el texto. Por favor, cópialo manualmente.",
+      timeout: 5000
+    });
+    console.error(error);
+  });
+});
+</dynscript>
+
+<h4 class="mdl-dialog__title">Exportar calendario</h4>
+<div class="mdl-dialog__content">
+  <p>Este es el código que contiene toda la información del calendario y que puedes usar de plantilla más tarde:</p>
+  <textarea class="code" readonly><?=security::htmlsafe(json_encode($export))?></textarea>
+</div>
+<div class="mdl-dialog__actions">
+  <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent cancel">Cerrar</button>
+  <button id="copy" class="mdl-button mdl-js-button mdl-js-ripple-effect">Copiar</button>
+</div>
diff --git a/src/dynamic/incidentattachments.php b/src/dynamic/incidentattachments.php
new file mode 100644
index 0000000..2402688
--- /dev/null
+++ b/src/dynamic/incidentattachments.php
@@ -0,0 +1,107 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::WORKER, security::METHOD_NOTFOUND);
+security::checkWorkerUIEnabled();
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$id = (int)$_GET["id"];
+
+$incident = incidents::get($id, true);
+if ($incident === false) security::notFound();
+
+$isAdmin = security::isAllowed(security::ADMIN);
+$status = incidents::getStatus($incident);
+
+$cantedit = in_array($status, incidents::$cannotEditCommentsStates);
+
+if (!$isAdmin) incidents::checkIncidentIsFromPerson($incident["id"]);
+?>
+
+<dynscript>
+document.querySelectorAll(".deleteattachment").forEach(el => {
+  el.addEventListener("click", e => {
+    dynDialog.load("dynamic/deleteattachment.php?id="+el.getAttribute("data-id")+"&name="+el.getAttribute("data-name")<?=(isset($_GET["continue"]) ? '+"&continue='.security::htmlsafe(urlencode($_GET["continue"])).'"' : '')?>);
+  });
+});
+</dynscript>
+
+<style>
+#dynDialog {
+  max-width: 380px;
+  width: auto;
+}
+
+.addAttachmentForm {
+  display: flex;
+  align-items: center;
+}
+
+.addAttachmentForm input[type="file"] {
+  width: 100%;
+  height: min-content;
+}
+
+.addAttachmentForm button {
+  min-width: min-content;
+}
+
+.attachmentDescription {
+  margin-top: 16px;
+}
+
+.attachmentDescription code {
+  font-size: 12px;
+}
+</style>
+
+<h4 class="mdl-dialog__title">Archivos adjuntos</h4>
+<div class="mdl-dialog__content">
+  <?php
+  $attachments = incidents::getAttachmentsFromIncident($incident);
+
+  if ($attachments === false) {
+    echo "<p>Ha ocurrido un problema cargando los archivos adjuntos.</p>";
+  } elseif (!count($attachments)) {
+    echo "<p>No hay ningún archivo adjunto</p>";
+  } else {
+    echo '<ul class="mdl-list">';
+    foreach ($attachments as $attachment) {
+      $extension = files::getFileExtension($attachment);
+      $icon = files::$mimeTypesIcons[$extension] ?? "broken_image";
+      $title = files::$readableMimeTypes[$extension] ?? "Documento desconocido";
+      echo '<li class="mdl-list__item">
+        <span class="mdl-list__item-primary-content">
+          <i class="material-icons mdl-list__item-icon">'.security::htmlsafe($icon).'</i>
+          '.security::htmlsafe($title).'
+        </span>
+        <a href="incidentattachment.php?id='.(int)$incident["id"].'&name='.security::htmlsafe($attachment).'" target="_blank" class="mdl-list__item-secondar-action mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect">
+          <i class="material-icons">open_in_new</i>
+        </a>'.
+        ($cantedit ? '' : '<button class="mdl-list__item-secondar-action mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect deleteattachment" data-id="'.(int)$id.'" data-name="'.security::htmlsafe($attachment).'">
+          <i class="material-icons">delete</i>
+        </button>').'
+      </li>';
+    }
+    echo "</ul>";
+  }
+
+  if (!$cantedit) {
+    ?>
+    <h5>Añade un archivo adjunto</h5>
+    <form action="doaddincidentattachment.php" method="POST" enctype="multipart/form-data" class="addAttachmentForm">
+      <input type="hidden" name="id" value="<?=(int)$incident["id"]?>">
+      <?php visual::addContinueInput(); ?>
+      <input type="file" name="file" accept="<?=security::htmlsafe(files::getAcceptAttribute())?>" required>
+      <button class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--raised">Subir</button>
+    </form>
+    <div class="attachmentDescription">Se aceptan archivos de hasta <?=security::htmlsafe(files::READABLE_MAX_SIZE)?> con los siguientes formatos: <code><?=security::htmlsafe(files::getAcceptAttribute(true))?></code></div>
+    <?php
+  }
+  ?>
+</div>
+<div class="mdl-dialog__actions">
+  <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Cerrar</button>
+</div>
diff --git a/src/dynamic/invalidateincident.php b/src/dynamic/invalidateincident.php
new file mode 100644
index 0000000..ceeeac3
--- /dev/null
+++ b/src/dynamic/invalidateincident.php
@@ -0,0 +1,29 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$id = (int)$_GET["id"];
+
+$incident = incidents::get($id);
+if ($incident === false) security::notFound();
+
+$status = incidents::getStatus($incident);
+if (!in_array($status, incidents::$canInvalidateStates)) security::notFound();
+?>
+
+<form action="doinvalidateincident.php" method="POST" autocomplete="off">
+  <input type="hidden" name="id" value="<?=(int)$id?>">
+  <?php visual::addContinueInput(); ?>
+  <h4 class="mdl-dialog__title">Invalidar incidencia</h4>
+  <div class="mdl-dialog__content">
+    <p>¿Estás seguro que quieres invalidar esta incidencia? <span style="color:#EF5350;font-weight:bold;">Esta acción es irreversible</span></p>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Invalidar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/invalidaterecord.php b/src/dynamic/invalidaterecord.php
new file mode 100644
index 0000000..2dc0dfc
--- /dev/null
+++ b/src/dynamic/invalidaterecord.php
@@ -0,0 +1,29 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::WORKER, security::METHOD_NOTFOUND);
+security::checkWorkerUIEnabled();
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$id = (int)$_GET["id"];
+
+$record = registry::get($id);
+if ($record === false || $record["invalidated"] != 0) security::notFound();
+
+$isAdmin = security::isAllowed(security::ADMIN);
+if (!$isAdmin) registry::checkRecordIsFromPerson($record["id"]);
+?>
+
+<form action="doinvalidaterecord.php" method="POST" autocomplete="off">
+  <input type="hidden" name="id" value="<?=(int)$id?>">
+  <h4 class="mdl-dialog__title">Invalidar elemento del registro</h4>
+  <div class="mdl-dialog__content">
+    <p>¿Estás seguro que quieres eliminar este elemento del registro? <span style="color:#EF5350;font-weight:bold;">Esta acción es irreversible</span></p>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Invalidar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/log.php b/src/dynamic/log.php
new file mode 100644
index 0000000..1c10dc5
--- /dev/null
+++ b/src/dynamic/log.php
@@ -0,0 +1,59 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$id = (int)$_GET["id"];
+
+$log = registry::getLog($id);
+if ($log === false) security::notFound();
+?>
+
+<style>
+#dynDialog {
+  max-width: 500px;
+  width: auto;
+}
+
+.log {
+  white-space: pre-wrap;
+}
+</style>
+
+<h4 class="mdl-dialog__title">
+  Log
+  <?php
+  if ($log["warningpos"] > 0) {
+    visual::addTooltip("warning", "El log contiene mensajes de advertencia");
+    ?>
+    <i class="material-icons mdl-color-text--orange help" id="warning">warning</i>
+    <?php
+  }
+
+  if ($log["errorpos"] > 0) {
+    visual::addTooltip("error", "El log contiene mensajes de error");
+    ?>
+    <i class="material-icons mdl-color-text--red help" id="error">error</i>
+    <?php
+  }
+
+  if ($log["fatalerrorpos"] > 0) {
+    visual::addTooltip("fatalerror", "El log contiene errores fatales");
+    ?>
+    <i class="material-icons mdl-color-text--red help-900" id="fatalerror">error</i>
+    <?php
+  }
+  ?>
+</h4>
+<div class="mdl-dialog__content">
+  <pre class="log"><?=registry::beautifyLog(security::htmlsafe($log["logdetails"]))?></pre>
+</div>
+<div class="mdl-dialog__actions">
+  <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--primary">Cerrar</button>
+</div>
+<?php
+visual::renderTooltips();
+?>
diff --git a/src/dynamic/sethelpresource.php b/src/dynamic/sethelpresource.php
new file mode 100644
index 0000000..2171dde
--- /dev/null
+++ b/src/dynamic/sethelpresource.php
@@ -0,0 +1,31 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::HYPERADMIN, security::METHOD_NOTFOUND);
+
+if (!security::checkParams("GET", [
+  ["place", security::PARAM_ISINT]
+])) {
+  security::notFound();
+}
+
+$place = $_GET["place"];
+if (!in_array($place, help::$places)) security::notFound();
+
+$url = help::get($place);
+?>
+
+<form action="dosethelpresource.php" method="POST" autocomplete="off">
+  <input type="hidden" name="place" value="<?=(int)$place?>">
+  <h4 class="mdl-dialog__title">Enlace de ayuda</h4>
+  <div class="mdl-dialog__content">
+    <p><b>Lugar:</b> <?=security::htmlsafe(help::$placesName[$place] ?? "undefined")?></b></p>
+    <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+      <input class="mdl-textfield__input" type="url" name="url" id="url" autocomplete="off"<?=($url !== false ? ' value="'.security::htmlsafe($url).'"' : '')?>>
+      <label class="mdl-textfield__label" for="url">URL</label>
+    </div>
+  </div>
+  <div class="mdl-dialog__actions">
+    <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--primary">Configurar</button>
+    <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+  </div>
+</form>
diff --git a/src/dynamic/user.php b/src/dynamic/user.php
new file mode 100644
index 0000000..d0aec1d
--- /dev/null
+++ b/src/dynamic/user.php
@@ -0,0 +1,59 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$p = people::get($_GET["id"], false);
+
+if ($p === false) {
+  security::notFound();
+}
+
+$companies = companies::getAll();
+$pcompanies = [];
+
+foreach($p["companies"] as $company) {
+  $pcompanies[] = $companies[$company];
+}
+
+$secondFactor = secondFactor::isEnabled($p["id"]);
+
+if ($secondFactor) {
+?>
+<dynscript>
+document.querySelector(".disable-second-factor").addEventListener("click", e => {
+  dynDialog.load("dynamic/disablesecondfactor.php?id=<?=(int)$p["id"]?>");
+});
+</dynscript>
+<?php
+}
+?>
+
+<style>
+#dynDialog {
+  max-width: 380px;
+  width: auto;
+}
+</style>
+
+<h4 class="mdl-dialog__title"><?=security::htmlsafe($p["name"])?></h4>
+<ul>
+  <li><b>Nombre de usuario:</b> <?=security::htmlsafe($p["username"])?></li>
+  <li><b>DNI:</b> <?=(!empty($p["dni"]) ? security::htmlsafe($p["dni"]) : "-")?></li>
+  <li><b>Correo electrónico:</b> <?=(!empty($p["email"]) ? "<a href=\"mailto:".security::htmlsafe(rawurlencode($p["email"]))."\" target=\"_blank\">".security::htmlsafe($p["email"])."</a>" : "-")?>
+  <li><b>Categoría:</b> <?=($p["categoryid"] == -1 ? "-" : security::htmlsafe($p["category"]))?></li>
+  <li><b>Dada de baja:</b> <?=($p["baixa"] == 1 ? visual::YES : "No")?></li>
+  <li><b>Empresas:</b> <?=security::htmlsafe((count($p["companies"]) ? implode(", ", $pcompanies) : "-"))?></li>
+  <li><b>Tipo de usuario:</b> <?=security::htmlsafe(security::$types[$p["type"]])?></li>
+  <?php if (secondFactor::isAvailable()) { ?><li><b>Verificación en dos pasos:</b> <?=($secondFactor ? 'activada <button class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent disable-second-factor">Desactivar</button>' : 'desactivada')?></li><?php } ?>
+</ul>
+
+<div class="mdl-dialog__actions">
+  <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cerrar</button>
+  <a href="userregistry.php?id=<?=(int)$p["id"]?>" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect"><i class="material-icons">list</i><span class="mdl-ripple"></span></a>
+  <a href="userincidents.php?id=<?=(int)$p["id"]?>" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect"><i class="material-icons">assignment_late</i><span class="mdl-ripple"></span></a>
+  <a href="workerschedule.php?id=<?=(int)$p["id"]?>" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect"><i class="material-icons">timelapse</i><span class="mdl-ripple"></span></a>
+</div>
diff --git a/src/dynamic/workhistory.php b/src/dynamic/workhistory.php
new file mode 100644
index 0000000..4c74000
--- /dev/null
+++ b/src/dynamic/workhistory.php
@@ -0,0 +1,91 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+if (!isset($_GET["id"])) {
+  security::notFound();
+}
+
+$id = (int)$_GET["id"];
+
+$worker = workers::get($id);
+if ($worker === false) security::notFound();
+?>
+
+<dynscript>
+document.getElementById("additem").addEventListener("click", e => {
+  dynDialog.load("dynamic/addworkhistoryitem.php?id="+parseInt(document.getElementById("additem").getAttribute("worker-id")));
+});
+
+document.querySelectorAll(".edititem").forEach(el => {
+  el.addEventListener("click", e => {
+    dynDialog.load("dynamic/editworkhistoryitem.php?id="+parseInt(el.getAttribute("data-id")));
+  });
+});
+
+document.querySelectorAll(".deleteitem").forEach(el => {
+  el.addEventListener("click", e => {
+    dynDialog.load("dynamic/deleteworkhistoryitem.php?id="+parseInt(el.getAttribute("data-id")));
+  });
+});
+</dynscript>
+
+<style>
+#dynDialog {
+  max-width: 380px;
+  width: auto;
+}
+
+#dynDialog .mdl-list {
+  margin-top: 0;
+  padding-top: 0;
+}
+
+.float-right {
+  float: right;
+}
+</style>
+
+<h4 class="mdl-dialog__title">Historial de altas y bajas</h4>
+<div class="mdl-dialog__content">
+  <p><b>Persona:</b> <?=security::htmlsafe($worker["name"])?><br>
+  <b>Empresa:</b> <?=security::htmlsafe($worker["companyname"])?></p>
+
+  <div class="float-right"><button id="additem" class="mdl-button mdl-js-button mdl-js-ripple-effect" worker-id="<?=(int)$worker["id"]?>"><i class="material-icons">add</i> Añadir alta/baja</button></div>
+  <div style="clear: both;"></div>
+
+  <?php
+  $items = workers::getWorkHistory($id);
+
+  if ($items === false) {
+    echo "<p>Ha ocurrido un problema cargando los elementos del historial.</p>";
+  } elseif (!count($items)) {
+    echo "<p>No hay ningún elmento en el historial, así que el aplicativo está considerando que el trabajador está de baja.</p>";
+  } else {
+    echo '<ul class="mdl-list">';
+    foreach ($items as $item) {
+      $icon = security::htmlsafe(workers::affiliationStatusIcon($item["status"]) ?? "indeterminate_check_box");
+      $helper = workers::affiliationStatusHelper($item["status"]);
+      $day = date("d/m/Y", $item["day"]);
+      $isAutomatic = workers::isAutomaticAffiliation($item["status"]);
+      echo '<li class="mdl-list__item '.($isAutomatic ? 'mdl-list__item--two-line' : '').'">
+        <span class="mdl-list__item-primary-content">
+          <i class="material-icons mdl-list__item-icon">'.security::htmlsafe($icon).'</i>
+          <span>'.security::htmlsafe($helper).' ('.security::htmlsafe($day).')</span>
+          '.($isAutomatic ? '<span class="mdl-list__item-sub-title">Registro automático</span>' : '').'
+        </span>
+        <button class="mdl-list__item-secondar-action mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect edititem" data-id="'.(int)$item["id"].'">
+          <i class="material-icons">edit</i>
+        </button>
+        <button class="mdl-list__item-secondar-action mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect deleteitem" data-id="'.(int)$item["id"].'">
+          <i class="material-icons">delete</i>
+        </button>
+      </li>';
+    }
+    echo "</ul>";
+  }
+  ?>
+</div>
+<div class="mdl-dialog__actions">
+  <button data-dyndialog-close class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Cerrar</button>
+</div>
diff --git a/src/editcalendar.php b/src/editcalendar.php
new file mode 100644
index 0000000..ac0f8ca
--- /dev/null
+++ b/src/editcalendar.php
@@ -0,0 +1,78 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+$mdHeaderRowBefore = visual::backBtn("calendars.php");
+
+if (!security::checkParams("GET", [
+  ["id", security::PARAM_NEMPTY]
+])) {
+  security::go("calendars.php");
+}
+
+$id = (int)$_GET["id"];
+
+$c = calendars::get($id);
+
+if ($c === false) {
+  security::go("calendars.php");
+}
+
+$details = json_decode($c["details"], true);
+
+if (json_last_error() !== JSON_ERROR_NONE) {
+  security::go("calendars.php?msg=unexpected");
+}
+
+$viewOnly = (isset($_GET["view"]) && $_GET["view"] == 1);
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+  <link rel="stylesheet" href="css/calendar.css">
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Calendario de &ldquo;<?=security::htmlsafe($c["categoryname"])?>&rdquo;</h2>
+          <?php
+          $current = new DateTime();
+          $current->setTimestamp($c["begins"]);
+          $ends = new DateTime();
+          $ends->setTimestamp($c["ends"]);
+          ?>
+          <form action="doeditcalendar.php" method="POST">
+            <input type="hidden" name="id" value="<?=(int)$id?>">
+            <?php
+            calendarsView::renderCalendar($current, $ends, function ($timestamp, $id, $dow, $dom, $extra) {
+              return ($extra[$timestamp] == $id);
+            }, $viewOnly, $details);
+
+            if (!$viewOnly)  {
+              ?>
+              <button class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent">Modificar</button>
+              <?php
+            }
+            ?>
+          </form>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <script src="js/calendar.js"></script>
+
+  <?php
+  visual::smartSnackbar([
+    ["inverted", "La fecha de inicio debe ser anterior a la fecha de fin."],
+    ["overlap", "El calendario que intentabas añadir se solapa con uno ya existente."]
+  ]);
+  ?>
+</body>
+</html>
diff --git a/src/export.php b/src/export.php
new file mode 100644
index 0000000..ba178ee
--- /dev/null
+++ b/src/export.php
@@ -0,0 +1,193 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+$companies = companies::getAll();
+
+$date = new DateTime();
+$interval = new DateInterval("P1D");
+$date->sub($interval);
+$yesterday = date("Y-m-d", $date->getTimestamp());
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+
+  <style>
+  .categories-select {
+    float: right;
+    border-left: 1px solid #ccc;
+    padding: 0 32px 16px 16px;
+    user-select: none;
+  }
+
+  .categories-select .select-all {
+    color: blue;
+    text-decoration: underline;
+    cursor: pointer;
+  }
+
+  @media (max-width: 500px) {
+    .categories-select {
+      float: none;
+      border-left: none;
+      border-top: 1px solid #ddd;
+      border-bottom: 1px solid #ddd;
+      padding: 0 0 16px 0;
+      margin-bottom: 16px;
+    }
+  }
+  </style>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Exportar registro</h2>
+          <p>Aquí puedes configurar cómo quieres exportar los datos del registro:</p>
+          <form action="doexport.php" method="GET">
+            <h5>Periodo</h5>
+            <p>Del <input type="date" name="begins" max="<?=security::htmlsafe($yesterday)?>" required> al <input type="date" name="ends" max="<?=security::htmlsafe($yesterday)?>" required></p>
+
+            <h5>Empresas</h5>
+            <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+              <div id="companies" class="mdlext-selectfield__select mdl-custom-selectfield__select" tabindex="0">-</div>
+              <ul class="mdl-menu mdl-menu--bottom mdl-js-menu mdl-custom-multiselect mdl-custom-multiselect-js" for="companies">
+                <?php
+                foreach (companies::getAll() as $id => $company) {
+                  if ($id == calendars::TYPE_FESTIU) continue;
+                  ?>
+                  <li class="mdl-menu__item mdl-custom-multiselect__item">
+                    <label class="mdl-checkbox mdl-js-checkbox" for="company-<?=(int)$id?>">
+                      <input type="checkbox" id="company-<?=(int)$id?>" name="companies[]" value="<?=(int)$id?>" data-value="<?=(int)$id?>" class="mdl-checkbox__input">
+                      <span class="mdl-checkbox__label"><?=security::htmlsafe($company)?></span>
+                    </label>
+                  </li>
+                  <?php
+                }
+                ?>
+              </ul>
+              <label for="companies" class="mdlext-selectfield__label always-focused mdl-color-text--primary">Empresas</label>
+            </div>
+
+            <h5>Trabajadores</h5>
+            <div class="categories-select">
+              <h6>Seleccionar:</h6>
+              <?php
+              $categories = categories::getAllWithWorkers();
+              foreach ($categories as $c) {
+                if (!count($c["workers"])) continue;
+                echo "<span class=\"select-all\" data-workers=\"".security::htmlsafe(implode(",", $c["workers"]))."\">".security::htmlsafe($c["name"])."</span><br>";
+              }
+              ?>
+            </div>
+            <div class="overflow-wrapper overflow-wrapper--for-table">
+              <table class="mdl-data-table mdl-js-data-table mdl-data-table--selectable mdl-shadow--2dp">
+                <thead>
+                  <tr>
+                    <?php
+                    if ($conf["debug"]) {
+                      ?>
+                      <th class="extra">ID</th>
+                      <?php
+                    }
+                    ?>
+                    <th class="mdl-data-table__cell--non-numeric">Nombre</th>
+                    <th class="mdl-data-table__cell--non-numeric">Empresa</th>
+                    <th class="mdl-data-table__cell--non-numeric extra">Categoría</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <?php
+                  $workers = people::getAll(false, true);
+                  foreach ($workers as $w) {
+                    ?>
+                    <tr data-worker-id="<?=(int)$w["workerid"]?>" data-company-id="<?=(int)$w["companyid"]?>">
+                      <?php
+                      if ($conf["debug"]) {
+                        ?>
+                        <td class="extra"><?=(int)$w["workerid"]?></td>
+                        <?php
+                      }
+                      ?>
+                      <td class="mdl-data-table__cell--non-numeric"><?=security::htmlsafe($w["name"])?></td>
+                      <td class="mdl-data-table__cell--non-numeric"><?=security::htmlsafe($companies[$w["companyid"]])?></td>
+                      <td class="mdl-data-table__cell--non-numeric extra"><?=security::htmlsafe($w["category"])?></td>
+                    </tr>
+                    <?php
+                  }
+                  ?>
+                </tbody>
+              </table>
+            </div>
+
+            <div style="clear: both;"></div>
+
+            <h5>Formato</h5>
+            <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+              <select name="format" id="format" class="mdlext-selectfield__select">
+                <?php
+                foreach (export::$formats as $i => $format) {
+                  echo '<option value="'.(int)$i.'">'.security::htmlsafe($format).'</option>';
+                }
+                ?>
+              </select>
+              <label for="format" class="mdlext-selectfield__label">Formato</label>
+            </div>
+
+            <div id="pdf">
+              <h5>Opciones para PDF</h5>
+              <p>
+                <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="ignoreempty">
+                  <input type="checkbox" id="ignoreempty" name="ignoreempty" value="1" class="mdl-switch__input" checked>
+                  <span class="mdl-switch__label">No incluir trabajadores que no tengan ningún registro ni incidencia</span>
+                </label>
+              </p>
+              <p>
+                <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="labelinvalid">
+                  <input type="checkbox" id="labelinvalid" name="labelinvalid" value="1" class="mdl-switch__input" checked>
+                  <span class="mdl-switch__label">Marcar en rojo incidencias/registros no validados</span>
+                </label>
+              </p>
+              <p style="font-weight: bold;">
+                Mostrar registros/incidencias que estén:
+              </p>
+              <p>
+                <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="showvalidated">
+                  <input type="checkbox" id="showvalidated" name="showvalidated" value="1" class="mdl-checkbox__input" checked>
+                  <span class="mdl-checkbox__label">Validados</span>
+                </label>
+                <br>
+                <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="shownotvalidated">
+                  <input type="checkbox" id="shownotvalidated" name="shownotvalidated" value="1" class="mdl-checkbox__input" checked>
+                  <span class="mdl-checkbox__label">No validados</span>
+                </label>
+              </p>
+            </div>
+            <br>
+            <button class="mdl-button mdl-js-button mdl-button--raised mdl-button--accent mdl-js-ripple-effect">Exportar</button>
+          </form>
+
+          <?php visual::printDebug("categories::getAllWithWorkers()", $categories); ?>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <?php
+  visual::smartSnackbar([
+    ["empty", "Faltan datos por introducir en el formulario."],
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."],
+    ["inverted", "La fecha de inicio debe ser anterior a la de fin."],
+    ["forecastingthefutureisimpossible", "La fecha de fin debe ser anterior al día de hoy."]
+  ]);
+  ?>
+
+  <script src="js/export.js"></script>
+</body>
+</html>
diff --git a/src/export4worker.php b/src/export4worker.php
new file mode 100644
index 0000000..76b72ca
--- /dev/null
+++ b/src/export4worker.php
@@ -0,0 +1,115 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER);
+
+if (!security::checkParams("GET", [
+  ["id", security::PARAM_ISINT]
+])) {
+  security::go("workerhome.php?msg=empty");
+}
+
+$isAdmin = security::isAllowed(security::ADMIN);
+
+if (!$isAdmin && people::userData("id") != $_GET["id"]) {
+  security::notFound();
+}
+
+$workers = workers::getPersonWorkers((int)$_GET["id"]);
+if ($workers === false) security::go("workerhome.php?msg=unexpected");
+
+$companies = companies::getAll();
+
+$date = new DateTime();
+$interval = new DateInterval("P1D");
+$date->sub($interval);
+$yesterday = date("Y-m-d", $date->getTimestamp());
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <?php helpView::renderHelpButton(help::PLACE_EXPORT_REGISTRY_PAGE, true); ?>
+          <h2>Exportar registro</h2>
+          <?php
+          if (count($workers)) {
+            ?>
+            <form action="doexport.php" method="GET">
+              <?php
+              foreach ($workers as $w) {
+                echo '<input type="hidden" name="workers[]" value="'.(int)$w["id"].'">';
+              }
+              ?>
+
+              <h5>Periodo</h5>
+              <p>Del <input type="date" name="begins" max="<?=security::htmlsafe($yesterday)?>" required> al <input type="date" name="ends" max="<?=security::htmlsafe($yesterday)?>" required></p>
+
+              <h5>Formato</h5>
+              <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+                <select name="format" id="format" class="mdlext-selectfield__select">
+                  <?php
+                  foreach (export::$formats as $i => $format) {
+                    if (!in_array($i, export::$workerFormats)) continue;
+                    echo '<option value="'.(int)$i.'">'.security::htmlsafe($format).'</option>';
+                  }
+                  ?>
+                </select>
+                <label for="format" class="mdlext-selectfield__label">Formato</label>
+              </div>
+
+              <div id="pdf">
+                <h5>Opciones para PDF</h5>
+                <p>
+                  <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="labelinvalid">
+                    <input type="checkbox" id="labelinvalid" name="labelinvalid" value="1" class="mdl-switch__input" checked>
+                    <span class="mdl-switch__label">Marcar en rojo incidencias/registros no validados</span>
+                  </label>
+                </p>
+                <p style="font-weight: bold;">
+                  Mostrar registros/incidencias que estén:
+                </p>
+                <p>
+                  <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="showvalidated">
+                    <input type="checkbox" id="showvalidated" name="showvalidated" value="1" class="mdl-checkbox__input" checked>
+                    <span class="mdl-checkbox__label">Validados</span>
+                  </label>
+                  <br>
+                  <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="shownotvalidated">
+                    <input type="checkbox" id="shownotvalidated" name="shownotvalidated" value="1" class="mdl-checkbox__input" checked>
+                    <span class="mdl-checkbox__label">No validados</span>
+                  </label>
+                </p>
+              </div>
+              <br>
+              <button class="mdl-button mdl-js-button mdl-button--raised mdl-button--accent mdl-js-ripple-effect">Exportar</button>
+            </form>
+            <?php
+          } else {
+            echo "<p>No puedes exportar el registro porque todavía no se te ha asignado ninguna empresa.</p>";
+          }
+          ?>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <?php
+  visual::smartSnackbar([
+    ["empty", "Faltan datos por introducir en el formulario."],
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."],
+    ["inverted", "La fecha de inicio debe ser anterior a la de fin."],
+    ["forecastingthefutureisimpossible", "La fecha de fin debe ser anterior al día de hoy."]
+  ]);
+  ?>
+
+  <script src="js/export.js"></script>
+</body>
+</html>
diff --git a/src/help.php b/src/help.php
new file mode 100644
index 0000000..2880272
--- /dev/null
+++ b/src/help.php
@@ -0,0 +1,61 @@
+<?php
+require_once("core.php");
+security::checkType(security::HYPERADMIN);
+
+$mdHeaderRowBefore = visual::backBtn("settings.php");
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Recursos de ayuda</h2>
+          <div class="overflow-wrapper overflow-wrapper--for-table">
+            <table class="mdl-data-table mdl-js-data-table mdl-shadow--2dp">
+              <thead>
+                <tr>
+                  <th class="mdl-data-table__cell--non-numeric">Sitio</th>
+                  <th class="mdl-data-table__cell--non-numeric">URL</th>
+                  <th class="mdl-data-table__cell--non-numeric"></th>
+                </tr>
+              </thead>
+              <tbody>
+                <?php
+                foreach (help::$places as $p) {
+                  $h = help::get($p);
+                  $isset = ($h !== false);
+                  ?>
+                  <tr<?=(!$isset ? " class='mdl-color-text--grey-600'" : "")?>>
+                    <td class="mdl-data-table__cell--non-numeric"><?=security::htmlsafe(help::$placesName[$p])?></td>
+                    <td class="mdl-data-table__cell--non-numeric"><?=($h === false ? "-" : $h)?></td>
+                    <td class='mdl-data-table__cell--non-numeric'><a href='dynamic/sethelpresource.php?place=<?=security::htmlsafe($p)?>' data-dyndialog-href='dynamic/sethelpresource.php?place=<?=security::htmlsafe($p)?>' title='Configurar enlace de ayuda'><i class='material-icons icon'>build</i></a></td>
+                  </tr>
+                  <?php
+                }
+                ?>
+              </tbody>
+            </table>
+          </div>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <?php
+  visual::smartSnackbar([
+    ["success", "Se ha configurado el enlace correctamente."],
+    ["invalidurl", "La URL proporcionada está malformada. Por favor, introduce una URL correcta."],
+    ["empty", "Faltan datos por introducir en el formulario."],
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."]
+  ]);
+  ?>
+</body>
+</html>
diff --git a/src/home.php b/src/home.php
new file mode 100644
index 0000000..e2a5b94
--- /dev/null
+++ b/src/home.php
@@ -0,0 +1,94 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+security::changeActiveView(visual::VIEW_ADMIN);
+
+$tips = array(
+  "companies" => "<a href='companies.php'>Añade una o más empresas</a> antes de empezar. Esta utilidad te permite organizar el horario de trabajadores de varias empresas en la misma aplicación.",
+  "categories" => "Opcionalmente, puedes <a href='categories.php'>crear categorías</a> para clasificar tus trabajadores, si te es conveniente.",
+  "typesincidents" => "Para poder rellenar incidencias, es necesario que <a href='incidenttypes.php'>crees tipos de incidencias</a>. Estos sirven para determinar si una incidencia de ese tipo significa que se han trabajado horas extra o por si lo contrario el trabajador se auyenta del centro de trabajo, o si las horas de incidencia están remuneradas, etc.",
+  "calendars" => "<a href='calendars.php'>Configura los calendarios</a> de días laborables, lectivos y festivos."
+);
+
+$checkEmptiness = ["companies", "categories", "typesincidents", "calendars"];
+$isEmpty = [];
+
+foreach ($checkEmptiness as $table) {
+  if (db::numRows($table) == 0) $isEmpty[] = $table;
+}
+
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+  <style>
+  .tip {
+    border-radius: 4px;
+    border: solid 1px #45b9dc;
+    padding: 22px 16px;
+  }
+
+  .tip p:last-child {
+    margin-bottom: 0;
+  }
+
+  li {
+    margin-bottom: 0.5em;
+  }
+
+  li.done {
+    color: #777;
+  }
+
+  li.done a {
+    color: #555;
+  }
+  </style>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Bienvenido</h2>
+          <?php
+          if (count($isEmpty)) {
+            ?>
+            <p>¡Hola <?=people::userData("name")?>! Antes de empezar a sacarle todo el jugo a la aplicación, nos gustaría sugerirte algunas acciones para empezar a configurarla:</p>
+            <ol>
+              <?php
+              foreach ($checkEmptiness as $t) {
+                $strike = !in_array($t, $isEmpty);
+                echo "<li".($strike ? " class='done'" : "").">".$tips[$t]."</li>";
+              }
+              ?>
+            </ol>
+            <p>Después de realizar estas acciones, te sugerimos añadir trabajadores y configurar sus horarios desde la sección <a href="users.php">Trabajadores</a>. También te puede interesar acceder a la sección <a href="scheduletemplates.php">Plantillas de horarios</a> si vas a configurar el mismo horario múltiples veces a varias personas.</p>
+            <?php
+          } else {
+            ?>
+        		<p>¡Hola <?=people::userData("name")?>! Bienvenido a tu Panel de Control.</p>
+            <?php
+            if (!secondFactor::isEnabled()) {
+              ?>
+              <hr>
+              <div class="tip mdl-color--blue-100 mdl-shadow--2dp">
+                <p><b>Consejo:</b> debido a que eres un <?=security::htmlsafe(strtolower(security::$types[people::userData("type")]))?> y por lo tanto tu usuario tiene acceso ilimitado a todos los datos del aplicativo, para evitar que estos caigan en manos de un agente malicioso, te recomendamos que actives la <span class="highlighted">verificación en dos pasos</span>.</p>
+                <p>La verificación en dos pasos consiste en la obligación de usar un segundo factor (como un código generado en tu móvil o tu huella dactilar) para iniciar sesión a parte de tu usuario y contraseña. <span class="highlighted">Mientras que una contraseña es relativamente fácil de obtener, para acceder al segundo factor el atacante debe tener acceso físico a un dispositivo tuyo, lo que disminuye el riesgo de sufrir un ataque.</span></p>
+                <p>Puedes aprender más sobre la verificación en dos pasos en <a href="https://avm99963.github.io/hores-external/trabajadores/verificacion-en-dos-pasos/" target="_blank" rel="noopener noreferrer">este artículo de ayuda</a>.</p>
+              </div>
+              <?php
+            }
+          }
+          ?>
+        </div>
+      </div>
+    </main>
+  </div>
+</body>
+</html>
diff --git a/src/inc/api.php b/src/inc/api.php
new file mode 100644
index 0000000..9b606be
--- /dev/null
+++ b/src/inc/api.php
@@ -0,0 +1,28 @@
+<?php
+class api {
+  public static function inputJson() {
+    $string = trim(file_get_contents("php://input"));
+
+    if (empty($string)) return false;
+
+    $json = json_decode($string, true);
+
+    if (json_last_error() !== JSON_ERROR_NONE) return false;
+
+    return $json;
+  }
+
+  public static function error($message = null) {
+    if ($message !== null) self::write([
+      'error' => true,
+      'message' => $message,
+    ]);
+    http_response_code(400);
+    exit();
+  }
+
+  public static function write($array) {
+    header('Content-Type: application/json');
+    echo json_encode($array);
+  }
+}
diff --git a/src/inc/calendars.php b/src/inc/calendars.php
new file mode 100644
index 0000000..a8b4a9f
--- /dev/null
+++ b/src/inc/calendars.php
@@ -0,0 +1,182 @@
+<?php
+class calendars {
+  const TYPE_FESTIU = 0;
+  const TYPE_FEINER = 1;
+  const TYPE_LECTIU = 2;
+
+  const NO_CALENDAR_APPLICABLE = 0;
+
+  public static $types = array(
+    0 => "Festivo",
+    2 => "Lectivo",
+    1 => "No lectivo"
+  );
+  public static $workingTypes = [1, 2];
+  public static $days = ["Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado", "Domingo"];
+  public static $months = ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"];
+
+  public static function parseFormCalendar($form, $ibegins, $iends, $timestamp = false) {
+    if ($timestamp) {
+      $current = new DateTime();
+      $current->setTimestamp((int)$ibegins);
+      $ends = new DateTime();
+      $ends->setTimestamp((int)$iends);
+    } else {
+      $current = new DateTime($ibegins);
+      $ends = new DateTime($iends);
+    }
+    $interval = new DateInterval("P1D");
+
+    if ($current->diff($ends)->invert === 1) {
+      return false;
+    }
+
+    $return = array(
+      "begins" => $current->getTimestamp(),
+      "ends" => $ends->getTimestamp(),
+      "calendar" => []
+    );
+
+    $possible_values = array_keys(self::$types);
+
+    $day = 0;
+    while ($current->diff($ends)->invert === 0) {
+      if (!isset($form[$day]) || !in_array($form[$day], $possible_values)) {
+        return false;
+      }
+
+      $return["calendar"][$current->getTimestamp()] = (int)$form[$day];
+
+      $day++;
+      $current->add($interval);
+    }
+
+    return $return;
+  }
+
+  public static function checkOverlap($category, $begins, $ends) {
+    global $con;
+
+    $scategory = (int)$category;
+    $sbegins = (int)$begins;
+    $sends = (int)$ends;
+
+    $query = mysqli_query($con, "SELECT * FROM calendars WHERE begins <= $sends AND ends >= $sbegins AND category = $scategory LIMIT 1");
+
+    return (mysqli_num_rows($query) > 0);
+  }
+
+  public static function add($category, $begins, $ends, $calendar) {
+    global $con;
+
+    if (self::checkOverlap($category, $begins, $ends)) {
+      return -1;
+    }
+
+    $scategory = (int)$category;
+    $sbegins = (int)$begins;
+    $sends = (int)$ends;
+    $scalendar = db::sanitize(json_encode($calendar));
+
+    return (mysqli_query($con, "INSERT INTO calendars (category, begins, ends, details) VALUES ($scategory, $sbegins, $sends, '$scalendar')") ? 0 : -2);
+  }
+
+  public static function edit($id, $calendar) {
+    global $con;
+
+    $sid = (int)$id;
+    $scalendar = db::sanitize(json_encode($calendar));
+
+    return (mysqli_query($con, "UPDATE calendars SET details = '$scalendar' WHERE id = $sid LIMIT 1"));
+  }
+
+  public static function get($id) {
+    global $con;
+
+    $sid = (int)$id;
+
+    $query = mysqli_query($con, "SELECT c.id id, c.begins begins, c.ends ends, c.details details, c.category category, ca.name categoryname FROM calendars c LEFT JOIN categories ca ON c.category = ca.id WHERE c.id = $sid");
+
+    if (!mysqli_num_rows($query)) {
+      return false;
+    }
+
+    $row = mysqli_fetch_assoc($query);
+
+    if ($row["category"] == -1) {
+      $row["categoryname"] = "Calendario por defecto";
+    }
+
+    return $row;
+  }
+
+  public static function getByCategory($id, $details = false) {
+    global $con;
+
+    $sid = (int)$id;
+
+    $query = mysqli_query($con, "SELECT id, begins, ends".($details ? ", details" : "")." FROM calendars WHERE category = $sid");
+
+    $calendars = [];
+
+    while ($row = mysqli_fetch_assoc($query)) {
+      $calendars[] = $row;
+    }
+
+    return $calendars;
+  }
+
+  public static function getAll() {
+    $categories = categories::getAll();
+    $categories[-1] = "Calendario por defecto";
+
+    $return = [];
+
+    foreach ($categories as $id => $category) {
+      $return[] = array(
+        "category" => $category,
+        "categoryid" => $id,
+        "calendars" => self::getByCategory($id)
+      );
+    }
+
+    return $return;
+  }
+
+  public static function getCurrentCalendarByCategory($category) {
+    global $con;
+
+    $scategory = (int)$category;
+    $stime = (int)time();
+
+    $query = mysqli_query($con, "SELECT id, category, begins, ends, details FROM calendars WHERE category IN (-1, $scategory) AND begins <= $stime AND ends >= $stime");
+    if ($query === false) return false;
+
+    $calendars = [];
+    while ($row = mysqli_fetch_assoc($query)) {
+      $row["details"] = json_decode($row["details"], true);
+      if (json_last_error() !== JSON_ERROR_NONE) return false;
+
+      $calendars[$row["category"]] = $row;
+    }
+
+    return $calendars[$category] ??  $calendars[-1] ?? self::NO_CALENDAR_APPLICABLE;
+  }
+
+  public static function remove($id) {
+    global $con;
+
+    $sid = (int)$id;
+
+    return mysqli_query($con, "DELETE FROM calendars WHERE id = $sid LIMIT 1");
+  }
+
+  public static function exists($id) {
+    global $con;
+
+    $sid = (int)$id;
+    $query = mysqli_query($con, "SELECT id FROM calendars WHERE id = $sid");
+
+    return (mysqli_num_rows($query) > 0);
+  }
+}
diff --git a/src/inc/calendarsView.php b/src/inc/calendarsView.php
new file mode 100644
index 0000000..eddc230
--- /dev/null
+++ b/src/inc/calendarsView.php
@@ -0,0 +1,44 @@
+<?php
+class calendarsView {
+  public static function renderCalendar($current, $ends, $selectedFunc, $disabled = false, $extra = false) {
+    $interval = new DateInterval("P1D");
+
+    echo '<div class="overflow-wrapper">';
+
+    $start = true;
+    $day = 0;
+    while ($current->diff($ends)->invert === 0) {
+      $dow = (int)$current->format("w");
+      if ($dow == 0) $dow = 7;
+      $dom = (int)$current->format("d");
+
+      if ($dow == 1) echo "</tr>";
+      if ($dom == 1) echo "</table>";
+      if ($dom == 1 || $start) echo "<div class='month'>".security::htmlsafe(ucfirst(strftime("%B %G", $current->getTimestamp())))."</div><table class='calendar'>";
+      if ($dow == 1 || $start) echo "<tr>";
+      if ($dom == 1 || $start) {
+        for ($i = 1; $i < $dow; $i++) {
+          echo "<td></td>";
+        }
+      }
+
+      echo "<td class='day'><span class='date'>".$dom."</span><br><select name='type[$day]'".($disabled ? " disabled" : "").">";
+
+      foreach (calendars::$types as $id => $type) {
+        echo "<option value='".(int)$id."'".($selectedFunc($current->getTimestamp(), $id, $dow, $dom, $extra) ? " selected" : "").">".security::htmlsafe($type)."</option>";
+      }
+
+      echo "</td>";
+
+      $start = false;
+      $day++;
+      $current->add($interval);
+    }
+
+    for ($i = $dow + 1; $i <= 7; $i++) {
+      echo "<td></td>";
+    }
+
+    echo "</tr></table></div>";
+  }
+}
diff --git a/src/inc/categories.php b/src/inc/categories.php
new file mode 100644
index 0000000..a75d998
--- /dev/null
+++ b/src/inc/categories.php
@@ -0,0 +1,161 @@
+<?php
+class categories {
+  private static function parseEmails($string) {
+    $string = str_replace(" ", "", $string);
+    $emails = explode(",", $string);
+
+    foreach ($emails as $i => &$e) {
+      if (empty($e)) {
+        unset($emails[$i]);
+      } else {
+        if (filter_var($e, FILTER_VALIDATE_EMAIL) === false) return false;
+      }
+    }
+
+    return $emails;
+  }
+
+  public static function readableEmails($emails) {
+    $array = json_decode($emails, true);
+    if (json_last_error() != JSON_ERROR_NONE) return false;
+    return implode(", ", $array);
+  }
+
+  private static function canBeParent($id) {
+    if ($id == 0) return true;
+
+    $category = self::get($id);
+    return ($category !== false && $category["parent"] == 0);
+  }
+
+  public static function add($category, $stringEmails, $parent) {
+    global $con;
+
+    $emails = (empty($stringEmails) ? [] : self::parseEmails($stringEmails));
+    if ($emails === false) return false;
+
+    if (!self::canBeParent($parent)) return false;
+
+    $scategory = db::sanitize($category);
+    $semails = db::sanitize(json_encode($emails));
+    $sparent = (int)$parent;
+
+    return mysqli_query($con, "INSERT INTO categories (name, emails, parent) VALUES ('$scategory', '$semails', $sparent)");
+  }
+
+  public static function edit($id, $name, $stringEmails, $parent) {
+    global $con;
+
+    $emails = (empty($stringEmails) ? [] : self::parseEmails($stringEmails));
+    if ($emails === false) return false;
+
+    if (!self::canBeParent($parent)) return false;
+
+    $sid = (int)$id;
+    $sname = db::sanitize($name);
+    $semails = db::sanitize(json_encode($emails));
+    $sparent = (int)$parent;
+
+    return mysqli_query($con, "UPDATE categories SET name = '$sname', emails = '$semails', parent = $sparent WHERE id = $sid LIMIT 1");
+  }
+
+  public static function get($id) {
+    global $con;
+
+    $sid = (int)$id;
+
+    $query = mysqli_query($con, "SELECT * FROM categories WHERE id = $sid");
+
+    if (!mysqli_num_rows($query)) return false;
+
+    return mysqli_fetch_assoc($query);
+  }
+
+  private static function addChilds(&$row) {
+    global $con;
+
+    $query = mysqli_query($con, "SELECT * FROM categories WHERE parent = ".(int)$row["id"]);
+
+    $row["childs"] = [];
+    while ($child = mysqli_fetch_assoc($query)) {
+      $row["childs"][] = $child;
+    }
+  }
+
+  public static function getAll($simplified = true, $withparents = true, $includechilds = false) {
+    global $con, $conf;
+
+    $query = mysqli_query($con, "SELECT ".($simplified ? "id, name" : "c.id id, c.name name, c.parent parent, c.emails emails, p.name parentname")." FROM categories c".($simplified ? "" : " LEFT JOIN categories p ON c.parent = p.id").($withparents ? "" : " WHERE c.parent = 0")." ORDER BY ".($conf["debug"] ? "id" : "name")." ASC");
+
+    $categories = [];
+
+    while ($row = mysqli_fetch_assoc($query)) {
+      if (!$simplified && $includechilds) self::addChilds($row);
+
+      if ($simplified) $categories[$row["id"]] = $row["name"];
+      else $categories[] = $row;
+    }
+
+    return $categories;
+  }
+
+  public static function getAllWithWorkers() {
+    global $con;
+
+    $categories = self::getAll(false);
+
+    foreach ($categories as &$category) {
+      $category["workers"] = [];
+
+      $query = mysqli_query($con, "SELECT w.id FROM workers w LEFT JOIN people p ON w.person = p.id WHERE p.category = ".(int)$category["id"]);
+
+      while ($row = mysqli_fetch_assoc($query)) {
+        $category["workers"][] = $row["id"];
+      }
+    }
+
+    return $categories;
+  }
+
+  public static function getChildren() {
+    global $con, $conf;
+
+    $query = mysqli_query($con, "SELECT p.id parent, c.id child FROM categories c LEFT JOIN categories p ON c.parent = p.id WHERE c.parent != 0");
+
+    $childs = [];
+
+    while ($row = mysqli_fetch_assoc($query)) {
+      if (!isset($childs[$row["parent"]])) {
+        $childs[$row["parent"]] = [];
+      }
+      $childs[$row["parent"]][] = $row["child"];
+    }
+
+    return $childs;
+  }
+
+  public static function exists($id) {
+    global $con;
+
+    if ($id == -1) return true;
+
+    $query = mysqli_query($con, "SELECT id FROM categories WHERE id = ".(int)$id);
+
+    return (mysqli_num_rows($query) > 0);
+  }
+
+  public static function getIdByName($name) {
+    global $con;
+
+    if (strtolower($name) == "sin categoría") return -1;
+
+    $sname = db::sanitize($name);
+    $query = mysqli_query($con, "SELECT id FROM categories WHERE name = '$sname'");
+
+    if (!mysqli_num_rows($query)) return false;
+
+    $row = mysqli_fetch_assoc($query);
+
+    return $row["id"];
+  }
+}
diff --git a/src/inc/common.php b/src/inc/common.php
new file mode 100644
index 0000000..26d02aa
--- /dev/null
+++ b/src/inc/common.php
@@ -0,0 +1,17 @@
+<?php
+class common {
+  public static function getDayTimestamp($originaltime) {
+    $datetime = new DateTime();
+    $datetime->setTimestamp($originaltime);
+
+    $rawdate = $datetime->format("Y-m-d")."T00:00:00";
+    $date = new DateTime($rawdate);
+
+    return $time = $date->getTimestamp();
+  }
+
+  public static function getTimestampFromRFC3339($rfc) {
+    $date = new DateTime($rfc);
+    return (int)$date->getTimestamp();
+  }
+}
diff --git a/src/inc/companies.php b/src/inc/companies.php
new file mode 100644
index 0000000..702151f
--- /dev/null
+++ b/src/inc/companies.php
@@ -0,0 +1,58 @@
+<?php
+class companies {
+  public static function add($company, $cif) {
+    global $con;
+
+    $scompany = db::sanitize($company);
+    $scif = db::sanitize($cif);
+    return mysqli_query($con, "INSERT INTO companies (name, cif) VALUES ('$scompany', '$scif')");
+  }
+
+  public static function edit($id, $name, $cif) {
+    global $con;
+
+    $sid = (int)$id;
+    $sname = db::sanitize($name);
+    $scif = db::sanitize($cif);
+
+    return mysqli_query($con, "UPDATE companies SET name = '$sname', cif = '$scif' WHERE id = $sid LIMIT 1");
+  }
+
+  public static function get($id) {
+    global $con;
+
+    $sid = (int)$id;
+
+    $query = mysqli_query($con, "SELECT * FROM companies WHERE id = $sid");
+
+    if (!mysqli_num_rows($query)) return false;
+
+    return mysqli_fetch_assoc($query);
+  }
+
+  public static function getAll($simplified = true, $mixed = false) {
+    global $con;
+
+    $query = mysqli_query($con, "SELECT * FROM companies ORDER BY id ASC");
+
+    $categories = [];
+
+    while ($row = mysqli_fetch_assoc($query)) {
+      if ($simplified) $categories[$row["id"]] = $row["name"];
+      elseif ($mixed) $categories[$row["id"]] = $row;
+      else $categories[] = $row;
+    }
+
+    return $categories;
+  }
+
+  public static function exists($id) {
+    global $con;
+
+    if ($id == -1) return true;
+
+    $query = mysqli_query($con, "SELECT id FROM companies WHERE id = ".(int)$id);
+
+    return (mysqli_num_rows($query) > 0);
+  }
+}
diff --git a/src/inc/csv.php b/src/inc/csv.php
new file mode 100644
index 0000000..ce44c81
--- /dev/null
+++ b/src/inc/csv.php
@@ -0,0 +1,31 @@
+<?php
+class csv {
+  public static $fields = ["dni", "name", "category", "email", "companies"];
+
+  public static function csv2array($filename) {
+    $file = fopen($filename, "r");
+
+    $return = [];
+
+    $i = 0;
+    while (($line = fgetcsv($file, null, ";")) !== false) {
+      if ($i == 0) {
+        if (count($line) < count(self::$fields)) return false;
+
+        for ($j = 0; $j < count(self::$fields); $j++) {
+          if ($line[$j] !== self::$fields[$j]) return false;
+        }
+      } else {
+        $return[$i] = [];
+        foreach (self::$fields as $j => $field) {
+          $return[$i][$field] = trim($line[$j]);
+        }
+      }
+      $i++;
+    }
+
+    fclose($file);
+
+    return $return;
+  }
+}
diff --git a/src/inc/db.php b/src/inc/db.php
new file mode 100644
index 0000000..293707a
--- /dev/null
+++ b/src/inc/db.php
@@ -0,0 +1,38 @@
+<?php
+class db {
+  const EXPORT_DB_FORMAT_SQL = 0;
+
+  public static function sanitize($string) {
+    global $con;
+    return mysqli_real_escape_string($con, $string);
+  }
+
+  public static function needsSetUp() {
+    global $con;
+
+    $checkquery = mysqli_query($con, "SELECT 1 FROM people LIMIT 1");
+
+    return ($checkquery === false);
+  }
+
+  public static function numRows($table) {
+    global $con;
+
+    $stable = preg_replace("/[^A-Za-z0-9 ]/", '', $table);
+
+    $query = mysqli_query($con, "SELECT 1 FROM $stable");
+
+    if ($query === false) return -1;
+
+    return mysqli_num_rows($query);
+  }
+
+  public static function limitPagination($start, $limit) {
+    $slimit = (int)$limit;
+    $sstart = $slimit*(int)$start;
+    if ($slimit > 100 || $slimit < 0) return false;
+    if ($sstart < 0) return false;
+
+    return ($slimit == 0 ? "" : " LIMIT $sstart,$slimit");
+  }
+}
diff --git a/src/inc/export.php b/src/inc/export.php
new file mode 100644
index 0000000..6800d50
--- /dev/null
+++ b/src/inc/export.php
@@ -0,0 +1,54 @@
+<?php
+class export {
+  const FORMAT_PDF = 1;
+  const FORMAT_DETAILEDPDF = 2;
+  const FORMAT_CSV_SCHEDULES = 3;
+  const FORMAT_CSV_INCIDENTS = 4;
+
+  public static $formats = array(
+    1 => "PDF con horario laboral",
+    2 => "PDF detallado",
+    3 => "CSV con horarios",
+    4 => "CSV con incidencias"
+  );
+  public static $workerFormats = [1, 3, 4];
+
+  public static $schedulesFields = ["id", "worker", "workerid", "dni", "company", "day", "beginswork", "endswork", "beginsbreakfast", "endsbreakfast", "beginslunch", "endslunch", "state"];
+  public static $incidentsFields = ["id", "worker", "workerid", "dni", "company", "creator", "updated", "updatedby", "confirmedby", "type", "day", "allday", "begins", "ends", "details", "workerdetails", "verified", "typepresent", "typepaid", "state"];
+
+  public static function convert($str) {
+    return iconv('UTF-8', 'windows-1252', $str);
+  }
+
+  public static function getDays($worker, $begins, $ends, $showvalidated, $shownotvalidated) {
+    $return = [];
+
+    $records = registry::getRecords($worker, $begins, $ends, false, false, false, 0, 0);
+    if ($records === false) return false;
+    foreach ($records as $record) {
+      if (!$showvalidated && $record["state"] === registry::STATE_VALIDATED_BY_WORKER) continue;
+      if (!$shownotvalidated && $record["state"] === registry::STATE_REGISTERED) continue;
+      if (!isset($return[$record["day"]])) $return[$record["day"]] = [];
+      $return[$record["day"]]["schedule"] = $record;
+    }
+
+    $incidents = incidents::getAll(false, 0, 0, $worker, $begins, $ends);
+    if ($incidents === false) return false;
+    foreach ($incidents as $incident) {
+      if ($incident["state"] !== incidents::STATE_REGISTERED && $incident["state"] !== incidents::STATE_VALIDATED_BY_WORKER) continue;
+      if (!$showvalidated && $incident["state"] === incidents::STATE_VALIDATED_BY_WORKER) continue;
+      if (!$shownotvalidated && $incident["state"] === incidents::STATE_REGISTERED) continue;
+      if (!isset($return[$incident["day"]])) $return[$incident["day"]] = [];
+      if (!isset($return[$incident["day"]]["incidents"])) $return[$incident["day"]]["incidents"] = [];
+      $return[$incident["day"]]["incidents"][] = $incident;
+    }
+
+    ksort($return);
+
+    return $return;
+  }
+
+  public static function sec2hours($sec) {
+    return round((double)$sec/3600, 2)." h";
+  }
+}
diff --git a/src/inc/files.php b/src/inc/files.php
new file mode 100644
index 0000000..5f9cce2
--- /dev/null
+++ b/src/inc/files.php
@@ -0,0 +1,106 @@
+<?php
+class files {
+  const NAME_LENGTH = 16;
+  const MAX_SIZE = 6*1024*1024;
+  const READABLE_MAX_SIZE = "6 MB";
+
+  public static $acceptedFormats = ["pdf", "png", "jpg", "jpeg", "bmp", "gif"];
+  public static $mimeTypes = array(
+    "pdf" => "application/pdf",
+    "png" => "image/png",
+    "jpg" => "image/jpeg",
+    "jpeg" => "image/jpeg",
+    "bmp" => "image/bmp",
+    "gif" => "image/gif"
+  );
+  public static $readableMimeTypes = array(
+    "pdf" => "Documento PDF",
+    "png" => "Imagen PNG",
+    "jpg" => "Imagen JPG",
+    "jpeg" => "Imagen JPG",
+    "bmp" => "Imagen BMP",
+    "gif" => "Imagen GIF"
+  );
+  public static $mimeTypesIcons = array(
+    "pdf" => "insert_drive_file",
+    "png" => "image",
+    "jpg" => "image",
+    "jpeg" => "image",
+    "bmp" => "image",
+    "gif" => "image"
+  );
+
+  public static function getAcceptAttribute($pretty = false) {
+    $formats = array_map(function($el) {
+      return ".".$el;
+    }, self::$acceptedFormats);
+
+    return implode(($pretty ? ", " : ","), $formats);
+  }
+
+  // From https://stackoverflow.com/a/31107425
+  private static function randomStr(int $length = 64, string $keyspace = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'): string {
+    if ($length < 1) {
+      throw new \RangeException("Length must be a positive integer");
+    }
+    $pieces = [];
+    $max = mb_strlen($keyspace, '8bit') - 1;
+    for ($i = 0; $i < $length; ++$i) {
+      $pieces []= $keyspace[random_int(0, $max)];
+    }
+    return implode('', $pieces);
+  }
+
+
+  private static function getName($ext) {
+    global $conf;
+
+    $filename = "";
+
+    do {
+      $filename = self::randomStr(self::NAME_LENGTH).".".$ext;
+    } while (file_exists($conf["attachmentsFolder"].$filename));
+
+    return $filename;
+  }
+
+  public static function getFileExtension($file) {
+    $filenameExploded = explode(".", $file);
+    return mb_strtolower($filenameExploded[count($filenameExploded) - 1]);
+  }
+
+  public static function uploadFile(&$file, &$name) {
+    global $conf;
+
+    if (!isset($file["error"]) || is_array($file["error"])) return 1;
+
+    switch ($file["error"]) {
+      case UPLOAD_ERR_OK:
+      break;
+
+      case UPLOAD_ERR_INI_SIZE:
+      case UPLOAD_ERR_FORM_SIZE:
+      return 2;
+      break;
+
+      default:
+      return 1;
+      break;
+    }
+
+    $ext = self::getFileExtension($file["name"]);
+
+    if ($file['size'] > self::MAX_SIZE) return 2;
+    if (!in_array($ext, self::$acceptedFormats)) return 3;
+
+    $name = self::getName($ext);
+
+    return (move_uploaded_file($file["tmp_name"], $conf["attachmentsFolder"].$name) ? 0 : 1);
+  }
+
+  public static function removeFile($name) {
+    global $conf;
+
+    return unlink($conf["attachmentsFolder"].$name);
+  }
+}
diff --git a/src/inc/help.php b/src/inc/help.php
new file mode 100644
index 0000000..3fe7179
--- /dev/null
+++ b/src/inc/help.php
@@ -0,0 +1,52 @@
+<?php
+class help {
+  const PLACE_INCIDENT_FORM = 0;
+  const PLACE_VALIDATION_PAGE = 1;
+  const PLACE_REGISTRY_PAGE = 2;
+  const PLACE_EXPORT_REGISTRY_PAGE = 3;
+
+  public static $places = [0, 1, 2, 3];
+  public static $placesName = [
+    0 => "Formulario de incidencias",
+    1 => "Página de validación",
+    2 => "Página de listado de registros",
+    3 => "Página de exportar registro"
+  ];
+
+  public static function exists($place) {
+    global $con;
+
+    $splace = (int)$place;
+
+    $query = mysqli_query($con, "SELECT 1 FROM help WHERE place = $splace");
+
+    return (mysqli_num_rows($query) > 0);
+  }
+
+  public static function set($place, $url) {
+    global $con;
+
+    if (!in_array($place, self::$places)) return -1;
+    if ($url !== "" && !filter_var($url, FILTER_VALIDATE_URL)) return 1;
+
+    $splace = (int)$place;
+    $surl = db::sanitize($url);
+
+    if (self::exists($place)) return (mysqli_query($con, "UPDATE help SET url = '$surl' WHERE place = $splace LIMIT 1") ? 0 : -1);
+    else return (mysqli_query($con, "INSERT INTO help (place, url) VALUES ('$splace', '$surl')") ? 0 : -1);
+  }
+
+  public static function get($place) {
+    global $con;
+
+    if (!in_array($place, self::$places)) return false;
+    $splace = (int)$place;
+
+    $query = mysqli_query($con, "SELECT url FROM help WHERE place = $splace");
+
+    if (mysqli_num_rows($query) > 0) {
+      $url = mysqli_fetch_assoc($query)["url"];
+      return ($url === "" ? false : $url);
+    } else return false;
+  }
+}
diff --git a/src/inc/helpView.php b/src/inc/helpView.php
new file mode 100644
index 0000000..d6b642a
--- /dev/null
+++ b/src/inc/helpView.php
@@ -0,0 +1,10 @@
+<?php
+class helpView {
+  public static function renderHelpButton($place, $topRight = false, $margin = false) {
+    $url = help::get($place);
+    if ($url === false) return;
+
+    echo ($topRight ? '<div class="help-btn--top-right'.($margin ? ' help-btn--top-right-margin': '').'">' : '').'<a href="'.security::htmlsafe($url).'" target="_blank" rel="noopener noreferrer" class="mdl-button mdl-button--colored mdl-button-js mdl-button--icon mdl-js-ripple-effect" id="help'.(int)$place.'"><i class="material-icons">help_outline</i><span class="mdl-ripple"></span></a>'.($topRight ? '</div>' : '');
+    echo '<div class="mdl-tooltip" for="help'.(int)$place.'">Ayuda</div>';
+  }
+}
diff --git a/src/inc/incidents.php b/src/inc/incidents.php
new file mode 100644
index 0000000..7664fe9
--- /dev/null
+++ b/src/inc/incidents.php
@@ -0,0 +1,609 @@
+<?php
+class incidents {
+  const STARTOFDAY = 0;
+  const ENDOFDAY = 60*60*24;
+
+  const PAGINATION_LIMIT = 20;
+
+  const UPDATE_STATE_NOT_UPDATED = 0;
+  const UPDATE_STATE_UPDATED = 1;
+
+  const STATE_UNVERIFIED = 0;
+  const STATE_REJECTED = 1;
+  const STATE_SCHEDULED = 2;
+  const STATE_REGISTERED = 3;
+  const STATE_MANUALLY_INVALIDATED = 4;
+  const STATE_VALIDATED_BY_WORKER = 5;
+
+  public static $stateIcons = array(
+    0 => "new_releases",
+    1 => "block",
+    2 => "schedule",
+    3 => "check",
+    4 => "delete_forever",
+    5 => "verified_user"
+  );
+
+  public static $stateIconColors = array(
+    0 => "mdl-color-text--orange",
+    1 => "mdl-color-text--red",
+    2 => "mdl-color-text--orange",
+    3 => "mdl-color-text--green",
+    4 => "mdl-color-text--red",
+    5 => "mdl-color-text--green"
+  );
+
+  public static $stateTooltips = array(
+    0 => "Pendiente de revisión",
+    1 => "Rechazada al revisar",
+    2 => "Programada",
+    3 => "Registrada",
+    4 => "Invalidada manualmente",
+    5 => "Validada"
+  );
+
+  public static $statesOrderForFilters = [0, 1, 4, 2, 3, 5];
+
+  public static $invalidStates = [incidents::STATE_REJECTED, incidents::STATE_MANUALLY_INVALIDATED];
+
+  public static $cannotEditCommentsStates = [];
+  public static $cannotEditStates = [self::STATE_VALIDATED_BY_WORKER, self::STATE_REGISTERED, self::STATE_MANUALLY_INVALIDATED, self::STATE_REJECTED];
+  public static $canRemoveStates = [self::STATE_SCHEDULED];
+  public static $canInvalidateStates = [self::STATE_VALIDATED_BY_WORKER, self::STATE_REGISTERED];
+  public static $workerCanEditStates = [self::STATE_UNVERIFIED];
+  public static $workerCanRemoveStates = [self::STATE_UNVERIFIED];
+
+  public static $adminPendingWhere = "i.verified = 0 AND i.confirmedby = -1";
+  public static $workerPendingWhere = "i.workervalidated = 0";
+  public static $activeWhere = "i.verified = 1 AND i.invalidated = 0";
+  public static $notInvalidatedOrRejectedWhere = "i.invalidated = 0 AND (i.verified = 1 OR i.confirmedby = -1)";
+
+  const FILTER_TYPE_ARRAY = 0;
+  const FILTER_TYPE_INT = 1;
+  const FILTER_TYPE_STRING = 2;
+
+  public static $filters = ["begins", "ends", "types", "states", "attachments", "details", "workerdetails"];
+  public static $filtersType = [
+    "begins" => self::FILTER_TYPE_STRING,
+    "ends" => self::FILTER_TYPE_STRING,
+    "types" => self::FILTER_TYPE_ARRAY,
+    "states" => self::FILTER_TYPE_ARRAY,
+    "attachments" => self::FILTER_TYPE_ARRAY,
+    "details" => self::FILTER_TYPE_ARRAY,
+    "workerdetails" => self::FILTER_TYPE_ARRAY
+  ];
+
+  public static $filtersSwitch = ["attachments", "details", "workerdetails"];
+  public static $filtersSwitchOptions = [
+    0 => "Sin",
+    1 => "Con"
+  ];
+  public static $filtersSwitchHelper = [
+    "attachments" => "archivo adjunto",
+    "details" => "observaciones de un administrador",
+    "workerdetails" => "observaciones del trabajador"
+  ];
+  public static $filtersSwitchMysqlField = [
+    "attachments" => "attachments",
+    "details" => "details",
+    "workerdetails" => "workerdetails"
+  ];
+
+  public static function getTypes($showHidden = true, $isForWorker = false) {
+    global $con, $conf;
+
+    $whereConditions = [];
+    if (!$showHidden) $whereConditions[] = "hidden = 0";
+    if ($isForWorker) $whereConditions[] = "workerfill = 1";
+
+    $where = (count($whereConditions) ? " WHERE ".implode(" AND ",$whereConditions) : "");
+
+    $query = mysqli_query($con, "SELECT * FROM typesincidents".$where." ORDER BY ".($conf["debug"] ? "id ASC" : "hidden ASC, name ASC"));
+
+    $incidents = [];
+
+    while ($row = mysqli_fetch_assoc($query)) {
+      $incidents[] = $row;
+    }
+
+    return $incidents;
+  }
+
+  public static function getTypesForm($isForWorker = false) {
+    return self::getTypes(false, $isForWorker);
+  }
+
+  public static function getType($id) {
+    global $con;
+
+    $sid = (int)$id;
+
+    $query = mysqli_query($con, "SELECT * FROM typesincidents WHERE id = $sid");
+
+    if (!mysqli_num_rows($query)) return false;
+
+    return mysqli_fetch_assoc($query);
+  }
+
+  public static function addType($name, $present, $paid, $workerfill, $notifies, $autovalidates, $hidden) {
+    global $con;
+
+    $sname = db::sanitize($name);
+    $spresent = (int)$present;
+    $spaid = (int)$paid;
+    $sworkerfill = (int)$workerfill;
+    $snotifies = (int)$notifies;
+    $sautovalidates = (int)$autovalidates;
+    $shidden = (int)$hidden;
+
+    return mysqli_query($con, "INSERT INTO typesincidents (name, present, paid, workerfill, notifies, autovalidates, hidden) VALUES ('$name', $spresent, $spaid, $sworkerfill, $snotifies, $sautovalidates, $shidden)");
+  }
+
+  public static function editType($id, $name, $present, $paid, $workerfill, $notifies, $autovalidates, $hidden) {
+    global $con;
+
+    $sid = (int)$id;
+    $sname = db::sanitize($name);
+    $spresent = (int)$present;
+    $spaid = (int)$paid;
+    $sworkerfill = (int)$workerfill;
+    $snotifies = (int)$notifies;
+    $sautovalidates = (int)$autovalidates;
+    $shidden = (int)$hidden;
+
+    return mysqli_query($con, "UPDATE typesincidents SET name = '$name', present = $spresent, paid = $spaid, workerfill = $sworkerfill, notifies = $snotifies, autovalidates = $sautovalidates, hidden = $shidden WHERE id = $sid LIMIT 1");
+  }
+
+  public static function addWhereConditionsHandledBySelect($select, &$whereConditions, $alreadyFilteredDates = false) {
+    if (!$alreadyFilteredDates) {
+      if ($select["enabled"]["begins"]) $whereConditions[] = "i.day >= ".(int)common::getTimestampFromRFC3339($select["selected"]["begins"]);
+      if ($select["enabled"]["ends"]) $whereConditions[] = "i.day <= ".(int)common::getTimestampFromRFC3339($select["selected"]["ends"]);
+    }
+
+    if ($select["enabled"]["types"]) {
+      $insideconditions = [];
+      foreach ($select["selected"]["types"] as $type) {
+        $insideconditions[] = "i.type = ".(int)$type;
+      }
+      $whereConditions[] = "(".implode(" OR ", $insideconditions).")";
+    }
+
+    foreach (self::$filtersSwitch as $f) {
+      if ($select["enabled"][$f]) {
+        foreach ($select["selected"][$f] as $onoff) {
+          $mysqlField = self::$filtersSwitchMysqlField[$f];
+
+          if ($onoff == "0") $insideconditions[] = "($mysqlField IS NULL OR $mysqlField = ''".($f == "attachments" ? " OR $mysqlField = '[]'" : "").")";
+          else if ($onoff == "1") $insideconditions[] = "($mysqlField IS NOT NULL AND $mysqlField <> ''".($f == "attachments" ? " AND $mysqlField <> '[]'" : "").")";
+        }
+        if (count($insideconditions) == 1) $whereConditions[] = $insideconditions[0];
+      }
+    }
+  }
+
+  public static function incidentIsWantedAccordingToSelect($select, $incident) {
+    if ($select["enabled"]["states"]) {
+      return in_array($incident["state"], $select["selected"]["states"]);
+    }
+
+    return true;
+  }
+
+  public static function todayPage($limit = self::PAGINATION_LIMIT) {
+    global $con;
+
+    $today = (int)common::getDayTimestamp(time());
+
+    $query = mysqli_query($con, "SELECT COUNT(*) count FROM incidents i WHERE i.day > $today");
+    if ($query === false) return 0;
+
+    $row = mysqli_fetch_assoc($query);
+    $first = $row["count"];
+
+    return floor($first/(int)$limit) + 1;
+  }
+
+  public static function numRows($select = null, $onlyAdminPending = false) {
+    global $con;
+
+    $whereConditions = [];
+
+    if ($select !== null) {
+      if (!$select["showResultsPaginated"]) return 0;
+
+      self::addWhereConditionsHandledBySelect($select, $whereConditions);
+    }
+
+    if ($onlyAdminPending) $whereConditions[] = self::$adminPendingWhere;
+
+    $where = (count($whereConditions) ? " WHERE ".implode(" AND ", $whereConditions) : "");
+    $query = mysqli_query($con, "SELECT COUNT(*) count FROM incidents i".$where);
+
+    if (!mysqli_num_rows($query)) return false;
+
+    $row = mysqli_fetch_assoc($query);
+
+    return (isset($row["count"]) ? (int)$row["count"] : false);
+  }
+
+  public static function numPending() {
+    return self::numRows(null, true);
+  }
+
+  public static function getStatus(&$incident, $beginsdatetime = -1) {
+    if ($beginsdatetime == -1) {
+      $date = new DateTime();
+      $date->setTimestamp($incident["day"]);
+      $dateFormat = $date->format("Y-m-d");
+
+      $begins = new DateTime($dateFormat."T".schedules::sec2time((int)$incident["begins"], false).":00");
+      $beginsdatetime = $begins->getTimestamp();
+    }
+
+    $time = time();
+
+    if ($incident["verified"] == 0 && $incident["confirmedby"] == -1) return self::STATE_UNVERIFIED;
+    elseif ($incident["verified"] == 0) return self::STATE_REJECTED;
+    elseif ($incident["invalidated"] == 1) return self::STATE_MANUALLY_INVALIDATED;
+    elseif ($beginsdatetime >= $time) return self::STATE_SCHEDULED;
+    elseif ($incident["workervalidated"] == 1) return self::STATE_VALIDATED_BY_WORKER;
+    else return self::STATE_REGISTERED;
+  }
+
+  public static function getAttachmentsFromIncident(&$incident) {
+    if (empty($incident["attachments"]) || $incident["attachments"] === null) return [];
+
+    $json = json_decode($incident["attachments"], true);
+    if (json_last_error() !== JSON_ERROR_NONE) return false;
+
+    return $json;
+  }
+
+  public static function getAttachments($id) {
+    global $con;
+
+    $sid = (int)$id;
+
+    $query = mysqli_query($con, "SELECT attachments FROM incidents WHERE id = ".$sid);
+    if (!mysqli_num_rows($query)) return false;
+
+    $incident = mysqli_fetch_assoc($query);
+
+    return self::getAttachmentsFromIncident($incident);
+  }
+
+  public static function addAttachment($id, &$file) {
+    global $con;
+
+    $name = "";
+    $status = files::uploadFile($file, $name);
+    if ($status !== 0) return $status;
+
+    $attachments = self::getAttachments($id);
+    $attachments[] = $name;
+
+    $sid = (int)$id;
+    $srawAttachments = db::sanitize(json_encode($attachments));
+
+    return (mysqli_query($con, "UPDATE incidents SET attachments = '".$srawAttachments."' WHERE id = $sid LIMIT 1") ? 0 : 1);
+  }
+
+  public static function deleteAttachment($id, $name = "ALL") {
+    global $con;
+
+    $incident = incidents::get($id, true);
+    if ($incident === false) return false;
+
+    $attachments = incidents::getAttachmentsFromIncident($incident);
+
+    if ($attachments === false) return false;
+    if (!count($attachments)) return ($name == "ALL");
+
+    $flag = false;
+
+    foreach ($attachments as $i => $attachment) {
+      if ($attachment == $name || $name === "ALL") {
+        $flag = true;
+
+        if (!files::removeFile($attachment)) return false;
+
+        unset($attachments[$i]);
+        $attachments = array_values($attachments);
+
+        $sid = (int)$id;
+        $srawAttachments = db::sanitize(json_encode($attachments));
+      }
+    }
+
+    return ($flag ? mysqli_query($con, "UPDATE incidents SET attachments = '".$srawAttachments."' WHERE id = $sid LIMIT 1") : false);
+  }
+
+  private static function magicIncident(&$row) {
+    $row["allday"] = ($row["begins"] == self::STARTOFDAY && $row["ends"] == self::ENDOFDAY);
+    $row["updatestate"] = ($row["updatedby"] == -1 ? self::UPDATE_STATE_NOT_UPDATED : self::UPDATE_STATE_UPDATED);
+
+    $date = new DateTime();
+    $date->setTimestamp($row["day"]);
+    $dateFormat = $date->format("Y-m-d");
+
+    $begins = new DateTime($dateFormat."T".schedules::sec2time((int)$row["begins"], false).":00");
+    $row["beginsdatetime"] = $begins->getTimestamp();
+    $ends = new DateTime($dateFormat."T".schedules::sec2time((int)$row["ends"], false).":00");
+    $row["endsdatetime"] = $ends->getTimestamp();
+
+    $row["state"] = self::getStatus($row, $row["beginsdatetime"]);
+  }
+
+  public static function get($id, $magic = false) {
+    global $con, $conf;
+
+    $sid = $id;
+
+    $query = mysqli_query($con, "SELECT * FROM incidents WHERE id = $sid");
+
+    if (!mysqli_num_rows($query)) return false;
+
+    $row = mysqli_fetch_assoc($query);
+    if ($magic) self::magicIncident($row);
+
+    return $row;
+  }
+
+  public static function getAll($onlyAdminPending = false, $start = 0, $limit = self::PAGINATION_LIMIT, $worker = "ALL", $begins = null, $ends = null, $onlyWorkerPending = false, $select = false) {
+    global $con, $conf;
+
+    $whereConditions = [];
+    if ($onlyAdminPending) $whereConditions[] = self::$adminPendingWhere;
+    if ($onlyWorkerPending) {
+      $whereConditions[] = self::$workerPendingWhere;
+      $whereConditions[] = self::$activeWhere;
+    }
+    if ($worker !== "ALL") $whereConditions[] = "i.worker = ".(int)$worker;
+    if ($begins !== null && $ends !== null) {
+      $whereConditions[] = "i.day <= ".(int)$ends." AND i.day >= ".(int)$begins;
+      $filteredDates = true;
+    } else $filteredDates = false;
+
+    if ($select !== false) self::addWhereConditionsHandledBySelect($select, $whereConditions, $filteredDates);
+
+    $where = (count($whereConditions) ? " WHERE ".implode(" AND ", $whereConditions) : "");
+
+    $query = mysqli_query($con, "SELECT i.*, t.id typeid, t.name typename, t.present typepresent, t.paid typepaid, t.workerfill typeworkerfill, t.notifies typenotifies, t.hidden typehidden, p.id personid, p.name workername, w.company companyid FROM incidents i LEFT JOIN typesincidents t ON i.type = t.id LEFT JOIN workers w ON i.worker = w.id LEFT JOIN people p ON w.person = p.id".$where." ORDER BY i.day DESC".db::limitPagination($start, $limit));
+
+    $return = [];
+    while ($row = mysqli_fetch_assoc($query)) {
+      self::magicIncident($row);
+      if ($onlyWorkerPending && $row["state"] !== self::STATE_REGISTERED) continue;
+      if ($select !== false && !self::incidentIsWantedAccordingToSelect($select, $row)) continue;
+      $return[] = $row;
+    }
+
+    return $return;
+  }
+
+  public static function checkOverlap($worker, $day, $begins, $ends, $sans = 0) {
+    global $con;
+
+    $sworker = (int)$worker;
+    $sday = (int)$day;
+    $sbegins = (int)$begins;
+    $sends = (int)$ends;
+    $ssans = (int)$sans;
+
+    $query = mysqli_query($con, "SELECT * FROM incidents i WHERE begins <= $sends AND ends >= $sbegins AND day = $sday AND worker = $sworker".($sans == 0 ? "" : " AND id <> $ssans")." AND ".self::$notInvalidatedOrRejectedWhere." LIMIT 1");
+
+    return (mysqli_num_rows($query) > 0);
+  }
+
+  public static function add($worker, $type, $details, $iday, $begins, $ends, $creator = "ME", $verified = 1, $alreadyTimestamp = false, $isWorkerView = false, $sendEmail = true, $forceAutoValidate = false) {
+    global $con, $conf;
+
+    // Gets information about the worker
+    $sworker = (int)$worker;
+    $workerInfo = workers::get($sworker);
+    if ($workerInfo === false) return 1;
+
+    // Sets who is the incident creator
+    if ($creator === "ME") $creator = people::userData("id");
+    $screator = (int)$creator;
+
+    // If the user is not an admin and the person to who we are going to add the incident is not the creator of the incident, do not continue
+    if (!security::isAllowed(security::ADMIN) && $workerInfo["person"] != $creator) return 5;
+
+    // Sanitizes other incident fields
+    $stype = (int)$type;
+    $sverified = (int)$verified;
+    $sdetails = db::sanitize($details);
+
+    // Gets information about the incident type
+    $incidenttype = self::getType($stype);
+    if ($incidenttype === false) return -1;
+
+    // Gets the timestamp of the incident day
+    if ($alreadyTimestamp) {
+      $sday = (int)$iday;
+    } else {
+      $day = new DateTime($iday);
+      $sday = (int)$day->getTimestamp();
+    }
+
+    // Gets the start and end times, and checks whether they are well-formed
+    $sbegins = (int)$begins;
+    $sends = (int)$ends;
+    if ($sbegins >= $sends) return 3;
+
+    // Checks whether the incident overlaps another incident
+    if (self::checkOverlap($worker, $sday, $begins, $ends)) return 2;
+
+    // Adds the incident
+    if (!mysqli_query($con, "INSERT INTO incidents (worker, creator, type, day, begins, ends, ".($isWorkerView ? "workerdetails" : "details").", verified) VALUES ($sworker, $screator, $stype, $sday, $sbegins, $sends, '$sdetails', $sverified)")) return -1;
+
+    // If the incident type is set to autovalidate or we pass the parameter to force the autovalidation, autovalidate it
+    if (($incidenttype["autovalidates"] == 1 || $forceAutoValidate) && !validations::validateIncident(mysqli_insert_id($con), validations::METHOD_AUTOVALIDATION, "ME", false, false)) {
+      return 5;
+    }
+
+    // Bonus: check whether we should send email notifications, and if applicable send them
+    if ($sendEmail && $conf["mail"]["enabled"] && ($conf["mail"]["capabilities"]["notifyOnWorkerIncidentCreation"] || $conf["mail"]["capabilities"]["notifyOnAdminIncidentCreation"] || $conf["mail"]["capabilities"]["notifyCategoryResponsiblesOnIncidentCreation"])) {
+      $workerName = people::workerData("name", $sworker);
+
+      $to = [];
+      if (($conf["mail"]["capabilities"]["notifyOnWorkerIncidentCreation"] && $isWorkerView) || ($conf["mail"]["capabilities"]["notifyOnAdminIncidentCreation"] && !$isWorkerView)) {
+        $to[] = array("email" => $conf["mail"]["adminEmail"]);
+      }
+
+      if ($conf["mail"]["capabilities"]["notifyCategoryResponsiblesOnIncidentCreation"] && $incidenttype["notifies"] == 1) {
+        $categoryid = people::workerData("category", $sworker);
+        $category = categories::get($categoryid);
+        if ($category === false) return 0;
+
+        $emails = json_decode($category["emails"], true);
+        if (json_last_error() === JSON_ERROR_NONE) {
+          foreach ($emails as $email) {
+            $to[] = array("email" => $email);
+          }
+        }
+      }
+
+      if (!count($to)) return 0;
+
+      $subject = "Incidencia del tipo \"".security::htmlsafe($incidenttype["name"])."\" creada para ".security::htmlsafe($workerName)." el ".strftime("%d %b %Y", $sday);
+      $body = mail::bodyTemplate("<p>Hola,</p>
+      <p>Este es un mensaje automático para avisarte de que ".security::htmlsafe(people::userData("name"))." ha introducido la siguiente incidencia en el sistema de registro horario:</p>
+      <ul>
+        <li><b>Trabajador:</b> ".security::htmlsafe($workerName)."</li>
+        <li><b>Motivo:</b> ".security::htmlsafe($incidenttype["name"])."</li>
+        <li><b>Fecha:</b> ".strftime("%d %b %Y", $sday)." ".(($sbegins == 0 && $sends == self::ENDOFDAY) ? "(todo el día)": schedules::sec2time($sbegins)."-".schedules::sec2time($sends))."</li>".
+        (!empty($details) ? "<li><b>Observaciones:</b> <span style='white-space: pre-wrap;'>".security::htmlsafe($details)."</span></li>" : "").
+      "</ul>
+      <p style='font-size: 11px;'>Has recibido este mensaje porque estás configurado como persona responsable de la categoría a la que pertenece este trabajador o eres el administrador del sistema.</p>");
+
+      return (mail::send($to, [], $subject, $body) ? 0 : 4);
+    }
+
+    return 0;
+  }
+
+  public static function edit($id, $type, $iday, $begins, $ends, $updatedby = "ME") {
+    global $con;
+
+    $sid = (int)$id;
+
+    $incident = incidents::get($id);
+    if ($incident === false) return 1;
+
+    $stype = (int)$type;
+    if ($updatedby === "ME") $updatedby = people::userData("id");
+    $supdatedby = (int)$updatedby;
+
+    $day = new DateTime($iday);
+    $sday = (int)$day->getTimestamp();
+
+    $sbegins = (int)$begins;
+    $sends = (int)$ends;
+    if ($sbegins >= $sends) return 3;
+    if (self::checkOverlap($incident["worker"], $sday, $begins, $ends, $id)) return 2;
+
+    return (mysqli_query($con, "UPDATE incidents SET type = $stype, day = $sday, begins = $sbegins, ends = $sends, updatedby = $supdatedby, workervalidated = 0, workervalidation = '' WHERE id = $sid LIMIT 1") ? 0 : -1);
+  }
+
+  public static function editDetails($id, $details, $updatedby = "ME") {
+    global $con;
+
+    $sid = (int)$id;
+    $sdetails = db::sanitize($details);
+    if ($updatedby === "ME") $updatedby = people::userData("id");
+    $supdatedby = (int)$updatedby;
+
+    $incident = self::get($sid);
+    if ($incident === false) return -1;
+
+    $status = self::getStatus($incident);
+    if (in_array($status, self::$cannotEditCommentsStates)) return 1;
+
+    return (mysqli_query($con, "UPDATE incidents SET details = '$sdetails', updatedby = '$supdatedby' WHERE id = $sid LIMIT 1") ? 0 : -1);
+  }
+
+  public static function verify($id, $value, $confirmedby = "ME") {
+    global $con, $conf;
+
+    $sid = (int)$id;
+    $svalue = ($value == 1 ? 1 : 0);
+    if ($confirmedby === "ME") $confirmedby = people::userData("id");
+    $sconfirmedby = (int)$confirmedby;
+
+    $incident = incidents::get($id);
+    if ($incident === false) return false;
+
+    $state = incidents::getStatus($incident);
+    if ($state != incidents::STATE_UNVERIFIED) return false;
+
+    if (!mysqli_query($con, "UPDATE incidents SET verified = $svalue, confirmedby = $sconfirmedby WHERE id = $sid LIMIT 1")) return false;
+
+    if ($conf["mail"]["enabled"] && $conf["mail"]["capabilities"]["notifyWorkerOnIncidentDecision"]) {
+      $workerEmail = people::workerData("email", $incident["worker"]);
+      if ($workerEmail !== false && !empty($workerEmail)) {
+        $workerName = people::workerData("name", $incident["worker"]);
+
+        $to = [array(
+          "email" => $workerEmail,
+          "name" => $workerName
+        )];
+        $subject = "Incidencia del ".strftime("%d %b %Y", $incident["day"])." ".($value == 1 ? "verificada" : "rechazada");
+        $body = mail::bodyTemplate("<p>Bienvenido ".security::htmlsafe($workerName).",</p>
+        <p>Este es un mensaje automático para avisarte de que la incidencia que introduciste para el día ".strftime("%d de %B de %Y", $incident["day"])." ha sido ".($value == 1 ? "aceptada" : "rechazada").".</p><p>Puedes ver el estado de todas tus incidencias en el <a href='".security::htmlsafe($conf["fullPath"])."'>aplicativo web</a>.</p>");
+
+        mail::send($to, [], $subject, $body);
+      }
+    }
+
+    return true;
+  }
+
+  public static function remove($id) {
+    global $con;
+
+    $sid = (int)$id;
+
+    $incident = incidents::get($id);
+    if ($incident === false) return false;
+
+    if (!self::deleteAttachment($id, "ALL")) return false;
+
+    return mysqli_query($con, "DELETE FROM incidents WHERE id = $sid LIMIT 1");
+  }
+
+  public static function invalidate($id, $updatedby = "ME") {
+    global $con;
+
+    $sid = (int)$id;
+    if ($updatedby === "ME") $updatedby = people::userData("id");
+    $supdatedby = (int)$updatedby;
+
+    $incident = incidents::get($id);
+    if ($incident === false) return false;
+
+    $state = incidents::getStatus($incident);
+    if (!in_array($state, self::$canInvalidateStates)) return false;
+
+    return mysqli_query($con, "UPDATE incidents SET invalidated = 1, updatedby = $supdatedby WHERE id = $sid LIMIT 1");
+  }
+
+  public static function checkIncidentIsFromPerson($incident, $person = "ME", $boolean = false) {
+    global $con;
+
+    if ($person == "ME") $person = people::userData("id");
+
+    $sincident = (int)$incident;
+    $sperson = (int)$person;
+
+    $query = mysqli_query($con, "SELECT i.id FROM incidents i INNER JOIN workers w ON i.worker = w.id INNER JOIN people p ON w.person = p.id WHERE p.id = $sperson AND i.id = $sincident LIMIT 1");
+
+    if (!mysqli_num_rows($query)) {
+      if ($boolean) return false;
+      security::denyUseMethod(security::METHOD_NOTFOUND);
+    }
+
+    return true;
+  }
+}
diff --git a/src/inc/incidentsView.php b/src/inc/incidentsView.php
new file mode 100644
index 0000000..ca646cb
--- /dev/null
+++ b/src/inc/incidentsView.php
@@ -0,0 +1,477 @@
+<?php
+class incidentsView {
+  public static $limitOptions = [10, 15, 20, 30, 40, 50];
+
+  public static $incidentsMsgs = [
+    ["added", "Se ha añadido la incidencia correctamente."],
+    ["modified", "Se ha modificado la incidencia correctamente."],
+    ["removed", "Se ha eliminado la incidencia correctamente."],
+    ["invalidated", "Se ha invalidado la incidencia correctamente."],
+    ["empty", "Faltan datos por introducir en el formulario."],
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."],
+    ["cannotmodify", "No se pueden modificar la incidencia porque ya ha sido registrada o se ha invalidado manualmente."],
+    ["verified1", "Se ha verificado la incidencia correctamente."],
+    ["verified0", "Se ha rechazado la incidencia correctamente."],
+    ["overlap", "La incidencia no se ha añadido porque se solapa con otra incidencia del mismo trabajador."],
+    ["order", "La hora de inicio debe ser anterior a la hora de fin."],
+    ["addedemailnotsent", "La incidencia se ha añadido, pero no se ha podido enviar un correo de notificación a los responsables de la categoría del trabajador. Por favor, notifica a estas personas manualmente."],
+    ["filesize", "El tamaño del archivo adjunto es demasiado grande (el límite es de ".files::READABLE_MAX_SIZE.")"],
+    ["filetype", "El formato del archivo no está soportado."],
+    ["attachmentadded", "Se ha añadido el archivo adjunto correctamente."],
+    ["attachmentdeleted", "Se ha eliminado el archivo adjunto correctamente."],
+    ["addedrecurring", "Se han añadido todas las incidencias pertinentes correctamente."],
+    ["unexpectedrecurring", "Ha habido algún problema añadiendo alguna(s) o todas las incidencias que se tenían que crear."],
+    ["addednotautovalidated", "La incidencia se ha añadido, pero no se ha podido autovalidar."],
+    ["deleteincidentsbulksuccess", "Las incidencias se han eliminado/invalidado correctamente."],
+    ["deleteincidentsbulkpartialsuccess", "Algunas incidencias (o todas) no se han podido eliminar/invalidar. Por favor, comprueba el resultado de la acción."]
+  ];
+
+  public static function renderIncidents(&$incidents, &$companies, $scrollable = false, $showPersonAndCompany = true, $isForWorker = false, $isForValidationView = false, $isForMassEdit = false, $continueUrl = "incidents.php") {
+    global $conf, $renderIncidentsAutoIncremental;
+    if (!isset($renderIncidentsAutoIncremental)) $renderIncidentsAutoIncremental = 0;
+    $menu = "";
+
+    $safeContinueUrl = security::htmlsafe(urlencode($continueUrl));
+
+    if ($isForMassEdit) {
+      ?>
+      <div class="left-actions">
+        <button id="deleteincidentsbulk" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect"><i class="material-icons icon">delete</i></button>
+      </div>
+      <?php
+    }
+    ?>
+    <div class="mdl-shadow--2dp overflow-wrapper incidents-wrapper<?=($scrollable ? " incidents-wrapper--scrollable" : "")?>">
+      <table class="incidents">
+        <?php
+        if ($isForValidationView || $isForMassEdit) {
+          ?>
+          <tr class="artificial-height">
+            <?php if ($conf["debug"]) echo "<td></td>"; ?>
+            <td class="icon-cell has-checkbox">
+              <label for="checkboxall_i_<?=$renderIncidentsAutoIncremental?>" class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" data-check-all="true">
+                <input type="checkbox" id="checkboxall_i_<?=$renderIncidentsAutoIncremental?>" class="mdl-checkbox__input" autocomplete="off">
+              </label>
+            </td>
+            <?php if ($conf["debug"]) echo "<td></td>"; ?>
+            <td></td>
+            <?php if (!$isForValidationView) echo "<td></td>"; ?>
+            <td></td>
+            <?php if ($showPersonAndCompany) echo "<td></td><td></td>"; ?>
+            <td></td>
+          </tr>
+          <?php
+        }
+
+        foreach ($incidents as $incident) {
+          $id = (int)$incident["id"]."_".(int)$renderIncidentsAutoIncremental;
+          $canEdit = (!$isForWorker && !in_array($incident["state"], incidents::$cannotEditStates)) || ($isForWorker && in_array($incident["state"], incidents::$workerCanEditStates));
+          $canRemove = (!$isForWorker && in_array($incident["state"], incidents::$canRemoveStates)) || ($isForWorker && in_array($incident["state"], incidents::$workerCanRemoveStates));
+          $canInvalidate = !$isForWorker && in_array($incident["state"], incidents::$canInvalidateStates);
+          $attachments = count(incidents::getAttachmentsFromIncident($incident));
+          ?>
+          <tr<?=(in_array($incident["state"], incidents::$invalidStates) ? ' class="mdl-color-text--grey-700 line-through"' : '')?>>
+            <?php if ($conf["debug"]) { ?><td><?=(int)$incident["id"]?></td><?php } ?>
+            <?php
+            if ($isForValidationView || $isForMassEdit) {
+              ?>
+              <td class="icon-cell has-checkbox">
+                <label for="checkbox_i_<?=$id?>" class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect">
+                  <input type="checkbox" id="checkbox_i_<?=$id?>" data-incident="<?=(int)$incident["id"]?>" class="mdl-checkbox__input"<?=($isForMassEdit && !in_array($incident["state"], incidents::$canRemoveStates) && !in_array($incident["state"], incidents::$canInvalidateStates) ? " disabled" : "")?> autocomplete="off">
+                </label>
+              </td>
+              <?php
+            }
+
+            if (!$isForValidationView) {
+              ?>
+              <td class="icon-cell">
+                <i id="state<?=$id?>" class="material-icons <?=security::htmlsafe(incidents::$stateIconColors[$incident["state"]])?>"><?=security::htmlsafe(incidents::$stateIcons[$incident["state"]])?></i>
+              </td>
+              <?php
+              visual::addTooltip("state".$id, security::htmlsafe(incidents::$stateTooltips[$incident["state"]]));
+            }
+            ?>
+            <td class="can-strike"><?=security::htmlsafe($incident["typename"])?></td>
+            <?php if ($showPersonAndCompany) {
+              ?>
+              <td class="can-strike"><span data-dyndialog-href="dynamic/user.php?id=<?=(int)$incident["personid"]?>"><?=security::htmlsafe($incident["workername"])?></span></td>
+              <td class="can-strike"><?=security::htmlsafe($companies[$incident["companyid"]])?></td>
+              <?php
+            }
+            ?>
+            <td class="can-strike"><?=strftime("%d %b %Y", $incident["day"])." ".security::htmlsafe($incident["allday"] ? "(todo el día)" : schedules::sec2time($incident["begins"])."-".schedules::sec2time($incident["ends"]))?></td>
+            <td>
+              <a href="dynamic/editincidentcomment.php?id=<?=(int)$incident["id"]?>&continue=<?=$safeContinueUrl?>" data-dyndialog-href="dynamic/editincidentcomment.php?id=<?=(int)$incident["id"]?>&continue=<?=$safeContinueUrl?>" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect" title="Ver/editar las observaciones"><i class="material-icons icon"><?=(empty($incident["details"]) && empty($incident["workerdetails"]) ? "mode_" : "")?>comment</i></a>
+              <span<?=($attachments > 0 ? ' class="mdl-badge mdl-badge--overlap" data-badge="'.$attachments.'"' : '')?>><a href="dynamic/incidentattachments.php?id=<?=(int)$incident["id"]?>&continue=<?=$safeContinueUrl?>" data-dyndialog-href="dynamic/incidentattachments.php?id=<?=(int)$incident["id"]?>&continue=<?=$safeContinueUrl?>" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect" title="Ver/gestionar los archivos adjuntos"><i class="material-icons icon">attach_file</i></a></span>
+              <button id="actions<?=$id?>" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect custom-actions-btn"><i class="material-icons icon">more_vert</i></button>
+              <?php
+              $menu .= '<ul class="mdl-menu mdl-menu--unaligned mdl-js-menu mdl-js-ripple-effect" for="actions'.$id.'">';
+
+              if ($canEdit) $menu .= '<a href="dynamic/editincident.php?id='.(int)$incident["id"].'&continue='.$safeContinueUrl.'" data-dyndialog-href="dynamic/editincident.php?id='.(int)$incident["id"].'&continue='.$safeContinueUrl.'"><li class="mdl-menu__item">Editar</li></a>';
+
+              if ($canRemove) $menu .= '<a href="dynamic/deleteincident.php?id='.(int)$incident["id"].'&continue='.$safeContinueUrl.'" data-dyndialog-href="dynamic/deleteincident.php?id='.(int)$incident["id"].'&continue='.$safeContinueUrl.'"><li class="mdl-menu__item">Eliminar</li></a>';
+
+              if ($canInvalidate) $menu .= '<a href="dynamic/invalidateincident.php?id='.(int)$incident["id"].'&continue='.$safeContinueUrl.'" data-dyndialog-href="dynamic/invalidateincident.php?id='.(int)$incident["id"].'&continue='.$safeContinueUrl.'"><li class="mdl-menu__item">Invalidar</li></a>';
+
+              $menu .= '<a href="dynamic/authorsincident.php?id='.(int)$incident["id"].'&continue='.$safeContinueUrl.'" data-dyndialog-href="dynamic/authorsincident.php?id='.(int)$incident["id"].'&continue='.$safeContinueUrl.'"><li class="mdl-menu__item">Autoría</li></a></ul>';
+
+              if ($incident["state"] == incidents::STATE_UNVERIFIED && !$isForWorker) {
+                ?>
+                <form action="doverifyincident.php" method="POST" class="verification-actions">
+                  <input type="hidden" name="id" value="<?=(int)$incident["id"]?>">
+                  <?php visual::addContinueInput($continueUrl); ?>
+                  <button name="value" value="1" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect"><i class="material-icons mdl-color-text--green">check</i></button><button name="value" value="0" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect"><i class="material-icons mdl-color-text--red">close</i></button>
+                </form>
+                <?php
+              }
+              ?>
+            </td>
+          </tr>
+          <?php
+          $renderIncidentsAutoIncremental++;
+        }
+        ?>
+      </table>
+      <?php echo $menu; ?>
+    </div>
+  <?php
+  }
+
+  public static function renderIncidentForm(&$workers, $valueFunction, $textFunction, &$companies, $isForWorker = false, $recurrent = false, $continueUrl = false) {
+    $prefix = ($recurrent ? "recurring" : "");
+    ?>
+    <script>
+    window.addEventListener("load", e1 => {
+      document.getElementById("<?=$prefix?>allday").addEventListener("change", e => {
+        var partialtime = document.getElementById("<?=$prefix?>partialtime");
+        if (e.target.checked) {
+          partialtime.classList.add("notvisible");
+        } else {
+          partialtime.classList.remove("notvisible");
+        }
+      });
+      <?php
+      if ($recurrent) {
+        ?>
+        var defaultFields = [
+          {
+            "name": "day",
+            "min": 0,
+            "max": 4
+          },
+          {
+            "name": "type",
+            "min": 1,
+            "max": 2
+          }
+        ];
+
+        defaultFields.forEach(field => {
+          for (var i = field.min; i <= field.max; i++) {
+            document.querySelector("[for=\""+field.name+"-"+i+"\"]").MaterialCheckbox.check();
+
+            var checkbox = document.getElementById(field.name+"-"+i);
+            checkbox.checked = true;
+            if ("createEvent" in document) {
+              var evt = document.createEvent("HTMLEvents");
+              evt.initEvent("change", false, true);
+              checkbox.dispatchEvent(evt);
+            }
+          }
+        });
+        <?php
+      }
+      ?>
+    });
+    </script>
+    <dialog class="mdl-dialog" id="add<?=$prefix?>incident">
+      <form action="doadd<?=$prefix?>incident.php" method="POST" autocomplete="off">
+        <?php
+        if ($continueUrl !== false) visual::addContinueInput($continueUrl);
+        helpView::renderHelpButton(help::PLACE_INCIDENT_FORM, true, true);
+        ?>
+        <h4 class="mdl-dialog__title">Añade una incidencia<?=($recurrent ? " recurrente" : "")?></h4>
+        <div class="mdl-dialog__content">
+          <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+            <select name="worker" id="<?=$prefix?>worker" class="mdlext-selectfield__select" data-required>
+              <option></option>
+              <?php
+              foreach ($workers as $worker) {
+                if ($worker["hidden"] == 1) continue;
+                echo '<option value="'.security::htmlsafe($valueFunction($worker, $companies)).'">'.security::htmlsafe($textFunction($worker, $companies)).'</option>';
+              }
+              ?>
+            </select>
+            <label for="<?=$prefix?>worker" class="mdlext-selectfield__label">Trabajador</label>
+          </div>
+          <br>
+          <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+            <select name="type" id="<?=$prefix?>type" class="mdlext-selectfield__select" data-required>
+              <option></option>
+              <?php
+              foreach (incidents::getTypesForm($isForWorker) as $i) {
+                echo '<option value="'.(int)$i["id"].'">'.security::htmlsafe($i["name"]).'</option>';
+              }
+              ?>
+            </select>
+            <label for="<?=$prefix?>type" class="mdlext-selectfield__label">Tipo</label>
+          </div>
+
+          <?php
+          if ($isForWorker) {
+            echo "<p>Para usar un tipo de incidencia que no esté en la lista, debes ponerte en contacto con RRHH para que rellenen ellos la incidencia manualmente.</p>";
+          }
+          ?>
+
+          <?php
+          if ($recurrent) {
+            ?>
+            <h5>Recurrencia</h5>
+            <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+              <input class="mdl-textfield__input" type="date" name="firstday" id="<?=$prefix?>firstday" autocomplete="off" data-required>
+              <label class="mdl-textfield__label always-focused" for="<?=$prefix?>firstday">Día de inicio</label>
+            </div>
+            <br>
+            <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+              <input class="mdl-textfield__input" type="date" name="lastday" id="<?=$prefix?>lastday" autocomplete="off" data-required>
+              <label class="mdl-textfield__label always-focused" for="<?=$prefix?>lastday">Día de fin</label>
+            </div>
+            <br>
+            <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+              <div id="dayMenu" class="mdlext-selectfield__select mdl-custom-selectfield__select" tabindex="0">-</div>
+              <ul class="mdl-menu mdl-menu--bottom mdl-js-menu mdl-custom-multiselect mdl-custom-multiselect-js" for="dayMenu">
+                <?php
+                foreach (calendars::$days as $id => $day) {
+                  ?>
+                  <li class="mdl-menu__item mdl-custom-multiselect__item">
+                    <label class="mdl-checkbox mdl-js-checkbox" for="day-<?=(int)$id?>">
+                      <input type="checkbox" id="day-<?=(int)$id?>" name="day[]" value="<?=(int)$id?>" data-value="<?=(int)$id?>" class="mdl-checkbox__input">
+                      <span class="mdl-checkbox__label"><?=security::htmlsafe($day)?></span>
+                    </label>
+                  </li>
+                  <?php
+                }
+                ?>
+              </ul>
+              <label for="day" class="mdlext-selectfield__label always-focused mdl-color-text--primary">Día de la semana</label>
+            </div>
+            <br>
+            <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+              <div id="dayType" class="mdlext-selectfield__select mdl-custom-selectfield__select" tabindex="0">-</div>
+              <ul class="mdl-menu mdl-menu--bottom mdl-js-menu mdl-custom-multiselect mdl-custom-multiselect-js" for="dayType">
+                <?php
+                foreach (calendars::$types as $id => $type) {
+                  if ($id == calendars::TYPE_FESTIU) continue;
+                  ?>
+                  <li class="mdl-menu__item mdl-custom-multiselect__item">
+                    <label class="mdl-checkbox mdl-js-checkbox" for="type-<?=(int)$id?>">
+                      <input type="checkbox" id="type-<?=(int)$id?>" name="daytype[]" value="<?=(int)$id?>" data-value="<?=(int)$id?>" class="mdl-checkbox__input">
+                      <span class="mdl-checkbox__label"><?=security::htmlsafe($type)?></span>
+                    </label>
+                  </li>
+                  <?php
+                }
+                ?>
+              </ul>
+              <label for="day" class="mdlext-selectfield__label always-focused mdl-color-text--primary">Tipo de día</label>
+            </div>
+            <h5>Afectación</h5>
+            <?php
+          } else {
+            ?>
+            <h5>Afectación</h5>
+            <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+              <input class="mdl-textfield__input" type="date" name="day" id="<?=$prefix?>day" autocomplete="off" data-required>
+              <label class="mdl-textfield__label always-focused" for="<?=$prefix?>day">Día</label>
+            </div>
+            <br>
+            <?php
+          }
+          ?>
+          <p>
+            <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="<?=$prefix?>allday">
+              <input type="checkbox" id="<?=$prefix?>allday" name="allday" value="1" class="mdl-switch__input">
+              <span class="mdl-switch__label">Día entero</span>
+            </label>
+          </p>
+          <div id="<?=$prefix?>partialtime">De <input type="time" name="begins"> a <input type="time" name="ends"></div>
+
+          <h5>Detalles adicionales</h5>
+          <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+            <textarea class="mdl-textfield__input" name="details" id="<?=$prefix?>details"></textarea>
+            <label class="mdl-textfield__label" for="<?=$prefix?>details">Observaciones (opcional)</label>
+          </div>
+          <?php
+          if (!$isForWorker && !$recurrent) {
+            ?>
+            <p>
+              <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="<?=$prefix?>autoverify">
+                <input type="checkbox" id="<?=$prefix?>autoverify" name="autoverify" value="1" class="mdl-switch__input" checked>
+                <span class="mdl-switch__label">Saltar la cola de revisión</span>
+              </label>
+            </p>
+            <?php
+          }
+
+          if (!$isForWorker) echo "<p>Las observaciones aparecerán en los PDFs que se exporten.</p>";
+          else echo "<p>Las observaciones serán únicamente visibles para los administradores del sistema.</p><p>Al añadir la incidencia, se guardará tu <a href=\"https://help.gnome.org/users/gnome-help/stable/net-what-is-ip-address.html.es\" target=\"_blank\" rel=\"noopener noreferrer\">dirección IP</a> y la fecha y hora actual para autovalidar la incidencia.</p>";
+          if (!$recurrent) echo '<p>Después de crear la incidencia podrás añadir archivos adjuntos haciendo clic en el botón <i class="material-icons" style="vertical-align: middle;">attach_file</i>.</p>';
+          ?>
+        </div>
+        <div class="mdl-dialog__actions">
+          <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Añadir</button>
+          <button onclick="event.preventDefault(); document.querySelector('#add<?=$prefix?>incident').close();" class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+        </div>
+      </form>
+    </dialog>
+    <?php
+  }
+
+  public static function renderFilterDialog($select) {
+    global $_GET;
+    ?>
+    <style>
+    #filter {
+      max-width: 300px;
+      width: auto;
+    }
+
+    #filter .mdl-checkbox {
+      height: auto;
+    }
+    </style>
+    <dialog class="mdl-dialog" id="filter">
+      <form action="incidents.php" method="GET" enctype="multipart/form-data">
+        <h4 class="mdl-dialog__title">Filtrar lista</h4>
+        <div class="mdl-dialog__content">
+          <h5>Por fecha</h5>
+          <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+            <input class="mdl-textfield__input" type="date" name="begins" id="begins" autocomplete="off" <?=$select["enabled"]["begins"] ? " value=\"".security::htmlsafe($select["selected"]["begins"])."\"" : ""?>>
+            <label class="mdl-textfield__label always-focused" for="begins">Fecha inicio (opcional)</label>
+          </div>
+          <br>
+          <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+            <input class="mdl-textfield__input" type="date" name="ends" id="ends" autocomplete="off" <?=$select["enabled"]["ends"] ? " value=\"".security::htmlsafe($select["selected"]["ends"])."\"" : ""?>>
+            <label class="mdl-textfield__label always-focused" for="ends">Fecha fin (opcional)</label>
+          </div>
+          <h5>Por estado <i id="tt_incidentstatusnotpaginated" class="material-icons help">info</i></h5>
+          <div class="mdl-tooltip" for="tt_incidentstatusnotpaginated">Al filtrar por estado, la página de resultados no estará paginada y saldrán todos los resultados en una misma página. La acción podría consumir bastantes recursos y tomar más tiempo del habitual.</div>
+          <?php
+          foreach (incidents::$statesOrderForFilters as $id) {
+            ?>
+            <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="state<?=(int)$id?>">
+              <input type="checkbox" id="state<?=(int)$id?>" name="states[<?=(int)$id?>]" class="mdl-checkbox__input" value="1"<?=(($select["enabled"]["states"] && in_array($id, $select["selected"]["states"])) ? " checked" : "")?>>
+              <span class="mdl-checkbox__label"><?=security::htmlsafe(incidents::$stateTooltips[$id])?></span>
+            </label>
+            <?php
+          }
+          ?>
+          <h5>Por tipo</h5>
+          <?php
+          $types = incidents::getTypes();
+          if ($types !== false) {
+            foreach ($types as $type) {
+              ?>
+              <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="type<?=(int)$type["id"]?>">
+                <input type="checkbox" id="type<?=(int)$type["id"]?>" name="types[<?=(int)$type["id"]?>]" class="mdl-checkbox__input" value="1"<?=(($select["enabled"]["types"] && in_array($type["id"], $select["selected"]["types"])) ? " checked" : "")?>>
+                <span class="mdl-checkbox__label"><?=security::htmlsafe($type["name"])?></span>
+              </label>
+              <?php
+            }
+          }
+          ?>
+          <h5>Miscelánea</h5>
+          <?php
+          foreach (incidents::$filtersSwitchHelper as $f => $helper) {
+            foreach (incidents::$filtersSwitchOptions as $value => $option) {
+              ?>
+              <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="<?=security::htmlsafe($f).(int)$value?>">
+                <input type="checkbox" id="<?=security::htmlsafe($f).(int)$value?>" name="<?=security::htmlsafe($f)?>[<?=(int)$value?>]" class="mdl-checkbox__input" value="1"<?=(($select["enabled"][$f] && in_array($value, $select["selected"][$f])) ? " checked" : "")?>>
+                <span class="mdl-checkbox__label"><?=security::htmlsafe($option." ".$helper)?></span>
+              </label>
+              <?php
+            }
+          }
+          ?>
+        </div>
+        <div class="mdl-dialog__actions">
+          <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Filtrar</button>
+          <button onclick="event.preventDefault(); document.querySelector('#filter').close();" class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+        </div>
+      </form>
+    </dialog>
+    <?php
+  }
+
+  public static function buildSelect() {
+    global $select, $_GET;
+
+    $select = array("showPendingQueue" => false, "isAnyEnabled" => false, "pageUrl" => "", "pageUrlHasParameters" => false, "showResultsPaginated" => true, "enabled" => [], "selected" => []);
+
+    $parameters = [];
+
+    foreach (incidents::$filters as $f) {
+      $fType = incidents::$filtersType[$f];
+
+      switch ($fType) {
+        case incidents::FILTER_TYPE_ARRAY:
+        $select["enabled"][$f] = isset($_GET[$f]);
+        break;
+
+        case incidents::FILTER_TYPE_INT:
+        $select["enabled"][$f] = isset($_GET[$f]) && $_GET[$f] !== "";
+        break;
+
+        case incidents::FILTER_TYPE_STRING:
+        $select["enabled"][$f] = isset($_GET[$f]) && !empty($_GET[$f]);
+        break;
+      }
+
+      if ($select["enabled"][$f]) {
+        switch ($fType) {
+          case incidents::FILTER_TYPE_ARRAY:
+          $select["selected"][$f] = (isset($_GET[$f]) ? array_keys($_GET[$f]) : []);
+          foreach ($select["selected"][$f] as $value) {
+            $parameters[] = urlencode($f)."[".urlencode($value)."]=1";
+          }
+          break;
+
+          case incidents::FILTER_TYPE_INT:
+          $select["selected"][$f] = (int)$_GET[$f];
+          $parameters[] = urlencode($f)."=".(int)$_GET[$f];
+          break;
+
+          case incidents::FILTER_TYPE_STRING:
+          $select["selected"][$f] = $_GET[$f];
+          $parameters[] = urlencode($f)."=".urlencode($_GET[$f]);
+          break;
+        }
+      }
+    }
+
+    foreach ($select["enabled"] as $enabled) {
+      if ($enabled) {
+        $select["isAnyEnabled"] = true;
+        break;
+      }
+    }
+
+    if (!$select["isAnyEnabled"] || (isset($_GET["forceQueue"]) && $_GET["forceQueue"] == "1")) $select["showPendingQueue"] = true;
+
+    $select["pageUrlHasParameters"] = (count($parameters) > 0);
+    $select["pageUrl"] = "incidents.php".($select["pageUrlHasParameters"] ? "?".implode("&", $parameters) : "");
+    $select["showResultsPaginated"] = !$select["enabled"]["states"];
+  }
+
+  public static function handleIncidentShortcuts() {
+    global $_GET;
+    if (isset($_GET["goTo"])) {
+      switch ($_GET["goTo"]) {
+        case "today":
+        security::go("incidents.php?page=".(int)incidents::todayPage());
+        break;
+      }
+    }
+  }
+}
diff --git a/src/inc/intervals.php b/src/inc/intervals.php
new file mode 100644
index 0000000..10c31f7
--- /dev/null
+++ b/src/inc/intervals.php
@@ -0,0 +1,32 @@
+<?php
+class intervals {
+  public static function wellFormed($i) {
+    return (isset($i[0]) && isset($i[1]) && $i[0] <= $i[1]);
+  }
+
+  public static function measure($i) {
+    return ($i[1] - $i[0]);
+  }
+
+  // Does A overlap B? (with "$open = true" meaning [0, 100] does not overlap [100, 200])
+  public static function overlaps($a, $b, $open = true) {
+    return ($open ? ($a[0] < $b[1] && $a[1] > $b[0]) : ($a[0] <= $b[1] && $a[1] >= $b[0]));
+  }
+
+  // Is A inside of B?
+  public static function isSubset($a, $b) {
+    return ($a[0] >= $b[0] && $a[1] <= $b[1]);
+  }
+
+  // Intersect A and B and return the corresponding interval
+  public static function intersect($a, $b) {
+    $int = [max($a[0], $b[0]), min($a[1], $b[1])];
+
+    return (self::wellFormed($int) ? $int : false);
+  }
+
+  // Return measure of the intersection
+  public static function measureIntersection($a, $b) {
+    return self::measure(self::intersect($a, $b));
+  }
+}
diff --git a/src/inc/listings.php b/src/inc/listings.php
new file mode 100644
index 0000000..b3aa258
--- /dev/null
+++ b/src/inc/listings.php
@@ -0,0 +1,102 @@
+<?php
+class listings {
+  public static function renderFilterDialog($form, $select) {
+    global $_GET;
+    ?>
+    <dialog class="mdl-dialog" id="filter">
+      <form action="<?=$form?>" method="GET" enctype="multipart/form-data">
+        <h4 class="mdl-dialog__title">Filtrar lista</h4>
+        <div class="mdl-dialog__content">
+          <h5>Categorías</h5>
+          <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="cat-1">
+            <input type="checkbox" id="cat-1" name="categories[-1]" class="mdl-checkbox__input" value="1"<?=(($select["enabled"]["categories"] && in_array(-1, $select["selected"]["categories"])) ? " checked" : "")?>>
+            <span class="mdl-checkbox__label">Sin categoría</span>
+          </label>
+          <?php
+          foreach (categories::getAll(false, false, true) as $c) {
+            $haschilds = (count($c["childs"]) > 0);
+            if ($haschilds) {
+              $subcategories_arr = [];
+              foreach ($c["childs"] as $child) {
+                $subcategories_arr[] = "&ldquo;".security::htmlsafe($child["name"])."&rdquo;";
+              }
+              $subcategories = implode(", ", $subcategories_arr);
+            }
+            ?>
+            <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="cat<?=(int)$c["id"]?>">
+              <input type="checkbox" id="cat<?=(int)$c["id"]?>" name="categories[<?=(int)$c["id"]?>]" class="mdl-checkbox__input" value="1"<?=(($select["enabled"]["categories"] && in_array($c["id"], $select["selected"]["categories"])) ? " checked" : "")?>>
+              <span class="mdl-checkbox__label"><?=security::htmlsafe($c["name"])?> <?php if ($haschilds) { ?><i id="haschilds<?=(int)$c["id"]?>" class="material-icons help">info</i><?php } ?></span>
+            </label>
+            <?php
+            if ($haschilds) {
+              ?>
+              <div class="mdl-tooltip" for="haschilds<?=(int)$c["id"]?>">Esta categoría incluye la<?=(count($c["childs"]) == 1 ? "" : "s")?> subcategoría<?=(count($c["childs"]) == 1 ? "" : "s")?> <?=$subcategories?></div>
+              <?php
+            }
+          }
+          ?>
+          <h5>Empresas</h5>
+          <?php
+          foreach (companies::getAll() as $id => $company) {
+            ?>
+            <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="comp<?=(int)$id?>">
+              <input type="checkbox" id="comp<?=(int)$id?>" name="companies[<?=(int)$id?>]" class="mdl-checkbox__input" value="1"<?=(($select["enabled"]["companies"] && in_array($id, $select["selected"]["companies"])) ? " checked" : "")?>>
+              <span class="mdl-checkbox__label"><?=security::htmlsafe($company)?></span>
+            </label>
+            <?php
+          }
+          ?>
+          <h5>Tipos de usuario</h5>
+          <?php
+          foreach (security::$types as $id => $type) {
+            ?>
+            <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="type<?=(int)$id?>">
+              <input type="checkbox" id="type<?=(int)$id?>" name="types[<?=(int)$id?>]" class="mdl-checkbox__input" value="1"<?=(($select["enabled"]["types"] && in_array($id, $select["selected"]["types"])) ? " checked" : "")?>>
+              <span class="mdl-checkbox__label"><?=security::htmlsafe($type)?></span>
+            </label>
+            <?php
+          }
+
+          if ($form == "workers.php") {
+            echo "<h5>Horario actual</h5>";
+            foreach (schedules::$workerScheduleStatusShort as $id => $type) {
+              ?>
+              <label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" for="schedulesstatus<?=(int)$id?>">
+                <input type="checkbox" id="schedulesstatus<?=(int)$id?>" name="schedulesstatus[<?=(int)$id?>]" class="mdl-checkbox__input" value="1"<?=(isset($_GET["schedulesstatus"][(int)$id]) ? " checked" : "")?>>
+                <span class="mdl-checkbox__label mdl-color-text--<?=security::htmlsafe(schedules::$workerScheduleStatusColors[$id])?>"><?=security::htmlsafe($type)?></span>
+              </label>
+              <?php
+            }
+          }
+          ?>
+        </div>
+        <div class="mdl-dialog__actions">
+          <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Filtrar</button>
+          <button onclick="event.preventDefault(); document.querySelector('#filter').close();" class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+        </div>
+      </form>
+    </dialog>
+    <?php
+  }
+
+  public static function buildSelect($form) {
+    global $select, $selectedSchedulesStatus, $_GET;
+
+    $select = array("enabled" => [], "selected" => []);
+
+    foreach (people::$filters as $f) {
+      $select["enabled"][$f] = isset($_GET[$f]);
+      if ($select["enabled"][$f]) {
+        $select["selected"][$f] = (isset($_GET[$f]) ? array_keys($_GET[$f]) : []);
+      }
+    }
+
+    if ($form == "workers.php") {
+      if (isset($_GET["schedulesstatus"]) && is_array($_GET["schedulesstatus"]) && count($_GET["schedulesstatus"])) {
+        $selectedSchedulesStatus = array_keys($_GET["schedulesstatus"]);
+      } else {
+        $selectedSchedulesStatus = false;
+      }
+    }
+  }
+}
diff --git a/src/inc/mail.php b/src/inc/mail.php
new file mode 100644
index 0000000..7f3b353
--- /dev/null
+++ b/src/inc/mail.php
@@ -0,0 +1,65 @@
+<?php
+use PHPMailer\PHPMailer\PHPMailer;
+use PHPMailer\PHPMailer\Exception;
+use PHPMailer\PHPMailer\SMTP;
+
+require __DIR__.'/../lib/PHPMailer/Exception.php';
+require __DIR__.'/../lib/PHPMailer/PHPMailer.php';
+require __DIR__.'/../lib/PHPMailer/SMTP.php';
+
+class mail {
+  public static function send($to, $attachments, $subject, $body, $isHTML = true, $plainBody = "") {
+    global $conf;
+
+    if (!$conf["mail"]["enabled"]) return false;
+
+    $mail = new PHPMailer();
+
+    try {
+      if ($conf["debug"]) {
+        $mail->SMTPDebug = SMTP::DEBUG_SERVER;
+      }
+      $mail->CharSet = "UTF-8";
+      $mail->isSMTP();
+      $mail->Host = $conf["mail"]["host"];
+      $mail->SMTPAuth = $conf["mail"]["smtpauth"];
+      $mail->Username = $conf["mail"]["username"];
+      $mail->Password = $conf["mail"]["password"];
+      $mail->SMTPSecure = 'tls';
+      $mail->Port = $conf["mail"]["port"];
+      $mail->Encoding = 'base64';
+      $mail->Timeout = 30;
+
+      $mail->setFrom($conf["mail"]["remitent"], $conf["mail"]["remitentName"]);
+
+      foreach ($to as $address) {
+        if (isset($address["name"])) $mail->addAddress($address["email"], $address["name"]);
+        else $mail->addAddress($address["email"]);
+      }
+
+      foreach ($attachments as $attachment) {
+        if (isset($attachment["name"])) $mail->addAttachment($attachment["path"], $attachment["name"]);
+        else $mail->addAttachment($attachment["email"]);
+      }
+
+      if ($isHTML) {
+        $mail->isHTML(true);
+        if (!empty($plainBody)) $mail->AltBody = $plainBody;
+      }
+
+      $mail->Subject = (!empty($conf["mail"]["subjectPrefix"]) ? $conf["mail"]["subjectPrefix"]." " : "").$subject;
+      $mail->Body = $body;
+
+      if (!$mail->send()) return false;
+    } catch (Exception $e) {
+      if ($conf["debug"]) echo $e."\n";
+      return false;
+    }
+
+    return true;
+  }
+
+  public static function bodyTemplate($msg) {
+    return "<div style=\"font-family: 'Helvetica', 'Arial', sans-serif;\">".$msg."</div>";
+  }
+}
diff --git a/src/inc/people.php b/src/inc/people.php
new file mode 100644
index 0000000..deccc10
--- /dev/null
+++ b/src/inc/people.php
@@ -0,0 +1,244 @@
+<?php
+class people {
+  public static $filters = ["categories", "types", "companies"];
+  public static $mysqlFilters = ["categories", "types"];
+  public static $mysqlFiltersFields = ["p.category", "p.type"];
+
+  public static function add($username, $name, $dni, $email, $category, $password_hash, $type) {
+    global $con;
+
+    $susername = db::sanitize($username);
+    $sname = db::sanitize($name);
+    $sdni = db::sanitize($dni);
+    $semail = db::sanitize($email);
+    $scategory = (int)$category;
+    $spassword_hash = db::sanitize($password_hash);
+    $stype = (int)$type;
+
+    if (!categories::exists($category) || !security::existsType($type)) return false;
+
+    return mysqli_query($con, "INSERT INTO people (username, name, dni, email, category, password, type) VALUES ('$susername', '$sname', '$sdni', '$semail', $scategory, '$spassword_hash', $stype)");
+  }
+
+  public static function edit($id, $username, $name, $dni, $email, $category, $type) {
+    global $con;
+
+    $sid = (int)$id;
+    $susername = db::sanitize($username);
+    $sname = db::sanitize($name);
+    $sdni = db::sanitize($dni);
+    $semail = db::sanitize($email);
+    $scategory = (int)$category;
+    $stype = (int)$type;
+
+    return mysqli_query($con, "UPDATE people SET username = '$susername', name = '$sname', dni = '$sdni', email = '$semail', category = $scategory, type = $stype WHERE id = $sid LIMIT 1");
+  }
+
+  public static function updatePassword($id, $password_hash) {
+    global $con;
+
+    $sid = (int)$id;
+    $spassword_hash = db::sanitize($password_hash);
+
+    return mysqli_query($con, "UPDATE people SET password = '$spassword_hash' WHERE id = $sid LIMIT 1");
+  }
+
+  public static function workerViewChangePassword($oldpassword, $newpassword) {
+    global $_SESSION;
+
+    if (!security::isUserPassword(false, $oldpassword)) return false;
+
+    return self::updatePassword($_SESSION["id"], password_hash($newpassword, PASSWORD_DEFAULT));
+  }
+
+  private static function addCompaniesToRow(&$row, $isWorker = false, $showHiddenCompanies = true) {
+    global $con;
+
+    $query = mysqli_query($con, "SELECT w.id id, w.company company, h.status status
+      FROM workers w ".workers::sqlAddonToGetStatusAttribute($row["id"]));
+
+    $row["baixa"] = true;
+    if ($isWorker) $row["hidden"] = true;
+    $row["companies"] = [];
+    while ($row2 = mysqli_fetch_assoc($query)) {
+      $baixa = workers::isHidden($row2["status"]);
+
+      if ($isWorker && $row2["id"] == $row["workerid"]) $row["hidden"] = $baixa;
+      if (!$baixa) $row["baixa"] = false;
+      if (!$showHiddenCompanies && $baixa) continue;
+      $row["companies"][$row2["id"]] = $row2["company"];
+    }
+  }
+
+  private static function filterCompanies($fc, $pc) { // Filter Companies, Person Companies
+    foreach ($pc as $c) {
+      if (in_array($c, $fc)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public static function get($id, $showHiddenCompanies = true) {
+    global $con;
+
+    $query = mysqli_query($con, "SELECT p.id id, p.username username, p.type type, p.name name, p.dni dni, p.email email, p.category categoryid, c.name category FROM people p LEFT JOIN categories c ON p.category = c.id WHERE p.id = ".(int)$id);
+
+    if (!mysqli_num_rows($query)) {
+      return false;
+    }
+
+    $row = mysqli_fetch_assoc($query);
+    self::addCompaniesToRow($row, false, $showHiddenCompanies);
+
+    return $row;
+  }
+
+  public static function getAll($select = false, $treatCompaniesSeparated = false) {
+    global $con, $conf;
+
+    $mysqlSelect = false;
+    if ($select !== false) {
+      $mysqlSelect = true;
+      $flag = false;
+      foreach (self::$mysqlFilters as $f) {
+        if ($select["enabled"][$f]) {
+          $flag = true;
+          break;
+        }
+      }
+
+      if (!$flag) {
+        $mysqlSelect = false;
+      }
+    }
+
+    if ($mysqlSelect !== false) {
+      $categoryChilds = categories::getChildren();
+      $where = " WHERE ";
+      $conditions = [];
+      foreach (self::$mysqlFilters as $i => $f) {
+        if ($select["enabled"][$f]) {
+          $insideconditions = [];
+          foreach ($select["selected"][$f] as $value) {
+            $insideconditions[] = self::$mysqlFiltersFields[$i]." = ".(int)$value;
+            if ($f == "categories" && isset($categoryChilds[(int)$value])) {
+              foreach ($categoryChilds[(int)$value] as $child) {
+                $insideconditions[] = self::$mysqlFiltersFields[$i]." = ".(int)$child;
+              }
+            }
+          }
+          $conditions[] = "(".implode(" OR ", $insideconditions).")";
+        }
+      }
+      $where .= implode(" AND ", $conditions);
+    } else {
+      $where = "";
+    }
+
+    $query = mysqli_query($con, "SELECT
+      p.id id,
+      p.username username,
+      p.type type,
+      p.name name,
+      p.dni dni,
+      p.email email,
+      p.category categoryid,
+      c.name category
+      ".($treatCompaniesSeparated ? ", w.id workerid, w.company companyid" : "")."
+    FROM people p
+    LEFT JOIN categories c
+      ON p.category = c.id
+    ".($treatCompaniesSeparated ? " RIGHT JOIN workers w
+      ON p.id = w.person" : "").$where);
+
+    $people = [];
+
+    while ($row = mysqli_fetch_assoc($query)) {
+      self::addCompaniesToRow($row, $treatCompaniesSeparated);
+
+      if ($select === false || !$select["enabled"]["companies"] || (!$treatCompaniesSeparated && self::filterCompanies($select["selected"]["companies"], $row["companies"]) || ($treatCompaniesSeparated && in_array($row["companyid"], $select["selected"]["companies"])))) {
+        $people[] = $row;
+      }
+    }
+
+    // Order people by name and baixa
+    if ($treatCompaniesSeparated) {
+      usort($people, function($a, $b) {
+        if ($a["hidden"] == 0 && $b["hidden"] == 1) return -1;
+        if ($a["hidden"] == 1 && $b["hidden"] == 0) return 1;
+        return ($a["name"] < $b["name"] ? -1 : ($a["name"] == $b["name"] ? 0 : 1));
+      });
+    } else {
+      usort($people, function($a, $b) {
+        if ($a["baixa"] == 0 && $b["baixa"] == 1) return -1;
+        if ($a["baixa"] == 1 && $b["baixa"] == 0) return 1;
+        return ($a["name"] < $b["name"] ? -1 : ($a["name"] == $b["name"] ? 0 : 1));
+      });
+    }
+
+    return $people;
+  }
+
+  public static function exists($id) {
+    global $con;
+
+    $query = mysqli_query($con, "SELECT id FROM people WHERE id = ".(int)$id);
+
+    return (mysqli_num_rows($query) > 0);
+  }
+
+  public static function addToCompany($id, $company) {
+    global $con;
+
+    $sid = (int)$id;
+    $scompany = (int)$company;
+
+    if (!companies::exists($scompany)) return false;
+    if (!people::exists($sid)) return false;
+
+    $query = mysqli_query($con, "SELECT id FROM workers WHERE person = $sid AND company = $scompany");
+    if (mysqli_num_rows($query)) return false;
+
+    $time = (int)time();
+
+    if (!mysqli_query($con, "INSERT INTO workers (person, company) VALUES ($sid, $scompany)")) return false;
+
+    $sworkerId = (int)mysqli_insert_id($con);
+    $stime = (int)time();
+
+    return mysqli_query($con, "INSERT INTO workhistory (worker, day, status) VALUES ($sworkerId, $stime, ".(int)workers::AFFILIATION_STATUS_AUTO_WORKING.")");
+  }
+
+  public static function userData($data, $id = "ME") {
+    global $con, $_SESSION;
+
+    if ($id == "ME" && $data == "id") return $_SESSION["id"];
+    if ($id == "ME") $id = $_SESSION["id"];
+    $sdata = preg_replace("/[^A-Za-z0-9 ]/", '', $data);
+    $sid = (int)$id;
+
+    $query = mysqli_query($con, "SELECT $sdata FROM people WHERE id = $sid");
+
+    if (!mysqli_num_rows($query)) return false;
+
+    $row = mysqli_fetch_assoc($query);
+
+    return $row[$sdata];
+  }
+
+  public static function workerData($data, $id) {
+    global $con, $_SESSION;
+
+    $sdata = preg_replace("/[^A-Za-z0-9 ]/", '', $data);
+    $sid = (int)$id;
+
+    $query = mysqli_query($con, "SELECT p.$sdata $sdata FROM people p INNER JOIN workers w ON p.id = w.person WHERE w.id = $sid");
+
+    if (!mysqli_num_rows($query)) return false;
+
+    $row = mysqli_fetch_assoc($query);
+
+    return $row[$sdata];
+  }
+}
diff --git a/src/inc/recovery.php b/src/inc/recovery.php
new file mode 100644
index 0000000..1f8307b
--- /dev/null
+++ b/src/inc/recovery.php
@@ -0,0 +1,140 @@
+<?php
+class recovery {
+  const TOKEN_BYTES = 32;
+  const EXPIRATION_TIME = 60*60*24;
+  const WELCOME_EXPIRATION_TIME = 60*60*24*90;
+
+  const EMAIL_TYPE_RECOVERY = 0;
+  const EMAIL_TYPE_WELCOME = 1;
+
+  const EMAIL_NOT_SET = 0;
+
+  public static function getUser($email, $dni) {
+    global $con;
+
+    $semail = db::sanitize($email);
+    $sdni = db::sanitize($dni);
+
+    $query = mysqli_query($con, "SELECT id FROM people WHERE email = '$semail' AND dni = '$sdni' LIMIT 1");
+
+    if (!mysqli_num_rows($query)) return false;
+
+    return mysqli_fetch_assoc($query)["id"];
+  }
+
+  private static function tokenIsUnique($token) {
+    global $con;
+
+    $stoken = db::sanitize($token);
+
+    $query = mysqli_query($con, "SELECT 1 FROM recovery WHERE token = '$stoken' LIMIT 1");
+
+    return !mysqli_num_rows($query);
+  }
+
+  private static function generateRandomToken() {
+    do {
+      $token = bin2hex(random_bytes(self::TOKEN_BYTES));
+    } while (!self::tokenIsUnique($token));
+
+    return $token;
+  }
+
+  private static function invalidateAllRecoveries($id) {
+    global $con;
+
+    $sid = (int)$id;
+
+    return mysqli_query($con, "UPDATE recovery SET used = 1 WHERE user = $sid");
+  }
+
+  public static function sendRecoveryMail($user, $token, $expires, $emailType = self::EMAIL_TYPE_RECOVERY) {
+    global $conf;
+
+    $to = [
+      ["email" => $user["email"]]
+    ];
+
+    $url = security::htmlsafe($conf["fullPath"]."recovery.php?token=".$token);
+
+    switch ($emailType) {
+      case self::EMAIL_TYPE_WELCOME:
+      $subject = "Datos de acceso de ".security::htmlsafe($user["name"]);
+      $body = mail::bodyTemplate("<p>Hola ".security::htmlsafe($user["name"]).",</p>
+      <p>Para poder acceder al aplicativo de control horario, adjuntamos un enlace donde podrás configurar tu contraseña:</p>
+      <ul>
+        <li><a href=\"$url\">$url</a></li>
+      </ul>
+      <p>Una vez establezcas tu contraseña, podrás iniciar sesión con la contraseña que has rellenado y tu usuario: <b><code>".security::htmlsafe($user["username"])."</code></b></p>
+      <p>Reciba un cordial saludo.</p>");
+      break;
+
+      default:
+      $subject = "Recuperar contraseña de ".security::htmlsafe($user["name"]);
+      $body = mail::bodyTemplate("<p>Hola ".security::htmlsafe($user["name"]).",</p>
+      <p>Alguien (seguramente tú) ha rellenado el formulario de recuperación de contraseñas para el aplicativo de registro horario.</p>
+      <p>Si no has sido tú, puede que alguien esté intentando entrar en tu zona de trabajador del aplicativo, y te agredeceríamos que lo comunicaras en la mayor brevedad posible a recursos humanos.</p>
+      <p>Si has sido tú, puedes restablecer tu contraseña haciendo clic en el siguiente enlace:</p>
+      <ul>
+        <li><a href=\"$url\">$url</a></li>
+      </ul>
+      <p>Además, aprovechamos para recordarte que tu usuario en el web es <b><code>".security::htmlsafe($user["username"])."</code></b></p>
+      <p>Reciba un cordial saludo.</p>");
+    }
+
+    return mail::send($to, [], $subject, $body);
+  }
+
+  public static function recover($id, $emailType = self::EMAIL_TYPE_RECOVERY) {
+    global $con;
+
+    $user = people::get($id);
+    if ($user === false) return false;
+    if (!isset($user["email"]) || empty($user["email"])) return self::EMAIL_NOT_SET;
+
+    $token = self::generateRandomToken();
+    $stoken = db::sanitize($token);
+    $sid = (int)$id;
+    $stime = (int)time();
+    $sexpires = (int)($stime + ($emailType === self::EMAIL_TYPE_WELCOME ? self::WELCOME_EXPIRATION_TIME : self::EXPIRATION_TIME));
+
+    if (!self::invalidateAllRecoveries($id)) return false;
+
+    if (!mysqli_query($con, "INSERT INTO recovery (user, token, timecreated, expires) VALUES ($sid, '$stoken', $stime, $sexpires)")) return false;
+
+    return self::sendRecoveryMail($user, $token, $sexpires, $emailType);
+  }
+
+  public static function getRecovery($token) {
+    global $con;
+
+    $stoken = db::sanitize($token);
+
+    $query = mysqli_query($con, "SELECT * FROM recovery WHERE token = '$stoken' LIMIT 1");
+
+    if (!mysqli_num_rows($query)) return false;
+
+    return mysqli_fetch_assoc($query);
+  }
+
+  public static function getUnusedRecovery($token) {
+    $recovery = self::getRecovery($token);
+    if ($recovery === false || $recovery["used"] != 0 || $recovery["expires"] < time()) return false;
+
+    return $recovery;
+  }
+
+  public static function finishRecovery($token, $password) {
+    global $con;
+
+    $stoken = db::sanitize($token);
+    $spassword = db::sanitize(password_hash($password, PASSWORD_DEFAULT));
+
+    $recovery = recovery::getUnusedRecovery($token);
+    if ($recovery === false) return false;
+
+    if (!self::invalidateAllRecoveries($recovery["user"])) return false;
+
+    return people::updatePassword($recovery["user"], $spassword);
+  }
+}
diff --git a/src/inc/recurringIncidents.php b/src/inc/recurringIncidents.php
new file mode 100644
index 0000000..b07bf66
--- /dev/null
+++ b/src/inc/recurringIncidents.php
@@ -0,0 +1,97 @@
+<?php
+class recurringIncidents {
+  /*public static function oldAdd($worker, $type, $details, $ifirstday, $ilastday, $begins, $ends, $creator = "ME", $typedays, $days, $alreadyTimestamp = false) {
+    global $con, $conf;
+
+    $sworker = (int)$worker;
+    $workerDetails = workers::get($sworker);
+    if ($workerDetails === false) return 1;
+
+    if ($creator === "ME") $creator = people::userData("id");
+    $screator = (int)$creator;
+
+    if (!security::isAllowed(security::ADMIN) && $workerDetails["person"] != $creator) return 5;
+
+    $stype = (int)$type;
+    $sverified = (int)$verified;
+    $sdetails = db::sanitize($details);
+
+    $incidenttype = self::getType($stype);
+    if ($incidenttype === false) return -1;
+
+    if ($alreadyTimestamp) {
+      $sfirstday = (int)$ifirstday;
+      $slastday = (int)$ilastday;
+    } else {
+      $firstday = new DateTime($ifirstday);
+      $sfirstday = (int)$firstday->getTimestamp();
+      $lastday = new DateTime($ilastday);
+      $slastday = (int)$lastday->getTimestamp();
+    }
+
+    if ($sfirstday >= $slastday) return 3;
+
+    $sbegins = (int)$begins;
+    $sends = (int)$ends;
+    if ($sbegins >= $sends) return 3;
+
+    $typedays = array_unique(array_map(function($el) {
+      return (int)$el;
+    }, $typedays));
+    foreach ($typedays as $typeday) {
+      if (!in_array($typeday, $workingTypes)) return 6;
+    }
+    $stypedays = json_encode($typedays);
+
+    if (!mysqli_query($con, "INSERT INTO recurringincidents (worker, creator, type, firstday, lastday, typedays, begins, ends, details) VALUES ($sworker, $screator, $stype, $sfirstday, $slastday, '$stypedays', $sbegins, $sends, '$sdetails')")) return -1;
+
+    return 0;
+  }*/ // NOTE: This was a first idea, to allow setting up recurring incidents like schedules, but we've changed how we'll handle them and so this is no longer useful.
+
+  public static function add($worker, $type, $details, $ifirstday, $ilastday, $begins, $ends, $creator = "ME", $typeDays, $days, $alreadyTimestamp = false) {
+    if ($alreadyTimestamp) {
+      $current = new DateTime();
+      $current->setTimestamp($ifirstday);
+      $lastday = new DateTime();
+      $lastday->setTimestamp($ilastday);
+    } else {
+      $current = new DateTime($ifirstday);
+      $lastday = new DateTime($ilastday);
+    }
+
+    $oneDay = new DateInterval("P1D");
+
+    $category = registry::getWorkerCategory($worker);
+    if ($category === false) return false;
+
+    $flag = true;
+
+    for (; $current->diff($lastday)->invert === 0; $current->add($oneDay)) {
+      $currentTimestamp = $current->getTimestamp();
+      $currentDay = (int)$current->format("N") - 1;
+
+      if (!in_array($currentDay, $days)) continue;
+
+      $calendarDays = registry::getDayTypes($currentTimestamp);
+      if ($calendarDays === false) return false;
+
+      if (isset($calendarDays[$category])) {
+        $typeDay = $calendarDays[$category];
+      } else if (isset($calendarDays[-1])) {
+        $typeDay = $calendarDays[-1];
+      } else {
+        $flag = false;
+        continue;
+      }
+
+      if (!in_array($typeDay, $typeDays)) continue;
+
+      if ($status = incidents::add($worker, $type, $details, $currentTimestamp, $begins, $ends, $creator, 1, true, false, false) !== 0) {
+        $flag = false;
+        continue;
+      }
+    }
+
+    return $flag;
+  }
+}
diff --git a/src/inc/registry.php b/src/inc/registry.php
new file mode 100644
index 0000000..363968d
--- /dev/null
+++ b/src/inc/registry.php
@@ -0,0 +1,393 @@
+<?php
+class registry {
+  const LOGS_PAGINATION_LIMIT = 30;
+  const REGISTRY_PAGINATION_LIMIT = 20;
+
+  const STATE_REGISTERED = 0;
+  const STATE_MANUALLY_INVALIDATED = 1;
+  const STATE_VALIDATED_BY_WORKER = 2;
+
+  public static $stateIcons = [
+    0 => "check",
+    1 => "delete_forever",
+    2 => "verified_user"
+  ];
+
+  public static $stateIconColors = [
+    0 => "mdl-color-text--green",
+    1 => "mdl-color-text--red",
+    2 => "mdl-color-text--green"
+  ];
+
+  public static $stateTooltips = [
+    0 => "Registrado",
+    1 => "Invalidado manualmente",
+    2 => "Validado"
+  ];
+
+  public static $workerPendingWhere = "r.workervalidated = 0";
+  public static $notInvalidatedWhere = "r.invalidated = 0";
+  public static $logsWarnings = "LOCATE('[warning]', logdetails) as warningpos, LOCATE('[error]', logdetails) as errorpos, LOCATE('[fatalerror]', logdetails) as fatalerrorpos";
+
+  private static function recordLog(&$log, $time, $executedby = -1, $quiet = false) {
+    global $con;
+
+    $slog = db::sanitize($log);
+    $sday = (int)$time;
+    $srealtime = (int)time();
+    $sexecutedby = (int)$executedby;
+
+    $status = mysqli_query($con, "INSERT INTO logs (realtime, day, executedby, logdetails) VALUES ($srealtime, $sday, $sexecutedby, '$slog')");
+
+    if (!$status) {
+      if (!$quiet) echo "[fatalerror] Couldn't record log into the database!\n";
+      return false;
+    } else {
+      if (!$quiet) echo "[success] Log recorded into the database.\n";
+      return mysqli_insert_id($con);
+    }
+  }
+
+  public static function getLogs($start = 0, $limit = self::LOGS_PAGINATION_LIMIT) {
+    global $con;
+
+    $query = mysqli_query($con, "SELECT id, realtime, day, executedby, ".self::$logsWarnings." FROM logs ORDER BY id DESC".db::limitPagination($start, $limit));
+
+    $return = [];
+    while ($row = mysqli_fetch_assoc($query)) {
+      $return[] = $row;
+    }
+
+    return $return;
+  }
+
+  public static function getLog($id) {
+    global $con;
+
+    $sid = (int)$id;
+
+    $query = mysqli_query($con, "SELECT logdetails, ".self::$logsWarnings." FROM logs WHERE id = $sid");
+
+    if (!mysqli_num_rows($query)) return false;
+
+    $row = mysqli_fetch_assoc($query);
+
+    return $row;
+  }
+
+  public static function beautifyLog($str) {
+    $str = str_replace("[info]", "<span style='font-weight: bold;' class='mdl-color-text--blue'>[info]</span>", $str);
+    $str = str_replace("[warning]", "<span style='font-weight: bold;' class='mdl-color-text--orange'>[warning]</span>", $str);
+    $str = str_replace("[error]", "<span style='font-weight: bold;' class='mdl-color-text--red'>[error]</span>", $str);
+    $str = str_replace("[fatalerror]", "<span style='font-weight: bold;' class='mdl-color-text--red-900'>[fatalerror]</span>", $str);
+    return $str;
+  }
+
+  private static function addToLog(&$log, $quiet, $msg) {
+    $log .= $msg;
+    if (!$quiet) echo $msg;
+  }
+
+  private static function alreadyRegistered($time, $worker) {
+    global $con;
+
+    $stime = (int)$time;
+    $sworker = (int)$worker;
+
+    $query = mysqli_query($con, "SELECT id FROM records WHERE worker = $sworker AND day = $stime AND invalidated = 0 LIMIT 1");
+
+    return (mysqli_num_rows($query) > 0);
+  }
+
+  private static function register($time, $worker, $schedule, $creator = -1) {
+    global $con;
+
+    $sworker = (int)$worker;
+    $stime = (int)$time;
+    $srealtime = (int)time();
+    $screator = (int)$creator;
+    $sbeginswork = (int)$schedule["beginswork"];
+    $sendswork = (int)$schedule["endswork"];
+    $sbeginsbreakfast = (int)$schedule["beginsbreakfast"];
+    $sendsbreakfast = (int)$schedule["endsbreakfast"];
+    $sbeginslunch = (int)$schedule["beginslunch"];
+    $sendslunch = (int)$schedule["endslunch"];
+
+    return mysqli_query($con, "INSERT INTO records (worker, day, created, creator, beginswork, endswork, beginsbreakfast, endsbreakfast, beginslunch, endslunch) VALUES ($sworker, $stime, $srealtime, $screator, $sbeginswork, $sendswork, $sbeginsbreakfast, $sendsbreakfast, $sbeginslunch, $sendslunch)");
+  }
+
+  public static function getWorkerCategory($id) {
+    global $con;
+
+    $sid = (int)$id;
+
+    $query = mysqli_query($con, "SELECT p.category category FROM workers w INNER JOIN people p ON w.person = p.id WHERE w.id = $sid");
+
+    if ($query === false || !mysqli_num_rows($query)) {
+      return false;
+    }
+
+    $row = mysqli_fetch_assoc($query);
+
+    return $row["category"];
+  }
+
+  public static function getDayTypes($time) {
+    global $con;
+
+    $stime = (int)$time;
+
+    $query = mysqli_query($con, "SELECT id, category, details FROM calendars WHERE begins <= $stime AND ends >= $stime");
+    if ($query === false) return false;
+
+    $calendars = [];
+    while ($row = mysqli_fetch_assoc($query)) {
+      $calendar = json_decode($row["details"], true);
+      if (json_last_error() !== JSON_ERROR_NONE) return false;
+      if (!isset($calendar[$time])) return false;
+
+      $calendars[$row["category"]] = $calendar[$time];
+    }
+
+    return $calendars;
+  }
+
+  private static function getApplicableSchedules($time) {
+    global $con;
+
+    $stime = (int)$time;
+
+    $query = mysqli_query($con, "SELECT id FROM schedules WHERE begins <= $stime AND ends >= $stime AND active = 1");
+    if ($query === false) return false;
+
+    $schedules = [];
+    while ($row = mysqli_fetch_assoc($query)) {
+      $schedules[] = $row["id"];
+    }
+
+    return $schedules;
+  }
+
+  public static function generateNow($originaltime, &$logId, $quiet = true, $executedby = -1, $workersWhitelist = false) {
+    global $con;
+
+    $log = "";
+
+    if ($workersWhitelist !== false) self::addToLog($log, $quiet, "[info] This is a partial registry generation, because a whitelist of workers was passed: [".implode(", ", $workersWhitelist)."]\n");
+
+    $datetime = new DateTime();
+    $datetime->setTimestamp($originaltime);
+    self::addToLog($log, $quiet, "[info] Time passed: ".$datetime->format("Y-m-d H:i:s")."\n");
+
+    $rawdate = $datetime->format("Y-m-d")."T00:00:00";
+    self::addToLog($log, $quiet, "[info] Working with this date: $rawdate\n");
+    $date = new DateTime($rawdate);
+
+    $time = $date->getTimestamp();
+    $dow = (int)$date->format("N") - 1;
+    self::addToLog($log, $quiet, "[info] Final date timestamp: $time, Dow: $dow\n");
+
+    $days = self::getDayTypes($time);
+    if ($days === false) {
+      self::addToLog($log, $quiet, "[fatalerror] An error occurred while loading the calendars.\n");
+      $logId = self::recordLog($log, $time, $executedby, $quiet);
+      return 1;
+    }
+
+    $schedules = self::getApplicableSchedules($time);
+    if ($schedules === false) {
+      self::addToLog($log, $quiet, "[fatalerror] An error occurred while loading the active schedules.\n");
+      $logId = self::recordLog($log, $time, $executedby, $quiet);
+      return 2;
+    }
+
+    self::addToLog($log, $quiet, "[info] Found ".count($schedules)." active schedule(s)\n");
+
+    foreach ($schedules as $scheduleid) {
+      self::addToLog($log, $quiet, "\n[info] Processing schedule $scheduleid\n");
+
+      $s = schedules::get($scheduleid);
+      if ($s === false) {
+        self::addToLog($log, $quiet, "[fatalerror] An error ocurred while loading schedule with id $scheduleid (it doesn't exist or there was an error with the SQL query)\n");
+        $logId = self::recordLog($log, $time, $executedby, $quiet);
+        return 3;
+      }
+
+      if ($workersWhitelist !== false && !in_array($s["worker"], $workersWhitelist)) {
+        self::addToLog($log, $quiet, "[info] This schedule's worker (".$s["worker"].") is not in the whitelist, so skipping\n");
+        continue;
+      }
+
+      $category = self::getWorkerCategory($s["worker"]);
+
+      if (isset($days[$category])) {
+        self::addToLog($log, $quiet, "[info] Using worker's (".$s["worker"].") category ($category) calendar\n");
+        $typeday = $days[$category];
+      } else if (isset($days[-1])) {
+        self::addToLog($log, $quiet, "[info] Using default calendar\n");
+        $typeday = $days[-1];
+      } else {
+        self::addToLog($log, $quiet, "[warning] No calendar applies, so skipping this schedule\n");
+        continue;
+      }
+
+      if (!isset($s["days"][$typeday])) {
+        self::addToLog($log, $quiet, "[info] This schedule doesn't have this type of day ($typeday) set up, so skipping\n");
+        continue;
+      }
+
+      if (!isset($s["days"][$typeday][$dow])) {
+        self::addToLog($log, $quiet, "[info] This schedule doesn't have a daily schedule for this day of the week ($dow) and type of day ($typeday), so skipping.\n");
+        continue;
+      }
+
+      self::addToLog($log, $quiet, "[info] Found matching daily schedule. We'll proceed to register it\n");
+
+      if (self::alreadyRegistered($time, $s["worker"])) {
+        self::addToLog($log, $quiet, "[warning] We're actually NOT going to register it because another registry already exists for this worker at the same day.\n");
+      } else {
+        if (self::register($time, $s["worker"], $s["days"][$typeday][$dow], $executedby)) {
+          self::addToLog($log, $quiet, "[info] Registered with id ".mysqli_insert_id($con)."\n");
+        } else {
+          self::addToLog($log, $quiet, "[error] Couldn't register this schedule because of an unknown error!\n");
+        }
+      }
+    }
+
+    $logId = self::recordLog($log, $time, $executedby, $quiet);
+
+    return 0;
+  }
+
+  public static function getStatus($row) {
+    if ($row["invalidated"] == 1) return self::STATE_MANUALLY_INVALIDATED;
+    elseif ($row["workervalidated"] == 1) return self::STATE_VALIDATED_BY_WORKER;
+    else return self::STATE_REGISTERED;
+  }
+
+  private static function magicRecord(&$row) {
+    $row["state"] = self::getStatus($row);
+  }
+
+  public static function get($id, $magic = false) {
+    global $con;
+
+    $sid = (int)$id;
+
+    $query = mysqli_query($con, "SELECT * FROM records WHERE id = $sid LIMIT 1");
+
+    if (!mysqli_num_rows($query)) return false;
+
+    $row = mysqli_fetch_assoc($query);
+    if ($magic) self::magicRecord($row);
+
+    return $row;
+  }
+
+  public static function getRecords($worker = false, $begins = false, $ends = false, $returnInvalid = false, $includeWorkerInfo = false, $sortByDateDesc = false, $start = 0, $limit = self::REGISTRY_PAGINATION_LIMIT, $treatWorkerAttributeAsUser = false, $onlyWorkerPending = false, $magic = true) {
+    global $con;
+
+    if ($treatWorkerAttributeAsUser && !$includeWorkerInfo) return false;
+
+    $where = [];
+
+    if ($worker !== false) $where[] = ($treatWorkerAttributeAsUser ? "w.person" : "r.worker")." = ".(int)$worker;
+
+    $dateLimit = ($begins !== false && $ends !== false);
+    if ($dateLimit) {
+      $where[] = "r.day >= ".(int)$begins;
+      $where[] = "r.day <= ".(int)$ends;
+    }
+
+    if (!$returnInvalid || $onlyWorkerPending) $where[] = self::$notInvalidatedWhere;
+
+    if ($onlyWorkerPending) $where[] = self::$workerPendingWhere;
+
+    $query = mysqli_query($con, "SELECT r.*".($includeWorkerInfo ? ", p.id personid, p.name workername, w.company companyid" : "")." FROM records r LEFT JOIN workers w ON r.worker = w.id".($includeWorkerInfo ? " LEFT JOIN people p ON w.person = p.id" : "").(count($where) ? " WHERE ".implode(" AND ", $where) : "")." ORDER BY".($sortByDateDesc ? " r.day DESC, w.company DESC," : "")." id DESC".db::limitPagination($start, $limit));
+
+    $return = [];
+    while ($row = mysqli_fetch_assoc($query)) {
+      if ($magic) self::magicRecord($row);
+      $return[] = $row;
+    }
+
+    return $return;
+  }
+
+  public static function getWorkerRecords($worker, $begins = false, $ends = false, $returnInvalid = false, $onlyWorkerPending = false) {
+    return self::getRecords($worker, $begins, $ends, $returnInvalid, false, true, 0, 0, false, $onlyWorkerPending);
+  }
+
+  public static function numRows($includingInvalidated = false) {
+    global $con;
+
+    $query = mysqli_query($con, "SELECT COUNT(*) count FROM records".($includingInvalidated ? "" : " WHERE invalidated = 0"));
+
+    if (!mysqli_num_rows($query)) return false;
+
+    $row = mysqli_fetch_assoc($query);
+
+    return (isset($row["count"]) ? (int)$row["count"] : false);
+  }
+
+  public static function numRowsUser($user, $includingInvalidated = true) {
+    global $con;
+
+    $where = [];
+    if (!$includingInvalidated) $where[] = "r.invlidated = 0";
+    $where[] = "w.person = ".(int)$user;
+
+    $query = mysqli_query($con, "SELECT COUNT(*) count FROM records r LEFT JOIN workers w ON r.worker = w.id".(count($where) ? " WHERE ".implode(" AND ", $where) : ""));
+
+    if (!mysqli_num_rows($query)) return false;
+
+    $row = mysqli_fetch_assoc($query);
+
+    return (isset($row["count"]) ? (int)$row["count"] : false);
+  }
+
+  public static function checkRecordIsFromPerson($record, $person = "ME", $boolean = false) {
+    global $con;
+
+    if ($person == "ME") $person = people::userData("id");
+
+    $srecord = (int)$record;
+    $sperson = (int)$person;
+
+    $query = mysqli_query($con, "SELECT r.id FROM records r INNER JOIN workers w ON r.worker = w.id INNER JOIN people p ON w.person = p.id WHERE p.id = $sperson AND r.id = $srecord LIMIT 1");
+
+    if (!mysqli_num_rows($query)) {
+      if ($boolean) return false;
+      security::denyUseMethod(security::METHOD_NOTFOUND);
+    }
+
+    return true;
+  }
+
+  public static function invalidate($id, $invalidatedby = "ME") {
+    global $con;
+
+    $sid = (int)$id;
+
+    if ($invalidatedby === "ME") $invalidatedby = people::userData("id");
+    $sinvalidatedby = (int)$invalidatedby;
+
+    return mysqli_query($con, "UPDATE records SET invalidated = 1, invalidatedby = $sinvalidatedby WHERE id = $sid LIMIT 1");
+  }
+
+  public static function invalidateAll($id, $beginsRawDate, $endsRawDate, $invalidatedby = "ME") {
+    global $con;
+
+    $beginsDate = new DateTime($beginsRawDate);
+    $sbegins = (int)$beginsDate->getTimestamp();
+    $endsDate = new DateTime($endsRawDate);
+    $sends = (int)$endsDate->getTimestamp();
+
+    $sid = (int)$id;
+
+    if ($invalidatedby === "ME") $invalidatedby = people::userData("id");
+    $sinvalidatedby = (int)$invalidatedby;
+
+    return mysqli_query($con, "UPDATE records SET invalidated = 1, invalidatedby = $sinvalidatedby WHERE worker = $sid AND invalidated = 0 AND day <= $sends AND day >= $sbegins");
+  }
+}
diff --git a/src/inc/registryView.php b/src/inc/registryView.php
new file mode 100644
index 0000000..1767bc3
--- /dev/null
+++ b/src/inc/registryView.php
@@ -0,0 +1,91 @@
+<?php
+class registryView {
+  public static function renderRegistry(&$registry, &$companies, $scrollable = false, $showPersonAndCompany = true, $isForWorker = false, $isForValidationView = false) {
+    global $conf, $renderRegistryAutoIncremental;
+    if (!isset($renderRegistryAutoIncremental)) $renderRegistryAutoIncremental = 0;
+    $menu = "";
+    ?>
+    <div class="mdl-shadow--2dp overflow-wrapper incidents-wrapper<?=($scrollable ? " incidents-wrapper--scrollable" : "")?>">
+      <table class="incidents">
+        <tr>
+          <?php if ($conf["debug"]) { ?><th>ID</th><?php } ?>
+          <th<?=($isForValidationView ? " class=\"has-checkbox\"" : "")?>>
+            <?php if ($isForValidationView) {
+              ?>
+              <label for="checkboxall_r_<?=$renderRegistryAutoIncremental?>" class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" data-check-all="true">
+                <input type="checkbox" id="checkboxall_r_<?=$renderRegistryAutoIncremental?>" class="mdl-checkbox__input" autocomplete="off">
+              </label>
+              <?php
+            }
+            ?>
+          </th>
+          <?php
+          if ($showPersonAndCompany) {
+            ?>
+            <th>Nombre</th>
+            <th>Empresa</th>
+            <?php
+          }
+          ?>
+          <th>Día</th>
+          <th>Jornada laboral</th>
+          <th>Desayuno</th>
+          <th>Comida</th>
+          <th></th>
+        </tr>
+        <?php
+        foreach ($registry as $record) {
+          $id = (int)$record["id"]."_".(int)$renderRegistryAutoIncremental;
+          $breakfastInt = [$record["beginsbreakfast"], $record["endsbreakfast"]];
+          $lunchInt = [$record["beginslunch"], $record["endslunch"]];
+          ?>
+          <tr<?=($record["invalidated"] == 1 ? ' class="mdl-color-text--grey-700 line-through"' : '')?>>
+            <?php if ($conf["debug"]) { ?><td><?=(int)$record["id"]?></td><?php } ?>
+            <td class="icon-cell<?=($isForValidationView ? " has-checkbox" : "")?>">
+              <?php
+              if ($isForValidationView) {
+                ?>
+                <label for="checkbox_r_<?=$id?>" class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect">
+                  <input type="checkbox" id="checkbox_r_<?=$id?>" data-record="<?=(int)$record["id"]?>" class="mdl-checkbox__input" autocomplete="off">
+                </label>
+                <?php
+              } else {
+                ?>
+                <i id="state<?=$id?>" class="material-icons <?=security::htmlsafe(registry::$stateIconColors[$record["state"]])?>"><?=security::htmlsafe(registry::$stateIcons[$record["state"]])?></i>
+                <?php
+                visual::addTooltip("state".$id, security::htmlsafe(registry::$stateTooltips[$record["state"]]));
+              }
+              ?>
+            </td>
+            <?php
+            if ($showPersonAndCompany) {
+              ?>
+              <td class="can-strike"><span<?=($isForWorker ? '' : ' data-dyndialog-href="dynamic/user.php?id='.(int)$record["personid"].'"')?>><?=security::htmlsafe($record["workername"])?></span></td>
+              <td class="can-strike"><?=security::htmlsafe($companies[$record["companyid"]])?></td>
+              <?php
+            }
+            ?>
+            <td class="can-strike"><?=strftime("%d %b %Y", $record["day"])?></td>
+            <td class="centered can-strike"><?=schedules::sec2time($record["beginswork"])." - ".schedules::sec2time($record["endswork"])?></td>
+            <td class="centered can-strike"><?=(intervals::measure($breakfastInt) == 0 ? "-" : ($isForWorker ? export::sec2hours(intervals::measure($breakfastInt)) : schedules::sec2time($record["beginsbreakfast"])." - ".schedules::sec2time($record["endsbreakfast"])))?></td>
+            <td class="centered can-strike"><?=(intervals::measure($lunchInt) == 0 ? "-" : ($isForWorker ? export::sec2hours(intervals::measure($lunchInt)) : schedules::sec2time($record["beginslunch"])." - ".schedules::sec2time($record["endslunch"])))?></td>
+            <td><button id="actions<?=$id?>" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect custom-actions-btn"><i class="material-icons icon">more_vert</i></button></td>
+          </tr>
+          <?php
+          $menu .= '<ul class="mdl-menu mdl-menu--unaligned mdl-js-menu mdl-js-ripple-effect" for="actions'.$id.'">';
+
+          if (!$isForWorker && $record["invalidated"] == 0) {
+            $menu .= '<a href="dynamic/invalidaterecord.php?id='.(int)$record["id"].'" data-dyndialog-href="dynamic/invalidaterecord.php?id='.(int)$record["id"].'"><li class="mdl-menu__item">Invalidar</li></a>';
+          }
+
+          $menu .= '<a href="dynamic/authorsrecord.php?id='.(int)$record["id"].'" data-dyndialog-href="dynamic/authorsrecord.php?id='.(int)$record["id"].'"><li class="mdl-menu__item">Autoría</li></a></ul>';
+
+          $renderRegistryAutoIncremental++;
+        }
+        ?>
+      </table>
+      <?php echo $menu; ?>
+    </div>
+  <?php
+  }
+}
diff --git a/src/inc/schedules.php b/src/inc/schedules.php
new file mode 100644
index 0000000..87b5ed6
--- /dev/null
+++ b/src/inc/schedules.php
@@ -0,0 +1,456 @@
+<?php
+class schedules {
+  const STATUS_NO_ACTIVE_SCHEDULE = 0;
+  const STATUS_HALFWAY_CONFIGURED_SCHEDULE = 1;
+  const STATUS_ACTIVE_SCHEDULE = 2;
+
+  public static $allEvents = ["work", "breakfast", "lunch"];
+  public static $otherEvents = ["breakfast", "lunch"];
+  public static $otherEventsDescription = [
+    "breakfast" => "Desayuno",
+    "lunch" => "Comida"
+  ];
+  public static $workerScheduleStatus = [
+    0 => "No hay ningún calendario activo",
+    1 => "Hay un calendario activo pero no está completamente configurado",
+    2 => "Hay un calendario activo completamente configurado"
+  ];
+  public static $workerScheduleStatusShort = [
+    0 => "No hay un horario activo",
+    1 => "No está configurado del todo",
+    2 => "Existe uno configurado"
+  ];
+  public static $workerScheduleStatusColors = [
+    0 => "red",
+    1 => "orange",
+    2 => "green"
+  ];
+
+  public static function time2sec($time) {
+    $e = explode(":", $time);
+    return ((int)$e[0]*60 + (int)$e[1])*60;
+  }
+
+  public static function sec2time($sec, $autoShowSeconds = true) {
+    $min = floor($sec/60);
+    $secr = $sec % 60;
+    return visual::padNum(floor($min/60), 2).":".visual::padNum(($min % 60), 2).($autoShowSeconds && $secr != 0 ? ":".visual::padNum($secr, 2) : "");
+  }
+
+  // TEMPLATES:
+
+  public static function addTemplate($name, $ibegins, $iends) {
+    global $con;
+
+    $sname = db::sanitize($name);
+    $begins = new DateTime($ibegins);
+    $sbegins = (int)$begins->getTimestamp();
+    $ends = new DateTime($iends);
+    $sends = (int)$ends->getTimestamp();
+
+    if (!intervals::wellFormed([$sbegins, $sends])) return 2;
+
+    return (mysqli_query($con, "INSERT INTO scheduletemplates (name, begins, ends) VALUES ('$sname', $sbegins, $sends)") ? 0 : 1);
+  }
+
+  private static function _addDaysToReturn(&$return, $stableDays, $sfieldSchedule, $sid) {
+    global $con;
+
+    $return["days"] = [];
+
+    $query2 = mysqli_query($con, "SELECT * FROM $stableDays WHERE $sfieldSchedule = $sid ORDER BY typeday ASC, day ASC");
+
+    while ($row = mysqli_fetch_assoc($query2)) {
+      if (!isset($return["days"][$row["typeday"]]))
+        $return["days"][$row["typeday"]] = [];
+
+      $return["days"][$row["typeday"]][$row["day"]] = $row;
+    }
+  }
+
+  private static function _get($id, $table, $tableDays, $fieldSchedule) {
+    global $con;
+
+    $sid = (int)$id;
+    $stable = preg_replace("/[^A-Za-z0-9 ]/", '', $table);
+    $stableDays = preg_replace("/[^A-Za-z0-9 ]/", '', $tableDays);
+    $sfieldSchedule = preg_replace("/[^A-Za-z0-9 ]/", '', $fieldSchedule);
+
+    $query = mysqli_query($con, "SELECT * FROM $stable WHERE id = $sid");
+
+    if ($query === false || !mysqli_num_rows($query)) return false;
+
+    $return = mysqli_fetch_assoc($query);
+
+    self::_addDaysToReturn($return, $stableDays, $sfieldSchedule, $sid);
+
+    return $return;
+  }
+
+  public static function getTemplate($id) {
+    return self::_get($id, "scheduletemplates", "scheduletemplatesdays", "template");
+  }
+
+  public static function getTemplates() {
+    global $con, $conf;
+
+    $query = mysqli_query($con, "SELECT * FROM scheduletemplates ORDER BY ".($conf["debug"] ? "id" : "name")." ASC");
+
+    $templates = [];
+
+    while ($row = mysqli_fetch_assoc($query)) {
+      $templates[] = $row;
+    }
+
+    return $templates;
+  }
+
+  public static function editTemplate($id, $name, $ibegins, $iends) {
+    global $con;
+
+    $sid = (int)$id;
+    $sname = db::sanitize($name);
+    $begins = new DateTime($ibegins);
+    $sbegins = (int)$begins->getTimestamp();
+    $ends = new DateTime($iends);
+    $sends = (int)$ends->getTimestamp();
+
+    if (!intervals::wellFormed([$sbegins, $sends])) return 2;
+
+    return (mysqli_query($con, "UPDATE scheduletemplates SET name = '$sname', begins = $sbegins, ends = $sends WHERE id = $sid LIMIT 1") ? 0 : 1);
+  }
+
+  public static function _remove($id, $table, $tableDays, $fieldSchedule) {
+    global $con;
+
+    $sid = (int)$id;
+    $stable = preg_replace("/[^A-Za-z0-9 ]/", '', $table);
+    $stableDays = preg_replace("/[^A-Za-z0-9 ]/", '', $tableDays);
+    $sfieldSchedule = preg_replace("/[^A-Za-z0-9 ]/", '', $fieldSchedule);
+
+    return (mysqli_query($con, "DELETE FROM $stable WHERE id = $sid LIMIT 1") && mysqli_query($con, "DELETE FROM $stableDays WHERE $fieldSchedule = $sid"));
+  }
+
+  public static function removeTemplate($id) {
+    return self::_remove($id, "scheduletemplates", "scheduletemplatesdays", "template");
+  }
+
+  private static function _exists($id, $table) {
+    global $con;
+
+    $sid = (int)$id;
+    $stable = preg_replace("/[^A-Za-z0-9 ]/", '', $table);
+
+    $query = mysqli_query($con, "SELECT id FROM $stable WHERE id = ".(int)$id);
+
+    return (mysqli_num_rows($query) > 0);
+  }
+
+  public static function templateExists($id) {
+    return self::_exists($id, "scheduletemplates");
+  }
+
+  public static function checkAddDayGeneric($begins, $ends, $beginsb, $endsb, $beginsl, $endsl) {
+    global $con;
+
+    $times = [];
+    $times["work"] = [$begins, $ends];
+    $times["breakfast"] = [$beginsb, $endsb];
+    $times["lunch"] = [$beginsl, $endsl];
+
+    foreach ($times as $time) {
+      if (intervals::wellFormed($time) === false) return 1;
+    }
+
+    if (intervals::measure($times["work"]) == 0) return 4;
+
+    if ((!intervals::isSubset($times["breakfast"], $times["work"]) && intervals::measure($times["breakfast"]) != 0) || (!intervals::isSubset($times["lunch"], $times["work"]) && intervals::measure($times["lunch"]) != 0)) return 2;
+
+    if (intervals::overlaps($times["breakfast"], $times["lunch"]) && intervals::measure($times["breakfast"]) != 0 && intervals::measure($times["lunch"]) != 0) return 3;
+
+    return 0;
+  }
+
+  private static function _checkAddDayParticular($id, $dow, $typeday, $table, $fieldSchedule) {
+    global $con;
+
+    $sid = (int)$id;
+    $sdow = (int)$dow;
+    $stypeday = (int)$typeday;
+    $stable = preg_replace("/[^A-Za-z0-9 ]/", '', $table);
+    $sfieldSchedule = preg_replace("/[^A-Za-z0-9 ]/", '', $fieldSchedule);
+
+    $query = mysqli_query($con, "SELECT id FROM $stable WHERE $fieldSchedule = $sid AND day = $sdow AND typeday = $stypeday");
+
+    return (!mysqli_num_rows($query));
+  }
+
+  public static function checkAddDay2TemplateParticular($id, $dow, $typeday) {
+    return self::_checkAddDayParticular($id, $dow, $typeday, "scheduletemplatesdays", "template");
+  }
+
+  private static function _addDay($id, $dow, $typeday, $begins, $ends, $beginsb, $endsb, $beginsl, $endsl, $table, $fieldSchedule) {
+    global $con;
+
+    $sid = (int)$id;
+    $sdow = (int)$dow;
+    $stypeday = (int)$typeday;
+    $sbegins = (int)$begins;
+    $sends = (int)$ends;
+    $sbeginsb = (int)$beginsb;
+    $sendsb = (int)$endsb;
+    $sbeginsl = (int)$beginsl;
+    $sendsl = (int)$endsl;
+    $stable = preg_replace("/[^A-Za-z0-9 ]/", '', $table);
+    $sfieldSchedule = preg_replace("/[^A-Za-z0-9 ]/", '', $fieldSchedule);
+
+    return mysqli_query($con, "INSERT INTO $stable ($sfieldSchedule, day, typeday, beginswork, endswork, beginsbreakfast, endsbreakfast, beginslunch, endslunch) VALUES ($sid, $sdow, $stypeday, $sbegins, $sends, $sbeginsb, $sendsb, $sbeginsl, $sendsl)");
+  }
+
+  public static function addDay2Template($id, $dow, $typeday, $begins, $ends, $beginsb, $endsb, $beginsl, $endsl) {
+    return self::_addDay($id, $dow, $typeday, $begins, $ends, $beginsb, $endsb, $beginsl, $endsl, "scheduletemplatesdays", "template");
+  }
+
+  public static function _getDay($id, $table) {
+    global $con;
+
+    $sid = (int)$id;
+    $stable = preg_replace("/[^A-Za-z0-9 ]/", '', $table);
+
+    $query = mysqli_query($con, "SELECT * FROM $stable WHERE id = $sid");
+
+    if (!mysqli_num_rows($query)) return false;
+
+    return mysqli_fetch_assoc($query);
+  }
+
+  public static function getTemplateDay($id) {
+    return self::_getDay($id, "scheduletemplatesdays");
+    global $con;
+  }
+
+  private static function _editDay($id, $begins, $ends, $beginsb, $endsb, $beginsl, $endsl, $table) {
+    global $con;
+
+    $sid = (int)$id;
+    $sbegins = (int)$begins;
+    $sends = (int)$ends;
+    $sbeginsb = (int)$beginsb;
+    $sendsb = (int)$endsb;
+    $sbeginsl = (int)$beginsl;
+    $sendsl = (int)$endsl;
+    $stable = preg_replace("/[^A-Za-z0-9 ]/", '', $table);
+
+    return mysqli_query($con, "UPDATE $stable SET beginswork = $sbegins, endswork = $sends, beginsbreakfast = $sbeginsb, endsbreakfast = $sendsb, beginslunch = $sbeginsl, endslunch = $sendsl WHERE id = $sid LIMIT 1");
+  }
+
+  public static function editTemplateDay($id, $begins, $ends, $beginsb, $endsb, $beginsl, $endsl) {
+    return self::_editDay($id, $begins, $ends, $beginsb, $endsb, $beginsl, $endsl, "scheduletemplatesdays");
+  }
+
+  private static function _removeDay($id, $table) {
+    global $con;
+
+    $sid = (int)$id;
+    $stable = preg_replace("/[^A-Za-z0-9 ]/", '', $table);
+
+    return mysqli_query($con, "DELETE FROM $stable WHERE id = $sid LIMIT 1");
+  }
+
+  public static function removeTemplateDay($id) {
+    return self::_removeDay($id, "scheduletemplatesdays");
+  }
+
+  private static function _dayExists($id, $table) {
+    global $con;
+
+    $sid = (int)$id;
+    $stable = preg_replace("/[^A-Za-z0-9 ]/", '', $table);
+
+    $query = mysqli_query($con, "SELECT id FROM $stable WHERE id = $sid");
+
+    return (mysqli_num_rows($query) > 0);
+  }
+
+  public static function templateDayExists($id) {
+    return self::_dayExists($id, "scheduletemplatesdays");
+  }
+
+  // SCHEDULES:
+
+  public static function checkOverlap($worker, $begins, $ends, $sans = 0) {
+    global $con;
+
+    $sworker = (int)$worker;
+    $sbegins = (int)$begins;
+    $sends = (int)$ends;
+    $ssans = (int)$sans;
+
+    $query = mysqli_query($con, "SELECT * FROM schedules WHERE begins <= $sends AND ends >= $sbegins AND worker = $sworker".($sans == 0 ? "" : " AND id <> $ssans")." LIMIT 1");
+
+    return (mysqli_num_rows($query) > 0);
+  }
+
+  public static function get($id) {
+    return self::_get($id, "schedules", "schedulesdays", "schedule");
+  }
+
+  public static function getAll($id, $showNotActive = true) {
+    global $con, $conf;
+
+    $sid = (int)$id;
+
+    $query = mysqli_query($con, "SELECT * FROM schedules WHERE worker = $sid".($showNotActive ? "" : " AND active = 1")." ORDER BY ".($conf["debug"] ? "id ASC" : "begins DESC"));
+
+    $schedules = [];
+
+    while ($row = mysqli_fetch_assoc($query)) {
+      $schedules[] = $row;
+    }
+
+    return $schedules;
+  }
+
+  public static function getCurrent($id = "ME", $isWorker = false) {
+    global $con;
+
+    if ($id == "ME") $id = people::userData("id");
+    $sid = (int)$id;
+
+    $date = new DateTime(date("Y-m-d")."T00:00:00");
+    $timestamp = (int)$date->getTimestamp();
+
+    $query = mysqli_query($con, "SELECT s.* FROM schedules s ".($isWorker ? "WHERE s.worker = $sid" : "LEFT JOIN workers w ON s.worker = w.id WHERE w.person = $sid")." AND s.active = 1 AND s.begins <= $timestamp AND s.ends >= $timestamp");
+
+    $return = [];
+
+    while ($row = mysqli_fetch_assoc($query)) {
+      self::_addDaysToReturn($row, "schedulesdays", "schedule", $row["id"]);
+      $return[] = $row;
+    }
+
+    return $return;
+  }
+
+  public static function getWorkerScheduleStatus($id) {
+    $currentSchedules = self::getCurrent($id, true);
+    if ($currentSchedules === false || !count($currentSchedules)) return ["status" => self::STATUS_NO_ACTIVE_SCHEDULE];
+
+    $schedule =& $currentSchedules[0];
+
+    foreach (calendars::$workingTypes as $type) {
+      if (!isset($schedule["days"][$type]) || !count($schedule["days"][$type])) return ["status" => self::STATUS_HALFWAY_CONFIGURED_SCHEDULE, "schedule" => $schedule["id"]];
+    }
+
+    return ["status" => self::STATUS_ACTIVE_SCHEDULE, "schedule" => $schedule["id"]];
+  }
+
+  public static function add($worker, $ibegins, $iends, $active = 0, $alreadyTimestamp = false) {
+    global $con;
+
+    $sworker = (int)$worker;
+    $sactive = (int)$active;
+    if ($alreadyTimestamp) {
+      $sbegins = (int)$ibegins;
+      $sends = (int)$iends;
+    } else {
+      $begins = new DateTime($ibegins);
+      $sbegins = (int)$begins->getTimestamp();
+      $ends = new DateTime($iends);
+      $sends = (int)$ends->getTimestamp();
+    }
+
+    if (!intervals::wellFormed([$sbegins, $sends])) return 3;
+
+    if (self::checkOverlap($worker, $sbegins, $sends)) {
+      return 1;
+    }
+
+    return (mysqli_query($con, "INSERT INTO schedules (worker, begins, ends, active) VALUES ('$sworker', $sbegins, $sends, $sactive)") ? 0 : 2);
+  }
+
+  public static function edit($id, $ibegins, $iends) {
+    global $con;
+
+    $sid = (int)$id;
+    $begins = new DateTime($ibegins);
+    $sbegins = (int)$begins->getTimestamp();
+    $ends = new DateTime($iends);
+    $sends = (int)$ends->getTimestamp();
+
+    if (!intervals::wellFormed([$sbegins, $sends])) return 3;
+
+    $actual = self::get($sid);
+    if ($actual === false) return 4;
+
+    if (self::checkOverlap($actual["worker"], $sbegins, $sends, $sid)) {
+      return 1;
+    }
+
+    return (mysqli_query($con, "UPDATE schedules SET begins = $sbegins, ends = $sends WHERE id = $sid LIMIT 1") ? 0 : 2);
+  }
+
+  public static function remove($id) {
+    return self::_remove($id, "schedules", "schedulesdays", "schedule");
+  }
+
+  public static function switchActive($id, $value) {
+    global $con;
+
+    $sid = (int)$id;
+    $svalue = (int)$value;
+    if ($svalue > 1 || $svalue < 0) return false;
+
+    return mysqli_query($con, "UPDATE schedules SET active = $svalue WHERE id = $sid LIMIT 1");
+  }
+
+  public static function exists($id) {
+    return self::_exists($id, "schedules");
+  }
+
+  public static function checkAddDay2ScheduleParticular($id, $dow, $typeday) {
+    return self::_checkAddDayParticular($id, $dow, $typeday, "schedulesdays", "schedule");
+  }
+
+  public static function addDay2Schedule($id, $dow, $typeday, $begins, $ends, $beginsb, $endsb, $beginsl, $endsl) {
+    return self::_addDay($id, $dow, $typeday, $begins, $ends, $beginsb, $endsb, $beginsl, $endsl, "schedulesdays", "schedule");
+  }
+
+  public static function getDay($id) {
+    return self::_getDay($id, "schedulesdays");
+    global $con;
+  }
+
+  public static function editDay($id, $begins, $ends, $beginsb, $endsb, $beginsl, $endsl) {
+    return self::_editDay($id, $begins, $ends, $beginsb, $endsb, $beginsl, $endsl, "schedulesdays");
+  }
+
+  public static function dayExists($id) {
+    return self::_dayExists($id, "schedulesdays");
+  }
+
+  public static function removeDay($id) {
+    return self::_removeDay($id, "schedulesdays");
+  }
+
+  public static function copyTemplate($template, $worker, $active) {
+    global $con;
+
+    $template = self::getTemplate($template);
+    if ($template === false) return 1;
+
+    $status = self::add($worker, $template["begins"], $template["ends"], $active, true);
+    if ($status != 0) return ($status + 1);
+
+    $id = mysqli_insert_id($con);
+
+    foreach ($template["days"] as $typeday) {
+      foreach ($typeday as $day) {
+        $status2 = self::addDay2Schedule($id, $day["day"], $day["typeday"], $day["beginswork"], $day["endswork"], $day["beginsbreakfast"], $day["endsbreakfast"], $day["beginslunch"], $day["endslunch"]);
+        if (!$status2) return -1;
+      }
+    }
+
+    return 0;
+  }
+}
diff --git a/src/inc/schedulesView.php b/src/inc/schedulesView.php
new file mode 100644
index 0000000..aab3a2f
--- /dev/null
+++ b/src/inc/schedulesView.php
@@ -0,0 +1,124 @@
+<?php
+class schedulesView {
+  const HOUR_HEIGHT = 30;
+
+  public static function renderSchedule(&$week, $editEnabled = false, $editLink, $deleteLink) {
+    $secmin = 24*60*60;
+    $secmax = 0;
+
+    foreach ($week as $day) {
+      if ($day["beginswork"] < $secmin) $secmin = $day["beginswork"];
+      if ($day["endswork"] > $secmax) $secmax = $day["endswork"];
+    }
+
+    $min = max(floor($secmin/(60*60)) - 1, 0);
+    $max = min(ceil($secmax/(60*60)) + 1, 24);
+    ?>
+    <div class="overflow-wrapper overflow-wrapper--for-table" style="text-align: center;">
+      <div class="schedule mdl-shadow--2dp">
+        <div class="sidetime">
+          <?php
+          for ($hour = $min; $hour <= $max; $hour++) {
+            ?>
+            <div class="hour">
+              <div class="hour--text"><?=security::htmlsafe(visual::padNum($hour, 2).":00")?></div>
+            </div>
+            <?php
+          }
+          ?>
+        </div>
+        <?php
+        for ($day = 0; $day < 7; $day++) {
+          $isset = isset($week[$day]);
+
+          if (!$isset && $day >= 5 && !isset($week[5]) && !isset($week[6])) continue;
+
+          if ($isset) {
+            $size = [];
+            $size["work"] = [];
+            $size["work"]["top"] = (($week[$day]["beginswork"]/(60*60)) - $min)*self::HOUR_HEIGHT;
+            $size["work"]["height"] = intervals::measure([$week[$day]["beginswork"], $week[$day]["endswork"]])/(60*60)*self::HOUR_HEIGHT;
+
+            foreach (schedules::$otherEvents as $event) {
+              if (intervals::measure([$week[$day]["begins".$event], $week[$day]["ends".$event]]) > 0) {
+                $size[$event] = [];
+                $size[$event]["top"] = ($week[$day]["begins".$event] - $week[$day]["beginswork"])/(60*60)*self::HOUR_HEIGHT;
+                $size[$event]["height"] = intervals::measure([$week[$day]["begins".$event], $week[$day]["ends".$event]])/(60*60)*self::HOUR_HEIGHT;
+              }
+            }
+          }
+          ?>
+          <div class="day">
+            <div class="day--header"><?=security::htmlsafe(calendars::$days[$day])?></div>
+            <div class="day--content">
+              <?php
+              if ($isset) {
+                ?>
+                <div class="work-event" style="top: <?=(int)round($size["work"]["top"])?>px; height: <?=(int)round($size["work"]["height"])?>px;">
+                  <?php
+                  if ($editEnabled) {
+                    ?>
+                    <div class="event--actions">
+                      <a href='<?=$editLink($week[$day])?>' data-dyndialog-href='<?=$editLink($week[$day])?>' title='Modificar horario'><i class='material-icons'>edit</i></a>
+                      <a href='<?=$deleteLink($week[$day])?>' data-dyndialog-href='<?=$deleteLink($week[$day])?>' title='Eliminar horario'><i class='material-icons'>delete</i></a>
+                    </div>
+                    <?php
+                  }
+                  ?>
+                  <div class="event--header">Trabajo</div>
+                  <div class="event--body"><?=security::htmlsafe(schedules::sec2time($week[$day]["beginswork"]))?> - <?=security::htmlsafe(schedules::sec2time($week[$day]["endswork"]))?></div>
+                  <?php
+                  foreach (schedules::$otherEvents as $event) {
+                    if (isset($size[$event])) {
+                      ?>
+                      <div class="inline-event" style="top: <?=(int)round($size[$event]["top"])?>px; height: <?=(int)round($size[$event]["height"])?>px;">
+                        <div class="event--header"><?=security::htmlsafe(schedules::$otherEventsDescription[$event])?></div>
+                        <div class="event--body"></div>
+                      </div>
+                      <?php
+                    }
+                  }
+                  ?>
+                </div>
+                <?php
+              }
+
+              for ($hour = $min; $hour <= $max; $hour++) {
+                ?>
+                <div class="hour"></div>
+                <?php
+              }
+              ?>
+            </div>
+          </div>
+          <?php
+        }
+        ?>
+      </div>
+    </div>
+  <?php
+  }
+
+  public static function renderPlainSchedule($week, $editEnabled = false, $editLink, $deleteLink, &$flag) {
+    foreach ($week as $day) {
+      $flag = true;
+      ?>
+      <h4><?=security::htmlsafe(calendars::$days[$day["day"]])?> <span class="mdl-color-text--grey-600">(<?=security::htmlsafe(calendars::$types[$day["typeday"]])?>)</span>
+        <?php
+        if ($editEnabled) {
+          ?>
+          <a href='<?=$editLink($day)?>' data-dyndialog-href='<?=$editLink($day)?>' title='Modificar horario' class='mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect'><i class='material-icons icon'>edit</i></a>
+          <a href='<?=$deleteLink($day)?>' data-dyndialog-href='<?=$deleteLink($day)?>' title='Eliminar horario' class='mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect'><i class='material-icons icon'>delete</i></a>
+          <?php
+        }
+        ?>
+      </h4>
+      <ul>
+        <li><b>Horario de trabajo:</b> <?=security::htmlsafe(schedules::sec2time($day["beginswork"]))?> - <?=security::htmlsafe(schedules::sec2time($day["endswork"]))?></li>
+        <?php if (intervals::measure([$day["beginsbreakfast"], $day["endsbreakfast"]]) > 0) { ?><li><b>Desayuno:</b> <?=security::htmlsafe(schedules::sec2time($day["beginsbreakfast"]))?> - <?=security::htmlsafe(schedules::sec2time($day["endsbreakfast"]))?></li><?php } ?>
+        <?php if (intervals::measure([$day["beginslunch"], $day["endslunch"]]) > 0) { ?><li><b>Comida:</b> <?=security::htmlsafe(schedules::sec2time($day["beginslunch"]))?> - <?=security::htmlsafe(schedules::sec2time($day["endslunch"]))?></li><?php } ?>
+      </ul>
+      <?php
+    }
+  }
+}
diff --git a/src/inc/secondFactor.php b/src/inc/secondFactor.php
new file mode 100644
index 0000000..1b0de75
--- /dev/null
+++ b/src/inc/secondFactor.php
@@ -0,0 +1,288 @@
+<?php
+require_once(__DIR__."/../lib/GoogleAuthenticator/GoogleAuthenticator.php");
+require_once(__DIR__."/../lib/WebAuthn/WebAuthn.php");
+
+class secondFactor {
+  public static function isAvailable() {
+    global $conf;
+    return (($conf["secondFactor"]["enabled"] ?? false) === true);
+  }
+
+  public static function checkAvailability() {
+    global $conf;
+
+    if (!self::isAvailable()) {
+      security::notFound();
+    }
+  }
+
+  public static function isEnabled($person = "ME") {
+    if (!self::isAvailable()) return false;
+    return (people::userData("secondfactor", $person) == 1);
+  }
+
+  public static function generateSecret() {
+    $authenticator = new GoogleAuthenticator();
+    return $authenticator->generateSecret();
+  }
+
+  public static function isValidSecret($secret) {
+    return (strlen($secret) === 32);
+  }
+
+  public static function checkCode($secret, $code) {
+    $authenticator = new GoogleAuthenticator();
+    return $authenticator->checkCode($secret, $code);
+  }
+
+  public static function checkPersonCode($person, $code) {
+    global $con;
+
+    $sperson = (int)$person;
+    $query = mysqli_query($con, "SELECT secret FROM totp WHERE person = $sperson LIMIT 1");
+    if ($query === false || !mysqli_num_rows($query)) return false;
+
+    $row = mysqli_fetch_assoc($query);
+
+    return self::checkCode($row["secret"], $code);
+  }
+
+  public static function disable($person = "ME", $personDisable = true) {
+    global $con;
+
+    if ($person == "ME") $person = people::userData("id");
+    $sperson = (int)$person;
+
+    return (mysqli_query($con, "DELETE FROM totp WHERE person = $sperson") && (!$personDisable || mysqli_query($con, "UPDATE people SET secondfactor = 0 WHERE id = $sperson LIMIT 1")) && mysqli_query($con, "DELETE FROM securitykeys WHERE person = $sperson"));
+  }
+
+  public static function enable($secret, $person = "ME") {
+    global $con;
+
+    if (!self::isValidSecret($secret)) return false;
+
+    if ($person == "ME") $person = people::userData("id");
+
+    self::disable($person, false);
+
+    $sperson = (int)$person;
+    $ssecret = db::sanitize($secret);
+    return (mysqli_query($con, "INSERT INTO totp (person, secret) VALUES ($sperson, '$ssecret')") && mysqli_query($con, "UPDATE people SET secondfactor = 1 WHERE id = $sperson LIMIT 1"));
+  }
+
+  private static function completeLogin($success) {
+    if ($success === true) {
+      $_SESSION["id"] = $_SESSION["firstfactorid"];
+      unset($_SESSION["firstfactorid"]);
+      return true;
+    }
+
+    if ($success === false) {
+      unset($_SESSION["firstfactorid"]);
+      return true;
+    }
+
+    return false;
+  }
+
+  public static function completeCodeChallenge($code) {
+    global $_SESSION;
+
+    $success = self::checkPersonCode($_SESSION["firstfactorid"], $code);
+    self::completeLogin($success);
+    return $success;
+  }
+
+  private static function newWebAuthn() {
+    global $conf;
+
+    if (!isset($conf["secondFactor"]) || !isset($conf["secondFactor"]["origin"])) {
+      throw new Exception('secondFactor is not enabled (or the origin is not set) in config.php.');
+    }
+
+    return new \lbuchs\WebAuthn\WebAuthn($conf["appName"], $conf["secondFactor"]["origin"], ["none"]);
+  }
+
+  public static function createRegistrationChallenge() {
+    global $_SESSION;
+
+    $WebAuthn = self::newWebAuthn();
+
+    $credentialIds = self::getCredentialIds();
+    $createArgs = $WebAuthn->getCreateArgs(people::userData("id"), people::userData("username"), people::userData("name"), 20, false, false, ($credentialIds === false ? [] : $credentialIds));
+    $_SESSION['webauthnchallenge'] = $WebAuthn->getChallenge();
+
+    return $createArgs;
+  }
+
+  public static function addSecurityKeyToDB($credentialId, $credentialPublicKey, $name) {
+    global $con;
+
+    $person = people::userData("id");
+    if ($person === false) return false;
+    $sperson = (int)$person;
+
+    $sname = db::sanitize($name);
+    $scredentialId = db::sanitize($credentialId);
+    $scredentialPublicKey = db::sanitize($credentialPublicKey);
+    $stime = (int)time();
+
+    return mysqli_query($con, "INSERT INTO securitykeys (person, name, credentialid, credentialpublickey, added) VALUES ($sperson, '$sname', '$scredentialId', '$scredentialPublicKey', $stime)");
+  }
+
+  public static function completeRegistrationChallenge($clientDataJSON, $attestationObject, $name) {
+    global $_SESSION;
+
+    $clientDataJSON = base64_decode($clientDataJSON);
+    $attestationObject = base64_decode($attestationObject);
+
+    if (!isset($_SESSION["webauthnchallenge"])) {
+      throw new Exception('The user didn\'t start the webauthn challenge.');
+    }
+    $challenge = $_SESSION["webauthnchallenge"];
+    unset($_SESSION["webauthnchallenge"]);
+
+    $WebAuthn = self::newWebAuthn();
+    $data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $challenge);
+
+    if (!self::addSecurityKeyToDB($data->credentialId, $data->credentialPublicKey, $name)) {
+      throw new Exception('Failed adding security key to DB.');
+    }
+
+    return ["status" => "ok"];
+  }
+
+  public static function getSecurityKeys($person = "ME") {
+    global $con;
+
+    if ($person == "ME") $person = people::userData("id");
+    if ($person === false) return false;
+    $sperson = (int)$person;
+
+    $query = mysqli_query($con, "SELECT * FROM securitykeys WHERE person = $sperson");
+    if ($query === false) return false;
+
+    $securityKeys = [];
+    while ($row = mysqli_fetch_assoc($query)) {
+      $securityKeys[] = $row;
+    }
+
+    return $securityKeys;
+  }
+
+  public static function getCredentialIds($person = "ME") {
+    $securityKeys = self::getSecurityKeys($person);
+    if ($securityKeys === false) return false;
+
+    $credentials = [];
+    foreach ($securityKeys as $s) $credentials[] = $s["credentialid"];
+
+    return $credentials;
+  }
+
+  public static function hasSecurityKeys($person) {
+    global $con;
+
+    $sperson = (int)$person;
+
+    $query = mysqli_query($con, "SELECT 1 FROM securitykeys WHERE person = $sperson LIMIT 1");
+    if ($query === false) return false;
+
+    return (mysqli_num_rows($query) > 0);
+  }
+
+  public static function createValidationChallenge() {
+    global $_SESSION;
+
+    $WebAuthn = self::newWebAuthn();
+
+    if (!isset($_SESSION["firstfactorid"])) {
+      throw new Exception('User didn\'t log in with the first factor.');
+    }
+    $credentialIds = self::getCredentialIds($_SESSION["firstfactorid"]);
+    if ($credentialIds === false || empty($credentialIds)) {
+      throw new Exception('The user credentials could not be obtained.');
+    }
+
+    $getArgs = $WebAuthn->getGetArgs($credentialIds);
+    $_SESSION['webauthnvalidationchallenge'] = $WebAuthn->getChallenge();
+
+    return $getArgs;
+  }
+
+  public static function getSecurityKey($credentialId, $person) {
+    $securityKeys = self::getSecurityKeys($person);
+
+    foreach ($securityKeys as $s) {
+      if ($s["credentialid"] == $credentialId) {
+        return $s;
+      }
+    }
+
+    return null;
+  }
+
+  public static function getSecurityKeyById($id) {
+    global $con;
+
+    $query = mysqli_query($con, "SELECT * FROM securitykeys WHERE id = $id");
+    if ($query === false || !mysqli_num_rows($query)) return false;
+
+    return mysqli_fetch_assoc($query);
+  }
+
+  public static function removeSecurityKey($id) {
+    global $con;
+
+    $sid = (int)$id;
+    $sperson = (int)people::userData("id");
+    if ($sperson === false) return false;
+
+    return mysqli_query($con, "DELETE FROM securitykeys WHERE id = $sid and PERSON = $sperson LIMIT 1");
+  }
+
+  private static function recordSecurityKeyUsageToDB($id) {
+    global $con;
+
+    $sid = (int)$id;
+    $stime = (int)time();
+
+    return mysqli_query($con, "UPDATE securitykeys SET lastused = $stime WHERE id = $sid LIMIT 1");
+  }
+
+  public static function completeValidationChallenge($id, $clientDataJSON, $authenticatorData, $signature) {
+    global $_SESSION;
+
+    $id = base64_decode($id);
+    $clientDataJSON = base64_decode($clientDataJSON);
+    $authenticatorData = base64_decode($authenticatorData);
+    $signature = base64_decode($signature);
+
+    if (!isset($_SESSION["webauthnvalidationchallenge"])) {
+      throw new Exception('The user didn\'t start the webauthn challenge.');
+    }
+    $challenge = $_SESSION["webauthnvalidationchallenge"];
+    unset($_SESSION["webauthnvalidationchallenge"]);
+
+    $securityKey = self::getSecurityKey($id, $_SESSION["firstfactorid"]);
+    $credentialPublicKey = $securityKey["credentialpublickey"] ?? null;
+    if ($credentialPublicKey === null) {
+      self::completeLogin(false);
+      throw new Exception('The security key could not be found.');
+    }
+
+    try {
+      $WebAuthn = self::newWebAuthn();
+
+      $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge);
+    } catch (Throwable $ex) {
+      self::completeLogin(false);
+      throw $ex;
+    }
+
+    self::recordSecurityKeyUsageToDB($securityKey["id"]);
+    self::completeLogin(true);
+
+    return ["status" => "ok"];
+  }
+}
diff --git a/src/inc/secondFactorView.php b/src/inc/secondFactorView.php
new file mode 100644
index 0000000..7ed9712
--- /dev/null
+++ b/src/inc/secondFactorView.php
@@ -0,0 +1,9 @@
+<?php
+class secondFactorView {
+  public static function renderSecret($secret) {
+    for ($i = 0; $i < strlen($secret); $i++) {
+      if ($i != 0 && $i % 4 == 0) echo " ";
+      echo $secret[$i];
+    }
+  }
+}
diff --git a/src/inc/security.php b/src/inc/security.php
new file mode 100644
index 0000000..1d3159b
--- /dev/null
+++ b/src/inc/security.php
@@ -0,0 +1,362 @@
+<?php
+class security {
+  const HYPERADMIN = 0;
+  const ADMIN = 2;
+  /*const SYSTEM_REVIEWER = 5;
+  const LOG_REVIEWER = 7;*/
+  const WORKER = 10;
+  const UNKNOWN = 1000;
+
+  const METHOD_UNSPECIFICED = -1;
+  const METHOD_REDIRECT = 0;
+  const METHOD_NOTFOUND = 1;
+
+  const PARAM_ISSET = 1;
+  const PARAM_NEMPTY = 2;
+  const PARAM_ISEMAIL = 4;
+  const PARAM_ISDATE = 8;
+  const PARAM_ISINT = 16;
+  const PARAM_ISTIME = 32;
+  const PARAM_ISARRAY = 64;
+  const PARAM_ISEMAILOREMPTY = 128;
+
+  const SIGNIN_STATE_SIGNED_IN = 0;
+  const SIGNIN_STATE_NEEDS_SECOND_FACTOR = 1;
+  const SIGNIN_STATE_SIGNED_OUT = 2;
+  const SIGNIN_STATE_THROTTLED = 3;
+
+  public static $types = [
+    10 => "Trabajador",
+    /*7 => "Revisor de logs",
+    5 => "Revisor del sistema",*/
+    2 => "Administrador",
+    0 => "Hiperadministrador"
+  ];
+
+  public static $automatedChecks = [self::PARAM_ISSET, self::PARAM_NEMPTY, self::PARAM_ISEMAIL, self::PARAM_ISDATE, self::PARAM_ISINT, self::PARAM_ISTIME, self::PARAM_ISARRAY, self::PARAM_ISEMAILOREMPTY];
+
+  public static $passwordHelperText = "La contraseña debe tener como mínimo 8 caracteres y una letra mayúscula.";
+
+  public static function go($page) {
+    global $conf;
+
+    if ($conf["superdebug"]) {
+      echo "Redirects are not enabled. We would like to redirect you here: <a href='".self::htmlsafe($page)."'>".self::htmlsafe($page)."</a>";
+    } else {
+      header("Location: ".$page);
+    }
+
+    exit();
+  }
+
+  public static function goHome() {
+    self::go("index.php");
+  }
+
+  public static function notFound() {
+    header('HTTP/1.0 404 Not Found');
+    exit();
+  }
+
+  public static function isSignedIn() {
+    global $_SESSION;
+
+    return isset($_SESSION["id"]);
+  }
+
+  public static function check() {
+    if (!self::isSignedIn()) {
+      self::goHome();
+    }
+  }
+
+  public static function userType() {
+    global $_SESSION, $con;
+
+    if (!isset($_SESSION["id"])) {
+      return self::UNKNOWN;
+    }
+
+    $query = mysqli_query($con, "SELECT type FROM people WHERE id = ".(int)$_SESSION["id"]);
+
+    if (!mysqli_num_rows($query)) {
+      return self::UNKNOWN;
+    }
+
+    $row = mysqli_fetch_assoc($query);
+
+    return $row["type"];
+  }
+
+  public static function isAllowed($type) {
+    $userType = (self::isSignedIn() ? self::userType() : self::UNKNOWN);
+
+    return $userType <= $type;
+  }
+
+  public static function denyUseMethod($method = self::METHOD_REDIRECT) {
+    if ($method === self::METHOD_NOTFOUND) {
+      self::notFound();
+    } else { // self::METHOD_REDIRECT or anything else
+      self::goHome();
+    }
+  }
+
+  public static function checkType($type, $method = self::METHOD_REDIRECT) {
+    if (!self::isAllowed($type)) {
+      self::denyUseMethod($method);
+    }
+  }
+
+  public static function checkWorkerUIEnabled() {
+    global $conf;
+
+    if (self::userType() >= self::WORKER && !$conf["enableWorkerUI"]) {
+      self::go("index.php?msg=unsupported");
+    }
+  }
+
+  // Code from https://timoh6.github.io/2015/05/07/Rate-limiting-web-application-login-attempts.html
+  private static function getIpAddresses() {
+    $ips = [];
+    $ips["remoteIp"] = inet_pton($_SERVER['REMOTE_ADDR']); // inet_pton can handle both IPv4 and IPv6 addresses, treat IPv6 addresses as /64 or /56 blocks.
+    $ips["remoteIpBlock"] = long2ip(ip2long($_SERVER['REMOTE_ADDR']) & 0xFFFFFF00); // Something like this to turn the last octet of IPv4 address into a 0.
+    return $ips;
+  }
+
+  public static function recordLoginAttempt($username) {
+    global $con;
+
+    $susername = db::sanitize($username);
+
+    $ips = self::getIpAddresses();
+    $sremoteIp = db::sanitize($ips["remoteIp"]);
+    $sremoteIpBlock = db::sanitize($ips["remoteIpBlock"]);
+
+    return mysqli_query($con, "INSERT INTO signinattempts (username, remoteip, remoteipblock, signinattempttime) VALUES ('$susername', '$sremoteIp', '$sremoteIpBlock', NOW())");
+  }
+
+  public static function getRateLimitingCounts($username) {
+    global $con, $conf;
+
+    $susername = db::sanitize($username);
+
+    $ips = self::getIpAddresses();
+    $sremoteIp = db::sanitize($ips["remoteIp"]);
+    $sremoteIpBlock = db::sanitize($ips["remoteIpBlock"]);
+
+    $query = mysqli_query($con, "SELECT
+        COUNT(*) AS global_attempt_count,
+        IFNULL(SUM(CASE WHEN remoteip = '$sremoteIp' THEN 1 ELSE 0 END), 0) AS ip_attempt_count,
+        IFNULL(SUM(CASE WHEN (remoteipblock = '$sremoteIpBlock') THEN 1 ELSE 0 END), 0) AS ip_block_attempt_count,
+        (SELECT COUNT(DISTINCT remoteipblock) FROM signinattempts WHERE username = '$susername' AND signinattempttime >= (NOW() - INTERVAL 10 SECOND )) AS ip_blocks_per_username_attempt_count,
+        (SELECT COUNT(*) FROM signinattempts WHERE username = '$susername' AND signinattempttime >= (NOW() - INTERVAL 10 SECOND )) AS username_attempt_count
+      FROM signinattempts
+      WHERE signinattempttime >= (NOW() - INTERVAL 10 second)");
+
+    if ($query === false) return false;
+
+    return mysqli_fetch_assoc($query);
+  }
+
+  public static function isSignInThrottled($username) {
+    global $conf;
+
+    $count = self::getRateLimitingCounts($username);
+
+    if ($count["global_attempt_count"] >= $conf["signinThrottling"]["attemptCountLimit"]["global"] ||
+        $count["ip_attempt_count"] >= $conf["signinThrottling"]["attemptCountLimit"]["ip"] ||
+        $count["ip_block_attempt_count"] >= $conf["signinThrottling"]["attemptCountLimit"]["ipBlock"] ||
+        $count["ip_blocks_per_username_attempt_count"] >= $conf["signinThrottling"]["attemptCountLimit"]["ipBlocksPerUsername"] ||
+        $count["username_attempt_count"] >= $conf["signinThrottling"]["attemptCountLimit"]["username"]) {
+      return true;
+    }
+
+    if (!self::recordLoginAttempt($username)) {
+      echo "There was an unexpected error, so you could not be authenticated. Please contact me@avm99963.com and let them know of the error. (2)";
+      exit();
+    }
+
+    return false;
+  }
+
+  public static function isUserPassword($username, $password) {
+    global $con, $_SESSION;
+
+    $susername = db::sanitize($username);
+    $query = mysqli_query($con, "SELECT id, password FROM people WHERE ".($username === false ? "id = ".(int)$_SESSION["id"] : "username = '".$susername."'"));
+
+    if (!mysqli_num_rows($query)) {
+      return false;
+    }
+
+    $row = mysqli_fetch_assoc($query);
+
+    if (!password_verify($password, $row["password"])) {
+      return false;
+    }
+
+    return $row["id"];
+  }
+
+  public static function signIn($username, $password) {
+    global $_SESSION;
+
+    if (self::isSignInThrottled($username)) return self::SIGNIN_STATE_THROTTLED;
+
+    $id = self::isUserPassword($username, $password);
+
+    if ($id !== false) {
+      if (secondFactor::isEnabled($id)) {
+        $_SESSION["firstfactorid"] = $id;
+        return self::SIGNIN_STATE_NEEDS_SECOND_FACTOR;
+      } else {
+        $_SESSION["id"] = $id;
+        return self::SIGNIN_STATE_SIGNED_IN;
+      }
+    }
+
+    return self::SIGNIN_STATE_SIGNED_OUT;
+  }
+
+  public static function redirectAfterSignIn() {
+    global $conf;
+
+    if (self::isAllowed(self::ADMIN)) {
+      self::changeActiveView(visual::VIEW_ADMIN);
+      self::go("home.php");
+    } else {
+      self::changeActiveView(visual::VIEW_WORKER);
+      self::go(($conf["enableWorkerUI"] ? "workerhome.php" : "index.php?msg=unsupported"));
+    }
+  }
+
+  public static function logout() {
+    global $_SESSION;
+
+    session_destroy();
+  }
+
+  public static function htmlsafe($string) {
+    if ($string === null) return '';
+    return htmlspecialchars((string)$string);
+  }
+
+  private static function failedCheckParams($parameter, $method, $check) {
+    global $conf;
+
+    if ($conf["superdebug"]) {
+      echo "Failed check ".(int)$check." using parameter '".self::htmlsafe($parameter)."' with method ".self::htmlsafe($method).".<br>";
+    }
+  }
+
+  public static function checkParam($param, $check) {
+    if ($check == self::PARAM_NEMPTY && empty($param)) {
+      return false;
+    }
+
+    if ($check == self::PARAM_ISEMAIL && filter_var($param, FILTER_VALIDATE_EMAIL) === false) {
+      return false;
+    }
+
+    if ($check == self::PARAM_ISDATE && preg_match("/^[0-9]+-[0-9]+-[0-9]+$/", $param) !== 1) {
+      return false;
+    }
+
+    if ($check == self::PARAM_ISINT && filter_var($param, FILTER_VALIDATE_INT) === false) {
+      return false;
+    }
+
+    if ($check == self::PARAM_ISTIME) {
+      if (preg_match("/^[0-9]+:[0-9]+$/", $param) !== 1) return false;
+
+      $time = explode(":", $param);
+      if ((int)$time[0] >= 24 || (int)$time[1] >= 60) return false;
+    }
+
+    if ($check == self::PARAM_ISARRAY) {
+      // Check whether the parameter is an array
+      if (is_array($param) === false) return false;
+
+      // Check that it is not a multidimensional array (we don't want that for any parameter!)
+      foreach ($param as &$el) {
+        if (is_array($el)) return false;
+      }
+    }
+
+    if ($check == self::PARAM_ISEMAILOREMPTY && $param !== "" && filter_var($param, FILTER_VALIDATE_EMAIL) === false) {
+      return false;
+    }
+
+    return true;
+  }
+
+  public static function checkParams($method, $parameters, $forceDisableDebug = false) {
+    global $_POST, $_GET;
+
+    if (!in_array($method, ["GET", "POST"])) {
+      return false;
+    }
+
+    foreach ($parameters as $p) {
+      if (!$p[1]) {
+        continue;
+      }
+
+      if (($method == "POST" && !isset($_POST[$p[0]])) || ($method == "GET" && !isset($_GET[$p[0]]))) {
+        if (!$forceDisableDebug) self::failedCheckParams($p[0], $method, self::PARAM_ISSET);
+        return false;
+      }
+
+      $value = ($method == "POST" ? $_POST[$p[0]] : $_GET[$p[0]]);
+
+      foreach (self::$automatedChecks as $check) {
+        if (($p[1] & $check) && !self::checkParam($value, $check)) {
+          if (!$forceDisableDebug) self::failedCheckParams($p[0], $method, $check);
+          return false;
+        }
+      }
+    }
+
+    return true;
+  }
+
+  public static function existsType($type) {
+    $types = array_keys(self::$types);
+
+    return in_array($type, $types);
+  }
+
+  public static function getActiveView() {
+    global $_SESSION;
+
+    if (!self::isAllowed(self::ADMIN)) {
+      return visual::VIEW_WORKER;
+    }
+
+    return (!isset($_SESSION["activeView"]) ? visual::VIEW_ADMIN : $_SESSION["activeView"]);
+  }
+
+  public static function isAdminView() {
+    return (self::getActiveView() === visual::VIEW_ADMIN);
+  }
+
+  public static function changeActiveView($view) {
+    $_SESSION["activeView"] = $view;
+  }
+
+  public static function passwordIsGoodEnough($password) {
+    return (strlen($password) >= 8 && preg_match('/[A-Z]/', $password));
+  }
+
+  public static function cleanSignInAttempts($retentionDays = "DEFAULT") {
+    global $con, $conf;
+
+    if ($retentionDays === "DEFAULT") $retentionDays = $conf["signinThrottling"]["retentionDays"];
+
+    $sretentionDays = (int)$retentionDays;
+    if ($retentionDays < 0) return false;
+
+    return mysqli_query($con, "DELETE FROM signinattempts WHERE signinattempttime < (NOW() - INTERVAL $sretentionDays DAY)");
+  }
+}
diff --git a/src/inc/validations.php b/src/inc/validations.php
new file mode 100644
index 0000000..d3c310f
--- /dev/null
+++ b/src/inc/validations.php
@@ -0,0 +1,234 @@
+<?php
+class validations {
+  const DEFAULT_REMINDER_GRACE_PERIOD = 3; // When sending email notifications about pending elements to verify,
+                                   // only elements effective until the current day minus the grace period
+                                   // will be checked for validation. (value is given in days)
+
+  const METHOD_SIMPLE = 0;
+  const METHOD_AUTOVALIDATION = 1;
+
+  public static $methodCodename = [
+    0 => "simple",
+    1 => "autovalidation"
+  ];
+
+  public static $methodName = [
+    0 => "Validación por dirección IP",
+    1 => "Validación automática"
+  ];
+
+  public static $methods = [self::METHOD_SIMPLE, self::METHOD_AUTOVALIDATION];
+  public static $manualMethods = [self::METHOD_SIMPLE];
+
+  public static function reminderGracePeriod() {
+    return $conf["validation"]["gracePeriod"] ?? self::DEFAULT_REMINDER_GRACE_PERIOD;
+  }
+
+  public static function numPending($userId = "ME", $gracePeriod = 0) {
+    global $con;
+
+    if ($userId === "ME") $userId = people::userData("id");
+    $suser = (int)$userId;
+
+    $query = mysqli_query($con, "SELECT COUNT(*) count FROM incidents i INNER JOIN workers w ON i.worker = w.id WHERE w.person = ".$suser." AND ".incidents::$workerPendingWhere." AND ".incidents::$activeWhere.($gracePeriod === false ? "" : " AND (i.day + i.begins) < ".(int)(time() - (int)$gracePeriod*(24*60*60))));
+
+    if (!mysqli_num_rows($query)) return "?";
+
+    $row = mysqli_fetch_assoc($query);
+
+    $count = (int)(isset($row["count"]) ? $row["count"] : 0);
+
+    $query2 = mysqli_query($con, "SELECT COUNT(*) count FROM records r INNER JOIN workers w ON r.worker = w.id WHERE w.person = ".$suser." AND ".registry::$notInvalidatedWhere." AND ".registry::$workerPendingWhere.($gracePeriod === false ? "" : " AND r.day < ".(int)(time() - (int)$gracePeriod*(24*60*60))));
+
+    if (!mysqli_num_rows($query2)) return "?";
+
+    $row2 = mysqli_fetch_assoc($query2);
+
+    return $count + (int)(isset($row2["count"]) ? $row2["count"] : 0);
+  }
+
+  public static function getAllowedMethods() {
+    global $conf;
+
+    $allowedMethods = [];
+    foreach (self::$manualMethods as $method) {
+      if (in_array($method, $conf["validation"]["allowedMethods"])) $allowedMethods[] = $method;
+    }
+
+    return $allowedMethods;
+  }
+
+  private static function string2array($string) {
+    if (!is_string($string) || empty($string)) return [];
+    $explode = explode(",", $string);
+
+    $array = [];
+    foreach ($explode as $el) {
+      $array[] = (int)$el;
+    }
+
+    return $array;
+  }
+
+  private static function createSimpleAttestation($method, $user) {
+    $attestation = [];
+    if (!isset($_SERVER["REMOTE_ADDR"])) return false;
+    $attestation["ipAddress"] = (string)$_SERVER["REMOTE_ADDR"];
+    if ($method === self::METHOD_AUTOVALIDATION) $attestation["user"] = (int)$user;
+    return $attestation;
+  }
+
+  private static function createIncidentAttestation(&$incident, $method, $user) {
+    switch ($method) {
+      case self::METHOD_SIMPLE:
+      case self::METHOD_AUTOVALIDATION:
+      return self::createSimpleAttestation($method, $user);
+      break;
+
+      default:
+      return false;
+    }
+  }
+
+  private static function createRecordAttestation(&$record, $method, $user) {
+    switch ($method) {
+      case self::METHOD_SIMPLE:
+      case self::METHOD_AUTOVALIDATION:
+      return self::createSimpleAttestation($method, $user);
+      break;
+
+      default:
+      return false;
+    }
+  }
+
+  private static function createValidation($method, $attestation) {
+    $validation = [];
+    $validation["method"] = (int)$method;
+    $validation["timestamp"] = time();
+    $validation["attestation"] = $attestation;
+
+    return $validation;
+  }
+
+  public static function validateIncident($id, $method, $user, $userCheck = true, $stateCheck = true) {
+    global $con;
+
+    if ($user == "ME") $user = people::userData("id");
+    if ($userCheck && !incidents::checkIncidentIsFromPerson($id, "ME", true)) return false;
+
+    $incident = incidents::get($id, true);
+    if ($incident === false || ($stateCheck && $incident["state"] !== incidents::STATE_REGISTERED)) return false;
+
+    $attestation = self::createIncidentAttestation($incident, $method, $user);
+    if ($attestation === false) return false;
+
+    $validation = self::createValidation($method, $attestation);
+
+    $svalidation = db::sanitize(json_encode($validation));
+    $sid = (int)$incident["id"];
+
+    return mysqli_query($con, "UPDATE incidents SET workervalidated = 1, workervalidation = '$svalidation' WHERE id = $sid LIMIT 1");
+  }
+
+  public static function validateRecord($id, $method, $user, $userCheck = true) {
+    global $con;
+
+    if ($user == "ME") $user = people::userData("id");
+    if ($userCheck && !registry::checkRecordIsFromPerson($id, "ME", true)) return false;
+
+    $record = registry::get($id, true);
+    if ($record === false || $record["state"] !== registry::STATE_REGISTERED) return false;
+
+    $attestation = self::createRecordAttestation($record, $method, $user);
+    if ($attestation === false) return false;
+
+    $validation = self::createValidation($method, $attestation);
+
+    $svalidation = db::sanitize(json_encode($validation));
+    $sid = (int)$record["id"];
+
+    return mysqli_query($con, "UPDATE records SET workervalidated = 1, workervalidation = '$svalidation' WHERE id = $sid LIMIT 1");
+  }
+
+  public static function validate($method, $incidents, $records, $user = "ME") {
+    if (!in_array($method, self::getAllowedMethods())) return -1;
+
+    $incidents = self::string2array($incidents);
+    $records = self::string2array($records);
+
+    if ($user == "ME") $user = people::userData("id");
+
+    $flag = false;
+    foreach ($incidents as $incident) {
+      if (!self::validateIncident($incident, $method, $user)) $flag = true;
+    }
+    foreach ($records as $record) {
+      if (!self::validateRecord($record, $method, $user)) $flag = true;
+    }
+
+    return ($flag ? 1 : 0);
+  }
+
+  public static function getPeopleWithPendingValidations($gracePeriod = "DEFAULT") {
+    if ($gracePeriod === "DEFAULT") $gracePeriod = self::reminderGracePeriod();
+
+    $pendingPeople = [];
+
+    $people = people::getAll();
+    foreach ($people as $p) {
+      $numPending = self::numPending((int)$p["id"], $gracePeriod);
+
+      if ($numPending > 0) {
+        $pending = [];
+        $pending["person"] = $p;
+        $pending["numPending"] = $numPending;
+
+        $pendingPeople[] = $pending;
+      }
+    }
+
+    return $pendingPeople;
+  }
+
+  public static function sendPendingValidationsReminder() {
+    global $conf;
+
+    if (!$conf["mail"]["enabled"]) {
+      echo "[error] The mail functionality is not enabled in config.php.\n";
+      return false;
+    }
+
+    if (!$conf["mail"]["capabilities"]["sendPendingValidationsReminder"]) {
+      echo "[error] The pending validation reminders functionality is not inabled in config.php.\n";
+      return false;
+    }
+
+    $pendingPeople = self::getPeopleWithPendingValidations();
+
+    foreach ($pendingPeople as $p) {
+      if (!isset($p["person"]["email"]) || empty($p["person"]["email"])) {
+        echo "[info] ".$p["person"]["id"]." doesn't have an email address defined.\n";
+        continue;
+      }
+
+      $to = [["email" => $p["person"]["email"]]];
+
+      $subject = "[Recordatorio] Tienes validaciones pendientes en el aplicativo de control horario";
+      $body = mail::bodyTemplate("<p>Hola ".security::htmlsafe(($p["person"]["name"]) ?? "").",</p>
+      <p>Este es un mensaje automático para avisarte de que tienes ".(int)$p["numPending"]." incidencias y/o registros pendientes de validar en el aplicativo de control horario.</p>
+      <p>Para validarlos, puedes acceder al aplicativo desde la siguiente dirección:</p>
+      <ul>
+        <li><a href='".security::htmlsafe($conf["fullPath"])."'>".security::htmlsafe($conf["fullPath"])."</a></li>
+      </ul>");
+
+      if (mail::send($to, [], $subject, $body)) {
+        echo "[info] Mail sent to ".$p["person"]["id"]." successfuly.\n";
+      } else {
+        echo "[error] The email couldn't be sent to ".$p["person"]["id"]."\n";
+      }
+    }
+
+    return true;
+  }
+}
diff --git a/src/inc/validationsView.php b/src/inc/validationsView.php
new file mode 100644
index 0000000..b6b2b68
--- /dev/null
+++ b/src/inc/validationsView.php
@@ -0,0 +1,105 @@
+<?php
+class validationsView {
+  public static function renderPendingValidations($userId) {
+    $workers = workers::getPersonWorkers((int)$userId);
+    $companies = companies::getAll();
+
+    if ($workers === false || $companies === false) {
+      return false;
+    }
+    ?>
+    <h4>Incidencias</h4>
+    <?php
+    $iFlag = false;
+    foreach ($workers as $w) {
+      $incidents = incidents::getAll(false, 0, 0, (int)$w["id"], null, null, true);
+      if ($incidents === false) continue;
+
+      if (count($incidents)) {
+        $iFlag = true;
+        echo "<h5>".security::htmlsafe($w["companyname"])."</h5>";
+        incidentsView::renderIncidents($incidents, $companies, false, false, true, true);
+      }
+
+      visual::printDebug("incidents::getAll(false, 0, 0, ".(int)$w["id"].", null, null, true)", $incidents);
+    }
+
+    if (!$iFlag) echo "<p>No hay incidencias pendientes para validar.</p>";
+    ?>
+
+    <h4>Registros de horario</h4>
+    <?php
+    $rFlag = false;
+    foreach ($workers as $w) {
+      $registry = registry::getWorkerRecords((int)$w["id"], false, false, false, true);
+      if ($registry === false) continue;
+
+      if (count($registry)) {
+        $rFlag = true;
+        echo "<h5>".security::htmlsafe($w["companyname"])."</h5>";
+        registryView::renderRegistry($registry, $companies, false, false, true, true);
+      }
+
+      visual::printDebug("registry::getWorkerRecords(".(int)$w["id"].", false, false, false, true)", $registry);
+    }
+
+    if (!$rFlag) echo "<p>No hay registros pendientes para validar.</p>";
+
+    if ($iFlag || $rFlag) {
+      ?>
+      <p style="margin-top: 16px;"><button id="submit" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent">Validar</button></p>
+      <?php
+    }
+  }
+
+  public static function renderChallengeInstructions($method) {
+    switch ($method) {
+      case validations::METHOD_SIMPLE:
+      ?>
+      <form action="dovalidate.php" method="POST">
+        <input type="hidden" name="incidents" value="<?=security::htmlsafe($_POST["incidents"] ?? "")?>">
+        <input type="hidden" name="records" value="<?=security::htmlsafe($_POST["records"] ?? "")?>">
+        <input type="hidden" name="method" value="<?=(int)validations::METHOD_SIMPLE?>">
+        <p>Para completar la validación guardaremos tu <a href="https://help.gnome.org/users/gnome-help/stable/net-what-is-ip-address.html.es" target="_blank" rel="noopener noreferrer">dirección IP</a> y la fecha y hora actual. Para proceder, haz clic en el siguiente botón:</p>
+        <p><button id="submit" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent">Confirmar validación</button></p>
+      </form>
+      <?php
+      break;
+
+      default:
+      echo "Undefined";
+    }
+  }
+
+  public static function noPIIIpAddress($validation) {
+    $ipAddress = $validation["attestation"]["ipAddress"] ?? "unknown";
+
+    if (security::isAdminView() || $validation["method"] == validations::METHOD_SIMPLE || ($validation["method"] == validations::METHOD_AUTOVALIDATION && $validation["attestation"]["user"] == people::userData("id"))) return $ipAddress;
+
+    return "oculta (solo visible para los administradores)";
+  }
+
+  public static function renderValidationInfo($validation) {
+    ?>
+    <li><b>Validada por el trabajador:</b></li>
+    <ul>
+      <li><b>Método de validación:</b> <?=security::htmlsafe(validations::$methodName[$validation["method"]])?></li>
+      <li><b>Fecha de validación:</b> <?=security::htmlsafe(date("d/m/Y H:i", $validation["timestamp"]))?></li>
+      <?php
+      switch ($validation["method"]) {
+        case validations::METHOD_SIMPLE:
+        echo "<li><b>Dirección IP:</b> ".security::htmlsafe(self::noPIIIpAddress($validation))."</li>";
+        break;
+
+        case validations::METHOD_AUTOVALIDATION:
+        echo "<li><b>Dirección IP:</b> ".security::htmlsafe(self::noPIIIpAddress($validation))."</li>";
+        echo "<li><b>Persona que autovalida la incidencia:</b> ".security::htmlsafe(people::userData("name", $validation["attestation"]["user"]))."</li>";
+        break;
+
+        default:
+      }
+      ?>
+    </ul>
+    <?php
+  }
+}
diff --git a/src/inc/visual.php b/src/inc/visual.php
new file mode 100644
index 0000000..02a3457
--- /dev/null
+++ b/src/inc/visual.php
@@ -0,0 +1,192 @@
+<?php
+class visual {
+  const VIEW_ADMIN = 0;
+  const VIEW_WORKER = 1;
+
+  // Old:
+  /*const YES = "✓";
+  const NO = "✗";*/
+
+  // New:
+  const YES = "✓";
+  const NO = "X";
+
+  public static function snackbar($msg, $timeout = 10000, $printHTML = true) {
+    if ($printHTML) echo '<div class="mdl-snackbar mdl-js-snackbar">
+      <div class="mdl-snackbar__text"></div>
+      <button type="button" class="mdl-snackbar__action"></button>
+    </div>';
+    echo '<script>
+    window.addEventListener("load", function() {
+      var notification = document.querySelector(".mdl-js-snackbar");
+      notification.MaterialSnackbar.showSnackbar(
+        {
+          message: "'.security::htmlsafe($msg).'",
+          timeout: '.(int)$timeout.'
+        }
+      );
+    });
+    </script>';
+  }
+
+  public static function smartSnackbar($msgs, $timeout = 10000, $printHTML = true) {
+    global $_GET;
+
+    if (!isset($_GET["msg"])) return;
+
+    foreach ($msgs as $msg) {
+      if ($_GET["msg"] == $msg[0]) {
+        self::snackbar($msg[1], $timeout, $printHTML);
+        return;
+      }
+    }
+  }
+
+  public static function debugJson($array) {
+    return security::htmlsafe(json_encode($array, JSON_PRETTY_PRINT));
+  }
+
+  public static function includeHead() {
+    include("includes/head.php");
+  }
+
+  public static function includeNav() {
+    global $conf, $mdHeaderRowMore, $mdHeaderMore, $mdHeaderRowBefore;
+
+    $activeView = security::getActiveView();
+    switch ($activeView) {
+      case self::VIEW_ADMIN:
+      include("includes/adminnav.php");
+      break;
+
+      case self::VIEW_WORKER:
+      include("includes/workernav.php");
+      break;
+
+      default:
+      exit();
+    }
+  }
+
+  public static function backBtn($url) {
+    return '<a class="backbtn mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect" href="'.$url.'"><i id="auto_backbtn" class="material-icons">arrow_back</i></a><div class="mdl-tooltip" for="auto_backbtn">Atrás</div><div style="width: 16px;"></div>';
+  }
+
+  public static function printDebug($function, $return, $always=false, $notjson=false) {
+    global $conf;
+
+    if ($always || $conf["debug"])
+      echo '<details class="debug margintop">
+        <summary>Debug:</summary>
+        <p><b>'.security::htmlsafe($function).'</b></p>
+        <div class="overflow-wrapper"><pre>'.($notjson ? security::htmlsafe(print_r($return, true)) : self::debugJson($return)).'</pre></div>
+      </details>';
+  }
+
+  public static function renderPagination($rows, $page, $limit = 10, $showLimitLink = false, $alreadyHasParameters = false, $limitChange = false, $highlightedPage = false) {
+    global $_GET;
+
+    $numPages = ($limit == 0 ? 1 : ceil($rows/$limit));
+    if ($numPages > 1) {
+      $currentPage = ((isset($_GET["page"]) && $_GET["page"] <= $numPages && $_GET["page"] >= 1) ? $_GET["page"] : 1);
+
+      echo '<div class="pagination">';
+      for ($i = 1; $i <= $numPages; $i++) {
+        echo ($i != $currentPage ? '<a class="page'.($i == $highlightedPage ? " mdl-color-text--green" : "").'" href="'.security::htmlsafe($page).($alreadyHasParameters ? "&" : "?").'page='.(int)$i.($showLimitLink ? '&limit='.(int)$limit : '').'">'.(int)$i.'</a> ' : '<b class="page">'.(int)$i.'</b> ');
+      }
+      echo '</div>';
+    }
+
+    if ($limitChange !== false) {
+      ?>
+      <div class="limit-change-container">Ver <select id="limit-change">
+        <?php
+        if (isset($limitChange["options"])) {
+          if (!in_array($limit, $limitChange["options"])) {
+            echo "<option value=\"".(int)$limit."\" selected>".(int)$limit."</option>";
+          }
+          foreach ($limitChange["options"] as $option) {
+            echo "<option value=\"".(int)$option."\"".($option == $limit ? " selected" : "").">".(int)$option."</option>";
+          }
+        }
+        ?>
+      </select> <?=security::htmlsafe($limitChange["elementName"])?> por página.</div>
+      <?php
+    }
+  }
+
+  public static function padNum($num, $length) {
+    return str_pad($num, $length, "0", STR_PAD_LEFT);
+  }
+
+  public static function isMDColor() {
+    global $conf;
+
+    return (($conf["backgroundColor"][0] ?? "") != "#");
+  }
+
+  public static function printBodyTag() {
+    global $conf;
+
+    $conf["backgroundColorIsDark"];
+    echo "<body ".(!visual::isMDColor() ? "style=\"background-color: ".security::htmlsafe($conf["backgroundColor"]).";\"" : "")."class=\"".(visual::isMDColor() ? "mdl-color--".security::htmlsafe($conf["backgroundColor"]) : "").($conf["backgroundColorIsDark"] ? " dark-background" : "")."\">";
+  }
+
+  // WARNING: We will not sanitize $msg, so sanitize it before calling this function!
+  public static function addTooltip($id, $msg) {
+    global $_tooltips;
+
+    if (!isset($_tooltips)) $_tooltips = "";
+    $_tooltips .= '<div class="mdl-tooltip" for="'.security::htmlsafe($id).'">'.$msg.'</div>';
+  }
+
+  public static function renderTooltips() {
+    global $_tooltips;
+
+    echo ($_tooltips ?? "");
+  }
+
+  private static function addMsgToUrl($url, $msg = false) {
+    if ($msg === false) return $url;
+    return $url.(preg_match("/\?/", $url) == 1 ? "&" : "?")."msg=".urlencode($msg);
+  }
+
+  public static function getContinueUrl($defaultUrl, $msg = false, $method = "GET") {
+    global $_GET, $_POST;
+
+    $url = "";
+
+    switch ($method) {
+      case "GET":
+      if (!isset($_GET["continue"])) return self::addMsgToUrl($defaultUrl, $msg);
+      $url = (string)$_GET["continue"];
+      break;
+
+      case "POST":
+      if (!isset($_POST["continue"])) return self::addMsgToUrl($defaultUrl, $msg);
+      $url = (string)$_POST["continue"];
+      break;
+
+      default:
+      return self::addMsgToUrl($defaultUrl, $msg);
+    }
+
+    if (!preg_match("/^[^\/\\\\]*$/", $url)) return self::addMsgToUrl($defaultUrl, $msg);
+
+    if ($msg !== false) $url = self::addMsgToUrl($url, $msg);
+
+    return $url;
+  }
+
+  public static function addContinueInput($url = false) {
+    global $_GET, $_POST;
+
+    if ($url === false) {
+      if (isset($_GET["continue"])) $url = $_GET["continue"];
+      elseif (isset($_POST["continue"])) $url = $_POST["continue"];
+      else return;
+    }
+
+    echo '<input type="hidden" name="continue" value="'.security::htmlsafe($url).'">';
+  }
+}
diff --git a/src/inc/workers.php b/src/inc/workers.php
new file mode 100644
index 0000000..6c6e1d9
--- /dev/null
+++ b/src/inc/workers.php
@@ -0,0 +1,171 @@
+<?php
+class workers {
+  const AFFILIATION_STATUS_NOTWORKING = 0;
+  const AFFILIATION_STATUS_WORKING = 1;
+  const AFFILIATION_STATUS_AUTO_NOTWORKING = 2;
+  const AFFILIATION_STATUS_AUTO_WORKING = 3;
+
+  public static $affiliationStatuses = [self::AFFILIATION_STATUS_NOTWORKING, self::AFFILIATION_STATUS_WORKING, self::AFFILIATION_STATUS_AUTO_NOTWORKING, self::AFFILIATION_STATUS_AUTO_WORKING];
+  public static $affiliationStatusesNotWorking = [self::AFFILIATION_STATUS_NOTWORKING, self::AFFILIATION_STATUS_AUTO_NOTWORKING];
+  public static $affiliationStatusesWorking = [self::AFFILIATION_STATUS_WORKING, self::AFFILIATION_STATUS_AUTO_WORKING];
+  public static $affiliationStatusesAutomatic = [self::AFFILIATION_STATUS_AUTO_WORKING, self::AFFILIATION_STATUS_AUTO_NOTWORKING];
+  public static $affiliationStatusesManual = [self::AFFILIATION_STATUS_WORKING, self::AFFILIATION_STATUS_NOTWORKING];
+
+  private static $return = "w.id id, w.person person, w.company company, c.name companyname, p.name name, p.dni dni";
+
+  public static function get($id) {
+    global $con;
+
+    $sid = (int)$id;
+
+    $query = mysqli_query($con, "SELECT ".self::$return."
+      FROM workers w
+      LEFT JOIN companies c ON w.company = c.id
+      LEFT JOIN people p ON w.person = p.id
+      WHERE w.id = $sid");
+
+    if (!mysqli_num_rows($query)) return false;
+
+    return mysqli_fetch_assoc($query);
+  }
+
+  public static function sqlAddonToGetStatusAttribute($id) {
+    $sid = (int)$id;
+
+    return "LEFT JOIN workhistory h ON w.id = h.worker
+    WHERE
+      w.person = $sid AND
+      (
+        (
+          SELECT COUNT(*)
+          FROM workhistory
+          WHERE worker = w.id AND day <= UNIX_TIMESTAMP()
+          LIMIT 1
+        ) = 0 OR
+
+        h.id = (
+          SELECT id
+          FROM workhistory
+          WHERE worker = w.id AND day <= UNIX_TIMESTAMP()
+          ORDER BY day DESC
+          LIMIT 1
+        )
+      )";
+  }
+
+  public static function getPersonWorkers($person, $simplify = false) {
+    global $con;
+
+    $query = mysqli_query($con, "SELECT ".($simplify ? "w.id id" : self::$return).", h.status status, h.day lastupdated
+      FROM workers w
+      LEFT JOIN companies c ON w.company = c.id
+      LEFT JOIN people p ON w.person = p.id
+      ".self::sqlAddonToGetStatusAttribute($person)."
+      ORDER BY
+        w.id ASC");
+
+    $results = [];
+
+    while ($row = mysqli_fetch_assoc($query)) {
+      $row["hidden"] = self::isHidden($row["status"]);
+      $results[] = ($simplify ? $row["id"] : $row);
+    }
+
+    return $results;
+  }
+
+  public static function isHidden($status) {
+    return (in_array($status, self::$affiliationStatusesWorking) ? 0 : 1);
+  }
+
+  public static function affiliationStatusHelper($status) {
+    return (self::isHidden($status) ? "Baja" : "Alta");
+  }
+
+  public static function affiliationStatusIcon($status) {
+    return (self::isHidden($status) ? "work_off" : "work");
+  }
+
+  public static function isAutomaticAffiliation($status) {
+    return in_array($status, self::$affiliationStatusesAutomatic);
+  }
+
+  public static function getWorkHistory($id) {
+    global $con;
+
+    $sid = (int)$id;
+
+    $query = mysqli_query($con, "SELECT * FROM workhistory WHERE worker = $sid ORDER BY day DESC");
+    if ($query === false) return false;
+
+    $items = [];
+    while ($row = mysqli_fetch_assoc($query)) {
+      $items[] = $row;
+    }
+
+    return $items;
+  }
+
+  public static function getWorkHistoryItem($id) {
+    global $con;
+
+    $sid = (int)$id;
+
+    $query = mysqli_query($con, "SELECT * FROM workhistory WHERE id = $sid");
+    if ($query === false || !mysqli_num_rows($query)) return false;
+
+    return mysqli_fetch_assoc($query);
+  }
+
+  public static function addWorkHistoryItem($id, $day, $status, $internal = false) {
+    global $con;
+
+    $sid = (int)$id;
+    $stime = (int)$day;
+    $sstatus = (int)$status;
+
+    if ((!$internal && !in_array($sstatus, self::$affiliationStatusesManual)) || ($internal && !in_array($affiliationStatuses))) return false;
+
+    if (!workers::exists($sid)) return false;
+
+    return mysqli_query($con, "INSERT INTO workhistory (worker, day, status) VALUES ($sid, $stime, $sstatus)");
+  }
+
+  public static function editWorkHistoryItem($id, $day, $status, $internal = false) {
+    global $con;
+
+    $sid = (int)$id;
+    $stime = (int)$day;
+    $sstatus = (int)$status;
+
+    if ((!$internal && !in_array($sstatus, self::$affiliationStatusesManual)) || ($internal && !in_array($affiliationStatuses))) return false;
+
+    if (!self::existsWorkHistoryItem($id)) return false;
+
+    return mysqli_query($con, "UPDATE workhistory SET day = $stime, status = $sstatus WHERE id = $sid LIMIT 1");
+  }
+
+  public static function deleteWorkHistoryItem($id) {
+    global $con;
+
+    $sid = (int)$id;
+
+    return mysqli_query($con, "DELETE FROM workhistory WHERE id = $sid LIMIT 1");
+  }
+
+  public static function exists($id) {
+    global $con;
+
+    $query = mysqli_query($con, "SELECT id FROM workers WHERE id = ".(int)$id);
+
+    return (mysqli_num_rows($query) > 0);
+  }
+
+  public static function existsWorkHistoryItem($id) {
+    global $con;
+
+    $query = mysqli_query($con, "SELECT 1 FROM workhistory WHERE id = ".(int)$id);
+
+    return (mysqli_num_rows($query) > 0);
+  }
+}
diff --git a/src/incidentattachment.php b/src/incidentattachment.php
new file mode 100644
index 0000000..9d4394d
--- /dev/null
+++ b/src/incidentattachment.php
@@ -0,0 +1,50 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER, security::METHOD_NOTFOUND);
+security::checkWorkerUIEnabled();
+
+$returnURL = (security::isAdminView() ? "incidents.php?" : "userincidents.php?id=".$_SESSION["id"]."&");
+
+if (!security::checkParams("GET", [
+  ["id", security::PARAM_ISINT],
+  ["name", security::PARAM_NEMPTY]
+])) {
+  security::go($returnURL."msg=unexpected");
+}
+
+$id = (int)$_GET["id"];
+$name = $_GET["name"];
+
+$incident = incidents::get($id, true);
+if ($incident === false) security::go($returnURL."msg=unexpected");
+
+if (!security::isAllowed(security::ADMIN)) incidents::checkIncidentIsFromPerson($incident["id"]);
+
+$attachments = incidents::getAttachmentsFromIncident($incident);
+
+if ($attachments === false || !count($attachments)) security::go($returnURL."msg=unexpected");
+
+$flag = false;
+
+foreach ($attachments as $attachment) {
+  if ($attachment == $name) {
+    $flag = true;
+
+    $fullpath = $conf["attachmentsFolder"].$attachment;
+    $extension = files::getFileExtension($attachment);
+
+    if (!isset(files::$mimeTypes[$extension])) {
+      exit();
+    }
+
+    header("Content-type: ".(files::$mimeTypes[$extension] ?? "application/octet-stream"));
+    header("Content-Disposition: filename=\"".$attachment."\"");
+    header("Content-Length: ".filesize($fullpath));
+    header("Cache-control: private");
+    readfile($fullpath);
+
+    break;
+  }
+}
+
+if ($flag === false) security::go($returnURL."msg=unexpected");
diff --git a/src/incidents.php b/src/incidents.php
new file mode 100644
index 0000000..fb693b1
--- /dev/null
+++ b/src/incidents.php
@@ -0,0 +1,172 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+incidentsView::handleIncidentShortcuts();
+
+incidentsView::buildSelect();
+
+$numRows = incidents::numRows($select);
+$companies = companies::getAll();
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+  <link rel="stylesheet" href="css/incidents.css">
+
+  <style>
+  .addincident, .addrecurringincident, .filter {
+    z-index: 1000;
+  }
+
+  .addincident {
+    position: fixed;
+    bottom: 16px;
+    right: 16px;
+  }
+
+  .addrecurringincident {
+    position: fixed;
+    bottom: 80px;
+    right: 25px;
+  }
+
+  .filter {
+    position: fixed;
+    bottom: 126px;
+    right: 25px;
+  }
+
+  .helper-bellow-incidents-list {
+    text-align: center;
+    font-size: 13px;
+  }
+
+  @media (max-width: 655px) {
+    .extra {
+      display: none;
+    }
+  }
+  </style>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <button class="addincident mdl-button md-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--accent"><i class="material-icons">add</i><span class="mdl-ripple"></span></button>
+    <button class="addrecurringincident mdl-button mdl-js-button mdl-button--fab mdl-button--mini-fab mdl-color--grey-200"><i class="material-icons">repeat</i><span class="mdl-ripple"></span></button>
+    <button class="filter mdl-button mdl-js-button mdl-button--fab mdl-button--mini-fab mdl-color--grey-200"><i class="material-icons">filter_list</i></button>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Incidencias</h2>
+
+          <?php
+          if ($select["showPendingQueue"]) {
+            $numRowsPending = incidents::numPending();
+            ?>
+            <h4>Cola de revisión (<?=$numRowsPending?>)<?=($select["isAnyEnabled"] ? ' <i id="tt_filtersarenotapplied" class="material-icons help">info</i>' : '')?></h4>
+            <?php
+            if ($select["isAnyEnabled"]) {
+              visual::addTooltip("tt_filtersarenotapplied", "Los filtros no se han aplicado a la cola de revisión.");
+            }
+
+            $incidentsPending = incidents::getAll(true);
+            if (count($incidentsPending)) {
+              incidentsView::renderIncidents($incidentsPending, $companies);
+            } else {
+              echo "Todas las incidencias ya han sido revisadas.";
+            }
+          }
+
+
+          $page = ($select["showResultsPaginated"] ? (isset($_GET["page"]) ? (int)$_GET["page"] - 1 : null) : 0);
+
+          if ($select["showResultsPaginated"]) {
+            $limit = (int)($_GET["limit"] ?? incidents::PAGINATION_LIMIT);
+            if ($limit <= 0 || $limit > 100) $limit = incidents::PAGINATION_LIMIT;
+          } else $limit = 0;
+
+          $incidents = incidents::getAll(null, $page, $limit, "ALL", null, null, false, $select);
+
+          if (!$select["showResultsPaginated"]) $numRows = count($incidents);
+          ?>
+
+          <h4><?=($select["isAnyEnabled"] ? "Incidencias filtradas" : "Todas las incidencias")?> (<?=(int)$numRows?>)</h4>
+
+          <?php
+          if (count($incidents)) {
+            incidentsView::renderIncidents($incidents, $companies, false, true, false, false, true);
+            /*if (!$select["showResultsPaginated"]) {
+              echo "<p class=\"helper-bellow-incidents-list mdl-color-text--grey-600\">Se está truncando la lista de incidencias a un máximo de  resultados, así que es posible que algunas incidencias no se muestren en la lista.<br>Puedes encontrar más incidencias cambiando el selector de fecha final de la sección de filtros a la fecha de la última incidencia de la lista.</p>";
+            }*/
+          } else if ($select["isAnyEnabled"]) {
+            echo "No se ha encontrado ninguna incidencia con los filtros seleccionados.";
+          } else {
+            echo "Todavía no existe ninguna incidencia. Puedes añadir una haciendo clic en el botón de la esquina inferior derecha.";
+          }
+          ?>
+
+          <?php
+          if ($select["showResultsPaginated"]) visual::renderPagination($numRows, $select["pageUrl"], $limit, ($limit == incidents::PAGINATION_LIMIT ? false : true), $select["pageUrlHasParameters"], [
+            "elementName" => "incidencias",
+            "options" => incidentsView::$limitOptions
+          ], incidents::todayPage($limit));
+          if ($select["showPendingQueue"]) visual::printDebug("incidents::getAll(true)", $incidentsPending);
+          visual::printDebug("incidents::getAll(null, ".(int)$page.", ".(int)$limit.", \"ALL\", null, null, false, \$select)", $incidents);
+          visual::printDebug("\$select", $select);
+          ?>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <?php
+  $workers = people::getAll(false, true);
+  incidentsView::renderIncidentForm($workers, function(&$worker) {
+    return (int)$worker["workerid"];
+  }, function (&$worker, &$companies) {
+    return $worker["name"].' ('.$companies[$worker["companyid"]].')';
+  }, $companies, false, false, "incidents.php");
+
+  incidentsView::renderIncidentForm($workers, function(&$worker) {
+    return (int)$worker["workerid"];
+  }, function (&$worker, &$companies) {
+    return $worker["name"].' ('.$companies[$worker["companyid"]].')';
+  }, $companies, false, true, "incidents.php");
+
+  if (isset($_GET["openRecurringFormWorker"])) {
+    $openWorker = (int)$_GET["openRecurringFormWorker"];
+    ?>
+    <script>
+    document.getElementById("recurringworker").value = "<?=security::htmlsafe($openWorker)?>";
+    document.getElementById("addrecurringincident").showModal();
+    </script>
+    <?php
+  }
+
+  incidentsView::renderFilterDialog($select);
+
+  visual::renderTooltips();
+  ?>
+
+  <div class="mdl-snackbar mdl-js-snackbar">
+    <div class="mdl-snackbar__text"></div>
+    <button type="button" class="mdl-snackbar__action"></button>
+  </div>
+
+  <?php
+  visual::smartSnackbar(incidentsView::$incidentsMsgs, 10000, false);
+  ?>
+
+  <script>
+  var _showResultsPaginated = <?=($select["showResultsPaginated"] ? "true" : "false")?>;
+  var _limit = <?=(int)$limit?>;
+  var _page = <?=(int)$page?>;
+  </script>
+  <script src="js/incidents.js"></script>
+  <script src="js/incidentsgeneric.js"></script>
+</body>
+</html>
diff --git a/src/incidenttypes.php b/src/incidenttypes.php
new file mode 100644
index 0000000..faeb753
--- /dev/null
+++ b/src/incidenttypes.php
@@ -0,0 +1,177 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+$mdHeaderRowBefore = visual::backBtn("settings.php");
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+
+  <style>
+  .addincident {
+    position: fixed;
+    bottom: 16px;
+    right: 16px;
+    z-index: 1000;
+  }
+
+  @media (max-width: 655px) {
+    .extra {
+      display: none;
+    }
+  }
+  </style>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <button class="addincident mdl-button md-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--accent"><i class="material-icons">add</i><span class="mdl-ripple"></span></button>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Tipos de incidencias</h2>
+          <?php
+          $incidents = incidents::getTypes();
+          if (count($incidents)) {
+            ?>
+            <div class="overflow-wrapper overflow-wrapper--for-table">
+              <table class="mdl-data-table mdl-js-data-table mdl-shadow--2dp">
+                <thead>
+                  <tr>
+                    <?php
+                    if ($conf["debug"]) {
+                      ?>
+                      <th class="extra">ID</th>
+                      <?php
+                    }
+                    ?>
+                    <th class="mdl-data-table__cell--non-numeric">Nombre</th>
+                    <th class="mdl-data-table__cell--centered"><i id="tt_present" class="material-icons help">business</i></th>
+                    <th class="mdl-data-table__cell--centered"><i id="tt_paid" class="material-icons help">euro_symbol</i></th>
+                    <th class="mdl-data-table__cell--centered"><i id="tt_workerfill" class="material-icons help">face</i></th>
+                    <th class="mdl-data-table__cell--centered"><i id="tt_notifies" class="material-icons help">email</i></th>
+                    <th class="mdl-data-table__cell--centered"><i id="tt_autovalidates" class="material-icons help">verified_user</i></th>
+                    <th class="mdl-data-table__cell--non-numeric"></th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <?php
+                  foreach ($incidents as $t) {
+                    ?>
+                    <tr<?=($t["hidden"] == 1 ? " class='mdl-color-text--grey-600'" : "")?>>
+                      <?php
+                      if ($conf["debug"]) {
+                        ?>
+                        <td class="extra"><?=(int)$t["id"]?></td>
+                        <?php
+                      }
+                      ?>
+                      <td class="mdl-data-table__cell--non-numeric"><?=security::htmlsafe($t["name"])?></td>
+                      <td class="mdl-data-table__cell--centered"><?=($t["present"] == 1 ? visual::YES : "")?></td>
+                      <td class="mdl-data-table__cell--centered"><?=($t["paid"] == 1 ? visual::YES : "")?></td>
+                      <td class="mdl-data-table__cell--centered"><?=($t["workerfill"] == 1 ? visual::YES : "")?></td>
+                      <td class="mdl-data-table__cell--centered"><?=($t["notifies"] == 1 ? visual::YES : "")?></td>
+                      <td class="mdl-data-table__cell--centered"><?=($t["autovalidates"] == 1 ? visual::YES : "")?></td>
+                      <td class='mdl-data-table__cell--non-numeric'><a href='dynamic/editincidenttype.php?id=<?=security::htmlsafe($t["id"])?>' data-dyndialog-href='dynamic/editincidenttype.php?id=<?=security::htmlsafe($t["id"])?>' title='Editar tipo de incidencia'><i class='material-icons icon'>edit</i></a></td>
+                    </tr>
+                    <?php
+                  }
+                  ?>
+                </tbody>
+              </table>
+            </div>
+            <?php
+            visual::addTooltip("tt_present", "Indica si el trabajador está físicamente presente en el espacio de trabajo durante la incidencia (es la opción que indica si las horas cuentan como positivas o negativas).");
+            visual::addTooltip("tt_paid", "Indica si el trabajador es remunerado las horas que dura la incidencia.");
+            visual::addTooltip("tt_workerfill", "Indica si se permite que el trabajador pueda rellenar una incidencia de este tipo él mismo (con la posterior verificación por parte de un administrador).");
+            visual::addTooltip("tt_notifies", "Indica si la introducción de una incidencia de este tipo notifica por correo electrónico a las personas especificadas en la categoría del trabajador.");
+            visual::addTooltip("tt_autovalidates", "Indica si al introducir una incidencia de este tipo se autovalida sin necesidad de ser validada posteriormente por el trabajador.");
+          } else {
+            ?>
+            <p>Todavía no hay definido ningún tipo de incidencia.</p>
+            <p>Puedes añadir uno haciendo clic en el botón de la esquina inferior derecha de la página.</p>
+            <?php
+          }
+          ?>
+
+          <?php visual::printDebug("incidents::getTypes()", $incidents); ?>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <dialog class="mdl-dialog" id="addincident">
+    <form action="doaddincidenttype.php" method="POST" autocomplete="off">
+      <h4 class="mdl-dialog__title">Añade un tipo de incidencia</h4>
+      <div class="mdl-dialog__content">
+        <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+          <input class="mdl-textfield__input" type="text" name="name" id="name" autocomplete="off" data-required>
+          <label class="mdl-textfield__label" for="name">Nombre del tipo de incidencia</label>
+        </div>
+        <p>
+          <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="present">
+            <input type="checkbox" id="present" name="present" value="1" class="mdl-switch__input">
+            <span class="mdl-switch__label">Presente <i class="material-icons help" id="add_present">help</i></span>
+          </label>
+          <div class="mdl-tooltip" for="add_present">Márquese si el trabajador está físicamente presente en el espacio de trabajo durante la incidencia.</div>
+        </p>
+        <p>
+          <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="paid">
+            <input type="checkbox" id="paid" name="paid" value="1" class="mdl-switch__input">
+            <span class="mdl-switch__label">Remunerada <i class="material-icons help" id="add_paid">help</i></span>
+          </label>
+          <div class="mdl-tooltip" for="add_paid">Márquese si el trabajador es remunerado las horas que dura la incidencia.</div>
+        </p>
+        <p>
+          <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="workerfill">
+            <input type="checkbox" id="workerfill" name="workerfill" value="1" class="mdl-switch__input">
+            <span class="mdl-switch__label">Puede autorrellenarse <i class="material-icons help" id="add_workerfill">help</i></span>
+          </label>
+        </p>
+        <div class="mdl-tooltip" for="add_workerfill">Márquese si se permite que el trabajador pueda rellenar una incidencia de este tipo él mismo (con la posterior verificación por parte de un administrador).</div>
+        <p>
+          <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="notifies">
+            <input type="checkbox" id="notifies" name="notifies" value="1" class="mdl-switch__input">
+            <span class="mdl-switch__label">Notifica <i class="material-icons help" id="add_notifies">help</i></span>
+          </label>
+          <div class="mdl-tooltip" for="add_notifies">Márquese si la introducción de una incidencia de este tipo se quiere que se notifique por correo electrónico a las personas especificadas en la categoría del trabajador.</div>
+        </p>
+        <p>
+          <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="autovalidates">
+            <input type="checkbox" id="autovalidates" name="autovalidates" value="1" class="mdl-switch__input">
+            <span class="mdl-switch__label">Se autovalida <i class="material-icons help" id="add_autovalidates">help</i></span>
+          </label>
+          <div class="mdl-tooltip" for="add_autovalidates">Márquese si al introducir una incidencia de este tipo se quiere que se autovalide sin necesidad de ser validada posteriormente por el trabajador.</div>
+        </p>
+        <p>
+          <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="hidden">
+            <input type="checkbox" id="hidden" name="hidden" value="1" class="mdl-switch__input">
+            <span class="mdl-switch__label">Oculto</span>
+          </label>
+        </p>
+      </div>
+      <div class="mdl-dialog__actions">
+        <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Añadir</button>
+        <button onclick="event.preventDefault(); document.querySelector('#addincident').close();" class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+      </div>
+    </form>
+  </dialog>
+
+  <?php
+  visual::renderTooltips();
+
+  visual::smartSnackbar([
+    ["added", "Se ha añadido el tipo de incidencia correctamente."],
+    ["modified", "Se ha modificado el tipo de incidencia correctamente."],
+    ["empty", "Faltan datos por introducir en el formulario."],
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."]
+  ]);
+  ?>
+
+  <script src="js/incidenttypes.js"></script>
+</body>
+</html>
diff --git a/src/includes/adminnav.php b/src/includes/adminnav.php
new file mode 100644
index 0000000..9f05730
--- /dev/null
+++ b/src/includes/adminnav.php
@@ -0,0 +1,44 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::ADMIN, security::METHOD_NOTFOUND);
+
+$pending = incidents::numPending();
+?>
+<header class="mdl-layout__header mdl-layout__header--scroll<?=(visual::isMDColor() ? security::htmlsafe(" mdl-color--".$conf["backgroundColor"]) : "")?>"<?=(!visual::isMDColor() ? " style=\"background-color: ".security::htmlsafe($conf["backgroundColor"]).";\"" : "")?>>
+  <div class="mdl-layout__header-row">
+    <!-- Title -->
+    <?php if (isset($mdHeaderRowBefore)) { echo $mdHeaderRowBefore; } ?>
+    <span class="mdl-layout-title">Panel de control</span>
+    <?php if (isset($mdHeaderRowMore)) { echo $mdHeaderRowMore; } ?>
+  </div>
+  <?php if (isset($mdHeaderMore)) { echo $mdHeaderMore; } ?>
+</header>
+<div class="mdl-layout__drawer">
+  <span class="mdl-layout-title"><?=(!empty($conf["logo"] ?? "") ? "<img class=\"logo\" src=\"".security::htmlsafe($conf["logo"])."\">" : "")?> <?=security::htmlsafe($conf["appName"])?></span>
+  <span class="subtitle mdl-color-text--grey-700"><?=security::htmlsafe(people::userData("name"))?></span>
+  <nav class="mdl-navigation">
+    <a class="mdl-navigation__link mdl-js-button mdl-js-ripple-effect" href="home.php"><i class="material-icons">dashboard</i> <span>Panel de Control</span></a>
+    <a class="mdl-navigation__link mdl-js-button mdl-js-ripple-effect" href="users.php"><i class="material-icons">group</i> <span>Personas</span></a>
+    <a class="mdl-navigation__link mdl-js-button mdl-js-ripple-effect" href="workers.php"><i class="material-icons">work</i> <span>Trabajadores</span></a>
+    <a class="mdl-navigation__link mdl-js-button mdl-js-ripple-effect" href="incidents.php?goTo=today"><i class="material-icons mdl-badge<?=($pending > 0 ? ' mdl-badge--overlap' : '')?>" <?=($pending > 0 ? 'data-badge="'.$pending.'"' : '')?>>assignment_late</i> <span>Incidencias</span></a>
+    <a class="mdl-navigation__link mdl-js-button mdl-js-ripple-effect" href="registry.php"><i class="material-icons">list</i> <span>Registro</span></a>
+    <a class="mdl-navigation__link mdl-js-button mdl-js-ripple-effect" href="export.php"><i class="material-icons">cloud_download</i> <span>Exportar registro</span></a>
+    <a class="mdl-navigation__link mdl-js-button mdl-js-ripple-effect" href="settings.php"><i class="material-icons">settings</i> <span>Configuración</span></a>
+    <?php
+    if ($conf["enableWorkerUI"]) {
+      ?>
+      <a class="mdl-navigation__link mdl-js-button mdl-js-ripple-effect" href="workerhome.php"><i class="material-icons">open_in_browser</i> <span>Vista de trabajador</span></a>
+      <?php
+    } elseif (secondFactor::isAvailable()) {
+      ?>
+      <a class="mdl-navigation__link mdl-js-button mdl-js-ripple-effect" href="security.php"><i class="material-icons">security</i> <span>Seguridad</span></a>
+      <?php
+    } else {
+      ?>
+      <a class="mdl-navigation__link mdl-js-button mdl-js-ripple-effect" href="changepassword.php"><i class="material-icons">account_circle</i> <span>Cambiar contraseña</span></a>
+      <?php
+    }
+    ?>
+    <a class="mdl-navigation__link mdl-js-button mdl-js-ripple-effect" class="mdl-navigation__link" href="logout.php"><i class="material-icons">power_settings_new</i> <span>Cerrar sesión</span></a>
+  </nav>
+</div>
diff --git a/src/includes/head.php b/src/includes/head.php
new file mode 100644
index 0000000..f375ecf
--- /dev/null
+++ b/src/includes/head.php
@@ -0,0 +1,18 @@
+<meta charset="UTF-8">
+<meta name="description" content="">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+
+<!-- Chrome for Android theme color -->
+<meta name="theme-color" content="#4caf50">
+
+<link rel="stylesheet" href="node_modules/material-design-lite/dist/material.green-pink.min.css">
+<script src="node_modules/material-design-lite/material.min.js"></script>
+<link rel="stylesheet" href="node_modules/material-icons/iconfont/material-icons.css">
+<link href='https://fonts.googleapis.com/css?family=Roboto:400,500,700,300' rel='stylesheet' type='text/css'>
+<link rel="stylesheet" href="node_modules/mdl-ext/lib/mdl-ext.min.css">
+<script src="node_modules/mdl-ext/lib/mdl-ext.min.js"></script>
+
+<script src="node_modules/dialog-polyfill/dist/dialog-polyfill.js"></script>
+<link rel="stylesheet" type="text/css" href="node_modules/dialog-polyfill/dist/dialog-polyfill.css">
+
+<script src="js/common.js"></script>
diff --git a/src/includes/workernav.php b/src/includes/workernav.php
new file mode 100644
index 0000000..51b6794
--- /dev/null
+++ b/src/includes/workernav.php
@@ -0,0 +1,45 @@
+<?php
+require_once(__DIR__."/../core.php");
+security::checkType(security::WORKER, security::METHOD_NOTFOUND);
+
+$pending = validations::numPending();
+?>
+<header class="mdl-layout__header mdl-layout__header--scroll<?=(visual::isMDColor() ? security::htmlsafe(" mdl-color--".$conf["backgroundColor"]) : "")?>"<?=(!visual::isMDColor() ? "style=\"background-color: ".security::htmlsafe($conf["backgroundColor"]).";\"" : "")?>>
+  <div class="mdl-layout__header-row">
+    <!-- Title -->
+    <?php if (isset($mdHeaderRowBefore)) { echo $mdHeaderRowBefore; } ?>
+    <span class="mdl-layout-title"><?=(security::isAllowed(security::ADMIN) ? "Vista de trabajador" : "Panel de control")?></span>
+    <?php if (isset($mdHeaderRowMore)) { echo $mdHeaderRowMore; } ?>
+  </div>
+  <?php if (isset($mdHeaderMore)) { echo $mdHeaderMore; } ?>
+</header>
+<div class="mdl-layout__drawer">
+  <span class="mdl-layout-title"><?=(!empty($conf["logo"] ?? "") ? "<img class=\"logo\" src=\"".security::htmlsafe($conf["logo"])."\">" : "")?> <?=security::htmlsafe($conf["appName"])?></span>
+  <span class="subtitle mdl-color-text--grey-700"><?=security::htmlsafe(people::userData("name"))?></span>
+  <nav class="mdl-navigation">
+    <a class="mdl-navigation__link mdl-js-button mdl-js-ripple-effect" href="workerhome.php"><i class="material-icons">dashboard</i> <span>Panel de Control</span></a>
+    <a class="mdl-navigation__link mdl-js-button mdl-js-ripple-effect" href="workerschedule.php"><i class="material-icons">timelapse</i> <span>Horario actual</span></a>
+    <a class="mdl-navigation__link mdl-js-button mdl-js-ripple-effect" href="userincidents.php?id=<?=(int)$_SESSION["id"]?>"><i class="material-icons mdl-badge">assignment_late</i> <span>Incidencias</span></a>
+    <a class="mdl-navigation__link mdl-js-button mdl-js-ripple-effect" href="userregistry.php?id=<?=(int)$_SESSION["id"]?>"><i class="material-icons mdl-badge">list</i> <span>Registro</span></a>
+    <a class="mdl-navigation__link mdl-js-button mdl-js-ripple-effect" href="validations.php"><i class="material-icons mdl-badge<?=($pending > 0 ? ' mdl-badge--overlap' : '')?>" <?=($pending > 0 ? 'data-badge="'.$pending.'"' : '')?>>verified_user</i> <span>Validaciones</span></a>
+    <a class="mdl-navigation__link mdl-js-button mdl-js-ripple-effect" href="export4worker.php?id=<?=(int)$_SESSION["id"]?>"><i class="material-icons mdl-badge">cloud_download</i> <span>Exportar registro</span></a>
+    <?php
+    if (secondFactor::isAvailable()) {
+      ?>
+      <a class="mdl-navigation__link mdl-js-button mdl-js-ripple-effect" href="security.php"><i class="material-icons">security</i> <span>Seguridad</span></a>
+      <?php
+    } else {
+      ?>
+      <a class="mdl-navigation__link mdl-js-button mdl-js-ripple-effect" href="changepassword.php"><i class="material-icons">account_circle</i> <span>Cambiar contraseña</span></a>
+      <?php
+    }
+
+    if (security::isAllowed(security::ADMIN)) {
+      ?>
+      <a class="mdl-navigation__link mdl-js-button mdl-js-ripple-effect" href="home.php"><i class="material-icons">open_in_browser</i> <span>Vista de administrador</span></a>
+      <?php
+    }
+    ?>
+    <a class="mdl-navigation__link mdl-js-button mdl-js-ripple-effect" class="mdl-navigation__link" href="logout.php"><i class="material-icons">power_settings_new</i> <span>Cerrar sesión</span></a>
+  </nav>
+</div>
diff --git a/src/index.php b/src/index.php
new file mode 100644
index 0000000..f9a829a
--- /dev/null
+++ b/src/index.php
@@ -0,0 +1,75 @@
+<?php
+require_once("core.php");
+
+if (security::isAllowed(security::ADMIN)) {
+  security::go("home.php");
+} elseif ($conf["enableWorkerUI"] && security::isAllowed(security::WORKER)) {
+  security::go("workerhome.php");
+}
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/index.css">
+  <script src="js/index.js"></script>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="login mdl-shadow--4dp">
+    <h2><?=security::htmlsafe($conf["appName"])?></h2>
+    <form action="signin.php" method="POST" autocomplete="off" id="formulario">
+      <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+        <input class="mdl-textfield__input" type="text" name="username" id="username" autocomplete="off" data-required>
+        <label class="mdl-textfield__label" for="username">Nombre de usuario</label>
+      </div>
+      <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+        <input class="mdl-textfield__input" type="password" name="password" id="password" autocomplete="off" data-required>
+        <label class="mdl-textfield__label" for="password">Contraseña</label>
+      </div>
+      <p><button type="submit" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent">Iniciar sesión</button><?php if ($conf["enableRecovery"]) { ?> <button id="recoverybtn" class="mdl-button mdl-js-button mdl-js-ripple-effect">Recuperar contraseña</button><?php } ?></p>
+		</form>
+  </div>
+
+  <?php
+  if ($conf["enableRecovery"]) {
+    ?>
+    <dialog class="mdl-dialog" id="recovery">
+      <form action="dostartrecovery.php" method="POST" enctype="multipart/form-data">
+        <h4 class="mdl-dialog__title">Recuperar contraseña</h4>
+        <div class="mdl-dialog__content">
+          <p>Para recuperarla, introduce los siguientes datos:</p>
+          <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+            <input class="mdl-textfield__input" type="email" name="email" id="email" autocomplete="off" data-required>
+            <label class="mdl-textfield__label" for="email">Correo electrónico</label>
+          </div>
+          <br>
+          <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+            <input class="mdl-textfield__input" type="text" name="dni" id="dni" autocomplete="off" data-required>
+            <label class="mdl-textfield__label" for="dni">DNI/NIF con letras mayúsculas</label>
+          </div>
+        </div>
+        <div class="mdl-dialog__actions">
+          <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Recuperar</button>
+          <button onclick="event.preventDefault(); document.querySelector('#recovery').close();" class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+        </div>
+      </form>
+    </dialog>
+    <?php
+  }
+
+  visual::smartSnackbar([
+    ["wrong", "Usuario y/o contraseña incorrecto."],
+    ["empty", "Por favor, rellena todos los campos."],
+    ["logout", "Has cerrado la sesión correctamente."],
+    ["unsupported", "Todavía no puedes acceder como trabajador a la interfaz de trabajador."],
+    ["unexpected", "Ha ocurrido un error inesperado."],
+    ["recovery", "Si los datos que has proporcionado son correctos, se ha enviado un mensaje al correo electrónico indicado para proceder con la recuperación."],
+    ["recovery2failed", "No se puede proceder con la recuperación, seguramente porque el enlace de recuperación ha expirado."],
+    ["recoverycompleted", "Se ha cambiado la contraseña correctamente. Puedes iniciar sesión ahora con la nueva contraseña."],
+    ["secondfactorwrongcode", "El código de verificación no es correcto."],
+    ["signinthrottled", "No se ha podido verificar si el usuario y contraseña son correctos. Por favor, prueba de iniciar sesión de nuevo en unos instantes."]
+  ]);
+  ?>
+</body>
+</html>
diff --git a/src/install.php b/src/install.php
new file mode 100644
index 0000000..30dfeff
--- /dev/null
+++ b/src/install.php
@@ -0,0 +1,287 @@
+<?php
+if (php_sapi_name() != "cli") {
+  exit();
+}
+
+// Classes autoload
+spl_autoload_register(function($className) {
+  if ($className == "lbuchs\WebAuthn\Binary\ByteBuffer") include_once(__DIR__."/lib/WebAuthn/Binary/ByteBuffer.php");
+  else include_once(__DIR__."/inc/".$className.".php");
+});
+
+require_once("config.php");
+$con = @mysqli_connect($conf["db"]["server"], $conf["db"]["user"], $conf["db"]["password"], $conf["db"]["database"]) or die("There was an error connecting to the database.");
+mysqli_set_charset($con, "utf8mb4");
+
+echo "Benvingut a l'instal·lador de l'aplicació 'Hores'.\n\n";
+echo "Entra els detalls del primer usuari administrador de l'aplicació:\n";
+echo "Nom d'usuari: ";
+$username = mysqli_real_escape_string($con, trim(fgets(STDIN)));
+
+echo "Contrasenya: ";
+system('stty -echo');
+$pw = mysqli_real_escape_string($con, password_hash(trim(fgets(STDIN)), PASSWORD_DEFAULT));
+system('stty echo');
+echo "\n";
+
+echo "Nom complet: ";
+$name = mysqli_real_escape_string($con, trim(fgets(STDIN)));
+
+echo "DNI: ";
+$dni = mysqli_real_escape_string($con, trim(fgets(STDIN)));
+
+echo "Correu electrònic: ";
+$email = mysqli_real_escape_string($con, trim(fgets(STDIN)));
+
+echo "\nGenerant taules de la base de dades:\n";
+
+$sql = [];
+
+$sql["people"] = "CREATE TABLE people (
+  id INT NOT NULL AUTO_INCREMENT,
+  PRIMARY KEY(id),
+  username VARCHAR(50) NOT NULL UNIQUE,
+  password VARCHAR(255) NOT NULL UNIQUE,
+  type INT,
+  name VARCHAR(255) NOT NULL,
+  dni VARCHAR(10) NOT NULL,
+  email VARCHAR(255) NOT NULL,
+  category INT DEFAULT -1,
+  INDEX(category),
+  secondfactor INT DEFAULT 0
+)";
+
+$sql["companies"] = "CREATE TABLE companies (
+  id INT NOT NULL AUTO_INCREMENT,
+  PRIMARY KEY(id),
+  name VARCHAR(100) NOT NULL UNIQUE,
+  cif VARCHAR(100) NOT NULL
+)";
+
+$sql["workers"] = "CREATE TABLE workers (
+  id INT NOT NULL AUTO_INCREMENT,
+  PRIMARY KEY(id),
+  person INT NOT NULL,
+  INDEX(person),
+  company INT NOT NULL,
+  INDEX(company)
+)";
+
+$sql["workhistory"] = "CREATE TABLE workhistory (
+  id INT NOT NULL AUTO_INCREMENT,
+  PRIMARY KEY(id),
+  worker INT NOT NULL,
+  INDEX(worker),
+  day INT NOT NULL,
+  status INT NOT NULL
+)";
+
+$sql["categories"] = "CREATE TABLE categories (
+  id INT NOT NULL AUTO_INCREMENT,
+  PRIMARY KEY(id),
+  name VARCHAR(100) NOT NULL UNIQUE,
+  parent INT NOT NULL,
+  emails TEXT
+)";
+
+$sql["calendars"] = "CREATE TABLE calendars (
+  id INT NOT NULL AUTO_INCREMENT,
+  PRIMARY KEY(id),
+  category INT NOT NULL,
+  begins INT NOT NULL,
+  ends INT NOT NULL,
+  details TEXT NOT NULL
+)";
+
+$sql["scheduletemplates"] = "CREATE TABLE scheduletemplates (
+  id INT NOT NULL AUTO_INCREMENT,
+  PRIMARY KEY(id),
+  name VARCHAR(50) NOT NULL UNIQUE,
+  begins INT NOT NULL,
+  ends INT NOT NULL
+)";
+
+$sql["scheduletemplatesdays"] = "CREATE TABLE scheduletemplatesdays (
+  id INT NOT NULL AUTO_INCREMENT,
+  PRIMARY KEY(id),
+  template INT NOT NULL,
+  INDEX(template),
+  day INT NOT NULL,
+  typeday INT NOT NULL,
+  beginswork INT NOT NULL,
+  endswork INT NOT NULL,
+  beginsbreakfast INT,
+  endsbreakfast INT,
+  beginslunch INT,
+  endslunch INT
+)";
+
+$sql["schedules"] = "CREATE TABLE schedules (
+  id INT NOT NULL AUTO_INCREMENT,
+  PRIMARY KEY(id),
+  worker INT NOT NULL,
+  INDEX(worker),
+  begins INT NOT NULL,
+  ends INT NOT NULL,
+  active INT NOT NULL
+)";
+
+$sql["schedulesdays"] = "CREATE TABLE schedulesdays (
+  id INT NOT NULL AUTO_INCREMENT,
+  PRIMARY KEY(id),
+  schedule INT NOT NULL,
+  INDEX(schedule),
+  day INT NOT NULL,
+  typeday INT NOT NULL,
+  beginswork INT NOT NULL,
+  endswork INT NOT NULL,
+  beginsbreakfast INT,
+  endsbreakfast INT,
+  beginslunch INT,
+  endslunch INT
+)";
+
+$sql["typesincidents"] = "CREATE TABLE typesincidents (
+  id INT NOT NULL AUTO_INCREMENT,
+  PRIMARY KEY(id),
+  name VARCHAR(100) NOT NULL UNIQUE,
+  present INT NOT NULL,
+  paid INT NOT NULL,
+  workerfill INT NOT NULL DEFAULT 0,
+  notifies INT NOT NULL DEFAULT 0,
+  autovalidates INT NOT NULL DEFAULT 0,
+  hidden INT NOT NULL DEFAULT 0
+)";
+
+$sql["incidents"] = "CREATE TABLE incidents (
+  id INT NOT NULL AUTO_INCREMENT,
+  PRIMARY KEY(id),
+  worker INT,
+  INDEX(worker),
+  creator INT,
+  updatedby INT DEFAULT -1,
+  confirmedby INT DEFAULT -1,
+  type INT NOT NULL,
+  day INT NOT NULL,
+  INDEX(day),
+  begins INT NOT NULL,
+  ends INT NOT NULL,
+  details TEXT,
+  workerdetails TEXT DEFAULT NULL,
+  attachments TEXT,
+  verified INT DEFAULT 0,
+  invalidated INT DEFAULT 0,
+  INDEX(invalidated),
+  workervalidated INT DEFAULT 0,
+  INDEX(workervalidated),
+  workervalidation TEXT
+)";
+
+/*$sql["recurringincidents"] = "CREATE TABLE recurringincidents (
+  id INT NOT NULL AUTO_INCREMENT,
+  PRIMARY KEY(id),
+  worker INT,
+  creator INT,
+  type INT NOT NULL,
+  firstday INT NOT NULL,
+  lastday INT NOT NULL,
+  typedays TEXT,
+  begins INT NOT NULL,
+  ends INT NOT NULL,
+  details TEXT
+)";*/ // NOTE: No longer needed
+
+$sql["records"] = "CREATE TABLE records (
+  id INT NOT NULL AUTO_INCREMENT,
+  PRIMARY KEY(id),
+  worker INT NOT NULL,
+  INDEX(worker),
+  day INT NOT NULL,
+  INDEX(day),
+  created INT NOT NULL,
+  creator INT NOT NULL,
+  beginswork INT NOT NULL,
+  endswork INT NOT NULL,
+  beginsbreakfast INT NOT NULL,
+  endsbreakfast INT NOT NULL,
+  beginslunch INT NOT NULL,
+  endslunch INT NOT NULL,
+  invalidated INT DEFAULT 0,
+  INDEX(invalidated),
+  invalidatedby INT DEFAULT -1,
+  workervalidated INT DEFAULT 0,
+  INDEX(workervalidated),
+  workervalidation TEXT
+)";
+
+$sql["logs"] = "CREATE TABLE logs (
+  id INT NOT NULL AUTO_INCREMENT,
+  PRIMARY KEY(id),
+  realtime INT NOT NULL,
+  day INT NOT NULL,
+  logdetails TEXT,
+  executedby INT DEFAULT -1
+)";
+
+$sql["recovery"] = "CREATE TABLE recovery (
+  id INT NOT NULL AUTO_INCREMENT,
+  PRIMARY KEY(id),
+  user INT NOT NULL,
+  token VARCHAR(64) NOT NULL UNIQUE,
+  timecreated INT NOT NULL,
+  expires INT NOT NULL,
+  used INT DEFAULT 0
+)";
+
+$sql["help"] = "CREATE TABLE help (
+  id INT NOT NULL AUTO_INCREMENT,
+  PRIMARY KEY(id),
+  place INT NOT NULL UNIQUE,
+  url VARCHAR(256) NOT NULL
+)";
+
+$sql["totp"] = "CREATE TABLE totp (
+  id INT NOT NULL AUTO_INCREMENT,
+  PRIMARY KEY(id),
+  person INT NOT NULL UNIQUE,
+  secret VARCHAR(256) NOT NULL
+)";
+
+$sql["securitykeys"] = "CREATE TABLE securitykeys (
+  id INT NOT NULL AUTO_INCREMENT,
+  PRIMARY KEY(id),
+  person INT NOT NULL,
+  name VARCHAR(100),
+  credentialid VARBINARY(500),
+  credentialpublickey TEXT,
+  added INT NOT NULL,
+  lastused INT
+)";
+
+$sql["signinattempts"] = "CREATE TABLE signinattempts (
+  username VARCHAR(100) NOT NULL,
+  KEY username (username),
+  remoteip VARBINARY(16) NOT NULL,
+  KEY remoteip (remoteip),
+  remoteipblock VARBINARY(16) NOT NULL,
+  KEY remoteipblock (remoteipblock),
+  signinattempttime DATETIME NOT NULL,
+  KEY signinattempttime (signinattempttime)
+)";
+
+foreach ($sql as $table => $sentence) {
+  if (mysqli_query($con, $sentence)) {
+    echo "Taula ".$table." creada satisfactòriament.\n";
+  } else {
+    die("Hi ha hagut un error creant la taula ".$table.": ".mysqli_error($con)."\n");
+  }
+}
+
+echo "\n";
+
+if (mysqli_query($con, "INSERT INTO people (username, password, type, name, dni, email) VALUES ('".$username."', '".$pw."', 0, '".$name."', '".$dni."', '".$email."')")) {
+  echo "Afegit el primer usuari satisfactòriament.\n\n";
+  echo "Instal·lació completada correctament.\n";
+} else {
+  echo "Hi ha hagut un error afegint el primer usuari.\n";
+}
diff --git a/src/interstitialvalidations.php b/src/interstitialvalidations.php
new file mode 100644
index 0000000..6fdc37b
--- /dev/null
+++ b/src/interstitialvalidations.php
@@ -0,0 +1,45 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER);
+
+$id = people::userData("id");
+
+$allowedMethods = validations::getAllowedMethods();
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+  <link rel="stylesheet" href="css/incidents.css">
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Validación</h2>
+          <div class="mdl-tabs mdl-js-tabs mdl-js-ripple-effect">
+            <div class="mdl-tabs__tab-bar">
+              <?php
+              foreach ($allowedMethods as $method) {
+                echo '<a href="#method'.security::htmlsafe(validations::$methodCodename[$method]).'" class="mdl-tabs__tab'.($method == $conf["validation"]["defaultMethod"] ? " is-active" : "").'">'.security::htmlsafe(validations::$methodName[$method]).'</a>';
+              }
+              ?>
+            </div>
+            <?php
+            foreach ($allowedMethods as $method) {
+              echo '<div class="mdl-tabs__panel'.($method == $conf["validation"]["defaultMethod"] ? " is-active" : "").'" id="method'.security::htmlsafe(validations::$methodCodename[$method]).'">';
+              validationsView::renderChallengeInstructions($method);
+              echo '</a>';
+            }
+            ?>
+          </div>
+        </div>
+      </div>
+    </main>
+  </div>
+</body>
+</html>
diff --git a/src/invalidatebulkrecords.php b/src/invalidatebulkrecords.php
new file mode 100644
index 0000000..c6cf09e
--- /dev/null
+++ b/src/invalidatebulkrecords.php
@@ -0,0 +1,88 @@
+<?php
+require_once("core.php");
+security::checkType(security::HYPERADMIN);
+
+$companies = companies::getAll();
+
+$mdHeaderRowBefore = visual::backBtn("powertools.php");
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Invalidar registros en masa</h2>
+          <p>Selecciona los trabajadores de los cuales quieres invalidar sus registros, y el periodo de tiempo aplicable:</p>
+          <form action="doinvalidatebulkrecords.php" method="POST">
+            <h5>Periodo de tiempo</h5>
+            <p>Del <input type="date" name="begins" required> al <input type="date" name="ends" required></p>
+
+            <h5>Personas</h5>
+
+            <div class="overflow-wrapper overflow-wrapper--for-table">
+              <table class="mdl-data-table mdl-js-data-table mdl-data-table--selectable mdl-shadow--2dp">
+                <thead>
+                  <tr>
+                    <?php
+                    if ($conf["debug"]) {
+                      ?>
+                      <th class="extra">ID</th>
+                      <?php
+                    }
+                    ?>
+                    <th class="mdl-data-table__cell--non-numeric">Nombre</th>
+                    <th class="mdl-data-table__cell--non-numeric">Empresa</th>
+                    <th class="mdl-data-table__cell--non-numeric extra">Categoría</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <?php
+                  $workers = people::getAll(false, true);
+                  foreach ($workers as $w) {
+                    ?>
+                    <tr data-worker-id="<?=(int)$w["workerid"]?>">
+                      <?php
+                      if ($conf["debug"]) {
+                        ?>
+                        <td class="extra"><?=(int)$w["workerid"]?></td>
+                        <?php
+                      }
+                      ?>
+                      <td class="mdl-data-table__cell--non-numeric"><?=security::htmlsafe($w["name"])?></td>
+                      <td class="mdl-data-table__cell--non-numeric"><?=security::htmlsafe($companies[$w["companyid"]])?></td>
+                      <td class="mdl-data-table__cell--non-numeric extra"><?=security::htmlsafe($w["category"])?></td>
+                    </tr>
+                    <?php
+                  }
+                  ?>
+                </tbody>
+              </table>
+            </div>
+            <br>
+            <button class="mdl-button mdl-js-button mdl-button--raised mdl-button--accent mdl-js-ripple-effect">Invalidar</button>
+          </form>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <?php
+  visual::smartSnackbar([
+    ["empty", "Faltan datos por introducir en el formulario."],
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."],
+    ["inverted", "La fecha de inicio debe ser igual o anterior a la de fin."],
+    ["success", "Se han invalidado todos los registros correspondientes correcamente."],
+    ["partialortotalfailure", "Varios (o todos) los registros no se han podido invalidar correctamente."]
+  ]);
+  ?>
+  <script src="js/invalidatebulkrecords.js"></script>
+</body>
+</html>
diff --git a/src/js/calendar.js b/src/js/calendar.js
new file mode 100644
index 0000000..920bf98
--- /dev/null
+++ b/src/js/calendar.js
@@ -0,0 +1,8 @@
+document.addEventListener("DOMContentLoaded", _ => {
+  document.querySelectorAll("select").forEach(el => {
+    el.addEventListener("change", _ => {
+      el.setAttribute("data-value", el.value);
+    });
+    el.setAttribute("data-value", el.value);
+  });
+});
diff --git a/src/js/categories.js b/src/js/categories.js
new file mode 100644
index 0000000..412f585
--- /dev/null
+++ b/src/js/categories.js
@@ -0,0 +1,6 @@
+window.addEventListener("load", function() {
+  document.querySelector(".addcategory").addEventListener("click", function() {
+    document.querySelector("#addcategory").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+});
diff --git a/src/js/common.js b/src/js/common.js
new file mode 100644
index 0000000..445016e
--- /dev/null
+++ b/src/js/common.js
@@ -0,0 +1,242 @@
+/**
+  * Functions hasClass(), addClass() and removeClass() developed by Jake Trent (http://jaketrent.com/post/addremove-classes-raw-javascript/)
+  */
+function hasClass(el, className) {
+  if (el.classList)
+    return el.classList.contains(className)
+  else
+    return !!el.className.match(new RegExp('(\\s|^)' + className + '(\\s|$)'))
+}
+
+function addClass(el, className) {
+  if (el.classList)
+    el.classList.add(className)
+  else if (!hasClass(el, className)) el.className += " " + className
+}
+
+function removeClass(el, className) {
+  if (el.classList)
+    el.classList.remove(className)
+  else if (hasClass(el, className)) {
+    var reg = new RegExp('(\\s|^)' + className + '(\\s|$)')
+    el.className=el.className.replace(reg, ' ')
+  }
+}
+
+// MultiSelect implementation:
+var MultiSelect = function MultiSelect(element) {
+  this.element_ = element;
+  this.selected_ = [];
+
+  var forElId = this.element_.getAttribute("for") || this.element_.getAttribute("data-for");
+  if (forElId) {
+    this.forEl_ = (forElId ? document.getElementById(this.element_.getAttribute("for")) : null);
+  }
+
+  this.init();
+};
+
+MultiSelect.prototype.renderSummary = function() {
+  if (this.forEl_) {
+    this.forEl_.innerText = this.selected_.join(", ") || "-";
+  }
+};
+
+MultiSelect.prototype.init = function() {
+  if (this.element_) {
+    this.element_.addEventListener("click", e => {
+      e.stopImmediatePropagation();
+    }, true);
+
+    this.element_.querySelectorAll(".mdl-custom-multiselect__item").forEach(item => {
+      var checkbox = item.querySelector("input[type=\"checkbox\"]");
+      var label = item.querySelector(".mdl-checkbox__label").innerText;
+      checkbox.addEventListener("change", e => {
+        if(checkbox.checked) {
+          this.selected_.push(label);
+        } else {
+          this.selected_ = this.selected_.filter(item => item !== label);
+        }
+        this.renderSummary();
+
+        var customEvent = new Event('custom-multiselect-change');
+        this.element_.dispatchEvent(customEvent);
+      });
+    });
+  }
+};
+
+var dynDialog = {
+  didItInit: false,
+  dialog: null,
+  url: null,
+  load: function(url, reload) {
+    if (this.didItInit === false) {
+      this.init();
+    }
+
+    if (this.url == url && reload !== true) {
+      this.show();
+      return;
+    }
+
+    this.url = url;
+
+    fetch(url).then(response => response.text()).then(response => {
+      if (this.dialog.open) {
+        this.close();
+      }
+
+      this.dialog.innerHTML = response;
+      componentHandler.upgradeElements(this.dialog);
+
+      this.dialog.querySelectorAll("[data-required]").forEach(input => {
+        input.setAttribute("required", "true");
+      });
+
+      var script = this.dialog.querySelectorAll("dynscript");
+      if (script.length > 0) {
+        for (var i = 0; i < script.length; i++) {
+          eval(script[i].innerText);
+        }
+      }
+      this.dialog.querySelectorAll("[data-dyndialog-close]").forEach(btn => {
+        btn.addEventListener("click", e => {
+          e.preventDefault();
+          this.close();
+        });
+      });
+
+      this.dialog.showModal();
+    });
+  },
+  reload: function() {
+    this.load(this.url, true);
+  },
+  show: function() {
+    this.dialog.showModal();
+  },
+  close: function() {
+    this.dialog.close();
+  },
+  init: function() {
+    this.dialog = document.createElement("dialog");
+    this.dialog.setAttribute("id", "dynDialog");
+    this.dialog.setAttribute("class", "mdl-dialog");
+    dialogPolyfill.registerDialog(this.dialog);
+    document.body.appendChild(this.dialog);
+
+    this.didItInit = true;
+  }
+};
+
+// From nodep-date-input-polyfill
+function isDateInputSupported() {
+  const input = document.createElement("input");
+  input.setAttribute("type", "date");
+
+  const notADateValue = "not-a-date";
+  input.setAttribute("value", notADateValue);
+
+  return (input.value !== notADateValue);
+}
+
+document.addEventListener("DOMContentLoaded", function() {
+  var dialogs = document.querySelectorAll("dialog");
+  for (var i = 0; i < dialogs.length; i++) {
+    dialogPolyfill.registerDialog(dialogs[i]);
+  }
+
+  document.querySelectorAll("[data-dyndialog-href]").forEach(link => {
+    link.addEventListener("click", e => {
+      e.preventDefault();
+      dynDialog.load(link.getAttribute("data-dyndialog-href"));
+    });
+  });
+
+  document.querySelectorAll(".mdl-custom-multiselect-js").forEach(menu => {
+    new MultiSelect(menu);
+  });
+});
+
+function importCSS(url) {
+  var link = document.createElement("link");
+  link.setAttribute("rel", "stylesheet");
+  link.setAttribute("href", url);
+  document.head.appendChild(link);
+}
+
+var loadScriptAsync = function(uri) {
+  return new Promise((resolve, reject) => {
+    var script = document.createElement('script');
+    script.src = uri;
+    script.async = true;
+    script.onload = () => {
+      resolve();
+    };
+    document.head.appendChild(script);
+  });
+}
+
+function hasParentDialog(el) {
+  while (el != document.documentElement) {
+    if (el.tagName == "DIALOG") {
+      return el;
+    }
+
+    el = el.parentNode;
+  }
+
+  return undefined;
+}
+
+function polyfillDateSupport(container) {
+  container.querySelectorAll("input[type=\"date\"]").forEach(el => {
+    el.setAttribute("type", "text");
+
+    dialogParent = hasParentDialog(el);
+
+    var options = {
+      format: "yyyy-mm-dd",
+      todayHighlight: true,
+      weekStart: 1,
+      zIndexOffset: 100,
+      container: dialogParent || container
+    };
+
+    if (el.hasAttribute("max")) {
+      options.endDate = el.getAttribute("max");
+    }
+
+    $(el).datepicker(options);
+  });
+
+  container.querySelectorAll("input[type=\"time\"]").forEach(el => {
+    el.setAttribute("placeholder", "hh:mm");
+  });
+}
+
+function initPolyfillDateSupport() {
+  console.info("Polyfilling date support");
+
+  importCSS("https://unpkg.com/bootstrap-datepicker@1.9.0/dist/css/bootstrap-datepicker3.standalone.min.css", "css");
+
+  loadScriptAsync("https://unpkg.com/jquery@3.4.1/dist/jquery.min.js", "js").then(_ => {
+    return loadScriptAsync("https://unpkg.com/bootstrap-datepicker@1.9.0/dist/js/bootstrap-datepicker.min.js");
+  }).then(_ => {
+    return loadScriptAsync("https://unpkg.com/bootstrap-datepicker@1.9.0/dist/locales/bootstrap-datepicker.es.min.js");
+  }).then(_ => {
+    console.log("[Date polyfill] Scripts loaded.");
+    polyfillDateSupport(document.documentElement);
+  });
+}
+
+window.addEventListener("load", function() {
+  document.querySelectorAll("[data-required]").forEach(input => {
+    input.setAttribute("required", "true");
+  });
+
+  if (!isDateInputSupported()) {
+    initPolyfillDateSupport();
+  }
+});
diff --git a/src/js/common_webauthn.js b/src/js/common_webauthn.js
new file mode 100644
index 0000000..fa95364
--- /dev/null
+++ b/src/js/common_webauthn.js
@@ -0,0 +1,44 @@
+/**
+ * convert RFC 1342-like base64 strings to array buffer
+ * @param {mixed} obj
+ * @returns {undefined}
+ */
+function recursiveBase64StrToArrayBuffer(obj) {
+    let prefix = '=?BINARY?B?';
+    let suffix = '?=';
+    if (typeof obj === 'object') {
+        for (let key in obj) {
+            if (typeof obj[key] === 'string') {
+                let str = obj[key];
+                if (str.substring(0, prefix.length) === prefix && str.substring(str.length - suffix.length) === suffix) {
+                    str = str.substring(prefix.length, str.length - suffix.length);
+
+                    let binary_string = window.atob(str);
+                    let len = binary_string.length;
+                    let bytes = new Uint8Array(len);
+                    for (var i = 0; i < len; i++)        {
+                        bytes[i] = binary_string.charCodeAt(i);
+                    }
+                    obj[key] = bytes.buffer;
+                }
+            } else {
+                recursiveBase64StrToArrayBuffer(obj[key]);
+            }
+        }
+    }
+}
+
+/**
+ * Convert a ArrayBuffer to Base64
+ * @param {ArrayBuffer} buffer
+ * @returns {String}
+ */
+function arrayBufferToBase64(buffer) {
+    var binary = '';
+    var bytes = new Uint8Array(buffer);
+    var len = bytes.byteLength;
+    for (var i = 0; i < len; i++) {
+        binary += String.fromCharCode( bytes[ i ] );
+    }
+    return window.btoa(binary);
+}
diff --git a/src/js/companies.js b/src/js/companies.js
new file mode 100644
index 0000000..aa8d9dd
--- /dev/null
+++ b/src/js/companies.js
@@ -0,0 +1,6 @@
+window.addEventListener("load", function() {
+  document.querySelector(".addcompany").addEventListener("click", function() {
+    document.querySelector("#addcompany").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+});
diff --git a/src/js/export.js b/src/js/export.js
new file mode 100644
index 0000000..9bdbbbe
--- /dev/null
+++ b/src/js/export.js
@@ -0,0 +1,71 @@
+function toggleTr(tr, show) {
+  var checkbox = tr.querySelector("label").MaterialCheckbox;
+  if (show) {
+    tr.style.display = "table-row";
+    checkbox.enable();
+  } else {
+    tr.style.display = "none";
+    checkbox.disable();
+  }
+}
+
+window.addEventListener("load", function() {
+  document.querySelectorAll("tr[data-worker-id]").forEach(tr => {
+    var checkbox = tr.querySelector("input[type=\"checkbox\"]");
+
+    checkbox.setAttribute("name", "workers[]");
+    checkbox.setAttribute("value", tr.getAttribute("data-worker-id"));
+
+    toggleTr(tr, false);
+  });
+
+  document.querySelectorAll(".select-all").forEach(el => {
+    el.addEventListener("click", e => {
+      var allchecked = true;
+      el.getAttribute("data-workers").split(",").forEach(workerid => {
+        var tr = document.querySelector("tr[data-worker-id=\""+workerid+"\"]");
+        var checkbox = tr.querySelector("label").MaterialCheckbox;
+        if (checkbox.inputElement_.disabled) return;
+        if (!checkbox.inputElement_.checked) allchecked = false;
+        tr.classList.add("is-selected");
+        checkbox.check();
+      });
+
+      if (allchecked) {
+        el.getAttribute("data-workers").split(",").forEach(workerid => {
+          var tr = document.querySelector("tr[data-worker-id=\""+workerid+"\"]");
+          var checkbox = tr.querySelector("label").MaterialCheckbox;
+          tr.classList.remove("is-selected");
+          checkbox.uncheck();
+        });
+      }
+    });
+  });
+
+  var multiselectEl = document.querySelector(".mdl-custom-multiselect");
+  if (multiselectEl !== null) {
+    multiselectEl.addEventListener("custom-multiselect-change", e => {
+      var companies = [];
+      document.querySelectorAll(".mdl-custom-multiselect .mdl-custom-multiselect__item input[type=\"checkbox\"]").forEach(checkbox => {
+        if (checkbox.checked) {
+          companies.push(checkbox.value);
+        }
+      });
+
+      document.querySelectorAll("tr[data-worker-id]").forEach(tr => {
+        toggleTr(tr, companies.includes(tr.getAttribute("data-company-id")));
+      });
+    });
+  }
+
+  document.querySelectorAll("input[name=\"companies\[\]\"]").forEach(input => {
+    input.checked = true;
+    var customevent = document.createEvent("HTMLEvents");
+    customevent.initEvent("change", false, true);
+    input.dispatchEvent(customevent);
+  });
+
+  document.getElementById("format").addEventListener("change", e => {
+    document.getElementById("pdf").style.display = (document.getElementById("format").value != "1" && document.getElementById("format").value != "2" ? "none" : "block");
+  });
+});
diff --git a/src/js/incidents.js b/src/js/incidents.js
new file mode 100644
index 0000000..0397cac
--- /dev/null
+++ b/src/js/incidents.js
@@ -0,0 +1,79 @@
+function getFormData() {
+  var incidents = [];
+
+  document.querySelectorAll("input[type=\"checkbox\"][data-incident]:checked").forEach(el => {
+    incidents.push(el.getAttribute("data-incident"));
+  });
+
+  return incidents;
+}
+
+function getParameters() {
+  var parameters = [];
+  var incidents = getFormData();
+  incidents.forEach(incident => {
+    parameters.push("incidents[]="+incident);
+  });
+
+  if (parameters.length == 0) return false;
+
+  return parameters.join("&");
+}
+
+window.addEventListener("load", function() {
+  document.querySelector(".addincident").addEventListener("click", function() {
+    document.querySelector("#addincident").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+
+  document.querySelector(".addrecurringincident").addEventListener("click", function() {
+    document.querySelector("#addrecurringincident").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+
+  document.querySelector(".filter").addEventListener("click", function() {
+    document.querySelector("#filter").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+
+  if (_showResultsPaginated) {
+    document.getElementById("limit-change").addEventListener("change", _ => {
+      var limit = parseInt(document.getElementById("limit-change").value);
+      var firstIncidentPos = _page*_limit;
+      var page = Math.floor(firstIncidentPos/limit) + 1;
+
+      var url = new URL(location.href);
+      url.searchParams.set("limit", limit);
+      url.searchParams.set("page", page);
+      location.href = url;
+    });
+  }
+
+  document.querySelectorAll(".mdl-checkbox[data-check-all=\"true\"] input[type=\"checkbox\"]").forEach(el => {
+    el.addEventListener("change", e => {
+      el.parentElement.parentElement.parentElement.parentElement.parentElement.querySelectorAll(".mdl-checkbox:not([data-check-all=\"true\"])").forEach(input => {
+        var checkbox = input.MaterialCheckbox;
+        if (checkbox.inputElement_.disabled) return;
+
+        if (el.checked) checkbox.check();
+        else checkbox.uncheck();
+      });
+    });
+  });
+
+  document.getElementById("deleteincidentsbulk").addEventListener("click", e => {
+    var parameters = getParameters();
+
+    if (parameters === false) {
+      document.querySelector(".mdl-js-snackbar").MaterialSnackbar.showSnackbar({
+        message: "Debes seleccionar al menos una incidencia para poder eliminar.",
+        timeout: 5000
+      });
+
+      return;
+    }
+
+    var url = "dynamic/deleteincidentsbulk.php?"+parameters;
+    dynDialog.load(url);
+  });
+});
diff --git a/src/js/incidentsgeneric.js b/src/js/incidentsgeneric.js
new file mode 100644
index 0000000..5d1323f
--- /dev/null
+++ b/src/js/incidentsgeneric.js
@@ -0,0 +1,13 @@
+window.addEventListener("load", function() {
+
+  document.querySelectorAll(".custom-actions-btn").forEach(el => {
+    el.addEventListener("click", e => {
+      var forId = el.getAttribute("id");
+      var menu = document.querySelector("[for=\""+forId+"\"]").parentElement;
+      var overflow = menu.parentElement;
+
+      menu.style.left = el.offsetLeft - menu.offsetWidth + el.offsetWidth - overflow.scrollLeft + 'px';
+      menu.style.top = el.offsetTop + el.offsetHeight - overflow.scrollTop  + 'px';
+    });
+  });
+});
diff --git a/src/js/incidenttypes.js b/src/js/incidenttypes.js
new file mode 100644
index 0000000..48e8ae4
--- /dev/null
+++ b/src/js/incidenttypes.js
@@ -0,0 +1,6 @@
+window.addEventListener("load", function() {
+  document.querySelector(".addincident").addEventListener("click", function() {
+    document.querySelector("#addincident").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+});
diff --git a/src/js/index.js b/src/js/index.js
new file mode 100644
index 0000000..8af86b3
--- /dev/null
+++ b/src/js/index.js
@@ -0,0 +1,8 @@
+window.addEventListener("load", function() {
+  if (document.querySelector("#recoverybtn")) {
+    document.querySelector("#recoverybtn").addEventListener("click", function(e) {
+      e.preventDefault();
+      document.querySelector("#recovery").showModal();
+    });
+  }
+});
diff --git a/src/js/invalidatebulkrecords.js b/src/js/invalidatebulkrecords.js
new file mode 100644
index 0000000..cfbe2d5
--- /dev/null
+++ b/src/js/invalidatebulkrecords.js
@@ -0,0 +1,8 @@
+window.addEventListener("load", function() {
+  document.querySelectorAll("tr[data-worker-id]").forEach(tr => {
+    var checkbox = tr.querySelector("input[type=\"checkbox\"]");
+
+    checkbox.setAttribute("name", "workers[]");
+    checkbox.setAttribute("value", tr.getAttribute("data-worker-id"));
+  });
+});
diff --git a/src/js/registry.js b/src/js/registry.js
new file mode 100644
index 0000000..807c4fd
--- /dev/null
+++ b/src/js/registry.js
@@ -0,0 +1,5 @@
+window.addEventListener("load", _ => {
+  document.getElementById("showinvalidated").addEventListener("change", e => {
+    document.getElementById("show-invalidated-form").submit();
+  });
+});
diff --git a/src/js/schedule.js b/src/js/schedule.js
new file mode 100644
index 0000000..d804872
--- /dev/null
+++ b/src/js/schedule.js
@@ -0,0 +1,6 @@
+window.addEventListener("load", function() {
+  document.querySelector(".addday").addEventListener("click", function() {
+    document.querySelector("#addday").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+});
diff --git a/src/js/scheduletemplates.js b/src/js/scheduletemplates.js
new file mode 100644
index 0000000..a236064
--- /dev/null
+++ b/src/js/scheduletemplates.js
@@ -0,0 +1,6 @@
+window.addEventListener("load", function() {
+  document.querySelector(".addtemplate").addEventListener("click", function() {
+    document.querySelector("#addtemplate").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+});
diff --git a/src/js/secondfactor.js b/src/js/secondfactor.js
new file mode 100644
index 0000000..f537146
--- /dev/null
+++ b/src/js/secondfactor.js
@@ -0,0 +1,107 @@
+function verify() {
+  if (!document.getElementById("code").checkValidity()) {
+    document.querySelector(".mdl-js-snackbar").MaterialSnackbar.showSnackbar({
+      message: "El código de verificación debe tener 6 cifras."
+    });
+
+    return;
+  }
+
+  var body = {
+    code: document.getElementById("code").value
+  };
+
+  var content = document.getElementById("content");
+  content.innerHTML = '<div class="mdl-spinner mdl-js-spinner is-active"></div>';
+  content.style.textAlign = "center";
+  componentHandler.upgradeElements(content);
+
+  fetch("ajax/verifysecuritycode.php", {
+    method: "POST",
+    headers: {
+      "Content-Type": "application/json"
+    },
+    body: JSON.stringify(body)
+  }).then(response => {
+    if (response.status !== 200) {
+      throw new Error("HTTP status is not 200.");
+    }
+
+    return response.json();
+  }).then(response => {
+    switch (response.status) {
+      case "ok":
+      document.location = "index.php";
+      break;
+
+      case "wrongCode":
+      document.location = "index.php?msg=secondfactorwrongcode";
+      break;
+
+      default:
+      console.error("An unknown status code was returned.");
+    }
+  }).catch(err => console.error("An unexpected error occurred.", err));
+}
+
+function verifyKeypress(e) {
+  if (event.keyCode == 13) {
+    verify();
+  }
+}
+
+function startWebauthn() {
+  fetch("ajax/startwebauthnauthentication.php", {
+    method: "POST"
+  }).then(response => {
+    if (response.status !== 200) {
+      response.text(); // @TODO: Remove this. It is only used so the response is available in Chrome Dev Tools
+      throw new Error("HTTP status is not 200.");
+    }
+
+    return response.json();
+  }).then(response => {
+    recursiveBase64StrToArrayBuffer(response);
+    return response;
+  }).then(getCredentialArgs => {
+    return navigator.credentials.get(getCredentialArgs);
+  }).then(cred => {
+    return {
+      id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
+      clientDataJSON: cred.response.clientDataJSON  ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
+      authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
+      signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null
+    };
+  }).then(JSON.stringify).then(AuthenticatorAttestationResponse => {
+    return window.fetch("ajax/completewebauthnauthentication.php", {
+      method: "POST",
+      body: AuthenticatorAttestationResponse,
+    });
+  }).then(response => {
+    if (response.status !== 200) {
+      response.text(); // @TODO: remove this. It is only used so the response is available in Chrome Dev Tools
+      throw new Error("HTTP status is not 200 (2).");
+    }
+
+    return response.json();
+  }).then(json => {
+    if (json.status == "ok") {
+      document.location = "index.php";
+    }
+  }).catch(err => console.error("An unexpected error occurred.", err));
+}
+
+window.addEventListener("load", function() {
+  if (document.getElementById("totp")) {
+    document.getElementById("verify").addEventListener("click", verify);
+    document.getElementById("code").addEventListener("keypress", verifyKeypress);
+    document.getElementById("code").focus();
+    document.querySelector("a[href=\"#totp\"]").addEventListener("click", _ => {
+      document.getElementById("code").focus();
+    });
+  }
+
+  if (document.getElementById("startwebauthn")) {
+    document.getElementById("startwebauthn").addEventListener("click", startWebauthn);
+  }
+});
diff --git a/src/js/securitykeys.js b/src/js/securitykeys.js
new file mode 100644
index 0000000..9f08e21
--- /dev/null
+++ b/src/js/securitykeys.js
@@ -0,0 +1,50 @@
+window.addEventListener("load", function() {
+  document.querySelector(".addsecuritykey").addEventListener("click", function() {
+    document.querySelector("#addsecuritykey").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+
+  document.getElementById("registersecuritykey").addEventListener("click", e => {
+    e.preventDefault();
+
+    if (document.getElementById("addsecuritykeyform").reportValidity()) {
+      fetch("ajax/addsecuritykey.php", {
+        method: "POST"
+      }).then(response => {
+        if (response.status !== 200) {
+          response.text(); // @TODO: Remove this. It is only used so the response is available in Chrome Dev Tools
+          throw new Error("HTTP status is not 200.");
+        }
+
+        return response.json();
+      }).then(response => {
+        recursiveBase64StrToArrayBuffer(response);
+        return response;
+      }).then(createCredentialArgs => {
+        return navigator.credentials.create(createCredentialArgs);
+      }).then(cred => {
+        return {
+            clientDataJSON: cred.response.clientDataJSON  ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
+            attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null,
+            name: document.getElementById("name").value
+        };
+      }).then(JSON.stringify).then(AuthenticatorAttestationResponse => {
+        return window.fetch("ajax/addsecuritykey2.php", {
+          method: "POST",
+          body: AuthenticatorAttestationResponse,
+        });
+      }).then(response => {
+        if (response.status !== 200) {
+          response.text(); // @TODO: remove this. It is only used so the response is available in Chrome Dev Tools
+          throw new Error("HTTP status is not 200 (2).");
+        }
+
+        return response.json();
+      }).then(json => {
+        if (json.status == "ok") {
+          document.location = "securitykeys.php?msg=securitykeyadded";
+        }
+      }).catch(err => console.error("An unexpected error occurred.", err));
+    }
+  });
+});
diff --git a/src/js/sendbulkpasswords.js b/src/js/sendbulkpasswords.js
new file mode 100644
index 0000000..bb6ac26
--- /dev/null
+++ b/src/js/sendbulkpasswords.js
@@ -0,0 +1,8 @@
+window.addEventListener("load", function() {
+  document.querySelectorAll("tr[data-person-id]").forEach(tr => {
+    var checkbox = tr.querySelector("input[type=\"checkbox\"]");
+
+    checkbox.setAttribute("name", "people[]");
+    checkbox.setAttribute("value", tr.getAttribute("data-person-id"));
+  });
+});
diff --git a/src/js/userincidents.js b/src/js/userincidents.js
new file mode 100644
index 0000000..48e8ae4
--- /dev/null
+++ b/src/js/userincidents.js
@@ -0,0 +1,6 @@
+window.addEventListener("load", function() {
+  document.querySelector(".addincident").addEventListener("click", function() {
+    document.querySelector("#addincident").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+});
diff --git a/src/js/users.js b/src/js/users.js
new file mode 100644
index 0000000..9eb383e
--- /dev/null
+++ b/src/js/users.js
@@ -0,0 +1,28 @@
+window.addEventListener("load", function() {
+  var datatable = $('.datatable').DataTable({
+    paging:   false,
+    ordering: false,
+    info:     false,
+    searching:true
+  });
+
+  document.querySelector("#usuario").addEventListener("input", function(evt) {
+    this.search(evt.target.value);
+    this.draw(true);
+  }.bind(datatable));
+
+  document.querySelector(".adduser").addEventListener("click", function() {
+    document.querySelector("#adduser").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+
+  document.querySelector(".importcsv").addEventListener("click", function() {
+    document.querySelector("#importcsv").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+
+  document.querySelector(".filter").addEventListener("click", function() {
+    document.querySelector("#filter").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+});
diff --git a/src/js/userschedule.js b/src/js/userschedule.js
new file mode 100644
index 0000000..0fa39cb
--- /dev/null
+++ b/src/js/userschedule.js
@@ -0,0 +1,6 @@
+window.addEventListener("load", function() {
+  document.querySelector(".addschedule").addEventListener("click", function() {
+    document.querySelector("#addschedule").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+});
diff --git a/src/js/validations.js b/src/js/validations.js
new file mode 100644
index 0000000..cecd3b7
--- /dev/null
+++ b/src/js/validations.js
@@ -0,0 +1,55 @@
+function getFormData() {
+  var data = {
+    "incidents": [],
+    "records": []
+  };
+
+  ["incident", "record"].forEach(key => {
+    document.querySelectorAll("input[type=\"checkbox\"][data-"+key+"]:checked").forEach(el => {
+      data[key+"s"].push(el.getAttribute("data-"+key));
+    });
+  });
+
+  return data;
+}
+
+window.addEventListener("load", function() {
+  document.querySelectorAll(".mdl-checkbox[data-check-all=\"true\"] input[type=\"checkbox\"]").forEach(el => {
+    el.addEventListener("change", e => {
+      el.parentElement.parentElement.parentElement.parentElement.parentElement.querySelectorAll(".mdl-checkbox:not([data-check-all=\"true\"])").forEach(input => {
+        var checkbox = input.MaterialCheckbox;
+
+        if (el.checked) checkbox.check();
+        else checkbox.uncheck();
+      });
+    });
+  });
+
+  document.querySelector("#submit").addEventListener("click", e => {
+    var data = getFormData();
+
+    if (data.incidents.length == 0 && data.records.length == 0) {
+      document.querySelector(".mdl-js-snackbar").MaterialSnackbar.showSnackbar({
+        message: "Debes seleccionar al menos una incidencia o registro para poder validar.",
+        timeout: 5000
+      });
+
+      return;
+    }
+
+    var form = document.createElement("form");
+    form.setAttribute("action", "interstitialvalidations.php");
+    form.setAttribute("method", "POST");
+    form.style.display = "none";
+
+    ["incidents", "records"].forEach(key => {
+      var input = document.createElement("input");
+      input.setAttribute("name", key);
+      input.setAttribute("value", data[key]);
+      form.appendChild(input);
+    });
+
+    document.body.appendChild(form);
+    form.submit();
+  });
+});
diff --git a/src/js/workers.js b/src/js/workers.js
new file mode 100644
index 0000000..88ac687
--- /dev/null
+++ b/src/js/workers.js
@@ -0,0 +1,71 @@
+function getRawWorkers() {
+  var parameters = [];
+  document.querySelectorAll("tr[data-worker-id]").forEach(tr => {
+    if (tr.querySelector("input[type=\"checkbox\"]").checked) {
+      parameters.push(tr.getAttribute("data-worker-id"));
+    }
+  });
+
+  return parameters;
+}
+
+function getParameters() {
+  var parameters = [];
+  var workers = getRawWorkers();
+  workers.forEach(worker => {
+    parameters.push("workers[]="+worker);
+  });
+
+  if (parameters.length == 0) return false;
+
+  return parameters.join("&");
+}
+
+window.addEventListener("load", function() {
+  var datatable = $('.datatable').DataTable({
+    paging:   false,
+    ordering: false,
+    info:     false,
+    searching:true
+  });
+
+  document.querySelector("#usuario").addEventListener("input", function(evt) {
+    this.search(evt.target.value);
+    this.draw(true);
+  }.bind(datatable));
+
+  document.querySelector(".filter").addEventListener("click", function() {
+    document.querySelector("#filter").showModal();
+    /* Or dialog.show(); to show the dialog without a backdrop. */
+  });
+
+  ["copytemplate", "addincidentbulk"].forEach(action => {
+    document.getElementById(action).addEventListener("click", function() {
+      var parameters = getParameters();
+      if (parameters === false) return;
+
+      var url = "dynamic/"+action+".php?"+parameters;
+      dynDialog.load(url);
+    });
+  });
+
+  document.getElementById("addrecurringincident").addEventListener("click", function () {
+    var workers = getRawWorkers();
+    if (workers.length > 1) {
+      if (document.querySelector(".mdl-js-snackbar") === null) {
+        document.body.insertAdjacentHTML('beforeend', '<div class="mdl-snackbar mdl-js-snackbar"><div class="mdl-snackbar__text"></div><button type="button" class="mdl-snackbar__action"></button></div>');
+        componentHandler.upgradeElement(document.querySelector(".mdl-js-snackbar"));
+      }
+
+      document.querySelector(".mdl-js-snackbar").MaterialSnackbar.showSnackbar(
+        {
+          message: "Solo se puede añadir una incidencia recurrente a un solo trabajador.",
+          timeout: 5000
+        }
+      );
+      // Display error message
+    } else if (workers.length == 1) {
+      window.location = "incidents.php?openRecurringFormWorker="+workers[0];
+    }
+  });
+});
diff --git a/src/lib/GoogleAuthenticator/FixedByteNotation.php b/src/lib/GoogleAuthenticator/FixedByteNotation.php
new file mode 100644
index 0000000..97d22f7
--- /dev/null
+++ b/src/lib/GoogleAuthenticator/FixedByteNotation.php
@@ -0,0 +1,276 @@
+<?php
+
+/**
+* FixedBitNotation
+*
+* @author Andre DeMarre
+* @package FixedBitNotation
+*/
+
+/**
+* The FixedBitNotation class is for binary to text conversion. It
+* can handle many encoding schemes, formally defined or not, that
+* use a fixed number of bits to encode each character.
+*
+* @package FixedBitNotation
+*/
+class FixedBitNotation
+{
+    protected $_chars;
+    protected $_bitsPerCharacter;
+    protected $_radix;
+    protected $_rightPadFinalBits;
+    protected $_padFinalGroup;
+    protected $_padCharacter;
+    protected $_charmap;
+    
+    /**
+    * Constructor
+    *
+    * @param integer $bitsPerCharacter Bits to use for each encoded
+    *                character
+    * @param string  $chars Base character alphabet
+    * @param boolean $rightPadFinalBits How to encode last character
+    * @param boolean $padFinalGroup Add padding to end of encoded
+    *                output
+    * @param string  $padCharacter Character to use for padding
+    */
+    public function __construct(
+    $bitsPerCharacter, $chars = NULL, $rightPadFinalBits = FALSE,
+    $padFinalGroup = FALSE, $padCharacter = '=')
+    {
+        // Ensure validity of $chars
+        if (!is_string($chars) || ($charLength = strlen($chars)) < 2) {
+            $chars = 
+            '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-,';
+            $charLength = 64;
+        }
+        
+        // Ensure validity of $bitsPerCharacter
+        if ($bitsPerCharacter < 1) {
+            // $bitsPerCharacter must be at least 1
+            $bitsPerCharacter = 1;
+            $radix = 2;
+            
+        } elseif ($charLength < 1 << $bitsPerCharacter) {
+            // Character length of $chars is too small for $bitsPerCharacter
+            // Set $bitsPerCharacter to greatest acceptable value
+            $bitsPerCharacter = 1;
+            $radix = 2;
+            
+            while ($charLength >= ($radix <<= 1) && $bitsPerCharacter < 8) {
+                $bitsPerCharacter++;
+            }
+            
+            $radix >>= 1;
+            
+        } elseif ($bitsPerCharacter > 8) {
+            // $bitsPerCharacter must not be greater than 8
+            $bitsPerCharacter = 8;
+            $radix = 256;
+            
+        } else {
+            $radix = 1 << $bitsPerCharacter;
+        }
+        
+        $this->_chars = $chars;
+        $this->_bitsPerCharacter = $bitsPerCharacter;
+        $this->_radix = $radix;
+        $this->_rightPadFinalBits = $rightPadFinalBits;
+        $this->_padFinalGroup = $padFinalGroup;
+        $this->_padCharacter = $padCharacter[0];
+    }
+    
+    /**
+    * Encode a string
+    *
+    * @param  string $rawString Binary data to encode
+    * @return string
+    */
+    public function encode($rawString)
+    {
+        // Unpack string into an array of bytes
+        $bytes = unpack('C*', $rawString);
+        $byteCount = count($bytes);
+        
+        $encodedString = '';
+        $byte = array_shift($bytes);
+        $bitsRead = 0;
+        
+        $chars = $this->_chars;
+        $bitsPerCharacter = $this->_bitsPerCharacter;
+        $rightPadFinalBits = $this->_rightPadFinalBits;
+        $padFinalGroup = $this->_padFinalGroup;
+        $padCharacter = $this->_padCharacter;
+        
+        // Generate encoded output; 
+        // each loop produces one encoded character
+        for ($c = 0; $c < $byteCount * 8 / $bitsPerCharacter; $c++) {
+            
+            // Get the bits needed for this encoded character
+            if ($bitsRead + $bitsPerCharacter > 8) {
+                // Not enough bits remain in this byte for the current
+                // character
+                // Save the remaining bits before getting the next byte
+                $oldBitCount = 8 - $bitsRead;
+                $oldBits = $byte ^ ($byte >> $oldBitCount << $oldBitCount);
+                $newBitCount = $bitsPerCharacter - $oldBitCount;
+                
+                if (!$bytes) {
+                    // Last bits; match final character and exit loop
+                    if ($rightPadFinalBits) $oldBits <<= $newBitCount;
+                    $encodedString .= $chars[$oldBits];
+                    
+                    if ($padFinalGroup) {
+                        // Array of the lowest common multiples of 
+                        // $bitsPerCharacter and 8, divided by 8
+                        $lcmMap = array(1 => 1, 2 => 1, 3 => 3, 4 => 1,
+                        5 => 5, 6 => 3, 7 => 7, 8 => 1);
+                        $bytesPerGroup = $lcmMap[$bitsPerCharacter];
+                        $pads = $bytesPerGroup * 8 / $bitsPerCharacter 
+                        - ceil((strlen($rawString) % $bytesPerGroup)
+                        * 8 / $bitsPerCharacter);
+                        $encodedString .= str_repeat($padCharacter[0], $pads);
+                    }
+                    
+                    break;
+                }
+                
+                // Get next byte
+                $byte = array_shift($bytes);
+                $bitsRead = 0;
+                
+            } else {
+                $oldBitCount = 0;
+                $newBitCount = $bitsPerCharacter;
+            }
+            
+            // Read only the needed bits from this byte
+            $bits = $byte >> 8 - ($bitsRead + ($newBitCount));
+            $bits ^= $bits >> $newBitCount << $newBitCount;
+            $bitsRead += $newBitCount;
+            
+            if ($oldBitCount) {
+                // Bits come from seperate bytes, add $oldBits to $bits
+                $bits = ($oldBits << $newBitCount) | $bits;
+            }
+            
+            $encodedString .= $chars[$bits];
+        }
+        
+        return $encodedString;
+    }
+    
+    /**
+    * Decode a string
+    *
+    * @param  string  $encodedString Data to decode
+    * @param  boolean $caseSensitive
+    * @param  boolean $strict Returns NULL if $encodedString contains
+    *                 an undecodable character
+    * @return string|NULL
+    */
+    public function decode($encodedString, $caseSensitive = TRUE,
+    $strict = FALSE)
+    {
+        if (!$encodedString || !is_string($encodedString)) {
+            // Empty string, nothing to decode
+            return '';
+        }
+        
+        $chars = $this->_chars;
+        $bitsPerCharacter = $this->_bitsPerCharacter;
+        $radix = $this->_radix;
+        $rightPadFinalBits = $this->_rightPadFinalBits;
+        $padFinalGroup = $this->_padFinalGroup;
+        $padCharacter = $this->_padCharacter;
+        
+        // Get index of encoded characters
+        if ($this->_charmap) {
+            $charmap = $this->_charmap;
+            
+        } else {
+            $charmap = array();
+            
+            for ($i = 0; $i < $radix; $i++) {
+                $charmap[$chars[$i]] = $i;
+            }
+            
+            $this->_charmap = $charmap;
+        }
+        
+        // The last encoded character is $encodedString[$lastNotatedIndex]
+        $lastNotatedIndex = strlen($encodedString) - 1;
+        
+        // Remove trailing padding characters
+        while ($encodedString[$lastNotatedIndex] == $padCharacter[0]) {
+            $encodedString = substr($encodedString, 0, $lastNotatedIndex);
+            $lastNotatedIndex--;
+        }
+        
+        $rawString = '';
+        $byte = 0;
+        $bitsWritten = 0;
+        
+        // Convert each encoded character to a series of unencoded bits
+        for ($c = 0; $c <= $lastNotatedIndex; $c++) {
+            
+            if (!isset($charmap[$encodedString[$c]]) && !$caseSensitive) {
+                // Encoded character was not found; try other case
+                if (isset($charmap[$cUpper 
+                = strtoupper($encodedString[$c])])) {
+                    $charmap[$encodedString[$c]] = $charmap[$cUpper];
+                    
+                } elseif (isset($charmap[$cLower 
+                = strtolower($encodedString[$c])])) {
+                    $charmap[$encodedString[$c]] = $charmap[$cLower];
+                }
+            }
+            
+            if (isset($charmap[$encodedString[$c]])) {
+                $bitsNeeded = 8 - $bitsWritten;
+                $unusedBitCount = $bitsPerCharacter - $bitsNeeded;
+                
+                // Get the new bits ready
+                if ($bitsNeeded > $bitsPerCharacter) {
+                    // New bits aren't enough to complete a byte; shift them 
+                    // left into position
+                    $newBits = $charmap[$encodedString[$c]] << $bitsNeeded 
+                    - $bitsPerCharacter;
+                    $bitsWritten += $bitsPerCharacter;
+                    
+                } elseif ($c != $lastNotatedIndex || $rightPadFinalBits) {
+                    // Zero or more too many bits to complete a byte; 
+                    // shift right
+                    $newBits = $charmap[$encodedString[$c]] >> $unusedBitCount;
+                    $bitsWritten = 8; //$bitsWritten += $bitsNeeded;
+                    
+                } else {
+                    // Final bits don't need to be shifted
+                    $newBits = $charmap[$encodedString[$c]];
+                    $bitsWritten = 8;
+                }
+                
+                $byte |= $newBits;
+                
+                if ($bitsWritten == 8 || $c == $lastNotatedIndex) {
+                    // Byte is ready to be written
+                    $rawString .= pack('C', $byte);
+                    
+                    if ($c != $lastNotatedIndex) {
+                        // Start the next byte
+                        $bitsWritten = $unusedBitCount;
+                        $byte = ($charmap[$encodedString[$c]] 
+                        ^ ($newBits << $unusedBitCount)) << 8 - $bitsWritten;
+                    }
+                }
+                
+            } elseif ($strict) {
+                // Unable to decode character; abort
+                return NULL;
+            }
+        }
+        
+        return $rawString;
+    }
+}
diff --git a/src/lib/GoogleAuthenticator/GoogleAuthenticator.php b/src/lib/GoogleAuthenticator/GoogleAuthenticator.php
new file mode 100644
index 0000000..62bc0cb
--- /dev/null
+++ b/src/lib/GoogleAuthenticator/GoogleAuthenticator.php
@@ -0,0 +1,87 @@
+<?php
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+include_once("FixedByteNotation.php");
+
+
+class GoogleAuthenticator {
+    static $PASS_CODE_LENGTH = 6;
+    static $PIN_MODULO;
+    static $SECRET_LENGTH = 20;
+
+    public function __construct() {
+        self::$PIN_MODULO = pow(10, self::$PASS_CODE_LENGTH);
+    }
+
+    public function checkCode($secret,$code) {
+        $time = floor(time() / 30);
+        for ( $i = -1; $i <= 1; $i++) {
+
+            if ($this->getCode($secret,$time + $i) == $code) {
+                return true;
+            }
+        }
+
+        return false;
+
+    }
+
+    public function getCode($secret,$time = null) {
+
+        if (!$time) {
+            $time = floor(time() / 30);
+        }
+        $base32 = new FixedBitNotation(5, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', TRUE, TRUE);
+        $secret = $base32->decode($secret);
+
+        $time = pack("N", $time);
+        $time = str_pad($time,8, chr(0), STR_PAD_LEFT);
+
+        $hash = hash_hmac('sha1',$time,$secret,true);
+        $offset = ord(substr($hash,-1));
+        $offset = $offset & 0xF;
+
+        $truncatedHash = self::hashToInt($hash, $offset) & 0x7FFFFFFF;
+        $pinValue = str_pad($truncatedHash % self::$PIN_MODULO,6,"0",STR_PAD_LEFT);;
+        return $pinValue;
+    }
+
+    protected  function hashToInt($bytes, $start) {
+        $input = substr($bytes, $start, strlen($bytes) - $start);
+        $val2 = unpack("N",substr($input,0,4));
+        return $val2[1];
+    }
+
+    public function getUrl($user, $hostname, $secret) {
+        $url =  sprintf("otpauth://totp/%s@%s?secret=%s", $user, $hostname, $secret);
+        $encoder = "https://www.google.com/chart?chs=200x200&chld=M|0&cht=qr&chl=";
+        $encoderURL = sprintf( "%sotpauth://totp/%s@%s&secret=%s",$encoder, $user, $hostname, $secret);
+
+        return $encoderURL;
+
+    }
+
+    public function generateSecret() {
+        $secret = "";
+        for($i = 1;  $i<= self::$SECRET_LENGTH;$i++) {
+            $c = random_int(0,255);
+            $secret .= pack("c",$c);
+        }
+        $base32 = new FixedBitNotation(5, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', TRUE, TRUE);
+        return  $base32->encode($secret);
+
+
+    }
+
+}
diff --git a/src/lib/PHPMailer/DSNConfigurator.php b/src/lib/PHPMailer/DSNConfigurator.php
new file mode 100644
index 0000000..566c961
--- /dev/null
+++ b/src/lib/PHPMailer/DSNConfigurator.php
@@ -0,0 +1,245 @@
+<?php
+
+/**
+ * PHPMailer - PHP email creation and transport class.
+ * PHP Version 5.5.
+ *
+ * @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
+ *
+ * @author    Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
+ * @author    Jim Jagielski (jimjag) <jimjag@gmail.com>
+ * @author    Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
+ * @author    Brent R. Matzelle (original founder)
+ * @copyright 2012 - 2023 Marcus Bointon
+ * @copyright 2010 - 2012 Jim Jagielski
+ * @copyright 2004 - 2009 Andy Prevost
+ * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
+ * @note      This program is distributed in the hope that it will be useful - WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.
+ */
+
+namespace PHPMailer\PHPMailer;
+
+/**
+ * Configure PHPMailer with DSN string.
+ *
+ * @see https://en.wikipedia.org/wiki/Data_source_name
+ *
+ * @author Oleg Voronkovich <oleg-voronkovich@yandex.ru>
+ */
+class DSNConfigurator
+{
+    /**
+     * Create new PHPMailer instance configured by DSN.
+     *
+     * @param string $dsn        DSN
+     * @param bool   $exceptions Should we throw external exceptions?
+     *
+     * @return PHPMailer
+     */
+    public static function mailer($dsn, $exceptions = null)
+    {
+        static $configurator = null;
+
+        if (null === $configurator) {
+            $configurator = new DSNConfigurator();
+        }
+
+        return $configurator->configure(new PHPMailer($exceptions), $dsn);
+    }
+
+    /**
+     * Configure PHPMailer instance with DSN string.
+     *
+     * @param PHPMailer $mailer PHPMailer instance
+     * @param string    $dsn    DSN
+     *
+     * @return PHPMailer
+     */
+    public function configure(PHPMailer $mailer, $dsn)
+    {
+        $config = $this->parseDSN($dsn);
+
+        $this->applyConfig($mailer, $config);
+
+        return $mailer;
+    }
+
+    /**
+     * Parse DSN string.
+     *
+     * @param string $dsn DSN
+     *
+     * @throws Exception If DSN is malformed
+     *
+     * @return array Configuration
+     */
+    private function parseDSN($dsn)
+    {
+        $config = $this->parseUrl($dsn);
+
+        if (false === $config || !isset($config['scheme']) || !isset($config['host'])) {
+            throw new Exception('Malformed DSN');
+        }
+
+        if (isset($config['query'])) {
+            parse_str($config['query'], $config['query']);
+        }
+
+        return $config;
+    }
+
+    /**
+     * Apply configuration to mailer.
+     *
+     * @param PHPMailer $mailer PHPMailer instance
+     * @param array     $config Configuration
+     *
+     * @throws Exception If scheme is invalid
+     */
+    private function applyConfig(PHPMailer $mailer, $config)
+    {
+        switch ($config['scheme']) {
+            case 'mail':
+                $mailer->isMail();
+                break;
+            case 'sendmail':
+                $mailer->isSendmail();
+                break;
+            case 'qmail':
+                $mailer->isQmail();
+                break;
+            case 'smtp':
+            case 'smtps':
+                $mailer->isSMTP();
+                $this->configureSMTP($mailer, $config);
+                break;
+            default:
+                throw new Exception(
+                    sprintf(
+                        'Invalid scheme: "%s". Allowed values: "mail", "sendmail", "qmail", "smtp", "smtps".',
+                        $config['scheme']
+                    )
+                );
+        }
+
+        if (isset($config['query'])) {
+            $this->configureOptions($mailer, $config['query']);
+        }
+    }
+
+    /**
+     * Configure SMTP.
+     *
+     * @param PHPMailer $mailer PHPMailer instance
+     * @param array     $config Configuration
+     */
+    private function configureSMTP($mailer, $config)
+    {
+        $isSMTPS = 'smtps' === $config['scheme'];
+
+        if ($isSMTPS) {
+            $mailer->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
+        }
+
+        $mailer->Host = $config['host'];
+
+        if (isset($config['port'])) {
+            $mailer->Port = $config['port'];
+        } elseif ($isSMTPS) {
+            $mailer->Port = SMTP::DEFAULT_SECURE_PORT;
+        }
+
+        $mailer->SMTPAuth = isset($config['user']) || isset($config['pass']);
+
+        if (isset($config['user'])) {
+            $mailer->Username = $config['user'];
+        }
+
+        if (isset($config['pass'])) {
+            $mailer->Password = $config['pass'];
+        }
+    }
+
+    /**
+     * Configure options.
+     *
+     * @param PHPMailer $mailer  PHPMailer instance
+     * @param array     $options Options
+     *
+     * @throws Exception If option is unknown
+     */
+    private function configureOptions(PHPMailer $mailer, $options)
+    {
+        $allowedOptions = get_object_vars($mailer);
+
+        unset($allowedOptions['Mailer']);
+        unset($allowedOptions['SMTPAuth']);
+        unset($allowedOptions['Username']);
+        unset($allowedOptions['Password']);
+        unset($allowedOptions['Hostname']);
+        unset($allowedOptions['Port']);
+        unset($allowedOptions['ErrorInfo']);
+
+        $allowedOptions = \array_keys($allowedOptions);
+
+        foreach ($options as $key => $value) {
+            if (!in_array($key, $allowedOptions)) {
+                throw new Exception(
+                    sprintf(
+                        'Unknown option: "%s". Allowed values: "%s"',
+                        $key,
+                        implode('", "', $allowedOptions)
+                    )
+                );
+            }
+
+            switch ($key) {
+                case 'AllowEmpty':
+                case 'SMTPAutoTLS':
+                case 'SMTPKeepAlive':
+                case 'SingleTo':
+                case 'UseSendmailOptions':
+                case 'do_verp':
+                case 'DKIM_copyHeaderFields':
+                    $mailer->$key = (bool) $value;
+                    break;
+                case 'Priority':
+                case 'SMTPDebug':
+                case 'WordWrap':
+                    $mailer->$key = (int) $value;
+                    break;
+                default:
+                    $mailer->$key = $value;
+                    break;
+            }
+        }
+    }
+
+    /**
+     * Parse a URL.
+     * Wrapper for the built-in parse_url function to work around a bug in PHP 5.5.
+     *
+     * @param string $url URL
+     *
+     * @return array|false
+     */
+    protected function parseUrl($url)
+    {
+        if (\PHP_VERSION_ID >= 50600 || false === strpos($url, '?')) {
+            return parse_url($url);
+        }
+
+        $chunks = explode('?', $url);
+        if (is_array($chunks)) {
+            $result = parse_url($chunks[0]);
+            if (is_array($result)) {
+                $result['query'] = $chunks[1];
+            }
+            return $result;
+        }
+
+        return false;
+    }
+}
diff --git a/src/lib/PHPMailer/Exception.php b/src/lib/PHPMailer/Exception.php
new file mode 100644
index 0000000..52eaf95
--- /dev/null
+++ b/src/lib/PHPMailer/Exception.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * PHPMailer Exception class.
+ * PHP Version 5.5.
+ *
+ * @see       https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
+ *
+ * @author    Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
+ * @author    Jim Jagielski (jimjag) <jimjag@gmail.com>
+ * @author    Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
+ * @author    Brent R. Matzelle (original founder)
+ * @copyright 2012 - 2020 Marcus Bointon
+ * @copyright 2010 - 2012 Jim Jagielski
+ * @copyright 2004 - 2009 Andy Prevost
+ * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
+ * @note      This program is distributed in the hope that it will be useful - WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.
+ */
+
+namespace PHPMailer\PHPMailer;
+
+/**
+ * PHPMailer exception handler.
+ *
+ * @author Marcus Bointon <phpmailer@synchromedia.co.uk>
+ */
+class Exception extends \Exception
+{
+    /**
+     * Prettify error message output.
+     *
+     * @return string
+     */
+    public function errorMessage()
+    {
+        return '<strong>' . htmlspecialchars($this->getMessage(), ENT_COMPAT | ENT_HTML401) . "</strong><br />\n";
+    }
+}
diff --git a/src/lib/PHPMailer/LICENSE b/src/lib/PHPMailer/LICENSE
new file mode 100644
index 0000000..f166cc5
--- /dev/null
+++ b/src/lib/PHPMailer/LICENSE
@@ -0,0 +1,502 @@
+                  GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard.  To achieve this, non-free programs must be
+allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+                  GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                            NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
\ No newline at end of file
diff --git a/src/lib/PHPMailer/OAuth.php b/src/lib/PHPMailer/OAuth.php
new file mode 100644
index 0000000..c1d5b77
--- /dev/null
+++ b/src/lib/PHPMailer/OAuth.php
@@ -0,0 +1,139 @@
+<?php
+
+/**
+ * PHPMailer - PHP email creation and transport class.
+ * PHP Version 5.5.
+ *
+ * @see       https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
+ *
+ * @author    Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
+ * @author    Jim Jagielski (jimjag) <jimjag@gmail.com>
+ * @author    Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
+ * @author    Brent R. Matzelle (original founder)
+ * @copyright 2012 - 2020 Marcus Bointon
+ * @copyright 2010 - 2012 Jim Jagielski
+ * @copyright 2004 - 2009 Andy Prevost
+ * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
+ * @note      This program is distributed in the hope that it will be useful - WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.
+ */
+
+namespace PHPMailer\PHPMailer;
+
+use League\OAuth2\Client\Grant\RefreshToken;
+use League\OAuth2\Client\Provider\AbstractProvider;
+use League\OAuth2\Client\Token\AccessToken;
+
+/**
+ * OAuth - OAuth2 authentication wrapper class.
+ * Uses the oauth2-client package from the League of Extraordinary Packages.
+ *
+ * @see     http://oauth2-client.thephpleague.com
+ *
+ * @author  Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
+ */
+class OAuth implements OAuthTokenProvider
+{
+    /**
+     * An instance of the League OAuth Client Provider.
+     *
+     * @var AbstractProvider
+     */
+    protected $provider;
+
+    /**
+     * The current OAuth access token.
+     *
+     * @var AccessToken
+     */
+    protected $oauthToken;
+
+    /**
+     * The user's email address, usually used as the login ID
+     * and also the from address when sending email.
+     *
+     * @var string
+     */
+    protected $oauthUserEmail = '';
+
+    /**
+     * The client secret, generated in the app definition of the service you're connecting to.
+     *
+     * @var string
+     */
+    protected $oauthClientSecret = '';
+
+    /**
+     * The client ID, generated in the app definition of the service you're connecting to.
+     *
+     * @var string
+     */
+    protected $oauthClientId = '';
+
+    /**
+     * The refresh token, used to obtain new AccessTokens.
+     *
+     * @var string
+     */
+    protected $oauthRefreshToken = '';
+
+    /**
+     * OAuth constructor.
+     *
+     * @param array $options Associative array containing
+     *                       `provider`, `userName`, `clientSecret`, `clientId` and `refreshToken` elements
+     */
+    public function __construct($options)
+    {
+        $this->provider = $options['provider'];
+        $this->oauthUserEmail = $options['userName'];
+        $this->oauthClientSecret = $options['clientSecret'];
+        $this->oauthClientId = $options['clientId'];
+        $this->oauthRefreshToken = $options['refreshToken'];
+    }
+
+    /**
+     * Get a new RefreshToken.
+     *
+     * @return RefreshToken
+     */
+    protected function getGrant()
+    {
+        return new RefreshToken();
+    }
+
+    /**
+     * Get a new AccessToken.
+     *
+     * @return AccessToken
+     */
+    protected function getToken()
+    {
+        return $this->provider->getAccessToken(
+            $this->getGrant(),
+            ['refresh_token' => $this->oauthRefreshToken]
+        );
+    }
+
+    /**
+     * Generate a base64-encoded OAuth token.
+     *
+     * @return string
+     */
+    public function getOauth64()
+    {
+        //Get a new token if it's not available or has expired
+        if (null === $this->oauthToken || $this->oauthToken->hasExpired()) {
+            $this->oauthToken = $this->getToken();
+        }
+
+        return base64_encode(
+            'user=' .
+            $this->oauthUserEmail .
+            "\001auth=Bearer " .
+            $this->oauthToken .
+            "\001\001"
+        );
+    }
+}
diff --git a/src/lib/PHPMailer/OAuthTokenProvider.php b/src/lib/PHPMailer/OAuthTokenProvider.php
new file mode 100644
index 0000000..1155507
--- /dev/null
+++ b/src/lib/PHPMailer/OAuthTokenProvider.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * PHPMailer - PHP email creation and transport class.
+ * PHP Version 5.5.
+ *
+ * @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
+ *
+ * @author    Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
+ * @author    Jim Jagielski (jimjag) <jimjag@gmail.com>
+ * @author    Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
+ * @author    Brent R. Matzelle (original founder)
+ * @copyright 2012 - 2020 Marcus Bointon
+ * @copyright 2010 - 2012 Jim Jagielski
+ * @copyright 2004 - 2009 Andy Prevost
+ * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
+ * @note      This program is distributed in the hope that it will be useful - WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.
+ */
+
+namespace PHPMailer\PHPMailer;
+
+/**
+ * OAuthTokenProvider - OAuth2 token provider interface.
+ * Provides base64 encoded OAuth2 auth strings for SMTP authentication.
+ *
+ * @see     OAuth
+ * @see     SMTP::authenticate()
+ *
+ * @author  Peter Scopes (pdscopes)
+ * @author  Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
+ */
+interface OAuthTokenProvider
+{
+    /**
+     * Generate a base64-encoded OAuth token ensuring that the access token has not expired.
+     * The string to be base 64 encoded should be in the form:
+     * "user=<user_email_address>\001auth=Bearer <access_token>\001\001"
+     *
+     * @return string
+     */
+    public function getOauth64();
+}
diff --git a/src/lib/PHPMailer/PHPMailer.php b/src/lib/PHPMailer/PHPMailer.php
new file mode 100644
index 0000000..7f56ea2
--- /dev/null
+++ b/src/lib/PHPMailer/PHPMailer.php
@@ -0,0 +1,5126 @@
+<?php
+
+/**
+ * PHPMailer - PHP email creation and transport class.
+ * PHP Version 5.5.
+ *
+ * @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
+ *
+ * @author    Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
+ * @author    Jim Jagielski (jimjag) <jimjag@gmail.com>
+ * @author    Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
+ * @author    Brent R. Matzelle (original founder)
+ * @copyright 2012 - 2020 Marcus Bointon
+ * @copyright 2010 - 2012 Jim Jagielski
+ * @copyright 2004 - 2009 Andy Prevost
+ * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
+ * @note      This program is distributed in the hope that it will be useful - WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.
+ */
+
+namespace PHPMailer\PHPMailer;
+
+/**
+ * PHPMailer - PHP email creation and transport class.
+ *
+ * @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
+ * @author Jim Jagielski (jimjag) <jimjag@gmail.com>
+ * @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
+ * @author Brent R. Matzelle (original founder)
+ */
+class PHPMailer
+{
+    const CHARSET_ASCII = 'us-ascii';
+    const CHARSET_ISO88591 = 'iso-8859-1';
+    const CHARSET_UTF8 = 'utf-8';
+
+    const CONTENT_TYPE_PLAINTEXT = 'text/plain';
+    const CONTENT_TYPE_TEXT_CALENDAR = 'text/calendar';
+    const CONTENT_TYPE_TEXT_HTML = 'text/html';
+    const CONTENT_TYPE_MULTIPART_ALTERNATIVE = 'multipart/alternative';
+    const CONTENT_TYPE_MULTIPART_MIXED = 'multipart/mixed';
+    const CONTENT_TYPE_MULTIPART_RELATED = 'multipart/related';
+
+    const ENCODING_7BIT = '7bit';
+    const ENCODING_8BIT = '8bit';
+    const ENCODING_BASE64 = 'base64';
+    const ENCODING_BINARY = 'binary';
+    const ENCODING_QUOTED_PRINTABLE = 'quoted-printable';
+
+    const ENCRYPTION_STARTTLS = 'tls';
+    const ENCRYPTION_SMTPS = 'ssl';
+
+    const ICAL_METHOD_REQUEST = 'REQUEST';
+    const ICAL_METHOD_PUBLISH = 'PUBLISH';
+    const ICAL_METHOD_REPLY = 'REPLY';
+    const ICAL_METHOD_ADD = 'ADD';
+    const ICAL_METHOD_CANCEL = 'CANCEL';
+    const ICAL_METHOD_REFRESH = 'REFRESH';
+    const ICAL_METHOD_COUNTER = 'COUNTER';
+    const ICAL_METHOD_DECLINECOUNTER = 'DECLINECOUNTER';
+
+    /**
+     * Email priority.
+     * Options: null (default), 1 = High, 3 = Normal, 5 = low.
+     * When null, the header is not set at all.
+     *
+     * @var int|null
+     */
+    public $Priority;
+
+    /**
+     * The character set of the message.
+     *
+     * @var string
+     */
+    public $CharSet = self::CHARSET_ISO88591;
+
+    /**
+     * The MIME Content-type of the message.
+     *
+     * @var string
+     */
+    public $ContentType = self::CONTENT_TYPE_PLAINTEXT;
+
+    /**
+     * The message encoding.
+     * Options: "8bit", "7bit", "binary", "base64", and "quoted-printable".
+     *
+     * @var string
+     */
+    public $Encoding = self::ENCODING_8BIT;
+
+    /**
+     * Holds the most recent mailer error message.
+     *
+     * @var string
+     */
+    public $ErrorInfo = '';
+
+    /**
+     * The From email address for the message.
+     *
+     * @var string
+     */
+    public $From = '';
+
+    /**
+     * The From name of the message.
+     *
+     * @var string
+     */
+    public $FromName = '';
+
+    /**
+     * The envelope sender of the message.
+     * This will usually be turned into a Return-Path header by the receiver,
+     * and is the address that bounces will be sent to.
+     * If not empty, will be passed via `-f` to sendmail or as the 'MAIL FROM' value over SMTP.
+     *
+     * @var string
+     */
+    public $Sender = '';
+
+    /**
+     * The Subject of the message.
+     *
+     * @var string
+     */
+    public $Subject = '';
+
+    /**
+     * An HTML or plain text message body.
+     * If HTML then call isHTML(true).
+     *
+     * @var string
+     */
+    public $Body = '';
+
+    /**
+     * The plain-text message body.
+     * This body can be read by mail clients that do not have HTML email
+     * capability such as mutt & Eudora.
+     * Clients that can read HTML will view the normal Body.
+     *
+     * @var string
+     */
+    public $AltBody = '';
+
+    /**
+     * An iCal message part body.
+     * Only supported in simple alt or alt_inline message types
+     * To generate iCal event structures, use classes like EasyPeasyICS or iCalcreator.
+     *
+     * @see http://sprain.ch/blog/downloads/php-class-easypeasyics-create-ical-files-with-php/
+     * @see http://kigkonsult.se/iCalcreator/
+     *
+     * @var string
+     */
+    public $Ical = '';
+
+    /**
+     * Value-array of "method" in Contenttype header "text/calendar"
+     *
+     * @var string[]
+     */
+    protected static $IcalMethods = [
+        self::ICAL_METHOD_REQUEST,
+        self::ICAL_METHOD_PUBLISH,
+        self::ICAL_METHOD_REPLY,
+        self::ICAL_METHOD_ADD,
+        self::ICAL_METHOD_CANCEL,
+        self::ICAL_METHOD_REFRESH,
+        self::ICAL_METHOD_COUNTER,
+        self::ICAL_METHOD_DECLINECOUNTER,
+    ];
+
+    /**
+     * The complete compiled MIME message body.
+     *
+     * @var string
+     */
+    protected $MIMEBody = '';
+
+    /**
+     * The complete compiled MIME message headers.
+     *
+     * @var string
+     */
+    protected $MIMEHeader = '';
+
+    /**
+     * Extra headers that createHeader() doesn't fold in.
+     *
+     * @var string
+     */
+    protected $mailHeader = '';
+
+    /**
+     * Word-wrap the message body to this number of chars.
+     * Set to 0 to not wrap. A useful value here is 78, for RFC2822 section 2.1.1 compliance.
+     *
+     * @see static::STD_LINE_LENGTH
+     *
+     * @var int
+     */
+    public $WordWrap = 0;
+
+    /**
+     * Which method to use to send mail.
+     * Options: "mail", "sendmail", or "smtp".
+     *
+     * @var string
+     */
+    public $Mailer = 'mail';
+
+    /**
+     * The path to the sendmail program.
+     *
+     * @var string
+     */
+    public $Sendmail = '/usr/sbin/sendmail';
+
+    /**
+     * Whether mail() uses a fully sendmail-compatible MTA.
+     * One which supports sendmail's "-oi -f" options.
+     *
+     * @var bool
+     */
+    public $UseSendmailOptions = true;
+
+    /**
+     * The email address that a reading confirmation should be sent to, also known as read receipt.
+     *
+     * @var string
+     */
+    public $ConfirmReadingTo = '';
+
+    /**
+     * The hostname to use in the Message-ID header and as default HELO string.
+     * If empty, PHPMailer attempts to find one with, in order,
+     * $_SERVER['SERVER_NAME'], gethostname(), php_uname('n'), or the value
+     * 'localhost.localdomain'.
+     *
+     * @see PHPMailer::$Helo
+     *
+     * @var string
+     */
+    public $Hostname = '';
+
+    /**
+     * An ID to be used in the Message-ID header.
+     * If empty, a unique id will be generated.
+     * You can set your own, but it must be in the format "<id@domain>",
+     * as defined in RFC5322 section 3.6.4 or it will be ignored.
+     *
+     * @see https://tools.ietf.org/html/rfc5322#section-3.6.4
+     *
+     * @var string
+     */
+    public $MessageID = '';
+
+    /**
+     * The message Date to be used in the Date header.
+     * If empty, the current date will be added.
+     *
+     * @var string
+     */
+    public $MessageDate = '';
+
+    /**
+     * SMTP hosts.
+     * Either a single hostname or multiple semicolon-delimited hostnames.
+     * You can also specify a different port
+     * for each host by using this format: [hostname:port]
+     * (e.g. "smtp1.example.com:25;smtp2.example.com").
+     * You can also specify encryption type, for example:
+     * (e.g. "tls://smtp1.example.com:587;ssl://smtp2.example.com:465").
+     * Hosts will be tried in order.
+     *
+     * @var string
+     */
+    public $Host = 'localhost';
+
+    /**
+     * The default SMTP server port.
+     *
+     * @var int
+     */
+    public $Port = 25;
+
+    /**
+     * The SMTP HELO/EHLO name used for the SMTP connection.
+     * Default is $Hostname. If $Hostname is empty, PHPMailer attempts to find
+     * one with the same method described above for $Hostname.
+     *
+     * @see PHPMailer::$Hostname
+     *
+     * @var string
+     */
+    public $Helo = '';
+
+    /**
+     * What kind of encryption to use on the SMTP connection.
+     * Options: '', static::ENCRYPTION_STARTTLS, or static::ENCRYPTION_SMTPS.
+     *
+     * @var string
+     */
+    public $SMTPSecure = '';
+
+    /**
+     * Whether to enable TLS encryption automatically if a server supports it,
+     * even if `SMTPSecure` is not set to 'tls'.
+     * Be aware that in PHP >= 5.6 this requires that the server's certificates are valid.
+     *
+     * @var bool
+     */
+    public $SMTPAutoTLS = true;
+
+    /**
+     * Whether to use SMTP authentication.
+     * Uses the Username and Password properties.
+     *
+     * @see PHPMailer::$Username
+     * @see PHPMailer::$Password
+     *
+     * @var bool
+     */
+    public $SMTPAuth = false;
+
+    /**
+     * Options array passed to stream_context_create when connecting via SMTP.
+     *
+     * @var array
+     */
+    public $SMTPOptions = [];
+
+    /**
+     * SMTP username.
+     *
+     * @var string
+     */
+    public $Username = '';
+
+    /**
+     * SMTP password.
+     *
+     * @var string
+     */
+    public $Password = '';
+
+    /**
+     * SMTP authentication type. Options are CRAM-MD5, LOGIN, PLAIN, XOAUTH2.
+     * If not specified, the first one from that list that the server supports will be selected.
+     *
+     * @var string
+     */
+    public $AuthType = '';
+
+    /**
+     * An implementation of the PHPMailer OAuthTokenProvider interface.
+     *
+     * @var OAuthTokenProvider
+     */
+    protected $oauth;
+
+    /**
+     * The SMTP server timeout in seconds.
+     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
+     *
+     * @var int
+     */
+    public $Timeout = 300;
+
+    /**
+     * Comma separated list of DSN notifications
+     * 'NEVER' under no circumstances a DSN must be returned to the sender.
+     *         If you use NEVER all other notifications will be ignored.
+     * 'SUCCESS' will notify you when your mail has arrived at its destination.
+     * 'FAILURE' will arrive if an error occurred during delivery.
+     * 'DELAY'   will notify you if there is an unusual delay in delivery, but the actual
+     *           delivery's outcome (success or failure) is not yet decided.
+     *
+     * @see https://tools.ietf.org/html/rfc3461 See section 4.1 for more information about NOTIFY
+     */
+    public $dsn = '';
+
+    /**
+     * SMTP class debug output mode.
+     * Debug output level.
+     * Options:
+     * @see SMTP::DEBUG_OFF: No output
+     * @see SMTP::DEBUG_CLIENT: Client messages
+     * @see SMTP::DEBUG_SERVER: Client and server messages
+     * @see SMTP::DEBUG_CONNECTION: As SERVER plus connection status
+     * @see SMTP::DEBUG_LOWLEVEL: Noisy, low-level data output, rarely needed
+     *
+     * @see SMTP::$do_debug
+     *
+     * @var int
+     */
+    public $SMTPDebug = 0;
+
+    /**
+     * How to handle debug output.
+     * Options:
+     * * `echo` Output plain-text as-is, appropriate for CLI
+     * * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
+     * * `error_log` Output to error log as configured in php.ini
+     * By default PHPMailer will use `echo` if run from a `cli` or `cli-server` SAPI, `html` otherwise.
+     * Alternatively, you can provide a callable expecting two params: a message string and the debug level:
+     *
+     * ```php
+     * $mail->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
+     * ```
+     *
+     * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
+     * level output is used:
+     *
+     * ```php
+     * $mail->Debugoutput = new myPsr3Logger;
+     * ```
+     *
+     * @see SMTP::$Debugoutput
+     *
+     * @var string|callable|\Psr\Log\LoggerInterface
+     */
+    public $Debugoutput = 'echo';
+
+    /**
+     * Whether to keep the SMTP connection open after each message.
+     * If this is set to true then the connection will remain open after a send,
+     * and closing the connection will require an explicit call to smtpClose().
+     * It's a good idea to use this if you are sending multiple messages as it reduces overhead.
+     * See the mailing list example for how to use it.
+     *
+     * @var bool
+     */
+    public $SMTPKeepAlive = false;
+
+    /**
+     * Whether to split multiple to addresses into multiple messages
+     * or send them all in one message.
+     * Only supported in `mail` and `sendmail` transports, not in SMTP.
+     *
+     * @var bool
+     *
+     * @deprecated 6.0.0 PHPMailer isn't a mailing list manager!
+     */
+    public $SingleTo = false;
+
+    /**
+     * Storage for addresses when SingleTo is enabled.
+     *
+     * @var array
+     */
+    protected $SingleToArray = [];
+
+    /**
+     * Whether to generate VERP addresses on send.
+     * Only applicable when sending via SMTP.
+     *
+     * @see https://en.wikipedia.org/wiki/Variable_envelope_return_path
+     * @see http://www.postfix.org/VERP_README.html Postfix VERP info
+     *
+     * @var bool
+     */
+    public $do_verp = false;
+
+    /**
+     * Whether to allow sending messages with an empty body.
+     *
+     * @var bool
+     */
+    public $AllowEmpty = false;
+
+    /**
+     * DKIM selector.
+     *
+     * @var string
+     */
+    public $DKIM_selector = '';
+
+    /**
+     * DKIM Identity.
+     * Usually the email address used as the source of the email.
+     *
+     * @var string
+     */
+    public $DKIM_identity = '';
+
+    /**
+     * DKIM passphrase.
+     * Used if your key is encrypted.
+     *
+     * @var string
+     */
+    public $DKIM_passphrase = '';
+
+    /**
+     * DKIM signing domain name.
+     *
+     * @example 'example.com'
+     *
+     * @var string
+     */
+    public $DKIM_domain = '';
+
+    /**
+     * DKIM Copy header field values for diagnostic use.
+     *
+     * @var bool
+     */
+    public $DKIM_copyHeaderFields = true;
+
+    /**
+     * DKIM Extra signing headers.
+     *
+     * @example ['List-Unsubscribe', 'List-Help']
+     *
+     * @var array
+     */
+    public $DKIM_extraHeaders = [];
+
+    /**
+     * DKIM private key file path.
+     *
+     * @var string
+     */
+    public $DKIM_private = '';
+
+    /**
+     * DKIM private key string.
+     *
+     * If set, takes precedence over `$DKIM_private`.
+     *
+     * @var string
+     */
+    public $DKIM_private_string = '';
+
+    /**
+     * Callback Action function name.
+     *
+     * The function that handles the result of the send email action.
+     * It is called out by send() for each email sent.
+     *
+     * Value can be any php callable: http://www.php.net/is_callable
+     *
+     * Parameters:
+     *   bool $result        result of the send action
+     *   array   $to            email addresses of the recipients
+     *   array   $cc            cc email addresses
+     *   array   $bcc           bcc email addresses
+     *   string  $subject       the subject
+     *   string  $body          the email body
+     *   string  $from          email address of sender
+     *   string  $extra         extra information of possible use
+     *                          "smtp_transaction_id' => last smtp transaction id
+     *
+     * @var string
+     */
+    public $action_function = '';
+
+    /**
+     * What to put in the X-Mailer header.
+     * Options: An empty string for PHPMailer default, whitespace/null for none, or a string to use.
+     *
+     * @var string|null
+     */
+    public $XMailer = '';
+
+    /**
+     * Which validator to use by default when validating email addresses.
+     * May be a callable to inject your own validator, but there are several built-in validators.
+     * The default validator uses PHP's FILTER_VALIDATE_EMAIL filter_var option.
+     *
+     * @see PHPMailer::validateAddress()
+     *
+     * @var string|callable
+     */
+    public static $validator = 'php';
+
+    /**
+     * An instance of the SMTP sender class.
+     *
+     * @var SMTP
+     */
+    protected $smtp;
+
+    /**
+     * The array of 'to' names and addresses.
+     *
+     * @var array
+     */
+    protected $to = [];
+
+    /**
+     * The array of 'cc' names and addresses.
+     *
+     * @var array
+     */
+    protected $cc = [];
+
+    /**
+     * The array of 'bcc' names and addresses.
+     *
+     * @var array
+     */
+    protected $bcc = [];
+
+    /**
+     * The array of reply-to names and addresses.
+     *
+     * @var array
+     */
+    protected $ReplyTo = [];
+
+    /**
+     * An array of all kinds of addresses.
+     * Includes all of $to, $cc, $bcc.
+     *
+     * @see PHPMailer::$to
+     * @see PHPMailer::$cc
+     * @see PHPMailer::$bcc
+     *
+     * @var array
+     */
+    protected $all_recipients = [];
+
+    /**
+     * An array of names and addresses queued for validation.
+     * In send(), valid and non duplicate entries are moved to $all_recipients
+     * and one of $to, $cc, or $bcc.
+     * This array is used only for addresses with IDN.
+     *
+     * @see PHPMailer::$to
+     * @see PHPMailer::$cc
+     * @see PHPMailer::$bcc
+     * @see PHPMailer::$all_recipients
+     *
+     * @var array
+     */
+    protected $RecipientsQueue = [];
+
+    /**
+     * An array of reply-to names and addresses queued for validation.
+     * In send(), valid and non duplicate entries are moved to $ReplyTo.
+     * This array is used only for addresses with IDN.
+     *
+     * @see PHPMailer::$ReplyTo
+     *
+     * @var array
+     */
+    protected $ReplyToQueue = [];
+
+    /**
+     * The array of attachments.
+     *
+     * @var array
+     */
+    protected $attachment = [];
+
+    /**
+     * The array of custom headers.
+     *
+     * @var array
+     */
+    protected $CustomHeader = [];
+
+    /**
+     * The most recent Message-ID (including angular brackets).
+     *
+     * @var string
+     */
+    protected $lastMessageID = '';
+
+    /**
+     * The message's MIME type.
+     *
+     * @var string
+     */
+    protected $message_type = '';
+
+    /**
+     * The array of MIME boundary strings.
+     *
+     * @var array
+     */
+    protected $boundary = [];
+
+    /**
+     * The array of available text strings for the current language.
+     *
+     * @var array
+     */
+    protected $language = [];
+
+    /**
+     * The number of errors encountered.
+     *
+     * @var int
+     */
+    protected $error_count = 0;
+
+    /**
+     * The S/MIME certificate file path.
+     *
+     * @var string
+     */
+    protected $sign_cert_file = '';
+
+    /**
+     * The S/MIME key file path.
+     *
+     * @var string
+     */
+    protected $sign_key_file = '';
+
+    /**
+     * The optional S/MIME extra certificates ("CA Chain") file path.
+     *
+     * @var string
+     */
+    protected $sign_extracerts_file = '';
+
+    /**
+     * The S/MIME password for the key.
+     * Used only if the key is encrypted.
+     *
+     * @var string
+     */
+    protected $sign_key_pass = '';
+
+    /**
+     * Whether to throw exceptions for errors.
+     *
+     * @var bool
+     */
+    protected $exceptions = false;
+
+    /**
+     * Unique ID used for message ID and boundaries.
+     *
+     * @var string
+     */
+    protected $uniqueid = '';
+
+    /**
+     * The PHPMailer Version number.
+     *
+     * @var string
+     */
+    const VERSION = '6.8.1';
+
+    /**
+     * Error severity: message only, continue processing.
+     *
+     * @var int
+     */
+    const STOP_MESSAGE = 0;
+
+    /**
+     * Error severity: message, likely ok to continue processing.
+     *
+     * @var int
+     */
+    const STOP_CONTINUE = 1;
+
+    /**
+     * Error severity: message, plus full stop, critical error reached.
+     *
+     * @var int
+     */
+    const STOP_CRITICAL = 2;
+
+    /**
+     * The SMTP standard CRLF line break.
+     * If you want to change line break format, change static::$LE, not this.
+     */
+    const CRLF = "\r\n";
+
+    /**
+     * "Folding White Space" a white space string used for line folding.
+     */
+    const FWS = ' ';
+
+    /**
+     * SMTP RFC standard line ending; Carriage Return, Line Feed.
+     *
+     * @var string
+     */
+    protected static $LE = self::CRLF;
+
+    /**
+     * The maximum line length supported by mail().
+     *
+     * Background: mail() will sometimes corrupt messages
+     * with headers longer than 65 chars, see #818.
+     *
+     * @var int
+     */
+    const MAIL_MAX_LINE_LENGTH = 63;
+
+    /**
+     * The maximum line length allowed by RFC 2822 section 2.1.1.
+     *
+     * @var int
+     */
+    const MAX_LINE_LENGTH = 998;
+
+    /**
+     * The lower maximum line length allowed by RFC 2822 section 2.1.1.
+     * This length does NOT include the line break
+     * 76 means that lines will be 77 or 78 chars depending on whether
+     * the line break format is LF or CRLF; both are valid.
+     *
+     * @var int
+     */
+    const STD_LINE_LENGTH = 76;
+
+    /**
+     * Constructor.
+     *
+     * @param bool $exceptions Should we throw external exceptions?
+     */
+    public function __construct($exceptions = null)
+    {
+        if (null !== $exceptions) {
+            $this->exceptions = (bool) $exceptions;
+        }
+        //Pick an appropriate debug output format automatically
+        $this->Debugoutput = (strpos(PHP_SAPI, 'cli') !== false ? 'echo' : 'html');
+    }
+
+    /**
+     * Destructor.
+     */
+    public function __destruct()
+    {
+        //Close any open SMTP connection nicely
+        $this->smtpClose();
+    }
+
+    /**
+     * Call mail() in a safe_mode-aware fashion.
+     * Also, unless sendmail_path points to sendmail (or something that
+     * claims to be sendmail), don't pass params (not a perfect fix,
+     * but it will do).
+     *
+     * @param string      $to      To
+     * @param string      $subject Subject
+     * @param string      $body    Message Body
+     * @param string      $header  Additional Header(s)
+     * @param string|null $params  Params
+     *
+     * @return bool
+     */
+    private function mailPassthru($to, $subject, $body, $header, $params)
+    {
+        //Check overloading of mail function to avoid double-encoding
+        if ((int)ini_get('mbstring.func_overload') & 1) {
+            $subject = $this->secureHeader($subject);
+        } else {
+            $subject = $this->encodeHeader($this->secureHeader($subject));
+        }
+        //Calling mail() with null params breaks
+        $this->edebug('Sending with mail()');
+        $this->edebug('Sendmail path: ' . ini_get('sendmail_path'));
+        $this->edebug("Envelope sender: {$this->Sender}");
+        $this->edebug("To: {$to}");
+        $this->edebug("Subject: {$subject}");
+        $this->edebug("Headers: {$header}");
+        if (!$this->UseSendmailOptions || null === $params) {
+            $result = @mail($to, $subject, $body, $header);
+        } else {
+            $this->edebug("Additional params: {$params}");
+            $result = @mail($to, $subject, $body, $header, $params);
+        }
+        $this->edebug('Result: ' . ($result ? 'true' : 'false'));
+        return $result;
+    }
+
+    /**
+     * Output debugging info via a user-defined method.
+     * Only generates output if debug output is enabled.
+     *
+     * @see PHPMailer::$Debugoutput
+     * @see PHPMailer::$SMTPDebug
+     *
+     * @param string $str
+     */
+    protected function edebug($str)
+    {
+        if ($this->SMTPDebug <= 0) {
+            return;
+        }
+        //Is this a PSR-3 logger?
+        if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
+            $this->Debugoutput->debug($str);
+
+            return;
+        }
+        //Avoid clash with built-in function names
+        if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
+            call_user_func($this->Debugoutput, $str, $this->SMTPDebug);
+
+            return;
+        }
+        switch ($this->Debugoutput) {
+            case 'error_log':
+                //Don't output, just log
+                /** @noinspection ForgottenDebugOutputInspection */
+                error_log($str);
+                break;
+            case 'html':
+                //Cleans up output a bit for a better looking, HTML-safe output
+                echo htmlentities(
+                    preg_replace('/[\r\n]+/', '', $str),
+                    ENT_QUOTES,
+                    'UTF-8'
+                ), "<br>\n";
+                break;
+            case 'echo':
+            default:
+                //Normalize line breaks
+                $str = preg_replace('/\r\n|\r/m', "\n", $str);
+                echo gmdate('Y-m-d H:i:s'),
+                "\t",
+                    //Trim trailing space
+                trim(
+                    //Indent for readability, except for trailing break
+                    str_replace(
+                        "\n",
+                        "\n                   \t                  ",
+                        trim($str)
+                    )
+                ),
+                "\n";
+        }
+    }
+
+    /**
+     * Sets message type to HTML or plain.
+     *
+     * @param bool $isHtml True for HTML mode
+     */
+    public function isHTML($isHtml = true)
+    {
+        if ($isHtml) {
+            $this->ContentType = static::CONTENT_TYPE_TEXT_HTML;
+        } else {
+            $this->ContentType = static::CONTENT_TYPE_PLAINTEXT;
+        }
+    }
+
+    /**
+     * Send messages using SMTP.
+     */
+    public function isSMTP()
+    {
+        $this->Mailer = 'smtp';
+    }
+
+    /**
+     * Send messages using PHP's mail() function.
+     */
+    public function isMail()
+    {
+        $this->Mailer = 'mail';
+    }
+
+    /**
+     * Send messages using $Sendmail.
+     */
+    public function isSendmail()
+    {
+        $ini_sendmail_path = ini_get('sendmail_path');
+
+        if (false === stripos($ini_sendmail_path, 'sendmail')) {
+            $this->Sendmail = '/usr/sbin/sendmail';
+        } else {
+            $this->Sendmail = $ini_sendmail_path;
+        }
+        $this->Mailer = 'sendmail';
+    }
+
+    /**
+     * Send messages using qmail.
+     */
+    public function isQmail()
+    {
+        $ini_sendmail_path = ini_get('sendmail_path');
+
+        if (false === stripos($ini_sendmail_path, 'qmail')) {
+            $this->Sendmail = '/var/qmail/bin/qmail-inject';
+        } else {
+            $this->Sendmail = $ini_sendmail_path;
+        }
+        $this->Mailer = 'qmail';
+    }
+
+    /**
+     * Add a "To" address.
+     *
+     * @param string $address The email address to send to
+     * @param string $name
+     *
+     * @throws Exception
+     *
+     * @return bool true on success, false if address already used or invalid in some way
+     */
+    public function addAddress($address, $name = '')
+    {
+        return $this->addOrEnqueueAnAddress('to', $address, $name);
+    }
+
+    /**
+     * Add a "CC" address.
+     *
+     * @param string $address The email address to send to
+     * @param string $name
+     *
+     * @throws Exception
+     *
+     * @return bool true on success, false if address already used or invalid in some way
+     */
+    public function addCC($address, $name = '')
+    {
+        return $this->addOrEnqueueAnAddress('cc', $address, $name);
+    }
+
+    /**
+     * Add a "BCC" address.
+     *
+     * @param string $address The email address to send to
+     * @param string $name
+     *
+     * @throws Exception
+     *
+     * @return bool true on success, false if address already used or invalid in some way
+     */
+    public function addBCC($address, $name = '')
+    {
+        return $this->addOrEnqueueAnAddress('bcc', $address, $name);
+    }
+
+    /**
+     * Add a "Reply-To" address.
+     *
+     * @param string $address The email address to reply to
+     * @param string $name
+     *
+     * @throws Exception
+     *
+     * @return bool true on success, false if address already used or invalid in some way
+     */
+    public function addReplyTo($address, $name = '')
+    {
+        return $this->addOrEnqueueAnAddress('Reply-To', $address, $name);
+    }
+
+    /**
+     * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer
+     * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still
+     * be modified after calling this function), addition of such addresses is delayed until send().
+     * Addresses that have been added already return false, but do not throw exceptions.
+     *
+     * @param string $kind    One of 'to', 'cc', 'bcc', or 'ReplyTo'
+     * @param string $address The email address
+     * @param string $name    An optional username associated with the address
+     *
+     * @throws Exception
+     *
+     * @return bool true on success, false if address already used or invalid in some way
+     */
+    protected function addOrEnqueueAnAddress($kind, $address, $name)
+    {
+        $pos = false;
+        if ($address !== null) {
+            $address = trim($address);
+            $pos = strrpos($address, '@');
+        }
+        if (false === $pos) {
+            //At-sign is missing.
+            $error_message = sprintf(
+                '%s (%s): %s',
+                $this->lang('invalid_address'),
+                $kind,
+                $address
+            );
+            $this->setError($error_message);
+            $this->edebug($error_message);
+            if ($this->exceptions) {
+                throw new Exception($error_message);
+            }
+
+            return false;
+        }
+        if ($name !== null && is_string($name)) {
+            $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
+        } else {
+            $name = '';
+        }
+        $params = [$kind, $address, $name];
+        //Enqueue addresses with IDN until we know the PHPMailer::$CharSet.
+        //Domain is assumed to be whatever is after the last @ symbol in the address
+        if (static::idnSupported() && $this->has8bitChars(substr($address, ++$pos))) {
+            if ('Reply-To' !== $kind) {
+                if (!array_key_exists($address, $this->RecipientsQueue)) {
+                    $this->RecipientsQueue[$address] = $params;
+
+                    return true;
+                }
+            } elseif (!array_key_exists($address, $this->ReplyToQueue)) {
+                $this->ReplyToQueue[$address] = $params;
+
+                return true;
+            }
+
+            return false;
+        }
+
+        //Immediately add standard addresses without IDN.
+        return call_user_func_array([$this, 'addAnAddress'], $params);
+    }
+
+    /**
+     * Set the boundaries to use for delimiting MIME parts.
+     * If you override this, ensure you set all 3 boundaries to unique values.
+     * The default boundaries include a "=_" sequence which cannot occur in quoted-printable bodies,
+     * as suggested by https://www.rfc-editor.org/rfc/rfc2045#section-6.7
+     *
+     * @return void
+     */
+    public function setBoundaries()
+    {
+        $this->uniqueid = $this->generateId();
+        $this->boundary[1] = 'b1=_' . $this->uniqueid;
+        $this->boundary[2] = 'b2=_' . $this->uniqueid;
+        $this->boundary[3] = 'b3=_' . $this->uniqueid;
+    }
+
+    /**
+     * Add an address to one of the recipient arrays or to the ReplyTo array.
+     * Addresses that have been added already return false, but do not throw exceptions.
+     *
+     * @param string $kind    One of 'to', 'cc', 'bcc', or 'ReplyTo'
+     * @param string $address The email address to send, resp. to reply to
+     * @param string $name
+     *
+     * @throws Exception
+     *
+     * @return bool true on success, false if address already used or invalid in some way
+     */
+    protected function addAnAddress($kind, $address, $name = '')
+    {
+        if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) {
+            $error_message = sprintf(
+                '%s: %s',
+                $this->lang('Invalid recipient kind'),
+                $kind
+            );
+            $this->setError($error_message);
+            $this->edebug($error_message);
+            if ($this->exceptions) {
+                throw new Exception($error_message);
+            }
+
+            return false;
+        }
+        if (!static::validateAddress($address)) {
+            $error_message = sprintf(
+                '%s (%s): %s',
+                $this->lang('invalid_address'),
+                $kind,
+                $address
+            );
+            $this->setError($error_message);
+            $this->edebug($error_message);
+            if ($this->exceptions) {
+                throw new Exception($error_message);
+            }
+
+            return false;
+        }
+        if ('Reply-To' !== $kind) {
+            if (!array_key_exists(strtolower($address), $this->all_recipients)) {
+                $this->{$kind}[] = [$address, $name];
+                $this->all_recipients[strtolower($address)] = true;
+
+                return true;
+            }
+        } elseif (!array_key_exists(strtolower($address), $this->ReplyTo)) {
+            $this->ReplyTo[strtolower($address)] = [$address, $name];
+
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Parse and validate a string containing one or more RFC822-style comma-separated email addresses
+     * of the form "display name <address>" into an array of name/address pairs.
+     * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available.
+     * Note that quotes in the name part are removed.
+     *
+     * @see http://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation
+     *
+     * @param string $addrstr The address list string
+     * @param bool   $useimap Whether to use the IMAP extension to parse the list
+     * @param string $charset The charset to use when decoding the address list string.
+     *
+     * @return array
+     */
+    public static function parseAddresses($addrstr, $useimap = true, $charset = self::CHARSET_ISO88591)
+    {
+        $addresses = [];
+        if ($useimap && function_exists('imap_rfc822_parse_adrlist')) {
+            //Use this built-in parser if it's available
+            $list = imap_rfc822_parse_adrlist($addrstr, '');
+            // Clear any potential IMAP errors to get rid of notices being thrown at end of script.
+            imap_errors();
+            foreach ($list as $address) {
+                if (
+                    '.SYNTAX-ERROR.' !== $address->host &&
+                    static::validateAddress($address->mailbox . '@' . $address->host)
+                ) {
+                    //Decode the name part if it's present and encoded
+                    if (
+                        property_exists($address, 'personal') &&
+                        //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled
+                        defined('MB_CASE_UPPER') &&
+                        preg_match('/^=\?.*\?=$/s', $address->personal)
+                    ) {
+                        $origCharset = mb_internal_encoding();
+                        mb_internal_encoding($charset);
+                        //Undo any RFC2047-encoded spaces-as-underscores
+                        $address->personal = str_replace('_', '=20', $address->personal);
+                        //Decode the name
+                        $address->personal = mb_decode_mimeheader($address->personal);
+                        mb_internal_encoding($origCharset);
+                    }
+
+                    $addresses[] = [
+                        'name' => (property_exists($address, 'personal') ? $address->personal : ''),
+                        'address' => $address->mailbox . '@' . $address->host,
+                    ];
+                }
+            }
+        } else {
+            //Use this simpler parser
+            $list = explode(',', $addrstr);
+            foreach ($list as $address) {
+                $address = trim($address);
+                //Is there a separate name part?
+                if (strpos($address, '<') === false) {
+                    //No separate name, just use the whole thing
+                    if (static::validateAddress($address)) {
+                        $addresses[] = [
+                            'name' => '',
+                            'address' => $address,
+                        ];
+                    }
+                } else {
+                    list($name, $email) = explode('<', $address);
+                    $email = trim(str_replace('>', '', $email));
+                    $name = trim($name);
+                    if (static::validateAddress($email)) {
+                        //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled
+                        //If this name is encoded, decode it
+                        if (defined('MB_CASE_UPPER') && preg_match('/^=\?.*\?=$/s', $name)) {
+                            $origCharset = mb_internal_encoding();
+                            mb_internal_encoding($charset);
+                            //Undo any RFC2047-encoded spaces-as-underscores
+                            $name = str_replace('_', '=20', $name);
+                            //Decode the name
+                            $name = mb_decode_mimeheader($name);
+                            mb_internal_encoding($origCharset);
+                        }
+                        $addresses[] = [
+                            //Remove any surrounding quotes and spaces from the name
+                            'name' => trim($name, '\'" '),
+                            'address' => $email,
+                        ];
+                    }
+                }
+            }
+        }
+
+        return $addresses;
+    }
+
+    /**
+     * Set the From and FromName properties.
+     *
+     * @param string $address
+     * @param string $name
+     * @param bool   $auto    Whether to also set the Sender address, defaults to true
+     *
+     * @throws Exception
+     *
+     * @return bool
+     */
+    public function setFrom($address, $name = '', $auto = true)
+    {
+        $address = trim((string)$address);
+        $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
+        //Don't validate now addresses with IDN. Will be done in send().
+        $pos = strrpos($address, '@');
+        if (
+            (false === $pos)
+            || ((!$this->has8bitChars(substr($address, ++$pos)) || !static::idnSupported())
+            && !static::validateAddress($address))
+        ) {
+            $error_message = sprintf(
+                '%s (From): %s',
+                $this->lang('invalid_address'),
+                $address
+            );
+            $this->setError($error_message);
+            $this->edebug($error_message);
+            if ($this->exceptions) {
+                throw new Exception($error_message);
+            }
+
+            return false;
+        }
+        $this->From = $address;
+        $this->FromName = $name;
+        if ($auto && empty($this->Sender)) {
+            $this->Sender = $address;
+        }
+
+        return true;
+    }
+
+    /**
+     * Return the Message-ID header of the last email.
+     * Technically this is the value from the last time the headers were created,
+     * but it's also the message ID of the last sent message except in
+     * pathological cases.
+     *
+     * @return string
+     */
+    public function getLastMessageID()
+    {
+        return $this->lastMessageID;
+    }
+
+    /**
+     * Check that a string looks like an email address.
+     * Validation patterns supported:
+     * * `auto` Pick best pattern automatically;
+     * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0;
+     * * `pcre` Use old PCRE implementation;
+     * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL;
+     * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements.
+     * * `noregex` Don't use a regex: super fast, really dumb.
+     * Alternatively you may pass in a callable to inject your own validator, for example:
+     *
+     * ```php
+     * PHPMailer::validateAddress('user@example.com', function($address) {
+     *     return (strpos($address, '@') !== false);
+     * });
+     * ```
+     *
+     * You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator.
+     *
+     * @param string          $address       The email address to check
+     * @param string|callable $patternselect Which pattern to use
+     *
+     * @return bool
+     */
+    public static function validateAddress($address, $patternselect = null)
+    {
+        if (null === $patternselect) {
+            $patternselect = static::$validator;
+        }
+        //Don't allow strings as callables, see SECURITY.md and CVE-2021-3603
+        if (is_callable($patternselect) && !is_string($patternselect)) {
+            return call_user_func($patternselect, $address);
+        }
+        //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321
+        if (strpos($address, "\n") !== false || strpos($address, "\r") !== false) {
+            return false;
+        }
+        switch ($patternselect) {
+            case 'pcre': //Kept for BC
+            case 'pcre8':
+                /*
+                 * A more complex and more permissive version of the RFC5322 regex on which FILTER_VALIDATE_EMAIL
+                 * is based.
+                 * In addition to the addresses allowed by filter_var, also permits:
+                 *  * dotless domains: `a@b`
+                 *  * comments: `1234 @ local(blah) .machine .example`
+                 *  * quoted elements: `'"test blah"@example.org'`
+                 *  * numeric TLDs: `a@b.123`
+                 *  * unbracketed IPv4 literals: `a@192.168.0.1`
+                 *  * IPv6 literals: 'first.last@[IPv6:a1::]'
+                 * Not all of these will necessarily work for sending!
+                 *
+                 * @see       http://squiloople.com/2009/12/20/email-address-validation/
+                 * @copyright 2009-2010 Michael Rushton
+                 * Feel free to use and redistribute this code. But please keep this copyright notice.
+                 */
+                return (bool) preg_match(
+                    '/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' .
+                    '((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' .
+                    '(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' .
+                    '([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' .
+                    '(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' .
+                    '(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' .
+                    '|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' .
+                    '|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' .
+                    '|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD',
+                    $address
+                );
+            case 'html5':
+                /*
+                 * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements.
+                 *
+                 * @see https://html.spec.whatwg.org/#e-mail-state-(type=email)
+                 */
+                return (bool) preg_match(
+                    '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' .
+                    '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD',
+                    $address
+                );
+            case 'php':
+            default:
+                return filter_var($address, FILTER_VALIDATE_EMAIL) !== false;
+        }
+    }
+
+    /**
+     * Tells whether IDNs (Internationalized Domain Names) are supported or not. This requires the
+     * `intl` and `mbstring` PHP extensions.
+     *
+     * @return bool `true` if required functions for IDN support are present
+     */
+    public static function idnSupported()
+    {
+        return function_exists('idn_to_ascii') && function_exists('mb_convert_encoding');
+    }
+
+    /**
+     * Converts IDN in given email address to its ASCII form, also known as punycode, if possible.
+     * Important: Address must be passed in same encoding as currently set in PHPMailer::$CharSet.
+     * This function silently returns unmodified address if:
+     * - No conversion is necessary (i.e. domain name is not an IDN, or is already in ASCII form)
+     * - Conversion to punycode is impossible (e.g. required PHP functions are not available)
+     *   or fails for any reason (e.g. domain contains characters not allowed in an IDN).
+     *
+     * @see PHPMailer::$CharSet
+     *
+     * @param string $address The email address to convert
+     *
+     * @return string The encoded address in ASCII form
+     */
+    public function punyencodeAddress($address)
+    {
+        //Verify we have required functions, CharSet, and at-sign.
+        $pos = strrpos($address, '@');
+        if (
+            !empty($this->CharSet) &&
+            false !== $pos &&
+            static::idnSupported()
+        ) {
+            $domain = substr($address, ++$pos);
+            //Verify CharSet string is a valid one, and domain properly encoded in this CharSet.
+            if ($this->has8bitChars($domain) && @mb_check_encoding($domain, $this->CharSet)) {
+                //Convert the domain from whatever charset it's in to UTF-8
+                $domain = mb_convert_encoding($domain, self::CHARSET_UTF8, $this->CharSet);
+                //Ignore IDE complaints about this line - method signature changed in PHP 5.4
+                $errorcode = 0;
+                if (defined('INTL_IDNA_VARIANT_UTS46')) {
+                    //Use the current punycode standard (appeared in PHP 7.2)
+                    $punycode = idn_to_ascii(
+                        $domain,
+                        \IDNA_DEFAULT | \IDNA_USE_STD3_RULES | \IDNA_CHECK_BIDI |
+                            \IDNA_CHECK_CONTEXTJ | \IDNA_NONTRANSITIONAL_TO_ASCII,
+                        \INTL_IDNA_VARIANT_UTS46
+                    );
+                } elseif (defined('INTL_IDNA_VARIANT_2003')) {
+                    //Fall back to this old, deprecated/removed encoding
+                    $punycode = idn_to_ascii($domain, $errorcode, \INTL_IDNA_VARIANT_2003);
+                } else {
+                    //Fall back to a default we don't know about
+                    $punycode = idn_to_ascii($domain, $errorcode);
+                }
+                if (false !== $punycode) {
+                    return substr($address, 0, $pos) . $punycode;
+                }
+            }
+        }
+
+        return $address;
+    }
+
+    /**
+     * Create a message and send it.
+     * Uses the sending method specified by $Mailer.
+     *
+     * @throws Exception
+     *
+     * @return bool false on error - See the ErrorInfo property for details of the error
+     */
+    public function send()
+    {
+        try {
+            if (!$this->preSend()) {
+                return false;
+            }
+
+            return $this->postSend();
+        } catch (Exception $exc) {
+            $this->mailHeader = '';
+            $this->setError($exc->getMessage());
+            if ($this->exceptions) {
+                throw $exc;
+            }
+
+            return false;
+        }
+    }
+
+    /**
+     * Prepare a message for sending.
+     *
+     * @throws Exception
+     *
+     * @return bool
+     */
+    public function preSend()
+    {
+        if (
+            'smtp' === $this->Mailer
+            || ('mail' === $this->Mailer && (\PHP_VERSION_ID >= 80000 || stripos(PHP_OS, 'WIN') === 0))
+        ) {
+            //SMTP mandates RFC-compliant line endings
+            //and it's also used with mail() on Windows
+            static::setLE(self::CRLF);
+        } else {
+            //Maintain backward compatibility with legacy Linux command line mailers
+            static::setLE(PHP_EOL);
+        }
+        //Check for buggy PHP versions that add a header with an incorrect line break
+        if (
+            'mail' === $this->Mailer
+            && ((\PHP_VERSION_ID >= 70000 && \PHP_VERSION_ID < 70017)
+                || (\PHP_VERSION_ID >= 70100 && \PHP_VERSION_ID < 70103))
+            && ini_get('mail.add_x_header') === '1'
+            && stripos(PHP_OS, 'WIN') === 0
+        ) {
+            trigger_error($this->lang('buggy_php'), E_USER_WARNING);
+        }
+
+        try {
+            $this->error_count = 0; //Reset errors
+            $this->mailHeader = '';
+
+            //Dequeue recipient and Reply-To addresses with IDN
+            foreach (array_merge($this->RecipientsQueue, $this->ReplyToQueue) as $params) {
+                $params[1] = $this->punyencodeAddress($params[1]);
+                call_user_func_array([$this, 'addAnAddress'], $params);
+            }
+            if (count($this->to) + count($this->cc) + count($this->bcc) < 1) {
+                throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL);
+            }
+
+            //Validate From, Sender, and ConfirmReadingTo addresses
+            foreach (['From', 'Sender', 'ConfirmReadingTo'] as $address_kind) {
+                $this->{$address_kind} = trim($this->{$address_kind});
+                if (empty($this->{$address_kind})) {
+                    continue;
+                }
+                $this->{$address_kind} = $this->punyencodeAddress($this->{$address_kind});
+                if (!static::validateAddress($this->{$address_kind})) {
+                    $error_message = sprintf(
+                        '%s (%s): %s',
+                        $this->lang('invalid_address'),
+                        $address_kind,
+                        $this->{$address_kind}
+                    );
+                    $this->setError($error_message);
+                    $this->edebug($error_message);
+                    if ($this->exceptions) {
+                        throw new Exception($error_message);
+                    }
+
+                    return false;
+                }
+            }
+
+            //Set whether the message is multipart/alternative
+            if ($this->alternativeExists()) {
+                $this->ContentType = static::CONTENT_TYPE_MULTIPART_ALTERNATIVE;
+            }
+
+            $this->setMessageType();
+            //Refuse to send an empty message unless we are specifically allowing it
+            if (!$this->AllowEmpty && empty($this->Body)) {
+                throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
+            }
+
+            //Trim subject consistently
+            $this->Subject = trim($this->Subject);
+            //Create body before headers in case body makes changes to headers (e.g. altering transfer encoding)
+            $this->MIMEHeader = '';
+            $this->MIMEBody = $this->createBody();
+            //createBody may have added some headers, so retain them
+            $tempheaders = $this->MIMEHeader;
+            $this->MIMEHeader = $this->createHeader();
+            $this->MIMEHeader .= $tempheaders;
+
+            //To capture the complete message when using mail(), create
+            //an extra header list which createHeader() doesn't fold in
+            if ('mail' === $this->Mailer) {
+                if (count($this->to) > 0) {
+                    $this->mailHeader .= $this->addrAppend('To', $this->to);
+                } else {
+                    $this->mailHeader .= $this->headerLine('To', 'undisclosed-recipients:;');
+                }
+                $this->mailHeader .= $this->headerLine(
+                    'Subject',
+                    $this->encodeHeader($this->secureHeader($this->Subject))
+                );
+            }
+
+            //Sign with DKIM if enabled
+            if (
+                !empty($this->DKIM_domain)
+                && !empty($this->DKIM_selector)
+                && (!empty($this->DKIM_private_string)
+                    || (!empty($this->DKIM_private)
+                        && static::isPermittedPath($this->DKIM_private)
+                        && file_exists($this->DKIM_private)
+                    )
+                )
+            ) {
+                $header_dkim = $this->DKIM_Add(
+                    $this->MIMEHeader . $this->mailHeader,
+                    $this->encodeHeader($this->secureHeader($this->Subject)),
+                    $this->MIMEBody
+                );
+                $this->MIMEHeader = static::stripTrailingWSP($this->MIMEHeader) . static::$LE .
+                    static::normalizeBreaks($header_dkim) . static::$LE;
+            }
+
+            return true;
+        } catch (Exception $exc) {
+            $this->setError($exc->getMessage());
+            if ($this->exceptions) {
+                throw $exc;
+            }
+
+            return false;
+        }
+    }
+
+    /**
+     * Actually send a message via the selected mechanism.
+     *
+     * @throws Exception
+     *
+     * @return bool
+     */
+    public function postSend()
+    {
+        try {
+            //Choose the mailer and send through it
+            switch ($this->Mailer) {
+                case 'sendmail':
+                case 'qmail':
+                    return $this->sendmailSend($this->MIMEHeader, $this->MIMEBody);
+                case 'smtp':
+                    return $this->smtpSend($this->MIMEHeader, $this->MIMEBody);
+                case 'mail':
+                    return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
+                default:
+                    $sendMethod = $this->Mailer . 'Send';
+                    if (method_exists($this, $sendMethod)) {
+                        return $this->{$sendMethod}($this->MIMEHeader, $this->MIMEBody);
+                    }
+
+                    return $this->mailSend($this->MIMEHeader, $this->MIMEBody);
+            }
+        } catch (Exception $exc) {
+            $this->setError($exc->getMessage());
+            $this->edebug($exc->getMessage());
+            if ($this->Mailer === 'smtp' && $this->SMTPKeepAlive == true && $this->smtp->connected()) {
+                $this->smtp->reset();
+            }
+            if ($this->exceptions) {
+                throw $exc;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Send mail using the $Sendmail program.
+     *
+     * @see PHPMailer::$Sendmail
+     *
+     * @param string $header The message headers
+     * @param string $body   The message body
+     *
+     * @throws Exception
+     *
+     * @return bool
+     */
+    protected function sendmailSend($header, $body)
+    {
+        if ($this->Mailer === 'qmail') {
+            $this->edebug('Sending with qmail');
+        } else {
+            $this->edebug('Sending with sendmail');
+        }
+        $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
+        //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
+        //A space after `-f` is optional, but there is a long history of its presence
+        //causing problems, so we don't use one
+        //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
+        //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html
+        //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html
+        //Example problem: https://www.drupal.org/node/1057954
+
+        //PHP 5.6 workaround
+        $sendmail_from_value = ini_get('sendmail_from');
+        if (empty($this->Sender) && !empty($sendmail_from_value)) {
+            //PHP config has a sender address we can use
+            $this->Sender = ini_get('sendmail_from');
+        }
+        //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
+        if (!empty($this->Sender) && static::validateAddress($this->Sender) && self::isShellSafe($this->Sender)) {
+            if ($this->Mailer === 'qmail') {
+                $sendmailFmt = '%s -f%s';
+            } else {
+                $sendmailFmt = '%s -oi -f%s -t';
+            }
+        } else {
+            //allow sendmail to choose a default envelope sender. It may
+            //seem preferable to force it to use the From header as with
+            //SMTP, but that introduces new problems (see
+            //<https://github.com/PHPMailer/PHPMailer/issues/2298>), and
+            //it has historically worked this way.
+            $sendmailFmt = '%s -oi -t';
+        }
+
+        $sendmail = sprintf($sendmailFmt, escapeshellcmd($this->Sendmail), $this->Sender);
+        $this->edebug('Sendmail path: ' . $this->Sendmail);
+        $this->edebug('Sendmail command: ' . $sendmail);
+        $this->edebug('Envelope sender: ' . $this->Sender);
+        $this->edebug("Headers: {$header}");
+
+        if ($this->SingleTo) {
+            foreach ($this->SingleToArray as $toAddr) {
+                $mail = @popen($sendmail, 'w');
+                if (!$mail) {
+                    throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
+                }
+                $this->edebug("To: {$toAddr}");
+                fwrite($mail, 'To: ' . $toAddr . "\n");
+                fwrite($mail, $header);
+                fwrite($mail, $body);
+                $result = pclose($mail);
+                $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet);
+                $this->doCallback(
+                    ($result === 0),
+                    [[$addrinfo['address'], $addrinfo['name']]],
+                    $this->cc,
+                    $this->bcc,
+                    $this->Subject,
+                    $body,
+                    $this->From,
+                    []
+                );
+                $this->edebug("Result: " . ($result === 0 ? 'true' : 'false'));
+                if (0 !== $result) {
+                    throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
+                }
+            }
+        } else {
+            $mail = @popen($sendmail, 'w');
+            if (!$mail) {
+                throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
+            }
+            fwrite($mail, $header);
+            fwrite($mail, $body);
+            $result = pclose($mail);
+            $this->doCallback(
+                ($result === 0),
+                $this->to,
+                $this->cc,
+                $this->bcc,
+                $this->Subject,
+                $body,
+                $this->From,
+                []
+            );
+            $this->edebug("Result: " . ($result === 0 ? 'true' : 'false'));
+            if (0 !== $result) {
+                throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL);
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters.
+     * Note that escapeshellarg and escapeshellcmd are inadequate for our purposes, especially on Windows.
+     *
+     * @see https://github.com/PHPMailer/PHPMailer/issues/924 CVE-2016-10045 bug report
+     *
+     * @param string $string The string to be validated
+     *
+     * @return bool
+     */
+    protected static function isShellSafe($string)
+    {
+        //It's not possible to use shell commands safely (which includes the mail() function) without escapeshellarg,
+        //but some hosting providers disable it, creating a security problem that we don't want to have to deal with,
+        //so we don't.
+        if (!function_exists('escapeshellarg') || !function_exists('escapeshellcmd')) {
+            return false;
+        }
+
+        if (
+            escapeshellcmd($string) !== $string
+            || !in_array(escapeshellarg($string), ["'$string'", "\"$string\""])
+        ) {
+            return false;
+        }
+
+        $length = strlen($string);
+
+        for ($i = 0; $i < $length; ++$i) {
+            $c = $string[$i];
+
+            //All other characters have a special meaning in at least one common shell, including = and +.
+            //Full stop (.) has a special meaning in cmd.exe, but its impact should be negligible here.
+            //Note that this does permit non-Latin alphanumeric characters based on the current locale.
+            if (!ctype_alnum($c) && strpos('@_-.', $c) === false) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Check whether a file path is of a permitted type.
+     * Used to reject URLs and phar files from functions that access local file paths,
+     * such as addAttachment.
+     *
+     * @param string $path A relative or absolute path to a file
+     *
+     * @return bool
+     */
+    protected static function isPermittedPath($path)
+    {
+        //Matches scheme definition from https://tools.ietf.org/html/rfc3986#section-3.1
+        return !preg_match('#^[a-z][a-z\d+.-]*://#i', $path);
+    }
+
+    /**
+     * Check whether a file path is safe, accessible, and readable.
+     *
+     * @param string $path A relative or absolute path to a file
+     *
+     * @return bool
+     */
+    protected static function fileIsAccessible($path)
+    {
+        if (!static::isPermittedPath($path)) {
+            return false;
+        }
+        $readable = is_file($path);
+        //If not a UNC path (expected to start with \\), check read permission, see #2069
+        if (strpos($path, '\\\\') !== 0) {
+            $readable = $readable && is_readable($path);
+        }
+        return  $readable;
+    }
+
+    /**
+     * Send mail using the PHP mail() function.
+     *
+     * @see http://www.php.net/manual/en/book.mail.php
+     *
+     * @param string $header The message headers
+     * @param string $body   The message body
+     *
+     * @throws Exception
+     *
+     * @return bool
+     */
+    protected function mailSend($header, $body)
+    {
+        $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
+
+        $toArr = [];
+        foreach ($this->to as $toaddr) {
+            $toArr[] = $this->addrFormat($toaddr);
+        }
+        $to = trim(implode(', ', $toArr));
+
+        //If there are no To-addresses (e.g. when sending only to BCC-addresses)
+        //the following should be added to get a correct DKIM-signature.
+        //Compare with $this->preSend()
+        if ($to === '') {
+            $to = 'undisclosed-recipients:;';
+        }
+
+        $params = null;
+        //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
+        //A space after `-f` is optional, but there is a long history of its presence
+        //causing problems, so we don't use one
+        //Exim docs: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-the_exim_command_line.html
+        //Sendmail docs: http://www.sendmail.org/~ca/email/man/sendmail.html
+        //Qmail docs: http://www.qmail.org/man/man8/qmail-inject.html
+        //Example problem: https://www.drupal.org/node/1057954
+        //CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
+
+        //PHP 5.6 workaround
+        $sendmail_from_value = ini_get('sendmail_from');
+        if (empty($this->Sender) && !empty($sendmail_from_value)) {
+            //PHP config has a sender address we can use
+            $this->Sender = ini_get('sendmail_from');
+        }
+        if (!empty($this->Sender) && static::validateAddress($this->Sender)) {
+            if (self::isShellSafe($this->Sender)) {
+                $params = sprintf('-f%s', $this->Sender);
+            }
+            $old_from = ini_get('sendmail_from');
+            ini_set('sendmail_from', $this->Sender);
+        }
+        $result = false;
+        if ($this->SingleTo && count($toArr) > 1) {
+            foreach ($toArr as $toAddr) {
+                $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params);
+                $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet);
+                $this->doCallback(
+                    $result,
+                    [[$addrinfo['address'], $addrinfo['name']]],
+                    $this->cc,
+                    $this->bcc,
+                    $this->Subject,
+                    $body,
+                    $this->From,
+                    []
+                );
+            }
+        } else {
+            $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params);
+            $this->doCallback($result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From, []);
+        }
+        if (isset($old_from)) {
+            ini_set('sendmail_from', $old_from);
+        }
+        if (!$result) {
+            throw new Exception($this->lang('instantiate'), self::STOP_CRITICAL);
+        }
+
+        return true;
+    }
+
+    /**
+     * Get an instance to use for SMTP operations.
+     * Override this function to load your own SMTP implementation,
+     * or set one with setSMTPInstance.
+     *
+     * @return SMTP
+     */
+    public function getSMTPInstance()
+    {
+        if (!is_object($this->smtp)) {
+            $this->smtp = new SMTP();
+        }
+
+        return $this->smtp;
+    }
+
+    /**
+     * Provide an instance to use for SMTP operations.
+     *
+     * @return SMTP
+     */
+    public function setSMTPInstance(SMTP $smtp)
+    {
+        $this->smtp = $smtp;
+
+        return $this->smtp;
+    }
+
+    /**
+     * Send mail via SMTP.
+     * Returns false if there is a bad MAIL FROM, RCPT, or DATA input.
+     *
+     * @see PHPMailer::setSMTPInstance() to use a different class.
+     *
+     * @uses \PHPMailer\PHPMailer\SMTP
+     *
+     * @param string $header The message headers
+     * @param string $body   The message body
+     *
+     * @throws Exception
+     *
+     * @return bool
+     */
+    protected function smtpSend($header, $body)
+    {
+        $header = static::stripTrailingWSP($header) . static::$LE . static::$LE;
+        $bad_rcpt = [];
+        if (!$this->smtpConnect($this->SMTPOptions)) {
+            throw new Exception($this->lang('smtp_connect_failed'), self::STOP_CRITICAL);
+        }
+        //Sender already validated in preSend()
+        if ('' === $this->Sender) {
+            $smtp_from = $this->From;
+        } else {
+            $smtp_from = $this->Sender;
+        }
+        if (!$this->smtp->mail($smtp_from)) {
+            $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError()));
+            throw new Exception($this->ErrorInfo, self::STOP_CRITICAL);
+        }
+
+        $callbacks = [];
+        //Attempt to send to all recipients
+        foreach ([$this->to, $this->cc, $this->bcc] as $togroup) {
+            foreach ($togroup as $to) {
+                if (!$this->smtp->recipient($to[0], $this->dsn)) {
+                    $error = $this->smtp->getError();
+                    $bad_rcpt[] = ['to' => $to[0], 'error' => $error['detail']];
+                    $isSent = false;
+                } else {
+                    $isSent = true;
+                }
+
+                $callbacks[] = ['issent' => $isSent, 'to' => $to[0], 'name' => $to[1]];
+            }
+        }
+
+        //Only send the DATA command if we have viable recipients
+        if ((count($this->all_recipients) > count($bad_rcpt)) && !$this->smtp->data($header . $body)) {
+            throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL);
+        }
+
+        $smtp_transaction_id = $this->smtp->getLastTransactionID();
+
+        if ($this->SMTPKeepAlive) {
+            $this->smtp->reset();
+        } else {
+            $this->smtp->quit();
+            $this->smtp->close();
+        }
+
+        foreach ($callbacks as $cb) {
+            $this->doCallback(
+                $cb['issent'],
+                [[$cb['to'], $cb['name']]],
+                [],
+                [],
+                $this->Subject,
+                $body,
+                $this->From,
+                ['smtp_transaction_id' => $smtp_transaction_id]
+            );
+        }
+
+        //Create error message for any bad addresses
+        if (count($bad_rcpt) > 0) {
+            $errstr = '';
+            foreach ($bad_rcpt as $bad) {
+                $errstr .= $bad['to'] . ': ' . $bad['error'];
+            }
+            throw new Exception($this->lang('recipients_failed') . $errstr, self::STOP_CONTINUE);
+        }
+
+        return true;
+    }
+
+    /**
+     * Initiate a connection to an SMTP server.
+     * Returns false if the operation failed.
+     *
+     * @param array $options An array of options compatible with stream_context_create()
+     *
+     * @throws Exception
+     *
+     * @uses \PHPMailer\PHPMailer\SMTP
+     *
+     * @return bool
+     */
+    public function smtpConnect($options = null)
+    {
+        if (null === $this->smtp) {
+            $this->smtp = $this->getSMTPInstance();
+        }
+
+        //If no options are provided, use whatever is set in the instance
+        if (null === $options) {
+            $options = $this->SMTPOptions;
+        }
+
+        //Already connected?
+        if ($this->smtp->connected()) {
+            return true;
+        }
+
+        $this->smtp->setTimeout($this->Timeout);
+        $this->smtp->setDebugLevel($this->SMTPDebug);
+        $this->smtp->setDebugOutput($this->Debugoutput);
+        $this->smtp->setVerp($this->do_verp);
+        if ($this->Host === null) {
+            $this->Host = 'localhost';
+        }
+        $hosts = explode(';', $this->Host);
+        $lastexception = null;
+
+        foreach ($hosts as $hostentry) {
+            $hostinfo = [];
+            if (
+                !preg_match(
+                    '/^(?:(ssl|tls):\/\/)?(.+?)(?::(\d+))?$/',
+                    trim($hostentry),
+                    $hostinfo
+                )
+            ) {
+                $this->edebug($this->lang('invalid_hostentry') . ' ' . trim($hostentry));
+                //Not a valid host entry
+                continue;
+            }
+            //$hostinfo[1]: optional ssl or tls prefix
+            //$hostinfo[2]: the hostname
+            //$hostinfo[3]: optional port number
+            //The host string prefix can temporarily override the current setting for SMTPSecure
+            //If it's not specified, the default value is used
+
+            //Check the host name is a valid name or IP address before trying to use it
+            if (!static::isValidHost($hostinfo[2])) {
+                $this->edebug($this->lang('invalid_host') . ' ' . $hostinfo[2]);
+                continue;
+            }
+            $prefix = '';
+            $secure = $this->SMTPSecure;
+            $tls = (static::ENCRYPTION_STARTTLS === $this->SMTPSecure);
+            if ('ssl' === $hostinfo[1] || ('' === $hostinfo[1] && static::ENCRYPTION_SMTPS === $this->SMTPSecure)) {
+                $prefix = 'ssl://';
+                $tls = false; //Can't have SSL and TLS at the same time
+                $secure = static::ENCRYPTION_SMTPS;
+            } elseif ('tls' === $hostinfo[1]) {
+                $tls = true;
+                //TLS doesn't use a prefix
+                $secure = static::ENCRYPTION_STARTTLS;
+            }
+            //Do we need the OpenSSL extension?
+            $sslext = defined('OPENSSL_ALGO_SHA256');
+            if (static::ENCRYPTION_STARTTLS === $secure || static::ENCRYPTION_SMTPS === $secure) {
+                //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled
+                if (!$sslext) {
+                    throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL);
+                }
+            }
+            $host = $hostinfo[2];
+            $port = $this->Port;
+            if (
+                array_key_exists(3, $hostinfo) &&
+                is_numeric($hostinfo[3]) &&
+                $hostinfo[3] > 0 &&
+                $hostinfo[3] < 65536
+            ) {
+                $port = (int) $hostinfo[3];
+            }
+            if ($this->smtp->connect($prefix . $host, $port, $this->Timeout, $options)) {
+                try {
+                    if ($this->Helo) {
+                        $hello = $this->Helo;
+                    } else {
+                        $hello = $this->serverHostname();
+                    }
+                    $this->smtp->hello($hello);
+                    //Automatically enable TLS encryption if:
+                    //* it's not disabled
+                    //* we have openssl extension
+                    //* we are not already using SSL
+                    //* the server offers STARTTLS
+                    if ($this->SMTPAutoTLS && $sslext && 'ssl' !== $secure && $this->smtp->getServerExt('STARTTLS')) {
+                        $tls = true;
+                    }
+                    if ($tls) {
+                        if (!$this->smtp->startTLS()) {
+                            $message = $this->getSmtpErrorMessage('connect_host');
+                            throw new Exception($message);
+                        }
+                        //We must resend EHLO after TLS negotiation
+                        $this->smtp->hello($hello);
+                    }
+                    if (
+                        $this->SMTPAuth && !$this->smtp->authenticate(
+                            $this->Username,
+                            $this->Password,
+                            $this->AuthType,
+                            $this->oauth
+                        )
+                    ) {
+                        throw new Exception($this->lang('authenticate'));
+                    }
+
+                    return true;
+                } catch (Exception $exc) {
+                    $lastexception = $exc;
+                    $this->edebug($exc->getMessage());
+                    //We must have connected, but then failed TLS or Auth, so close connection nicely
+                    $this->smtp->quit();
+                }
+            }
+        }
+        //If we get here, all connection attempts have failed, so close connection hard
+        $this->smtp->close();
+        //As we've caught all exceptions, just report whatever the last one was
+        if ($this->exceptions && null !== $lastexception) {
+            throw $lastexception;
+        }
+        if ($this->exceptions) {
+            // no exception was thrown, likely $this->smtp->connect() failed
+            $message = $this->getSmtpErrorMessage('connect_host');
+            throw new Exception($message);
+        }
+
+        return false;
+    }
+
+    /**
+     * Close the active SMTP session if one exists.
+     */
+    public function smtpClose()
+    {
+        if ((null !== $this->smtp) && $this->smtp->connected()) {
+            $this->smtp->quit();
+            $this->smtp->close();
+        }
+    }
+
+    /**
+     * Set the language for error messages.
+     * The default language is English.
+     *
+     * @param string $langcode  ISO 639-1 2-character language code (e.g. French is "fr")
+     *                          Optionally, the language code can be enhanced with a 4-character
+     *                          script annotation and/or a 2-character country annotation.
+     * @param string $lang_path Path to the language file directory, with trailing separator (slash)
+     *                          Do not set this from user input!
+     *
+     * @return bool Returns true if the requested language was loaded, false otherwise.
+     */
+    public function setLanguage($langcode = 'en', $lang_path = '')
+    {
+        //Backwards compatibility for renamed language codes
+        $renamed_langcodes = [
+            'br' => 'pt_br',
+            'cz' => 'cs',
+            'dk' => 'da',
+            'no' => 'nb',
+            'se' => 'sv',
+            'rs' => 'sr',
+            'tg' => 'tl',
+            'am' => 'hy',
+        ];
+
+        if (array_key_exists($langcode, $renamed_langcodes)) {
+            $langcode = $renamed_langcodes[$langcode];
+        }
+
+        //Define full set of translatable strings in English
+        $PHPMAILER_LANG = [
+            'authenticate' => 'SMTP Error: Could not authenticate.',
+            'buggy_php' => 'Your version of PHP is affected by a bug that may result in corrupted messages.' .
+                ' To fix it, switch to sending using SMTP, disable the mail.add_x_header option in' .
+                ' your php.ini, switch to MacOS or Linux, or upgrade your PHP to version 7.0.17+ or 7.1.3+.',
+            'connect_host' => 'SMTP Error: Could not connect to SMTP host.',
+            'data_not_accepted' => 'SMTP Error: data not accepted.',
+            'empty_message' => 'Message body empty',
+            'encoding' => 'Unknown encoding: ',
+            'execute' => 'Could not execute: ',
+            'extension_missing' => 'Extension missing: ',
+            'file_access' => 'Could not access file: ',
+            'file_open' => 'File Error: Could not open file: ',
+            'from_failed' => 'The following From address failed: ',
+            'instantiate' => 'Could not instantiate mail function.',
+            'invalid_address' => 'Invalid address: ',
+            'invalid_header' => 'Invalid header name or value',
+            'invalid_hostentry' => 'Invalid hostentry: ',
+            'invalid_host' => 'Invalid host: ',
+            'mailer_not_supported' => ' mailer is not supported.',
+            'provide_address' => 'You must provide at least one recipient email address.',
+            'recipients_failed' => 'SMTP Error: The following recipients failed: ',
+            'signing' => 'Signing Error: ',
+            'smtp_code' => 'SMTP code: ',
+            'smtp_code_ex' => 'Additional SMTP info: ',
+            'smtp_connect_failed' => 'SMTP connect() failed.',
+            'smtp_detail' => 'Detail: ',
+            'smtp_error' => 'SMTP server error: ',
+            'variable_set' => 'Cannot set or reset variable: ',
+        ];
+        if (empty($lang_path)) {
+            //Calculate an absolute path so it can work if CWD is not here
+            $lang_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR;
+        }
+
+        //Validate $langcode
+        $foundlang = true;
+        $langcode  = strtolower($langcode);
+        if (
+            !preg_match('/^(?P<lang>[a-z]{2})(?P<script>_[a-z]{4})?(?P<country>_[a-z]{2})?$/', $langcode, $matches)
+            && $langcode !== 'en'
+        ) {
+            $foundlang = false;
+            $langcode = 'en';
+        }
+
+        //There is no English translation file
+        if ('en' !== $langcode) {
+            $langcodes = [];
+            if (!empty($matches['script']) && !empty($matches['country'])) {
+                $langcodes[] = $matches['lang'] . $matches['script'] . $matches['country'];
+            }
+            if (!empty($matches['country'])) {
+                $langcodes[] = $matches['lang'] . $matches['country'];
+            }
+            if (!empty($matches['script'])) {
+                $langcodes[] = $matches['lang'] . $matches['script'];
+            }
+            $langcodes[] = $matches['lang'];
+
+            //Try and find a readable language file for the requested language.
+            $foundFile = false;
+            foreach ($langcodes as $code) {
+                $lang_file = $lang_path . 'phpmailer.lang-' . $code . '.php';
+                if (static::fileIsAccessible($lang_file)) {
+                    $foundFile = true;
+                    break;
+                }
+            }
+
+            if ($foundFile === false) {
+                $foundlang = false;
+            } else {
+                $lines = file($lang_file);
+                foreach ($lines as $line) {
+                    //Translation file lines look like this:
+                    //$PHPMAILER_LANG['authenticate'] = 'SMTP-Fehler: Authentifizierung fehlgeschlagen.';
+                    //These files are parsed as text and not PHP so as to avoid the possibility of code injection
+                    //See https://blog.stevenlevithan.com/archives/match-quoted-string
+                    $matches = [];
+                    if (
+                        preg_match(
+                            '/^\$PHPMAILER_LANG\[\'([a-z\d_]+)\'\]\s*=\s*(["\'])(.+)*?\2;/',
+                            $line,
+                            $matches
+                        ) &&
+                        //Ignore unknown translation keys
+                        array_key_exists($matches[1], $PHPMAILER_LANG)
+                    ) {
+                        //Overwrite language-specific strings so we'll never have missing translation keys.
+                        $PHPMAILER_LANG[$matches[1]] = (string)$matches[3];
+                    }
+                }
+            }
+        }
+        $this->language = $PHPMAILER_LANG;
+
+        return $foundlang; //Returns false if language not found
+    }
+
+    /**
+     * Get the array of strings for the current language.
+     *
+     * @return array
+     */
+    public function getTranslations()
+    {
+        if (empty($this->language)) {
+            $this->setLanguage(); // Set the default language.
+        }
+
+        return $this->language;
+    }
+
+    /**
+     * Create recipient headers.
+     *
+     * @param string $type
+     * @param array  $addr An array of recipients,
+     *                     where each recipient is a 2-element indexed array with element 0 containing an address
+     *                     and element 1 containing a name, like:
+     *                     [['joe@example.com', 'Joe User'], ['zoe@example.com', 'Zoe User']]
+     *
+     * @return string
+     */
+    public function addrAppend($type, $addr)
+    {
+        $addresses = [];
+        foreach ($addr as $address) {
+            $addresses[] = $this->addrFormat($address);
+        }
+
+        return $type . ': ' . implode(', ', $addresses) . static::$LE;
+    }
+
+    /**
+     * Format an address for use in a message header.
+     *
+     * @param array $addr A 2-element indexed array, element 0 containing an address, element 1 containing a name like
+     *                    ['joe@example.com', 'Joe User']
+     *
+     * @return string
+     */
+    public function addrFormat($addr)
+    {
+        if (!isset($addr[1]) || ($addr[1] === '')) { //No name provided
+            return $this->secureHeader($addr[0]);
+        }
+
+        return $this->encodeHeader($this->secureHeader($addr[1]), 'phrase') .
+            ' <' . $this->secureHeader($addr[0]) . '>';
+    }
+
+    /**
+     * Word-wrap message.
+     * For use with mailers that do not automatically perform wrapping
+     * and for quoted-printable encoded messages.
+     * Original written by philippe.
+     *
+     * @param string $message The message to wrap
+     * @param int    $length  The line length to wrap to
+     * @param bool   $qp_mode Whether to run in Quoted-Printable mode
+     *
+     * @return string
+     */
+    public function wrapText($message, $length, $qp_mode = false)
+    {
+        if ($qp_mode) {
+            $soft_break = sprintf(' =%s', static::$LE);
+        } else {
+            $soft_break = static::$LE;
+        }
+        //If utf-8 encoding is used, we will need to make sure we don't
+        //split multibyte characters when we wrap
+        $is_utf8 = static::CHARSET_UTF8 === strtolower($this->CharSet);
+        $lelen = strlen(static::$LE);
+        $crlflen = strlen(static::$LE);
+
+        $message = static::normalizeBreaks($message);
+        //Remove a trailing line break
+        if (substr($message, -$lelen) === static::$LE) {
+            $message = substr($message, 0, -$lelen);
+        }
+
+        //Split message into lines
+        $lines = explode(static::$LE, $message);
+        //Message will be rebuilt in here
+        $message = '';
+        foreach ($lines as $line) {
+            $words = explode(' ', $line);
+            $buf = '';
+            $firstword = true;
+            foreach ($words as $word) {
+                if ($qp_mode && (strlen($word) > $length)) {
+                    $space_left = $length - strlen($buf) - $crlflen;
+                    if (!$firstword) {
+                        if ($space_left > 20) {
+                            $len = $space_left;
+                            if ($is_utf8) {
+                                $len = $this->utf8CharBoundary($word, $len);
+                            } elseif ('=' === substr($word, $len - 1, 1)) {
+                                --$len;
+                            } elseif ('=' === substr($word, $len - 2, 1)) {
+                                $len -= 2;
+                            }
+                            $part = substr($word, 0, $len);
+                            $word = substr($word, $len);
+                            $buf .= ' ' . $part;
+                            $message .= $buf . sprintf('=%s', static::$LE);
+                        } else {
+                            $message .= $buf . $soft_break;
+                        }
+                        $buf = '';
+                    }
+                    while ($word !== '') {
+                        if ($length <= 0) {
+                            break;
+                        }
+                        $len = $length;
+                        if ($is_utf8) {
+                            $len = $this->utf8CharBoundary($word, $len);
+                        } elseif ('=' === substr($word, $len - 1, 1)) {
+                            --$len;
+                        } elseif ('=' === substr($word, $len - 2, 1)) {
+                            $len -= 2;
+                        }
+                        $part = substr($word, 0, $len);
+                        $word = (string) substr($word, $len);
+
+                        if ($word !== '') {
+                            $message .= $part . sprintf('=%s', static::$LE);
+                        } else {
+                            $buf = $part;
+                        }
+                    }
+                } else {
+                    $buf_o = $buf;
+                    if (!$firstword) {
+                        $buf .= ' ';
+                    }
+                    $buf .= $word;
+
+                    if ('' !== $buf_o && strlen($buf) > $length) {
+                        $message .= $buf_o . $soft_break;
+                        $buf = $word;
+                    }
+                }
+                $firstword = false;
+            }
+            $message .= $buf . static::$LE;
+        }
+
+        return $message;
+    }
+
+    /**
+     * Find the last character boundary prior to $maxLength in a utf-8
+     * quoted-printable encoded string.
+     * Original written by Colin Brown.
+     *
+     * @param string $encodedText utf-8 QP text
+     * @param int    $maxLength   Find the last character boundary prior to this length
+     *
+     * @return int
+     */
+    public function utf8CharBoundary($encodedText, $maxLength)
+    {
+        $foundSplitPos = false;
+        $lookBack = 3;
+        while (!$foundSplitPos) {
+            $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack);
+            $encodedCharPos = strpos($lastChunk, '=');
+            if (false !== $encodedCharPos) {
+                //Found start of encoded character byte within $lookBack block.
+                //Check the encoded byte value (the 2 chars after the '=')
+                $hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2);
+                $dec = hexdec($hex);
+                if ($dec < 128) {
+                    //Single byte character.
+                    //If the encoded char was found at pos 0, it will fit
+                    //otherwise reduce maxLength to start of the encoded char
+                    if ($encodedCharPos > 0) {
+                        $maxLength -= $lookBack - $encodedCharPos;
+                    }
+                    $foundSplitPos = true;
+                } elseif ($dec >= 192) {
+                    //First byte of a multi byte character
+                    //Reduce maxLength to split at start of character
+                    $maxLength -= $lookBack - $encodedCharPos;
+                    $foundSplitPos = true;
+                } elseif ($dec < 192) {
+                    //Middle byte of a multi byte character, look further back
+                    $lookBack += 3;
+                }
+            } else {
+                //No encoded character found
+                $foundSplitPos = true;
+            }
+        }
+
+        return $maxLength;
+    }
+
+    /**
+     * Apply word wrapping to the message body.
+     * Wraps the message body to the number of chars set in the WordWrap property.
+     * You should only do this to plain-text bodies as wrapping HTML tags may break them.
+     * This is called automatically by createBody(), so you don't need to call it yourself.
+     */
+    public function setWordWrap()
+    {
+        if ($this->WordWrap < 1) {
+            return;
+        }
+
+        switch ($this->message_type) {
+            case 'alt':
+            case 'alt_inline':
+            case 'alt_attach':
+            case 'alt_inline_attach':
+                $this->AltBody = $this->wrapText($this->AltBody, $this->WordWrap);
+                break;
+            default:
+                $this->Body = $this->wrapText($this->Body, $this->WordWrap);
+                break;
+        }
+    }
+
+    /**
+     * Assemble message headers.
+     *
+     * @return string The assembled headers
+     */
+    public function createHeader()
+    {
+        $result = '';
+
+        $result .= $this->headerLine('Date', '' === $this->MessageDate ? self::rfcDate() : $this->MessageDate);
+
+        //The To header is created automatically by mail(), so needs to be omitted here
+        if ('mail' !== $this->Mailer) {
+            if ($this->SingleTo) {
+                foreach ($this->to as $toaddr) {
+                    $this->SingleToArray[] = $this->addrFormat($toaddr);
+                }
+            } elseif (count($this->to) > 0) {
+                $result .= $this->addrAppend('To', $this->to);
+            } elseif (count($this->cc) === 0) {
+                $result .= $this->headerLine('To', 'undisclosed-recipients:;');
+            }
+        }
+        $result .= $this->addrAppend('From', [[trim($this->From), $this->FromName]]);
+
+        //sendmail and mail() extract Cc from the header before sending
+        if (count($this->cc) > 0) {
+            $result .= $this->addrAppend('Cc', $this->cc);
+        }
+
+        //sendmail and mail() extract Bcc from the header before sending
+        if (
+            (
+                'sendmail' === $this->Mailer || 'qmail' === $this->Mailer || 'mail' === $this->Mailer
+            )
+            && count($this->bcc) > 0
+        ) {
+            $result .= $this->addrAppend('Bcc', $this->bcc);
+        }
+
+        if (count($this->ReplyTo) > 0) {
+            $result .= $this->addrAppend('Reply-To', $this->ReplyTo);
+        }
+
+        //mail() sets the subject itself
+        if ('mail' !== $this->Mailer) {
+            $result .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->Subject)));
+        }
+
+        //Only allow a custom message ID if it conforms to RFC 5322 section 3.6.4
+        //https://tools.ietf.org/html/rfc5322#section-3.6.4
+        if (
+            '' !== $this->MessageID &&
+            preg_match(
+                '/^<((([a-z\d!#$%&\'*+\/=?^_`{|}~-]+(\.[a-z\d!#$%&\'*+\/=?^_`{|}~-]+)*)' .
+                '|("(([\x01-\x08\x0B\x0C\x0E-\x1F\x7F]|[\x21\x23-\x5B\x5D-\x7E])' .
+                '|(\\[\x01-\x09\x0B\x0C\x0E-\x7F]))*"))@(([a-z\d!#$%&\'*+\/=?^_`{|}~-]+' .
+                '(\.[a-z\d!#$%&\'*+\/=?^_`{|}~-]+)*)|(\[(([\x01-\x08\x0B\x0C\x0E-\x1F\x7F]' .
+                '|[\x21-\x5A\x5E-\x7E])|(\\[\x01-\x09\x0B\x0C\x0E-\x7F]))*\])))>$/Di',
+                $this->MessageID
+            )
+        ) {
+            $this->lastMessageID = $this->MessageID;
+        } else {
+            $this->lastMessageID = sprintf('<%s@%s>', $this->uniqueid, $this->serverHostname());
+        }
+        $result .= $this->headerLine('Message-ID', $this->lastMessageID);
+        if (null !== $this->Priority) {
+            $result .= $this->headerLine('X-Priority', $this->Priority);
+        }
+        if ('' === $this->XMailer) {
+            //Empty string for default X-Mailer header
+            $result .= $this->headerLine(
+                'X-Mailer',
+                'PHPMailer ' . self::VERSION . ' (https://github.com/PHPMailer/PHPMailer)'
+            );
+        } elseif (is_string($this->XMailer) && trim($this->XMailer) !== '') {
+            //Some string
+            $result .= $this->headerLine('X-Mailer', trim($this->XMailer));
+        } //Other values result in no X-Mailer header
+
+        if ('' !== $this->ConfirmReadingTo) {
+            $result .= $this->headerLine('Disposition-Notification-To', '<' . $this->ConfirmReadingTo . '>');
+        }
+
+        //Add custom headers
+        foreach ($this->CustomHeader as $header) {
+            $result .= $this->headerLine(
+                trim($header[0]),
+                $this->encodeHeader(trim($header[1]))
+            );
+        }
+        if (!$this->sign_key_file) {
+            $result .= $this->headerLine('MIME-Version', '1.0');
+            $result .= $this->getMailMIME();
+        }
+
+        return $result;
+    }
+
+    /**
+     * Get the message MIME type headers.
+     *
+     * @return string
+     */
+    public function getMailMIME()
+    {
+        $result = '';
+        $ismultipart = true;
+        switch ($this->message_type) {
+            case 'inline':
+                $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
+                $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"');
+                break;
+            case 'attach':
+            case 'inline_attach':
+            case 'alt_attach':
+            case 'alt_inline_attach':
+                $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_MIXED . ';');
+                $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"');
+                break;
+            case 'alt':
+            case 'alt_inline':
+                $result .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');
+                $result .= $this->textLine(' boundary="' . $this->boundary[1] . '"');
+                break;
+            default:
+                //Catches case 'plain': and case '':
+                $result .= $this->textLine('Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet);
+                $ismultipart = false;
+                break;
+        }
+        //RFC1341 part 5 says 7bit is assumed if not specified
+        if (static::ENCODING_7BIT !== $this->Encoding) {
+            //RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE
+            if ($ismultipart) {
+                if (static::ENCODING_8BIT === $this->Encoding) {
+                    $result .= $this->headerLine('Content-Transfer-Encoding', static::ENCODING_8BIT);
+                }
+                //The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible
+            } else {
+                $result .= $this->headerLine('Content-Transfer-Encoding', $this->Encoding);
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Returns the whole MIME message.
+     * Includes complete headers and body.
+     * Only valid post preSend().
+     *
+     * @see PHPMailer::preSend()
+     *
+     * @return string
+     */
+    public function getSentMIMEMessage()
+    {
+        return static::stripTrailingWSP($this->MIMEHeader . $this->mailHeader) .
+            static::$LE . static::$LE . $this->MIMEBody;
+    }
+
+    /**
+     * Create a unique ID to use for boundaries.
+     *
+     * @return string
+     */
+    protected function generateId()
+    {
+        $len = 32; //32 bytes = 256 bits
+        $bytes = '';
+        if (function_exists('random_bytes')) {
+            try {
+                $bytes = random_bytes($len);
+            } catch (\Exception $e) {
+                //Do nothing
+            }
+        } elseif (function_exists('openssl_random_pseudo_bytes')) {
+            /** @noinspection CryptographicallySecureRandomnessInspection */
+            $bytes = openssl_random_pseudo_bytes($len);
+        }
+        if ($bytes === '') {
+            //We failed to produce a proper random string, so make do.
+            //Use a hash to force the length to the same as the other methods
+            $bytes = hash('sha256', uniqid((string) mt_rand(), true), true);
+        }
+
+        //We don't care about messing up base64 format here, just want a random string
+        return str_replace(['=', '+', '/'], '', base64_encode(hash('sha256', $bytes, true)));
+    }
+
+    /**
+     * Assemble the message body.
+     * Returns an empty string on failure.
+     *
+     * @throws Exception
+     *
+     * @return string The assembled message body
+     */
+    public function createBody()
+    {
+        $body = '';
+        //Create unique IDs and preset boundaries
+        $this->setBoundaries();
+
+        if ($this->sign_key_file) {
+            $body .= $this->getMailMIME() . static::$LE;
+        }
+
+        $this->setWordWrap();
+
+        $bodyEncoding = $this->Encoding;
+        $bodyCharSet = $this->CharSet;
+        //Can we do a 7-bit downgrade?
+        if (static::ENCODING_8BIT === $bodyEncoding && !$this->has8bitChars($this->Body)) {
+            $bodyEncoding = static::ENCODING_7BIT;
+            //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
+            $bodyCharSet = static::CHARSET_ASCII;
+        }
+        //If lines are too long, and we're not already using an encoding that will shorten them,
+        //change to quoted-printable transfer encoding for the body part only
+        if (static::ENCODING_BASE64 !== $this->Encoding && static::hasLineLongerThanMax($this->Body)) {
+            $bodyEncoding = static::ENCODING_QUOTED_PRINTABLE;
+        }
+
+        $altBodyEncoding = $this->Encoding;
+        $altBodyCharSet = $this->CharSet;
+        //Can we do a 7-bit downgrade?
+        if (static::ENCODING_8BIT === $altBodyEncoding && !$this->has8bitChars($this->AltBody)) {
+            $altBodyEncoding = static::ENCODING_7BIT;
+            //All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
+            $altBodyCharSet = static::CHARSET_ASCII;
+        }
+        //If lines are too long, and we're not already using an encoding that will shorten them,
+        //change to quoted-printable transfer encoding for the alt body part only
+        if (static::ENCODING_BASE64 !== $altBodyEncoding && static::hasLineLongerThanMax($this->AltBody)) {
+            $altBodyEncoding = static::ENCODING_QUOTED_PRINTABLE;
+        }
+        //Use this as a preamble in all multipart message types
+        $mimepre = '';
+        switch ($this->message_type) {
+            case 'inline':
+                $body .= $mimepre;
+                $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);
+                $body .= $this->encodeString($this->Body, $bodyEncoding);
+                $body .= static::$LE;
+                $body .= $this->attachAll('inline', $this->boundary[1]);
+                break;
+            case 'attach':
+                $body .= $mimepre;
+                $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding);
+                $body .= $this->encodeString($this->Body, $bodyEncoding);
+                $body .= static::$LE;
+                $body .= $this->attachAll('attachment', $this->boundary[1]);
+                break;
+            case 'inline_attach':
+                $body .= $mimepre;
+                $body .= $this->textLine('--' . $this->boundary[1]);
+                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
+                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '";');
+                $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"');
+                $body .= static::$LE;
+                $body .= $this->getBoundary($this->boundary[2], $bodyCharSet, '', $bodyEncoding);
+                $body .= $this->encodeString($this->Body, $bodyEncoding);
+                $body .= static::$LE;
+                $body .= $this->attachAll('inline', $this->boundary[2]);
+                $body .= static::$LE;
+                $body .= $this->attachAll('attachment', $this->boundary[1]);
+                break;
+            case 'alt':
+                $body .= $mimepre;
+                $body .= $this->getBoundary(
+                    $this->boundary[1],
+                    $altBodyCharSet,
+                    static::CONTENT_TYPE_PLAINTEXT,
+                    $altBodyEncoding
+                );
+                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
+                $body .= static::$LE;
+                $body .= $this->getBoundary(
+                    $this->boundary[1],
+                    $bodyCharSet,
+                    static::CONTENT_TYPE_TEXT_HTML,
+                    $bodyEncoding
+                );
+                $body .= $this->encodeString($this->Body, $bodyEncoding);
+                $body .= static::$LE;
+                if (!empty($this->Ical)) {
+                    $method = static::ICAL_METHOD_REQUEST;
+                    foreach (static::$IcalMethods as $imethod) {
+                        if (stripos($this->Ical, 'METHOD:' . $imethod) !== false) {
+                            $method = $imethod;
+                            break;
+                        }
+                    }
+                    $body .= $this->getBoundary(
+                        $this->boundary[1],
+                        '',
+                        static::CONTENT_TYPE_TEXT_CALENDAR . '; method=' . $method,
+                        ''
+                    );
+                    $body .= $this->encodeString($this->Ical, $this->Encoding);
+                    $body .= static::$LE;
+                }
+                $body .= $this->endBoundary($this->boundary[1]);
+                break;
+            case 'alt_inline':
+                $body .= $mimepre;
+                $body .= $this->getBoundary(
+                    $this->boundary[1],
+                    $altBodyCharSet,
+                    static::CONTENT_TYPE_PLAINTEXT,
+                    $altBodyEncoding
+                );
+                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
+                $body .= static::$LE;
+                $body .= $this->textLine('--' . $this->boundary[1]);
+                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
+                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '";');
+                $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"');
+                $body .= static::$LE;
+                $body .= $this->getBoundary(
+                    $this->boundary[2],
+                    $bodyCharSet,
+                    static::CONTENT_TYPE_TEXT_HTML,
+                    $bodyEncoding
+                );
+                $body .= $this->encodeString($this->Body, $bodyEncoding);
+                $body .= static::$LE;
+                $body .= $this->attachAll('inline', $this->boundary[2]);
+                $body .= static::$LE;
+                $body .= $this->endBoundary($this->boundary[1]);
+                break;
+            case 'alt_attach':
+                $body .= $mimepre;
+                $body .= $this->textLine('--' . $this->boundary[1]);
+                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');
+                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '"');
+                $body .= static::$LE;
+                $body .= $this->getBoundary(
+                    $this->boundary[2],
+                    $altBodyCharSet,
+                    static::CONTENT_TYPE_PLAINTEXT,
+                    $altBodyEncoding
+                );
+                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
+                $body .= static::$LE;
+                $body .= $this->getBoundary(
+                    $this->boundary[2],
+                    $bodyCharSet,
+                    static::CONTENT_TYPE_TEXT_HTML,
+                    $bodyEncoding
+                );
+                $body .= $this->encodeString($this->Body, $bodyEncoding);
+                $body .= static::$LE;
+                if (!empty($this->Ical)) {
+                    $method = static::ICAL_METHOD_REQUEST;
+                    foreach (static::$IcalMethods as $imethod) {
+                        if (stripos($this->Ical, 'METHOD:' . $imethod) !== false) {
+                            $method = $imethod;
+                            break;
+                        }
+                    }
+                    $body .= $this->getBoundary(
+                        $this->boundary[2],
+                        '',
+                        static::CONTENT_TYPE_TEXT_CALENDAR . '; method=' . $method,
+                        ''
+                    );
+                    $body .= $this->encodeString($this->Ical, $this->Encoding);
+                }
+                $body .= $this->endBoundary($this->boundary[2]);
+                $body .= static::$LE;
+                $body .= $this->attachAll('attachment', $this->boundary[1]);
+                break;
+            case 'alt_inline_attach':
+                $body .= $mimepre;
+                $body .= $this->textLine('--' . $this->boundary[1]);
+                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_ALTERNATIVE . ';');
+                $body .= $this->textLine(' boundary="' . $this->boundary[2] . '"');
+                $body .= static::$LE;
+                $body .= $this->getBoundary(
+                    $this->boundary[2],
+                    $altBodyCharSet,
+                    static::CONTENT_TYPE_PLAINTEXT,
+                    $altBodyEncoding
+                );
+                $body .= $this->encodeString($this->AltBody, $altBodyEncoding);
+                $body .= static::$LE;
+                $body .= $this->textLine('--' . $this->boundary[2]);
+                $body .= $this->headerLine('Content-Type', static::CONTENT_TYPE_MULTIPART_RELATED . ';');
+                $body .= $this->textLine(' boundary="' . $this->boundary[3] . '";');
+                $body .= $this->textLine(' type="' . static::CONTENT_TYPE_TEXT_HTML . '"');
+                $body .= static::$LE;
+                $body .= $this->getBoundary(
+                    $this->boundary[3],
+                    $bodyCharSet,
+                    static::CONTENT_TYPE_TEXT_HTML,
+                    $bodyEncoding
+                );
+                $body .= $this->encodeString($this->Body, $bodyEncoding);
+                $body .= static::$LE;
+                $body .= $this->attachAll('inline', $this->boundary[3]);
+                $body .= static::$LE;
+                $body .= $this->endBoundary($this->boundary[2]);
+                $body .= static::$LE;
+                $body .= $this->attachAll('attachment', $this->boundary[1]);
+                break;
+            default:
+                //Catch case 'plain' and case '', applies to simple `text/plain` and `text/html` body content types
+                //Reset the `Encoding` property in case we changed it for line length reasons
+                $this->Encoding = $bodyEncoding;
+                $body .= $this->encodeString($this->Body, $this->Encoding);
+                break;
+        }
+
+        if ($this->isError()) {
+            $body = '';
+            if ($this->exceptions) {
+                throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL);
+            }
+        } elseif ($this->sign_key_file) {
+            try {
+                if (!defined('PKCS7_TEXT')) {
+                    throw new Exception($this->lang('extension_missing') . 'openssl');
+                }
+
+                $file = tempnam(sys_get_temp_dir(), 'srcsign');
+                $signed = tempnam(sys_get_temp_dir(), 'mailsign');
+                file_put_contents($file, $body);
+
+                //Workaround for PHP bug https://bugs.php.net/bug.php?id=69197
+                if (empty($this->sign_extracerts_file)) {
+                    $sign = @openssl_pkcs7_sign(
+                        $file,
+                        $signed,
+                        'file://' . realpath($this->sign_cert_file),
+                        ['file://' . realpath($this->sign_key_file), $this->sign_key_pass],
+                        []
+                    );
+                } else {
+                    $sign = @openssl_pkcs7_sign(
+                        $file,
+                        $signed,
+                        'file://' . realpath($this->sign_cert_file),
+                        ['file://' . realpath($this->sign_key_file), $this->sign_key_pass],
+                        [],
+                        PKCS7_DETACHED,
+                        $this->sign_extracerts_file
+                    );
+                }
+
+                @unlink($file);
+                if ($sign) {
+                    $body = file_get_contents($signed);
+                    @unlink($signed);
+                    //The message returned by openssl contains both headers and body, so need to split them up
+                    $parts = explode("\n\n", $body, 2);
+                    $this->MIMEHeader .= $parts[0] . static::$LE . static::$LE;
+                    $body = $parts[1];
+                } else {
+                    @unlink($signed);
+                    throw new Exception($this->lang('signing') . openssl_error_string());
+                }
+            } catch (Exception $exc) {
+                $body = '';
+                if ($this->exceptions) {
+                    throw $exc;
+                }
+            }
+        }
+
+        return $body;
+    }
+
+    /**
+     * Get the boundaries that this message will use
+     * @return array
+     */
+    public function getBoundaries()
+    {
+        if (empty($this->boundary)) {
+            $this->setBoundaries();
+        }
+        return $this->boundary;
+    }
+
+    /**
+     * Return the start of a message boundary.
+     *
+     * @param string $boundary
+     * @param string $charSet
+     * @param string $contentType
+     * @param string $encoding
+     *
+     * @return string
+     */
+    protected function getBoundary($boundary, $charSet, $contentType, $encoding)
+    {
+        $result = '';
+        if ('' === $charSet) {
+            $charSet = $this->CharSet;
+        }
+        if ('' === $contentType) {
+            $contentType = $this->ContentType;
+        }
+        if ('' === $encoding) {
+            $encoding = $this->Encoding;
+        }
+        $result .= $this->textLine('--' . $boundary);
+        $result .= sprintf('Content-Type: %s; charset=%s', $contentType, $charSet);
+        $result .= static::$LE;
+        //RFC1341 part 5 says 7bit is assumed if not specified
+        if (static::ENCODING_7BIT !== $encoding) {
+            $result .= $this->headerLine('Content-Transfer-Encoding', $encoding);
+        }
+        $result .= static::$LE;
+
+        return $result;
+    }
+
+    /**
+     * Return the end of a message boundary.
+     *
+     * @param string $boundary
+     *
+     * @return string
+     */
+    protected function endBoundary($boundary)
+    {
+        return static::$LE . '--' . $boundary . '--' . static::$LE;
+    }
+
+    /**
+     * Set the message type.
+     * PHPMailer only supports some preset message types, not arbitrary MIME structures.
+     */
+    protected function setMessageType()
+    {
+        $type = [];
+        if ($this->alternativeExists()) {
+            $type[] = 'alt';
+        }
+        if ($this->inlineImageExists()) {
+            $type[] = 'inline';
+        }
+        if ($this->attachmentExists()) {
+            $type[] = 'attach';
+        }
+        $this->message_type = implode('_', $type);
+        if ('' === $this->message_type) {
+            //The 'plain' message_type refers to the message having a single body element, not that it is plain-text
+            $this->message_type = 'plain';
+        }
+    }
+
+    /**
+     * Format a header line.
+     *
+     * @param string     $name
+     * @param string|int $value
+     *
+     * @return string
+     */
+    public function headerLine($name, $value)
+    {
+        return $name . ': ' . $value . static::$LE;
+    }
+
+    /**
+     * Return a formatted mail line.
+     *
+     * @param string $value
+     *
+     * @return string
+     */
+    public function textLine($value)
+    {
+        return $value . static::$LE;
+    }
+
+    /**
+     * Add an attachment from a path on the filesystem.
+     * Never use a user-supplied path to a file!
+     * Returns false if the file could not be found or read.
+     * Explicitly *does not* support passing URLs; PHPMailer is not an HTTP client.
+     * If you need to do that, fetch the resource yourself and pass it in via a local file or string.
+     *
+     * @param string $path        Path to the attachment
+     * @param string $name        Overrides the attachment name
+     * @param string $encoding    File encoding (see $Encoding)
+     * @param string $type        MIME type, e.g. `image/jpeg`; determined automatically from $path if not specified
+     * @param string $disposition Disposition to use
+     *
+     * @throws Exception
+     *
+     * @return bool
+     */
+    public function addAttachment(
+        $path,
+        $name = '',
+        $encoding = self::ENCODING_BASE64,
+        $type = '',
+        $disposition = 'attachment'
+    ) {
+        try {
+            if (!static::fileIsAccessible($path)) {
+                throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);
+            }
+
+            //If a MIME type is not specified, try to work it out from the file name
+            if ('' === $type) {
+                $type = static::filenameToType($path);
+            }
+
+            $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME);
+            if ('' === $name) {
+                $name = $filename;
+            }
+            if (!$this->validateEncoding($encoding)) {
+                throw new Exception($this->lang('encoding') . $encoding);
+            }
+
+            $this->attachment[] = [
+                0 => $path,
+                1 => $filename,
+                2 => $name,
+                3 => $encoding,
+                4 => $type,
+                5 => false, //isStringAttachment
+                6 => $disposition,
+                7 => $name,
+            ];
+        } catch (Exception $exc) {
+            $this->setError($exc->getMessage());
+            $this->edebug($exc->getMessage());
+            if ($this->exceptions) {
+                throw $exc;
+            }
+
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Return the array of attachments.
+     *
+     * @return array
+     */
+    public function getAttachments()
+    {
+        return $this->attachment;
+    }
+
+    /**
+     * Attach all file, string, and binary attachments to the message.
+     * Returns an empty string on failure.
+     *
+     * @param string $disposition_type
+     * @param string $boundary
+     *
+     * @throws Exception
+     *
+     * @return string
+     */
+    protected function attachAll($disposition_type, $boundary)
+    {
+        //Return text of body
+        $mime = [];
+        $cidUniq = [];
+        $incl = [];
+
+        //Add all attachments
+        foreach ($this->attachment as $attachment) {
+            //Check if it is a valid disposition_filter
+            if ($attachment[6] === $disposition_type) {
+                //Check for string attachment
+                $string = '';
+                $path = '';
+                $bString = $attachment[5];
+                if ($bString) {
+                    $string = $attachment[0];
+                } else {
+                    $path = $attachment[0];
+                }
+
+                $inclhash = hash('sha256', serialize($attachment));
+                if (in_array($inclhash, $incl, true)) {
+                    continue;
+                }
+                $incl[] = $inclhash;
+                $name = $attachment[2];
+                $encoding = $attachment[3];
+                $type = $attachment[4];
+                $disposition = $attachment[6];
+                $cid = $attachment[7];
+                if ('inline' === $disposition && array_key_exists($cid, $cidUniq)) {
+                    continue;
+                }
+                $cidUniq[$cid] = true;
+
+                $mime[] = sprintf('--%s%s', $boundary, static::$LE);
+                //Only include a filename property if we have one
+                if (!empty($name)) {
+                    $mime[] = sprintf(
+                        'Content-Type: %s; name=%s%s',
+                        $type,
+                        static::quotedString($this->encodeHeader($this->secureHeader($name))),
+                        static::$LE
+                    );
+                } else {
+                    $mime[] = sprintf(
+                        'Content-Type: %s%s',
+                        $type,
+                        static::$LE
+                    );
+                }
+                //RFC1341 part 5 says 7bit is assumed if not specified
+                if (static::ENCODING_7BIT !== $encoding) {
+                    $mime[] = sprintf('Content-Transfer-Encoding: %s%s', $encoding, static::$LE);
+                }
+
+                //Only set Content-IDs on inline attachments
+                if ((string) $cid !== '' && $disposition === 'inline') {
+                    $mime[] = 'Content-ID: <' . $this->encodeHeader($this->secureHeader($cid)) . '>' . static::$LE;
+                }
+
+                //Allow for bypassing the Content-Disposition header
+                if (!empty($disposition)) {
+                    $encoded_name = $this->encodeHeader($this->secureHeader($name));
+                    if (!empty($encoded_name)) {
+                        $mime[] = sprintf(
+                            'Content-Disposition: %s; filename=%s%s',
+                            $disposition,
+                            static::quotedString($encoded_name),
+                            static::$LE . static::$LE
+                        );
+                    } else {
+                        $mime[] = sprintf(
+                            'Content-Disposition: %s%s',
+                            $disposition,
+                            static::$LE . static::$LE
+                        );
+                    }
+                } else {
+                    $mime[] = static::$LE;
+                }
+
+                //Encode as string attachment
+                if ($bString) {
+                    $mime[] = $this->encodeString($string, $encoding);
+                } else {
+                    $mime[] = $this->encodeFile($path, $encoding);
+                }
+                if ($this->isError()) {
+                    return '';
+                }
+                $mime[] = static::$LE;
+            }
+        }
+
+        $mime[] = sprintf('--%s--%s', $boundary, static::$LE);
+
+        return implode('', $mime);
+    }
+
+    /**
+     * Encode a file attachment in requested format.
+     * Returns an empty string on failure.
+     *
+     * @param string $path     The full path to the file
+     * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'
+     *
+     * @return string
+     */
+    protected function encodeFile($path, $encoding = self::ENCODING_BASE64)
+    {
+        try {
+            if (!static::fileIsAccessible($path)) {
+                throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
+            }
+            $file_buffer = file_get_contents($path);
+            if (false === $file_buffer) {
+                throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE);
+            }
+            $file_buffer = $this->encodeString($file_buffer, $encoding);
+
+            return $file_buffer;
+        } catch (Exception $exc) {
+            $this->setError($exc->getMessage());
+            $this->edebug($exc->getMessage());
+            if ($this->exceptions) {
+                throw $exc;
+            }
+
+            return '';
+        }
+    }
+
+    /**
+     * Encode a string in requested format.
+     * Returns an empty string on failure.
+     *
+     * @param string $str      The text to encode
+     * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable'
+     *
+     * @throws Exception
+     *
+     * @return string
+     */
+    public function encodeString($str, $encoding = self::ENCODING_BASE64)
+    {
+        $encoded = '';
+        switch (strtolower($encoding)) {
+            case static::ENCODING_BASE64:
+                $encoded = chunk_split(
+                    base64_encode($str),
+                    static::STD_LINE_LENGTH,
+                    static::$LE
+                );
+                break;
+            case static::ENCODING_7BIT:
+            case static::ENCODING_8BIT:
+                $encoded = static::normalizeBreaks($str);
+                //Make sure it ends with a line break
+                if (substr($encoded, -(strlen(static::$LE))) !== static::$LE) {
+                    $encoded .= static::$LE;
+                }
+                break;
+            case static::ENCODING_BINARY:
+                $encoded = $str;
+                break;
+            case static::ENCODING_QUOTED_PRINTABLE:
+                $encoded = $this->encodeQP($str);
+                break;
+            default:
+                $this->setError($this->lang('encoding') . $encoding);
+                if ($this->exceptions) {
+                    throw new Exception($this->lang('encoding') . $encoding);
+                }
+                break;
+        }
+
+        return $encoded;
+    }
+
+    /**
+     * Encode a header value (not including its label) optimally.
+     * Picks shortest of Q, B, or none. Result includes folding if needed.
+     * See RFC822 definitions for phrase, comment and text positions.
+     *
+     * @param string $str      The header value to encode
+     * @param string $position What context the string will be used in
+     *
+     * @return string
+     */
+    public function encodeHeader($str, $position = 'text')
+    {
+        $matchcount = 0;
+        switch (strtolower($position)) {
+            case 'phrase':
+                if (!preg_match('/[\200-\377]/', $str)) {
+                    //Can't use addslashes as we don't know the value of magic_quotes_sybase
+                    $encoded = addcslashes($str, "\0..\37\177\\\"");
+                    if (($str === $encoded) && !preg_match('/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str)) {
+                        return $encoded;
+                    }
+
+                    return "\"$encoded\"";
+                }
+                $matchcount = preg_match_all('/[^\040\041\043-\133\135-\176]/', $str, $matches);
+                break;
+            /* @noinspection PhpMissingBreakStatementInspection */
+            case 'comment':
+                $matchcount = preg_match_all('/[()"]/', $str, $matches);
+            //fallthrough
+            case 'text':
+            default:
+                $matchcount += preg_match_all('/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches);
+                break;
+        }
+
+        if ($this->has8bitChars($str)) {
+            $charset = $this->CharSet;
+        } else {
+            $charset = static::CHARSET_ASCII;
+        }
+
+        //Q/B encoding adds 8 chars and the charset ("` =?<charset>?[QB]?<content>?=`").
+        $overhead = 8 + strlen($charset);
+
+        if ('mail' === $this->Mailer) {
+            $maxlen = static::MAIL_MAX_LINE_LENGTH - $overhead;
+        } else {
+            $maxlen = static::MAX_LINE_LENGTH - $overhead;
+        }
+
+        //Select the encoding that produces the shortest output and/or prevents corruption.
+        if ($matchcount > strlen($str) / 3) {
+            //More than 1/3 of the content needs encoding, use B-encode.
+            $encoding = 'B';
+        } elseif ($matchcount > 0) {
+            //Less than 1/3 of the content needs encoding, use Q-encode.
+            $encoding = 'Q';
+        } elseif (strlen($str) > $maxlen) {
+            //No encoding needed, but value exceeds max line length, use Q-encode to prevent corruption.
+            $encoding = 'Q';
+        } else {
+            //No reformatting needed
+            $encoding = false;
+        }
+
+        switch ($encoding) {
+            case 'B':
+                if ($this->hasMultiBytes($str)) {
+                    //Use a custom function which correctly encodes and wraps long
+                    //multibyte strings without breaking lines within a character
+                    $encoded = $this->base64EncodeWrapMB($str, "\n");
+                } else {
+                    $encoded = base64_encode($str);
+                    $maxlen -= $maxlen % 4;
+                    $encoded = trim(chunk_split($encoded, $maxlen, "\n"));
+                }
+                $encoded = preg_replace('/^(.*)$/m', ' =?' . $charset . "?$encoding?\\1?=", $encoded);
+                break;
+            case 'Q':
+                $encoded = $this->encodeQ($str, $position);
+                $encoded = $this->wrapText($encoded, $maxlen, true);
+                $encoded = str_replace('=' . static::$LE, "\n", trim($encoded));
+                $encoded = preg_replace('/^(.*)$/m', ' =?' . $charset . "?$encoding?\\1?=", $encoded);
+                break;
+            default:
+                return $str;
+        }
+
+        return trim(static::normalizeBreaks($encoded));
+    }
+
+    /**
+     * Check if a string contains multi-byte characters.
+     *
+     * @param string $str multi-byte text to wrap encode
+     *
+     * @return bool
+     */
+    public function hasMultiBytes($str)
+    {
+        if (function_exists('mb_strlen')) {
+            return strlen($str) > mb_strlen($str, $this->CharSet);
+        }
+
+        //Assume no multibytes (we can't handle without mbstring functions anyway)
+        return false;
+    }
+
+    /**
+     * Does a string contain any 8-bit chars (in any charset)?
+     *
+     * @param string $text
+     *
+     * @return bool
+     */
+    public function has8bitChars($text)
+    {
+        return (bool) preg_match('/[\x80-\xFF]/', $text);
+    }
+
+    /**
+     * Encode and wrap long multibyte strings for mail headers
+     * without breaking lines within a character.
+     * Adapted from a function by paravoid.
+     *
+     * @see http://www.php.net/manual/en/function.mb-encode-mimeheader.php#60283
+     *
+     * @param string $str       multi-byte text to wrap encode
+     * @param string $linebreak string to use as linefeed/end-of-line
+     *
+     * @return string
+     */
+    public function base64EncodeWrapMB($str, $linebreak = null)
+    {
+        $start = '=?' . $this->CharSet . '?B?';
+        $end = '?=';
+        $encoded = '';
+        if (null === $linebreak) {
+            $linebreak = static::$LE;
+        }
+
+        $mb_length = mb_strlen($str, $this->CharSet);
+        //Each line must have length <= 75, including $start and $end
+        $length = 75 - strlen($start) - strlen($end);
+        //Average multi-byte ratio
+        $ratio = $mb_length / strlen($str);
+        //Base64 has a 4:3 ratio
+        $avgLength = floor($length * $ratio * .75);
+
+        $offset = 0;
+        for ($i = 0; $i < $mb_length; $i += $offset) {
+            $lookBack = 0;
+            do {
+                $offset = $avgLength - $lookBack;
+                $chunk = mb_substr($str, $i, $offset, $this->CharSet);
+                $chunk = base64_encode($chunk);
+                ++$lookBack;
+            } while (strlen($chunk) > $length);
+            $encoded .= $chunk . $linebreak;
+        }
+
+        //Chomp the last linefeed
+        return substr($encoded, 0, -strlen($linebreak));
+    }
+
+    /**
+     * Encode a string in quoted-printable format.
+     * According to RFC2045 section 6.7.
+     *
+     * @param string $string The text to encode
+     *
+     * @return string
+     */
+    public function encodeQP($string)
+    {
+        return static::normalizeBreaks(quoted_printable_encode($string));
+    }
+
+    /**
+     * Encode a string using Q encoding.
+     *
+     * @see http://tools.ietf.org/html/rfc2047#section-4.2
+     *
+     * @param string $str      the text to encode
+     * @param string $position Where the text is going to be used, see the RFC for what that means
+     *
+     * @return string
+     */
+    public function encodeQ($str, $position = 'text')
+    {
+        //There should not be any EOL in the string
+        $pattern = '';
+        $encoded = str_replace(["\r", "\n"], '', $str);
+        switch (strtolower($position)) {
+            case 'phrase':
+                //RFC 2047 section 5.3
+                $pattern = '^A-Za-z0-9!*+\/ -';
+                break;
+            /*
+             * RFC 2047 section 5.2.
+             * Build $pattern without including delimiters and []
+             */
+            /* @noinspection PhpMissingBreakStatementInspection */
+            case 'comment':
+                $pattern = '\(\)"';
+            /* Intentional fall through */
+            case 'text':
+            default:
+                //RFC 2047 section 5.1
+                //Replace every high ascii, control, =, ? and _ characters
+                $pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $pattern;
+                break;
+        }
+        $matches = [];
+        if (preg_match_all("/[{$pattern}]/", $encoded, $matches)) {
+            //If the string contains an '=', make sure it's the first thing we replace
+            //so as to avoid double-encoding
+            $eqkey = array_search('=', $matches[0], true);
+            if (false !== $eqkey) {
+                unset($matches[0][$eqkey]);
+                array_unshift($matches[0], '=');
+            }
+            foreach (array_unique($matches[0]) as $char) {
+                $encoded = str_replace($char, '=' . sprintf('%02X', ord($char)), $encoded);
+            }
+        }
+        //Replace spaces with _ (more readable than =20)
+        //RFC 2047 section 4.2(2)
+        return str_replace(' ', '_', $encoded);
+    }
+
+    /**
+     * Add a string or binary attachment (non-filesystem).
+     * This method can be used to attach ascii or binary data,
+     * such as a BLOB record from a database.
+     *
+     * @param string $string      String attachment data
+     * @param string $filename    Name of the attachment
+     * @param string $encoding    File encoding (see $Encoding)
+     * @param string $type        File extension (MIME) type
+     * @param string $disposition Disposition to use
+     *
+     * @throws Exception
+     *
+     * @return bool True on successfully adding an attachment
+     */
+    public function addStringAttachment(
+        $string,
+        $filename,
+        $encoding = self::ENCODING_BASE64,
+        $type = '',
+        $disposition = 'attachment'
+    ) {
+        try {
+            //If a MIME type is not specified, try to work it out from the file name
+            if ('' === $type) {
+                $type = static::filenameToType($filename);
+            }
+
+            if (!$this->validateEncoding($encoding)) {
+                throw new Exception($this->lang('encoding') . $encoding);
+            }
+
+            //Append to $attachment array
+            $this->attachment[] = [
+                0 => $string,
+                1 => $filename,
+                2 => static::mb_pathinfo($filename, PATHINFO_BASENAME),
+                3 => $encoding,
+                4 => $type,
+                5 => true, //isStringAttachment
+                6 => $disposition,
+                7 => 0,
+            ];
+        } catch (Exception $exc) {
+            $this->setError($exc->getMessage());
+            $this->edebug($exc->getMessage());
+            if ($this->exceptions) {
+                throw $exc;
+            }
+
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Add an embedded (inline) attachment from a file.
+     * This can include images, sounds, and just about any other document type.
+     * These differ from 'regular' attachments in that they are intended to be
+     * displayed inline with the message, not just attached for download.
+     * This is used in HTML messages that embed the images
+     * the HTML refers to using the `$cid` value in `img` tags, for example `<img src="cid:mylogo">`.
+     * Never use a user-supplied path to a file!
+     *
+     * @param string $path        Path to the attachment
+     * @param string $cid         Content ID of the attachment; Use this to reference
+     *                            the content when using an embedded image in HTML
+     * @param string $name        Overrides the attachment filename
+     * @param string $encoding    File encoding (see $Encoding) defaults to `base64`
+     * @param string $type        File MIME type (by default mapped from the `$path` filename's extension)
+     * @param string $disposition Disposition to use: `inline` (default) or `attachment`
+     *                            (unlikely you want this – {@see `addAttachment()`} instead)
+     *
+     * @return bool True on successfully adding an attachment
+     * @throws Exception
+     *
+     */
+    public function addEmbeddedImage(
+        $path,
+        $cid,
+        $name = '',
+        $encoding = self::ENCODING_BASE64,
+        $type = '',
+        $disposition = 'inline'
+    ) {
+        try {
+            if (!static::fileIsAccessible($path)) {
+                throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE);
+            }
+
+            //If a MIME type is not specified, try to work it out from the file name
+            if ('' === $type) {
+                $type = static::filenameToType($path);
+            }
+
+            if (!$this->validateEncoding($encoding)) {
+                throw new Exception($this->lang('encoding') . $encoding);
+            }
+
+            $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME);
+            if ('' === $name) {
+                $name = $filename;
+            }
+
+            //Append to $attachment array
+            $this->attachment[] = [
+                0 => $path,
+                1 => $filename,
+                2 => $name,
+                3 => $encoding,
+                4 => $type,
+                5 => false, //isStringAttachment
+                6 => $disposition,
+                7 => $cid,
+            ];
+        } catch (Exception $exc) {
+            $this->setError($exc->getMessage());
+            $this->edebug($exc->getMessage());
+            if ($this->exceptions) {
+                throw $exc;
+            }
+
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Add an embedded stringified attachment.
+     * This can include images, sounds, and just about any other document type.
+     * If your filename doesn't contain an extension, be sure to set the $type to an appropriate MIME type.
+     *
+     * @param string $string      The attachment binary data
+     * @param string $cid         Content ID of the attachment; Use this to reference
+     *                            the content when using an embedded image in HTML
+     * @param string $name        A filename for the attachment. If this contains an extension,
+     *                            PHPMailer will attempt to set a MIME type for the attachment.
+     *                            For example 'file.jpg' would get an 'image/jpeg' MIME type.
+     * @param string $encoding    File encoding (see $Encoding), defaults to 'base64'
+     * @param string $type        MIME type - will be used in preference to any automatically derived type
+     * @param string $disposition Disposition to use
+     *
+     * @throws Exception
+     *
+     * @return bool True on successfully adding an attachment
+     */
+    public function addStringEmbeddedImage(
+        $string,
+        $cid,
+        $name = '',
+        $encoding = self::ENCODING_BASE64,
+        $type = '',
+        $disposition = 'inline'
+    ) {
+        try {
+            //If a MIME type is not specified, try to work it out from the name
+            if ('' === $type && !empty($name)) {
+                $type = static::filenameToType($name);
+            }
+
+            if (!$this->validateEncoding($encoding)) {
+                throw new Exception($this->lang('encoding') . $encoding);
+            }
+
+            //Append to $attachment array
+            $this->attachment[] = [
+                0 => $string,
+                1 => $name,
+                2 => $name,
+                3 => $encoding,
+                4 => $type,
+                5 => true, //isStringAttachment
+                6 => $disposition,
+                7 => $cid,
+            ];
+        } catch (Exception $exc) {
+            $this->setError($exc->getMessage());
+            $this->edebug($exc->getMessage());
+            if ($this->exceptions) {
+                throw $exc;
+            }
+
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Validate encodings.
+     *
+     * @param string $encoding
+     *
+     * @return bool
+     */
+    protected function validateEncoding($encoding)
+    {
+        return in_array(
+            $encoding,
+            [
+                self::ENCODING_7BIT,
+                self::ENCODING_QUOTED_PRINTABLE,
+                self::ENCODING_BASE64,
+                self::ENCODING_8BIT,
+                self::ENCODING_BINARY,
+            ],
+            true
+        );
+    }
+
+    /**
+     * Check if an embedded attachment is present with this cid.
+     *
+     * @param string $cid
+     *
+     * @return bool
+     */
+    protected function cidExists($cid)
+    {
+        foreach ($this->attachment as $attachment) {
+            if ('inline' === $attachment[6] && $cid === $attachment[7]) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Check if an inline attachment is present.
+     *
+     * @return bool
+     */
+    public function inlineImageExists()
+    {
+        foreach ($this->attachment as $attachment) {
+            if ('inline' === $attachment[6]) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Check if an attachment (non-inline) is present.
+     *
+     * @return bool
+     */
+    public function attachmentExists()
+    {
+        foreach ($this->attachment as $attachment) {
+            if ('attachment' === $attachment[6]) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Check if this message has an alternative body set.
+     *
+     * @return bool
+     */
+    public function alternativeExists()
+    {
+        return !empty($this->AltBody);
+    }
+
+    /**
+     * Clear queued addresses of given kind.
+     *
+     * @param string $kind 'to', 'cc', or 'bcc'
+     */
+    public function clearQueuedAddresses($kind)
+    {
+        $this->RecipientsQueue = array_filter(
+            $this->RecipientsQueue,
+            static function ($params) use ($kind) {
+                return $params[0] !== $kind;
+            }
+        );
+    }
+
+    /**
+     * Clear all To recipients.
+     */
+    public function clearAddresses()
+    {
+        foreach ($this->to as $to) {
+            unset($this->all_recipients[strtolower($to[0])]);
+        }
+        $this->to = [];
+        $this->clearQueuedAddresses('to');
+    }
+
+    /**
+     * Clear all CC recipients.
+     */
+    public function clearCCs()
+    {
+        foreach ($this->cc as $cc) {
+            unset($this->all_recipients[strtolower($cc[0])]);
+        }
+        $this->cc = [];
+        $this->clearQueuedAddresses('cc');
+    }
+
+    /**
+     * Clear all BCC recipients.
+     */
+    public function clearBCCs()
+    {
+        foreach ($this->bcc as $bcc) {
+            unset($this->all_recipients[strtolower($bcc[0])]);
+        }
+        $this->bcc = [];
+        $this->clearQueuedAddresses('bcc');
+    }
+
+    /**
+     * Clear all ReplyTo recipients.
+     */
+    public function clearReplyTos()
+    {
+        $this->ReplyTo = [];
+        $this->ReplyToQueue = [];
+    }
+
+    /**
+     * Clear all recipient types.
+     */
+    public function clearAllRecipients()
+    {
+        $this->to = [];
+        $this->cc = [];
+        $this->bcc = [];
+        $this->all_recipients = [];
+        $this->RecipientsQueue = [];
+    }
+
+    /**
+     * Clear all filesystem, string, and binary attachments.
+     */
+    public function clearAttachments()
+    {
+        $this->attachment = [];
+    }
+
+    /**
+     * Clear all custom headers.
+     */
+    public function clearCustomHeaders()
+    {
+        $this->CustomHeader = [];
+    }
+
+    /**
+     * Add an error message to the error container.
+     *
+     * @param string $msg
+     */
+    protected function setError($msg)
+    {
+        ++$this->error_count;
+        if ('smtp' === $this->Mailer && null !== $this->smtp) {
+            $lasterror = $this->smtp->getError();
+            if (!empty($lasterror['error'])) {
+                $msg .= $this->lang('smtp_error') . $lasterror['error'];
+                if (!empty($lasterror['detail'])) {
+                    $msg .= ' ' . $this->lang('smtp_detail') . $lasterror['detail'];
+                }
+                if (!empty($lasterror['smtp_code'])) {
+                    $msg .= ' ' . $this->lang('smtp_code') . $lasterror['smtp_code'];
+                }
+                if (!empty($lasterror['smtp_code_ex'])) {
+                    $msg .= ' ' . $this->lang('smtp_code_ex') . $lasterror['smtp_code_ex'];
+                }
+            }
+        }
+        $this->ErrorInfo = $msg;
+    }
+
+    /**
+     * Return an RFC 822 formatted date.
+     *
+     * @return string
+     */
+    public static function rfcDate()
+    {
+        //Set the time zone to whatever the default is to avoid 500 errors
+        //Will default to UTC if it's not set properly in php.ini
+        date_default_timezone_set(@date_default_timezone_get());
+
+        return date('D, j M Y H:i:s O');
+    }
+
+    /**
+     * Get the server hostname.
+     * Returns 'localhost.localdomain' if unknown.
+     *
+     * @return string
+     */
+    protected function serverHostname()
+    {
+        $result = '';
+        if (!empty($this->Hostname)) {
+            $result = $this->Hostname;
+        } elseif (isset($_SERVER) && array_key_exists('SERVER_NAME', $_SERVER)) {
+            $result = $_SERVER['SERVER_NAME'];
+        } elseif (function_exists('gethostname') && gethostname() !== false) {
+            $result = gethostname();
+        } elseif (php_uname('n') !== false) {
+            $result = php_uname('n');
+        }
+        if (!static::isValidHost($result)) {
+            return 'localhost.localdomain';
+        }
+
+        return $result;
+    }
+
+    /**
+     * Validate whether a string contains a valid value to use as a hostname or IP address.
+     * IPv6 addresses must include [], e.g. `[::1]`, not just `::1`.
+     *
+     * @param string $host The host name or IP address to check
+     *
+     * @return bool
+     */
+    public static function isValidHost($host)
+    {
+        //Simple syntax limits
+        if (
+            empty($host)
+            || !is_string($host)
+            || strlen($host) > 256
+            || !preg_match('/^([a-zA-Z\d.-]*|\[[a-fA-F\d:]+\])$/', $host)
+        ) {
+            return false;
+        }
+        //Looks like a bracketed IPv6 address
+        if (strlen($host) > 2 && substr($host, 0, 1) === '[' && substr($host, -1, 1) === ']') {
+            return filter_var(substr($host, 1, -1), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
+        }
+        //If removing all the dots results in a numeric string, it must be an IPv4 address.
+        //Need to check this first because otherwise things like `999.0.0.0` are considered valid host names
+        if (is_numeric(str_replace('.', '', $host))) {
+            //Is it a valid IPv4 address?
+            return filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
+        }
+        //Is it a syntactically valid hostname (when embeded in a URL)?
+        return filter_var('http://' . $host, FILTER_VALIDATE_URL) !== false;
+    }
+
+    /**
+     * Get an error message in the current language.
+     *
+     * @param string $key
+     *
+     * @return string
+     */
+    protected function lang($key)
+    {
+        if (count($this->language) < 1) {
+            $this->setLanguage(); //Set the default language
+        }
+
+        if (array_key_exists($key, $this->language)) {
+            if ('smtp_connect_failed' === $key) {
+                //Include a link to troubleshooting docs on SMTP connection failure.
+                //This is by far the biggest cause of support questions
+                //but it's usually not PHPMailer's fault.
+                return $this->language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting';
+            }
+
+            return $this->language[$key];
+        }
+
+        //Return the key as a fallback
+        return $key;
+    }
+
+    /**
+     * Build an error message starting with a generic one and adding details if possible.
+     *
+     * @param string $base_key
+     * @return string
+     */
+    private function getSmtpErrorMessage($base_key)
+    {
+        $message = $this->lang($base_key);
+        $error = $this->smtp->getError();
+        if (!empty($error['error'])) {
+            $message .= ' ' . $error['error'];
+            if (!empty($error['detail'])) {
+                $message .= ' ' . $error['detail'];
+            }
+        }
+
+        return $message;
+    }
+
+    /**
+     * Check if an error occurred.
+     *
+     * @return bool True if an error did occur
+     */
+    public function isError()
+    {
+        return $this->error_count > 0;
+    }
+
+    /**
+     * Add a custom header.
+     * $name value can be overloaded to contain
+     * both header name and value (name:value).
+     *
+     * @param string      $name  Custom header name
+     * @param string|null $value Header value
+     *
+     * @return bool True if a header was set successfully
+     * @throws Exception
+     */
+    public function addCustomHeader($name, $value = null)
+    {
+        if (null === $value && strpos($name, ':') !== false) {
+            //Value passed in as name:value
+            list($name, $value) = explode(':', $name, 2);
+        }
+        $name = trim($name);
+        $value = (null === $value) ? '' : trim($value);
+        //Ensure name is not empty, and that neither name nor value contain line breaks
+        if (empty($name) || strpbrk($name . $value, "\r\n") !== false) {
+            if ($this->exceptions) {
+                throw new Exception($this->lang('invalid_header'));
+            }
+
+            return false;
+        }
+        $this->CustomHeader[] = [$name, $value];
+
+        return true;
+    }
+
+    /**
+     * Returns all custom headers.
+     *
+     * @return array
+     */
+    public function getCustomHeaders()
+    {
+        return $this->CustomHeader;
+    }
+
+    /**
+     * Create a message body from an HTML string.
+     * Automatically inlines images and creates a plain-text version by converting the HTML,
+     * overwriting any existing values in Body and AltBody.
+     * Do not source $message content from user input!
+     * $basedir is prepended when handling relative URLs, e.g. <img src="/images/a.png"> and must not be empty
+     * will look for an image file in $basedir/images/a.png and convert it to inline.
+     * If you don't provide a $basedir, relative paths will be left untouched (and thus probably break in email)
+     * Converts data-uri images into embedded attachments.
+     * If you don't want to apply these transformations to your HTML, just set Body and AltBody directly.
+     *
+     * @param string        $message  HTML message string
+     * @param string        $basedir  Absolute path to a base directory to prepend to relative paths to images
+     * @param bool|callable $advanced Whether to use the internal HTML to text converter
+     *                                or your own custom converter
+     * @return string The transformed message body
+     *
+     * @throws Exception
+     *
+     * @see PHPMailer::html2text()
+     */
+    public function msgHTML($message, $basedir = '', $advanced = false)
+    {
+        preg_match_all('/(?<!-)(src|background)=["\'](.*)["\']/Ui', $message, $images);
+        if (array_key_exists(2, $images)) {
+            if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) {
+                //Ensure $basedir has a trailing /
+                $basedir .= '/';
+            }
+            foreach ($images[2] as $imgindex => $url) {
+                //Convert data URIs into embedded images
+                //e.g. "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
+                $match = [];
+                if (preg_match('#^data:(image/(?:jpe?g|gif|png));?(base64)?,(.+)#', $url, $match)) {
+                    if (count($match) === 4 && static::ENCODING_BASE64 === $match[2]) {
+                        $data = base64_decode($match[3]);
+                    } elseif ('' === $match[2]) {
+                        $data = rawurldecode($match[3]);
+                    } else {
+                        //Not recognised so leave it alone
+                        continue;
+                    }
+                    //Hash the decoded data, not the URL, so that the same data-URI image used in multiple places
+                    //will only be embedded once, even if it used a different encoding
+                    $cid = substr(hash('sha256', $data), 0, 32) . '@phpmailer.0'; //RFC2392 S 2
+
+                    if (!$this->cidExists($cid)) {
+                        $this->addStringEmbeddedImage(
+                            $data,
+                            $cid,
+                            'embed' . $imgindex,
+                            static::ENCODING_BASE64,
+                            $match[1]
+                        );
+                    }
+                    $message = str_replace(
+                        $images[0][$imgindex],
+                        $images[1][$imgindex] . '="cid:' . $cid . '"',
+                        $message
+                    );
+                    continue;
+                }
+                if (
+                    //Only process relative URLs if a basedir is provided (i.e. no absolute local paths)
+                    !empty($basedir)
+                    //Ignore URLs containing parent dir traversal (..)
+                    && (strpos($url, '..') === false)
+                    //Do not change urls that are already inline images
+                    && 0 !== strpos($url, 'cid:')
+                    //Do not change absolute URLs, including anonymous protocol
+                    && !preg_match('#^[a-z][a-z0-9+.-]*:?//#i', $url)
+                ) {
+                    $filename = static::mb_pathinfo($url, PATHINFO_BASENAME);
+                    $directory = dirname($url);
+                    if ('.' === $directory) {
+                        $directory = '';
+                    }
+                    //RFC2392 S 2
+                    $cid = substr(hash('sha256', $url), 0, 32) . '@phpmailer.0';
+                    if (strlen($basedir) > 1 && '/' !== substr($basedir, -1)) {
+                        $basedir .= '/';
+                    }
+                    if (strlen($directory) > 1 && '/' !== substr($directory, -1)) {
+                        $directory .= '/';
+                    }
+                    if (
+                        $this->addEmbeddedImage(
+                            $basedir . $directory . $filename,
+                            $cid,
+                            $filename,
+                            static::ENCODING_BASE64,
+                            static::_mime_types((string) static::mb_pathinfo($filename, PATHINFO_EXTENSION))
+                        )
+                    ) {
+                        $message = preg_replace(
+                            '/' . $images[1][$imgindex] . '=["\']' . preg_quote($url, '/') . '["\']/Ui',
+                            $images[1][$imgindex] . '="cid:' . $cid . '"',
+                            $message
+                        );
+                    }
+                }
+            }
+        }
+        $this->isHTML();
+        //Convert all message body line breaks to LE, makes quoted-printable encoding work much better
+        $this->Body = static::normalizeBreaks($message);
+        $this->AltBody = static::normalizeBreaks($this->html2text($message, $advanced));
+        if (!$this->alternativeExists()) {
+            $this->AltBody = 'This is an HTML-only message. To view it, activate HTML in your email application.'
+                . static::$LE;
+        }
+
+        return $this->Body;
+    }
+
+    /**
+     * Convert an HTML string into plain text.
+     * This is used by msgHTML().
+     * Note - older versions of this function used a bundled advanced converter
+     * which was removed for license reasons in #232.
+     * Example usage:
+     *
+     * ```php
+     * //Use default conversion
+     * $plain = $mail->html2text($html);
+     * //Use your own custom converter
+     * $plain = $mail->html2text($html, function($html) {
+     *     $converter = new MyHtml2text($html);
+     *     return $converter->get_text();
+     * });
+     * ```
+     *
+     * @param string        $html     The HTML text to convert
+     * @param bool|callable $advanced Any boolean value to use the internal converter,
+     *                                or provide your own callable for custom conversion.
+     *                                *Never* pass user-supplied data into this parameter
+     *
+     * @return string
+     */
+    public function html2text($html, $advanced = false)
+    {
+        if (is_callable($advanced)) {
+            return call_user_func($advanced, $html);
+        }
+
+        return html_entity_decode(
+            trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html))),
+            ENT_QUOTES,
+            $this->CharSet
+        );
+    }
+
+    /**
+     * Get the MIME type for a file extension.
+     *
+     * @param string $ext File extension
+     *
+     * @return string MIME type of file
+     */
+    public static function _mime_types($ext = '')
+    {
+        $mimes = [
+            'xl' => 'application/excel',
+            'js' => 'application/javascript',
+            'hqx' => 'application/mac-binhex40',
+            'cpt' => 'application/mac-compactpro',
+            'bin' => 'application/macbinary',
+            'doc' => 'application/msword',
+            'word' => 'application/msword',
+            'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+            'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
+            'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
+            'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
+            'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+            'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide',
+            'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+            'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
+            'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
+            'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
+            'class' => 'application/octet-stream',
+            'dll' => 'application/octet-stream',
+            'dms' => 'application/octet-stream',
+            'exe' => 'application/octet-stream',
+            'lha' => 'application/octet-stream',
+            'lzh' => 'application/octet-stream',
+            'psd' => 'application/octet-stream',
+            'sea' => 'application/octet-stream',
+            'so' => 'application/octet-stream',
+            'oda' => 'application/oda',
+            'pdf' => 'application/pdf',
+            'ai' => 'application/postscript',
+            'eps' => 'application/postscript',
+            'ps' => 'application/postscript',
+            'smi' => 'application/smil',
+            'smil' => 'application/smil',
+            'mif' => 'application/vnd.mif',
+            'xls' => 'application/vnd.ms-excel',
+            'ppt' => 'application/vnd.ms-powerpoint',
+            'wbxml' => 'application/vnd.wap.wbxml',
+            'wmlc' => 'application/vnd.wap.wmlc',
+            'dcr' => 'application/x-director',
+            'dir' => 'application/x-director',
+            'dxr' => 'application/x-director',
+            'dvi' => 'application/x-dvi',
+            'gtar' => 'application/x-gtar',
+            'php3' => 'application/x-httpd-php',
+            'php4' => 'application/x-httpd-php',
+            'php' => 'application/x-httpd-php',
+            'phtml' => 'application/x-httpd-php',
+            'phps' => 'application/x-httpd-php-source',
+            'swf' => 'application/x-shockwave-flash',
+            'sit' => 'application/x-stuffit',
+            'tar' => 'application/x-tar',
+            'tgz' => 'application/x-tar',
+            'xht' => 'application/xhtml+xml',
+            'xhtml' => 'application/xhtml+xml',
+            'zip' => 'application/zip',
+            'mid' => 'audio/midi',
+            'midi' => 'audio/midi',
+            'mp2' => 'audio/mpeg',
+            'mp3' => 'audio/mpeg',
+            'm4a' => 'audio/mp4',
+            'mpga' => 'audio/mpeg',
+            'aif' => 'audio/x-aiff',
+            'aifc' => 'audio/x-aiff',
+            'aiff' => 'audio/x-aiff',
+            'ram' => 'audio/x-pn-realaudio',
+            'rm' => 'audio/x-pn-realaudio',
+            'rpm' => 'audio/x-pn-realaudio-plugin',
+            'ra' => 'audio/x-realaudio',
+            'wav' => 'audio/x-wav',
+            'mka' => 'audio/x-matroska',
+            'bmp' => 'image/bmp',
+            'gif' => 'image/gif',
+            'jpeg' => 'image/jpeg',
+            'jpe' => 'image/jpeg',
+            'jpg' => 'image/jpeg',
+            'png' => 'image/png',
+            'tiff' => 'image/tiff',
+            'tif' => 'image/tiff',
+            'webp' => 'image/webp',
+            'avif' => 'image/avif',
+            'heif' => 'image/heif',
+            'heifs' => 'image/heif-sequence',
+            'heic' => 'image/heic',
+            'heics' => 'image/heic-sequence',
+            'eml' => 'message/rfc822',
+            'css' => 'text/css',
+            'html' => 'text/html',
+            'htm' => 'text/html',
+            'shtml' => 'text/html',
+            'log' => 'text/plain',
+            'text' => 'text/plain',
+            'txt' => 'text/plain',
+            'rtx' => 'text/richtext',
+            'rtf' => 'text/rtf',
+            'vcf' => 'text/vcard',
+            'vcard' => 'text/vcard',
+            'ics' => 'text/calendar',
+            'xml' => 'text/xml',
+            'xsl' => 'text/xml',
+            'csv' => 'text/csv',
+            'wmv' => 'video/x-ms-wmv',
+            'mpeg' => 'video/mpeg',
+            'mpe' => 'video/mpeg',
+            'mpg' => 'video/mpeg',
+            'mp4' => 'video/mp4',
+            'm4v' => 'video/mp4',
+            'mov' => 'video/quicktime',
+            'qt' => 'video/quicktime',
+            'rv' => 'video/vnd.rn-realvideo',
+            'avi' => 'video/x-msvideo',
+            'movie' => 'video/x-sgi-movie',
+            'webm' => 'video/webm',
+            'mkv' => 'video/x-matroska',
+        ];
+        $ext = strtolower($ext);
+        if (array_key_exists($ext, $mimes)) {
+            return $mimes[$ext];
+        }
+
+        return 'application/octet-stream';
+    }
+
+    /**
+     * Map a file name to a MIME type.
+     * Defaults to 'application/octet-stream', i.e.. arbitrary binary data.
+     *
+     * @param string $filename A file name or full path, does not need to exist as a file
+     *
+     * @return string
+     */
+    public static function filenameToType($filename)
+    {
+        //In case the path is a URL, strip any query string before getting extension
+        $qpos = strpos($filename, '?');
+        if (false !== $qpos) {
+            $filename = substr($filename, 0, $qpos);
+        }
+        $ext = static::mb_pathinfo($filename, PATHINFO_EXTENSION);
+
+        return static::_mime_types($ext);
+    }
+
+    /**
+     * Multi-byte-safe pathinfo replacement.
+     * Drop-in replacement for pathinfo(), but multibyte- and cross-platform-safe.
+     *
+     * @see http://www.php.net/manual/en/function.pathinfo.php#107461
+     *
+     * @param string     $path    A filename or path, does not need to exist as a file
+     * @param int|string $options Either a PATHINFO_* constant,
+     *                            or a string name to return only the specified piece
+     *
+     * @return string|array
+     */
+    public static function mb_pathinfo($path, $options = null)
+    {
+        $ret = ['dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''];
+        $pathinfo = [];
+        if (preg_match('#^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^.\\\\/]+?)|))[\\\\/.]*$#m', $path, $pathinfo)) {
+            if (array_key_exists(1, $pathinfo)) {
+                $ret['dirname'] = $pathinfo[1];
+            }
+            if (array_key_exists(2, $pathinfo)) {
+                $ret['basename'] = $pathinfo[2];
+            }
+            if (array_key_exists(5, $pathinfo)) {
+                $ret['extension'] = $pathinfo[5];
+            }
+            if (array_key_exists(3, $pathinfo)) {
+                $ret['filename'] = $pathinfo[3];
+            }
+        }
+        switch ($options) {
+            case PATHINFO_DIRNAME:
+            case 'dirname':
+                return $ret['dirname'];
+            case PATHINFO_BASENAME:
+            case 'basename':
+                return $ret['basename'];
+            case PATHINFO_EXTENSION:
+            case 'extension':
+                return $ret['extension'];
+            case PATHINFO_FILENAME:
+            case 'filename':
+                return $ret['filename'];
+            default:
+                return $ret;
+        }
+    }
+
+    /**
+     * Set or reset instance properties.
+     * You should avoid this function - it's more verbose, less efficient, more error-prone and
+     * harder to debug than setting properties directly.
+     * Usage Example:
+     * `$mail->set('SMTPSecure', static::ENCRYPTION_STARTTLS);`
+     *   is the same as:
+     * `$mail->SMTPSecure = static::ENCRYPTION_STARTTLS;`.
+     *
+     * @param string $name  The property name to set
+     * @param mixed  $value The value to set the property to
+     *
+     * @return bool
+     */
+    public function set($name, $value = '')
+    {
+        if (property_exists($this, $name)) {
+            $this->{$name} = $value;
+
+            return true;
+        }
+        $this->setError($this->lang('variable_set') . $name);
+
+        return false;
+    }
+
+    /**
+     * Strip newlines to prevent header injection.
+     *
+     * @param string $str
+     *
+     * @return string
+     */
+    public function secureHeader($str)
+    {
+        return trim(str_replace(["\r", "\n"], '', $str));
+    }
+
+    /**
+     * Normalize line breaks in a string.
+     * Converts UNIX LF, Mac CR and Windows CRLF line breaks into a single line break format.
+     * Defaults to CRLF (for message bodies) and preserves consecutive breaks.
+     *
+     * @param string $text
+     * @param string $breaktype What kind of line break to use; defaults to static::$LE
+     *
+     * @return string
+     */
+    public static function normalizeBreaks($text, $breaktype = null)
+    {
+        if (null === $breaktype) {
+            $breaktype = static::$LE;
+        }
+        //Normalise to \n
+        $text = str_replace([self::CRLF, "\r"], "\n", $text);
+        //Now convert LE as needed
+        if ("\n" !== $breaktype) {
+            $text = str_replace("\n", $breaktype, $text);
+        }
+
+        return $text;
+    }
+
+    /**
+     * Remove trailing whitespace from a string.
+     *
+     * @param string $text
+     *
+     * @return string The text to remove whitespace from
+     */
+    public static function stripTrailingWSP($text)
+    {
+        return rtrim($text, " \r\n\t");
+    }
+
+    /**
+     * Strip trailing line breaks from a string.
+     *
+     * @param string $text
+     *
+     * @return string The text to remove breaks from
+     */
+    public static function stripTrailingBreaks($text)
+    {
+        return rtrim($text, "\r\n");
+    }
+
+    /**
+     * Return the current line break format string.
+     *
+     * @return string
+     */
+    public static function getLE()
+    {
+        return static::$LE;
+    }
+
+    /**
+     * Set the line break format string, e.g. "\r\n".
+     *
+     * @param string $le
+     */
+    protected static function setLE($le)
+    {
+        static::$LE = $le;
+    }
+
+    /**
+     * Set the public and private key files and password for S/MIME signing.
+     *
+     * @param string $cert_filename
+     * @param string $key_filename
+     * @param string $key_pass            Password for private key
+     * @param string $extracerts_filename Optional path to chain certificate
+     */
+    public function sign($cert_filename, $key_filename, $key_pass, $extracerts_filename = '')
+    {
+        $this->sign_cert_file = $cert_filename;
+        $this->sign_key_file = $key_filename;
+        $this->sign_key_pass = $key_pass;
+        $this->sign_extracerts_file = $extracerts_filename;
+    }
+
+    /**
+     * Quoted-Printable-encode a DKIM header.
+     *
+     * @param string $txt
+     *
+     * @return string
+     */
+    public function DKIM_QP($txt)
+    {
+        $line = '';
+        $len = strlen($txt);
+        for ($i = 0; $i < $len; ++$i) {
+            $ord = ord($txt[$i]);
+            if (((0x21 <= $ord) && ($ord <= 0x3A)) || $ord === 0x3C || ((0x3E <= $ord) && ($ord <= 0x7E))) {
+                $line .= $txt[$i];
+            } else {
+                $line .= '=' . sprintf('%02X', $ord);
+            }
+        }
+
+        return $line;
+    }
+
+    /**
+     * Generate a DKIM signature.
+     *
+     * @param string $signHeader
+     *
+     * @throws Exception
+     *
+     * @return string The DKIM signature value
+     */
+    public function DKIM_Sign($signHeader)
+    {
+        if (!defined('PKCS7_TEXT')) {
+            if ($this->exceptions) {
+                throw new Exception($this->lang('extension_missing') . 'openssl');
+            }
+
+            return '';
+        }
+        $privKeyStr = !empty($this->DKIM_private_string) ?
+            $this->DKIM_private_string :
+            file_get_contents($this->DKIM_private);
+        if ('' !== $this->DKIM_passphrase) {
+            $privKey = openssl_pkey_get_private($privKeyStr, $this->DKIM_passphrase);
+        } else {
+            $privKey = openssl_pkey_get_private($privKeyStr);
+        }
+        if (openssl_sign($signHeader, $signature, $privKey, 'sha256WithRSAEncryption')) {
+            if (\PHP_MAJOR_VERSION < 8) {
+                openssl_pkey_free($privKey);
+            }
+
+            return base64_encode($signature);
+        }
+        if (\PHP_MAJOR_VERSION < 8) {
+            openssl_pkey_free($privKey);
+        }
+
+        return '';
+    }
+
+    /**
+     * Generate a DKIM canonicalization header.
+     * Uses the 'relaxed' algorithm from RFC6376 section 3.4.2.
+     * Canonicalized headers should *always* use CRLF, regardless of mailer setting.
+     *
+     * @see https://tools.ietf.org/html/rfc6376#section-3.4.2
+     *
+     * @param string $signHeader Header
+     *
+     * @return string
+     */
+    public function DKIM_HeaderC($signHeader)
+    {
+        //Normalize breaks to CRLF (regardless of the mailer)
+        $signHeader = static::normalizeBreaks($signHeader, self::CRLF);
+        //Unfold header lines
+        //Note PCRE \s is too broad a definition of whitespace; RFC5322 defines it as `[ \t]`
+        //@see https://tools.ietf.org/html/rfc5322#section-2.2
+        //That means this may break if you do something daft like put vertical tabs in your headers.
+        $signHeader = preg_replace('/\r\n[ \t]+/', ' ', $signHeader);
+        //Break headers out into an array
+        $lines = explode(self::CRLF, $signHeader);
+        foreach ($lines as $key => $line) {
+            //If the header is missing a :, skip it as it's invalid
+            //This is likely to happen because the explode() above will also split
+            //on the trailing LE, leaving an empty line
+            if (strpos($line, ':') === false) {
+                continue;
+            }
+            list($heading, $value) = explode(':', $line, 2);
+            //Lower-case header name
+            $heading = strtolower($heading);
+            //Collapse white space within the value, also convert WSP to space
+            $value = preg_replace('/[ \t]+/', ' ', $value);
+            //RFC6376 is slightly unclear here - it says to delete space at the *end* of each value
+            //But then says to delete space before and after the colon.
+            //Net result is the same as trimming both ends of the value.
+            //By elimination, the same applies to the field name
+            $lines[$key] = trim($heading, " \t") . ':' . trim($value, " \t");
+        }
+
+        return implode(self::CRLF, $lines);
+    }
+
+    /**
+     * Generate a DKIM canonicalization body.
+     * Uses the 'simple' algorithm from RFC6376 section 3.4.3.
+     * Canonicalized bodies should *always* use CRLF, regardless of mailer setting.
+     *
+     * @see https://tools.ietf.org/html/rfc6376#section-3.4.3
+     *
+     * @param string $body Message Body
+     *
+     * @return string
+     */
+    public function DKIM_BodyC($body)
+    {
+        if (empty($body)) {
+            return self::CRLF;
+        }
+        //Normalize line endings to CRLF
+        $body = static::normalizeBreaks($body, self::CRLF);
+
+        //Reduce multiple trailing line breaks to a single one
+        return static::stripTrailingBreaks($body) . self::CRLF;
+    }
+
+    /**
+     * Create the DKIM header and body in a new message header.
+     *
+     * @param string $headers_line Header lines
+     * @param string $subject      Subject
+     * @param string $body         Body
+     *
+     * @throws Exception
+     *
+     * @return string
+     */
+    public function DKIM_Add($headers_line, $subject, $body)
+    {
+        $DKIMsignatureType = 'rsa-sha256'; //Signature & hash algorithms
+        $DKIMcanonicalization = 'relaxed/simple'; //Canonicalization methods of header & body
+        $DKIMquery = 'dns/txt'; //Query method
+        $DKIMtime = time();
+        //Always sign these headers without being asked
+        //Recommended list from https://tools.ietf.org/html/rfc6376#section-5.4.1
+        $autoSignHeaders = [
+            'from',
+            'to',
+            'cc',
+            'date',
+            'subject',
+            'reply-to',
+            'message-id',
+            'content-type',
+            'mime-version',
+            'x-mailer',
+        ];
+        if (stripos($headers_line, 'Subject') === false) {
+            $headers_line .= 'Subject: ' . $subject . static::$LE;
+        }
+        $headerLines = explode(static::$LE, $headers_line);
+        $currentHeaderLabel = '';
+        $currentHeaderValue = '';
+        $parsedHeaders = [];
+        $headerLineIndex = 0;
+        $headerLineCount = count($headerLines);
+        foreach ($headerLines as $headerLine) {
+            $matches = [];
+            if (preg_match('/^([^ \t]*?)(?::[ \t]*)(.*)$/', $headerLine, $matches)) {
+                if ($currentHeaderLabel !== '') {
+                    //We were previously in another header; This is the start of a new header, so save the previous one
+                    $parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue];
+                }
+                $currentHeaderLabel = $matches[1];
+                $currentHeaderValue = $matches[2];
+            } elseif (preg_match('/^[ \t]+(.*)$/', $headerLine, $matches)) {
+                //This is a folded continuation of the current header, so unfold it
+                $currentHeaderValue .= ' ' . $matches[1];
+            }
+            ++$headerLineIndex;
+            if ($headerLineIndex >= $headerLineCount) {
+                //This was the last line, so finish off this header
+                $parsedHeaders[] = ['label' => $currentHeaderLabel, 'value' => $currentHeaderValue];
+            }
+        }
+        $copiedHeaders = [];
+        $headersToSignKeys = [];
+        $headersToSign = [];
+        foreach ($parsedHeaders as $header) {
+            //Is this header one that must be included in the DKIM signature?
+            if (in_array(strtolower($header['label']), $autoSignHeaders, true)) {
+                $headersToSignKeys[] = $header['label'];
+                $headersToSign[] = $header['label'] . ': ' . $header['value'];
+                if ($this->DKIM_copyHeaderFields) {
+                    $copiedHeaders[] = $header['label'] . ':' . //Note no space after this, as per RFC
+                        str_replace('|', '=7C', $this->DKIM_QP($header['value']));
+                }
+                continue;
+            }
+            //Is this an extra custom header we've been asked to sign?
+            if (in_array($header['label'], $this->DKIM_extraHeaders, true)) {
+                //Find its value in custom headers
+                foreach ($this->CustomHeader as $customHeader) {
+                    if ($customHeader[0] === $header['label']) {
+                        $headersToSignKeys[] = $header['label'];
+                        $headersToSign[] = $header['label'] . ': ' . $header['value'];
+                        if ($this->DKIM_copyHeaderFields) {
+                            $copiedHeaders[] = $header['label'] . ':' . //Note no space after this, as per RFC
+                                str_replace('|', '=7C', $this->DKIM_QP($header['value']));
+                        }
+                        //Skip straight to the next header
+                        continue 2;
+                    }
+                }
+            }
+        }
+        $copiedHeaderFields = '';
+        if ($this->DKIM_copyHeaderFields && count($copiedHeaders) > 0) {
+            //Assemble a DKIM 'z' tag
+            $copiedHeaderFields = ' z=';
+            $first = true;
+            foreach ($copiedHeaders as $copiedHeader) {
+                if (!$first) {
+                    $copiedHeaderFields .= static::$LE . ' |';
+                }
+                //Fold long values
+                if (strlen($copiedHeader) > self::STD_LINE_LENGTH - 3) {
+                    $copiedHeaderFields .= substr(
+                        chunk_split($copiedHeader, self::STD_LINE_LENGTH - 3, static::$LE . self::FWS),
+                        0,
+                        -strlen(static::$LE . self::FWS)
+                    );
+                } else {
+                    $copiedHeaderFields .= $copiedHeader;
+                }
+                $first = false;
+            }
+            $copiedHeaderFields .= ';' . static::$LE;
+        }
+        $headerKeys = ' h=' . implode(':', $headersToSignKeys) . ';' . static::$LE;
+        $headerValues = implode(static::$LE, $headersToSign);
+        $body = $this->DKIM_BodyC($body);
+        //Base64 of packed binary SHA-256 hash of body
+        $DKIMb64 = base64_encode(pack('H*', hash('sha256', $body)));
+        $ident = '';
+        if ('' !== $this->DKIM_identity) {
+            $ident = ' i=' . $this->DKIM_identity . ';' . static::$LE;
+        }
+        //The DKIM-Signature header is included in the signature *except for* the value of the `b` tag
+        //which is appended after calculating the signature
+        //https://tools.ietf.org/html/rfc6376#section-3.5
+        $dkimSignatureHeader = 'DKIM-Signature: v=1;' .
+            ' d=' . $this->DKIM_domain . ';' .
+            ' s=' . $this->DKIM_selector . ';' . static::$LE .
+            ' a=' . $DKIMsignatureType . ';' .
+            ' q=' . $DKIMquery . ';' .
+            ' t=' . $DKIMtime . ';' .
+            ' c=' . $DKIMcanonicalization . ';' . static::$LE .
+            $headerKeys .
+            $ident .
+            $copiedHeaderFields .
+            ' bh=' . $DKIMb64 . ';' . static::$LE .
+            ' b=';
+        //Canonicalize the set of headers
+        $canonicalizedHeaders = $this->DKIM_HeaderC(
+            $headerValues . static::$LE . $dkimSignatureHeader
+        );
+        $signature = $this->DKIM_Sign($canonicalizedHeaders);
+        $signature = trim(chunk_split($signature, self::STD_LINE_LENGTH - 3, static::$LE . self::FWS));
+
+        return static::normalizeBreaks($dkimSignatureHeader . $signature);
+    }
+
+    /**
+     * Detect if a string contains a line longer than the maximum line length
+     * allowed by RFC 2822 section 2.1.1.
+     *
+     * @param string $str
+     *
+     * @return bool
+     */
+    public static function hasLineLongerThanMax($str)
+    {
+        return (bool) preg_match('/^(.{' . (self::MAX_LINE_LENGTH + strlen(static::$LE)) . ',})/m', $str);
+    }
+
+    /**
+     * If a string contains any "special" characters, double-quote the name,
+     * and escape any double quotes with a backslash.
+     *
+     * @param string $str
+     *
+     * @return string
+     *
+     * @see RFC822 3.4.1
+     */
+    public static function quotedString($str)
+    {
+        if (preg_match('/[ ()<>@,;:"\/\[\]?=]/', $str)) {
+            //If the string contains any of these chars, it must be double-quoted
+            //and any double quotes must be escaped with a backslash
+            return '"' . str_replace('"', '\\"', $str) . '"';
+        }
+
+        //Return the string untouched, it doesn't need quoting
+        return $str;
+    }
+
+    /**
+     * Allows for public read access to 'to' property.
+     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
+     *
+     * @return array
+     */
+    public function getToAddresses()
+    {
+        return $this->to;
+    }
+
+    /**
+     * Allows for public read access to 'cc' property.
+     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
+     *
+     * @return array
+     */
+    public function getCcAddresses()
+    {
+        return $this->cc;
+    }
+
+    /**
+     * Allows for public read access to 'bcc' property.
+     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
+     *
+     * @return array
+     */
+    public function getBccAddresses()
+    {
+        return $this->bcc;
+    }
+
+    /**
+     * Allows for public read access to 'ReplyTo' property.
+     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
+     *
+     * @return array
+     */
+    public function getReplyToAddresses()
+    {
+        return $this->ReplyTo;
+    }
+
+    /**
+     * Allows for public read access to 'all_recipients' property.
+     * Before the send() call, queued addresses (i.e. with IDN) are not yet included.
+     *
+     * @return array
+     */
+    public function getAllRecipientAddresses()
+    {
+        return $this->all_recipients;
+    }
+
+    /**
+     * Perform a callback.
+     *
+     * @param bool   $isSent
+     * @param array  $to
+     * @param array  $cc
+     * @param array  $bcc
+     * @param string $subject
+     * @param string $body
+     * @param string $from
+     * @param array  $extra
+     */
+    protected function doCallback($isSent, $to, $cc, $bcc, $subject, $body, $from, $extra)
+    {
+        if (!empty($this->action_function) && is_callable($this->action_function)) {
+            call_user_func($this->action_function, $isSent, $to, $cc, $bcc, $subject, $body, $from, $extra);
+        }
+    }
+
+    /**
+     * Get the OAuthTokenProvider instance.
+     *
+     * @return OAuthTokenProvider
+     */
+    public function getOAuth()
+    {
+        return $this->oauth;
+    }
+
+    /**
+     * Set an OAuthTokenProvider instance.
+     */
+    public function setOAuth(OAuthTokenProvider $oauth)
+    {
+        $this->oauth = $oauth;
+    }
+}
diff --git a/src/lib/PHPMailer/POP3.php b/src/lib/PHPMailer/POP3.php
new file mode 100644
index 0000000..b92a1f2
--- /dev/null
+++ b/src/lib/PHPMailer/POP3.php
@@ -0,0 +1,467 @@
+<?php
+
+/**
+ * PHPMailer POP-Before-SMTP Authentication Class.
+ * PHP Version 5.5.
+ *
+ * @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
+ *
+ * @author    Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
+ * @author    Jim Jagielski (jimjag) <jimjag@gmail.com>
+ * @author    Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
+ * @author    Brent R. Matzelle (original founder)
+ * @copyright 2012 - 2020 Marcus Bointon
+ * @copyright 2010 - 2012 Jim Jagielski
+ * @copyright 2004 - 2009 Andy Prevost
+ * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
+ * @note      This program is distributed in the hope that it will be useful - WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.
+ */
+
+namespace PHPMailer\PHPMailer;
+
+/**
+ * PHPMailer POP-Before-SMTP Authentication Class.
+ * Specifically for PHPMailer to use for RFC1939 POP-before-SMTP authentication.
+ * 1) This class does not support APOP authentication.
+ * 2) Opening and closing lots of POP3 connections can be quite slow. If you need
+ *   to send a batch of emails then just perform the authentication once at the start,
+ *   and then loop through your mail sending script. Providing this process doesn't
+ *   take longer than the verification period lasts on your POP3 server, you should be fine.
+ * 3) This is really ancient technology; you should only need to use it to talk to very old systems.
+ * 4) This POP3 class is deliberately lightweight and incomplete, implementing just
+ *   enough to do authentication.
+ *   If you want a more complete class there are other POP3 classes for PHP available.
+ *
+ * @author Richard Davey (original author) <rich@corephp.co.uk>
+ * @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
+ * @author Jim Jagielski (jimjag) <jimjag@gmail.com>
+ * @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
+ */
+class POP3
+{
+    /**
+     * The POP3 PHPMailer Version number.
+     *
+     * @var string
+     */
+    const VERSION = '6.8.1';
+
+    /**
+     * Default POP3 port number.
+     *
+     * @var int
+     */
+    const DEFAULT_PORT = 110;
+
+    /**
+     * Default timeout in seconds.
+     *
+     * @var int
+     */
+    const DEFAULT_TIMEOUT = 30;
+
+    /**
+     * POP3 class debug output mode.
+     * Debug output level.
+     * Options:
+     * @see POP3::DEBUG_OFF: No output
+     * @see POP3::DEBUG_SERVER: Server messages, connection/server errors
+     * @see POP3::DEBUG_CLIENT: Client and Server messages, connection/server errors
+     *
+     * @var int
+     */
+    public $do_debug = self::DEBUG_OFF;
+
+    /**
+     * POP3 mail server hostname.
+     *
+     * @var string
+     */
+    public $host;
+
+    /**
+     * POP3 port number.
+     *
+     * @var int
+     */
+    public $port;
+
+    /**
+     * POP3 Timeout Value in seconds.
+     *
+     * @var int
+     */
+    public $tval;
+
+    /**
+     * POP3 username.
+     *
+     * @var string
+     */
+    public $username;
+
+    /**
+     * POP3 password.
+     *
+     * @var string
+     */
+    public $password;
+
+    /**
+     * Resource handle for the POP3 connection socket.
+     *
+     * @var resource
+     */
+    protected $pop_conn;
+
+    /**
+     * Are we connected?
+     *
+     * @var bool
+     */
+    protected $connected = false;
+
+    /**
+     * Error container.
+     *
+     * @var array
+     */
+    protected $errors = [];
+
+    /**
+     * Line break constant.
+     */
+    const LE = "\r\n";
+
+    /**
+     * Debug level for no output.
+     *
+     * @var int
+     */
+    const DEBUG_OFF = 0;
+
+    /**
+     * Debug level to show server -> client messages
+     * also shows clients connection errors or errors from server
+     *
+     * @var int
+     */
+    const DEBUG_SERVER = 1;
+
+    /**
+     * Debug level to show client -> server and server -> client messages.
+     *
+     * @var int
+     */
+    const DEBUG_CLIENT = 2;
+
+    /**
+     * Simple static wrapper for all-in-one POP before SMTP.
+     *
+     * @param string   $host        The hostname to connect to
+     * @param int|bool $port        The port number to connect to
+     * @param int|bool $timeout     The timeout value
+     * @param string   $username
+     * @param string   $password
+     * @param int      $debug_level
+     *
+     * @return bool
+     */
+    public static function popBeforeSmtp(
+        $host,
+        $port = false,
+        $timeout = false,
+        $username = '',
+        $password = '',
+        $debug_level = 0
+    ) {
+        $pop = new self();
+
+        return $pop->authorise($host, $port, $timeout, $username, $password, $debug_level);
+    }
+
+    /**
+     * Authenticate with a POP3 server.
+     * A connect, login, disconnect sequence
+     * appropriate for POP-before SMTP authorisation.
+     *
+     * @param string   $host        The hostname to connect to
+     * @param int|bool $port        The port number to connect to
+     * @param int|bool $timeout     The timeout value
+     * @param string   $username
+     * @param string   $password
+     * @param int      $debug_level
+     *
+     * @return bool
+     */
+    public function authorise($host, $port = false, $timeout = false, $username = '', $password = '', $debug_level = 0)
+    {
+        $this->host = $host;
+        //If no port value provided, use default
+        if (false === $port) {
+            $this->port = static::DEFAULT_PORT;
+        } else {
+            $this->port = (int) $port;
+        }
+        //If no timeout value provided, use default
+        if (false === $timeout) {
+            $this->tval = static::DEFAULT_TIMEOUT;
+        } else {
+            $this->tval = (int) $timeout;
+        }
+        $this->do_debug = $debug_level;
+        $this->username = $username;
+        $this->password = $password;
+        //Reset the error log
+        $this->errors = [];
+        //Connect
+        $result = $this->connect($this->host, $this->port, $this->tval);
+        if ($result) {
+            $login_result = $this->login($this->username, $this->password);
+            if ($login_result) {
+                $this->disconnect();
+
+                return true;
+            }
+        }
+        //We need to disconnect regardless of whether the login succeeded
+        $this->disconnect();
+
+        return false;
+    }
+
+    /**
+     * Connect to a POP3 server.
+     *
+     * @param string   $host
+     * @param int|bool $port
+     * @param int      $tval
+     *
+     * @return bool
+     */
+    public function connect($host, $port = false, $tval = 30)
+    {
+        //Are we already connected?
+        if ($this->connected) {
+            return true;
+        }
+
+        //On Windows this will raise a PHP Warning error if the hostname doesn't exist.
+        //Rather than suppress it with @fsockopen, capture it cleanly instead
+        set_error_handler([$this, 'catchWarning']);
+
+        if (false === $port) {
+            $port = static::DEFAULT_PORT;
+        }
+
+        //Connect to the POP3 server
+        $errno = 0;
+        $errstr = '';
+        $this->pop_conn = fsockopen(
+            $host, //POP3 Host
+            $port, //Port #
+            $errno, //Error Number
+            $errstr, //Error Message
+            $tval
+        ); //Timeout (seconds)
+        //Restore the error handler
+        restore_error_handler();
+
+        //Did we connect?
+        if (false === $this->pop_conn) {
+            //It would appear not...
+            $this->setError(
+                "Failed to connect to server $host on port $port. errno: $errno; errstr: $errstr"
+            );
+
+            return false;
+        }
+
+        //Increase the stream time-out
+        stream_set_timeout($this->pop_conn, $tval, 0);
+
+        //Get the POP3 server response
+        $pop3_response = $this->getResponse();
+        //Check for the +OK
+        if ($this->checkResponse($pop3_response)) {
+            //The connection is established and the POP3 server is talking
+            $this->connected = true;
+
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Log in to the POP3 server.
+     * Does not support APOP (RFC 2828, 4949).
+     *
+     * @param string $username
+     * @param string $password
+     *
+     * @return bool
+     */
+    public function login($username = '', $password = '')
+    {
+        if (!$this->connected) {
+            $this->setError('Not connected to POP3 server');
+            return false;
+        }
+        if (empty($username)) {
+            $username = $this->username;
+        }
+        if (empty($password)) {
+            $password = $this->password;
+        }
+
+        //Send the Username
+        $this->sendString("USER $username" . static::LE);
+        $pop3_response = $this->getResponse();
+        if ($this->checkResponse($pop3_response)) {
+            //Send the Password
+            $this->sendString("PASS $password" . static::LE);
+            $pop3_response = $this->getResponse();
+            if ($this->checkResponse($pop3_response)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Disconnect from the POP3 server.
+     */
+    public function disconnect()
+    {
+        // If could not connect at all, no need to disconnect
+        if ($this->pop_conn === false) {
+            return;
+        }
+
+        $this->sendString('QUIT' . static::LE);
+
+        // RFC 1939 shows POP3 server sending a +OK response to the QUIT command.
+        // Try to get it.  Ignore any failures here.
+        try {
+            $this->getResponse();
+        } catch (Exception $e) {
+            //Do nothing
+        }
+
+        //The QUIT command may cause the daemon to exit, which will kill our connection
+        //So ignore errors here
+        try {
+            @fclose($this->pop_conn);
+        } catch (Exception $e) {
+            //Do nothing
+        }
+
+        // Clean up attributes.
+        $this->connected = false;
+        $this->pop_conn  = false;
+    }
+
+    /**
+     * Get a response from the POP3 server.
+     *
+     * @param int $size The maximum number of bytes to retrieve
+     *
+     * @return string
+     */
+    protected function getResponse($size = 128)
+    {
+        $response = fgets($this->pop_conn, $size);
+        if ($this->do_debug >= self::DEBUG_SERVER) {
+            echo 'Server -> Client: ', $response;
+        }
+
+        return $response;
+    }
+
+    /**
+     * Send raw data to the POP3 server.
+     *
+     * @param string $string
+     *
+     * @return int
+     */
+    protected function sendString($string)
+    {
+        if ($this->pop_conn) {
+            if ($this->do_debug >= self::DEBUG_CLIENT) { //Show client messages when debug >= 2
+                echo 'Client -> Server: ', $string;
+            }
+
+            return fwrite($this->pop_conn, $string, strlen($string));
+        }
+
+        return 0;
+    }
+
+    /**
+     * Checks the POP3 server response.
+     * Looks for for +OK or -ERR.
+     *
+     * @param string $string
+     *
+     * @return bool
+     */
+    protected function checkResponse($string)
+    {
+        if (strpos($string, '+OK') !== 0) {
+            $this->setError("Server reported an error: $string");
+
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Add an error to the internal error store.
+     * Also display debug output if it's enabled.
+     *
+     * @param string $error
+     */
+    protected function setError($error)
+    {
+        $this->errors[] = $error;
+        if ($this->do_debug >= self::DEBUG_SERVER) {
+            echo '<pre>';
+            foreach ($this->errors as $e) {
+                print_r($e);
+            }
+            echo '</pre>';
+        }
+    }
+
+    /**
+     * Get an array of error messages, if any.
+     *
+     * @return array
+     */
+    public function getErrors()
+    {
+        return $this->errors;
+    }
+
+    /**
+     * POP3 connection error handler.
+     *
+     * @param int    $errno
+     * @param string $errstr
+     * @param string $errfile
+     * @param int    $errline
+     */
+    protected function catchWarning($errno, $errstr, $errfile, $errline)
+    {
+        $this->setError(
+            'Connecting to the POP3 server raised a PHP warning:' .
+            "errno: $errno errstr: $errstr; errfile: $errfile; errline: $errline"
+        );
+    }
+}
diff --git a/src/lib/PHPMailer/SMTP.php b/src/lib/PHPMailer/SMTP.php
new file mode 100644
index 0000000..2b63840
--- /dev/null
+++ b/src/lib/PHPMailer/SMTP.php
@@ -0,0 +1,1466 @@
+<?php
+
+/**
+ * PHPMailer RFC821 SMTP email transport class.
+ * PHP Version 5.5.
+ *
+ * @see       https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
+ *
+ * @author    Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
+ * @author    Jim Jagielski (jimjag) <jimjag@gmail.com>
+ * @author    Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
+ * @author    Brent R. Matzelle (original founder)
+ * @copyright 2012 - 2020 Marcus Bointon
+ * @copyright 2010 - 2012 Jim Jagielski
+ * @copyright 2004 - 2009 Andy Prevost
+ * @license   http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
+ * @note      This program is distributed in the hope that it will be useful - WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.
+ */
+
+namespace PHPMailer\PHPMailer;
+
+/**
+ * PHPMailer RFC821 SMTP email transport class.
+ * Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server.
+ *
+ * @author Chris Ryan
+ * @author Marcus Bointon <phpmailer@synchromedia.co.uk>
+ */
+class SMTP
+{
+    /**
+     * The PHPMailer SMTP version number.
+     *
+     * @var string
+     */
+    const VERSION = '6.8.1';
+
+    /**
+     * SMTP line break constant.
+     *
+     * @var string
+     */
+    const LE = "\r\n";
+
+    /**
+     * The SMTP port to use if one is not specified.
+     *
+     * @var int
+     */
+    const DEFAULT_PORT = 25;
+
+    /**
+     * The SMTPs port to use if one is not specified.
+     *
+     * @var int
+     */
+    const DEFAULT_SECURE_PORT = 465;
+
+    /**
+     * The maximum line length allowed by RFC 5321 section 4.5.3.1.6,
+     * *excluding* a trailing CRLF break.
+     *
+     * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.6
+     *
+     * @var int
+     */
+    const MAX_LINE_LENGTH = 998;
+
+    /**
+     * The maximum line length allowed for replies in RFC 5321 section 4.5.3.1.5,
+     * *including* a trailing CRLF line break.
+     *
+     * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.5
+     *
+     * @var int
+     */
+    const MAX_REPLY_LENGTH = 512;
+
+    /**
+     * Debug level for no output.
+     *
+     * @var int
+     */
+    const DEBUG_OFF = 0;
+
+    /**
+     * Debug level to show client -> server messages.
+     *
+     * @var int
+     */
+    const DEBUG_CLIENT = 1;
+
+    /**
+     * Debug level to show client -> server and server -> client messages.
+     *
+     * @var int
+     */
+    const DEBUG_SERVER = 2;
+
+    /**
+     * Debug level to show connection status, client -> server and server -> client messages.
+     *
+     * @var int
+     */
+    const DEBUG_CONNECTION = 3;
+
+    /**
+     * Debug level to show all messages.
+     *
+     * @var int
+     */
+    const DEBUG_LOWLEVEL = 4;
+
+    /**
+     * Debug output level.
+     * Options:
+     * * self::DEBUG_OFF (`0`) No debug output, default
+     * * self::DEBUG_CLIENT (`1`) Client commands
+     * * self::DEBUG_SERVER (`2`) Client commands and server responses
+     * * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status
+     * * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages.
+     *
+     * @var int
+     */
+    public $do_debug = self::DEBUG_OFF;
+
+    /**
+     * How to handle debug output.
+     * Options:
+     * * `echo` Output plain-text as-is, appropriate for CLI
+     * * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
+     * * `error_log` Output to error log as configured in php.ini
+     * Alternatively, you can provide a callable expecting two params: a message string and the debug level:
+     *
+     * ```php
+     * $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
+     * ```
+     *
+     * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
+     * level output is used:
+     *
+     * ```php
+     * $mail->Debugoutput = new myPsr3Logger;
+     * ```
+     *
+     * @var string|callable|\Psr\Log\LoggerInterface
+     */
+    public $Debugoutput = 'echo';
+
+    /**
+     * Whether to use VERP.
+     *
+     * @see http://en.wikipedia.org/wiki/Variable_envelope_return_path
+     * @see http://www.postfix.org/VERP_README.html Info on VERP
+     *
+     * @var bool
+     */
+    public $do_verp = false;
+
+    /**
+     * The timeout value for connection, in seconds.
+     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
+     * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure.
+     *
+     * @see http://tools.ietf.org/html/rfc2821#section-4.5.3.2
+     *
+     * @var int
+     */
+    public $Timeout = 300;
+
+    /**
+     * How long to wait for commands to complete, in seconds.
+     * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
+     *
+     * @var int
+     */
+    public $Timelimit = 300;
+
+    /**
+     * Patterns to extract an SMTP transaction id from reply to a DATA command.
+     * The first capture group in each regex will be used as the ID.
+     * MS ESMTP returns the message ID, which may not be correct for internal tracking.
+     *
+     * @var string[]
+     */
+    protected $smtp_transaction_id_patterns = [
+        'exim' => '/[\d]{3} OK id=(.*)/',
+        'sendmail' => '/[\d]{3} 2.0.0 (.*) Message/',
+        'postfix' => '/[\d]{3} 2.0.0 Ok: queued as (.*)/',
+        'Microsoft_ESMTP' => '/[0-9]{3} 2.[\d].0 (.*)@(?:.*) Queued mail for delivery/',
+        'Amazon_SES' => '/[\d]{3} Ok (.*)/',
+        'SendGrid' => '/[\d]{3} Ok: queued as (.*)/',
+        'CampaignMonitor' => '/[\d]{3} 2.0.0 OK:([a-zA-Z\d]{48})/',
+        'Haraka' => '/[\d]{3} Message Queued \((.*)\)/',
+        'ZoneMTA' => '/[\d]{3} Message queued as (.*)/',
+        'Mailjet' => '/[\d]{3} OK queued as (.*)/',
+    ];
+
+    /**
+     * The last transaction ID issued in response to a DATA command,
+     * if one was detected.
+     *
+     * @var string|bool|null
+     */
+    protected $last_smtp_transaction_id;
+
+    /**
+     * The socket for the server connection.
+     *
+     * @var ?resource
+     */
+    protected $smtp_conn;
+
+    /**
+     * Error information, if any, for the last SMTP command.
+     *
+     * @var array
+     */
+    protected $error = [
+        'error' => '',
+        'detail' => '',
+        'smtp_code' => '',
+        'smtp_code_ex' => '',
+    ];
+
+    /**
+     * The reply the server sent to us for HELO.
+     * If null, no HELO string has yet been received.
+     *
+     * @var string|null
+     */
+    protected $helo_rply;
+
+    /**
+     * The set of SMTP extensions sent in reply to EHLO command.
+     * Indexes of the array are extension names.
+     * Value at index 'HELO' or 'EHLO' (according to command that was sent)
+     * represents the server name. In case of HELO it is the only element of the array.
+     * Other values can be boolean TRUE or an array containing extension options.
+     * If null, no HELO/EHLO string has yet been received.
+     *
+     * @var array|null
+     */
+    protected $server_caps;
+
+    /**
+     * The most recent reply received from the server.
+     *
+     * @var string
+     */
+    protected $last_reply = '';
+
+    /**
+     * Output debugging info via a user-selected method.
+     *
+     * @param string $str   Debug string to output
+     * @param int    $level The debug level of this message; see DEBUG_* constants
+     *
+     * @see SMTP::$Debugoutput
+     * @see SMTP::$do_debug
+     */
+    protected function edebug($str, $level = 0)
+    {
+        if ($level > $this->do_debug) {
+            return;
+        }
+        //Is this a PSR-3 logger?
+        if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
+            $this->Debugoutput->debug($str);
+
+            return;
+        }
+        //Avoid clash with built-in function names
+        if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
+            call_user_func($this->Debugoutput, $str, $level);
+
+            return;
+        }
+        switch ($this->Debugoutput) {
+            case 'error_log':
+                //Don't output, just log
+                error_log($str);
+                break;
+            case 'html':
+                //Cleans up output a bit for a better looking, HTML-safe output
+                echo gmdate('Y-m-d H:i:s'), ' ', htmlentities(
+                    preg_replace('/[\r\n]+/', '', $str),
+                    ENT_QUOTES,
+                    'UTF-8'
+                ), "<br>\n";
+                break;
+            case 'echo':
+            default:
+                //Normalize line breaks
+                $str = preg_replace('/\r\n|\r/m', "\n", $str);
+                echo gmdate('Y-m-d H:i:s'),
+                "\t",
+                    //Trim trailing space
+                trim(
+                    //Indent for readability, except for trailing break
+                    str_replace(
+                        "\n",
+                        "\n                   \t                  ",
+                        trim($str)
+                    )
+                ),
+                "\n";
+        }
+    }
+
+    /**
+     * Connect to an SMTP server.
+     *
+     * @param string $host    SMTP server IP or host name
+     * @param int    $port    The port number to connect to
+     * @param int    $timeout How long to wait for the connection to open
+     * @param array  $options An array of options for stream_context_create()
+     *
+     * @return bool
+     */
+    public function connect($host, $port = null, $timeout = 30, $options = [])
+    {
+        //Clear errors to avoid confusion
+        $this->setError('');
+        //Make sure we are __not__ connected
+        if ($this->connected()) {
+            //Already connected, generate error
+            $this->setError('Already connected to a server');
+
+            return false;
+        }
+        if (empty($port)) {
+            $port = self::DEFAULT_PORT;
+        }
+        //Connect to the SMTP server
+        $this->edebug(
+            "Connection: opening to $host:$port, timeout=$timeout, options=" .
+            (count($options) > 0 ? var_export($options, true) : 'array()'),
+            self::DEBUG_CONNECTION
+        );
+
+        $this->smtp_conn = $this->getSMTPConnection($host, $port, $timeout, $options);
+
+        if ($this->smtp_conn === false) {
+            //Error info already set inside `getSMTPConnection()`
+            return false;
+        }
+
+        $this->edebug('Connection: opened', self::DEBUG_CONNECTION);
+
+        //Get any announcement
+        $this->last_reply = $this->get_lines();
+        $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
+        $responseCode = (int)substr($this->last_reply, 0, 3);
+        if ($responseCode === 220) {
+            return true;
+        }
+        //Anything other than a 220 response means something went wrong
+        //RFC 5321 says the server will wait for us to send a QUIT in response to a 554 error
+        //https://tools.ietf.org/html/rfc5321#section-3.1
+        if ($responseCode === 554) {
+            $this->quit();
+        }
+        //This will handle 421 responses which may not wait for a QUIT (e.g. if the server is being shut down)
+        $this->edebug('Connection: closing due to error', self::DEBUG_CONNECTION);
+        $this->close();
+        return false;
+    }
+
+    /**
+     * Create connection to the SMTP server.
+     *
+     * @param string $host    SMTP server IP or host name
+     * @param int    $port    The port number to connect to
+     * @param int    $timeout How long to wait for the connection to open
+     * @param array  $options An array of options for stream_context_create()
+     *
+     * @return false|resource
+     */
+    protected function getSMTPConnection($host, $port = null, $timeout = 30, $options = [])
+    {
+        static $streamok;
+        //This is enabled by default since 5.0.0 but some providers disable it
+        //Check this once and cache the result
+        if (null === $streamok) {
+            $streamok = function_exists('stream_socket_client');
+        }
+
+        $errno = 0;
+        $errstr = '';
+        if ($streamok) {
+            $socket_context = stream_context_create($options);
+            set_error_handler([$this, 'errorHandler']);
+            $connection = stream_socket_client(
+                $host . ':' . $port,
+                $errno,
+                $errstr,
+                $timeout,
+                STREAM_CLIENT_CONNECT,
+                $socket_context
+            );
+        } else {
+            //Fall back to fsockopen which should work in more places, but is missing some features
+            $this->edebug(
+                'Connection: stream_socket_client not available, falling back to fsockopen',
+                self::DEBUG_CONNECTION
+            );
+            set_error_handler([$this, 'errorHandler']);
+            $connection = fsockopen(
+                $host,
+                $port,
+                $errno,
+                $errstr,
+                $timeout
+            );
+        }
+        restore_error_handler();
+
+        //Verify we connected properly
+        if (!is_resource($connection)) {
+            $this->setError(
+                'Failed to connect to server',
+                '',
+                (string) $errno,
+                $errstr
+            );
+            $this->edebug(
+                'SMTP ERROR: ' . $this->error['error']
+                . ": $errstr ($errno)",
+                self::DEBUG_CLIENT
+            );
+
+            return false;
+        }
+
+        //SMTP server can take longer to respond, give longer timeout for first read
+        //Windows does not have support for this timeout function
+        if (strpos(PHP_OS, 'WIN') !== 0) {
+            $max = (int)ini_get('max_execution_time');
+            //Don't bother if unlimited, or if set_time_limit is disabled
+            if (0 !== $max && $timeout > $max && strpos(ini_get('disable_functions'), 'set_time_limit') === false) {
+                @set_time_limit($timeout);
+            }
+            stream_set_timeout($connection, $timeout, 0);
+        }
+
+        return $connection;
+    }
+
+    /**
+     * Initiate a TLS (encrypted) session.
+     *
+     * @return bool
+     */
+    public function startTLS()
+    {
+        if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) {
+            return false;
+        }
+
+        //Allow the best TLS version(s) we can
+        $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
+
+        //PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT
+        //so add them back in manually if we can
+        if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
+            $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
+            $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
+        }
+
+        //Begin encrypted connection
+        set_error_handler([$this, 'errorHandler']);
+        $crypto_ok = stream_socket_enable_crypto(
+            $this->smtp_conn,
+            true,
+            $crypto_method
+        );
+        restore_error_handler();
+
+        return (bool) $crypto_ok;
+    }
+
+    /**
+     * Perform SMTP authentication.
+     * Must be run after hello().
+     *
+     * @see    hello()
+     *
+     * @param string $username The user name
+     * @param string $password The password
+     * @param string $authtype The auth type (CRAM-MD5, PLAIN, LOGIN, XOAUTH2)
+     * @param OAuthTokenProvider $OAuth An optional OAuthTokenProvider instance for XOAUTH2 authentication
+     *
+     * @return bool True if successfully authenticated
+     */
+    public function authenticate(
+        $username,
+        $password,
+        $authtype = null,
+        $OAuth = null
+    ) {
+        if (!$this->server_caps) {
+            $this->setError('Authentication is not allowed before HELO/EHLO');
+
+            return false;
+        }
+
+        if (array_key_exists('EHLO', $this->server_caps)) {
+            //SMTP extensions are available; try to find a proper authentication method
+            if (!array_key_exists('AUTH', $this->server_caps)) {
+                $this->setError('Authentication is not allowed at this stage');
+                //'at this stage' means that auth may be allowed after the stage changes
+                //e.g. after STARTTLS
+
+                return false;
+            }
+
+            $this->edebug('Auth method requested: ' . ($authtype ?: 'UNSPECIFIED'), self::DEBUG_LOWLEVEL);
+            $this->edebug(
+                'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']),
+                self::DEBUG_LOWLEVEL
+            );
+
+            //If we have requested a specific auth type, check the server supports it before trying others
+            if (null !== $authtype && !in_array($authtype, $this->server_caps['AUTH'], true)) {
+                $this->edebug('Requested auth method not available: ' . $authtype, self::DEBUG_LOWLEVEL);
+                $authtype = null;
+            }
+
+            if (empty($authtype)) {
+                //If no auth mechanism is specified, attempt to use these, in this order
+                //Try CRAM-MD5 first as it's more secure than the others
+                foreach (['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'] as $method) {
+                    if (in_array($method, $this->server_caps['AUTH'], true)) {
+                        $authtype = $method;
+                        break;
+                    }
+                }
+                if (empty($authtype)) {
+                    $this->setError('No supported authentication methods found');
+
+                    return false;
+                }
+                $this->edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL);
+            }
+
+            if (!in_array($authtype, $this->server_caps['AUTH'], true)) {
+                $this->setError("The requested authentication method \"$authtype\" is not supported by the server");
+
+                return false;
+            }
+        } elseif (empty($authtype)) {
+            $authtype = 'LOGIN';
+        }
+        switch ($authtype) {
+            case 'PLAIN':
+                //Start authentication
+                if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) {
+                    return false;
+                }
+                //Send encoded username and password
+                if (
+                    //Format from https://tools.ietf.org/html/rfc4616#section-2
+                    //We skip the first field (it's forgery), so the string starts with a null byte
+                    !$this->sendCommand(
+                        'User & Password',
+                        base64_encode("\0" . $username . "\0" . $password),
+                        235
+                    )
+                ) {
+                    return false;
+                }
+                break;
+            case 'LOGIN':
+                //Start authentication
+                if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) {
+                    return false;
+                }
+                if (!$this->sendCommand('Username', base64_encode($username), 334)) {
+                    return false;
+                }
+                if (!$this->sendCommand('Password', base64_encode($password), 235)) {
+                    return false;
+                }
+                break;
+            case 'CRAM-MD5':
+                //Start authentication
+                if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) {
+                    return false;
+                }
+                //Get the challenge
+                $challenge = base64_decode(substr($this->last_reply, 4));
+
+                //Build the response
+                $response = $username . ' ' . $this->hmac($challenge, $password);
+
+                //send encoded credentials
+                return $this->sendCommand('Username', base64_encode($response), 235);
+            case 'XOAUTH2':
+                //The OAuth instance must be set up prior to requesting auth.
+                if (null === $OAuth) {
+                    return false;
+                }
+                $oauth = $OAuth->getOauth64();
+
+                //Start authentication
+                if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
+                    return false;
+                }
+                break;
+            default:
+                $this->setError("Authentication method \"$authtype\" is not supported");
+
+                return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Calculate an MD5 HMAC hash.
+     * Works like hash_hmac('md5', $data, $key)
+     * in case that function is not available.
+     *
+     * @param string $data The data to hash
+     * @param string $key  The key to hash with
+     *
+     * @return string
+     */
+    protected function hmac($data, $key)
+    {
+        if (function_exists('hash_hmac')) {
+            return hash_hmac('md5', $data, $key);
+        }
+
+        //The following borrowed from
+        //http://php.net/manual/en/function.mhash.php#27225
+
+        //RFC 2104 HMAC implementation for php.
+        //Creates an md5 HMAC.
+        //Eliminates the need to install mhash to compute a HMAC
+        //by Lance Rushing
+
+        $bytelen = 64; //byte length for md5
+        if (strlen($key) > $bytelen) {
+            $key = pack('H*', md5($key));
+        }
+        $key = str_pad($key, $bytelen, chr(0x00));
+        $ipad = str_pad('', $bytelen, chr(0x36));
+        $opad = str_pad('', $bytelen, chr(0x5c));
+        $k_ipad = $key ^ $ipad;
+        $k_opad = $key ^ $opad;
+
+        return md5($k_opad . pack('H*', md5($k_ipad . $data)));
+    }
+
+    /**
+     * Check connection state.
+     *
+     * @return bool True if connected
+     */
+    public function connected()
+    {
+        if (is_resource($this->smtp_conn)) {
+            $sock_status = stream_get_meta_data($this->smtp_conn);
+            if ($sock_status['eof']) {
+                //The socket is valid but we are not connected
+                $this->edebug(
+                    'SMTP NOTICE: EOF caught while checking if connected',
+                    self::DEBUG_CLIENT
+                );
+                $this->close();
+
+                return false;
+            }
+
+            return true; //everything looks good
+        }
+
+        return false;
+    }
+
+    /**
+     * Close the socket and clean up the state of the class.
+     * Don't use this function without first trying to use QUIT.
+     *
+     * @see quit()
+     */
+    public function close()
+    {
+        $this->server_caps = null;
+        $this->helo_rply = null;
+        if (is_resource($this->smtp_conn)) {
+            //Close the connection and cleanup
+            fclose($this->smtp_conn);
+            $this->smtp_conn = null; //Makes for cleaner serialization
+            $this->edebug('Connection: closed', self::DEBUG_CONNECTION);
+        }
+    }
+
+    /**
+     * Send an SMTP DATA command.
+     * Issues a data command and sends the msg_data to the server,
+     * finalizing the mail transaction. $msg_data is the message
+     * that is to be sent with the headers. Each header needs to be
+     * on a single line followed by a <CRLF> with the message headers
+     * and the message body being separated by an additional <CRLF>.
+     * Implements RFC 821: DATA <CRLF>.
+     *
+     * @param string $msg_data Message data to send
+     *
+     * @return bool
+     */
+    public function data($msg_data)
+    {
+        //This will use the standard timelimit
+        if (!$this->sendCommand('DATA', 'DATA', 354)) {
+            return false;
+        }
+
+        /* The server is ready to accept data!
+         * According to rfc821 we should not send more than 1000 characters on a single line (including the LE)
+         * so we will break the data up into lines by \r and/or \n then if needed we will break each of those into
+         * smaller lines to fit within the limit.
+         * We will also look for lines that start with a '.' and prepend an additional '.'.
+         * NOTE: this does not count towards line-length limit.
+         */
+
+        //Normalize line breaks before exploding
+        $lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $msg_data));
+
+        /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field
+         * of the first line (':' separated) does not contain a space then it _should_ be a header, and we will
+         * process all lines before a blank line as headers.
+         */
+
+        $field = substr($lines[0], 0, strpos($lines[0], ':'));
+        $in_headers = false;
+        if (!empty($field) && strpos($field, ' ') === false) {
+            $in_headers = true;
+        }
+
+        foreach ($lines as $line) {
+            $lines_out = [];
+            if ($in_headers && $line === '') {
+                $in_headers = false;
+            }
+            //Break this line up into several smaller lines if it's too long
+            //Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len),
+            while (isset($line[self::MAX_LINE_LENGTH])) {
+                //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on
+                //so as to avoid breaking in the middle of a word
+                $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' ');
+                //Deliberately matches both false and 0
+                if (!$pos) {
+                    //No nice break found, add a hard break
+                    $pos = self::MAX_LINE_LENGTH - 1;
+                    $lines_out[] = substr($line, 0, $pos);
+                    $line = substr($line, $pos);
+                } else {
+                    //Break at the found point
+                    $lines_out[] = substr($line, 0, $pos);
+                    //Move along by the amount we dealt with
+                    $line = substr($line, $pos + 1);
+                }
+                //If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1
+                if ($in_headers) {
+                    $line = "\t" . $line;
+                }
+            }
+            $lines_out[] = $line;
+
+            //Send the lines to the server
+            foreach ($lines_out as $line_out) {
+                //Dot-stuffing as per RFC5321 section 4.5.2
+                //https://tools.ietf.org/html/rfc5321#section-4.5.2
+                if (!empty($line_out) && $line_out[0] === '.') {
+                    $line_out = '.' . $line_out;
+                }
+                $this->client_send($line_out . static::LE, 'DATA');
+            }
+        }
+
+        //Message data has been sent, complete the command
+        //Increase timelimit for end of DATA command
+        $savetimelimit = $this->Timelimit;
+        $this->Timelimit *= 2;
+        $result = $this->sendCommand('DATA END', '.', 250);
+        $this->recordLastTransactionID();
+        //Restore timelimit
+        $this->Timelimit = $savetimelimit;
+
+        return $result;
+    }
+
+    /**
+     * Send an SMTP HELO or EHLO command.
+     * Used to identify the sending server to the receiving server.
+     * This makes sure that client and server are in a known state.
+     * Implements RFC 821: HELO <SP> <domain> <CRLF>
+     * and RFC 2821 EHLO.
+     *
+     * @param string $host The host name or IP to connect to
+     *
+     * @return bool
+     */
+    public function hello($host = '')
+    {
+        //Try extended hello first (RFC 2821)
+        if ($this->sendHello('EHLO', $host)) {
+            return true;
+        }
+
+        //Some servers shut down the SMTP service here (RFC 5321)
+        if (substr($this->helo_rply, 0, 3) == '421') {
+            return false;
+        }
+
+        return $this->sendHello('HELO', $host);
+    }
+
+    /**
+     * Send an SMTP HELO or EHLO command.
+     * Low-level implementation used by hello().
+     *
+     * @param string $hello The HELO string
+     * @param string $host  The hostname to say we are
+     *
+     * @return bool
+     *
+     * @see hello()
+     */
+    protected function sendHello($hello, $host)
+    {
+        $noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250);
+        $this->helo_rply = $this->last_reply;
+        if ($noerror) {
+            $this->parseHelloFields($hello);
+        } else {
+            $this->server_caps = null;
+        }
+
+        return $noerror;
+    }
+
+    /**
+     * Parse a reply to HELO/EHLO command to discover server extensions.
+     * In case of HELO, the only parameter that can be discovered is a server name.
+     *
+     * @param string $type `HELO` or `EHLO`
+     */
+    protected function parseHelloFields($type)
+    {
+        $this->server_caps = [];
+        $lines = explode("\n", $this->helo_rply);
+
+        foreach ($lines as $n => $s) {
+            //First 4 chars contain response code followed by - or space
+            $s = trim(substr($s, 4));
+            if (empty($s)) {
+                continue;
+            }
+            $fields = explode(' ', $s);
+            if (!empty($fields)) {
+                if (!$n) {
+                    $name = $type;
+                    $fields = $fields[0];
+                } else {
+                    $name = array_shift($fields);
+                    switch ($name) {
+                        case 'SIZE':
+                            $fields = ($fields ? $fields[0] : 0);
+                            break;
+                        case 'AUTH':
+                            if (!is_array($fields)) {
+                                $fields = [];
+                            }
+                            break;
+                        default:
+                            $fields = true;
+                    }
+                }
+                $this->server_caps[$name] = $fields;
+            }
+        }
+    }
+
+    /**
+     * Send an SMTP MAIL command.
+     * Starts a mail transaction from the email address specified in
+     * $from. Returns true if successful or false otherwise. If True
+     * the mail transaction is started and then one or more recipient
+     * commands may be called followed by a data command.
+     * Implements RFC 821: MAIL <SP> FROM:<reverse-path> <CRLF>.
+     *
+     * @param string $from Source address of this message
+     *
+     * @return bool
+     */
+    public function mail($from)
+    {
+        $useVerp = ($this->do_verp ? ' XVERP' : '');
+
+        return $this->sendCommand(
+            'MAIL FROM',
+            'MAIL FROM:<' . $from . '>' . $useVerp,
+            250
+        );
+    }
+
+    /**
+     * Send an SMTP QUIT command.
+     * Closes the socket if there is no error or the $close_on_error argument is true.
+     * Implements from RFC 821: QUIT <CRLF>.
+     *
+     * @param bool $close_on_error Should the connection close if an error occurs?
+     *
+     * @return bool
+     */
+    public function quit($close_on_error = true)
+    {
+        $noerror = $this->sendCommand('QUIT', 'QUIT', 221);
+        $err = $this->error; //Save any error
+        if ($noerror || $close_on_error) {
+            $this->close();
+            $this->error = $err; //Restore any error from the quit command
+        }
+
+        return $noerror;
+    }
+
+    /**
+     * Send an SMTP RCPT command.
+     * Sets the TO argument to $toaddr.
+     * Returns true if the recipient was accepted false if it was rejected.
+     * Implements from RFC 821: RCPT <SP> TO:<forward-path> <CRLF>.
+     *
+     * @param string $address The address the message is being sent to
+     * @param string $dsn     Comma separated list of DSN notifications. NEVER, SUCCESS, FAILURE
+     *                        or DELAY. If you specify NEVER all other notifications are ignored.
+     *
+     * @return bool
+     */
+    public function recipient($address, $dsn = '')
+    {
+        if (empty($dsn)) {
+            $rcpt = 'RCPT TO:<' . $address . '>';
+        } else {
+            $dsn = strtoupper($dsn);
+            $notify = [];
+
+            if (strpos($dsn, 'NEVER') !== false) {
+                $notify[] = 'NEVER';
+            } else {
+                foreach (['SUCCESS', 'FAILURE', 'DELAY'] as $value) {
+                    if (strpos($dsn, $value) !== false) {
+                        $notify[] = $value;
+                    }
+                }
+            }
+
+            $rcpt = 'RCPT TO:<' . $address . '> NOTIFY=' . implode(',', $notify);
+        }
+
+        return $this->sendCommand(
+            'RCPT TO',
+            $rcpt,
+            [250, 251]
+        );
+    }
+
+    /**
+     * Send an SMTP RSET command.
+     * Abort any transaction that is currently in progress.
+     * Implements RFC 821: RSET <CRLF>.
+     *
+     * @return bool True on success
+     */
+    public function reset()
+    {
+        return $this->sendCommand('RSET', 'RSET', 250);
+    }
+
+    /**
+     * Send a command to an SMTP server and check its return code.
+     *
+     * @param string    $command       The command name - not sent to the server
+     * @param string    $commandstring The actual command to send
+     * @param int|array $expect        One or more expected integer success codes
+     *
+     * @return bool True on success
+     */
+    protected function sendCommand($command, $commandstring, $expect)
+    {
+        if (!$this->connected()) {
+            $this->setError("Called $command without being connected");
+
+            return false;
+        }
+        //Reject line breaks in all commands
+        if ((strpos($commandstring, "\n") !== false) || (strpos($commandstring, "\r") !== false)) {
+            $this->setError("Command '$command' contained line breaks");
+
+            return false;
+        }
+        $this->client_send($commandstring . static::LE, $command);
+
+        $this->last_reply = $this->get_lines();
+        //Fetch SMTP code and possible error code explanation
+        $matches = [];
+        if (preg_match('/^([\d]{3})[ -](?:([\d]\\.[\d]\\.[\d]{1,2}) )?/', $this->last_reply, $matches)) {
+            $code = (int) $matches[1];
+            $code_ex = (count($matches) > 2 ? $matches[2] : null);
+            //Cut off error code from each response line
+            $detail = preg_replace(
+                "/{$code}[ -]" .
+                ($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . '/m',
+                '',
+                $this->last_reply
+            );
+        } else {
+            //Fall back to simple parsing if regex fails
+            $code = (int) substr($this->last_reply, 0, 3);
+            $code_ex = null;
+            $detail = substr($this->last_reply, 4);
+        }
+
+        $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
+
+        if (!in_array($code, (array) $expect, true)) {
+            $this->setError(
+                "$command command failed",
+                $detail,
+                $code,
+                $code_ex
+            );
+            $this->edebug(
+                'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply,
+                self::DEBUG_CLIENT
+            );
+
+            return false;
+        }
+
+        //Don't clear the error store when using keepalive
+        if ($command !== 'RSET') {
+            $this->setError('');
+        }
+
+        return true;
+    }
+
+    /**
+     * Send an SMTP SAML command.
+     * Starts a mail transaction from the email address specified in $from.
+     * Returns true if successful or false otherwise. If True
+     * the mail transaction is started and then one or more recipient
+     * commands may be called followed by a data command. This command
+     * will send the message to the users terminal if they are logged
+     * in and send them an email.
+     * Implements RFC 821: SAML <SP> FROM:<reverse-path> <CRLF>.
+     *
+     * @param string $from The address the message is from
+     *
+     * @return bool
+     */
+    public function sendAndMail($from)
+    {
+        return $this->sendCommand('SAML', "SAML FROM:$from", 250);
+    }
+
+    /**
+     * Send an SMTP VRFY command.
+     *
+     * @param string $name The name to verify
+     *
+     * @return bool
+     */
+    public function verify($name)
+    {
+        return $this->sendCommand('VRFY', "VRFY $name", [250, 251]);
+    }
+
+    /**
+     * Send an SMTP NOOP command.
+     * Used to keep keep-alives alive, doesn't actually do anything.
+     *
+     * @return bool
+     */
+    public function noop()
+    {
+        return $this->sendCommand('NOOP', 'NOOP', 250);
+    }
+
+    /**
+     * Send an SMTP TURN command.
+     * This is an optional command for SMTP that this class does not support.
+     * This method is here to make the RFC821 Definition complete for this class
+     * and _may_ be implemented in future.
+     * Implements from RFC 821: TURN <CRLF>.
+     *
+     * @return bool
+     */
+    public function turn()
+    {
+        $this->setError('The SMTP TURN command is not implemented');
+        $this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT);
+
+        return false;
+    }
+
+    /**
+     * Send raw data to the server.
+     *
+     * @param string $data    The data to send
+     * @param string $command Optionally, the command this is part of, used only for controlling debug output
+     *
+     * @return int|bool The number of bytes sent to the server or false on error
+     */
+    public function client_send($data, $command = '')
+    {
+        //If SMTP transcripts are left enabled, or debug output is posted online
+        //it can leak credentials, so hide credentials in all but lowest level
+        if (
+            self::DEBUG_LOWLEVEL > $this->do_debug &&
+            in_array($command, ['User & Password', 'Username', 'Password'], true)
+        ) {
+            $this->edebug('CLIENT -> SERVER: [credentials hidden]', self::DEBUG_CLIENT);
+        } else {
+            $this->edebug('CLIENT -> SERVER: ' . $data, self::DEBUG_CLIENT);
+        }
+        set_error_handler([$this, 'errorHandler']);
+        $result = fwrite($this->smtp_conn, $data);
+        restore_error_handler();
+
+        return $result;
+    }
+
+    /**
+     * Get the latest error.
+     *
+     * @return array
+     */
+    public function getError()
+    {
+        return $this->error;
+    }
+
+    /**
+     * Get SMTP extensions available on the server.
+     *
+     * @return array|null
+     */
+    public function getServerExtList()
+    {
+        return $this->server_caps;
+    }
+
+    /**
+     * Get metadata about the SMTP server from its HELO/EHLO response.
+     * The method works in three ways, dependent on argument value and current state:
+     *   1. HELO/EHLO has not been sent - returns null and populates $this->error.
+     *   2. HELO has been sent -
+     *     $name == 'HELO': returns server name
+     *     $name == 'EHLO': returns boolean false
+     *     $name == any other string: returns null and populates $this->error
+     *   3. EHLO has been sent -
+     *     $name == 'HELO'|'EHLO': returns the server name
+     *     $name == any other string: if extension $name exists, returns True
+     *       or its options (e.g. AUTH mechanisms supported). Otherwise returns False.
+     *
+     * @param string $name Name of SMTP extension or 'HELO'|'EHLO'
+     *
+     * @return string|bool|null
+     */
+    public function getServerExt($name)
+    {
+        if (!$this->server_caps) {
+            $this->setError('No HELO/EHLO was sent');
+
+            return null;
+        }
+
+        if (!array_key_exists($name, $this->server_caps)) {
+            if ('HELO' === $name) {
+                return $this->server_caps['EHLO'];
+            }
+            if ('EHLO' === $name || array_key_exists('EHLO', $this->server_caps)) {
+                return false;
+            }
+            $this->setError('HELO handshake was used; No information about server extensions available');
+
+            return null;
+        }
+
+        return $this->server_caps[$name];
+    }
+
+    /**
+     * Get the last reply from the server.
+     *
+     * @return string
+     */
+    public function getLastReply()
+    {
+        return $this->last_reply;
+    }
+
+    /**
+     * Read the SMTP server's response.
+     * Either before eof or socket timeout occurs on the operation.
+     * With SMTP we can tell if we have more lines to read if the
+     * 4th character is '-' symbol. If it is a space then we don't
+     * need to read anything else.
+     *
+     * @return string
+     */
+    protected function get_lines()
+    {
+        //If the connection is bad, give up straight away
+        if (!is_resource($this->smtp_conn)) {
+            return '';
+        }
+        $data = '';
+        $endtime = 0;
+        stream_set_timeout($this->smtp_conn, $this->Timeout);
+        if ($this->Timelimit > 0) {
+            $endtime = time() + $this->Timelimit;
+        }
+        $selR = [$this->smtp_conn];
+        $selW = null;
+        while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) {
+            //Must pass vars in here as params are by reference
+            //solution for signals inspired by https://github.com/symfony/symfony/pull/6540
+            set_error_handler([$this, 'errorHandler']);
+            $n = stream_select($selR, $selW, $selW, $this->Timelimit);
+            restore_error_handler();
+
+            if ($n === false) {
+                $message = $this->getError()['detail'];
+
+                $this->edebug(
+                    'SMTP -> get_lines(): select failed (' . $message . ')',
+                    self::DEBUG_LOWLEVEL
+                );
+
+                //stream_select returns false when the `select` system call is interrupted
+                //by an incoming signal, try the select again
+                if (stripos($message, 'interrupted system call') !== false) {
+                    $this->edebug(
+                        'SMTP -> get_lines(): retrying stream_select',
+                        self::DEBUG_LOWLEVEL
+                    );
+                    $this->setError('');
+                    continue;
+                }
+
+                break;
+            }
+
+            if (!$n) {
+                $this->edebug(
+                    'SMTP -> get_lines(): select timed-out in (' . $this->Timelimit . ' sec)',
+                    self::DEBUG_LOWLEVEL
+                );
+                break;
+            }
+
+            //Deliberate noise suppression - errors are handled afterwards
+            $str = @fgets($this->smtp_conn, self::MAX_REPLY_LENGTH);
+            $this->edebug('SMTP INBOUND: "' . trim($str) . '"', self::DEBUG_LOWLEVEL);
+            $data .= $str;
+            //If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled),
+            //or 4th character is a space or a line break char, we are done reading, break the loop.
+            //String array access is a significant micro-optimisation over strlen
+            if (!isset($str[3]) || $str[3] === ' ' || $str[3] === "\r" || $str[3] === "\n") {
+                break;
+            }
+            //Timed-out? Log and break
+            $info = stream_get_meta_data($this->smtp_conn);
+            if ($info['timed_out']) {
+                $this->edebug(
+                    'SMTP -> get_lines(): stream timed-out (' . $this->Timeout . ' sec)',
+                    self::DEBUG_LOWLEVEL
+                );
+                break;
+            }
+            //Now check if reads took too long
+            if ($endtime && time() > $endtime) {
+                $this->edebug(
+                    'SMTP -> get_lines(): timelimit reached (' .
+                    $this->Timelimit . ' sec)',
+                    self::DEBUG_LOWLEVEL
+                );
+                break;
+            }
+        }
+
+        return $data;
+    }
+
+    /**
+     * Enable or disable VERP address generation.
+     *
+     * @param bool $enabled
+     */
+    public function setVerp($enabled = false)
+    {
+        $this->do_verp = $enabled;
+    }
+
+    /**
+     * Get VERP address generation mode.
+     *
+     * @return bool
+     */
+    public function getVerp()
+    {
+        return $this->do_verp;
+    }
+
+    /**
+     * Set error messages and codes.
+     *
+     * @param string $message      The error message
+     * @param string $detail       Further detail on the error
+     * @param string $smtp_code    An associated SMTP error code
+     * @param string $smtp_code_ex Extended SMTP code
+     */
+    protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '')
+    {
+        $this->error = [
+            'error' => $message,
+            'detail' => $detail,
+            'smtp_code' => $smtp_code,
+            'smtp_code_ex' => $smtp_code_ex,
+        ];
+    }
+
+    /**
+     * Set debug output method.
+     *
+     * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it
+     */
+    public function setDebugOutput($method = 'echo')
+    {
+        $this->Debugoutput = $method;
+    }
+
+    /**
+     * Get debug output method.
+     *
+     * @return string
+     */
+    public function getDebugOutput()
+    {
+        return $this->Debugoutput;
+    }
+
+    /**
+     * Set debug output level.
+     *
+     * @param int $level
+     */
+    public function setDebugLevel($level = 0)
+    {
+        $this->do_debug = $level;
+    }
+
+    /**
+     * Get debug output level.
+     *
+     * @return int
+     */
+    public function getDebugLevel()
+    {
+        return $this->do_debug;
+    }
+
+    /**
+     * Set SMTP timeout.
+     *
+     * @param int $timeout The timeout duration in seconds
+     */
+    public function setTimeout($timeout = 0)
+    {
+        $this->Timeout = $timeout;
+    }
+
+    /**
+     * Get SMTP timeout.
+     *
+     * @return int
+     */
+    public function getTimeout()
+    {
+        return $this->Timeout;
+    }
+
+    /**
+     * Reports an error number and string.
+     *
+     * @param int    $errno   The error number returned by PHP
+     * @param string $errmsg  The error message returned by PHP
+     * @param string $errfile The file the error occurred in
+     * @param int    $errline The line number the error occurred on
+     */
+    protected function errorHandler($errno, $errmsg, $errfile = '', $errline = 0)
+    {
+        $notice = 'Connection failed.';
+        $this->setError(
+            $notice,
+            $errmsg,
+            (string) $errno
+        );
+        $this->edebug(
+            "$notice Error #$errno: $errmsg [$errfile line $errline]",
+            self::DEBUG_CONNECTION
+        );
+    }
+
+    /**
+     * Extract and return the ID of the last SMTP transaction based on
+     * a list of patterns provided in SMTP::$smtp_transaction_id_patterns.
+     * Relies on the host providing the ID in response to a DATA command.
+     * If no reply has been received yet, it will return null.
+     * If no pattern was matched, it will return false.
+     *
+     * @return bool|string|null
+     */
+    protected function recordLastTransactionID()
+    {
+        $reply = $this->getLastReply();
+
+        if (empty($reply)) {
+            $this->last_smtp_transaction_id = null;
+        } else {
+            $this->last_smtp_transaction_id = false;
+            foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) {
+                $matches = [];
+                if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) {
+                    $this->last_smtp_transaction_id = trim($matches[1]);
+                    break;
+                }
+            }
+        }
+
+        return $this->last_smtp_transaction_id;
+    }
+
+    /**
+     * Get the queue/transaction ID of the last SMTP transaction
+     * If no reply has been received yet, it will return null.
+     * If no pattern was matched, it will return false.
+     *
+     * @return bool|string|null
+     *
+     * @see recordLastTransactionID()
+     */
+    public function getLastTransactionID()
+    {
+        return $this->last_smtp_transaction_id;
+    }
+}
diff --git a/src/lib/WebAuthn/Attestation/AttestationObject.php b/src/lib/WebAuthn/Attestation/AttestationObject.php
new file mode 100644
index 0000000..65151ea
--- /dev/null
+++ b/src/lib/WebAuthn/Attestation/AttestationObject.php
@@ -0,0 +1,179 @@
+<?php
+
+namespace lbuchs\WebAuthn\Attestation;
+use lbuchs\WebAuthn\WebAuthnException;
+use lbuchs\WebAuthn\CBOR\CborDecoder;
+use lbuchs\WebAuthn\Binary\ByteBuffer;
+
+/**
+ * @author Lukas Buchs
+ * @license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT
+ */
+class AttestationObject {
+    private $_authenticatorData;
+    private $_attestationFormat;
+    private $_attestationFormatName;
+
+    public function __construct($binary , $allowedFormats) {
+        $enc = CborDecoder::decode($binary);
+        // validation
+        if (!\is_array($enc) || !\array_key_exists('fmt', $enc) || !is_string($enc['fmt'])) {
+            throw new WebAuthnException('invalid attestation format', WebAuthnException::INVALID_DATA);
+        }
+
+        if (!\array_key_exists('attStmt', $enc) || !\is_array($enc['attStmt'])) {
+            throw new WebAuthnException('invalid attestation format (attStmt not available)', WebAuthnException::INVALID_DATA);
+        }
+
+        if (!\array_key_exists('authData', $enc) || !\is_object($enc['authData']) || !($enc['authData'] instanceof ByteBuffer)) {
+            throw new WebAuthnException('invalid attestation format (authData not available)', WebAuthnException::INVALID_DATA);
+        }
+
+        $this->_authenticatorData = new AuthenticatorData($enc['authData']->getBinaryString());
+        $this->_attestationFormatName = $enc['fmt'];
+
+        // Format ok?
+        if (!in_array($this->_attestationFormatName, $allowedFormats)) {
+            throw new WebAuthnException('invalid atttestation format: ' . $this->_attestationFormatName, WebAuthnException::INVALID_DATA);
+        }
+
+
+        switch ($this->_attestationFormatName) {
+            case 'android-key': $this->_attestationFormat = new Format\AndroidKey($enc, $this->_authenticatorData); break;
+            case 'android-safetynet': $this->_attestationFormat = new Format\AndroidSafetyNet($enc, $this->_authenticatorData); break;
+            case 'apple': $this->_attestationFormat = new Format\Apple($enc, $this->_authenticatorData); break;
+            case 'fido-u2f': $this->_attestationFormat = new Format\U2f($enc, $this->_authenticatorData); break;
+            case 'none': $this->_attestationFormat = new Format\None($enc, $this->_authenticatorData); break;
+            case 'packed': $this->_attestationFormat = new Format\Packed($enc, $this->_authenticatorData); break;
+            case 'tpm': $this->_attestationFormat = new Format\Tpm($enc, $this->_authenticatorData); break;
+            default: throw new WebAuthnException('invalid attestation format: ' . $enc['fmt'], WebAuthnException::INVALID_DATA);
+        }
+    }
+
+    /**
+     * returns the attestation format name
+     * @return string
+     */
+    public function getAttestationFormatName() {
+        return $this->_attestationFormatName;
+    }
+
+    /**
+     * returns the attestation format class
+     * @return Format\FormatBase
+     */
+    public function getAttestationFormat() {
+        return $this->_attestationFormat;
+    }
+
+    /**
+     * returns the attestation public key in PEM format
+     * @return AuthenticatorData
+     */
+    public function getAuthenticatorData() {
+        return $this->_authenticatorData;
+    }
+
+    /**
+     * returns the certificate chain as PEM
+     * @return string|null
+     */
+    public function getCertificateChain() {
+        return $this->_attestationFormat->getCertificateChain();
+    }
+
+    /**
+     * return the certificate issuer as string
+     * @return string
+     */
+    public function getCertificateIssuer() {
+        $pem = $this->getCertificatePem();
+        $issuer = '';
+        if ($pem) {
+            $certInfo = \openssl_x509_parse($pem);
+            if (\is_array($certInfo) && \array_key_exists('issuer', $certInfo) && \is_array($certInfo['issuer'])) {
+
+                $cn = $certInfo['issuer']['CN'] ?? '';
+                $o = $certInfo['issuer']['O'] ?? '';
+                $ou = $certInfo['issuer']['OU'] ?? '';
+
+                if ($cn) {
+                    $issuer .= $cn;
+                }
+                if ($issuer && ($o || $ou)) {
+                    $issuer .= ' (' . trim($o . ' ' . $ou) . ')';
+                } else {
+                    $issuer .= trim($o . ' ' . $ou);
+                }
+            }
+        }
+
+        return $issuer;
+    }
+
+    /**
+     * return the certificate subject as string
+     * @return string
+     */
+    public function getCertificateSubject() {
+        $pem = $this->getCertificatePem();
+        $subject = '';
+        if ($pem) {
+            $certInfo = \openssl_x509_parse($pem);
+            if (\is_array($certInfo) && \array_key_exists('subject', $certInfo) && \is_array($certInfo['subject'])) {
+
+                $cn = $certInfo['subject']['CN'] ?? '';
+                $o = $certInfo['subject']['O'] ?? '';
+                $ou = $certInfo['subject']['OU'] ?? '';
+
+                if ($cn) {
+                    $subject .= $cn;
+                }
+                if ($subject && ($o || $ou)) {
+                    $subject .= ' (' . trim($o . ' ' . $ou) . ')';
+                } else {
+                    $subject .= trim($o . ' ' . $ou);
+                }
+            }
+        }
+
+        return $subject;
+    }
+
+    /**
+     * returns the key certificate in PEM format
+     * @return string
+     */
+    public function getCertificatePem() {
+        return $this->_attestationFormat->getCertificatePem();
+    }
+
+    /**
+     * checks validity of the signature
+     * @param string $clientDataHash
+     * @return bool
+     * @throws WebAuthnException
+     */
+    public function validateAttestation($clientDataHash) {
+        return $this->_attestationFormat->validateAttestation($clientDataHash);
+    }
+
+    /**
+     * validates the certificate against root certificates
+     * @param array $rootCas
+     * @return boolean
+     * @throws WebAuthnException
+     */
+    public function validateRootCertificate($rootCas) {
+        return $this->_attestationFormat->validateRootCertificate($rootCas);
+    }
+
+    /**
+     * checks if the RpId-Hash is valid
+     * @param string$rpIdHash
+     * @return bool
+     */
+    public function validateRpIdHash($rpIdHash) {
+        return $rpIdHash === $this->_authenticatorData->getRpIdHash();
+    }
+}
diff --git a/src/lib/WebAuthn/Attestation/AuthenticatorData.php b/src/lib/WebAuthn/Attestation/AuthenticatorData.php
new file mode 100644
index 0000000..0a9eec8
--- /dev/null
+++ b/src/lib/WebAuthn/Attestation/AuthenticatorData.php
@@ -0,0 +1,481 @@
+<?php
+
+namespace lbuchs\WebAuthn\Attestation;
+use lbuchs\WebAuthn\WebAuthnException;
+use lbuchs\WebAuthn\CBOR\CborDecoder;
+use lbuchs\WebAuthn\Binary\ByteBuffer;
+
+/**
+ * @author Lukas Buchs
+ * @license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT
+ */
+class AuthenticatorData {
+    protected $_binary;
+    protected $_rpIdHash;
+    protected $_flags;
+    protected $_signCount;
+    protected $_attestedCredentialData;
+    protected $_extensionData;
+
+
+
+    // Cose encoded keys
+    private static $_COSE_KTY = 1;
+    private static $_COSE_ALG = 3;
+
+    // Cose curve
+    private static $_COSE_CRV = -1;
+    private static $_COSE_X = -2;
+    private static $_COSE_Y = -3;
+
+    // Cose RSA PS256
+    private static $_COSE_N = -1;
+    private static $_COSE_E = -2;
+
+    // EC2 key type
+    private static $_EC2_TYPE = 2;
+    private static $_EC2_ES256 = -7;
+    private static $_EC2_P256 = 1;
+
+    // RSA key type
+    private static $_RSA_TYPE = 3;
+    private static $_RSA_RS256 = -257;
+
+    // OKP key type
+    private static $_OKP_TYPE = 1;
+    private static $_OKP_ED25519 = 6;
+    private static $_OKP_EDDSA = -8;
+
+    /**
+     * Parsing the authenticatorData binary.
+     * @param string $binary
+     * @throws WebAuthnException
+     */
+    public function __construct($binary) {
+        if (!\is_string($binary) || \strlen($binary) < 37) {
+            throw new WebAuthnException('Invalid authenticatorData input', WebAuthnException::INVALID_DATA);
+        }
+        $this->_binary = $binary;
+
+        // Read infos from binary
+        // https://www.w3.org/TR/webauthn/#sec-authenticator-data
+
+        // RP ID
+        $this->_rpIdHash = \substr($binary, 0, 32);
+
+        // flags (1 byte)
+        $flags = \unpack('Cflags', \substr($binary, 32, 1))['flags'];
+        $this->_flags = $this->_readFlags($flags);
+
+        // signature counter: 32-bit unsigned big-endian integer.
+        $this->_signCount = \unpack('Nsigncount', \substr($binary, 33, 4))['signcount'];
+
+        $offset = 37;
+        // https://www.w3.org/TR/webauthn/#sec-attested-credential-data
+        if ($this->_flags->attestedDataIncluded) {
+            $this->_attestedCredentialData = $this->_readAttestData($binary, $offset);
+        }
+
+        if ($this->_flags->extensionDataIncluded) {
+            $this->_readExtensionData(\substr($binary, $offset));
+        }
+    }
+
+    /**
+     * Authenticator Attestation Globally Unique Identifier, a unique number
+     * that identifies the model of the authenticator (not the specific instance
+     * of the authenticator)
+     * The aaguid may be 0 if the user is using a old u2f device and/or if
+     * the browser is using the fido-u2f format.
+     * @return string
+     * @throws WebAuthnException
+     */
+    public function getAAGUID() {
+        if (!($this->_attestedCredentialData instanceof \stdClass)) {
+            throw  new WebAuthnException('credential data not included in authenticator data', WebAuthnException::INVALID_DATA);
+        }
+        return $this->_attestedCredentialData->aaguid;
+    }
+
+    /**
+     * returns the authenticatorData as binary
+     * @return string
+     */
+    public function getBinary() {
+        return $this->_binary;
+    }
+
+    /**
+     * returns the credentialId
+     * @return string
+     * @throws WebAuthnException
+     */
+    public function getCredentialId() {
+        if (!($this->_attestedCredentialData instanceof \stdClass)) {
+            throw  new WebAuthnException('credential id not included in authenticator data', WebAuthnException::INVALID_DATA);
+        }
+        return $this->_attestedCredentialData->credentialId;
+    }
+
+    /**
+     * returns the public key in PEM format
+     * @return string
+     */
+    public function getPublicKeyPem() {
+        if (!($this->_attestedCredentialData instanceof \stdClass) || !isset($this->_attestedCredentialData->credentialPublicKey)) {
+            throw  new WebAuthnException('credential data not included in authenticator data', WebAuthnException::INVALID_DATA);
+        }
+        
+        $der = null;
+        switch ($this->_attestedCredentialData->credentialPublicKey->kty ?? null) {
+            case self::$_EC2_TYPE: $der = $this->_getEc2Der(); break;
+            case self::$_RSA_TYPE: $der = $this->_getRsaDer(); break;
+            case self::$_OKP_TYPE: $der = $this->_getOkpDer(); break;
+            default: throw new WebAuthnException('invalid key type', WebAuthnException::INVALID_DATA);
+        }
+
+        $pem = '-----BEGIN PUBLIC KEY-----' . "\n";
+        $pem .= \chunk_split(\base64_encode($der), 64, "\n");
+        $pem .= '-----END PUBLIC KEY-----' . "\n";
+        return $pem;
+    }
+
+    /**
+     * returns the public key in U2F format
+     * @return string
+     * @throws WebAuthnException
+     */
+    public function getPublicKeyU2F() {
+        if (!($this->_attestedCredentialData instanceof \stdClass) || !isset($this->_attestedCredentialData->credentialPublicKey)) {
+            throw  new WebAuthnException('credential data not included in authenticator data', WebAuthnException::INVALID_DATA);
+        }
+        if (($this->_attestedCredentialData->credentialPublicKey->kty ?? null) !== self::$_EC2_TYPE) {
+            throw new WebAuthnException('signature algorithm not ES256', WebAuthnException::INVALID_PUBLIC_KEY);
+        }
+        return "\x04" . // ECC uncompressed
+                $this->_attestedCredentialData->credentialPublicKey->x .
+                $this->_attestedCredentialData->credentialPublicKey->y;
+    }
+
+    /**
+     * returns the SHA256 hash of the relying party id (=hostname)
+     * @return string
+     */
+    public function getRpIdHash() {
+        return $this->_rpIdHash;
+    }
+
+    /**
+     * returns the sign counter
+     * @return int
+     */
+    public function getSignCount() {
+        return $this->_signCount;
+    }
+
+    /**
+     * returns true if the user is present
+     * @return boolean
+     */
+    public function getUserPresent() {
+        return $this->_flags->userPresent;
+    }
+
+    /**
+     * returns true if the user is verified
+     * @return boolean
+     */
+    public function getUserVerified() {
+        return $this->_flags->userVerified;
+    }
+
+    // -----------------------------------------------
+    // PRIVATE
+    // -----------------------------------------------
+
+    /**
+     * Returns DER encoded EC2 key
+     * @return string
+     */
+    private function _getEc2Der() {
+        return $this->_der_sequence(
+            $this->_der_sequence(
+                $this->_der_oid("\x2A\x86\x48\xCE\x3D\x02\x01") . // OID 1.2.840.10045.2.1 ecPublicKey
+                $this->_der_oid("\x2A\x86\x48\xCE\x3D\x03\x01\x07")  // 1.2.840.10045.3.1.7 prime256v1
+            ) .
+            $this->_der_bitString($this->getPublicKeyU2F())
+        );
+    }
+
+    /**
+     * Returns DER encoded EdDSA key
+     * @return string
+     */
+    private function _getOkpDer() {
+        return $this->_der_sequence(
+            $this->_der_sequence(
+                $this->_der_oid("\x2B\x65\x70") // OID 1.3.101.112 curveEd25519 (EdDSA 25519 signature algorithm)
+            ) .
+            $this->_der_bitString($this->_attestedCredentialData->credentialPublicKey->x)
+        );
+    }
+
+    /**
+     * Returns DER encoded RSA key
+     * @return string
+     */
+    private function _getRsaDer() {
+        return $this->_der_sequence(
+            $this->_der_sequence(
+                $this->_der_oid("\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01") . // OID 1.2.840.113549.1.1.1 rsaEncryption
+                $this->_der_nullValue()
+            ) .
+            $this->_der_bitString(
+                $this->_der_sequence(
+                    $this->_der_unsignedInteger($this->_attestedCredentialData->credentialPublicKey->n) .
+                    $this->_der_unsignedInteger($this->_attestedCredentialData->credentialPublicKey->e)
+                )
+            )
+        );
+    }
+
+    /**
+     * reads the flags from flag byte
+     * @param string $binFlag
+     * @return \stdClass
+     */
+    private function _readFlags($binFlag) {
+        $flags = new \stdClass();
+
+        $flags->bit_0 = !!($binFlag & 1);
+        $flags->bit_1 = !!($binFlag & 2);
+        $flags->bit_2 = !!($binFlag & 4);
+        $flags->bit_3 = !!($binFlag & 8);
+        $flags->bit_4 = !!($binFlag & 16);
+        $flags->bit_5 = !!($binFlag & 32);
+        $flags->bit_6 = !!($binFlag & 64);
+        $flags->bit_7 = !!($binFlag & 128);
+
+        // named flags
+        $flags->userPresent = $flags->bit_0;
+        $flags->userVerified = $flags->bit_2;
+        $flags->attestedDataIncluded = $flags->bit_6;
+        $flags->extensionDataIncluded = $flags->bit_7;
+        return $flags;
+    }
+
+    /**
+     * read attested data
+     * @param string $binary
+     * @param int $endOffset
+     * @return \stdClass
+     * @throws WebAuthnException
+     */
+    private function _readAttestData($binary, &$endOffset) {
+        $attestedCData = new \stdClass();
+        if (\strlen($binary) <= 55) {
+            throw new WebAuthnException('Attested data should be present but is missing', WebAuthnException::INVALID_DATA);
+        }
+
+        // The AAGUID of the authenticator
+        $attestedCData->aaguid = \substr($binary, 37, 16);
+
+        //Byte length L of Credential ID, 16-bit unsigned big-endian integer.
+        $length = \unpack('nlength', \substr($binary, 53, 2))['length'];
+        $attestedCData->credentialId = \substr($binary, 55, $length);
+
+        // set end offset
+        $endOffset = 55 + $length;
+
+        // extract public key
+        $attestedCData->credentialPublicKey = $this->_readCredentialPublicKey($binary, 55 + $length, $endOffset);
+
+        return $attestedCData;
+    }
+
+    /**
+     * reads COSE key-encoded elliptic curve public key in EC2 format
+     * @param string $binary
+     * @param int $endOffset
+     * @return \stdClass
+     * @throws WebAuthnException
+     */
+    private function _readCredentialPublicKey($binary, $offset, &$endOffset) {
+        $enc = CborDecoder::decodeInPlace($binary, $offset, $endOffset);
+
+        // COSE key-encoded elliptic curve public key in EC2 format
+        $credPKey = new \stdClass();
+        $credPKey->kty = $enc[self::$_COSE_KTY];
+        $credPKey->alg = $enc[self::$_COSE_ALG];
+
+        switch ($credPKey->alg) {
+            case self::$_EC2_ES256: $this->_readCredentialPublicKeyES256($credPKey, $enc); break;
+            case self::$_RSA_RS256: $this->_readCredentialPublicKeyRS256($credPKey, $enc); break;
+            case self::$_OKP_EDDSA: $this->_readCredentialPublicKeyEDDSA($credPKey, $enc); break;
+        }
+
+        return $credPKey;
+    }
+
+    /**
+     * extract EDDSA informations from cose
+     * @param \stdClass $credPKey
+     * @param \stdClass $enc
+     * @throws WebAuthnException
+     */
+    private function _readCredentialPublicKeyEDDSA(&$credPKey, $enc) {
+        $credPKey->crv = $enc[self::$_COSE_CRV];
+        $credPKey->x   = $enc[self::$_COSE_X] instanceof ByteBuffer ? $enc[self::$_COSE_X]->getBinaryString() : null;
+        unset ($enc);
+
+        // Validation
+        if ($credPKey->kty !== self::$_OKP_TYPE) {
+            throw new WebAuthnException('public key not in OKP format', WebAuthnException::INVALID_PUBLIC_KEY);
+        }
+
+        if ($credPKey->alg !== self::$_OKP_EDDSA) {
+            throw new WebAuthnException('signature algorithm not EdDSA', WebAuthnException::INVALID_PUBLIC_KEY);
+        }
+
+        if ($credPKey->crv !== self::$_OKP_ED25519) {
+            throw new WebAuthnException('curve not Ed25519', WebAuthnException::INVALID_PUBLIC_KEY);
+        }
+
+        if (\strlen($credPKey->x) !== 32) {
+            throw new WebAuthnException('Invalid X-coordinate', WebAuthnException::INVALID_PUBLIC_KEY);
+        }
+    }
+
+    /**
+     * extract ES256 informations from cose
+     * @param \stdClass $credPKey
+     * @param \stdClass $enc
+     * @throws WebAuthnException
+     */
+    private function _readCredentialPublicKeyES256(&$credPKey, $enc) {
+        $credPKey->crv = $enc[self::$_COSE_CRV];
+        $credPKey->x   = $enc[self::$_COSE_X] instanceof ByteBuffer ? $enc[self::$_COSE_X]->getBinaryString() : null;
+        $credPKey->y   = $enc[self::$_COSE_Y] instanceof ByteBuffer ? $enc[self::$_COSE_Y]->getBinaryString() : null;
+        unset ($enc);
+
+        // Validation
+        if ($credPKey->kty !== self::$_EC2_TYPE) {
+            throw new WebAuthnException('public key not in EC2 format', WebAuthnException::INVALID_PUBLIC_KEY);
+        }
+
+        if ($credPKey->alg !== self::$_EC2_ES256) {
+            throw new WebAuthnException('signature algorithm not ES256', WebAuthnException::INVALID_PUBLIC_KEY);
+        }
+
+        if ($credPKey->crv !== self::$_EC2_P256) {
+            throw new WebAuthnException('curve not P-256', WebAuthnException::INVALID_PUBLIC_KEY);
+        }
+
+        if (\strlen($credPKey->x) !== 32) {
+            throw new WebAuthnException('Invalid X-coordinate', WebAuthnException::INVALID_PUBLIC_KEY);
+        }
+
+        if (\strlen($credPKey->y) !== 32) {
+            throw new WebAuthnException('Invalid Y-coordinate', WebAuthnException::INVALID_PUBLIC_KEY);
+        }
+    }
+
+    /**
+     * extract RS256 informations from COSE
+     * @param \stdClass $credPKey
+     * @param \stdClass $enc
+     * @throws WebAuthnException
+     */
+    private function _readCredentialPublicKeyRS256(&$credPKey, $enc) {
+        $credPKey->n = $enc[self::$_COSE_N] instanceof ByteBuffer ? $enc[self::$_COSE_N]->getBinaryString() : null;
+        $credPKey->e = $enc[self::$_COSE_E] instanceof ByteBuffer ? $enc[self::$_COSE_E]->getBinaryString() : null;
+        unset ($enc);
+
+        // Validation
+        if ($credPKey->kty !== self::$_RSA_TYPE) {
+            throw new WebAuthnException('public key not in RSA format', WebAuthnException::INVALID_PUBLIC_KEY);
+        }
+
+        if ($credPKey->alg !== self::$_RSA_RS256) {
+            throw new WebAuthnException('signature algorithm not ES256', WebAuthnException::INVALID_PUBLIC_KEY);
+        }
+
+        if (\strlen($credPKey->n) !== 256) {
+            throw new WebAuthnException('Invalid RSA modulus', WebAuthnException::INVALID_PUBLIC_KEY);
+        }
+
+        if (\strlen($credPKey->e) !== 3) {
+            throw new WebAuthnException('Invalid RSA public exponent', WebAuthnException::INVALID_PUBLIC_KEY);
+        }
+
+    }
+
+    /**
+     * reads cbor encoded extension data.
+     * @param string $binary
+     * @return array
+     * @throws WebAuthnException
+     */
+    private function _readExtensionData($binary) {
+        $ext = CborDecoder::decode($binary);
+        if (!\is_array($ext)) {
+            throw new WebAuthnException('invalid extension data', WebAuthnException::INVALID_DATA);
+        }
+
+        return $ext;
+    }
+
+
+    // ---------------
+    // DER functions
+    // ---------------
+
+    private function _der_length($len) {
+        if ($len < 128) {
+            return \chr($len);
+        }
+        $lenBytes = '';
+        while ($len > 0) {
+            $lenBytes = \chr($len % 256) . $lenBytes;
+            $len = \intdiv($len, 256);
+        }
+        return \chr(0x80 | \strlen($lenBytes)) . $lenBytes;
+    }
+
+    private function _der_sequence($contents) {
+        return "\x30" . $this->_der_length(\strlen($contents)) . $contents;
+    }
+
+    private function _der_oid($encoded) {
+        return "\x06" . $this->_der_length(\strlen($encoded)) . $encoded;
+    }
+
+    private function _der_bitString($bytes) {
+        return "\x03" . $this->_der_length(\strlen($bytes) + 1) . "\x00" . $bytes;
+    }
+
+    private function _der_nullValue() {
+        return "\x05\x00";
+    }
+
+    private function _der_unsignedInteger($bytes) {
+        $len = \strlen($bytes);
+
+        // Remove leading zero bytes
+        for ($i = 0; $i < ($len - 1); $i++) {
+            if (\ord($bytes[$i]) !== 0) {
+                break;
+            }
+        }
+        if ($i !== 0) {
+            $bytes = \substr($bytes, $i);
+        }
+
+        // If most significant bit is set, prefix with another zero to prevent it being seen as negative number
+        if ((\ord($bytes[0]) & 0x80) !== 0) {
+            $bytes = "\x00" . $bytes;
+        }
+
+        return "\x02" . $this->_der_length(\strlen($bytes)) . $bytes;
+    }
+}
diff --git a/src/lib/WebAuthn/Attestation/Format/AndroidKey.php b/src/lib/WebAuthn/Attestation/Format/AndroidKey.php
new file mode 100644
index 0000000..4581272
--- /dev/null
+++ b/src/lib/WebAuthn/Attestation/Format/AndroidKey.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace lbuchs\WebAuthn\Attestation\Format;
+use lbuchs\WebAuthn\Attestation\AuthenticatorData;
+use lbuchs\WebAuthn\WebAuthnException;
+use lbuchs\WebAuthn\Binary\ByteBuffer;
+
+class AndroidKey extends FormatBase {
+    private $_alg;
+    private $_signature;
+    private $_x5c;
+
+    public function __construct($AttestionObject, AuthenticatorData $authenticatorData) {
+        parent::__construct($AttestionObject, $authenticatorData);
+
+        // check u2f data
+        $attStmt = $this->_attestationObject['attStmt'];
+
+        if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) {
+            throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA);
+        }
+
+        if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) {
+            throw new WebAuthnException('no signature found', WebAuthnException::INVALID_DATA);
+        }
+
+        if (!\array_key_exists('x5c', $attStmt) || !\is_array($attStmt['x5c']) || \count($attStmt['x5c']) < 1) {
+            throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
+        }
+
+        if (!\is_object($attStmt['x5c'][0]) || !($attStmt['x5c'][0] instanceof ByteBuffer)) {
+            throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
+        }
+
+        $this->_alg = $attStmt['alg'];
+        $this->_signature = $attStmt['sig']->getBinaryString();
+        $this->_x5c = $attStmt['x5c'][0]->getBinaryString();
+
+        if (count($attStmt['x5c']) > 1) {
+            for ($i=1; $i<count($attStmt['x5c']); $i++) {
+                $this->_x5c_chain[] = $attStmt['x5c'][$i]->getBinaryString();
+            }
+            unset ($i);
+        }
+    }
+
+
+    /*
+     * returns the key certificate in PEM format
+     * @return string
+     */
+    public function getCertificatePem() {
+        return $this->_createCertificatePem($this->_x5c);
+    }
+
+    /**
+     * @param string $clientDataHash
+     */
+    public function validateAttestation($clientDataHash) {
+        $publicKey = \openssl_pkey_get_public($this->getCertificatePem());
+
+        if ($publicKey === false) {
+            throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY);
+        }
+
+        // Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash
+        // using the attestation public key in attestnCert with the algorithm specified in alg.
+        $dataToVerify = $this->_authenticatorData->getBinary();
+        $dataToVerify .= $clientDataHash;
+
+        $coseAlgorithm = $this->_getCoseAlgorithm($this->_alg);
+
+        // check certificate
+        return \openssl_verify($dataToVerify, $this->_signature, $publicKey, $coseAlgorithm->openssl) === 1;
+    }
+
+    /**
+     * validates the certificate against root certificates
+     * @param array $rootCas
+     * @return boolean
+     * @throws WebAuthnException
+     */
+    public function validateRootCertificate($rootCas) {
+        $chainC = $this->_createX5cChainFile();
+        if ($chainC) {
+            $rootCas[] = $chainC;
+        }
+
+        $v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
+        if ($v === -1) {
+            throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
+        }
+        return $v;
+    }
+}
+
diff --git a/src/lib/WebAuthn/Attestation/Format/AndroidSafetyNet.php b/src/lib/WebAuthn/Attestation/Format/AndroidSafetyNet.php
new file mode 100644
index 0000000..ba0db52
--- /dev/null
+++ b/src/lib/WebAuthn/Attestation/Format/AndroidSafetyNet.php
@@ -0,0 +1,152 @@
+<?php
+
+
+namespace lbuchs\WebAuthn\Attestation\Format;
+use lbuchs\WebAuthn\Attestation\AuthenticatorData;
+use lbuchs\WebAuthn\WebAuthnException;
+use lbuchs\WebAuthn\Binary\ByteBuffer;
+
+class AndroidSafetyNet extends FormatBase {
+    private $_signature;
+    private $_signedValue;
+    private $_x5c;
+    private $_payload;
+
+    public function __construct($AttestionObject, AuthenticatorData $authenticatorData) {
+        parent::__construct($AttestionObject, $authenticatorData);
+
+        // check data
+        $attStmt = $this->_attestationObject['attStmt'];
+
+        if (!\array_key_exists('ver', $attStmt) || !$attStmt['ver']) {
+            throw new WebAuthnException('invalid Android Safety Net Format', WebAuthnException::INVALID_DATA);
+        }
+
+        if (!\array_key_exists('response', $attStmt) || !($attStmt['response'] instanceof ByteBuffer)) {
+            throw new WebAuthnException('invalid Android Safety Net Format', WebAuthnException::INVALID_DATA);
+        }
+
+        $response = $attStmt['response']->getBinaryString();
+
+        // Response is a JWS [RFC7515] object in Compact Serialization.
+        // JWSs have three segments separated by two period ('.') characters
+        $parts = \explode('.', $response);
+        unset ($response);
+        if (\count($parts) !== 3) {
+            throw new WebAuthnException('invalid JWS data', WebAuthnException::INVALID_DATA);
+        }
+
+        $header = $this->_base64url_decode($parts[0]);
+        $payload = $this->_base64url_decode($parts[1]);
+        $this->_signature = $this->_base64url_decode($parts[2]);
+        $this->_signedValue = $parts[0] . '.' . $parts[1];
+        unset ($parts);
+
+        $header = \json_decode($header);
+        $payload = \json_decode($payload);
+
+        if (!($header instanceof \stdClass)) {
+            throw new WebAuthnException('invalid JWS header', WebAuthnException::INVALID_DATA);
+        }
+        if (!($payload instanceof \stdClass)) {
+            throw new WebAuthnException('invalid JWS payload', WebAuthnException::INVALID_DATA);
+        }
+
+        if (!isset($header->x5c) || !is_array($header->x5c) || count($header->x5c) === 0) {
+            throw new WebAuthnException('No X.509 signature in JWS Header', WebAuthnException::INVALID_DATA);
+        }
+
+        // algorithm
+        if (!\in_array($header->alg, array('RS256', 'ES256'))) {
+            throw new WebAuthnException('invalid JWS algorithm ' . $header->alg, WebAuthnException::INVALID_DATA);
+        }
+
+        $this->_x5c = \base64_decode($header->x5c[0]);
+        $this->_payload = $payload;
+
+        if (count($header->x5c) > 1) {
+            for ($i=1; $i<count($header->x5c); $i++) {
+                $this->_x5c_chain[] = \base64_decode($header->x5c[$i]);
+            }
+            unset ($i);
+        }
+    }
+
+    /**
+     * ctsProfileMatch: A stricter verdict of device integrity.
+     * If the value of ctsProfileMatch is true, then the profile of the device running your app matches
+     * the profile of a device that has passed Android compatibility testing and
+     * has been approved as a Google-certified Android device.
+     * @return bool
+     */
+    public function ctsProfileMatch() {
+        return isset($this->_payload->ctsProfileMatch) ? !!$this->_payload->ctsProfileMatch : false;
+    }
+
+
+    /*
+     * returns the key certificate in PEM format
+     * @return string
+     */
+    public function getCertificatePem() {
+        return $this->_createCertificatePem($this->_x5c);
+    }
+
+    /**
+     * @param string $clientDataHash
+     */
+    public function validateAttestation($clientDataHash) {
+        $publicKey = \openssl_pkey_get_public($this->getCertificatePem());
+
+        // Verify that the nonce in the response is identical to the Base64 encoding
+        // of the SHA-256 hash of the concatenation of authenticatorData and clientDataHash.
+        if (empty($this->_payload->nonce) || $this->_payload->nonce !== \base64_encode(\hash('SHA256', $this->_authenticatorData->getBinary() . $clientDataHash, true))) {
+            throw new WebAuthnException('invalid nonce in JWS payload', WebAuthnException::INVALID_DATA);
+        }
+
+        // Verify that attestationCert is issued to the hostname "attest.android.com"
+        $certInfo = \openssl_x509_parse($this->getCertificatePem());
+        if (!\is_array($certInfo) || ($certInfo['subject']['CN'] ?? '') !== 'attest.android.com') {
+            throw new WebAuthnException('invalid certificate CN in JWS (' . ($certInfo['subject']['CN'] ?? '-'). ')', WebAuthnException::INVALID_DATA);
+        }
+
+        // Verify that the basicIntegrity attribute in the payload of response is true.
+        if (empty($this->_payload->basicIntegrity)) {
+            throw new WebAuthnException('invalid basicIntegrity in payload', WebAuthnException::INVALID_DATA);
+        }
+
+        // check certificate
+        return \openssl_verify($this->_signedValue, $this->_signature, $publicKey, OPENSSL_ALGO_SHA256) === 1;
+    }
+
+
+    /**
+     * validates the certificate against root certificates
+     * @param array $rootCas
+     * @return boolean
+     * @throws WebAuthnException
+     */
+    public function validateRootCertificate($rootCas) {
+        $chainC = $this->_createX5cChainFile();
+        if ($chainC) {
+            $rootCas[] = $chainC;
+        }
+
+        $v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
+        if ($v === -1) {
+            throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
+        }
+        return $v;
+    }
+
+
+    /**
+     * decode base64 url
+     * @param string $data
+     * @return string
+     */
+    private function _base64url_decode($data) {
+        return \base64_decode(\strtr($data, '-_', '+/') . \str_repeat('=', 3 - (3 + \strlen($data)) % 4));
+    }
+}
+
diff --git a/src/lib/WebAuthn/Attestation/Format/Apple.php b/src/lib/WebAuthn/Attestation/Format/Apple.php
new file mode 100644
index 0000000..e4f38e0
--- /dev/null
+++ b/src/lib/WebAuthn/Attestation/Format/Apple.php
@@ -0,0 +1,139 @@
+<?php
+
+
+namespace lbuchs\WebAuthn\Attestation\Format;
+use lbuchs\WebAuthn\Attestation\AuthenticatorData;
+use lbuchs\WebAuthn\WebAuthnException;
+use lbuchs\WebAuthn\Binary\ByteBuffer;
+
+class Apple extends FormatBase {
+    private $_x5c;
+
+    public function __construct($AttestionObject, AuthenticatorData $authenticatorData) {
+        parent::__construct($AttestionObject, $authenticatorData);
+
+        // check packed data
+        $attStmt = $this->_attestationObject['attStmt'];
+
+
+        // certificate for validation
+        if (\array_key_exists('x5c', $attStmt) && \is_array($attStmt['x5c']) && \count($attStmt['x5c']) > 0) {
+
+            // The attestation certificate attestnCert MUST be the first element in the array
+            $attestnCert = array_shift($attStmt['x5c']);
+
+            if (!($attestnCert instanceof ByteBuffer)) {
+                throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
+            }
+
+            $this->_x5c = $attestnCert->getBinaryString();
+
+            // certificate chain
+            foreach ($attStmt['x5c'] as $chain) {
+                if ($chain instanceof ByteBuffer) {
+                    $this->_x5c_chain[] = $chain->getBinaryString();
+                }
+            }
+        } else {
+            throw new WebAuthnException('invalid Apple attestation statement: missing x5c', WebAuthnException::INVALID_DATA);
+        }
+    }
+
+
+    /*
+     * returns the key certificate in PEM format
+     * @return string|null
+     */
+    public function getCertificatePem() {
+        return $this->_createCertificatePem($this->_x5c);
+    }
+
+    /**
+     * @param string $clientDataHash
+     */
+    public function validateAttestation($clientDataHash) {
+        return $this->_validateOverX5c($clientDataHash);
+    }
+
+    /**
+     * validates the certificate against root certificates
+     * @param array $rootCas
+     * @return boolean
+     * @throws WebAuthnException
+     */
+    public function validateRootCertificate($rootCas) {
+        $chainC = $this->_createX5cChainFile();
+        if ($chainC) {
+            $rootCas[] = $chainC;
+        }
+
+        $v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
+        if ($v === -1) {
+            throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
+        }
+        return $v;
+    }
+
+    /**
+     * validate if x5c is present
+     * @param string $clientDataHash
+     * @return bool
+     * @throws WebAuthnException
+     */
+    protected function _validateOverX5c($clientDataHash) {
+        $publicKey = \openssl_pkey_get_public($this->getCertificatePem());
+
+        if ($publicKey === false) {
+            throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY);
+        }
+
+        // Concatenate authenticatorData and clientDataHash to form nonceToHash.
+        $nonceToHash = $this->_authenticatorData->getBinary();
+        $nonceToHash .= $clientDataHash;
+
+        // Perform SHA-256 hash of nonceToHash to produce nonce
+        $nonce = hash('SHA256', $nonceToHash, true);
+
+        $credCert = openssl_x509_read($this->getCertificatePem());
+        if ($credCert === false) {
+            throw new WebAuthnException('invalid x5c certificate: ' . \openssl_error_string(), WebAuthnException::INVALID_DATA);
+        }
+
+        $keyData = openssl_pkey_get_details(openssl_pkey_get_public($credCert));
+        $key = is_array($keyData) && array_key_exists('key', $keyData) ? $keyData['key'] : null;
+
+
+        // Verify that nonce equals the value of the extension with OID ( 1.2.840.113635.100.8.2 ) in credCert.
+        $parsedCredCert = openssl_x509_parse($credCert);
+        $nonceExtension = $parsedCredCert['extensions']['1.2.840.113635.100.8.2'] ?? '';
+
+        // nonce padded by ASN.1 string: 30 24 A1 22 04 20
+        // 30     — type tag indicating sequence
+        // 24     — 36 byte following
+        //   A1   — Enumerated [1]
+        //   22   — 34 byte following
+        //     04 — type tag indicating octet string
+        //     20 — 32 byte following
+
+        $asn1Padding = "\x30\x24\xA1\x22\x04\x20";
+        if (substr($nonceExtension, 0, strlen($asn1Padding)) === $asn1Padding) {
+            $nonceExtension = substr($nonceExtension, strlen($asn1Padding));
+        }
+
+        if ($nonceExtension !== $nonce) {
+            throw new WebAuthnException('nonce doesn\'t equal the value of the extension with OID 1.2.840.113635.100.8.2', WebAuthnException::INVALID_DATA);
+        }
+
+        // Verify that the credential public key equals the Subject Public Key of credCert.
+        $authKeyData = openssl_pkey_get_details(openssl_pkey_get_public($this->_authenticatorData->getPublicKeyPem()));
+        $authKey = is_array($authKeyData) && array_key_exists('key', $authKeyData) ? $authKeyData['key'] : null;
+
+        if ($key === null || $key !== $authKey) {
+            throw new WebAuthnException('credential public key doesn\'t equal the Subject Public Key of credCert', WebAuthnException::INVALID_DATA);
+        }
+
+        return true;
+    }
+
+}
+
diff --git a/src/lib/WebAuthn/Attestation/Format/FormatBase.php b/src/lib/WebAuthn/Attestation/Format/FormatBase.php
new file mode 100644
index 0000000..eed916b
--- /dev/null
+++ b/src/lib/WebAuthn/Attestation/Format/FormatBase.php
@@ -0,0 +1,193 @@
+<?php
+
+
+namespace lbuchs\WebAuthn\Attestation\Format;
+use lbuchs\WebAuthn\WebAuthnException;
+use lbuchs\WebAuthn\Attestation\AuthenticatorData;
+
+
+abstract class FormatBase {
+    protected $_attestationObject = null;
+    protected $_authenticatorData = null;
+    protected $_x5c_chain = array();
+    protected $_x5c_tempFile = null;
+
+    /**
+     *
+     * @param Array $AttestionObject
+     * @param AuthenticatorData $authenticatorData
+     */
+    public function __construct($AttestionObject, AuthenticatorData $authenticatorData) {
+        $this->_attestationObject = $AttestionObject;
+        $this->_authenticatorData = $authenticatorData;
+    }
+
+    /**
+     *
+     */
+    public function __destruct() {
+        // delete X.509 chain certificate file after use
+        if ($this->_x5c_tempFile && \is_file($this->_x5c_tempFile)) {
+            \unlink($this->_x5c_tempFile);
+        }
+    }
+
+    /**
+     * returns the certificate chain in PEM format
+     * @return string|null
+     */
+    public function getCertificateChain() {
+        if ($this->_x5c_tempFile && \is_file($this->_x5c_tempFile)) {
+            return \file_get_contents($this->_x5c_tempFile);
+        }
+        return null;
+    }
+
+    /**
+     * returns the key X.509 certificate in PEM format
+     * @return string
+     */
+    public function getCertificatePem() {
+        // need to be overwritten
+        return null;
+    }
+
+    /**
+     * checks validity of the signature
+     * @param string $clientDataHash
+     * @return bool
+     * @throws WebAuthnException
+     */
+    public function validateAttestation($clientDataHash) {
+        // need to be overwritten
+        return false;
+    }
+
+    /**
+     * validates the certificate against root certificates
+     * @param array $rootCas
+     * @return boolean
+     * @throws WebAuthnException
+     */
+    public function validateRootCertificate($rootCas) {
+        // need to be overwritten
+        return false;
+    }
+
+
+    /**
+     * create a PEM encoded certificate with X.509 binary data
+     * @param string $x5c
+     * @return string
+     */
+    protected function _createCertificatePem($x5c) {
+        $pem = '-----BEGIN CERTIFICATE-----' . "\n";
+        $pem .= \chunk_split(\base64_encode($x5c), 64, "\n");
+        $pem .= '-----END CERTIFICATE-----' . "\n";
+        return $pem;
+    }
+
+    /**
+     * creates a PEM encoded chain file
+     * @return type
+     */
+    protected function _createX5cChainFile() {
+        $content = '';
+        if (\is_array($this->_x5c_chain) && \count($this->_x5c_chain) > 0) {
+            foreach ($this->_x5c_chain as $x5c) {
+                $certInfo = \openssl_x509_parse($this->_createCertificatePem($x5c));
+
+                // check if certificate is self signed
+                if (\is_array($certInfo) && \is_array($certInfo['issuer']) && \is_array($certInfo['subject'])) {
+                    $selfSigned = false;
+
+                    $subjectKeyIdentifier = $certInfo['extensions']['subjectKeyIdentifier'] ?? null;
+                    $authorityKeyIdentifier = $certInfo['extensions']['authorityKeyIdentifier'] ?? null;
+
+                    if ($authorityKeyIdentifier && substr($authorityKeyIdentifier, 0, 6) === 'keyid:') {
+                        $authorityKeyIdentifier = substr($authorityKeyIdentifier, 6);
+                    }
+                    if ($subjectKeyIdentifier && substr($subjectKeyIdentifier, 0, 6) === 'keyid:') {
+                        $subjectKeyIdentifier = substr($subjectKeyIdentifier, 6);
+                    }
+
+                    if (($subjectKeyIdentifier && !$authorityKeyIdentifier) || ($authorityKeyIdentifier && $authorityKeyIdentifier === $subjectKeyIdentifier)) {
+                        $selfSigned = true;
+                    }
+
+                    if (!$selfSigned) {
+                        $content .= "\n" . $this->_createCertificatePem($x5c) . "\n";
+                    }
+                }
+            }
+        }
+
+        if ($content) {
+            $this->_x5c_tempFile = \sys_get_temp_dir() . '/x5c_chain_' . \base_convert(\rand(), 10, 36) . '.pem';
+            if (\file_put_contents($this->_x5c_tempFile, $content) !== false) {
+                return $this->_x5c_tempFile;
+            }
+        }
+
+        return null;
+    }
+
+
+    /**
+     * returns the name and openssl key for provided cose number.
+     * @param int $coseNumber
+     * @return \stdClass|null
+     */
+    protected function _getCoseAlgorithm($coseNumber) {
+        // https://www.iana.org/assignments/cose/cose.xhtml#algorithms
+        $coseAlgorithms = array(
+            array(
+                'hash' => 'SHA1',
+                'openssl' => OPENSSL_ALGO_SHA1,
+                'cose' => array(
+                    -65535  // RS1
+                )),
+
+            array(
+                'hash' => 'SHA256',
+                'openssl' => OPENSSL_ALGO_SHA256,
+                'cose' => array(
+                    -257, // RS256
+                    -37,  // PS256
+                    -7,   // ES256
+                    5     // HMAC256
+                )),
+
+            array(
+                'hash' => 'SHA384',
+                'openssl' => OPENSSL_ALGO_SHA384,
+                'cose' => array(
+                    -258, // RS384
+                    -38,  // PS384
+                    -35,  // ES384
+                    6     // HMAC384
+                )),
+
+            array(
+                'hash' => 'SHA512',
+                'openssl' => OPENSSL_ALGO_SHA512,
+                'cose' => array(
+                    -259, // RS512
+                    -39,  // PS512
+                    -36,  // ES512
+                    7     // HMAC512
+                ))
+        );
+
+        foreach ($coseAlgorithms as $coseAlgorithm) {
+            if (\in_array($coseNumber, $coseAlgorithm['cose'], true)) {
+                $return = new \stdClass();
+                $return->hash = $coseAlgorithm['hash'];
+                $return->openssl = $coseAlgorithm['openssl'];
+                return $return;
+            }
+        }
+
+        return null;
+    }
+}
diff --git a/src/lib/WebAuthn/Attestation/Format/None.php b/src/lib/WebAuthn/Attestation/Format/None.php
new file mode 100644
index 0000000..ba95e40
--- /dev/null
+++ b/src/lib/WebAuthn/Attestation/Format/None.php
@@ -0,0 +1,41 @@
+<?php
+
+
+namespace lbuchs\WebAuthn\Attestation\Format;
+use lbuchs\WebAuthn\Attestation\AuthenticatorData;
+use lbuchs\WebAuthn\WebAuthnException;
+
+class None extends FormatBase {
+
+
+    public function __construct($AttestionObject, AuthenticatorData $authenticatorData) {
+        parent::__construct($AttestionObject, $authenticatorData);
+    }
+
+
+    /*
+     * returns the key certificate in PEM format
+     * @return string
+     */
+    public function getCertificatePem() {
+        return null;
+    }
+
+    /**
+     * @param string $clientDataHash
+     */
+    public function validateAttestation($clientDataHash) {
+        return true;
+    }
+
+    /**
+     * validates the certificate against root certificates.
+     * Format 'none' does not contain any ca, so always false.
+     * @param array $rootCas
+     * @return boolean
+     * @throws WebAuthnException
+     */
+    public function validateRootCertificate($rootCas) {
+        return false;
+    }
+}
diff --git a/src/lib/WebAuthn/Attestation/Format/Packed.php b/src/lib/WebAuthn/Attestation/Format/Packed.php
new file mode 100644
index 0000000..c2ced07
--- /dev/null
+++ b/src/lib/WebAuthn/Attestation/Format/Packed.php
@@ -0,0 +1,139 @@
+<?php
+
+
+namespace lbuchs\WebAuthn\Attestation\Format;
+use lbuchs\WebAuthn\Attestation\AuthenticatorData;
+use lbuchs\WebAuthn\WebAuthnException;
+use lbuchs\WebAuthn\Binary\ByteBuffer;
+
+class Packed extends FormatBase {
+    private $_alg;
+    private $_signature;
+    private $_x5c;
+
+    public function __construct($AttestionObject, AuthenticatorData $authenticatorData) {
+        parent::__construct($AttestionObject, $authenticatorData);
+
+        // check packed data
+        $attStmt = $this->_attestationObject['attStmt'];
+
+        if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) {
+            throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA);
+        }
+
+        if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) {
+            throw new WebAuthnException('no signature found', WebAuthnException::INVALID_DATA);
+        }
+
+        $this->_alg = $attStmt['alg'];
+        $this->_signature = $attStmt['sig']->getBinaryString();
+
+        // certificate for validation
+        if (\array_key_exists('x5c', $attStmt) && \is_array($attStmt['x5c']) && \count($attStmt['x5c']) > 0) {
+
+            // The attestation certificate attestnCert MUST be the first element in the array
+            $attestnCert = array_shift($attStmt['x5c']);
+
+            if (!($attestnCert instanceof ByteBuffer)) {
+                throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
+            }
+
+            $this->_x5c = $attestnCert->getBinaryString();
+
+            // certificate chain
+            foreach ($attStmt['x5c'] as $chain) {
+                if ($chain instanceof ByteBuffer) {
+                    $this->_x5c_chain[] = $chain->getBinaryString();
+                }
+            }
+        }
+    }
+
+
+    /*
+     * returns the key certificate in PEM format
+     * @return string|null
+     */
+    public function getCertificatePem() {
+        if (!$this->_x5c) {
+            return null;
+        }
+        return $this->_createCertificatePem($this->_x5c);
+    }
+
+    /**
+     * @param string $clientDataHash
+     */
+    public function validateAttestation($clientDataHash) {
+        if ($this->_x5c) {
+            return $this->_validateOverX5c($clientDataHash);
+        } else {
+            return $this->_validateSelfAttestation($clientDataHash);
+        }
+    }
+
+    /**
+     * validates the certificate against root certificates
+     * @param array $rootCas
+     * @return boolean
+     * @throws WebAuthnException
+     */
+    public function validateRootCertificate($rootCas) {
+        if (!$this->_x5c) {
+            return false;
+        }
+
+        $chainC = $this->_createX5cChainFile();
+        if ($chainC) {
+            $rootCas[] = $chainC;
+        }
+
+        $v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
+        if ($v === -1) {
+            throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
+        }
+        return $v;
+    }
+
+    /**
+     * validate if x5c is present
+     * @param string $clientDataHash
+     * @return bool
+     * @throws WebAuthnException
+     */
+    protected function _validateOverX5c($clientDataHash) {
+        $publicKey = \openssl_pkey_get_public($this->getCertificatePem());
+
+        if ($publicKey === false) {
+            throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY);
+        }
+
+        // Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash
+        // using the attestation public key in attestnCert with the algorithm specified in alg.
+        $dataToVerify = $this->_authenticatorData->getBinary();
+        $dataToVerify .= $clientDataHash;
+
+        $coseAlgorithm = $this->_getCoseAlgorithm($this->_alg);
+
+        // check certificate
+        return \openssl_verify($dataToVerify, $this->_signature, $publicKey, $coseAlgorithm->openssl) === 1;
+    }
+
+    /**
+     * validate if self attestation is in use
+     * @param string $clientDataHash
+     * @return bool
+     */
+    protected function _validateSelfAttestation($clientDataHash) {
+        // Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash
+        // using the credential public key with alg.
+        $dataToVerify = $this->_authenticatorData->getBinary();
+        $dataToVerify .= $clientDataHash;
+
+        $publicKey = $this->_authenticatorData->getPublicKeyPem();
+
+        // check certificate
+        return \openssl_verify($dataToVerify, $this->_signature, $publicKey, OPENSSL_ALGO_SHA256) === 1;
+    }
+}
+
diff --git a/src/lib/WebAuthn/Attestation/Format/Tpm.php b/src/lib/WebAuthn/Attestation/Format/Tpm.php
new file mode 100644
index 0000000..338cd45
--- /dev/null
+++ b/src/lib/WebAuthn/Attestation/Format/Tpm.php
@@ -0,0 +1,180 @@
+<?php
+
+
+namespace lbuchs\WebAuthn\Attestation\Format;
+use lbuchs\WebAuthn\Attestation\AuthenticatorData;
+use lbuchs\WebAuthn\WebAuthnException;
+use lbuchs\WebAuthn\Binary\ByteBuffer;
+
+class Tpm extends FormatBase {
+    private $_TPM_GENERATED_VALUE = "\xFF\x54\x43\x47";
+    private $_TPM_ST_ATTEST_CERTIFY = "\x80\x17";
+    private $_alg;
+    private $_signature;
+    private $_pubArea;
+    private $_x5c;
+
+    /**
+     * @var ByteBuffer
+     */
+    private $_certInfo;
+
+
+    public function __construct($AttestionObject, AuthenticatorData $authenticatorData) {
+        parent::__construct($AttestionObject, $authenticatorData);
+
+        // check packed data
+        $attStmt = $this->_attestationObject['attStmt'];
+
+        if (!\array_key_exists('ver', $attStmt) || $attStmt['ver'] !== '2.0') {
+            throw new WebAuthnException('invalid tpm version: ' . $attStmt['ver'], WebAuthnException::INVALID_DATA);
+        }
+
+        if (!\array_key_exists('alg', $attStmt) || $this->_getCoseAlgorithm($attStmt['alg']) === null) {
+            throw new WebAuthnException('unsupported alg: ' . $attStmt['alg'], WebAuthnException::INVALID_DATA);
+        }
+
+        if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) {
+            throw new WebAuthnException('signature not found', WebAuthnException::INVALID_DATA);
+        }
+
+        if (!\array_key_exists('certInfo', $attStmt) || !\is_object($attStmt['certInfo']) || !($attStmt['certInfo'] instanceof ByteBuffer)) {
+            throw new WebAuthnException('certInfo not found', WebAuthnException::INVALID_DATA);
+        }
+
+        if (!\array_key_exists('pubArea', $attStmt) || !\is_object($attStmt['pubArea']) || !($attStmt['pubArea'] instanceof ByteBuffer)) {
+            throw new WebAuthnException('pubArea not found', WebAuthnException::INVALID_DATA);
+        }
+
+        $this->_alg = $attStmt['alg'];
+        $this->_signature = $attStmt['sig']->getBinaryString();
+        $this->_certInfo = $attStmt['certInfo'];
+        $this->_pubArea = $attStmt['pubArea'];
+
+        // certificate for validation
+        if (\array_key_exists('x5c', $attStmt) && \is_array($attStmt['x5c']) && \count($attStmt['x5c']) > 0) {
+
+            // The attestation certificate attestnCert MUST be the first element in the array
+            $attestnCert = array_shift($attStmt['x5c']);
+
+            if (!($attestnCert instanceof ByteBuffer)) {
+                throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
+            }
+
+            $this->_x5c = $attestnCert->getBinaryString();
+
+            // certificate chain
+            foreach ($attStmt['x5c'] as $chain) {
+                if ($chain instanceof ByteBuffer) {
+                    $this->_x5c_chain[] = $chain->getBinaryString();
+                }
+            }
+
+        } else {
+            throw new WebAuthnException('no x5c certificate found', WebAuthnException::INVALID_DATA);
+        }
+    }
+
+
+    /*
+     * returns the key certificate in PEM format
+     * @return string|null
+     */
+    public function getCertificatePem() {
+        if (!$this->_x5c) {
+            return null;
+        }
+        return $this->_createCertificatePem($this->_x5c);
+    }
+
+    /**
+     * @param string $clientDataHash
+     */
+    public function validateAttestation($clientDataHash) {
+        return $this->_validateOverX5c($clientDataHash);
+    }
+
+    /**
+     * validates the certificate against root certificates
+     * @param array $rootCas
+     * @return boolean
+     * @throws WebAuthnException
+     */
+    public function validateRootCertificate($rootCas) {
+        if (!$this->_x5c) {
+            return false;
+        }
+
+        $chainC = $this->_createX5cChainFile();
+        if ($chainC) {
+            $rootCas[] = $chainC;
+        }
+
+        $v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
+        if ($v === -1) {
+            throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
+        }
+        return $v;
+    }
+
+    /**
+     * validate if x5c is present
+     * @param string $clientDataHash
+     * @return bool
+     * @throws WebAuthnException
+     */
+    protected function _validateOverX5c($clientDataHash) {
+        $publicKey = \openssl_pkey_get_public($this->getCertificatePem());
+
+        if ($publicKey === false) {
+            throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY);
+        }
+
+        // Concatenate authenticatorData and clientDataHash to form attToBeSigned.
+        $attToBeSigned = $this->_authenticatorData->getBinary();
+        $attToBeSigned .= $clientDataHash;
+
+        // Validate that certInfo is valid:
+
+        // Verify that magic is set to TPM_GENERATED_VALUE.
+        if ($this->_certInfo->getBytes(0, 4) !== $this->_TPM_GENERATED_VALUE) {
+            throw new WebAuthnException('tpm magic not TPM_GENERATED_VALUE', WebAuthnException::INVALID_DATA);
+        }
+
+        // Verify that type is set to TPM_ST_ATTEST_CERTIFY.
+        if ($this->_certInfo->getBytes(4, 2) !== $this->_TPM_ST_ATTEST_CERTIFY) {
+            throw new WebAuthnException('tpm type not TPM_ST_ATTEST_CERTIFY', WebAuthnException::INVALID_DATA);
+        }
+
+        $offset = 6;
+        $qualifiedSigner = $this->_tpmReadLengthPrefixed($this->_certInfo, $offset);
+        $extraData = $this->_tpmReadLengthPrefixed($this->_certInfo, $offset);
+        $coseAlg = $this->_getCoseAlgorithm($this->_alg);
+
+        // Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg".
+        if ($extraData->getBinaryString() !== \hash($coseAlg->hash, $attToBeSigned, true)) {
+            throw new WebAuthnException('certInfo:extraData not hash of attToBeSigned', WebAuthnException::INVALID_DATA);
+        }
+
+        // Verify the sig is a valid signature over certInfo using the attestation
+        // public key in aikCert with the algorithm specified in alg.
+        return \openssl_verify($this->_certInfo->getBinaryString(), $this->_signature, $publicKey, $coseAlg->openssl) === 1;
+    }
+
+
+    /**
+     * returns next part of ByteBuffer
+     * @param ByteBuffer $buffer
+     * @param int $offset
+     * @return ByteBuffer
+     */
+    protected function _tpmReadLengthPrefixed(ByteBuffer $buffer, &$offset) {
+        $len = $buffer->getUint16Val($offset);
+        $data = $buffer->getBytes($offset + 2, $len);
+        $offset += (2 + $len);
+
+        return new ByteBuffer($data);
+    }
+
+}
+
diff --git a/src/lib/WebAuthn/Attestation/Format/U2f.php b/src/lib/WebAuthn/Attestation/Format/U2f.php
new file mode 100644
index 0000000..2b51ba8
--- /dev/null
+++ b/src/lib/WebAuthn/Attestation/Format/U2f.php
@@ -0,0 +1,93 @@
+<?php
+
+
+namespace lbuchs\WebAuthn\Attestation\Format;
+use lbuchs\WebAuthn\Attestation\AuthenticatorData;
+use lbuchs\WebAuthn\WebAuthnException;
+use lbuchs\WebAuthn\Binary\ByteBuffer;
+
+class U2f extends FormatBase {
+    private $_alg = -7;
+    private $_signature;
+    private $_x5c;
+
+    public function __construct($AttestionObject, AuthenticatorData $authenticatorData) {
+        parent::__construct($AttestionObject, $authenticatorData);
+
+        // check u2f data
+        $attStmt = $this->_attestationObject['attStmt'];
+
+        if (\array_key_exists('alg', $attStmt) && $attStmt['alg'] !== $this->_alg) {
+            throw new WebAuthnException('u2f only accepts algorithm -7 ("ES256"), but got ' . $attStmt['alg'], WebAuthnException::INVALID_DATA);
+        }
+
+        if (!\array_key_exists('sig', $attStmt) || !\is_object($attStmt['sig']) || !($attStmt['sig'] instanceof ByteBuffer)) {
+            throw new WebAuthnException('no signature found', WebAuthnException::INVALID_DATA);
+        }
+
+        if (!\array_key_exists('x5c', $attStmt) || !\is_array($attStmt['x5c']) || \count($attStmt['x5c']) !== 1) {
+            throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
+        }
+
+        if (!\is_object($attStmt['x5c'][0]) || !($attStmt['x5c'][0] instanceof ByteBuffer)) {
+            throw new WebAuthnException('invalid x5c certificate', WebAuthnException::INVALID_DATA);
+        }
+
+        $this->_signature = $attStmt['sig']->getBinaryString();
+        $this->_x5c = $attStmt['x5c'][0]->getBinaryString();
+    }
+
+
+    /*
+     * returns the key certificate in PEM format
+     * @return string
+     */
+    public function getCertificatePem() {
+        $pem = '-----BEGIN CERTIFICATE-----' . "\n";
+        $pem .= \chunk_split(\base64_encode($this->_x5c), 64, "\n");
+        $pem .= '-----END CERTIFICATE-----' . "\n";
+        return $pem;
+    }
+
+    /**
+     * @param string $clientDataHash
+     */
+    public function validateAttestation($clientDataHash) {
+        $publicKey = \openssl_pkey_get_public($this->getCertificatePem());
+
+        if ($publicKey === false) {
+            throw new WebAuthnException('invalid public key: ' . \openssl_error_string(), WebAuthnException::INVALID_PUBLIC_KEY);
+        }
+
+        // Let verificationData be the concatenation of (0x00 || rpIdHash || clientDataHash || credentialId || publicKeyU2F)
+        $dataToVerify = "\x00";
+        $dataToVerify .= $this->_authenticatorData->getRpIdHash();
+        $dataToVerify .= $clientDataHash;
+        $dataToVerify .= $this->_authenticatorData->getCredentialId();
+        $dataToVerify .= $this->_authenticatorData->getPublicKeyU2F();
+
+        $coseAlgorithm = $this->_getCoseAlgorithm($this->_alg);
+
+        // check certificate
+        return \openssl_verify($dataToVerify, $this->_signature, $publicKey, $coseAlgorithm->openssl) === 1;
+    }
+
+    /**
+     * validates the certificate against root certificates
+     * @param array $rootCas
+     * @return boolean
+     * @throws WebAuthnException
+     */
+    public function validateRootCertificate($rootCas) {
+        $chainC = $this->_createX5cChainFile();
+        if ($chainC) {
+            $rootCas[] = $chainC;
+        }
+
+        $v = \openssl_x509_checkpurpose($this->getCertificatePem(), -1, $rootCas);
+        if ($v === -1) {
+            throw new WebAuthnException('error on validating root certificate: ' . \openssl_error_string(), WebAuthnException::CERTIFICATE_NOT_TRUSTED);
+        }
+        return $v;
+    }
+}
diff --git a/src/lib/WebAuthn/Binary/ByteBuffer.php b/src/lib/WebAuthn/Binary/ByteBuffer.php
new file mode 100644
index 0000000..861ed60
--- /dev/null
+++ b/src/lib/WebAuthn/Binary/ByteBuffer.php
@@ -0,0 +1,300 @@
+<?php
+
+
+namespace lbuchs\WebAuthn\Binary;
+use lbuchs\WebAuthn\WebAuthnException;
+
+/**
+ * Modified version of https://github.com/madwizard-thomas/webauthn-server/blob/master/src/Format/ByteBuffer.php
+ * Copyright © 2018 Thomas Bleeker - MIT licensed
+ * Modified by Lukas Buchs
+ * Thanks Thomas for your work!
+ */
+class ByteBuffer implements \JsonSerializable, \Serializable {
+    /**
+     * @var bool
+     */
+    public static $useBase64UrlEncoding = false;
+
+    /**
+     * @var string
+     */
+    private $_data;
+
+    /**
+     * @var int
+     */
+    private $_length;
+
+    public function __construct($binaryData) {
+        $this->_data = (string)$binaryData;
+        $this->_length = \strlen($binaryData);
+    }
+
+
+    // -----------------------
+    // PUBLIC STATIC
+    // -----------------------
+
+    /**
+     * create a ByteBuffer from a base64 url encoded string
+     * @param string $base64url
+     * @return ByteBuffer
+     */
+    public static function fromBase64Url($base64url): ByteBuffer {
+        $bin = self::_base64url_decode($base64url);
+        if ($bin === false) {
+            throw new WebAuthnException('ByteBuffer: Invalid base64 url string', WebAuthnException::BYTEBUFFER);
+        }
+        return new ByteBuffer($bin);
+    }
+
+    /**
+     * create a ByteBuffer from a base64 url encoded string
+     * @param string $hex
+     * @return ByteBuffer
+     */
+    public static function fromHex($hex): ByteBuffer {
+        $bin = \hex2bin($hex);
+        if ($bin === false) {
+            throw new WebAuthnException('ByteBuffer: Invalid hex string', WebAuthnException::BYTEBUFFER);
+        }
+        return new ByteBuffer($bin);
+    }
+
+    /**
+     * create a random ByteBuffer
+     * @param string $length
+     * @return ByteBuffer
+     */
+    public static function randomBuffer($length): ByteBuffer {
+        if (\function_exists('random_bytes')) { // >PHP 7.0
+            return new ByteBuffer(\random_bytes($length));
+
+        } else if (\function_exists('openssl_random_pseudo_bytes')) {
+            return new ByteBuffer(\openssl_random_pseudo_bytes($length));
+
+        } else {
+            throw new WebAuthnException('ByteBuffer: cannot generate random bytes', WebAuthnException::BYTEBUFFER);
+        }
+    }
+
+    // -----------------------
+    // PUBLIC
+    // -----------------------
+
+    public function getBytes($offset, $length): string {
+        if ($offset < 0 || $length < 0 || ($offset + $length > $this->_length)) {
+            throw new WebAuthnException('ByteBuffer: Invalid offset or length', WebAuthnException::BYTEBUFFER);
+        }
+        return \substr($this->_data, $offset, $length);
+    }
+
+    public function getByteVal($offset): int {
+        if ($offset < 0 || $offset >= $this->_length) {
+            throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
+        }
+        return \ord(\substr($this->_data, $offset, 1));
+    }
+
+    public function getJson($jsonFlags=0) {
+        $data = \json_decode($this->getBinaryString(), null, 512, $jsonFlags);
+        if (\json_last_error() !== JSON_ERROR_NONE) {
+            throw new WebAuthnException(\json_last_error_msg(), WebAuthnException::BYTEBUFFER);
+        }
+        return $data;
+    }
+
+    public function getLength(): int {
+        return $this->_length;
+    }
+
+    public function getUint16Val($offset) {
+        if ($offset < 0 || ($offset + 2) > $this->_length) {
+            throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
+        }
+        return unpack('n', $this->_data, $offset)[1];
+    }
+
+    public function getUint32Val($offset) {
+        if ($offset < 0 || ($offset + 4) > $this->_length) {
+            throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
+        }
+        $val = unpack('N', $this->_data, $offset)[1];
+
+        // Signed integer overflow causes signed negative numbers
+        if ($val < 0) {
+            throw new WebAuthnException('ByteBuffer: Value out of integer range.', WebAuthnException::BYTEBUFFER);
+        }
+        return $val;
+    }
+
+    public function getUint64Val($offset) {
+        if (PHP_INT_SIZE < 8) {
+            throw new WebAuthnException('ByteBuffer: 64-bit values not supported by this system', WebAuthnException::BYTEBUFFER);
+        }
+        if ($offset < 0 || ($offset + 8) > $this->_length) {
+            throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
+        }
+        $val = unpack('J', $this->_data, $offset)[1];
+
+        // Signed integer overflow causes signed negative numbers
+        if ($val < 0) {
+            throw new WebAuthnException('ByteBuffer: Value out of integer range.', WebAuthnException::BYTEBUFFER);
+        }
+
+        return $val;
+    }
+
+    public function getHalfFloatVal($offset) {
+        //FROM spec pseudo decode_half(unsigned char *halfp)
+        $half = $this->getUint16Val($offset);
+
+        $exp = ($half >> 10) & 0x1f;
+        $mant = $half & 0x3ff;
+
+        if ($exp === 0) {
+            $val = $mant * (2 ** -24);
+        } elseif ($exp !== 31) {
+            $val = ($mant + 1024) * (2 ** ($exp - 25));
+        } else {
+            $val = ($mant === 0) ? INF : NAN;
+        }
+
+        return ($half & 0x8000) ? -$val : $val;
+    }
+
+    public function getFloatVal($offset) {
+        if ($offset < 0 || ($offset + 4) > $this->_length) {
+            throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
+        }
+        return unpack('G', $this->_data, $offset)[1];
+    }
+
+    public function getDoubleVal($offset) {
+        if ($offset < 0 || ($offset + 8) > $this->_length) {
+            throw new WebAuthnException('ByteBuffer: Invalid offset', WebAuthnException::BYTEBUFFER);
+        }
+        return unpack('E', $this->_data, $offset)[1];
+    }
+
+    /**
+     * @return string
+     */
+    public function getBinaryString(): string {
+        return $this->_data;
+    }
+
+    /**
+     * @param string|ByteBuffer $buffer
+     * @return bool
+     */
+    public function equals($buffer): bool {
+        if (is_object($buffer) && $buffer instanceof ByteBuffer) {
+            return $buffer->getBinaryString() === $this->getBinaryString();
+
+        } else if (is_string($buffer)) {
+            return $buffer === $this->getBinaryString();
+        }
+        
+        return false;
+    }
+
+    /**
+     * @return string
+     */
+    public function getHex(): string {
+        return \bin2hex($this->_data);
+    }
+
+    /**
+     * @return bool
+     */
+    public function isEmpty(): bool {
+        return $this->_length === 0;
+    }
+
+
+    /**
+     * jsonSerialize interface
+     * return binary data in RFC 1342-Like serialized string
+     * @return string
+     */
+    public function jsonSerialize(): string {
+        if (ByteBuffer::$useBase64UrlEncoding) {
+            return self::_base64url_encode($this->_data);
+
+        } else {
+            return '=?BINARY?B?' . \base64_encode($this->_data) . '?=';
+        }
+    }
+
+    /**
+     * Serializable-Interface
+     * @return string
+     */
+    public function serialize(): string {
+        return \serialize($this->_data);
+    }
+
+    /**
+     * Serializable-Interface
+     * @param string $serialized
+     */
+    public function unserialize($serialized) {
+        $this->_data = \unserialize($serialized);
+        $this->_length = \strlen($this->_data);
+    }
+
+    /**
+     * (PHP 8 deprecates Serializable-Interface)
+     * @return array
+     */
+    public function __serialize(): array {
+        return [
+            'data' => \serialize($this->_data)
+        ];
+    }
+
+    /**
+     * object to string
+     * @return string
+     */
+    public function __toString(): string {
+        return $this->getHex();
+    }
+
+    /**
+     * (PHP 8 deprecates Serializable-Interface)
+     * @param array $data
+     * @return void
+     */
+    public function __unserialize($data) {
+        if ($data && isset($data['data'])) {
+            $this->_data = \unserialize($data['data']);
+            $this->_length = \strlen($this->_data);
+        }
+    }
+
+    // -----------------------
+    // PROTECTED STATIC
+    // -----------------------
+
+    /**
+     * base64 url decoding
+     * @param string $data
+     * @return string
+     */
+    protected static function _base64url_decode($data): string {
+        return \base64_decode(\strtr($data, '-_', '+/') . \str_repeat('=', 3 - (3 + \strlen($data)) % 4));
+    }
+
+    /**
+     * base64 url encoding
+     * @param string $data
+     * @return string
+     */
+    protected static function _base64url_encode($data): string {
+        return \rtrim(\strtr(\base64_encode($data), '+/', '-_'), '=');
+    }
+}
diff --git a/src/lib/WebAuthn/CBOR/CborDecoder.php b/src/lib/WebAuthn/CBOR/CborDecoder.php
new file mode 100644
index 0000000..e6b5427
--- /dev/null
+++ b/src/lib/WebAuthn/CBOR/CborDecoder.php
@@ -0,0 +1,220 @@
+<?php
+
+
+namespace lbuchs\WebAuthn\CBOR;
+use lbuchs\WebAuthn\WebAuthnException;
+use lbuchs\WebAuthn\Binary\ByteBuffer;
+
+/**
+ * Modified version of https://github.com/madwizard-thomas/webauthn-server/blob/master/src/Format/CborDecoder.php
+ * Copyright © 2018 Thomas Bleeker - MIT licensed
+ * Modified by Lukas Buchs
+ * Thanks Thomas for your work!
+ */
+class CborDecoder {
+    const CBOR_MAJOR_UNSIGNED_INT = 0;
+    const CBOR_MAJOR_TEXT_STRING = 3;
+    const CBOR_MAJOR_FLOAT_SIMPLE = 7;
+    const CBOR_MAJOR_NEGATIVE_INT = 1;
+    const CBOR_MAJOR_ARRAY = 4;
+    const CBOR_MAJOR_TAG = 6;
+    const CBOR_MAJOR_MAP = 5;
+    const CBOR_MAJOR_BYTE_STRING = 2;
+
+    /**
+     * @param ByteBuffer|string $bufOrBin
+     * @return mixed
+     * @throws WebAuthnException
+     */
+    public static function decode($bufOrBin) {
+        $buf = $bufOrBin instanceof ByteBuffer ? $bufOrBin : new ByteBuffer($bufOrBin);
+
+        $offset = 0;
+        $result = self::_parseItem($buf, $offset);
+        if ($offset !== $buf->getLength()) {
+            throw new WebAuthnException('Unused bytes after data item.', WebAuthnException::CBOR);
+        }
+        return $result;
+    }
+
+    /**
+     * @param ByteBuffer|string $bufOrBin
+     * @param int $startOffset
+     * @param int|null $endOffset
+     * @return mixed
+     */
+    public static function decodeInPlace($bufOrBin, $startOffset, &$endOffset = null) {
+        $buf = $bufOrBin instanceof ByteBuffer ? $bufOrBin : new ByteBuffer($bufOrBin);
+
+        $offset = $startOffset;
+        $data = self::_parseItem($buf, $offset);
+        $endOffset = $offset;
+        return $data;
+    }
+
+    // ---------------------
+    // protected
+    // ---------------------
+
+    /**
+     * @param ByteBuffer $buf
+     * @param int $offset
+     * @return mixed
+     */
+    protected static function _parseItem(ByteBuffer $buf, &$offset) {
+        $first = $buf->getByteVal($offset++);
+        $type = $first >> 5;
+        $val = $first & 0b11111;
+
+        if ($type === self::CBOR_MAJOR_FLOAT_SIMPLE) {
+            return self::_parseFloatSimple($val, $buf, $offset);
+        }
+
+        $val = self::_parseExtraLength($val, $buf, $offset);
+
+        return self::_parseItemData($type, $val, $buf, $offset);
+    }
+
+    protected static function _parseFloatSimple($val, ByteBuffer $buf, &$offset) {
+        switch ($val) {
+            case 24:
+                $val = $buf->getByteVal($offset);
+                $offset++;
+                return self::_parseSimple($val);
+
+            case 25:
+                $floatValue = $buf->getHalfFloatVal($offset);
+                $offset += 2;
+                return $floatValue;
+
+            case 26:
+                $floatValue = $buf->getFloatVal($offset);
+                $offset += 4;
+                return $floatValue;
+
+            case 27:
+                $floatValue = $buf->getDoubleVal($offset);
+                $offset += 8;
+                return $floatValue;
+
+            case 28:
+            case 29:
+            case 30:
+                throw new WebAuthnException('Reserved value used.', WebAuthnException::CBOR);
+
+            case 31:
+                throw new WebAuthnException('Indefinite length is not supported.', WebAuthnException::CBOR);
+        }
+
+        return self::_parseSimple($val);
+    }
+
+    /**
+     * @param int $val
+     * @return mixed
+     * @throws WebAuthnException
+     */
+    protected static function _parseSimple($val) {
+        if ($val === 20) {
+            return false;
+        }
+        if ($val === 21) {
+            return true;
+        }
+        if ($val === 22) {
+            return null;
+        }
+        throw new WebAuthnException(sprintf('Unsupported simple value %d.', $val), WebAuthnException::CBOR);
+    }
+
+    protected static function _parseExtraLength($val, ByteBuffer $buf, &$offset) {
+        switch ($val) {
+            case 24:
+                $val = $buf->getByteVal($offset);
+                $offset++;
+                break;
+
+            case 25:
+                $val = $buf->getUint16Val($offset);
+                $offset += 2;
+                break;
+
+            case 26:
+                $val = $buf->getUint32Val($offset);
+                $offset += 4;
+                break;
+
+            case 27:
+                $val = $buf->getUint64Val($offset);
+                $offset += 8;
+                break;
+
+            case 28:
+            case 29:
+            case 30:
+                throw new WebAuthnException('Reserved value used.', WebAuthnException::CBOR);
+
+            case 31:
+                throw new WebAuthnException('Indefinite length is not supported.', WebAuthnException::CBOR);
+        }
+
+        return $val;
+    }
+
+    protected static function _parseItemData($type, $val, ByteBuffer $buf, &$offset) {
+        switch ($type) {
+            case self::CBOR_MAJOR_UNSIGNED_INT: // uint
+                return $val;
+
+            case self::CBOR_MAJOR_NEGATIVE_INT:
+                return -1 - $val;
+
+            case self::CBOR_MAJOR_BYTE_STRING:
+                $data = $buf->getBytes($offset, $val);
+                $offset += $val;
+                return new ByteBuffer($data); // bytes
+
+            case self::CBOR_MAJOR_TEXT_STRING:
+                $data = $buf->getBytes($offset, $val);
+                $offset += $val;
+                return $data; // UTF-8
+
+            case self::CBOR_MAJOR_ARRAY:
+                return self::_parseArray($buf, $offset, $val);
+
+            case self::CBOR_MAJOR_MAP:
+                return self::_parseMap($buf, $offset, $val);
+
+            case self::CBOR_MAJOR_TAG:
+                return self::_parseItem($buf, $offset); // 1 embedded data item
+        }
+
+        // This should never be reached
+        throw new WebAuthnException(sprintf('Unknown major type %d.', $type), WebAuthnException::CBOR);
+    }
+
+    protected static function _parseMap(ByteBuffer $buf, &$offset, $count) {
+        $map = array();
+
+        for ($i = 0; $i < $count; $i++) {
+            $mapKey = self::_parseItem($buf, $offset);
+            $mapVal = self::_parseItem($buf, $offset);
+
+            if (!\is_int($mapKey) && !\is_string($mapKey)) {
+                throw new WebAuthnException('Can only use strings or integers as map keys', WebAuthnException::CBOR);
+            }
+
+            $map[$mapKey] = $mapVal; // todo dup
+        }
+        return $map;
+    }
+
+    protected static function _parseArray(ByteBuffer $buf, &$offset, $count) {
+        $arr = array();
+        for ($i = 0; $i < $count; $i++) {
+            $arr[] = self::_parseItem($buf, $offset);
+        }
+
+        return $arr;
+    }
+}
diff --git a/src/lib/WebAuthn/LICENSE b/src/lib/WebAuthn/LICENSE
new file mode 100644
index 0000000..0580d1b
--- /dev/null
+++ b/src/lib/WebAuthn/LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright © 2022 Lukas Buchs
+Copyright © 2018 Thomas Bleeker (CBOR & ByteBuffer part)
+
+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:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/src/lib/WebAuthn/WebAuthn.php b/src/lib/WebAuthn/WebAuthn.php
new file mode 100644
index 0000000..0da0aa6
--- /dev/null
+++ b/src/lib/WebAuthn/WebAuthn.php
@@ -0,0 +1,677 @@
+<?php
+
+namespace lbuchs\WebAuthn;
+use lbuchs\WebAuthn\Binary\ByteBuffer;
+require_once 'WebAuthnException.php';
+require_once 'Binary/ByteBuffer.php';
+require_once 'Attestation/AttestationObject.php';
+require_once 'Attestation/AuthenticatorData.php';
+require_once 'Attestation/Format/FormatBase.php';
+require_once 'Attestation/Format/None.php';
+require_once 'Attestation/Format/AndroidKey.php';
+require_once 'Attestation/Format/AndroidSafetyNet.php';
+require_once 'Attestation/Format/Apple.php';
+require_once 'Attestation/Format/Packed.php';
+require_once 'Attestation/Format/Tpm.php';
+require_once 'Attestation/Format/U2f.php';
+require_once 'CBOR/CborDecoder.php';
+
+/**
+ * WebAuthn
+ * @author Lukas Buchs
+ * @license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT
+ */
+class WebAuthn {
+    // relying party
+    private $_rpName;
+    private $_rpId;
+    private $_rpIdHash;
+    private $_challenge;
+    private $_signatureCounter;
+    private $_caFiles;
+    private $_formats;
+
+    /**
+     * Initialize a new WebAuthn server
+     * @param string $rpName the relying party name
+     * @param string $rpId the relying party ID = the domain name
+     * @param bool $useBase64UrlEncoding true to use base64 url encoding for binary data in json objects. Default is a RFC 1342-Like serialized string.
+     * @throws WebAuthnException
+     */
+    public function __construct($rpName, $rpId, $allowedFormats=null, $useBase64UrlEncoding=false) {
+        $this->_rpName = $rpName;
+        $this->_rpId = $rpId;
+        $this->_rpIdHash = \hash('sha256', $rpId, true);
+        ByteBuffer::$useBase64UrlEncoding = !!$useBase64UrlEncoding;
+        $supportedFormats = array('android-key', 'android-safetynet', 'apple', 'fido-u2f', 'none', 'packed', 'tpm');
+
+        if (!\function_exists('\openssl_open')) {
+            throw new WebAuthnException('OpenSSL-Module not installed');
+        }
+
+        if (!\in_array('SHA256', \array_map('\strtoupper', \openssl_get_md_methods()))) {
+            throw new WebAuthnException('SHA256 not supported by this openssl installation.');
+        }
+
+        // default: all format
+        if (!is_array($allowedFormats)) {
+            $allowedFormats = $supportedFormats;
+        }
+        $this->_formats = $allowedFormats;
+
+        // validate formats
+        $invalidFormats = \array_diff($this->_formats, $supportedFormats);
+        if (!$this->_formats || $invalidFormats) {
+            throw new WebAuthnException('invalid formats on construct: ' . implode(', ', $invalidFormats));
+        }
+    }
+
+    /**
+     * add a root certificate to verify new registrations
+     * @param string $path file path of / directory with root certificates
+     * @param array|null $certFileExtensions if adding a direction, all files with provided extension are added. default: pem, crt, cer, der
+     */
+    public function addRootCertificates($path, $certFileExtensions=null) {
+        if (!\is_array($this->_caFiles)) {
+            $this->_caFiles = [];
+        }
+        if ($certFileExtensions === null) {
+            $certFileExtensions = array('pem', 'crt', 'cer', 'der');
+        }
+        $path = \rtrim(\trim($path), '\\/');
+        if (\is_dir($path)) {
+            foreach (\scandir($path) as $ca) {
+                if (\is_file($path . DIRECTORY_SEPARATOR . $ca) && \in_array(\strtolower(\pathinfo($ca, PATHINFO_EXTENSION)), $certFileExtensions)) {
+                    $this->addRootCertificates($path . DIRECTORY_SEPARATOR . $ca);
+                }
+            }
+        } else if (\is_file($path) && !\in_array(\realpath($path), $this->_caFiles)) {
+            $this->_caFiles[] = \realpath($path);
+        }
+    }
+
+    /**
+     * Returns the generated challenge to save for later validation
+     * @return ByteBuffer
+     */
+    public function getChallenge() {
+        return $this->_challenge;
+    }
+
+    /**
+     * generates the object for a key registration
+     * provide this data to navigator.credentials.create
+     * @param string $userId
+     * @param string $userName
+     * @param string $userDisplayName
+     * @param int $timeout timeout in seconds
+     * @param bool|string $requireResidentKey      'required', if the key should be stored by the authentication device
+     *                                             Valid values:
+     *                                             true = required
+     *                                             false = preferred
+     *                                             string 'required' 'preferred' 'discouraged'
+     * @param bool|string $requireUserVerification indicates that you require user verification and will fail the operation
+     *                                             if the response does not have the UV flag set.
+     *                                             Valid values:
+     *                                             true = required
+     *                                             false = preferred
+     *                                             string 'required' 'preferred' 'discouraged'
+     * @param bool|null $crossPlatformAttachment   true for cross-platform devices (eg. fido usb),
+     *                                             false for platform devices (eg. windows hello, android safetynet),
+     *                                             null for both
+     * @param array $excludeCredentialIds a array of ids, which are already registered, to prevent re-registration
+     * @return \stdClass
+     */
+    public function getCreateArgs($userId, $userName, $userDisplayName, $timeout=20, $requireResidentKey=false, $requireUserVerification=false, $crossPlatformAttachment=null, $excludeCredentialIds=[]) {
+
+        $args = new \stdClass();
+        $args->publicKey = new \stdClass();
+
+        // relying party
+        $args->publicKey->rp = new \stdClass();
+        $args->publicKey->rp->name = $this->_rpName;
+        $args->publicKey->rp->id = $this->_rpId;
+
+        $args->publicKey->authenticatorSelection = new \stdClass();
+        $args->publicKey->authenticatorSelection->userVerification = 'preferred';
+
+        // validate User Verification Requirement
+        if (\is_bool($requireUserVerification)) {
+            $args->publicKey->authenticatorSelection->userVerification = $requireUserVerification ? 'required' : 'preferred';
+
+        } else if (\is_string($requireUserVerification) && \in_array(\strtolower($requireUserVerification), ['required', 'preferred', 'discouraged'])) {
+            $args->publicKey->authenticatorSelection->userVerification = \strtolower($requireUserVerification);
+        }
+
+        // validate Resident Key Requirement
+        if (\is_bool($requireResidentKey) && $requireResidentKey) {
+            $args->publicKey->authenticatorSelection->requireResidentKey = true;
+            $args->publicKey->authenticatorSelection->residentKey = 'required';
+
+        } else if (\is_string($requireResidentKey) && \in_array(\strtolower($requireResidentKey), ['required', 'preferred', 'discouraged'])) {
+            $requireResidentKey = \strtolower($requireResidentKey);
+            $args->publicKey->authenticatorSelection->residentKey = $requireResidentKey;
+            $args->publicKey->authenticatorSelection->requireResidentKey = $requireResidentKey === 'required';
+        }
+
+        // filte authenticators attached with the specified authenticator attachment modality
+        if (\is_bool($crossPlatformAttachment)) {
+            $args->publicKey->authenticatorSelection->authenticatorAttachment = $crossPlatformAttachment ? 'cross-platform' : 'platform';
+        }
+
+        // user
+        $args->publicKey->user = new \stdClass();
+        $args->publicKey->user->id = new ByteBuffer($userId); // binary
+        $args->publicKey->user->name = $userName;
+        $args->publicKey->user->displayName = $userDisplayName;
+
+        // supported algorithms
+        $args->publicKey->pubKeyCredParams = [];
+
+        if (function_exists('sodium_crypto_sign_verify_detached') || \in_array('ed25519', \openssl_get_curve_names(), true)) {
+            $tmp = new \stdClass();
+            $tmp->type = 'public-key';
+            $tmp->alg = -8; // EdDSA
+            $args->publicKey->pubKeyCredParams[] = $tmp;
+            unset ($tmp);
+        }
+
+        if (\in_array('prime256v1', \openssl_get_curve_names(), true)) {
+            $tmp = new \stdClass();
+            $tmp->type = 'public-key';
+            $tmp->alg = -7; // ES256
+            $args->publicKey->pubKeyCredParams[] = $tmp;
+            unset ($tmp);
+        }
+
+        $tmp = new \stdClass();
+        $tmp->type = 'public-key';
+        $tmp->alg = -257; // RS256
+        $args->publicKey->pubKeyCredParams[] = $tmp;
+        unset ($tmp);
+
+        // if there are root certificates added, we need direct attestation to validate
+        // against the root certificate. If there are no root-certificates added,
+        // anonymization ca are also accepted, because we can't validate the root anyway.
+        $attestation = 'indirect';
+        if (\is_array($this->_caFiles)) {
+            $attestation = 'direct';
+        }
+
+        $args->publicKey->attestation = \count($this->_formats) === 1 && \in_array('none', $this->_formats) ? 'none' : $attestation;
+        $args->publicKey->extensions = new \stdClass();
+        $args->publicKey->extensions->exts = true;
+        $args->publicKey->timeout = $timeout * 1000; // microseconds
+        $args->publicKey->challenge = $this->_createChallenge(); // binary
+
+        //prevent re-registration by specifying existing credentials
+        $args->publicKey->excludeCredentials = [];
+
+        if (is_array($excludeCredentialIds)) {
+            foreach ($excludeCredentialIds as $id) {
+                $tmp = new \stdClass();
+                $tmp->id = $id instanceof ByteBuffer ? $id : new ByteBuffer($id);  // binary
+                $tmp->type = 'public-key';
+                $tmp->transports = array('usb', 'nfc', 'ble', 'hybrid', 'internal');
+                $args->publicKey->excludeCredentials[] = $tmp;
+                unset ($tmp);
+            }
+        }
+
+        return $args;
+    }
+
+    /**
+     * generates the object for key validation
+     * Provide this data to navigator.credentials.get
+     * @param array $credentialIds binary
+     * @param int $timeout timeout in seconds
+     * @param bool $allowUsb allow removable USB
+     * @param bool $allowNfc allow Near Field Communication (NFC)
+     * @param bool $allowBle allow Bluetooth
+     * @param bool $allowHybrid allow a combination of (often separate) data-transport and proximity mechanisms.
+     * @param bool $allowInternal allow client device-specific transport. These authenticators are not removable from the client device.
+     * @param bool|string $requireUserVerification indicates that you require user verification and will fail the operation
+     *                                             if the response does not have the UV flag set.
+     *                                             Valid values:
+     *                                             true = required
+     *                                             false = preferred
+     *                                             string 'required' 'preferred' 'discouraged'
+     * @return \stdClass
+     */
+    public function getGetArgs($credentialIds=[], $timeout=20, $allowUsb=true, $allowNfc=true, $allowBle=true, $allowHybrid=true, $allowInternal=true, $requireUserVerification=false) {
+
+        // validate User Verification Requirement
+        if (\is_bool($requireUserVerification)) {
+            $requireUserVerification = $requireUserVerification ? 'required' : 'preferred';
+        } else if (\is_string($requireUserVerification) && \in_array(\strtolower($requireUserVerification), ['required', 'preferred', 'discouraged'])) {
+            $requireUserVerification = \strtolower($requireUserVerification);
+        } else {
+            $requireUserVerification = 'preferred';
+        }
+
+        $args = new \stdClass();
+        $args->publicKey = new \stdClass();
+        $args->publicKey->timeout = $timeout * 1000; // microseconds
+        $args->publicKey->challenge = $this->_createChallenge();  // binary
+        $args->publicKey->userVerification = $requireUserVerification;
+        $args->publicKey->rpId = $this->_rpId;
+
+        if (\is_array($credentialIds) && \count($credentialIds) > 0) {
+            $args->publicKey->allowCredentials = [];
+
+            foreach ($credentialIds as $id) {
+                $tmp = new \stdClass();
+                $tmp->id = $id instanceof ByteBuffer ? $id : new ByteBuffer($id);  // binary
+                $tmp->transports = [];
+
+                if ($allowUsb) {
+                    $tmp->transports[] = 'usb';
+                }
+                if ($allowNfc) {
+                    $tmp->transports[] = 'nfc';
+                }
+                if ($allowBle) {
+                    $tmp->transports[] = 'ble';
+                }
+                if ($allowHybrid) {
+                    $tmp->transports[] = 'hybrid';
+                }
+                if ($allowInternal) {
+                    $tmp->transports[] = 'internal';
+                }
+
+                $tmp->type = 'public-key';
+                $args->publicKey->allowCredentials[] = $tmp;
+                unset ($tmp);
+            }
+        }
+
+        return $args;
+    }
+
+    /**
+     * returns the new signature counter value.
+     * returns null if there is no counter
+     * @return ?int
+     */
+    public function getSignatureCounter() {
+        return \is_int($this->_signatureCounter) ? $this->_signatureCounter : null;
+    }
+
+    /**
+     * process a create request and returns data to save for future logins
+     * @param string $clientDataJSON binary from browser
+     * @param string $attestationObject binary from browser
+     * @param string|ByteBuffer $challenge binary used challange
+     * @param bool $requireUserVerification true, if the device must verify user (e.g. by biometric data or pin)
+     * @param bool $requireUserPresent false, if the device must NOT check user presence (e.g. by pressing a button)
+     * @param bool $failIfRootMismatch false, if there should be no error thrown if root certificate doesn't match
+     * @param bool $requireCtsProfileMatch false, if you don't want to check if the device is approved as a Google-certified Android device.
+     * @return \stdClass
+     * @throws WebAuthnException
+     */
+    public function processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true, $failIfRootMismatch=true, $requireCtsProfileMatch=true) {
+        $clientDataHash = \hash('sha256', $clientDataJSON, true);
+        $clientData = \json_decode($clientDataJSON);
+        $challenge = $challenge instanceof ByteBuffer ? $challenge : new ByteBuffer($challenge);
+
+        // security: https://www.w3.org/TR/webauthn/#registering-a-new-credential
+
+        // 2. Let C, the client data claimed as collected during the credential creation,
+        //    be the result of running an implementation-specific JSON parser on JSONtext.
+        if (!\is_object($clientData)) {
+            throw new WebAuthnException('invalid client data', WebAuthnException::INVALID_DATA);
+        }
+
+        // 3. Verify that the value of C.type is webauthn.create.
+        if (!\property_exists($clientData, 'type') || $clientData->type !== 'webauthn.create') {
+            throw new WebAuthnException('invalid type', WebAuthnException::INVALID_TYPE);
+        }
+
+        // 4. Verify that the value of C.challenge matches the challenge that was sent to the authenticator in the create() call.
+        if (!\property_exists($clientData, 'challenge') || ByteBuffer::fromBase64Url($clientData->challenge)->getBinaryString() !== $challenge->getBinaryString()) {
+            throw new WebAuthnException('invalid challenge', WebAuthnException::INVALID_CHALLENGE);
+        }
+
+        // 5. Verify that the value of C.origin matches the Relying Party's origin.
+        if (!\property_exists($clientData, 'origin') || !$this->_checkOrigin($clientData->origin)) {
+            throw new WebAuthnException('invalid origin', WebAuthnException::INVALID_ORIGIN);
+        }
+
+        // Attestation
+        $attestationObject = new Attestation\AttestationObject($attestationObject, $this->_formats);
+
+        // 9. Verify that the RP ID hash in authData is indeed the SHA-256 hash of the RP ID expected by the RP.
+        if (!$attestationObject->validateRpIdHash($this->_rpIdHash)) {
+            throw new WebAuthnException('invalid rpId hash', WebAuthnException::INVALID_RELYING_PARTY);
+        }
+
+        // 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature
+        if (!$attestationObject->validateAttestation($clientDataHash)) {
+            throw new WebAuthnException('invalid certificate signature', WebAuthnException::INVALID_SIGNATURE);
+        }
+
+        // Android-SafetyNet: if required, check for Compatibility Testing Suite (CTS).
+        if ($requireCtsProfileMatch && $attestationObject->getAttestationFormat() instanceof Attestation\Format\AndroidSafetyNet) {
+            if (!$attestationObject->getAttestationFormat()->ctsProfileMatch()) {
+                 throw new WebAuthnException('invalid ctsProfileMatch: device is not approved as a Google-certified Android device.', WebAuthnException::ANDROID_NOT_TRUSTED);
+            }
+        }
+
+        // 15. If validation is successful, obtain a list of acceptable trust anchors
+        $rootValid = is_array($this->_caFiles) ? $attestationObject->validateRootCertificate($this->_caFiles) : null;
+        if ($failIfRootMismatch && is_array($this->_caFiles) && !$rootValid) {
+            throw new WebAuthnException('invalid root certificate', WebAuthnException::CERTIFICATE_NOT_TRUSTED);
+        }
+
+        // 10. Verify that the User Present bit of the flags in authData is set.
+        $userPresent = $attestationObject->getAuthenticatorData()->getUserPresent();
+        if ($requireUserPresent && !$userPresent) {
+            throw new WebAuthnException('user not present during authentication', WebAuthnException::USER_PRESENT);
+        }
+
+        // 11. If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set.
+        $userVerified = $attestationObject->getAuthenticatorData()->getUserVerified();
+        if ($requireUserVerification && !$userVerified) {
+            throw new WebAuthnException('user not verified during authentication', WebAuthnException::USER_VERIFICATED);
+        }
+
+        $signCount = $attestationObject->getAuthenticatorData()->getSignCount();
+        if ($signCount > 0) {
+            $this->_signatureCounter = $signCount;
+        }
+
+        // prepare data to store for future logins
+        $data = new \stdClass();
+        $data->rpId = $this->_rpId;
+        $data->attestationFormat = $attestationObject->getAttestationFormatName();
+        $data->credentialId = $attestationObject->getAuthenticatorData()->getCredentialId();
+        $data->credentialPublicKey = $attestationObject->getAuthenticatorData()->getPublicKeyPem();
+        $data->certificateChain = $attestationObject->getCertificateChain();
+        $data->certificate = $attestationObject->getCertificatePem();
+        $data->certificateIssuer = $attestationObject->getCertificateIssuer();
+        $data->certificateSubject = $attestationObject->getCertificateSubject();
+        $data->signatureCounter = $this->_signatureCounter;
+        $data->AAGUID = $attestationObject->getAuthenticatorData()->getAAGUID();
+        $data->rootValid = $rootValid;
+        $data->userPresent = $userPresent;
+        $data->userVerified = $userVerified;
+        return $data;
+    }
+
+
+    /**
+     * process a get request
+     * @param string $clientDataJSON binary from browser
+     * @param string $authenticatorData binary from browser
+     * @param string $signature binary from browser
+     * @param string $credentialPublicKey string PEM-formated public key from used credentialId
+     * @param string|ByteBuffer $challenge  binary from used challange
+     * @param int $prevSignatureCnt signature count value of the last login
+     * @param bool $requireUserVerification true, if the device must verify user (e.g. by biometric data or pin)
+     * @param bool $requireUserPresent true, if the device must check user presence (e.g. by pressing a button)
+     * @return boolean true if get is successful
+     * @throws WebAuthnException
+     */
+    public function processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge, $prevSignatureCnt=null, $requireUserVerification=false, $requireUserPresent=true) {
+        $authenticatorObj = new Attestation\AuthenticatorData($authenticatorData);
+        $clientDataHash = \hash('sha256', $clientDataJSON, true);
+        $clientData = \json_decode($clientDataJSON);
+        $challenge = $challenge instanceof ByteBuffer ? $challenge : new ByteBuffer($challenge);
+
+        // https://www.w3.org/TR/webauthn/#verifying-assertion
+
+        // 1. If the allowCredentials option was given when this authentication ceremony was initiated,
+        //    verify that credential.id identifies one of the public key credentials that were listed in allowCredentials.
+        //    -> TO BE VERIFIED BY IMPLEMENTATION
+
+        // 2. If credential.response.userHandle is present, verify that the user identified
+        //    by this value is the owner of the public key credential identified by credential.id.
+        //    -> TO BE VERIFIED BY IMPLEMENTATION
+
+        // 3. Using credential’s id attribute (or the corresponding rawId, if base64url encoding is
+        //    inappropriate for your use case), look up the corresponding credential public key.
+        //    -> TO BE LOOKED UP BY IMPLEMENTATION
+
+        // 5. Let JSONtext be the result of running UTF-8 decode on the value of cData.
+        if (!\is_object($clientData)) {
+            throw new WebAuthnException('invalid client data', WebAuthnException::INVALID_DATA);
+        }
+
+        // 7. Verify that the value of C.type is the string webauthn.get.
+        if (!\property_exists($clientData, 'type') || $clientData->type !== 'webauthn.get') {
+            throw new WebAuthnException('invalid type', WebAuthnException::INVALID_TYPE);
+        }
+
+        // 8. Verify that the value of C.challenge matches the challenge that was sent to the
+        //    authenticator in the PublicKeyCredentialRequestOptions passed to the get() call.
+        if (!\property_exists($clientData, 'challenge') || ByteBuffer::fromBase64Url($clientData->challenge)->getBinaryString() !== $challenge->getBinaryString()) {
+            throw new WebAuthnException('invalid challenge', WebAuthnException::INVALID_CHALLENGE);
+        }
+
+        // 9. Verify that the value of C.origin matches the Relying Party's origin.
+        if (!\property_exists($clientData, 'origin') || !$this->_checkOrigin($clientData->origin)) {
+            throw new WebAuthnException('invalid origin', WebAuthnException::INVALID_ORIGIN);
+        }
+
+        // 11. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the Relying Party.
+        if ($authenticatorObj->getRpIdHash() !== $this->_rpIdHash) {
+            throw new WebAuthnException('invalid rpId hash', WebAuthnException::INVALID_RELYING_PARTY);
+        }
+
+        // 12. Verify that the User Present bit of the flags in authData is set
+        if ($requireUserPresent && !$authenticatorObj->getUserPresent()) {
+            throw new WebAuthnException('user not present during authentication', WebAuthnException::USER_PRESENT);
+        }
+
+        // 13. If user verification is required for this assertion, verify that the User Verified bit of the flags in authData is set.
+        if ($requireUserVerification && !$authenticatorObj->getUserVerified()) {
+            throw new WebAuthnException('user not verificated during authentication', WebAuthnException::USER_VERIFICATED);
+        }
+
+        // 14. Verify the values of the client extension outputs
+        //     (extensions not implemented)
+
+        // 16. Using the credential public key looked up in step 3, verify that sig is a valid signature
+        //     over the binary concatenation of authData and hash.
+        $dataToVerify = '';
+        $dataToVerify .= $authenticatorData;
+        $dataToVerify .= $clientDataHash;
+
+        if (!$this->_verifySignature($dataToVerify, $signature, $credentialPublicKey)) {
+            throw new WebAuthnException('invalid signature', WebAuthnException::INVALID_SIGNATURE);
+        }
+
+        $signatureCounter = $authenticatorObj->getSignCount();
+        if ($signatureCounter !== 0) {
+            $this->_signatureCounter = $signatureCounter;
+        }
+
+        // 17. If either of the signature counter value authData.signCount or
+        //     previous signature count is nonzero, and if authData.signCount
+        //     less than or equal to previous signature count, it's a signal
+        //     that the authenticator may be cloned
+        if ($prevSignatureCnt !== null) {
+            if ($signatureCounter !== 0 || $prevSignatureCnt !== 0) {
+                if ($prevSignatureCnt >= $signatureCounter) {
+                    throw new WebAuthnException('signature counter not valid', WebAuthnException::SIGNATURE_COUNTER);
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Downloads root certificates from FIDO Alliance Metadata Service (MDS) to a specific folder
+     * https://fidoalliance.org/metadata/
+     * @param string $certFolder Folder path to save the certificates in PEM format.
+     * @param bool $deleteCerts delete certificates in the target folder before adding the new ones.
+     * @return int number of cetificates
+     * @throws WebAuthnException
+     */
+    public function queryFidoMetaDataService($certFolder, $deleteCerts=true) {
+        $url = 'https://mds.fidoalliance.org/';
+        $raw = null;
+        if (\function_exists('curl_init')) {
+            $ch = \curl_init($url);
+            \curl_setopt($ch, CURLOPT_HEADER, false);
+            \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+            \curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+            \curl_setopt($ch, CURLOPT_USERAGENT, 'github.com/lbuchs/WebAuthn - A simple PHP WebAuthn server library');
+            $raw = \curl_exec($ch);
+            \curl_close($ch);
+        } else {
+            $raw = \file_get_contents($url);
+        }
+
+        $certFolder = \rtrim(\realpath($certFolder), '\\/');
+        if (!is_dir($certFolder)) {
+            throw new WebAuthnException('Invalid folder path for query FIDO Alliance Metadata Service');
+        }
+
+        if (!\is_string($raw)) {
+            throw new WebAuthnException('Unable to query FIDO Alliance Metadata Service');
+        }
+
+        $jwt = \explode('.', $raw);
+        if (\count($jwt) !== 3) {
+            throw new WebAuthnException('Invalid JWT from FIDO Alliance Metadata Service');
+        }
+
+        if ($deleteCerts) {
+            foreach (\scandir($certFolder) as $ca) {
+                if (\substr($ca, -4) === '.pem') {
+                    if (\unlink($certFolder . DIRECTORY_SEPARATOR . $ca) === false) {
+                        throw new WebAuthnException('Cannot delete certs in folder for FIDO Alliance Metadata Service');
+                    }
+                }
+            }
+        }
+
+        list($header, $payload, $hash) = $jwt;
+        $payload = Binary\ByteBuffer::fromBase64Url($payload)->getJson();
+
+        $count = 0;
+        if (\is_object($payload) && \property_exists($payload, 'entries') && \is_array($payload->entries)) {
+            foreach ($payload->entries as $entry) {
+                if (\is_object($entry) && \property_exists($entry, 'metadataStatement') && \is_object($entry->metadataStatement)) {
+                    $description = $entry->metadataStatement->description ?? null;
+                    $attestationRootCertificates = $entry->metadataStatement->attestationRootCertificates ?? null;
+
+                    if ($description && $attestationRootCertificates) {
+
+                        // create filename
+                        $certFilename = \preg_replace('/[^a-z0-9]/i', '_', $description);
+                        $certFilename = \trim(\preg_replace('/\_{2,}/i', '_', $certFilename),'_') . '.pem';
+                        $certFilename = \strtolower($certFilename);
+
+                        // add certificate
+                        $certContent = $description . "\n";
+                        $certContent .= \str_repeat('-', \mb_strlen($description)) . "\n";
+
+                        foreach ($attestationRootCertificates as $attestationRootCertificate) {
+                            $attestationRootCertificate = \str_replace(["\n", "\r", ' '], '', \trim($attestationRootCertificate));
+                            $count++;
+                            $certContent .= "\n-----BEGIN CERTIFICATE-----\n";
+                            $certContent .= \chunk_split($attestationRootCertificate, 64, "\n");
+                            $certContent .= "-----END CERTIFICATE-----\n";
+                        }
+
+                        if (\file_put_contents($certFolder . DIRECTORY_SEPARATOR . $certFilename, $certContent) === false) {
+                            throw new WebAuthnException('unable to save certificate from FIDO Alliance Metadata Service');
+                        }
+                    }
+                }
+            }
+        }
+
+        return $count;
+    }
+
+    // -----------------------------------------------
+    // PRIVATE
+    // -----------------------------------------------
+
+    /**
+     * checks if the origin matchs the RP ID
+     * @param string $origin
+     * @return boolean
+     * @throws WebAuthnException
+     */
+    private function _checkOrigin($origin) {
+        // https://www.w3.org/TR/webauthn/#rp-id
+
+        // The origin's scheme must be https
+        if ($this->_rpId !== 'localhost' && \parse_url($origin, PHP_URL_SCHEME) !== 'https') {
+            return false;
+        }
+
+        // extract host from origin
+        $host = \parse_url($origin, PHP_URL_HOST);
+        $host = \trim($host, '.');
+
+        // The RP ID must be equal to the origin's effective domain, or a registrable
+        // domain suffix of the origin's effective domain.
+        return \preg_match('/' . \preg_quote($this->_rpId) . '$/i', $host) === 1;
+    }
+
+    /**
+     * generates a new challange
+     * @param int $length
+     * @return string
+     * @throws WebAuthnException
+     */
+    private function _createChallenge($length = 32) {
+        if (!$this->_challenge) {
+            $this->_challenge = ByteBuffer::randomBuffer($length);
+        }
+        return $this->_challenge;
+    }
+
+    /**
+     * check if the signature is valid.
+     * @param string $dataToVerify
+     * @param string $signature
+     * @param string $credentialPublicKey PEM format
+     * @return bool
+     */
+    private function _verifySignature($dataToVerify, $signature, $credentialPublicKey) {
+
+        // Use Sodium to verify EdDSA 25519 as its not yet supported by openssl
+        if (\function_exists('sodium_crypto_sign_verify_detached') && !\in_array('ed25519', \openssl_get_curve_names(), true)) {
+            $pkParts = [];
+            if (\preg_match('/BEGIN PUBLIC KEY\-+(?:\s|\n|\r)+([^\-]+)(?:\s|\n|\r)*\-+END PUBLIC KEY/i', $credentialPublicKey, $pkParts)) {
+                $rawPk = \base64_decode($pkParts[1]);
+
+                // 30        = der sequence
+                // 2a        = length 42 byte
+                // 30        = der sequence
+                // 05        = lenght 5 byte
+                // 06        = der OID
+                // 03        = OID length 3 byte
+                // 2b 65 70  = OID 1.3.101.112 curveEd25519 (EdDSA 25519 signature algorithm)
+                // 03        = der bit string
+                // 21        = length 33 byte
+                // 00        = null padding
+                // [...]     = 32 byte x-curve
+                $okpPrefix = "\x30\x2a\x30\x05\x06\x03\x2b\x65\x70\x03\x21\x00";
+
+                if ($rawPk && \strlen($rawPk) === 44 && \substr($rawPk,0, \strlen($okpPrefix)) === $okpPrefix) {
+                    $publicKeyXCurve = \substr($rawPk, \strlen($okpPrefix));
+
+                    return \sodium_crypto_sign_verify_detached($signature, $dataToVerify, $publicKeyXCurve);
+                }
+            }
+        }
+
+        // verify with openSSL
+        $publicKey = \openssl_pkey_get_public($credentialPublicKey);
+        if ($publicKey === false) {
+            throw new WebAuthnException('public key invalid', WebAuthnException::INVALID_PUBLIC_KEY);
+        }
+
+        return \openssl_verify($dataToVerify, $signature, $publicKey, OPENSSL_ALGO_SHA256) === 1;
+    }
+}
diff --git a/src/lib/WebAuthn/WebAuthnException.php b/src/lib/WebAuthn/WebAuthnException.php
new file mode 100644
index 0000000..f27eeec
--- /dev/null
+++ b/src/lib/WebAuthn/WebAuthnException.php
@@ -0,0 +1,28 @@
+<?php
+namespace lbuchs\WebAuthn;
+
+/**
+ * @author Lukas Buchs
+ * @license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT
+ */
+class WebAuthnException extends \Exception {
+    const INVALID_DATA = 1;
+    const INVALID_TYPE = 2;
+    const INVALID_CHALLENGE = 3;
+    const INVALID_ORIGIN = 4;
+    const INVALID_RELYING_PARTY = 5;
+    const INVALID_SIGNATURE = 6;
+    const INVALID_PUBLIC_KEY = 7;
+    const CERTIFICATE_NOT_TRUSTED = 8;
+    const USER_PRESENT = 9;
+    const USER_VERIFICATED = 10;
+    const SIGNATURE_COUNTER = 11;
+    const CRYPTO_STRONG = 13;
+    const BYTEBUFFER = 14;
+    const CBOR = 15;
+    const ANDROID_NOT_TRUSTED = 16;
+
+    public function __construct($message = "", $code = 0, $previous = null) {
+        parent::__construct($message, $code, $previous);
+    }
+}
diff --git a/src/lib/datatables/dataTables.material.min.js b/src/lib/datatables/dataTables.material.min.js
new file mode 100644
index 0000000..bafd12e
--- /dev/null
+++ b/src/lib/datatables/dataTables.material.min.js
@@ -0,0 +1,8 @@
+/*!
+ DataTables Bootstrap 3 integration
+ ©2011-2015 SpryMedia Ltd - datatables.net/license
+*/
+(function(c){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(a){return c(a,window,document)}):"object"===typeof exports?module.exports=function(a,d){a||(a=window);if(!d||!d.fn.dataTable)d=require("datatables.net")(a,d).$;return c(d,a,a.document)}:c(jQuery,window,document)})(function(c,a,d,r){var g=c.fn.dataTable;c.extend(!0,g.defaults,{dom:"<'mdl-grid'<'mdl-cell mdl-cell--6-col'l><'mdl-cell mdl-cell--6-col'f>><'mdl-grid dt-table'<'mdl-cell mdl-cell--12-col'tr>><'mdl-grid'<'mdl-cell mdl-cell--4-col'i><'mdl-cell mdl-cell--8-col'p>>",
+renderer:"material"});c.extend(g.ext.classes,{sWrapper:"dataTables_wrapper form-inline dt-material",sFilterInput:"form-control input-sm",sLengthSelect:"form-control input-sm",sProcessing:"dataTables_processing panel panel-default"});g.ext.renderer.pageButton.material=function(a,h,s,t,i,n){var o=new g.Api(a),l=a.oLanguage.oPaginate,u=a.oLanguage.oAria.paginate||{},f,e,p=0,q=function(d,g){var m,h,j,b,k=function(a){a.preventDefault();!c(a.currentTarget).hasClass("disabled")&&o.page()!=a.data.action&&
+o.page(a.data.action).draw("page")};m=0;for(h=g.length;m<h;m++)if(b=g[m],c.isArray(b))q(d,b);else{f="";j=!1;switch(b){case "ellipsis":f="&#x2026;";e="disabled";break;case "first":f=l.sFirst;e=b+(0<i?"":" disabled");break;case "previous":f=l.sPrevious;e=b+(0<i?"":" disabled");break;case "next":f=l.sNext;e=b+(i<n-1?"":" disabled");break;case "last":f=l.sLast;e=b+(i<n-1?"":" disabled");break;default:f=b+1,e="",j=i===b}j&&(e+=" mdl-button--raised mdl-button--colored");f&&(j=c("<button>",{"class":"mdl-button "+
+e,id:0===s&&"string"===typeof b?a.sTableId+"_"+b:null,"aria-controls":a.sTableId,"aria-label":u[b],"data-dt-idx":p,tabindex:a.iTabIndex,disabled:-1!==e.indexOf("disabled")}).html(f).appendTo(d),a.oApi._fnBindAction(j,{action:b},k),p++)}},k;try{k=c(h).find(d.activeElement).data("dt-idx")}catch(v){}q(c(h).empty().html('<div class="pagination"/>').children(),t);k!==r&&c(h).find("[data-dt-idx="+k+"]").focus()};return g});
diff --git a/src/lib/fpdf-easytable/LICENSE b/src/lib/fpdf-easytable/LICENSE
new file mode 100644
index 0000000..00d2e13
--- /dev/null
+++ b/src/lib/fpdf-easytable/LICENSE
@@ -0,0 +1,24 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to <http://unlicense.org/>
\ No newline at end of file
diff --git a/src/lib/fpdf-easytable/Pics/a.png b/src/lib/fpdf-easytable/Pics/a.png
new file mode 100644
index 0000000..10c6f5b
--- /dev/null
+++ b/src/lib/fpdf-easytable/Pics/a.png
Binary files differ
diff --git a/src/lib/fpdf-easytable/Pics/b.png b/src/lib/fpdf-easytable/Pics/b.png
new file mode 100644
index 0000000..8a31a08
--- /dev/null
+++ b/src/lib/fpdf-easytable/Pics/b.png
Binary files differ
diff --git a/src/lib/fpdf-easytable/Pics/c.png b/src/lib/fpdf-easytable/Pics/c.png
new file mode 100644
index 0000000..a63817f
--- /dev/null
+++ b/src/lib/fpdf-easytable/Pics/c.png
Binary files differ
diff --git a/src/lib/fpdf-easytable/Pics/d.png b/src/lib/fpdf-easytable/Pics/d.png
new file mode 100644
index 0000000..bd146c8
--- /dev/null
+++ b/src/lib/fpdf-easytable/Pics/d.png
Binary files differ
diff --git a/src/lib/fpdf-easytable/Pics/fpdf.png b/src/lib/fpdf-easytable/Pics/fpdf.png
new file mode 100644
index 0000000..955fa7f
--- /dev/null
+++ b/src/lib/fpdf-easytable/Pics/fpdf.png
Binary files differ
diff --git a/src/lib/fpdf-easytable/Pics/fpdflogo.gif b/src/lib/fpdf-easytable/Pics/fpdflogo.gif
new file mode 100644
index 0000000..60b628a
--- /dev/null
+++ b/src/lib/fpdf-easytable/Pics/fpdflogo.gif
Binary files differ
diff --git a/src/lib/fpdf-easytable/Pics/fpdflogo.png b/src/lib/fpdf-easytable/Pics/fpdflogo.png
new file mode 100644
index 0000000..2d96301
--- /dev/null
+++ b/src/lib/fpdf-easytable/Pics/fpdflogo.png
Binary files differ
diff --git a/src/lib/fpdf-easytable/Pics/julytemperatures.png b/src/lib/fpdf-easytable/Pics/julytemperatures.png
new file mode 100644
index 0000000..3003f53
--- /dev/null
+++ b/src/lib/fpdf-easytable/Pics/julytemperatures.png
Binary files differ
diff --git a/src/lib/fpdf-easytable/Pics/monthtemperature.png b/src/lib/fpdf-easytable/Pics/monthtemperature.png
new file mode 100644
index 0000000..5538ef9
--- /dev/null
+++ b/src/lib/fpdf-easytable/Pics/monthtemperature.png
Binary files differ
diff --git a/src/lib/fpdf-easytable/Pics/snow.png b/src/lib/fpdf-easytable/Pics/snow.png
new file mode 100644
index 0000000..02a21c7
--- /dev/null
+++ b/src/lib/fpdf-easytable/Pics/snow.png
Binary files differ
diff --git a/src/lib/fpdf-easytable/Pics/temperature.png b/src/lib/fpdf-easytable/Pics/temperature.png
new file mode 100644
index 0000000..cf527ca
--- /dev/null
+++ b/src/lib/fpdf-easytable/Pics/temperature.png
Binary files differ
diff --git a/src/lib/fpdf-easytable/Pics/temperaturexaltitude.png b/src/lib/fpdf-easytable/Pics/temperaturexaltitude.png
new file mode 100644
index 0000000..93fffe3
--- /dev/null
+++ b/src/lib/fpdf-easytable/Pics/temperaturexaltitude.png
Binary files differ
diff --git a/src/lib/fpdf-easytable/Pics/tick.png b/src/lib/fpdf-easytable/Pics/tick.png
new file mode 100755
index 0000000..42ded6b
--- /dev/null
+++ b/src/lib/fpdf-easytable/Pics/tick.png
Binary files differ
diff --git a/src/lib/fpdf-easytable/Pics/wind.png b/src/lib/fpdf-easytable/Pics/wind.png
new file mode 100644
index 0000000..7b8db4a
--- /dev/null
+++ b/src/lib/fpdf-easytable/Pics/wind.png
Binary files differ
diff --git a/src/lib/fpdf-easytable/README.md b/src/lib/fpdf-easytable/README.md
new file mode 100644
index 0000000..6dc8785
--- /dev/null
+++ b/src/lib/fpdf-easytable/README.md
@@ -0,0 +1,963 @@
+# FPDF EasyTable
+
+It is a PHP class that provides an easy way to make tables for PDF documents that are generated with the 
+[FPDF library](http://www.fpdf.org).
+
+Making tables for PDF documents with this class is as easy and flexible as
+making HTML tables. And using CSS-style strings we can customize the look of 
+the tables in the same fashion HTML tables are styling with CSS. 
+
+No messy code with confusing arrays of attributes and texts.
+
+No complicated configuration files.
+
+Building and styling a table with easyTable is simple, clean and fast.
+
+```
+ $table=new easyTable($pdf, 3, 'width:100;');
+ 
+ $table->easyCell('Text 1', 'rowspan:2; valign:T'); 
+ $table->easyCell('Text 2', 'bgcolor:#b3ccff; rowspan:2');
+ $table->easyCell('Text 3');
+ $table->printRow();
+ 
+ $table->rowStyle('min-height:20');
+ $table->easyCell('Text 4', 'bgcolor:#3377ff; rowspan:2');
+ $table->printRow();
+
+ $table->easyCell('Text 5', 'bgcolor:#99bbff;colspan:2');
+ $table->printRow();
+ 
+ $table->endTable();
+```
+
+# Content
+ 
+- [Features](#features)
+- [Comparisons](#comparisons)
+- [Examples](#examples)
+- [Requirements](#requirements)
+- [Manual Install](#manual-install)
+- [Quick Start](#quick-start)
+- [Documentation](#documentation)
+- [Fonts And UTF8 Support](#fonts-and-utf8-support)
+- [Using with FPDI](#using-with-fpdi)
+- [Tag based font style](#tag-based-font-style)
+- [User units](#user-units) **_NEW FEATURE!!_**
+- [Common error](#common-error)
+- [Get In Touch](#get-in-touch)
+- [Donations](#donations)
+- [License](#license)
+
+# Features
+
+- Table and columns width can be defined in [user units](#user-units) or percentage
+
+- Every table cell is a fully customizable 
+  (font family, font size, font color, background color, position of the text, 
+  vertical padding, horizontal padding...)
+
+- Cells can span to multiple columns and rows
+
+- Tables split automatically on page-break
+
+- Set of header row, so the header can be added automatically on every new page
+
+- Attribute values can be defined globally, at the table level (affect all the cells in the table), 
+  at the row level (will affect just the cells in that row), or locally at the cell level
+
+- Rows can be set to split on page break if it does not fit in the current page or to be printed
+  on the next page
+
+- Images can be added to table cells
+
+- Text can be added on top or below an image in a cell
+
+- [UTF8 Support](#fonts-and-utf8-support)
+
+- [Tag based font style](#tag-based-font-style) which allows to mix different font families, font styles, font size and font color in the same cell! **_NEW FEATURE!!_** 
+
+- [Links](#tag-based-font-style) **_NEW FEATURE!!_** 
+
+
+
+
+# Comparisons
+
+**easyTable vs kind-of-HTML-to-PDF**
+
+To use HTML code to make tables for PDF documents is a kind of sloppy hack. To begin with
+to convert HTML code into a kind of FPDF you need a parcer meaning there is a penalty in performace;
+and second the results are very poor. 
+
+*HTML code*
+
+    <table align:"right" style="border-collapse:collapse">
+      <tr>
+        <td rowspan="2" style="width:30%; background:#ffb3ec;">Text 1</td>
+        <td colspan="2" style="background:#FF66AA;">Text 2</td>
+      </tr>
+      <tr>
+        <td style="width:35%; background:#33ffff;">Text 3 </td>
+        <td style="width:35%; background:#ffff33;">Text 4 </td>
+      </tr>
+    </table>
+
+*easyTable code*
+
+    $table=new easyTable($pdf, '%{30, 35, 35}', 'align:R; border:1');
+      $table->easyCell('Text 1', 'rowspan:2; bgcolor:#ffb3ec');
+      $table->easyCell('Text 2', 'colspan:2; bgcolor:#FF66AA');
+      $table->printRow();
+
+      $table->easyCell('Text 3', 'bgcolor:#33ffff');
+      $table->easyCell('Text 4', 'bgcolor:#ffff33');
+      $table->printRow();
+    $table->endTable(5);
+
+
+
+# Examples
+
+- [Evolution of a table](https://github.com/fpdf-easytable/fpdf-easytable/blob/master/example-1.pdf)
+  * [Code](https://github.com/fpdf-easytable/fpdf-easytable/blob/master/example1.php)
+  
+- [Table with header](https://github.com/fpdf-easytable/fpdf-easytable/blob/master/example-2.pdf)
+  * [Code](https://github.com/fpdf-easytable/fpdf-easytable/blob/master/example2.php)
+
+- [Simple invoice](https://github.com/fpdf-easytable/fpdf-easytable/blob/master/example-3.pdf)
+  * [Code](https://github.com/fpdf-easytable/fpdf-easytable/blob/master/example3.php)
+
+- [More examples](https://github.com/fpdf-easytable/fpdf-easytable/blob/master/example-4.pdf)
+  * [Code](https://github.com/fpdf-easytable/fpdf-easytable/blob/master/example4.php)
+ 
+
+# Requirements
+
+- PHP 5.6 or higher
+- [FPDF 1.81](http://www.fpdf.org).
+- exfpdf.php (included in this project)
+
+# Manual Install
+
+Download the EasyTable class and FPDF class and put the contents in a directory in your project structure.
+Be sure you are using [FPDF 1.81](http://www.fpdf.org).
+
+# Quick Start
+
+- create a fpdf object with exfpdf class (extension of fpdf class)
+- create a easyTable object
+```
+    $table=new easyTable($pdf, 3, 'border:1');
+```    
+- add some rows
+```
+    $table->easyCell('Text 1', 'valign:T'); 
+    $table->easyCell('Text 2', 'bgcolor:#b3ccff;');
+    $table->easyCell('Text 3');
+    $table->printRow();
+
+    $table->rowStyle('min-height:20; align:{C}');   // let's adjust the height of this row
+    $table->easyCell('Text 4', 'colspan:3');
+    $table->printRow();
+```
+- when it is done, do not forget to terminate the table
+```
+    $table->endTable(4);
+```
+  [Result](https://github.com/fpdf-easytable/fpdf-easytable/blob/master/basic_example.pdf)
+  * [Code](https://github.com/fpdf-easytable/fpdf-easytable/blob/master/basic_example.php)
+
+
+# Documentation
+
+**function __construct( FPDF-object $fpdf_obj, Mix $num_cols[, string $style = '' ])**

+
+*Description:*
+
+   Constructs an easyTable object

+
+*Parameters:*
+
+fpdf_obj
+
+    the current FPDF object (constructed with the FPDF library)  
+    that is being used to write the current PDF document
+
+num_cols
+
+    this parameter can be a positive integer (the number of columns)
+    or a string of the following form
+   
+    I) a positive integer, the number of columns for the table. The width of every 
+    column will be equal to the width of the table (given by the width property) divided by 
+    the number of columns ($num_cols)
+
+    II) a string of the form '{c1, c2, c3,... cN}'. In this case every element in the curly 
+    brackets is a positive numeric value that represent the width of a column. Thus, 
+    the n-th numeric value is the width of the n-th colum. If the sum of all the width of 
+    the columns is bigger than the width of the table but less than the width of the document, 
+    the table will stretch to the sum of the columns width. However, if the sum of the columns 
+    is bigger than the width of the document, the width of every column will be reduce proportionally 
+    to make the total sum equal to the width of the document. 
+
+    III) a string of the form '%{c1, c2, c3,... cN}'. Similar to the previous case, but this time 
+    every element represents a percentage of the width of the table. In this case it the sum of 
+    this percentages is bigger than 100, the execution will be terminated.
+
+style
+
+    the global style for the table (see documentation) a semicolon-separated string of attribute 
+    values that defines the default layout of the table and all the cells and their contents 
+    (see Documentation section in README.md)
+
+*Examples:*
+```
+    $table= new easyTable($fpdf, 3);
+    $table= new easyTable($fpdf, '{35, 45, 55}', 'width:135;');
+    $table= new easyTable($fpdf, '%{35, 45, 55}', 'width:190;');
+```   
+   
+*Return value:*
+
+   An easyTable object    
+
+
+**function rowStyle( string $style )**

+
+*Description:*
+
+   Set or overwrite the style for all the cells in the current row.

+
+*Parameters:*
+
+style
+   
+   a semicolon-separated string of attribute values that defines the 
+   layout of all the cells and its content in the current row 
+   (see Documentation section in README.md)
+
+*Return values*
+
+   Void
+   
+*Notes:*
+
+   This function should be called before the first cell of the current row
+
+
+**function easyCell( string $data [, string $style = '' ])**
+
+*Description:*
+
+    Makes a cell in the table
+
+*Parameters:*
+
+data   
+    the content of the respective cell
+
+style (optional)
+    a semicolon-separated string of attribute values that defines the 
+    layout of the cell and its content (see Documentation section in README.md)
+
+*Return value*
+
+    void
+   
+
+
+**function printRow ( [ bool $setAsHeader = false ] )**
+
+*Description:*
+
+   This function indicates the end of the current row. 
+
+*Parameters:*
+
+setAsHeader (optional)
+    When it is set as true, it sets the current row as the header
+    for the table; this means that the current row will be printed as the first
+    row of the table (table header) on every page that the table splits on.
+    Remark: 1. In order to work, the table attribute split-row should set as false. 
+            2. Just the first row where this parameter is set as true will be
+               used as header any other will printed as a normal row.
+            3. For row headers with cells that spans to multiple rows, 
+               the last the parameter should be set in the last row 
+               of the group. See [example 2](https://github.com/fpdf-easytable/fpdf-easytable/blob/master/example-2.pdf)
+
+*Return values*
+
+   Void
+
+*Note:*
+
+   This function will print the current row as far as the following holds:
+```   
+      total_rowspan=0
+```
+   where total_rowspan is set as 
+```   
+      total_rowspan=max(total_rowspan, max(rowspan of cell in the current row))-1;
+```             
+
+**function endTable( [ int $bottomMargin = 2 ])**
+
+*Description:*
+
+   Unset all the data members of the easyTable object
+
+*Parameters:*
+
+bottomMargin (optional)
+
+   Optional. Specify the size in [user units](#user-units) of the bottom margin for the table. Default 2 in user units.
+   
+   If it is negative, the vertical position will be set before
+   the end of the table.   
+
+*Return values*
+
+   Void
+
+
+**Style String**
+
+In the same fashion as in-line CSS style, Easy Table uses 
+strings of semicolon-separated pairs of properties/values to
+define the styles to be applied to the different parts of 
+a table. A value set on a property at the table level will be inherited 
+by all the rows (therefore all the cells) in the table. A value set
+on a property at the row level, will be overwrite the value inherited from the table
+and, will be passed to all the cells in that row, unless a cell 
+defines its own value for that property.
+
+In what follows, we are going to use the following notation:
+
+    C=cell
+    R=row
+    T=table
+
+**PROPERTY** [C/R/T] means that the property PROPERTY can be set on cells, rows, table. 
+
+Full list of properties:
+
+**width** [T]
+
+The width property sets the width of a table.
+This property can be defined in [user units](#user-units) or in percentage of the width of the document.
+
+Syntax:
+
+    width:user-units|%;
+
+Examples:
+
+    width:145;// 145mm if the user units is mm
+    width:70%;
+
+Default: the width of the document minus the right and left margin.
+   
+**border** [C/R/T]
+
+The border property indicates if borders must be drawn around the cell or the cells. 
+The value can be either a number:
+
+    0: no border
+    1: frame
+
+or a string containing some or all of the following characters (in any order):
+
+    L: left border
+    T: top border
+    R: right border
+    B: bottom border
+
+Default value: 0. 
+
+
+**border-color** [C/R/T]
+
+The border-color property is used to set the colour of the border to be drawn 
+around the cells. The value can be: Hex color code or RGB color code.
+
+Syntax:
+
+    border-color: Hex |RGB;
+
+Example:
+
+    border-color:#ABCABC;
+    border-color:#ABC;
+    border-color:79,140, 200;
+
+Default value: the current drawn colour set in the document
+
+Note: beware that when set this attribute at the cell level, because the borders of the cells 
+      overlap each other, the results might not be as expected on adjacent cell with different
+      border color.
+
+
+**border-width** [T]
+
+The border-width property is used to set the width of the lines the border is made of.
+
+Syntax:
+
+    border-width:0.5;
+
+Default value: the current drawing line width of the document.
+
+Note: beware that if the border-width is set to thick, the border might overlap the content
+      of the cells. In that case you will have to set appropriate paddingX and paddingY on the cells.
+      (See paddingX and paddingY properties below).
+
+
+**split-row** [T]
+
+This property indicate if a row that is at the bottom of the page should be split or not
+when it reaches the bottom margin, except for rows that contains cell that span
+to different rows, in this case the row splits. By the fault, any row that does not fit in the page
+is printed in the next page. Setting the property to false, it will split any row
+between the pages. 
+
+Example: 
+
+    split-row:false;
+
+
+**l-margin** [T]
+
+This property indicate the distance from the left margin from where the table should start.
+
+Syntax:
+
+    l-maring:user-units;
+
+Example:
+
+    l-maring:45;
+
+Default value: 0.
+
+**min-height** [R] 
+
+The min-height property set the minimum height for all the cells (with rowspan:1) in that
+specific row.
+
+Syntax:
+
+    min-height:user-units;
+
+Example:
+
+    min-height:35;
+
+
+Default value: 0.
+
+**align** [C/R/T] 
+
+This property indicates the horizontal alignment of the element in which it is set.
+The values can be: 
+
+    L: to the left
+    C: at the centre
+    R: to the right
+    J: justified (applicable just on cells)
+
+Syntax for tables is:
+
+    align:A;
+    align:A{A-1A-2A-3A-4...};
+
+Explanation: the first character indicates the horizontal alignment of the table 
+(as far as l-margin is not set), while the optional string is a string of the form: 
+{A-1A-2A-3A-4...} (curly brackets included) where A-1, A-2, A-3, A-4, etc. can be L, C, R or J and
+the A-n letter indicates the horizontal alignment for the content of all the cells in the
+n-th row. If the number of rows is greater than the length of the optional string, 
+the overflowed rows will have default alignment to the left (L).
+
+Example: (table with 10 rows)
+
+    align:R{CCCLLJ};
+
+means that the table is aligned to the right of the document, the content of the 
+cells in the first three rows will be aligned at the centre, the content of the
+cells in the 4-th and 5-th rows will be aligned to the left and the the content of cells in 
+the 6-th row will be aligned to the right, while the rest of the cell contents in the remaining 
+rows will be aligned to the left.
+
+Syntax for rows is
+
+    align:{A-1A-2A-3A-4...};
+
+where A-1, A-2,... etc are as in the table case and with the same functionality: 
+the A-n character indicates the alignment of the cells in the n-th column that are
+in the respective row.
+
+Example:
+
+    align:{LRCCRRLJ}
+
+Syntax for cells is
+
+    align:A;
+
+where A can be L, C, R, J.
+
+Default value: L.
+
+**valign** [C/R/T] 
+
+The property valign defines the vertical alignment of the content of the cells.
+The values can be:
+
+    T: top
+    M: middle
+    B: bottom
+
+Example:
+
+    valign:M; 
+
+Default: T.
+
+Remark: *when using valign property on cell with image property set (see below),
+if the cell does not have text, the behaviour of valign is as expected, this is,
+the image is positioned accordingly to the value of valign. However, if the cell contains
+text, the image and the text are valign-ed in the middle of the cell but 
+top (T) or middle (M) valign set the text on top of the image, while valign:B set the text 
+under the image.*
+
+
+**bgcolor** [C/R/T]
+
+The bgcolor property defines the background colour of the cells
+The value can be: Hex color code or RGB color code.
+
+Syntax:
+
+    bgcolor:Hex | RGB;
+
+Example:
+
+    bgcolor:#ABCABC;
+    bgcolor:#ABC;    
+    bgcolor:79,140, 200;
+
+Default: the current fill color set in the document
+
+
+**font-family** [C/R/T],
+
+It can be either a name defined by the FPDF method AddFont() or 
+one of the standard families (case insensitive):
+
+    Courier (fixed-width)
+    Helvetica or Arial (synonymous; sans serif)
+    Times (serif)
+    Symbol (symbolic)
+    ZapfDingbats (symbolic)
+
+It is also possible to pass an empty string. In that case, the current family is kept. 
+
+Example:
+
+    font-family:times;
+
+Default: the font-family set in the document.
+
+**font-style** [C/R/T]
+
+Possible values are (case insensitive):
+
+    empty string: regular
+    B: bold
+    I: italic
+    U: underline
+
+or any combination. The default value is regular. Bold and italic styles do not 
+apply to Symbol and ZapfDingbats. 
+
+Example:
+
+    font-style:IBU
+
+Default: empty;
+
+**font-size** [C/R/T]
+
+Font size in points.
+
+Example:
+
+    font-size:16;
+
+Default: the current font size of the document.
+
+**font-color** [C/R/T]
+
+This property defines the color of the font for the cells
+The value can be: Hex color code or RGB color code.
+
+Syntax:
+
+    font-color:Hex |RGB;
+
+Example:
+
+    font-color:#ABCABC;
+    font-color:#ABC;    
+    font-color:79,140, 200;
+
+Default value: the current font color set in the document
+
+**line-height** [C/R/T]
+
+The line-height property specifies the line height.
+
+Syntax:
+
+    line-height:number;
+
+Example:
+
+    line-height:1.2;
+
+Default value: 1.
+
+**paddingX** [C/R/T] 
+
+The paddingX property sets the left and right padding (space) of the cells.
+
+Syntax:
+
+    paddingX:user-units;
+
+Example:
+
+    paddingX:4;
+
+Default: 0.5.
+
+**paddingY** [C/R/T] 
+
+The paddingY property sets the top and bottom padding (space) of the cells.
+
+Syntax
+
+    paddingY:user-units;
+
+Example:
+
+    paddingY:3;
+
+Default: 1.
+
+**colspan** [C]
+
+The colspan attribute defines the number of columns a cell should span.
+
+Syntax:
+
+    colspan:4;
+
+Default 1
+
+**rowspan** [C]
+The rowspan attribute defines the number of rows a cell should span.
+
+Syntax:
+
+    rowspan:2;
+
+Default 1
+
+**img** [C]
+The img attribute defines the image and its dimensions to be set in the cell.
+
+Syntax:
+
+    img:image.png,w80,h50;
+    img:image.png,h50;
+    img:image.png;
+
+If no dimensions are specified, the image dimensions are calculate proportionally 
+to fit the width of the cell.
+If one out of the two dimensions (width or height) is specified but not the other
+the one that is not specified is calculated proportionally.
+Default value: empty.
+
+# Fonts And UTF8 Support
+
+1. Get the ttf files for the font you want to use and save them in a directory
+   Fonts. 
+   
+   **NOTE:** If you want to use bold, italic or bold-italic font styles you need 
+   the respective font files too.
+
+   **NOTE:** the font must contain the characters you want to use
+   
+2. Using the script makefont.php part of FPDF library (in the makefont directory)
+
+       me@laptop:/path/to/FPDF/makefont$ php makefont.php /path/to/Fonts/My_font.ttf ENCODE
+
+	Note: use the right encode in order to use utf-8 symbols [FPDF: Tutorial 7](http://www.fpdf.org/en/tutorial/tuto7.htm).
+
+   **NOTE:** the font must contain the characters corresponding to the selected encoding.  
+   
+3. The last command will create the files My_font.php and My_font.z in 
+   the directory /path/to/FPDF/makefont move those file to the directory 
+   /path/to/FPDF/font
+   
+4. You are ready to use your fonts in your script:
+
+       $pdf = new PDF();
+       $pdf->AddFont('Cool-font','','My_font.php');  // Define the new font to use in the PDF object
+       
+       // more code
+       
+       $table=new easyTable($pdf, ...);
+       $table->easyCell(iconv("UTF-8", "ENCODE",'Hello World'), 'font-color:#66686b;font-family:Cool-font');
+       //etc...
+
+5. [Example](https://github.com/fpdf-easytable/fpdf-easytable/blob/master/basic_example.pdf): 
+   we get a ttf font file (my_font.ttf) that support the language and symbols we want to use.
+   For this example we are using Russian. The encode that we are using for Russian is KOI8-R
+
+       php makefont.php /path/to/font_ttf/my_font.ttf KOI8-R
+   
+   then we copy the resulting files to the font directory of FPDF library. 
+   
+   Then, in our script:
+
+       $pdf = new PDF();
+ 
+       $pdf->AddFont('FontUTF8','','my_font.php'); 
+  
+       $pdf->SetFont('FontUTF8','',8); // set default font for the document 
+
+       $table=new easyTable($pdf, ...);
+	
+       $Table->easyCell(iconv("UTF-8", "KOI8-R", "дебет дефинитионес цу")); // Notice the encode KOI8-R
+
+   or
+   
+       $pdf = new PDF();
+       $pdf->AddPage();
+       $pdf->SetFont('Arial','B',16);
+       
+       $pdf->AddFont('FontUTF8','','my_font.php'); 
+  
+       $table=new easyTable($pdf, 5, '...');	
+       $Table->easyCell(iconv("UTF-8", "KOI8-R", "дебет дефинитионес цу"), 'font-family:FontUTF8;'); 
+       
+
+   NOTE: For more about the right encode visit [FPDF: Tutorial 7](http://www.fpdf.org/en/tutorial/tuto7.htm)
+   and [php inconv](http://php.net/manual/en/function.iconv.php)
+   
+# Using with FPDI
+
+If your project requieres easyTable and [FPDI](https://www.setasign.com/products/fpdi/about/), this is
+how you should do it. Assuming that fpdf.php, easyTable.php, exfpdf.php, fpdi.php and any 
+other files from this libraries are in the same directory. 
+
+The class exfpdf should extends the class fpdi instead of the class fpdf. So in exfpdf.php:
+
+    <?php
+
+    class exFPDF extends FPDI
+    {
+    etc
+    etc
+
+And in your project:
+
+    <?php
+    include 'fpdf.php';
+    include 'fpdi.php';
+    include 'exfpdf.php';
+    include 'easyTable.php';
+
+    //$pdf = new FPDI(); remember exfpdf is extending the fpdi class
+    $pdf=new exFPDF(); // so we initiate exFPDF instead of FPDI
+
+    // add a page
+    $pdf->AddPage();
+    // set the source file
+    $pdf->setSourceFile("example-2.pdf");
+    // import page 1
+    $tplIdx = $pdf->importPage(1);
+    // use the imported page and place it at point 10,10 with a width of 100 mm
+    $pdf->useTemplate($tplIdx, 10, 10, 100);
+
+    //add another page
+    $pdf->AddPage();
+    $pdf->SetFont('helvetica','',10);
+
+    $table1=new easyTable($pdf, 2);
+    $table1->easyCell('Sales Invoice', 'font-size:30; font-style:B; font-color:#00bfff;');
+    $table1->easyCell('', 'img:fpdf.png, w80; align:R;');
+    $table1->printRow();
+    //etc
+    //etc
+    
+# Tag Based Font Style
+
+The new version of FPDF EasyTable can handle tag-based font styles at string level.
+
+    $table->easyCell('Project: EasyTable', 'font-family:lato; font-size:30; font-color:#00bfff;');
+
+now we can do:
+
+    $table->easyCell('<b>Project:</b> <s "font-size:20; font-family:times">EasyTable</s>', 'font-family:lato; font-size:30; font-color:#00bfff;');
+
+The font style set at the string level will over write any other font style set at the cell, row or table level.
+
+Please see the [example](https://github.com/fpdf-easytable/fpdf-easytable/blob/master/basic_example.pdf)
+  * [Code](https://github.com/fpdf-easytable/fpdf-easytable/blob/master/basic_example.php)
+
+Tags
+
+**<s "fontstyle"></s>**
+
+font-style is a semicolon separated string which can include: font-size, font-family, font-style, font-color, href;
+
+   Note: Remember to define every font your project needs.
+
+    $pdf->AddFont('MyFabFont','','my_font.php');   
+    $pdf->AddFont('MyFabFont','B','my_font_bold.php');   
+    $pdf->AddFont('MyFabFont','I','my_font_italic.php');   
+    $pdf->AddFont('MyFabFont','BI','my_font_bolditalic.php');   
+
+font-color can be Hex color code or RGB color code.
+
+**Shortcuts** 
+
+    <b></b> is equalt to: <s "font-style:B"></s>
+    <i></i> is equalt to: <s "font-style:I"></s>
+
+**Tags can be nested**
+
+When nested tags are used, the result is similar to the case in HTML documents.
+
+    <b>Helo <i>world</i></b>
+
+     <s "font-style:I; font-color#abc123; font-family:times">Hello  <s "font-style:B; font-family:lato; font-size:20">world</s></s>
+
+- Different font style can be applied to the letters of a word.
+
+````
+	<b>H<i>e</i><s "font-family:myfont">ll<s "font-size">o</s></s></b> 
+````
+
+**Links**
+
+Use the property 'href' to set links
+
+    <b>Helo <s "font-family:my_fab_font; font-color:#AABBCC; href:http://www.example.com">world</s></b>
+
+
+**Escape sequence**
+
+The sequence '\\<s' is parced as '<s'
+
+    <b>Helo <s "font-family:my_fab_font;">\<sammy@example.com></s></b>
+
+
+# User units
+
+EasyTable supports the same user units (pt/mm/cm/in) supported by [FPDF: construct](http://www.fpdf.org/en/doc/__construct.htm).
+Bare in mind that any unit related setting (width, border, etc.) needs to be in the respective unit set at the 
+top document. For example if the units for the document is set as inch, then, all the settings
+unit related will be considered in the same user units, for instance min-height:1.2; will mean 1.2in.
+
+# Common Error
+
+A very typical situation is: *"EasyTable works in my localhost but it does not
+work in remote server"*... Seriously, what on earth it has to do with EasyTable?... And the error
+reported is 
+    Fatal error: Uncaught exception 'Exception' with message 'FPDF error: Some data has 
+    already been output, can't send PDF file... etc etc...
+
+This happens because when the server runs the script to output a PDF document, it sets the headers 
+as PDF document, however something is trying to output text or html code (for instance an exception or an 
+echo statment) so the server can not change the header for the respective output. 
+is outputting html/txt data.
+
+Another very common error is to forget to add the fonts and its appropiated style (I, B, IB) used in the document. 
+Let's suppose that in your document you use "my_favourite_font". Then you need to add 
+
+    $pdf->AddFont('MyFabFont','','my_favourite_font.php'); 
+    
+if you are using the bold version of it, then you must add:      
+
+    $pdf->AddFont('MyFabFont','B','my_favourite_font_bold.php');   
+
+if you are using the italic version, then you need to add:
+
+    $pdf->AddFont('MyFabFont','I','my_favourite_font_italic.php');   
+
+if you are using the bold-italic, then
+
+    $pdf->AddFont('MyFabFont','BI','my_favourite_font_bolditalic.php');
+
+You need to generate each of the font files that needs to be added in your project
+('my_favourite_font_bold.php', 'my_favourite_font_italic.php', 'my_favourite_font_bolditalic.php'),
+refer to your font documentation and see [Fonts And UTF8 Support](https://github.com/fpdf-easytable/fpdf-easytable#fonts-and-utf8-support).
+
+# Get In Touch
+
+Your comments and questions are welcome: easytable@yandex.com (with the subject: EasyTable)
+
+# Other projects
+
+- [Simple Unit Test](https://github.com/fpdf-easytable/simple_unit_test) PHP unit test as it should be.
+- [SimpleCharts.js](https://github.com/fpdf-easytable/simpleCharts.js)
+- [Ajax Server Response Hander](https://github.com/fpdf-easytable/ajax_server_response_hander) Simplify server response from ajax calls
+- [Crypt](https://github.com/fpdf-easytable/Crypt)
+- [Duplicate Image Finder](https://github.com/volatilflerovium/Duplicate_Image_Finder)
+- [Classic 3D-Puzzles](https://github.com/volatilflerovium/3D-Puzzles)
+
+# Donations
+
+Any monetary contribution would be appreciated :-)
+
+If you are using this for the company you work for, they are getting the money, you are getting 
+the medals and I am getting nothing! Is that fair?
+
+It does cost NOTHING to push the freaky star button!!
+
+[![Donate](https://www.paypalobjects.com/en_US/GB/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=JALWQVBS2KGQC)
+
+
+# License
+
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to <http://unlicense.org/>

+

+
+
diff --git a/src/lib/fpdf-easytable/basic_example.pdf b/src/lib/fpdf-easytable/basic_example.pdf
new file mode 100644
index 0000000..0acbcc0
--- /dev/null
+++ b/src/lib/fpdf-easytable/basic_example.pdf
Binary files differ
diff --git a/src/lib/fpdf-easytable/basic_example.php b/src/lib/fpdf-easytable/basic_example.php
new file mode 100644
index 0000000..fd46cec
--- /dev/null
+++ b/src/lib/fpdf-easytable/basic_example.php
@@ -0,0 +1,49 @@
+<?php
+ include 'fpdf.php';
+ include 'exfpdf.php';
+ include 'easyTable.php';
+
+ $pdf=new exFPDF();
+ $pdf->AddPage(); 
+ $pdf->SetFont('helvetica','',10);
+
+ $pdf->AddFont('lato','','Lato-Regular.php');
+
+ $pdf->Write(6, 'Some writing...');
+
+ $pdf->Ln(5);
+
+ $pdf->Write(6, 'Integer eget risus non dui scelerisque consectetur. Integer eleifend in nibh in mattis. Aenean eu justo quis mauris tempus eleifend. Praesent malesuada turpis ut justo semper tempor. Integer varius, nisi non elementum molestie, leo arcu euismod velit, eu tempor ligula diam convallis sem. Sed ultrices hendrerit suscipit. Pellentesque volutpat a urna nec placerat. Etiam auctor dapibus leo nec ullamcorper. Nullam id placerat elit. Vivamus ut quam a metus tincidunt laoreet sit amet a ligula. Sed rutrum felis ipsum, sit amet finibus magna tincidunt id. Suspendisse vel urna interdum lacus luctus ornare. Curabitur ultricies nunc est, eget rhoncus orci vestibulum eleifend. In in consequat mi. Curabitur sodales magna at consequat molestie. Aliquam vulputate, neque varius maximus imperdiet, nisi orci accumsan risus, sit amet placerat augue ipsum eget elit. Quisque sodales orci non est tincidunt tincidunt. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. In ut diam in dolor ultricies accumsan sit amet eu ex. Pellentesque aliquet scelerisque ullamcorper. Aenean porta enim eget nisl viverra euismod sed non eros. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque at imperdiet sem, non volutpat metus. Phasellus sed velit sed orci iaculis venenatis ac id risus.');
+ 
+ $pdf->Ln(10);
+
+ $pdf->AddFont('FontUTF8','','Arimo-Regular.php'); 
+ $pdf->AddFont('FontUTF8','B','Arimo-Bold.php'); 
+ $pdf->AddFont('FontUTF8','BI','Arimo-BoldItalic.php'); 
+ $pdf->AddFont('FontUTF8','I','Arimo-Italic.php'); 
+ 
+ $table=new easyTable($pdf, 3, 'border:1;font-size:12;');
+ 
+    $table->easyCell('<b>This text is in bold</b> and <i>this is in italic</i> and this is normal', 'valign:T; bgcolor:#eee6ff;'); 
+    $table->easyCell("T<s 'font-color:#3399ff; font-style:B; font-size:19'>h</s><s 'font-color:#00cc00; font-style:I; font-size:21'>i</s>s <s 'font-color:#ff0000; font-style:BI; font-size:8'>is</s> a <s 'font-color:#0099ff; font-size:25'><b>c</b><i>r</i><bi>a</bi><b>z</b><i>y</i></s> text");
+    $table->easyCell("<s 'font-color:#4da6ff; font-style:B; font-size:18'><i>And can do it with Russian, just because <s 'font-size:25; font-color:#66ff99'>we can!</s></i></s>", 'font-style:I;');
+    $table->printRow();
+
+    $table->rowStyle('min-height:60; align:{C};font-size:18;');   // let's adjust the height of this row
+    $table->easyCell(iconv("UTF-8", 'KOI8-R', '<b>Вери</b> порро <i>номинати</i> вел ех, <b><i>еум</i></b> те лаореет импедит, <s "font-style:B;font-size:18; font-color:#3399ff">ест но ферри ириуре.</s> Ет вис реяуе хомеро. Перфецто сцрипсерит вис еу, нам ин ассум пробатус. Фиерент импердиет аппеллантур меи но, граеце яуодси пертинациа вел ад, не при лудус оратио тациматес. Хис дебет дефинитионес цу.
+    
+    <s "font-family:times;font-size:20;">Set different font-families <s "font-family:lato;font-size:25;">in the same cell</s></s>  
+    
+    <s "font-family:times;font-size:18;">This is a link <s "font-family:lato;font-size:20;font-color:#0099ff; href:http://www.fpdf.org/">FPDF</s></s>  
+    
+    '), 'colspan:4; font-family:FontUTF8; font-size:12;');
+
+    $table->printRow();
+
+ $table->endTable(4);
+ 
+//-----------------------------------------
+
+ $pdf->Output(); 
+
+?>
\ No newline at end of file
diff --git a/src/lib/fpdf-easytable/easyTable.php b/src/lib/fpdf-easytable/easyTable.php
new file mode 100755
index 0000000..7a3840a
--- /dev/null
+++ b/src/lib/fpdf-easytable/easyTable.php
@@ -0,0 +1,1017 @@
+<?php
+ /*********************************************************************
+ * FPDF easyTable                                                     *
+ *                                                                    *
+ * Version: 2.0                                                       *
+ * Date:    12-10-2017                                                *
+ * Author:  Dan Machado                                               *
+ * Require  exFPDF v2.0                                              *
+ **********************************************************************/
+  
+class easyTable{
+   const LP=0.4;
+   const XPadding=1;
+   const YPadding=1;
+   const IMGPadding=0.5;
+   const PBThreshold=30;
+   static private $table_counter=false;
+   static private $style=array('width'=>false, 'border'=>false, 'border-color'=>false,
+   'border-width'=>false, 'line-height'=>false,
+   'align'=>'', 'valign'=>'', 'bgcolor'=>false, 'split-row'=>false, 'l-margin'=>false,
+   'font-family'=>false, 'font-style'=>false,'font-size'=>false, 'font-color'=>false,
+   'paddingX'=>false, 'paddingY'=>false);
+   private $pdf_obj;
+   private $document_style;
+   private $table_style;
+   private $col_num;
+   private $col_width;
+   private $baseX;
+   private $row_style_def;
+   private $row_style;
+   private $row_heights;
+   private $row_data;
+   private $rows;
+   private $total_rowspan;
+   private $col_counter;
+   private $grid;
+   private $blocks;
+   private $overflow;
+   private $header_row;
+   private $new_table;
+
+   private function get_available($colspan, $rowspan){
+      static $k=0;
+      if(count($this->grid)==0){
+         $k=0;
+      }
+      while(isset($this->grid[$k])){
+         $k++;
+      }
+      for($i=0; $i<=$colspan; $i++){
+         for($j=0; $j<=$rowspan; $j++){
+            $this->grid[$k+$i+$j*$this->col_num]=true;
+         }
+      }
+      return $k;
+   }
+
+   private function get_style($str, $c){
+      $result=self::$style;
+      if($c=='C'){
+         $result['colspan']=0;
+         $result['rowspan']=0;
+         $result['img']=false;
+      }
+      if($c=='C' || $c=='R'){
+         unset($result['width']);
+         unset($result['border-width']);
+         unset($result['split-row']);
+         unset($result['l-margin']);
+      }
+      if($c=='R' || $c=='T'){
+         if($c=='R'){
+            $result['c-align']=array_pad(array(), $this->col_num, 'L');
+         }
+         else{
+            $result['c-align']=array();
+         }
+      }
+      if($c=='R'){
+         $result['min-height']=false;
+      }
+      $tmp=explode(';', $str);
+      foreach($tmp as $x){
+         if($x && strpos($x,':')>0){
+            $r=explode(':',$x);
+            $r[0]=trim($r[0]);
+            $r[1]=trim($r[1]);
+            if(isset($result[$r[0]])){
+               $result[$r[0]]=$r[1];
+            }
+         }
+      }
+      return $result;
+   }
+
+   private function inherating(&$sty, $setting, $c){
+      if($c=='C'){
+         $sty[$setting]=$this->row_style[$setting];
+      }
+      elseif($c=='R'){
+         $sty[$setting]=$this->table_style[$setting];
+      }
+      else{
+         $sty[$setting]=$this->document_style[$setting];
+      }
+   }
+   
+   private function conv_units($x)
+   {
+      if($this->pdf_obj->get_scale_factor()==72/25.4)
+      {
+         return $x;
+      }
+
+      if($this->pdf_obj->get_scale_factor()==72/2.54)
+      {
+         return $x/10;
+      }
+
+      if($this->pdf_obj->get_scale_factor()==72)
+      {
+         return $x/25.4;
+      }
+
+      return $x/0.3527777778;
+   }
+
+   private function set_style($str, $c, $pos=''){
+      $sty=$this->get_style($str, $c);
+      if($c=='T'){
+         if(is_numeric($sty['width'])){
+            $sty['width']=min(abs($sty['width']),$this->document_style['document_width']);
+            if($sty['width']==0){
+               $sty['width']=$this->document_style['document_width'];
+            }
+         }
+         else{
+            $x=strpos($sty['width'], '%');
+            if($x!=false){
+               $x=min(abs(substr($sty['width'], 0, $x)), 100);
+               if($x){
+                  $sty['width']=$x*$this->document_style['document_width']/100.0;
+               }
+               else{
+                  $sty['width']=$this->document_style['document_width'];
+               }
+            }
+            else{
+               $sty['width']=$this->document_style['document_width'];
+            }
+         }
+         if(!is_numeric($sty['l-margin'])){
+            $sty['l-margin']=0;
+         }
+         else{
+            $sty['l-margin']=abs($sty['l-margin']);
+         }
+         if(is_numeric($sty['border-width'])){
+            $sty['border-width']=abs($sty['border-width']);
+         }
+         else{
+            $sty['border-width']=false;
+         }
+         if($sty['split-row']=='false'){
+            $sty['split-row']=false;
+         }
+         elseif($sty['split-row']!==false){
+            $sty['split-row']=true;
+         }
+      }
+      if($c=='R'){
+         if(!is_numeric($sty['min-height']) || $sty['min-height']<0){
+            $sty['min-height']=0;
+         }
+      }
+      if(!is_numeric($sty['paddingX'])){
+         if($c=='C' || $c=='R'){
+            $this->inherating($sty, 'paddingX', $c);
+         }
+         else{
+            $sty['paddingX']=$this->conv_units(self::XPadding);
+         }
+      }
+      $sty['paddingX']=abs($sty['paddingX']);
+      if(!is_numeric($sty['paddingY'])){
+         if($c=='C' || $c=='R'){
+            $this->inherating($sty, 'paddingY', $c);
+         }
+         else{
+            $sty['paddingY']=$this->conv_units(self::YPadding);
+         }
+      }
+      $sty['paddingY']=abs($sty['paddingY']);
+      if($sty['border']===false && ($c=='C' || $c=='R')){
+         $this->inherating($sty, 'border', $c);
+      }
+      else{
+         $border=array('T'=>1, 'R'=>1, 'B'=>1, 'L'=>1);
+         if(!(is_numeric($sty['border']) && $sty['border']==1)){
+            foreach($border as $k=>$v){
+               $border[$k]=0;
+               if(strpos($sty['border'], $k)!==false){
+                  $border[$k]=1;
+               }
+            }
+         }
+         $sty['border']=$border;
+      }
+      $color_settings=array('bgcolor', 'font-color', 'border-color');
+      foreach($color_settings as $setting){
+         if($sty[$setting]===false || !($this->pdf_obj->is_hex($sty[$setting]) || $this->pdf_obj->is_rgb($sty[$setting]))){
+            if($c=='C' || $c=='R'){
+               $this->inherating($sty, $setting, $c);
+            }
+            elseif($setting=='font-color'){
+               $sty[$setting]=$this->document_style[$setting];
+            }
+         }
+         else{
+            $sty[$setting]=$sty[$setting];
+         }
+      }
+      $font_settings=array('font-family', 'font-style', 'font-size');
+      foreach($font_settings as $setting){
+         if($sty[$setting]===false){
+            $this->inherating($sty, $setting, $c);
+         }
+      }
+      if(is_numeric($sty['line-height'])){
+         $sty['line-height']=$this->conv_units(self::LP)*abs($sty['line-height']);
+      }
+      else{
+         if($c=='C' || $c=='R'){
+            $this->inherating($sty,'line-height', $c);
+         }
+         else{
+            $sty['line-height']=$this->conv_units(self::LP);
+         }
+      }
+      if($c=='C'){
+         if($sty['img']){
+            $tmp=explode(',', $sty['img']);
+            if(file_exists($tmp[0])){
+               $sty['img']=array('path'=>'', 'h'=>0, 'w'=>0);
+               $img=@ getimagesize($tmp[0]);
+               $sty['img']['path']=$tmp[0];
+               for($i=1; $i<3; $i++){
+                  if(isset($tmp[$i])){
+                     $tmp[$i]=trim(strtolower($tmp[$i]));
+                     if($tmp[$i][0]=='w' || $tmp[$i][0]=='h'){
+                        $t=substr($tmp[$i],1);
+                        if(is_numeric($t)){
+                           $sty['img'][$tmp[$i][0]]=abs($t);
+                        }
+                     }
+                  }
+               }
+               $ration=$img[0]/$img[1];
+               if($sty['img']['w']+$sty['img']['h']==0){
+                  $sty['img']['w']=$img[0];
+                  $sty['img']['h']=$img[1];
+               }
+               elseif($sty['img']['w']==0){
+                  $sty['img']['w']=$sty['img']['h']*$ration;
+               }
+               elseif($sty['img']['h']==0){
+                  $sty['img']['h']=$sty['img']['w']/$ration;
+               }
+            }
+            else{
+               $sty['img']='failed to open stream: file ' . $tmp[0] .' does not exist';
+            }
+         }
+         if(is_numeric($sty['colspan']) && $sty['colspan']>0){
+            $sty['colspan']--;
+         }
+         else{
+            $sty['colspan']=0;
+         }
+         if(is_numeric($sty['rowspan']) && $sty['rowspan']>0){
+            $sty['rowspan']--;
+         }
+         else{
+            $sty['rowspan']=0;
+         }
+         if($sty['valign']==false && ($sty['rowspan']>0 || $sty['img']!==false)){
+            $sty['valign']='M';
+         }
+         if($sty['align']==false && $sty['img']!==false){
+            $sty['align']='C';
+         }
+      }
+      if($c=='T' || $c=='R'){
+         $tmp=explode('{',$sty['align']);
+         if($c=='T'){
+            $sty['align']=trim($tmp[0]);
+         }
+         if(isset($tmp[1])){
+            $tmp[1]=trim($tmp[1], '}');
+            if(strlen($tmp[1])){
+               for($i=0; $i<strlen($tmp[1]); $i++){
+                  if(preg_match("/[LCRJ]/", $tmp[1][$i])!=0){
+                     $sty['c-align'][$i]=$tmp[1][$i];
+                  }
+                  else{
+                     $sty['c-align'][$i]='L';
+                  }
+               }
+            }
+            if($c=='R'){
+               $sty['align']='L';
+               $sty['c-align']=array_slice($sty['c-align'],0,$this->col_num);
+            }
+         }
+      }
+      if($sty['align']!='L' && $sty['align']!='C' && $sty['align']!='R' && $sty['align']!='J'){
+         if($c=='C'){
+            $sty['align']=$this->row_style['c-align'][$pos];
+         }
+         elseif($c=='R'){
+            $sty['align']='L';
+            $sty['c-align']=$this->table_style['c-align'];
+         }
+         else{
+            $sty['align']='C';
+         }
+      }
+      elseif($c=='T' && $sty['align']=='J'){
+         $sty['align']='C';
+      }
+      if($sty['valign']!='T' && $sty['valign']!='M' && $sty['valign']!='B'){
+         if($c=='C' || $c=='R'){
+            $this->inherating($sty, 'valign', $c);
+         }
+         else{
+            $sty['valign']='T';
+         }
+      }
+      return $sty;
+   }
+
+   private function row_content_loop($counter, $f){
+      $t=0;
+      if($counter>0){
+         $t=$this->rows[$counter-1];
+      }
+      for($index=$t; $index<$this->rows[$counter]; $index++){
+         $f($index);
+      }
+   }
+
+   private function mk_border($i, $y, $split){
+      $w=$this->row_data[$i][2];
+      $h=$this->row_data[$i][5];
+      if($split){
+         $h=$this->pdf_obj->PageBreak()-$y;
+      }
+      if($this->row_data[$i][1]['border-color']!=false){
+         $this->pdf_obj->resetColor($this->row_data[$i][1]['border-color'], 'D');
+      }
+      $a=array(1, 1, 1, 0);
+      $borders=array('L'=>3, 'T'=>0, 'R'=>1, 'B'=>2);
+      foreach($borders as $border=>$j){
+         if($this->row_data[$i][1]['border'][$border]){
+            if($border=='B'){
+               if($split==0){
+                  $this->pdf_obj->Line($this->row_data[$i][6]+(1+$a[($j+2)%4])%2*$w, $y+(1+$a[($j+1)%4])%2 * $h, $this->row_data[$i][6]+$a[$j%4]*$w, $y+($a[($j+3)%4])%2 *$h);
+               }
+            }
+            else{
+               $this->pdf_obj->Line($this->row_data[$i][6]+(1+$a[($j+2)%4])%2*$w, $y+(1+$a[($j+1)%4])%2 * $h, $this->row_data[$i][6]+$a[$j%4]*$w, $y+($a[($j+3)%4])%2 *$h);
+            }
+         }
+      }
+      
+      if($this->row_data[$i][1]['border-color']!=false){
+         $this->pdf_obj->resetColor($this->document_style['bgcolor'], 'D');
+      }
+      if($split){
+         $this->pdf_obj->row_data[$i][1]['border']['T']=0;
+      }
+   }
+
+   private function print_text($i, $y, $split){
+      $padding=$this->row_data[$i][1]['padding-y'];
+      $k=$padding;
+      if($this->row_data[$i][1]['img']!==false){
+         if($this->row_data[$i][1]['valign']=='B'){
+            $k+=$this->row_data[$i][1]['img']['h']+$this->conv_units(self::IMGPadding);
+         }
+      }
+      $l=0;
+      if(count($this->row_data[$i][0])){
+         $x=$this->row_data[$i][6]+$this->row_data[$i][1]['paddingX'];
+         $xpadding=2*$this->row_data[$i][1]['paddingX'];
+         $l=count($this->row_data[$i][0])* $this->row_data[$i][1]['line-height']*$this->row_data[$i][1]['font-size'];
+         $this->pdf_obj->SetXY($x, $y+$k);
+         $this->pdf_obj->CellBlock($this->row_data[$i][2]-$xpadding, $this->row_data[$i][1]['line-height'], $this->row_data[$i][0], $this->row_data[$i][1]['align']);
+         $this->pdf_obj->resetFont($this->document_style['font-family'], $this->document_style['font-style'], $this->document_style['font-size']);
+         $this->pdf_obj->resetColor($this->document_style['font-color'], 'T');
+      }
+      if($this->row_data[$i][1]['img']!==false ){
+         $x=$this->row_data[$i][6];
+         $k=$padding;
+         if($this->row_data[$i][1]['valign']!='B'){
+            $k+=$l+$this->conv_units(self::IMGPadding);
+         }
+         if($this->imgbreak($i, $y)==0 && $y+$k+$this->row_data[$i][1]['img']['h']<$this->pdf_obj->PageBreak()){
+            $x+=$this->row_data[$i][1]['paddingX'];
+            if($this->row_data[$i][2]>$this->row_data[$i][1]['img']['w']){
+               if($this->row_data[$i][1]['align']=='C'){
+                  $x-=$this->row_data[$i][1]['paddingX'];
+                  $x+=($this->row_data[$i][2]-$this->row_data[$i][1]['img']['w'])/2;
+               }
+               elseif($this->row_data[$i][1]['align']=='R'){
+                  $x+=$this->row_data[$i][2]-$this->row_data[$i][1]['img']['w'];
+                  $x-=2*$this->row_data[$i][1]['paddingX'];
+               }
+            }
+            $this->pdf_obj->Image($this->row_data[$i][1]['img']['path'], $x, $y+$k, $this->row_data[$i][1]['img']['w'], $this->row_data[$i][1]['img']['h']);
+         	$this->row_data[$i][1]['img']=false;
+         }
+      }
+   }
+   
+
+   private function mk_bg($i, $T, $split){
+      $h=$this->row_data[$i][5];
+      if($split){
+         $h=$this->pdf_obj->PageBreak()-$T;
+      }
+      if($this->row_data[$i][1]['bgcolor']!=false){
+         $this->pdf_obj->resetColor($this->row_data[$i][1]['bgcolor']);
+         $this->pdf_obj->Rect($this->row_data[$i][6], $T, $this->row_data[$i][2], $h, 'F');
+         $this->pdf_obj->resetColor($this->document_style['bgcolor']);
+      }
+   }
+
+   private function printing_loop($swap=false){
+      $this->swap_data($swap);
+      $y=$this->pdf_obj->GetY();
+      $tmp=array();
+      $rw=array();
+      $ztmp=array();
+      $total_cells=count($this->row_data);
+      while(count($tmp)!=$total_cells){
+         $a=count($this->rows);
+         $h=0;
+         $y=$this->pdf_obj->GetY();
+         for($j=0; $j<count($this->rows); $j++){
+            $T=$y+$h;
+            if($T<$this->pdf_obj->PageBreak()){
+
+                  $this->row_content_loop($j, function($index)use($T, $tmp){
+                  if(!isset($tmp[$index])){
+                     $split_cell=$this->scan_for_breaks($index,$T, false);
+                     $this->mk_bg($index, $T, $split_cell);
+                  }
+               });
+               if(!isset($rw[$j])){
+                  if($this->pdf_obj->PageBreak()-($T+$this->row_heights[$j])>=0){
+                     $h+=$this->row_heights[$j];
+                  }
+                  else{
+                     $a=$j+1;
+                     break;
+                  }
+               }
+            }
+            else{
+               $a=$j+1;
+               break;
+            }
+         }
+         $h=0;
+         for($j=0; $j<$a; $j++){
+            $T=$y+$h;
+            if($T<$this->pdf_obj->PageBreak()){
+
+                  $this->row_content_loop($j, function($index)use($T, &$tmp, &$ztmp){
+                  if(!isset($tmp[$index])){
+                     $split_cell=$this->scan_for_breaks($index,$T);
+                     $this->mk_border($index, $T, $split_cell);
+                     $this->print_text($index, $T, $split_cell);
+                     if($split_cell==0){
+                        $tmp[$index]=$index;
+                     }
+                     else{
+                        $ztmp[]=$index;
+                     }
+                  }
+               });
+               if(!isset($rw[$j])){
+                  $tw=$this->pdf_obj->PageBreak()-($T+$this->row_heights[$j]);
+                  if($tw>=0){
+                     $h+=$this->row_heights[$j];
+                     $rw[$j]=$j;
+                  }
+                  else{
+                     $this->row_heights[$j]=$this->overflow-$tw;
+                  }
+               }
+            }
+         }
+         if(count($tmp)!=$total_cells){
+            foreach($ztmp as $index){
+               $this->row_data[$index][5]=$this->row_data[$index][7]+$this->overflow;
+               if(isset($this->row_data[$index][8])){
+                  $this->row_data[$index][1]['padding-y']=$this->row_data[$index][8];
+                  unset($this->row_data[$index][8]);
+               }
+            }
+            $this->overflow=0;
+            $ztmp=array();
+            $this->pdf_obj->addPage($this->document_style['orientation'], $this->pdf_obj->get_page_size(), $this->pdf_obj->get_rotation());
+         }
+         else{
+            $y+=$h;
+         }
+      }
+      $this->pdf_obj->SetXY($this->baseX, $y);
+      $this->swap_data($swap);
+   }
+
+   private function imgbreak($i, $y){
+      $li=$y+$this->row_data[$i][1]['padding-y'];
+      $ls=$this->row_data[$i][1]['img']['h'];
+      if($this->row_data[$i][1]['valign']=='B'){
+         $ls+=$li;
+      }
+      else{
+         $li+=$this->row_data[$i][3]-$this->row_data[$i][1]['img']['h'];
+         $ls+=$li;
+      }
+      $result=0;
+      if($li<$this->pdf_obj->PageBreak() && $this->pdf_obj->PageBreak()<$ls){
+         $result=$this->pdf_obj->PageBreak()-$li;
+      }
+      return $result;
+   }
+
+   private function scan_for_breaks($index, $H, $l=true){
+      $print_cell=0;
+      $h=($H+$this->row_data[$index][5])-$this->pdf_obj->PageBreak();
+      if($h>0){
+         if($l){
+            $rr=$this->pdf_obj->PageBreak()-($H+$this->row_data[$index][1]['padding-y']);
+            if($rr>0){
+               $mx=0;
+               if(count($this->row_data[$index][0]) && $this->row_data[$index][1]['img']!==false){
+                  $mx=$this->imgbreak($index, $H);
+                  if($mx==0){
+                     if($this->row_data[$index][1]['valign']=='B'){
+                        $rr-=$this->row_data[$index][1]['img']['h'];
+                     }
+                  }
+               }
+               $nh=0;
+               $keys=array_keys($this->row_data[$index][0]);
+               foreach($keys as $i){
+                  $nh+=$this->row_data[$index][0][$i]['height'];
+               }
+               $nh*=$this->row_data[$index][1]['line-height'];
+               if($mx==0){
+               	if($rr<$nh && $rr>0){
+                     $nw=0;
+                     foreach($keys as $i){
+                        $nw+=$this->row_data[$index][0][$i]['height']*$this->row_data[$index][1]['line-height'];
+                        if($nw>$rr){
+                           $nw-=$this->row_data[$index][0][$i]['height']*$this->row_data[$index][1]['line-height'];
+                	         $mx=$rr-$nw;
+                           break;
+                        }
+                     }
+               	}
+                  else{
+                     if($rr< $this->row_data[$index][1]['img']['h']){
+                        $mx=$this->row_data[$index][1]['img']['h'];
+                     }
+                  }
+               }
+               $this->overflow=max($this->overflow, $mx);
+               $this->row_data[$index][8]=1;
+            }
+            else{
+               $this->row_data[$index][8]=-1*$rr;
+            }
+            $this->row_data[$index][7]=$h;
+         }
+         $print_cell=1;
+      }
+      return $print_cell;
+   }
+
+   private function swap_data($swap){
+      if($swap==false){
+         return;
+      }
+      static $data=array();
+      if(count($data)==0){
+         $data=array('header_data'=>$this->header_row['row_data'], 'row_heights'=>&$this->row_heights, 'row_data'=>&$this->row_data, 'rows'=>&$this->rows);
+         unset($this->row_heights, $this->row_data, $this->rows);
+         $this->row_heights=&$this->header_row['row_heights'];
+         $this->row_data=&$this->header_row['row_data'];
+         $this->rows=&$this->header_row['rows'];
+      }
+      else{
+         $this->header_row['row_data']=$data['header_data'];
+         unset($this->row_heights, $this->row_data, $this->rows);
+         $this->row_heights=$data['row_heights'];
+         $this->row_data=$data['row_data'];
+         $this->rows=$data['rows'];
+         $data=array();
+      }
+   }
+   /********************************************************************
+
+   function __construct( FPDF-object $fpdf_obj, Mix $num_cols[, string $style = '' ])
+   -----------------------------------------------------
+   Description:
+   Constructs an easyTable object
+   Parameters:
+   fpdf_obj
+   the current FPDF object (constructed with the FPDF library)
+   that is being used to write the current PDF document
+   num_cols
+   this parameter can be a positive integer (the number of columns)
+   or a string of the following form
+   I) a positive integer, the number of columns for the table. The width
+   of every column will be equal to the width of the table (given by the width property)
+   divided by the number of columns ($num_cols)
+   II) a string of the form '{c1, c2, c3,... cN}'. In this case every
+   element in the curly brackets is a positive numeric value that represent
+   the width of a column. Thus, the n-th numeric value is the width
+   of the n-th colum. If the sum of all the width of the columns is bigger than
+   the width of the table but less than the width of the document, the table
+   will stretch to the sum of the columns width. However, if the sum of the
+   columns is bigger than the width of the document, the width of every column
+   will be reduce proportionally to make the total sum equal to the width of the document.
+   III) a string of the form '%{c1, c2, c3,... cN}'. Similar to the previous case, but
+   this time every element represents a percentage of the width of the table.
+   In this case it the sum of this percentages is bigger than 100, the execution will
+   be terminated.
+   style
+   the global style for the table (see documentation)
+   a semicolon-separated string of attribute values that defines the
+   default layout of the table and all the cells and their contents
+   (see Documentation section in README.md)
+   Examples:
+   $table= new easyTable($fpdf, 3);
+   $table= new easyTable($fpdf, '{35, 45, 55}', 'width:135;');
+   $table= new easyTable($fpdf, '%{35, 45, 55}', 'width:190;');
+   Return value:
+   An easyTable object
+   ***********************************************************************/
+
+   public function __construct($fpdf_obj, $num_cols, $style=''){
+      if(self::$table_counter){
+         error_log('Please use the end_table method to terminate the last table');
+         exit();
+      }
+      self::$table_counter=true;
+      $this->pdf_obj=&$fpdf_obj;
+      $this->document_style['bgcolor']=$this->pdf_obj->get_color('fill');
+      $this->document_style['font-family']=$this->pdf_obj->current_font('family');
+      $this->document_style['font-style']=$this->pdf_obj->current_font('style');
+      $this->document_style['font-size']=$this->pdf_obj->current_font('size');
+      $this->document_style['font-color']=$this->pdf_obj->get_color('text');
+      $this->document_style['document_width']=$this->pdf_obj->GetPageWidth()-$this->pdf_obj->get_margin('l')-$this->pdf_obj->get_margin('r');
+      $this->document_style['orientation']=$this->pdf_obj->get_orientation();
+      $this->document_style['line-width']=$this->pdf_obj->get_linewidth();
+      $this->table_style=$this->set_style($style, 'T');
+      $this->col_num=false;
+      $this->col_width=array();
+      if(is_int($num_cols) && $num_cols!=0){
+         $this->col_num=abs($num_cols);
+         $this->col_width=array_pad(array(), abs($num_cols), $this->table_style['width']/abs($num_cols));
+      }
+      elseif(is_string($num_cols)){
+         $num_cols=trim($num_cols, '}, ');
+         if($num_cols[0]!='{' && $num_cols[0]!='%'){
+            error_log('Bad format for columns in Table constructor');
+            exit();
+         }
+         $tmp=explode('{', $num_cols);
+         $tp=trim($tmp[0]);
+         $num_cols=explode(',', $tmp[1]);
+         $w=0;
+         foreach($num_cols as $c){
+            if(!is_numeric($c)){
+               error_log('Bad parameter format for columns in Table constructor');
+               exit();
+            }
+            if(abs($c)){
+               $w+=abs($c);
+               $this->col_width[]=abs($c);
+            }
+            else{
+               error_log('Column width can not be zero');
+            }
+         }
+         $this->col_num=count($this->col_width);
+         if($tp=='%'){
+            if($w!=100){
+               error_log('The sum of the percentages of the columns is not 100');
+               exit();
+            }
+            foreach($this->col_width as $i=>$c){
+               $this->col_width[$i]=$c*$this->table_style['width']/100;
+            }
+         }
+         elseif($w!=$this->table_style['width'] && $w){
+            if($w<$this->document_style['document_width']){
+               $this->table_style['width']=$w;
+            }
+            else{
+               $this->table_style['width']=$this->document_style['document_width'];
+               $d=$this->table_style['width']/$w;
+               for($i=0; $i<count($num_cols); $i++){
+                  $this->col_width[$i]*=$d;
+               }
+            }
+         }
+      }
+      if($this->col_num==false){
+         error_log('Unspecified number of columns in Table constructor');
+         exit();
+      }
+      $this->table_style['c-align']=array_pad($this->table_style['c-align'], $this->col_num, 'L');
+      if($this->table_style['l-margin']){
+         $this->baseX=$this->pdf_obj->get_margin('l')+min($this->table_style['l-margin'],$this->document_style['document_width']-$this->table_style['width']);
+      }
+      else{
+         if($this->table_style['align']=='L'){
+            $this->baseX=$this->pdf_obj->get_margin('l');
+         }
+         elseif($this->table_style['align']=='R'){
+            $this->baseX=$this->pdf_obj->get_margin('l')+$this->document_style['document_width']-$this->table_style['width'];
+         }
+         else{
+            $this->baseX=$this->pdf_obj->get_margin('l')+($this->document_style['document_width']-$this->table_style['width'])/2;
+         }
+      }
+      $this->row_style_def=$this->set_style('', 'R');
+      $this->row_style=$this->row_style_def;
+      $this->row_heights=array();
+      $this->row_data=array();
+      $this->rows=array();
+      $this->total_rowspan=0;
+      $this->col_counter=0;
+      $this->grid=array();
+      $this->blocks=array();
+      $this->overflow=0;
+      if($this->table_style['border-width']!=false){
+         $this->pdf_obj->SetLineWidth($this->table_style['border-width']);
+      }
+      $this->header_row=array();
+      $this->new_table=true;
+   }
+   /***********************************************************************
+
+   function rowStyle( string $style )
+   -------------------------------------------------------------
+   Description:
+   Set or overwrite the style for all the cells in the current row.
+   Parameters:
+   style
+   a semicolon-separated string of attribute values that defines the
+   layout of all the cells and its content in the current row
+   (see Documentation section in README.md)
+   Return values
+   Void
+   Notes:
+
+   This function should be called before the first cell of the current row
+   ***********************************************************************/
+
+   public function rowStyle($style){
+      $this->row_style=$this->set_style($style, 'R');
+   }
+   /***********************************************************************
+
+   function easyCell( string $data [, string $style = '' ])
+   ------------------------------------------------------------------------
+   Description:
+   Makes a cell in the table
+   Parameters:
+   data
+   the content of the respective cell
+   style (optional)
+   a semicolon-separated string of attribute values that defines the
+   layout of the cell and its content (see Documentation section in README.md)
+   Return value
+   void
+   ***********************************************************************/
+
+   public function easyCell($data, $style=''){
+      if($this->col_counter<$this->col_num){
+         $sty=$this->set_style($style, 'C', $this->col_counter);
+         $this->col_counter++;
+         $row_number=count($this->rows);
+         $cell_index=count($this->row_data);
+         $cell_pos=$this->get_available($sty['colspan'], $sty['rowspan']);
+         $colm=$cell_pos %$this->col_num;
+         if($sty['img']!=false && $data!='' && $sty['valign']=='M'){
+            $sty['valign']=$this->row_style['valign'];
+         }
+         if($sty['rowspan']){
+            $this->total_rowspan=max($this->total_rowspan, $sty['rowspan']);
+            $this->blocks[$cell_index]=array($cell_index, $row_number, $sty['rowspan']);
+         }
+         $w=$this->col_width[$colm];
+         $r=0;
+         while($r<$sty['colspan'] && $this->col_counter<$this->col_num){
+            $this->col_counter++;
+            $colm++;
+            $w+=$this->col_width[$colm];
+            $r++;
+         }
+         $w-=2*$sty['paddingX'];
+         if($sty['img']!==false && is_string($sty['img'])){
+            $data=$sty['img'];
+            $sty['img']=false;
+         }
+         $data=& $this->pdf_obj->extMultiCell($sty['font-family'], $sty['font-style'], $sty['font-size'], $sty['font-color'], $w, $data);
+         $h=0;
+         $rn=count($data);
+         for($ri=0; $ri<$rn; $ri++){
+            $h+=$data[$ri]['height']*$sty['line-height'];
+         }
+         if($sty['img']){
+            if($sty['img']['w']>$w){
+               $sty['img']['h']=$w*$sty['img']['h']/$sty['img']['w'];
+               $sty['img']['w']=$w;
+            }
+            if($h){
+               $h+=$this->conv_units(self::IMGPadding);
+            }
+            $h+=$sty['img']['h'];
+         }
+         $w+=2*$sty['paddingX'];
+         
+         $posx=$this->baseX;
+         $d=$cell_pos %$this->col_num;
+         for($k=0; $k<$d; $k++){
+            $posx+=$this->col_width[$k];
+         }
+         $this->row_data[$cell_index]=array($data, $sty, $w, $h, $cell_pos, 0, $posx, 0);
+         
+      }
+   }
+   /***********************************************************************
+
+   function printRow ( [ bool $setAsHeader = false ] )
+   ------------------------------------------------------------------------
+   Description:
+
+   This function indicates the end of the current row.
+   Parameters:
+   setAsHeader (optional)
+   Optional. When it is set as true, it sets the current row as the header
+   for the table; this means that the current row will be printed as the first
+   row of the table (table header) on every page that the table splits on.
+   Remark: 1. In order to work, the table attribute split-row should set as true.
+   2. Just the first row where this parameter is set as true will be
+   used as header any other will printed as a normal row.
+   Return values
+   Void
+   Note:
+
+   This function will print the current row as far as the following holds:
+   total_rowspan=0
+   where total_rowspan is set as
+   total_rowspan=max(total_rowspan, max(rowspan of cell in the current row))-1;
+   ***********************************************************************/
+
+   public function printRow($setAsHeader=false){
+      $this->col_counter=0;
+      $row_number=count($this->rows);
+      $this->rows[$row_number]=count($this->row_data);
+      $mx=$this->row_style['min-height'];
+
+         $this->row_content_loop($row_number, function($index)use(&$mx){
+         if($this->row_data[$index][1]['rowspan']==0){
+            $mx=max($mx, $this->row_data[$index][3]+2*$this->row_data[$index][1]['paddingY']);
+         }
+      });
+      $this->row_heights[$row_number]=$mx;
+      
+      if($this->total_rowspan>0){
+         $this->total_rowspan--;
+      }
+      else{
+         $row_number=count($this->rows);
+         if(count($this->blocks)>0){
+            
+            foreach($this->blocks as $bk_id=>$block){
+               $h=0;
+               for($i=$block[1]; $i<=$block[1]+$block[2]; $i++){
+                  $h+=$this->row_heights[$i];
+               }
+               $t=$this->row_data[$block[0]][3]+2*$this->row_data[$block[0]][1]['paddingY'];
+               if($h>0 && $h<$t){
+                  for($i=$block[1]; $i<=$block[1]+$block[2]; $i++){
+                     $this->row_heights[$i]*=$t/$h;
+                  }
+               }
+            }
+            foreach($this->blocks as $j=>$block){
+               $h=0;
+               for($i=$block[1]; $i<=$block[1]+$block[2]; $i++){
+                  $h+=$this->row_heights[$i];
+               }
+               $this->row_data[$j][5]=$h;
+            }
+         }
+         $block_height=0;
+         for($j=0; $j<$row_number; $j++){
+
+               $this->row_content_loop($j, function($index)use($j, $block_height){
+               if($this->row_data[$index][1]['rowspan']==0){
+                  $this->row_data[$index][5]=$this->row_heights[$j];
+               }
+               $this->row_data[$index][1]['padding-y']=$this->row_data[$index][1]['paddingY'];
+               if($this->row_data[$index][1]['valign']=='M' || ($this->row_data[$index][1]['img'] && count($this->row_data[$index][0]))){
+                  $this->row_data[$index][1]['padding-y']=($this->row_data[$index][5]-$this->row_data[$index][3])/2;
+               }
+               elseif($this->row_data[$index][1]['valign']=='B'){
+                  $this->row_data[$index][1]['padding-y']=$this->row_data[$index][5]-($this->row_data[$index][3]+$this->row_data[$index][1]['paddingY']);
+               }
+            });
+            $block_height+=$this->row_heights[$j];
+         }
+
+			$rowIsHeader=1+(count($this->header_row)>0);
+         if($setAsHeader===true){
+            if(count($this->header_row)==0){
+               $this->header_row['row_heights']=$this->row_heights;
+               $this->header_row['row_data']=$this->row_data;
+               $this->header_row['rows']=$this->rows;
+            }
+         }
+         if($this->table_style['split-row']==false && $this->pdf_obj->PageBreak()<$this->pdf_obj->GetY()+max($block_height,$this->row_heights[0])){
+            $this->pdf_obj->addPage($this->document_style['orientation'], $this->pdf_obj->get_page_size(), $this->pdf_obj->get_rotation());
+            if(count($this->header_row)>0){
+               $this->printing_loop(true);
+               $rowIsHeader--;
+            }
+         }
+         
+         if($this->new_table){
+            if(count($this->header_row)>0){
+               $r=$this->pdf_obj->PageBreak()-($this->pdf_obj->GetY()+$block_height);
+               if($r<0 || $r<$this->conv_units(self::PBThreshold)){
+                  $this->pdf_obj->addPage($this->document_style['orientation'], $this->pdf_obj->get_page_size(), $this->pdf_obj->get_rotation());
+               }
+            }
+            $this->new_table=false;
+         }
+         if($rowIsHeader>0){
+				$this->printing_loop();
+			}
+         $this->grid=array();
+         $this->row_data=array();
+         $this->rows=array();
+         $this->row_heights=array();
+         $this->blocks=array();
+         $this->overflow=0;
+         $this->new_table=false;
+      }
+      $this->row_style=$this->row_style_def;
+   }
+   /***********************************************************************
+
+   function endTable( [int $bottomMargin=2])
+   ------------------------------------------
+   Description:
+   Unset all the data members of the easyTable object
+   Parameters:
+   bottomMargin (optional)
+   Optional. Specify the number of white lines left after
+   the last row of the table. Default 2.
+   If it is negative, the vertical position will be set before
+   the end of the table.
+   Return values
+   Void
+   ***********************************************************************/
+
+   public function endTable($bottomMargin=2){
+      self::$table_counter=false;
+      if($this->table_style['border-width']!=false){
+         $this->pdf_obj->SetLineWidth($this->document_style['line-width']);
+      }
+      $this->pdf_obj->SetX($this->pdf_obj->get_margin('l'));
+      $this->pdf_obj->Ln($bottomMargin);
+      $this->pdf_obj->resetStaticData();
+      unset($this->pdf_obj);
+      unset($this->document_style);
+      unset($this->table_style);
+      unset($this->col_num);
+      unset($this->col_width);
+      unset($this->baseX);
+      unset($this->row_style_def);
+      unset($this->row_style);
+      unset($this->row_heights);
+      unset($this->row_data);
+      unset($this->rows);
+      unset($this->total_rowspan);
+      unset($this->col_counter);
+      unset($this->grid);
+      unset($this->blocks);
+      unset($this->overflow);
+      unset($this->header_row);
+   }
+   
+}
+?>
diff --git a/src/lib/fpdf-easytable/exfpdf.php b/src/lib/fpdf-easytable/exfpdf.php
new file mode 100755
index 0000000..d61a247
--- /dev/null
+++ b/src/lib/fpdf-easytable/exfpdf.php
@@ -0,0 +1,401 @@
+<?php
+ /*********************************************************************
+ * exFPDF  extend FPDF v1.81                                                    *
+ *                                                                    *
+ * Version: 2.2                                                       *
+ * Date:    12-10-2017                                                *
+ * Author:  Dan Machado                                               *
+ * Require  FPDF v1.81, formatedstring v1.0                                                *
+ **********************************************************************/
+ include 'formatedstring.php';
+ class exFPDF extends FPDF{
+
+    public function PageBreak(){
+       return $this->PageBreakTrigger;
+   }
+
+   public function current_font($c){
+      if($c=='family'){
+         return $this->FontFamily;
+      }
+      elseif($c=='style'){
+         return $this->FontStyle;
+      }
+      elseif($c=='size'){
+         return $this->FontSizePt;
+      }
+   }
+
+   public function get_color($c){
+      if($c=='fill'){
+         return $this->FillColor;
+      }
+      elseif($c=='text'){
+         return $this->TextColor;
+      }
+   }
+
+   public function get_page_width(){
+      return $this->w;
+   }
+
+   public function get_margin($c){
+      if($c=='l'){
+         return $this->lMargin;
+      }
+      elseif($c=='r'){
+         return $this->rMargin;
+      }
+      elseif($c=='t'){
+         return $this->tMargin;
+      }
+   }
+
+   public function get_linewidth(){
+      return $this->LineWidth;
+   }
+
+   public function get_orientation(){
+      return $this->CurOrientation;
+   }
+
+   public function get_page_size()
+   {
+    	return $this->CurPageSize;
+   }
+
+   public function get_rotation()
+   {
+      return $this->CurRotation;
+   }
+
+   public function get_scale_factor()
+   {
+      return $this->k;
+   }
+
+   static private $hex=array('0'=>0,'1'=>1,'2'=>2,'3'=>3,'4'=>4,'5'=>5,'6'=>6,'7'=>7,'8'=>8,'9'=>9,
+   'A'=>10,'B'=>11,'C'=>12,'D'=>13,'E'=>14,'F'=>15);
+
+   public function is_rgb($str){
+      $a=true;
+      $tmp=explode(',', trim($str, ','));
+      foreach($tmp as $color){
+         if(!is_numeric($color) || $color<0 || $color>255){
+            $a=false;
+            break;
+         }
+      }
+      return $a;
+   }
+
+   public function is_hex($str){
+      $a=true;
+      $str=strtoupper($str);
+      $n=strlen($str);
+      if(($n==7 || $n==4) && $str[0]=='#'){
+         for($i=1; $i<$n; $i++){
+            if(!isset(self::$hex[$str[$i]])){
+               $a=false;
+               break;
+            }
+         }
+      }
+      else{
+         $a=false;
+      }
+      return $a;
+   }
+
+   public function hextodec($str){
+      $result=array();
+      $str=strtoupper(substr($str,1));
+      $n=strlen($str);
+      for($i=0; $i<3; $i++){
+         if($n==6){
+            $result[$i]=self::$hex[$str[2*$i]]*16+self::$hex[$str[2*$i+1]];
+         }
+         else{
+            $result[$i]=self::$hex[$str[$i]]*16+self::$hex[$str[$i]];
+         }
+      }
+      return $result;
+   }
+   static private $options=array('F'=>'', 'T'=>'', 'D'=>'');
+
+   public function resetColor($str, $p='F'){
+      if(isset(self::$options[$p]) && self::$options[$p]!=$str){
+         self::$options[$p]=$str;
+         $array=array();
+         if($this->is_hex($str)){
+            $array=$this->hextodec($str);
+         }
+         elseif($this->is_rgb($str)){
+            $array=explode(',', trim($str, ','));
+            for($i=0; $i<3; $i++){
+               if(!isset($array[$i])){
+                  $array[$i]=0;
+               }
+            }
+         }
+         else{
+            $array=array(null, null, null);
+            $i=0;
+            $tmp=explode(' ', $str);
+            foreach($tmp as $c){
+               if(is_numeric($c)){
+                  $array[$i]=$c*256;
+                  $i++;
+               }
+            }
+         }
+         if($p=='T'){
+            $this->SetTextColor($array[0],$array[1],$array[2]);
+         }
+         elseif($p=='D'){
+            $this->SetDrawColor($array[0],$array[1], $array[2]);
+         }
+         elseif($p=='F'){
+            $this->SetFillColor($array[0],$array[1],$array[2]);
+         }
+      }
+   }
+   static private $font_def='';
+
+   public function resetFont($font_family, $font_style, $font_size){
+      if(self::$font_def!=$font_family .'-' . $font_style . '-' .$font_size){
+         self::$font_def=$font_family .'-' . $font_style . '-' .$font_size;
+         $this->SetFont($font_family, $font_style, $font_size);
+      }
+   }
+
+   public function resetStaticData(){
+      self::$font_def='';
+      self::$options=array('F'=>'', 'T'=>'', 'D'=>'');
+   }
+
+   /***********************************************************************
+   *
+   * Based on FPDF method SetFont
+   *
+   ************************************************************************/
+
+   private function &FontData($family, $style, $size){
+      if($family=='')
+      $family = $this->FontFamily;
+      else
+      $family = strtolower($family);
+      $style = strtoupper($style);
+      if(strpos($style,'U')!==false){
+         $this->underline = true;
+         $style = str_replace('U','',$style);
+      }
+      if($style=='IB')
+      $style = 'BI';
+      $fontkey = $family.$style;
+      if(!isset($this->fonts[$fontkey])){
+         if($family=='arial')
+         $family = 'helvetica';
+         if(in_array($family,$this->CoreFonts)){
+            if($family=='symbol' || $family=='zapfdingbats')
+            $style = '';
+            $fontkey = $family.$style;
+            if(!isset($this->fonts[$fontkey]))
+            $this->AddFont($family,$style);
+         }
+         else
+         $this->Error('Undefined font: '.$family.' '.$style);
+      }
+      $result['FontSize'] = $size/$this->k;
+      $result['CurrentFont']=&$this->fonts[$fontkey];
+      return $result;
+   }
+    
+
+   private function setLines(&$fstring, $p, $q){
+      $parced_str=& $fstring->parced_str;
+      $lines=& $fstring->lines;
+      $linesmap=& $fstring->linesmap;
+      $cfty=$fstring->get_current_style($p);
+      $ffs=$cfty['font-family'] . $cfty['style'];
+      if(!isset($fstring->used_fonts[$ffs])){
+         $fstring->used_fonts[$ffs]=& $this->FontData($cfty['font-family'], $cfty['style'], $cfty['font-size']);
+      }
+      $cw=& $fstring->used_fonts[$ffs]['CurrentFont']['cw'];
+      $wmax = $fstring->width*1000*$this->k;
+      $j=count($lines)-1;
+      $k=strlen($lines[$j]);
+         if(!isset($linesmap[$j][0])) {
+         $linesmap[$j]=array($p,$p, 0);
+      }
+      $sl=$cw[' ']*$cfty['font-size'];
+      $x=$a=$linesmap[$j][2];
+      if($k>0){
+         $x+=$sl;
+         $lines[$j].=' ';
+         $linesmap[$j][2]+=$sl;
+      }
+      $u=$p;
+      $t='';
+      $l=$p+$q;
+      $ftmp='';
+      for($i=$p; $i<$l; $i++){
+            if($ftmp!=$ffs){
+            $cfty=$fstring->get_current_style($i);
+            $ffs=$cfty['font-family'] . $cfty['style'];
+            if(!isset($fstring->used_fonts[$ffs])){
+               $fstring->used_fonts[$ffs]=& $this->FontData($cfty['font-family'], $cfty['style'], $cfty['font-size']);
+            }
+            $cw=& $fstring->used_fonts[$ffs]['CurrentFont']['cw'];
+            $ftmp=$ffs;
+         }
+         $x+=$cw[$parced_str[$i]]*$cfty['font-size'];
+         if($x>$wmax){
+            if($a>0){
+               $t=substr($parced_str,$p, $i-$p);
+               $lines[$j]=substr($lines[$j],0,$k);
+               $linesmap[$j][1]=$p-1;
+               $linesmap[$j][2]=$a;
+               $x-=($a+$sl);
+               $a=0;
+               $u=$p;
+            }
+            else{
+               $x=$cw[$parced_str[$i]]*$cfty['font-size'];
+               $t='';
+               $u=$i;
+            }
+            $j++;
+            $lines[$j]=$t;
+            $linesmap[$j]=array();
+            $linesmap[$j][0]=$u;
+            $linesmap[$j][2]=0;
+         }
+         $lines[$j].=$parced_str[$i];
+         $linesmap[$j][1]=$i;
+         $linesmap[$j][2]=$x;
+      }
+      return;
+   }
+
+   public function &extMultiCell($font_family, $font_style, $font_size, $font_color, $w, $txt){
+      $result=array();
+      if($w==0){
+         return $result;
+      }
+      $current_font=array('font-family'=>$font_family, 'style'=>$font_style, 'font-size'=>$font_size, 'font-color'=>$font_color);
+      $fstring=new formatedString($txt, $w, $current_font);
+      $word='';
+      $p=0;
+      $i=0;
+      $n=strlen($fstring->parced_str);
+      while($i<$n){
+         $word.=$fstring->parced_str[$i];
+         if($fstring->parced_str[$i]=="\n" || $fstring->parced_str[$i]==' ' || $i==$n-1){
+            $word=trim($word);
+            $this->setLines($fstring, $p, strlen($word));
+            $p=$i+1;
+            $word='';
+            if($fstring->parced_str[$i]=="\n" && $i<$n-1){
+               $z=0;
+               $j=count($fstring->lines);
+               $fstring->lines[$j]='';
+               $fstring->linesmap[$j]=array();
+            }
+         }
+         $i++;
+      }
+      if($n==0){
+         return $result;
+      }
+      $n=count($fstring->lines);
+         for($i=0; $i<$n; $i++){
+         $result[$i]=$fstring->break_by_style($i);
+      }
+      return $result;
+   }
+
+   private function GetMixStringWidth($line){
+      $w = 0;
+      foreach($line['chunks'] as $i=>$chunk){
+         $t=0;
+         $cf=& $this->FontData($line['style'][$i]['font-family'], $line['style'][$i]['style'], $line['style'][$i]['font-size']);
+         $cw=& $cf['CurrentFont']['cw'];
+         $s=implode('', explode(' ',$chunk));
+         $l = strlen($s);
+         for($j=0;$j<$l;$j++){
+            $t+=$cw[$s[$j]];
+         }
+         $w+=$t*$line['style'][$i]['font-size'];
+      }
+      return $w;
+   }
+
+   public function CellBlock($w, $lh, &$lines, $align='J'){
+      if($w==0){
+         return;
+      }
+      $ctmp='';
+      $ftmp='';
+      foreach($lines as $i=>$line){
+         $k = $this->k;
+         if($this->y+$lh*$line['height']>$this->PageBreakTrigger){
+            break;
+         }
+         $dx=0;
+         $dw=0;
+         if($line['width']!=0){
+            if($align=='R'){
+               $dx = $w-$line['width']/($this->k*1000);
+            }
+            elseif($align=='C'){
+               $dx = ($w-$line['width']/($this->k*1000))/2;
+            }
+            if($align=='J'){
+               $tmp=explode(' ', implode('',$line['chunks']));
+               $ns=count($tmp);
+               if($ns>1){
+                  $sx=implode('',$tmp);
+                  $delta=$this->GetMixStringWidth($line)/($this->k*1000);
+                  $dw=($w-$delta)*(1/($ns-1));
+               }
+            }
+         }
+         $xx=$this->x+$dx;
+         foreach($line['chunks'] as $tj=>$txt){
+            $this->resetFont($line['style'][$tj]['font-family'], $line['style'][$tj]['style'], $line['style'][$tj]['font-size']);
+            $this->resetColor($line['style'][$tj]['font-color'], 'T');
+            $y=$this->y+0.5*$lh*$line['height'] +0.3*$line['height']/$this->k;
+            if($dw){
+               $tmp=explode(' ', $txt);
+               foreach($tmp as $e=>$tt){
+                  if($e>0){
+                     $xx+=$dw;
+                     if($tt==''){
+                        continue;
+                     }
+                  }
+                  $this->Text($xx, $y, $tt);
+                     if($line['style'][$tj]['href']){
+                     $yr=$this->y+0.5*($lh*$line['height']-$line['height']/$this->k);
+                     $this->Link($xx, $yr, $this->GetStringWidth($txt),$line['height']/$this->k, $line['style'][$tj]['href']);
+                  }
+                  $xx+=$this->GetStringWidth($tt);
+               }
+            }
+            else{
+               $this->Text($xx, $y, $txt);
+                  if($line['style'][$tj]['href']){
+                  $yr=$this->y+0.5*($lh*$line['height']-$line['height']/$this->k);
+                  $this->Link($xx, $yr, $this->GetStringWidth($txt),$line['height']/$this->k, $line['style'][$tj]['href']);
+               }
+               $xx+=$this->GetStringWidth($txt);
+            }
+         }
+         unset($lines[$i]);
+         $this->y += $lh*$line['height'];
+      }
+   }
+   
+}
+?>
diff --git a/src/lib/fpdf-easytable/font/Apache License.txt b/src/lib/fpdf-easytable/font/Apache License.txt
new file mode 100644
index 0000000..989e2c5
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/Apache License.txt
@@ -0,0 +1,201 @@
+Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
\ No newline at end of file
diff --git a/src/lib/fpdf-easytable/font/Arimo-Bold.php b/src/lib/fpdf-easytable/font/Arimo-Bold.php
new file mode 100644
index 0000000..50ed855
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/Arimo-Bold.php
@@ -0,0 +1,26 @@
+<?php
+$type = 'TrueType';
+$name = 'Arimo-Bold';
+$desc = array('Ascent'=>728,'Descent'=>-210,'CapHeight'=>688,'Flags'=>32,'FontBBox'=>'[-482 -376 1304 1033]','ItalicAngle'=>0,'StemV'=>120,'MissingWidth'=>750);
+$up = -106;
+$ut = 105;
+$cw = array(
+	chr(0)=>750,chr(1)=>750,chr(2)=>750,chr(3)=>750,chr(4)=>750,chr(5)=>750,chr(6)=>750,chr(7)=>750,chr(8)=>750,chr(9)=>750,chr(10)=>750,chr(11)=>750,chr(12)=>750,chr(13)=>750,chr(14)=>750,chr(15)=>750,chr(16)=>750,chr(17)=>750,chr(18)=>750,chr(19)=>750,chr(20)=>750,chr(21)=>750,
+	chr(22)=>750,chr(23)=>750,chr(24)=>750,chr(25)=>750,chr(26)=>750,chr(27)=>750,chr(28)=>750,chr(29)=>750,chr(30)=>750,chr(31)=>750,' '=>278,'!'=>333,'"'=>474,'#'=>556,'$'=>556,'%'=>889,'&'=>722,'\''=>238,'('=>333,')'=>333,'*'=>389,'+'=>584,
+	','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>333,';'=>333,'<'=>584,'='=>584,'>'=>584,'?'=>611,'@'=>975,'A'=>722,
+	'B'=>722,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>556,'K'=>722,'L'=>611,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944,
+	'X'=>667,'Y'=>667,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>584,'_'=>556,'`'=>333,'a'=>556,'b'=>611,'c'=>556,'d'=>611,'e'=>556,'f'=>333,'g'=>611,'h'=>611,'i'=>278,'j'=>278,'k'=>556,'l'=>278,'m'=>889,
+	'n'=>611,'o'=>611,'p'=>611,'q'=>611,'r'=>389,'s'=>556,'t'=>333,'u'=>611,'v'=>556,'w'=>778,'x'=>556,'y'=>556,'z'=>500,'{'=>389,'|'=>280,'}'=>389,'~'=>584,chr(127)=>750,chr(128)=>708,chr(129)=>625,chr(130)=>708,chr(131)=>708,
+	chr(132)=>708,chr(133)=>708,chr(134)=>708,chr(135)=>708,chr(136)=>708,chr(137)=>708,chr(138)=>708,chr(139)=>708,chr(140)=>708,chr(141)=>708,chr(142)=>708,chr(143)=>708,chr(144)=>708,chr(145)=>708,chr(146)=>729,chr(147)=>604,chr(148)=>604,chr(149)=>278,chr(150)=>549,chr(151)=>549,chr(152)=>549,chr(153)=>549,
+	chr(154)=>278,chr(155)=>604,chr(156)=>400,chr(157)=>333,chr(158)=>333,chr(159)=>549,chr(160)=>708,chr(161)=>708,chr(162)=>708,chr(163)=>556,chr(164)=>708,chr(165)=>708,chr(166)=>708,chr(167)=>708,chr(168)=>708,chr(169)=>708,chr(170)=>708,chr(171)=>708,chr(172)=>708,chr(173)=>708,chr(174)=>708,chr(175)=>708,
+	chr(176)=>708,chr(177)=>708,chr(178)=>708,chr(179)=>669,chr(180)=>708,chr(181)=>708,chr(182)=>708,chr(183)=>708,chr(184)=>708,chr(185)=>708,chr(186)=>708,chr(187)=>708,chr(188)=>708,chr(189)=>708,chr(190)=>708,chr(191)=>737,chr(192)=>854,chr(193)=>556,chr(194)=>618,chr(195)=>615,chr(196)=>635,chr(197)=>556,
+	chr(198)=>875,chr(199)=>417,chr(200)=>556,chr(201)=>615,chr(202)=>615,chr(203)=>500,chr(204)=>635,chr(205)=>740,chr(206)=>604,chr(207)=>611,chr(208)=>604,chr(209)=>583,chr(210)=>611,chr(211)=>556,chr(212)=>490,chr(213)=>556,chr(214)=>709,chr(215)=>615,chr(216)=>615,chr(217)=>854,chr(218)=>497,chr(219)=>833,
+	chr(220)=>552,chr(221)=>844,chr(222)=>581,chr(223)=>729,chr(224)=>1031,chr(225)=>722,chr(226)=>719,chr(227)=>730,chr(228)=>712,chr(229)=>667,chr(230)=>854,chr(231)=>567,chr(232)=>667,chr(233)=>719,chr(234)=>719,chr(235)=>610,chr(236)=>702,chr(237)=>833,chr(238)=>722,chr(239)=>778,chr(240)=>719,chr(241)=>719,
+	chr(242)=>667,chr(243)=>722,chr(244)=>611,chr(245)=>622,chr(246)=>904,chr(247)=>722,chr(248)=>719,chr(249)=>979,chr(250)=>626,chr(251)=>1005,chr(252)=>711,chr(253)=>1019,chr(254)=>703,chr(255)=>870);
+$enc = 'KOI8-R';
+$diff = '128 /SF100000 /SF110000 /SF010000 /SF030000 /SF020000 /SF040000 /SF080000 /SF090000 /SF060000 /SF070000 /SF050000 /upblock /dnblock /block /lfblock /rtblock /ltshade /shade /dkshade /integraltp /filledbox /periodcentered /radical /approxequal /lessequal /greaterequal /space /integralbt /degree /twosuperior /periodcentered /divide /SF430000 /SF240000 /SF510000 /afii10071 /SF520000 /SF390000 /SF220000 /SF210000 /SF250000 /SF500000 /SF490000 /SF380000 /SF280000 /SF270000 /SF260000 /SF360000 /SF370000 /SF420000 /SF190000 /afii10023 /SF200000 /SF230000 /SF470000 /SF480000 /SF410000 /SF450000 /SF460000 /SF400000 /SF540000 /SF530000 /SF440000 /copyright /afii10096 /afii10065 /afii10066 /afii10088 /afii10069 /afii10070 /afii10086 /afii10068 /afii10087 /afii10074 /afii10075 /afii10076 /afii10077 /afii10078 /afii10079 /afii10080 /afii10081 /afii10097 /afii10082 /afii10083 /afii10084 /afii10085 /afii10072 /afii10067 /afii10094 /afii10093 /afii10073 /afii10090 /afii10095 /afii10091 /afii10089 /afii10092 /afii10048 /afii10017 /afii10018 /afii10040 /afii10021 /afii10022 /afii10038 /afii10020 /afii10039 /afii10026 /afii10027 /afii10028 /afii10029 /afii10030 /afii10031 /afii10032 /afii10033 /afii10049 /afii10034 /afii10035 /afii10036 /afii10037 /afii10024 /afii10019 /afii10046 /afii10045 /afii10025 /afii10042 /afii10047 /afii10043 /afii10041 /afii10044';
+$uv = array(0=>array(0,128),128=>9472,129=>9474,130=>9484,131=>9488,132=>9492,133=>9496,134=>9500,135=>9508,136=>9516,137=>9524,138=>9532,139=>9600,140=>9604,141=>9608,142=>9612,143=>array(9616,4),147=>8992,148=>9632,149=>array(8729,2),151=>8776,152=>array(8804,2),154=>160,155=>8993,156=>176,157=>178,158=>183,159=>247,160=>array(9552,3),163=>1105,164=>array(9555,15),179=>1025,180=>array(9570,11),191=>169,192=>1102,193=>array(1072,2),195=>1094,196=>array(1076,2),198=>1092,199=>1075,200=>1093,201=>array(1080,8),209=>1103,210=>array(1088,4),214=>1078,215=>1074,216=>1100,217=>1099,218=>1079,219=>1096,220=>1101,221=>1097,222=>1095,223=>1098,224=>1070,225=>array(1040,2),227=>1062,228=>array(1044,2),230=>1060,231=>1043,232=>1061,233=>array(1048,8),241=>1071,242=>array(1056,4),246=>1046,247=>1042,248=>1068,249=>1067,250=>1047,251=>1064,252=>1069,253=>1065,254=>1063,255=>1066);
+$file = 'Arimo-Bold.z';
+$originalsize = 49388;
+$subsetted = true;
+?>
diff --git a/src/lib/fpdf-easytable/font/Arimo-Bold.z b/src/lib/fpdf-easytable/font/Arimo-Bold.z
new file mode 100644
index 0000000..8f3e396
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/Arimo-Bold.z
Binary files differ
diff --git a/src/lib/fpdf-easytable/font/Arimo-BoldItalic.php b/src/lib/fpdf-easytable/font/Arimo-BoldItalic.php
new file mode 100644
index 0000000..dcec8f2
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/Arimo-BoldItalic.php
@@ -0,0 +1,26 @@
+<?php
+$type = 'TrueType';
+$name = 'Arimo-BoldItalic';
+$desc = array('Ascent'=>728,'Descent'=>-210,'CapHeight'=>688,'Flags'=>96,'FontBBox'=>'[-477 -376 1357 1030]','ItalicAngle'=>-12,'StemV'=>120,'MissingWidth'=>750);
+$up = -106;
+$ut = 105;
+$cw = array(
+	chr(0)=>750,chr(1)=>750,chr(2)=>750,chr(3)=>750,chr(4)=>750,chr(5)=>750,chr(6)=>750,chr(7)=>750,chr(8)=>750,chr(9)=>750,chr(10)=>750,chr(11)=>750,chr(12)=>750,chr(13)=>750,chr(14)=>750,chr(15)=>750,chr(16)=>750,chr(17)=>750,chr(18)=>750,chr(19)=>750,chr(20)=>750,chr(21)=>750,
+	chr(22)=>750,chr(23)=>750,chr(24)=>750,chr(25)=>750,chr(26)=>750,chr(27)=>750,chr(28)=>750,chr(29)=>750,chr(30)=>750,chr(31)=>750,' '=>278,'!'=>333,'"'=>474,'#'=>556,'$'=>556,'%'=>889,'&'=>722,'\''=>238,'('=>333,')'=>333,'*'=>389,'+'=>584,
+	','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>333,';'=>333,'<'=>584,'='=>584,'>'=>584,'?'=>611,'@'=>975,'A'=>722,
+	'B'=>722,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>556,'K'=>722,'L'=>611,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944,
+	'X'=>667,'Y'=>667,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>584,'_'=>556,'`'=>333,'a'=>556,'b'=>611,'c'=>556,'d'=>611,'e'=>556,'f'=>333,'g'=>611,'h'=>611,'i'=>278,'j'=>278,'k'=>556,'l'=>278,'m'=>889,
+	'n'=>611,'o'=>611,'p'=>611,'q'=>611,'r'=>389,'s'=>556,'t'=>333,'u'=>611,'v'=>556,'w'=>778,'x'=>556,'y'=>556,'z'=>500,'{'=>389,'|'=>280,'}'=>389,'~'=>584,chr(127)=>750,chr(128)=>708,chr(129)=>625,chr(130)=>708,chr(131)=>708,
+	chr(132)=>708,chr(133)=>708,chr(134)=>708,chr(135)=>708,chr(136)=>708,chr(137)=>708,chr(138)=>708,chr(139)=>708,chr(140)=>708,chr(141)=>708,chr(142)=>708,chr(143)=>708,chr(144)=>708,chr(145)=>708,chr(146)=>729,chr(147)=>604,chr(148)=>604,chr(149)=>278,chr(150)=>549,chr(151)=>549,chr(152)=>549,chr(153)=>549,
+	chr(154)=>278,chr(155)=>604,chr(156)=>400,chr(157)=>333,chr(158)=>333,chr(159)=>549,chr(160)=>708,chr(161)=>708,chr(162)=>708,chr(163)=>556,chr(164)=>708,chr(165)=>708,chr(166)=>708,chr(167)=>708,chr(168)=>708,chr(169)=>708,chr(170)=>708,chr(171)=>708,chr(172)=>708,chr(173)=>708,chr(174)=>708,chr(175)=>708,
+	chr(176)=>708,chr(177)=>708,chr(178)=>708,chr(179)=>667,chr(180)=>708,chr(181)=>708,chr(182)=>708,chr(183)=>708,chr(184)=>708,chr(185)=>708,chr(186)=>708,chr(187)=>708,chr(188)=>708,chr(189)=>708,chr(190)=>708,chr(191)=>737,chr(192)=>865,chr(193)=>556,chr(194)=>619,chr(195)=>646,chr(196)=>618,chr(197)=>556,
+	chr(198)=>885,chr(199)=>534,chr(200)=>556,chr(201)=>611,chr(202)=>611,chr(203)=>507,chr(204)=>622,chr(205)=>740,chr(206)=>604,chr(207)=>611,chr(208)=>611,chr(209)=>589,chr(210)=>611,chr(211)=>556,chr(212)=>889,chr(213)=>556,chr(214)=>736,chr(215)=>604,chr(216)=>594,chr(217)=>854,chr(218)=>510,chr(219)=>889,
+	chr(220)=>552,chr(221)=>935,chr(222)=>583,chr(223)=>707,chr(224)=>1042,chr(225)=>722,chr(226)=>708,chr(227)=>729,chr(228)=>722,chr(229)=>667,chr(230)=>781,chr(231)=>614,chr(232)=>667,chr(233)=>719,chr(234)=>719,chr(235)=>615,chr(236)=>687,chr(237)=>833,chr(238)=>722,chr(239)=>778,chr(240)=>719,chr(241)=>729,
+	chr(242)=>667,chr(243)=>722,chr(244)=>611,chr(245)=>677,chr(246)=>927,chr(247)=>722,chr(248)=>708,chr(249)=>1000,chr(250)=>643,chr(251)=>979,chr(252)=>719,chr(253)=>989,chr(254)=>708,chr(255)=>854);
+$enc = 'KOI8-R';
+$diff = '128 /SF100000 /SF110000 /SF010000 /SF030000 /SF020000 /SF040000 /SF080000 /SF090000 /SF060000 /SF070000 /SF050000 /upblock /dnblock /block /lfblock /rtblock /ltshade /shade /dkshade /integraltp /filledbox /periodcentered /radical /approxequal /lessequal /greaterequal /space /integralbt /degree /twosuperior /periodcentered /divide /SF430000 /SF240000 /SF510000 /afii10071 /SF520000 /SF390000 /SF220000 /SF210000 /SF250000 /SF500000 /SF490000 /SF380000 /SF280000 /SF270000 /SF260000 /SF360000 /SF370000 /SF420000 /SF190000 /afii10023 /SF200000 /SF230000 /SF470000 /SF480000 /SF410000 /SF450000 /SF460000 /SF400000 /SF540000 /SF530000 /SF440000 /copyright /afii10096 /afii10065 /afii10066 /afii10088 /afii10069 /afii10070 /afii10086 /afii10068 /afii10087 /afii10074 /afii10075 /afii10076 /afii10077 /afii10078 /afii10079 /afii10080 /afii10081 /afii10097 /afii10082 /afii10083 /afii10084 /afii10085 /afii10072 /afii10067 /afii10094 /afii10093 /afii10073 /afii10090 /afii10095 /afii10091 /afii10089 /afii10092 /afii10048 /afii10017 /afii10018 /afii10040 /afii10021 /afii10022 /afii10038 /afii10020 /afii10039 /afii10026 /afii10027 /afii10028 /afii10029 /afii10030 /afii10031 /afii10032 /afii10033 /afii10049 /afii10034 /afii10035 /afii10036 /afii10037 /afii10024 /afii10019 /afii10046 /afii10045 /afii10025 /afii10042 /afii10047 /afii10043 /afii10041 /afii10044';
+$uv = array(0=>array(0,128),128=>9472,129=>9474,130=>9484,131=>9488,132=>9492,133=>9496,134=>9500,135=>9508,136=>9516,137=>9524,138=>9532,139=>9600,140=>9604,141=>9608,142=>9612,143=>array(9616,4),147=>8992,148=>9632,149=>array(8729,2),151=>8776,152=>array(8804,2),154=>160,155=>8993,156=>176,157=>178,158=>183,159=>247,160=>array(9552,3),163=>1105,164=>array(9555,15),179=>1025,180=>array(9570,11),191=>169,192=>1102,193=>array(1072,2),195=>1094,196=>array(1076,2),198=>1092,199=>1075,200=>1093,201=>array(1080,8),209=>1103,210=>array(1088,4),214=>1078,215=>1074,216=>1100,217=>1099,218=>1079,219=>1096,220=>1101,221=>1097,222=>1095,223=>1098,224=>1070,225=>array(1040,2),227=>1062,228=>array(1044,2),230=>1060,231=>1043,232=>1061,233=>array(1048,8),241=>1071,242=>array(1056,4),246=>1046,247=>1042,248=>1068,249=>1067,250=>1047,251=>1064,252=>1069,253=>1065,254=>1063,255=>1066);
+$file = 'Arimo-BoldItalic.z';
+$originalsize = 45912;
+$subsetted = true;
+?>
diff --git a/src/lib/fpdf-easytable/font/Arimo-BoldItalic.z b/src/lib/fpdf-easytable/font/Arimo-BoldItalic.z
new file mode 100644
index 0000000..6f3682b
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/Arimo-BoldItalic.z
Binary files differ
diff --git a/src/lib/fpdf-easytable/font/Arimo-Italic.php b/src/lib/fpdf-easytable/font/Arimo-Italic.php
new file mode 100644
index 0000000..e681820
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/Arimo-Italic.php
@@ -0,0 +1,26 @@
+<?php
+$type = 'TrueType';
+$name = 'Arimo-Italic';
+$desc = array('Ascent'=>728,'Descent'=>-208,'CapHeight'=>688,'Flags'=>96,'FontBBox'=>'[-664 -303 1361 1014]','ItalicAngle'=>-12,'StemV'=>70,'MissingWidth'=>750);
+$up = -106;
+$ut = 73;
+$cw = array(
+	chr(0)=>750,chr(1)=>750,chr(2)=>750,chr(3)=>750,chr(4)=>750,chr(5)=>750,chr(6)=>750,chr(7)=>750,chr(8)=>750,chr(9)=>750,chr(10)=>750,chr(11)=>750,chr(12)=>750,chr(13)=>750,chr(14)=>750,chr(15)=>750,chr(16)=>750,chr(17)=>750,chr(18)=>750,chr(19)=>750,chr(20)=>750,chr(21)=>750,
+	chr(22)=>750,chr(23)=>750,chr(24)=>750,chr(25)=>750,chr(26)=>750,chr(27)=>750,chr(28)=>750,chr(29)=>750,chr(30)=>750,chr(31)=>750,' '=>278,'!'=>278,'"'=>355,'#'=>556,'$'=>556,'%'=>889,'&'=>667,'\''=>191,'('=>333,')'=>333,'*'=>389,'+'=>584,
+	','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>278,';'=>278,'<'=>584,'='=>584,'>'=>584,'?'=>556,'@'=>1015,'A'=>667,
+	'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>500,'K'=>667,'L'=>556,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944,
+	'X'=>667,'Y'=>667,'Z'=>611,'['=>278,'\\'=>278,']'=>278,'^'=>469,'_'=>556,'`'=>333,'a'=>556,'b'=>556,'c'=>500,'d'=>556,'e'=>556,'f'=>278,'g'=>556,'h'=>556,'i'=>222,'j'=>222,'k'=>500,'l'=>222,'m'=>833,
+	'n'=>556,'o'=>556,'p'=>556,'q'=>556,'r'=>333,'s'=>500,'t'=>278,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>500,'{'=>334,'|'=>260,'}'=>334,'~'=>584,chr(127)=>750,chr(128)=>708,chr(129)=>625,chr(130)=>708,chr(131)=>708,
+	chr(132)=>708,chr(133)=>708,chr(134)=>708,chr(135)=>708,chr(136)=>708,chr(137)=>708,chr(138)=>708,chr(139)=>708,chr(140)=>708,chr(141)=>708,chr(142)=>708,chr(143)=>708,chr(144)=>708,chr(145)=>708,chr(146)=>729,chr(147)=>604,chr(148)=>604,chr(149)=>278,chr(150)=>549,chr(151)=>549,chr(152)=>549,chr(153)=>549,
+	chr(154)=>278,chr(155)=>604,chr(156)=>400,chr(157)=>333,chr(158)=>333,chr(159)=>549,chr(160)=>708,chr(161)=>708,chr(162)=>708,chr(163)=>556,chr(164)=>708,chr(165)=>708,chr(166)=>708,chr(167)=>708,chr(168)=>708,chr(169)=>708,chr(170)=>708,chr(171)=>708,chr(172)=>708,chr(173)=>708,chr(174)=>708,chr(175)=>708,
+	chr(176)=>708,chr(177)=>708,chr(178)=>708,chr(179)=>667,chr(180)=>708,chr(181)=>708,chr(182)=>708,chr(183)=>708,chr(184)=>708,chr(185)=>708,chr(186)=>708,chr(187)=>708,chr(188)=>708,chr(189)=>708,chr(190)=>708,chr(191)=>737,chr(192)=>752,chr(193)=>556,chr(194)=>563,chr(195)=>572,chr(196)=>553,chr(197)=>556,
+	chr(198)=>835,chr(199)=>493,chr(200)=>500,chr(201)=>556,chr(202)=>556,chr(203)=>472,chr(204)=>564,chr(205)=>686,chr(206)=>550,chr(207)=>556,chr(208)=>550,chr(209)=>534,chr(210)=>556,chr(211)=>500,chr(212)=>833,chr(213)=>500,chr(214)=>688,chr(215)=>522,chr(216)=>526,chr(217)=>736,chr(218)=>465,chr(219)=>830,
+	chr(220)=>492,chr(221)=>851,chr(222)=>518,chr(223)=>621,chr(224)=>1022,chr(225)=>667,chr(226)=>651,chr(227)=>727,chr(228)=>704,chr(229)=>667,chr(230)=>795,chr(231)=>544,chr(232)=>667,chr(233)=>715,chr(234)=>715,chr(235)=>589,chr(236)=>686,chr(237)=>833,chr(238)=>722,chr(239)=>778,chr(240)=>725,chr(241)=>682,
+	chr(242)=>667,chr(243)=>722,chr(244)=>611,chr(245)=>639,chr(246)=>917,chr(247)=>667,chr(248)=>651,chr(249)=>886,chr(250)=>614,chr(251)=>920,chr(252)=>694,chr(253)=>923,chr(254)=>673,chr(255)=>805);
+$enc = 'KOI8-R';
+$diff = '128 /SF100000 /SF110000 /SF010000 /SF030000 /SF020000 /SF040000 /SF080000 /SF090000 /SF060000 /SF070000 /SF050000 /upblock /dnblock /block /lfblock /rtblock /ltshade /shade /dkshade /integraltp /filledbox /periodcentered /radical /approxequal /lessequal /greaterequal /space /integralbt /degree /twosuperior /periodcentered /divide /SF430000 /SF240000 /SF510000 /afii10071 /SF520000 /SF390000 /SF220000 /SF210000 /SF250000 /SF500000 /SF490000 /SF380000 /SF280000 /SF270000 /SF260000 /SF360000 /SF370000 /SF420000 /SF190000 /afii10023 /SF200000 /SF230000 /SF470000 /SF480000 /SF410000 /SF450000 /SF460000 /SF400000 /SF540000 /SF530000 /SF440000 /copyright /afii10096 /afii10065 /afii10066 /afii10088 /afii10069 /afii10070 /afii10086 /afii10068 /afii10087 /afii10074 /afii10075 /afii10076 /afii10077 /afii10078 /afii10079 /afii10080 /afii10081 /afii10097 /afii10082 /afii10083 /afii10084 /afii10085 /afii10072 /afii10067 /afii10094 /afii10093 /afii10073 /afii10090 /afii10095 /afii10091 /afii10089 /afii10092 /afii10048 /afii10017 /afii10018 /afii10040 /afii10021 /afii10022 /afii10038 /afii10020 /afii10039 /afii10026 /afii10027 /afii10028 /afii10029 /afii10030 /afii10031 /afii10032 /afii10033 /afii10049 /afii10034 /afii10035 /afii10036 /afii10037 /afii10024 /afii10019 /afii10046 /afii10045 /afii10025 /afii10042 /afii10047 /afii10043 /afii10041 /afii10044';
+$uv = array(0=>array(0,128),128=>9472,129=>9474,130=>9484,131=>9488,132=>9492,133=>9496,134=>9500,135=>9508,136=>9516,137=>9524,138=>9532,139=>9600,140=>9604,141=>9608,142=>9612,143=>array(9616,4),147=>8992,148=>9632,149=>array(8729,2),151=>8776,152=>array(8804,2),154=>160,155=>8993,156=>176,157=>178,158=>183,159=>247,160=>array(9552,3),163=>1105,164=>array(9555,15),179=>1025,180=>array(9570,11),191=>169,192=>1102,193=>array(1072,2),195=>1094,196=>array(1076,2),198=>1092,199=>1075,200=>1093,201=>array(1080,8),209=>1103,210=>array(1088,4),214=>1078,215=>1074,216=>1100,217=>1099,218=>1079,219=>1096,220=>1101,221=>1097,222=>1095,223=>1098,224=>1070,225=>array(1040,2),227=>1062,228=>array(1044,2),230=>1060,231=>1043,232=>1061,233=>array(1048,8),241=>1071,242=>array(1056,4),246=>1046,247=>1042,248=>1068,249=>1067,250=>1047,251=>1064,252=>1069,253=>1065,254=>1063,255=>1066);
+$file = 'Arimo-Italic.z';
+$originalsize = 44228;
+$subsetted = true;
+?>
diff --git a/src/lib/fpdf-easytable/font/Arimo-Italic.z b/src/lib/fpdf-easytable/font/Arimo-Italic.z
new file mode 100644
index 0000000..28ea8cc
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/Arimo-Italic.z
Binary files differ
diff --git a/src/lib/fpdf-easytable/font/Arimo-Regular.php b/src/lib/fpdf-easytable/font/Arimo-Regular.php
new file mode 100644
index 0000000..d6db3d3
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/Arimo-Regular.php
@@ -0,0 +1,26 @@
+<?php
+$type = 'TrueType';
+$name = 'Arimo';
+$desc = array('Ascent'=>728,'Descent'=>-210,'CapHeight'=>688,'Flags'=>32,'FontBBox'=>'[-544 -303 1302 980]','ItalicAngle'=>0,'StemV'=>70,'MissingWidth'=>750);
+$up = -106;
+$ut = 73;
+$cw = array(
+	chr(0)=>750,chr(1)=>750,chr(2)=>750,chr(3)=>750,chr(4)=>750,chr(5)=>750,chr(6)=>750,chr(7)=>750,chr(8)=>750,chr(9)=>750,chr(10)=>750,chr(11)=>750,chr(12)=>750,chr(13)=>750,chr(14)=>750,chr(15)=>750,chr(16)=>750,chr(17)=>750,chr(18)=>750,chr(19)=>750,chr(20)=>750,chr(21)=>750,
+	chr(22)=>750,chr(23)=>750,chr(24)=>750,chr(25)=>750,chr(26)=>750,chr(27)=>750,chr(28)=>750,chr(29)=>750,chr(30)=>750,chr(31)=>750,' '=>278,'!'=>278,'"'=>355,'#'=>556,'$'=>556,'%'=>889,'&'=>667,'\''=>191,'('=>333,')'=>333,'*'=>389,'+'=>584,
+	','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>278,';'=>278,'<'=>584,'='=>584,'>'=>584,'?'=>556,'@'=>1015,'A'=>667,
+	'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>500,'K'=>667,'L'=>556,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944,
+	'X'=>667,'Y'=>667,'Z'=>611,'['=>278,'\\'=>278,']'=>278,'^'=>469,'_'=>556,'`'=>333,'a'=>556,'b'=>556,'c'=>500,'d'=>556,'e'=>556,'f'=>278,'g'=>556,'h'=>556,'i'=>222,'j'=>222,'k'=>500,'l'=>222,'m'=>833,
+	'n'=>556,'o'=>556,'p'=>556,'q'=>556,'r'=>333,'s'=>500,'t'=>278,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>500,'{'=>334,'|'=>260,'}'=>334,'~'=>584,chr(127)=>750,chr(128)=>708,chr(129)=>625,chr(130)=>708,chr(131)=>708,
+	chr(132)=>708,chr(133)=>708,chr(134)=>708,chr(135)=>708,chr(136)=>708,chr(137)=>708,chr(138)=>708,chr(139)=>708,chr(140)=>708,chr(141)=>708,chr(142)=>708,chr(143)=>708,chr(144)=>708,chr(145)=>708,chr(146)=>729,chr(147)=>604,chr(148)=>604,chr(149)=>278,chr(150)=>549,chr(151)=>549,chr(152)=>549,chr(153)=>549,
+	chr(154)=>278,chr(155)=>604,chr(156)=>400,chr(157)=>333,chr(158)=>333,chr(159)=>549,chr(160)=>708,chr(161)=>708,chr(162)=>708,chr(163)=>556,chr(164)=>708,chr(165)=>708,chr(166)=>708,chr(167)=>708,chr(168)=>708,chr(169)=>708,chr(170)=>708,chr(171)=>708,chr(172)=>708,chr(173)=>708,chr(174)=>708,chr(175)=>708,
+	chr(176)=>708,chr(177)=>708,chr(178)=>708,chr(179)=>667,chr(180)=>708,chr(181)=>708,chr(182)=>708,chr(183)=>708,chr(184)=>708,chr(185)=>708,chr(186)=>708,chr(187)=>708,chr(188)=>708,chr(189)=>708,chr(190)=>708,chr(191)=>737,chr(192)=>750,chr(193)=>556,chr(194)=>573,chr(195)=>573,chr(196)=>583,chr(197)=>556,
+	chr(198)=>823,chr(199)=>365,chr(200)=>500,chr(201)=>559,chr(202)=>559,chr(203)=>438,chr(204)=>583,chr(205)=>688,chr(206)=>552,chr(207)=>556,chr(208)=>542,chr(209)=>542,chr(210)=>556,chr(211)=>500,chr(212)=>458,chr(213)=>500,chr(214)=>669,chr(215)=>531,chr(216)=>521,chr(217)=>719,chr(218)=>458,chr(219)=>802,
+	chr(220)=>510,chr(221)=>823,chr(222)=>521,chr(223)=>625,chr(224)=>1010,chr(225)=>667,chr(226)=>656,chr(227)=>740,chr(228)=>677,chr(229)=>667,chr(230)=>760,chr(231)=>542,chr(232)=>667,chr(233)=>719,chr(234)=>719,chr(235)=>583,chr(236)=>656,chr(237)=>833,chr(238)=>722,chr(239)=>778,chr(240)=>719,chr(241)=>722,
+	chr(242)=>667,chr(243)=>722,chr(244)=>611,chr(245)=>635,chr(246)=>923,chr(247)=>667,chr(248)=>656,chr(249)=>885,chr(250)=>604,chr(251)=>917,chr(252)=>719,chr(253)=>938,chr(254)=>667,chr(255)=>792);
+$enc = 'KOI8-R';
+$diff = '128 /SF100000 /SF110000 /SF010000 /SF030000 /SF020000 /SF040000 /SF080000 /SF090000 /SF060000 /SF070000 /SF050000 /upblock /dnblock /block /lfblock /rtblock /ltshade /shade /dkshade /integraltp /filledbox /periodcentered /radical /approxequal /lessequal /greaterequal /space /integralbt /degree /twosuperior /periodcentered /divide /SF430000 /SF240000 /SF510000 /afii10071 /SF520000 /SF390000 /SF220000 /SF210000 /SF250000 /SF500000 /SF490000 /SF380000 /SF280000 /SF270000 /SF260000 /SF360000 /SF370000 /SF420000 /SF190000 /afii10023 /SF200000 /SF230000 /SF470000 /SF480000 /SF410000 /SF450000 /SF460000 /SF400000 /SF540000 /SF530000 /SF440000 /copyright /afii10096 /afii10065 /afii10066 /afii10088 /afii10069 /afii10070 /afii10086 /afii10068 /afii10087 /afii10074 /afii10075 /afii10076 /afii10077 /afii10078 /afii10079 /afii10080 /afii10081 /afii10097 /afii10082 /afii10083 /afii10084 /afii10085 /afii10072 /afii10067 /afii10094 /afii10093 /afii10073 /afii10090 /afii10095 /afii10091 /afii10089 /afii10092 /afii10048 /afii10017 /afii10018 /afii10040 /afii10021 /afii10022 /afii10038 /afii10020 /afii10039 /afii10026 /afii10027 /afii10028 /afii10029 /afii10030 /afii10031 /afii10032 /afii10033 /afii10049 /afii10034 /afii10035 /afii10036 /afii10037 /afii10024 /afii10019 /afii10046 /afii10045 /afii10025 /afii10042 /afii10047 /afii10043 /afii10041 /afii10044';
+$uv = array(0=>array(0,128),128=>9472,129=>9474,130=>9484,131=>9488,132=>9492,133=>9496,134=>9500,135=>9508,136=>9516,137=>9524,138=>9532,139=>9600,140=>9604,141=>9608,142=>9612,143=>array(9616,4),147=>8992,148=>9632,149=>array(8729,2),151=>8776,152=>array(8804,2),154=>160,155=>8993,156=>176,157=>178,158=>183,159=>247,160=>array(9552,3),163=>1105,164=>array(9555,15),179=>1025,180=>array(9570,11),191=>169,192=>1102,193=>array(1072,2),195=>1094,196=>array(1076,2),198=>1092,199=>1075,200=>1093,201=>array(1080,8),209=>1103,210=>array(1088,4),214=>1078,215=>1074,216=>1100,217=>1099,218=>1079,219=>1096,220=>1101,221=>1097,222=>1095,223=>1098,224=>1070,225=>array(1040,2),227=>1062,228=>array(1044,2),230=>1060,231=>1043,232=>1061,233=>array(1048,8),241=>1071,242=>array(1056,4),246=>1046,247=>1042,248=>1068,249=>1067,250=>1047,251=>1064,252=>1069,253=>1065,254=>1063,255=>1066);
+$file = 'Arimo-Regular.z';
+$originalsize = 45516;
+$subsetted = true;
+?>
diff --git a/src/lib/fpdf-easytable/font/Arimo-Regular.z b/src/lib/fpdf-easytable/font/Arimo-Regular.z
new file mode 100644
index 0000000..937e26d
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/Arimo-Regular.z
Binary files differ
diff --git a/src/lib/fpdf-easytable/font/Lato-Regular.php b/src/lib/fpdf-easytable/font/Lato-Regular.php
new file mode 100644
index 0000000..dbbd929
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/Lato-Regular.php
@@ -0,0 +1,25 @@
+<?php
+$type = 'TrueType';
+$name = 'Lato-Regular';
+$desc = array('Ascent'=>805,'Descent'=>-195,'CapHeight'=>717,'Flags'=>32,'FontBBox'=>'[-547 -269 1343 1079]','ItalicAngle'=>0,'StemV'=>70,'MissingWidth'=>532);
+$up = -233;
+$ut = 45;
+$cw = array(
+	chr(0)=>532,chr(1)=>532,chr(2)=>532,chr(3)=>532,chr(4)=>532,chr(5)=>532,chr(6)=>532,chr(7)=>532,chr(8)=>532,chr(9)=>532,chr(10)=>532,chr(11)=>532,chr(12)=>532,chr(13)=>532,chr(14)=>532,chr(15)=>532,chr(16)=>532,chr(17)=>532,chr(18)=>532,chr(19)=>532,chr(20)=>532,chr(21)=>532,
+	chr(22)=>532,chr(23)=>532,chr(24)=>532,chr(25)=>532,chr(26)=>532,chr(27)=>532,chr(28)=>532,chr(29)=>532,chr(30)=>532,chr(31)=>532,' '=>256,'!'=>269,'"'=>371,'#'=>580,'$'=>580,'%'=>802,'&'=>712,'\''=>204,'('=>267,')'=>267,'*'=>425,'+'=>580,
+	','=>227,'-'=>372,'.'=>236,'/'=>452,'0'=>580,'1'=>580,'2'=>580,'3'=>580,'4'=>580,'5'=>580,'6'=>580,'7'=>580,'8'=>580,'9'=>580,':'=>250,';'=>262,'<'=>580,'='=>580,'>'=>580,'?'=>448,'@'=>837,'A'=>677,
+	'B'=>647,'C'=>668,'D'=>761,'E'=>578,'F'=>566,'G'=>731,'H'=>764,'I'=>280,'J'=>423,'K'=>663,'L'=>514,'M'=>929,'N'=>764,'O'=>801,'P'=>601,'Q'=>801,'R'=>627,'S'=>543,'T'=>591,'U'=>736,'V'=>677,'W'=>1036,
+	'X'=>649,'Y'=>624,'Z'=>602,'['=>306,'\\'=>452,']'=>306,'^'=>580,'_'=>459,'`'=>400,'a'=>497,'b'=>560,'c'=>478,'d'=>560,'e'=>528,'f'=>351,'g'=>520,'h'=>558,'i'=>240,'j'=>240,'k'=>508,'l'=>236,'m'=>823,
+	'n'=>558,'o'=>567,'p'=>561,'q'=>560,'r'=>364,'s'=>433,'t'=>359,'u'=>558,'v'=>516,'w'=>786,'x'=>498,'y'=>516,'z'=>452,'{'=>301,'|'=>251,'}'=>301,'~'=>580,chr(127)=>532,chr(128)=>580,chr(129)=>532,chr(130)=>216,chr(131)=>338,
+	chr(132)=>368,chr(133)=>750,chr(134)=>580,chr(135)=>580,chr(136)=>400,chr(137)=>1164,chr(138)=>543,chr(139)=>281,chr(140)=>1091,chr(141)=>532,chr(142)=>602,chr(143)=>532,chr(144)=>532,chr(145)=>214,chr(146)=>214,chr(147)=>366,chr(148)=>366,chr(149)=>580,chr(150)=>580,chr(151)=>794,chr(152)=>400,chr(153)=>751,
+	chr(154)=>433,chr(155)=>282,chr(156)=>873,chr(157)=>532,chr(158)=>452,chr(159)=>624,chr(160)=>256,chr(161)=>252,chr(162)=>580,chr(163)=>580,chr(164)=>580,chr(165)=>580,chr(166)=>254,chr(167)=>495,chr(168)=>400,chr(169)=>832,chr(170)=>368,chr(171)=>429,chr(172)=>580,chr(173)=>372,chr(174)=>832,chr(175)=>400,
+	chr(176)=>415,chr(177)=>580,chr(178)=>334,chr(179)=>334,chr(180)=>400,chr(181)=>650,chr(182)=>701,chr(183)=>260,chr(184)=>400,chr(185)=>334,chr(186)=>402,chr(187)=>430,chr(188)=>734,chr(189)=>726,chr(190)=>742,chr(191)=>440,chr(192)=>677,chr(193)=>677,chr(194)=>677,chr(195)=>677,chr(196)=>677,chr(197)=>677,
+	chr(198)=>931,chr(199)=>668,chr(200)=>578,chr(201)=>578,chr(202)=>578,chr(203)=>578,chr(204)=>280,chr(205)=>280,chr(206)=>280,chr(207)=>280,chr(208)=>769,chr(209)=>764,chr(210)=>801,chr(211)=>801,chr(212)=>801,chr(213)=>801,chr(214)=>801,chr(215)=>580,chr(216)=>801,chr(217)=>736,chr(218)=>736,chr(219)=>736,
+	chr(220)=>736,chr(221)=>624,chr(222)=>599,chr(223)=>580,chr(224)=>497,chr(225)=>497,chr(226)=>497,chr(227)=>497,chr(228)=>497,chr(229)=>497,chr(230)=>805,chr(231)=>478,chr(232)=>528,chr(233)=>528,chr(234)=>528,chr(235)=>528,chr(236)=>240,chr(237)=>240,chr(238)=>240,chr(239)=>240,chr(240)=>566,chr(241)=>558,
+	chr(242)=>567,chr(243)=>567,chr(244)=>567,chr(245)=>567,chr(246)=>567,chr(247)=>580,chr(248)=>567,chr(249)=>558,chr(250)=>558,chr(251)=>558,chr(252)=>558,chr(253)=>516,chr(254)=>561,chr(255)=>516);
+$enc = 'cp1252';
+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));
+$file = 'Lato-Regular.z';
+$originalsize = 43428;
+$subsetted = true;
+?>
diff --git a/src/lib/fpdf-easytable/font/Lato-Regular.z b/src/lib/fpdf-easytable/font/Lato-Regular.z
new file mode 100644
index 0000000..e6c7731
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/Lato-Regular.z
Binary files differ
diff --git a/src/lib/fpdf-easytable/font/courier.php b/src/lib/fpdf-easytable/font/courier.php
new file mode 100755
index 0000000..67dbeda
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/courier.php
@@ -0,0 +1,10 @@
+<?php

+$type = 'Core';

+$name = 'Courier';

+$up = -100;

+$ut = 50;

+for($i=0;$i<=255;$i++)

+	$cw[chr($i)] = 600;

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf-easytable/font/courierb.php b/src/lib/fpdf-easytable/font/courierb.php
new file mode 100755
index 0000000..62550a4
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/courierb.php
@@ -0,0 +1,10 @@
+<?php

+$type = 'Core';

+$name = 'Courier-Bold';

+$up = -100;

+$ut = 50;

+for($i=0;$i<=255;$i++)

+	$cw[chr($i)] = 600;

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf-easytable/font/courierbi.php b/src/lib/fpdf-easytable/font/courierbi.php
new file mode 100755
index 0000000..6a3ecc6
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/courierbi.php
@@ -0,0 +1,10 @@
+<?php

+$type = 'Core';

+$name = 'Courier-BoldOblique';

+$up = -100;

+$ut = 50;

+for($i=0;$i<=255;$i++)

+	$cw[chr($i)] = 600;

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf-easytable/font/courieri.php b/src/lib/fpdf-easytable/font/courieri.php
new file mode 100755
index 0000000..b88e098
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/courieri.php
@@ -0,0 +1,10 @@
+<?php

+$type = 'Core';

+$name = 'Courier-Oblique';

+$up = -100;

+$ut = 50;

+for($i=0;$i<=255;$i++)

+	$cw[chr($i)] = 600;

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf-easytable/font/helvetica.php b/src/lib/fpdf-easytable/font/helvetica.php
new file mode 100755
index 0000000..2be3eca
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/helvetica.php
@@ -0,0 +1,21 @@
+<?php

+$type = 'Core';

+$name = 'Helvetica';

+$up = -100;

+$ut = 50;

+$cw = array(

+	chr(0)=>278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278,

+	chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>278,'"'=>355,'#'=>556,'$'=>556,'%'=>889,'&'=>667,'\''=>191,'('=>333,')'=>333,'*'=>389,'+'=>584,

+	','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>278,';'=>278,'<'=>584,'='=>584,'>'=>584,'?'=>556,'@'=>1015,'A'=>667,

+	'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>500,'K'=>667,'L'=>556,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944,

+	'X'=>667,'Y'=>667,'Z'=>611,'['=>278,'\\'=>278,']'=>278,'^'=>469,'_'=>556,'`'=>333,'a'=>556,'b'=>556,'c'=>500,'d'=>556,'e'=>556,'f'=>278,'g'=>556,'h'=>556,'i'=>222,'j'=>222,'k'=>500,'l'=>222,'m'=>833,

+	'n'=>556,'o'=>556,'p'=>556,'q'=>556,'r'=>333,'s'=>500,'t'=>278,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>500,'{'=>334,'|'=>260,'}'=>334,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>222,chr(131)=>556,

+	chr(132)=>333,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>222,chr(146)=>222,chr(147)=>333,chr(148)=>333,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000,

+	chr(154)=>500,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>260,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333,

+	chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>556,chr(182)=>537,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667,

+	chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722,

+	chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>500,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>556,chr(241)=>556,

+	chr(242)=>556,chr(243)=>556,chr(244)=>556,chr(245)=>556,chr(246)=>556,chr(247)=>584,chr(248)=>611,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500);

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf-easytable/font/helveticab.php b/src/lib/fpdf-easytable/font/helveticab.php
new file mode 100755
index 0000000..c88394c
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/helveticab.php
@@ -0,0 +1,21 @@
+<?php

+$type = 'Core';

+$name = 'Helvetica-Bold';

+$up = -100;

+$ut = 50;

+$cw = array(

+	chr(0)=>278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278,

+	chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>333,'"'=>474,'#'=>556,'$'=>556,'%'=>889,'&'=>722,'\''=>238,'('=>333,')'=>333,'*'=>389,'+'=>584,

+	','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>333,';'=>333,'<'=>584,'='=>584,'>'=>584,'?'=>611,'@'=>975,'A'=>722,

+	'B'=>722,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>556,'K'=>722,'L'=>611,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944,

+	'X'=>667,'Y'=>667,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>584,'_'=>556,'`'=>333,'a'=>556,'b'=>611,'c'=>556,'d'=>611,'e'=>556,'f'=>333,'g'=>611,'h'=>611,'i'=>278,'j'=>278,'k'=>556,'l'=>278,'m'=>889,

+	'n'=>611,'o'=>611,'p'=>611,'q'=>611,'r'=>389,'s'=>556,'t'=>333,'u'=>611,'v'=>556,'w'=>778,'x'=>556,'y'=>556,'z'=>500,'{'=>389,'|'=>280,'}'=>389,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>278,chr(131)=>556,

+	chr(132)=>500,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>278,chr(146)=>278,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000,

+	chr(154)=>556,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>280,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333,

+	chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>611,chr(182)=>556,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722,

+	chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722,

+	chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>556,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>611,chr(241)=>611,

+	chr(242)=>611,chr(243)=>611,chr(244)=>611,chr(245)=>611,chr(246)=>611,chr(247)=>584,chr(248)=>611,chr(249)=>611,chr(250)=>611,chr(251)=>611,chr(252)=>611,chr(253)=>556,chr(254)=>611,chr(255)=>556);

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf-easytable/font/helveticabi.php b/src/lib/fpdf-easytable/font/helveticabi.php
new file mode 100755
index 0000000..bcea807
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/helveticabi.php
@@ -0,0 +1,21 @@
+<?php

+$type = 'Core';

+$name = 'Helvetica-BoldOblique';

+$up = -100;

+$ut = 50;

+$cw = array(

+	chr(0)=>278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278,

+	chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>333,'"'=>474,'#'=>556,'$'=>556,'%'=>889,'&'=>722,'\''=>238,'('=>333,')'=>333,'*'=>389,'+'=>584,

+	','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>333,';'=>333,'<'=>584,'='=>584,'>'=>584,'?'=>611,'@'=>975,'A'=>722,

+	'B'=>722,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>556,'K'=>722,'L'=>611,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944,

+	'X'=>667,'Y'=>667,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>584,'_'=>556,'`'=>333,'a'=>556,'b'=>611,'c'=>556,'d'=>611,'e'=>556,'f'=>333,'g'=>611,'h'=>611,'i'=>278,'j'=>278,'k'=>556,'l'=>278,'m'=>889,

+	'n'=>611,'o'=>611,'p'=>611,'q'=>611,'r'=>389,'s'=>556,'t'=>333,'u'=>611,'v'=>556,'w'=>778,'x'=>556,'y'=>556,'z'=>500,'{'=>389,'|'=>280,'}'=>389,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>278,chr(131)=>556,

+	chr(132)=>500,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>278,chr(146)=>278,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000,

+	chr(154)=>556,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>280,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333,

+	chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>611,chr(182)=>556,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722,

+	chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722,

+	chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>556,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>611,chr(241)=>611,

+	chr(242)=>611,chr(243)=>611,chr(244)=>611,chr(245)=>611,chr(246)=>611,chr(247)=>584,chr(248)=>611,chr(249)=>611,chr(250)=>611,chr(251)=>611,chr(252)=>611,chr(253)=>556,chr(254)=>611,chr(255)=>556);

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf-easytable/font/helveticai.php b/src/lib/fpdf-easytable/font/helveticai.php
new file mode 100755
index 0000000..a328b04
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/helveticai.php
@@ -0,0 +1,21 @@
+<?php

+$type = 'Core';

+$name = 'Helvetica-Oblique';

+$up = -100;

+$ut = 50;

+$cw = array(

+	chr(0)=>278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278,

+	chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>278,'"'=>355,'#'=>556,'$'=>556,'%'=>889,'&'=>667,'\''=>191,'('=>333,')'=>333,'*'=>389,'+'=>584,

+	','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>278,';'=>278,'<'=>584,'='=>584,'>'=>584,'?'=>556,'@'=>1015,'A'=>667,

+	'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>500,'K'=>667,'L'=>556,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944,

+	'X'=>667,'Y'=>667,'Z'=>611,'['=>278,'\\'=>278,']'=>278,'^'=>469,'_'=>556,'`'=>333,'a'=>556,'b'=>556,'c'=>500,'d'=>556,'e'=>556,'f'=>278,'g'=>556,'h'=>556,'i'=>222,'j'=>222,'k'=>500,'l'=>222,'m'=>833,

+	'n'=>556,'o'=>556,'p'=>556,'q'=>556,'r'=>333,'s'=>500,'t'=>278,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>500,'{'=>334,'|'=>260,'}'=>334,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>222,chr(131)=>556,

+	chr(132)=>333,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>222,chr(146)=>222,chr(147)=>333,chr(148)=>333,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000,

+	chr(154)=>500,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>260,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333,

+	chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>556,chr(182)=>537,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667,

+	chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722,

+	chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>500,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>556,chr(241)=>556,

+	chr(242)=>556,chr(243)=>556,chr(244)=>556,chr(245)=>556,chr(246)=>556,chr(247)=>584,chr(248)=>611,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500);

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf-easytable/font/symbol.php b/src/lib/fpdf-easytable/font/symbol.php
new file mode 100755
index 0000000..5b9147b
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/symbol.php
@@ -0,0 +1,20 @@
+<?php

+$type = 'Core';

+$name = 'Symbol';

+$up = -100;

+$ut = 50;

+$cw = array(

+	chr(0)=>250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250,

+	chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>713,'#'=>500,'$'=>549,'%'=>833,'&'=>778,'\''=>439,'('=>333,')'=>333,'*'=>500,'+'=>549,

+	','=>250,'-'=>549,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>278,';'=>278,'<'=>549,'='=>549,'>'=>549,'?'=>444,'@'=>549,'A'=>722,

+	'B'=>667,'C'=>722,'D'=>612,'E'=>611,'F'=>763,'G'=>603,'H'=>722,'I'=>333,'J'=>631,'K'=>722,'L'=>686,'M'=>889,'N'=>722,'O'=>722,'P'=>768,'Q'=>741,'R'=>556,'S'=>592,'T'=>611,'U'=>690,'V'=>439,'W'=>768,

+	'X'=>645,'Y'=>795,'Z'=>611,'['=>333,'\\'=>863,']'=>333,'^'=>658,'_'=>500,'`'=>500,'a'=>631,'b'=>549,'c'=>549,'d'=>494,'e'=>439,'f'=>521,'g'=>411,'h'=>603,'i'=>329,'j'=>603,'k'=>549,'l'=>549,'m'=>576,

+	'n'=>521,'o'=>549,'p'=>549,'q'=>521,'r'=>549,'s'=>603,'t'=>439,'u'=>576,'v'=>713,'w'=>686,'x'=>493,'y'=>686,'z'=>494,'{'=>480,'|'=>200,'}'=>480,'~'=>549,chr(127)=>0,chr(128)=>0,chr(129)=>0,chr(130)=>0,chr(131)=>0,

+	chr(132)=>0,chr(133)=>0,chr(134)=>0,chr(135)=>0,chr(136)=>0,chr(137)=>0,chr(138)=>0,chr(139)=>0,chr(140)=>0,chr(141)=>0,chr(142)=>0,chr(143)=>0,chr(144)=>0,chr(145)=>0,chr(146)=>0,chr(147)=>0,chr(148)=>0,chr(149)=>0,chr(150)=>0,chr(151)=>0,chr(152)=>0,chr(153)=>0,

+	chr(154)=>0,chr(155)=>0,chr(156)=>0,chr(157)=>0,chr(158)=>0,chr(159)=>0,chr(160)=>750,chr(161)=>620,chr(162)=>247,chr(163)=>549,chr(164)=>167,chr(165)=>713,chr(166)=>500,chr(167)=>753,chr(168)=>753,chr(169)=>753,chr(170)=>753,chr(171)=>1042,chr(172)=>987,chr(173)=>603,chr(174)=>987,chr(175)=>603,

+	chr(176)=>400,chr(177)=>549,chr(178)=>411,chr(179)=>549,chr(180)=>549,chr(181)=>713,chr(182)=>494,chr(183)=>460,chr(184)=>549,chr(185)=>549,chr(186)=>549,chr(187)=>549,chr(188)=>1000,chr(189)=>603,chr(190)=>1000,chr(191)=>658,chr(192)=>823,chr(193)=>686,chr(194)=>795,chr(195)=>987,chr(196)=>768,chr(197)=>768,

+	chr(198)=>823,chr(199)=>768,chr(200)=>768,chr(201)=>713,chr(202)=>713,chr(203)=>713,chr(204)=>713,chr(205)=>713,chr(206)=>713,chr(207)=>713,chr(208)=>768,chr(209)=>713,chr(210)=>790,chr(211)=>790,chr(212)=>890,chr(213)=>823,chr(214)=>549,chr(215)=>250,chr(216)=>713,chr(217)=>603,chr(218)=>603,chr(219)=>1042,

+	chr(220)=>987,chr(221)=>603,chr(222)=>987,chr(223)=>603,chr(224)=>494,chr(225)=>329,chr(226)=>790,chr(227)=>790,chr(228)=>786,chr(229)=>713,chr(230)=>384,chr(231)=>384,chr(232)=>384,chr(233)=>384,chr(234)=>384,chr(235)=>384,chr(236)=>494,chr(237)=>494,chr(238)=>494,chr(239)=>494,chr(240)=>0,chr(241)=>329,

+	chr(242)=>274,chr(243)=>686,chr(244)=>686,chr(245)=>686,chr(246)=>384,chr(247)=>384,chr(248)=>384,chr(249)=>384,chr(250)=>384,chr(251)=>384,chr(252)=>494,chr(253)=>494,chr(254)=>494,chr(255)=>0);

+$uv = array(32=>160,33=>33,34=>8704,35=>35,36=>8707,37=>array(37,2),39=>8715,40=>array(40,2),42=>8727,43=>array(43,2),45=>8722,46=>array(46,18),64=>8773,65=>array(913,2),67=>935,68=>array(916,2),70=>934,71=>915,72=>919,73=>921,74=>977,75=>array(922,4),79=>array(927,2),81=>920,82=>929,83=>array(931,3),86=>962,87=>937,88=>926,89=>936,90=>918,91=>91,92=>8756,93=>93,94=>8869,95=>95,96=>63717,97=>array(945,2),99=>967,100=>array(948,2),102=>966,103=>947,104=>951,105=>953,106=>981,107=>array(954,4),111=>array(959,2),113=>952,114=>961,115=>array(963,3),118=>982,119=>969,120=>958,121=>968,122=>950,123=>array(123,3),126=>8764,160=>8364,161=>978,162=>8242,163=>8804,164=>8725,165=>8734,166=>402,167=>9827,168=>9830,169=>9829,170=>9824,171=>8596,172=>array(8592,4),176=>array(176,2),178=>8243,179=>8805,180=>215,181=>8733,182=>8706,183=>8226,184=>247,185=>array(8800,2),187=>8776,188=>8230,189=>array(63718,2),191=>8629,192=>8501,193=>8465,194=>8476,195=>8472,196=>8855,197=>8853,198=>8709,199=>array(8745,2),201=>8835,202=>8839,203=>8836,204=>8834,205=>8838,206=>array(8712,2),208=>8736,209=>8711,210=>63194,211=>63193,212=>63195,213=>8719,214=>8730,215=>8901,216=>172,217=>array(8743,2),219=>8660,220=>array(8656,4),224=>9674,225=>9001,226=>array(63720,3),229=>8721,230=>array(63723,10),241=>9002,242=>8747,243=>8992,244=>63733,245=>8993,246=>array(63734,9));

+?>

diff --git a/src/lib/fpdf-easytable/font/times.php b/src/lib/fpdf-easytable/font/times.php
new file mode 100755
index 0000000..f78850f
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/times.php
@@ -0,0 +1,21 @@
+<?php

+$type = 'Core';

+$name = 'Times-Roman';

+$up = -100;

+$ut = 50;

+$cw = array(

+	chr(0)=>250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250,

+	chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>408,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>180,'('=>333,')'=>333,'*'=>500,'+'=>564,

+	','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>278,';'=>278,'<'=>564,'='=>564,'>'=>564,'?'=>444,'@'=>921,'A'=>722,

+	'B'=>667,'C'=>667,'D'=>722,'E'=>611,'F'=>556,'G'=>722,'H'=>722,'I'=>333,'J'=>389,'K'=>722,'L'=>611,'M'=>889,'N'=>722,'O'=>722,'P'=>556,'Q'=>722,'R'=>667,'S'=>556,'T'=>611,'U'=>722,'V'=>722,'W'=>944,

+	'X'=>722,'Y'=>722,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>469,'_'=>500,'`'=>333,'a'=>444,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>333,'g'=>500,'h'=>500,'i'=>278,'j'=>278,'k'=>500,'l'=>278,'m'=>778,

+	'n'=>500,'o'=>500,'p'=>500,'q'=>500,'r'=>333,'s'=>389,'t'=>278,'u'=>500,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>444,'{'=>480,'|'=>200,'}'=>480,'~'=>541,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500,

+	chr(132)=>444,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>889,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>444,chr(148)=>444,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>980,

+	chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>444,chr(159)=>722,chr(160)=>250,chr(161)=>333,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>200,chr(167)=>500,chr(168)=>333,chr(169)=>760,chr(170)=>276,chr(171)=>500,chr(172)=>564,chr(173)=>333,chr(174)=>760,chr(175)=>333,

+	chr(176)=>400,chr(177)=>564,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>500,chr(182)=>453,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>310,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>444,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722,

+	chr(198)=>889,chr(199)=>667,chr(200)=>611,chr(201)=>611,chr(202)=>611,chr(203)=>611,chr(204)=>333,chr(205)=>333,chr(206)=>333,chr(207)=>333,chr(208)=>722,chr(209)=>722,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>564,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722,

+	chr(220)=>722,chr(221)=>722,chr(222)=>556,chr(223)=>500,chr(224)=>444,chr(225)=>444,chr(226)=>444,chr(227)=>444,chr(228)=>444,chr(229)=>444,chr(230)=>667,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>500,

+	chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>564,chr(248)=>500,chr(249)=>500,chr(250)=>500,chr(251)=>500,chr(252)=>500,chr(253)=>500,chr(254)=>500,chr(255)=>500);

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf-easytable/font/timesb.php b/src/lib/fpdf-easytable/font/timesb.php
new file mode 100755
index 0000000..0516750
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/timesb.php
@@ -0,0 +1,21 @@
+<?php

+$type = 'Core';

+$name = 'Times-Bold';

+$up = -100;

+$ut = 50;

+$cw = array(

+	chr(0)=>250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250,

+	chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>555,'#'=>500,'$'=>500,'%'=>1000,'&'=>833,'\''=>278,'('=>333,')'=>333,'*'=>500,'+'=>570,

+	','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>570,'='=>570,'>'=>570,'?'=>500,'@'=>930,'A'=>722,

+	'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>778,'I'=>389,'J'=>500,'K'=>778,'L'=>667,'M'=>944,'N'=>722,'O'=>778,'P'=>611,'Q'=>778,'R'=>722,'S'=>556,'T'=>667,'U'=>722,'V'=>722,'W'=>1000,

+	'X'=>722,'Y'=>722,'Z'=>667,'['=>333,'\\'=>278,']'=>333,'^'=>581,'_'=>500,'`'=>333,'a'=>500,'b'=>556,'c'=>444,'d'=>556,'e'=>444,'f'=>333,'g'=>500,'h'=>556,'i'=>278,'j'=>333,'k'=>556,'l'=>278,'m'=>833,

+	'n'=>556,'o'=>500,'p'=>556,'q'=>556,'r'=>444,'s'=>389,'t'=>333,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>444,'{'=>394,'|'=>220,'}'=>394,'~'=>520,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500,

+	chr(132)=>500,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>667,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>1000,

+	chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>444,chr(159)=>722,chr(160)=>250,chr(161)=>333,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>220,chr(167)=>500,chr(168)=>333,chr(169)=>747,chr(170)=>300,chr(171)=>500,chr(172)=>570,chr(173)=>333,chr(174)=>747,chr(175)=>333,

+	chr(176)=>400,chr(177)=>570,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>556,chr(182)=>540,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>330,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722,

+	chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>389,chr(205)=>389,chr(206)=>389,chr(207)=>389,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>570,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722,

+	chr(220)=>722,chr(221)=>722,chr(222)=>611,chr(223)=>556,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>722,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>556,

+	chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>570,chr(248)=>500,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500);

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf-easytable/font/timesbi.php b/src/lib/fpdf-easytable/font/timesbi.php
new file mode 100755
index 0000000..32fe25e
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/timesbi.php
@@ -0,0 +1,21 @@
+<?php

+$type = 'Core';

+$name = 'Times-BoldItalic';

+$up = -100;

+$ut = 50;

+$cw = array(

+	chr(0)=>250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250,

+	chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>389,'"'=>555,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>278,'('=>333,')'=>333,'*'=>500,'+'=>570,

+	','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>570,'='=>570,'>'=>570,'?'=>500,'@'=>832,'A'=>667,

+	'B'=>667,'C'=>667,'D'=>722,'E'=>667,'F'=>667,'G'=>722,'H'=>778,'I'=>389,'J'=>500,'K'=>667,'L'=>611,'M'=>889,'N'=>722,'O'=>722,'P'=>611,'Q'=>722,'R'=>667,'S'=>556,'T'=>611,'U'=>722,'V'=>667,'W'=>889,

+	'X'=>667,'Y'=>611,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>570,'_'=>500,'`'=>333,'a'=>500,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>333,'g'=>500,'h'=>556,'i'=>278,'j'=>278,'k'=>500,'l'=>278,'m'=>778,

+	'n'=>556,'o'=>500,'p'=>500,'q'=>500,'r'=>389,'s'=>389,'t'=>278,'u'=>556,'v'=>444,'w'=>667,'x'=>500,'y'=>444,'z'=>389,'{'=>348,'|'=>220,'}'=>348,'~'=>570,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500,

+	chr(132)=>500,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>944,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>1000,

+	chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>389,chr(159)=>611,chr(160)=>250,chr(161)=>389,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>220,chr(167)=>500,chr(168)=>333,chr(169)=>747,chr(170)=>266,chr(171)=>500,chr(172)=>606,chr(173)=>333,chr(174)=>747,chr(175)=>333,

+	chr(176)=>400,chr(177)=>570,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>576,chr(182)=>500,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>300,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667,

+	chr(198)=>944,chr(199)=>667,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>389,chr(205)=>389,chr(206)=>389,chr(207)=>389,chr(208)=>722,chr(209)=>722,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>570,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722,

+	chr(220)=>722,chr(221)=>611,chr(222)=>611,chr(223)=>500,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>722,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>556,

+	chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>570,chr(248)=>500,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>444,chr(254)=>500,chr(255)=>444);

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf-easytable/font/timesi.php b/src/lib/fpdf-easytable/font/timesi.php
new file mode 100755
index 0000000..b0e5a62
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/timesi.php
@@ -0,0 +1,21 @@
+<?php

+$type = 'Core';

+$name = 'Times-Italic';

+$up = -100;

+$ut = 50;

+$cw = array(

+	chr(0)=>250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250,

+	chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>420,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>214,'('=>333,')'=>333,'*'=>500,'+'=>675,

+	','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>675,'='=>675,'>'=>675,'?'=>500,'@'=>920,'A'=>611,

+	'B'=>611,'C'=>667,'D'=>722,'E'=>611,'F'=>611,'G'=>722,'H'=>722,'I'=>333,'J'=>444,'K'=>667,'L'=>556,'M'=>833,'N'=>667,'O'=>722,'P'=>611,'Q'=>722,'R'=>611,'S'=>500,'T'=>556,'U'=>722,'V'=>611,'W'=>833,

+	'X'=>611,'Y'=>556,'Z'=>556,'['=>389,'\\'=>278,']'=>389,'^'=>422,'_'=>500,'`'=>333,'a'=>500,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>278,'g'=>500,'h'=>500,'i'=>278,'j'=>278,'k'=>444,'l'=>278,'m'=>722,

+	'n'=>500,'o'=>500,'p'=>500,'q'=>500,'r'=>389,'s'=>389,'t'=>278,'u'=>500,'v'=>444,'w'=>667,'x'=>444,'y'=>444,'z'=>389,'{'=>400,'|'=>275,'}'=>400,'~'=>541,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500,

+	chr(132)=>556,chr(133)=>889,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>500,chr(139)=>333,chr(140)=>944,chr(141)=>350,chr(142)=>556,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>556,chr(148)=>556,chr(149)=>350,chr(150)=>500,chr(151)=>889,chr(152)=>333,chr(153)=>980,

+	chr(154)=>389,chr(155)=>333,chr(156)=>667,chr(157)=>350,chr(158)=>389,chr(159)=>556,chr(160)=>250,chr(161)=>389,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>275,chr(167)=>500,chr(168)=>333,chr(169)=>760,chr(170)=>276,chr(171)=>500,chr(172)=>675,chr(173)=>333,chr(174)=>760,chr(175)=>333,

+	chr(176)=>400,chr(177)=>675,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>500,chr(182)=>523,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>310,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>611,chr(193)=>611,chr(194)=>611,chr(195)=>611,chr(196)=>611,chr(197)=>611,

+	chr(198)=>889,chr(199)=>667,chr(200)=>611,chr(201)=>611,chr(202)=>611,chr(203)=>611,chr(204)=>333,chr(205)=>333,chr(206)=>333,chr(207)=>333,chr(208)=>722,chr(209)=>667,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>675,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722,

+	chr(220)=>722,chr(221)=>556,chr(222)=>611,chr(223)=>500,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>667,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>500,

+	chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>675,chr(248)=>500,chr(249)=>500,chr(250)=>500,chr(251)=>500,chr(252)=>500,chr(253)=>444,chr(254)=>500,chr(255)=>444);

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf-easytable/font/zapfdingbats.php b/src/lib/fpdf-easytable/font/zapfdingbats.php
new file mode 100755
index 0000000..b9d0309
--- /dev/null
+++ b/src/lib/fpdf-easytable/font/zapfdingbats.php
@@ -0,0 +1,20 @@
+<?php

+$type = 'Core';

+$name = 'ZapfDingbats';

+$up = -100;

+$ut = 50;

+$cw = array(

+	chr(0)=>0,chr(1)=>0,chr(2)=>0,chr(3)=>0,chr(4)=>0,chr(5)=>0,chr(6)=>0,chr(7)=>0,chr(8)=>0,chr(9)=>0,chr(10)=>0,chr(11)=>0,chr(12)=>0,chr(13)=>0,chr(14)=>0,chr(15)=>0,chr(16)=>0,chr(17)=>0,chr(18)=>0,chr(19)=>0,chr(20)=>0,chr(21)=>0,

+	chr(22)=>0,chr(23)=>0,chr(24)=>0,chr(25)=>0,chr(26)=>0,chr(27)=>0,chr(28)=>0,chr(29)=>0,chr(30)=>0,chr(31)=>0,' '=>278,'!'=>974,'"'=>961,'#'=>974,'$'=>980,'%'=>719,'&'=>789,'\''=>790,'('=>791,')'=>690,'*'=>960,'+'=>939,

+	','=>549,'-'=>855,'.'=>911,'/'=>933,'0'=>911,'1'=>945,'2'=>974,'3'=>755,'4'=>846,'5'=>762,'6'=>761,'7'=>571,'8'=>677,'9'=>763,':'=>760,';'=>759,'<'=>754,'='=>494,'>'=>552,'?'=>537,'@'=>577,'A'=>692,

+	'B'=>786,'C'=>788,'D'=>788,'E'=>790,'F'=>793,'G'=>794,'H'=>816,'I'=>823,'J'=>789,'K'=>841,'L'=>823,'M'=>833,'N'=>816,'O'=>831,'P'=>923,'Q'=>744,'R'=>723,'S'=>749,'T'=>790,'U'=>792,'V'=>695,'W'=>776,

+	'X'=>768,'Y'=>792,'Z'=>759,'['=>707,'\\'=>708,']'=>682,'^'=>701,'_'=>826,'`'=>815,'a'=>789,'b'=>789,'c'=>707,'d'=>687,'e'=>696,'f'=>689,'g'=>786,'h'=>787,'i'=>713,'j'=>791,'k'=>785,'l'=>791,'m'=>873,

+	'n'=>761,'o'=>762,'p'=>762,'q'=>759,'r'=>759,'s'=>892,'t'=>892,'u'=>788,'v'=>784,'w'=>438,'x'=>138,'y'=>277,'z'=>415,'{'=>392,'|'=>392,'}'=>668,'~'=>668,chr(127)=>0,chr(128)=>390,chr(129)=>390,chr(130)=>317,chr(131)=>317,

+	chr(132)=>276,chr(133)=>276,chr(134)=>509,chr(135)=>509,chr(136)=>410,chr(137)=>410,chr(138)=>234,chr(139)=>234,chr(140)=>334,chr(141)=>334,chr(142)=>0,chr(143)=>0,chr(144)=>0,chr(145)=>0,chr(146)=>0,chr(147)=>0,chr(148)=>0,chr(149)=>0,chr(150)=>0,chr(151)=>0,chr(152)=>0,chr(153)=>0,

+	chr(154)=>0,chr(155)=>0,chr(156)=>0,chr(157)=>0,chr(158)=>0,chr(159)=>0,chr(160)=>0,chr(161)=>732,chr(162)=>544,chr(163)=>544,chr(164)=>910,chr(165)=>667,chr(166)=>760,chr(167)=>760,chr(168)=>776,chr(169)=>595,chr(170)=>694,chr(171)=>626,chr(172)=>788,chr(173)=>788,chr(174)=>788,chr(175)=>788,

+	chr(176)=>788,chr(177)=>788,chr(178)=>788,chr(179)=>788,chr(180)=>788,chr(181)=>788,chr(182)=>788,chr(183)=>788,chr(184)=>788,chr(185)=>788,chr(186)=>788,chr(187)=>788,chr(188)=>788,chr(189)=>788,chr(190)=>788,chr(191)=>788,chr(192)=>788,chr(193)=>788,chr(194)=>788,chr(195)=>788,chr(196)=>788,chr(197)=>788,

+	chr(198)=>788,chr(199)=>788,chr(200)=>788,chr(201)=>788,chr(202)=>788,chr(203)=>788,chr(204)=>788,chr(205)=>788,chr(206)=>788,chr(207)=>788,chr(208)=>788,chr(209)=>788,chr(210)=>788,chr(211)=>788,chr(212)=>894,chr(213)=>838,chr(214)=>1016,chr(215)=>458,chr(216)=>748,chr(217)=>924,chr(218)=>748,chr(219)=>918,

+	chr(220)=>927,chr(221)=>928,chr(222)=>928,chr(223)=>834,chr(224)=>873,chr(225)=>828,chr(226)=>924,chr(227)=>924,chr(228)=>917,chr(229)=>930,chr(230)=>931,chr(231)=>463,chr(232)=>883,chr(233)=>836,chr(234)=>836,chr(235)=>867,chr(236)=>867,chr(237)=>696,chr(238)=>696,chr(239)=>874,chr(240)=>0,chr(241)=>874,

+	chr(242)=>760,chr(243)=>946,chr(244)=>771,chr(245)=>865,chr(246)=>771,chr(247)=>888,chr(248)=>967,chr(249)=>888,chr(250)=>831,chr(251)=>873,chr(252)=>927,chr(253)=>970,chr(254)=>918,chr(255)=>0);

+$uv = array(32=>32,33=>array(9985,4),37=>9742,38=>array(9990,4),42=>9755,43=>9758,44=>array(9996,28),72=>9733,73=>array(10025,35),108=>9679,109=>10061,110=>9632,111=>array(10063,4),115=>9650,116=>9660,117=>9670,118=>10070,119=>9687,120=>array(10072,7),128=>array(10088,14),161=>array(10081,7),168=>9827,169=>9830,170=>9829,171=>9824,172=>array(9312,10),182=>array(10102,31),213=>8594,214=>array(8596,2),216=>array(10136,24),241=>array(10161,14));

+?>

diff --git a/src/lib/fpdf-easytable/formatedstring.php b/src/lib/fpdf-easytable/formatedstring.php
new file mode 100644
index 0000000..682842d
--- /dev/null
+++ b/src/lib/fpdf-easytable/formatedstring.php
@@ -0,0 +1,209 @@
+<?php
+ /**********************************************************************
+ * formateString                                                       *
+ *                                                                     *
+ * Version: 1.2                                                        *
+ * Date:    04-10-2017                                                 *
+ * Author:  Dan Machado                                                *
+ * Use within exfpdf class                                             *
+ **********************************************************************/
+ class formatedString{
+    public $parced_str;
+    public $style_map;
+    public $positions;
+    private $np;
+    public $iterator;
+    public $width;
+    public $lines;
+    public $linesmap;
+    public $used_fonts;
+
+    private function get_style($str){
+       $style=array('font-family'=>false, 'font-weight'=>false, 'font-style'=>false,
+       'font-size'=>false, 'font-color'=>false, 'href'=>'');
+       $tmp=explode(';', trim($str, '",\', '));
+       foreach($tmp as $x){
+          if($x && strpos($x,':')>0){
+             $r=explode(':',$x);
+             $r[0]=trim($r[0]);
+             $r[1]=trim($r[1]);
+             if(isset($style[$r[0]]) || $r[0]=='style'){
+                if($r[0]=='style' || $r[0]=='font-style'){
+                   $r[1]=strtoupper($r[1]);
+                   if(strpos($r[1], 'B')!==false){
+                      $style['font-weight']='B';
+                  }
+                  if(strpos($r[1], 'I')!==false){
+                     $style['font-style']='I';
+                  }
+                  if(strpos($r[1], 'U')!==false){
+                     $style['font-style'].='U';
+                  }
+               }
+               elseif($r[1]){
+                  if($r[0]=='href'){
+                     $style[$r[0]]=implode(':', array_slice($r,1));
+                  }
+                  else{
+                     $style[$r[0]]=$r[1];
+                  }
+               }
+            }
+         }
+      }
+      return $style;
+   }
+    
+
+   private function style_merge($style1, $style2){
+      $result=$style1;
+      foreach($style2 as $k=>$v){
+         if($v){
+            $result[$k]=$v;
+         }
+      }
+      return $result;
+   }
+
+   private function style_parcer($text, &$font_data){
+      $str=trim(strtr($text, array("\r"=>'', "\t"=>'')));
+      $rep=array('[bB]{1}'=>'B', '[iI]{1}'=>'I', '[iI]{1}[ ]*[bB]{1}'=>'BI', '[bB]{1}[ ]*[iI]{1}'=>'BI' );
+      foreach($rep as $a=>$v){
+         $str=preg_replace('/<[ ]*'.$a.'[ ]*>/', "<$v>", $str);
+         $str=preg_replace('/<[ ]*\/+[ ]*'.$a.'[ ]*>/', "</$v>", $str);
+      }
+      $str=preg_replace('/<BI>/', '<s "font-weight:B;font-style:I">', $str);
+      $str=preg_replace('/<\/BI>/', "</s>", $str);
+      $str=preg_replace('/<B>/', '<s "font-weight:B;">', $str);
+      $str=preg_replace('/<\/B>/', "</s>", $str);
+      $str=preg_replace('/<I>/', '<s "font-style:I;">', $str);
+      $str=preg_replace('/<\/I>/', "</s>", $str);
+      $open=array();
+      $total=array();
+      $lt="<s";
+      $rt="</s>";
+      $j=strpos($str, $lt, 0);
+      while($j!==false){
+            if($j>0 && ord($str[$j-1])==92){
+            $j=strpos($str, $lt, $j+1);
+            continue;
+         }
+         $k=strpos($str, '>',$j+1);
+         $open[$j]=substr($str, $j+2, $k-($j+2));
+         $total[]=$j;
+         $j=strpos($str, $lt, $j+1);
+      }
+      $j=strpos($str, $rt, 0);
+      while($j!==false){
+         $total[]=$j;
+         $j=strpos($str, $rt, $j+1);
+      }
+      sort($total);
+      
+      $cs='';
+      foreach($font_data as $k=>$v){
+         $cs.=$k . ':'. $v . '; ';
+      }
+      $cs=$this->get_style($cs);
+      $tmp=array($cs);
+      $blocks=array();
+      $blockstyle=array();
+      $n=count($total);
+      $k=0;
+      for($i=0; $i<$n; $i++){
+         $blocks[]=substr($str, $k, $total[$i]-$k);
+         $blockstyle[]=$cs;
+         if(isset($open[$total[$i]])){
+            $cs=$this->style_merge($cs, $this->get_style($open[$total[$i]]));
+            array_push($tmp, $cs);
+            $k=strpos($str, '>',$total[$i]+1)+1;
+         }
+         else{
+            $k=$total[$i]+4;
+            array_pop($tmp);
+            $l=count($tmp)-1;
+            $cs=$tmp[$l];
+         }
+      }
+      if($k<strlen($str)){
+         $blocks[]=substr($str, $k);
+         $blockstyle[]=$cs;
+      }
+      $n=count($blocks);
+      for($i=0; $i<$n; $i++){
+         $this->parced_str.=strtr($blocks[$i], array('\<s'=>'<s'));
+         if(strlen($blocks[$i])>0){
+            $blockstyle[$i]['style']=$blockstyle[$i]['font-weight'] . $blockstyle[$i]['font-style'];
+            unset($blockstyle[$i]['font-weight']);
+            unset($blockstyle[$i]['font-style']);
+            $this->style_map[strlen($this->parced_str)-1]=$blockstyle[$i];
+         }
+      }
+   }
+
+   public function __construct($text, $width, &$font_data){
+      $this->iterator=0;
+      $this->parced_str='';
+      $this->style_map=array();
+      $this->style_parcer($text, $font_data);
+      $this->positions=array_keys($this->style_map);
+      $this->np=(bool)count($this->positions);
+      $this->width=$width;
+      $this->lines=array('');
+      $this->linesmap[0]=array(0, 0, 0);
+      $this->used_fonts=array();
+   }
+
+   public function get_str(){
+      return $this->parced_str;
+   }
+
+   public function get_current_style($i){
+      if(!$this->np){
+         return '';
+      }
+      while($this->positions[$this->iterator]<$i){
+         $this->iterator++;
+      }
+      return $this->style_map[$this->positions[$this->iterator]];
+   }
+   
+
+      public function break_by_style($t){
+      $i=$this->linesmap[$t][0];
+      $j=$this->linesmap[$t][1];
+      $this->iterator=0;
+      $result=array('chunks'=>array(), 'style'=>array(), 'height'=>0, 'width'=>$this->linesmap[$t][2]);
+      if(strlen($this->parced_str)==0){
+         return $result;
+      }
+      $cs=$this->get_current_style($i);
+      $result['height']=$cs['font-size'];
+      $r=0;
+      $result['chunks'][$r]='';
+      $result['style'][$r]=$cs;
+      while($this->parced_str[$j]==' '){
+         $j--;
+      }
+      $tmp=$i;
+         for($k=$i; $k<=$j; $k++){
+         if($this->parced_str[$tmp]==' ' && $this->parced_str[$k]==' '){
+            $tmp=$k;
+            continue;
+         }
+            if($cs!=$this->get_current_style($k)) {
+            $r++;
+            $cs=$this->get_current_style($k);
+            $result['chunks'][$r]='';
+            $result['style'][$r]=$cs;
+            if($result['height']<$cs['font-size']){
+               $result['height']=$cs['font-size'];
+            }
+         }
+         $result['chunks'][$r].=$this->parced_str[$k];
+         $tmp=$k;
+      }
+      return $result;
+   }
+}
+?>
diff --git a/src/lib/fpdf/FAQ.htm b/src/lib/fpdf/FAQ.htm
new file mode 100644
index 0000000..dd263ec
--- /dev/null
+++ b/src/lib/fpdf/FAQ.htm
@@ -0,0 +1,270 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>FAQ</title>

+<link type="text/css" rel="stylesheet" href="fpdf.css">

+<style type="text/css">

+ul {list-style-type:none; margin:0; padding:0}

+ul#answers li {margin-top:1.8em}

+.question {font-weight:bold; color:#900000}

+</style>

+</head>

+<body>

+<h1>FAQ</h1>

+<ul>

+<li><b>1.</b> <a href='#q1'>What's exactly the license of FPDF? Are there any usage restrictions?</a></li>

+<li><b>2.</b> <a href='#q2'>I get the following error when I try to generate a PDF: Some data has already been output, can't send PDF file</a></li>

+<li><b>3.</b> <a href='#q3'>Accented letters are replaced by some strange characters like é.</a></li>

+<li><b>4.</b> <a href='#q4'>I try to display the Euro symbol but it doesn't work.</a></li>

+<li><b>5.</b> <a href='#q5'>I try to display a variable in the Header method but nothing prints.</a></li>

+<li><b>6.</b> <a href='#q6'>I have defined the Header and Footer methods in my PDF class but nothing shows.</a></li>

+<li><b>7.</b> <a href='#q7'>I can't make line breaks work. I put \n in the string printed by MultiCell but it doesn't work.</a></li>

+<li><b>8.</b> <a href='#q8'>I use jQuery to generate the PDF but it doesn't show.</a></li>

+<li><b>9.</b> <a href='#q9'>I draw a frame with very precise dimensions, but when printed I notice some differences.</a></li>

+<li><b>10.</b> <a href='#q10'>I'd like to use the whole surface of the page, but when printed I always have some margins. How can I get rid of them?</a></li>

+<li><b>11.</b> <a href='#q11'>How can I put a background in my PDF?</a></li>

+<li><b>12.</b> <a href='#q12'>How can I set a specific header or footer on the first page?</a></li>

+<li><b>13.</b> <a href='#q13'>I'd like to use extensions provided by different scripts. How can I combine them?</a></li>

+<li><b>14.</b> <a href='#q14'>How can I open the PDF in a new tab?</a></li>

+<li><b>15.</b> <a href='#q15'>How can I send the PDF by email?</a></li>

+<li><b>16.</b> <a href='#q16'>What's the limit of the file sizes I can generate with FPDF?</a></li>

+<li><b>17.</b> <a href='#q17'>Can I modify a PDF with FPDF?</a></li>

+<li><b>18.</b> <a href='#q18'>I'd like to make a search engine in PHP and index PDF files. Can I do it with FPDF?</a></li>

+<li><b>19.</b> <a href='#q19'>Can I convert an HTML page to PDF with FPDF?</a></li>

+<li><b>20.</b> <a href='#q20'>Can I concatenate PDF files with FPDF?</a></li>

+</ul>

+

+<ul id='answers'>

+<li id='q1'>

+<p><b>1.</b> <span class='question'>What's exactly the license of FPDF? Are there any usage restrictions?</span></p>

+FPDF is released under a permissive license: there is no usage restriction. You may embed it

+freely in your application (commercial or not), with or without modifications.

+</li>

+

+<li id='q2'>

+<p><b>2.</b> <span class='question'>I get the following error when I try to generate a PDF: Some data has already been output, can't send PDF file</span></p>

+You must send nothing to the browser except the PDF itself: no HTML, no space, no carriage return. A common

+case is having extra blank at the end of an included script file.<br>

+<br>

+The message may be followed by this indication:<br>

+<br>

+(output started at script.php:X)<br>

+<br>

+which gives you exactly the script and line number responsible for the output. If you don't see it,

+try adding this line at the very beginning of your script:

+<div class="doc-source">

+<pre><code>ob_end_clean();</code></pre>

+</div>

+</li>

+

+<li id='q3'>

+<p><b>3.</b> <span class='question'>Accented letters are replaced by some strange characters like é.</span></p>

+Don't use UTF-8 with the standard fonts; they expect text encoded in windows-1252.

+You can perform a conversion with iconv:

+<div class="doc-source">

+<pre><code>$str = iconv('UTF-8', 'windows-1252', $str);</code></pre>

+</div>

+Or with mbstring:

+<div class="doc-source">

+<pre><code>$str = mb_convert_encoding($str, 'windows-1252', 'UTF-8');</code></pre>

+</div>

+In case you need characters outside windows-1252, take a look at tutorial #7 or

+<a href="http://www.fpdf.org/?go=script&amp;id=92" target="_blank">tFPDF</a>.

+</li>

+

+<li id='q4'>

+<p><b>4.</b> <span class='question'>I try to display the Euro symbol but it doesn't work.</span></p>

+The standard fonts have the Euro character at position 128. You can define a constant like this

+for convenience:

+<div class="doc-source">

+<pre><code>define('EURO', chr(128));</code></pre>

+</div>

+</li>

+

+<li id='q5'>

+<p><b>5.</b> <span class='question'>I try to display a variable in the Header method but nothing prints.</span></p>

+You have to use the <code>global</code> keyword to access global variables, for example:

+<div class="doc-source">

+<pre><code>function Header()

+{

+    global $title;

+

+    $this-&gt;SetFont('Arial', 'B', 15);

+    $this-&gt;Cell(0, 10, $title, 1, 1, 'C');

+}

+

+$title = 'My title';</code></pre>

+</div>

+Alternatively, you can use an object property:

+<div class="doc-source">

+<pre><code>function Header()

+{

+    $this-&gt;SetFont('Arial', 'B', 15);

+    $this-&gt;Cell(0, 10, $this-&gt;title, 1, 1, 'C');

+}

+

+$pdf-&gt;title = 'My title';</code></pre>

+</div>

+</li>

+

+<li id='q6'>

+<p><b>6.</b> <span class='question'>I have defined the Header and Footer methods in my PDF class but nothing shows.</span></p>

+You have to create an object from the PDF class, not FPDF:

+<div class="doc-source">

+<pre><code>$pdf = new PDF();</code></pre>

+</div>

+</li>

+

+<li id='q7'>

+<p><b>7.</b> <span class='question'>I can't make line breaks work. I put \n in the string printed by MultiCell but it doesn't work.</span></p>

+You have to enclose your string with double quotes, not single ones.

+</li>

+

+<li id='q8'>

+<p><b>8.</b> <span class='question'>I use jQuery to generate the PDF but it doesn't show.</span></p>

+Don't use an AJAX request to retrieve the PDF.

+</li>

+

+<li id='q9'>

+<p><b>9.</b> <span class='question'>I draw a frame with very precise dimensions, but when printed I notice some differences.</span></p>

+To respect dimensions, select "None" for the Page Scaling setting instead of "Shrink to Printable Area" in the print dialog box.

+</li>

+

+<li id='q10'>

+<p><b>10.</b> <span class='question'>I'd like to use the whole surface of the page, but when printed I always have some margins. How can I get rid of them?</span></p>

+Printers have physical margins (different depending on the models); it is therefore impossible to remove

+them and print on the whole surface of the paper.

+</li>

+

+<li id='q11'>

+<p><b>11.</b> <span class='question'>How can I put a background in my PDF?</span></p>

+For a picture, call Image() in the Header() method, before any other output. To set a background color, use Rect().

+</li>

+

+<li id='q12'>

+<p><b>12.</b> <span class='question'>How can I set a specific header or footer on the first page?</span></p>

+Just test the page number:

+<div class="doc-source">

+<pre><code>function Header()

+{

+    if($this-&gt;PageNo()==1)

+    {

+        //First page

+        ...

+    }

+    else

+    {

+        //Other pages

+        ...

+    }

+}</code></pre>

+</div>

+</li>

+

+<li id='q13'>

+<p><b>13.</b> <span class='question'>I'd like to use extensions provided by different scripts. How can I combine them?</span></p>

+Use an inheritance chain. If you have two classes, say A in a.php:

+<div class="doc-source">

+<pre><code>require('fpdf.php');

+

+class A extends FPDF

+{

+...

+}</code></pre>

+</div>

+and B in b.php:

+<div class="doc-source">

+<pre><code>require('fpdf.php');

+

+class B extends FPDF

+{

+...

+}</code></pre>

+</div>

+then make B extend A:

+<div class="doc-source">

+<pre><code>require('a.php');

+

+class B extends A

+{

+...

+}</code></pre>

+</div>

+and make your own class extend B:

+<div class="doc-source">

+<pre><code>require('b.php');

+

+class PDF extends B

+{

+...

+}

+

+$pdf = new PDF();</code></pre>

+</div>

+</li>

+

+<li id='q14'>

+<p><b>14.</b> <span class='question'>How can I open the PDF in a new tab?</span></p>

+Just do the same as you would for an HTML page or anything else: add a target="_blank" to your link or form.

+</li>

+

+<li id='q15'>

+<p><b>15.</b> <span class='question'>How can I send the PDF by email?</span></p>

+As for any other file, but an easy way is to use <a href="https://github.com/PHPMailer/PHPMailer" target="_blank">PHPMailer</a> and

+its in-memory attachment:

+<div class="doc-source">

+<pre><code>$mail = new PHPMailer();

+...

+$doc = $pdf-&gt;Output('S');

+$mail-&gt;AddStringAttachment($doc, 'doc.pdf', 'base64', 'application/pdf');

+$mail-&gt;Send();</code></pre>

+</div>

+</li>

+

+<li id='q16'>

+<p><b>16.</b> <span class='question'>What's the limit of the file sizes I can generate with FPDF?</span></p>

+There is no particular limit. There are some constraints, however:

+<br>

+<br>

+- There is usually a maximum memory size allocated to PHP scripts. For very big documents,

+especially with images, the limit may be reached (the file being built in memory). The

+parameter is configured in the php.ini file.

+<br>

+<br>

+- The maximum execution time allocated to scripts defaults to 30 seconds. This limit can of course

+be easily reached. It is configured in php.ini and may be altered dynamically with set_time_limit().

+<br>

+<br>

+You can work around the memory limit with <a href="http://www.fpdf.org/?go=script&amp;id=76" target="_blank">this script</a>.

+</li>

+

+<li id='q17'>

+<p><b>17.</b> <span class='question'>Can I modify a PDF with FPDF?</span></p>

+It's possible to import pages from an existing PDF document thanks to the

+<a href="https://www.setasign.com/products/fpdi/about/" target="_blank">FPDI</a> extension.

+Then you can add some content to them.

+</li>

+

+<li id='q18'>

+<p><b>18.</b> <span class='question'>I'd like to make a search engine in PHP and index PDF files. Can I do it with FPDF?</span></p>

+No. But a GPL C utility does exist, pdftotext, which is able to extract the textual content from a PDF.

+It's provided with the <a href="https://www.xpdfreader.com" target="_blank">Xpdf</a> package.

+</li>

+

+<li id='q19'>

+<p><b>19.</b> <span class='question'>Can I convert an HTML page to PDF with FPDF?</span></p>

+Not real-world pages. But a GPL C utility does exist, <a href="https://www.msweet.org/htmldoc/" target="_blank">HTMLDOC</a>,

+which allows to do it and gives good results.

+</li>

+

+<li id='q20'>

+<p><b>20.</b> <span class='question'>Can I concatenate PDF files with FPDF?</span></p>

+Not directly, but it's possible to use <a href="https://www.setasign.com/products/fpdi/demos/concatenate-fake/" target="_blank">FPDI</a>

+to perform that task. Some free command-line tools also exist:

+<a href="https://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/" target="_blank">pdftk</a> and

+<a href="http://thierry.schmit.free.fr/spip/spip.php?article15" target="_blank">mbtPdfAsm</a>.

+</li>

+</ul>

+</body>

+</html>

diff --git a/src/lib/fpdf/changelog.htm b/src/lib/fpdf/changelog.htm
new file mode 100644
index 0000000..1c111fa
--- /dev/null
+++ b/src/lib/fpdf/changelog.htm
@@ -0,0 +1,188 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>Changelog</title>

+<link type="text/css" rel="stylesheet" href="fpdf.css">

+<style type="text/css">

+dd {margin:1em 0 1em 1em}

+</style>

+</head>

+<body>

+<h1>Changelog</h1>

+<dl>

+<dt><strong>v1.86</strong> (2023-06-25)</dt>

+<dd>

+- Added a parameter to AddFont() to specify the directory where to load the font definition file.<br>

+- Fixed a bug related to the PDF creation date.<br>

+</dd>

+<dt><strong>v1.85</strong> (2022-11-10)</dt>

+<dd>

+- Removed deprecation notices on PHP 8.2.<br>

+- Removed notices when passing null values instead of strings.<br>

+- The FPDF_VERSION constant was replaced by a class constant.<br>

+- The creation date of the PDF now includes the timezone.<br>

+- The content-type is now always application/pdf, even for downloads.<br>

+</dd>

+<dt><strong>v1.84</strong> (2021-08-28)</dt>

+<dd>

+- Fixed an issue related to annotations.<br>

+</dd>

+<dt><strong>v1.83</strong> (2021-04-18)</dt>

+<dd>

+- Fixed an issue related to annotations.<br>

+</dd>

+<dt><strong>v1.82</strong> (2019-12-07)</dt>

+<dd>

+- Removed a deprecation notice on PHP 7.4.<br>

+</dd>

+<dt><strong>v1.81</strong> (2015-12-20)</dt>

+<dd>

+- Added GetPageWidth() and GetPageHeight().<br>

+- Fixed a bug in SetXY().<br>

+</dd>

+<dt><strong>v1.8</strong> (2015-11-29)</dt>

+<dd>

+- PHP 5.1.0 or higher is now required.<br>

+- The MakeFont utility now subsets fonts, which can greatly reduce font sizes.<br>

+- Added ToUnicode CMaps to improve text extraction.<br>

+- Added a parameter to AddPage() to rotate the page.<br>

+- Added a parameter to SetY() to indicate whether the x position should be reset or not.<br>

+- Added a parameter to Output() to specify the encoding of the name, and special characters are now properly encoded. Additionally the order of the first two parameters was reversed to be more logical (however the old order is still supported for compatibility).<br>

+- The Error() method now throws an exception.<br>

+- Adding contents before the first AddPage() or after Close() now raises an error.<br>

+- Outputting text with no font selected now raises an error.<br>

+</dd>

+<dt><strong>v1.7</strong> (2011-06-18)</dt>

+<dd>

+- The MakeFont utility has been completely rewritten and doesn't depend on ttf2pt1 anymore.<br>

+- Alpha channel is now supported for PNGs.<br>

+- When inserting an image, it's now possible to specify its resolution.<br>

+- Default resolution for images was increased from 72 to 96 dpi.<br>

+- When inserting a GIF image, no temporary file is used anymore if the PHP version is 5.1 or higher.<br>

+- When output buffering is enabled and the PDF is about to be sent, the buffer is now cleared if it contains only a UTF-8 BOM and/or whitespace (instead of throwing an error).<br>

+- Symbol and ZapfDingbats fonts now support underline style.<br>

+- Custom page sizes are now checked to ensure that width is smaller than height.<br>

+- Standard font files were changed to use the same format as user fonts.<br>

+- A bug in the embedding of Type1 fonts was fixed.<br>

+- A bug related to SetDisplayMode() and the current locale was fixed.<br>

+- A display issue occurring with the Adobe Reader X plug-in was fixed.<br>

+- An issue related to transparency with some versions of Adobe Reader was fixed.<br>

+- The Content-Length header was removed because it caused an issue when the HTTP server applies compression.<br>

+</dd>

+<dt><strong>v1.6</strong> (2008-08-03)</dt>

+<dd>

+- PHP 4.3.10 or higher is now required.<br>

+- GIF image support.<br>

+- Images can now trigger page breaks.<br>

+- Possibility to have different page formats in a single document.<br>

+- Document properties (author, creator, keywords, subject and title) can now be specified in UTF-8.<br>

+- Fixed a bug: when a PNG was inserted through a URL, an error sometimes occurred.<br>

+- An automatic page break in Header() doesn't cause an infinite loop any more.<br>

+- Removed some warning messages appearing with recent PHP versions.<br>

+- Added HTTP headers to reduce problems with IE.<br>

+</dd>

+<dt><strong>v1.53</strong> (2004-12-31)</dt>

+<dd>

+- When the font subdirectory is in the same directory as fpdf.php, it's no longer necessary to define the FPDF_FONTPATH constant.<br>

+- The array $HTTP_SERVER_VARS is no longer used. It could cause trouble on PHP5-based configurations with the register_long_arrays option disabled.<br>

+- Fixed a problem related to Type1 font embedding which caused trouble to some PDF processors.<br>

+- The file name sent to the browser could not contain a space character.<br>

+- The Cell() method could not print the number 0 (you had to pass the string '0').<br>

+</dd>

+<dt><strong>v1.52</strong> (2003-12-30)</dt>

+<dd>

+- Image() now displays the image at 72 dpi if no dimension is given.<br>

+- Output() takes a string as second parameter to indicate destination.<br>

+- Open() is now called automatically by AddPage().<br>

+- Inserting remote JPEG images doesn't generate an error any longer.<br>

+- Decimal separator is forced to dot in the constructor.<br>

+- Added several encodings (Turkish, Thai, Hebrew, Ukrainian and Vietnamese).<br>

+- The last line of a right-aligned MultiCell() was not correctly aligned if it was terminated by a carriage return.<br>

+- No more error message about already sent headers when outputting the PDF to the standard output from the command line.<br>

+- The underlining was going too far for text containing characters \, ( or ).<br>

+- $HTTP_ENV_VARS has been replaced by $HTTP_SERVER_VARS.<br>

+</dd>

+<dt><strong>v1.51</strong> (2002-08-03)</dt>

+<dd>

+- Type1 font support.<br>

+- Added Baltic encoding.<br>

+- The class now works internally in points with the origin at the bottom in order to avoid two bugs occurring with Acrobat 5:<br>&nbsp;&nbsp;* The line thickness was too large when printed on Windows 98 SE and ME.<br>&nbsp;&nbsp;* TrueType fonts didn't appear immediately inside the plug-in (a substitution font was used), one had to cause a window refresh to make them show up.<br>

+- It's no longer necessary to set the decimal separator as dot to produce valid documents.<br>

+- The clickable area in a cell was always on the left independently from the text alignment.<br>

+- JPEG images in CMYK mode appeared in inverted colors.<br>

+- Transparent PNG images in grayscale or true color mode were incorrectly handled.<br>

+- Adding new fonts now works correctly even with the magic_quotes_runtime option set to on.<br>

+</dd>

+<dt><strong>v1.5</strong> (2002-05-28)</dt>

+<dd>

+- TrueType font (AddFont()) and encoding support (Western and Eastern Europe, Cyrillic and Greek).<br>

+- Added Write() method.<br>

+- Added underlined style.<br>

+- Internal and external link support (AddLink(), SetLink(), Link()).<br>

+- Added right margin management and methods SetRightMargin(), SetTopMargin().<br>

+- Modification of SetDisplayMode() to select page layout.<br>

+- The border parameter of MultiCell() now lets choose borders to draw as Cell().<br>

+- When a document contains no page, Close() now calls AddPage() instead of causing a fatal error.<br>

+</dd>

+<dt><strong>v1.41</strong> (2002-03-13)</dt>

+<dd>

+- Fixed SetDisplayMode() which no longer worked (the PDF viewer used its default display).<br>

+</dd>

+<dt><strong>v1.4</strong> (2002-03-02)</dt>

+<dd>

+- PHP3 is no longer supported.<br>

+- Page compression (SetCompression()).<br>

+- Choice of page format and possibility to change orientation inside document.<br>

+- Added AcceptPageBreak() method.<br>

+- Ability to print the total number of pages (AliasNbPages()).<br>

+- Choice of cell borders to draw.<br>

+- New mode for Cell(): the current position can now move under the cell.<br>

+- Ability to include an image by specifying height only (width is calculated automatically).<br>

+- Fixed a bug: when a justified line triggered a page break, the footer inherited the corresponding word spacing.<br>

+</dd>

+<dt><strong>v1.31</strong> (2002-01-12)</dt>

+<dd>

+- Fixed a bug in drawing frame with MultiCell(): the last line always started from the left margin.<br>

+- Removed Expires HTTP header (gives trouble in some situations).<br>

+- Added Content-disposition HTTP header (seems to help in some situations).<br>

+</dd>

+<dt><strong>v1.3</strong> (2001-12-03)</dt>

+<dd>

+- Line break and text justification support (MultiCell()).<br>

+- Color support (SetDrawColor(), SetFillColor(), SetTextColor()). Possibility to draw filled rectangles and paint cell background.<br>

+- A cell whose width is declared null extends up to the right margin of the page.<br>

+- Line width is now retained from page to page and defaults to 0.2 mm.<br>

+- Added SetXY() method.<br>

+- Fixed a passing by reference done in a deprecated manner for PHP4.<br>

+</dd>

+<dt><strong>v1.2</strong> (2001-11-11)</dt>

+<dd>

+- Added font metric files and GetStringWidth() method.<br>

+- Centering and right-aligning text in cells.<br>

+- Display mode control (SetDisplayMode()).<br>

+- Added methods to set document properties (SetAuthor(), SetCreator(), SetKeywords(), SetSubject(), SetTitle()).<br>

+- Possibility to force PDF download by browser.<br>

+- Added SetX() and GetX() methods.<br>

+- During automatic page break, current abscissa is now retained.<br>

+</dd>

+<dt><strong>v1.11</strong> (2001-10-20)</dt>

+<dd>

+- PNG support doesn't require PHP4/zlib any more. Data are now put directly into PDF without any decompression/recompression stage.<br>

+- Image insertion now works correctly even with magic_quotes_runtime option set to on.<br>

+</dd>

+<dt><strong>v1.1</strong> (2001-10-07)</dt>

+<dd>

+- JPEG and PNG image support.<br>

+</dd>

+<dt><strong>v1.01</strong> (2001-10-03)</dt>

+<dd>

+- Fixed a bug involving page break: in case when Header() doesn't specify a font, the one from previous page was not restored and produced an incorrect document.<br>

+</dd>

+<dt><strong>v1.0</strong> (2001-09-17)</dt>

+<dd>

+- First version.<br>

+</dd>

+</dl>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/__construct.htm b/src/lib/fpdf/doc/__construct.htm
new file mode 100644
index 0000000..c79165e
--- /dev/null
+++ b/src/lib/fpdf/doc/__construct.htm
@@ -0,0 +1,63 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>__construct</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>__construct</h1>

+<code>__construct([<b>string</b> orientation [, <b>string</b> unit [, <b>mixed</b> size]]])</code>

+<h2>Description</h2>

+This is the class constructor. It allows to set up the page size, the orientation and the

+unit of measure used in all methods (except for font sizes).

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>orientation</code></dt>

+<dd>

+Default page orientation. Possible values are (case insensitive):

+<ul>

+<li><code>P</code> or <code>Portrait</code></li>

+<li><code>L</code> or <code>Landscape</code></li>

+</ul>

+Default value is <code>P</code>.

+</dd>

+<dt><code>unit</code></dt>

+<dd>

+User unit. Possible values are:

+<ul>

+<li><code>pt</code>: point</li>

+<li><code>mm</code>: millimeter</li>

+<li><code>cm</code>: centimeter</li>

+<li><code>in</code>: inch</li>

+</ul>

+A point equals 1/72 of inch, that is to say about 0.35 mm (an inch being 2.54 cm). This

+is a very common unit in typography; font sizes are expressed in that unit.

+<br>

+<br>

+Default value is <code>mm</code>.

+</dd>

+<dt><code>size</code></dt>

+<dd>

+The size used for pages. It can be either one of the following values (case insensitive):

+<ul>

+<li><code>A3</code></li>

+<li><code>A4</code></li>

+<li><code>A5</code></li>

+<li><code>Letter</code></li>

+<li><code>Legal</code></li>

+</ul>

+or an array containing the width and the height (expressed in the unit given by <code>unit</code>).<br>

+<br>

+Default value is <code>A4</code>.

+</dd>

+</dl>

+<h2>Example</h2>

+Document with a custom 100x150 mm page size:

+<div class="doc-source">

+<pre><code>$pdf = new FPDF('P', 'mm', array(100,150));</code></pre>

+</div>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/acceptpagebreak.htm b/src/lib/fpdf/doc/acceptpagebreak.htm
new file mode 100644
index 0000000..640d1e0
--- /dev/null
+++ b/src/lib/fpdf/doc/acceptpagebreak.htm
@@ -0,0 +1,63 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>AcceptPageBreak</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>AcceptPageBreak</h1>

+<code><b>boolean</b> AcceptPageBreak()</code>

+<h2>Description</h2>

+Whenever a page break condition is met, the method is called, and the break is issued or not

+depending on the returned value. The default implementation returns a value according to the

+mode selected by SetAutoPageBreak().

+<br>

+This method is called automatically and should not be called directly by the application.

+<h2>Example</h2>

+The method is overriden in an inherited class in order to obtain a 3 column layout:

+<div class="doc-source">

+<pre><code>class PDF extends FPDF

+{

+    protected $col = 0;

+

+    function SetCol($col)

+    {

+        // Move position to a column

+        $this-&gt;col = $col;

+        $x = 10 + $col*65;

+        $this-&gt;SetLeftMargin($x);

+        $this-&gt;SetX($x);

+    }

+

+    function AcceptPageBreak()

+    {

+        if($this-&gt;col&lt;2)

+        {

+            // Go to next column

+            $this-&gt;SetCol($this-&gt;col+1);

+            $this-&gt;SetY(10);

+            return false;

+        }

+        else

+        {

+            // Go back to first column and issue page break

+            $this-&gt;SetCol(0);

+            return true;

+        }

+    }

+}

+

+$pdf = new PDF();

+$pdf-&gt;AddPage();

+$pdf-&gt;SetFont('Arial', '', 12);

+for($i=1;$i&lt;=300;$i++)

+    $pdf-&gt;Cell(0, 5, "Line $i", 0, 1);

+$pdf-&gt;Output();</code></pre>

+</div>

+<h2>See also</h2>

+<a href="setautopagebreak.htm">SetAutoPageBreak</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/addfont.htm b/src/lib/fpdf/doc/addfont.htm
new file mode 100644
index 0000000..1ce0505
--- /dev/null
+++ b/src/lib/fpdf/doc/addfont.htm
@@ -0,0 +1,67 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>AddFont</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>AddFont</h1>

+<code>AddFont(<b>string</b> family [, <b>string</b> style [, <b>string</b> file [, <b>string</b> dir]]])</code>

+<h2>Description</h2>

+Imports a TrueType, OpenType or Type1 font and makes it available. It is necessary to generate a font

+definition file first with the MakeFont utility.

+<br>

+<br>

+The definition file (and the font file itself in case of embedding) must be present in:

+<ul>

+<li>The directory indicated by the 4th parameter (if that parameter is set)</li>

+<li>The directory indicated by the <code>FPDF_FONTPATH</code> constant (if that constant is defined)</li>

+<li>The <code>font</code> directory located in the same directory as <code>fpdf.php</code></li>

+</ul>

+If the file is not found, the error "Could not include font definition file" is raised.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>family</code></dt>

+<dd>

+Font family. The name can be chosen arbitrarily. If it is a standard family name, it will

+override the corresponding font.

+</dd>

+<dt><code>style</code></dt>

+<dd>

+Font style. Possible values are (case insensitive):

+<ul>

+<li>empty string: regular</li>

+<li><code>B</code>: bold</li>

+<li><code>I</code>: italic</li>

+<li><code>BI</code> or <code>IB</code>: bold italic</li>

+</ul>

+The default value is regular.

+</dd>

+<dt><code>file</code></dt>

+<dd>

+The name of the font definition file.

+<br>

+By default, it is built from the family and style, in lower case with no space.

+</dd>

+<dt><code>dir</code></dt>

+<dd>

+The directory where to load the definition file.

+<br>

+If not specified, the default directory will be used.

+</dd>

+</dl>

+<h2>Example</h2>

+<div class="doc-source">

+<pre><code>$pdf-&gt;AddFont('Comic', 'I');</code></pre>

+</div>

+is equivalent to:

+<div class="doc-source">

+<pre><code>$pdf-&gt;AddFont('Comic', 'I', 'comici.php');</code></pre>

+</div>

+<h2>See also</h2>

+<a href="setfont.htm">SetFont</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/addlink.htm b/src/lib/fpdf/doc/addlink.htm
new file mode 100644
index 0000000..0e8586a
--- /dev/null
+++ b/src/lib/fpdf/doc/addlink.htm
@@ -0,0 +1,26 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>AddLink</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>AddLink</h1>

+<code><b>int</b> AddLink()</code>

+<h2>Description</h2>

+Creates a new internal link and returns its identifier. An internal link is a clickable area

+which directs to another place within the document.

+<br>

+The identifier can then be passed to Cell(), Write(), Image() or Link(). The destination is

+defined with SetLink().

+<h2>See also</h2>

+<a href="cell.htm">Cell</a>,

+<a href="write.htm">Write</a>,

+<a href="image.htm">Image</a>,

+<a href="link.htm">Link</a>,

+<a href="setlink.htm">SetLink</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/addpage.htm b/src/lib/fpdf/doc/addpage.htm
new file mode 100644
index 0000000..f75bc83
--- /dev/null
+++ b/src/lib/fpdf/doc/addpage.htm
@@ -0,0 +1,61 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>AddPage</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>AddPage</h1>

+<code>AddPage([<b>string</b> orientation [, <b>mixed</b> size [, <b>int</b> rotation]]])</code>

+<h2>Description</h2>

+Adds a new page to the document. If a page is already present, the Footer() method is called

+first to output the footer. Then the page is added, the current position set to the top-left

+corner according to the left and top margins, and Header() is called to display the header.

+<br>

+The font which was set before calling is automatically restored. There is no need to call

+SetFont() again if you want to continue with the same font. The same is true for colors and

+line width.

+<br>

+The origin of the coordinate system is at the top-left corner and increasing ordinates go

+downwards.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>orientation</code></dt>

+<dd>

+Page orientation. Possible values are (case insensitive):

+<ul>

+<li><code>P</code> or <code>Portrait</code></li>

+<li><code>L</code> or <code>Landscape</code></li>

+</ul>

+The default value is the one passed to the constructor.

+</dd>

+<dt><code>size</code></dt>

+<dd>

+Page size. It can be either one of the following values (case insensitive):

+<ul>

+<li><code>A3</code></li>

+<li><code>A4</code></li>

+<li><code>A5</code></li>

+<li><code>Letter</code></li>

+<li><code>Legal</code></li>

+</ul>

+or an array containing the width and the height (expressed in user unit).<br>

+<br>

+The default value is the one passed to the constructor.

+</dd>

+<dt><code>rotation</code></dt>

+<dd>

+Angle by which to rotate the page. It must be a multiple of 90; positive values

+mean clockwise rotation. The default value is <code>0</code>.

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="__construct.htm">__construct</a>,

+<a href="header.htm">Header</a>,

+<a href="footer.htm">Footer</a>,

+<a href="setmargins.htm">SetMargins</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/aliasnbpages.htm b/src/lib/fpdf/doc/aliasnbpages.htm
new file mode 100644
index 0000000..7afa385
--- /dev/null
+++ b/src/lib/fpdf/doc/aliasnbpages.htm
@@ -0,0 +1,45 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>AliasNbPages</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>AliasNbPages</h1>

+<code>AliasNbPages([<b>string</b> alias])</code>

+<h2>Description</h2>

+Defines an alias for the total number of pages. It will be substituted as the document is

+closed.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>alias</code></dt>

+<dd>

+The alias. Default value: <code>{nb}</code>.

+</dd>

+</dl>

+<h2>Example</h2>

+<div class="doc-source">

+<pre><code>class PDF extends FPDF

+{

+    function Footer()

+    {

+        // Go to 1.5 cm from bottom

+        $this-&gt;SetY(-15);

+        // Select Arial italic 8

+        $this-&gt;SetFont('Arial', 'I', 8);

+        // Print current and total page numbers

+        $this-&gt;Cell(0, 10, 'Page '.$this-&gt;PageNo().'/{nb}', 0, 0, 'C');

+    }

+}

+

+$pdf = new PDF();

+$pdf-&gt;AliasNbPages();</code></pre>

+</div>

+<h2>See also</h2>

+<a href="pageno.htm">PageNo</a>,

+<a href="footer.htm">Footer</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/cell.htm b/src/lib/fpdf/doc/cell.htm
new file mode 100644
index 0000000..c2d9d1b
--- /dev/null
+++ b/src/lib/fpdf/doc/cell.htm
@@ -0,0 +1,104 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>Cell</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>Cell</h1>

+<code>Cell(<b>float</b> w [, <b>float</b> h [, <b>string</b> txt [, <b>mixed</b> border [, <b>int</b> ln [, <b>string</b> align [, <b>boolean</b> fill [, <b>mixed</b> link]]]]]]])</code>

+<h2>Description</h2>

+Prints a cell (rectangular area) with optional borders, background color and character string.

+The upper-left corner of the cell corresponds to the current position. The text can be aligned

+or centered. After the call, the current position moves to the right or to the next line. It is

+possible to put a link on the text.

+<br>

+If automatic page breaking is enabled and the cell goes beyond the limit, a page break is

+done before outputting.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>w</code></dt>

+<dd>

+Cell width. If <code>0</code>, the cell extends up to the right margin.

+</dd>

+<dt><code>h</code></dt>

+<dd>

+Cell height.

+Default value: <code>0</code>.

+</dd>

+<dt><code>txt</code></dt>

+<dd>

+String to print.

+Default value: empty string.

+</dd>

+<dt><code>border</code></dt>

+<dd>

+Indicates if borders must be drawn around the cell. The value can be either a number:

+<ul>

+<li><code>0</code>: no border</li>

+<li><code>1</code>: frame</li>

+</ul>

+or a string containing some or all of the following characters (in any order):

+<ul>

+<li><code>L</code>: left</li>

+<li><code>T</code>: top</li>

+<li><code>R</code>: right</li>

+<li><code>B</code>: bottom</li>

+</ul>

+Default value: <code>0</code>.

+</dd>

+<dt><code>ln</code></dt>

+<dd>

+Indicates where the current position should go after the call. Possible values are:

+<ul>

+<li><code>0</code>: to the right</li>

+<li><code>1</code>: to the beginning of the next line</li>

+<li><code>2</code>: below</li>

+</ul>

+Putting <code>1</code> is equivalent to putting <code>0</code> and calling Ln() just after.

+Default value: <code>0</code>.

+</dd>

+<dt><code>align</code></dt>

+<dd>

+Allows to center or align the text. Possible values are:

+<ul>

+<li><code>L</code> or empty string: left align (default value)</li>

+<li><code>C</code>: center</li>

+<li><code>R</code>: right align</li>

+</ul>

+</dd>

+<dt><code>fill</code></dt>

+<dd>

+Indicates if the cell background must be painted (<code>true</code>) or transparent (<code>false</code>).

+Default value: <code>false</code>.

+</dd>

+<dt><code>link</code></dt>

+<dd>

+URL or identifier returned by AddLink().

+</dd>

+</dl>

+<h2>Example</h2>

+<div class="doc-source">

+<pre><code>// Set font

+$pdf-&gt;SetFont('Arial', 'B', 16);

+// Move to 8 cm to the right

+$pdf-&gt;Cell(80);

+// Centered text in a framed 20*10 mm cell and line break

+$pdf-&gt;Cell(20, 10, 'Title', 1, 1, 'C');</code></pre>

+</div>

+<h2>See also</h2>

+<a href="setfont.htm">SetFont</a>,

+<a href="setdrawcolor.htm">SetDrawColor</a>,

+<a href="setfillcolor.htm">SetFillColor</a>,

+<a href="settextcolor.htm">SetTextColor</a>,

+<a href="setlinewidth.htm">SetLineWidth</a>,

+<a href="addlink.htm">AddLink</a>,

+<a href="ln.htm">Ln</a>,

+<a href="multicell.htm">MultiCell</a>,

+<a href="write.htm">Write</a>,

+<a href="setautopagebreak.htm">SetAutoPageBreak</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/close.htm b/src/lib/fpdf/doc/close.htm
new file mode 100644
index 0000000..596cfe4
--- /dev/null
+++ b/src/lib/fpdf/doc/close.htm
@@ -0,0 +1,21 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>Close</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>Close</h1>

+<code>Close()</code>

+<h2>Description</h2>

+Terminates the PDF document. It is not necessary to call this method explicitly because Output()

+does it automatically.

+<br>

+If the document contains no page, AddPage() is called to prevent from getting an invalid document.

+<h2>See also</h2>

+<a href="output.htm">Output</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/error.htm b/src/lib/fpdf/doc/error.htm
new file mode 100644
index 0000000..16bcde9
--- /dev/null
+++ b/src/lib/fpdf/doc/error.htm
@@ -0,0 +1,26 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>Error</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>Error</h1>

+<code>Error(<b>string</b> msg)</code>

+<h2>Description</h2>

+This method is automatically called in case of a fatal error; it simply throws an exception

+with the provided message.<br>

+An inherited class may override it to customize the error handling but the method should

+never return, otherwise the resulting document would probably be invalid.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>msg</code></dt>

+<dd>

+The error message.

+</dd>

+</dl>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/footer.htm b/src/lib/fpdf/doc/footer.htm
new file mode 100644
index 0000000..40bf6b1
--- /dev/null
+++ b/src/lib/fpdf/doc/footer.htm
@@ -0,0 +1,35 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>Footer</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>Footer</h1>

+<code>Footer()</code>

+<h2>Description</h2>

+This method is used to render the page footer. It is automatically called by AddPage() and

+Close() and should not be called directly by the application. The implementation in FPDF is

+empty, so you have to subclass it and override the method if you want a specific processing.

+<h2>Example</h2>

+<div class="doc-source">

+<pre><code>class PDF extends FPDF

+{

+    function Footer()

+    {

+        // Go to 1.5 cm from bottom

+        $this-&gt;SetY(-15);

+        // Select Arial italic 8

+        $this-&gt;SetFont('Arial', 'I', 8);

+        // Print centered page number

+        $this-&gt;Cell(0, 10, 'Page '.$this-&gt;PageNo(), 0, 0, 'C');

+    }

+}</code></pre>

+</div>

+<h2>See also</h2>

+<a href="header.htm">Header</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/getpageheight.htm b/src/lib/fpdf/doc/getpageheight.htm
new file mode 100644
index 0000000..2727c0e
--- /dev/null
+++ b/src/lib/fpdf/doc/getpageheight.htm
@@ -0,0 +1,18 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>GetPageHeight</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>GetPageHeight</h1>

+<code><b>float</b> GetPageHeight()</code>

+<h2>Description</h2>

+Returns the current page height.

+<h2>See also</h2>

+<a href="getpagewidth.htm">GetPageWidth</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/getpagewidth.htm b/src/lib/fpdf/doc/getpagewidth.htm
new file mode 100644
index 0000000..3343389
--- /dev/null
+++ b/src/lib/fpdf/doc/getpagewidth.htm
@@ -0,0 +1,18 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>GetPageWidth</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>GetPageWidth</h1>

+<code><b>float</b> GetPageWidth()</code>

+<h2>Description</h2>

+Returns the current page width.

+<h2>See also</h2>

+<a href="getpageheight.htm">GetPageHeight</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/getstringwidth.htm b/src/lib/fpdf/doc/getstringwidth.htm
new file mode 100644
index 0000000..4aba124
--- /dev/null
+++ b/src/lib/fpdf/doc/getstringwidth.htm
@@ -0,0 +1,23 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>GetStringWidth</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>GetStringWidth</h1>

+<code><b>float</b> GetStringWidth(<b>string</b> s)</code>

+<h2>Description</h2>

+Returns the length of a string in user unit. A font must be selected.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>s</code></dt>

+<dd>

+The string whose length is to be computed.

+</dd>

+</dl>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/getx.htm b/src/lib/fpdf/doc/getx.htm
new file mode 100644
index 0000000..91bed5d
--- /dev/null
+++ b/src/lib/fpdf/doc/getx.htm
@@ -0,0 +1,20 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>GetX</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>GetX</h1>

+<code><b>float</b> GetX()</code>

+<h2>Description</h2>

+Returns the abscissa of the current position.

+<h2>See also</h2>

+<a href="setx.htm">SetX</a>,

+<a href="gety.htm">GetY</a>,

+<a href="sety.htm">SetY</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/gety.htm b/src/lib/fpdf/doc/gety.htm
new file mode 100644
index 0000000..4e28883
--- /dev/null
+++ b/src/lib/fpdf/doc/gety.htm
@@ -0,0 +1,20 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>GetY</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>GetY</h1>

+<code><b>float</b> GetY()</code>

+<h2>Description</h2>

+Returns the ordinate of the current position.

+<h2>See also</h2>

+<a href="sety.htm">SetY</a>,

+<a href="getx.htm">GetX</a>,

+<a href="setx.htm">SetX</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/header.htm b/src/lib/fpdf/doc/header.htm
new file mode 100644
index 0000000..851afde
--- /dev/null
+++ b/src/lib/fpdf/doc/header.htm
@@ -0,0 +1,37 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>Header</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>Header</h1>

+<code>Header()</code>

+<h2>Description</h2>

+This method is used to render the page header. It is automatically called by AddPage() and

+should not be called directly by the application. The implementation in FPDF is empty, so

+you have to subclass it and override the method if you want a specific processing.

+<h2>Example</h2>

+<div class="doc-source">

+<pre><code>class PDF extends FPDF

+{

+    function Header()

+    {

+        // Select Arial bold 15

+        $this-&gt;SetFont('Arial', 'B', 15);

+        // Move to the right

+        $this-&gt;Cell(80);

+        // Framed title

+        $this-&gt;Cell(30, 10, 'Title', 1, 0, 'C');

+        // Line break

+        $this-&gt;Ln(20);

+    }

+}</code></pre>

+</div>

+<h2>See also</h2>

+<a href="footer.htm">Footer</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/image.htm b/src/lib/fpdf/doc/image.htm
new file mode 100644
index 0000000..b2296ab
--- /dev/null
+++ b/src/lib/fpdf/doc/image.htm
@@ -0,0 +1,99 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>Image</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>Image</h1>

+<code>Image(<b>string</b> file [, <b>float</b> x [, <b>float</b> y [, <b>float</b> w [, <b>float</b> h [, <b>string</b> type [, <b>mixed</b> link]]]]]])</code>

+<h2>Description</h2>

+Puts an image. The size it will take on the page can be specified in different ways:

+<ul>

+<li>explicit width and height (expressed in user unit or dpi)</li>

+<li>one explicit dimension, the other being calculated automatically in order to keep the original proportions</li>

+<li>no explicit dimension, in which case the image is put at 96 dpi</li>

+</ul>

+Supported formats are JPEG, PNG and GIF. The GD extension is required for GIF.

+<br>

+<br>

+For JPEGs, all flavors are allowed:

+<ul>

+<li>gray scales</li>

+<li>true colors (24 bits)</li>

+<li>CMYK (32 bits)</li>

+</ul>

+For PNGs, are allowed:

+<ul>

+<li>gray scales on at most 8 bits (256 levels)</li>

+<li>indexed colors</li>

+<li>true colors (24 bits)</li>

+</ul>

+For GIFs: in case of an animated GIF, only the first frame is displayed.<br>

+<br>

+Transparency is supported.<br>

+<br>

+The format can be specified explicitly or inferred from the file extension.<br>

+<br>

+It is possible to put a link on the image.<br>

+<br>

+Remark: if an image is used several times, only one copy is embedded in the file.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>file</code></dt>

+<dd>

+Path or URL of the image.

+</dd>

+<dt><code>x</code></dt>

+<dd>

+Abscissa of the upper-left corner. If not specified or equal to <code>null</code>, the current abscissa

+is used.

+</dd>

+<dt><code>y</code></dt>

+<dd>

+Ordinate of the upper-left corner. If not specified or equal to <code>null</code>, the current ordinate

+is used; moreover, a page break is triggered first if necessary (in case automatic page breaking is enabled)

+and, after the call, the current ordinate is moved to the bottom of the image.

+</dd>

+<dt><code>w</code></dt>

+<dd>

+Width of the image in the page. There are three cases:

+<ul>

+<li>If the value is positive, it represents the width in user unit</li>

+<li>If the value is negative, the absolute value represents the horizontal resolution in dpi</li>

+<li>If the value is not specified or equal to zero, it is automatically calculated</li>

+</ul>

+</dd>

+<dt><code>h</code></dt>

+<dd>

+Height of the image in the page. There are three cases:

+<ul>

+<li>If the value is positive, it represents the height in user unit</li>

+<li>If the value is negative, the absolute value represents the vertical resolution in dpi</li>

+<li>If the value is not specified or equal to zero, it is automatically calculated</li>

+</ul>

+</dd>

+<dt><code>type</code></dt>

+<dd>

+Image format. Possible values are (case insensitive): <code>JPG</code>, <code>JPEG</code>, <code>PNG</code> and <code>GIF</code>.

+If not specified, the type is inferred from the file extension.

+</dd>

+<dt><code>link</code></dt>

+<dd>

+URL or identifier returned by AddLink().

+</dd>

+</dl>

+<h2>Example</h2>

+<div class="doc-source">

+<pre><code>// Insert a logo in the top-left corner at 300 dpi

+$pdf-&gt;Image('logo.png', 10, 10, -300);

+// Insert a dynamic image from a URL

+$pdf-&gt;Image('http://chart.googleapis.com/chart?cht=p3&amp;chd=t:60,40&amp;chs=250x100&amp;chl=Hello|World', 60, 30, 90, 0, 'PNG');</code></pre>

+</div>

+<h2>See also</h2>

+<a href="addlink.htm">AddLink</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/index.htm b/src/lib/fpdf/doc/index.htm
new file mode 100644
index 0000000..97f4769
--- /dev/null
+++ b/src/lib/fpdf/doc/index.htm
@@ -0,0 +1,59 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>Documentation</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>Documentation</h1>

+<a href="__construct.htm">__construct</a> - constructor<br>

+<a href="acceptpagebreak.htm">AcceptPageBreak</a> - accept or not automatic page break<br>

+<a href="addfont.htm">AddFont</a> - add a new font<br>

+<a href="addlink.htm">AddLink</a> - create an internal link<br>

+<a href="addpage.htm">AddPage</a> - add a new page<br>

+<a href="aliasnbpages.htm">AliasNbPages</a> - define an alias for number of pages<br>

+<a href="cell.htm">Cell</a> - print a cell<br>

+<a href="close.htm">Close</a> - terminate the document<br>

+<a href="error.htm">Error</a> - fatal error<br>

+<a href="footer.htm">Footer</a> - page footer<br>

+<a href="getpageheight.htm">GetPageHeight</a> - get current page height<br>

+<a href="getpagewidth.htm">GetPageWidth</a> - get current page width<br>

+<a href="getstringwidth.htm">GetStringWidth</a> - compute string length<br>

+<a href="getx.htm">GetX</a> - get current x position<br>

+<a href="gety.htm">GetY</a> - get current y position<br>

+<a href="header.htm">Header</a> - page header<br>

+<a href="image.htm">Image</a> - output an image<br>

+<a href="line.htm">Line</a> - draw a line<br>

+<a href="link.htm">Link</a> - put a link<br>

+<a href="ln.htm">Ln</a> - line break<br>

+<a href="multicell.htm">MultiCell</a> - print text with line breaks<br>

+<a href="output.htm">Output</a> - save or send the document<br>

+<a href="pageno.htm">PageNo</a> - page number<br>

+<a href="rect.htm">Rect</a> - draw a rectangle<br>

+<a href="setauthor.htm">SetAuthor</a> - set the document author<br>

+<a href="setautopagebreak.htm">SetAutoPageBreak</a> - set the automatic page breaking mode<br>

+<a href="setcompression.htm">SetCompression</a> - turn compression on or off<br>

+<a href="setcreator.htm">SetCreator</a> - set document creator<br>

+<a href="setdisplaymode.htm">SetDisplayMode</a> - set display mode<br>

+<a href="setdrawcolor.htm">SetDrawColor</a> - set drawing color<br>

+<a href="setfillcolor.htm">SetFillColor</a> - set filling color<br>

+<a href="setfont.htm">SetFont</a> - set font<br>

+<a href="setfontsize.htm">SetFontSize</a> - set font size<br>

+<a href="setkeywords.htm">SetKeywords</a> - associate keywords with document<br>

+<a href="setleftmargin.htm">SetLeftMargin</a> - set left margin<br>

+<a href="setlinewidth.htm">SetLineWidth</a> - set line width<br>

+<a href="setlink.htm">SetLink</a> - set internal link destination<br>

+<a href="setmargins.htm">SetMargins</a> - set margins<br>

+<a href="setrightmargin.htm">SetRightMargin</a> - set right margin<br>

+<a href="setsubject.htm">SetSubject</a> - set document subject<br>

+<a href="settextcolor.htm">SetTextColor</a> - set text color<br>

+<a href="settitle.htm">SetTitle</a> - set document title<br>

+<a href="settopmargin.htm">SetTopMargin</a> - set top margin<br>

+<a href="setx.htm">SetX</a> - set current x position<br>

+<a href="setxy.htm">SetXY</a> - set current x and y positions<br>

+<a href="sety.htm">SetY</a> - set current y position and optionally reset x<br>

+<a href="text.htm">Text</a> - print a string<br>

+<a href="write.htm">Write</a> - print flowing text<br>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/line.htm b/src/lib/fpdf/doc/line.htm
new file mode 100644
index 0000000..5cb0607
--- /dev/null
+++ b/src/lib/fpdf/doc/line.htm
@@ -0,0 +1,38 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>Line</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>Line</h1>

+<code>Line(<b>float</b> x1, <b>float</b> y1, <b>float</b> x2, <b>float</b> y2)</code>

+<h2>Description</h2>

+Draws a line between two points.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>x1</code></dt>

+<dd>

+Abscissa of first point.

+</dd>

+<dt><code>y1</code></dt>

+<dd>

+Ordinate of first point.

+</dd>

+<dt><code>x2</code></dt>

+<dd>

+Abscissa of second point.

+</dd>

+<dt><code>y2</code></dt>

+<dd>

+Ordinate of second point.

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="setlinewidth.htm">SetLineWidth</a>,

+<a href="setdrawcolor.htm">SetDrawColor</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/link.htm b/src/lib/fpdf/doc/link.htm
new file mode 100644
index 0000000..46ba0f2
--- /dev/null
+++ b/src/lib/fpdf/doc/link.htm
@@ -0,0 +1,46 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>Link</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>Link</h1>

+<code>Link(<b>float</b> x, <b>float</b> y, <b>float</b> w, <b>float</b> h, <b>mixed</b> link)</code>

+<h2>Description</h2>

+Puts a link on a rectangular area of the page. Text or image links are generally put via Cell(),

+Write() or Image(), but this method can be useful for instance to define a clickable area inside

+an image.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>x</code></dt>

+<dd>

+Abscissa of the upper-left corner of the rectangle.

+</dd>

+<dt><code>y</code></dt>

+<dd>

+Ordinate of the upper-left corner of the rectangle.

+</dd>

+<dt><code>w</code></dt>

+<dd>

+Width of the rectangle.

+</dd>

+<dt><code>h</code></dt>

+<dd>

+Height of the rectangle.

+</dd>

+<dt><code>link</code></dt>

+<dd>

+URL or identifier returned by AddLink().

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="addlink.htm">AddLink</a>,

+<a href="cell.htm">Cell</a>,

+<a href="write.htm">Write</a>,

+<a href="image.htm">Image</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/ln.htm b/src/lib/fpdf/doc/ln.htm
new file mode 100644
index 0000000..871e507
--- /dev/null
+++ b/src/lib/fpdf/doc/ln.htm
@@ -0,0 +1,28 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>Ln</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>Ln</h1>

+<code>Ln([<b>float</b> h])</code>

+<h2>Description</h2>

+Performs a line break. The current abscissa goes back to the left margin and the ordinate

+increases by the amount passed in parameter.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>h</code></dt>

+<dd>

+The height of the break.

+<br>

+By default, the value equals the height of the last printed cell.

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="cell.htm">Cell</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/multicell.htm b/src/lib/fpdf/doc/multicell.htm
new file mode 100644
index 0000000..5361670
--- /dev/null
+++ b/src/lib/fpdf/doc/multicell.htm
@@ -0,0 +1,76 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>MultiCell</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>MultiCell</h1>

+<code>MultiCell(<b>float</b> w, <b>float</b> h, <b>string</b> txt [, <b>mixed</b> border [, <b>string</b> align [, <b>boolean</b> fill]]])</code>

+<h2>Description</h2>

+This method allows printing text with line breaks. They can be automatic (as soon as the

+text reaches the right border of the cell) or explicit (via the \n character). As many cells

+as necessary are output, one below the other.

+<br>

+Text can be aligned, centered or justified. The cell block can be framed and the background

+painted.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>w</code></dt>

+<dd>

+Width of cells. If <code>0</code>, they extend up to the right margin of the page.

+</dd>

+<dt><code>h</code></dt>

+<dd>

+Height of cells.

+</dd>

+<dt><code>txt</code></dt>

+<dd>

+String to print.

+</dd>

+<dt><code>border</code></dt>

+<dd>

+Indicates if borders must be drawn around the cell block. The value can be either a number:

+<ul>

+<li><code>0</code>: no border</li>

+<li><code>1</code>: frame</li>

+</ul>

+or a string containing some or all of the following characters (in any order):

+<ul>

+<li><code>L</code>: left</li>

+<li><code>T</code>: top</li>

+<li><code>R</code>: right</li>

+<li><code>B</code>: bottom</li>

+</ul>

+Default value: <code>0</code>.

+</dd>

+<dt><code>align</code></dt>

+<dd>

+Sets the text alignment. Possible values are:

+<ul>

+<li><code>L</code>: left alignment</li>

+<li><code>C</code>: center</li>

+<li><code>R</code>: right alignment</li>

+<li><code>J</code>: justification (default value)</li>

+</ul>

+</dd>

+<dt><code>fill</code></dt>

+<dd>

+Indicates if the cell background must be painted (<code>true</code>) or transparent (<code>false</code>).

+Default value: <code>false</code>.

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="setfont.htm">SetFont</a>,

+<a href="setdrawcolor.htm">SetDrawColor</a>,

+<a href="setfillcolor.htm">SetFillColor</a>,

+<a href="settextcolor.htm">SetTextColor</a>,

+<a href="setlinewidth.htm">SetLineWidth</a>,

+<a href="cell.htm">Cell</a>,

+<a href="write.htm">Write</a>,

+<a href="setautopagebreak.htm">SetAutoPageBreak</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/output.htm b/src/lib/fpdf/doc/output.htm
new file mode 100644
index 0000000..7204bc2
--- /dev/null
+++ b/src/lib/fpdf/doc/output.htm
@@ -0,0 +1,55 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>Output</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>Output</h1>

+<code><b>string</b> Output([<b>string</b> dest [, <b>string</b> name [, <b>boolean</b> isUTF8]]])</code>

+<h2>Description</h2>

+Send the document to a given destination: browser, file or string. In the case of a browser, the

+PDF viewer may be used or a download may be forced.

+<br>

+The method first calls Close() if necessary to terminate the document.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>dest</code></dt>

+<dd>

+Destination where to send the document. It can be one of the following:

+<ul>

+<li><code>I</code>: send the file inline to the browser. The PDF viewer is used if available.</li>

+<li><code>D</code>: send to the browser and force a file download with the name given by <code>name</code>.</li>

+<li><code>F</code>: save to a local file with the name given by <code>name</code> (may include a path).</li>

+<li><code>S</code>: return the document as a string.</li>

+</ul>

+The default value is <code>I</code>.

+</dd>

+<dt><code>name</code></dt>

+<dd>

+The name of the file. It is ignored in case of destination <code>S</code>.<br>

+The default value is <code>doc.pdf</code>.

+</dd>

+<dt><code>isUTF8</code></dt>

+<dd>

+Indicates if <code>name</code> is encoded in ISO-8859-1 (<code>false</code>) or UTF-8 (<code>true</code>).

+Only used for destinations <code>I</code> and <code>D</code>.<br>

+The default value is <code>false</code>.

+</dd>

+</dl>

+<h2>Example</h2>

+Save the document to a local directory:

+<div class="doc-source">

+<pre><code>$pdf-&gt;Output('F', 'reports/report.pdf');</code></pre>

+</div>

+Force a download:

+<div class="doc-source">

+<pre><code>$pdf-&gt;Output('D', 'report.pdf');</code></pre>

+</div>

+<h2>See also</h2>

+<a href="close.htm">Close</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/pageno.htm b/src/lib/fpdf/doc/pageno.htm
new file mode 100644
index 0000000..cfbea9f
--- /dev/null
+++ b/src/lib/fpdf/doc/pageno.htm
@@ -0,0 +1,18 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>PageNo</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>PageNo</h1>

+<code><b>int</b> PageNo()</code>

+<h2>Description</h2>

+Returns the current page number.

+<h2>See also</h2>

+<a href="aliasnbpages.htm">AliasNbPages</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/rect.htm b/src/lib/fpdf/doc/rect.htm
new file mode 100644
index 0000000..7f2e5b8
--- /dev/null
+++ b/src/lib/fpdf/doc/rect.htm
@@ -0,0 +1,48 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>Rect</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>Rect</h1>

+<code>Rect(<b>float</b> x, <b>float</b> y, <b>float</b> w, <b>float</b> h [, <b>string</b> style])</code>

+<h2>Description</h2>

+Outputs a rectangle. It can be drawn (border only), filled (with no border) or both.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>x</code></dt>

+<dd>

+Abscissa of upper-left corner.

+</dd>

+<dt><code>y</code></dt>

+<dd>

+Ordinate of upper-left corner.

+</dd>

+<dt><code>w</code></dt>

+<dd>

+Width.

+</dd>

+<dt><code>h</code></dt>

+<dd>

+Height.

+</dd>

+<dt><code>style</code></dt>

+<dd>

+Style of rendering. Possible values are:

+<ul>

+<li><code>D</code> or empty string: draw. This is the default value.</li>

+<li><code>F</code>: fill</li>

+<li><code>DF</code> or <code>FD</code>: draw and fill</li>

+</ul>

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="setlinewidth.htm">SetLineWidth</a>,

+<a href="setdrawcolor.htm">SetDrawColor</a>,

+<a href="setfillcolor.htm">SetFillColor</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/setauthor.htm b/src/lib/fpdf/doc/setauthor.htm
new file mode 100644
index 0000000..3406d1b
--- /dev/null
+++ b/src/lib/fpdf/doc/setauthor.htm
@@ -0,0 +1,33 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>SetAuthor</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>SetAuthor</h1>

+<code>SetAuthor(<b>string</b> author [, <b>boolean</b> isUTF8])</code>

+<h2>Description</h2>

+Defines the author of the document.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>author</code></dt>

+<dd>

+The name of the author.

+</dd>

+<dt><code>isUTF8</code></dt>

+<dd>

+Indicates if the string is encoded in ISO-8859-1 (<code>false</code>) or UTF-8 (<code>true</code>).<br>

+Default value: <code>false</code>.

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="setcreator.htm">SetCreator</a>,

+<a href="setkeywords.htm">SetKeywords</a>,

+<a href="setsubject.htm">SetSubject</a>,

+<a href="settitle.htm">SetTitle</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/setautopagebreak.htm b/src/lib/fpdf/doc/setautopagebreak.htm
new file mode 100644
index 0000000..ae598d0
--- /dev/null
+++ b/src/lib/fpdf/doc/setautopagebreak.htm
@@ -0,0 +1,33 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>SetAutoPageBreak</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>SetAutoPageBreak</h1>

+<code>SetAutoPageBreak(<b>boolean</b> auto [, <b>float</b> margin])</code>

+<h2>Description</h2>

+Enables or disables the automatic page breaking mode. When enabling, the second parameter is

+the distance from the bottom of the page that defines the triggering limit. By default, the

+mode is on and the margin is 2 cm.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>auto</code></dt>

+<dd>

+Boolean indicating if mode should be on or off.

+</dd>

+<dt><code>margin</code></dt>

+<dd>

+Distance from the bottom of the page.

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="cell.htm">Cell</a>,

+<a href="multicell.htm">MultiCell</a>,

+<a href="acceptpagebreak.htm">AcceptPageBreak</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/setcompression.htm b/src/lib/fpdf/doc/setcompression.htm
new file mode 100644
index 0000000..d60c905
--- /dev/null
+++ b/src/lib/fpdf/doc/setcompression.htm
@@ -0,0 +1,31 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>SetCompression</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>SetCompression</h1>

+<code>SetCompression(<b>boolean</b> compress)</code>

+<h2>Description</h2>

+Activates or deactivates page compression. When activated, the internal representation of

+each page is compressed, which leads to a compression ratio of about 2 for the resulting

+document.

+<br>

+Compression is on by default.

+<br>

+<br>

+<strong>Note:</strong> the Zlib extension is required for this feature. If not present, compression

+will be turned off.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>compress</code></dt>

+<dd>

+Boolean indicating if compression must be enabled.

+</dd>

+</dl>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/setcreator.htm b/src/lib/fpdf/doc/setcreator.htm
new file mode 100644
index 0000000..d19ddde
--- /dev/null
+++ b/src/lib/fpdf/doc/setcreator.htm
@@ -0,0 +1,34 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>SetCreator</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>SetCreator</h1>

+<code>SetCreator(<b>string</b> creator [, <b>boolean</b> isUTF8])</code>

+<h2>Description</h2>

+Defines the creator of the document. This is typically the name of the application that

+generates the PDF.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>creator</code></dt>

+<dd>

+The name of the creator.

+</dd>

+<dt><code>isUTF8</code></dt>

+<dd>

+Indicates if the string is encoded in ISO-8859-1 (<code>false</code>) or UTF-8 (<code>true</code>).<br>

+Default value: <code>false</code>.

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="setauthor.htm">SetAuthor</a>,

+<a href="setkeywords.htm">SetKeywords</a>,

+<a href="setsubject.htm">SetSubject</a>,

+<a href="settitle.htm">SetTitle</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/setdisplaymode.htm b/src/lib/fpdf/doc/setdisplaymode.htm
new file mode 100644
index 0000000..5861581
--- /dev/null
+++ b/src/lib/fpdf/doc/setdisplaymode.htm
@@ -0,0 +1,45 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>SetDisplayMode</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>SetDisplayMode</h1>

+<code>SetDisplayMode(<b>mixed</b> zoom [, <b>string</b> layout])</code>

+<h2>Description</h2>

+Defines the way the document is to be displayed by the viewer. The zoom level can be set: pages can be

+displayed entirely on screen, occupy the full width of the window, use real size, be scaled by a

+specific zooming factor or use viewer default (configured in the Preferences menu of Adobe Reader).

+The page layout can be specified too: single at once, continuous display, two columns or viewer

+default.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>zoom</code></dt>

+<dd>

+The zoom to use. It can be one of the following string values:

+<ul>

+<li><code>fullpage</code>: displays the entire page on screen</li>

+<li><code>fullwidth</code>: uses maximum width of window</li>

+<li><code>real</code>: uses real size (equivalent to 100% zoom)</li>

+<li><code>default</code>: uses viewer default mode</li>

+</ul>

+or a number indicating the zooming factor to use.

+</dd>

+<dt><code>layout</code></dt>

+<dd>

+The page layout. Possible values are:

+<ul>

+<li><code>single</code>: displays one page at once</li>

+<li><code>continuous</code>: displays pages continuously</li>

+<li><code>two</code>: displays two pages on two columns</li>

+<li><code>default</code>: uses viewer default mode</li>

+</ul>

+Default value is <code>default</code>.

+</dd>

+</dl>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/setdrawcolor.htm b/src/lib/fpdf/doc/setdrawcolor.htm
new file mode 100644
index 0000000..42ce061
--- /dev/null
+++ b/src/lib/fpdf/doc/setdrawcolor.htm
@@ -0,0 +1,41 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>SetDrawColor</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>SetDrawColor</h1>

+<code>SetDrawColor(<b>int</b> r [, <b>int</b> g, <b>int</b> b])</code>

+<h2>Description</h2>

+Defines the color used for all drawing operations (lines, rectangles and cell borders). It

+can be expressed in RGB components or gray scale. The method can be called before the first

+page is created and the value is retained from page to page.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>r</code></dt>

+<dd>

+If <code>g</code> et <code>b</code> are given, red component; if not, indicates the gray level.

+Value between 0 and 255.

+</dd>

+<dt><code>g</code></dt>

+<dd>

+Green component (between 0 and 255).

+</dd>

+<dt><code>b</code></dt>

+<dd>

+Blue component (between 0 and 255).

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="setfillcolor.htm">SetFillColor</a>,

+<a href="settextcolor.htm">SetTextColor</a>,

+<a href="line.htm">Line</a>,

+<a href="rect.htm">Rect</a>,

+<a href="cell.htm">Cell</a>,

+<a href="multicell.htm">MultiCell</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/setfillcolor.htm b/src/lib/fpdf/doc/setfillcolor.htm
new file mode 100644
index 0000000..df519a5
--- /dev/null
+++ b/src/lib/fpdf/doc/setfillcolor.htm
@@ -0,0 +1,40 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>SetFillColor</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>SetFillColor</h1>

+<code>SetFillColor(<b>int</b> r [, <b>int</b> g, <b>int</b> b])</code>

+<h2>Description</h2>

+Defines the color used for all filling operations (filled rectangles and cell backgrounds).

+It can be expressed in RGB components or gray scale. The method can be called before the first

+page is created and the value is retained from page to page.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>r</code></dt>

+<dd>

+If <code>g</code> and <code>b</code> are given, red component; if not, indicates the gray level.

+Value between 0 and 255.

+</dd>

+<dt><code>g</code></dt>

+<dd>

+Green component (between 0 and 255).

+</dd>

+<dt><code>b</code></dt>

+<dd>

+Blue component (between 0 and 255).

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="setdrawcolor.htm">SetDrawColor</a>,

+<a href="settextcolor.htm">SetTextColor</a>,

+<a href="rect.htm">Rect</a>,

+<a href="cell.htm">Cell</a>,

+<a href="multicell.htm">MultiCell</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/setfont.htm b/src/lib/fpdf/doc/setfont.htm
new file mode 100644
index 0000000..d7fb73c
--- /dev/null
+++ b/src/lib/fpdf/doc/setfont.htm
@@ -0,0 +1,78 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>SetFont</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>SetFont</h1>

+<code>SetFont(<b>string</b> family [, <b>string</b> style [, <b>float</b> size]])</code>

+<h2>Description</h2>

+Sets the font used to print character strings. It is mandatory to call this method at least once before printing text.

+<br>

+<br>

+The font can be either a standard one or a font added by the AddFont() method. Standard fonts

+use the Windows encoding cp1252 (Western Europe).

+<br>

+<br>

+The method can be called before the first page is created and the font is kept from page to page.

+<br>

+<br>

+If you just wish to change the current font size, it is simpler to call SetFontSize().

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>family</code></dt>

+<dd>

+Family font. It can be either a name defined by AddFont() or one of the standard families (case

+insensitive):

+<ul>

+<li><code>Courier</code> (fixed-width)</li>

+<li><code>Helvetica</code> or <code>Arial</code> (synonymous; sans serif)</li>

+<li><code>Times</code> (serif)</li>

+<li><code>Symbol</code> (symbolic)</li>

+<li><code>ZapfDingbats</code> (symbolic)</li>

+</ul>

+It is also possible to pass an empty string. In that case, the current family is kept.

+</dd>

+<dt><code>style</code></dt>

+<dd>

+Font style. Possible values are (case insensitive):

+<ul>

+<li>empty string: regular</li>

+<li><code>B</code>: bold</li>

+<li><code>I</code>: italic</li>

+<li><code>U</code>: underline</li>

+</ul>

+or any combination. The default value is regular.

+Bold and italic styles do not apply to <code>Symbol</code> and <code>ZapfDingbats</code>.

+</dd>

+<dt><code>size</code></dt>

+<dd>

+Font size in points.

+<br>

+The default value is the current size. If no size has been specified since the beginning of

+the document, the value is 12.

+</dd>

+</dl>

+<h2>Example</h2>

+<div class="doc-source">

+<pre><code>// Times regular 12

+$pdf-&gt;SetFont('Times');

+// Arial bold 14

+$pdf-&gt;SetFont('Arial', 'B', 14);

+// Removes bold

+$pdf-&gt;SetFont('');

+// Times bold, italic and underlined 14

+$pdf-&gt;SetFont('Times', 'BIU');</code></pre>

+</div>

+<h2>See also</h2>

+<a href="addfont.htm">AddFont</a>,

+<a href="setfontsize.htm">SetFontSize</a>,

+<a href="cell.htm">Cell</a>,

+<a href="multicell.htm">MultiCell</a>,

+<a href="write.htm">Write</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/setfontsize.htm b/src/lib/fpdf/doc/setfontsize.htm
new file mode 100644
index 0000000..a5badd4
--- /dev/null
+++ b/src/lib/fpdf/doc/setfontsize.htm
@@ -0,0 +1,25 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>SetFontSize</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>SetFontSize</h1>

+<code>SetFontSize(<b>float</b> size)</code>

+<h2>Description</h2>

+Defines the size of the current font.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>size</code></dt>

+<dd>

+The size (in points).

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="setfont.htm">SetFont</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/setkeywords.htm b/src/lib/fpdf/doc/setkeywords.htm
new file mode 100644
index 0000000..647f12f
--- /dev/null
+++ b/src/lib/fpdf/doc/setkeywords.htm
@@ -0,0 +1,33 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>SetKeywords</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>SetKeywords</h1>

+<code>SetKeywords(<b>string</b> keywords [, <b>boolean</b> isUTF8])</code>

+<h2>Description</h2>

+Associates keywords with the document, generally in the form 'keyword1 keyword2 ...'.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>keywords</code></dt>

+<dd>

+The list of keywords.

+</dd>

+<dt><code>isUTF8</code></dt>

+<dd>

+Indicates if the string is encoded in ISO-8859-1 (<code>false</code>) or UTF-8 (<code>true</code>).<br>

+Default value: <code>false</code>.

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="setauthor.htm">SetAuthor</a>,

+<a href="setcreator.htm">SetCreator</a>,

+<a href="setsubject.htm">SetSubject</a>,

+<a href="settitle.htm">SetTitle</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/setleftmargin.htm b/src/lib/fpdf/doc/setleftmargin.htm
new file mode 100644
index 0000000..d8d2f3b
--- /dev/null
+++ b/src/lib/fpdf/doc/setleftmargin.htm
@@ -0,0 +1,30 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>SetLeftMargin</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>SetLeftMargin</h1>

+<code>SetLeftMargin(<b>float</b> margin)</code>

+<h2>Description</h2>

+Defines the left margin. The method can be called before creating the first page.

+<br>

+If the current abscissa gets out of page, it is brought back to the margin.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>margin</code></dt>

+<dd>

+The margin.

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="settopmargin.htm">SetTopMargin</a>,

+<a href="setrightmargin.htm">SetRightMargin</a>,

+<a href="setautopagebreak.htm">SetAutoPageBreak</a>,

+<a href="setmargins.htm">SetMargins</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/setlinewidth.htm b/src/lib/fpdf/doc/setlinewidth.htm
new file mode 100644
index 0000000..92b18f8
--- /dev/null
+++ b/src/lib/fpdf/doc/setlinewidth.htm
@@ -0,0 +1,29 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>SetLineWidth</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>SetLineWidth</h1>

+<code>SetLineWidth(<b>float</b> width)</code>

+<h2>Description</h2>

+Defines the line width. By default, the value equals 0.2 mm. The method can be called before

+the first page is created and the value is retained from page to page.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>width</code></dt>

+<dd>

+The width.

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="line.htm">Line</a>,

+<a href="rect.htm">Rect</a>,

+<a href="cell.htm">Cell</a>,

+<a href="multicell.htm">MultiCell</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/setlink.htm b/src/lib/fpdf/doc/setlink.htm
new file mode 100644
index 0000000..443517b
--- /dev/null
+++ b/src/lib/fpdf/doc/setlink.htm
@@ -0,0 +1,34 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>SetLink</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>SetLink</h1>

+<code>SetLink(<b>int</b> link [, <b>float</b> y [, <b>int</b> page]])</code>

+<h2>Description</h2>

+Defines the page and position a link points to.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>link</code></dt>

+<dd>

+The link identifier returned by AddLink().

+</dd>

+<dt><code>y</code></dt>

+<dd>

+Ordinate of target position; <code>-1</code> indicates the current position.

+The default value is <code>0</code> (top of page).

+</dd>

+<dt><code>page</code></dt>

+<dd>

+Number of target page; <code>-1</code> indicates the current page. This is the default value.

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="addlink.htm">AddLink</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/setmargins.htm b/src/lib/fpdf/doc/setmargins.htm
new file mode 100644
index 0000000..dc9b9ee
--- /dev/null
+++ b/src/lib/fpdf/doc/setmargins.htm
@@ -0,0 +1,37 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>SetMargins</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>SetMargins</h1>

+<code>SetMargins(<b>float</b> left, <b>float</b> top [, <b>float</b> right])</code>

+<h2>Description</h2>

+Defines the left, top and right margins. By default, they equal 1 cm. Call this method to change

+them.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>left</code></dt>

+<dd>

+Left margin.

+</dd>

+<dt><code>top</code></dt>

+<dd>

+Top margin.

+</dd>

+<dt><code>right</code></dt>

+<dd>

+Right margin. Default value is the left one.

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="setleftmargin.htm">SetLeftMargin</a>,

+<a href="settopmargin.htm">SetTopMargin</a>,

+<a href="setrightmargin.htm">SetRightMargin</a>,

+<a href="setautopagebreak.htm">SetAutoPageBreak</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/setrightmargin.htm b/src/lib/fpdf/doc/setrightmargin.htm
new file mode 100644
index 0000000..1c45166
--- /dev/null
+++ b/src/lib/fpdf/doc/setrightmargin.htm
@@ -0,0 +1,28 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>SetRightMargin</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>SetRightMargin</h1>

+<code>SetRightMargin(<b>float</b> margin)</code>

+<h2>Description</h2>

+Defines the right margin. The method can be called before creating the first page.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>margin</code></dt>

+<dd>

+The margin.

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="setleftmargin.htm">SetLeftMargin</a>,

+<a href="settopmargin.htm">SetTopMargin</a>,

+<a href="setautopagebreak.htm">SetAutoPageBreak</a>,

+<a href="setmargins.htm">SetMargins</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/setsubject.htm b/src/lib/fpdf/doc/setsubject.htm
new file mode 100644
index 0000000..c4643f1
--- /dev/null
+++ b/src/lib/fpdf/doc/setsubject.htm
@@ -0,0 +1,33 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>SetSubject</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>SetSubject</h1>

+<code>SetSubject(<b>string</b> subject [, <b>boolean</b> isUTF8])</code>

+<h2>Description</h2>

+Defines the subject of the document.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>subject</code></dt>

+<dd>

+The subject.

+</dd>

+<dt><code>isUTF8</code></dt>

+<dd>

+Indicates if the string is encoded in ISO-8859-1 (<code>false</code>) or UTF-8 (<code>true</code>).<br>

+Default value: <code>false</code>.

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="setauthor.htm">SetAuthor</a>,

+<a href="setcreator.htm">SetCreator</a>,

+<a href="setkeywords.htm">SetKeywords</a>,

+<a href="settitle.htm">SetTitle</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/settextcolor.htm b/src/lib/fpdf/doc/settextcolor.htm
new file mode 100644
index 0000000..a7fbd97
--- /dev/null
+++ b/src/lib/fpdf/doc/settextcolor.htm
@@ -0,0 +1,40 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>SetTextColor</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>SetTextColor</h1>

+<code>SetTextColor(<b>int</b> r [, <b>int</b> g, <b>int</b> b])</code>

+<h2>Description</h2>

+Defines the color used for text. It can be expressed in RGB components or gray scale. The

+method can be called before the first page is created and the value is retained from page to

+page.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>r</code></dt>

+<dd>

+If <code>g</code> et <code>b</code> are given, red component; if not, indicates the gray level.

+Value between 0 and 255.

+</dd>

+<dt><code>g</code></dt>

+<dd>

+Green component (between 0 and 255).

+</dd>

+<dt><code>b</code></dt>

+<dd>

+Blue component (between 0 and 255).

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="setdrawcolor.htm">SetDrawColor</a>,

+<a href="setfillcolor.htm">SetFillColor</a>,

+<a href="text.htm">Text</a>,

+<a href="cell.htm">Cell</a>,

+<a href="multicell.htm">MultiCell</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/settitle.htm b/src/lib/fpdf/doc/settitle.htm
new file mode 100644
index 0000000..b8b6925
--- /dev/null
+++ b/src/lib/fpdf/doc/settitle.htm
@@ -0,0 +1,33 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>SetTitle</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>SetTitle</h1>

+<code>SetTitle(<b>string</b> title [, <b>boolean</b> isUTF8])</code>

+<h2>Description</h2>

+Defines the title of the document.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>title</code></dt>

+<dd>

+The title.

+</dd>

+<dt><code>isUTF8</code></dt>

+<dd>

+Indicates if the string is encoded in ISO-8859-1 (<code>false</code>) or UTF-8 (<code>true</code>).<br>

+Default value: <code>false</code>.

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="setauthor.htm">SetAuthor</a>,

+<a href="setcreator.htm">SetCreator</a>,

+<a href="setkeywords.htm">SetKeywords</a>,

+<a href="setsubject.htm">SetSubject</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/settopmargin.htm b/src/lib/fpdf/doc/settopmargin.htm
new file mode 100644
index 0000000..46a7952
--- /dev/null
+++ b/src/lib/fpdf/doc/settopmargin.htm
@@ -0,0 +1,28 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>SetTopMargin</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>SetTopMargin</h1>

+<code>SetTopMargin(<b>float</b> margin)</code>

+<h2>Description</h2>

+Defines the top margin. The method can be called before creating the first page.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>margin</code></dt>

+<dd>

+The margin.

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="setleftmargin.htm">SetLeftMargin</a>,

+<a href="setrightmargin.htm">SetRightMargin</a>,

+<a href="setautopagebreak.htm">SetAutoPageBreak</a>,

+<a href="setmargins.htm">SetMargins</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/setx.htm b/src/lib/fpdf/doc/setx.htm
new file mode 100644
index 0000000..282898c
--- /dev/null
+++ b/src/lib/fpdf/doc/setx.htm
@@ -0,0 +1,29 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>SetX</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>SetX</h1>

+<code>SetX(<b>float</b> x)</code>

+<h2>Description</h2>

+Defines the abscissa of the current position. If the passed value is negative, it is relative

+to the right of the page.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>x</code></dt>

+<dd>

+The value of the abscissa.

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="getx.htm">GetX</a>,

+<a href="gety.htm">GetY</a>,

+<a href="sety.htm">SetY</a>,

+<a href="setxy.htm">SetXY</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/setxy.htm b/src/lib/fpdf/doc/setxy.htm
new file mode 100644
index 0000000..36eee62
--- /dev/null
+++ b/src/lib/fpdf/doc/setxy.htm
@@ -0,0 +1,31 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>SetXY</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>SetXY</h1>

+<code>SetXY(<b>float</b> x, <b>float</b> y)</code>

+<h2>Description</h2>

+Defines the abscissa and ordinate of the current position. If the passed values are negative,

+they are relative respectively to the right and bottom of the page.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>x</code></dt>

+<dd>

+The value of the abscissa.

+</dd>

+<dt><code>y</code></dt>

+<dd>

+The value of the ordinate.

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="setx.htm">SetX</a>,

+<a href="sety.htm">SetY</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/sety.htm b/src/lib/fpdf/doc/sety.htm
new file mode 100644
index 0000000..19a4451
--- /dev/null
+++ b/src/lib/fpdf/doc/sety.htm
@@ -0,0 +1,33 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>SetY</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>SetY</h1>

+<code>SetY(<b>float</b> y [, <b>boolean</b> resetX])</code>

+<h2>Description</h2>

+Sets the ordinate and optionally moves the current abscissa back to the left margin. If the value

+is negative, it is relative to the bottom of the page.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>y</code></dt>

+<dd>

+The value of the ordinate.

+</dd>

+<dt><code>resetX</code></dt>

+<dd>

+Whether to reset the abscissa. Default value: <code>true</code>.

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="getx.htm">GetX</a>,

+<a href="gety.htm">GetY</a>,

+<a href="setx.htm">SetX</a>,

+<a href="setxy.htm">SetXY</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/text.htm b/src/lib/fpdf/doc/text.htm
new file mode 100644
index 0000000..11d5600
--- /dev/null
+++ b/src/lib/fpdf/doc/text.htm
@@ -0,0 +1,39 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>Text</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>Text</h1>

+<code>Text(<b>float</b> x, <b>float</b> y, <b>string</b> txt)</code>

+<h2>Description</h2>

+Prints a character string. The origin is on the left of the first character, on the baseline.

+This method allows to place a string precisely on the page, but it is usually easier to use

+Cell(), MultiCell() or Write() which are the standard methods to print text.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>x</code></dt>

+<dd>

+Abscissa of the origin.

+</dd>

+<dt><code>y</code></dt>

+<dd>

+Ordinate of the origin.

+</dd>

+<dt><code>txt</code></dt>

+<dd>

+String to print.

+</dd>

+</dl>

+<h2>See also</h2>

+<a href="setfont.htm">SetFont</a>,

+<a href="settextcolor.htm">SetTextColor</a>,

+<a href="cell.htm">Cell</a>,

+<a href="multicell.htm">MultiCell</a>,

+<a href="write.htm">Write</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/doc/write.htm b/src/lib/fpdf/doc/write.htm
new file mode 100644
index 0000000..9653540
--- /dev/null
+++ b/src/lib/fpdf/doc/write.htm
@@ -0,0 +1,51 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">

+<html>

+<head>

+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

+<title>Write</title>

+<link type="text/css" rel="stylesheet" href="../fpdf.css">

+</head>

+<body>

+<h1>Write</h1>

+<code>Write(<b>float</b> h, <b>string</b> txt [, <b>mixed</b> link])</code>

+<h2>Description</h2>

+This method prints text from the current position. When the right margin is reached (or the \n

+character is met) a line break occurs and text continues from the left margin. Upon method exit,

+the current position is left just at the end of the text.

+<br>

+It is possible to put a link on the text.

+<h2>Parameters</h2>

+<dl class="param">

+<dt><code>h</code></dt>

+<dd>

+Line height.

+</dd>

+<dt><code>txt</code></dt>

+<dd>

+String to print.

+</dd>

+<dt><code>link</code></dt>

+<dd>

+URL or identifier returned by AddLink().

+</dd>

+</dl>

+<h2>Example</h2>

+<div class="doc-source">

+<pre><code>// Begin with regular font

+$pdf-&gt;SetFont('Arial', '', 14);

+$pdf-&gt;Write(5, 'Visit ');

+// Then put a blue underlined link

+$pdf-&gt;SetTextColor(0, 0, 255);

+$pdf-&gt;SetFont('', 'U');

+$pdf-&gt;Write(5, 'www.fpdf.org', 'http://www.fpdf.org');</code></pre>

+</div>

+<h2>See also</h2>

+<a href="setfont.htm">SetFont</a>,

+<a href="settextcolor.htm">SetTextColor</a>,

+<a href="addlink.htm">AddLink</a>,

+<a href="multicell.htm">MultiCell</a>,

+<a href="setautopagebreak.htm">SetAutoPageBreak</a>

+<hr style="margin-top:1.5em">

+<div style="text-align:center"><a href="index.htm">Index</a></div>

+</body>

+</html>

diff --git a/src/lib/fpdf/font/courier.php b/src/lib/fpdf/font/courier.php
new file mode 100644
index 0000000..67dbeda
--- /dev/null
+++ b/src/lib/fpdf/font/courier.php
@@ -0,0 +1,10 @@
+<?php

+$type = 'Core';

+$name = 'Courier';

+$up = -100;

+$ut = 50;

+for($i=0;$i<=255;$i++)

+	$cw[chr($i)] = 600;

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf/font/courierb.php b/src/lib/fpdf/font/courierb.php
new file mode 100644
index 0000000..62550a4
--- /dev/null
+++ b/src/lib/fpdf/font/courierb.php
@@ -0,0 +1,10 @@
+<?php

+$type = 'Core';

+$name = 'Courier-Bold';

+$up = -100;

+$ut = 50;

+for($i=0;$i<=255;$i++)

+	$cw[chr($i)] = 600;

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf/font/courierbi.php b/src/lib/fpdf/font/courierbi.php
new file mode 100644
index 0000000..6a3ecc6
--- /dev/null
+++ b/src/lib/fpdf/font/courierbi.php
@@ -0,0 +1,10 @@
+<?php

+$type = 'Core';

+$name = 'Courier-BoldOblique';

+$up = -100;

+$ut = 50;

+for($i=0;$i<=255;$i++)

+	$cw[chr($i)] = 600;

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf/font/courieri.php b/src/lib/fpdf/font/courieri.php
new file mode 100644
index 0000000..b88e098
--- /dev/null
+++ b/src/lib/fpdf/font/courieri.php
@@ -0,0 +1,10 @@
+<?php

+$type = 'Core';

+$name = 'Courier-Oblique';

+$up = -100;

+$ut = 50;

+for($i=0;$i<=255;$i++)

+	$cw[chr($i)] = 600;

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf/font/helvetica.php b/src/lib/fpdf/font/helvetica.php
new file mode 100644
index 0000000..2be3eca
--- /dev/null
+++ b/src/lib/fpdf/font/helvetica.php
@@ -0,0 +1,21 @@
+<?php

+$type = 'Core';

+$name = 'Helvetica';

+$up = -100;

+$ut = 50;

+$cw = array(

+	chr(0)=>278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278,

+	chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>278,'"'=>355,'#'=>556,'$'=>556,'%'=>889,'&'=>667,'\''=>191,'('=>333,')'=>333,'*'=>389,'+'=>584,

+	','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>278,';'=>278,'<'=>584,'='=>584,'>'=>584,'?'=>556,'@'=>1015,'A'=>667,

+	'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>500,'K'=>667,'L'=>556,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944,

+	'X'=>667,'Y'=>667,'Z'=>611,'['=>278,'\\'=>278,']'=>278,'^'=>469,'_'=>556,'`'=>333,'a'=>556,'b'=>556,'c'=>500,'d'=>556,'e'=>556,'f'=>278,'g'=>556,'h'=>556,'i'=>222,'j'=>222,'k'=>500,'l'=>222,'m'=>833,

+	'n'=>556,'o'=>556,'p'=>556,'q'=>556,'r'=>333,'s'=>500,'t'=>278,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>500,'{'=>334,'|'=>260,'}'=>334,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>222,chr(131)=>556,

+	chr(132)=>333,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>222,chr(146)=>222,chr(147)=>333,chr(148)=>333,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000,

+	chr(154)=>500,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>260,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333,

+	chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>556,chr(182)=>537,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667,

+	chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722,

+	chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>500,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>556,chr(241)=>556,

+	chr(242)=>556,chr(243)=>556,chr(244)=>556,chr(245)=>556,chr(246)=>556,chr(247)=>584,chr(248)=>611,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500);

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf/font/helveticab.php b/src/lib/fpdf/font/helveticab.php
new file mode 100644
index 0000000..c88394c
--- /dev/null
+++ b/src/lib/fpdf/font/helveticab.php
@@ -0,0 +1,21 @@
+<?php

+$type = 'Core';

+$name = 'Helvetica-Bold';

+$up = -100;

+$ut = 50;

+$cw = array(

+	chr(0)=>278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278,

+	chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>333,'"'=>474,'#'=>556,'$'=>556,'%'=>889,'&'=>722,'\''=>238,'('=>333,')'=>333,'*'=>389,'+'=>584,

+	','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>333,';'=>333,'<'=>584,'='=>584,'>'=>584,'?'=>611,'@'=>975,'A'=>722,

+	'B'=>722,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>556,'K'=>722,'L'=>611,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944,

+	'X'=>667,'Y'=>667,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>584,'_'=>556,'`'=>333,'a'=>556,'b'=>611,'c'=>556,'d'=>611,'e'=>556,'f'=>333,'g'=>611,'h'=>611,'i'=>278,'j'=>278,'k'=>556,'l'=>278,'m'=>889,

+	'n'=>611,'o'=>611,'p'=>611,'q'=>611,'r'=>389,'s'=>556,'t'=>333,'u'=>611,'v'=>556,'w'=>778,'x'=>556,'y'=>556,'z'=>500,'{'=>389,'|'=>280,'}'=>389,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>278,chr(131)=>556,

+	chr(132)=>500,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>278,chr(146)=>278,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000,

+	chr(154)=>556,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>280,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333,

+	chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>611,chr(182)=>556,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722,

+	chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722,

+	chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>556,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>611,chr(241)=>611,

+	chr(242)=>611,chr(243)=>611,chr(244)=>611,chr(245)=>611,chr(246)=>611,chr(247)=>584,chr(248)=>611,chr(249)=>611,chr(250)=>611,chr(251)=>611,chr(252)=>611,chr(253)=>556,chr(254)=>611,chr(255)=>556);

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf/font/helveticabi.php b/src/lib/fpdf/font/helveticabi.php
new file mode 100644
index 0000000..bcea807
--- /dev/null
+++ b/src/lib/fpdf/font/helveticabi.php
@@ -0,0 +1,21 @@
+<?php

+$type = 'Core';

+$name = 'Helvetica-BoldOblique';

+$up = -100;

+$ut = 50;

+$cw = array(

+	chr(0)=>278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278,

+	chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>333,'"'=>474,'#'=>556,'$'=>556,'%'=>889,'&'=>722,'\''=>238,'('=>333,')'=>333,'*'=>389,'+'=>584,

+	','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>333,';'=>333,'<'=>584,'='=>584,'>'=>584,'?'=>611,'@'=>975,'A'=>722,

+	'B'=>722,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>556,'K'=>722,'L'=>611,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944,

+	'X'=>667,'Y'=>667,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>584,'_'=>556,'`'=>333,'a'=>556,'b'=>611,'c'=>556,'d'=>611,'e'=>556,'f'=>333,'g'=>611,'h'=>611,'i'=>278,'j'=>278,'k'=>556,'l'=>278,'m'=>889,

+	'n'=>611,'o'=>611,'p'=>611,'q'=>611,'r'=>389,'s'=>556,'t'=>333,'u'=>611,'v'=>556,'w'=>778,'x'=>556,'y'=>556,'z'=>500,'{'=>389,'|'=>280,'}'=>389,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>278,chr(131)=>556,

+	chr(132)=>500,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>278,chr(146)=>278,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000,

+	chr(154)=>556,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>280,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333,

+	chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>611,chr(182)=>556,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722,

+	chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722,

+	chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>556,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>611,chr(241)=>611,

+	chr(242)=>611,chr(243)=>611,chr(244)=>611,chr(245)=>611,chr(246)=>611,chr(247)=>584,chr(248)=>611,chr(249)=>611,chr(250)=>611,chr(251)=>611,chr(252)=>611,chr(253)=>556,chr(254)=>611,chr(255)=>556);

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf/font/helveticai.php b/src/lib/fpdf/font/helveticai.php
new file mode 100644
index 0000000..a328b04
--- /dev/null
+++ b/src/lib/fpdf/font/helveticai.php
@@ -0,0 +1,21 @@
+<?php

+$type = 'Core';

+$name = 'Helvetica-Oblique';

+$up = -100;

+$ut = 50;

+$cw = array(

+	chr(0)=>278,chr(1)=>278,chr(2)=>278,chr(3)=>278,chr(4)=>278,chr(5)=>278,chr(6)=>278,chr(7)=>278,chr(8)=>278,chr(9)=>278,chr(10)=>278,chr(11)=>278,chr(12)=>278,chr(13)=>278,chr(14)=>278,chr(15)=>278,chr(16)=>278,chr(17)=>278,chr(18)=>278,chr(19)=>278,chr(20)=>278,chr(21)=>278,

+	chr(22)=>278,chr(23)=>278,chr(24)=>278,chr(25)=>278,chr(26)=>278,chr(27)=>278,chr(28)=>278,chr(29)=>278,chr(30)=>278,chr(31)=>278,' '=>278,'!'=>278,'"'=>355,'#'=>556,'$'=>556,'%'=>889,'&'=>667,'\''=>191,'('=>333,')'=>333,'*'=>389,'+'=>584,

+	','=>278,'-'=>333,'.'=>278,'/'=>278,'0'=>556,'1'=>556,'2'=>556,'3'=>556,'4'=>556,'5'=>556,'6'=>556,'7'=>556,'8'=>556,'9'=>556,':'=>278,';'=>278,'<'=>584,'='=>584,'>'=>584,'?'=>556,'@'=>1015,'A'=>667,

+	'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>722,'I'=>278,'J'=>500,'K'=>667,'L'=>556,'M'=>833,'N'=>722,'O'=>778,'P'=>667,'Q'=>778,'R'=>722,'S'=>667,'T'=>611,'U'=>722,'V'=>667,'W'=>944,

+	'X'=>667,'Y'=>667,'Z'=>611,'['=>278,'\\'=>278,']'=>278,'^'=>469,'_'=>556,'`'=>333,'a'=>556,'b'=>556,'c'=>500,'d'=>556,'e'=>556,'f'=>278,'g'=>556,'h'=>556,'i'=>222,'j'=>222,'k'=>500,'l'=>222,'m'=>833,

+	'n'=>556,'o'=>556,'p'=>556,'q'=>556,'r'=>333,'s'=>500,'t'=>278,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>500,'{'=>334,'|'=>260,'}'=>334,'~'=>584,chr(127)=>350,chr(128)=>556,chr(129)=>350,chr(130)=>222,chr(131)=>556,

+	chr(132)=>333,chr(133)=>1000,chr(134)=>556,chr(135)=>556,chr(136)=>333,chr(137)=>1000,chr(138)=>667,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>222,chr(146)=>222,chr(147)=>333,chr(148)=>333,chr(149)=>350,chr(150)=>556,chr(151)=>1000,chr(152)=>333,chr(153)=>1000,

+	chr(154)=>500,chr(155)=>333,chr(156)=>944,chr(157)=>350,chr(158)=>500,chr(159)=>667,chr(160)=>278,chr(161)=>333,chr(162)=>556,chr(163)=>556,chr(164)=>556,chr(165)=>556,chr(166)=>260,chr(167)=>556,chr(168)=>333,chr(169)=>737,chr(170)=>370,chr(171)=>556,chr(172)=>584,chr(173)=>333,chr(174)=>737,chr(175)=>333,

+	chr(176)=>400,chr(177)=>584,chr(178)=>333,chr(179)=>333,chr(180)=>333,chr(181)=>556,chr(182)=>537,chr(183)=>278,chr(184)=>333,chr(185)=>333,chr(186)=>365,chr(187)=>556,chr(188)=>834,chr(189)=>834,chr(190)=>834,chr(191)=>611,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667,

+	chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>278,chr(205)=>278,chr(206)=>278,chr(207)=>278,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>584,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722,

+	chr(220)=>722,chr(221)=>667,chr(222)=>667,chr(223)=>611,chr(224)=>556,chr(225)=>556,chr(226)=>556,chr(227)=>556,chr(228)=>556,chr(229)=>556,chr(230)=>889,chr(231)=>500,chr(232)=>556,chr(233)=>556,chr(234)=>556,chr(235)=>556,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>556,chr(241)=>556,

+	chr(242)=>556,chr(243)=>556,chr(244)=>556,chr(245)=>556,chr(246)=>556,chr(247)=>584,chr(248)=>611,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500);

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf/font/symbol.php b/src/lib/fpdf/font/symbol.php
new file mode 100644
index 0000000..5b9147b
--- /dev/null
+++ b/src/lib/fpdf/font/symbol.php
@@ -0,0 +1,20 @@
+<?php

+$type = 'Core';

+$name = 'Symbol';

+$up = -100;

+$ut = 50;

+$cw = array(

+	chr(0)=>250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250,

+	chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>713,'#'=>500,'$'=>549,'%'=>833,'&'=>778,'\''=>439,'('=>333,')'=>333,'*'=>500,'+'=>549,

+	','=>250,'-'=>549,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>278,';'=>278,'<'=>549,'='=>549,'>'=>549,'?'=>444,'@'=>549,'A'=>722,

+	'B'=>667,'C'=>722,'D'=>612,'E'=>611,'F'=>763,'G'=>603,'H'=>722,'I'=>333,'J'=>631,'K'=>722,'L'=>686,'M'=>889,'N'=>722,'O'=>722,'P'=>768,'Q'=>741,'R'=>556,'S'=>592,'T'=>611,'U'=>690,'V'=>439,'W'=>768,

+	'X'=>645,'Y'=>795,'Z'=>611,'['=>333,'\\'=>863,']'=>333,'^'=>658,'_'=>500,'`'=>500,'a'=>631,'b'=>549,'c'=>549,'d'=>494,'e'=>439,'f'=>521,'g'=>411,'h'=>603,'i'=>329,'j'=>603,'k'=>549,'l'=>549,'m'=>576,

+	'n'=>521,'o'=>549,'p'=>549,'q'=>521,'r'=>549,'s'=>603,'t'=>439,'u'=>576,'v'=>713,'w'=>686,'x'=>493,'y'=>686,'z'=>494,'{'=>480,'|'=>200,'}'=>480,'~'=>549,chr(127)=>0,chr(128)=>0,chr(129)=>0,chr(130)=>0,chr(131)=>0,

+	chr(132)=>0,chr(133)=>0,chr(134)=>0,chr(135)=>0,chr(136)=>0,chr(137)=>0,chr(138)=>0,chr(139)=>0,chr(140)=>0,chr(141)=>0,chr(142)=>0,chr(143)=>0,chr(144)=>0,chr(145)=>0,chr(146)=>0,chr(147)=>0,chr(148)=>0,chr(149)=>0,chr(150)=>0,chr(151)=>0,chr(152)=>0,chr(153)=>0,

+	chr(154)=>0,chr(155)=>0,chr(156)=>0,chr(157)=>0,chr(158)=>0,chr(159)=>0,chr(160)=>750,chr(161)=>620,chr(162)=>247,chr(163)=>549,chr(164)=>167,chr(165)=>713,chr(166)=>500,chr(167)=>753,chr(168)=>753,chr(169)=>753,chr(170)=>753,chr(171)=>1042,chr(172)=>987,chr(173)=>603,chr(174)=>987,chr(175)=>603,

+	chr(176)=>400,chr(177)=>549,chr(178)=>411,chr(179)=>549,chr(180)=>549,chr(181)=>713,chr(182)=>494,chr(183)=>460,chr(184)=>549,chr(185)=>549,chr(186)=>549,chr(187)=>549,chr(188)=>1000,chr(189)=>603,chr(190)=>1000,chr(191)=>658,chr(192)=>823,chr(193)=>686,chr(194)=>795,chr(195)=>987,chr(196)=>768,chr(197)=>768,

+	chr(198)=>823,chr(199)=>768,chr(200)=>768,chr(201)=>713,chr(202)=>713,chr(203)=>713,chr(204)=>713,chr(205)=>713,chr(206)=>713,chr(207)=>713,chr(208)=>768,chr(209)=>713,chr(210)=>790,chr(211)=>790,chr(212)=>890,chr(213)=>823,chr(214)=>549,chr(215)=>250,chr(216)=>713,chr(217)=>603,chr(218)=>603,chr(219)=>1042,

+	chr(220)=>987,chr(221)=>603,chr(222)=>987,chr(223)=>603,chr(224)=>494,chr(225)=>329,chr(226)=>790,chr(227)=>790,chr(228)=>786,chr(229)=>713,chr(230)=>384,chr(231)=>384,chr(232)=>384,chr(233)=>384,chr(234)=>384,chr(235)=>384,chr(236)=>494,chr(237)=>494,chr(238)=>494,chr(239)=>494,chr(240)=>0,chr(241)=>329,

+	chr(242)=>274,chr(243)=>686,chr(244)=>686,chr(245)=>686,chr(246)=>384,chr(247)=>384,chr(248)=>384,chr(249)=>384,chr(250)=>384,chr(251)=>384,chr(252)=>494,chr(253)=>494,chr(254)=>494,chr(255)=>0);

+$uv = array(32=>160,33=>33,34=>8704,35=>35,36=>8707,37=>array(37,2),39=>8715,40=>array(40,2),42=>8727,43=>array(43,2),45=>8722,46=>array(46,18),64=>8773,65=>array(913,2),67=>935,68=>array(916,2),70=>934,71=>915,72=>919,73=>921,74=>977,75=>array(922,4),79=>array(927,2),81=>920,82=>929,83=>array(931,3),86=>962,87=>937,88=>926,89=>936,90=>918,91=>91,92=>8756,93=>93,94=>8869,95=>95,96=>63717,97=>array(945,2),99=>967,100=>array(948,2),102=>966,103=>947,104=>951,105=>953,106=>981,107=>array(954,4),111=>array(959,2),113=>952,114=>961,115=>array(963,3),118=>982,119=>969,120=>958,121=>968,122=>950,123=>array(123,3),126=>8764,160=>8364,161=>978,162=>8242,163=>8804,164=>8725,165=>8734,166=>402,167=>9827,168=>9830,169=>9829,170=>9824,171=>8596,172=>array(8592,4),176=>array(176,2),178=>8243,179=>8805,180=>215,181=>8733,182=>8706,183=>8226,184=>247,185=>array(8800,2),187=>8776,188=>8230,189=>array(63718,2),191=>8629,192=>8501,193=>8465,194=>8476,195=>8472,196=>8855,197=>8853,198=>8709,199=>array(8745,2),201=>8835,202=>8839,203=>8836,204=>8834,205=>8838,206=>array(8712,2),208=>8736,209=>8711,210=>63194,211=>63193,212=>63195,213=>8719,214=>8730,215=>8901,216=>172,217=>array(8743,2),219=>8660,220=>array(8656,4),224=>9674,225=>9001,226=>array(63720,3),229=>8721,230=>array(63723,10),241=>9002,242=>8747,243=>8992,244=>63733,245=>8993,246=>array(63734,9));

+?>

diff --git a/src/lib/fpdf/font/times.php b/src/lib/fpdf/font/times.php
new file mode 100644
index 0000000..f78850f
--- /dev/null
+++ b/src/lib/fpdf/font/times.php
@@ -0,0 +1,21 @@
+<?php

+$type = 'Core';

+$name = 'Times-Roman';

+$up = -100;

+$ut = 50;

+$cw = array(

+	chr(0)=>250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250,

+	chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>408,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>180,'('=>333,')'=>333,'*'=>500,'+'=>564,

+	','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>278,';'=>278,'<'=>564,'='=>564,'>'=>564,'?'=>444,'@'=>921,'A'=>722,

+	'B'=>667,'C'=>667,'D'=>722,'E'=>611,'F'=>556,'G'=>722,'H'=>722,'I'=>333,'J'=>389,'K'=>722,'L'=>611,'M'=>889,'N'=>722,'O'=>722,'P'=>556,'Q'=>722,'R'=>667,'S'=>556,'T'=>611,'U'=>722,'V'=>722,'W'=>944,

+	'X'=>722,'Y'=>722,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>469,'_'=>500,'`'=>333,'a'=>444,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>333,'g'=>500,'h'=>500,'i'=>278,'j'=>278,'k'=>500,'l'=>278,'m'=>778,

+	'n'=>500,'o'=>500,'p'=>500,'q'=>500,'r'=>333,'s'=>389,'t'=>278,'u'=>500,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>444,'{'=>480,'|'=>200,'}'=>480,'~'=>541,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500,

+	chr(132)=>444,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>889,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>444,chr(148)=>444,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>980,

+	chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>444,chr(159)=>722,chr(160)=>250,chr(161)=>333,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>200,chr(167)=>500,chr(168)=>333,chr(169)=>760,chr(170)=>276,chr(171)=>500,chr(172)=>564,chr(173)=>333,chr(174)=>760,chr(175)=>333,

+	chr(176)=>400,chr(177)=>564,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>500,chr(182)=>453,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>310,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>444,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722,

+	chr(198)=>889,chr(199)=>667,chr(200)=>611,chr(201)=>611,chr(202)=>611,chr(203)=>611,chr(204)=>333,chr(205)=>333,chr(206)=>333,chr(207)=>333,chr(208)=>722,chr(209)=>722,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>564,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722,

+	chr(220)=>722,chr(221)=>722,chr(222)=>556,chr(223)=>500,chr(224)=>444,chr(225)=>444,chr(226)=>444,chr(227)=>444,chr(228)=>444,chr(229)=>444,chr(230)=>667,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>500,

+	chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>564,chr(248)=>500,chr(249)=>500,chr(250)=>500,chr(251)=>500,chr(252)=>500,chr(253)=>500,chr(254)=>500,chr(255)=>500);

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf/font/timesb.php b/src/lib/fpdf/font/timesb.php
new file mode 100644
index 0000000..0516750
--- /dev/null
+++ b/src/lib/fpdf/font/timesb.php
@@ -0,0 +1,21 @@
+<?php

+$type = 'Core';

+$name = 'Times-Bold';

+$up = -100;

+$ut = 50;

+$cw = array(

+	chr(0)=>250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250,

+	chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>555,'#'=>500,'$'=>500,'%'=>1000,'&'=>833,'\''=>278,'('=>333,')'=>333,'*'=>500,'+'=>570,

+	','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>570,'='=>570,'>'=>570,'?'=>500,'@'=>930,'A'=>722,

+	'B'=>667,'C'=>722,'D'=>722,'E'=>667,'F'=>611,'G'=>778,'H'=>778,'I'=>389,'J'=>500,'K'=>778,'L'=>667,'M'=>944,'N'=>722,'O'=>778,'P'=>611,'Q'=>778,'R'=>722,'S'=>556,'T'=>667,'U'=>722,'V'=>722,'W'=>1000,

+	'X'=>722,'Y'=>722,'Z'=>667,'['=>333,'\\'=>278,']'=>333,'^'=>581,'_'=>500,'`'=>333,'a'=>500,'b'=>556,'c'=>444,'d'=>556,'e'=>444,'f'=>333,'g'=>500,'h'=>556,'i'=>278,'j'=>333,'k'=>556,'l'=>278,'m'=>833,

+	'n'=>556,'o'=>500,'p'=>556,'q'=>556,'r'=>444,'s'=>389,'t'=>333,'u'=>556,'v'=>500,'w'=>722,'x'=>500,'y'=>500,'z'=>444,'{'=>394,'|'=>220,'}'=>394,'~'=>520,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500,

+	chr(132)=>500,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>1000,chr(141)=>350,chr(142)=>667,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>1000,

+	chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>444,chr(159)=>722,chr(160)=>250,chr(161)=>333,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>220,chr(167)=>500,chr(168)=>333,chr(169)=>747,chr(170)=>300,chr(171)=>500,chr(172)=>570,chr(173)=>333,chr(174)=>747,chr(175)=>333,

+	chr(176)=>400,chr(177)=>570,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>556,chr(182)=>540,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>330,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>722,chr(193)=>722,chr(194)=>722,chr(195)=>722,chr(196)=>722,chr(197)=>722,

+	chr(198)=>1000,chr(199)=>722,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>389,chr(205)=>389,chr(206)=>389,chr(207)=>389,chr(208)=>722,chr(209)=>722,chr(210)=>778,chr(211)=>778,chr(212)=>778,chr(213)=>778,chr(214)=>778,chr(215)=>570,chr(216)=>778,chr(217)=>722,chr(218)=>722,chr(219)=>722,

+	chr(220)=>722,chr(221)=>722,chr(222)=>611,chr(223)=>556,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>722,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>556,

+	chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>570,chr(248)=>500,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>500,chr(254)=>556,chr(255)=>500);

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf/font/timesbi.php b/src/lib/fpdf/font/timesbi.php
new file mode 100644
index 0000000..32fe25e
--- /dev/null
+++ b/src/lib/fpdf/font/timesbi.php
@@ -0,0 +1,21 @@
+<?php

+$type = 'Core';

+$name = 'Times-BoldItalic';

+$up = -100;

+$ut = 50;

+$cw = array(

+	chr(0)=>250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250,

+	chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>389,'"'=>555,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>278,'('=>333,')'=>333,'*'=>500,'+'=>570,

+	','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>570,'='=>570,'>'=>570,'?'=>500,'@'=>832,'A'=>667,

+	'B'=>667,'C'=>667,'D'=>722,'E'=>667,'F'=>667,'G'=>722,'H'=>778,'I'=>389,'J'=>500,'K'=>667,'L'=>611,'M'=>889,'N'=>722,'O'=>722,'P'=>611,'Q'=>722,'R'=>667,'S'=>556,'T'=>611,'U'=>722,'V'=>667,'W'=>889,

+	'X'=>667,'Y'=>611,'Z'=>611,'['=>333,'\\'=>278,']'=>333,'^'=>570,'_'=>500,'`'=>333,'a'=>500,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>333,'g'=>500,'h'=>556,'i'=>278,'j'=>278,'k'=>500,'l'=>278,'m'=>778,

+	'n'=>556,'o'=>500,'p'=>500,'q'=>500,'r'=>389,'s'=>389,'t'=>278,'u'=>556,'v'=>444,'w'=>667,'x'=>500,'y'=>444,'z'=>389,'{'=>348,'|'=>220,'}'=>348,'~'=>570,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500,

+	chr(132)=>500,chr(133)=>1000,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>556,chr(139)=>333,chr(140)=>944,chr(141)=>350,chr(142)=>611,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>500,chr(148)=>500,chr(149)=>350,chr(150)=>500,chr(151)=>1000,chr(152)=>333,chr(153)=>1000,

+	chr(154)=>389,chr(155)=>333,chr(156)=>722,chr(157)=>350,chr(158)=>389,chr(159)=>611,chr(160)=>250,chr(161)=>389,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>220,chr(167)=>500,chr(168)=>333,chr(169)=>747,chr(170)=>266,chr(171)=>500,chr(172)=>606,chr(173)=>333,chr(174)=>747,chr(175)=>333,

+	chr(176)=>400,chr(177)=>570,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>576,chr(182)=>500,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>300,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>667,chr(193)=>667,chr(194)=>667,chr(195)=>667,chr(196)=>667,chr(197)=>667,

+	chr(198)=>944,chr(199)=>667,chr(200)=>667,chr(201)=>667,chr(202)=>667,chr(203)=>667,chr(204)=>389,chr(205)=>389,chr(206)=>389,chr(207)=>389,chr(208)=>722,chr(209)=>722,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>570,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722,

+	chr(220)=>722,chr(221)=>611,chr(222)=>611,chr(223)=>500,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>722,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>556,

+	chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>570,chr(248)=>500,chr(249)=>556,chr(250)=>556,chr(251)=>556,chr(252)=>556,chr(253)=>444,chr(254)=>500,chr(255)=>444);

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf/font/timesi.php b/src/lib/fpdf/font/timesi.php
new file mode 100644
index 0000000..b0e5a62
--- /dev/null
+++ b/src/lib/fpdf/font/timesi.php
@@ -0,0 +1,21 @@
+<?php

+$type = 'Core';

+$name = 'Times-Italic';

+$up = -100;

+$ut = 50;

+$cw = array(

+	chr(0)=>250,chr(1)=>250,chr(2)=>250,chr(3)=>250,chr(4)=>250,chr(5)=>250,chr(6)=>250,chr(7)=>250,chr(8)=>250,chr(9)=>250,chr(10)=>250,chr(11)=>250,chr(12)=>250,chr(13)=>250,chr(14)=>250,chr(15)=>250,chr(16)=>250,chr(17)=>250,chr(18)=>250,chr(19)=>250,chr(20)=>250,chr(21)=>250,

+	chr(22)=>250,chr(23)=>250,chr(24)=>250,chr(25)=>250,chr(26)=>250,chr(27)=>250,chr(28)=>250,chr(29)=>250,chr(30)=>250,chr(31)=>250,' '=>250,'!'=>333,'"'=>420,'#'=>500,'$'=>500,'%'=>833,'&'=>778,'\''=>214,'('=>333,')'=>333,'*'=>500,'+'=>675,

+	','=>250,'-'=>333,'.'=>250,'/'=>278,'0'=>500,'1'=>500,'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>333,';'=>333,'<'=>675,'='=>675,'>'=>675,'?'=>500,'@'=>920,'A'=>611,

+	'B'=>611,'C'=>667,'D'=>722,'E'=>611,'F'=>611,'G'=>722,'H'=>722,'I'=>333,'J'=>444,'K'=>667,'L'=>556,'M'=>833,'N'=>667,'O'=>722,'P'=>611,'Q'=>722,'R'=>611,'S'=>500,'T'=>556,'U'=>722,'V'=>611,'W'=>833,

+	'X'=>611,'Y'=>556,'Z'=>556,'['=>389,'\\'=>278,']'=>389,'^'=>422,'_'=>500,'`'=>333,'a'=>500,'b'=>500,'c'=>444,'d'=>500,'e'=>444,'f'=>278,'g'=>500,'h'=>500,'i'=>278,'j'=>278,'k'=>444,'l'=>278,'m'=>722,

+	'n'=>500,'o'=>500,'p'=>500,'q'=>500,'r'=>389,'s'=>389,'t'=>278,'u'=>500,'v'=>444,'w'=>667,'x'=>444,'y'=>444,'z'=>389,'{'=>400,'|'=>275,'}'=>400,'~'=>541,chr(127)=>350,chr(128)=>500,chr(129)=>350,chr(130)=>333,chr(131)=>500,

+	chr(132)=>556,chr(133)=>889,chr(134)=>500,chr(135)=>500,chr(136)=>333,chr(137)=>1000,chr(138)=>500,chr(139)=>333,chr(140)=>944,chr(141)=>350,chr(142)=>556,chr(143)=>350,chr(144)=>350,chr(145)=>333,chr(146)=>333,chr(147)=>556,chr(148)=>556,chr(149)=>350,chr(150)=>500,chr(151)=>889,chr(152)=>333,chr(153)=>980,

+	chr(154)=>389,chr(155)=>333,chr(156)=>667,chr(157)=>350,chr(158)=>389,chr(159)=>556,chr(160)=>250,chr(161)=>389,chr(162)=>500,chr(163)=>500,chr(164)=>500,chr(165)=>500,chr(166)=>275,chr(167)=>500,chr(168)=>333,chr(169)=>760,chr(170)=>276,chr(171)=>500,chr(172)=>675,chr(173)=>333,chr(174)=>760,chr(175)=>333,

+	chr(176)=>400,chr(177)=>675,chr(178)=>300,chr(179)=>300,chr(180)=>333,chr(181)=>500,chr(182)=>523,chr(183)=>250,chr(184)=>333,chr(185)=>300,chr(186)=>310,chr(187)=>500,chr(188)=>750,chr(189)=>750,chr(190)=>750,chr(191)=>500,chr(192)=>611,chr(193)=>611,chr(194)=>611,chr(195)=>611,chr(196)=>611,chr(197)=>611,

+	chr(198)=>889,chr(199)=>667,chr(200)=>611,chr(201)=>611,chr(202)=>611,chr(203)=>611,chr(204)=>333,chr(205)=>333,chr(206)=>333,chr(207)=>333,chr(208)=>722,chr(209)=>667,chr(210)=>722,chr(211)=>722,chr(212)=>722,chr(213)=>722,chr(214)=>722,chr(215)=>675,chr(216)=>722,chr(217)=>722,chr(218)=>722,chr(219)=>722,

+	chr(220)=>722,chr(221)=>556,chr(222)=>611,chr(223)=>500,chr(224)=>500,chr(225)=>500,chr(226)=>500,chr(227)=>500,chr(228)=>500,chr(229)=>500,chr(230)=>667,chr(231)=>444,chr(232)=>444,chr(233)=>444,chr(234)=>444,chr(235)=>444,chr(236)=>278,chr(237)=>278,chr(238)=>278,chr(239)=>278,chr(240)=>500,chr(241)=>500,

+	chr(242)=>500,chr(243)=>500,chr(244)=>500,chr(245)=>500,chr(246)=>500,chr(247)=>675,chr(248)=>500,chr(249)=>500,chr(250)=>500,chr(251)=>500,chr(252)=>500,chr(253)=>444,chr(254)=>500,chr(255)=>444);

+$enc = 'cp1252';

+$uv = array(0=>array(0,128),128=>8364,130=>8218,131=>402,132=>8222,133=>8230,134=>array(8224,2),136=>710,137=>8240,138=>352,139=>8249,140=>338,142=>381,145=>array(8216,2),147=>array(8220,2),149=>8226,150=>array(8211,2),152=>732,153=>8482,154=>353,155=>8250,156=>339,158=>382,159=>376,160=>array(160,96));

+?>

diff --git a/src/lib/fpdf/font/zapfdingbats.php b/src/lib/fpdf/font/zapfdingbats.php
new file mode 100644
index 0000000..b9d0309
--- /dev/null
+++ b/src/lib/fpdf/font/zapfdingbats.php
@@ -0,0 +1,20 @@
+<?php

+$type = 'Core';

+$name = 'ZapfDingbats';

+$up = -100;

+$ut = 50;

+$cw = array(

+	chr(0)=>0,chr(1)=>0,chr(2)=>0,chr(3)=>0,chr(4)=>0,chr(5)=>0,chr(6)=>0,chr(7)=>0,chr(8)=>0,chr(9)=>0,chr(10)=>0,chr(11)=>0,chr(12)=>0,chr(13)=>0,chr(14)=>0,chr(15)=>0,chr(16)=>0,chr(17)=>0,chr(18)=>0,chr(19)=>0,chr(20)=>0,chr(21)=>0,

+	chr(22)=>0,chr(23)=>0,chr(24)=>0,chr(25)=>0,chr(26)=>0,chr(27)=>0,chr(28)=>0,chr(29)=>0,chr(30)=>0,chr(31)=>0,' '=>278,'!'=>974,'"'=>961,'#'=>974,'$'=>980,'%'=>719,'&'=>789,'\''=>790,'('=>791,')'=>690,'*'=>960,'+'=>939,

+	','=>549,'-'=>855,'.'=>911,'/'=>933,'0'=>911,'1'=>945,'2'=>974,'3'=>755,'4'=>846,'5'=>762,'6'=>761,'7'=>571,'8'=>677,'9'=>763,':'=>760,';'=>759,'<'=>754,'='=>494,'>'=>552,'?'=>537,'@'=>577,'A'=>692,

+	'B'=>786,'C'=>788,'D'=>788,'E'=>790,'F'=>793,'G'=>794,'H'=>816,'I'=>823,'J'=>789,'K'=>841,'L'=>823,'M'=>833,'N'=>816,'O'=>831,'P'=>923,'Q'=>744,'R'=>723,'S'=>749,'T'=>790,'U'=>792,'V'=>695,'W'=>776,

+	'X'=>768,'Y'=>792,'Z'=>759,'['=>707,'\\'=>708,']'=>682,'^'=>701,'_'=>826,'`'=>815,'a'=>789,'b'=>789,'c'=>707,'d'=>687,'e'=>696,'f'=>689,'g'=>786,'h'=>787,'i'=>713,'j'=>791,'k'=>785,'l'=>791,'m'=>873,

+	'n'=>761,'o'=>762,'p'=>762,'q'=>759,'r'=>759,'s'=>892,'t'=>892,'u'=>788,'v'=>784,'w'=>438,'x'=>138,'y'=>277,'z'=>415,'{'=>392,'|'=>392,'}'=>668,'~'=>668,chr(127)=>0,chr(128)=>390,chr(129)=>390,chr(130)=>317,chr(131)=>317,

+	chr(132)=>276,chr(133)=>276,chr(134)=>509,chr(135)=>509,chr(136)=>410,chr(137)=>410,chr(138)=>234,chr(139)=>234,chr(140)=>334,chr(141)=>334,chr(142)=>0,chr(143)=>0,chr(144)=>0,chr(145)=>0,chr(146)=>0,chr(147)=>0,chr(148)=>0,chr(149)=>0,chr(150)=>0,chr(151)=>0,chr(152)=>0,chr(153)=>0,

+	chr(154)=>0,chr(155)=>0,chr(156)=>0,chr(157)=>0,chr(158)=>0,chr(159)=>0,chr(160)=>0,chr(161)=>732,chr(162)=>544,chr(163)=>544,chr(164)=>910,chr(165)=>667,chr(166)=>760,chr(167)=>760,chr(168)=>776,chr(169)=>595,chr(170)=>694,chr(171)=>626,chr(172)=>788,chr(173)=>788,chr(174)=>788,chr(175)=>788,

+	chr(176)=>788,chr(177)=>788,chr(178)=>788,chr(179)=>788,chr(180)=>788,chr(181)=>788,chr(182)=>788,chr(183)=>788,chr(184)=>788,chr(185)=>788,chr(186)=>788,chr(187)=>788,chr(188)=>788,chr(189)=>788,chr(190)=>788,chr(191)=>788,chr(192)=>788,chr(193)=>788,chr(194)=>788,chr(195)=>788,chr(196)=>788,chr(197)=>788,

+	chr(198)=>788,chr(199)=>788,chr(200)=>788,chr(201)=>788,chr(202)=>788,chr(203)=>788,chr(204)=>788,chr(205)=>788,chr(206)=>788,chr(207)=>788,chr(208)=>788,chr(209)=>788,chr(210)=>788,chr(211)=>788,chr(212)=>894,chr(213)=>838,chr(214)=>1016,chr(215)=>458,chr(216)=>748,chr(217)=>924,chr(218)=>748,chr(219)=>918,

+	chr(220)=>927,chr(221)=>928,chr(222)=>928,chr(223)=>834,chr(224)=>873,chr(225)=>828,chr(226)=>924,chr(227)=>924,chr(228)=>917,chr(229)=>930,chr(230)=>931,chr(231)=>463,chr(232)=>883,chr(233)=>836,chr(234)=>836,chr(235)=>867,chr(236)=>867,chr(237)=>696,chr(238)=>696,chr(239)=>874,chr(240)=>0,chr(241)=>874,

+	chr(242)=>760,chr(243)=>946,chr(244)=>771,chr(245)=>865,chr(246)=>771,chr(247)=>888,chr(248)=>967,chr(249)=>888,chr(250)=>831,chr(251)=>873,chr(252)=>927,chr(253)=>970,chr(254)=>918,chr(255)=>0);

+$uv = array(32=>32,33=>array(9985,4),37=>9742,38=>array(9990,4),42=>9755,43=>9758,44=>array(9996,28),72=>9733,73=>array(10025,35),108=>9679,109=>10061,110=>9632,111=>array(10063,4),115=>9650,116=>9660,117=>9670,118=>10070,119=>9687,120=>array(10072,7),128=>array(10088,14),161=>array(10081,7),168=>9827,169=>9830,170=>9829,171=>9824,172=>array(9312,10),182=>array(10102,31),213=>8594,214=>array(8596,2),216=>array(10136,24),241=>array(10161,14));

+?>

diff --git a/src/lib/fpdf/fpdf.css b/src/lib/fpdf/fpdf.css
new file mode 100644
index 0000000..dd2c540
--- /dev/null
+++ b/src/lib/fpdf/fpdf.css
@@ -0,0 +1,21 @@
+body {font-family:"Times New Roman",serif}

+h1 {font:bold 135% Arial,sans-serif; color:#4000A0; margin-bottom:0.9em}

+h2 {font:bold 95% Arial,sans-serif; color:#900000; margin-top:1.5em; margin-bottom:1em}

+dl.param dt {text-decoration:underline}

+dl.param dd {margin-top:1em; margin-bottom:1em}

+dl.param ul {margin-top:1em; margin-bottom:1em}

+tt, code, kbd {font-family:"Courier New",Courier,monospace; font-size:82%}

+div.source {margin-top:1.4em; margin-bottom:1.3em}

+div.source pre {display:table; border:1px solid #24246A; width:100%; margin:0em; font-family:inherit; font-size:100%}

+div.source code {display:block; border:1px solid #C5C5EC; background-color:#F0F5FF; padding:6px; color:#000000}

+div.doc-source {margin-top:1.4em; margin-bottom:1.3em}

+div.doc-source pre {display:table; width:100%; margin:0em; font-family:inherit; font-size:100%}

+div.doc-source code {display:block; background-color:#E0E0E0; padding:4px}

+.kw {color:#000080; font-weight:bold}

+.str {color:#CC0000}

+.cmt {color:#008000}

+p.demo {text-align:center; margin-top:-0.9em}

+a.demo {text-decoration:none; font-weight:bold; color:#0000CC}

+a.demo:link {text-decoration:none; font-weight:bold; color:#0000CC}

+a.demo:hover {text-decoration:none; font-weight:bold; color:#0000FF}

+a.demo:active {text-decoration:none; font-weight:bold; color:#0000FF}

diff --git a/src/lib/fpdf/fpdf.php b/src/lib/fpdf/fpdf.php
new file mode 100644
index 0000000..ebee958
--- /dev/null
+++ b/src/lib/fpdf/fpdf.php
@@ -0,0 +1,1934 @@
+<?php

+/*******************************************************************************

+* FPDF                                                                         *

+*                                                                              *

+* Version: 1.86                                                                *

+* Date:    2023-06-25                                                          *

+* Author:  Olivier PLATHEY                                                     *

+*******************************************************************************/

+

+class FPDF

+{

+const VERSION = '1.86';

+protected $page;               // current page number

+protected $n;                  // current object number

+protected $offsets;            // array of object offsets

+protected $buffer;             // buffer holding in-memory PDF

+protected $pages;              // array containing pages

+protected $state;              // current document state

+protected $compress;           // compression flag

+protected $iconv;              // whether iconv is available

+protected $k;                  // scale factor (number of points in user unit)

+protected $DefOrientation;     // default orientation

+protected $CurOrientation;     // current orientation

+protected $StdPageSizes;       // standard page sizes

+protected $DefPageSize;        // default page size

+protected $CurPageSize;        // current page size

+protected $CurRotation;        // current page rotation

+protected $PageInfo;           // page-related data

+protected $wPt, $hPt;          // dimensions of current page in points

+protected $w, $h;              // dimensions of current page in user unit

+protected $lMargin;            // left margin

+protected $tMargin;            // top margin

+protected $rMargin;            // right margin

+protected $bMargin;            // page break margin

+protected $cMargin;            // cell margin

+protected $x, $y;              // current position in user unit

+protected $lasth;              // height of last printed cell

+protected $LineWidth;          // line width in user unit

+protected $fontpath;           // directory containing fonts

+protected $CoreFonts;          // array of core font names

+protected $fonts;              // array of used fonts

+protected $FontFiles;          // array of font files

+protected $encodings;          // array of encodings

+protected $cmaps;              // array of ToUnicode CMaps

+protected $FontFamily;         // current font family

+protected $FontStyle;          // current font style

+protected $underline;          // underlining flag

+protected $CurrentFont;        // current font info

+protected $FontSizePt;         // current font size in points

+protected $FontSize;           // current font size in user unit

+protected $DrawColor;          // commands for drawing color

+protected $FillColor;          // commands for filling color

+protected $TextColor;          // commands for text color

+protected $ColorFlag;          // indicates whether fill and text colors are different

+protected $WithAlpha;          // indicates whether alpha channel is used

+protected $ws;                 // word spacing

+protected $images;             // array of used images

+protected $PageLinks;          // array of links in pages

+protected $links;              // array of internal links

+protected $AutoPageBreak;      // automatic page breaking

+protected $PageBreakTrigger;   // threshold used to trigger page breaks

+protected $InHeader;           // flag set when processing header

+protected $InFooter;           // flag set when processing footer

+protected $AliasNbPages;       // alias for total number of pages

+protected $ZoomMode;           // zoom display mode

+protected $LayoutMode;         // layout display mode

+protected $metadata;           // document properties

+protected $CreationDate;       // document creation date

+protected $PDFVersion;         // PDF version number

+

+/*******************************************************************************

+*                               Public methods                                 *

+*******************************************************************************/

+

+function __construct($orientation='P', $unit='mm', $size='A4')

+{

+	// Initialization of properties

+	$this->state = 0;

+	$this->page = 0;

+	$this->n = 2;

+	$this->buffer = '';

+	$this->pages = array();

+	$this->PageInfo = array();

+	$this->fonts = array();

+	$this->FontFiles = array();

+	$this->encodings = array();

+	$this->cmaps = array();

+	$this->images = array();

+	$this->links = array();

+	$this->InHeader = false;

+	$this->InFooter = false;

+	$this->lasth = 0;

+	$this->FontFamily = '';

+	$this->FontStyle = '';

+	$this->FontSizePt = 12;

+	$this->underline = false;

+	$this->DrawColor = '0 G';

+	$this->FillColor = '0 g';

+	$this->TextColor = '0 g';

+	$this->ColorFlag = false;

+	$this->WithAlpha = false;

+	$this->ws = 0;

+	$this->iconv = function_exists('iconv');

+	// Font path

+	if(defined('FPDF_FONTPATH'))

+		$this->fontpath = FPDF_FONTPATH;

+	else

+		$this->fontpath = dirname(__FILE__).'/font/';

+	// Core fonts

+	$this->CoreFonts = array('courier', 'helvetica', 'times', 'symbol', 'zapfdingbats');

+	// Scale factor

+	if($unit=='pt')

+		$this->k = 1;

+	elseif($unit=='mm')

+		$this->k = 72/25.4;

+	elseif($unit=='cm')

+		$this->k = 72/2.54;

+	elseif($unit=='in')

+		$this->k = 72;

+	else

+		$this->Error('Incorrect unit: '.$unit);

+	// Page sizes

+	$this->StdPageSizes = array('a3'=>array(841.89,1190.55), 'a4'=>array(595.28,841.89), 'a5'=>array(420.94,595.28),

+		'letter'=>array(612,792), 'legal'=>array(612,1008));

+	$size = $this->_getpagesize($size);

+	$this->DefPageSize = $size;

+	$this->CurPageSize = $size;

+	// Page orientation

+	$orientation = strtolower($orientation);

+	if($orientation=='p' || $orientation=='portrait')

+	{

+		$this->DefOrientation = 'P';

+		$this->w = $size[0];

+		$this->h = $size[1];

+	}

+	elseif($orientation=='l' || $orientation=='landscape')

+	{

+		$this->DefOrientation = 'L';

+		$this->w = $size[1];

+		$this->h = $size[0];

+	}

+	else

+		$this->Error('Incorrect orientation: '.$orientation);

+	$this->CurOrientation = $this->DefOrientation;

+	$this->wPt = $this->w*$this->k;

+	$this->hPt = $this->h*$this->k;

+	// Page rotation

+	$this->CurRotation = 0;

+	// Page margins (1 cm)

+	$margin = 28.35/$this->k;

+	$this->SetMargins($margin,$margin);

+	// Interior cell margin (1 mm)

+	$this->cMargin = $margin/10;

+	// Line width (0.2 mm)

+	$this->LineWidth = .567/$this->k;

+	// Automatic page break

+	$this->SetAutoPageBreak(true,2*$margin);

+	// Default display mode

+	$this->SetDisplayMode('default');

+	// Enable compression

+	$this->SetCompression(true);

+	// Metadata

+	$this->metadata = array('Producer'=>'FPDF '.self::VERSION);

+	// Set default PDF version number

+	$this->PDFVersion = '1.3';

+}

+

+function SetMargins($left, $top, $right=null)

+{

+	// Set left, top and right margins

+	$this->lMargin = $left;

+	$this->tMargin = $top;

+	if($right===null)

+		$right = $left;

+	$this->rMargin = $right;

+}

+

+function SetLeftMargin($margin)

+{

+	// Set left margin

+	$this->lMargin = $margin;

+	if($this->page>0 && $this->x<$margin)

+		$this->x = $margin;

+}

+

+function SetTopMargin($margin)

+{

+	// Set top margin

+	$this->tMargin = $margin;

+}

+

+function SetRightMargin($margin)

+{

+	// Set right margin

+	$this->rMargin = $margin;

+}

+

+function SetAutoPageBreak($auto, $margin=0)

+{

+	// Set auto page break mode and triggering margin

+	$this->AutoPageBreak = $auto;

+	$this->bMargin = $margin;

+	$this->PageBreakTrigger = $this->h-$margin;

+}

+

+function SetDisplayMode($zoom, $layout='default')

+{

+	// Set display mode in viewer

+	if($zoom=='fullpage' || $zoom=='fullwidth' || $zoom=='real' || $zoom=='default' || !is_string($zoom))

+		$this->ZoomMode = $zoom;

+	else

+		$this->Error('Incorrect zoom display mode: '.$zoom);

+	if($layout=='single' || $layout=='continuous' || $layout=='two' || $layout=='default')

+		$this->LayoutMode = $layout;

+	else

+		$this->Error('Incorrect layout display mode: '.$layout);

+}

+

+function SetCompression($compress)

+{

+	// Set page compression

+	if(function_exists('gzcompress'))

+		$this->compress = $compress;

+	else

+		$this->compress = false;

+}

+

+function SetTitle($title, $isUTF8=false)

+{

+	// Title of document

+	$this->metadata['Title'] = $isUTF8 ? $title : $this->_UTF8encode($title);

+}

+

+function SetAuthor($author, $isUTF8=false)

+{

+	// Author of document

+	$this->metadata['Author'] = $isUTF8 ? $author : $this->_UTF8encode($author);

+}

+

+function SetSubject($subject, $isUTF8=false)

+{

+	// Subject of document

+	$this->metadata['Subject'] = $isUTF8 ? $subject : $this->_UTF8encode($subject);

+}

+

+function SetKeywords($keywords, $isUTF8=false)

+{

+	// Keywords of document

+	$this->metadata['Keywords'] = $isUTF8 ? $keywords : $this->_UTF8encode($keywords);

+}

+

+function SetCreator($creator, $isUTF8=false)

+{

+	// Creator of document

+	$this->metadata['Creator'] = $isUTF8 ? $creator : $this->_UTF8encode($creator);

+}

+

+function AliasNbPages($alias='{nb}')

+{

+	// Define an alias for total number of pages

+	$this->AliasNbPages = $alias;

+}

+

+function Error($msg)

+{

+	// Fatal error

+	throw new Exception('FPDF error: '.$msg);

+}

+

+function Close()

+{

+	// Terminate document

+	if($this->state==3)

+		return;

+	if($this->page==0)

+		$this->AddPage();

+	// Page footer

+	$this->InFooter = true;

+	$this->Footer();

+	$this->InFooter = false;

+	// Close page

+	$this->_endpage();

+	// Close document

+	$this->_enddoc();

+}

+

+function AddPage($orientation='', $size='', $rotation=0)

+{

+	// Start a new page

+	if($this->state==3)

+		$this->Error('The document is closed');

+	$family = $this->FontFamily;

+	$style = $this->FontStyle.($this->underline ? 'U' : '');

+	$fontsize = $this->FontSizePt;

+	$lw = $this->LineWidth;

+	$dc = $this->DrawColor;

+	$fc = $this->FillColor;

+	$tc = $this->TextColor;

+	$cf = $this->ColorFlag;

+	if($this->page>0)

+	{

+		// Page footer

+		$this->InFooter = true;

+		$this->Footer();

+		$this->InFooter = false;

+		// Close page

+		$this->_endpage();

+	}

+	// Start new page

+	$this->_beginpage($orientation,$size,$rotation);

+	// Set line cap style to square

+	$this->_out('2 J');

+	// Set line width

+	$this->LineWidth = $lw;

+	$this->_out(sprintf('%.2F w',$lw*$this->k));

+	// Set font

+	if($family)

+		$this->SetFont($family,$style,$fontsize);

+	// Set colors

+	$this->DrawColor = $dc;

+	if($dc!='0 G')

+		$this->_out($dc);

+	$this->FillColor = $fc;

+	if($fc!='0 g')

+		$this->_out($fc);

+	$this->TextColor = $tc;

+	$this->ColorFlag = $cf;

+	// Page header

+	$this->InHeader = true;

+	$this->Header();

+	$this->InHeader = false;

+	// Restore line width

+	if($this->LineWidth!=$lw)

+	{

+		$this->LineWidth = $lw;

+		$this->_out(sprintf('%.2F w',$lw*$this->k));

+	}

+	// Restore font

+	if($family)

+		$this->SetFont($family,$style,$fontsize);

+	// Restore colors

+	if($this->DrawColor!=$dc)

+	{

+		$this->DrawColor = $dc;

+		$this->_out($dc);

+	}

+	if($this->FillColor!=$fc)

+	{

+		$this->FillColor = $fc;

+		$this->_out($fc);

+	}

+	$this->TextColor = $tc;

+	$this->ColorFlag = $cf;

+}

+

+function Header()

+{

+	// To be implemented in your own inherited class

+}

+

+function Footer()

+{

+	// To be implemented in your own inherited class

+}

+

+function PageNo()

+{

+	// Get current page number

+	return $this->page;

+}

+

+function SetDrawColor($r, $g=null, $b=null)

+{

+	// Set color for all stroking operations

+	if(($r==0 && $g==0 && $b==0) || $g===null)

+		$this->DrawColor = sprintf('%.3F G',$r/255);

+	else

+		$this->DrawColor = sprintf('%.3F %.3F %.3F RG',$r/255,$g/255,$b/255);

+	if($this->page>0)

+		$this->_out($this->DrawColor);

+}

+

+function SetFillColor($r, $g=null, $b=null)

+{

+	// Set color for all filling operations

+	if(($r==0 && $g==0 && $b==0) || $g===null)

+		$this->FillColor = sprintf('%.3F g',$r/255);

+	else

+		$this->FillColor = sprintf('%.3F %.3F %.3F rg',$r/255,$g/255,$b/255);

+	$this->ColorFlag = ($this->FillColor!=$this->TextColor);

+	if($this->page>0)

+		$this->_out($this->FillColor);

+}

+

+function SetTextColor($r, $g=null, $b=null)

+{

+	// Set color for text

+	if(($r==0 && $g==0 && $b==0) || $g===null)

+		$this->TextColor = sprintf('%.3F g',$r/255);

+	else

+		$this->TextColor = sprintf('%.3F %.3F %.3F rg',$r/255,$g/255,$b/255);

+	$this->ColorFlag = ($this->FillColor!=$this->TextColor);

+}

+

+function GetStringWidth($s)

+{

+	// Get width of a string in the current font

+	$cw = $this->CurrentFont['cw'];

+	$w = 0;

+	$s = (string)$s;

+	$l = strlen($s);

+	for($i=0;$i<$l;$i++)

+		$w += $cw[$s[$i]];

+	return $w*$this->FontSize/1000;

+}

+

+function SetLineWidth($width)

+{

+	// Set line width

+	$this->LineWidth = $width;

+	if($this->page>0)

+		$this->_out(sprintf('%.2F w',$width*$this->k));

+}

+

+function Line($x1, $y1, $x2, $y2)

+{

+	// Draw a line

+	$this->_out(sprintf('%.2F %.2F m %.2F %.2F l S',$x1*$this->k,($this->h-$y1)*$this->k,$x2*$this->k,($this->h-$y2)*$this->k));

+}

+

+function Rect($x, $y, $w, $h, $style='')

+{

+	// Draw a rectangle

+	if($style=='F')

+		$op = 'f';

+	elseif($style=='FD' || $style=='DF')

+		$op = 'B';

+	else

+		$op = 'S';

+	$this->_out(sprintf('%.2F %.2F %.2F %.2F re %s',$x*$this->k,($this->h-$y)*$this->k,$w*$this->k,-$h*$this->k,$op));

+}

+

+function AddFont($family, $style='', $file='', $dir='')

+{

+	// Add a TrueType, OpenType or Type1 font

+	$family = strtolower($family);

+	if($file=='')

+		$file = str_replace(' ','',$family).strtolower($style).'.php';

+	$style = strtoupper($style);

+	if($style=='IB')

+		$style = 'BI';

+	$fontkey = $family.$style;

+	if(isset($this->fonts[$fontkey]))

+		return;

+	if(strpos($file,'/')!==false || strpos($file,"\\")!==false)

+		$this->Error('Incorrect font definition file name: '.$file);

+	if($dir=='')

+		$dir = $this->fontpath;

+	if(substr($dir,-1)!='/' && substr($dir,-1)!='\\')

+		$dir .= '/';

+	$info = $this->_loadfont($dir.$file);

+	$info['i'] = count($this->fonts)+1;

+	if(!empty($info['file']))

+	{

+		// Embedded font

+		$info['file'] = $dir.$info['file'];

+		if($info['type']=='TrueType')

+			$this->FontFiles[$info['file']] = array('length1'=>$info['originalsize']);

+		else

+			$this->FontFiles[$info['file']] = array('length1'=>$info['size1'], 'length2'=>$info['size2']);

+	}

+	$this->fonts[$fontkey] = $info;

+}

+

+function SetFont($family, $style='', $size=0)

+{

+	// Select a font; size given in points

+	if($family=='')

+		$family = $this->FontFamily;

+	else

+		$family = strtolower($family);

+	$style = strtoupper($style);

+	if(strpos($style,'U')!==false)

+	{

+		$this->underline = true;

+		$style = str_replace('U','',$style);

+	}

+	else

+		$this->underline = false;

+	if($style=='IB')

+		$style = 'BI';

+	if($size==0)

+		$size = $this->FontSizePt;

+	// Test if font is already selected

+	if($this->FontFamily==$family && $this->FontStyle==$style && $this->FontSizePt==$size)

+		return;

+	// Test if font is already loaded

+	$fontkey = $family.$style;

+	if(!isset($this->fonts[$fontkey]))

+	{

+		// Test if one of the core fonts

+		if($family=='arial')

+			$family = 'helvetica';

+		if(in_array($family,$this->CoreFonts))

+		{

+			if($family=='symbol' || $family=='zapfdingbats')

+				$style = '';

+			$fontkey = $family.$style;

+			if(!isset($this->fonts[$fontkey]))

+				$this->AddFont($family,$style);

+		}

+		else

+			$this->Error('Undefined font: '.$family.' '.$style);

+	}

+	// Select it

+	$this->FontFamily = $family;

+	$this->FontStyle = $style;

+	$this->FontSizePt = $size;

+	$this->FontSize = $size/$this->k;

+	$this->CurrentFont = $this->fonts[$fontkey];

+	if($this->page>0)

+		$this->_out(sprintf('BT /F%d %.2F Tf ET',$this->CurrentFont['i'],$this->FontSizePt));

+}

+

+function SetFontSize($size)

+{

+	// Set font size in points

+	if($this->FontSizePt==$size)

+		return;

+	$this->FontSizePt = $size;

+	$this->FontSize = $size/$this->k;

+	if($this->page>0 && isset($this->CurrentFont))

+		$this->_out(sprintf('BT /F%d %.2F Tf ET',$this->CurrentFont['i'],$this->FontSizePt));

+}

+

+function AddLink()

+{

+	// Create a new internal link

+	$n = count($this->links)+1;

+	$this->links[$n] = array(0, 0);

+	return $n;

+}

+

+function SetLink($link, $y=0, $page=-1)

+{

+	// Set destination of internal link

+	if($y==-1)

+		$y = $this->y;

+	if($page==-1)

+		$page = $this->page;

+	$this->links[$link] = array($page, $y);

+}

+

+function Link($x, $y, $w, $h, $link)

+{

+	// Put a link on the page

+	$this->PageLinks[$this->page][] = array($x*$this->k, $this->hPt-$y*$this->k, $w*$this->k, $h*$this->k, $link);

+}

+

+function Text($x, $y, $txt)

+{

+	// Output a string

+	if(!isset($this->CurrentFont))

+		$this->Error('No font has been set');

+	$txt = (string)$txt;

+	$s = sprintf('BT %.2F %.2F Td (%s) Tj ET',$x*$this->k,($this->h-$y)*$this->k,$this->_escape($txt));

+	if($this->underline && $txt!=='')

+		$s .= ' '.$this->_dounderline($x,$y,$txt);

+	if($this->ColorFlag)

+		$s = 'q '.$this->TextColor.' '.$s.' Q';

+	$this->_out($s);

+}

+

+function AcceptPageBreak()

+{

+	// Accept automatic page break or not

+	return $this->AutoPageBreak;

+}

+

+function Cell($w, $h=0, $txt='', $border=0, $ln=0, $align='', $fill=false, $link='')

+{

+	// Output a cell

+	$k = $this->k;

+	if($this->y+$h>$this->PageBreakTrigger && !$this->InHeader && !$this->InFooter && $this->AcceptPageBreak())

+	{

+		// Automatic page break

+		$x = $this->x;

+		$ws = $this->ws;

+		if($ws>0)

+		{

+			$this->ws = 0;

+			$this->_out('0 Tw');

+		}

+		$this->AddPage($this->CurOrientation,$this->CurPageSize,$this->CurRotation);

+		$this->x = $x;

+		if($ws>0)

+		{

+			$this->ws = $ws;

+			$this->_out(sprintf('%.3F Tw',$ws*$k));

+		}

+	}

+	if($w==0)

+		$w = $this->w-$this->rMargin-$this->x;

+	$s = '';

+	if($fill || $border==1)

+	{

+		if($fill)

+			$op = ($border==1) ? 'B' : 'f';

+		else

+			$op = 'S';

+		$s = sprintf('%.2F %.2F %.2F %.2F re %s ',$this->x*$k,($this->h-$this->y)*$k,$w*$k,-$h*$k,$op);

+	}

+	if(is_string($border))

+	{

+		$x = $this->x;

+		$y = $this->y;

+		if(strpos($border,'L')!==false)

+			$s .= sprintf('%.2F %.2F m %.2F %.2F l S ',$x*$k,($this->h-$y)*$k,$x*$k,($this->h-($y+$h))*$k);

+		if(strpos($border,'T')!==false)

+			$s .= sprintf('%.2F %.2F m %.2F %.2F l S ',$x*$k,($this->h-$y)*$k,($x+$w)*$k,($this->h-$y)*$k);

+		if(strpos($border,'R')!==false)

+			$s .= sprintf('%.2F %.2F m %.2F %.2F l S ',($x+$w)*$k,($this->h-$y)*$k,($x+$w)*$k,($this->h-($y+$h))*$k);

+		if(strpos($border,'B')!==false)

+			$s .= sprintf('%.2F %.2F m %.2F %.2F l S ',$x*$k,($this->h-($y+$h))*$k,($x+$w)*$k,($this->h-($y+$h))*$k);

+	}

+	$txt = (string)$txt;

+	if($txt!=='')

+	{

+		if(!isset($this->CurrentFont))

+			$this->Error('No font has been set');

+		if($align=='R')

+			$dx = $w-$this->cMargin-$this->GetStringWidth($txt);

+		elseif($align=='C')

+			$dx = ($w-$this->GetStringWidth($txt))/2;

+		else

+			$dx = $this->cMargin;

+		if($this->ColorFlag)

+			$s .= 'q '.$this->TextColor.' ';

+		$s .= sprintf('BT %.2F %.2F Td (%s) Tj ET',($this->x+$dx)*$k,($this->h-($this->y+.5*$h+.3*$this->FontSize))*$k,$this->_escape($txt));

+		if($this->underline)

+			$s .= ' '.$this->_dounderline($this->x+$dx,$this->y+.5*$h+.3*$this->FontSize,$txt);

+		if($this->ColorFlag)

+			$s .= ' Q';

+		if($link)

+			$this->Link($this->x+$dx,$this->y+.5*$h-.5*$this->FontSize,$this->GetStringWidth($txt),$this->FontSize,$link);

+	}

+	if($s)

+		$this->_out($s);

+	$this->lasth = $h;

+	if($ln>0)

+	{

+		// Go to next line

+		$this->y += $h;

+		if($ln==1)

+			$this->x = $this->lMargin;

+	}

+	else

+		$this->x += $w;

+}

+

+function MultiCell($w, $h, $txt, $border=0, $align='J', $fill=false)

+{

+	// Output text with automatic or explicit line breaks

+	if(!isset($this->CurrentFont))

+		$this->Error('No font has been set');

+	$cw = $this->CurrentFont['cw'];

+	if($w==0)

+		$w = $this->w-$this->rMargin-$this->x;

+	$wmax = ($w-2*$this->cMargin)*1000/$this->FontSize;

+	$s = str_replace("\r",'',(string)$txt);

+	$nb = strlen($s);

+	if($nb>0 && $s[$nb-1]=="\n")

+		$nb--;

+	$b = 0;

+	if($border)

+	{

+		if($border==1)

+		{

+			$border = 'LTRB';

+			$b = 'LRT';

+			$b2 = 'LR';

+		}

+		else

+		{

+			$b2 = '';

+			if(strpos($border,'L')!==false)

+				$b2 .= 'L';

+			if(strpos($border,'R')!==false)

+				$b2 .= 'R';

+			$b = (strpos($border,'T')!==false) ? $b2.'T' : $b2;

+		}

+	}

+	$sep = -1;

+	$i = 0;

+	$j = 0;

+	$l = 0;

+	$ns = 0;

+	$nl = 1;

+	while($i<$nb)

+	{

+		// Get next character

+		$c = $s[$i];

+		if($c=="\n")

+		{

+			// Explicit line break

+			if($this->ws>0)

+			{

+				$this->ws = 0;

+				$this->_out('0 Tw');

+			}

+			$this->Cell($w,$h,substr($s,$j,$i-$j),$b,2,$align,$fill);

+			$i++;

+			$sep = -1;

+			$j = $i;

+			$l = 0;

+			$ns = 0;

+			$nl++;

+			if($border && $nl==2)

+				$b = $b2;

+			continue;

+		}

+		if($c==' ')

+		{

+			$sep = $i;

+			$ls = $l;

+			$ns++;

+		}

+		$l += $cw[$c];

+		if($l>$wmax)

+		{

+			// Automatic line break

+			if($sep==-1)

+			{

+				if($i==$j)

+					$i++;

+				if($this->ws>0)

+				{

+					$this->ws = 0;

+					$this->_out('0 Tw');

+				}

+				$this->Cell($w,$h,substr($s,$j,$i-$j),$b,2,$align,$fill);

+			}

+			else

+			{

+				if($align=='J')

+				{

+					$this->ws = ($ns>1) ? ($wmax-$ls)/1000*$this->FontSize/($ns-1) : 0;

+					$this->_out(sprintf('%.3F Tw',$this->ws*$this->k));

+				}

+				$this->Cell($w,$h,substr($s,$j,$sep-$j),$b,2,$align,$fill);

+				$i = $sep+1;

+			}

+			$sep = -1;

+			$j = $i;

+			$l = 0;

+			$ns = 0;

+			$nl++;

+			if($border && $nl==2)

+				$b = $b2;

+		}

+		else

+			$i++;

+	}

+	// Last chunk

+	if($this->ws>0)

+	{

+		$this->ws = 0;

+		$this->_out('0 Tw');

+	}

+	if($border && strpos($border,'B')!==false)

+		$b .= 'B';

+	$this->Cell($w,$h,substr($s,$j,$i-$j),$b,2,$align,$fill);

+	$this->x = $this->lMargin;

+}

+

+function Write($h, $txt, $link='')

+{

+	// Output text in flowing mode

+	if(!isset($this->CurrentFont))

+		$this->Error('No font has been set');

+	$cw = $this->CurrentFont['cw'];

+	$w = $this->w-$this->rMargin-$this->x;

+	$wmax = ($w-2*$this->cMargin)*1000/$this->FontSize;

+	$s = str_replace("\r",'',(string)$txt);

+	$nb = strlen($s);

+	$sep = -1;

+	$i = 0;

+	$j = 0;

+	$l = 0;

+	$nl = 1;

+	while($i<$nb)

+	{

+		// Get next character

+		$c = $s[$i];

+		if($c=="\n")

+		{

+			// Explicit line break

+			$this->Cell($w,$h,substr($s,$j,$i-$j),0,2,'',false,$link);

+			$i++;

+			$sep = -1;

+			$j = $i;

+			$l = 0;

+			if($nl==1)

+			{

+				$this->x = $this->lMargin;

+				$w = $this->w-$this->rMargin-$this->x;

+				$wmax = ($w-2*$this->cMargin)*1000/$this->FontSize;

+			}

+			$nl++;

+			continue;

+		}

+		if($c==' ')

+			$sep = $i;

+		$l += $cw[$c];

+		if($l>$wmax)

+		{

+			// Automatic line break

+			if($sep==-1)

+			{

+				if($this->x>$this->lMargin)

+				{

+					// Move to next line

+					$this->x = $this->lMargin;

+					$this->y += $h;

+					$w = $this->w-$this->rMargin-$this->x;

+					$wmax = ($w-2*$this->cMargin)*1000/$this->FontSize;

+					$i++;

+					$nl++;

+					continue;

+				}

+				if($i==$j)

+					$i++;

+				$this->Cell($w,$h,substr($s,$j,$i-$j),0,2,'',false,$link);

+			}

+			else

+			{

+				$this->Cell($w,$h,substr($s,$j,$sep-$j),0,2,'',false,$link);

+				$i = $sep+1;

+			}

+			$sep = -1;

+			$j = $i;

+			$l = 0;

+			if($nl==1)

+			{

+				$this->x = $this->lMargin;

+				$w = $this->w-$this->rMargin-$this->x;

+				$wmax = ($w-2*$this->cMargin)*1000/$this->FontSize;

+			}

+			$nl++;

+		}

+		else

+			$i++;

+	}

+	// Last chunk

+	if($i!=$j)

+		$this->Cell($l/1000*$this->FontSize,$h,substr($s,$j),0,0,'',false,$link);

+}

+

+function Ln($h=null)

+{

+	// Line feed; default value is the last cell height

+	$this->x = $this->lMargin;

+	if($h===null)

+		$this->y += $this->lasth;

+	else

+		$this->y += $h;

+}

+

+function Image($file, $x=null, $y=null, $w=0, $h=0, $type='', $link='')

+{

+	// Put an image on the page

+	if($file=='')

+		$this->Error('Image file name is empty');

+	if(!isset($this->images[$file]))

+	{

+		// First use of this image, get info

+		if($type=='')

+		{

+			$pos = strrpos($file,'.');

+			if(!$pos)

+				$this->Error('Image file has no extension and no type was specified: '.$file);

+			$type = substr($file,$pos+1);

+		}

+		$type = strtolower($type);

+		if($type=='jpeg')

+			$type = 'jpg';

+		$mtd = '_parse'.$type;

+		if(!method_exists($this,$mtd))

+			$this->Error('Unsupported image type: '.$type);

+		$info = $this->$mtd($file);

+		$info['i'] = count($this->images)+1;

+		$this->images[$file] = $info;

+	}

+	else

+		$info = $this->images[$file];

+

+	// Automatic width and height calculation if needed

+	if($w==0 && $h==0)

+	{

+		// Put image at 96 dpi

+		$w = -96;

+		$h = -96;

+	}

+	if($w<0)

+		$w = -$info['w']*72/$w/$this->k;

+	if($h<0)

+		$h = -$info['h']*72/$h/$this->k;

+	if($w==0)

+		$w = $h*$info['w']/$info['h'];

+	if($h==0)

+		$h = $w*$info['h']/$info['w'];

+

+	// Flowing mode

+	if($y===null)

+	{

+		if($this->y+$h>$this->PageBreakTrigger && !$this->InHeader && !$this->InFooter && $this->AcceptPageBreak())

+		{

+			// Automatic page break

+			$x2 = $this->x;

+			$this->AddPage($this->CurOrientation,$this->CurPageSize,$this->CurRotation);

+			$this->x = $x2;

+		}

+		$y = $this->y;

+		$this->y += $h;

+	}

+

+	if($x===null)

+		$x = $this->x;

+	$this->_out(sprintf('q %.2F 0 0 %.2F %.2F %.2F cm /I%d Do Q',$w*$this->k,$h*$this->k,$x*$this->k,($this->h-($y+$h))*$this->k,$info['i']));

+	if($link)

+		$this->Link($x,$y,$w,$h,$link);

+}

+

+function GetPageWidth()

+{

+	// Get current page width

+	return $this->w;

+}

+

+function GetPageHeight()

+{

+	// Get current page height

+	return $this->h;

+}

+

+function GetX()

+{

+	// Get x position

+	return $this->x;

+}

+

+function SetX($x)

+{

+	// Set x position

+	if($x>=0)

+		$this->x = $x;

+	else

+		$this->x = $this->w+$x;

+}

+

+function GetY()

+{

+	// Get y position

+	return $this->y;

+}

+

+function SetY($y, $resetX=true)

+{

+	// Set y position and optionally reset x

+	if($y>=0)

+		$this->y = $y;

+	else

+		$this->y = $this->h+$y;

+	if($resetX)

+		$this->x = $this->lMargin;

+}

+

+function SetXY($x, $y)

+{

+	// Set x and y positions

+	$this->SetX($x);

+	$this->SetY($y,false);

+}

+

+function Output($dest='', $name='', $isUTF8=false)

+{

+	// Output PDF to some destination

+	$this->Close();

+	if(strlen($name)==1 && strlen($dest)!=1)

+	{

+		// Fix parameter order

+		$tmp = $dest;

+		$dest = $name;

+		$name = $tmp;

+	}

+	if($dest=='')

+		$dest = 'I';

+	if($name=='')

+		$name = 'doc.pdf';

+	switch(strtoupper($dest))

+	{

+		case 'I':

+			// Send to standard output

+			$this->_checkoutput();

+			if(PHP_SAPI!='cli')

+			{

+				// We send to a browser

+				header('Content-Type: application/pdf');

+				header('Content-Disposition: inline; '.$this->_httpencode('filename',$name,$isUTF8));

+				header('Cache-Control: private, max-age=0, must-revalidate');

+				header('Pragma: public');

+			}

+			echo $this->buffer;

+			break;

+		case 'D':

+			// Download file

+			$this->_checkoutput();

+			header('Content-Type: application/pdf');

+			header('Content-Disposition: attachment; '.$this->_httpencode('filename',$name,$isUTF8));

+			header('Cache-Control: private, max-age=0, must-revalidate');

+			header('Pragma: public');

+			echo $this->buffer;

+			break;

+		case 'F':

+			// Save to local file

+			if(!file_put_contents($name,$this->buffer))

+				$this->Error('Unable to create output file: '.$name);

+			break;

+		case 'S':

+			// Return as a string

+			return $this->buffer;

+		default:

+			$this->Error('Incorrect output destination: '.$dest);

+	}

+	return '';

+}

+

+/*******************************************************************************

+*                              Protected methods                               *

+*******************************************************************************/

+

+protected function _checkoutput()

+{

+	if(PHP_SAPI!='cli')

+	{

+		if(headers_sent($file,$line))

+			$this->Error("Some data has already been output, can't send PDF file (output started at $file:$line)");

+	}

+	if(ob_get_length())

+	{

+		// The output buffer is not empty

+		if(preg_match('/^(\xEF\xBB\xBF)?\s*$/',ob_get_contents()))

+		{

+			// It contains only a UTF-8 BOM and/or whitespace, let's clean it

+			ob_clean();

+		}

+		else

+			$this->Error("Some data has already been output, can't send PDF file");

+	}

+}

+

+protected function _getpagesize($size)

+{

+	if(is_string($size))

+	{

+		$size = strtolower($size);

+		if(!isset($this->StdPageSizes[$size]))

+			$this->Error('Unknown page size: '.$size);

+		$a = $this->StdPageSizes[$size];

+		return array($a[0]/$this->k, $a[1]/$this->k);

+	}

+	else

+	{

+		if($size[0]>$size[1])

+			return array($size[1], $size[0]);

+		else

+			return $size;

+	}

+}

+

+protected function _beginpage($orientation, $size, $rotation)

+{

+	$this->page++;

+	$this->pages[$this->page] = '';

+	$this->PageLinks[$this->page] = array();

+	$this->state = 2;

+	$this->x = $this->lMargin;

+	$this->y = $this->tMargin;

+	$this->FontFamily = '';

+	// Check page size and orientation

+	if($orientation=='')

+		$orientation = $this->DefOrientation;

+	else

+		$orientation = strtoupper($orientation[0]);

+	if($size=='')

+		$size = $this->DefPageSize;

+	else

+		$size = $this->_getpagesize($size);

+	if($orientation!=$this->CurOrientation || $size[0]!=$this->CurPageSize[0] || $size[1]!=$this->CurPageSize[1])

+	{

+		// New size or orientation

+		if($orientation=='P')

+		{

+			$this->w = $size[0];

+			$this->h = $size[1];

+		}

+		else

+		{

+			$this->w = $size[1];

+			$this->h = $size[0];

+		}

+		$this->wPt = $this->w*$this->k;

+		$this->hPt = $this->h*$this->k;

+		$this->PageBreakTrigger = $this->h-$this->bMargin;

+		$this->CurOrientation = $orientation;

+		$this->CurPageSize = $size;

+	}

+	if($orientation!=$this->DefOrientation || $size[0]!=$this->DefPageSize[0] || $size[1]!=$this->DefPageSize[1])

+		$this->PageInfo[$this->page]['size'] = array($this->wPt, $this->hPt);

+	if($rotation!=0)

+	{

+		if($rotation%90!=0)

+			$this->Error('Incorrect rotation value: '.$rotation);

+		$this->PageInfo[$this->page]['rotation'] = $rotation;

+	}

+	$this->CurRotation = $rotation;

+}

+

+protected function _endpage()

+{

+	$this->state = 1;

+}

+

+protected function _loadfont($path)

+{

+	// Load a font definition file

+	include($path);

+	if(!isset($name))

+		$this->Error('Could not include font definition file: '.$path);

+	if(isset($enc))

+		$enc = strtolower($enc);

+	if(!isset($subsetted))

+		$subsetted = false;

+	return get_defined_vars();

+}

+

+protected function _isascii($s)

+{

+	// Test if string is ASCII

+	$nb = strlen($s);

+	for($i=0;$i<$nb;$i++)

+	{

+		if(ord($s[$i])>127)

+			return false;

+	}

+	return true;

+}

+

+protected function _httpencode($param, $value, $isUTF8)

+{

+	// Encode HTTP header field parameter

+	if($this->_isascii($value))

+		return $param.'="'.$value.'"';

+	if(!$isUTF8)

+		$value = $this->_UTF8encode($value);

+	return $param."*=UTF-8''".rawurlencode($value);

+}

+

+protected function _UTF8encode($s)

+{

+	// Convert ISO-8859-1 to UTF-8

+	if($this->iconv)

+		return iconv('ISO-8859-1','UTF-8',$s);

+	$res = '';

+	$nb = strlen($s);

+	for($i=0;$i<$nb;$i++)

+	{

+		$c = $s[$i];

+		$v = ord($c);

+		if($v>=128)

+		{

+			$res .= chr(0xC0 | ($v >> 6));

+			$res .= chr(0x80 | ($v & 0x3F));

+		}

+		else

+			$res .= $c;

+	}

+	return $res;

+}

+

+protected function _UTF8toUTF16($s)

+{

+	// Convert UTF-8 to UTF-16BE with BOM

+	$res = "\xFE\xFF";

+	if($this->iconv)

+		return $res.iconv('UTF-8','UTF-16BE',$s);

+	$nb = strlen($s);

+	$i = 0;

+	while($i<$nb)

+	{

+		$c1 = ord($s[$i++]);

+		if($c1>=224)

+		{

+			// 3-byte character

+			$c2 = ord($s[$i++]);

+			$c3 = ord($s[$i++]);

+			$res .= chr((($c1 & 0x0F)<<4) + (($c2 & 0x3C)>>2));

+			$res .= chr((($c2 & 0x03)<<6) + ($c3 & 0x3F));

+		}

+		elseif($c1>=192)

+		{

+			// 2-byte character

+			$c2 = ord($s[$i++]);

+			$res .= chr(($c1 & 0x1C)>>2);

+			$res .= chr((($c1 & 0x03)<<6) + ($c2 & 0x3F));

+		}

+		else

+		{

+			// Single-byte character

+			$res .= "\0".chr($c1);

+		}

+	}

+	return $res;

+}

+

+protected function _escape($s)

+{

+	// Escape special characters

+	if(strpos($s,'(')!==false || strpos($s,')')!==false || strpos($s,'\\')!==false || strpos($s,"\r")!==false)

+		return str_replace(array('\\','(',')',"\r"), array('\\\\','\\(','\\)','\\r'), $s);

+	else

+		return $s;

+}

+

+protected function _textstring($s)

+{

+	// Format a text string

+	if(!$this->_isascii($s))

+		$s = $this->_UTF8toUTF16($s);

+	return '('.$this->_escape($s).')';

+}

+

+protected function _dounderline($x, $y, $txt)

+{

+	// Underline text

+	$up = $this->CurrentFont['up'];

+	$ut = $this->CurrentFont['ut'];

+	$w = $this->GetStringWidth($txt)+$this->ws*substr_count($txt,' ');

+	return sprintf('%.2F %.2F %.2F %.2F re f',$x*$this->k,($this->h-($y-$up/1000*$this->FontSize))*$this->k,$w*$this->k,-$ut/1000*$this->FontSizePt);

+}

+

+protected function _parsejpg($file)

+{

+	// Extract info from a JPEG file

+	$a = getimagesize($file);

+	if(!$a)

+		$this->Error('Missing or incorrect image file: '.$file);

+	if($a[2]!=2)

+		$this->Error('Not a JPEG file: '.$file);

+	if(!isset($a['channels']) || $a['channels']==3)

+		$colspace = 'DeviceRGB';

+	elseif($a['channels']==4)

+		$colspace = 'DeviceCMYK';

+	else

+		$colspace = 'DeviceGray';

+	$bpc = isset($a['bits']) ? $a['bits'] : 8;

+	$data = file_get_contents($file);

+	return array('w'=>$a[0], 'h'=>$a[1], 'cs'=>$colspace, 'bpc'=>$bpc, 'f'=>'DCTDecode', 'data'=>$data);

+}

+

+protected function _parsepng($file)

+{

+	// Extract info from a PNG file

+	$f = fopen($file,'rb');

+	if(!$f)

+		$this->Error('Can\'t open image file: '.$file);

+	$info = $this->_parsepngstream($f,$file);

+	fclose($f);

+	return $info;

+}

+

+protected function _parsepngstream($f, $file)

+{

+	// Check signature

+	if($this->_readstream($f,8)!=chr(137).'PNG'.chr(13).chr(10).chr(26).chr(10))

+		$this->Error('Not a PNG file: '.$file);

+

+	// Read header chunk

+	$this->_readstream($f,4);

+	if($this->_readstream($f,4)!='IHDR')

+		$this->Error('Incorrect PNG file: '.$file);

+	$w = $this->_readint($f);

+	$h = $this->_readint($f);

+	$bpc = ord($this->_readstream($f,1));

+	if($bpc>8)

+		$this->Error('16-bit depth not supported: '.$file);

+	$ct = ord($this->_readstream($f,1));

+	if($ct==0 || $ct==4)

+		$colspace = 'DeviceGray';

+	elseif($ct==2 || $ct==6)

+		$colspace = 'DeviceRGB';

+	elseif($ct==3)

+		$colspace = 'Indexed';

+	else

+		$this->Error('Unknown color type: '.$file);

+	if(ord($this->_readstream($f,1))!=0)

+		$this->Error('Unknown compression method: '.$file);

+	if(ord($this->_readstream($f,1))!=0)

+		$this->Error('Unknown filter method: '.$file);

+	if(ord($this->_readstream($f,1))!=0)

+		$this->Error('Interlacing not supported: '.$file);

+	$this->_readstream($f,4);

+	$dp = '/Predictor 15 /Colors '.($colspace=='DeviceRGB' ? 3 : 1).' /BitsPerComponent '.$bpc.' /Columns '.$w;

+

+	// Scan chunks looking for palette, transparency and image data

+	$pal = '';

+	$trns = '';

+	$data = '';

+	do

+	{

+		$n = $this->_readint($f);

+		$type = $this->_readstream($f,4);

+		if($type=='PLTE')

+		{

+			// Read palette

+			$pal = $this->_readstream($f,$n);

+			$this->_readstream($f,4);

+		}

+		elseif($type=='tRNS')

+		{

+			// Read transparency info

+			$t = $this->_readstream($f,$n);

+			if($ct==0)

+				$trns = array(ord(substr($t,1,1)));

+			elseif($ct==2)

+				$trns = array(ord(substr($t,1,1)), ord(substr($t,3,1)), ord(substr($t,5,1)));

+			else

+			{

+				$pos = strpos($t,chr(0));

+				if($pos!==false)

+					$trns = array($pos);

+			}

+			$this->_readstream($f,4);

+		}

+		elseif($type=='IDAT')

+		{

+			// Read image data block

+			$data .= $this->_readstream($f,$n);

+			$this->_readstream($f,4);

+		}

+		elseif($type=='IEND')

+			break;

+		else

+			$this->_readstream($f,$n+4);

+	}

+	while($n);

+

+	if($colspace=='Indexed' && empty($pal))

+		$this->Error('Missing palette in '.$file);

+	$info = array('w'=>$w, 'h'=>$h, 'cs'=>$colspace, 'bpc'=>$bpc, 'f'=>'FlateDecode', 'dp'=>$dp, 'pal'=>$pal, 'trns'=>$trns);

+	if($ct>=4)

+	{

+		// Extract alpha channel

+		if(!function_exists('gzuncompress'))

+			$this->Error('Zlib not available, can\'t handle alpha channel: '.$file);

+		$data = gzuncompress($data);

+		$color = '';

+		$alpha = '';

+		if($ct==4)

+		{

+			// Gray image

+			$len = 2*$w;

+			for($i=0;$i<$h;$i++)

+			{

+				$pos = (1+$len)*$i;

+				$color .= $data[$pos];

+				$alpha .= $data[$pos];

+				$line = substr($data,$pos+1,$len);

+				$color .= preg_replace('/(.)./s','$1',$line);

+				$alpha .= preg_replace('/.(.)/s','$1',$line);

+			}

+		}

+		else

+		{

+			// RGB image

+			$len = 4*$w;

+			for($i=0;$i<$h;$i++)

+			{

+				$pos = (1+$len)*$i;

+				$color .= $data[$pos];

+				$alpha .= $data[$pos];

+				$line = substr($data,$pos+1,$len);

+				$color .= preg_replace('/(.{3})./s','$1',$line);

+				$alpha .= preg_replace('/.{3}(.)/s','$1',$line);

+			}

+		}

+		unset($data);

+		$data = gzcompress($color);

+		$info['smask'] = gzcompress($alpha);

+		$this->WithAlpha = true;

+		if($this->PDFVersion<'1.4')

+			$this->PDFVersion = '1.4';

+	}

+	$info['data'] = $data;

+	return $info;

+}

+

+protected function _readstream($f, $n)

+{

+	// Read n bytes from stream

+	$res = '';

+	while($n>0 && !feof($f))

+	{

+		$s = fread($f,$n);

+		if($s===false)

+			$this->Error('Error while reading stream');

+		$n -= strlen($s);

+		$res .= $s;

+	}

+	if($n>0)

+		$this->Error('Unexpected end of stream');

+	return $res;

+}

+

+protected function _readint($f)

+{

+	// Read a 4-byte integer from stream

+	$a = unpack('Ni',$this->_readstream($f,4));

+	return $a['i'];

+}

+

+protected function _parsegif($file)

+{

+	// Extract info from a GIF file (via PNG conversion)

+	if(!function_exists('imagepng'))

+		$this->Error('GD extension is required for GIF support');

+	if(!function_exists('imagecreatefromgif'))

+		$this->Error('GD has no GIF read support');

+	$im = imagecreatefromgif($file);

+	if(!$im)

+		$this->Error('Missing or incorrect image file: '.$file);

+	imageinterlace($im,0);

+	ob_start();

+	imagepng($im);

+	$data = ob_get_clean();

+	imagedestroy($im);

+	$f = fopen('php://temp','rb+');

+	if(!$f)

+		$this->Error('Unable to create memory stream');

+	fwrite($f,$data);

+	rewind($f);

+	$info = $this->_parsepngstream($f,$file);

+	fclose($f);

+	return $info;

+}

+

+protected function _out($s)

+{

+	// Add a line to the current page

+	if($this->state==2)

+		$this->pages[$this->page] .= $s."\n";

+	elseif($this->state==0)

+		$this->Error('No page has been added yet');

+	elseif($this->state==1)

+		$this->Error('Invalid call');

+	elseif($this->state==3)

+		$this->Error('The document is closed');

+}

+

+protected function _put($s)

+{

+	// Add a line to the document

+	$this->buffer .= $s."\n";

+}

+

+protected function _getoffset()

+{

+	return strlen($this->buffer);

+}

+

+protected function _newobj($n=null)

+{

+	// Begin a new object

+	if($n===null)

+		$n = ++$this->n;

+	$this->offsets[$n] = $this->_getoffset();

+	$this->_put($n.' 0 obj');

+}

+

+protected function _putstream($data)

+{

+	$this->_put('stream');

+	$this->_put($data);

+	$this->_put('endstream');

+}

+

+protected function _putstreamobject($data)

+{

+	if($this->compress)

+	{

+		$entries = '/Filter /FlateDecode ';

+		$data = gzcompress($data);

+	}

+	else

+		$entries = '';

+	$entries .= '/Length '.strlen($data);

+	$this->_newobj();

+	$this->_put('<<'.$entries.'>>');

+	$this->_putstream($data);

+	$this->_put('endobj');

+}

+

+protected function _putlinks($n)

+{

+	foreach($this->PageLinks[$n] as $pl)

+	{

+		$this->_newobj();

+		$rect = sprintf('%.2F %.2F %.2F %.2F',$pl[0],$pl[1],$pl[0]+$pl[2],$pl[1]-$pl[3]);

+		$s = '<</Type /Annot /Subtype /Link /Rect ['.$rect.'] /Border [0 0 0] ';

+		if(is_string($pl[4]))

+			$s .= '/A <</S /URI /URI '.$this->_textstring($pl[4]).'>>>>';

+		else

+		{

+			$l = $this->links[$pl[4]];

+			if(isset($this->PageInfo[$l[0]]['size']))

+				$h = $this->PageInfo[$l[0]]['size'][1];

+			else

+				$h = ($this->DefOrientation=='P') ? $this->DefPageSize[1]*$this->k : $this->DefPageSize[0]*$this->k;

+			$s .= sprintf('/Dest [%d 0 R /XYZ 0 %.2F null]>>',$this->PageInfo[$l[0]]['n'],$h-$l[1]*$this->k);

+		}

+		$this->_put($s);

+		$this->_put('endobj');

+	}

+}

+

+protected function _putpage($n)

+{

+	$this->_newobj();

+	$this->_put('<</Type /Page');

+	$this->_put('/Parent 1 0 R');

+	if(isset($this->PageInfo[$n]['size']))

+		$this->_put(sprintf('/MediaBox [0 0 %.2F %.2F]',$this->PageInfo[$n]['size'][0],$this->PageInfo[$n]['size'][1]));

+	if(isset($this->PageInfo[$n]['rotation']))

+		$this->_put('/Rotate '.$this->PageInfo[$n]['rotation']);

+	$this->_put('/Resources 2 0 R');

+	if(!empty($this->PageLinks[$n]))

+	{

+		$s = '/Annots [';

+		foreach($this->PageLinks[$n] as $pl)

+			$s .= $pl[5].' 0 R ';

+		$s .= ']';

+		$this->_put($s);

+	}

+	if($this->WithAlpha)

+		$this->_put('/Group <</Type /Group /S /Transparency /CS /DeviceRGB>>');

+	$this->_put('/Contents '.($this->n+1).' 0 R>>');

+	$this->_put('endobj');

+	// Page content

+	if(!empty($this->AliasNbPages))

+		$this->pages[$n] = str_replace($this->AliasNbPages,$this->page,$this->pages[$n]);

+	$this->_putstreamobject($this->pages[$n]);

+	// Link annotations

+	$this->_putlinks($n);

+}

+

+protected function _putpages()

+{

+	$nb = $this->page;

+	$n = $this->n;

+	for($i=1;$i<=$nb;$i++)

+	{

+		$this->PageInfo[$i]['n'] = ++$n;

+		$n++;

+		foreach($this->PageLinks[$i] as &$pl)

+			$pl[5] = ++$n;

+		unset($pl);

+	}

+	for($i=1;$i<=$nb;$i++)

+		$this->_putpage($i);

+	// Pages root

+	$this->_newobj(1);

+	$this->_put('<</Type /Pages');

+	$kids = '/Kids [';

+	for($i=1;$i<=$nb;$i++)

+		$kids .= $this->PageInfo[$i]['n'].' 0 R ';

+	$kids .= ']';

+	$this->_put($kids);

+	$this->_put('/Count '.$nb);

+	if($this->DefOrientation=='P')

+	{

+		$w = $this->DefPageSize[0];

+		$h = $this->DefPageSize[1];

+	}

+	else

+	{

+		$w = $this->DefPageSize[1];

+		$h = $this->DefPageSize[0];

+	}

+	$this->_put(sprintf('/MediaBox [0 0 %.2F %.2F]',$w*$this->k,$h*$this->k));

+	$this->_put('>>');

+	$this->_put('endobj');

+}

+

+protected function _putfonts()

+{

+	foreach($this->FontFiles as $file=>$info)

+	{

+		// Font file embedding

+		$this->_newobj();

+		$this->FontFiles[$file]['n'] = $this->n;

+		$font = file_get_contents($file);

+		if(!$font)

+			$this->Error('Font file not found: '.$file);

+		$compressed = (substr($file,-2)=='.z');

+		if(!$compressed && isset($info['length2']))

+			$font = substr($font,6,$info['length1']).substr($font,6+$info['length1']+6,$info['length2']);

+		$this->_put('<</Length '.strlen($font));

+		if($compressed)

+			$this->_put('/Filter /FlateDecode');

+		$this->_put('/Length1 '.$info['length1']);

+		if(isset($info['length2']))

+			$this->_put('/Length2 '.$info['length2'].' /Length3 0');

+		$this->_put('>>');

+		$this->_putstream($font);

+		$this->_put('endobj');

+	}

+	foreach($this->fonts as $k=>$font)

+	{

+		// Encoding

+		if(isset($font['diff']))

+		{

+			if(!isset($this->encodings[$font['enc']]))

+			{

+				$this->_newobj();

+				$this->_put('<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences ['.$font['diff'].']>>');

+				$this->_put('endobj');

+				$this->encodings[$font['enc']] = $this->n;

+			}

+		}

+		// ToUnicode CMap

+		if(isset($font['uv']))

+		{

+			if(isset($font['enc']))

+				$cmapkey = $font['enc'];

+			else

+				$cmapkey = $font['name'];

+			if(!isset($this->cmaps[$cmapkey]))

+			{

+				$cmap = $this->_tounicodecmap($font['uv']);

+				$this->_putstreamobject($cmap);

+				$this->cmaps[$cmapkey] = $this->n;

+			}

+		}

+		// Font object

+		$this->fonts[$k]['n'] = $this->n+1;

+		$type = $font['type'];

+		$name = $font['name'];

+		if($font['subsetted'])

+			$name = 'AAAAAA+'.$name;

+		if($type=='Core')

+		{

+			// Core font

+			$this->_newobj();

+			$this->_put('<</Type /Font');

+			$this->_put('/BaseFont /'.$name);

+			$this->_put('/Subtype /Type1');

+			if($name!='Symbol' && $name!='ZapfDingbats')

+				$this->_put('/Encoding /WinAnsiEncoding');

+			if(isset($font['uv']))

+				$this->_put('/ToUnicode '.$this->cmaps[$cmapkey].' 0 R');

+			$this->_put('>>');

+			$this->_put('endobj');

+		}

+		elseif($type=='Type1' || $type=='TrueType')

+		{

+			// Additional Type1 or TrueType/OpenType font

+			$this->_newobj();

+			$this->_put('<</Type /Font');

+			$this->_put('/BaseFont /'.$name);

+			$this->_put('/Subtype /'.$type);

+			$this->_put('/FirstChar 32 /LastChar 255');

+			$this->_put('/Widths '.($this->n+1).' 0 R');

+			$this->_put('/FontDescriptor '.($this->n+2).' 0 R');

+			if(isset($font['diff']))

+				$this->_put('/Encoding '.$this->encodings[$font['enc']].' 0 R');

+			else

+				$this->_put('/Encoding /WinAnsiEncoding');

+			if(isset($font['uv']))

+				$this->_put('/ToUnicode '.$this->cmaps[$cmapkey].' 0 R');

+			$this->_put('>>');

+			$this->_put('endobj');

+			// Widths

+			$this->_newobj();

+			$cw = $font['cw'];

+			$s = '[';

+			for($i=32;$i<=255;$i++)

+				$s .= $cw[chr($i)].' ';

+			$this->_put($s.']');

+			$this->_put('endobj');

+			// Descriptor

+			$this->_newobj();

+			$s = '<</Type /FontDescriptor /FontName /'.$name;

+			foreach($font['desc'] as $k=>$v)

+				$s .= ' /'.$k.' '.$v;

+			if(!empty($font['file']))

+				$s .= ' /FontFile'.($type=='Type1' ? '' : '2').' '.$this->FontFiles[$font['file']]['n'].' 0 R';

+			$this->_put($s.'>>');

+			$this->_put('endobj');

+		}

+		else

+		{

+			// Allow for additional types

+			$mtd = '_put'.strtolower($type);

+			if(!method_exists($this,$mtd))

+				$this->Error('Unsupported font type: '.$type);

+			$this->$mtd($font);

+		}

+	}

+}

+

+protected function _tounicodecmap($uv)

+{

+	$ranges = '';

+	$nbr = 0;

+	$chars = '';

+	$nbc = 0;

+	foreach($uv as $c=>$v)

+	{

+		if(is_array($v))

+		{

+			$ranges .= sprintf("<%02X> <%02X> <%04X>\n",$c,$c+$v[1]-1,$v[0]);

+			$nbr++;

+		}

+		else

+		{

+			$chars .= sprintf("<%02X> <%04X>\n",$c,$v);

+			$nbc++;

+		}

+	}

+	$s = "/CIDInit /ProcSet findresource begin\n";

+	$s .= "12 dict begin\n";

+	$s .= "begincmap\n";

+	$s .= "/CIDSystemInfo\n";

+	$s .= "<</Registry (Adobe)\n";

+	$s .= "/Ordering (UCS)\n";

+	$s .= "/Supplement 0\n";

+	$s .= ">> def\n";

+	$s .= "/CMapName /Adobe-Identity-UCS def\n";

+	$s .= "/CMapType 2 def\n";

+	$s .= "1 begincodespacerange\n";

+	$s .= "<00> <FF>\n";

+	$s .= "endcodespacerange\n";

+	if($nbr>0)

+	{

+		$s .= "$nbr beginbfrange\n";

+		$s .= $ranges;

+		$s .= "endbfrange\n";

+	}

+	if($nbc>0)

+	{

+		$s .= "$nbc beginbfchar\n";

+		$s .= $chars;

+		$s .= "endbfchar\n";

+	}

+	$s .= "endcmap\n";

+	$s .= "CMapName currentdict /CMap defineresource pop\n";

+	$s .= "end\n";

+	$s .= "end";

+	return $s;

+}

+

+protected function _putimages()

+{

+	foreach(array_keys($this->images) as $file)

+	{

+		$this->_putimage($this->images[$file]);

+		unset($this->images[$file]['data']);

+		unset($this->images[$file]['smask']);

+	}

+}

+

+protected function _putimage(&$info)

+{

+	$this->_newobj();

+	$info['n'] = $this->n;

+	$this->_put('<</Type /XObject');

+	$this->_put('/Subtype /Image');

+	$this->_put('/Width '.$info['w']);

+	$this->_put('/Height '.$info['h']);

+	if($info['cs']=='Indexed')

+		$this->_put('/ColorSpace [/Indexed /DeviceRGB '.(strlen($info['pal'])/3-1).' '.($this->n+1).' 0 R]');

+	else

+	{

+		$this->_put('/ColorSpace /'.$info['cs']);

+		if($info['cs']=='DeviceCMYK')

+			$this->_put('/Decode [1 0 1 0 1 0 1 0]');

+	}

+	$this->_put('/BitsPerComponent '.$info['bpc']);

+	if(isset($info['f']))

+		$this->_put('/Filter /'.$info['f']);

+	if(isset($info['dp']))

+		$this->_put('/DecodeParms <<'.$info['dp'].'>>');

+	if(isset($info['trns']) && is_array($info['trns']))

+	{

+		$trns = '';

+		for($i=0;$i<count($info['trns']);$i++)

+			$trns .= $info['trns'][$i].' '.$info['trns'][$i].' ';

+		$this->_put('/Mask ['.$trns.']');

+	}

+	if(isset($info['smask']))

+		$this->_put('/SMask '.($this->n+1).' 0 R');

+	$this->_put('/Length '.strlen($info['data']).'>>');

+	$this->_putstream($info['data']);

+	$this->_put('endobj');

+	// Soft mask

+	if(isset($info['smask']))

+	{

+		$dp = '/Predictor 15 /Colors 1 /BitsPerComponent 8 /Columns '.$info['w'];

+		$smask = array('w'=>$info['w'], 'h'=>$info['h'], 'cs'=>'DeviceGray', 'bpc'=>8, 'f'=>$info['f'], 'dp'=>$dp, 'data'=>$info['smask']);

+		$this->_putimage($smask);

+	}

+	// Palette

+	if($info['cs']=='Indexed')

+		$this->_putstreamobject($info['pal']);

+}

+

+protected function _putxobjectdict()

+{

+	foreach($this->images as $image)

+		$this->_put('/I'.$image['i'].' '.$image['n'].' 0 R');

+}

+

+protected function _putresourcedict()

+{

+	$this->_put('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]');

+	$this->_put('/Font <<');

+	foreach($this->fonts as $font)

+		$this->_put('/F'.$font['i'].' '.$font['n'].' 0 R');

+	$this->_put('>>');

+	$this->_put('/XObject <<');

+	$this->_putxobjectdict();

+	$this->_put('>>');

+}

+

+protected function _putresources()

+{

+	$this->_putfonts();

+	$this->_putimages();

+	// Resource dictionary

+	$this->_newobj(2);

+	$this->_put('<<');

+	$this->_putresourcedict();

+	$this->_put('>>');

+	$this->_put('endobj');

+}

+

+protected function _putinfo()

+{

+	$date = @date('YmdHisO',$this->CreationDate);

+	$this->metadata['CreationDate'] = 'D:'.substr($date,0,-2)."'".substr($date,-2)."'";

+	foreach($this->metadata as $key=>$value)

+		$this->_put('/'.$key.' '.$this->_textstring($value));

+}

+

+protected function _putcatalog()

+{

+	$n = $this->PageInfo[1]['n'];

+	$this->_put('/Type /Catalog');

+	$this->_put('/Pages 1 0 R');

+	if($this->ZoomMode=='fullpage')

+		$this->_put('/OpenAction ['.$n.' 0 R /Fit]');

+	elseif($this->ZoomMode=='fullwidth')

+		$this->_put('/OpenAction ['.$n.' 0 R /FitH null]');

+	elseif($this->ZoomMode=='real')

+		$this->_put('/OpenAction ['.$n.' 0 R /XYZ null null 1]');

+	elseif(!is_string($this->ZoomMode))

+		$this->_put('/OpenAction ['.$n.' 0 R /XYZ null null '.sprintf('%.2F',$this->ZoomMode/100).']');

+	if($this->LayoutMode=='single')

+		$this->_put('/PageLayout /SinglePage');

+	elseif($this->LayoutMode=='continuous')

+		$this->_put('/PageLayout /OneColumn');

+	elseif($this->LayoutMode=='two')

+		$this->_put('/PageLayout /TwoColumnLeft');

+}

+

+protected function _putheader()

+{

+	$this->_put('%PDF-'.$this->PDFVersion);

+}

+

+protected function _puttrailer()

+{

+	$this->_put('/Size '.($this->n+1));

+	$this->_put('/Root '.$this->n.' 0 R');

+	$this->_put('/Info '.($this->n-1).' 0 R');

+}

+

+protected function _enddoc()

+{

+	$this->CreationDate = time();

+	$this->_putheader();

+	$this->_putpages();

+	$this->_putresources();

+	// Info

+	$this->_newobj();

+	$this->_put('<<');

+	$this->_putinfo();

+	$this->_put('>>');

+	$this->_put('endobj');

+	// Catalog

+	$this->_newobj();

+	$this->_put('<<');

+	$this->_putcatalog();

+	$this->_put('>>');

+	$this->_put('endobj');

+	// Cross-ref

+	$offset = $this->_getoffset();

+	$this->_put('xref');

+	$this->_put('0 '.($this->n+1));

+	$this->_put('0000000000 65535 f ');

+	for($i=1;$i<=$this->n;$i++)

+		$this->_put(sprintf('%010d 00000 n ',$this->offsets[$i]));

+	// Trailer

+	$this->_put('trailer');

+	$this->_put('<<');

+	$this->_puttrailer();

+	$this->_put('>>');

+	$this->_put('startxref');

+	$this->_put($offset);

+	$this->_put('%%EOF');

+	$this->state = 3;

+}

+}

+?>

diff --git a/src/lib/fpdf/install.txt b/src/lib/fpdf/install.txt
new file mode 100644
index 0000000..73ded64
--- /dev/null
+++ b/src/lib/fpdf/install.txt
@@ -0,0 +1,15 @@
+The FPDF library is made up of the following elements:

+

+- the main file, fpdf.php, which contains the class

+- the font definition files located in the font directory

+

+The font definition files are necessary as soon as you want to output some text in a document.

+If they are not accessible, the SetFont() method will produce the following error:

+

+FPDF error: Could not include font definition file

+

+

+Remarks:

+

+- Only the files corresponding to the fonts actually used are necessary

+- The tutorials provided in this package are ready to be executed

diff --git a/src/lib/fpdf/license.txt b/src/lib/fpdf/license.txt
new file mode 100644
index 0000000..fd811c6
--- /dev/null
+++ b/src/lib/fpdf/license.txt
@@ -0,0 +1,6 @@
+Permission is hereby granted, free of charge, to any person obtaining a copy

+of this software to use, copy, modify, distribute, sublicense, and/or sell

+copies of the software, and to permit persons to whom the software is furnished

+to do so.

+

+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED.
\ No newline at end of file
diff --git a/src/lib/fpdf/makefont/cp1250.map b/src/lib/fpdf/makefont/cp1250.map
new file mode 100644
index 0000000..ec110af
--- /dev/null
+++ b/src/lib/fpdf/makefont/cp1250.map
@@ -0,0 +1,251 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+20AC Euro
+!82 U+201A quotesinglbase
+!84 U+201E quotedblbase
+!85 U+2026 ellipsis
+!86 U+2020 dagger
+!87 U+2021 daggerdbl
+!89 U+2030 perthousand
+!8A U+0160 Scaron
+!8B U+2039 guilsinglleft
+!8C U+015A Sacute
+!8D U+0164 Tcaron
+!8E U+017D Zcaron
+!8F U+0179 Zacute
+!91 U+2018 quoteleft
+!92 U+2019 quoteright
+!93 U+201C quotedblleft
+!94 U+201D quotedblright
+!95 U+2022 bullet
+!96 U+2013 endash
+!97 U+2014 emdash
+!99 U+2122 trademark
+!9A U+0161 scaron
+!9B U+203A guilsinglright
+!9C U+015B sacute
+!9D U+0165 tcaron
+!9E U+017E zcaron
+!9F U+017A zacute
+!A0 U+00A0 space
+!A1 U+02C7 caron
+!A2 U+02D8 breve
+!A3 U+0141 Lslash
+!A4 U+00A4 currency
+!A5 U+0104 Aogonek
+!A6 U+00A6 brokenbar
+!A7 U+00A7 section
+!A8 U+00A8 dieresis
+!A9 U+00A9 copyright
+!AA U+015E Scedilla
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD hyphen
+!AE U+00AE registered
+!AF U+017B Zdotaccent
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+02DB ogonek
+!B3 U+0142 lslash
+!B4 U+00B4 acute
+!B5 U+00B5 mu
+!B6 U+00B6 paragraph
+!B7 U+00B7 periodcentered
+!B8 U+00B8 cedilla
+!B9 U+0105 aogonek
+!BA U+015F scedilla
+!BB U+00BB guillemotright
+!BC U+013D Lcaron
+!BD U+02DD hungarumlaut
+!BE U+013E lcaron
+!BF U+017C zdotaccent
+!C0 U+0154 Racute
+!C1 U+00C1 Aacute
+!C2 U+00C2 Acircumflex
+!C3 U+0102 Abreve
+!C4 U+00C4 Adieresis
+!C5 U+0139 Lacute
+!C6 U+0106 Cacute
+!C7 U+00C7 Ccedilla
+!C8 U+010C Ccaron
+!C9 U+00C9 Eacute
+!CA U+0118 Eogonek
+!CB U+00CB Edieresis
+!CC U+011A Ecaron
+!CD U+00CD Iacute
+!CE U+00CE Icircumflex
+!CF U+010E Dcaron
+!D0 U+0110 Dcroat
+!D1 U+0143 Nacute
+!D2 U+0147 Ncaron
+!D3 U+00D3 Oacute
+!D4 U+00D4 Ocircumflex
+!D5 U+0150 Ohungarumlaut
+!D6 U+00D6 Odieresis
+!D7 U+00D7 multiply
+!D8 U+0158 Rcaron
+!D9 U+016E Uring
+!DA U+00DA Uacute
+!DB U+0170 Uhungarumlaut
+!DC U+00DC Udieresis
+!DD U+00DD Yacute
+!DE U+0162 Tcommaaccent
+!DF U+00DF germandbls
+!E0 U+0155 racute
+!E1 U+00E1 aacute
+!E2 U+00E2 acircumflex
+!E3 U+0103 abreve
+!E4 U+00E4 adieresis
+!E5 U+013A lacute
+!E6 U+0107 cacute
+!E7 U+00E7 ccedilla
+!E8 U+010D ccaron
+!E9 U+00E9 eacute
+!EA U+0119 eogonek
+!EB U+00EB edieresis
+!EC U+011B ecaron
+!ED U+00ED iacute
+!EE U+00EE icircumflex
+!EF U+010F dcaron
+!F0 U+0111 dcroat
+!F1 U+0144 nacute
+!F2 U+0148 ncaron
+!F3 U+00F3 oacute
+!F4 U+00F4 ocircumflex
+!F5 U+0151 ohungarumlaut
+!F6 U+00F6 odieresis
+!F7 U+00F7 divide
+!F8 U+0159 rcaron
+!F9 U+016F uring
+!FA U+00FA uacute
+!FB U+0171 uhungarumlaut
+!FC U+00FC udieresis
+!FD U+00FD yacute
+!FE U+0163 tcommaaccent
+!FF U+02D9 dotaccent
diff --git a/src/lib/fpdf/makefont/cp1251.map b/src/lib/fpdf/makefont/cp1251.map
new file mode 100644
index 0000000..de6a198
--- /dev/null
+++ b/src/lib/fpdf/makefont/cp1251.map
@@ -0,0 +1,255 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+0402 afii10051
+!81 U+0403 afii10052
+!82 U+201A quotesinglbase
+!83 U+0453 afii10100
+!84 U+201E quotedblbase
+!85 U+2026 ellipsis
+!86 U+2020 dagger
+!87 U+2021 daggerdbl
+!88 U+20AC Euro
+!89 U+2030 perthousand
+!8A U+0409 afii10058
+!8B U+2039 guilsinglleft
+!8C U+040A afii10059
+!8D U+040C afii10061
+!8E U+040B afii10060
+!8F U+040F afii10145
+!90 U+0452 afii10099
+!91 U+2018 quoteleft
+!92 U+2019 quoteright
+!93 U+201C quotedblleft
+!94 U+201D quotedblright
+!95 U+2022 bullet
+!96 U+2013 endash
+!97 U+2014 emdash
+!99 U+2122 trademark
+!9A U+0459 afii10106
+!9B U+203A guilsinglright
+!9C U+045A afii10107
+!9D U+045C afii10109
+!9E U+045B afii10108
+!9F U+045F afii10193
+!A0 U+00A0 space
+!A1 U+040E afii10062
+!A2 U+045E afii10110
+!A3 U+0408 afii10057
+!A4 U+00A4 currency
+!A5 U+0490 afii10050
+!A6 U+00A6 brokenbar
+!A7 U+00A7 section
+!A8 U+0401 afii10023
+!A9 U+00A9 copyright
+!AA U+0404 afii10053
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD hyphen
+!AE U+00AE registered
+!AF U+0407 afii10056
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+0406 afii10055
+!B3 U+0456 afii10103
+!B4 U+0491 afii10098
+!B5 U+00B5 mu
+!B6 U+00B6 paragraph
+!B7 U+00B7 periodcentered
+!B8 U+0451 afii10071
+!B9 U+2116 afii61352
+!BA U+0454 afii10101
+!BB U+00BB guillemotright
+!BC U+0458 afii10105
+!BD U+0405 afii10054
+!BE U+0455 afii10102
+!BF U+0457 afii10104
+!C0 U+0410 afii10017
+!C1 U+0411 afii10018
+!C2 U+0412 afii10019
+!C3 U+0413 afii10020
+!C4 U+0414 afii10021
+!C5 U+0415 afii10022
+!C6 U+0416 afii10024
+!C7 U+0417 afii10025
+!C8 U+0418 afii10026
+!C9 U+0419 afii10027
+!CA U+041A afii10028
+!CB U+041B afii10029
+!CC U+041C afii10030
+!CD U+041D afii10031
+!CE U+041E afii10032
+!CF U+041F afii10033
+!D0 U+0420 afii10034
+!D1 U+0421 afii10035
+!D2 U+0422 afii10036
+!D3 U+0423 afii10037
+!D4 U+0424 afii10038
+!D5 U+0425 afii10039
+!D6 U+0426 afii10040
+!D7 U+0427 afii10041
+!D8 U+0428 afii10042
+!D9 U+0429 afii10043
+!DA U+042A afii10044
+!DB U+042B afii10045
+!DC U+042C afii10046
+!DD U+042D afii10047
+!DE U+042E afii10048
+!DF U+042F afii10049
+!E0 U+0430 afii10065
+!E1 U+0431 afii10066
+!E2 U+0432 afii10067
+!E3 U+0433 afii10068
+!E4 U+0434 afii10069
+!E5 U+0435 afii10070
+!E6 U+0436 afii10072
+!E7 U+0437 afii10073
+!E8 U+0438 afii10074
+!E9 U+0439 afii10075
+!EA U+043A afii10076
+!EB U+043B afii10077
+!EC U+043C afii10078
+!ED U+043D afii10079
+!EE U+043E afii10080
+!EF U+043F afii10081
+!F0 U+0440 afii10082
+!F1 U+0441 afii10083
+!F2 U+0442 afii10084
+!F3 U+0443 afii10085
+!F4 U+0444 afii10086
+!F5 U+0445 afii10087
+!F6 U+0446 afii10088
+!F7 U+0447 afii10089
+!F8 U+0448 afii10090
+!F9 U+0449 afii10091
+!FA U+044A afii10092
+!FB U+044B afii10093
+!FC U+044C afii10094
+!FD U+044D afii10095
+!FE U+044E afii10096
+!FF U+044F afii10097
diff --git a/src/lib/fpdf/makefont/cp1252.map b/src/lib/fpdf/makefont/cp1252.map
new file mode 100644
index 0000000..dd490e5
--- /dev/null
+++ b/src/lib/fpdf/makefont/cp1252.map
@@ -0,0 +1,251 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+20AC Euro
+!82 U+201A quotesinglbase
+!83 U+0192 florin
+!84 U+201E quotedblbase
+!85 U+2026 ellipsis
+!86 U+2020 dagger
+!87 U+2021 daggerdbl
+!88 U+02C6 circumflex
+!89 U+2030 perthousand
+!8A U+0160 Scaron
+!8B U+2039 guilsinglleft
+!8C U+0152 OE
+!8E U+017D Zcaron
+!91 U+2018 quoteleft
+!92 U+2019 quoteright
+!93 U+201C quotedblleft
+!94 U+201D quotedblright
+!95 U+2022 bullet
+!96 U+2013 endash
+!97 U+2014 emdash
+!98 U+02DC tilde
+!99 U+2122 trademark
+!9A U+0161 scaron
+!9B U+203A guilsinglright
+!9C U+0153 oe
+!9E U+017E zcaron
+!9F U+0178 Ydieresis
+!A0 U+00A0 space
+!A1 U+00A1 exclamdown
+!A2 U+00A2 cent
+!A3 U+00A3 sterling
+!A4 U+00A4 currency
+!A5 U+00A5 yen
+!A6 U+00A6 brokenbar
+!A7 U+00A7 section
+!A8 U+00A8 dieresis
+!A9 U+00A9 copyright
+!AA U+00AA ordfeminine
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD hyphen
+!AE U+00AE registered
+!AF U+00AF macron
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+00B2 twosuperior
+!B3 U+00B3 threesuperior
+!B4 U+00B4 acute
+!B5 U+00B5 mu
+!B6 U+00B6 paragraph
+!B7 U+00B7 periodcentered
+!B8 U+00B8 cedilla
+!B9 U+00B9 onesuperior
+!BA U+00BA ordmasculine
+!BB U+00BB guillemotright
+!BC U+00BC onequarter
+!BD U+00BD onehalf
+!BE U+00BE threequarters
+!BF U+00BF questiondown
+!C0 U+00C0 Agrave
+!C1 U+00C1 Aacute
+!C2 U+00C2 Acircumflex
+!C3 U+00C3 Atilde
+!C4 U+00C4 Adieresis
+!C5 U+00C5 Aring
+!C6 U+00C6 AE
+!C7 U+00C7 Ccedilla
+!C8 U+00C8 Egrave
+!C9 U+00C9 Eacute
+!CA U+00CA Ecircumflex
+!CB U+00CB Edieresis
+!CC U+00CC Igrave
+!CD U+00CD Iacute
+!CE U+00CE Icircumflex
+!CF U+00CF Idieresis
+!D0 U+00D0 Eth
+!D1 U+00D1 Ntilde
+!D2 U+00D2 Ograve
+!D3 U+00D3 Oacute
+!D4 U+00D4 Ocircumflex
+!D5 U+00D5 Otilde
+!D6 U+00D6 Odieresis
+!D7 U+00D7 multiply
+!D8 U+00D8 Oslash
+!D9 U+00D9 Ugrave
+!DA U+00DA Uacute
+!DB U+00DB Ucircumflex
+!DC U+00DC Udieresis
+!DD U+00DD Yacute
+!DE U+00DE Thorn
+!DF U+00DF germandbls
+!E0 U+00E0 agrave
+!E1 U+00E1 aacute
+!E2 U+00E2 acircumflex
+!E3 U+00E3 atilde
+!E4 U+00E4 adieresis
+!E5 U+00E5 aring
+!E6 U+00E6 ae
+!E7 U+00E7 ccedilla
+!E8 U+00E8 egrave
+!E9 U+00E9 eacute
+!EA U+00EA ecircumflex
+!EB U+00EB edieresis
+!EC U+00EC igrave
+!ED U+00ED iacute
+!EE U+00EE icircumflex
+!EF U+00EF idieresis
+!F0 U+00F0 eth
+!F1 U+00F1 ntilde
+!F2 U+00F2 ograve
+!F3 U+00F3 oacute
+!F4 U+00F4 ocircumflex
+!F5 U+00F5 otilde
+!F6 U+00F6 odieresis
+!F7 U+00F7 divide
+!F8 U+00F8 oslash
+!F9 U+00F9 ugrave
+!FA U+00FA uacute
+!FB U+00FB ucircumflex
+!FC U+00FC udieresis
+!FD U+00FD yacute
+!FE U+00FE thorn
+!FF U+00FF ydieresis
diff --git a/src/lib/fpdf/makefont/cp1253.map b/src/lib/fpdf/makefont/cp1253.map
new file mode 100644
index 0000000..4bd826f
--- /dev/null
+++ b/src/lib/fpdf/makefont/cp1253.map
@@ -0,0 +1,239 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+20AC Euro
+!82 U+201A quotesinglbase
+!83 U+0192 florin
+!84 U+201E quotedblbase
+!85 U+2026 ellipsis
+!86 U+2020 dagger
+!87 U+2021 daggerdbl
+!89 U+2030 perthousand
+!8B U+2039 guilsinglleft
+!91 U+2018 quoteleft
+!92 U+2019 quoteright
+!93 U+201C quotedblleft
+!94 U+201D quotedblright
+!95 U+2022 bullet
+!96 U+2013 endash
+!97 U+2014 emdash
+!99 U+2122 trademark
+!9B U+203A guilsinglright
+!A0 U+00A0 space
+!A1 U+0385 dieresistonos
+!A2 U+0386 Alphatonos
+!A3 U+00A3 sterling
+!A4 U+00A4 currency
+!A5 U+00A5 yen
+!A6 U+00A6 brokenbar
+!A7 U+00A7 section
+!A8 U+00A8 dieresis
+!A9 U+00A9 copyright
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD hyphen
+!AE U+00AE registered
+!AF U+2015 afii00208
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+00B2 twosuperior
+!B3 U+00B3 threesuperior
+!B4 U+0384 tonos
+!B5 U+00B5 mu
+!B6 U+00B6 paragraph
+!B7 U+00B7 periodcentered
+!B8 U+0388 Epsilontonos
+!B9 U+0389 Etatonos
+!BA U+038A Iotatonos
+!BB U+00BB guillemotright
+!BC U+038C Omicrontonos
+!BD U+00BD onehalf
+!BE U+038E Upsilontonos
+!BF U+038F Omegatonos
+!C0 U+0390 iotadieresistonos
+!C1 U+0391 Alpha
+!C2 U+0392 Beta
+!C3 U+0393 Gamma
+!C4 U+0394 Delta
+!C5 U+0395 Epsilon
+!C6 U+0396 Zeta
+!C7 U+0397 Eta
+!C8 U+0398 Theta
+!C9 U+0399 Iota
+!CA U+039A Kappa
+!CB U+039B Lambda
+!CC U+039C Mu
+!CD U+039D Nu
+!CE U+039E Xi
+!CF U+039F Omicron
+!D0 U+03A0 Pi
+!D1 U+03A1 Rho
+!D3 U+03A3 Sigma
+!D4 U+03A4 Tau
+!D5 U+03A5 Upsilon
+!D6 U+03A6 Phi
+!D7 U+03A7 Chi
+!D8 U+03A8 Psi
+!D9 U+03A9 Omega
+!DA U+03AA Iotadieresis
+!DB U+03AB Upsilondieresis
+!DC U+03AC alphatonos
+!DD U+03AD epsilontonos
+!DE U+03AE etatonos
+!DF U+03AF iotatonos
+!E0 U+03B0 upsilondieresistonos
+!E1 U+03B1 alpha
+!E2 U+03B2 beta
+!E3 U+03B3 gamma
+!E4 U+03B4 delta
+!E5 U+03B5 epsilon
+!E6 U+03B6 zeta
+!E7 U+03B7 eta
+!E8 U+03B8 theta
+!E9 U+03B9 iota
+!EA U+03BA kappa
+!EB U+03BB lambda
+!EC U+03BC mu
+!ED U+03BD nu
+!EE U+03BE xi
+!EF U+03BF omicron
+!F0 U+03C0 pi
+!F1 U+03C1 rho
+!F2 U+03C2 sigma1
+!F3 U+03C3 sigma
+!F4 U+03C4 tau
+!F5 U+03C5 upsilon
+!F6 U+03C6 phi
+!F7 U+03C7 chi
+!F8 U+03C8 psi
+!F9 U+03C9 omega
+!FA U+03CA iotadieresis
+!FB U+03CB upsilondieresis
+!FC U+03CC omicrontonos
+!FD U+03CD upsilontonos
+!FE U+03CE omegatonos
diff --git a/src/lib/fpdf/makefont/cp1254.map b/src/lib/fpdf/makefont/cp1254.map
new file mode 100644
index 0000000..829473b
--- /dev/null
+++ b/src/lib/fpdf/makefont/cp1254.map
@@ -0,0 +1,249 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+20AC Euro
+!82 U+201A quotesinglbase
+!83 U+0192 florin
+!84 U+201E quotedblbase
+!85 U+2026 ellipsis
+!86 U+2020 dagger
+!87 U+2021 daggerdbl
+!88 U+02C6 circumflex
+!89 U+2030 perthousand
+!8A U+0160 Scaron
+!8B U+2039 guilsinglleft
+!8C U+0152 OE
+!91 U+2018 quoteleft
+!92 U+2019 quoteright
+!93 U+201C quotedblleft
+!94 U+201D quotedblright
+!95 U+2022 bullet
+!96 U+2013 endash
+!97 U+2014 emdash
+!98 U+02DC tilde
+!99 U+2122 trademark
+!9A U+0161 scaron
+!9B U+203A guilsinglright
+!9C U+0153 oe
+!9F U+0178 Ydieresis
+!A0 U+00A0 space
+!A1 U+00A1 exclamdown
+!A2 U+00A2 cent
+!A3 U+00A3 sterling
+!A4 U+00A4 currency
+!A5 U+00A5 yen
+!A6 U+00A6 brokenbar
+!A7 U+00A7 section
+!A8 U+00A8 dieresis
+!A9 U+00A9 copyright
+!AA U+00AA ordfeminine
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD hyphen
+!AE U+00AE registered
+!AF U+00AF macron
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+00B2 twosuperior
+!B3 U+00B3 threesuperior
+!B4 U+00B4 acute
+!B5 U+00B5 mu
+!B6 U+00B6 paragraph
+!B7 U+00B7 periodcentered
+!B8 U+00B8 cedilla
+!B9 U+00B9 onesuperior
+!BA U+00BA ordmasculine
+!BB U+00BB guillemotright
+!BC U+00BC onequarter
+!BD U+00BD onehalf
+!BE U+00BE threequarters
+!BF U+00BF questiondown
+!C0 U+00C0 Agrave
+!C1 U+00C1 Aacute
+!C2 U+00C2 Acircumflex
+!C3 U+00C3 Atilde
+!C4 U+00C4 Adieresis
+!C5 U+00C5 Aring
+!C6 U+00C6 AE
+!C7 U+00C7 Ccedilla
+!C8 U+00C8 Egrave
+!C9 U+00C9 Eacute
+!CA U+00CA Ecircumflex
+!CB U+00CB Edieresis
+!CC U+00CC Igrave
+!CD U+00CD Iacute
+!CE U+00CE Icircumflex
+!CF U+00CF Idieresis
+!D0 U+011E Gbreve
+!D1 U+00D1 Ntilde
+!D2 U+00D2 Ograve
+!D3 U+00D3 Oacute
+!D4 U+00D4 Ocircumflex
+!D5 U+00D5 Otilde
+!D6 U+00D6 Odieresis
+!D7 U+00D7 multiply
+!D8 U+00D8 Oslash
+!D9 U+00D9 Ugrave
+!DA U+00DA Uacute
+!DB U+00DB Ucircumflex
+!DC U+00DC Udieresis
+!DD U+0130 Idotaccent
+!DE U+015E Scedilla
+!DF U+00DF germandbls
+!E0 U+00E0 agrave
+!E1 U+00E1 aacute
+!E2 U+00E2 acircumflex
+!E3 U+00E3 atilde
+!E4 U+00E4 adieresis
+!E5 U+00E5 aring
+!E6 U+00E6 ae
+!E7 U+00E7 ccedilla
+!E8 U+00E8 egrave
+!E9 U+00E9 eacute
+!EA U+00EA ecircumflex
+!EB U+00EB edieresis
+!EC U+00EC igrave
+!ED U+00ED iacute
+!EE U+00EE icircumflex
+!EF U+00EF idieresis
+!F0 U+011F gbreve
+!F1 U+00F1 ntilde
+!F2 U+00F2 ograve
+!F3 U+00F3 oacute
+!F4 U+00F4 ocircumflex
+!F5 U+00F5 otilde
+!F6 U+00F6 odieresis
+!F7 U+00F7 divide
+!F8 U+00F8 oslash
+!F9 U+00F9 ugrave
+!FA U+00FA uacute
+!FB U+00FB ucircumflex
+!FC U+00FC udieresis
+!FD U+0131 dotlessi
+!FE U+015F scedilla
+!FF U+00FF ydieresis
diff --git a/src/lib/fpdf/makefont/cp1255.map b/src/lib/fpdf/makefont/cp1255.map
new file mode 100644
index 0000000..079e10c
--- /dev/null
+++ b/src/lib/fpdf/makefont/cp1255.map
@@ -0,0 +1,233 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+20AC Euro
+!82 U+201A quotesinglbase
+!83 U+0192 florin
+!84 U+201E quotedblbase
+!85 U+2026 ellipsis
+!86 U+2020 dagger
+!87 U+2021 daggerdbl
+!88 U+02C6 circumflex
+!89 U+2030 perthousand
+!8B U+2039 guilsinglleft
+!91 U+2018 quoteleft
+!92 U+2019 quoteright
+!93 U+201C quotedblleft
+!94 U+201D quotedblright
+!95 U+2022 bullet
+!96 U+2013 endash
+!97 U+2014 emdash
+!98 U+02DC tilde
+!99 U+2122 trademark
+!9B U+203A guilsinglright
+!A0 U+00A0 space
+!A1 U+00A1 exclamdown
+!A2 U+00A2 cent
+!A3 U+00A3 sterling
+!A4 U+20AA afii57636
+!A5 U+00A5 yen
+!A6 U+00A6 brokenbar
+!A7 U+00A7 section
+!A8 U+00A8 dieresis
+!A9 U+00A9 copyright
+!AA U+00D7 multiply
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD sfthyphen
+!AE U+00AE registered
+!AF U+00AF macron
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+00B2 twosuperior
+!B3 U+00B3 threesuperior
+!B4 U+00B4 acute
+!B5 U+00B5 mu
+!B6 U+00B6 paragraph
+!B7 U+00B7 middot
+!B8 U+00B8 cedilla
+!B9 U+00B9 onesuperior
+!BA U+00F7 divide
+!BB U+00BB guillemotright
+!BC U+00BC onequarter
+!BD U+00BD onehalf
+!BE U+00BE threequarters
+!BF U+00BF questiondown
+!C0 U+05B0 afii57799
+!C1 U+05B1 afii57801
+!C2 U+05B2 afii57800
+!C3 U+05B3 afii57802
+!C4 U+05B4 afii57793
+!C5 U+05B5 afii57794
+!C6 U+05B6 afii57795
+!C7 U+05B7 afii57798
+!C8 U+05B8 afii57797
+!C9 U+05B9 afii57806
+!CB U+05BB afii57796
+!CC U+05BC afii57807
+!CD U+05BD afii57839
+!CE U+05BE afii57645
+!CF U+05BF afii57841
+!D0 U+05C0 afii57842
+!D1 U+05C1 afii57804
+!D2 U+05C2 afii57803
+!D3 U+05C3 afii57658
+!D4 U+05F0 afii57716
+!D5 U+05F1 afii57717
+!D6 U+05F2 afii57718
+!D7 U+05F3 gereshhebrew
+!D8 U+05F4 gershayimhebrew
+!E0 U+05D0 afii57664
+!E1 U+05D1 afii57665
+!E2 U+05D2 afii57666
+!E3 U+05D3 afii57667
+!E4 U+05D4 afii57668
+!E5 U+05D5 afii57669
+!E6 U+05D6 afii57670
+!E7 U+05D7 afii57671
+!E8 U+05D8 afii57672
+!E9 U+05D9 afii57673
+!EA U+05DA afii57674
+!EB U+05DB afii57675
+!EC U+05DC afii57676
+!ED U+05DD afii57677
+!EE U+05DE afii57678
+!EF U+05DF afii57679
+!F0 U+05E0 afii57680
+!F1 U+05E1 afii57681
+!F2 U+05E2 afii57682
+!F3 U+05E3 afii57683
+!F4 U+05E4 afii57684
+!F5 U+05E5 afii57685
+!F6 U+05E6 afii57686
+!F7 U+05E7 afii57687
+!F8 U+05E8 afii57688
+!F9 U+05E9 afii57689
+!FA U+05EA afii57690
+!FD U+200E afii299
+!FE U+200F afii300
diff --git a/src/lib/fpdf/makefont/cp1257.map b/src/lib/fpdf/makefont/cp1257.map
new file mode 100644
index 0000000..2f2ecfa
--- /dev/null
+++ b/src/lib/fpdf/makefont/cp1257.map
@@ -0,0 +1,244 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+20AC Euro
+!82 U+201A quotesinglbase
+!84 U+201E quotedblbase
+!85 U+2026 ellipsis
+!86 U+2020 dagger
+!87 U+2021 daggerdbl
+!89 U+2030 perthousand
+!8B U+2039 guilsinglleft
+!8D U+00A8 dieresis
+!8E U+02C7 caron
+!8F U+00B8 cedilla
+!91 U+2018 quoteleft
+!92 U+2019 quoteright
+!93 U+201C quotedblleft
+!94 U+201D quotedblright
+!95 U+2022 bullet
+!96 U+2013 endash
+!97 U+2014 emdash
+!99 U+2122 trademark
+!9B U+203A guilsinglright
+!9D U+00AF macron
+!9E U+02DB ogonek
+!A0 U+00A0 space
+!A2 U+00A2 cent
+!A3 U+00A3 sterling
+!A4 U+00A4 currency
+!A6 U+00A6 brokenbar
+!A7 U+00A7 section
+!A8 U+00D8 Oslash
+!A9 U+00A9 copyright
+!AA U+0156 Rcommaaccent
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD hyphen
+!AE U+00AE registered
+!AF U+00C6 AE
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+00B2 twosuperior
+!B3 U+00B3 threesuperior
+!B4 U+00B4 acute
+!B5 U+00B5 mu
+!B6 U+00B6 paragraph
+!B7 U+00B7 periodcentered
+!B8 U+00F8 oslash
+!B9 U+00B9 onesuperior
+!BA U+0157 rcommaaccent
+!BB U+00BB guillemotright
+!BC U+00BC onequarter
+!BD U+00BD onehalf
+!BE U+00BE threequarters
+!BF U+00E6 ae
+!C0 U+0104 Aogonek
+!C1 U+012E Iogonek
+!C2 U+0100 Amacron
+!C3 U+0106 Cacute
+!C4 U+00C4 Adieresis
+!C5 U+00C5 Aring
+!C6 U+0118 Eogonek
+!C7 U+0112 Emacron
+!C8 U+010C Ccaron
+!C9 U+00C9 Eacute
+!CA U+0179 Zacute
+!CB U+0116 Edotaccent
+!CC U+0122 Gcommaaccent
+!CD U+0136 Kcommaaccent
+!CE U+012A Imacron
+!CF U+013B Lcommaaccent
+!D0 U+0160 Scaron
+!D1 U+0143 Nacute
+!D2 U+0145 Ncommaaccent
+!D3 U+00D3 Oacute
+!D4 U+014C Omacron
+!D5 U+00D5 Otilde
+!D6 U+00D6 Odieresis
+!D7 U+00D7 multiply
+!D8 U+0172 Uogonek
+!D9 U+0141 Lslash
+!DA U+015A Sacute
+!DB U+016A Umacron
+!DC U+00DC Udieresis
+!DD U+017B Zdotaccent
+!DE U+017D Zcaron
+!DF U+00DF germandbls
+!E0 U+0105 aogonek
+!E1 U+012F iogonek
+!E2 U+0101 amacron
+!E3 U+0107 cacute
+!E4 U+00E4 adieresis
+!E5 U+00E5 aring
+!E6 U+0119 eogonek
+!E7 U+0113 emacron
+!E8 U+010D ccaron
+!E9 U+00E9 eacute
+!EA U+017A zacute
+!EB U+0117 edotaccent
+!EC U+0123 gcommaaccent
+!ED U+0137 kcommaaccent
+!EE U+012B imacron
+!EF U+013C lcommaaccent
+!F0 U+0161 scaron
+!F1 U+0144 nacute
+!F2 U+0146 ncommaaccent
+!F3 U+00F3 oacute
+!F4 U+014D omacron
+!F5 U+00F5 otilde
+!F6 U+00F6 odieresis
+!F7 U+00F7 divide
+!F8 U+0173 uogonek
+!F9 U+0142 lslash
+!FA U+015B sacute
+!FB U+016B umacron
+!FC U+00FC udieresis
+!FD U+017C zdotaccent
+!FE U+017E zcaron
+!FF U+02D9 dotaccent
diff --git a/src/lib/fpdf/makefont/cp1258.map b/src/lib/fpdf/makefont/cp1258.map
new file mode 100644
index 0000000..fed915f
--- /dev/null
+++ b/src/lib/fpdf/makefont/cp1258.map
@@ -0,0 +1,247 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+20AC Euro
+!82 U+201A quotesinglbase
+!83 U+0192 florin
+!84 U+201E quotedblbase
+!85 U+2026 ellipsis
+!86 U+2020 dagger
+!87 U+2021 daggerdbl
+!88 U+02C6 circumflex
+!89 U+2030 perthousand
+!8B U+2039 guilsinglleft
+!8C U+0152 OE
+!91 U+2018 quoteleft
+!92 U+2019 quoteright
+!93 U+201C quotedblleft
+!94 U+201D quotedblright
+!95 U+2022 bullet
+!96 U+2013 endash
+!97 U+2014 emdash
+!98 U+02DC tilde
+!99 U+2122 trademark
+!9B U+203A guilsinglright
+!9C U+0153 oe
+!9F U+0178 Ydieresis
+!A0 U+00A0 space
+!A1 U+00A1 exclamdown
+!A2 U+00A2 cent
+!A3 U+00A3 sterling
+!A4 U+00A4 currency
+!A5 U+00A5 yen
+!A6 U+00A6 brokenbar
+!A7 U+00A7 section
+!A8 U+00A8 dieresis
+!A9 U+00A9 copyright
+!AA U+00AA ordfeminine
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD hyphen
+!AE U+00AE registered
+!AF U+00AF macron
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+00B2 twosuperior
+!B3 U+00B3 threesuperior
+!B4 U+00B4 acute
+!B5 U+00B5 mu
+!B6 U+00B6 paragraph
+!B7 U+00B7 periodcentered
+!B8 U+00B8 cedilla
+!B9 U+00B9 onesuperior
+!BA U+00BA ordmasculine
+!BB U+00BB guillemotright
+!BC U+00BC onequarter
+!BD U+00BD onehalf
+!BE U+00BE threequarters
+!BF U+00BF questiondown
+!C0 U+00C0 Agrave
+!C1 U+00C1 Aacute
+!C2 U+00C2 Acircumflex
+!C3 U+0102 Abreve
+!C4 U+00C4 Adieresis
+!C5 U+00C5 Aring
+!C6 U+00C6 AE
+!C7 U+00C7 Ccedilla
+!C8 U+00C8 Egrave
+!C9 U+00C9 Eacute
+!CA U+00CA Ecircumflex
+!CB U+00CB Edieresis
+!CC U+0300 gravecomb
+!CD U+00CD Iacute
+!CE U+00CE Icircumflex
+!CF U+00CF Idieresis
+!D0 U+0110 Dcroat
+!D1 U+00D1 Ntilde
+!D2 U+0309 hookabovecomb
+!D3 U+00D3 Oacute
+!D4 U+00D4 Ocircumflex
+!D5 U+01A0 Ohorn
+!D6 U+00D6 Odieresis
+!D7 U+00D7 multiply
+!D8 U+00D8 Oslash
+!D9 U+00D9 Ugrave
+!DA U+00DA Uacute
+!DB U+00DB Ucircumflex
+!DC U+00DC Udieresis
+!DD U+01AF Uhorn
+!DE U+0303 tildecomb
+!DF U+00DF germandbls
+!E0 U+00E0 agrave
+!E1 U+00E1 aacute
+!E2 U+00E2 acircumflex
+!E3 U+0103 abreve
+!E4 U+00E4 adieresis
+!E5 U+00E5 aring
+!E6 U+00E6 ae
+!E7 U+00E7 ccedilla
+!E8 U+00E8 egrave
+!E9 U+00E9 eacute
+!EA U+00EA ecircumflex
+!EB U+00EB edieresis
+!EC U+0301 acutecomb
+!ED U+00ED iacute
+!EE U+00EE icircumflex
+!EF U+00EF idieresis
+!F0 U+0111 dcroat
+!F1 U+00F1 ntilde
+!F2 U+0323 dotbelowcomb
+!F3 U+00F3 oacute
+!F4 U+00F4 ocircumflex
+!F5 U+01A1 ohorn
+!F6 U+00F6 odieresis
+!F7 U+00F7 divide
+!F8 U+00F8 oslash
+!F9 U+00F9 ugrave
+!FA U+00FA uacute
+!FB U+00FB ucircumflex
+!FC U+00FC udieresis
+!FD U+01B0 uhorn
+!FE U+20AB dong
+!FF U+00FF ydieresis
diff --git a/src/lib/fpdf/makefont/cp874.map b/src/lib/fpdf/makefont/cp874.map
new file mode 100644
index 0000000..1006e6b
--- /dev/null
+++ b/src/lib/fpdf/makefont/cp874.map
@@ -0,0 +1,225 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+20AC Euro
+!85 U+2026 ellipsis
+!91 U+2018 quoteleft
+!92 U+2019 quoteright
+!93 U+201C quotedblleft
+!94 U+201D quotedblright
+!95 U+2022 bullet
+!96 U+2013 endash
+!97 U+2014 emdash
+!A0 U+00A0 space
+!A1 U+0E01 kokaithai
+!A2 U+0E02 khokhaithai
+!A3 U+0E03 khokhuatthai
+!A4 U+0E04 khokhwaithai
+!A5 U+0E05 khokhonthai
+!A6 U+0E06 khorakhangthai
+!A7 U+0E07 ngonguthai
+!A8 U+0E08 chochanthai
+!A9 U+0E09 chochingthai
+!AA U+0E0A chochangthai
+!AB U+0E0B sosothai
+!AC U+0E0C chochoethai
+!AD U+0E0D yoyingthai
+!AE U+0E0E dochadathai
+!AF U+0E0F topatakthai
+!B0 U+0E10 thothanthai
+!B1 U+0E11 thonangmonthothai
+!B2 U+0E12 thophuthaothai
+!B3 U+0E13 nonenthai
+!B4 U+0E14 dodekthai
+!B5 U+0E15 totaothai
+!B6 U+0E16 thothungthai
+!B7 U+0E17 thothahanthai
+!B8 U+0E18 thothongthai
+!B9 U+0E19 nonuthai
+!BA U+0E1A bobaimaithai
+!BB U+0E1B poplathai
+!BC U+0E1C phophungthai
+!BD U+0E1D fofathai
+!BE U+0E1E phophanthai
+!BF U+0E1F fofanthai
+!C0 U+0E20 phosamphaothai
+!C1 U+0E21 momathai
+!C2 U+0E22 yoyakthai
+!C3 U+0E23 roruathai
+!C4 U+0E24 ruthai
+!C5 U+0E25 lolingthai
+!C6 U+0E26 luthai
+!C7 U+0E27 wowaenthai
+!C8 U+0E28 sosalathai
+!C9 U+0E29 sorusithai
+!CA U+0E2A sosuathai
+!CB U+0E2B hohipthai
+!CC U+0E2C lochulathai
+!CD U+0E2D oangthai
+!CE U+0E2E honokhukthai
+!CF U+0E2F paiyannoithai
+!D0 U+0E30 saraathai
+!D1 U+0E31 maihanakatthai
+!D2 U+0E32 saraaathai
+!D3 U+0E33 saraamthai
+!D4 U+0E34 saraithai
+!D5 U+0E35 saraiithai
+!D6 U+0E36 sarauethai
+!D7 U+0E37 saraueethai
+!D8 U+0E38 sarauthai
+!D9 U+0E39 sarauuthai
+!DA U+0E3A phinthuthai
+!DF U+0E3F bahtthai
+!E0 U+0E40 saraethai
+!E1 U+0E41 saraaethai
+!E2 U+0E42 saraothai
+!E3 U+0E43 saraaimaimuanthai
+!E4 U+0E44 saraaimaimalaithai
+!E5 U+0E45 lakkhangyaothai
+!E6 U+0E46 maiyamokthai
+!E7 U+0E47 maitaikhuthai
+!E8 U+0E48 maiekthai
+!E9 U+0E49 maithothai
+!EA U+0E4A maitrithai
+!EB U+0E4B maichattawathai
+!EC U+0E4C thanthakhatthai
+!ED U+0E4D nikhahitthai
+!EE U+0E4E yamakkanthai
+!EF U+0E4F fongmanthai
+!F0 U+0E50 zerothai
+!F1 U+0E51 onethai
+!F2 U+0E52 twothai
+!F3 U+0E53 threethai
+!F4 U+0E54 fourthai
+!F5 U+0E55 fivethai
+!F6 U+0E56 sixthai
+!F7 U+0E57 seventhai
+!F8 U+0E58 eightthai
+!F9 U+0E59 ninethai
+!FA U+0E5A angkhankhuthai
+!FB U+0E5B khomutthai
diff --git a/src/lib/fpdf/makefont/iso-8859-1.map b/src/lib/fpdf/makefont/iso-8859-1.map
new file mode 100644
index 0000000..61740a3
--- /dev/null
+++ b/src/lib/fpdf/makefont/iso-8859-1.map
@@ -0,0 +1,256 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+0080 .notdef
+!81 U+0081 .notdef
+!82 U+0082 .notdef
+!83 U+0083 .notdef
+!84 U+0084 .notdef
+!85 U+0085 .notdef
+!86 U+0086 .notdef
+!87 U+0087 .notdef
+!88 U+0088 .notdef
+!89 U+0089 .notdef
+!8A U+008A .notdef
+!8B U+008B .notdef
+!8C U+008C .notdef
+!8D U+008D .notdef
+!8E U+008E .notdef
+!8F U+008F .notdef
+!90 U+0090 .notdef
+!91 U+0091 .notdef
+!92 U+0092 .notdef
+!93 U+0093 .notdef
+!94 U+0094 .notdef
+!95 U+0095 .notdef
+!96 U+0096 .notdef
+!97 U+0097 .notdef
+!98 U+0098 .notdef
+!99 U+0099 .notdef
+!9A U+009A .notdef
+!9B U+009B .notdef
+!9C U+009C .notdef
+!9D U+009D .notdef
+!9E U+009E .notdef
+!9F U+009F .notdef
+!A0 U+00A0 space
+!A1 U+00A1 exclamdown
+!A2 U+00A2 cent
+!A3 U+00A3 sterling
+!A4 U+00A4 currency
+!A5 U+00A5 yen
+!A6 U+00A6 brokenbar
+!A7 U+00A7 section
+!A8 U+00A8 dieresis
+!A9 U+00A9 copyright
+!AA U+00AA ordfeminine
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD hyphen
+!AE U+00AE registered
+!AF U+00AF macron
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+00B2 twosuperior
+!B3 U+00B3 threesuperior
+!B4 U+00B4 acute
+!B5 U+00B5 mu
+!B6 U+00B6 paragraph
+!B7 U+00B7 periodcentered
+!B8 U+00B8 cedilla
+!B9 U+00B9 onesuperior
+!BA U+00BA ordmasculine
+!BB U+00BB guillemotright
+!BC U+00BC onequarter
+!BD U+00BD onehalf
+!BE U+00BE threequarters
+!BF U+00BF questiondown
+!C0 U+00C0 Agrave
+!C1 U+00C1 Aacute
+!C2 U+00C2 Acircumflex
+!C3 U+00C3 Atilde
+!C4 U+00C4 Adieresis
+!C5 U+00C5 Aring
+!C6 U+00C6 AE
+!C7 U+00C7 Ccedilla
+!C8 U+00C8 Egrave
+!C9 U+00C9 Eacute
+!CA U+00CA Ecircumflex
+!CB U+00CB Edieresis
+!CC U+00CC Igrave
+!CD U+00CD Iacute
+!CE U+00CE Icircumflex
+!CF U+00CF Idieresis
+!D0 U+00D0 Eth
+!D1 U+00D1 Ntilde
+!D2 U+00D2 Ograve
+!D3 U+00D3 Oacute
+!D4 U+00D4 Ocircumflex
+!D5 U+00D5 Otilde
+!D6 U+00D6 Odieresis
+!D7 U+00D7 multiply
+!D8 U+00D8 Oslash
+!D9 U+00D9 Ugrave
+!DA U+00DA Uacute
+!DB U+00DB Ucircumflex
+!DC U+00DC Udieresis
+!DD U+00DD Yacute
+!DE U+00DE Thorn
+!DF U+00DF germandbls
+!E0 U+00E0 agrave
+!E1 U+00E1 aacute
+!E2 U+00E2 acircumflex
+!E3 U+00E3 atilde
+!E4 U+00E4 adieresis
+!E5 U+00E5 aring
+!E6 U+00E6 ae
+!E7 U+00E7 ccedilla
+!E8 U+00E8 egrave
+!E9 U+00E9 eacute
+!EA U+00EA ecircumflex
+!EB U+00EB edieresis
+!EC U+00EC igrave
+!ED U+00ED iacute
+!EE U+00EE icircumflex
+!EF U+00EF idieresis
+!F0 U+00F0 eth
+!F1 U+00F1 ntilde
+!F2 U+00F2 ograve
+!F3 U+00F3 oacute
+!F4 U+00F4 ocircumflex
+!F5 U+00F5 otilde
+!F6 U+00F6 odieresis
+!F7 U+00F7 divide
+!F8 U+00F8 oslash
+!F9 U+00F9 ugrave
+!FA U+00FA uacute
+!FB U+00FB ucircumflex
+!FC U+00FC udieresis
+!FD U+00FD yacute
+!FE U+00FE thorn
+!FF U+00FF ydieresis
diff --git a/src/lib/fpdf/makefont/iso-8859-11.map b/src/lib/fpdf/makefont/iso-8859-11.map
new file mode 100644
index 0000000..9168812
--- /dev/null
+++ b/src/lib/fpdf/makefont/iso-8859-11.map
@@ -0,0 +1,248 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+0080 .notdef
+!81 U+0081 .notdef
+!82 U+0082 .notdef
+!83 U+0083 .notdef
+!84 U+0084 .notdef
+!85 U+0085 .notdef
+!86 U+0086 .notdef
+!87 U+0087 .notdef
+!88 U+0088 .notdef
+!89 U+0089 .notdef
+!8A U+008A .notdef
+!8B U+008B .notdef
+!8C U+008C .notdef
+!8D U+008D .notdef
+!8E U+008E .notdef
+!8F U+008F .notdef
+!90 U+0090 .notdef
+!91 U+0091 .notdef
+!92 U+0092 .notdef
+!93 U+0093 .notdef
+!94 U+0094 .notdef
+!95 U+0095 .notdef
+!96 U+0096 .notdef
+!97 U+0097 .notdef
+!98 U+0098 .notdef
+!99 U+0099 .notdef
+!9A U+009A .notdef
+!9B U+009B .notdef
+!9C U+009C .notdef
+!9D U+009D .notdef
+!9E U+009E .notdef
+!9F U+009F .notdef
+!A0 U+00A0 space
+!A1 U+0E01 kokaithai
+!A2 U+0E02 khokhaithai
+!A3 U+0E03 khokhuatthai
+!A4 U+0E04 khokhwaithai
+!A5 U+0E05 khokhonthai
+!A6 U+0E06 khorakhangthai
+!A7 U+0E07 ngonguthai
+!A8 U+0E08 chochanthai
+!A9 U+0E09 chochingthai
+!AA U+0E0A chochangthai
+!AB U+0E0B sosothai
+!AC U+0E0C chochoethai
+!AD U+0E0D yoyingthai
+!AE U+0E0E dochadathai
+!AF U+0E0F topatakthai
+!B0 U+0E10 thothanthai
+!B1 U+0E11 thonangmonthothai
+!B2 U+0E12 thophuthaothai
+!B3 U+0E13 nonenthai
+!B4 U+0E14 dodekthai
+!B5 U+0E15 totaothai
+!B6 U+0E16 thothungthai
+!B7 U+0E17 thothahanthai
+!B8 U+0E18 thothongthai
+!B9 U+0E19 nonuthai
+!BA U+0E1A bobaimaithai
+!BB U+0E1B poplathai
+!BC U+0E1C phophungthai
+!BD U+0E1D fofathai
+!BE U+0E1E phophanthai
+!BF U+0E1F fofanthai
+!C0 U+0E20 phosamphaothai
+!C1 U+0E21 momathai
+!C2 U+0E22 yoyakthai
+!C3 U+0E23 roruathai
+!C4 U+0E24 ruthai
+!C5 U+0E25 lolingthai
+!C6 U+0E26 luthai
+!C7 U+0E27 wowaenthai
+!C8 U+0E28 sosalathai
+!C9 U+0E29 sorusithai
+!CA U+0E2A sosuathai
+!CB U+0E2B hohipthai
+!CC U+0E2C lochulathai
+!CD U+0E2D oangthai
+!CE U+0E2E honokhukthai
+!CF U+0E2F paiyannoithai
+!D0 U+0E30 saraathai
+!D1 U+0E31 maihanakatthai
+!D2 U+0E32 saraaathai
+!D3 U+0E33 saraamthai
+!D4 U+0E34 saraithai
+!D5 U+0E35 saraiithai
+!D6 U+0E36 sarauethai
+!D7 U+0E37 saraueethai
+!D8 U+0E38 sarauthai
+!D9 U+0E39 sarauuthai
+!DA U+0E3A phinthuthai
+!DF U+0E3F bahtthai
+!E0 U+0E40 saraethai
+!E1 U+0E41 saraaethai
+!E2 U+0E42 saraothai
+!E3 U+0E43 saraaimaimuanthai
+!E4 U+0E44 saraaimaimalaithai
+!E5 U+0E45 lakkhangyaothai
+!E6 U+0E46 maiyamokthai
+!E7 U+0E47 maitaikhuthai
+!E8 U+0E48 maiekthai
+!E9 U+0E49 maithothai
+!EA U+0E4A maitrithai
+!EB U+0E4B maichattawathai
+!EC U+0E4C thanthakhatthai
+!ED U+0E4D nikhahitthai
+!EE U+0E4E yamakkanthai
+!EF U+0E4F fongmanthai
+!F0 U+0E50 zerothai
+!F1 U+0E51 onethai
+!F2 U+0E52 twothai
+!F3 U+0E53 threethai
+!F4 U+0E54 fourthai
+!F5 U+0E55 fivethai
+!F6 U+0E56 sixthai
+!F7 U+0E57 seventhai
+!F8 U+0E58 eightthai
+!F9 U+0E59 ninethai
+!FA U+0E5A angkhankhuthai
+!FB U+0E5B khomutthai
diff --git a/src/lib/fpdf/makefont/iso-8859-15.map b/src/lib/fpdf/makefont/iso-8859-15.map
new file mode 100644
index 0000000..6c2b571
--- /dev/null
+++ b/src/lib/fpdf/makefont/iso-8859-15.map
@@ -0,0 +1,256 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+0080 .notdef
+!81 U+0081 .notdef
+!82 U+0082 .notdef
+!83 U+0083 .notdef
+!84 U+0084 .notdef
+!85 U+0085 .notdef
+!86 U+0086 .notdef
+!87 U+0087 .notdef
+!88 U+0088 .notdef
+!89 U+0089 .notdef
+!8A U+008A .notdef
+!8B U+008B .notdef
+!8C U+008C .notdef
+!8D U+008D .notdef
+!8E U+008E .notdef
+!8F U+008F .notdef
+!90 U+0090 .notdef
+!91 U+0091 .notdef
+!92 U+0092 .notdef
+!93 U+0093 .notdef
+!94 U+0094 .notdef
+!95 U+0095 .notdef
+!96 U+0096 .notdef
+!97 U+0097 .notdef
+!98 U+0098 .notdef
+!99 U+0099 .notdef
+!9A U+009A .notdef
+!9B U+009B .notdef
+!9C U+009C .notdef
+!9D U+009D .notdef
+!9E U+009E .notdef
+!9F U+009F .notdef
+!A0 U+00A0 space
+!A1 U+00A1 exclamdown
+!A2 U+00A2 cent
+!A3 U+00A3 sterling
+!A4 U+20AC Euro
+!A5 U+00A5 yen
+!A6 U+0160 Scaron
+!A7 U+00A7 section
+!A8 U+0161 scaron
+!A9 U+00A9 copyright
+!AA U+00AA ordfeminine
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD hyphen
+!AE U+00AE registered
+!AF U+00AF macron
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+00B2 twosuperior
+!B3 U+00B3 threesuperior
+!B4 U+017D Zcaron
+!B5 U+00B5 mu
+!B6 U+00B6 paragraph
+!B7 U+00B7 periodcentered
+!B8 U+017E zcaron
+!B9 U+00B9 onesuperior
+!BA U+00BA ordmasculine
+!BB U+00BB guillemotright
+!BC U+0152 OE
+!BD U+0153 oe
+!BE U+0178 Ydieresis
+!BF U+00BF questiondown
+!C0 U+00C0 Agrave
+!C1 U+00C1 Aacute
+!C2 U+00C2 Acircumflex
+!C3 U+00C3 Atilde
+!C4 U+00C4 Adieresis
+!C5 U+00C5 Aring
+!C6 U+00C6 AE
+!C7 U+00C7 Ccedilla
+!C8 U+00C8 Egrave
+!C9 U+00C9 Eacute
+!CA U+00CA Ecircumflex
+!CB U+00CB Edieresis
+!CC U+00CC Igrave
+!CD U+00CD Iacute
+!CE U+00CE Icircumflex
+!CF U+00CF Idieresis
+!D0 U+00D0 Eth
+!D1 U+00D1 Ntilde
+!D2 U+00D2 Ograve
+!D3 U+00D3 Oacute
+!D4 U+00D4 Ocircumflex
+!D5 U+00D5 Otilde
+!D6 U+00D6 Odieresis
+!D7 U+00D7 multiply
+!D8 U+00D8 Oslash
+!D9 U+00D9 Ugrave
+!DA U+00DA Uacute
+!DB U+00DB Ucircumflex
+!DC U+00DC Udieresis
+!DD U+00DD Yacute
+!DE U+00DE Thorn
+!DF U+00DF germandbls
+!E0 U+00E0 agrave
+!E1 U+00E1 aacute
+!E2 U+00E2 acircumflex
+!E3 U+00E3 atilde
+!E4 U+00E4 adieresis
+!E5 U+00E5 aring
+!E6 U+00E6 ae
+!E7 U+00E7 ccedilla
+!E8 U+00E8 egrave
+!E9 U+00E9 eacute
+!EA U+00EA ecircumflex
+!EB U+00EB edieresis
+!EC U+00EC igrave
+!ED U+00ED iacute
+!EE U+00EE icircumflex
+!EF U+00EF idieresis
+!F0 U+00F0 eth
+!F1 U+00F1 ntilde
+!F2 U+00F2 ograve
+!F3 U+00F3 oacute
+!F4 U+00F4 ocircumflex
+!F5 U+00F5 otilde
+!F6 U+00F6 odieresis
+!F7 U+00F7 divide
+!F8 U+00F8 oslash
+!F9 U+00F9 ugrave
+!FA U+00FA uacute
+!FB U+00FB ucircumflex
+!FC U+00FC udieresis
+!FD U+00FD yacute
+!FE U+00FE thorn
+!FF U+00FF ydieresis
diff --git a/src/lib/fpdf/makefont/iso-8859-16.map b/src/lib/fpdf/makefont/iso-8859-16.map
new file mode 100644
index 0000000..202c8fe
--- /dev/null
+++ b/src/lib/fpdf/makefont/iso-8859-16.map
@@ -0,0 +1,256 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+0080 .notdef
+!81 U+0081 .notdef
+!82 U+0082 .notdef
+!83 U+0083 .notdef
+!84 U+0084 .notdef
+!85 U+0085 .notdef
+!86 U+0086 .notdef
+!87 U+0087 .notdef
+!88 U+0088 .notdef
+!89 U+0089 .notdef
+!8A U+008A .notdef
+!8B U+008B .notdef
+!8C U+008C .notdef
+!8D U+008D .notdef
+!8E U+008E .notdef
+!8F U+008F .notdef
+!90 U+0090 .notdef
+!91 U+0091 .notdef
+!92 U+0092 .notdef
+!93 U+0093 .notdef
+!94 U+0094 .notdef
+!95 U+0095 .notdef
+!96 U+0096 .notdef
+!97 U+0097 .notdef
+!98 U+0098 .notdef
+!99 U+0099 .notdef
+!9A U+009A .notdef
+!9B U+009B .notdef
+!9C U+009C .notdef
+!9D U+009D .notdef
+!9E U+009E .notdef
+!9F U+009F .notdef
+!A0 U+00A0 space
+!A1 U+0104 Aogonek
+!A2 U+0105 aogonek
+!A3 U+0141 Lslash
+!A4 U+20AC Euro
+!A5 U+201E quotedblbase
+!A6 U+0160 Scaron
+!A7 U+00A7 section
+!A8 U+0161 scaron
+!A9 U+00A9 copyright
+!AA U+0218 Scommaaccent
+!AB U+00AB guillemotleft
+!AC U+0179 Zacute
+!AD U+00AD hyphen
+!AE U+017A zacute
+!AF U+017B Zdotaccent
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+010C Ccaron
+!B3 U+0142 lslash
+!B4 U+017D Zcaron
+!B5 U+201D quotedblright
+!B6 U+00B6 paragraph
+!B7 U+00B7 periodcentered
+!B8 U+017E zcaron
+!B9 U+010D ccaron
+!BA U+0219 scommaaccent
+!BB U+00BB guillemotright
+!BC U+0152 OE
+!BD U+0153 oe
+!BE U+0178 Ydieresis
+!BF U+017C zdotaccent
+!C0 U+00C0 Agrave
+!C1 U+00C1 Aacute
+!C2 U+00C2 Acircumflex
+!C3 U+0102 Abreve
+!C4 U+00C4 Adieresis
+!C5 U+0106 Cacute
+!C6 U+00C6 AE
+!C7 U+00C7 Ccedilla
+!C8 U+00C8 Egrave
+!C9 U+00C9 Eacute
+!CA U+00CA Ecircumflex
+!CB U+00CB Edieresis
+!CC U+00CC Igrave
+!CD U+00CD Iacute
+!CE U+00CE Icircumflex
+!CF U+00CF Idieresis
+!D0 U+0110 Dcroat
+!D1 U+0143 Nacute
+!D2 U+00D2 Ograve
+!D3 U+00D3 Oacute
+!D4 U+00D4 Ocircumflex
+!D5 U+0150 Ohungarumlaut
+!D6 U+00D6 Odieresis
+!D7 U+015A Sacute
+!D8 U+0170 Uhungarumlaut
+!D9 U+00D9 Ugrave
+!DA U+00DA Uacute
+!DB U+00DB Ucircumflex
+!DC U+00DC Udieresis
+!DD U+0118 Eogonek
+!DE U+021A Tcommaaccent
+!DF U+00DF germandbls
+!E0 U+00E0 agrave
+!E1 U+00E1 aacute
+!E2 U+00E2 acircumflex
+!E3 U+0103 abreve
+!E4 U+00E4 adieresis
+!E5 U+0107 cacute
+!E6 U+00E6 ae
+!E7 U+00E7 ccedilla
+!E8 U+00E8 egrave
+!E9 U+00E9 eacute
+!EA U+00EA ecircumflex
+!EB U+00EB edieresis
+!EC U+00EC igrave
+!ED U+00ED iacute
+!EE U+00EE icircumflex
+!EF U+00EF idieresis
+!F0 U+0111 dcroat
+!F1 U+0144 nacute
+!F2 U+00F2 ograve
+!F3 U+00F3 oacute
+!F4 U+00F4 ocircumflex
+!F5 U+0151 ohungarumlaut
+!F6 U+00F6 odieresis
+!F7 U+015B sacute
+!F8 U+0171 uhungarumlaut
+!F9 U+00F9 ugrave
+!FA U+00FA uacute
+!FB U+00FB ucircumflex
+!FC U+00FC udieresis
+!FD U+0119 eogonek
+!FE U+021B tcommaaccent
+!FF U+00FF ydieresis
diff --git a/src/lib/fpdf/makefont/iso-8859-2.map b/src/lib/fpdf/makefont/iso-8859-2.map
new file mode 100644
index 0000000..65ae09f
--- /dev/null
+++ b/src/lib/fpdf/makefont/iso-8859-2.map
@@ -0,0 +1,256 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+0080 .notdef
+!81 U+0081 .notdef
+!82 U+0082 .notdef
+!83 U+0083 .notdef
+!84 U+0084 .notdef
+!85 U+0085 .notdef
+!86 U+0086 .notdef
+!87 U+0087 .notdef
+!88 U+0088 .notdef
+!89 U+0089 .notdef
+!8A U+008A .notdef
+!8B U+008B .notdef
+!8C U+008C .notdef
+!8D U+008D .notdef
+!8E U+008E .notdef
+!8F U+008F .notdef
+!90 U+0090 .notdef
+!91 U+0091 .notdef
+!92 U+0092 .notdef
+!93 U+0093 .notdef
+!94 U+0094 .notdef
+!95 U+0095 .notdef
+!96 U+0096 .notdef
+!97 U+0097 .notdef
+!98 U+0098 .notdef
+!99 U+0099 .notdef
+!9A U+009A .notdef
+!9B U+009B .notdef
+!9C U+009C .notdef
+!9D U+009D .notdef
+!9E U+009E .notdef
+!9F U+009F .notdef
+!A0 U+00A0 space
+!A1 U+0104 Aogonek
+!A2 U+02D8 breve
+!A3 U+0141 Lslash
+!A4 U+00A4 currency
+!A5 U+013D Lcaron
+!A6 U+015A Sacute
+!A7 U+00A7 section
+!A8 U+00A8 dieresis
+!A9 U+0160 Scaron
+!AA U+015E Scedilla
+!AB U+0164 Tcaron
+!AC U+0179 Zacute
+!AD U+00AD hyphen
+!AE U+017D Zcaron
+!AF U+017B Zdotaccent
+!B0 U+00B0 degree
+!B1 U+0105 aogonek
+!B2 U+02DB ogonek
+!B3 U+0142 lslash
+!B4 U+00B4 acute
+!B5 U+013E lcaron
+!B6 U+015B sacute
+!B7 U+02C7 caron
+!B8 U+00B8 cedilla
+!B9 U+0161 scaron
+!BA U+015F scedilla
+!BB U+0165 tcaron
+!BC U+017A zacute
+!BD U+02DD hungarumlaut
+!BE U+017E zcaron
+!BF U+017C zdotaccent
+!C0 U+0154 Racute
+!C1 U+00C1 Aacute
+!C2 U+00C2 Acircumflex
+!C3 U+0102 Abreve
+!C4 U+00C4 Adieresis
+!C5 U+0139 Lacute
+!C6 U+0106 Cacute
+!C7 U+00C7 Ccedilla
+!C8 U+010C Ccaron
+!C9 U+00C9 Eacute
+!CA U+0118 Eogonek
+!CB U+00CB Edieresis
+!CC U+011A Ecaron
+!CD U+00CD Iacute
+!CE U+00CE Icircumflex
+!CF U+010E Dcaron
+!D0 U+0110 Dcroat
+!D1 U+0143 Nacute
+!D2 U+0147 Ncaron
+!D3 U+00D3 Oacute
+!D4 U+00D4 Ocircumflex
+!D5 U+0150 Ohungarumlaut
+!D6 U+00D6 Odieresis
+!D7 U+00D7 multiply
+!D8 U+0158 Rcaron
+!D9 U+016E Uring
+!DA U+00DA Uacute
+!DB U+0170 Uhungarumlaut
+!DC U+00DC Udieresis
+!DD U+00DD Yacute
+!DE U+0162 Tcommaaccent
+!DF U+00DF germandbls
+!E0 U+0155 racute
+!E1 U+00E1 aacute
+!E2 U+00E2 acircumflex
+!E3 U+0103 abreve
+!E4 U+00E4 adieresis
+!E5 U+013A lacute
+!E6 U+0107 cacute
+!E7 U+00E7 ccedilla
+!E8 U+010D ccaron
+!E9 U+00E9 eacute
+!EA U+0119 eogonek
+!EB U+00EB edieresis
+!EC U+011B ecaron
+!ED U+00ED iacute
+!EE U+00EE icircumflex
+!EF U+010F dcaron
+!F0 U+0111 dcroat
+!F1 U+0144 nacute
+!F2 U+0148 ncaron
+!F3 U+00F3 oacute
+!F4 U+00F4 ocircumflex
+!F5 U+0151 ohungarumlaut
+!F6 U+00F6 odieresis
+!F7 U+00F7 divide
+!F8 U+0159 rcaron
+!F9 U+016F uring
+!FA U+00FA uacute
+!FB U+0171 uhungarumlaut
+!FC U+00FC udieresis
+!FD U+00FD yacute
+!FE U+0163 tcommaaccent
+!FF U+02D9 dotaccent
diff --git a/src/lib/fpdf/makefont/iso-8859-4.map b/src/lib/fpdf/makefont/iso-8859-4.map
new file mode 100644
index 0000000..a7d87bf
--- /dev/null
+++ b/src/lib/fpdf/makefont/iso-8859-4.map
@@ -0,0 +1,256 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+0080 .notdef
+!81 U+0081 .notdef
+!82 U+0082 .notdef
+!83 U+0083 .notdef
+!84 U+0084 .notdef
+!85 U+0085 .notdef
+!86 U+0086 .notdef
+!87 U+0087 .notdef
+!88 U+0088 .notdef
+!89 U+0089 .notdef
+!8A U+008A .notdef
+!8B U+008B .notdef
+!8C U+008C .notdef
+!8D U+008D .notdef
+!8E U+008E .notdef
+!8F U+008F .notdef
+!90 U+0090 .notdef
+!91 U+0091 .notdef
+!92 U+0092 .notdef
+!93 U+0093 .notdef
+!94 U+0094 .notdef
+!95 U+0095 .notdef
+!96 U+0096 .notdef
+!97 U+0097 .notdef
+!98 U+0098 .notdef
+!99 U+0099 .notdef
+!9A U+009A .notdef
+!9B U+009B .notdef
+!9C U+009C .notdef
+!9D U+009D .notdef
+!9E U+009E .notdef
+!9F U+009F .notdef
+!A0 U+00A0 space
+!A1 U+0104 Aogonek
+!A2 U+0138 kgreenlandic
+!A3 U+0156 Rcommaaccent
+!A4 U+00A4 currency
+!A5 U+0128 Itilde
+!A6 U+013B Lcommaaccent
+!A7 U+00A7 section
+!A8 U+00A8 dieresis
+!A9 U+0160 Scaron
+!AA U+0112 Emacron
+!AB U+0122 Gcommaaccent
+!AC U+0166 Tbar
+!AD U+00AD hyphen
+!AE U+017D Zcaron
+!AF U+00AF macron
+!B0 U+00B0 degree
+!B1 U+0105 aogonek
+!B2 U+02DB ogonek
+!B3 U+0157 rcommaaccent
+!B4 U+00B4 acute
+!B5 U+0129 itilde
+!B6 U+013C lcommaaccent
+!B7 U+02C7 caron
+!B8 U+00B8 cedilla
+!B9 U+0161 scaron
+!BA U+0113 emacron
+!BB U+0123 gcommaaccent
+!BC U+0167 tbar
+!BD U+014A Eng
+!BE U+017E zcaron
+!BF U+014B eng
+!C0 U+0100 Amacron
+!C1 U+00C1 Aacute
+!C2 U+00C2 Acircumflex
+!C3 U+00C3 Atilde
+!C4 U+00C4 Adieresis
+!C5 U+00C5 Aring
+!C6 U+00C6 AE
+!C7 U+012E Iogonek
+!C8 U+010C Ccaron
+!C9 U+00C9 Eacute
+!CA U+0118 Eogonek
+!CB U+00CB Edieresis
+!CC U+0116 Edotaccent
+!CD U+00CD Iacute
+!CE U+00CE Icircumflex
+!CF U+012A Imacron
+!D0 U+0110 Dcroat
+!D1 U+0145 Ncommaaccent
+!D2 U+014C Omacron
+!D3 U+0136 Kcommaaccent
+!D4 U+00D4 Ocircumflex
+!D5 U+00D5 Otilde
+!D6 U+00D6 Odieresis
+!D7 U+00D7 multiply
+!D8 U+00D8 Oslash
+!D9 U+0172 Uogonek
+!DA U+00DA Uacute
+!DB U+00DB Ucircumflex
+!DC U+00DC Udieresis
+!DD U+0168 Utilde
+!DE U+016A Umacron
+!DF U+00DF germandbls
+!E0 U+0101 amacron
+!E1 U+00E1 aacute
+!E2 U+00E2 acircumflex
+!E3 U+00E3 atilde
+!E4 U+00E4 adieresis
+!E5 U+00E5 aring
+!E6 U+00E6 ae
+!E7 U+012F iogonek
+!E8 U+010D ccaron
+!E9 U+00E9 eacute
+!EA U+0119 eogonek
+!EB U+00EB edieresis
+!EC U+0117 edotaccent
+!ED U+00ED iacute
+!EE U+00EE icircumflex
+!EF U+012B imacron
+!F0 U+0111 dcroat
+!F1 U+0146 ncommaaccent
+!F2 U+014D omacron
+!F3 U+0137 kcommaaccent
+!F4 U+00F4 ocircumflex
+!F5 U+00F5 otilde
+!F6 U+00F6 odieresis
+!F7 U+00F7 divide
+!F8 U+00F8 oslash
+!F9 U+0173 uogonek
+!FA U+00FA uacute
+!FB U+00FB ucircumflex
+!FC U+00FC udieresis
+!FD U+0169 utilde
+!FE U+016B umacron
+!FF U+02D9 dotaccent
diff --git a/src/lib/fpdf/makefont/iso-8859-5.map b/src/lib/fpdf/makefont/iso-8859-5.map
new file mode 100644
index 0000000..f9cd4ed
--- /dev/null
+++ b/src/lib/fpdf/makefont/iso-8859-5.map
@@ -0,0 +1,256 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+0080 .notdef
+!81 U+0081 .notdef
+!82 U+0082 .notdef
+!83 U+0083 .notdef
+!84 U+0084 .notdef
+!85 U+0085 .notdef
+!86 U+0086 .notdef
+!87 U+0087 .notdef
+!88 U+0088 .notdef
+!89 U+0089 .notdef
+!8A U+008A .notdef
+!8B U+008B .notdef
+!8C U+008C .notdef
+!8D U+008D .notdef
+!8E U+008E .notdef
+!8F U+008F .notdef
+!90 U+0090 .notdef
+!91 U+0091 .notdef
+!92 U+0092 .notdef
+!93 U+0093 .notdef
+!94 U+0094 .notdef
+!95 U+0095 .notdef
+!96 U+0096 .notdef
+!97 U+0097 .notdef
+!98 U+0098 .notdef
+!99 U+0099 .notdef
+!9A U+009A .notdef
+!9B U+009B .notdef
+!9C U+009C .notdef
+!9D U+009D .notdef
+!9E U+009E .notdef
+!9F U+009F .notdef
+!A0 U+00A0 space
+!A1 U+0401 afii10023
+!A2 U+0402 afii10051
+!A3 U+0403 afii10052
+!A4 U+0404 afii10053
+!A5 U+0405 afii10054
+!A6 U+0406 afii10055
+!A7 U+0407 afii10056
+!A8 U+0408 afii10057
+!A9 U+0409 afii10058
+!AA U+040A afii10059
+!AB U+040B afii10060
+!AC U+040C afii10061
+!AD U+00AD hyphen
+!AE U+040E afii10062
+!AF U+040F afii10145
+!B0 U+0410 afii10017
+!B1 U+0411 afii10018
+!B2 U+0412 afii10019
+!B3 U+0413 afii10020
+!B4 U+0414 afii10021
+!B5 U+0415 afii10022
+!B6 U+0416 afii10024
+!B7 U+0417 afii10025
+!B8 U+0418 afii10026
+!B9 U+0419 afii10027
+!BA U+041A afii10028
+!BB U+041B afii10029
+!BC U+041C afii10030
+!BD U+041D afii10031
+!BE U+041E afii10032
+!BF U+041F afii10033
+!C0 U+0420 afii10034
+!C1 U+0421 afii10035
+!C2 U+0422 afii10036
+!C3 U+0423 afii10037
+!C4 U+0424 afii10038
+!C5 U+0425 afii10039
+!C6 U+0426 afii10040
+!C7 U+0427 afii10041
+!C8 U+0428 afii10042
+!C9 U+0429 afii10043
+!CA U+042A afii10044
+!CB U+042B afii10045
+!CC U+042C afii10046
+!CD U+042D afii10047
+!CE U+042E afii10048
+!CF U+042F afii10049
+!D0 U+0430 afii10065
+!D1 U+0431 afii10066
+!D2 U+0432 afii10067
+!D3 U+0433 afii10068
+!D4 U+0434 afii10069
+!D5 U+0435 afii10070
+!D6 U+0436 afii10072
+!D7 U+0437 afii10073
+!D8 U+0438 afii10074
+!D9 U+0439 afii10075
+!DA U+043A afii10076
+!DB U+043B afii10077
+!DC U+043C afii10078
+!DD U+043D afii10079
+!DE U+043E afii10080
+!DF U+043F afii10081
+!E0 U+0440 afii10082
+!E1 U+0441 afii10083
+!E2 U+0442 afii10084
+!E3 U+0443 afii10085
+!E4 U+0444 afii10086
+!E5 U+0445 afii10087
+!E6 U+0446 afii10088
+!E7 U+0447 afii10089
+!E8 U+0448 afii10090
+!E9 U+0449 afii10091
+!EA U+044A afii10092
+!EB U+044B afii10093
+!EC U+044C afii10094
+!ED U+044D afii10095
+!EE U+044E afii10096
+!EF U+044F afii10097
+!F0 U+2116 afii61352
+!F1 U+0451 afii10071
+!F2 U+0452 afii10099
+!F3 U+0453 afii10100
+!F4 U+0454 afii10101
+!F5 U+0455 afii10102
+!F6 U+0456 afii10103
+!F7 U+0457 afii10104
+!F8 U+0458 afii10105
+!F9 U+0459 afii10106
+!FA U+045A afii10107
+!FB U+045B afii10108
+!FC U+045C afii10109
+!FD U+00A7 section
+!FE U+045E afii10110
+!FF U+045F afii10193
diff --git a/src/lib/fpdf/makefont/iso-8859-7.map b/src/lib/fpdf/makefont/iso-8859-7.map
new file mode 100644
index 0000000..e163796
--- /dev/null
+++ b/src/lib/fpdf/makefont/iso-8859-7.map
@@ -0,0 +1,250 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+0080 .notdef
+!81 U+0081 .notdef
+!82 U+0082 .notdef
+!83 U+0083 .notdef
+!84 U+0084 .notdef
+!85 U+0085 .notdef
+!86 U+0086 .notdef
+!87 U+0087 .notdef
+!88 U+0088 .notdef
+!89 U+0089 .notdef
+!8A U+008A .notdef
+!8B U+008B .notdef
+!8C U+008C .notdef
+!8D U+008D .notdef
+!8E U+008E .notdef
+!8F U+008F .notdef
+!90 U+0090 .notdef
+!91 U+0091 .notdef
+!92 U+0092 .notdef
+!93 U+0093 .notdef
+!94 U+0094 .notdef
+!95 U+0095 .notdef
+!96 U+0096 .notdef
+!97 U+0097 .notdef
+!98 U+0098 .notdef
+!99 U+0099 .notdef
+!9A U+009A .notdef
+!9B U+009B .notdef
+!9C U+009C .notdef
+!9D U+009D .notdef
+!9E U+009E .notdef
+!9F U+009F .notdef
+!A0 U+00A0 space
+!A1 U+2018 quoteleft
+!A2 U+2019 quoteright
+!A3 U+00A3 sterling
+!A6 U+00A6 brokenbar
+!A7 U+00A7 section
+!A8 U+00A8 dieresis
+!A9 U+00A9 copyright
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD hyphen
+!AF U+2015 afii00208
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+00B2 twosuperior
+!B3 U+00B3 threesuperior
+!B4 U+0384 tonos
+!B5 U+0385 dieresistonos
+!B6 U+0386 Alphatonos
+!B7 U+00B7 periodcentered
+!B8 U+0388 Epsilontonos
+!B9 U+0389 Etatonos
+!BA U+038A Iotatonos
+!BB U+00BB guillemotright
+!BC U+038C Omicrontonos
+!BD U+00BD onehalf
+!BE U+038E Upsilontonos
+!BF U+038F Omegatonos
+!C0 U+0390 iotadieresistonos
+!C1 U+0391 Alpha
+!C2 U+0392 Beta
+!C3 U+0393 Gamma
+!C4 U+0394 Delta
+!C5 U+0395 Epsilon
+!C6 U+0396 Zeta
+!C7 U+0397 Eta
+!C8 U+0398 Theta
+!C9 U+0399 Iota
+!CA U+039A Kappa
+!CB U+039B Lambda
+!CC U+039C Mu
+!CD U+039D Nu
+!CE U+039E Xi
+!CF U+039F Omicron
+!D0 U+03A0 Pi
+!D1 U+03A1 Rho
+!D3 U+03A3 Sigma
+!D4 U+03A4 Tau
+!D5 U+03A5 Upsilon
+!D6 U+03A6 Phi
+!D7 U+03A7 Chi
+!D8 U+03A8 Psi
+!D9 U+03A9 Omega
+!DA U+03AA Iotadieresis
+!DB U+03AB Upsilondieresis
+!DC U+03AC alphatonos
+!DD U+03AD epsilontonos
+!DE U+03AE etatonos
+!DF U+03AF iotatonos
+!E0 U+03B0 upsilondieresistonos
+!E1 U+03B1 alpha
+!E2 U+03B2 beta
+!E3 U+03B3 gamma
+!E4 U+03B4 delta
+!E5 U+03B5 epsilon
+!E6 U+03B6 zeta
+!E7 U+03B7 eta
+!E8 U+03B8 theta
+!E9 U+03B9 iota
+!EA U+03BA kappa
+!EB U+03BB lambda
+!EC U+03BC mu
+!ED U+03BD nu
+!EE U+03BE xi
+!EF U+03BF omicron
+!F0 U+03C0 pi
+!F1 U+03C1 rho
+!F2 U+03C2 sigma1
+!F3 U+03C3 sigma
+!F4 U+03C4 tau
+!F5 U+03C5 upsilon
+!F6 U+03C6 phi
+!F7 U+03C7 chi
+!F8 U+03C8 psi
+!F9 U+03C9 omega
+!FA U+03CA iotadieresis
+!FB U+03CB upsilondieresis
+!FC U+03CC omicrontonos
+!FD U+03CD upsilontonos
+!FE U+03CE omegatonos
diff --git a/src/lib/fpdf/makefont/iso-8859-9.map b/src/lib/fpdf/makefont/iso-8859-9.map
new file mode 100644
index 0000000..48c123a
--- /dev/null
+++ b/src/lib/fpdf/makefont/iso-8859-9.map
@@ -0,0 +1,256 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+0080 .notdef
+!81 U+0081 .notdef
+!82 U+0082 .notdef
+!83 U+0083 .notdef
+!84 U+0084 .notdef
+!85 U+0085 .notdef
+!86 U+0086 .notdef
+!87 U+0087 .notdef
+!88 U+0088 .notdef
+!89 U+0089 .notdef
+!8A U+008A .notdef
+!8B U+008B .notdef
+!8C U+008C .notdef
+!8D U+008D .notdef
+!8E U+008E .notdef
+!8F U+008F .notdef
+!90 U+0090 .notdef
+!91 U+0091 .notdef
+!92 U+0092 .notdef
+!93 U+0093 .notdef
+!94 U+0094 .notdef
+!95 U+0095 .notdef
+!96 U+0096 .notdef
+!97 U+0097 .notdef
+!98 U+0098 .notdef
+!99 U+0099 .notdef
+!9A U+009A .notdef
+!9B U+009B .notdef
+!9C U+009C .notdef
+!9D U+009D .notdef
+!9E U+009E .notdef
+!9F U+009F .notdef
+!A0 U+00A0 space
+!A1 U+00A1 exclamdown
+!A2 U+00A2 cent
+!A3 U+00A3 sterling
+!A4 U+00A4 currency
+!A5 U+00A5 yen
+!A6 U+00A6 brokenbar
+!A7 U+00A7 section
+!A8 U+00A8 dieresis
+!A9 U+00A9 copyright
+!AA U+00AA ordfeminine
+!AB U+00AB guillemotleft
+!AC U+00AC logicalnot
+!AD U+00AD hyphen
+!AE U+00AE registered
+!AF U+00AF macron
+!B0 U+00B0 degree
+!B1 U+00B1 plusminus
+!B2 U+00B2 twosuperior
+!B3 U+00B3 threesuperior
+!B4 U+00B4 acute
+!B5 U+00B5 mu
+!B6 U+00B6 paragraph
+!B7 U+00B7 periodcentered
+!B8 U+00B8 cedilla
+!B9 U+00B9 onesuperior
+!BA U+00BA ordmasculine
+!BB U+00BB guillemotright
+!BC U+00BC onequarter
+!BD U+00BD onehalf
+!BE U+00BE threequarters
+!BF U+00BF questiondown
+!C0 U+00C0 Agrave
+!C1 U+00C1 Aacute
+!C2 U+00C2 Acircumflex
+!C3 U+00C3 Atilde
+!C4 U+00C4 Adieresis
+!C5 U+00C5 Aring
+!C6 U+00C6 AE
+!C7 U+00C7 Ccedilla
+!C8 U+00C8 Egrave
+!C9 U+00C9 Eacute
+!CA U+00CA Ecircumflex
+!CB U+00CB Edieresis
+!CC U+00CC Igrave
+!CD U+00CD Iacute
+!CE U+00CE Icircumflex
+!CF U+00CF Idieresis
+!D0 U+011E Gbreve
+!D1 U+00D1 Ntilde
+!D2 U+00D2 Ograve
+!D3 U+00D3 Oacute
+!D4 U+00D4 Ocircumflex
+!D5 U+00D5 Otilde
+!D6 U+00D6 Odieresis
+!D7 U+00D7 multiply
+!D8 U+00D8 Oslash
+!D9 U+00D9 Ugrave
+!DA U+00DA Uacute
+!DB U+00DB Ucircumflex
+!DC U+00DC Udieresis
+!DD U+0130 Idotaccent
+!DE U+015E Scedilla
+!DF U+00DF germandbls
+!E0 U+00E0 agrave
+!E1 U+00E1 aacute
+!E2 U+00E2 acircumflex
+!E3 U+00E3 atilde
+!E4 U+00E4 adieresis
+!E5 U+00E5 aring
+!E6 U+00E6 ae
+!E7 U+00E7 ccedilla
+!E8 U+00E8 egrave
+!E9 U+00E9 eacute
+!EA U+00EA ecircumflex
+!EB U+00EB edieresis
+!EC U+00EC igrave
+!ED U+00ED iacute
+!EE U+00EE icircumflex
+!EF U+00EF idieresis
+!F0 U+011F gbreve
+!F1 U+00F1 ntilde
+!F2 U+00F2 ograve
+!F3 U+00F3 oacute
+!F4 U+00F4 ocircumflex
+!F5 U+00F5 otilde
+!F6 U+00F6 odieresis
+!F7 U+00F7 divide
+!F8 U+00F8 oslash
+!F9 U+00F9 ugrave
+!FA U+00FA uacute
+!FB U+00FB ucircumflex
+!FC U+00FC udieresis
+!FD U+0131 dotlessi
+!FE U+015F scedilla
+!FF U+00FF ydieresis
diff --git a/src/lib/fpdf/makefont/koi8-r.map b/src/lib/fpdf/makefont/koi8-r.map
new file mode 100644
index 0000000..6ad5d05
--- /dev/null
+++ b/src/lib/fpdf/makefont/koi8-r.map
@@ -0,0 +1,256 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+2500 SF100000
+!81 U+2502 SF110000
+!82 U+250C SF010000
+!83 U+2510 SF030000
+!84 U+2514 SF020000
+!85 U+2518 SF040000
+!86 U+251C SF080000
+!87 U+2524 SF090000
+!88 U+252C SF060000
+!89 U+2534 SF070000
+!8A U+253C SF050000
+!8B U+2580 upblock
+!8C U+2584 dnblock
+!8D U+2588 block
+!8E U+258C lfblock
+!8F U+2590 rtblock
+!90 U+2591 ltshade
+!91 U+2592 shade
+!92 U+2593 dkshade
+!93 U+2320 integraltp
+!94 U+25A0 filledbox
+!95 U+2219 periodcentered
+!96 U+221A radical
+!97 U+2248 approxequal
+!98 U+2264 lessequal
+!99 U+2265 greaterequal
+!9A U+00A0 space
+!9B U+2321 integralbt
+!9C U+00B0 degree
+!9D U+00B2 twosuperior
+!9E U+00B7 periodcentered
+!9F U+00F7 divide
+!A0 U+2550 SF430000
+!A1 U+2551 SF240000
+!A2 U+2552 SF510000
+!A3 U+0451 afii10071
+!A4 U+2553 SF520000
+!A5 U+2554 SF390000
+!A6 U+2555 SF220000
+!A7 U+2556 SF210000
+!A8 U+2557 SF250000
+!A9 U+2558 SF500000
+!AA U+2559 SF490000
+!AB U+255A SF380000
+!AC U+255B SF280000
+!AD U+255C SF270000
+!AE U+255D SF260000
+!AF U+255E SF360000
+!B0 U+255F SF370000
+!B1 U+2560 SF420000
+!B2 U+2561 SF190000
+!B3 U+0401 afii10023
+!B4 U+2562 SF200000
+!B5 U+2563 SF230000
+!B6 U+2564 SF470000
+!B7 U+2565 SF480000
+!B8 U+2566 SF410000
+!B9 U+2567 SF450000
+!BA U+2568 SF460000
+!BB U+2569 SF400000
+!BC U+256A SF540000
+!BD U+256B SF530000
+!BE U+256C SF440000
+!BF U+00A9 copyright
+!C0 U+044E afii10096
+!C1 U+0430 afii10065
+!C2 U+0431 afii10066
+!C3 U+0446 afii10088
+!C4 U+0434 afii10069
+!C5 U+0435 afii10070
+!C6 U+0444 afii10086
+!C7 U+0433 afii10068
+!C8 U+0445 afii10087
+!C9 U+0438 afii10074
+!CA U+0439 afii10075
+!CB U+043A afii10076
+!CC U+043B afii10077
+!CD U+043C afii10078
+!CE U+043D afii10079
+!CF U+043E afii10080
+!D0 U+043F afii10081
+!D1 U+044F afii10097
+!D2 U+0440 afii10082
+!D3 U+0441 afii10083
+!D4 U+0442 afii10084
+!D5 U+0443 afii10085
+!D6 U+0436 afii10072
+!D7 U+0432 afii10067
+!D8 U+044C afii10094
+!D9 U+044B afii10093
+!DA U+0437 afii10073
+!DB U+0448 afii10090
+!DC U+044D afii10095
+!DD U+0449 afii10091
+!DE U+0447 afii10089
+!DF U+044A afii10092
+!E0 U+042E afii10048
+!E1 U+0410 afii10017
+!E2 U+0411 afii10018
+!E3 U+0426 afii10040
+!E4 U+0414 afii10021
+!E5 U+0415 afii10022
+!E6 U+0424 afii10038
+!E7 U+0413 afii10020
+!E8 U+0425 afii10039
+!E9 U+0418 afii10026
+!EA U+0419 afii10027
+!EB U+041A afii10028
+!EC U+041B afii10029
+!ED U+041C afii10030
+!EE U+041D afii10031
+!EF U+041E afii10032
+!F0 U+041F afii10033
+!F1 U+042F afii10049
+!F2 U+0420 afii10034
+!F3 U+0421 afii10035
+!F4 U+0422 afii10036
+!F5 U+0423 afii10037
+!F6 U+0416 afii10024
+!F7 U+0412 afii10019
+!F8 U+042C afii10046
+!F9 U+042B afii10045
+!FA U+0417 afii10025
+!FB U+0428 afii10042
+!FC U+042D afii10047
+!FD U+0429 afii10043
+!FE U+0427 afii10041
+!FF U+042A afii10044
diff --git a/src/lib/fpdf/makefont/koi8-u.map b/src/lib/fpdf/makefont/koi8-u.map
new file mode 100644
index 0000000..40a7e4f
--- /dev/null
+++ b/src/lib/fpdf/makefont/koi8-u.map
@@ -0,0 +1,256 @@
+!00 U+0000 .notdef
+!01 U+0001 .notdef
+!02 U+0002 .notdef
+!03 U+0003 .notdef
+!04 U+0004 .notdef
+!05 U+0005 .notdef
+!06 U+0006 .notdef
+!07 U+0007 .notdef
+!08 U+0008 .notdef
+!09 U+0009 .notdef
+!0A U+000A .notdef
+!0B U+000B .notdef
+!0C U+000C .notdef
+!0D U+000D .notdef
+!0E U+000E .notdef
+!0F U+000F .notdef
+!10 U+0010 .notdef
+!11 U+0011 .notdef
+!12 U+0012 .notdef
+!13 U+0013 .notdef
+!14 U+0014 .notdef
+!15 U+0015 .notdef
+!16 U+0016 .notdef
+!17 U+0017 .notdef
+!18 U+0018 .notdef
+!19 U+0019 .notdef
+!1A U+001A .notdef
+!1B U+001B .notdef
+!1C U+001C .notdef
+!1D U+001D .notdef
+!1E U+001E .notdef
+!1F U+001F .notdef
+!20 U+0020 space
+!21 U+0021 exclam
+!22 U+0022 quotedbl
+!23 U+0023 numbersign
+!24 U+0024 dollar
+!25 U+0025 percent
+!26 U+0026 ampersand
+!27 U+0027 quotesingle
+!28 U+0028 parenleft
+!29 U+0029 parenright
+!2A U+002A asterisk
+!2B U+002B plus
+!2C U+002C comma
+!2D U+002D hyphen
+!2E U+002E period
+!2F U+002F slash
+!30 U+0030 zero
+!31 U+0031 one
+!32 U+0032 two
+!33 U+0033 three
+!34 U+0034 four
+!35 U+0035 five
+!36 U+0036 six
+!37 U+0037 seven
+!38 U+0038 eight
+!39 U+0039 nine
+!3A U+003A colon
+!3B U+003B semicolon
+!3C U+003C less
+!3D U+003D equal
+!3E U+003E greater
+!3F U+003F question
+!40 U+0040 at
+!41 U+0041 A
+!42 U+0042 B
+!43 U+0043 C
+!44 U+0044 D
+!45 U+0045 E
+!46 U+0046 F
+!47 U+0047 G
+!48 U+0048 H
+!49 U+0049 I
+!4A U+004A J
+!4B U+004B K
+!4C U+004C L
+!4D U+004D M
+!4E U+004E N
+!4F U+004F O
+!50 U+0050 P
+!51 U+0051 Q
+!52 U+0052 R
+!53 U+0053 S
+!54 U+0054 T
+!55 U+0055 U
+!56 U+0056 V
+!57 U+0057 W
+!58 U+0058 X
+!59 U+0059 Y
+!5A U+005A Z
+!5B U+005B bracketleft
+!5C U+005C backslash
+!5D U+005D bracketright
+!5E U+005E asciicircum
+!5F U+005F underscore
+!60 U+0060 grave
+!61 U+0061 a
+!62 U+0062 b
+!63 U+0063 c
+!64 U+0064 d
+!65 U+0065 e
+!66 U+0066 f
+!67 U+0067 g
+!68 U+0068 h
+!69 U+0069 i
+!6A U+006A j
+!6B U+006B k
+!6C U+006C l
+!6D U+006D m
+!6E U+006E n
+!6F U+006F o
+!70 U+0070 p
+!71 U+0071 q
+!72 U+0072 r
+!73 U+0073 s
+!74 U+0074 t
+!75 U+0075 u
+!76 U+0076 v
+!77 U+0077 w
+!78 U+0078 x
+!79 U+0079 y
+!7A U+007A z
+!7B U+007B braceleft
+!7C U+007C bar
+!7D U+007D braceright
+!7E U+007E asciitilde
+!7F U+007F .notdef
+!80 U+2500 SF100000
+!81 U+2502 SF110000
+!82 U+250C SF010000
+!83 U+2510 SF030000
+!84 U+2514 SF020000
+!85 U+2518 SF040000
+!86 U+251C SF080000
+!87 U+2524 SF090000
+!88 U+252C SF060000
+!89 U+2534 SF070000
+!8A U+253C SF050000
+!8B U+2580 upblock
+!8C U+2584 dnblock
+!8D U+2588 block
+!8E U+258C lfblock
+!8F U+2590 rtblock
+!90 U+2591 ltshade
+!91 U+2592 shade
+!92 U+2593 dkshade
+!93 U+2320 integraltp
+!94 U+25A0 filledbox
+!95 U+2022 bullet
+!96 U+221A radical
+!97 U+2248 approxequal
+!98 U+2264 lessequal
+!99 U+2265 greaterequal
+!9A U+00A0 space
+!9B U+2321 integralbt
+!9C U+00B0 degree
+!9D U+00B2 twosuperior
+!9E U+00B7 periodcentered
+!9F U+00F7 divide
+!A0 U+2550 SF430000
+!A1 U+2551 SF240000
+!A2 U+2552 SF510000
+!A3 U+0451 afii10071
+!A4 U+0454 afii10101
+!A5 U+2554 SF390000
+!A6 U+0456 afii10103
+!A7 U+0457 afii10104
+!A8 U+2557 SF250000
+!A9 U+2558 SF500000
+!AA U+2559 SF490000
+!AB U+255A SF380000
+!AC U+255B SF280000
+!AD U+0491 afii10098
+!AE U+255D SF260000
+!AF U+255E SF360000
+!B0 U+255F SF370000
+!B1 U+2560 SF420000
+!B2 U+2561 SF190000
+!B3 U+0401 afii10023
+!B4 U+0404 afii10053
+!B5 U+2563 SF230000
+!B6 U+0406 afii10055
+!B7 U+0407 afii10056
+!B8 U+2566 SF410000
+!B9 U+2567 SF450000
+!BA U+2568 SF460000
+!BB U+2569 SF400000
+!BC U+256A SF540000
+!BD U+0490 afii10050
+!BE U+256C SF440000
+!BF U+00A9 copyright
+!C0 U+044E afii10096
+!C1 U+0430 afii10065
+!C2 U+0431 afii10066
+!C3 U+0446 afii10088
+!C4 U+0434 afii10069
+!C5 U+0435 afii10070
+!C6 U+0444 afii10086
+!C7 U+0433 afii10068
+!C8 U+0445 afii10087
+!C9 U+0438 afii10074
+!CA U+0439 afii10075
+!CB U+043A afii10076
+!CC U+043B afii10077
+!CD U+043C afii10078
+!CE U+043D afii10079
+!CF U+043E afii10080
+!D0 U+043F afii10081
+!D1 U+044F afii10097
+!D2 U+0440 afii10082
+!D3 U+0441 afii10083
+!D4 U+0442 afii10084
+!D5 U+0443 afii10085
+!D6 U+0436 afii10072
+!D7 U+0432 afii10067
+!D8 U+044C afii10094
+!D9 U+044B afii10093
+!DA U+0437 afii10073
+!DB U+0448 afii10090
+!DC U+044D afii10095
+!DD U+0449 afii10091
+!DE U+0447 afii10089
+!DF U+044A afii10092
+!E0 U+042E afii10048
+!E1 U+0410 afii10017
+!E2 U+0411 afii10018
+!E3 U+0426 afii10040
+!E4 U+0414 afii10021
+!E5 U+0415 afii10022
+!E6 U+0424 afii10038
+!E7 U+0413 afii10020
+!E8 U+0425 afii10039
+!E9 U+0418 afii10026
+!EA U+0419 afii10027
+!EB U+041A afii10028
+!EC U+041B afii10029
+!ED U+041C afii10030
+!EE U+041D afii10031
+!EF U+041E afii10032
+!F0 U+041F afii10033
+!F1 U+042F afii10049
+!F2 U+0420 afii10034
+!F3 U+0421 afii10035
+!F4 U+0422 afii10036
+!F5 U+0423 afii10037
+!F6 U+0416 afii10024
+!F7 U+0412 afii10019
+!F8 U+042C afii10046
+!F9 U+042B afii10045
+!FA U+0417 afii10025
+!FB U+0428 afii10042
+!FC U+042D afii10047
+!FD U+0429 afii10043
+!FE U+0427 afii10041
+!FF U+042A afii10044
diff --git a/src/lib/fpdf/makefont/makefont.php b/src/lib/fpdf/makefont/makefont.php
new file mode 100644
index 0000000..fbe8dcf
--- /dev/null
+++ b/src/lib/fpdf/makefont/makefont.php
@@ -0,0 +1,447 @@
+<?php

+/*******************************************************************************

+* Utility to generate font definition files                                    *

+*                                                                              *

+* Version: 1.31                                                                *

+* Date:    2019-12-07                                                          *

+* Author:  Olivier PLATHEY                                                     *

+*******************************************************************************/

+

+require('ttfparser.php');

+

+function Message($txt, $severity='')

+{

+	if(PHP_SAPI=='cli')

+	{

+		if($severity)

+			echo "$severity: ";

+		echo "$txt\n";

+	}

+	else

+	{

+		if($severity)

+			echo "<b>$severity</b>: ";

+		echo "$txt<br>";

+	}

+}

+

+function Notice($txt)

+{

+	Message($txt, 'Notice');

+}

+

+function Warning($txt)

+{

+	Message($txt, 'Warning');

+}

+

+function Error($txt)

+{

+	Message($txt, 'Error');

+	exit;

+}

+

+function LoadMap($enc)

+{

+	$file = dirname(__FILE__).'/'.strtolower($enc).'.map';

+	$a = file($file);

+	if(empty($a))

+		Error('Encoding not found: '.$enc);

+	$map = array_fill(0, 256, array('uv'=>-1, 'name'=>'.notdef'));

+	foreach($a as $line)

+	{

+		$e = explode(' ', rtrim($line));

+		$c = hexdec(substr($e[0],1));

+		$uv = hexdec(substr($e[1],2));

+		$name = $e[2];

+		$map[$c] = array('uv'=>$uv, 'name'=>$name);

+	}

+	return $map;

+}

+

+function GetInfoFromTrueType($file, $embed, $subset, $map)

+{

+	// Return information from a TrueType font

+	try

+	{

+		$ttf = new TTFParser($file);

+		$ttf->Parse();

+	}

+	catch(Exception $e)

+	{

+		Error($e->getMessage());

+	}

+	if($embed)

+	{

+		if(!$ttf->embeddable)

+			Error('Font license does not allow embedding');

+		if($subset)

+		{

+			$chars = array();

+			foreach($map as $v)

+			{

+				if($v['name']!='.notdef')

+					$chars[] = $v['uv'];

+			}

+			$ttf->Subset($chars);

+			$info['Data'] = $ttf->Build();

+		}

+		else

+			$info['Data'] = file_get_contents($file);

+		$info['OriginalSize'] = strlen($info['Data']);

+	}

+	$k = 1000/$ttf->unitsPerEm;

+	$info['FontName'] = $ttf->postScriptName;

+	$info['Bold'] = $ttf->bold;

+	$info['ItalicAngle'] = $ttf->italicAngle;

+	$info['IsFixedPitch'] = $ttf->isFixedPitch;

+	$info['Ascender'] = round($k*$ttf->typoAscender);

+	$info['Descender'] = round($k*$ttf->typoDescender);

+	$info['UnderlineThickness'] = round($k*$ttf->underlineThickness);

+	$info['UnderlinePosition'] = round($k*$ttf->underlinePosition);

+	$info['FontBBox'] = array(round($k*$ttf->xMin), round($k*$ttf->yMin), round($k*$ttf->xMax), round($k*$ttf->yMax));

+	$info['CapHeight'] = round($k*$ttf->capHeight);

+	$info['MissingWidth'] = round($k*$ttf->glyphs[0]['w']);

+	$widths = array_fill(0, 256, $info['MissingWidth']);

+	foreach($map as $c=>$v)

+	{

+		if($v['name']!='.notdef')

+		{

+			if(isset($ttf->chars[$v['uv']]))

+			{

+				$id = $ttf->chars[$v['uv']];

+				$w = $ttf->glyphs[$id]['w'];

+				$widths[$c] = round($k*$w);

+			}

+			else

+				Warning('Character '.$v['name'].' is missing');

+		}

+	}

+	$info['Widths'] = $widths;

+	return $info;

+}

+

+function GetInfoFromType1($file, $embed, $map)

+{

+	// Return information from a Type1 font

+	if($embed)

+	{

+		$f = fopen($file, 'rb');

+		if(!$f)

+			Error('Can\'t open font file');

+		// Read first segment

+		$a = unpack('Cmarker/Ctype/Vsize', fread($f,6));

+		if($a['marker']!=128)

+			Error('Font file is not a valid binary Type1');

+		$size1 = $a['size'];

+		$data = fread($f, $size1);

+		// Read second segment

+		$a = unpack('Cmarker/Ctype/Vsize', fread($f,6));

+		if($a['marker']!=128)

+			Error('Font file is not a valid binary Type1');

+		$size2 = $a['size'];

+		$data .= fread($f, $size2);

+		fclose($f);

+		$info['Data'] = $data;

+		$info['Size1'] = $size1;

+		$info['Size2'] = $size2;

+	}

+

+	$afm = substr($file, 0, -3).'afm';

+	if(!file_exists($afm))

+		Error('AFM font file not found: '.$afm);

+	$a = file($afm);

+	if(empty($a))

+		Error('AFM file empty or not readable');

+	foreach($a as $line)

+	{

+		$e = explode(' ', rtrim($line));

+		if(count($e)<2)

+			continue;

+		$entry = $e[0];

+		if($entry=='C')

+		{

+			$w = $e[4];

+			$name = $e[7];

+			$cw[$name] = $w;

+		}

+		elseif($entry=='FontName')

+			$info['FontName'] = $e[1];

+		elseif($entry=='Weight')

+			$info['Weight'] = $e[1];

+		elseif($entry=='ItalicAngle')

+			$info['ItalicAngle'] = (int)$e[1];

+		elseif($entry=='Ascender')

+			$info['Ascender'] = (int)$e[1];

+		elseif($entry=='Descender')

+			$info['Descender'] = (int)$e[1];

+		elseif($entry=='UnderlineThickness')

+			$info['UnderlineThickness'] = (int)$e[1];

+		elseif($entry=='UnderlinePosition')

+			$info['UnderlinePosition'] = (int)$e[1];

+		elseif($entry=='IsFixedPitch')

+			$info['IsFixedPitch'] = ($e[1]=='true');

+		elseif($entry=='FontBBox')

+			$info['FontBBox'] = array((int)$e[1], (int)$e[2], (int)$e[3], (int)$e[4]);

+		elseif($entry=='CapHeight')

+			$info['CapHeight'] = (int)$e[1];

+		elseif($entry=='StdVW')

+			$info['StdVW'] = (int)$e[1];

+	}

+

+	if(!isset($info['FontName']))

+		Error('FontName missing in AFM file');

+	if(!isset($info['Ascender']))

+		$info['Ascender'] = $info['FontBBox'][3];

+	if(!isset($info['Descender']))

+		$info['Descender'] = $info['FontBBox'][1];

+	$info['Bold'] = isset($info['Weight']) && preg_match('/bold|black/i', $info['Weight']);

+	if(isset($cw['.notdef']))

+		$info['MissingWidth'] = $cw['.notdef'];

+	else

+		$info['MissingWidth'] = 0;

+	$widths = array_fill(0, 256, $info['MissingWidth']);

+	foreach($map as $c=>$v)

+	{

+		if($v['name']!='.notdef')

+		{

+			if(isset($cw[$v['name']]))

+				$widths[$c] = $cw[$v['name']];

+			else

+				Warning('Character '.$v['name'].' is missing');

+		}

+	}

+	$info['Widths'] = $widths;

+	return $info;

+}

+

+function MakeFontDescriptor($info)

+{

+	// Ascent

+	$fd = "array('Ascent'=>".$info['Ascender'];

+	// Descent

+	$fd .= ",'Descent'=>".$info['Descender'];

+	// CapHeight

+	if(!empty($info['CapHeight']))

+		$fd .= ",'CapHeight'=>".$info['CapHeight'];

+	else

+		$fd .= ",'CapHeight'=>".$info['Ascender'];

+	// Flags

+	$flags = 0;

+	if($info['IsFixedPitch'])

+		$flags += 1<<0;

+	$flags += 1<<5;

+	if($info['ItalicAngle']!=0)

+		$flags += 1<<6;

+	$fd .= ",'Flags'=>".$flags;

+	// FontBBox

+	$fbb = $info['FontBBox'];

+	$fd .= ",'FontBBox'=>'[".$fbb[0].' '.$fbb[1].' '.$fbb[2].' '.$fbb[3]."]'";

+	// ItalicAngle

+	$fd .= ",'ItalicAngle'=>".$info['ItalicAngle'];

+	// StemV

+	if(isset($info['StdVW']))

+		$stemv = $info['StdVW'];

+	elseif($info['Bold'])

+		$stemv = 120;

+	else

+		$stemv = 70;

+	$fd .= ",'StemV'=>".$stemv;

+	// MissingWidth

+	$fd .= ",'MissingWidth'=>".$info['MissingWidth'].')';

+	return $fd;

+}

+

+function MakeWidthArray($widths)

+{

+	$s = "array(\n\t";

+	for($c=0;$c<=255;$c++)

+	{

+		if(chr($c)=="'")

+			$s .= "'\\''";

+		elseif(chr($c)=="\\")

+			$s .= "'\\\\'";

+		elseif($c>=32 && $c<=126)

+			$s .= "'".chr($c)."'";

+		else

+			$s .= "chr($c)";

+		$s .= '=>'.$widths[$c];

+		if($c<255)

+			$s .= ',';

+		if(($c+1)%22==0)

+			$s .= "\n\t";

+	}

+	$s .= ')';

+	return $s;

+}

+

+function MakeFontEncoding($map)

+{

+	// Build differences from reference encoding

+	$ref = LoadMap('cp1252');

+	$s = '';

+	$last = 0;

+	for($c=32;$c<=255;$c++)

+	{

+		if($map[$c]['name']!=$ref[$c]['name'])

+		{

+			if($c!=$last+1)

+				$s .= $c.' ';

+			$last = $c;

+			$s .= '/'.$map[$c]['name'].' ';

+		}

+	}

+	return rtrim($s);

+}

+

+function MakeUnicodeArray($map)

+{

+	// Build mapping to Unicode values

+	$ranges = array();

+	foreach($map as $c=>$v)

+	{

+		$uv = $v['uv'];

+		if($uv!=-1)

+		{

+			if(isset($range))

+			{

+				if($c==$range[1]+1 && $uv==$range[3]+1)

+				{

+					$range[1]++;

+					$range[3]++;

+				}

+				else

+				{

+					$ranges[] = $range;

+					$range = array($c, $c, $uv, $uv);

+				}

+			}

+			else

+				$range = array($c, $c, $uv, $uv);

+		}

+	}

+	$ranges[] = $range;

+

+	foreach($ranges as $range)

+	{

+		if(isset($s))

+			$s .= ',';

+		else

+			$s = 'array(';

+		$s .= $range[0].'=>';

+		$nb = $range[1]-$range[0]+1;

+		if($nb>1)

+			$s .= 'array('.$range[2].','.$nb.')';

+		else

+			$s .= $range[2];

+	}

+	$s .= ')';

+	return $s;

+}

+

+function SaveToFile($file, $s, $mode)

+{

+	$f = fopen($file, 'w'.$mode);

+	if(!$f)

+		Error('Can\'t write to file '.$file);

+	fwrite($f, $s);

+	fclose($f);

+}

+

+function MakeDefinitionFile($file, $type, $enc, $embed, $subset, $map, $info)

+{

+	$s = "<?php\n";

+	$s .= '$type = \''.$type."';\n";

+	$s .= '$name = \''.$info['FontName']."';\n";

+	$s .= '$desc = '.MakeFontDescriptor($info).";\n";

+	$s .= '$up = '.$info['UnderlinePosition'].";\n";

+	$s .= '$ut = '.$info['UnderlineThickness'].";\n";

+	$s .= '$cw = '.MakeWidthArray($info['Widths']).";\n";

+	$s .= '$enc = \''.$enc."';\n";

+	$diff = MakeFontEncoding($map);

+	if($diff)

+		$s .= '$diff = \''.$diff."';\n";

+	$s .= '$uv = '.MakeUnicodeArray($map).";\n";

+	if($embed)

+	{

+		$s .= '$file = \''.$info['File']."';\n";

+		if($type=='Type1')

+		{

+			$s .= '$size1 = '.$info['Size1'].";\n";

+			$s .= '$size2 = '.$info['Size2'].";\n";

+		}

+		else

+		{

+			$s .= '$originalsize = '.$info['OriginalSize'].";\n";

+			if($subset)

+				$s .= "\$subsetted = true;\n";

+		}

+	}

+	$s .= "?>\n";

+	SaveToFile($file, $s, 't');

+}

+

+function MakeFont($fontfile, $enc='cp1252', $embed=true, $subset=true)

+{

+	// Generate a font definition file

+	if(!file_exists($fontfile))

+		Error('Font file not found: '.$fontfile);

+	$ext = strtolower(substr($fontfile,-3));

+	if($ext=='ttf' || $ext=='otf')

+		$type = 'TrueType';

+	elseif($ext=='pfb')

+		$type = 'Type1';

+	else

+		Error('Unrecognized font file extension: '.$ext);

+

+	$map = LoadMap($enc);

+

+	if($type=='TrueType')

+		$info = GetInfoFromTrueType($fontfile, $embed, $subset, $map);

+	else

+		$info = GetInfoFromType1($fontfile, $embed, $map);

+

+	$basename = substr(basename($fontfile), 0, -4);

+	if($embed)

+	{

+		if(function_exists('gzcompress'))

+		{

+			$file = $basename.'.z';

+			SaveToFile($file, gzcompress($info['Data']), 'b');

+			$info['File'] = $file;

+			Message('Font file compressed: '.$file);

+		}

+		else

+		{

+			$info['File'] = basename($fontfile);

+			$subset = false;

+			Notice('Font file could not be compressed (zlib extension not available)');

+		}

+	}

+

+	MakeDefinitionFile($basename.'.php', $type, $enc, $embed, $subset, $map, $info);

+	Message('Font definition file generated: '.$basename.'.php');

+}

+

+if(PHP_SAPI=='cli')

+{

+	// Command-line interface

+	ini_set('log_errors', '0');

+	if($argc==1)

+		die("Usage: php makefont.php fontfile [encoding] [embed] [subset]\n");

+	$fontfile = $argv[1];

+	if($argc>=3)

+		$enc = $argv[2];

+	else

+		$enc = 'cp1252';

+	if($argc>=4)

+		$embed = ($argv[3]=='true' || $argv[3]=='1');

+	else

+		$embed = true;

+	if($argc>=5)

+		$subset = ($argv[4]=='true' || $argv[4]=='1');

+	else

+		$subset = true;

+	MakeFont($fontfile, $enc, $embed, $subset);

+}

+?>

diff --git a/src/lib/fpdf/makefont/ttfparser.php b/src/lib/fpdf/makefont/ttfparser.php
new file mode 100644
index 0000000..b5acf29
--- /dev/null
+++ b/src/lib/fpdf/makefont/ttfparser.php
@@ -0,0 +1,714 @@
+<?php

+/*******************************************************************************

+* Class to parse and subset TrueType fonts                                     *

+*                                                                              *

+* Version: 1.11                                                                *

+* Date:    2021-04-18                                                          *

+* Author:  Olivier PLATHEY                                                     *

+*******************************************************************************/

+

+class TTFParser

+{

+	protected $f;

+	protected $tables;

+	protected $numberOfHMetrics;

+	protected $numGlyphs;

+	protected $glyphNames;

+	protected $indexToLocFormat;

+	protected $subsettedChars;

+	protected $subsettedGlyphs;

+	public $chars;

+	public $glyphs;

+	public $unitsPerEm;

+	public $xMin, $yMin, $xMax, $yMax;

+	public $postScriptName;

+	public $embeddable;

+	public $bold;

+	public $typoAscender;

+	public $typoDescender;

+	public $capHeight;

+	public $italicAngle;

+	public $underlinePosition;

+	public $underlineThickness;

+	public $isFixedPitch;

+

+	function __construct($file)

+	{

+		$this->f = fopen($file, 'rb');

+		if(!$this->f)

+			$this->Error('Can\'t open file: '.$file);

+	}

+

+	function __destruct()

+	{

+		if(is_resource($this->f))

+			fclose($this->f);

+	}

+

+	function Parse()

+	{

+		$this->ParseOffsetTable();

+		$this->ParseHead();

+		$this->ParseHhea();

+		$this->ParseMaxp();

+		$this->ParseHmtx();

+		$this->ParseLoca();

+		$this->ParseGlyf();

+		$this->ParseCmap();

+		$this->ParseName();

+		$this->ParseOS2();

+		$this->ParsePost();

+	}

+

+	function ParseOffsetTable()

+	{

+		$version = $this->Read(4);

+		if($version=='OTTO')

+			$this->Error('OpenType fonts based on PostScript outlines are not supported');

+		if($version!="\x00\x01\x00\x00")

+			$this->Error('Unrecognized file format');

+		$numTables = $this->ReadUShort();

+		$this->Skip(3*2); // searchRange, entrySelector, rangeShift

+		$this->tables = array();

+		for($i=0;$i<$numTables;$i++)

+		{

+			$tag = $this->Read(4);

+			$checkSum = $this->Read(4);

+			$offset = $this->ReadULong();

+			$length = $this->ReadULong();

+			$this->tables[$tag] = array('offset'=>$offset, 'length'=>$length, 'checkSum'=>$checkSum);

+		}

+	}	

+

+	function ParseHead()

+	{

+		$this->Seek('head');

+		$this->Skip(3*4); // version, fontRevision, checkSumAdjustment

+		$magicNumber = $this->ReadULong();

+		if($magicNumber!=0x5F0F3CF5)

+			$this->Error('Incorrect magic number');

+		$this->Skip(2); // flags

+		$this->unitsPerEm = $this->ReadUShort();

+		$this->Skip(2*8); // created, modified

+		$this->xMin = $this->ReadShort();

+		$this->yMin = $this->ReadShort();

+		$this->xMax = $this->ReadShort();

+		$this->yMax = $this->ReadShort();

+		$this->Skip(3*2); // macStyle, lowestRecPPEM, fontDirectionHint

+		$this->indexToLocFormat = $this->ReadShort();

+	}

+

+	function ParseHhea()

+	{

+		$this->Seek('hhea');

+		$this->Skip(4+15*2);

+		$this->numberOfHMetrics = $this->ReadUShort();

+	}

+

+	function ParseMaxp()

+	{

+		$this->Seek('maxp');

+		$this->Skip(4);

+		$this->numGlyphs = $this->ReadUShort();

+	}

+

+	function ParseHmtx()

+	{

+		$this->Seek('hmtx');

+		$this->glyphs = array();

+		for($i=0;$i<$this->numberOfHMetrics;$i++)

+		{

+			$advanceWidth = $this->ReadUShort();

+			$lsb = $this->ReadShort();

+			$this->glyphs[$i] = array('w'=>$advanceWidth, 'lsb'=>$lsb);

+		}

+		for($i=$this->numberOfHMetrics;$i<$this->numGlyphs;$i++)

+		{

+			$lsb = $this->ReadShort();

+			$this->glyphs[$i] = array('w'=>$advanceWidth, 'lsb'=>$lsb);

+		}

+	}

+

+	function ParseLoca()

+	{

+		$this->Seek('loca');

+		$offsets = array();

+		if($this->indexToLocFormat==0)

+		{

+			// Short format

+			for($i=0;$i<=$this->numGlyphs;$i++)

+				$offsets[] = 2*$this->ReadUShort();

+		}

+		else

+		{

+			// Long format

+			for($i=0;$i<=$this->numGlyphs;$i++)

+				$offsets[] = $this->ReadULong();

+		}

+		for($i=0;$i<$this->numGlyphs;$i++)

+		{

+			$this->glyphs[$i]['offset'] = $offsets[$i];

+			$this->glyphs[$i]['length'] = $offsets[$i+1] - $offsets[$i];

+		}

+	}

+

+	function ParseGlyf()

+	{

+		$tableOffset = $this->tables['glyf']['offset'];

+		foreach($this->glyphs as &$glyph)

+		{

+			if($glyph['length']>0)

+			{

+				fseek($this->f, $tableOffset+$glyph['offset'], SEEK_SET);

+				if($this->ReadShort()<0)

+				{

+					// Composite glyph

+					$this->Skip(4*2); // xMin, yMin, xMax, yMax

+					$offset = 5*2;

+					$a = array();

+					do

+					{

+						$flags = $this->ReadUShort();

+						$index = $this->ReadUShort();

+						$a[$offset+2] = $index;

+						if($flags & 1) // ARG_1_AND_2_ARE_WORDS

+							$skip = 2*2;

+						else

+							$skip = 2;

+						if($flags & 8) // WE_HAVE_A_SCALE

+							$skip += 2;

+						elseif($flags & 64) // WE_HAVE_AN_X_AND_Y_SCALE

+							$skip += 2*2;

+						elseif($flags & 128) // WE_HAVE_A_TWO_BY_TWO

+							$skip += 4*2;

+						$this->Skip($skip);

+						$offset += 2*2 + $skip;

+					}

+					while($flags & 32); // MORE_COMPONENTS

+					$glyph['components'] = $a;

+				}

+			}

+		}

+	}

+

+	function ParseCmap()

+	{

+		$this->Seek('cmap');

+		$this->Skip(2); // version

+		$numTables = $this->ReadUShort();

+		$offset31 = 0;

+		for($i=0;$i<$numTables;$i++)

+		{

+			$platformID = $this->ReadUShort();

+			$encodingID = $this->ReadUShort();

+			$offset = $this->ReadULong();

+			if($platformID==3 && $encodingID==1)

+				$offset31 = $offset;

+		}

+		if($offset31==0)

+			$this->Error('No Unicode encoding found');

+

+		$startCount = array();

+		$endCount = array();

+		$idDelta = array();

+		$idRangeOffset = array();

+		$this->chars = array();

+		fseek($this->f, $this->tables['cmap']['offset']+$offset31, SEEK_SET);

+		$format = $this->ReadUShort();

+		if($format!=4)

+			$this->Error('Unexpected subtable format: '.$format);

+		$this->Skip(2*2); // length, language

+		$segCount = $this->ReadUShort()/2;

+		$this->Skip(3*2); // searchRange, entrySelector, rangeShift

+		for($i=0;$i<$segCount;$i++)

+			$endCount[$i] = $this->ReadUShort();

+		$this->Skip(2); // reservedPad

+		for($i=0;$i<$segCount;$i++)

+			$startCount[$i] = $this->ReadUShort();

+		for($i=0;$i<$segCount;$i++)

+			$idDelta[$i] = $this->ReadShort();

+		$offset = ftell($this->f);

+		for($i=0;$i<$segCount;$i++)

+			$idRangeOffset[$i] = $this->ReadUShort();

+

+		for($i=0;$i<$segCount;$i++)

+		{

+			$c1 = $startCount[$i];

+			$c2 = $endCount[$i];

+			$d = $idDelta[$i];

+			$ro = $idRangeOffset[$i];

+			if($ro>0)

+				fseek($this->f, $offset+2*$i+$ro, SEEK_SET);

+			for($c=$c1;$c<=$c2;$c++)

+			{

+				if($c==0xFFFF)

+					break;

+				if($ro>0)

+				{

+					$gid = $this->ReadUShort();

+					if($gid>0)

+						$gid += $d;

+				}

+				else

+					$gid = $c+$d;

+				if($gid>=65536)

+					$gid -= 65536;

+				if($gid>0)

+					$this->chars[$c] = $gid;

+			}

+		}

+	}

+

+	function ParseName()

+	{

+		$this->Seek('name');

+		$tableOffset = $this->tables['name']['offset'];

+		$this->postScriptName = '';

+		$this->Skip(2); // format

+		$count = $this->ReadUShort();

+		$stringOffset = $this->ReadUShort();

+		for($i=0;$i<$count;$i++)

+		{

+			$this->Skip(3*2); // platformID, encodingID, languageID

+			$nameID = $this->ReadUShort();

+			$length = $this->ReadUShort();

+			$offset = $this->ReadUShort();

+			if($nameID==6)

+			{

+				// PostScript name

+				fseek($this->f, $tableOffset+$stringOffset+$offset, SEEK_SET);

+				$s = $this->Read($length);

+				$s = str_replace(chr(0), '', $s);

+				$s = preg_replace('|[ \[\](){}<>/%]|', '', $s);

+				$this->postScriptName = $s;

+				break;

+			}

+		}

+		if($this->postScriptName=='')

+			$this->Error('PostScript name not found');

+	}

+

+	function ParseOS2()

+	{

+		$this->Seek('OS/2');

+		$version = $this->ReadUShort();

+		$this->Skip(3*2); // xAvgCharWidth, usWeightClass, usWidthClass

+		$fsType = $this->ReadUShort();

+		$this->embeddable = ($fsType!=2) && ($fsType & 0x200)==0;

+		$this->Skip(11*2+10+4*4+4);

+		$fsSelection = $this->ReadUShort();

+		$this->bold = ($fsSelection & 32)!=0;

+		$this->Skip(2*2); // usFirstCharIndex, usLastCharIndex

+		$this->typoAscender = $this->ReadShort();

+		$this->typoDescender = $this->ReadShort();

+		if($version>=2)

+		{

+			$this->Skip(3*2+2*4+2);

+			$this->capHeight = $this->ReadShort();

+		}

+		else

+			$this->capHeight = 0;

+	}

+

+	function ParsePost()

+	{

+		$this->Seek('post');

+		$version = $this->ReadULong();

+		$this->italicAngle = $this->ReadShort();

+		$this->Skip(2); // Skip decimal part

+		$this->underlinePosition = $this->ReadShort();

+		$this->underlineThickness = $this->ReadShort();

+		$this->isFixedPitch = ($this->ReadULong()!=0);

+		if($version==0x20000)

+		{

+			// Extract glyph names

+			$this->Skip(4*4); // min/max usage

+			$this->Skip(2); // numberOfGlyphs

+			$glyphNameIndex = array();

+			$names = array();

+			$numNames = 0;

+			for($i=0;$i<$this->numGlyphs;$i++)

+			{

+				$index = $this->ReadUShort();

+				$glyphNameIndex[] = $index;

+				if($index>=258 && $index-257>$numNames)

+					$numNames = $index-257;

+			}

+			for($i=0;$i<$numNames;$i++)

+			{

+				$len = ord($this->Read(1));

+				$names[] = $this->Read($len);

+			}

+			foreach($glyphNameIndex as $i=>$index)

+			{

+				if($index>=258)

+					$this->glyphs[$i]['name'] = $names[$index-258];

+				else

+					$this->glyphs[$i]['name'] = $index;

+			}

+			$this->glyphNames = true;

+		}

+		else

+			$this->glyphNames = false;

+	}

+

+	function Subset($chars)

+	{

+		$this->subsettedGlyphs = array();

+		$this->AddGlyph(0);

+		$this->subsettedChars = array();

+		foreach($chars as $char)

+		{

+			if(isset($this->chars[$char]))

+			{

+				$this->subsettedChars[] = $char;

+				$this->AddGlyph($this->chars[$char]);

+			}

+		}

+	}

+

+	function AddGlyph($id)

+	{

+		if(!isset($this->glyphs[$id]['ssid']))

+		{

+			$this->glyphs[$id]['ssid'] = count($this->subsettedGlyphs);

+			$this->subsettedGlyphs[] = $id;

+			if(isset($this->glyphs[$id]['components']))

+			{

+				foreach($this->glyphs[$id]['components'] as $cid)

+					$this->AddGlyph($cid);

+			}

+		}

+	}

+

+	function Build()

+	{

+		$this->BuildCmap();

+		$this->BuildHhea();

+		$this->BuildHmtx();

+		$this->BuildLoca();

+		$this->BuildGlyf();

+		$this->BuildMaxp();

+		$this->BuildPost();

+		return $this->BuildFont();

+	}

+

+	function BuildCmap()

+	{

+		if(!isset($this->subsettedChars))

+			return;

+

+		// Divide charset in contiguous segments

+		$chars = $this->subsettedChars;

+		sort($chars);

+		$segments = array();

+		$segment = array($chars[0], $chars[0]);

+		for($i=1;$i<count($chars);$i++)

+		{

+			if($chars[$i]>$segment[1]+1)

+			{

+				$segments[] = $segment;

+				$segment = array($chars[$i], $chars[$i]);

+			}

+			else

+				$segment[1]++;

+		}

+		$segments[] = $segment;

+		$segments[] = array(0xFFFF, 0xFFFF);

+		$segCount = count($segments);

+

+		// Build a Format 4 subtable

+		$startCount = array();

+		$endCount = array();

+		$idDelta = array();

+		$idRangeOffset = array();

+		$glyphIdArray = '';

+		for($i=0;$i<$segCount;$i++)

+		{

+			list($start, $end) = $segments[$i];

+			$startCount[] = $start;

+			$endCount[] = $end;

+			if($start!=$end)

+			{

+				// Segment with multiple chars

+				$idDelta[] = 0;

+				$idRangeOffset[] = strlen($glyphIdArray) + ($segCount-$i)*2;

+				for($c=$start;$c<=$end;$c++)

+				{

+					$ssid = $this->glyphs[$this->chars[$c]]['ssid'];

+					$glyphIdArray .= pack('n', $ssid);

+				}

+			}

+			else

+			{

+				// Segment with a single char

+				if($start<0xFFFF)

+					$ssid = $this->glyphs[$this->chars[$start]]['ssid'];

+				else

+					$ssid = 0;

+				$idDelta[] = $ssid - $start;

+				$idRangeOffset[] = 0;

+			}

+		}

+		$entrySelector = 0;

+		$n = $segCount;

+		while($n!=1)

+		{

+			$n = $n>>1;

+			$entrySelector++;

+		}

+		$searchRange = (1<<$entrySelector)*2;

+		$rangeShift = 2*$segCount - $searchRange;

+		$cmap = pack('nnnn', 2*$segCount, $searchRange, $entrySelector, $rangeShift);

+		foreach($endCount as $val)

+			$cmap .= pack('n', $val);

+		$cmap .= pack('n', 0); // reservedPad

+		foreach($startCount as $val)

+			$cmap .= pack('n', $val);

+		foreach($idDelta as $val)

+			$cmap .= pack('n', $val);

+		foreach($idRangeOffset as $val)

+			$cmap .= pack('n', $val);

+		$cmap .= $glyphIdArray;

+

+		$data = pack('nn', 0, 1); // version, numTables

+		$data .= pack('nnN', 3, 1, 12); // platformID, encodingID, offset

+		$data .= pack('nnn', 4, 6+strlen($cmap), 0); // format, length, language

+		$data .= $cmap;

+		$this->SetTable('cmap', $data);

+	}

+

+	function BuildHhea()

+	{

+		$this->LoadTable('hhea');

+		$numberOfHMetrics = count($this->subsettedGlyphs);

+		$data = substr_replace($this->tables['hhea']['data'], pack('n',$numberOfHMetrics), 4+15*2, 2);

+		$this->SetTable('hhea', $data);

+	}

+

+	function BuildHmtx()

+	{

+		$data = '';

+		foreach($this->subsettedGlyphs as $id)

+		{

+			$glyph = $this->glyphs[$id];

+			$data .= pack('nn', $glyph['w'], $glyph['lsb']);

+		}

+		$this->SetTable('hmtx', $data);

+	}

+

+	function BuildLoca()

+	{

+		$data = '';

+		$offset = 0;

+		foreach($this->subsettedGlyphs as $id)

+		{

+			if($this->indexToLocFormat==0)

+				$data .= pack('n', $offset/2);

+			else

+				$data .= pack('N', $offset);

+			$offset += $this->glyphs[$id]['length'];

+		}

+		if($this->indexToLocFormat==0)

+			$data .= pack('n', $offset/2);

+		else

+			$data .= pack('N', $offset);

+		$this->SetTable('loca', $data);

+	}

+

+	function BuildGlyf()

+	{

+		$tableOffset = $this->tables['glyf']['offset'];

+		$data = '';

+		foreach($this->subsettedGlyphs as $id)

+		{

+			$glyph = $this->glyphs[$id];

+			fseek($this->f, $tableOffset+$glyph['offset'], SEEK_SET);

+			$glyph_data = $this->Read($glyph['length']);

+			if(isset($glyph['components']))

+			{

+				// Composite glyph

+				foreach($glyph['components'] as $offset=>$cid)

+				{

+					$ssid = $this->glyphs[$cid]['ssid'];

+					$glyph_data = substr_replace($glyph_data, pack('n',$ssid), $offset, 2);

+				}

+			}

+			$data .= $glyph_data;

+		}

+		$this->SetTable('glyf', $data);

+	}

+

+	function BuildMaxp()

+	{

+		$this->LoadTable('maxp');

+		$numGlyphs = count($this->subsettedGlyphs);

+		$data = substr_replace($this->tables['maxp']['data'], pack('n',$numGlyphs), 4, 2);

+		$this->SetTable('maxp', $data);

+	}

+

+	function BuildPost()

+	{

+		$this->Seek('post');

+		if($this->glyphNames)

+		{

+			// Version 2.0

+			$numberOfGlyphs = count($this->subsettedGlyphs);

+			$numNames = 0;

+			$names = '';

+			$data = $this->Read(2*4+2*2+5*4);

+			$data .= pack('n', $numberOfGlyphs);

+			foreach($this->subsettedGlyphs as $id)

+			{

+				$name = $this->glyphs[$id]['name'];

+				if(is_string($name))

+				{

+					$data .= pack('n', 258+$numNames);

+					$names .= chr(strlen($name)).$name;

+					$numNames++;

+				}

+				else

+					$data .= pack('n', $name);

+			}

+			$data .= $names;

+		}

+		else

+		{

+			// Version 3.0

+			$this->Skip(4);

+			$data = "\x00\x03\x00\x00";

+			$data .= $this->Read(4+2*2+5*4);

+		}

+		$this->SetTable('post', $data);

+	}

+

+	function BuildFont()

+	{

+		$tags = array();

+		foreach(array('cmap', 'cvt ', 'fpgm', 'glyf', 'head', 'hhea', 'hmtx', 'loca', 'maxp', 'name', 'post', 'prep') as $tag)

+		{

+			if(isset($this->tables[$tag]))

+				$tags[] = $tag;

+		}

+		$numTables = count($tags);

+		$offset = 12 + 16*$numTables;

+		foreach($tags as $tag)

+		{

+			if(!isset($this->tables[$tag]['data']))

+				$this->LoadTable($tag);

+			$this->tables[$tag]['offset'] = $offset;

+			$offset += strlen($this->tables[$tag]['data']);

+		}

+

+		// Build offset table

+		$entrySelector = 0;

+		$n = $numTables;

+		while($n!=1)

+		{

+			$n = $n>>1;

+			$entrySelector++;

+		}

+		$searchRange = 16*(1<<$entrySelector);

+		$rangeShift = 16*$numTables - $searchRange;

+		$offsetTable = pack('nnnnnn', 1, 0, $numTables, $searchRange, $entrySelector, $rangeShift);

+		foreach($tags as $tag)

+		{

+			$table = $this->tables[$tag];

+			$offsetTable .= $tag.$table['checkSum'].pack('NN', $table['offset'], $table['length']);

+		}

+

+		// Compute checkSumAdjustment (0xB1B0AFBA - font checkSum)

+		$s = $this->CheckSum($offsetTable);

+		foreach($tags as $tag)

+			$s .= $this->tables[$tag]['checkSum'];

+		$a = unpack('n2', $this->CheckSum($s));

+		$high = 0xB1B0 + ($a[1]^0xFFFF);

+		$low = 0xAFBA + ($a[2]^0xFFFF) + 1;

+		$checkSumAdjustment = pack('nn', $high+($low>>16), $low);

+		$this->tables['head']['data'] = substr_replace($this->tables['head']['data'], $checkSumAdjustment, 8, 4);

+

+		$font = $offsetTable;

+		foreach($tags as $tag)

+			$font .= $this->tables[$tag]['data'];

+

+		return $font;

+	}

+

+	function LoadTable($tag)

+	{

+		$this->Seek($tag);

+		$length = $this->tables[$tag]['length'];

+		$n = $length % 4;

+		if($n>0)

+			$length += 4 - $n;

+		$this->tables[$tag]['data'] = $this->Read($length);

+	}

+

+	function SetTable($tag, $data)

+	{

+		$length = strlen($data);

+		$n = $length % 4;

+		if($n>0)

+			$data = str_pad($data, $length+4-$n, "\x00");

+		$this->tables[$tag]['data'] = $data;

+		$this->tables[$tag]['length'] = $length;

+		$this->tables[$tag]['checkSum'] = $this->CheckSum($data);

+	}

+

+	function Seek($tag)

+	{

+		if(!isset($this->tables[$tag]))

+			$this->Error('Table not found: '.$tag);

+		fseek($this->f, $this->tables[$tag]['offset'], SEEK_SET);

+	}

+

+	function Skip($n)

+	{

+		fseek($this->f, $n, SEEK_CUR);

+	}

+

+	function Read($n)

+	{

+		return $n>0 ? fread($this->f, $n) : '';

+	}

+

+	function ReadUShort()

+	{

+		$a = unpack('nn', fread($this->f,2));

+		return $a['n'];

+	}

+

+	function ReadShort()

+	{

+		$a = unpack('nn', fread($this->f,2));

+		$v = $a['n'];

+		if($v>=0x8000)

+			$v -= 65536;

+		return $v;

+	}

+

+	function ReadULong()

+	{

+		$a = unpack('NN', fread($this->f,4));

+		return $a['N'];

+	}

+

+	function CheckSum($s)

+	{

+		$n = strlen($s);

+		$high = 0;

+		$low = 0;

+		for($i=0;$i<$n;$i+=4)

+		{

+			$high += (ord($s[$i])<<8) + ord($s[$i+1]);

+			$low += (ord($s[$i+2])<<8) + ord($s[$i+3]);

+		}

+		return pack('nn', $high+($low>>16), $low);

+	}

+

+	function Error($msg)

+	{

+		throw new Exception($msg);

+	}

+}

+?>

diff --git a/src/lib/qrcodejs/qrcode.min.js b/src/lib/qrcodejs/qrcode.min.js
new file mode 100644
index 0000000..993e88f
--- /dev/null
+++ b/src/lib/qrcodejs/qrcode.min.js
@@ -0,0 +1 @@
+var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c<a.length&&0==a[c];)c++;this.num=new Array(a.length-c+b);for(var d=0;d<a.length-c;d++)this.num[d]=a[d+c]}function j(a,b){this.totalCount=a,this.dataCount=b}function k(){this.buffer=[],this.length=0}function m(){return"undefined"!=typeof CanvasRenderingContext2D}function n(){var a=!1,b=navigator.userAgent;return/android/i.test(b)&&(a=!0,aMat=b.toString().match(/android ([0-9]\.[0-9])/i),aMat&&aMat[1]&&(a=parseFloat(aMat[1]))),a}function r(a,b){for(var c=1,e=s(a),f=0,g=l.length;g>=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d<this.moduleCount;d++){this.modules[d]=new Array(this.moduleCount);for(var e=0;e<this.moduleCount;e++)this.modules[d][e]=null}this.setupPositionProbePattern(0,0),this.setupPositionProbePattern(this.moduleCount-7,0),this.setupPositionProbePattern(0,this.moduleCount-7),this.setupPositionAdjustPattern(),this.setupTimingPattern(),this.setupTypeInfo(a,c),this.typeNumber>=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f<this.modules.length;f++)for(var g=f*e,h=0;h<this.modules[f].length;h++){var i=h*e,j=this.modules[f][h];j&&(d.beginFill(0,100),d.moveTo(i,g),d.lineTo(i+e,g),d.lineTo(i+e,g+e),d.lineTo(i,g+e),d.endFill())}return d},setupTimingPattern:function(){for(var a=8;a<this.moduleCount-8;a++)null==this.modules[a][6]&&(this.modules[a][6]=0==a%2);for(var b=8;b<this.moduleCount-8;b++)null==this.modules[6][b]&&(this.modules[6][b]=0==b%2)},setupPositionAdjustPattern:function(){for(var a=f.getPatternPosition(this.typeNumber),b=0;b<a.length;b++)for(var c=0;c<a.length;c++){var d=a[b],e=a[c];if(null==this.modules[d][e])for(var g=-2;2>=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g<a.length&&(j=1==(1&a[g]>>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h<d.length;h++){var i=d[h];g.put(i.mode,4),g.put(i.getLength(),f.getLengthInBits(i.mode,a)),i.write(g)}for(var l=0,h=0;h<e.length;h++)l+=e[h].dataCount;if(g.getLengthInBits()>8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j<b.length;j++){var k=b[j].dataCount,l=b[j].totalCount-k;d=Math.max(d,k),e=Math.max(e,l),g[j]=new Array(k);for(var m=0;m<g[j].length;m++)g[j][m]=255&a.buffer[m+c];c+=k;var n=f.getErrorCorrectPolynomial(l),o=new i(g[j],n.getLength()-1),p=o.mod(n);h[j]=new Array(n.getLength()-1);for(var m=0;m<h[j].length;m++){var q=m+p.getLength()-h[j].length;h[j][m]=q>=0?p.get(q):0}}for(var r=0,m=0;m<b.length;m++)r+=b[m].totalCount;for(var s=new Array(r),t=0,m=0;d>m;m++)for(var j=0;j<b.length;j++)m<g[j].length&&(s[t++]=g[j][m]);for(var m=0;e>m;m++)for(var j=0;j<b.length;j++)m<h[j].length&&(s[t++]=h[j][m]);return s};for(var c={MODE_NUMBER:1,MODE_ALPHA_NUM:2,MODE_8BIT_BYTE:4,MODE_KANJI:8},d={L:1,M:0,Q:3,H:2},e={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7},f={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:1335,G18:7973,G15_MASK:21522,getBCHTypeInfo:function(a){for(var b=a<<10;f.getBCHDigit(b)-f.getBCHDigit(f.G15)>=0;)b^=f.G15<<f.getBCHDigit(b)-f.getBCHDigit(f.G15);return(a<<10|b)^f.G15_MASK},getBCHTypeNumber:function(a){for(var b=a<<12;f.getBCHDigit(b)-f.getBCHDigit(f.G18)>=0;)b^=f.G18<<f.getBCHDigit(b)-f.getBCHDigit(f.G18);return a<<12|b},getBCHDigit:function(a){for(var b=0;0!=a;)b++,a>>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<<h;for(var h=8;256>h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;c<this.getLength();c++)for(var d=0;d<a.getLength();d++)b[c+d]^=g.gexp(g.glog(this.get(c))+g.glog(a.get(d)));return new i(b,0)},mod:function(a){if(this.getLength()-a.getLength()<0)return this;for(var b=g.glog(this.get(0))-g.glog(a.get(0)),c=new Array(this.getLength()),d=0;d<this.getLength();d++)c[d]=this.get(d);for(var d=0;d<a.getLength();d++)c[d]^=g.gexp(g.glog(a.get(d))+b);return new i(c,0).mod(a)}},j.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]],j.getRSBlocks=function(a,b){var c=j.getRsBlockTable(a,b);if(void 0==c)throw new Error("bad rs block @ typeNumber:"+a+"/errorCorrectLevel:"+b);for(var d=c.length/3,e=[],f=0;d>f;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=['<table style="border:0;border-collapse:collapse;">'],h=0;d>h;h++){g.push("<tr>");for(var i=0;d>i;i++)g.push('<td style="border:0;border-collapse:collapse;padding:0;margin:0;width:'+e+"px;height:"+f+"px;background-color:"+(a.isDark(h,i)?b.colorDark:b.colorLight)+';"></td>');g.push("</tr>")}g.push("</table>"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}();
\ No newline at end of file
diff --git a/src/logout.php b/src/logout.php
new file mode 100644
index 0000000..cc13d12
--- /dev/null
+++ b/src/logout.php
@@ -0,0 +1,5 @@
+<?php
+require_once("core.php");
+
+security::logout();
+security::go("index.php?msg=logout");
diff --git a/src/logs.php b/src/logs.php
new file mode 100644
index 0000000..ddd74d9
--- /dev/null
+++ b/src/logs.php
@@ -0,0 +1,108 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+$mdHeaderRowBefore = visual::backBtn("settings.php");
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+
+  <style>
+  td .material-icons {
+    vertical-align: middle;
+  }
+  </style>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Logs</h2>
+          <?php
+          $page = (isset($_GET["page"]) ? (int)$_GET["page"] - 1 : null);
+
+          $logs = registry::getLogs($page);
+          if (count($logs)) {
+            ?>
+            <div class="overflow-wrapper overflow-wrapper--for-table">
+              <table class="mdl-data-table mdl-js-data-table mdl-shadow--2dp">
+                <thead>
+                  <tr>
+                    <th class="extra">ID</th>
+                    <th class="mdl-data-table__cell--non-numeric">Hora de ejecución</th>
+                    <th class="mdl-data-table__cell--non-numeric">Día registrado</th>
+                    <th class="mdl-data-table__cell--non-numeric">Ejecutado por</th>
+                    <th class="mdl-data-table__cell--non-numeric"></th>
+                    <th class="mdl-data-table__cell--non-numeric"></th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <?php
+                  $tooltips = "";
+                  foreach ($logs as $l) {
+                    ?>
+                    <tr>
+                      <td class="extra"><?=(int)$l["id"]?></td>
+                      <td class="mdl-data-table__cell--non-numeric"><?=security::htmlsafe(date("d/m/Y H:i", $l["realtime"]))?></td>
+                      <td class="mdl-data-table__cell--non-numeric"><?=security::htmlsafe(date("d/m/Y", $l["day"]))?></td>
+                      <td class="mdl-data-table__cell--non-numeric"><?=($l["executedby"] == -1 ? "<span style='font-family: monospace;'>cron</span>" : security::htmlsafe(people::userData("name", $l["executedby"])))?></td>
+                      <td class="mdl-data-table__cell--non-numeric">
+                        <?php
+                        if ($l["warningpos"] > 0) {
+                          $tooltips .= '<div class="mdl-tooltip" for="warning_'.(int)$l["id"].'">El log contiene mensajes de advertencia</div>';
+                          ?>
+                          <i class="material-icons mdl-color-text--orange help" id="warning_<?=(int)$l["id"]?>">warning</i>
+                          <?php
+                        }
+
+                        if ($l["errorpos"] > 0) {
+                          $tooltips .= '<div class="mdl-tooltip" for="error_'.(int)$l["id"].'">El log contiene mensajes de error</div>';
+                          ?>
+                          <i class="material-icons mdl-color-text--red help" id="error_<?=(int)$l["id"]?>">error</i>
+                          <?php
+                        }
+
+                        if ($l["fatalerrorpos"] > 0) {
+                          $tooltips .= '<div class="mdl-tooltip" for="fatalerror_'.(int)$l["id"].'">El log contiene errores fatales</div>';
+                          ?>
+                          <i class="material-icons mdl-color-text--red help-900" id="fatalerror_<?=(int)$l["id"]?>">error</i>
+                          <?php
+                        }
+                        ?>
+                      </td>
+                      <td class="mdl-data-table__cell--non-numeric">
+                        <a href="dynamic/log.php?id=<?=(int)$l["id"]?>" data-dyndialog-href="dynamic/log.php?id=<?=(int)$l["id"]?>" title="Ver el log"><i class='material-icons icon'>notes</i></a>
+                      </td>
+                    </tr>
+                    <?php
+                  }
+                  ?>
+                </tbody>
+              </table>
+            </div>
+            <?php
+            echo $tooltips;
+          } else {
+            ?>
+            <p>No existe ningún log todavía.</p>
+            <?php
+          }
+          ?>
+
+          <?php
+          $numLogs = db::numRows("logs");
+          visual::renderPagination($numLogs, "logs.php", registry::LOGS_PAGINATION_LIMIT);
+          visual::printDebug("registry::getLogs(".(int)$page.")", $logs);
+          ?>
+        </div>
+      </div>
+    </main>
+  </div>
+</body>
+</html>
diff --git a/src/manuallygenerateregistry.php b/src/manuallygenerateregistry.php
new file mode 100644
index 0000000..fb6d1bb
--- /dev/null
+++ b/src/manuallygenerateregistry.php
@@ -0,0 +1,139 @@
+<?php
+require_once("core.php");
+security::checkType(security::HYPERADMIN);
+
+$companies = companies::getAll();
+
+$advancedMode = (isset($_GET["advanced"]) && $_GET["advanced"] == "1");
+if ($advancedMode) $conf["backgroundColor"] = "red-200";
+
+$mdHeaderRowBefore = visual::backBtn("powertools.php");
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+  <style>
+  .advanced-mode {
+    border: dotted 3px red;
+    padding: 13px 13px 29px 13px;
+  }
+  </style>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp<?=($advancedMode ? " advanced-mode" : "")?>">
+          <div class="actions">
+            <?php
+            if ($advancedMode) {
+              ?>
+              <a href="manuallygenerateregistry.php" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect">Desactivar modo avanzado</a>
+              <?php
+            } else {
+              ?>
+              <a href="manuallygenerateregistry.php?advanced=1" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect">Activar modo avanzado</a>
+              <?php
+            }
+            ?>
+          </div>
+
+          <h2>Generar registros manualmente</h2>
+          <form action="domanuallygenerateregistry.php" method="POST">
+            <?php
+            if ($advancedMode) {
+              ?>
+              <input type="hidden" name="advanced" value="1">
+              <p><span style="font-weight: bold; color: red;">ATENCIÓN:</span> al generar los registros en un intervalo de días, hay que hacer los siguientes pasos adicionales para asegurarse que estos se generan correctamente:</p>
+              <ol>
+                <li>Antes de hacer clic en el botón <b>Generar</b>, hay que asegurarse que <b>las fechas de inicio y fin sean correctas</b>. Si no, se podrían generar registros en muchos días no deseados, y el resultado puede ser muy costoso de revertir.</li>
+                <li>Al acabarse de generar los registros, hay que ir al apartado <b>Logs</b> de la configuración y asegurarse que los logs correspondientes a los días generados <b>no tengan ningún icono de error al lado en el listado</b>. Si ha habido un error, cabe la posibilidad de que los registros no se hayan generado correctamente y hace falta comprobar si se han generado o no todos. (Para más información, léase <a href="https://avm99963.github.io/hores-external/administradores/registros/#ver-logs" target="_blank" rel="noopener noreferrer">este artículo de ayuda</a>)</li>
+              </ol>
+              <p>Teniendo en cuenta esto, selecciona los trabajadores y el periodo de días para el cual quieres generar los registros:</p>
+              <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+                <input class="mdl-textfield__input" type="date" name="begins" id="begins" autocomplete="off" data-required>
+                <label class="mdl-textfield__label always-focused" for="begins">Día inicio</label>
+              </div>
+              <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+                <input class="mdl-textfield__input" type="date" name="ends" id="ends" autocomplete="off" data-required>
+                <label class="mdl-textfield__label always-focused" for="ends">Día fin</label>
+              </div>
+              <?php
+            } else {
+              ?>
+              <p>Selecciona los trabajadores y el día para el cual quieres generar los registros:</p>
+              <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+                <input class="mdl-textfield__input" type="date" name="day" id="day" autocomplete="off" data-required>
+                <label class="mdl-textfield__label always-focused" for="day">Día</label>
+              </div>
+              <?php
+            }
+            ?>
+
+            <div class="overflow-wrapper overflow-wrapper--for-table">
+              <table class="mdl-data-table mdl-js-data-table mdl-data-table--selectable mdl-shadow--2dp">
+                <thead>
+                  <tr>
+                    <?php
+                    if ($conf["debug"]) {
+                      ?>
+                      <th class="extra">ID</th>
+                      <?php
+                    }
+                    ?>
+                    <th class="mdl-data-table__cell--non-numeric">Nombre</th>
+                    <th class="mdl-data-table__cell--non-numeric">Empresa</th>
+                    <th class="mdl-data-table__cell--non-numeric extra">Categoría</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <?php
+                  $workers = people::getAll(false, true);
+                  foreach ($workers as $w) {
+                    ?>
+                    <tr data-worker-id="<?=(int)$w["workerid"]?>">
+                      <?php
+                      if ($conf["debug"]) {
+                        ?>
+                        <td class="extra"><?=(int)$w["workerid"]?></td>
+                        <?php
+                      }
+                      ?>
+                      <td class="mdl-data-table__cell--non-numeric"><?=security::htmlsafe($w["name"])?></td>
+                      <td class="mdl-data-table__cell--non-numeric"><?=security::htmlsafe($companies[$w["companyid"]])?></td>
+                      <td class="mdl-data-table__cell--non-numeric extra"><?=security::htmlsafe($w["category"])?></td>
+                    </tr>
+                    <?php
+                  }
+                  ?>
+                </tbody>
+              </table>
+            </div>
+            <br>
+            <button class="mdl-button mdl-js-button mdl-button--raised mdl-button--accent mdl-js-ripple-effect">Generar</button>
+          </form>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <?php
+  visual::smartSnackbar([
+    ["empty", "Faltan datos por introducir en el formulario."],
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."],
+    ["done", "Se ha ejecutado la acción. Accede al apartado de logs para ver si se han generado los registros correctamente o no."],
+    ["generatederr", "Ha ocurrido un error y no se ha podido guardar el log con la información de lo que ha hecho el programa."]
+  ]);
+
+  $logId = (int)($_GET["logId"] ?? 0);
+  if ($logId > 0) {
+    echo "<script>dynDialog.load('dynamic/log.php?id=".$logId."')</script>";
+  }
+  ?>
+  <script src="js/invalidatebulkrecords.js"></script>
+</body>
+</html>
diff --git a/src/node_modules b/src/node_modules
new file mode 120000
index 0000000..68a084a
--- /dev/null
+++ b/src/node_modules
@@ -0,0 +1 @@
+../node_modules
\ No newline at end of file
diff --git a/src/pendingvalidations.php b/src/pendingvalidations.php
new file mode 100644
index 0000000..8757384
--- /dev/null
+++ b/src/pendingvalidations.php
@@ -0,0 +1,91 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+$mdHeaderRowBefore = visual::backBtn("powertools.php");
+
+$gracePeriod = (int)($_GET["gracePeriod"] ?? validations::reminderGracePeriod());
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Validaciones pendientes</h2>
+          <form action="pendingvalidations.php" method="GET">
+            <p>Contar elementos que lleven más de <input type="text" name="gracePeriod" value="<?=(int)$gracePeriod?>" size="2"> días pendientes de validar. <button class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent">Generar tabla</button></p>
+          </form>
+          <?php
+          $pending = validations::getPeopleWithPendingValidations($gracePeriod);
+          usort($pending, function($a, $b) {
+            $n =& $a["numPending"];
+            $m =& $b["numPending"];
+            return ($n > $m ? -1 : ($n < $m ? 1 : 0));
+          });
+
+          if (count($pending)) {
+            ?>
+            <div class="overflow-wrapper overflow-wrapper--for-table">
+              <table class="mdl-data-table mdl-js-data-table mdl-shadow--2dp">
+                <thead>
+                  <tr>
+                    <?php
+                    if ($conf["debug"]) {
+                      ?>
+                      <th class="extra">ID</th>
+                      <?php
+                    }
+                    ?>
+                    <th class="mdl-data-table__cell--non-numeric">Nombre</th>
+                    <th class="mdl-data-table__cell--non-numeric">Validaciones pendientes</th>
+                    <th class="mdl-data-table__cell--non-numeric"></th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <?php
+                  foreach ($pending as $p) {
+                    ?>
+                    <tr>
+                      <?php
+                      if ($conf["debug"]) {
+                        ?>
+                        <td class="extra"><?=(int)$p["person"]["id"]?></td>
+                        <?php
+                      }
+                      ?>
+                      <td class="mdl-data-table__cell--non-numeric"><?=security::htmlsafe($p["person"]["name"])?></td>
+                      <td class="mdl-data-table__cell--non-numeric"><?=(int)$p["numPending"]?></td>
+                      <td class='mdl-data-table__cell--non-numeric'>
+                        <a href='userincidents.php?id=<?=(int)$p["person"]["id"]?>' title='Ver y gestionar las incidencias del trabajador'><i class='material-icons icon'>assignment_late</i></a>
+                        <a href='userregistry.php?id=<?=(int)$p["person"]["id"]?>' title='Ver y gestionar los registros del trabajador'><i class='material-icons icon'>list</i></a>
+                      </td>
+                    </tr>
+                    <?php
+                  }
+                  ?>
+                </tbody>
+              </table>
+            </div>
+            <?php
+          } else {
+            ?>
+            <p>Todavía no existe ninguna persona.</p>
+            <?php
+          }
+          ?>
+
+          <?php visual::printDebug("validations::getPeopleWithPendingValidations(".$gracePeriod.")", $pending); ?>
+        </div>
+      </div>
+    </main>
+  </div>
+</body>
+</html>
diff --git a/src/powertools.php b/src/powertools.php
new file mode 100644
index 0000000..f099aba
--- /dev/null
+++ b/src/powertools.php
@@ -0,0 +1,71 @@
+<?php
+require_once("core.php");
+security::checkType(security::HYPERADMIN);
+
+$mdHeaderRowBefore = visual::backBtn("settings.php");
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Herramientas avanzadas</h2>
+          <a class="clicky-container" href="backupdb.php">
+            <div class="clicky mdl-js-ripple-effect">
+              <div class="text">
+                <span class="title">Exportar base de datos</span><br>
+                <span class="description">Genera un archivo con sentencias SQL para hacer una copia de seguridad de la base de datos.</span>
+              </div>
+              <div class="mdl-ripple"></div>
+            </div>
+          </a>
+          <a class="clicky-container" href="sendbulkpasswords.php">
+            <div class="clicky mdl-js-ripple-effect">
+              <div class="text">
+                <span class="title">Enviar enlaces para generar contraseña</span><br>
+                <span class="description">Envía en masa a los trabajadores que desees un enlace vía correo electrónico para que establezcan una contraseña y puedan entrar en el aplicativo.</span>
+              </div>
+              <div class="mdl-ripple"></div>
+            </div>
+          </a>
+          <a class="clicky-container" href="invalidatebulkrecords.php">
+            <div class="clicky mdl-js-ripple-effect">
+              <div class="text">
+                <span class="title">Invalidar registros en masa</span><br>
+                <span class="description">Invalida los registros de ciertos trabajadores en un periodo de tiempo concreto.</span>
+              </div>
+              <div class="mdl-ripple"></div>
+            </div>
+          </a>
+          <a class="clicky-container" href="manuallygenerateregistry.php">
+            <div class="clicky mdl-js-ripple-effect">
+              <div class="text">
+                <span class="title">Generar registros manualmente</span><br>
+                <span class="description">Ejecuta el programa que genera registros a partir de los horarios configurados en un día concreto y para un subconjunto de trabajadores.</span>
+              </div>
+              <div class="mdl-ripple"></div>
+            </div>
+          </a>
+          <a class="clicky-container" href="pendingvalidations.php">
+            <div class="clicky mdl-js-ripple-effect">
+              <div class="text">
+                <span class="title">Validaciones pendientes</span><br>
+                <span class="description">Ver un listado de personas que muestra el número de validaciones pendientes que tiene cada una.</span>
+              </div>
+              <div class="mdl-ripple"></div>
+            </div>
+          </a>
+        </div>
+      </div>
+    </main>
+  </div>
+</body>
+</html>
diff --git a/src/recovery.php b/src/recovery.php
new file mode 100644
index 0000000..34a41df
--- /dev/null
+++ b/src/recovery.php
@@ -0,0 +1,44 @@
+<?php
+require_once("core.php");
+
+if (!security::checkParams("GET", [
+  ["token", security::PARAM_NEMPTY]
+])) {
+  security::go("index.php?msg=unexpected");
+}
+
+$token = $_GET["token"];
+
+$recovery = recovery::getUnusedRecovery($token);
+if ($recovery === false) security::go("index.php?msg=recovery2failed");
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/index.css">
+  <script src="js/index.js"></script>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="login mdl-shadow--4dp">
+    <h2>Restablecer contraseña</h2>
+    <form action="dorecovery.php" method="POST" autocomplete="off" id="formulario">
+      <input type="hidden" name="token" value="<?=security::htmlsafe($recovery["token"])?>">
+      <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+        <input class="mdl-textfield__input" type="password" name="password" id="password" autocomplete="off" data-required>
+        <label class="mdl-textfield__label" for="password">Contraseña</label>
+      </div>
+      <p class="mdl-color-text--grey-600"><?=security::htmlsafe(security::$passwordHelperText)?></p>
+
+      <p><button type="submit" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent">Restablecer</button></p>
+		</form>
+  </div>
+
+  <?php
+  visual::smartSnackbar([
+    ["weakpassword", security::$passwordHelperText]
+  ]);
+  ?>
+</body>
+</html>
diff --git a/src/registry.php b/src/registry.php
new file mode 100644
index 0000000..de1c123
--- /dev/null
+++ b/src/registry.php
@@ -0,0 +1,74 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+$companies = companies::getAll();
+$showInvalidated = (isset($_GET["showinvalidated"]) && $_GET["showinvalidated"] == 1);
+$numRows = registry::numRows($showInvalidated);
+$page = (isset($_GET["page"]) ? (int)$_GET["page"] - 1 : null);
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+  <link rel="stylesheet" href="css/incidents.css">
+
+  <style>
+  @media (max-width: 655px) {
+    .extra {
+      display: none;
+    }
+  }
+  </style>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <form action="registry.php" method="GET" id="show-invalidated-form" class="actions" style="padding-right: 32px;">
+            <input type="hidden" name="page" value="<?=(int)($_GET["page"] ?? 1)?>">
+            <label class="mdl-switch mdl-js-switch mdl-js-ripple-effect" for="showinvalidated">
+              <input type="checkbox" id="showinvalidated" name="showinvalidated" value="1" class="mdl-switch__input"<?=($showInvalidated ? " checked" : "")?>>
+              <span class="mdl-switch__label mdl-color-text--grey-700">Mostrar registros invalidados</span>
+            </label>
+          </form>
+
+          <h2>Registro</h2>
+
+          <p>Total: <?=(int)$numRows?> registros</p>
+
+          <?php
+          $registry = registry::getRecords(false, false, false, $showInvalidated, true, true, $page);
+          if (count($registry)) {
+            registryView::renderRegistry($registry, $companies);
+          } else {
+            echo "El registro está vacío.";
+          }
+          ?>
+
+          <?php
+          visual::renderPagination($numRows, "registry.php?".($showInvalidated ? "showinvalidated=1" : ""), incidents::PAGINATION_LIMIT, false, true);
+          visual::printDebug("registry::getRecords(false, false, false, ".($showInvalidated ? "true" : "false").", true, true, ".(int)$page.")", $registry);
+          ?>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <?php
+  visual::renderTooltips();
+
+  visual::smartSnackbar([
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."],
+    ["invalidated", "Se ha invalidado el registro correctamente."]
+  ]);
+  ?>
+
+  <script src="js/incidentsgeneric.js"></script>
+  <script src="js/registry.js"></script>
+</body>
+</html>
diff --git a/src/schedule.php b/src/schedule.php
new file mode 100644
index 0000000..1839415
--- /dev/null
+++ b/src/schedule.php
@@ -0,0 +1,244 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER);
+security::checkWorkerUIEnabled();
+
+$isAdmin = security::isAllowed(security::ADMIN);
+
+if (!security::checkParams("GET", [
+  ["id", security::PARAM_ISINT]
+])) {
+  security::go((security::isAdminView() ? "workers.php" : "userschedule.php?id=".(int)$_SESSION["id"]));
+}
+
+$id = (int)$_GET["id"];
+
+$schedule = schedules::get($id);
+
+if ($schedule === false) {
+  security::go((security::isAdminView() ? "workers.php" : "userschedule.php?id=".(int)$_SESSION["id"]));
+}
+
+$worker = workers::get($schedule["worker"]);
+
+if ($worker === false) {
+  security::go((security::isAdminView() ? "workers.php" : "userschedule.php?id=".(int)$_SESSION["id"])."?msg=unexpected");
+}
+
+if (!$isAdmin && people::userData("id") != $worker["person"]) {
+  security::notFound();
+}
+
+$plaintext = isset($_GET["plaintext"]) && $_GET["plaintext"] == "1";
+
+$mdHeaderRowBefore = visual::backBtn((security::isAdminView() ? "userschedule.php?id=".$worker["person"] : "userschedule.php?id=".(int)$_SESSION["id"]));
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+  <link rel="stylesheet" href="css/schedule.css">
+
+  <?php
+  if (security::isAdminView()) {
+    ?>
+    <style>
+    .addday {
+      position: fixed;
+      bottom: 16px;
+      right: 16px;
+      z-index: 1000;
+    }
+    </style>
+    <?php
+  }
+  ?>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php
+    visual::includeNav();
+
+    if (security::isAdminView()) {
+      ?>
+      <button class="addday mdl-button md-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--accent"><i class="material-icons">add</i><span class="mdl-ripple"></span></button>
+      <?php
+    }
+    ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <div class="actions">
+            <?php
+            if (security::isAdminView()) {
+              ?>
+              <form action="doactiveschedule.php" method="POST" style="display: inline-block;">
+                <input type="hidden" name="id" value="<?=(int)$schedule["id"]?>">
+                <input type="hidden" name="value" value="<?=((int)$schedule["active"] + 1)%2?>">
+                <button class="mdl-button mdl-js-button mdl-button--raised mdl-button--accent mdl-js-ripple-effect" ><?=($schedule["active"] == 0 ? "Activar" : "Desactivar")?></button>
+              </form>
+              <?php
+            }
+            ?>
+            <button id="menu" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect"><i class="material-icons">more_vert</i></button>
+          </div>
+
+          <ul class="mdl-menu mdl-menu--bottom-right mdl-js-menu mdl-js-ripple-effect" for="menu">
+            <?php
+            if (security::isAdminView()) {
+              ?>
+              <a data-dyndialog-href="dynamic/editschedule.php?id=<?=(int)$schedule["id"]?>" href="dynamic/editschedule.php?id=<?=(int)$schedule["id"]?>"><li class="mdl-menu__item">Editar detalles</li></a>
+              <a data-dyndialog-href="dynamic/deleteschedule.php?id=<?=(int)$schedule["id"]?>" href="dynamic/deleteschedule.php?id=<?=(int)$schedule["id"]?>"><li class="mdl-menu__item">Eliminar</li></a>
+              <?php
+            }
+            ?>
+            <a href="schedule.php?id=<?=(int)$schedule["id"]?>&plaintext=<?=($plaintext ? "0" : "1")?>"><li class="mdl-menu__item"><?=($plaintext ? "Versión enriquecida" : "Versión en texto plano")?></li></a>
+          </ul>
+
+          <h2>Horario semanal</h2>
+
+          <p>
+            <b>Trabajador:</b> <?=security::htmlsafe($worker["name"])?> (<?=security::htmlsafe($worker["companyname"])?>)<br>
+            <b>Validez:</b> del <?=security::htmlsafe(date("d/m/Y", $schedule["begins"]))?> al <?=security::htmlsafe(date("d/m/Y", $schedule["ends"]))?><br>
+            <b>Activo:</b> <?=($schedule["active"] == 1 ? visual::YES : visual::NO)?>
+          </p>
+
+          <?php
+          if ($plaintext) {
+            echo "<hr>";
+
+            $flag = false;
+            foreach ($schedule["days"] as $typeday) {
+              if (security::isAdminView()) {
+                schedulesView::renderPlainSchedule($typeday, true, function($day) {
+                  return "dynamic/editday.php?id=".(int)$day["id"];
+                }, function ($day) {
+                  return "dynamic/deleteday.php?id=".(int)$day["id"];
+                }, $flag);
+              } else {
+                schedulesView::renderPlainSchedule($typeday, false, null, null, $flag);
+              }
+            }
+
+            if (!$flag) {
+              echo "<p>Este horario todavía no está configurado.</p>".(security::isAdminView() ? "<p>Haz clic en el botón de la parte inferior derecha para empezar a rellenar los horarios diarios.</p>" : "");
+            }
+          } else {
+            foreach (calendars::$types as $tdid => $type) {
+              if ($tdid == calendars::TYPE_FESTIU) continue;
+
+              $tdisset = isset($schedule["days"][$tdid]);
+
+              echo "<h4>".security::htmlsafe(calendars::$types[$tdid])."</h4>";
+
+              if ($tdisset) {
+                if (security::isAdminView()) {
+                  schedulesView::renderSchedule($schedule["days"][$tdid], true, function($day) {
+                    return "dynamic/editday.php?id=".(int)$day["id"];
+                  }, function ($day) {
+                    return "dynamic/deleteday.php?id=".(int)$day["id"];
+                  });
+                } else {
+                  schedulesView::renderSchedule($schedule["days"][$tdid], false, null, null);
+                }
+              } else {
+                echo "<p>Todavía no hay configurado ningún horario en días del tipo \"".security::htmlsafe(calendars::$types[$tdid])."\".</p>";
+              }
+            }
+          }
+          ?>
+
+          <?php visual::printDebug("schedules::get(".(int)$schedule["id"].")", $schedule); ?>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <?php
+    if (security::isAdminView()) {
+      ?>
+    <dialog class="mdl-dialog" id="addday">
+      <form action="doadddayschedule.php" method="POST" autocomplete="off">
+        <input type="hidden" name="id" value="<?=(int)$schedule["id"]?>">
+        <h4 class="mdl-dialog__title">Añade un nuevo horario</h4>
+        <div class="mdl-dialog__content">
+          <h5>Día</h5>
+          <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+            <div id="dayMenu" class="mdlext-selectfield__select mdl-custom-selectfield__select" tabindex="0">-</div>
+            <ul class="mdl-menu mdl-menu--bottom mdl-js-menu mdl-custom-multiselect mdl-custom-multiselect-js" for="dayMenu">
+              <?php
+              foreach (calendars::$days as $id => $day) {
+                ?>
+                <li class="mdl-menu__item mdl-custom-multiselect__item">
+                  <label class="mdl-checkbox mdl-js-checkbox" for="day-<?=(int)$id?>">
+                    <input type="checkbox" id="day-<?=(int)$id?>" name="day[]" value="<?=(int)$id?>" data-value="<?=(int)$id?>" class="mdl-checkbox__input">
+                    <span class="mdl-checkbox__label"><?=security::htmlsafe($day)?></span>
+                  </label>
+                </li>
+                <?php
+              }
+              ?>
+            </ul>
+            <label for="day" class="mdlext-selectfield__label always-focused mdl-color-text--primary">Día de la semana</label>
+          </div>
+          <br>
+          <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+            <div id="dayType" class="mdlext-selectfield__select mdl-custom-selectfield__select" tabindex="0">-</div>
+            <ul class="mdl-menu mdl-menu--bottom mdl-js-menu mdl-custom-multiselect mdl-custom-multiselect-js" for="dayType">
+              <?php
+              foreach (calendars::$types as $id => $type) {
+                if ($id == calendars::TYPE_FESTIU) continue;
+                ?>
+                <li class="mdl-menu__item mdl-custom-multiselect__item">
+                  <label class="mdl-checkbox mdl-js-checkbox" for="type-<?=(int)$id?>">
+                    <input type="checkbox" id="type-<?=(int)$id?>" name="type[]" value="<?=(int)$id?>" data-value="<?=(int)$id?>" class="mdl-checkbox__input">
+                    <span class="mdl-checkbox__label"><?=security::htmlsafe($type)?></span>
+                  </label>
+                </li>
+                <?php
+              }
+              ?>
+            </ul>
+            <label for="day" class="mdlext-selectfield__label always-focused mdl-color-text--primary">Tipo de día</label>
+          </div>
+
+          <h5>Jornada laboral</h5>
+          <p>De <input type="time" name="beginswork" required> a <input type="time" name="endswork" required></p>
+
+          <h5>Desayuno</h5>
+          <p>De <input type="time" name="beginsbreakfast"> a <input type="time" name="endsbreakfast"></p>
+
+          <h5>Comida</h5>
+          <p>De <input type="time" name="beginslunch"> a <input type="time" name="endslunch"></p>
+        </div>
+        <div class="mdl-dialog__actions">
+          <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--primary">Crear</button>
+          <button onclick="event.preventDefault(); document.querySelector('#addday').close();" class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+        </div>
+      </form>
+    </dialog>
+
+    <script src="js/schedule.js"></script>
+    <?php
+  }
+
+  visual::smartSnackbar([
+    ["added", "Se ha añadido el horario correctamente."],
+    ["modified", "Se ha modificado el horario correctamente."],
+    ["deleted", "Se ha eliminado el horario diario correctamente."],
+    ["empty", "Faltan datos por introducir en el formulario."],
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."],
+    ["errorcheck1", "La hora de inicio debe ser anterior a la hora de fin."],
+    ["errorcheck2", "El desayuno y comida deben estar dentro del horario de trabajo."],
+    ["errorcheck3", "El desayuno y comida no se pueden solapar."],
+    ["errorcheck4", "El horario de trabajo no puede ser nulo."],
+    ["existing", "Algunos horarios que has intentado introducir ya existían. Estos no se han añadido."],
+    ["activeswitched0", "Horario desactivado correctamente."],
+    ["activeswitched1", "Horario activado correctamente."],
+    ["order", "La fecha de inicio debe ser anterior a la fecha de fin."]
+  ]);
+  ?>
+</body>
+</html>
diff --git a/src/scheduletemplate.php b/src/scheduletemplate.php
new file mode 100644
index 0000000..9a1f337
--- /dev/null
+++ b/src/scheduletemplate.php
@@ -0,0 +1,183 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+if (!security::checkParams("GET", [
+  ["id", security::PARAM_ISINT]
+])) {
+  security::go("scheduletemplates.php");
+}
+
+$id = (int)$_GET["id"];
+
+$template = schedules::getTemplate($id);
+
+if ($template === false) {
+  security::go("scheduletemplates.php");
+}
+
+$plaintext = isset($_GET["plaintext"]) && $_GET["plaintext"] == "1";
+
+$mdHeaderRowBefore = visual::backBtn("scheduletemplates.php");
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+  <link rel="stylesheet" href="css/schedule.css">
+
+  <style>
+  .addday {
+    position: fixed;
+    bottom: 16px;
+    right: 16px;
+    z-index: 1000;
+  }
+  </style>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <button class="addday mdl-button md-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--accent"><i class="material-icons">add</i><span class="mdl-ripple"></span></button>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <div class="actions">
+            <button id="menu" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect"><i class="material-icons">more_vert</i></button>
+          </div>
+
+          <ul class="mdl-menu mdl-menu--bottom-right mdl-js-menu mdl-js-ripple-effect" for="menu">
+            <a data-dyndialog-href="dynamic/editscheduletemplate.php?id=<?=(int)$template["id"]?>" href="dynamic/editscheduletemplate.php?id=<?=(int)$template["id"]?>"><li class="mdl-menu__item">Editar detalles</li></a>
+            <a data-dyndialog-href="dynamic/deletescheduletemplate.php?id=<?=(int)$template["id"]?>" href="dynamic/deletescheduletemplate.php?id=<?=(int)$template["id"]?>"><li class="mdl-menu__item">Eliminar</li></a>
+            <a href="scheduletemplate.php?id=<?=(int)$template["id"]?>&plaintext=<?=($plaintext ? "0" : "1")?>"><li class="mdl-menu__item"><?=($plaintext ? "Versión enriquecida" : "Versión en texto plano")?></li></a>
+          </ul>
+
+          <h2>Plantilla &ldquo;<?=security::htmlsafe($template["name"])?>&rdquo;</h2>
+
+          <p><b>Validez:</b> del <?=security::htmlsafe(date("d/m/Y", $template["begins"]))?> al <?=security::htmlsafe(date("d/m/Y", $template["ends"]))?></p>
+
+          <?php
+          if ($plaintext) {
+            echo "<hr>";
+
+            $flag = false;
+            foreach ($template["days"] as $typeday) {
+              schedulesView::renderPlainSchedule($typeday, true, function($day) {
+                return "dynamic/edittemplateday.php?id=".(int)$day["id"];
+              }, function ($day) {
+                return "dynamic/deletetemplateday.php?id=".(int)$day["id"];
+              }, $flag);
+            }
+
+            if (!$flag) {
+              echo "<p>Esta plantilla todavía no está configurada.</p><p>Haz clic en el botón de la parte inferior derecha para empezar a rellenar los horarios de la plantilla.</p>";
+            }
+          } else {
+            foreach (calendars::$types as $tdid => $type) {
+              if ($tdid == calendars::TYPE_FESTIU) continue;
+
+              $tdisset = isset($template["days"][$tdid]);
+
+              echo "<h4>".security::htmlsafe(calendars::$types[$tdid])."</h4>";
+
+              if ($tdisset) {
+                schedulesView::renderSchedule($template["days"][$tdid], true, function($day) {
+                  return "dynamic/edittemplateday.php?id=".(int)$day["id"];
+                }, function ($day) {
+                  return "dynamic/deletetemplateday.php?id=".(int)$day["id"];
+                });
+              } else {
+                echo "<p>Todavía no hay configurado ningún horario en días del tipo \"".security::htmlsafe(calendars::$types[$tdid])."\".</p>";
+              }
+            }
+          }
+          ?>
+
+          <?php visual::printDebug("schedules::getTemplate(".(int)$template["id"].")", $template); ?>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <dialog class="mdl-dialog" id="addday">
+    <form action="doadddayscheduletemplate.php" method="POST" autocomplete="off">
+      <input type="hidden" name="id" value="<?=(int)$template["id"]?>">
+      <h4 class="mdl-dialog__title">Añade un nuevo horario</h4>
+      <div class="mdl-dialog__content">
+        <h5>Día</h5>
+        <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+          <div id="dayMenu" class="mdlext-selectfield__select mdl-custom-selectfield__select" tabindex="0">-</div>
+          <ul class="mdl-menu mdl-menu--bottom mdl-js-menu mdl-custom-multiselect mdl-custom-multiselect-js" for="dayMenu">
+            <?php
+            foreach (calendars::$days as $id => $day) {
+              ?>
+              <li class="mdl-menu__item mdl-custom-multiselect__item">
+                <label class="mdl-checkbox mdl-js-checkbox" for="day-<?=(int)$id?>">
+                  <input type="checkbox" id="day-<?=(int)$id?>" name="day[]" value="<?=(int)$id?>" data-value="<?=(int)$id?>" class="mdl-checkbox__input">
+                  <span class="mdl-checkbox__label"><?=security::htmlsafe($day)?></span>
+                </label>
+              </li>
+              <?php
+            }
+            ?>
+          </ul>
+          <label for="day" class="mdlext-selectfield__label always-focused mdl-color-text--primary">Día de la semana</label>
+        </div>
+        <br>
+        <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+          <div id="dayType" class="mdlext-selectfield__select mdl-custom-selectfield__select" tabindex="0">-</div>
+          <ul class="mdl-menu mdl-menu--bottom mdl-js-menu mdl-custom-multiselect mdl-custom-multiselect-js" for="dayType">
+            <?php
+            foreach (calendars::$types as $id => $type) {
+              if ($id == calendars::TYPE_FESTIU) continue;
+              ?>
+              <li class="mdl-menu__item mdl-custom-multiselect__item">
+                <label class="mdl-checkbox mdl-js-checkbox" for="type-<?=(int)$id?>">
+                  <input type="checkbox" id="type-<?=(int)$id?>" name="type[]" value="<?=(int)$id?>" data-value="<?=(int)$id?>" class="mdl-checkbox__input">
+                  <span class="mdl-checkbox__label"><?=security::htmlsafe($type)?></span>
+                </label>
+              </li>
+              <?php
+            }
+            ?>
+          </ul>
+          <label for="day" class="mdlext-selectfield__label always-focused mdl-color-text--primary">Tipo de día</label>
+        </div>
+
+        <h5>Jornada laboral</h5>
+        <p>De <input type="time" name="beginswork" required> a <input type="time" name="endswork" required></p>
+
+        <h5>Desayuno</h5>
+        <p>De <input type="time" name="beginsbreakfast"> a <input type="time" name="endsbreakfast"></p>
+
+        <h5>Comida</h5>
+        <p>De <input type="time" name="beginslunch"> a <input type="time" name="endslunch"></p>
+      </div>
+      <div class="mdl-dialog__actions">
+        <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--primary">Crear</button>
+        <button onclick="event.preventDefault(); document.querySelector('#addday').close();" class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+      </div>
+    </form>
+  </dialog>
+
+  <?php
+  visual::smartSnackbar([
+    ["added", "Se ha añadido el horario correctamente."],
+    ["modified", "Se ha modificado el horario correctamente."],
+    ["deleted", "Se ha eliminado el horario correctamente."],
+    ["empty", "Faltan datos por introducir en el formulario."],
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."],
+    ["errorcheck1", "La hora de inicio debe ser anterior a la hora de fin."],
+    ["errorcheck2", "El desayuno y comida deben estar dentro del horario de trabajo."],
+    ["errorcheck3", "El desayuno y comida no se pueden solapar."],
+    ["errorcheck4", "El horario de trabajo no puede ser nulo."],
+    ["existing", "Algunos horarios que has intentado introducir ya existían. Estos no se han añadido."],
+    ["order", "La fecha de inicio debe ser anterior a la fecha de fin."]
+  ]);
+  ?>
+
+  <script src="js/schedule.js"></script>
+</body>
+</html>
diff --git a/src/scheduletemplates.php b/src/scheduletemplates.php
new file mode 100644
index 0000000..f0265d6
--- /dev/null
+++ b/src/scheduletemplates.php
@@ -0,0 +1,99 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+$mdHeaderRowBefore = visual::backBtn("workers.php");
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+
+  <style>
+  .addtemplate {
+    position: fixed;
+    bottom: 16px;
+    right: 16px;
+    z-index: 1000;
+  }
+  </style>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <button class="addtemplate mdl-button md-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--accent"><i class="material-icons">add</i><span class="mdl-ripple"></span></button>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Plantillas de horarios</h2>
+          <?php
+          $templates = schedules::getTemplates();
+          if (count($templates)) {
+              foreach ($templates as $t) {
+              ?>
+              <a href="scheduletemplate.php?id=<?=(int)$t["id"]?>" class="clicky-container">
+                <div class="clicky mdl-js-ripple-effect">
+                  <div class="text">
+                    <span class="title"><?=security::htmlsafe($t["name"])?></span><br>
+                    <span class="description"><?=security::htmlsafe(date("d/m/Y", $t["begins"]))?> - <?=security::htmlsafe(date("d/m/Y", $t["ends"]))?></span>
+                  </div>
+                  <div class="mdl-ripple"></div>
+                </div>
+              </a>
+              <?php
+            }
+          } else {
+            ?>
+            <p>Todavía no has creado ninguna plantilla.</p>
+            <p>Puedes añadir una haciendo clic en el botón de la esquina inferior derecha de la página.</p>
+            <?php
+          }
+          ?>
+
+          <?php visual::printDebug("schedules::getTemplates()", $templates); ?>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <dialog class="mdl-dialog" id="addtemplate">
+    <form action="doaddscheduletemplate.php" method="POST" autocomplete="off">
+      <h4 class="mdl-dialog__title">Crea una nueva plantilla</h4>
+      <div class="mdl-dialog__content">
+        <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+          <input class="mdl-textfield__input" type="text" name="name" id="name" autocomplete="off" data-required>
+          <label class="mdl-textfield__label" for="name">Nombre de la plantilla</label>
+        </div>
+        <br>
+        <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+          <input class="mdl-textfield__input" type="date" name="begins" id="begins" autocomplete="off" data-required>
+          <label class="mdl-textfield__label always-focused" for="begins">Fecha inicio de validez del horario</label>
+        </div>
+        <br>
+        <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+          <input class="mdl-textfield__input" type="date" name="ends" id="ends" autocomplete="off" data-required>
+          <label class="mdl-textfield__label always-focused" for="ends">Fecha fin de validez del horario</label>
+        </div>
+      </div>
+      <div class="mdl-dialog__actions">
+        <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Crear</button>
+        <button onclick="event.preventDefault(); document.querySelector('#addtemplate').close();" class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+      </div>
+    </form>
+  </dialog>
+
+  <?php
+  visual::smartSnackbar([
+    ["added", "Se ha añadido la plantilla correctamente."],
+    ["deleted", "Se ha eliminado la plantilla correctamente."],
+    ["empty", "Faltan datos por introducir en el formulario."],
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."],
+    ["order", "La fecha de inicio debe ser anterior a la fecha de fin."]
+  ]);
+  ?>
+
+  <script src="js/scheduletemplates.js"></script>
+</body>
+</html>
diff --git a/src/security.php b/src/security.php
new file mode 100644
index 0000000..58275ed
--- /dev/null
+++ b/src/security.php
@@ -0,0 +1,75 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER);
+security::checkWorkerUIEnabled();
+secondFactor::checkAvailability();
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+
+  <style>
+  .highlighted {
+    font-weight: 500;
+  }
+  </style>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Configuración de seguridad</h2>
+          <a class="clicky-container" href="changepassword.php">
+            <div class="clicky mdl-js-ripple-effect">
+              <div class="text">
+                <span class="title">Cambiar contraseña</span><br>
+                <span class="description">Haz clic para cambiar la contraseña de tu usuario.</span>
+              </div>
+              <div class="mdl-ripple"></div>
+            </div>
+          </a>
+
+          <hr>
+
+          <h4>Verificación en dos pasos</h4>
+          <p>La <span class="highlighted">verificación en dos pasos</span> es un sistema que <span class="highlighted">evita que terceros no autorizados puedan iniciar sesión</span> si te roban la contraseña.</p>
+          <?php
+          if (secondFactor::isEnabled()) {
+            ?>
+            <p>Cada vez que inicies sesión, tendrás que <span class="highlighted">introducir un código</span> que se genera automáticamente en tu móvil o <span class="highlighted">una llave de seguridad</span> para verificar que eres tú.</p>
+            <p>La verificación en dos pasos <span class="highlighted">está activada</span>.</p>
+            <p><a href="securitykeys.php" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect"><i class="material-icons">vpn_key</i> <span>Llaves de seguridad</span></a> <a href="dynamic/disablesecondfactor.php?id=<?=(int)$_SESSION["id"]?>" data-dyndialog-href="dynamic/disablesecondfactor.php?id=<?=(int)$_SESSION["id"]?>" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent">Desactivar verificación en 2 pasos</a></p>
+            <?php
+          } else {
+            ?>
+            <p>Si la activas, a parte de introducir tu contraseña, tendrás que <span class="highlighted">introducir un código que se genera automáticamente en tu móvil</span> para verificar que eres tú quien intenta iniciar sesión.</p>
+            <p>A parte, también puedes configurar como segundo factor una <span class="highlighted">llave de seguridad</span> física en vez de la verificación por código.</p>
+            <p>Actualmente la verificación en dos pasos <span class="highlighted">no está activada</span>. Puedes configurarla haciendo clic en el siguiente botón:</p>
+            <p><a href="dynamic/enablesecondfactor.php?id=<?=(int)$_SESSION["id"]?>" data-dyndialog-href="dynamic/enablesecondfactor.php?id=<?=(int)$_SESSION["id"]?>" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent">Empezar</a></p>
+            <?php
+          }
+          ?>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <?php
+  visual::smartSnackbar([
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."],
+    ["empty", "Faltan datos por introducir en el formulario."],
+    ["wrongcode", "El código de verificación introducido no es correcto."],
+    ["enabledsecondfactor", "Se ha activado la verificación en dos pasos correctamente."],
+    ["wrongpassword", "La contraseña introducida no es correcta."],
+    ["disabledsecondfactor", "Se ha desactivado la verificación en dos pasos correctamente."]
+  ]);
+  ?>
+
+  <script src="lib/qrcodejs/qrcode.min.js"></script>
+</body>
+</html>
diff --git a/src/securitykeys.php b/src/securitykeys.php
new file mode 100644
index 0000000..f00625c
--- /dev/null
+++ b/src/securitykeys.php
@@ -0,0 +1,107 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER);
+security::checkWorkerUIEnabled();
+secondFactor::checkAvailability();
+
+$mdHeaderRowBefore = visual::backBtn("security.php");
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+
+  <style>
+  .addsecuritykey {
+    position: fixed;
+    bottom: 16px;
+    right: 16px;
+    z-index: 1000;
+  }
+  </style>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <button class="addsecuritykey mdl-button md-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--accent"><i class="material-icons">add</i><span class="mdl-ripple"></span></button>
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Llaves de seguridad</h2>
+          <?php
+          $securityKeys = secondFactor::getSecurityKeys();
+          if ($securityKeys === false) {
+            echo "<p>Ha ocurrido un error inesperado y no se ha podido obtener un listado de tus llaves de seguridad.</p>";
+          } elseif (count($securityKeys)) {
+            ?>
+            <div class="overflow-wrapper overflow-wrapper--for-table">
+              <table class="mdl-data-table mdl-js-data-table mdl-shadow--2dp">
+                <thead>
+                  <tr>
+                    <th class="mdl-data-table__cell--non-numeric">Nombre</th>
+                    <th class="mdl-data-table__cell--non-numeric">Fecha de registro</th>
+                    <th class="mdl-data-table__cell--non-numeric">Último uso</th>
+                    <th class="mdl-data-table__cell--non-numeric"></th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <?php
+                  foreach ($securityKeys as $s) {
+                    ?>
+                    <tr>
+                      <td class="mdl-data-table__cell--non-numeric"><?=security::htmlsafe($s["name"])?></td>
+                      <td class="mdl-data-table__cell--non-numeric"><?=security::htmlsafe($s["added"] !== null ? date("d/m/Y H:i", $s["added"]) : "-")?></td>
+                      <td class="mdl-data-table__cell--non-numeric"><?=security::htmlsafe($s["lastused"] !== null ? date("d/m/Y H:i", $s["lastused"]) : "-")?></td>
+                      <td class='mdl-data-table__cell--non-numeric'><a href='dynamic/deletesecuritykey.php?id=<?=(int)$s["id"]?>' data-dyndialog-href='dynamic/deletesecuritykey.php?id=<?=(int)$s["id"]?>' title='Eliminar llave de seguridad'><i class='material-icons icon'>delete</i></a></td>
+                    </tr>
+                    <?php
+                  }
+                  ?>
+                </tbody>
+              </table>
+            </div>
+            <?php
+          } else {
+            ?>
+            <p>Todavía no has añadido ninguna llave de seguridad.</p>
+            <p>Puedes añadir una haciendo clic en el botón de la esquina inferior derecha de la página.</p>
+            <?php
+          }
+          ?>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <dialog class="mdl-dialog" id="addsecuritykey">
+    <form method="POST" id="addsecuritykeyform">
+      <h4 class="mdl-dialog__title">Añadir llave de seguridad</h4>
+      <div class="mdl-dialog__content">
+        <p>Introduce un nombre para la llave de seguridad y haz clic en el botón añadir para empezar el proceso de registro:</p>
+        <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+          <input class="mdl-textfield__input" type="text" name="name" id="name" autocomplete="off" data-required>
+          <label class="mdl-textfield__label" for="name">Nombre</label>
+        </div>
+      </div>
+      <div class="mdl-dialog__actions">
+        <button id="registersecuritykey" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Registrar</button>
+        <button onclick="event.preventDefault(); document.querySelector('#addsecuritykey').close();" class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+      </div>
+    </form>
+  </dialog>
+
+  <?php
+  visual::smartSnackbar([
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."],
+    ["empty", "Faltan datos por introducir en el formulario."],
+    ["securitykeyadded", "Se ha registrado correctamente la llave de seguridad."],
+    ["securitykeydeleted", "Se ha eliminado correctamente la llave de seguridad."]
+  ]);
+  ?>
+
+  <script src="js/common_webauthn.js"></script>
+  <script src="js/securitykeys.js"></script>
+</body>
+</html>
diff --git a/src/sendbulkpasswords.php b/src/sendbulkpasswords.php
new file mode 100644
index 0000000..a5b9fce
--- /dev/null
+++ b/src/sendbulkpasswords.php
@@ -0,0 +1,77 @@
+<?php
+require_once("core.php");
+security::checkType(security::HYPERADMIN);
+
+$mdHeaderRowBefore = visual::backBtn("powertools.php");
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Enviar enlaces para generar contraseña</h2>
+          <p>Selecciona las personas a las que quieres enviar un correo para que puedan establecer su contraseña:</p>
+          <form action="dosendbulkpasswords.php" method="POST">
+            <div class="overflow-wrapper overflow-wrapper--for-table">
+              <table class="mdl-data-table mdl-js-data-table mdl-data-table--selectable mdl-shadow--2dp">
+                <thead>
+                  <tr>
+                    <?php
+                    if ($conf["debug"]) {
+                      ?>
+                      <th class="extra">ID</th>
+                      <?php
+                    }
+                    ?>
+                    <th class="mdl-data-table__cell--non-numeric">Nombre</th>
+                    <th class="mdl-data-table__cell--non-numeric extra">Categoría</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <?php
+                  $people = people::getAll();
+                  foreach ($people as $p) {
+                    ?>
+                    <tr data-person-id="<?=(int)$p["id"]?>">
+                      <?php
+                      if ($conf["debug"]) {
+                        ?>
+                        <td class="extra"><?=(int)$p["id"]?></td>
+                        <?php
+                      }
+                      ?>
+                      <td class="mdl-data-table__cell--non-numeric"><?=security::htmlsafe($p["name"])?></td>
+                      <td class="mdl-data-table__cell--non-numeric extra"><?=security::htmlsafe($p["category"])?></td>
+                    </tr>
+                    <?php
+                  }
+                  ?>
+                </tbody>
+              </table>
+            </div>
+
+            <br>
+            <button class="mdl-button mdl-js-button mdl-button--raised mdl-button--accent mdl-js-ripple-effect">Enviar</button>
+          </form>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <?php
+  visual::smartSnackbar([
+    ["empty", "Faltan datos por introducir en el formulario."],
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."]
+  ]);
+  ?>
+  <script src="js/sendbulkpasswords.js"></script>
+</body>
+</html>
diff --git a/src/settings.php b/src/settings.php
new file mode 100644
index 0000000..67eda8b
--- /dev/null
+++ b/src/settings.php
@@ -0,0 +1,114 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+$conf_redacted = $conf;
+foreach ($conf_redacted["db"] as &$el) {
+  $el = "*CENSURADO*";
+}
+$conf_redacted["mail"]["password"] = "*CENSURADO*";
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+
+  <style>
+  a {
+    color: blue;
+  }
+  </style>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Configuración</h2>
+          <a class="clicky-container" href="companies.php">
+            <div class="clicky mdl-js-ripple-effect">
+              <div class="text">
+                <span class="title">Empresas</span><br>
+                <span class="description">Configura las diferentes empresas de la aplicación.</span>
+              </div>
+              <div class="mdl-ripple"></div>
+            </div>
+          </a>
+          <a class="clicky-container" href="categories.php">
+            <div class="clicky mdl-js-ripple-effect">
+              <div class="text">
+                <span class="title">Categorías de trabajadores</span><br>
+                <span class="description">Configura las categorías en las que se pueden clasificar los trabajadores.</span>
+              </div>
+              <div class="mdl-ripple"></div>
+            </div>
+          </a>
+          <a class="clicky-container" href="incidenttypes.php">
+            <div class="clicky mdl-js-ripple-effect">
+              <div class="text">
+                <span class="title">Tipos de incidencias</span><br>
+                <span class="description">Configura los diferentes motivos que se pueden seleccionar al crear una incidencia.</span>
+              </div>
+              <div class="mdl-ripple"></div>
+            </div>
+          </a>
+          <a class="clicky-container" href="calendars.php">
+            <div class="clicky mdl-js-ripple-effect">
+              <div class="text">
+                <span class="title">Calendarios</span><br>
+                <span class="description">Configura los días festivos, lectivos y laborables del año.</span>
+              </div>
+              <div class="mdl-ripple"></div>
+            </div>
+          </a>
+          <a class="clicky-container" href="logs.php">
+            <div class="clicky mdl-js-ripple-effect">
+              <div class="text">
+                <span class="title">Logs</span><br>
+                <span class="description">Ver los logs del programa que registra los horarios diariamente.</span>
+              </div>
+              <div class="mdl-ripple"></div>
+            </div>
+          </a>
+          <?php
+          if (security::isAllowed(security::HYPERADMIN)) {
+            ?>
+            <a class="clicky-container" href="help.php">
+              <div class="clicky mdl-js-ripple-effect">
+                <div class="text">
+                  <span class="title">Recursos de ayuda</span><br>
+                  <span class="description">Configura los enlaces de ayuda que se ofrecen a los trabajadores en el aplicativo.</span>
+                </div>
+                <div class="mdl-ripple"></div>
+              </div>
+            </a>
+            <a class="clicky-container" href="powertools.php">
+              <div class="clicky mdl-js-ripple-effect">
+                <div class="text">
+                  <span class="title">Herramientas avanzadas</span><br>
+                  <span class="description">Ver herramientas para hiperadministradores del aplicativo.</span>
+                </div>
+                <div class="mdl-ripple"></div>
+              </div>
+            </a>
+            <?php
+          }
+
+          if ($conf["debug"]) {
+            ?>
+            <details class="debug margintop">
+              <summary>Ajustes establecidos en el fichero <code>config.php</code>:</summary>
+              <pre><?=visual::debugJson($conf_redacted); ?></pre>
+            </details>
+            <?php
+          }
+          ?>
+        </div>
+      </div>
+    </main>
+  </div>
+</body>
+</html>
diff --git a/src/signin.php b/src/signin.php
new file mode 100644
index 0000000..295416c
--- /dev/null
+++ b/src/signin.php
@@ -0,0 +1,23 @@
+<?php
+require_once("core.php");
+
+if (!isset($_POST["username"]) || !isset($_POST["password"]) || empty($_POST["username"]) || empty($_POST["password"])) {
+  security::go("index.php?msg=empty");
+}
+
+switch (security::signIn($_POST["username"], $_POST["password"])) {
+  case security::SIGNIN_STATE_SIGNED_IN:
+  security::redirectAfterSignIn();
+  break;
+
+  case security::SIGNIN_STATE_NEEDS_SECOND_FACTOR:
+  security::go("signinsecondfactor.php");
+  break;
+
+  case security::SIGNIN_STATE_THROTTLED:
+  security::go("index.php?msg=signinthrottled");
+  break;
+
+  default:
+  security::go("index.php?msg=wrong");
+}
diff --git a/src/signinsecondfactor.php b/src/signinsecondfactor.php
new file mode 100644
index 0000000..9b5ecd1
--- /dev/null
+++ b/src/signinsecondfactor.php
@@ -0,0 +1,92 @@
+<?php
+require_once("core.php");
+
+if (security::userType() !== security::UNKNOWN || !isset($_SESSION["firstfactorid"]) || !secondFactor::isEnabled($_SESSION["firstfactorid"])) {
+  security::goHome();
+}
+
+secondFactor::checkAvailability();
+
+$hasSecurityKeys = secondFactor::hasSecurityKeys($_SESSION["firstfactorid"]);
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/index.css">
+  <style>
+  .login {
+    max-width: 500px;
+  }
+
+  .mdl-tabs__tab-bar {
+    height: auto;
+  }
+
+  .mdl-tabs__tab {
+    overflow: visible;
+    height: 100%;
+    line-height: 1.5;
+    padding-top: 14px;
+    padding-bottom: 14px;
+  }
+
+  .mdl-tabs__panel {
+    padding-top: 16px;
+  }
+
+  #content .mdl-spinner {
+    margin: 16px auto;
+  }
+
+  #webauthn {
+    text-align: center;
+  }
+  </style>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="login mdl-shadow--4dp">
+    <h2>Verificación en dos pasos</h2>
+    <div id="content">
+      <div class="mdl-tabs mdl-js-tabs mdl-js-ripple-effect">
+        <div class="mdl-tabs__tab-bar">
+          <a href="#totp" class="mdl-tabs__tab<?=($hasSecurityKeys ? "" : " is-active")?>">Código de verificación</a>
+          <?php if ($hasSecurityKeys) { ?><a href="#webauthn" class="mdl-tabs__tab is-active">Llave de seguridad</a><?php } ?>
+        </div>
+
+        <div class="mdl-tabs__panel<?=($hasSecurityKeys ? "" : " is-active")?>" id="totp">
+          <p>Introduce el código de verificación generado por tu aplicación para móviles.</p>
+
+          <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+            <input class="mdl-textfield__input" type="text" name="code" id="code" autocomplete="off" inputmode="numeric" pattern="[0-9]{6}" data-required>
+            <label class="mdl-textfield__label" for="code">Código de verificación</label>
+          </div>
+          <br>
+          <button id="verify" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent">Verificar</button>
+        </div>
+
+        <?php
+        if ($hasSecurityKeys) {
+          ?>
+          <div class="mdl-tabs__panel is-active" id="webauthn">
+            <p>Cuando estés listo para autenticarte, pulsa el siguiente botón:</p>
+
+            <button id="startwebauthn" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect mdl-button--accent">Usar llave de seguridad</button>
+          </div>
+          <?php
+        }
+        ?>
+      </div>
+    </div>
+  </div>
+
+  <div class="mdl-snackbar mdl-js-snackbar">
+    <div class="mdl-snackbar__text"></div>
+    <button type="button" class="mdl-snackbar__action"></button>
+  </div>
+
+  <script src="js/common_webauthn.js"></script>
+  <script src="js/secondfactor.js"></script>
+</body>
+</html>
diff --git a/src/userincidents.php b/src/userincidents.php
new file mode 100644
index 0000000..e46ee88
--- /dev/null
+++ b/src/userincidents.php
@@ -0,0 +1,142 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER);
+security::checkWorkerUIEnabled();
+
+$isAdmin = security::isAllowed(security::ADMIN);
+
+if (!security::checkParams("GET", [
+  ["id", security::PARAM_ISINT]
+])) {
+  security::go((security::isAdminView() ? "workers.php" : "workerhome.php"));
+}
+
+$id = (int)$_GET["id"];
+
+if (!$isAdmin && people::userData("id") != $id) {
+  security::notFound();
+}
+
+$p = people::get($id);
+
+if ($p === false) {
+  security::go((security::isAdminView() ? "workers.php" : "workerhome.php"));
+}
+
+$workers = workers::getPersonWorkers((int)$p["id"]);
+$companies = companies::getAll();
+
+if ($workers === false || $companies === false) {
+  security::go((security::isAdminView() ? "workers.php" : "workerhome.php")."?msg=unexpected");
+}
+
+if (security::isAdminView()) $mdHeaderRowBefore = visual::backBtn("workers.php");
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+  <link rel="stylesheet" href="css/incidents.css">
+
+  <style>
+  .addincident, .addrecurringincident {
+    z-index: 1000;
+  }
+
+  .addincident {
+    position: fixed;
+    bottom: 16px;
+    right: 16px;
+  }
+  <?php
+  if (security::isAdminView()) {
+    ?>
+    .addrecurringincident {
+      position: fixed;
+      bottom: 80px;
+      right: 25px;
+    }
+    <?php
+  }
+  ?>
+  </style>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php
+    visual::includeNav();
+    if (security::isAdminView()) {
+      ?>
+      <button class="addrecurringincident mdl-button mdl-js-button mdl-button--fab mdl-button--mini-fab mdl-color--grey-200"><i class="material-icons">repeat</i><span class="mdl-ripple"></span></button>
+      <?php
+    }
+    ?>
+    <button class="addincident mdl-button md-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--accent"><i class="material-icons">add</i><span class="mdl-ripple"></span></button>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <?php
+          $title = "Incidencias".(security::isAdminView() ? " de &ldquo;".security::htmlsafe($p["name"])."&rdquo;" : "");
+          ?>
+          <h2><?=$title?></h2>
+
+          <?php
+          if (count($workers)) {
+            foreach ($workers as $w) {
+              $incidents = incidents::getAll(false, 0, 0, (int)$w["id"]);
+
+              echo "<h4>".security::htmlsafe($companies[$w["company"]]).($w["hidden"] == 1 ? " <span class='mdl-color-text--grey-600'>(dada de baja)</span>" : "")."</h4>";
+
+              if (count($incidents)) {
+                incidentsView::renderIncidents($incidents, $companies, true, false, !security::isAdminView(), false, false, "userincidents.php?id=".(int)$p["id"]);
+              } else {
+                echo "<p>Todavía no existe ninguna incidencia ".(security::isAdminView() ? "para este trabajador " : "")."en esta empresa.</p>";
+              }
+
+              visual::printDebug("incidents::getAll(false, 0, 0, ".(int)$w["id"].")", $incidents);
+            }
+          } else {
+            echo "<p>".(security::isAdminView() ? "Antes de poder visualizar y añadir incidencias a este trabajador deberías darlo de alta en alguna empresa." : "No puedes visualizar ni añadir incidencias porque todavía no se te ha asignado ninguna empresa.")."</p>";
+          }
+          ?>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <?php
+  incidentsView::renderIncidentForm($workers, function(&$worker) {
+    return (int)$worker["id"];
+  }, function (&$worker, &$companies) {
+    return $worker["name"].' ('.$companies[$worker["company"]].')';
+  }, $companies, !$isAdmin, false, "userincidents.php?id=".(int)$p["id"]);
+
+  if (security::isAdminView()) {
+    incidentsView::renderIncidentForm($workers, function(&$worker) {
+      return (int)$worker["id"];
+    }, function (&$worker, &$companies) {
+      return $worker["name"].' ('.$companies[$worker["company"]].')';
+    }, $companies, false, true, "userincidents.php?id=".(int)$p["id"]);
+    ?>
+    <script>
+    window.addEventListener("load", function() {
+      document.querySelector(".addrecurringincident").addEventListener("click", function() {
+        document.querySelector("#addrecurringincident").showModal();
+        /* Or dialog.show(); to show the dialog without a backdrop. */
+      });
+    });
+    </script>
+    <?php
+  }
+
+  visual::renderTooltips();
+
+  visual::smartSnackbar(incidentsView::$incidentsMsgs);
+  ?>
+
+  <script src="js/userincidents.js"></script>
+  <script src="js/incidentsgeneric.js"></script>
+</body>
+</html>
diff --git a/src/userregistry.php b/src/userregistry.php
new file mode 100644
index 0000000..17bb29e
--- /dev/null
+++ b/src/userregistry.php
@@ -0,0 +1,109 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER);
+security::checkWorkerUIEnabled();
+
+$isAdmin = security::isAllowed(security::ADMIN);
+
+$mainURL = (security::isAdminView() ? "workers.php" : "workerhome.php");
+
+if (!security::checkParams("GET", [
+  ["id", security::PARAM_ISINT]
+])) {
+  security::go($mainURL);
+}
+
+$id = (int)$_GET["id"];
+
+if (!$isAdmin && people::userData("id") != $id) {
+  security::notFound();
+}
+
+$p = people::get($id);
+
+if ($p === false) {
+  security::go($mainURL);
+}
+
+$workers = workers::getPersonWorkers((int)$p["id"]);
+$companies = companies::getAll();
+
+if ($workers === false || $companies === false) {
+  security::go($mainURL."?msg=unexpected");
+}
+
+$numRows = registry::numRowsUser($p["id"]);
+
+if (security::isAdminView()) $mdHeaderRowBefore = visual::backBtn("workers.php");
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+  <link rel="stylesheet" href="css/incidents.css">
+  <style>
+  @media (max-width: 400px) {
+    #exportbtn {
+      display: none;
+    }
+  }
+  </style>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php
+    visual::includeNav();
+    ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <?php
+          if (!security::isAdminView()) {
+            ?>
+            <div class="actions">
+              <a href="export4worker.php?id=<?=(int)$_SESSION["id"]?>" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect" id="exportbtn"><i class="material-icons">cloud_download</i> Exportar registro e incidencias</a>
+              <?php helpView::renderHelpButton(help::PLACE_REGISTRY_PAGE); ?>
+            </div>
+            <?php
+          }
+          ?>
+
+          <?php
+          $title = "Registro".(security::isAdminView() ? " de &ldquo;".security::htmlsafe($p["name"])."&rdquo;" : "");
+          ?>
+          <h2><?=$title?></h2>
+
+          <p>Total: <?=(int)$numRows?> registros</p>
+
+          <?php
+          $page = (isset($_GET["page"]) ? (int)$_GET["page"] - 1 : null);
+
+          $registry = registry::getRecords($p["id"], false, false, true, true, true, $page, registry::REGISTRY_PAGINATION_LIMIT, true);
+          if (count($registry)) {
+            registryView::renderRegistry($registry, $companies, false, true, !security::isAdminView());
+          }
+          ?>
+
+          <?php
+          visual::renderPagination($numRows, "userregistry.php?id=".(int)$p["id"], incidents::PAGINATION_LIMIT, false, true);
+          visual::printDebug("registry::getRecords(".(int)$p["id"].", false, false, true, true, true, ".(int)$page.", registry::REGISTRY_PAGINATION_LIMIT, true)", $registry);
+          ?>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <?php
+  visual::renderTooltips();
+
+  visual::smartSnackbar([
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."]
+  ]);
+  ?>
+
+  <script src="js/userincidents.js"></script>
+  <script src="js/incidentsgeneric.js"></script>
+</body>
+</html>
diff --git a/src/users.php b/src/users.php
new file mode 100644
index 0000000..d90faaf
--- /dev/null
+++ b/src/users.php
@@ -0,0 +1,237 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+$mdHeaderRowMore = '<div class="mdl-layout-spacer"></div>
+<div class="mdl-textfield mdl-js-textfield mdl-textfield--expandable
+            mdl-textfield--floating-label mdl-textfield--align-right">
+  <label class="mdl-button mdl-js-button mdl-button--icon"
+         for="usuario">
+    <i class="material-icons">search</i>
+  </label>
+  <div class="mdl-textfield__expandable-holder">
+    <input class="mdl-textfield__input" type="text" name="usuario"
+           id="usuario">
+  </div>
+</div>';
+
+listings::buildSelect("users.php");
+
+$categories = categories::getAll();
+$companies = companies::getAll();
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+
+  <style>
+  .adduser {
+    position: fixed;
+    bottom: 16px;
+    right: 16px;
+  }
+  .importcsv {
+    position:fixed;
+    bottom: 80px;
+    right: 25px;
+  }
+  .filter {
+    position:fixed;
+    bottom: 126px;
+    right: 25px;
+  }
+  .adduser, .importcsv, .filter {
+    z-index: 1000;
+  }
+
+  @media (max-width: 655px) {
+    .extra {
+      display: none;
+    }
+  }
+
+  /* Hide datable's search box */
+  .dataTables_wrapper .mdl-grid:first-child {
+    display: none;
+  }
+  .dt-table {
+    padding: 0!important;
+  }
+  .dt-table .mdl-cell {
+    margin: 0!important;
+  }
+  #usuario {
+    position: relative;
+  }
+  </style>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <button class="adduser mdl-button md-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--accent"><i class="material-icons">person_add</i><span class="mdl-ripple"></span></button>
+    <button class="importcsv mdl-button mdl-js-button mdl-button--fab mdl-button--mini-fab mdl-color--grey-200"><i class="material-icons">file_upload</i></button>
+    <button class="filter mdl-button mdl-js-button mdl-button--fab mdl-button--mini-fab mdl-color--grey-200"><i class="material-icons">filter_list</i></button>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Personas</h2>
+          <div class="overflow-wrapper overflow-wrapper--for-table">
+            <table class="mdl-data-table mdl-js-data-table mdl-shadow--2dp datatable">
+              <thead>
+                <tr>
+                  <?php
+                  if ($conf["debug"]) {
+                    ?>
+                    <th class="extra">ID</th>
+                    <?php
+                  }
+                  ?>
+                  <th class="mdl-data-table__cell--non-numeric">Nombre</th>
+                  <th class="mdl-data-table__cell--non-numeric">Categoría</th>
+                  <th class="mdl-data-table__cell--non-numeric extra">Tipo</th>
+                  <th class="mdl-data-table__cell--centered">Baja</th>
+                  <th class="mdl-data-table__cell--non-numeric"></th>
+                </tr>
+              </thead>
+              <tbody>
+                <?php
+                $people = people::getAll($select);
+                foreach ($people as $p) {
+                  ?>
+                  <tr>
+                    <?php
+                    if ($conf["debug"]) {
+                      ?>
+                      <td class="extra"><?=(int)$p["id"]?></td>
+                      <?php
+                    }
+                    ?>
+                    <td class="mdl-data-table__cell--non-numeric"><?=security::htmlsafe($p["name"])?></td>
+                    <td class="mdl-data-table__cell--non-numeric"><?=security::htmlsafe(($p["categoryid"] == -1 ? "-" : $p["category"]))?></td>
+                    <td class="mdl-data-table__cell--non-numeric extra"><?=security::htmlsafe(security::$types[$p["type"]])?></td>
+                    <td class="mdl-data-table__cell--centered"><?=($p["baixa"] == 1 ? visual::YES : "")?></td>
+                    <td class='mdl-data-table__cell--non-numeric'>
+                      <a href='dynamic/user.php?id=<?=(int)$p['id']?>' data-dyndialog-href='dynamic/user.php?id=<?=(int)$p['id']?>' title='Ver información completa'><i class='material-icons icon'>open_in_new</i></a>
+                      <?php if (security::isAllowed($p['type'])) { ?>
+                        <a href='dynamic/edituser.php?id=<?=(int)$p['id']?>' data-dyndialog-href='dynamic/edituser.php?id=<?=(int)$p['id']?>' title='Editar persona'><i class='material-icons icon'>edit</i></a>
+                      <?php } ?>
+                      <a href='dynamic/companyuser.php?id=<?=(int)$p['id']?>' data-dyndialog-href='dynamic/companyuser.php?id=<?=(int)$p['id']?>' title='Ver y añadir empresas a la persona'><i class='material-icons icon'>work</i></a>
+                      <?php if (count($p['companies'])) { ?>
+                        <a href='workerschedule.php?id=<?=(int)$p['id']?>' title='Ver y gestionar los horarios de la persona'><i class='material-icons icon'>timelapse</i></a>
+                        <a href='userincidents.php?id=<?=(int)$p['id']?>' title='Ver y gestionar las incidencias del trabajador'><i class='material-icons icon'>assignment_late</i></a>
+                        <a href='userregistry.php?id=<?=(int)$p['id']?>' title='Ver y gestionar los registros del trabajador'><i class='material-icons icon'>list</i></a>
+                      <?php } ?>
+                    </td>
+                  </tr>
+                  <?php
+                }
+                ?>
+              </tbody>
+            </table>
+          </div>
+
+          <?php visual::printDebug("people::getAll(\$select)", $people); ?>
+          <?php visual::printDebug("\$select", $select); ?>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <dialog class="mdl-dialog" id="adduser">
+    <form action="doadduser.php" method="POST" autocomplete="off">
+      <h4 class="mdl-dialog__title">Añade un trabajador</h4>
+      <div class="mdl-dialog__content">
+        <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+          <input class="mdl-textfield__input" type="text" name="username" id="username" autocomplete="off" data-required>
+          <label class="mdl-textfield__label" for="username">Nombre de usuario</label>
+        </div>
+        <br>
+        <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+          <input class="mdl-textfield__input" type="text" name="name" id="name" autocomplete="off" data-required>
+          <label class="mdl-textfield__label" for="name">Nombre</label>
+        </div>
+        <br>
+        <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+          <input class="mdl-textfield__input" type="text" name="dni" id="dni" autocomplete="off">
+          <label class="mdl-textfield__label" for="dni">DNI (opcional)</label>
+        </div>
+        <br>
+        <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+          <input class="mdl-textfield__input" type="email" name="email" id="email" autocomplete="off">
+          <label class="mdl-textfield__label" for="email">Correo electrónico (opcional)</label>
+        </div>
+        <br>
+        <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+          <select name="category" id="category" class="mdlext-selectfield__select">
+            <option value="-1"></option>
+            <?php
+            foreach ($categories as $id => $category) {
+              echo '<option value="'.(int)$id.'">'.security::htmlsafe($category).'</option>';
+            }
+            ?>
+          </select>
+          <label for="category" class="mdlext-selectfield__label">Categoría (opcional)</label>
+        </div>
+        <br>
+        <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+          <input class="mdl-textfield__input" type="password" name="password" id="password" autocomplete="off" data-required>
+          <label class="mdl-textfield__label" for="password">Contraseña</label>
+        </div>
+        <p><?=security::htmlsafe(security::$passwordHelperText)?></p>
+        <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label" data-required>
+          <select name="type" id="type" class="mdlext-selectfield__select">
+            <?php
+            foreach (security::$types as $i => $type) {
+              echo '<option value="'.(int)$i.'"'.(security::isAllowed($i) ? "" : " disabled").'>'.security::htmlsafe($type).'</option>';
+            }
+            ?>
+          </select>
+          <label for="type" class="mdlext-selectfield__label">Tipo</label>
+        </div>
+      </div>
+      <div class="mdl-dialog__actions">
+        <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Añadir</button>
+        <button onclick="event.preventDefault(); document.querySelector('#adduser').close();" class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+      </div>
+    </form>
+  </dialog>
+
+  <dialog class="mdl-dialog" id="importcsv">
+    <form action="csvimport.php" method="POST" enctype="multipart/form-data">
+      <h4 class="mdl-dialog__title">Importar CSV</h4>
+      <div class="mdl-dialog__content">
+        <p>Selecciona debajo el archivo CSV:</p>
+        <p><input type="file" name="file" accept=".csv" required></p>
+        <p>El formato de la cabecera debe ser: <code><?=security::htmlsafe(implode(";", csv::$fields))?></code></p>
+        <p>En la columna <code>category</code>, introduce el ID de la categoría ya creada en el sistema (o <code>-1</code> si no quieres definir una categoría para esa persona), y en la columna <code>companies</code> introduce una lista separada por comas de los IDs de las empresas que quieres añadir a esa persona.</p>
+      </div>
+      <div class="mdl-dialog__actions">
+        <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Importar</button>
+        <button onclick="event.preventDefault(); document.querySelector('#importcsv').close();" class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+      </div>
+    </form>
+  </dialog>
+
+  <?php listings::renderFilterDialog("users.php", $select); ?>
+
+  <?php
+  visual::smartSnackbar([
+    ["added", "Se ha añadido la persona correctamente."],
+    ["modified", "Se ha modificado la persona correctamente."],
+    ["empty", "Faltan datos por introducir en el formulario o el correo electrónico es incorrecto."],
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."],
+    ["couldntupdatepassword", "Se ha actualizado la información pero no se ha podido actualizar la contraseña. Inténtelo de nuevo en unos segundos."],
+    ["weakpassword", security::$passwordHelperText],
+    ["disabledsecondfactor", "Se ha desactivado la verificación en dos pasos correctamente."]
+  ]);
+  ?>
+
+  <script src="js/users.js"></script>
+  <script src="node_modules/jquery/dist/jquery.min.js"></script>
+  <script src="node_modules/datatables/media/js/jquery.dataTables.min.js"></script>
+  <script src="lib/datatables/dataTables.material.min.js"></script>
+</body>
+</html>
diff --git a/src/userschedule.php b/src/userschedule.php
new file mode 100644
index 0000000..a3eebe5
--- /dev/null
+++ b/src/userschedule.php
@@ -0,0 +1,159 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER);
+security::checkWorkerUIEnabled();
+
+$isAdmin = security::isAllowed(security::ADMIN);
+
+if (!security::checkParams("GET", [
+  ["id", security::PARAM_ISINT]
+])) {
+  security::go((security::isAdminView() ? "workers.php" : "workerschedule.php"));
+}
+
+$id = (int)$_GET["id"];
+
+if (!$isAdmin && people::userData("id") != $id) {
+  security::notFound();
+}
+
+$p = people::get($id);
+
+if ($p === false) {
+  security::go((security::isAdminView() ? "workers.php" : "workerschedule.php"));
+}
+
+$workers = workers::getPersonWorkers((int)$p["id"]);
+$companies = companies::getAll();
+
+if ($workers === false || $companies === false) {
+  security::go((security::isAdminView() ? "workers.php?msg=unexpected" : "workerschedule.php?msg=unexpected"));
+}
+
+$mdHeaderRowBefore = visual::backBtn("workerschedule.php".(security::isAdminView() ? "?id=".$id : ""));
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+  <?php
+  if (security::isAdminView()) {
+    ?>
+    <style>
+    .addschedule {
+      position: fixed;
+      bottom: 16px;
+      right: 16px;
+      z-index: 1000;
+    }
+    </style>
+    <?php
+  }
+  ?>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php
+    visual::includeNav();
+
+    if (security::isAdminView()) {
+      ?>
+      <button class="addschedule mdl-button md-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--accent"><i class="material-icons">add</i><span class="mdl-ripple"></span></button>
+      <?php
+    }
+    ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <?php $title = (security::isAdminView() ? "Horarios de &ldquo;".security::htmlsafe($p["name"])."&rdquo;" : "Todos los horarios"); ?>
+          <h2><?=$title?></h2>
+
+          <?php
+          if (count($workers)) {
+            foreach ($workers as $w) {
+              $schedules = schedules::getAll((int)$w["id"], security::isAdminView());
+
+              echo "<h4>".security::htmlsafe($companies[$w["company"]])."</h4>";
+
+              if (count($schedules)) {
+                foreach ($schedules as $s) {
+                  ?>
+                  <a href="schedule.php?id=<?=(int)$s["id"]?>" class="clicky-container">
+                    <div class="clicky mdl-js-ripple-effect">
+                      <div class="text">
+                        <span class="title"><?=security::htmlsafe(date("d/m/Y", $s["begins"]))?> - <?=security::htmlsafe(date("d/m/Y", $s["ends"]))?></span><br>
+                        <span class="description">Activo: <?=($s["active"] == 1 ? visual::YES : visual::NO)?></span>
+                      </div>
+                      <div class="mdl-ripple"></div>
+                    </div>
+                  </a>
+                  <?php
+                }
+              } else {
+                echo "<p>".(security::isAdminView() ? "Todavía no se ha definido ningún horario para este trabajador en esta empresa" : "Todavía no se ha definido ningún horario para esta empresa.")."</p>";
+              }
+
+              visual::printDebug("schedules::getAll(".(int)$w["id"].")", $schedules);
+            }
+          } else {
+            echo "<p>".(security::isAdminView() ? "Antes de poder definir horarios para este trabajador deberías darlo de alta en alguna empresa." : "Todavía no se te ha definido ningún horario.")."</p>";
+          }
+          ?>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <?php
+  if (security::isAdminView()) {
+    ?>
+    <dialog class="mdl-dialog" id="addschedule">
+      <form action="doaddschedule.php" method="POST" autocomplete="off">
+        <h4 class="mdl-dialog__title">Crea un nuevo horario semanal</h4>
+        <div class="mdl-dialog__content">
+          <div class="mdlext-selectfield mdlext-js-selectfield mdlext-selectfield--floating-label">
+            <select name="worker" id="worker" class="mdlext-selectfield__select" data-required>
+              <?php
+              foreach ($workers as $w) {
+                echo '<option value="'.(int)$w["id"].'">'.security::htmlsafe($companies[$w["company"]]).'</option>';
+              }
+              ?>
+            </select>
+            <label for="worker" class="mdlext-selectfield__label">Empresa</label>
+          </div>
+          <br>
+          <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+            <input class="mdl-textfield__input" type="date" name="begins" id="begins" autocomplete="off" data-required>
+            <label class="mdl-textfield__label always-focused" for="begins">Fecha inicio de validez</label>
+          </div>
+          <br>
+          <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
+            <input class="mdl-textfield__input" type="date" name="ends" id="ends" autocomplete="off" data-required>
+            <label class="mdl-textfield__label always-focused" for="ends">Fecha fin de validez</label>
+          </div>
+        </div>
+        <div class="mdl-dialog__actions">
+          <button type="submit" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--accent">Crear</button>
+          <button onclick="event.preventDefault(); document.querySelector('#addschedule').close();" class="mdl-button mdl-js-button mdl-js-ripple-effect cancel">Cancelar</button>
+        </div>
+      </form>
+    </dialog>
+    <?php
+  }
+  ?>
+
+  <?php
+  visual::smartSnackbar([
+    ["deleted", "Se ha eliminado el horario semanal correctamente."],
+    ["empty", "Faltan datos por introducir en el formulario."],
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."],
+    ["overlaps", "El horario que intentabas añadir se solapa con otro."],
+    ["order", "La fecha de inicio debe ser anterior a la fecha de fin."]
+  ]);
+  ?>
+
+  <?php if (security::isAdminView()) { ?><script src="js/userschedule.js"></script><?php } ?>
+</body>
+</html>
diff --git a/src/validations.php b/src/validations.php
new file mode 100644
index 0000000..f0bab4c
--- /dev/null
+++ b/src/validations.php
@@ -0,0 +1,48 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER);
+
+$id = people::userData("id");
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+  <link rel="stylesheet" href="css/incidents.css">
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <?php helpView::renderHelpButton(help::PLACE_VALIDATION_PAGE, true); ?>
+          <h2>Validaciones</h2>
+          <p>Selecciona todas las incidencias y registros de horario a los cuales quieres dar validez.</p>
+          <?php
+          validationsView::renderPendingValidations($id);
+          ?>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <div class="mdl-snackbar mdl-js-snackbar">
+    <div class="mdl-snackbar__text"></div>
+    <button type="button" class="mdl-snackbar__action"></button>
+  </div>
+
+  <?php
+  visual::smartSnackbar([
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."],
+    ["success", "Se ha realizado la validación correctamente."],
+    ["partialsuccess", "Ha ocurrido un error validando alguno de los elementos. Inténtelo de nuevo en unos segundos."]
+  ], 10000, false);
+  ?>
+
+  <script src="js/incidentsgeneric.js"></script>
+  <script src="js/validations.js"></script>
+</body>
+</html>
diff --git a/src/workercalendar.php b/src/workercalendar.php
new file mode 100644
index 0000000..b795fc7
--- /dev/null
+++ b/src/workercalendar.php
@@ -0,0 +1,64 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER);
+security::checkWorkerUIEnabled();
+
+$id = (int)people::userData("id");
+
+$p = people::get($id);
+
+if ($p === false) {
+  security::go((security::isAdminView() ? "workers.php" : "workerhome.php"));
+}
+
+$cal = calendars::getCurrentCalendarByCategory($p["categoryid"]);
+if ($cal === false) security::go("workerhome.php?msg=unexpected");
+
+$mdHeaderRowBefore = visual::backBtn("workerschedule.php");
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+  <link rel="stylesheet" href="css/calendar.css">
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Calendario laboral</h2>
+
+          <?php
+          if ($cal === calendars::NO_CALENDAR_APPLICABLE) {
+            echo "<p>Actualmente no tienes asignado ningún calendario laboral.</p>";
+          } else {
+            $current = new DateTime();
+            $current->setTimestamp($cal["begins"]);
+            $ends = new DateTime();
+            $ends->setTimestamp($cal["ends"]);
+
+            calendarsView::renderCalendar($current, $ends, function ($timestamp, $id, $dow, $dom, $extra) {
+              return ($extra[$timestamp] == $id);
+            }, true, $cal["details"]);
+          }
+          ?>
+
+          <?php visual::printDebug("calendars::getCurrentCalendarByCategory(".(int)$p["categoryid"].")", $cal); ?>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <script src="js/calendar.js"></script>
+
+  <?php
+  visual::smartSnackbar([
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."]
+  ]);
+  ?>
+</body>
+</html>
diff --git a/src/workerhome.php b/src/workerhome.php
new file mode 100644
index 0000000..65aeef9
--- /dev/null
+++ b/src/workerhome.php
@@ -0,0 +1,48 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER);
+security::checkWorkerUIEnabled();
+
+security::changeActiveView(visual::VIEW_WORKER);
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+
+  <style>
+  a {
+    color: blue;
+  }
+  </style>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <h2>Bienvenido</h2>
+        	<p>¡Hola <?=people::userData("name")?>! Bienvenido a tu Panel de Control.</p>
+          <p>Estas son las diferentes secciones que encontrarás en el aplicativo:</p>
+          <ul>
+            <li><b><a href="workerschedule.php">Horario actual</a>:</b> aquí puedes <span class="highlighted">ver tu horario actual</span>, tu <span class="highlighted">calendario laboral</span> y un <span class="highlighted">histórico de todos los horarios</span> que tienes asignados.</li>
+            <li><b><a href="userincidents.php?id=<?=(int)$_SESSION["id"]?>">Incidencias</a>:</b> aquí puedes ver las <span class="highlighted">incidencias que tienes asignadas</span> e <span class="highlighted">introducir una nueva incidencia</span>.</li>
+            <li><b><a href="userregistry.php?id=<?=(int)$_SESSION["id"]?>">Registro</a>:</b> aquí se encuentran los <span class="highlighted">registros de tu horario diario</span>. No tiene en cuenta las incidencias que has creado porque es únicamente el horario base de tu jornada laboral.</li>
+            <li><b><a href="validations.php?>">Validaciones</a>:</b> aquí puedes <span class="highlighted">validar tus incidencias y registros de tu horario diario</span> que todavía no hayas validado.</li>
+            <li><b><a href="export4worker.php?id=<?=(int)$_SESSION["id"]?>">Exportar registro</a>:</b> aquí puedes <span class="highlighted">descargar tu registro incluyendo incidencias como PDF</span>.</li>
+          </ul>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <?php
+  visual::smartSnackbar([
+    ["passwordchanged", "Se ha cambiado la contraseña correctamente."]
+  ]);
+  ?>
+</body>
+</html>
diff --git a/src/workers.php b/src/workers.php
new file mode 100644
index 0000000..f6318d5
--- /dev/null
+++ b/src/workers.php
@@ -0,0 +1,171 @@
+<?php
+require_once("core.php");
+security::checkType(security::ADMIN);
+
+$mdHeaderRowMore = '<div class="mdl-layout-spacer"></div>
+<div class="mdl-textfield mdl-js-textfield mdl-textfield--expandable
+            mdl-textfield--floating-label mdl-textfield--align-right">
+  <label class="mdl-button mdl-js-button mdl-button--icon"
+         for="usuario">
+    <i class="material-icons">search</i>
+  </label>
+  <div class="mdl-textfield__expandable-holder">
+    <input class="mdl-textfield__input" type="text" name="usuario"
+           id="usuario">
+  </div>
+</div>';
+
+listings::buildSelect("workers.php");
+
+$companies = companies::getAll();
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+
+  <style>
+  .filter {
+    position:fixed;
+    bottom: 25px;
+    right: 25px;
+    z-index: 1000;
+  }
+
+  @media (max-width: 655px) {
+    .extra {
+      display: none;
+    }
+  }
+
+  table.has-actions-above {
+    border-top: 0!important;
+  }
+
+  /* Hide datable's search box */
+  .dataTables_wrapper .mdl-grid:first-child {
+    display: none;
+  }
+  .dt-table {
+    padding: 0!important;
+  }
+  .dt-table .mdl-cell {
+    margin: 0!important;
+  }
+  #usuario {
+    position: relative;
+  }
+  </style>
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+
+    <button class="filter mdl-button mdl-js-button mdl-button--fab mdl-button--mini-fab mdl-color--grey-200"><i class="material-icons">filter_list</i></button>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <div class="actions">
+            <a href="scheduletemplates.php" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect"><i class="material-icons">timelapse</i> Administrar plantillas</a>
+          </div>
+
+          <h2>Trabajadores</h2>
+          <div class="left-actions">
+            <button id="copytemplate" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect"><i class="material-icons icon">add_alarm</i></button>
+            <button id="addincidentbulk" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect"><i class="material-icons icon">note_add</i></button>
+            <button id="addrecurringincident" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect"><i class="material-icons icon">repeat</i></button>
+          </div>
+          <?php
+          visual::addTooltip("copytemplate", "Copiar plantilla de horario a los trabajadores seleccionados");
+          visual::addTooltip("addincidentbulk", "Añadir incidencia a los trabajadores seleccionados");
+          visual::addTooltip("addrecurringincident", "Añadir incidencia recurrente al trabajador seleccionado");
+          ?>
+          <div class="overflow-wrapper overflow-wrapper--for-table">
+            <table class="mdl-data-table mdl-js-data-table mdl-data-table--selectable mdl-shadow--2dp datatable has-actions-above">
+              <thead>
+                <tr>
+                  <?php
+                  if ($conf["debug"]) {
+                    ?>
+                    <th class="extra">ID</th>
+                    <?php
+                  }
+                  ?>
+                  <th class="mdl-data-table__cell--non-numeric">Nombre</th>
+                  <th class="mdl-data-table__cell--non-numeric">Empresa</th>
+                  <th class="mdl-data-table__cell--non-numeric extra">Categoría</th>
+                  <th class="mdl-data-table__cell--non-numeric"></th>
+                </tr>
+              </thead>
+              <tbody>
+                <?php
+                $workers = people::getAll($select, true);
+                foreach ($workers as $w) {
+                  $schedulesStatusDetailed = schedules::getWorkerScheduleStatus($w["workerid"]);
+                  $schedulesStatus = $schedulesStatusDetailed["status"];
+                  if ($selectedSchedulesStatus !== false && !in_array($schedulesStatus, $selectedSchedulesStatus)) continue;
+                  ?>
+                  <tr data-worker-id="<?=(int)$w["workerid"]?>"<?=($w["hidden"] == 1 ? "class=\"mdl-color-text--grey-700\"" : "")?>>
+                    <?php
+                    if ($conf["debug"]) {
+                      ?>
+                      <td class="extra"><?=(int)$w["workerid"]?></td>
+                      <?php
+                    }
+                    ?>
+                    <td class="mdl-data-table__cell--non-numeric"><?=($w["hidden"] == 1 ? "<s>" : "").security::htmlsafe($w["name"]).($w["hidden"] == 1 ? "</s>" : "")?></td>
+                    <td class="mdl-data-table__cell--non-numeric"><?=($w["hidden"] == 1 ? "<s>" : "").security::htmlsafe($companies[$w["companyid"]]).($w["hidden"] == 1 ? "</s>" : "")?></td>
+                    <td class="mdl-data-table__cell--non-numeric extra"><?=($w["hidden"] == 1 ? "<s>" : "").security::htmlsafe($w["category"]).($w["hidden"] == 1 ? "</s>" : "")?></td>
+                    <td class='mdl-data-table__cell--non-numeric'>
+                      <a href='<?=($schedulesStatus == schedules::STATUS_NO_ACTIVE_SCHEDULE ? "userschedule" : "workerschedule")?>.php?id=<?=(int)$w['id']?>' title='Ver y gestionar los horarios del trabajador' class='mdl-color-text--<?=security::htmlsafe((schedules::$workerScheduleStatusColors[$schedulesStatus] ?? "black"))?>'><i class='material-icons icon-no-black'>timelapse</i></a>
+                      <a href='userincidents.php?id=<?=(int)$w['id']?>' title='Ver y gestionar las incidencias del trabajador'><i class='material-icons icon'>assignment_late</i></a>
+                      <a href='userregistry.php?id=<?=(int)$w['id']?>' title='Ver y gestionar los registros del trabajador'><i class='material-icons icon'>list</i></a>
+                      <a href='dynamic/workhistory.php?id=<?=(int)$w['workerid']?>' data-dyndialog-href='dynamic/workhistory.php?id=<?=(int)$w['workerid']?>' title='Acceder al historial de altas y bajas'><i class='material-icons icon'>history</i></a>
+                    </td>
+                  </tr>
+                  <?php
+                }
+                ?>
+              </tbody>
+            </table>
+          </div>
+
+          <?php visual::printDebug("people::getAll(\$select, true)", $workers); ?>
+          <?php visual::printDebug("\$select", $select); ?>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <?php listings::renderFilterDialog("workers.php", $select); ?>
+
+  <?php
+  visual::renderTooltips();
+
+  visual::smartSnackbar([
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."],
+    ["copied", "Plantilla copiada correctamente."],
+    ["empty", "Faltaban datos por rellenar en el formulario o ha ocurrido un error inesperado."]
+  ]);
+  ?>
+
+  <script src="js/workers.js"></script>
+  <script src="node_modules/jquery/dist/jquery.min.js"></script>
+  <script src="node_modules/datatables/media/js/jquery.dataTables.min.js"></script>
+  <script src="lib/datatables/dataTables.material.min.js"></script>
+
+  <?php
+  if (isset($_GET["openWorkerHistory"])) {
+    ?>
+    <script>
+    window.addEventListener("load", _ => {
+      dynDialog.load("dynamic/workhistory.php?id=<?=(int)$_GET["openWorkerHistory"]?>");
+    });
+    </script>
+    <?php
+  }
+  ?>
+</body>
+</html>
diff --git a/src/workerschedule.php b/src/workerschedule.php
new file mode 100644
index 0000000..9ba43e6
--- /dev/null
+++ b/src/workerschedule.php
@@ -0,0 +1,115 @@
+<?php
+require_once("core.php");
+security::checkType(security::WORKER);
+security::checkWorkerUIEnabled();
+
+$isAdmin = security::isAllowed(security::ADMIN);
+
+$id = $_GET["id"] ?? people::userData("id");
+$id = (int)$id;
+
+if (!$isAdmin && people::userData("id") != $id) {
+  security::notFound();
+}
+
+$p = people::get($id);
+
+if ($p === false) {
+  security::go((security::isAdminView() ? "workers.php" : "workerhome.php"));
+}
+
+if (security::isAdminView()) $mdHeaderRowBefore = visual::backBtn("workers.php");
+
+$plaintext = isset($_GET["plaintext"]) && $_GET["plaintext"] == "1";
+$companies = companies::getAll();
+?>
+<!DOCTYPE html>
+<html>
+<head>
+  <title><?php echo $conf["appName"]; ?></title>
+  <?php visual::includeHead(); ?>
+  <link rel="stylesheet" href="css/dashboard.css">
+  <link rel="stylesheet" href="css/schedule.css">
+</head>
+<?php visual::printBodyTag(); ?>
+  <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header mdl-layout--fixed-drawer">
+    <?php visual::includeNav(); ?>
+    <main class="mdl-layout__content">
+      <div class="page-content">
+        <div class="main mdl-shadow--4dp">
+          <div class="actions">
+            <?php
+            if (!security::isAdminView()) {
+              ?>
+              <a href="workercalendar.php" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect"><i class="material-icons">today</i> Calendario laboral</a>
+              <?php
+            }
+            ?>
+            <a href="userschedule.php?id=<?=(int)$id?>" class="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect"><i class="material-icons">timelapse</i> <?=(security::isAdminView() ? "Administrar los horarios" : "Todos los horarios")?></a>
+          </div>
+
+          <h2>Horario actual<?=(security::isAdminView() ? " de &ldquo;".security::htmlsafe($p["name"])."&rdquo;" : "")?></h2>
+
+          <?php
+          $schedules = schedules::getCurrent($id);
+
+          if (!count($schedules)) {
+            echo "<p>Actualmente no tienes asignado ningún horario.</p>";
+          } else {
+            foreach ($schedules as $i => $schedule) {
+              $worker = workers::get($schedule["worker"]);
+
+              if (security::isAdminView()) {
+                ?>
+                <a href="schedule.php?id=<?=(int)$schedule["id"]?>" class="mdl-button mdl-js-button mdl-button--icon mdl-js-ripple-effect" style="float: right;"><i class="material-icons">edit</i><span class="mdl-ripple"></span></a>
+                <?php
+              }
+              ?>
+              <p>
+                <b>Empresa:</b> <?=$companies[$worker["company"]]?><br>
+                <b>Validez:</b> del <?=security::htmlsafe(date("d/m/Y", $schedule["begins"]))?> al <?=security::htmlsafe(date("d/m/Y", $schedule["ends"]))?>
+              </p>
+              <?php
+              if ($plaintext) {
+                $flag = false;
+                foreach ($schedule["days"] as $typeday) {
+                  schedulesView::renderPlainSchedule($typeday, false, null, null, $flag);
+                }
+
+                if (!$flag) {
+                  echo "<p>Tu horario todavía no está configurado.</p>";
+                }
+              } else {
+                foreach (calendars::$types as $tdid => $type) {
+                  if ($tdid == calendars::TYPE_FESTIU) continue;
+
+                  $tdisset = isset($schedule["days"][$tdid]);
+
+                  echo "<h4>".security::htmlsafe(calendars::$types[$tdid])."</h4>";
+
+                  if ($tdisset) {
+                    schedulesView::renderSchedule($schedule["days"][$tdid], false, null, null);
+                  } else {
+                    echo "<p>Todavía no hay configurado ningún horario en días del tipo \"".security::htmlsafe(calendars::$types[$tdid])."\".</p>";
+                  }
+                }
+              }
+
+              if ($i + 1 != count($schedules)) echo "<hr>";
+            }
+          }
+          ?>
+
+          <?php visual::printDebug("schedules::getCurrent()", $schedules); ?>
+        </div>
+      </div>
+    </main>
+  </div>
+
+  <?php
+  visual::smartSnackbar([
+    ["unexpected", "Ha ocurrido un error inesperado. Inténtelo de nuevo en unos segundos."]
+  ]);
+  ?>
+</body>
+</html>