From 5c78fc761d7afd7aafd21f521ac19a15b59facf3 Mon Sep 17 00:00:00 2001
From: Jan-Hendrik Willms <tleilax+studip@gmail.com>
Date: Wed, 30 Oct 2024 12:50:28 +0000
Subject: [PATCH] refactor holiday() function into Holidays class, allowing ids
 for holidays and...

Closes #2795

Merge request studip/studip!1904
---
 RELEASE-NOTES.md                              |   1 +
 app/controllers/admin/holidays.php            |  81 +++-
 app/views/admin/holidays/holidays.php         |  52 +++
 ..._add_customized_holidays_configuration.php |  35 ++
 lib/calendar_functions.inc.php                | 117 +-----
 lib/classes/Holidays.php                      | 379 ++++++++++++++++++
 lib/navigation/AdminNavigation.php            |   2 +-
 7 files changed, 539 insertions(+), 128 deletions(-)
 create mode 100644 app/views/admin/holidays/holidays.php
 create mode 100644 db/migrations/6.0.26_add_customized_holidays_configuration.php
 create mode 100644 lib/classes/Holidays.php

diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index f90ccb9cd4e..ee788e2013e 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -6,6 +6,7 @@
 
 - Der Stud.IP-Cache ist nun kompatibel zu PSR-6. ([TIC #3701](https://gitlab.studip.de/studip/studip/-/issues/3701))
 - Das `User`-Model hat die Methode `hasPermissionLevel()` erhalten, um einfach abfragen zu können, ob eine Person einen bestimmten Berechtigungsstatus hat. ([Issue #3453](https://gitlab.studip.de/studip/studip/-/issues/3453))
+- In der Standort-Verwaltung können nun nicht nur Ferien sondern auch Feiertage konfiguriert werden. Dies erlaubt das Markieren von Feiertagen als gesetzliche Feiertage, da diese je nach Bundesland variieren können. ([Issue #2795](https://gitlab.studip.de/studip/studip/-/issues/2795))
 
 ## Breaking changes
 
diff --git a/app/controllers/admin/holidays.php b/app/controllers/admin/holidays.php
index 8abdefc24fd..d87a54188e0 100644
--- a/app/controllers/admin/holidays.php
+++ b/app/controllers/admin/holidays.php
@@ -35,7 +35,7 @@ class Admin_HolidaysController extends AuthenticatedController
             URLHelper::addLinkParam('filter', $this->filter);
         }
 
-        $this->setSidebar();
+        $this->setSidebar($action);
     }
 
     /**
@@ -123,13 +123,33 @@ class Admin_HolidaysController extends AuthenticatedController
         $this->redirect('admin/holidays');
     }
 
+    public function holidays_action(): void
+    {
+        $this->holidays = Holidays::getHolidays(true, true);
+        $this->customized = Config::get()->CUSTOMIZED_HOLIDAYS;
+    }
+
+    public function store_holidays_action(): void
+    {
+        CSRFProtection::verifyUnsafeRequest();
+
+        Config::get()->store(
+            'CUSTOMIZED_HOLIDAYS',
+            Request::intArray('holidays')
+        );
+
+        PageLayout::postSuccess(_('Die Änderungen wurden gespeichert.'));
+
+        $this->redirect($this->holidaysURL());
+    }
+
     /**
      * Checks a string if it is a valid date and returns the according
      * unix timestamp if valid.
      *
      * @param string $name  Parameter name to extract from request
      * @param string $time Optional time segment
-     * @return mixed Unix timestamp or false if not valid
+     * @return int|false Unix timestamp or false if not valid
      */
     private function getTimeStamp($name, $time = '0:00:00')
     {
@@ -146,27 +166,46 @@ class Admin_HolidaysController extends AuthenticatedController
     /**
      * Adds the content to sidebar
      */
-    private function setSidebar()
+    private function setSidebar(string $action): void
     {
         $sidebar = Sidebar::Get();
 
-        $views = new ViewsWidget();
-        $views->addLink(_('Alle Einträge'),
-                        $this->url_for('admin/holidays', ['filter' => null]))
-              ->setActive(!$this->filter);
-        $views->addLink(_('Aktuelle/zukünftige Einträge'),
-                        $this->url_for('admin/holidays', ['filter' => 'current']))
-              ->setActive($this->filter === 'current');
-        $views->addLink(_('Vergangene Einträge'),
-                        $this->url_for('admin/holidays', ['filter' => 'past']))
-              ->setActive($this->filter === 'past');
-        $sidebar->addWidget($views);
-
-        $links = new ActionsWidget();
-        $links->addLink(_('Neue Ferien anlegen'),
-                        $this->url_for('admin/holidays/edit', ['filter' => null]),
-                        Icon::create('add', 'clickable'))
-              ->asDialog('size=auto');
-        $sidebar->addWidget($links);
+        $is_vacation_view = !in_array($action, ['holidays', 'store_holidays']);
+
+        $views = $sidebar->addWidget(new ViewsWidget());
+        $views->addLink(
+            _('Ferien'),
+            $this->indexURL()
+        )->setActive($is_vacation_view);
+        $views->addLink(
+            _('Feiertage'),
+            $this->holidaysURL()
+        )->setActive(!$is_vacation_view);
+
+        if (!$is_vacation_view) {
+            return;
+        }
+
+        $views = $sidebar->addWidget(new ViewsWidget());
+        $views->setTitle(_('Ansichtseinstellungen'));
+        $views->addLink(
+            _('Alle Einträge'),
+            $this->indexURL(['filter' => null])
+        )->setActive(!$this->filter);
+        $views->addLink(
+            _('Aktuelle/zukünftige Einträge'),
+            $this->indexURL(['filter' => 'current'])
+        )->setActive($this->filter === 'current');
+        $views->addLink(
+            _('Vergangene Einträge'),
+            $this->indexURL(['filter' => 'past'])
+        )->setActive($this->filter === 'past');
+
+        $links = $sidebar->addWidget(new ActionsWidget());
+        $links->addLink(
+            _('Neue Ferien anlegen'),
+            $this->editURL(['filter' => null]),
+            Icon::create('add')
+        )->asDialog('size=auto');
     }
 }
diff --git a/app/views/admin/holidays/holidays.php b/app/views/admin/holidays/holidays.php
new file mode 100644
index 00000000000..2471f5273ac
--- /dev/null
+++ b/app/views/admin/holidays/holidays.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * @var Admin_HolidaysController $controller
+ * @var array<int, array{name: string, col: int}> $holidays
+ * @var int[] $customized
+ */
+?>
+<form action="<?= $controller->store_holidays() ?>" method="post" class="default">
+    <?= CSRFProtection::tokenTag() ?>
+
+    <table class="default">
+        <caption><?= _('Feiertage') ?></caption>
+        <colgroup>
+            <col>
+            <col style="width: 20%">
+        </colgroup>
+        <thead>
+        <tr>
+            <th><?= _('Feiertag') ?></th>
+            <th style="text-align: center">
+                <?= _('Als "gesetzlich" festlegen') ?>
+            </th>
+        </tr>
+        </thead>
+        <tbody>
+        <? foreach ($holidays as $id => $holiday): ?>
+            <tr>
+                <td>
+                    <label for="holiday-<?= htmlReady($id) ?>" class="undecorated">
+                        <?= htmlReady($holiday['name']) ?>
+                    </label>
+                </td>
+                <td style="text-align: center">
+                    <input type="checkbox"
+                           id="holiday-<?= htmlReady($id) ?>"
+                           name="holidays[]"
+                           value="<?= htmlReady($id) ?>"
+                           <? if ($holiday['col'] === Holidays::WEIGHT_PUBLIC_HOLIDAY || in_array($id, $customized)) echo 'checked'; ?>
+                           <? if ($holiday['col'] === Holidays::WEIGHT_PUBLIC_HOLIDAY) echo 'disabled'; ?>>
+                </td>
+            </tr>
+        <? endforeach; ?>
+        </tbody>
+        <tfoot>
+            <tr>
+                <td colspan="2">
+                    <?= Studip\Button::createAccept(_('Speichern')) ?>
+                </td>
+            </tr>
+        </tfoot>
+    </table>
+</form>
diff --git a/db/migrations/6.0.26_add_customized_holidays_configuration.php b/db/migrations/6.0.26_add_customized_holidays_configuration.php
new file mode 100644
index 00000000000..cb21301a7a7
--- /dev/null
+++ b/db/migrations/6.0.26_add_customized_holidays_configuration.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @see https://gitlab.studip.de/studip/studip/-/issues/2795
+ */
+final class AddCustomizedHolidaysConfiguration extends Migration
+{
+    public function description()
+    {
+        return 'Adds a confiugration for customized holidays';
+    }
+
+    protected function up()
+    {
+        $query = "INSERT INTO `config` (
+                      `field`, `value`, `type`, `range`, `section`,
+                      `mkdate`, `chdate`,
+                      `description`
+                  ) VALUES (
+                      'CUSTOMIZED_HOLIDAYS', '[]', 'array', 'global', 'global',
+                      UNIX_TIMESTAMP(), UNIX_TIMESTAMP(),
+                      'Speichert die internen Ids von Feiertagen, die als gesetztlich markiert werden sollen'
+                  )";
+        DBManager::get()->exec($query);
+    }
+
+    protected function down()
+    {
+        $query = "DELETE `config`, `config_values`
+                  FROM `config`
+                  LEFT JOIN `config_values` USING (`field`)
+                  WHERE `field` = 'CUSTOMIZED_HOLIDAYS'";
+        DBManager::get()->exec($query);
+    }
+}
diff --git a/lib/calendar_functions.inc.php b/lib/calendar_functions.inc.php
index deec85a19ec..b35e9a1c904 100644
--- a/lib/calendar_functions.inc.php
+++ b/lib/calendar_functions.inc.php
@@ -36,112 +36,17 @@
 // +---------------------------------------------------------------------------+
 
 
-// Hier jezt die ultimative Feiertags-"Berechnung"
-// Zurueckgegeben wird ein Array mit Namen des Feiertages ("name") und
-// Faerbungsgrad ("col", 0 bis 2).
-
-function holiday ($tmstamp, $mod = "") {
-    // erstmal brauchen wir den Ostersonntag fuer die meisten kirchlichen Feiertage
-//  $easterday = easter_date(date("Y", $tmstamp)); // geht leider nicht
-    // Berechnung nach Carters Algorithmus (gueltig von 1900 - 2099)
-    $tmstamp = mktime(0,0,0,date("n",$tmstamp),date("j",$tmstamp),date("Y",$tmstamp));
-    $year = date("Y", $tmstamp);
-    $b = 225 - 11 * ($year % 19);
-    $d = (($b - 21) % 30) + 21;
-    if ($d > 48)
-        $d--;
-    $e = ($year + abs($year / 4) + $d + 1) % 7;
-    $q = $d + 7 - $e;
-    if ($q < 32)
-        $easterday = date("z", mktime(0, 0, 0, 3, $q, $year)) + 1;
-    else
-        $easterday = date("z", mktime(0, 0, 0, 4, $q - 31, $year)) + 1;
-
-    $name = '';
-    // Differenz in Tagen zu Ostertag berechnen
-    $doy = date("z", $tmstamp) + 1;
-    $dif = $doy - $easterday;
-    switch ($dif) {
-        case -48: $name = _("Rosenmontag"); $col = 1; break;
-        case -47: $name = _("Fastnacht"); $col = 1; break;
-        case -46: $name = _("Aschermittwoch"); $col = 1; break;
-    //  case -8: $name = _("Palmsonntag"); $col = 1; break;
-        case  -2: $name = _("Karfreitag"); $col = 3; break;
-        case   0: $name = _("Ostersonntag"); $col = 3; break;
-        case   1: $name = _("Ostermontag"); $col = 3; break;
-        case  39: $name = _("Christi Himmelfahrt"); $col = 3; break;
-        case  49: $name = _("Pfingstsonntag"); $col = 3; break;
-        case  50: $name = _("Pfingstmontag"); $col = 3; break;
-        case  60: $name = _("Fronleichnam"); $col = 1; break;
-    }
-
-    // die unveraenderlichen Feiertage
-    switch ($doy) {
-        case   1: $name = _("Neujahr"); $col = 3; break;
-        case   6: $name = _("Hl. Drei Könige"); $col = 1; break;
-    }
-
-    // Schaltjahre nicht vergessen
-    if (date("L", $tmstamp))
-        $doy--;
-    switch ($doy) {
-        case  79: $name = _("Frühlingsanfang"); $col = 1; break;
-        case 121: $name = _("Maifeiertag"); $col = 3; break;
-//      case 125: $name = _("Europatag"); $col = 1; break;
-        case 172: $name = _("Sommeranfang"); $col = 1; break;
-        case 266: $name = _("Herbstanfang"); $col = 1; break;
-        case 276: $name = _("Tag der deutschen Einheit"); $col = 3; break;
-        case 304: $name = _("Reformationstag"); $col = $year == 2017 ? 3 : 2; break;
-        case 305: $name = _("Allerheiligen"); $col = 1; break;
-        case 315: $name = _("Martinstag"); $col = 1; break;
-        case 340: $name = _("Nikolaus"); $col = 1; break;
-        case 355: $name = _("Winteranfang"); $col = 1; break;
-        case 358: $name = _("Hl. Abend"); $col = 1; break;
-        case 359: $name = _("1. Weihnachtstag"); $col = 3; break;
-        case 360: $name = _("2. Weihnachtstag"); $col = 3; break;
-        case 365: $name = _("Silvester"); $col = 1; break;
-    }
-
-    // Die Sonntagsfeiertage
-    if (date("w", $tmstamp) == 0) {
-        if ($doy > 127 && $doy < 135) {
-            $name = _("Muttertag");
-            $col = 1;
-        }
-        else if ($doy > 266 && $doy < 274) {
-            $name = _("Erntedank");
-            $col = 1;
-        }
-        else if ($doy > 316 && $doy < 324) {
-            $name = _("Volkstrauertag");
-            $col = 2;
-        }
-        else if ($doy > 323 && $doy < 331) {
-            $name = _("Totensonntag");
-            $col = 1;
-        }
-        else if ($doy > 330 && $doy < 338) {
-            $name = _("1. Advent");
-            $col = 2;
-        }
-        else if ($doy > 337 && $doy < 345) {
-            $name = _("2. Advent");
-            $col = 2;
-        }
-        else if ($doy > 344 && $doy < 352) {
-            $name = _("3. Advent");
-            $col = 2;
-        }
-        else if ($doy > 351 && $doy < 359) {
-            $name = _("4. Advent");
-            $col = 2;
-        }
-    }
-
-    if ($name)
-        return ["name" => $name, "col" => $col];
-
-    return FALSE;
+/**
+ * Hier jezt die ultimative Feiertags-"Berechnung"
+ * Zurueckgegeben wird ein Array mit Namen des Feiertages ("name") und
+ * Faerbungsgrad ("col", 1 bis 3; 3 bedeutet Termin fällt aus).
+ *
+ * @param $tmstamp
+ * @return array{name: string, col: int}|false
+ * @see Holidays::isHoliday()
+ */
+function holiday ($tmstamp) {
+    return Holidays::isHoliday($tmstamp);
 }
 
 // ueberprueft eine Datumsangabe, die in einen Timestamp gewandelt werden soll
diff --git a/lib/classes/Holidays.php b/lib/classes/Holidays.php
new file mode 100644
index 00000000000..682952dfba7
--- /dev/null
+++ b/lib/classes/Holidays.php
@@ -0,0 +1,379 @@
+<?php
+
+/**
+ * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
+ * @since Stud.IP 5.5
+ */
+final class Holidays
+{
+    public const HOLIDAY_ALL_SAINTS_DAY = 0;
+    public const HOLIDAY_ASCENSION_DAY = 1;
+    public const HOLIDAY_ASH_WEDNESDAY = 2;
+    public const HOLIDAY_CARNIVAL = 3;
+    public const HOLIDAY_CHRISTMAS_DAY = 4;
+    public const HOLIDAY_CHRISTMAS_DAY_2 = 5;
+    public const HOLIDAY_CHRISTMAS_EVE = 6;
+    public const HOLIDAY_CORPUS_CHRISTI = 7;
+    public const HOLIDAY_EASTER_MONDAY = 8;
+    public const HOLIDAY_EASTER_SUNDAY = 9;
+    public const HOLIDAY_EPIPHANY = 10;
+    public const HOLIDAY_FIRST_SUNDAY_OF_ADVENT = 11;
+    public const HOLIDAY_FOURTH_SUNDAY_OF_ADVENT = 12;
+    public const HOLIDAY_GERMAN_UNITY_DAY = 13;
+    public const HOLIDAY_GOOD_FRIDAY = 14;
+    public const HOLIDAY_MARTINMAS = 15;
+    public const HOLIDAY_MAY_DAY = 16;
+    public const HOLIDAY_MOTHERS_DAY = 17;
+    public const HOLIDAY_NEW_YEAR = 18;
+    public const HOLIDAY_NEW_YEARS_EVE = 19;
+    public const HOLIDAY_REFORMATION_DAY = 20;
+    public const HOLIDAY_REMEMBRANCE_DAY = 21;
+    public const HOLIDAY_SECOND_SUNDAY_OF_ADVENT = 22;
+    public const HOLIDAY_SHROVE_MONDAY = 23;
+    public const HOLIDAY_START_OF_AUTUMN = 24;
+    public const HOLIDAY_START_OF_SPRING = 25;
+    public const HOLIDAY_START_OF_SUMMER = 26;
+    public const HOLIDAY_START_OF_WINTER = 27;
+    public const HOLIDAY_ST_NICHOLAS_DAY = 28;
+    public const HOLIDAY_SUNDAY_OF_THE_DEAD = 29;
+    public const HOLIDAY_THANKSGIVING = 30;
+    public const HOLIDAY_THIRD_SUNDAY_OF_ADVENT = 31;
+    public const HOLIDAY_WHIT_MONDAY = 32;
+    public const HOLIDAY_WHIT_SUNDAY = 33;
+    public const HOLIDAY_INTERNATIONAL_WOMENS_DAY = 34;
+    public const HOLIDAY_PEACE_FESTIVAL = 35;
+    public const HOLIDAY_ASSUMPTION_DAY = 36;
+    public const HOLIDAY_WORLD_CHILDRENS_DAY = 37;
+    public const HOLIDAY_DAY_OF_PRAYER_AND_REPENTANCE = 38;
+
+    public const WEIGHT_HOLIDAY = 1;
+    public const WEIGHT_OTHER_HOLIDAY = 2;
+    public const WEIGHT_PUBLIC_HOLIDAY = 3;
+
+    private const PUBLIC_HOLIDAYS = [
+        self::HOLIDAY_ASCENSION_DAY,
+        self::HOLIDAY_CHRISTMAS_DAY,
+        self::HOLIDAY_CHRISTMAS_DAY_2,
+        self::HOLIDAY_EASTER_MONDAY,
+        self::HOLIDAY_EASTER_SUNDAY,
+        self::HOLIDAY_GERMAN_UNITY_DAY,
+        self::HOLIDAY_GOOD_FRIDAY,
+        self::HOLIDAY_MAY_DAY,
+        self::HOLIDAY_NEW_YEAR,
+        self::HOLIDAY_WHIT_MONDAY,
+        self::HOLIDAY_WHIT_SUNDAY,
+    ];
+
+    private const OTHER_HOLIDAYS = [
+        self::HOLIDAY_FIRST_SUNDAY_OF_ADVENT,
+        self::HOLIDAY_FOURTH_SUNDAY_OF_ADVENT,
+        self::HOLIDAY_REMEMBRANCE_DAY,
+        self::HOLIDAY_SECOND_SUNDAY_OF_ADVENT,
+        self::HOLIDAY_THIRD_SUNDAY_OF_ADVENT,
+    ];
+
+    public static function getHolidays(bool $sorted = true, bool $ignore_customized_holidays = false): array
+    {
+        $reflection = new ReflectionClass(self::class);
+        $holiday_constants = array_filter(
+            $reflection->getConstants(),
+            function (string $constant): bool {
+                return str_starts_with($constant, 'HOLIDAY_');
+            },
+            ARRAY_FILTER_USE_KEY
+        );
+
+        $holidays = array_map(
+            function (int $id): array {
+                return [
+                    'name' => self::translateId($id),
+                    'col' => self::getHolidaySignificance($id, time(), true),
+                ];
+            },
+            array_values($holiday_constants)
+        );
+
+        if ($sorted) {
+            uasort(
+                $holidays,
+                function ($a, $b) {
+                    return strnatcasecmp($a['name'], $b['name']);
+                }
+            );
+        }
+
+        return $holidays;
+    }
+
+    /**
+     * @param int $timestamp
+     *
+     * @return array{name: string, col: int}|false
+     */
+    public static function isHoliday(int $timestamp)
+    {
+        $holiday_id = self::getHolidayId($timestamp);
+        if (!$holiday_id) {
+            return false;
+        }
+
+        return [
+            'name' => self::translateId($holiday_id),
+            'col'  => self::getHolidaySignificance($holiday_id, $timestamp),
+        ];
+    }
+
+    private static function getHolidayId(int $timestamp): ?int
+    {
+        // erstmal brauchen wir den Ostersonntag fuer die meisten kirchlichen Feiertage
+        //  $easterday = easter_date(date("Y", $timestamp)); // geht leider nicht
+        // Berechnung nach Carters Algorithmus (gueltig von 1900 - 2099)
+        $timestamp = mktime(
+            0, 0, 0,
+            date('n', $timestamp),
+            date('j', $timestamp),
+            date('Y', $timestamp)
+        );
+        $year = date('Y', $timestamp);
+        $b = 225 - 11 * ($year % 19);
+        $d = (($b - 21) % 30) + 21;
+        if ($d > 48) {
+            $d--;
+        }
+        $e = ($year + abs($year / 4) + $d + 1) % 7;
+        $q = $d + 7 - $e;
+        if ($q < 32) {
+            $easterday = date('z', mktime(0, 0, 0, 3, $q, $year)) + 1;
+        } else {
+            $easterday = date('z', mktime(0, 0, 0, 4, $q - 31, $year)) + 1;
+        }
+
+        $id = null;
+        $col = 1;
+        // Differenz in Tagen zu Ostertag berechnen
+        $doy = date("z", $timestamp) + 1;
+        $dif = $doy - $easterday;
+        switch ($dif) {
+            case -48:
+                return self::HOLIDAY_SHROVE_MONDAY;
+            case -47:
+                return self::HOLIDAY_CARNIVAL;
+            case -46:
+                return self::HOLIDAY_ASH_WEDNESDAY;
+            case  -2:
+                return self::HOLIDAY_GOOD_FRIDAY;
+            case   0:
+                return self::HOLIDAY_EASTER_SUNDAY;
+            case   1:
+                return self::HOLIDAY_EASTER_MONDAY;
+            case  39:
+                return self::HOLIDAY_ASCENSION_DAY;
+            case  49:
+                return self::HOLIDAY_WHIT_SUNDAY;
+            case  50:
+                return self::HOLIDAY_WHIT_MONDAY;
+            case  60:
+                return self::HOLIDAY_CORPUS_CHRISTI;
+        }
+
+        // die unveraenderlichen Feiertage
+        switch ($doy) {
+            case 1:
+                return self::HOLIDAY_NEW_YEAR;
+            case 6:
+                return self::HOLIDAY_EPIPHANY;
+        }
+
+        // Schaltjahre nicht vergessen
+        if (date('L', $timestamp)) {
+            $doy -= 1;
+        }
+        switch ($doy) {
+            case 67:
+                return self::HOLIDAY_INTERNATIONAL_WOMENS_DAY;
+            case 79:
+                return self::HOLIDAY_START_OF_SPRING;
+            case 121:
+                return self::HOLIDAY_MAY_DAY;
+            case 172:
+                return self::HOLIDAY_START_OF_SUMMER;
+            case 220:
+                return self::HOLIDAY_PEACE_FESTIVAL;
+            case 227:
+                return self::HOLIDAY_ASSUMPTION_DAY;
+            case 263:
+                return self::HOLIDAY_WORLD_CHILDRENS_DAY;
+            case 266:
+                return self::HOLIDAY_START_OF_AUTUMN;
+            case 276:
+                return self::HOLIDAY_GERMAN_UNITY_DAY;
+            case 304:
+                return self::HOLIDAY_REFORMATION_DAY;
+            case 305:
+                return self::HOLIDAY_ALL_SAINTS_DAY;
+            case 315:
+                return self::HOLIDAY_MARTINMAS;
+            case 340:
+                return self::HOLIDAY_ST_NICHOLAS_DAY;
+            case 355:
+                return self::HOLIDAY_START_OF_WINTER;
+            case 358:
+                return self::HOLIDAY_CHRISTMAS_EVE;
+            case 359:
+                return self::HOLIDAY_CHRISTMAS_DAY;
+            case 360:
+                return self::HOLIDAY_CHRISTMAS_DAY_2;
+            case 365:
+                return self::HOLIDAY_NEW_YEARS_EVE;
+        }
+
+        // Buß- und Bettag am Mittwoch vor dem 23.11.
+        if (date('w', $timestamp) == 3 && $doy > 319 && $doy < 327) {
+            return self::HOLIDAY_DAY_OF_PRAYER_AND_REPENTANCE;
+        }
+
+        // Die Sonntagsfeiertage
+        if (date('w', $timestamp) == 0) {
+            if ($doy > 127 && $doy < 135) {
+                return self::HOLIDAY_MOTHERS_DAY;
+            }
+            if ($doy > 266 && $doy < 274) {
+                return self::HOLIDAY_THANKSGIVING;
+            }
+            if ($doy > 316 && $doy < 324) {
+                return self::HOLIDAY_REMEMBRANCE_DAY;
+            }
+            if ($doy > 323 && $doy < 331) {
+                return self::HOLIDAY_SUNDAY_OF_THE_DEAD;
+            }
+            if ($doy > 330 && $doy < 338) {
+                return self::HOLIDAY_FIRST_SUNDAY_OF_ADVENT;
+            }
+            if ($doy > 337 && $doy < 345) {
+                return self::HOLIDAY_SECOND_SUNDAY_OF_ADVENT;
+            }
+            if ($doy > 344 && $doy < 352) {
+                return self::HOLIDAY_THIRD_SUNDAY_OF_ADVENT;
+            }
+            if ($doy > 351 && $doy < 359) {
+                return self::HOLIDAY_FOURTH_SUNDAY_OF_ADVENT;
+            }
+        }
+
+        return null;
+    }
+
+    private static function getHolidaySignificance(
+        int $holiday_id,
+        int $timestamp,
+        bool $ignore_customized_holidays = false
+    ): int
+    {
+        if (!$ignore_customized_holidays) {
+            $customized_holidays = Config::get()->CUSTOMIZED_HOLIDAYS;
+            if (in_array($holiday_id, $customized_holidays)) {
+                return self::WEIGHT_PUBLIC_HOLIDAY;
+            }
+        }
+
+        if ($holiday_id === self::HOLIDAY_REFORMATION_DAY) {
+            return date('Y', $timestamp) == 2017 ? self::WEIGHT_PUBLIC_HOLIDAY : self::WEIGHT_OTHER_HOLIDAY;
+        }
+
+        if (in_array($holiday_id, self::PUBLIC_HOLIDAYS)) {
+            return self::WEIGHT_PUBLIC_HOLIDAY;
+        }
+
+        if (in_array($holiday_id, self::OTHER_HOLIDAYS)) {
+            return self::WEIGHT_OTHER_HOLIDAY;
+        }
+
+        return self::WEIGHT_HOLIDAY;
+    }
+
+    private static function translateId(int $id): string
+    {
+        switch ($id) {
+            case self::HOLIDAY_ASH_WEDNESDAY:
+                return _('Aschermittwoch');
+            case self::HOLIDAY_CARNIVAL:
+                return _('Fastnacht');
+            case self::HOLIDAY_SHROVE_MONDAY:
+                return _('Rosenmontag');
+            case self::HOLIDAY_GOOD_FRIDAY:
+                return _('Karfreitag');
+            case self::HOLIDAY_EASTER_SUNDAY:
+                return _('Ostersonntag');
+            case self::HOLIDAY_EASTER_MONDAY:
+                return _('Ostermontag');
+            case self::HOLIDAY_ASCENSION_DAY:
+                return _('Christi Himmelfahrt');
+            case self::HOLIDAY_WHIT_SUNDAY:
+                return _('Pfingstsonntag');
+            case self::HOLIDAY_WHIT_MONDAY:
+                return _('Pfingstmontag');
+            case self::HOLIDAY_CORPUS_CHRISTI:
+                return _('Fronleichnam');
+            case self::HOLIDAY_NEW_YEAR:
+                return _('Neujahr');
+            case self::HOLIDAY_EPIPHANY:
+                return _('Hl. Drei Könige');
+            case self::HOLIDAY_START_OF_SPRING:
+                return _('Frühlingsanfang');
+            case self::HOLIDAY_MAY_DAY:
+                return _('Maifeiertag');
+            case self::HOLIDAY_START_OF_SUMMER:
+                return _('Sommeranfang');
+            case self::HOLIDAY_START_OF_AUTUMN:
+                return _('Herbstanfang');
+            case self::HOLIDAY_GERMAN_UNITY_DAY:
+                return _('Tag der deutschen Einheit');
+            case self::HOLIDAY_REFORMATION_DAY:
+                return _('Reformationstag');
+            case self::HOLIDAY_ALL_SAINTS_DAY:
+                return _('Allerheiligen');
+            case self::HOLIDAY_MARTINMAS:
+                return _('Martinstag');
+            case self::HOLIDAY_ST_NICHOLAS_DAY:
+                return _('Nikolaus');
+            case self::HOLIDAY_START_OF_WINTER:
+                return _('Winteranfang');
+            case self::HOLIDAY_CHRISTMAS_EVE:
+                return _('Hl. Abend');
+            case self::HOLIDAY_CHRISTMAS_DAY:
+                return _('1. Weihnachtstag');
+            case self::HOLIDAY_CHRISTMAS_DAY_2:
+                return _('2. Weihnachtstag');
+            case self::HOLIDAY_NEW_YEARS_EVE:
+                return _('Silvester');
+            case self::HOLIDAY_MOTHERS_DAY:
+                return _('Muttertag');
+            case self::HOLIDAY_THANKSGIVING:
+                 return _('Erntedank');
+            case self::HOLIDAY_REMEMBRANCE_DAY:
+                return _('Volkstrauertag');
+            case self::HOLIDAY_SUNDAY_OF_THE_DEAD:
+                return _('Totensonntag');
+            case self::HOLIDAY_FIRST_SUNDAY_OF_ADVENT:
+                return _('1. Advent');
+            case self::HOLIDAY_SECOND_SUNDAY_OF_ADVENT:
+                return _('2. Advent');
+            case self::HOLIDAY_THIRD_SUNDAY_OF_ADVENT:
+                return _('3. Advent');
+            case self::HOLIDAY_FOURTH_SUNDAY_OF_ADVENT:
+                return _('4. Advent');
+            case self::HOLIDAY_INTERNATIONAL_WOMENS_DAY:
+                return _('Internationaler Frauentag');
+            case self::HOLIDAY_PEACE_FESTIVAL:
+                return _('Friedensfest');
+            case self::HOLIDAY_ASSUMPTION_DAY:
+                return _('Mariä Himmelfahrt');
+            case self::HOLIDAY_WORLD_CHILDRENS_DAY:
+                return _('Weltkindertag');
+            case self::HOLIDAY_DAY_OF_PRAYER_AND_REPENTANCE:
+                return _('Buß- und Bettag');
+            default:
+                throw new InvalidArgumentException("Invalid holiday id {$id}");
+        }
+    }
+}
diff --git a/lib/navigation/AdminNavigation.php b/lib/navigation/AdminNavigation.php
index 1b86ca11d50..1dab05bf22e 100644
--- a/lib/navigation/AdminNavigation.php
+++ b/lib/navigation/AdminNavigation.php
@@ -100,7 +100,7 @@ class AdminNavigation extends Navigation
         if ($perm->have_perm('root')) {
             if (Config::get()->SEMESTER_ADMINISTRATION_ENABLE) {
                 $navigation->addSubNavigation('semester', new Navigation(_('Semester'), 'dispatch.php/admin/semester'));
-                $navigation->addSubNavigation('holidays', new Navigation(_('Ferien'), 'dispatch.php/admin/holidays'));
+                $navigation->addSubNavigation('holidays', new Navigation(_('Ferien und Feiertage'), 'dispatch.php/admin/holidays'));
             }
 
             if (Config::get()->EXTERN_ENABLE) {
-- 
GitLab