diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index cbab857e67..bcd47e7633 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -22,7 +22,7 @@ jobs:
steps:
- uses: actions/checkout@v7.0.1
- name: Set up Python
- uses: actions/setup-python@v6.3.0
+ uses: actions/setup-python@v7.0.0
with:
python-version: "3.13"
- name: Install dependencies
diff --git a/.github/workflows/lint-and-analyse-php.yml b/.github/workflows/lint-and-analyse-php.yml
index 9d4b84da0c..e2db00d093 100644
--- a/.github/workflows/lint-and-analyse-php.yml
+++ b/.github/workflows/lint-and-analyse-php.yml
@@ -23,7 +23,7 @@ jobs:
- uses: actions/checkout@v7.0.1
with:
fetch-depth: 0
- - uses: actions/setup-python@v6.3.0
+ - uses: actions/setup-python@v7.0.0
with:
python-version: "3.13"
- run: pip install --upgrade tox
diff --git a/Presenters/Admin/ManageConfigurationPresenter.php b/Presenters/Admin/ManageConfigurationPresenter.php
index 38757d42dd..04e18e6019 100644
--- a/Presenters/Admin/ManageConfigurationPresenter.php
+++ b/Presenters/Admin/ManageConfigurationPresenter.php
@@ -224,6 +224,10 @@ public function Update()
}
}
+ if (!$isPluginConfig) {
+ $mergedSettings = $this->EnableIcsSubscriptionIfNeeded($mergedSettings);
+ }
+
Log::Debug('Saving %s settings', count($configSettings));
// WriteSettings will handle the proper nesting based on file type
@@ -242,6 +246,42 @@ public function SetHomepage()
ServiceLocator::GetDatabase()->Execute($command);
}
+ /**
+ * Generates and stores an ICS subscription key when the admin enables ICS
+ * through this settings page without setting a key, so subscription links
+ * do not remain broken until an unrelated schedule/resource/user toggle
+ * triggers key generation.
+ *
+ * @param array $mergedSettings
+ * @return array
+ */
+ private function EnableIcsSubscriptionIfNeeded(array $mergedSettings): array
+ {
+ if (!isset($mergedSettings['ics']) || !is_array($mergedSettings['ics'])) {
+ return $mergedSettings;
+ }
+
+ if (ConfigKeys::hasEnv(ConfigKeys::ICS_SUBSCRIPTION_KEY)) {
+ return $mergedSettings;
+ }
+
+ $icsEnabled = filter_var(
+ $mergedSettings['ics']['enabled'] ?? ConfigKeys::ICS_ENABLED['default'],
+ FILTER_VALIDATE_BOOLEAN
+ );
+
+ $newKey = ConfigurationFile::GenerateSubscriptionKeyIfNeeded(
+ $icsEnabled,
+ $mergedSettings['ics']['subscription.key'] ?? ''
+ );
+
+ if ($newKey !== null) {
+ $mergedSettings['ics']['subscription.key'] = $newKey;
+ }
+
+ return $mergedSettings;
+ }
+
private function ShouldBeSkipped(string $key, ?ConfigKey $meta): bool
{
if ($meta === null) {
diff --git a/Web/assets/vendor/datatables-buttons/2.4.2/css/buttons.bootstrap5.min.css b/Web/assets/vendor/datatables-buttons/2.4.2/css/buttons.bootstrap5.min.css
deleted file mode 100644
index 3229c89006..0000000000
--- a/Web/assets/vendor/datatables-buttons/2.4.2/css/buttons.bootstrap5.min.css
+++ /dev/null
@@ -1 +0,0 @@
-@keyframes dtb-spinner{100%{transform:rotate(360deg)}}@-o-keyframes dtb-spinner{100%{-o-transform:rotate(360deg);transform:rotate(360deg)}}@-ms-keyframes dtb-spinner{100%{-ms-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes dtb-spinner{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-moz-keyframes dtb-spinner{100%{-moz-transform:rotate(360deg);transform:rotate(360deg)}}div.dataTables_wrapper{position:relative}div.dt-buttons{position:initial}div.dt-buttons .dt-button{overflow:hidden;text-overflow:ellipsis}div.dt-button-info{position:fixed;top:50%;left:50%;width:400px;margin-top:-100px;margin-left:-200px;background-color:white;border-radius:.75em;box-shadow:3px 4px 10px 1px rgba(0, 0, 0, 0.8);text-align:center;z-index:2003;overflow:hidden}div.dt-button-info h2{padding:2rem 2rem 1rem 2rem;margin:0;font-weight:normal}div.dt-button-info>div{padding:1em 2em 2em 2em}div.dtb-popover-close{position:absolute;top:6px;right:6px;width:22px;height:22px;text-align:center;border-radius:3px;cursor:pointer;z-index:2003}button.dtb-hide-drop{display:none !important}div.dt-button-collection-title{text-align:center;padding:.3em 0 .5em;margin-left:.5em;margin-right:.5em;font-size:.9em}div.dt-button-collection-title:empty{display:none}span.dt-button-spacer{display:inline-block;margin:.5em;white-space:nowrap}span.dt-button-spacer.bar{border-left:1px solid rgba(0, 0, 0, 0.3);vertical-align:middle;padding-left:.5em}span.dt-button-spacer.bar:empty{height:1em;width:1px;padding-left:0}div.dt-button-collection .dt-button-active{padding-right:3em}div.dt-button-collection .dt-button-active:after{position:absolute;top:50%;margin-top:-10px;right:1em;display:inline-block;content:"✓";color:inherit}div.dt-button-collection .dt-button-active.dt-button-split{padding-right:0}div.dt-button-collection .dt-button-active.dt-button-split:after{display:none}div.dt-button-collection .dt-button-active.dt-button-split>*:first-child{padding-right:3em}div.dt-button-collection .dt-button-active.dt-button-split>*:first-child:after{position:absolute;top:50%;margin-top:-10px;right:1em;display:inline-block;content:"✓";color:inherit}div.dt-button-collection .dt-button-active-a a{padding-right:3em}div.dt-button-collection .dt-button-active-a a:after{position:absolute;right:1em;display:inline-block;content:"✓";color:inherit}div.dt-button-collection span.dt-button-spacer{width:100%;font-size:.9em;text-align:center;margin:.5em 0}div.dt-button-collection span.dt-button-spacer:empty{height:0;width:100%}div.dt-button-collection span.dt-button-spacer.bar{border-left:none;border-bottom:1px solid rgba(0, 0, 0, 0.1);padding-left:0}html.dark div.dt-button-info{background-color:var(--dt-html-background);border:1px solid rgba(255, 255, 255, 0.15)}div.dt-buttons div.btn-group{position:initial}div.dt-buttons div.dropdown-menu{margin-top:4px}div.dt-buttons div.dropdown-menu .dt-button{position:relative}div.dt-buttons div.dropdown-menu div.dt-button-split{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:flex-start;align-content:flex-start;align-items:stretch}div.dt-buttons div.dropdown-menu div.dt-button-split a:first-child{min-width:auto;flex:1 0 50px;padding-right:0}div.dt-buttons div.dropdown-menu div.dt-button-split button:last-child{min-width:33px;flex:0;background:transparent;border:none;line-height:1rem;color:var(--bs-dropdown-link-color);padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x)}div.dt-buttons div.dropdown-menu div.dt-button-split button:last-child:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}div.dt-buttons div.dropdown-menu div.dt-button-split button:last-child:after{position:relative;left:-3px}div.dt-buttons div.dropdown-menu.fixed{position:fixed;display:block;top:50%;left:50%;margin-left:-75px;border-radius:5px;background-color:white;padding:.5em}div.dt-buttons div.dropdown-menu.fixed.two-column{margin-left:-200px}div.dt-buttons div.dropdown-menu.fixed.three-column{margin-left:-225px}div.dt-buttons div.dropdown-menu.fixed.four-column{margin-left:-300px}div.dt-buttons div.dropdown-menu.fixed.columns{margin-left:-409px}@media screen and (max-width: 1024px){div.dt-buttons div.dropdown-menu.fixed.columns{margin-left:-308px}}@media screen and (max-width: 640px){div.dt-buttons div.dropdown-menu.fixed.columns{margin-left:-203px}}@media screen and (max-width: 460px){div.dt-buttons div.dropdown-menu.fixed.columns{margin-left:-100px}}div.dt-buttons div.dropdown-menu.fixed>:last-child{max-height:100vh;overflow:auto}div.dt-buttons div.dropdown-menu.two-column>:last-child,div.dt-buttons div.dropdown-menu.three-column>:last-child,div.dt-buttons div.dropdown-menu.four-column>:last-child{display:block !important;-webkit-column-gap:8px;-moz-column-gap:8px;-ms-column-gap:8px;-o-column-gap:8px;column-gap:8px}div.dt-buttons div.dropdown-menu.two-column>:last-child>*,div.dt-buttons div.dropdown-menu.three-column>:last-child>*,div.dt-buttons div.dropdown-menu.four-column>:last-child>*{-webkit-column-break-inside:avoid;break-inside:avoid}div.dt-buttons div.dropdown-menu.two-column{width:400px}div.dt-buttons div.dropdown-menu.two-column>:last-child{padding-bottom:1px;column-count:2}div.dt-buttons div.dropdown-menu.three-column{width:450px}div.dt-buttons div.dropdown-menu.three-column>:last-child{padding-bottom:1px;column-count:3}div.dt-buttons div.dropdown-menu.four-column{width:600px}div.dt-buttons div.dropdown-menu.four-column>:last-child{padding-bottom:1px;column-count:4}div.dt-buttons div.dropdown-menu .dt-button{border-radius:0}div.dt-buttons div.dropdown-menu.columns{width:auto}div.dt-buttons div.dropdown-menu.columns>:last-child{display:flex;flex-wrap:wrap;justify-content:flex-start;align-items:center;gap:6px;width:818px;padding-bottom:1px}div.dt-buttons div.dropdown-menu.columns>:last-child .dt-button{min-width:200px;flex:0 1;margin:0}div.dt-buttons div.dropdown-menu.columns.dtb-b3>:last-child,div.dt-buttons div.dropdown-menu.columns.dtb-b2>:last-child,div.dt-buttons div.dropdown-menu.columns.dtb-b1>:last-child{justify-content:space-between}div.dt-buttons div.dropdown-menu.columns.dtb-b3 .dt-button{flex:1 1 32%}div.dt-buttons div.dropdown-menu.columns.dtb-b2 .dt-button{flex:1 1 48%}div.dt-buttons div.dropdown-menu.columns.dtb-b1 .dt-button{flex:1 1 100%}@media screen and (max-width: 1024px){div.dt-buttons div.dropdown-menu.columns>:last-child{width:612px}}@media screen and (max-width: 640px){div.dt-buttons div.dropdown-menu.columns>:last-child{width:406px}div.dt-buttons div.dropdown-menu.columns.dtb-b3 .dt-button{flex:0 1 32%}}@media screen and (max-width: 460px){div.dt-buttons div.dropdown-menu.columns>:last-child{width:200px}}div.dt-buttons span.dt-button-spacer.empty{margin:1px}div.dt-buttons span.dt-button-spacer.bar:empty{height:inherit}div.dt-buttons .btn.processing{color:rgba(0, 0, 0, 0.2)}div.dt-buttons .btn.processing:after{position:absolute;top:50%;left:50%;width:16px;height:16px;margin:-8px 0 0 -8px;box-sizing:border-box;display:block;content:" ";border:2px solid rgb(40, 40, 40);border-radius:50%;border-left-color:transparent;border-right-color:transparent;animation:dtb-spinner 1500ms infinite linear;-o-animation:dtb-spinner 1500ms infinite linear;-ms-animation:dtb-spinner 1500ms infinite linear;-webkit-animation:dtb-spinner 1500ms infinite linear;-moz-animation:dtb-spinner 1500ms infinite linear}div.dt-button-background{position:fixed;top:0;left:0;width:100%;height:100%;z-index:999}@media screen and (max-width: 767px){div.dt-buttons{float:none;width:100%;text-align:center;margin-bottom:.5em}div.dt-buttons a.btn{float:none}}:root[data-bs-theme=dark] div.dropdown-menu.dt-button-collection.fixed{background-color:rgb(33, 37, 41);border:1px solid rgba(255, 255, 255, 0.15);border-radius:8px}
diff --git a/Web/assets/vendor/datatables-buttons/2.4.2/js/buttons.bootstrap5.min.js b/Web/assets/vendor/datatables-buttons/2.4.2/js/buttons.bootstrap5.min.js
deleted file mode 100644
index e544ed9543..0000000000
--- a/Web/assets/vendor/datatables-buttons/2.4.2/js/buttons.bootstrap5.min.js
+++ /dev/null
@@ -1,4 +0,0 @@
-/*! Bootstrap integration for DataTables' Buttons
- * © SpryMedia Ltd - datatables.net/license
- */
-!function(o){var e,a;"function"==typeof define&&define.amd?define(["jquery","datatables.net-bs5","datatables.net-buttons"],function(t){return o(t,window,document)}):"object"==typeof exports?(e=require("jquery"),a=function(t,n){n.fn.dataTable||require("datatables.net-bs5")(t,n),n.fn.dataTable.Buttons||require("datatables.net-buttons")(t,n)},"undefined"==typeof window?module.exports=function(t,n){return t=t||window,n=n||e(t),a(t,n),o(n,0,t.document)}:(a(window,e),module.exports=o(e,window,window.document))):o(jQuery,window,document)}(function(o,t,n,e){"use strict";var a=o.fn.dataTable;return o.extend(!0,a.Buttons.defaults,{dom:{container:{className:"dt-buttons btn-group flex-wrap"},button:{className:"btn btn-secondary",active:"active"},collection:{action:{dropHtml:""},container:{tag:"div",className:"dropdown-menu dt-button-collection"},closeButton:!1,button:{tag:"a",className:"dt-button dropdown-item",active:"dt-button-active",disabled:"disabled",spacer:{className:"dropdown-divider",tag:"hr"}}},split:{action:{tag:"a",className:"btn btn-secondary dt-button-split-drop-button",closeButton:!1},dropdown:{tag:"button",dropHtml:"",className:"btn btn-secondary dt-button-split-drop dropdown-toggle dropdown-toggle-split",closeButton:!1,align:"split-left",splitAlignClass:"dt-button-split-left"},wrapper:{tag:"div",className:"dt-button-split btn-group",closeButton:!1}}},buttonCreated:function(t,n){return t.buttons?o('
').append(n):n}}),a.ext.buttons.collection.className+=" dropdown-toggle",a.ext.buttons.collection.rightAlignClassName="dropdown-menu-right",a});
\ No newline at end of file
diff --git a/Web/assets/vendor/datatables-buttons/2.4.2/js/buttons.colVis.min.js b/Web/assets/vendor/datatables-buttons/2.4.2/js/buttons.colVis.min.js
deleted file mode 100644
index ba1fecb75c..0000000000
--- a/Web/assets/vendor/datatables-buttons/2.4.2/js/buttons.colVis.min.js
+++ /dev/null
@@ -1,5 +0,0 @@
-/*!
- * Column visibility buttons for Buttons and DataTables.
- * © SpryMedia Ltd - datatables.net/license
- */
-!function(t){var o,i;"function"==typeof define&&define.amd?define(["jquery","datatables.net","datatables.net-buttons"],function(n){return t(n,window,document)}):"object"==typeof exports?(o=require("jquery"),i=function(n,e){e.fn.dataTable||require("datatables.net")(n,e),e.fn.dataTable.Buttons||require("datatables.net-buttons")(n,e)},"undefined"==typeof window?module.exports=function(n,e){return n=n||window,e=e||o(n),i(n,e),t(e,0,n.document)}:(i(window,o),module.exports=t(o,window,window.document))):t(jQuery,window,document)}(function(n,e,t,l){"use strict";var o=n.fn.dataTable;return n.extend(o.ext.buttons,{colvis:function(o,i){var l=null,n={extend:"collection",init:function(n,e){l=e},text:function(n){return n.i18n("buttons.colvis","Column visibility")},className:"buttons-colvis",closeButton:!1,buttons:[{extend:"columnsToggle",columns:i.columns,columnText:i.columnText}]};return o.on("column-reorder.dt"+i.namespace,function(n,e,t){o.button(null,o.button(null,l).node()).collectionRebuild([{extend:"columnsToggle",columns:i.columns,columnText:i.columnText}])}),n},columnsToggle:function(n,e){return n.columns(e.columns).indexes().map(function(n){return{extend:"columnToggle",columns:n,columnText:e.columnText}}).toArray()},columnToggle:function(n,e){return{extend:"columnVisibility",columns:e.columns,columnText:e.columnText}},columnsVisibility:function(n,e){return n.columns(e.columns).indexes().map(function(n){return{extend:"columnVisibility",columns:n,visibility:e.visibility,columnText:e.columnText}}).toArray()},columnVisibility:{columns:l,text:function(n,e,t){return t._columnText(n,t)},className:"buttons-columnVisibility",action:function(n,e,t,o){var e=e.columns(o.columns),i=e.visible();e.visible(o.visibility!==l?o.visibility:!(i.length&&i[0]))},init:function(o,n,i){var l=this;n.attr("data-cv-idx",i.columns),o.on("column-visibility.dt"+i.namespace,function(n,e){e.bDestroying||e.nTable!=o.settings()[0].nTable||l.active(o.column(i.columns).visible())}).on("column-reorder.dt"+i.namespace,function(n,e,t){i.destroying||1===o.columns(i.columns).count()&&(l.text(i._columnText(o,i)),l.active(o.column(i.columns).visible()))}),this.active(o.column(i.columns).visible())},destroy:function(n,e,t){n.off("column-visibility.dt"+t.namespace).off("column-reorder.dt"+t.namespace)},_columnText:function(n,e){var t=n.column(e.columns).index(),o=n.settings()[0].aoColumns[t].sTitle;return o=(o=o||n.column(t).header().innerHTML).replace(/\n/g," ").replace(/
/gi," ").replace(/