xref: /plugin/calendar/admin.php (revision 7e8ea635dd19058d6f7c428adbbe02d9702096d7)
11d05cddcSAtari911<?php
21d05cddcSAtari911/**
31d05cddcSAtari911 * Calendar Plugin - Admin Interface
41d05cddcSAtari911 * Clean rewrite - Configuration only
51d05cddcSAtari911 * Version: 3.3
61d05cddcSAtari911 */
71d05cddcSAtari911
81d05cddcSAtari911if(!defined('DOKU_INC')) die();
91d05cddcSAtari911
101d05cddcSAtari911class admin_plugin_calendar extends DokuWiki_Admin_Plugin {
111d05cddcSAtari911
121d05cddcSAtari911    public function getMenuText($language) {
131d05cddcSAtari911        return 'Calendar Management';
141d05cddcSAtari911    }
151d05cddcSAtari911
161d05cddcSAtari911    public function getMenuSort() {
171d05cddcSAtari911        return 100;
181d05cddcSAtari911    }
191d05cddcSAtari911
201d05cddcSAtari911    public function forAdminOnly() {
211d05cddcSAtari911        return true;
221d05cddcSAtari911    }
231d05cddcSAtari911
24*7e8ea635SAtari911    /**
25*7e8ea635SAtari911     * Public entry point for AJAX actions routed from action.php
26*7e8ea635SAtari911     */
27*7e8ea635SAtari911    public function handleAjaxAction($action) {
28*7e8ea635SAtari911        // Verify admin privileges for all admin AJAX actions
29*7e8ea635SAtari911        if (!auth_isadmin()) {
30*7e8ea635SAtari911            echo json_encode(['success' => false, 'error' => 'Admin access required']);
31*7e8ea635SAtari911            return;
32*7e8ea635SAtari911        }
33*7e8ea635SAtari911
34*7e8ea635SAtari911        switch ($action) {
35*7e8ea635SAtari911            case 'cleanup_empty_namespaces': $this->handleCleanupEmptyNamespaces(); break;
36*7e8ea635SAtari911            case 'trim_all_past_recurring': $this->handleTrimAllPastRecurring(); break;
37*7e8ea635SAtari911            case 'rescan_recurring': $this->handleRescanRecurring(); break;
38*7e8ea635SAtari911            case 'extend_recurring': $this->handleExtendRecurring(); break;
39*7e8ea635SAtari911            case 'trim_recurring': $this->handleTrimRecurring(); break;
40*7e8ea635SAtari911            case 'pause_recurring': $this->handlePauseRecurring(); break;
41*7e8ea635SAtari911            case 'resume_recurring': $this->handleResumeRecurring(); break;
42*7e8ea635SAtari911            case 'change_start_recurring': $this->handleChangeStartRecurring(); break;
43*7e8ea635SAtari911            case 'change_pattern_recurring': $this->handleChangePatternRecurring(); break;
44*7e8ea635SAtari911            default:
45*7e8ea635SAtari911                echo json_encode(['success' => false, 'error' => 'Unknown admin action']);
46*7e8ea635SAtari911        }
47*7e8ea635SAtari911    }
48*7e8ea635SAtari911
491d05cddcSAtari911    public function handle() {
501d05cddcSAtari911        global $INPUT;
511d05cddcSAtari911
521d05cddcSAtari911        $action = $INPUT->str('action');
531d05cddcSAtari911
541d05cddcSAtari911        if ($action === 'clear_cache') {
551d05cddcSAtari911            $this->clearCache();
561d05cddcSAtari911        } elseif ($action === 'save_config') {
571d05cddcSAtari911            $this->saveConfig();
581d05cddcSAtari911        } elseif ($action === 'delete_recurring_series') {
591d05cddcSAtari911            $this->deleteRecurringSeries();
601d05cddcSAtari911        } elseif ($action === 'edit_recurring_series') {
611d05cddcSAtari911            $this->editRecurringSeries();
621d05cddcSAtari911        } elseif ($action === 'move_selected_events') {
631d05cddcSAtari911            $this->moveEvents();
641d05cddcSAtari911        } elseif ($action === 'move_single_event') {
651d05cddcSAtari911            $this->moveSingleEvent();
661d05cddcSAtari911        } elseif ($action === 'delete_selected_events') {
671d05cddcSAtari911            $this->deleteSelectedEvents();
681d05cddcSAtari911        } elseif ($action === 'create_namespace') {
691d05cddcSAtari911            $this->createNamespace();
701d05cddcSAtari911        } elseif ($action === 'delete_namespace') {
711d05cddcSAtari911            $this->deleteNamespace();
729ccd446eSAtari911        } elseif ($action === 'rename_namespace') {
739ccd446eSAtari911            $this->renameNamespace();
741d05cddcSAtari911        } elseif ($action === 'run_sync') {
751d05cddcSAtari911            $this->runSync();
761d05cddcSAtari911        } elseif ($action === 'stop_sync') {
771d05cddcSAtari911            $this->stopSync();
781d05cddcSAtari911        } elseif ($action === 'upload_update') {
791d05cddcSAtari911            $this->uploadUpdate();
801d05cddcSAtari911        } elseif ($action === 'delete_backup') {
811d05cddcSAtari911            $this->deleteBackup();
821d05cddcSAtari911        } elseif ($action === 'rename_backup') {
831d05cddcSAtari911            $this->renameBackup();
841d05cddcSAtari911        } elseif ($action === 'restore_backup') {
851d05cddcSAtari911            $this->restoreBackup();
869ccd446eSAtari911        } elseif ($action === 'create_manual_backup') {
879ccd446eSAtari911            $this->createManualBackup();
881d05cddcSAtari911        } elseif ($action === 'export_config') {
891d05cddcSAtari911            $this->exportConfig();
901d05cddcSAtari911        } elseif ($action === 'import_config') {
911d05cddcSAtari911            $this->importConfig();
921d05cddcSAtari911        } elseif ($action === 'get_log') {
931d05cddcSAtari911            $this->getLog();
94*7e8ea635SAtari911        } elseif ($action === 'cleanup_empty_namespaces') {
95*7e8ea635SAtari911            $this->handleCleanupEmptyNamespaces();
96*7e8ea635SAtari911        } elseif ($action === 'trim_all_past_recurring') {
97*7e8ea635SAtari911            $this->handleTrimAllPastRecurring();
98*7e8ea635SAtari911        } elseif ($action === 'rescan_recurring') {
99*7e8ea635SAtari911            $this->handleRescanRecurring();
100*7e8ea635SAtari911        } elseif ($action === 'extend_recurring') {
101*7e8ea635SAtari911            $this->handleExtendRecurring();
102*7e8ea635SAtari911        } elseif ($action === 'trim_recurring') {
103*7e8ea635SAtari911            $this->handleTrimRecurring();
104*7e8ea635SAtari911        } elseif ($action === 'pause_recurring') {
105*7e8ea635SAtari911            $this->handlePauseRecurring();
106*7e8ea635SAtari911        } elseif ($action === 'resume_recurring') {
107*7e8ea635SAtari911            $this->handleResumeRecurring();
108*7e8ea635SAtari911        } elseif ($action === 'change_start_recurring') {
109*7e8ea635SAtari911            $this->handleChangeStartRecurring();
110*7e8ea635SAtari911        } elseif ($action === 'change_pattern_recurring') {
111*7e8ea635SAtari911            $this->handleChangePatternRecurring();
1121d05cddcSAtari911        } elseif ($action === 'clear_log') {
1131d05cddcSAtari911            $this->clearLogFile();
1141d05cddcSAtari911        } elseif ($action === 'download_log') {
1151d05cddcSAtari911            $this->downloadLog();
1161d05cddcSAtari911        } elseif ($action === 'rescan_events') {
1171d05cddcSAtari911            $this->rescanEvents();
1181d05cddcSAtari911        } elseif ($action === 'export_all_events') {
1191d05cddcSAtari911            $this->exportAllEvents();
1201d05cddcSAtari911        } elseif ($action === 'import_all_events') {
1211d05cddcSAtari911            $this->importAllEvents();
1221d05cddcSAtari911        } elseif ($action === 'preview_cleanup') {
1231d05cddcSAtari911            $this->previewCleanup();
1241d05cddcSAtari911        } elseif ($action === 'cleanup_events') {
1251d05cddcSAtari911            $this->cleanupEvents();
1264590242dSAtari911        } elseif ($action === 'save_important_namespaces') {
1274590242dSAtari911            $this->saveImportantNamespaces();
1281d05cddcSAtari911        }
1291d05cddcSAtari911    }
1301d05cddcSAtari911
1311d05cddcSAtari911    public function html() {
1321d05cddcSAtari911        global $INPUT;
1331d05cddcSAtari911
1349ccd446eSAtari911        // Get current tab - default to 'manage' (Manage Events tab)
1359ccd446eSAtari911        $tab = $INPUT->str('tab', 'manage');
1361d05cddcSAtari911
1379ccd446eSAtari911        // Get template colors
1389ccd446eSAtari911        $colors = $this->getTemplateColors();
1399ccd446eSAtari911        $accentColor = '#00cc07'; // Keep calendar plugin accent color
1409ccd446eSAtari911
1419ccd446eSAtari911        // Tab navigation (Manage Events, Update Plugin, Outlook Sync, Themes)
1429ccd446eSAtari911        echo '<div style="border-bottom:2px solid ' . $colors['border'] . '; margin:10px 0 15px 0;">';
1439ccd446eSAtari911        echo '<a href="?do=admin&page=calendar&tab=manage" style="display:inline-block; padding:8px 16px; text-decoration:none; color:' . ($tab === 'manage' ? $accentColor : $colors['text']) . '; border-bottom:3px solid ' . ($tab === 'manage' ? $accentColor : 'transparent') . '; font-weight:' . ($tab === 'manage' ? 'bold' : 'normal') . ';">�� Manage Events</a>';
1449ccd446eSAtari911        echo '<a href="?do=admin&page=calendar&tab=update" style="display:inline-block; padding:8px 16px; text-decoration:none; color:' . ($tab === 'update' ? $accentColor : $colors['text']) . '; border-bottom:3px solid ' . ($tab === 'update' ? $accentColor : 'transparent') . '; font-weight:' . ($tab === 'update' ? 'bold' : 'normal') . ';">�� Update Plugin</a>';
1459ccd446eSAtari911        echo '<a href="?do=admin&page=calendar&tab=config" style="display:inline-block; padding:8px 16px; text-decoration:none; color:' . ($tab === 'config' ? $accentColor : $colors['text']) . '; border-bottom:3px solid ' . ($tab === 'config' ? $accentColor : 'transparent') . '; font-weight:' . ($tab === 'config' ? 'bold' : 'normal') . ';">⚙️ Outlook Sync</a>';
1469ccd446eSAtari911        echo '<a href="?do=admin&page=calendar&tab=themes" style="display:inline-block; padding:8px 16px; text-decoration:none; color:' . ($tab === 'themes' ? $accentColor : $colors['text']) . '; border-bottom:3px solid ' . ($tab === 'themes' ? $accentColor : 'transparent') . '; font-weight:' . ($tab === 'themes' ? 'bold' : 'normal') . ';">�� Themes</a>';
1471d05cddcSAtari911        echo '</div>';
1481d05cddcSAtari911
1491d05cddcSAtari911        // Render appropriate tab
1501d05cddcSAtari911        if ($tab === 'config') {
1519ccd446eSAtari911            $this->renderConfigTab($colors);
1521d05cddcSAtari911        } elseif ($tab === 'manage') {
1539ccd446eSAtari911            $this->renderManageTab($colors);
1549ccd446eSAtari911        } elseif ($tab === 'themes') {
1559ccd446eSAtari911            $this->renderThemesTab($colors);
1561d05cddcSAtari911        } else {
1579ccd446eSAtari911            $this->renderUpdateTab($colors);
1581d05cddcSAtari911        }
1591d05cddcSAtari911    }
1601d05cddcSAtari911
1619ccd446eSAtari911    private function renderConfigTab($colors = null) {
1621d05cddcSAtari911        global $INPUT;
1631d05cddcSAtari911
1649ccd446eSAtari911        // Use defaults if not provided
1659ccd446eSAtari911        if ($colors === null) {
1669ccd446eSAtari911            $colors = $this->getTemplateColors();
1679ccd446eSAtari911        }
1689ccd446eSAtari911
1691d05cddcSAtari911        // Load current config
1701d05cddcSAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
1711d05cddcSAtari911        $config = [];
1721d05cddcSAtari911        if (file_exists($configFile)) {
1731d05cddcSAtari911            $config = include $configFile;
1741d05cddcSAtari911        }
1751d05cddcSAtari911
1761d05cddcSAtari911        // Show message if present
1771d05cddcSAtari911        if ($INPUT->has('msg')) {
1781d05cddcSAtari911            $msg = hsc($INPUT->str('msg'));
1791d05cddcSAtari911            $type = $INPUT->str('msgtype', 'success');
1801d05cddcSAtari911            $class = ($type === 'success') ? 'msg success' : 'msg error';
1811d05cddcSAtari911            echo "<div class=\"$class\" style=\"padding:10px; margin:10px 0; border-left:3px solid " . ($type === 'success' ? '#28a745' : '#dc3545') . "; background:" . ($type === 'success' ? '#d4edda' : '#f8d7da') . "; border-radius:3px;\">";
1821d05cddcSAtari911            echo $msg;
1831d05cddcSAtari911            echo "</div>";
1841d05cddcSAtari911        }
1851d05cddcSAtari911
1861d05cddcSAtari911        echo '<h2 style="margin:10px 0; font-size:20px;">Outlook Sync Configuration</h2>';
1871d05cddcSAtari911
1881d05cddcSAtari911        // Import/Export buttons
1891d05cddcSAtari911        echo '<div style="display:flex; gap:10px; margin-bottom:15px;">';
1901d05cddcSAtari911        echo '<button type="button" onclick="exportConfig()" style="background:#00cc07; color:white; padding:8px 16px; border:none; border-radius:3px; cursor:pointer; font-size:13px; font-weight:bold;">�� Export Config</button>';
1911d05cddcSAtari911        echo '<button type="button" onclick="document.getElementById(\'importFileInput\').click()" style="background:#7b1fa2; color:white; padding:8px 16px; border:none; border-radius:3px; cursor:pointer; font-size:13px; font-weight:bold;">�� Import Config</button>';
1921d05cddcSAtari911        echo '<input type="file" id="importFileInput" accept=".enc" style="display:none;" onchange="importConfig(this)">';
1931d05cddcSAtari911        echo '<span id="importStatus" style="margin-left:10px; font-size:12px;"></span>';
1941d05cddcSAtari911        echo '</div>';
1951d05cddcSAtari911
1961d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar" style="max-width:900px;">';
1971d05cddcSAtari911        echo '<input type="hidden" name="action" value="save_config">';
1981d05cddcSAtari911
1991d05cddcSAtari911        // Azure Credentials
2009ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">';
2011d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Microsoft Azure App Credentials</h3>';
2029ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:0.85em; margin:0 0 10px 0;">Register at <a href="https://portal.azure.com" target="_blank" style="color:#00cc07;">Azure Portal</a> → App registrations</p>';
2031d05cddcSAtari911
2041d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Tenant ID</label>';
2059ccd446eSAtari911        echo '<input type="text" name="tenant_id" value="' . hsc($config['tenant_id'] ?? '') . '" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" required style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px;">';
2061d05cddcSAtari911
2071d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Client ID (Application ID)</label>';
2089ccd446eSAtari911        echo '<input type="text" name="client_id" value="' . hsc($config['client_id'] ?? '') . '" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" required style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px;">';
2091d05cddcSAtari911
2101d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Client Secret</label>';
2119ccd446eSAtari911        echo '<input type="password" name="client_secret" value="' . hsc($config['client_secret'] ?? '') . '" placeholder="Enter client secret" required style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px;">';
2121d05cddcSAtari911        echo '<p style="color:#999; font-size:0.8em; margin:3px 0 0;">⚠️ Keep this secret safe!</p>';
2131d05cddcSAtari911        echo '</div>';
2141d05cddcSAtari911
2151d05cddcSAtari911        // Outlook Settings
2169ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">';
2171d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Outlook Settings</h3>';
2181d05cddcSAtari911
2191d05cddcSAtari911        echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">';
2201d05cddcSAtari911
2211d05cddcSAtari911        echo '<div>';
2221d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">User Email</label>';
2239ccd446eSAtari911        echo '<input type="email" name="user_email" value="' . hsc($config['user_email'] ?? '') . '" placeholder="your.email@company.com" required style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px;">';
2241d05cddcSAtari911        echo '</div>';
2251d05cddcSAtari911
2261d05cddcSAtari911        echo '<div>';
2271d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Timezone</label>';
2289ccd446eSAtari911        echo '<input type="text" name="timezone" value="' . hsc($config['timezone'] ?? 'America/Los_Angeles') . '" placeholder="America/Los_Angeles" style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px;">';
2291d05cddcSAtari911        echo '</div>';
2301d05cddcSAtari911
2311d05cddcSAtari911        echo '<div>';
2321d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Default Category</label>';
2339ccd446eSAtari911        echo '<input type="text" name="default_category" value="' . hsc($config['default_category'] ?? 'Blue category') . '" placeholder="Blue category" style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px;">';
2341d05cddcSAtari911        echo '</div>';
2351d05cddcSAtari911
2361d05cddcSAtari911        echo '<div>';
2371d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Reminder (minutes)</label>';
2389ccd446eSAtari911        echo '<input type="number" name="reminder_minutes" value="' . hsc($config['reminder_minutes'] ?? 15) . '" placeholder="15" style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px;">';
2391d05cddcSAtari911        echo '</div>';
2401d05cddcSAtari911
2411d05cddcSAtari911        echo '</div>'; // end grid
2421d05cddcSAtari911        echo '</div>';
2431d05cddcSAtari911
2441d05cddcSAtari911        // Sync Options
2459ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">';
2461d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Sync Options</h3>';
2471d05cddcSAtari911
2481d05cddcSAtari911        $syncCompleted = isset($config['sync_completed_tasks']) ? $config['sync_completed_tasks'] : false;
2491d05cddcSAtari911        echo '<label style="display:inline-block; margin:5px 15px 5px 0; font-size:13px;"><input type="checkbox" name="sync_completed_tasks" value="1" ' . ($syncCompleted ? 'checked' : '') . '> Sync completed tasks</label>';
2501d05cddcSAtari911
2511d05cddcSAtari911        $deleteOutlook = isset($config['delete_outlook_events']) ? $config['delete_outlook_events'] : true;
2521d05cddcSAtari911        echo '<label style="display:inline-block; margin:5px 15px 5px 0; font-size:13px;"><input type="checkbox" name="delete_outlook_events" value="1" ' . ($deleteOutlook ? 'checked' : '') . '> Delete from Outlook when removed</label>';
2531d05cddcSAtari911
2541d05cddcSAtari911        $syncAll = isset($config['sync_all_namespaces']) ? $config['sync_all_namespaces'] : true;
2551d05cddcSAtari911        echo '<label style="display:inline-block; margin:5px 0; font-size:13px;"><input type="checkbox" name="sync_all_namespaces" value="1" onclick="toggleNamespaceSelection(this)" ' . ($syncAll ? 'checked' : '') . '> Sync all namespaces</label>';
2561d05cddcSAtari911
2571d05cddcSAtari911        // Namespace selection (shown when sync_all is unchecked)
2581d05cddcSAtari911        echo '<div id="namespace_selection" style="margin-top:10px; ' . ($syncAll ? 'display:none;' : '') . '">';
2591d05cddcSAtari911        echo '<label style="display:block; font-weight:bold; margin-bottom:5px; font-size:13px;">Select namespaces to sync:</label>';
2601d05cddcSAtari911
2611d05cddcSAtari911        // Get available namespaces
2621d05cddcSAtari911        $availableNamespaces = $this->getAllNamespaces();
2631d05cddcSAtari911        $selectedNamespaces = isset($config['sync_namespaces']) ? $config['sync_namespaces'] : [];
2641d05cddcSAtari911
2659ccd446eSAtari911        echo '<div style="max-height:150px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px; padding:8px; background:' . $colors['bg'] . ';">';
2661d05cddcSAtari911        echo '<label style="display:block; margin:3px 0;"><input type="checkbox" name="sync_namespaces[]" value=""> (default)</label>';
2671d05cddcSAtari911        foreach ($availableNamespaces as $ns) {
2681d05cddcSAtari911            if ($ns !== '') {
2691d05cddcSAtari911                $checked = in_array($ns, $selectedNamespaces) ? 'checked' : '';
2701d05cddcSAtari911                echo '<label style="display:block; margin:3px 0;"><input type="checkbox" name="sync_namespaces[]" value="' . hsc($ns) . '" ' . $checked . '> ' . hsc($ns) . '</label>';
2711d05cddcSAtari911            }
2721d05cddcSAtari911        }
2731d05cddcSAtari911        echo '</div>';
2741d05cddcSAtari911        echo '</div>';
2751d05cddcSAtari911
2761d05cddcSAtari911        echo '<script>
2771d05cddcSAtari911        function toggleNamespaceSelection(checkbox) {
2781d05cddcSAtari911            document.getElementById("namespace_selection").style.display = checkbox.checked ? "none" : "block";
2791d05cddcSAtari911        }
2801d05cddcSAtari911        </script>';
2811d05cddcSAtari911
2821d05cddcSAtari911        echo '</div>';
2831d05cddcSAtari911
2841d05cddcSAtari911        // Namespace and Color Mapping - Side by Side
2851d05cddcSAtari911        echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px; margin:10px 0;">';
2861d05cddcSAtari911
2871d05cddcSAtari911        // Namespace Mapping
2889ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; border-left:3px solid #00cc07; border-radius:3px;">';
2891d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Namespace → Category</h3>';
2909ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:0.8em; margin:0 0 5px;">One per line: namespace=Category</p>';
2919ccd446eSAtari911        echo '<textarea name="category_mapping" rows="6" style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-family:monospace; font-size:12px; resize:vertical;" placeholder="work=Blue category&#10;personal=Green category">';
2921d05cddcSAtari911        if (isset($config['category_mapping']) && is_array($config['category_mapping'])) {
2931d05cddcSAtari911            foreach ($config['category_mapping'] as $ns => $cat) {
2941d05cddcSAtari911                echo hsc($ns) . '=' . hsc($cat) . "\n";
2951d05cddcSAtari911            }
2961d05cddcSAtari911        }
2971d05cddcSAtari911        echo '</textarea>';
2981d05cddcSAtari911        echo '</div>';
2991d05cddcSAtari911
3001d05cddcSAtari911        // Color Mapping with Color Picker
3019ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; border-left:3px solid #00cc07; border-radius:3px;">';
3021d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Event Color → Category</h3>';
3039ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:0.8em; margin:0 0 8px;">Map calendar colors to Outlook categories</p>';
3041d05cddcSAtari911
3051d05cddcSAtari911        // Define calendar colors and Outlook categories (only the main 6 colors)
3061d05cddcSAtari911        $calendarColors = [
3071d05cddcSAtari911            '#3498db' => 'Blue',
3081d05cddcSAtari911            '#2ecc71' => 'Green',
3091d05cddcSAtari911            '#e74c3c' => 'Red',
3101d05cddcSAtari911            '#f39c12' => 'Orange',
3111d05cddcSAtari911            '#9b59b6' => 'Purple',
3121d05cddcSAtari911            '#1abc9c' => 'Teal'
3131d05cddcSAtari911        ];
3141d05cddcSAtari911
3151d05cddcSAtari911        $outlookCategories = [
3161d05cddcSAtari911            'Blue category',
3171d05cddcSAtari911            'Green category',
3181d05cddcSAtari911            'Orange category',
3191d05cddcSAtari911            'Red category',
3201d05cddcSAtari911            'Yellow category',
3211d05cddcSAtari911            'Purple category'
3221d05cddcSAtari911        ];
3231d05cddcSAtari911
3241d05cddcSAtari911        // Load existing color mappings
3251d05cddcSAtari911        $existingMappings = isset($config['color_mapping']) && is_array($config['color_mapping'])
3261d05cddcSAtari911            ? $config['color_mapping']
3271d05cddcSAtari911            : [];
3281d05cddcSAtari911
3291d05cddcSAtari911        // Display color mapping rows
3301d05cddcSAtari911        echo '<div id="colorMappings" style="max-height:200px; overflow-y:auto;">';
3311d05cddcSAtari911
3321d05cddcSAtari911        $rowIndex = 0;
3331d05cddcSAtari911        foreach ($calendarColors as $hexColor => $colorName) {
3341d05cddcSAtari911            $selectedCategory = isset($existingMappings[$hexColor]) ? $existingMappings[$hexColor] : '';
3351d05cddcSAtari911
3361d05cddcSAtari911            echo '<div style="display:flex; gap:8px; align-items:center; margin-bottom:6px;">';
3371d05cddcSAtari911
3381d05cddcSAtari911            // Color preview box
3391d05cddcSAtari911            echo '<div style="width:24px; height:24px; background:' . $hexColor . '; border:2px solid #ddd; border-radius:3px; flex-shrink:0;"></div>';
3401d05cddcSAtari911
3411d05cddcSAtari911            // Color name
3429ccd446eSAtari911            echo '<span style="font-size:12px; min-width:90px; color:' . $colors['text'] . ';">' . $colorName . '</span>';
3431d05cddcSAtari911
3441d05cddcSAtari911            // Arrow
3451d05cddcSAtari911            echo '<span style="color:#999; font-size:12px;">→</span>';
3461d05cddcSAtari911
3471d05cddcSAtari911            // Outlook category dropdown
3489ccd446eSAtari911            echo '<select name="color_map_' . $rowIndex . '" style="flex:1; padding:4px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;">';
3491d05cddcSAtari911            echo '<option value="">-- None --</option>';
3501d05cddcSAtari911            foreach ($outlookCategories as $category) {
3511d05cddcSAtari911                $selected = ($selectedCategory === $category) ? 'selected' : '';
3521d05cddcSAtari911                echo '<option value="' . hsc($category) . '" ' . $selected . '>' . hsc($category) . '</option>';
3531d05cddcSAtari911            }
3541d05cddcSAtari911            echo '</select>';
3551d05cddcSAtari911
3561d05cddcSAtari911            // Hidden input for the hex color
3571d05cddcSAtari911            echo '<input type="hidden" name="color_hex_' . $rowIndex . '" value="' . $hexColor . '">';
3581d05cddcSAtari911
3591d05cddcSAtari911            echo '</div>';
3601d05cddcSAtari911            $rowIndex++;
3611d05cddcSAtari911        }
3621d05cddcSAtari911
3631d05cddcSAtari911        echo '</div>';
3641d05cddcSAtari911
3651d05cddcSAtari911        // Hidden input to track number of color mappings
3661d05cddcSAtari911        echo '<input type="hidden" name="color_mapping_count" value="' . $rowIndex . '">';
3671d05cddcSAtari911
3681d05cddcSAtari911        echo '</div>';
3691d05cddcSAtari911
3701d05cddcSAtari911        echo '</div>'; // end grid
3711d05cddcSAtari911
3721d05cddcSAtari911        // Submit button
3731d05cddcSAtari911        echo '<button type="submit" style="background:#00cc07; color:white; padding:10px 20px; border:none; border-radius:3px; cursor:pointer; font-size:14px; font-weight:bold; margin:10px 0;">�� Save Configuration</button>';
3741d05cddcSAtari911        echo '</form>';
3751d05cddcSAtari911
3761d05cddcSAtari911        // JavaScript for Import/Export
3771d05cddcSAtari911        echo '<script>
3781d05cddcSAtari911        async function exportConfig() {
3791d05cddcSAtari911            try {
3801d05cddcSAtari911                const response = await fetch("?do=admin&page=calendar&action=export_config&call=ajax", {
3811d05cddcSAtari911                    method: "POST"
3821d05cddcSAtari911                });
3831d05cddcSAtari911                const data = await response.json();
3841d05cddcSAtari911
3851d05cddcSAtari911                if (data.success) {
3861d05cddcSAtari911                    // Create download link
3871d05cddcSAtari911                    const blob = new Blob([data.encrypted], {type: "application/octet-stream"});
3881d05cddcSAtari911                    const url = URL.createObjectURL(blob);
3891d05cddcSAtari911                    const a = document.createElement("a");
3901d05cddcSAtari911                    a.href = url;
3911d05cddcSAtari911                    a.download = "sync_config_" + new Date().toISOString().split("T")[0] + ".enc";
3921d05cddcSAtari911                    document.body.appendChild(a);
3931d05cddcSAtari911                    a.click();
3941d05cddcSAtari911                    document.body.removeChild(a);
3951d05cddcSAtari911                    URL.revokeObjectURL(url);
3961d05cddcSAtari911
3971d05cddcSAtari911                    alert("✅ Config exported successfully!\\n\\n⚠️ This file contains encrypted credentials.\\nKeep it secure!");
3981d05cddcSAtari911                } else {
3991d05cddcSAtari911                    alert("❌ Export failed: " + data.message);
4001d05cddcSAtari911                }
4011d05cddcSAtari911            } catch (error) {
4021d05cddcSAtari911                alert("❌ Error: " + error.message);
4031d05cddcSAtari911            }
4041d05cddcSAtari911        }
4051d05cddcSAtari911
4061d05cddcSAtari911        async function importConfig(input) {
4071d05cddcSAtari911            const file = input.files[0];
4081d05cddcSAtari911            if (!file) return;
4091d05cddcSAtari911
4101d05cddcSAtari911            const status = document.getElementById("importStatus");
4111d05cddcSAtari911            status.textContent = "⏳ Importing...";
4121d05cddcSAtari911            status.style.color = "#00cc07";
4131d05cddcSAtari911
4141d05cddcSAtari911            try {
4151d05cddcSAtari911                const encrypted = await file.text();
4161d05cddcSAtari911
4171d05cddcSAtari911                const formData = new FormData();
4181d05cddcSAtari911                formData.append("encrypted_config", encrypted);
4191d05cddcSAtari911
4201d05cddcSAtari911                const response = await fetch("?do=admin&page=calendar&action=import_config&call=ajax", {
4211d05cddcSAtari911                    method: "POST",
4221d05cddcSAtari911                    body: formData
4231d05cddcSAtari911                });
4241d05cddcSAtari911                const data = await response.json();
4251d05cddcSAtari911
4261d05cddcSAtari911                if (data.success) {
4271d05cddcSAtari911                    status.textContent = "✅ Import successful! Reloading...";
4281d05cddcSAtari911                    status.style.color = "#28a745";
4291d05cddcSAtari911                    setTimeout(() => {
4301d05cddcSAtari911                        window.location.reload();
4311d05cddcSAtari911                    }, 1500);
4321d05cddcSAtari911                } else {
4331d05cddcSAtari911                    status.textContent = "❌ Import failed: " + data.message;
4341d05cddcSAtari911                    status.style.color = "#dc3545";
4351d05cddcSAtari911                }
4361d05cddcSAtari911            } catch (error) {
4371d05cddcSAtari911                status.textContent = "❌ Error: " + error.message;
4381d05cddcSAtari911                status.style.color = "#dc3545";
4391d05cddcSAtari911            }
4401d05cddcSAtari911
4411d05cddcSAtari911            // Reset file input
4421d05cddcSAtari911            input.value = "";
4431d05cddcSAtari911        }
4441d05cddcSAtari911        </script>';
4451d05cddcSAtari911
4461d05cddcSAtari911        // Sync Controls Section
4479ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:15px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">';
4481d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Sync Controls</h3>';
4491d05cddcSAtari911
4501d05cddcSAtari911        // Check cron job status
4511d05cddcSAtari911        $cronStatus = $this->getCronStatus();
4521d05cddcSAtari911
4531d05cddcSAtari911        // Check log file permissions
4541d05cddcSAtari911        $logFile = DOKU_PLUGIN . 'calendar/sync.log';
4551d05cddcSAtari911        $logWritable = is_writable($logFile) || is_writable(dirname($logFile));
4561d05cddcSAtari911
4571d05cddcSAtari911        echo '<div style="display:flex; gap:10px; align-items:center; margin-bottom:10px;">';
4581d05cddcSAtari911        echo '<button onclick="runSyncNow()" id="syncBtn" style="background:#00cc07; color:white; padding:8px 16px; border:none; border-radius:3px; cursor:pointer; font-size:13px; font-weight:bold;">▶️ Run Sync Now</button>';
4591d05cddcSAtari911        echo '<button onclick="stopSyncNow()" id="stopBtn" style="background:#e74c3c; color:white; padding:8px 16px; border:none; border-radius:3px; cursor:pointer; font-size:13px; font-weight:bold; display:none;">⏹️ Stop Sync</button>';
4601d05cddcSAtari911
4611d05cddcSAtari911        if ($cronStatus['active']) {
4629ccd446eSAtari911            echo '<span style="color:' . $colors['text'] . '; font-size:12px;">⏰ ' . hsc($cronStatus['frequency']) . '</span>';
4631d05cddcSAtari911        } else {
4641d05cddcSAtari911            echo '<span style="color:#999; font-size:12px;">⚠️ No cron job detected</span>';
4651d05cddcSAtari911        }
4661d05cddcSAtari911
4679ccd446eSAtari911        echo '<span id="syncStatus" style="color:' . $colors['text'] . '; font-size:12px; margin-left:auto;"></span>';
4681d05cddcSAtari911        echo '</div>';
4691d05cddcSAtari911
4701d05cddcSAtari911        // Show permission warning if log not writable
4711d05cddcSAtari911        if (!$logWritable) {
4721d05cddcSAtari911            echo '<div style="background:#fff3e0; border-left:3px solid #ff9800; padding:8px; margin:8px 0; border-radius:3px;">';
4731d05cddcSAtari911            echo '<span style="color:#e65100; font-size:11px;">⚠️ Log file not writable. Run: <code style="background:#f0f0f0; padding:2px 4px; border-radius:2px;">chmod 666 ' . $logFile . '</code></span>';
4741d05cddcSAtari911            echo '</div>';
4751d05cddcSAtari911        }
4761d05cddcSAtari911
4771d05cddcSAtari911        // Show debug info if cron detected
4781d05cddcSAtari911        if ($cronStatus['active'] && !empty($cronStatus['full_line'])) {
4791d05cddcSAtari911            echo '<details style="margin-top:5px;">';
4801d05cddcSAtari911            echo '<summary style="cursor:pointer; color:#999; font-size:11px;">Show cron details</summary>';
4811d05cddcSAtari911            echo '<pre style="background:#f0f0f0; padding:8px; border-radius:3px; font-size:10px; margin:5px 0; overflow-x:auto;">' . hsc($cronStatus['full_line']) . '</pre>';
4821d05cddcSAtari911            echo '</details>';
4831d05cddcSAtari911        }
4841d05cddcSAtari911
4851d05cddcSAtari911        if (!$cronStatus['active']) {
4861d05cddcSAtari911            echo '<p style="color:#999; font-size:11px; margin:5px 0;">To enable automatic syncing, add to crontab: <code style="background:#f0f0f0; padding:2px 4px; border-radius:2px;">*/30 * * * * cd ' . DOKU_PLUGIN . 'calendar && php sync_outlook.php</code></p>';
4871d05cddcSAtari911        }
4881d05cddcSAtari911
4891d05cddcSAtari911        echo '</div>';
4901d05cddcSAtari911
4911d05cddcSAtari911        // JavaScript for Run Sync Now
4921d05cddcSAtari911        echo '<script>
4931d05cddcSAtari911        let syncAbortController = null;
4941d05cddcSAtari911
4951d05cddcSAtari911        function runSyncNow() {
4961d05cddcSAtari911            const btn = document.getElementById("syncBtn");
4971d05cddcSAtari911            const stopBtn = document.getElementById("stopBtn");
4981d05cddcSAtari911            const status = document.getElementById("syncStatus");
4991d05cddcSAtari911
5001d05cddcSAtari911            btn.disabled = true;
5011d05cddcSAtari911            btn.style.display = "none";
5021d05cddcSAtari911            stopBtn.style.display = "inline-block";
5031d05cddcSAtari911            btn.textContent = "⏳ Running...";
5041d05cddcSAtari911            btn.style.background = "#999";
5051d05cddcSAtari911            status.textContent = "Starting sync...";
5061d05cddcSAtari911            status.style.color = "#00cc07";
5071d05cddcSAtari911
5081d05cddcSAtari911            // Create abort controller for this sync
5091d05cddcSAtari911            syncAbortController = new AbortController();
5101d05cddcSAtari911
5111d05cddcSAtari911            fetch("?do=admin&page=calendar&action=run_sync&call=ajax", {
5121d05cddcSAtari911                method: "POST",
5131d05cddcSAtari911                signal: syncAbortController.signal
5141d05cddcSAtari911            })
5151d05cddcSAtari911                .then(response => response.json())
5161d05cddcSAtari911                .then(data => {
5171d05cddcSAtari911                    if (data.success) {
5181d05cddcSAtari911                        status.textContent = "✅ " + data.message;
5191d05cddcSAtari911                        status.style.color = "#28a745";
5201d05cddcSAtari911                    } else {
5211d05cddcSAtari911                        status.textContent = "❌ " + data.message;
5221d05cddcSAtari911                        status.style.color = "#dc3545";
5231d05cddcSAtari911                    }
5241d05cddcSAtari911                    btn.disabled = false;
5251d05cddcSAtari911                    btn.style.display = "inline-block";
5261d05cddcSAtari911                    stopBtn.style.display = "none";
5271d05cddcSAtari911                    btn.textContent = "▶️ Run Sync Now";
5281d05cddcSAtari911                    btn.style.background = "#00cc07";
5291d05cddcSAtari911                    syncAbortController = null;
5301d05cddcSAtari911
5311d05cddcSAtari911                    // Clear status after 10 seconds
5321d05cddcSAtari911                    setTimeout(() => {
5331d05cddcSAtari911                        status.textContent = "";
5341d05cddcSAtari911                    }, 10000);
5351d05cddcSAtari911                })
5361d05cddcSAtari911                .catch(error => {
5371d05cddcSAtari911                    if (error.name === "AbortError") {
5381d05cddcSAtari911                        status.textContent = "⏹️ Sync stopped by user";
5391d05cddcSAtari911                        status.style.color = "#ff9800";
5401d05cddcSAtari911                    } else {
5411d05cddcSAtari911                        status.textContent = "❌ Error: " + error.message;
5421d05cddcSAtari911                        status.style.color = "#dc3545";
5431d05cddcSAtari911                    }
5441d05cddcSAtari911                    btn.disabled = false;
5451d05cddcSAtari911                    btn.style.display = "inline-block";
5461d05cddcSAtari911                    stopBtn.style.display = "none";
5471d05cddcSAtari911                    btn.textContent = "▶️ Run Sync Now";
5481d05cddcSAtari911                    btn.style.background = "#00cc07";
5491d05cddcSAtari911                    syncAbortController = null;
5501d05cddcSAtari911                });
5511d05cddcSAtari911        }
5521d05cddcSAtari911
5531d05cddcSAtari911        function stopSyncNow() {
5541d05cddcSAtari911            const status = document.getElementById("syncStatus");
5551d05cddcSAtari911
5561d05cddcSAtari911            status.textContent = "⏹️ Sending stop signal...";
5571d05cddcSAtari911            status.style.color = "#ff9800";
5581d05cddcSAtari911
5591d05cddcSAtari911            // First, send stop signal to server
5601d05cddcSAtari911            fetch("?do=admin&page=calendar&action=stop_sync&call=ajax", {
5611d05cddcSAtari911                method: "POST"
5621d05cddcSAtari911            })
5631d05cddcSAtari911            .then(response => response.json())
5641d05cddcSAtari911            .then(data => {
5651d05cddcSAtari911                if (data.success) {
5661d05cddcSAtari911                    status.textContent = "⏹️ Stop signal sent - sync will abort soon";
5671d05cddcSAtari911                    status.style.color = "#ff9800";
5681d05cddcSAtari911                } else {
5691d05cddcSAtari911                    status.textContent = "⚠️ " + data.message;
5701d05cddcSAtari911                    status.style.color = "#ff9800";
5711d05cddcSAtari911                }
5721d05cddcSAtari911            })
5731d05cddcSAtari911            .catch(error => {
5741d05cddcSAtari911                status.textContent = "⚠️ Error sending stop signal: " + error.message;
5751d05cddcSAtari911                status.style.color = "#ff9800";
5761d05cddcSAtari911            });
5771d05cddcSAtari911
5781d05cddcSAtari911            // Also abort the fetch request
5791d05cddcSAtari911            if (syncAbortController) {
5801d05cddcSAtari911                syncAbortController.abort();
5811d05cddcSAtari911                status.textContent = "⏹️ Stopping sync...";
5821d05cddcSAtari911                status.style.color = "#ff9800";
5831d05cddcSAtari911            }
5841d05cddcSAtari911        }
5851d05cddcSAtari911        </script>';
5861d05cddcSAtari911
5871d05cddcSAtari911        // Log Viewer Section - More Compact
5889ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:15px 0 10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">';
5891d05cddcSAtari911        echo '<h3 style="margin:0 0 5px 0; color:#00cc07; font-size:16px;">�� Live Sync Log</h3>';
5909ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:0.8em; margin:0 0 8px;">Updates every 2 seconds</p>';
5911d05cddcSAtari911
5921d05cddcSAtari911        // Log viewer container
5931d05cddcSAtari911        echo '<div style="background:#1e1e1e; border-radius:5px; overflow:hidden; box-shadow:0 2px 4px rgba(0,0,0,0.3);">';
5941d05cddcSAtari911
5951d05cddcSAtari911        // Log header - More compact
5961d05cddcSAtari911        echo '<div style="background:#2d2d2d; padding:6px 10px; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #444;">';
5971d05cddcSAtari911        echo '<span style="color:#00cc07; font-family:monospace; font-weight:bold; font-size:12px;">sync.log</span>';
5981d05cddcSAtari911        echo '<div>';
5991d05cddcSAtari911        echo '<button id="pauseBtn" onclick="togglePause()" style="background:#666; color:white; border:none; padding:4px 8px; border-radius:3px; cursor:pointer; margin-right:4px; font-size:11px;">⏸ Pause</button>';
6001d05cddcSAtari911        echo '<button onclick="clearLog()" style="background:#e74c3c; color:white; border:none; padding:4px 8px; border-radius:3px; cursor:pointer; margin-right:4px; font-size:11px;">��️ Clear</button>';
6011d05cddcSAtari911        echo '<button onclick="downloadLog()" style="background:#666; color:white; border:none; padding:4px 8px; border-radius:3px; cursor:pointer; font-size:11px;">�� Download</button>';
6021d05cddcSAtari911        echo '</div>';
6031d05cddcSAtari911        echo '</div>';
6041d05cddcSAtari911
6051d05cddcSAtari911        // Log content - Reduced height to 250px
6061d05cddcSAtari911        echo '<pre id="logContent" style="background:#1e1e1e; color:#00cc07; font-family:monospace; font-size:11px; padding:10px; margin:0; overflow-x:auto; white-space:pre-wrap; word-wrap:break-word; line-height:1.4; max-height:250px; overflow-y:auto;">Loading log...</pre>';
6071d05cddcSAtari911
6081d05cddcSAtari911        echo '</div>';
6091d05cddcSAtari911        echo '</div>';
6101d05cddcSAtari911
6111d05cddcSAtari911        // JavaScript for log viewer
6121d05cddcSAtari911        echo '<script>
6131d05cddcSAtari911        let refreshInterval = null;
6141d05cddcSAtari911        let isPaused = false;
6151d05cddcSAtari911
6161d05cddcSAtari911        function refreshLog() {
6171d05cddcSAtari911            if (isPaused) return;
6181d05cddcSAtari911
6191d05cddcSAtari911            fetch("?do=admin&page=calendar&action=get_log&call=ajax")
6201d05cddcSAtari911                .then(response => response.json())
6211d05cddcSAtari911                .then(data => {
6221d05cddcSAtari911                    const logContent = document.getElementById("logContent");
6231d05cddcSAtari911                    if (logContent) {
6241d05cddcSAtari911                        logContent.textContent = data.log || "No log data available";
6251d05cddcSAtari911                        logContent.scrollTop = logContent.scrollHeight;
6261d05cddcSAtari911                    }
6271d05cddcSAtari911                })
6281d05cddcSAtari911                .catch(error => {
6291d05cddcSAtari911                    console.error("Error fetching log:", error);
6301d05cddcSAtari911                });
6311d05cddcSAtari911        }
6321d05cddcSAtari911
6331d05cddcSAtari911        function togglePause() {
6341d05cddcSAtari911            isPaused = !isPaused;
6351d05cddcSAtari911            const btn = document.getElementById("pauseBtn");
6361d05cddcSAtari911            if (isPaused) {
6371d05cddcSAtari911                btn.textContent = "▶ Resume";
6381d05cddcSAtari911                btn.style.background = "#00cc07";
6391d05cddcSAtari911            } else {
6401d05cddcSAtari911                btn.textContent = "⏸ Pause";
6411d05cddcSAtari911                btn.style.background = "#666";
6421d05cddcSAtari911                refreshLog();
6431d05cddcSAtari911            }
6441d05cddcSAtari911        }
6451d05cddcSAtari911
6461d05cddcSAtari911        function clearLog() {
6471d05cddcSAtari911            if (!confirm("Clear the sync log file?\\n\\nThis will delete all log entries.")) {
6481d05cddcSAtari911                return;
6491d05cddcSAtari911            }
6501d05cddcSAtari911
6511d05cddcSAtari911            fetch("?do=admin&page=calendar&action=clear_log&call=ajax", {
6521d05cddcSAtari911                method: "POST"
6531d05cddcSAtari911            })
6541d05cddcSAtari911                .then(response => response.json())
6551d05cddcSAtari911                .then(data => {
6561d05cddcSAtari911                    if (data.success) {
6571d05cddcSAtari911                        refreshLog();
6581d05cddcSAtari911                        alert("Log cleared successfully");
6591d05cddcSAtari911                    } else {
6601d05cddcSAtari911                        alert("Error clearing log: " + data.message);
6611d05cddcSAtari911                    }
6621d05cddcSAtari911                })
6631d05cddcSAtari911                .catch(error => {
6641d05cddcSAtari911                    alert("Error: " + error.message);
6651d05cddcSAtari911                });
6661d05cddcSAtari911        }
6671d05cddcSAtari911
6681d05cddcSAtari911        function downloadLog() {
6691d05cddcSAtari911            window.location.href = "?do=admin&page=calendar&action=download_log";
6701d05cddcSAtari911        }
6711d05cddcSAtari911
6721d05cddcSAtari911        // Start auto-refresh
6731d05cddcSAtari911        refreshLog();
6741d05cddcSAtari911        refreshInterval = setInterval(refreshLog, 2000);
6751d05cddcSAtari911
6761d05cddcSAtari911        // Cleanup on page unload
6771d05cddcSAtari911        window.addEventListener("beforeunload", function() {
6781d05cddcSAtari911            if (refreshInterval) {
6791d05cddcSAtari911                clearInterval(refreshInterval);
6801d05cddcSAtari911            }
6811d05cddcSAtari911        });
6821d05cddcSAtari911        </script>';
6831d05cddcSAtari911    }
6841d05cddcSAtari911
6859ccd446eSAtari911    private function renderManageTab($colors = null) {
6861d05cddcSAtari911        global $INPUT;
6871d05cddcSAtari911
6889ccd446eSAtari911        // Use defaults if not provided
6899ccd446eSAtari911        if ($colors === null) {
6909ccd446eSAtari911            $colors = $this->getTemplateColors();
6919ccd446eSAtari911        }
6929ccd446eSAtari911
6931d05cddcSAtari911        // Show message if present
6941d05cddcSAtari911        if ($INPUT->has('msg')) {
6951d05cddcSAtari911            $msg = hsc($INPUT->str('msg'));
6961d05cddcSAtari911            $type = $INPUT->str('msgtype', 'success');
6971d05cddcSAtari911            echo "<div style=\"padding:10px; margin:10px 0; border-left:3px solid " . ($type === 'success' ? '#28a745' : '#dc3545') . "; background:" . ($type === 'success' ? '#d4edda' : '#f8d7da') . "; border-radius:3px;\">";
6981d05cddcSAtari911            echo $msg;
6991d05cddcSAtari911            echo "</div>";
7001d05cddcSAtari911        }
7011d05cddcSAtari911
7021d05cddcSAtari911        echo '<h2 style="margin:10px 0; font-size:20px;">Manage Calendar Events</h2>';
7031d05cddcSAtari911
7049ccd446eSAtari911        // Events Manager Section
7059ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
7061d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Events Manager</h3>';
7079ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:11px; margin:0 0 10px;">Scan, export, and import all calendar events across all namespaces.</p>';
7081d05cddcSAtari911
7091d05cddcSAtari911        // Get event statistics
7101d05cddcSAtari911        $stats = $this->getEventStatistics();
7111d05cddcSAtari911
7121d05cddcSAtari911        // Statistics display
7139ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:10px; border-radius:3px; margin-bottom:10px; border:1px solid ' . $colors['border'] . ';">';
7141d05cddcSAtari911        echo '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(150px, 1fr)); gap:10px; font-size:12px;">';
7151d05cddcSAtari911
7161d05cddcSAtari911        echo '<div style="background:#f3e5f5; padding:8px; border-radius:3px; text-align:center;">';
7171d05cddcSAtari911        echo '<div style="font-size:24px; font-weight:bold; color:#7b1fa2;">' . $stats['total_events'] . '</div>';
7189ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; font-size:10px;">Total Events</div>';
7191d05cddcSAtari911        echo '</div>';
7201d05cddcSAtari911
7211d05cddcSAtari911        echo '<div style="background:#f3e5f5; padding:8px; border-radius:3px; text-align:center;">';
7221d05cddcSAtari911        echo '<div style="font-size:24px; font-weight:bold; color:#7b1fa2;">' . $stats['total_namespaces'] . '</div>';
7239ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; font-size:10px;">Namespaces</div>';
7241d05cddcSAtari911        echo '</div>';
7251d05cddcSAtari911
7261d05cddcSAtari911        echo '<div style="background:#e8f5e9; padding:8px; border-radius:3px; text-align:center;">';
7271d05cddcSAtari911        echo '<div style="font-size:24px; font-weight:bold; color:#388e3c;">' . $stats['total_files'] . '</div>';
7289ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; font-size:10px;">JSON Files</div>';
7291d05cddcSAtari911        echo '</div>';
7301d05cddcSAtari911
7311d05cddcSAtari911        echo '<div style="background:#fff3e0; padding:8px; border-radius:3px; text-align:center;">';
7321d05cddcSAtari911        echo '<div style="font-size:24px; font-weight:bold; color:#f57c00;">' . $stats['total_recurring'] . '</div>';
7339ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; font-size:10px;">Recurring</div>';
7341d05cddcSAtari911        echo '</div>';
7351d05cddcSAtari911
7361d05cddcSAtari911        echo '</div>';
7371d05cddcSAtari911
7381d05cddcSAtari911        // Last scan time
7391d05cddcSAtari911        if (!empty($stats['last_scan'])) {
7409ccd446eSAtari911            echo '<div style="margin-top:8px; color:' . $colors['text'] . '; font-size:10px;">Last scanned: ' . hsc($stats['last_scan']) . '</div>';
7411d05cddcSAtari911        }
7421d05cddcSAtari911
7431d05cddcSAtari911        echo '</div>';
7441d05cddcSAtari911
7451d05cddcSAtari911        // Action buttons
7461d05cddcSAtari911        echo '<div style="display:flex; gap:10px; flex-wrap:wrap; margin-top:10px;">';
7471d05cddcSAtari911
7481d05cddcSAtari911        // Rescan button
7491d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" style="display:inline;">';
7501d05cddcSAtari911        echo '<input type="hidden" name="action" value="rescan_events">';
7511d05cddcSAtari911        echo '<button type="submit" style="background:#00cc07; color:white; border:none; padding:8px 16px; border-radius:3px; cursor:pointer; font-size:12px; display:flex; align-items:center; gap:6px;">';
7521d05cddcSAtari911        echo '<span>��</span><span>Re-scan Events</span>';
7531d05cddcSAtari911        echo '</button>';
7541d05cddcSAtari911        echo '</form>';
7551d05cddcSAtari911
7561d05cddcSAtari911        // Export button
7571d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" style="display:inline;">';
7581d05cddcSAtari911        echo '<input type="hidden" name="action" value="export_all_events">';
7591d05cddcSAtari911        echo '<button type="submit" style="background:#7b1fa2; color:white; border:none; padding:8px 16px; border-radius:3px; cursor:pointer; font-size:12px; display:flex; align-items:center; gap:6px;">';
7601d05cddcSAtari911        echo '<span>��</span><span>Export All Events</span>';
7611d05cddcSAtari911        echo '</button>';
7621d05cddcSAtari911        echo '</form>';
7631d05cddcSAtari911
7641d05cddcSAtari911        // Import button (with file upload)
7651d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" enctype="multipart/form-data" style="display:inline;" onsubmit="return confirm(\'Import will merge with existing events. Continue?\')">';
7661d05cddcSAtari911        echo '<input type="hidden" name="action" value="import_all_events">';
7671d05cddcSAtari911        echo '<label style="background:#7b1fa2; color:white; border:none; padding:8px 16px; border-radius:3px; cursor:pointer; font-size:12px; display:inline-flex; align-items:center; gap:6px;">';
7681d05cddcSAtari911        echo '<span>��</span><span>Import Events</span>';
7691d05cddcSAtari911        echo '<input type="file" name="import_file" accept=".json,.zip" required style="display:none;" onchange="this.form.submit()">';
7701d05cddcSAtari911        echo '</label>';
7711d05cddcSAtari911        echo '</form>';
7721d05cddcSAtari911
7731d05cddcSAtari911        echo '</div>';
7741d05cddcSAtari911
7751d05cddcSAtari911        // Breakdown by namespace
7761d05cddcSAtari911        if (!empty($stats['by_namespace'])) {
7771d05cddcSAtari911            echo '<details style="margin-top:12px;">';
7781d05cddcSAtari911            echo '<summary style="cursor:pointer; padding:6px; background:#e9e9e9; border-radius:3px; font-size:11px; font-weight:bold;">View Breakdown by Namespace</summary>';
7799ccd446eSAtari911            echo '<div style="margin-top:8px; max-height:200px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px;">';
7801d05cddcSAtari911            echo '<table style="width:100%; border-collapse:collapse; font-size:11px;">';
7811d05cddcSAtari911            echo '<thead style="position:sticky; top:0; background:#f5f5f5;">';
7821d05cddcSAtari911            echo '<tr>';
7831d05cddcSAtari911            echo '<th style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd;">Namespace</th>';
7841d05cddcSAtari911            echo '<th style="padding:4px 6px; text-align:right; border-bottom:2px solid #ddd;">Events</th>';
7851d05cddcSAtari911            echo '<th style="padding:4px 6px; text-align:right; border-bottom:2px solid #ddd;">Files</th>';
7861d05cddcSAtari911            echo '</tr></thead><tbody>';
7871d05cddcSAtari911
7881d05cddcSAtari911            foreach ($stats['by_namespace'] as $ns => $nsStats) {
7891d05cddcSAtari911                echo '<tr style="border-bottom:1px solid #eee;">';
7901d05cddcSAtari911                echo '<td style="padding:4px 6px;"><code style="background:#f0f0f0; padding:1px 3px; border-radius:2px; font-size:10px;">' . hsc($ns ?: '(default)') . '</code></td>';
7911d05cddcSAtari911                echo '<td style="padding:4px 6px; text-align:right;"><strong>' . $nsStats['events'] . '</strong></td>';
7921d05cddcSAtari911                echo '<td style="padding:4px 6px; text-align:right;">' . $nsStats['files'] . '</td>';
7931d05cddcSAtari911                echo '</tr>';
7941d05cddcSAtari911            }
7951d05cddcSAtari911
7961d05cddcSAtari911            echo '</tbody></table>';
7971d05cddcSAtari911            echo '</div>';
7981d05cddcSAtari911            echo '</details>';
7991d05cddcSAtari911        }
8001d05cddcSAtari911
8011d05cddcSAtari911        echo '</div>';
8021d05cddcSAtari911
8034590242dSAtari911        // Important Namespaces Section
8044590242dSAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
8054590242dSAtari911        $importantConfig = [];
8064590242dSAtari911        if (file_exists($configFile)) {
8074590242dSAtari911            $importantConfig = include $configFile;
8084590242dSAtari911        }
8094590242dSAtari911        $importantNsValue = isset($importantConfig['important_namespaces']) ? $importantConfig['important_namespaces'] : 'important';
8104590242dSAtari911
8114590242dSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
8124590242dSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Important Namespaces (Sidebar Widget)</h3>';
8134590242dSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:11px; margin:0 0 8px;">Events from these namespaces will be highlighted in purple in the sidebar widget\'s "Important Events" section.</p>';
8144590242dSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" style="display:flex; gap:8px; align-items:center;">';
8154590242dSAtari911        echo '<input type="hidden" name="action" value="save_important_namespaces">';
8164590242dSAtari911        echo '<input type="text" name="important_namespaces" value="' . hsc($importantNsValue) . '" style="flex:1; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;" placeholder="important,urgent,priority">';
8174590242dSAtari911        echo '<button type="submit" style="background:#00cc07; color:white; padding:6px 16px; border:none; border-radius:3px; cursor:pointer; font-size:12px; font-weight:bold; white-space:nowrap;">Save</button>';
8184590242dSAtari911        echo '</form>';
8194590242dSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:10px; margin:4px 0 0;">Comma-separated list of namespace names</p>';
8204590242dSAtari911        echo '</div>';
8214590242dSAtari911
8229ccd446eSAtari911        // Cleanup Events Section
8239ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
8249ccd446eSAtari911        echo '<h3 style="margin:0 0 6px 0; color:#00cc07; font-size:16px;">�� Cleanup Old Events</h3>';
8259ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:11px; margin:0 0 12px;">Delete events based on criteria below. Automatic backup created before deletion.</p>';
8261d05cddcSAtari911
8271d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" id="cleanupForm">';
8281d05cddcSAtari911        echo '<input type="hidden" name="action" value="cleanup_events">';
8291d05cddcSAtari911
8301d05cddcSAtari911        // Compact options layout
8319ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:10px; border:1px solid ' . $colors['border'] . '; border-radius:3px; margin-bottom:10px;">';
8321d05cddcSAtari911
8331d05cddcSAtari911        // Radio buttons in a row
8341d05cddcSAtari911        echo '<div style="display:flex; gap:20px; margin-bottom:10px; padding-bottom:8px; border-bottom:1px solid #f0f0f0;">';
8351d05cddcSAtari911        echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">';
8361d05cddcSAtari911        echo '<input type="radio" name="cleanup_type" value="age" checked onchange="updateCleanupOptions()">';
8371d05cddcSAtari911        echo '<span>By Age</span>';
8381d05cddcSAtari911        echo '</label>';
8391d05cddcSAtari911        echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">';
8401d05cddcSAtari911        echo '<input type="radio" name="cleanup_type" value="status" onchange="updateCleanupOptions()">';
8411d05cddcSAtari911        echo '<span>By Status</span>';
8421d05cddcSAtari911        echo '</label>';
8431d05cddcSAtari911        echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">';
8441d05cddcSAtari911        echo '<input type="radio" name="cleanup_type" value="range" onchange="updateCleanupOptions()">';
8451d05cddcSAtari911        echo '<span>By Date Range</span>';
8461d05cddcSAtari911        echo '</label>';
8471d05cddcSAtari911        echo '</div>';
8481d05cddcSAtari911
8491d05cddcSAtari911        // Age options
8501d05cddcSAtari911        echo '<div id="age-options" style="padding:6px 0;">';
8519ccd446eSAtari911        echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">Delete events older than:</span>';
8521d05cddcSAtari911        echo '<select name="age_value" style="width:50px; padding:3px 4px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px; margin-right:4px;">';
8531d05cddcSAtari911        for ($i = 1; $i <= 24; $i++) {
8541d05cddcSAtari911            $sel = $i === 6 ? ' selected' : '';
8551d05cddcSAtari911            echo '<option value="' . $i . '"' . $sel . '>' . $i . '</option>';
8561d05cddcSAtari911        }
8571d05cddcSAtari911        echo '</select>';
8581d05cddcSAtari911        echo '<select name="age_unit" style="width:80px; padding:3px 4px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px;">';
8591d05cddcSAtari911        echo '<option value="months" selected>months</option>';
8601d05cddcSAtari911        echo '<option value="years">years</option>';
8611d05cddcSAtari911        echo '</select>';
8621d05cddcSAtari911        echo '</div>';
8631d05cddcSAtari911
8641d05cddcSAtari911        // Status options
8651d05cddcSAtari911        echo '<div id="status-options" style="padding:6px 0; opacity:0.4;">';
8669ccd446eSAtari911        echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">Delete:</span>';
8671d05cddcSAtari911        echo '<label style="display:inline-block; font-size:11px; margin-right:12px; cursor:pointer;"><input type="checkbox" name="delete_completed" value="1" style="margin-right:3px;"> Completed tasks</label>';
8681d05cddcSAtari911        echo '<label style="display:inline-block; font-size:11px; cursor:pointer;"><input type="checkbox" name="delete_past" value="1" style="margin-right:3px;"> Past events</label>';
8691d05cddcSAtari911        echo '</div>';
8701d05cddcSAtari911
8711d05cddcSAtari911        // Range options
8721d05cddcSAtari911        echo '<div id="range-options" style="padding:6px 0; opacity:0.4;">';
8739ccd446eSAtari911        echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">From:</span>';
8741d05cddcSAtari911        echo '<input type="date" name="range_start" style="padding:3px 6px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px; margin-right:10px;">';
8759ccd446eSAtari911        echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">To:</span>';
8761d05cddcSAtari911        echo '<input type="date" name="range_end" style="padding:3px 6px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px;">';
8771d05cddcSAtari911        echo '</div>';
8781d05cddcSAtari911
8791d05cddcSAtari911        echo '</div>';
8801d05cddcSAtari911
8811d05cddcSAtari911        // Namespace filter - compact
8829ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:8px 10px; border:1px solid ' . $colors['border'] . '; border-radius:3px; margin-bottom:10px; display:flex; align-items:center; gap:8px;">';
8831d05cddcSAtari911        echo '<label style="font-size:11px; font-weight:600; white-space:nowrap; color:#555;">Namespace:</label>';
8841d05cddcSAtari911        echo '<input type="text" name="namespace_filter" placeholder="Leave empty for all, or specify: work, personal, etc." style="flex:1; padding:4px 8px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px;">';
8851d05cddcSAtari911        echo '</div>';
8861d05cddcSAtari911
8871d05cddcSAtari911        // Action buttons - compact row
8881d05cddcSAtari911        echo '<div style="display:flex; gap:8px; align-items:center;">';
8891d05cddcSAtari911        echo '<button type="button" onclick="previewCleanup()" style="background:#7b1fa2; color:white; border:none; padding:6px 14px; border-radius:3px; cursor:pointer; font-size:11px; font-weight:600;">��️ Preview</button>';
8901d05cddcSAtari911        echo '<button type="submit" onclick="return confirmCleanup()" style="background:#dc3545; color:white; border:none; padding:6px 14px; border-radius:3px; cursor:pointer; font-size:11px; font-weight:600;">��️ Delete</button>';
8911d05cddcSAtari911        echo '<span style="font-size:10px; color:#999;">⚠️ Backup created automatically</span>';
8921d05cddcSAtari911        echo '</div>';
8931d05cddcSAtari911
8941d05cddcSAtari911        echo '</form>';
8951d05cddcSAtari911
8961d05cddcSAtari911        // Preview results area
8971d05cddcSAtari911        echo '<div id="cleanup-preview" style="margin-top:10px; display:none;"></div>';
8981d05cddcSAtari911
8991d05cddcSAtari911        echo '<script>
9001d05cddcSAtari911        function updateCleanupOptions() {
9011d05cddcSAtari911            const type = document.querySelector(\'input[name="cleanup_type"]:checked\').value;
9021d05cddcSAtari911
9031d05cddcSAtari911            // Show selected, gray out others
9041d05cddcSAtari911            document.getElementById(\'age-options\').style.opacity = type === \'age\' ? \'1\' : \'0.4\';
9051d05cddcSAtari911            document.getElementById(\'status-options\').style.opacity = type === \'status\' ? \'1\' : \'0.4\';
9061d05cddcSAtari911            document.getElementById(\'range-options\').style.opacity = type === \'range\' ? \'1\' : \'0.4\';
9071d05cddcSAtari911
9081d05cddcSAtari911            // Enable/disable inputs
9091d05cddcSAtari911            document.querySelectorAll(\'#age-options select\').forEach(el => el.disabled = type !== \'age\');
9101d05cddcSAtari911            document.querySelectorAll(\'#status-options input\').forEach(el => el.disabled = type !== \'status\');
9111d05cddcSAtari911            document.querySelectorAll(\'#range-options input\').forEach(el => el.disabled = type !== \'range\');
9121d05cddcSAtari911        }
9131d05cddcSAtari911
9141d05cddcSAtari911        function previewCleanup() {
9151d05cddcSAtari911            const form = document.getElementById(\'cleanupForm\');
9161d05cddcSAtari911            const formData = new FormData(form);
9171d05cddcSAtari911            formData.set(\'action\', \'preview_cleanup\');
9181d05cddcSAtari911
9191d05cddcSAtari911            const preview = document.getElementById(\'cleanup-preview\');
9209ccd446eSAtari911            preview.innerHTML = \'<div style="text-align:center; padding:20px; color:' . $colors['text'] . ';">Loading preview...</div>\';
9211d05cddcSAtari911            preview.style.display = \'block\';
9221d05cddcSAtari911
9231d05cddcSAtari911            fetch(\'?do=admin&page=calendar&tab=manage\', {
9241d05cddcSAtari911                method: \'POST\',
9251d05cddcSAtari911                body: new URLSearchParams(formData)
9261d05cddcSAtari911            })
9271d05cddcSAtari911            .then(r => r.json())
9281d05cddcSAtari911            .then(data => {
9291d05cddcSAtari911                if (data.count === 0) {
9301d05cddcSAtari911                    let html = \'<div style="background:#d4edda; border:1px solid #c3e6cb; padding:10px; border-radius:3px; font-size:12px; color:#155724;">✅ No events match the criteria. Nothing would be deleted.</div>\';
9311d05cddcSAtari911
9321d05cddcSAtari911                    // Show debug info if available
9331d05cddcSAtari911                    if (data.debug) {
9349ccd446eSAtari911                        html += \'<details style="margin-top:8px; font-size:11px; color:' . $colors['text'] . ';">\';
9351d05cddcSAtari911                        html += \'<summary style="cursor:pointer;">Debug Info</summary>\';
9361d05cddcSAtari911                        html += \'<pre style="background:#f5f5f5; padding:6px; margin-top:4px; border-radius:3px; overflow-x:auto;">\' + JSON.stringify(data.debug, null, 2) + \'</pre>\';
9371d05cddcSAtari911                        html += \'</details>\';
9381d05cddcSAtari911                    }
9391d05cddcSAtari911
9401d05cddcSAtari911                    preview.innerHTML = html;
9411d05cddcSAtari911                } else {
9421d05cddcSAtari911                    let html = \'<div style="background:#f8d7da; border:1px solid #f5c6cb; padding:10px; border-radius:3px; font-size:12px; color:#721c24;">\';
9431d05cddcSAtari911                    html += \'<strong>⚠️ Warning:</strong> The following \' + data.count + \' event(s) would be deleted:<br><br>\';
9449ccd446eSAtari911                    html += \'<div style="max-height:150px; overflow-y:auto; margin-top:6px; background:' . $colors['bg'] . '; padding:6px; border-radius:3px;">\';
9451d05cddcSAtari911                    data.events.forEach(evt => {
9461d05cddcSAtari911                        html += \'<div style="padding:3px; border-bottom:1px solid #eee; font-size:11px;">\';
9471d05cddcSAtari911                        html += \'\' + evt.title + \' (\' + evt.date + \')\';
9481d05cddcSAtari911                        if (evt.namespace) html += \' <span style="background:#e3f2fd; padding:1px 4px; border-radius:2px; font-size:9px;">\' + evt.namespace + \'</span>\';
9491d05cddcSAtari911                        html += \'</div>\';
9501d05cddcSAtari911                    });
9511d05cddcSAtari911                    html += \'</div></div>\';
9521d05cddcSAtari911                    preview.innerHTML = html;
9531d05cddcSAtari911                }
9541d05cddcSAtari911            })
9551d05cddcSAtari911            .catch(err => {
9561d05cddcSAtari911                preview.innerHTML = \'<div style="background:#f8d7da; border:1px solid #f5c6cb; padding:10px; border-radius:3px; font-size:12px; color:#721c24;">Error loading preview</div>\';
9571d05cddcSAtari911            });
9581d05cddcSAtari911        }
9591d05cddcSAtari911
9601d05cddcSAtari911        function confirmCleanup() {
9611d05cddcSAtari911            return confirm(\'Are you sure you want to delete these events? A backup will be created first, but this action cannot be easily undone.\');
9621d05cddcSAtari911        }
9631d05cddcSAtari911
9641d05cddcSAtari911        updateCleanupOptions();
9651d05cddcSAtari911        </script>';
9661d05cddcSAtari911
9671d05cddcSAtari911        echo '</div>';
9681d05cddcSAtari911
9691d05cddcSAtari911        // Recurring Events Section
970*7e8ea635SAtari911        echo '<div id="recurring-section" style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
971*7e8ea635SAtari911        echo '<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:8px;">';
972*7e8ea635SAtari911        echo '<h3 style="margin:0; color:#00cc07; font-size:16px;">�� Recurring Events</h3>';
973*7e8ea635SAtari911        echo '<div style="display:flex; gap:6px;">';
974*7e8ea635SAtari911        echo '<button onclick="trimAllPastRecurring()" id="trim-all-past-btn" style="background:#e74c3c; color:#fff; border:none; padding:4px 12px; border-radius:3px; cursor:pointer; font-size:11px; font-weight:600; transition:all 0.15s;" onmouseover="this.style.filter=\'brightness(1.2)\'" onmouseout="this.style.filter=\'none\'">✂️ Trim All Past</button>';
975*7e8ea635SAtari911        echo '<button onclick="rescanRecurringEvents()" id="rescan-recurring-btn" style="background:#00cc07; color:#fff; border:none; padding:4px 12px; border-radius:3px; cursor:pointer; font-size:11px; font-weight:600; transition:all 0.15s;" onmouseover="this.style.filter=\'brightness(1.2)\'" onmouseout="this.style.filter=\'none\'">�� Rescan</button>';
976*7e8ea635SAtari911        echo '</div>';
977*7e8ea635SAtari911        echo '</div>';
9781d05cddcSAtari911
9791d05cddcSAtari911        $recurringEvents = $this->findRecurringEvents();
9801d05cddcSAtari911
981*7e8ea635SAtari911        echo '<div id="recurring-content">';
982*7e8ea635SAtari911        $this->renderRecurringTable($recurringEvents, $colors);
9831d05cddcSAtari911        echo '</div>';
9841d05cddcSAtari911        echo '</div>';
9851d05cddcSAtari911
9861d05cddcSAtari911        // Compact Tree-based Namespace Manager
9879ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
9881d05cddcSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Namespace Explorer</h3>';
9899ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:11px; margin:0 0 8px;">Select events and move between namespaces. Drag & drop also supported.</p>';
9901d05cddcSAtari911
9911d05cddcSAtari911        // Search bar
9921d05cddcSAtari911        echo '<div style="margin-bottom:8px;">';
9939ccd446eSAtari911        echo '<input type="text" id="searchEvents" onkeyup="filterEvents()" placeholder="�� Search events by title..." style="width:100%; padding:6px 10px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;">';
9941d05cddcSAtari911        echo '</div>';
9951d05cddcSAtari911
9961d05cddcSAtari911        $eventsByNamespace = $this->getEventsByNamespace();
9971d05cddcSAtari911
9981d05cddcSAtari911        // Control bar
9991d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=manage" id="moveForm">';
10001d05cddcSAtari911        echo '<input type="hidden" name="action" value="move_selected_events" id="formAction">';
10011d05cddcSAtari911        echo '<div style="background:#2d2d2d; color:white; padding:6px 10px; border-radius:3px; margin-bottom:8px; display:flex; gap:8px; align-items:center; font-size:12px;">';
10021d05cddcSAtari911        echo '<button type="button" onclick="selectAll()" style="background:#00cc07; color:white; border:none; padding:4px 8px; border-radius:2px; cursor:pointer; font-size:11px;">☑ All</button>';
10031d05cddcSAtari911        echo '<button type="button" onclick="deselectAll()" style="background:#666; color:white; border:none; padding:4px 8px; border-radius:2px; cursor:pointer; font-size:11px;">☐ None</button>';
10041d05cddcSAtari911        echo '<button type="button" onclick="deleteSelected()" style="background:#e74c3c; color:white; border:none; padding:4px 8px; border-radius:2px; cursor:pointer; font-size:11px; margin-left:10px;">��️ Delete</button>';
10051d05cddcSAtari911        echo '<span style="margin-left:10px;">Move to:</span>';
10069ccd446eSAtari911        echo '<input list="namespaceList" name="target_namespace" required style="padding:3px 6px; border:1px solid ' . $colors['border'] . '; border-radius:2px; font-size:11px; min-width:150px;" placeholder="Type or select...">';
10071d05cddcSAtari911        echo '<datalist id="namespaceList">';
10081d05cddcSAtari911        echo '<option value="">(default)</option>';
10091d05cddcSAtari911        foreach (array_keys($eventsByNamespace) as $ns) {
10101d05cddcSAtari911            if ($ns !== '') {
10111d05cddcSAtari911                echo '<option value="' . hsc($ns) . '">' . hsc($ns) . '</option>';
10121d05cddcSAtari911            }
10131d05cddcSAtari911        }
10141d05cddcSAtari911        echo '</datalist>';
10151d05cddcSAtari911        echo '<button type="submit" style="background:#00cc07; color:white; border:none; padding:4px 10px; border-radius:2px; cursor:pointer; font-size:11px; font-weight:bold;">➡️ Move</button>';
10161d05cddcSAtari911        echo '<button type="button" onclick="createNewNamespace()" style="background:#7b1fa2; color:white; border:none; padding:4px 10px; border-radius:2px; cursor:pointer; font-size:11px; font-weight:bold; margin-left:5px;">➕ New Namespace</button>';
1017*7e8ea635SAtari911        echo '<button type="button" onclick="cleanupEmptyNamespaces()" id="cleanup-ns-btn" style="background:#e74c3c; color:white; border:none; padding:4px 10px; border-radius:2px; cursor:pointer; font-size:11px; font-weight:bold; margin-left:5px;">�� Cleanup</button>';
10181d05cddcSAtari911        echo '<span id="selectedCount" style="margin-left:auto; color:#00cc07; font-size:11px;">0 selected</span>';
10191d05cddcSAtari911        echo '</div>';
10201d05cddcSAtari911
1021*7e8ea635SAtari911        // Cleanup status message - displayed prominently after control bar
1022*7e8ea635SAtari911        echo '<div id="cleanup-ns-status" style="font-size:12px; margin-bottom:8px; min-height:18px;"></div>';
1023*7e8ea635SAtari911
10241d05cddcSAtari911        echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">';
10251d05cddcSAtari911
10261d05cddcSAtari911        // Event list with checkboxes
10271d05cddcSAtari911        echo '<div>';
10289ccd446eSAtari911        echo '<div style="max-height:450px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px; background:' . $colors['bg'] . ';">';
10291d05cddcSAtari911
10301d05cddcSAtari911        foreach ($eventsByNamespace as $namespace => $data) {
10311d05cddcSAtari911            $nsId = 'ns_' . md5($namespace);
10321d05cddcSAtari911            $eventCount = count($data['events']);
10331d05cddcSAtari911
10341d05cddcSAtari911            echo '<div style="border-bottom:1px solid #ddd;">';
10351d05cddcSAtari911
10361d05cddcSAtari911            // Namespace header - ultra compact
10371d05cddcSAtari911            echo '<div style="background:#f5f5f5; padding:3px 6px; display:flex; justify-content:space-between; align-items:center; font-size:11px;">';
10381d05cddcSAtari911            echo '<div style="display:flex; align-items:center; gap:4px;">';
10391d05cddcSAtari911            echo '<span onclick="toggleNamespace(\'' . $nsId . '\')" style="cursor:pointer; width:12px; display:inline-block; font-size:10px;"><span id="' . $nsId . '_arrow">▶</span></span>';
10401d05cddcSAtari911            echo '<input type="checkbox" onclick="toggleNamespaceSelect(\'' . $nsId . '\')" id="' . $nsId . '_check" style="margin:0; width:12px; height:12px;">';
10411d05cddcSAtari911            echo '<span onclick="toggleNamespace(\'' . $nsId . '\')" style="cursor:pointer; font-weight:600; font-size:11px;">�� ' . hsc($namespace ?: '(default)') . '</span>';
10421d05cddcSAtari911            echo '</div>';
10431d05cddcSAtari911            echo '<div style="display:flex; gap:3px; align-items:center;">';
10441d05cddcSAtari911            echo '<span style="background:#00cc07; color:white; padding:0px 4px; border-radius:6px; font-size:9px; line-height:14px;">' . $eventCount . '</span>';
10459ccd446eSAtari911            echo '<button type="button" onclick="renameNamespace(\'' . hsc($namespace) . '\')" style="background:#3498db; color:white; border:none; padding:1px 4px; border-radius:2px; cursor:pointer; font-size:9px; line-height:14px;" title="Rename namespace">✏️</button>';
10461d05cddcSAtari911            echo '<button type="button" onclick="deleteNamespace(\'' . hsc($namespace) . '\')" style="background:#e74c3c; color:white; border:none; padding:1px 4px; border-radius:2px; cursor:pointer; font-size:9px; line-height:14px;">��️</button>';
10471d05cddcSAtari911            echo '</div>';
10481d05cddcSAtari911            echo '</div>';
10491d05cddcSAtari911
10501d05cddcSAtari911            // Events - ultra compact
10511d05cddcSAtari911            echo '<div id="' . $nsId . '" style="display:none; max-height:150px; overflow-y:auto;">';
10521d05cddcSAtari911            foreach ($data['events'] as $event) {
10531d05cddcSAtari911                $eventId = $event['id'] . '|' . $namespace . '|' . $event['date'] . '|' . $event['month'];
10541d05cddcSAtari911                $checkId = 'evt_' . md5($eventId);
10551d05cddcSAtari911
10561d05cddcSAtari911                echo '<div draggable="true" ondragstart="dragStart(event, \'' . hsc($eventId) . '\')" style="padding:2px 6px 2px 16px; border-bottom:1px solid #f8f8f8; display:flex; align-items:center; gap:4px; font-size:10px; cursor:move;" class="event-row" onmouseover="this.style.background=\'#f9f9f9\'" onmouseout="this.style.background=\'white\'">';
10571d05cddcSAtari911                echo '<input type="checkbox" name="events[]" value="' . hsc($eventId) . '" class="event-checkbox ' . $nsId . '_events" id="' . $checkId . '" onclick="updateCount()" style="margin:0; width:12px; height:12px;">';
10581d05cddcSAtari911                echo '<div style="flex:1; min-width:0;">';
10591d05cddcSAtari911                echo '<div style="font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-size:10px;">' . hsc($event['title']) . '</div>';
10601d05cddcSAtari911                echo '<div style="color:#999; font-size:9px;">' . hsc($event['date']) . ($event['startTime'] ? ' • ' . hsc($event['startTime']) : '') . '</div>';
10611d05cddcSAtari911                echo '</div>';
10621d05cddcSAtari911                echo '</div>';
10631d05cddcSAtari911            }
10641d05cddcSAtari911            echo '</div>';
10651d05cddcSAtari911            echo '</div>';
10661d05cddcSAtari911        }
10671d05cddcSAtari911
10681d05cddcSAtari911        echo '</div>';
10691d05cddcSAtari911        echo '</div>';
10701d05cddcSAtari911
10711d05cddcSAtari911        // Drop zones - ultra compact
10721d05cddcSAtari911        echo '<div>';
10731d05cddcSAtari911        echo '<div style="background:#00cc07; color:white; padding:3px 6px; border-radius:3px 3px 0 0; font-size:11px; font-weight:bold;">�� Drop Target</div>';
10749ccd446eSAtari911        echo '<div style="border:1px solid ' . $colors['border'] . '; border-top:none; border-radius:0 0 3px 3px; max-height:450px; overflow-y:auto; background:' . $colors['bg'] . ';">';
10751d05cddcSAtari911
10761d05cddcSAtari911        foreach (array_keys($eventsByNamespace) as $namespace) {
10779ccd446eSAtari911            echo '<div ondrop="drop(event, \'' . hsc($namespace) . '\')" ondragover="allowDrop(event)" style="padding:5px 6px; border-bottom:1px solid #eee; background:' . $colors['bg'] . '; min-height:28px;" onmouseover="this.style.background=\'#f0fff0\'" onmouseout="this.style.background=\'white\'">';
10781d05cddcSAtari911            echo '<div style="font-size:11px; font-weight:600; color:#00cc07;">�� ' . hsc($namespace ?: '(default)') . '</div>';
10791d05cddcSAtari911            echo '<div style="color:#999; font-size:9px; margin-top:1px;">Drop here</div>';
10801d05cddcSAtari911            echo '</div>';
10811d05cddcSAtari911        }
10821d05cddcSAtari911
10831d05cddcSAtari911        echo '</div>';
10841d05cddcSAtari911        echo '</div>';
10851d05cddcSAtari911
10861d05cddcSAtari911        echo '</div>'; // end grid
10871d05cddcSAtari911        echo '</form>';
10881d05cddcSAtari911
10891d05cddcSAtari911        echo '</div>';
10901d05cddcSAtari911
10911d05cddcSAtari911        // JavaScript
10921d05cddcSAtari911        echo '<script>
1093*7e8ea635SAtari911        var adminColors = {
1094*7e8ea635SAtari911            text: "' . $colors['text'] . '",
1095*7e8ea635SAtari911            bg: "' . $colors['bg'] . '",
1096*7e8ea635SAtari911            border: "' . $colors['border'] . '"
1097*7e8ea635SAtari911        };
10981d05cddcSAtari911        // Table sorting functionality - defined early so onclick handlers work
10991d05cddcSAtari911        let sortDirection = {}; // Track sort direction for each column
11001d05cddcSAtari911
1101*7e8ea635SAtari911        function cleanupEmptyNamespaces() {
1102*7e8ea635SAtari911            var btn = document.getElementById("cleanup-ns-btn");
1103*7e8ea635SAtari911            var status = document.getElementById("cleanup-ns-status");
1104*7e8ea635SAtari911            if (btn) { btn.textContent = "⏳ Scanning..."; btn.disabled = true; }
1105*7e8ea635SAtari911            if (status) { status.innerHTML = ""; }
1106*7e8ea635SAtari911
1107*7e8ea635SAtari911            // Dry run first
1108*7e8ea635SAtari911            fetch(DOKU_BASE + "lib/exe/ajax.php", {
1109*7e8ea635SAtari911                method: "POST",
1110*7e8ea635SAtari911                headers: {"Content-Type": "application/x-www-form-urlencoded"},
1111*7e8ea635SAtari911                body: "call=plugin_calendar&action=cleanup_empty_namespaces&dry_run=1&sectok=" + JSINFO.sectok
1112*7e8ea635SAtari911            })
1113*7e8ea635SAtari911            .then(function(r) { return r.json(); })
1114*7e8ea635SAtari911            .then(function(data) {
1115*7e8ea635SAtari911                if (btn) { btn.textContent = "�� Cleanup"; btn.disabled = false; }
1116*7e8ea635SAtari911                if (!data.success) {
1117*7e8ea635SAtari911                    if (status) { status.innerHTML = "<span style=\\\'color:#e74c3c;\\\'>❌ " + (data.error || "Failed") + "</span>"; }
1118*7e8ea635SAtari911                    return;
1119*7e8ea635SAtari911                }
1120*7e8ea635SAtari911
1121*7e8ea635SAtari911                var details = data.details || [];
1122*7e8ea635SAtari911                var totalActions = details.length;
1123*7e8ea635SAtari911
1124*7e8ea635SAtari911                if (totalActions === 0) {
1125*7e8ea635SAtari911                    if (status) { status.innerHTML = "<span style=\\\'color:#00cc07;\\\'>✅ No empty namespaces or orphan calendar folders found.</span>"; }
1126*7e8ea635SAtari911                    return;
1127*7e8ea635SAtari911                }
1128*7e8ea635SAtari911
1129*7e8ea635SAtari911                // Build detail list for confirm
1130*7e8ea635SAtari911                var msg = "Found " + totalActions + " item(s) to clean up:\\n\\n";
1131*7e8ea635SAtari911                for (var i = 0; i < details.length; i++) {
1132*7e8ea635SAtari911                    msg += "• " + details[i] + "\\n";
1133*7e8ea635SAtari911                }
1134*7e8ea635SAtari911                msg += "\\nProceed with cleanup?";
1135*7e8ea635SAtari911
1136*7e8ea635SAtari911                if (!confirm(msg)) return;
1137*7e8ea635SAtari911
1138*7e8ea635SAtari911                // Execute
1139*7e8ea635SAtari911                if (btn) { btn.textContent = "⏳ Cleaning..."; btn.disabled = true; }
1140*7e8ea635SAtari911                fetch(DOKU_BASE + "lib/exe/ajax.php", {
1141*7e8ea635SAtari911                    method: "POST",
1142*7e8ea635SAtari911                    headers: {"Content-Type": "application/x-www-form-urlencoded"},
1143*7e8ea635SAtari911                    body: "call=plugin_calendar&action=cleanup_empty_namespaces&sectok=" + JSINFO.sectok
1144*7e8ea635SAtari911                })
1145*7e8ea635SAtari911                .then(function(r) { return r.json(); })
1146*7e8ea635SAtari911                .then(function(data2) {
1147*7e8ea635SAtari911                    var msgText = data2.message || "Cleanup complete";
1148*7e8ea635SAtari911                    if (data2.details && data2.details.length > 0) {
1149*7e8ea635SAtari911                        msgText += " (" + data2.details.join(", ") + ")";
1150*7e8ea635SAtari911                    }
1151*7e8ea635SAtari911                    window.location.href = "?do=admin&page=calendar&tab=manage&msg=" + encodeURIComponent(msgText) + "&msgtype=success";
1152*7e8ea635SAtari911                });
1153*7e8ea635SAtari911            })
1154*7e8ea635SAtari911            .catch(function(err) {
1155*7e8ea635SAtari911                if (btn) { btn.textContent = "�� Cleanup"; btn.disabled = false; }
1156*7e8ea635SAtari911                if (status) { status.innerHTML = "<span style=\\\'color:#e74c3c;\\\'>❌ Error: " + err + "</span>"; }
1157*7e8ea635SAtari911            });
1158*7e8ea635SAtari911        }
1159*7e8ea635SAtari911        function trimAllPastRecurring() {
1160*7e8ea635SAtari911            var btn = document.getElementById("trim-all-past-btn");
1161*7e8ea635SAtari911            if (btn) { btn.textContent = "⏳ Counting..."; btn.disabled = true; }
1162*7e8ea635SAtari911
1163*7e8ea635SAtari911            // Step 1: dry run to get count
1164*7e8ea635SAtari911            fetch(DOKU_BASE + "lib/exe/ajax.php", {
1165*7e8ea635SAtari911                method: "POST",
1166*7e8ea635SAtari911                headers: {"Content-Type": "application/x-www-form-urlencoded"},
1167*7e8ea635SAtari911                body: "call=plugin_calendar&action=trim_all_past_recurring&dry_run=1&sectok=" + JSINFO.sectok
1168*7e8ea635SAtari911            })
1169*7e8ea635SAtari911            .then(function(r) { return r.json(); })
1170*7e8ea635SAtari911            .then(function(data) {
1171*7e8ea635SAtari911                if (btn) { btn.textContent = "✂️ Trim All Past"; btn.disabled = false; }
1172*7e8ea635SAtari911                var count = data.count || 0;
1173*7e8ea635SAtari911                if (count === 0) {
1174*7e8ea635SAtari911                    alert("No past recurring events found to remove.");
1175*7e8ea635SAtari911                    return;
1176*7e8ea635SAtari911                }
1177*7e8ea635SAtari911                if (!confirm("Found " + count + " past recurring event" + (count !== 1 ? "s" : "") + " to remove.\n\nThis cannot be undone. Proceed?")) return;
1178*7e8ea635SAtari911
1179*7e8ea635SAtari911                // Step 2: actually delete
1180*7e8ea635SAtari911                if (btn) { btn.textContent = "⏳ Trimming..."; btn.disabled = true; }
1181*7e8ea635SAtari911                fetch(DOKU_BASE + "lib/exe/ajax.php", {
1182*7e8ea635SAtari911                    method: "POST",
1183*7e8ea635SAtari911                    headers: {"Content-Type": "application/x-www-form-urlencoded"},
1184*7e8ea635SAtari911                    body: "call=plugin_calendar&action=trim_all_past_recurring&sectok=" + JSINFO.sectok
1185*7e8ea635SAtari911                })
1186*7e8ea635SAtari911                .then(function(r) { return r.json(); })
1187*7e8ea635SAtari911                .then(function(data2) {
1188*7e8ea635SAtari911                    if (btn) {
1189*7e8ea635SAtari911                        btn.textContent = data2.success ? ("✅ Removed " + (data2.count || 0)) : "❌ Failed";
1190*7e8ea635SAtari911                        btn.disabled = false;
1191*7e8ea635SAtari911                    }
1192*7e8ea635SAtari911                    setTimeout(function() { if (btn) btn.textContent = "✂️ Trim All Past"; }, 3000);
1193*7e8ea635SAtari911                    rescanRecurringEvents();
1194*7e8ea635SAtari911                });
1195*7e8ea635SAtari911            })
1196*7e8ea635SAtari911            .catch(function(err) {
1197*7e8ea635SAtari911                if (btn) { btn.textContent = "✂️ Trim All Past"; btn.disabled = false; }
1198*7e8ea635SAtari911            });
1199*7e8ea635SAtari911        }
1200*7e8ea635SAtari911
1201*7e8ea635SAtari911        function rescanRecurringEvents() {
1202*7e8ea635SAtari911            var btn = document.getElementById("rescan-recurring-btn");
1203*7e8ea635SAtari911            var content = document.getElementById("recurring-content");
1204*7e8ea635SAtari911            if (btn) { btn.textContent = "⏳ Scanning..."; btn.disabled = true; }
1205*7e8ea635SAtari911
1206*7e8ea635SAtari911            fetch(DOKU_BASE + "lib/exe/ajax.php", {
1207*7e8ea635SAtari911                method: "POST",
1208*7e8ea635SAtari911                headers: {"Content-Type": "application/x-www-form-urlencoded"},
1209*7e8ea635SAtari911                body: "call=plugin_calendar&action=rescan_recurring&sectok=" + JSINFO.sectok
1210*7e8ea635SAtari911            })
1211*7e8ea635SAtari911            .then(function(r) { return r.json(); })
1212*7e8ea635SAtari911            .then(function(data) {
1213*7e8ea635SAtari911                if (data.success && content) {
1214*7e8ea635SAtari911                    content.innerHTML = data.html;
1215*7e8ea635SAtari911                }
1216*7e8ea635SAtari911                if (btn) { btn.textContent = "�� Rescan (" + (data.count || 0) + " found)"; btn.disabled = false; }
1217*7e8ea635SAtari911                setTimeout(function() { if (btn) btn.textContent = "�� Rescan"; }, 3000);
1218*7e8ea635SAtari911            })
1219*7e8ea635SAtari911            .catch(function(err) {
1220*7e8ea635SAtari911                if (btn) { btn.textContent = "�� Rescan"; btn.disabled = false; }
1221*7e8ea635SAtari911                console.error("Rescan failed:", err);
1222*7e8ea635SAtari911            });
1223*7e8ea635SAtari911        }
1224*7e8ea635SAtari911
1225*7e8ea635SAtari911        function recurringAction(action, params, statusEl) {
1226*7e8ea635SAtari911            if (statusEl) statusEl.textContent = "⏳ Working...";
1227*7e8ea635SAtari911            var body = "call=plugin_calendar&action=" + action + "&sectok=" + JSINFO.sectok;
1228*7e8ea635SAtari911            for (var key in params) {
1229*7e8ea635SAtari911                body += "&" + encodeURIComponent(key) + "=" + encodeURIComponent(params[key]);
1230*7e8ea635SAtari911            }
1231*7e8ea635SAtari911            return fetch(DOKU_BASE + "lib/exe/ajax.php", {
1232*7e8ea635SAtari911                method: "POST",
1233*7e8ea635SAtari911                headers: {"Content-Type": "application/x-www-form-urlencoded"},
1234*7e8ea635SAtari911                body: body
1235*7e8ea635SAtari911            })
1236*7e8ea635SAtari911            .then(function(r) { return r.json(); })
1237*7e8ea635SAtari911            .then(function(data) {
1238*7e8ea635SAtari911                if (statusEl) {
1239*7e8ea635SAtari911                    statusEl.textContent = data.success ? ("✅ " + data.message) : ("❌ " + (data.error || "Failed"));
1240*7e8ea635SAtari911                    statusEl.style.color = data.success ? "#00cc07" : "#e74c3c";
1241*7e8ea635SAtari911                }
1242*7e8ea635SAtari911                return data;
1243*7e8ea635SAtari911            })
1244*7e8ea635SAtari911            .catch(function(err) {
1245*7e8ea635SAtari911                if (statusEl) { statusEl.textContent = "❌ Error: " + err; statusEl.style.color = "#e74c3c"; }
1246*7e8ea635SAtari911            });
1247*7e8ea635SAtari911        }
1248*7e8ea635SAtari911
1249*7e8ea635SAtari911        function manageRecurringSeries(title, namespace, count, firstDate, pattern, hasFlag) {
1250*7e8ea635SAtari911            var isPaused = title.indexOf("⏸") === 0;
1251*7e8ea635SAtari911            var cleanTitle = title.replace(/^⏸\s*/, "");
1252*7e8ea635SAtari911            var safeTitle = title.replace(/\x27/g, "\\\x27");
1253*7e8ea635SAtari911            var todayStr = new Date().toISOString().split("T")[0];
1254*7e8ea635SAtari911
1255*7e8ea635SAtari911            var dialog = document.createElement("div");
1256*7e8ea635SAtari911            dialog.style.cssText = "position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); display:flex; align-items:center; justify-content:center; z-index:10000;";
1257*7e8ea635SAtari911            dialog.addEventListener("click", function(e) { if (e.target === dialog) dialog.remove(); });
1258*7e8ea635SAtari911
1259*7e8ea635SAtari911            var h = "<div style=\"background:' . $colors['bg'] . '; padding:20px; border-radius:8px; min-width:520px; max-width:700px; max-height:90vh; overflow-y:auto; font-family:system-ui,sans-serif;\">";
1260*7e8ea635SAtari911            h += "<h3 style=\"margin:0 0 5px; color:#00cc07;\">⚙️ Manage Recurring Series</h3>";
1261*7e8ea635SAtari911            h += "<p style=\"margin:0 0 15px; color:' . $colors['text'] . '; font-size:13px;\"><strong>" + cleanTitle + "</strong> — " + count + " occurrences, " + pattern + ", starts " + firstDate + "</p>";
1262*7e8ea635SAtari911            h += "<div id=\"manage-status\" style=\"font-size:12px; min-height:18px; margin-bottom:10px;\"></div>";
1263*7e8ea635SAtari911
1264*7e8ea635SAtari911            // Extend
1265*7e8ea635SAtari911            h += "<div style=\"border:1px solid ' . $colors['border'] . '; border-radius:4px; padding:10px; margin-bottom:10px;\">";
1266*7e8ea635SAtari911            h += "<div style=\"font-weight:700; color:#00cc07; font-size:12px; margin-bottom:6px;\">�� Extend Series</div>";
1267*7e8ea635SAtari911            h += "<div style=\"display:flex; gap:8px; align-items:end;\">";
1268*7e8ea635SAtari911            h += "<div><label style=\"font-size:11px; display:block; margin-bottom:2px;\">Add occurrences:</label>";
1269*7e8ea635SAtari911            h += "<input type=\"number\" id=\"manage-extend-count\" value=\"4\" min=\"1\" max=\"52\" style=\"width:60px; padding:4px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;\"></div>";
1270*7e8ea635SAtari911            h += "<div><label style=\"font-size:11px; display:block; margin-bottom:2px;\">Days apart:</label>";
1271*7e8ea635SAtari911            h += "<select id=\"manage-extend-interval\" style=\"padding:4px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;\">";
1272*7e8ea635SAtari911            h += "<option value=\"1\">Daily</option><option value=\"7\" selected>Weekly</option><option value=\"14\">Bi-weekly</option><option value=\"30\">Monthly</option><option value=\"90\">Quarterly</option><option value=\"365\">Yearly</option></select></div>";
1273*7e8ea635SAtari911            h += "<button onclick=\"recurringAction(\x27extend_recurring\x27, {title:\x27" + safeTitle + "\x27, namespace:\x27" + namespace + "\x27, count:document.getElementById(\x27manage-extend-count\x27).value, interval_days:document.getElementById(\x27manage-extend-interval\x27).value}, document.getElementById(\x27manage-status\x27))\" style=\"background:#00cc07; color:#fff; border:none; padding:5px 12px; border-radius:3px; cursor:pointer; font-size:11px; font-weight:600;\">Extend</button>";
1274*7e8ea635SAtari911            h += "</div></div>";
1275*7e8ea635SAtari911
1276*7e8ea635SAtari911            // Trim
1277*7e8ea635SAtari911            h += "<div style=\"border:1px solid ' . $colors['border'] . '; border-radius:4px; padding:10px; margin-bottom:10px;\">";
1278*7e8ea635SAtari911            h += "<div style=\"font-weight:700; color:#e74c3c; font-size:12px; margin-bottom:6px;\">✂️ Trim Past Events</div>";
1279*7e8ea635SAtari911            h += "<div style=\"display:flex; gap:8px; align-items:end;\">";
1280*7e8ea635SAtari911            h += "<div><label style=\"font-size:11px; display:block; margin-bottom:2px;\">Remove before:</label>";
1281*7e8ea635SAtari911            h += "<input type=\"date\" id=\"manage-trim-date\" value=\"" + todayStr + "\" style=\"padding:4px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;\"></div>";
1282*7e8ea635SAtari911            h += "<button onclick=\"if(confirm(\x27Remove all occurrences before \x27 + document.getElementById(\x27manage-trim-date\x27).value + \x27?\x27)) recurringAction(\x27trim_recurring\x27, {title:\x27" + safeTitle + "\x27, namespace:\x27" + namespace + "\x27, cutoff_date:document.getElementById(\x27manage-trim-date\x27).value}, document.getElementById(\x27manage-status\x27))\" style=\"background:#e74c3c; color:#fff; border:none; padding:5px 12px; border-radius:3px; cursor:pointer; font-size:11px; font-weight:600;\">Trim</button>";
1283*7e8ea635SAtari911            h += "</div></div>";
1284*7e8ea635SAtari911
1285*7e8ea635SAtari911            // Change Pattern
1286*7e8ea635SAtari911            h += "<div style=\"border:1px solid ' . $colors['border'] . '; border-radius:4px; padding:10px; margin-bottom:10px;\">";
1287*7e8ea635SAtari911            h += "<div style=\"font-weight:700; color:#ff9800; font-size:12px; margin-bottom:6px;\">�� Change Pattern</div>";
1288*7e8ea635SAtari911            h += "<p style=\"font-size:11px; color:' . $colors['text'] . '; margin:0 0 6px; opacity:0.7;\">Respaces future occurrences only. Past events stay in place.</p>";
1289*7e8ea635SAtari911            h += "<div style=\"display:flex; gap:8px; align-items:end;\">";
1290*7e8ea635SAtari911            h += "<div><label style=\"font-size:11px; display:block; margin-bottom:2px;\">New interval:</label>";
1291*7e8ea635SAtari911            h += "<select id=\"manage-pattern-interval\" style=\"padding:4px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;\">";
1292*7e8ea635SAtari911            h += "<option value=\"1\">Daily</option><option value=\"7\">Weekly</option><option value=\"14\">Bi-weekly</option><option value=\"30\">Monthly</option><option value=\"90\">Quarterly</option><option value=\"365\">Yearly</option></select></div>";
1293*7e8ea635SAtari911            h += "<button onclick=\"if(confirm(\x27Respace all future occurrences?\x27)) recurringAction(\x27change_pattern_recurring\x27, {title:\x27" + safeTitle + "\x27, namespace:\x27" + namespace + "\x27, interval_days:document.getElementById(\x27manage-pattern-interval\x27).value}, document.getElementById(\x27manage-status\x27))\" style=\"background:#ff9800; color:#fff; border:none; padding:5px 12px; border-radius:3px; cursor:pointer; font-size:11px; font-weight:600;\">Change</button>";
1294*7e8ea635SAtari911            h += "</div></div>";
1295*7e8ea635SAtari911
1296*7e8ea635SAtari911            // Change Start Date
1297*7e8ea635SAtari911            h += "<div style=\"border:1px solid ' . $colors['border'] . '; border-radius:4px; padding:10px; margin-bottom:10px;\">";
1298*7e8ea635SAtari911            h += "<div style=\"font-weight:700; color:#2196f3; font-size:12px; margin-bottom:6px;\">�� Change Start Date</div>";
1299*7e8ea635SAtari911            h += "<p style=\"font-size:11px; color:' . $colors['text'] . '; margin:0 0 6px; opacity:0.7;\">Shifts ALL occurrences by the difference between old and new start date.</p>";
1300*7e8ea635SAtari911            h += "<div style=\"display:flex; gap:8px; align-items:end;\">";
1301*7e8ea635SAtari911            h += "<div><label style=\"font-size:11px; display:block; margin-bottom:2px;\">Current: " + firstDate + "</label>";
1302*7e8ea635SAtari911            h += "<input type=\"date\" id=\"manage-start-date\" value=\"" + firstDate + "\" style=\"padding:4px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;\"></div>";
1303*7e8ea635SAtari911            h += "<button onclick=\"if(confirm(\x27Shift all occurrences to new start date?\x27)) recurringAction(\x27change_start_recurring\x27, {title:\x27" + safeTitle + "\x27, namespace:\x27" + namespace + "\x27, new_start_date:document.getElementById(\x27manage-start-date\x27).value}, document.getElementById(\x27manage-status\x27))\" style=\"background:#2196f3; color:#fff; border:none; padding:5px 12px; border-radius:3px; cursor:pointer; font-size:11px; font-weight:600;\">Shift</button>";
1304*7e8ea635SAtari911            h += "</div></div>";
1305*7e8ea635SAtari911
1306*7e8ea635SAtari911            // Pause/Resume
1307*7e8ea635SAtari911            h += "<div style=\"border:1px solid ' . $colors['border'] . '; border-radius:4px; padding:10px; margin-bottom:10px;\">";
1308*7e8ea635SAtari911            h += "<div style=\"font-weight:700; color:#9c27b0; font-size:12px; margin-bottom:6px;\">" + (isPaused ? "▶️ Resume Series" : "⏸ Pause Series") + "</div>";
1309*7e8ea635SAtari911            h += "<p style=\"font-size:11px; color:' . $colors['text'] . '; margin:0 0 6px; opacity:0.7;\">" + (isPaused ? "Removes ⏸ prefix and paused flag from all occurrences." : "Adds ⏸ prefix to future occurrences. They remain in the calendar but are visually marked as paused.") + "</p>";
1310*7e8ea635SAtari911            h += "<button onclick=\"recurringAction(\x27" + (isPaused ? "resume_recurring" : "pause_recurring") + "\x27, {title:\x27" + safeTitle + "\x27, namespace:\x27" + namespace + "\x27}, document.getElementById(\x27manage-status\x27))\" style=\"background:#9c27b0; color:#fff; border:none; padding:5px 12px; border-radius:3px; cursor:pointer; font-size:11px; font-weight:600;\">" + (isPaused ? "▶️ Resume" : "⏸ Pause") + "</button>";
1311*7e8ea635SAtari911            h += "</div>";
1312*7e8ea635SAtari911
1313*7e8ea635SAtari911            // Close
1314*7e8ea635SAtari911            h += "<div style=\"text-align:right; margin-top:10px;\">";
1315*7e8ea635SAtari911            h += "<button onclick=\"this.closest(\x27[style*=fixed]\x27).remove(); rescanRecurringEvents();\" style=\"background:#666; color:#fff; border:none; padding:8px 20px; border-radius:3px; cursor:pointer; font-weight:600;\">Close</button>";
1316*7e8ea635SAtari911            h += "</div></div>";
1317*7e8ea635SAtari911
1318*7e8ea635SAtari911            dialog.innerHTML = h;
1319*7e8ea635SAtari911            document.body.appendChild(dialog);
1320*7e8ea635SAtari911        }
1321*7e8ea635SAtari911
13221d05cddcSAtari911        function sortRecurringTable(columnIndex) {
13231d05cddcSAtari911            const table = document.getElementById("recurringTable");
13241d05cddcSAtari911            const tbody = document.getElementById("recurringTableBody");
13251d05cddcSAtari911
13269ccd446eSAtari911            if (!table || !tbody) return;
13271d05cddcSAtari911
13281d05cddcSAtari911            const rows = Array.from(tbody.querySelectorAll("tr"));
13299ccd446eSAtari911            if (rows.length === 0) return;
13301d05cddcSAtari911
13311d05cddcSAtari911            // Toggle sort direction for this column
13321d05cddcSAtari911            if (!sortDirection[columnIndex]) {
13331d05cddcSAtari911                sortDirection[columnIndex] = "asc";
13341d05cddcSAtari911            } else {
13351d05cddcSAtari911                sortDirection[columnIndex] = sortDirection[columnIndex] === "asc" ? "desc" : "asc";
13361d05cddcSAtari911            }
13371d05cddcSAtari911
13381d05cddcSAtari911            const direction = sortDirection[columnIndex];
13391d05cddcSAtari911            const isNumeric = columnIndex === 4; // Count column
13401d05cddcSAtari911
13411d05cddcSAtari911            // Sort rows
13421d05cddcSAtari911            rows.sort((a, b) => {
13431d05cddcSAtari911                let aValue = a.cells[columnIndex].textContent.trim();
13441d05cddcSAtari911                let bValue = b.cells[columnIndex].textContent.trim();
13451d05cddcSAtari911
13461d05cddcSAtari911                // Extract text from code elements for namespace column
13471d05cddcSAtari911                if (columnIndex === 1) {
13481d05cddcSAtari911                    const aCode = a.cells[columnIndex].querySelector("code");
13491d05cddcSAtari911                    const bCode = b.cells[columnIndex].querySelector("code");
13501d05cddcSAtari911                    aValue = aCode ? aCode.textContent.trim() : aValue;
13511d05cddcSAtari911                    bValue = bCode ? bCode.textContent.trim() : bValue;
13521d05cddcSAtari911                }
13531d05cddcSAtari911
13541d05cddcSAtari911                // Extract number from strong elements for count column
13551d05cddcSAtari911                if (isNumeric) {
13561d05cddcSAtari911                    const aStrong = a.cells[columnIndex].querySelector("strong");
13571d05cddcSAtari911                    const bStrong = b.cells[columnIndex].querySelector("strong");
13581d05cddcSAtari911                    aValue = aStrong ? parseInt(aStrong.textContent.trim()) : 0;
13591d05cddcSAtari911                    bValue = bStrong ? parseInt(bStrong.textContent.trim()) : 0;
13601d05cddcSAtari911
13611d05cddcSAtari911                    return direction === "asc" ? aValue - bValue : bValue - aValue;
13621d05cddcSAtari911                }
13631d05cddcSAtari911
13641d05cddcSAtari911                // String comparison
13651d05cddcSAtari911                if (direction === "asc") {
13661d05cddcSAtari911                    return aValue.localeCompare(bValue);
13671d05cddcSAtari911                } else {
13681d05cddcSAtari911                    return bValue.localeCompare(aValue);
13691d05cddcSAtari911                }
13701d05cddcSAtari911            });
13711d05cddcSAtari911
13721d05cddcSAtari911            // Update arrows
13731d05cddcSAtari911            const headers = table.querySelectorAll("th");
13741d05cddcSAtari911            headers.forEach((header, index) => {
13751d05cddcSAtari911                const arrow = header.querySelector(".sort-arrow");
13761d05cddcSAtari911                if (arrow) {
13771d05cddcSAtari911                    if (index === columnIndex) {
13781d05cddcSAtari911                        arrow.textContent = direction === "asc" ? "↑" : "↓";
13791d05cddcSAtari911                        arrow.style.color = "#00cc07";
13801d05cddcSAtari911                    } else {
13811d05cddcSAtari911                        arrow.textContent = "⇅";
13821d05cddcSAtari911                        arrow.style.color = "#999";
13831d05cddcSAtari911                    }
13841d05cddcSAtari911                }
13851d05cddcSAtari911            });
13861d05cddcSAtari911
13871d05cddcSAtari911            // Rebuild tbody
13881d05cddcSAtari911            rows.forEach(row => tbody.appendChild(row));
13891d05cddcSAtari911        }
13901d05cddcSAtari911
13911d05cddcSAtari911        function filterRecurringEvents() {
13921d05cddcSAtari911            const searchInput = document.getElementById("searchRecurring");
13931d05cddcSAtari911            const filter = normalizeText(searchInput.value);
13941d05cddcSAtari911            const tbody = document.getElementById("recurringTableBody");
13951d05cddcSAtari911            const rows = tbody.getElementsByTagName("tr");
13961d05cddcSAtari911
13971d05cddcSAtari911            for (let i = 0; i < rows.length; i++) {
13981d05cddcSAtari911                const row = rows[i];
13991d05cddcSAtari911                const titleCell = row.getElementsByTagName("td")[0];
14001d05cddcSAtari911
14011d05cddcSAtari911                if (titleCell) {
14021d05cddcSAtari911                    const titleText = normalizeText(titleCell.textContent || titleCell.innerText);
14031d05cddcSAtari911
14041d05cddcSAtari911                    if (titleText.indexOf(filter) > -1) {
14051d05cddcSAtari911                        row.classList.remove("recurring-row-hidden");
14061d05cddcSAtari911                    } else {
14071d05cddcSAtari911                        row.classList.add("recurring-row-hidden");
14081d05cddcSAtari911                    }
14091d05cddcSAtari911                }
14101d05cddcSAtari911            }
14111d05cddcSAtari911        }
14121d05cddcSAtari911
14131d05cddcSAtari911        function normalizeText(text) {
14141d05cddcSAtari911            // Convert to lowercase
14151d05cddcSAtari911            text = text.toLowerCase();
14161d05cddcSAtari911
14171d05cddcSAtari911            // Remove apostrophes and quotes
14181d05cddcSAtari911            text = text.replace(/[\'\"]/g, "");
14191d05cddcSAtari911
14201d05cddcSAtari911            // Replace accented characters with regular ones
14211d05cddcSAtari911            text = text.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
14221d05cddcSAtari911
14231d05cddcSAtari911            // Remove special characters except spaces and alphanumeric
14241d05cddcSAtari911            text = text.replace(/[^a-z0-9\s]/g, "");
14251d05cddcSAtari911
14261d05cddcSAtari911            // Collapse multiple spaces
14271d05cddcSAtari911            text = text.replace(/\s+/g, " ");
14281d05cddcSAtari911
14291d05cddcSAtari911            return text.trim();
14301d05cddcSAtari911        }
14311d05cddcSAtari911
14321d05cddcSAtari911        function filterEvents() {
14331d05cddcSAtari911            const searchText = normalizeText(document.getElementById("searchEvents").value);
14341d05cddcSAtari911            const eventRows = document.querySelectorAll(".event-row");
14351d05cddcSAtari911            let visibleCount = 0;
14361d05cddcSAtari911
14371d05cddcSAtari911            eventRows.forEach(row => {
14381d05cddcSAtari911                const titleElement = row.querySelector("div div");
14391d05cddcSAtari911                const originalTitle = titleElement.getAttribute("data-original-title") || titleElement.textContent;
14401d05cddcSAtari911
14411d05cddcSAtari911                // Store original title if not already stored
14421d05cddcSAtari911                if (!titleElement.getAttribute("data-original-title")) {
14431d05cddcSAtari911                    titleElement.setAttribute("data-original-title", originalTitle);
14441d05cddcSAtari911                }
14451d05cddcSAtari911
14461d05cddcSAtari911                const normalizedTitle = normalizeText(originalTitle);
14471d05cddcSAtari911
14481d05cddcSAtari911                if (normalizedTitle.includes(searchText) || searchText === "") {
14491d05cddcSAtari911                    row.style.display = "flex";
14501d05cddcSAtari911                    visibleCount++;
14511d05cddcSAtari911                } else {
14521d05cddcSAtari911                    row.style.display = "none";
14531d05cddcSAtari911                }
14541d05cddcSAtari911            });
14551d05cddcSAtari911
14561d05cddcSAtari911            // Update namespace visibility and counts
14571d05cddcSAtari911            document.querySelectorAll("[id^=ns_]").forEach(nsDiv => {
14581d05cddcSAtari911                if (nsDiv.id.endsWith("_arrow") || nsDiv.id.endsWith("_check")) return;
14591d05cddcSAtari911
14601d05cddcSAtari911                const visibleEvents = nsDiv.querySelectorAll(".event-row[style*=\\"display: flex\\"], .event-row:not([style*=\\"display: none\\"])").length;
14611d05cddcSAtari911                const nsId = nsDiv.id;
14621d05cddcSAtari911                const arrow = document.getElementById(nsId + "_arrow");
14631d05cddcSAtari911
14641d05cddcSAtari911                // Auto-expand namespaces with matches when searching
14651d05cddcSAtari911                if (searchText && visibleEvents > 0) {
14661d05cddcSAtari911                    nsDiv.style.display = "block";
14671d05cddcSAtari911                    if (arrow) arrow.textContent = "▼";
14681d05cddcSAtari911                }
14691d05cddcSAtari911            });
14701d05cddcSAtari911        }
14711d05cddcSAtari911
14721d05cddcSAtari911        function toggleNamespace(id) {
14731d05cddcSAtari911            const elem = document.getElementById(id);
14741d05cddcSAtari911            const arrow = document.getElementById(id + "_arrow");
14751d05cddcSAtari911            if (elem.style.display === "none") {
14761d05cddcSAtari911                elem.style.display = "block";
14771d05cddcSAtari911                arrow.textContent = "▼";
14781d05cddcSAtari911            } else {
14791d05cddcSAtari911                elem.style.display = "none";
14801d05cddcSAtari911                arrow.textContent = "▶";
14811d05cddcSAtari911            }
14821d05cddcSAtari911        }
14831d05cddcSAtari911
14841d05cddcSAtari911        function toggleNamespaceSelect(nsId) {
14851d05cddcSAtari911            const checkbox = document.getElementById(nsId + "_check");
14861d05cddcSAtari911            const events = document.querySelectorAll("." + nsId + "_events");
14871d05cddcSAtari911
14881d05cddcSAtari911            // Only select visible events (not hidden by search)
14891d05cddcSAtari911            events.forEach(cb => {
14901d05cddcSAtari911                const eventRow = cb.closest(".event-row");
14911d05cddcSAtari911                if (eventRow && eventRow.style.display !== "none") {
14921d05cddcSAtari911                    cb.checked = checkbox.checked;
14931d05cddcSAtari911                }
14941d05cddcSAtari911            });
14951d05cddcSAtari911            updateCount();
14961d05cddcSAtari911        }
14971d05cddcSAtari911
14981d05cddcSAtari911        function selectAll() {
14991d05cddcSAtari911            // Only select visible events
15001d05cddcSAtari911            document.querySelectorAll(".event-checkbox").forEach(cb => {
15011d05cddcSAtari911                const eventRow = cb.closest(".event-row");
15021d05cddcSAtari911                if (eventRow && eventRow.style.display !== "none") {
15031d05cddcSAtari911                    cb.checked = true;
15041d05cddcSAtari911                }
15051d05cddcSAtari911            });
15061d05cddcSAtari911            // Update namespace checkboxes to indeterminate if partially selected
15071d05cddcSAtari911            document.querySelectorAll("input[id$=_check]").forEach(nsCheckbox => {
15081d05cddcSAtari911                const nsId = nsCheckbox.id.replace("_check", "");
15091d05cddcSAtari911                const events = document.querySelectorAll("." + nsId + "_events");
15101d05cddcSAtari911                const visibleEvents = Array.from(events).filter(cb => {
15111d05cddcSAtari911                    const row = cb.closest(".event-row");
15121d05cddcSAtari911                    return row && row.style.display !== "none";
15131d05cddcSAtari911                });
15141d05cddcSAtari911                const checkedVisible = visibleEvents.filter(cb => cb.checked);
15151d05cddcSAtari911
15161d05cddcSAtari911                if (checkedVisible.length === visibleEvents.length && visibleEvents.length > 0) {
15171d05cddcSAtari911                    nsCheckbox.checked = true;
15181d05cddcSAtari911                } else if (checkedVisible.length > 0) {
15191d05cddcSAtari911                    nsCheckbox.indeterminate = true;
15201d05cddcSAtari911                } else {
15211d05cddcSAtari911                    nsCheckbox.checked = false;
15221d05cddcSAtari911                }
15231d05cddcSAtari911            });
15241d05cddcSAtari911            updateCount();
15251d05cddcSAtari911        }
15261d05cddcSAtari911
15271d05cddcSAtari911        function deselectAll() {
15281d05cddcSAtari911            document.querySelectorAll(".event-checkbox").forEach(cb => cb.checked = false);
15291d05cddcSAtari911            document.querySelectorAll("input[id$=_check]").forEach(cb => {
15301d05cddcSAtari911                cb.checked = false;
15311d05cddcSAtari911                cb.indeterminate = false;
15321d05cddcSAtari911            });
15331d05cddcSAtari911            updateCount();
15341d05cddcSAtari911        }
15351d05cddcSAtari911
15361d05cddcSAtari911        function deleteSelected() {
15371d05cddcSAtari911            const checkedBoxes = document.querySelectorAll(".event-checkbox:checked");
15381d05cddcSAtari911            if (checkedBoxes.length === 0) {
15391d05cddcSAtari911                alert("No events selected");
15401d05cddcSAtari911                return;
15411d05cddcSAtari911            }
15421d05cddcSAtari911
15431d05cddcSAtari911            const count = checkedBoxes.length;
15441d05cddcSAtari911            if (!confirm(`Delete ${count} selected event(s)?\\n\\nThis cannot be undone!`)) {
15451d05cddcSAtari911                return;
15461d05cddcSAtari911            }
15471d05cddcSAtari911
15481d05cddcSAtari911            const form = document.createElement("form");
15491d05cddcSAtari911            form.method = "POST";
15501d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
15511d05cddcSAtari911
15521d05cddcSAtari911            const actionInput = document.createElement("input");
15531d05cddcSAtari911            actionInput.type = "hidden";
15541d05cddcSAtari911            actionInput.name = "action";
15551d05cddcSAtari911            actionInput.value = "delete_selected_events";
15561d05cddcSAtari911            form.appendChild(actionInput);
15571d05cddcSAtari911
15581d05cddcSAtari911            checkedBoxes.forEach(cb => {
15591d05cddcSAtari911                const eventInput = document.createElement("input");
15601d05cddcSAtari911                eventInput.type = "hidden";
15611d05cddcSAtari911                eventInput.name = "events[]";
15621d05cddcSAtari911                eventInput.value = cb.value;
15631d05cddcSAtari911                form.appendChild(eventInput);
15641d05cddcSAtari911            });
15651d05cddcSAtari911
15661d05cddcSAtari911            document.body.appendChild(form);
15671d05cddcSAtari911            form.submit();
15681d05cddcSAtari911        }
15691d05cddcSAtari911
15701d05cddcSAtari911        function createNewNamespace() {
15711d05cddcSAtari911            const namespaceName = prompt("Enter new namespace name:\\n\\nExamples:\\n- work\\n- personal\\n- projects:alpha\\n- aspen:travel:2025");
15721d05cddcSAtari911
15731d05cddcSAtari911            if (!namespaceName) {
15741d05cddcSAtari911                return; // Cancelled
15751d05cddcSAtari911            }
15761d05cddcSAtari911
15771d05cddcSAtari911            // Validate namespace name
15781d05cddcSAtari911            if (!/^[a-zA-Z0-9_:-]+$/.test(namespaceName)) {
15791d05cddcSAtari911                alert("Invalid namespace name.\\n\\nUse only letters, numbers, underscore, hyphen, and colon.\\nExample: work:projects:alpha");
15801d05cddcSAtari911                return;
15811d05cddcSAtari911            }
15821d05cddcSAtari911
15831d05cddcSAtari911            // Submit form to create namespace
15841d05cddcSAtari911            const form = document.createElement("form");
15851d05cddcSAtari911            form.method = "POST";
15861d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
15871d05cddcSAtari911
15881d05cddcSAtari911            const actionInput = document.createElement("input");
15891d05cddcSAtari911            actionInput.type = "hidden";
15901d05cddcSAtari911            actionInput.name = "action";
15911d05cddcSAtari911            actionInput.value = "create_namespace";
15921d05cddcSAtari911            form.appendChild(actionInput);
15931d05cddcSAtari911
15941d05cddcSAtari911            const namespaceInput = document.createElement("input");
15951d05cddcSAtari911            namespaceInput.type = "hidden";
15961d05cddcSAtari911            namespaceInput.name = "namespace_name";
15971d05cddcSAtari911            namespaceInput.value = namespaceName;
15981d05cddcSAtari911            form.appendChild(namespaceInput);
15991d05cddcSAtari911
16001d05cddcSAtari911            document.body.appendChild(form);
16011d05cddcSAtari911            form.submit();
16021d05cddcSAtari911        }
16031d05cddcSAtari911
16041d05cddcSAtari911        function updateCount() {
16051d05cddcSAtari911            const count = document.querySelectorAll(".event-checkbox:checked").length;
16061d05cddcSAtari911            document.getElementById("selectedCount").textContent = count + " selected";
16071d05cddcSAtari911        }
16081d05cddcSAtari911
16091d05cddcSAtari911        function deleteNamespace(namespace) {
16101d05cddcSAtari911            const displayName = namespace || "(default)";
16111d05cddcSAtari911            if (!confirm("Delete ENTIRE namespace: " + displayName + "?\\n\\nThis will delete ALL events in this namespace!\\n\\nThis cannot be undone!")) {
16121d05cddcSAtari911                return;
16131d05cddcSAtari911            }
16141d05cddcSAtari911            const form = document.createElement("form");
16151d05cddcSAtari911            form.method = "POST";
16161d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
16171d05cddcSAtari911            const actionInput = document.createElement("input");
16181d05cddcSAtari911            actionInput.type = "hidden";
16191d05cddcSAtari911            actionInput.name = "action";
16201d05cddcSAtari911            actionInput.value = "delete_namespace";
16211d05cddcSAtari911            form.appendChild(actionInput);
16221d05cddcSAtari911            const nsInput = document.createElement("input");
16231d05cddcSAtari911            nsInput.type = "hidden";
16241d05cddcSAtari911            nsInput.name = "namespace";
16251d05cddcSAtari911            nsInput.value = namespace;
16261d05cddcSAtari911            form.appendChild(nsInput);
16271d05cddcSAtari911            document.body.appendChild(form);
16281d05cddcSAtari911            form.submit();
16291d05cddcSAtari911        }
16301d05cddcSAtari911
16319ccd446eSAtari911        function renameNamespace(oldNamespace) {
16329ccd446eSAtari911            const displayName = oldNamespace || "(default)";
16339ccd446eSAtari911            const newName = prompt("Rename namespace: " + displayName + "\\n\\nEnter new name:", oldNamespace);
16349ccd446eSAtari911            if (newName === null || newName === oldNamespace) {
16359ccd446eSAtari911                return; // Cancelled or no change
16369ccd446eSAtari911            }
16379ccd446eSAtari911            const form = document.createElement("form");
16389ccd446eSAtari911            form.method = "POST";
16399ccd446eSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
16409ccd446eSAtari911            const actionInput = document.createElement("input");
16419ccd446eSAtari911            actionInput.type = "hidden";
16429ccd446eSAtari911            actionInput.name = "action";
16439ccd446eSAtari911            actionInput.value = "rename_namespace";
16449ccd446eSAtari911            form.appendChild(actionInput);
16459ccd446eSAtari911            const oldInput = document.createElement("input");
16469ccd446eSAtari911            oldInput.type = "hidden";
16479ccd446eSAtari911            oldInput.name = "old_namespace";
16489ccd446eSAtari911            oldInput.value = oldNamespace;
16499ccd446eSAtari911            form.appendChild(oldInput);
16509ccd446eSAtari911            const newInput = document.createElement("input");
16519ccd446eSAtari911            newInput.type = "hidden";
16529ccd446eSAtari911            newInput.name = "new_namespace";
16539ccd446eSAtari911            newInput.value = newName;
16549ccd446eSAtari911            form.appendChild(newInput);
16559ccd446eSAtari911            document.body.appendChild(form);
16569ccd446eSAtari911            form.submit();
16579ccd446eSAtari911        }
16589ccd446eSAtari911
16591d05cddcSAtari911        let draggedEvent = null;
16601d05cddcSAtari911
16611d05cddcSAtari911        function dragStart(event, eventId) {
16621d05cddcSAtari911            const checkbox = event.target.closest(".event-row").querySelector(".event-checkbox");
16631d05cddcSAtari911
16641d05cddcSAtari911            // If this event is checked, drag all checked events
16651d05cddcSAtari911            const checkedBoxes = document.querySelectorAll(".event-checkbox:checked");
16661d05cddcSAtari911            if (checkbox && checkbox.checked && checkedBoxes.length > 1) {
16671d05cddcSAtari911                // Dragging multiple selected events
16681d05cddcSAtari911                draggedEvent = "MULTIPLE";
16691d05cddcSAtari911                event.dataTransfer.setData("text/plain", "MULTIPLE");
16701d05cddcSAtari911            } else {
16711d05cddcSAtari911                // Dragging single event
16721d05cddcSAtari911                draggedEvent = eventId;
16731d05cddcSAtari911                event.dataTransfer.setData("text/plain", eventId);
16741d05cddcSAtari911            }
16751d05cddcSAtari911            event.dataTransfer.effectAllowed = "move";
16761d05cddcSAtari911            event.target.style.opacity = "0.5";
16771d05cddcSAtari911        }
16781d05cddcSAtari911
16791d05cddcSAtari911        function allowDrop(event) {
16801d05cddcSAtari911            event.preventDefault();
16811d05cddcSAtari911            event.dataTransfer.dropEffect = "move";
16821d05cddcSAtari911        }
16831d05cddcSAtari911
16841d05cddcSAtari911        function drop(event, targetNamespace) {
16851d05cddcSAtari911            event.preventDefault();
16861d05cddcSAtari911
16871d05cddcSAtari911            if (draggedEvent === "MULTIPLE") {
16881d05cddcSAtari911                // Move all selected events
16891d05cddcSAtari911                const checkedBoxes = document.querySelectorAll(".event-checkbox:checked");
16901d05cddcSAtari911                if (checkedBoxes.length === 0) return;
16911d05cddcSAtari911
16921d05cddcSAtari911                const form = document.createElement("form");
16931d05cddcSAtari911                form.method = "POST";
16941d05cddcSAtari911                form.action = "?do=admin&page=calendar&tab=manage";
16951d05cddcSAtari911
16961d05cddcSAtari911                const actionInput = document.createElement("input");
16971d05cddcSAtari911                actionInput.type = "hidden";
16981d05cddcSAtari911                actionInput.name = "action";
16991d05cddcSAtari911                actionInput.value = "move_selected_events";
17001d05cddcSAtari911                form.appendChild(actionInput);
17011d05cddcSAtari911
17021d05cddcSAtari911                checkedBoxes.forEach(cb => {
17031d05cddcSAtari911                    const eventInput = document.createElement("input");
17041d05cddcSAtari911                    eventInput.type = "hidden";
17051d05cddcSAtari911                    eventInput.name = "events[]";
17061d05cddcSAtari911                    eventInput.value = cb.value;
17071d05cddcSAtari911                    form.appendChild(eventInput);
17081d05cddcSAtari911                });
17091d05cddcSAtari911
17101d05cddcSAtari911                const targetInput = document.createElement("input");
17111d05cddcSAtari911                targetInput.type = "hidden";
17121d05cddcSAtari911                targetInput.name = "target_namespace";
17131d05cddcSAtari911                targetInput.value = targetNamespace;
17141d05cddcSAtari911                form.appendChild(targetInput);
17151d05cddcSAtari911
17161d05cddcSAtari911                document.body.appendChild(form);
17171d05cddcSAtari911                form.submit();
17181d05cddcSAtari911            } else {
17191d05cddcSAtari911                // Move single event
17201d05cddcSAtari911                if (!draggedEvent) return;
17211d05cddcSAtari911                const parts = draggedEvent.split("|");
17221d05cddcSAtari911                const sourceNamespace = parts[1];
17231d05cddcSAtari911                if (sourceNamespace === targetNamespace) return;
17241d05cddcSAtari911
17251d05cddcSAtari911                const form = document.createElement("form");
17261d05cddcSAtari911                form.method = "POST";
17271d05cddcSAtari911                form.action = "?do=admin&page=calendar&tab=manage";
17281d05cddcSAtari911                const actionInput = document.createElement("input");
17291d05cddcSAtari911                actionInput.type = "hidden";
17301d05cddcSAtari911                actionInput.name = "action";
17311d05cddcSAtari911                actionInput.value = "move_single_event";
17321d05cddcSAtari911                form.appendChild(actionInput);
17331d05cddcSAtari911                const eventInput = document.createElement("input");
17341d05cddcSAtari911                eventInput.type = "hidden";
17351d05cddcSAtari911                eventInput.name = "event";
17361d05cddcSAtari911                eventInput.value = draggedEvent;
17371d05cddcSAtari911                form.appendChild(eventInput);
17381d05cddcSAtari911                const targetInput = document.createElement("input");
17391d05cddcSAtari911                targetInput.type = "hidden";
17401d05cddcSAtari911                targetInput.name = "target_namespace";
17411d05cddcSAtari911                targetInput.value = targetNamespace;
17421d05cddcSAtari911                form.appendChild(targetInput);
17431d05cddcSAtari911                document.body.appendChild(form);
17441d05cddcSAtari911                form.submit();
17451d05cddcSAtari911            }
17461d05cddcSAtari911        }
17471d05cddcSAtari911
17481d05cddcSAtari911        function editRecurringSeries(title, namespace) {
17499ccd446eSAtari911            // Get available namespaces from the namespace explorer
17509ccd446eSAtari911            const namespaces = new Set();
17511d05cddcSAtari911
17529ccd446eSAtari911            // Method 1: Try to get from namespace explorer folder names
17539ccd446eSAtari911            document.querySelectorAll("[id^=ns_]").forEach(el => {
17549ccd446eSAtari911                const nsSpan = el.querySelector("span:nth-child(3)");
17559ccd446eSAtari911                if (nsSpan) {
17569ccd446eSAtari911                    let nsText = nsSpan.textContent.replace("�� ", "").trim();
17579ccd446eSAtari911                    if (nsText && nsText !== "(default)") {
17589ccd446eSAtari911                        namespaces.add(nsText);
17599ccd446eSAtari911                    }
17609ccd446eSAtari911                }
17619ccd446eSAtari911            });
17629ccd446eSAtari911
17639ccd446eSAtari911            // Method 2: Get from datalist if it exists
17649ccd446eSAtari911            document.querySelectorAll("#namespaceList option").forEach(opt => {
17659ccd446eSAtari911                if (opt.value && opt.value !== "") {
17669ccd446eSAtari911                    namespaces.add(opt.value);
17679ccd446eSAtari911                }
17689ccd446eSAtari911            });
17699ccd446eSAtari911
17709ccd446eSAtari911            // Convert to sorted array
17719ccd446eSAtari911            const nsArray = Array.from(namespaces).sort();
17729ccd446eSAtari911
17739ccd446eSAtari911            // Build options - include current namespace AND all others
17749ccd446eSAtari911            let nsOptions = "<option value=\\"\\">(default)</option>";
17759ccd446eSAtari911
17769ccd446eSAtari911            // Add current namespace if it\'s not default
17779ccd446eSAtari911            if (namespace && namespace !== "") {
17789ccd446eSAtari911                nsOptions += "<option value=\\"" + namespace + "\\" selected>" + namespace + " (current)</option>";
17799ccd446eSAtari911            }
17809ccd446eSAtari911
17819ccd446eSAtari911            // Add all other namespaces
17829ccd446eSAtari911            for (const ns of nsArray) {
17839ccd446eSAtari911                if (ns !== namespace) {
17849ccd446eSAtari911                    nsOptions += "<option value=\\"" + ns + "\\">" + ns + "</option>";
17851d05cddcSAtari911                }
17861d05cddcSAtari911            }
17871d05cddcSAtari911
17881d05cddcSAtari911            // Show edit dialog for recurring events
17891d05cddcSAtari911            const dialog = document.createElement("div");
17901d05cddcSAtari911            dialog.style.cssText = "position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.5); display:flex; align-items:center; justify-content:center; z-index:10000;";
17911d05cddcSAtari911
17921d05cddcSAtari911            // Close on clicking background
17931d05cddcSAtari911            dialog.addEventListener("click", function(e) {
17941d05cddcSAtari911                if (e.target === dialog) {
17951d05cddcSAtari911                    dialog.remove();
17961d05cddcSAtari911                }
17971d05cddcSAtari911            });
17981d05cddcSAtari911
17991d05cddcSAtari911            dialog.innerHTML = `
18009ccd446eSAtari911                <div style="background:' . $colors['bg'] . '; padding:20px; border-radius:8px; min-width:500px; max-width:700px; max-height:90vh; overflow-y:auto;">
18011d05cddcSAtari911                    <h3 style="margin:0 0 15px; color:#00cc07;">Edit Recurring Event</h3>
18029ccd446eSAtari911                    <p style="margin:0 0 15px; color:' . $colors['text'] . '; font-size:13px;">Changes will apply to ALL occurrences of: <strong>${title}</strong></p>
18031d05cddcSAtari911
18041d05cddcSAtari911                    <form id="editRecurringForm" style="display:flex; flex-direction:column; gap:12px;">
18051d05cddcSAtari911                        <div>
18061d05cddcSAtari911                            <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">New Title:</label>
18079ccd446eSAtari911                            <input type="text" name="new_title" value="${title}" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;" required>
18081d05cddcSAtari911                        </div>
18091d05cddcSAtari911
18101d05cddcSAtari911                        <div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">
18111d05cddcSAtari911                            <div>
18121d05cddcSAtari911                                <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">Start Time:</label>
18139ccd446eSAtari911                                <input type="time" name="start_time" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;">
18141d05cddcSAtari911                                <small style="color:#999; font-size:11px;">Leave blank to keep current</small>
18151d05cddcSAtari911                            </div>
18161d05cddcSAtari911                            <div>
18171d05cddcSAtari911                                <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">End Time:</label>
18189ccd446eSAtari911                                <input type="time" name="end_time" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;">
18191d05cddcSAtari911                                <small style="color:#999; font-size:11px;">Leave blank to keep current</small>
18201d05cddcSAtari911                            </div>
18211d05cddcSAtari911                        </div>
18221d05cddcSAtari911
18231d05cddcSAtari911                        <div>
18241d05cddcSAtari911                            <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">Interval (days between occurrences):</label>
18259ccd446eSAtari911                            <select name="interval" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;">
18261d05cddcSAtari911                                <option value="">Keep current interval</option>
18271d05cddcSAtari911                                <option value="1">Daily (1 day)</option>
18281d05cddcSAtari911                                <option value="7">Weekly (7 days)</option>
18291d05cddcSAtari911                                <option value="14">Bi-weekly (14 days)</option>
18301d05cddcSAtari911                                <option value="30">Monthly (30 days)</option>
18311d05cddcSAtari911                                <option value="365">Yearly (365 days)</option>
18321d05cddcSAtari911                            </select>
18331d05cddcSAtari911                        </div>
18341d05cddcSAtari911
18351d05cddcSAtari911                        <div>
18361d05cddcSAtari911                            <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">Move to Namespace:</label>
18379ccd446eSAtari911                            <select name="new_namespace" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;">
18381d05cddcSAtari911                                ${nsOptions}
18391d05cddcSAtari911                            </select>
18401d05cddcSAtari911                        </div>
18411d05cddcSAtari911
18421d05cddcSAtari911                        <div style="display:flex; gap:10px; margin-top:10px;">
18431d05cddcSAtari911                            <button type="submit" style="flex:1; background:#00cc07; color:white; padding:10px; border:none; border-radius:3px; cursor:pointer; font-weight:bold;">Save Changes</button>
18441d05cddcSAtari911                            <button type="button" onclick="closeEditDialog()" style="flex:1; background:#999; color:white; padding:10px; border:none; border-radius:3px; cursor:pointer;">Cancel</button>
18451d05cddcSAtari911                        </div>
18461d05cddcSAtari911                    </form>
18471d05cddcSAtari911                </div>
18481d05cddcSAtari911            `;
18491d05cddcSAtari911
18501d05cddcSAtari911            document.body.appendChild(dialog);
18511d05cddcSAtari911
18521d05cddcSAtari911            // Add close function to window
18531d05cddcSAtari911            window.closeEditDialog = function() {
18541d05cddcSAtari911                dialog.remove();
18551d05cddcSAtari911            };
18561d05cddcSAtari911
18571d05cddcSAtari911            // Handle form submission
18581d05cddcSAtari911            dialog.querySelector("#editRecurringForm").addEventListener("submit", function(e) {
18591d05cddcSAtari911                e.preventDefault();
18601d05cddcSAtari911                const formData = new FormData(this);
18611d05cddcSAtari911
18621d05cddcSAtari911                // Submit the edit
18631d05cddcSAtari911                const form = document.createElement("form");
18641d05cddcSAtari911                form.method = "POST";
18651d05cddcSAtari911                form.action = "?do=admin&page=calendar&tab=manage";
18661d05cddcSAtari911
18671d05cddcSAtari911                const actionInput = document.createElement("input");
18681d05cddcSAtari911                actionInput.type = "hidden";
18691d05cddcSAtari911                actionInput.name = "action";
18701d05cddcSAtari911                actionInput.value = "edit_recurring_series";
18711d05cddcSAtari911                form.appendChild(actionInput);
18721d05cddcSAtari911
18731d05cddcSAtari911                const oldTitleInput = document.createElement("input");
18741d05cddcSAtari911                oldTitleInput.type = "hidden";
18751d05cddcSAtari911                oldTitleInput.name = "old_title";
18761d05cddcSAtari911                oldTitleInput.value = title;
18771d05cddcSAtari911                form.appendChild(oldTitleInput);
18781d05cddcSAtari911
18791d05cddcSAtari911                const oldNamespaceInput = document.createElement("input");
18801d05cddcSAtari911                oldNamespaceInput.type = "hidden";
18811d05cddcSAtari911                oldNamespaceInput.name = "old_namespace";
18821d05cddcSAtari911                oldNamespaceInput.value = namespace;
18831d05cddcSAtari911                form.appendChild(oldNamespaceInput);
18841d05cddcSAtari911
18851d05cddcSAtari911                // Add all form fields
18861d05cddcSAtari911                for (let [key, value] of formData.entries()) {
18871d05cddcSAtari911                    const input = document.createElement("input");
18881d05cddcSAtari911                    input.type = "hidden";
18891d05cddcSAtari911                    input.name = key;
18901d05cddcSAtari911                    input.value = value;
18911d05cddcSAtari911                    form.appendChild(input);
18921d05cddcSAtari911                }
18931d05cddcSAtari911
18941d05cddcSAtari911                document.body.appendChild(form);
18951d05cddcSAtari911                form.submit();
18961d05cddcSAtari911            });
18971d05cddcSAtari911        }
18981d05cddcSAtari911
18991d05cddcSAtari911        function deleteRecurringSeries(title, namespace) {
19001d05cddcSAtari911            const displayNs = namespace || "(default)";
19011d05cddcSAtari911            if (!confirm("Delete ALL occurrences of: " + title + " (" + displayNs + ")?\\n\\nThis cannot be undone!")) {
19021d05cddcSAtari911                return;
19031d05cddcSAtari911            }
19041d05cddcSAtari911            const form = document.createElement("form");
19051d05cddcSAtari911            form.method = "POST";
19061d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=manage";
19071d05cddcSAtari911            const actionInput = document.createElement("input");
19081d05cddcSAtari911            actionInput.type = "hidden";
19091d05cddcSAtari911            actionInput.name = "action";
19101d05cddcSAtari911            actionInput.value = "delete_recurring_series";
19111d05cddcSAtari911            form.appendChild(actionInput);
19121d05cddcSAtari911            const titleInput = document.createElement("input");
19131d05cddcSAtari911            titleInput.type = "hidden";
19141d05cddcSAtari911            titleInput.name = "event_title";
19151d05cddcSAtari911            titleInput.value = title;
19161d05cddcSAtari911            form.appendChild(titleInput);
19171d05cddcSAtari911            const namespaceInput = document.createElement("input");
19181d05cddcSAtari911            namespaceInput.type = "hidden";
19191d05cddcSAtari911            namespaceInput.name = "namespace";
19201d05cddcSAtari911            namespaceInput.value = namespace;
19211d05cddcSAtari911            form.appendChild(namespaceInput);
19221d05cddcSAtari911            document.body.appendChild(form);
19231d05cddcSAtari911            form.submit();
19241d05cddcSAtari911        }
19251d05cddcSAtari911
19261d05cddcSAtari911        document.addEventListener("dragend", function(e) {
19271d05cddcSAtari911            if (e.target.draggable) {
19281d05cddcSAtari911                e.target.style.opacity = "1";
19291d05cddcSAtari911            }
19301d05cddcSAtari911        });
19311d05cddcSAtari911        </script>';
19321d05cddcSAtari911    }
19331d05cddcSAtari911
19349ccd446eSAtari911    private function renderUpdateTab($colors = null) {
19351d05cddcSAtari911        global $INPUT;
19361d05cddcSAtari911
19379ccd446eSAtari911        // Use defaults if not provided
19389ccd446eSAtari911        if ($colors === null) {
19399ccd446eSAtari911            $colors = $this->getTemplateColors();
19409ccd446eSAtari911        }
19411d05cddcSAtari911
19429ccd446eSAtari911        echo '<h2 style="margin:10px 0; font-size:20px;">�� Update Plugin</h2>';
19431d05cddcSAtari911
19441d05cddcSAtari911        // Show message if present
19451d05cddcSAtari911        if ($INPUT->has('msg')) {
19461d05cddcSAtari911            $msg = hsc($INPUT->str('msg'));
19471d05cddcSAtari911            $type = $INPUT->str('msgtype', 'success');
19481d05cddcSAtari911            $class = ($type === 'success') ? 'msg success' : 'msg error';
19499ccd446eSAtari911            echo "<div class=\"$class\" style=\"padding:10px; margin:10px 0; border-left:3px solid " . ($type === 'success' ? '#28a745' : '#dc3545') . "; background:" . ($type === 'success' ? '#d4edda' : '#f8d7da') . "; border-radius:3px; max-width:1200px;\">";
19501d05cddcSAtari911            echo $msg;
19511d05cddcSAtari911            echo "</div>";
19521d05cddcSAtari911        }
19531d05cddcSAtari911
19549ccd446eSAtari911        // Show current version FIRST (MOVED TO TOP)
19551d05cddcSAtari911        $pluginInfo = DOKU_PLUGIN . 'calendar/plugin.info.txt';
19561d05cddcSAtari911        $info = ['version' => 'Unknown', 'date' => 'Unknown', 'name' => 'Calendar Plugin', 'author' => 'Unknown', 'email' => '', 'desc' => ''];
19571d05cddcSAtari911        if (file_exists($pluginInfo)) {
19581d05cddcSAtari911            $info = array_merge($info, confToHash($pluginInfo));
19591d05cddcSAtari911        }
19601d05cddcSAtari911
19619ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
19629ccd446eSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Current Version</h3>';
19631d05cddcSAtari911        echo '<div style="font-size:12px; line-height:1.6;">';
19641d05cddcSAtari911        echo '<div style="margin:4px 0;"><strong>Version:</strong> ' . hsc($info['version']) . ' (' . hsc($info['date']) . ')</div>';
19651d05cddcSAtari911        echo '<div style="margin:4px 0;"><strong>Author:</strong> ' . hsc($info['author']) . ($info['email'] ? ' &lt;' . hsc($info['email']) . '&gt;' : '') . '</div>';
19661d05cddcSAtari911        if ($info['desc']) {
19671d05cddcSAtari911            echo '<div style="margin:4px 0;"><strong>Description:</strong> ' . hsc($info['desc']) . '</div>';
19681d05cddcSAtari911        }
19691d05cddcSAtari911        echo '<div style="margin:4px 0;"><strong>Location:</strong> <code style="background:#f0f0f0; padding:2px 4px; border-radius:2px;">' . DOKU_PLUGIN . 'calendar/</code></div>';
19701d05cddcSAtari911        echo '</div>';
19711d05cddcSAtari911
19721d05cddcSAtari911        // Check permissions
19731d05cddcSAtari911        $pluginDir = DOKU_PLUGIN . 'calendar/';
19741d05cddcSAtari911        $pluginWritable = is_writable($pluginDir);
19751d05cddcSAtari911        $parentWritable = is_writable(DOKU_PLUGIN);
19761d05cddcSAtari911
19779ccd446eSAtari911        echo '<div style="margin-top:8px; padding-top:8px; border-top:1px solid ' . $colors['border'] . ';">';
19781d05cddcSAtari911        if ($pluginWritable && $parentWritable) {
19791d05cddcSAtari911            echo '<p style="margin:5px 0; font-size:13px; color:#28a745;"><strong>✅ Permissions:</strong> OK - ready to update</p>';
19801d05cddcSAtari911        } else {
19811d05cddcSAtari911            echo '<p style="margin:5px 0; font-size:13px; color:#dc3545;"><strong>❌ Permissions:</strong> Issues detected</p>';
19821d05cddcSAtari911            if (!$pluginWritable) {
19831d05cddcSAtari911                echo '<p style="margin:2px 0 2px 20px; font-size:12px; color:#dc3545;">Plugin directory not writable</p>';
19841d05cddcSAtari911            }
19851d05cddcSAtari911            if (!$parentWritable) {
19861d05cddcSAtari911                echo '<p style="margin:2px 0 2px 20px; font-size:12px; color:#dc3545;">Parent directory not writable</p>';
19871d05cddcSAtari911            }
19889ccd446eSAtari911            echo '<p style="margin:5px 0; font-size:12px; color:' . $colors['text'] . ';">Fix with: <code style="background:#f0f0f0; padding:2px 4px; border-radius:2px;">chmod -R 755 ' . DOKU_PLUGIN . 'calendar/</code></p>';
19899ccd446eSAtari911            echo '<p style="margin:2px 0; font-size:12px; color:' . $colors['text'] . ';">Or: <code style="background:#f0f0f0; padding:2px 4px; border-radius:2px;">chown -R www-data:www-data ' . DOKU_PLUGIN . 'calendar/</code></p>';
19901d05cddcSAtari911        }
19911d05cddcSAtari911        echo '</div>';
19921d05cddcSAtari911
19931d05cddcSAtari911        echo '</div>';
19941d05cddcSAtari911
19959ccd446eSAtari911        // Combined upload and notes section (SIDE BY SIDE)
19969ccd446eSAtari911        echo '<div style="display:flex; gap:15px; max-width:1200px; margin:10px 0;">';
19971d05cddcSAtari911
19989ccd446eSAtari911        // Left side - Upload form (60% width)
19999ccd446eSAtari911        echo '<div style="flex:1; min-width:0; background:' . $colors['bg'] . '; padding:12px; border-left:3px solid #00cc07; border-radius:3px;">';
20009ccd446eSAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Upload New Version</h3>';
20019ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:13px; margin:0 0 10px;">Upload a calendar plugin ZIP file to update. Your configuration will be preserved.</p>';
20021d05cddcSAtari911
20031d05cddcSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=update" enctype="multipart/form-data" id="uploadForm">';
20041d05cddcSAtari911        echo '<input type="hidden" name="action" value="upload_update">';
20051d05cddcSAtari911        echo '<div style="margin:10px 0;">';
20069ccd446eSAtari911        echo '<input type="file" name="plugin_zip" accept=".zip" required style="padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:13px; width:100%;">';
20071d05cddcSAtari911        echo '</div>';
20081d05cddcSAtari911        echo '<div style="margin:10px 0;">';
20091d05cddcSAtari911        echo '<label style="display:flex; align-items:center; gap:8px; font-size:13px;">';
20101d05cddcSAtari911        echo '<input type="checkbox" name="backup_first" value="1" checked>';
20111d05cddcSAtari911        echo '<span>Create backup before updating (Recommended)</span>';
20121d05cddcSAtari911        echo '</label>';
20131d05cddcSAtari911        echo '</div>';
20149ccd446eSAtari911
20159ccd446eSAtari911        // Buttons side by side
20169ccd446eSAtari911        echo '<div style="display:flex; gap:10px; align-items:center; flex-wrap:wrap;">';
20171d05cddcSAtari911        echo '<button type="submit" onclick="return confirmUpload()" style="background:#00cc07; color:white; padding:10px 20px; border:none; border-radius:3px; cursor:pointer; font-size:14px; font-weight:bold;">�� Upload & Install</button>';
20181d05cddcSAtari911        echo '</form>';
20199ccd446eSAtari911
20209ccd446eSAtari911        // Clear Cache button (next to Upload button)
20219ccd446eSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=update" style="display:inline; margin:0;">';
20229ccd446eSAtari911        echo '<input type="hidden" name="action" value="clear_cache">';
20239ccd446eSAtari911        echo '<input type="hidden" name="tab" value="update">';
20249ccd446eSAtari911        echo '<button type="submit" onclick="return confirm(\'Clear all DokuWiki cache? This will refresh all plugin files.\')" style="background:#ff9800; color:white; padding:10px 20px; border:none; border-radius:3px; cursor:pointer; font-size:14px; font-weight:bold;">��️ Clear Cache</button>';
20259ccd446eSAtari911        echo '</form>';
20261d05cddcSAtari911        echo '</div>';
20271d05cddcSAtari911
20289ccd446eSAtari911        echo '<p style="margin:8px 0 0 0; font-size:12px; color:' . $colors['text'] . ';">Clear the DokuWiki cache if changes aren\'t appearing or after updating the plugin.</p>';
20299ccd446eSAtari911        echo '</div>';
20309ccd446eSAtari911
20319ccd446eSAtari911        // Right side - Important Notes (40% width)
20329ccd446eSAtari911        echo '<div style="flex:0 0 350px; min-width:0; background:#fff3e0; border-left:3px solid #ff9800; padding:12px; border-radius:3px;">';
20331d05cddcSAtari911        echo '<h4 style="margin:0 0 5px 0; color:#e65100; font-size:14px;">⚠️ Important Notes</h4>';
20349ccd446eSAtari911        echo '<ul style="margin:5px 0; padding-left:20px; font-size:12px; color:#e65100; line-height:1.6;">';
20351d05cddcSAtari911        echo '<li>This will replace all plugin files</li>';
20361d05cddcSAtari911        echo '<li>Configuration files (sync_config.php) will be preserved</li>';
20371d05cddcSAtari911        echo '<li>Event data will not be affected</li>';
20389ccd446eSAtari911        echo '<li>Backup will be saved to: <code style="font-size:10px;">calendar.backup.vX.X.X.YYYY-MM-DD_HH-MM-SS.zip</code></li>';
20391d05cddcSAtari911        echo '<li>Make sure the ZIP file is a valid calendar plugin</li>';
20401d05cddcSAtari911        echo '</ul>';
20411d05cddcSAtari911        echo '</div>';
20421d05cddcSAtari911
20439ccd446eSAtari911        echo '</div>'; // End flex container
20449ccd446eSAtari911
20459ccd446eSAtari911        // Changelog section - Timeline viewer
2046*7e8ea635SAtari911        echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">';
2047*7e8ea635SAtari911        echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">�� Version History</h3>';
20489ccd446eSAtari911
20499ccd446eSAtari911        $changelogFile = DOKU_PLUGIN . 'calendar/CHANGELOG.md';
20509ccd446eSAtari911        if (file_exists($changelogFile)) {
20519ccd446eSAtari911            $changelog = file_get_contents($changelogFile);
20529ccd446eSAtari911
20539ccd446eSAtari911            // Parse ALL versions into structured data
20549ccd446eSAtari911            $lines = explode("\n", $changelog);
20559ccd446eSAtari911            $versions = [];
20569ccd446eSAtari911            $currentVersion = null;
2057*7e8ea635SAtari911            $currentSubsection = '';
20589ccd446eSAtari911
20599ccd446eSAtari911            foreach ($lines as $line) {
2060*7e8ea635SAtari911                $trimmed = trim($line);
20619ccd446eSAtari911
20629ccd446eSAtari911                // Version header (## Version X.X.X or ## Version X.X.X (date) - title)
2063*7e8ea635SAtari911                if (preg_match('/^## Version (.+?)(?:\s*\(([^)]+)\))?\s*(?:-\s*(.+))?$/', $trimmed, $matches)) {
20649ccd446eSAtari911                    if ($currentVersion !== null) {
20659ccd446eSAtari911                        $versions[] = $currentVersion;
20669ccd446eSAtari911                    }
20679ccd446eSAtari911                    $currentVersion = [
20689ccd446eSAtari911                        'number' => trim($matches[1]),
20699ccd446eSAtari911                        'date' => isset($matches[2]) ? trim($matches[2]) : '',
20709ccd446eSAtari911                        'title' => isset($matches[3]) ? trim($matches[3]) : '',
20719ccd446eSAtari911                        'items' => []
20729ccd446eSAtari911                    ];
2073*7e8ea635SAtari911                    $currentSubsection = '';
20749ccd446eSAtari911                }
2075*7e8ea635SAtari911                // Subsection header (### Something)
2076*7e8ea635SAtari911                elseif ($currentVersion !== null && preg_match('/^### (.+)$/', $trimmed, $matches)) {
2077*7e8ea635SAtari911                    $currentSubsection = trim($matches[1]);
20789ccd446eSAtari911                    $currentVersion['items'][] = [
2079*7e8ea635SAtari911                        'type' => 'section',
2080*7e8ea635SAtari911                        'desc' => $currentSubsection
2081*7e8ea635SAtari911                    ];
2082*7e8ea635SAtari911                }
2083*7e8ea635SAtari911                // Formatted item (- **Type:** description)
2084*7e8ea635SAtari911                elseif ($currentVersion !== null && preg_match('/^- \*\*(.+?):\*\*\s*(.+)$/', $trimmed, $matches)) {
2085*7e8ea635SAtari911                    $currentVersion['items'][] = [
2086*7e8ea635SAtari911                        'type' => trim($matches[1]),
2087*7e8ea635SAtari911                        'desc' => trim($matches[2])
2088*7e8ea635SAtari911                    ];
2089*7e8ea635SAtari911                }
2090*7e8ea635SAtari911                // Plain bullet item (- something)
2091*7e8ea635SAtari911                elseif ($currentVersion !== null && preg_match('/^- (.+)$/', $trimmed, $matches)) {
2092*7e8ea635SAtari911                    $currentVersion['items'][] = [
2093*7e8ea635SAtari911                        'type' => $currentSubsection ?: 'Changed',
2094*7e8ea635SAtari911                        'desc' => trim($matches[1])
20959ccd446eSAtari911                    ];
20969ccd446eSAtari911                }
20979ccd446eSAtari911            }
2098*7e8ea635SAtari911            // Don't forget last version
20999ccd446eSAtari911            if ($currentVersion !== null) {
21009ccd446eSAtari911                $versions[] = $currentVersion;
21019ccd446eSAtari911            }
21029ccd446eSAtari911
21039ccd446eSAtari911            $totalVersions = count($versions);
21049ccd446eSAtari911            $uniqueId = 'changelog_' . substr(md5(microtime()), 0, 6);
21059ccd446eSAtari911
2106*7e8ea635SAtari911            // Find the index of the currently running version
2107*7e8ea635SAtari911            $runningVersion = trim($info['version']);
2108*7e8ea635SAtari911            $runningIndex = 0;
2109*7e8ea635SAtari911            foreach ($versions as $idx => $ver) {
2110*7e8ea635SAtari911                if (trim($ver['number']) === $runningVersion) {
2111*7e8ea635SAtari911                    $runningIndex = $idx;
2112*7e8ea635SAtari911                    break;
2113*7e8ea635SAtari911                }
2114*7e8ea635SAtari911            }
2115*7e8ea635SAtari911
21169ccd446eSAtari911            if ($totalVersions > 0) {
21179ccd446eSAtari911                // Timeline navigation bar
21189ccd446eSAtari911                echo '<div id="' . $uniqueId . '_wrap" style="position:relative;">';
21199ccd446eSAtari911
21209ccd446eSAtari911                // Nav controls
21219ccd446eSAtari911                echo '<div style="display:flex; align-items:center; gap:8px; margin-bottom:10px;">';
2122*7e8ea635SAtari911                echo '<button id="' . $uniqueId . '_prev" onclick="changelogNav(\'' . $uniqueId . '\', -1)" style="background:none; border:1px solid ' . $colors['border'] . '; color:' . $colors['text'] . '; width:32px; height:32px; border-radius:50%; cursor:pointer; font-size:16px; display:flex; align-items:center; justify-content:center; transition:all 0.15s;" onmouseover="this.style.borderColor=\'#00cc07\'; this.style.color=\'#00cc07\'" onmouseout="this.style.borderColor=\'' . $colors['border'] . '\'; this.style.color=\'' . $colors['text'] . '\'">‹</button>';
2123*7e8ea635SAtari911                echo '<div style="flex:1; text-align:center; display:flex; align-items:center; justify-content:center; gap:10px;">';
21249ccd446eSAtari911                echo '<span id="' . $uniqueId . '_counter" style="font-size:11px; color:' . $colors['text'] . '; opacity:0.7;">1 of ' . $totalVersions . '</span>';
2125*7e8ea635SAtari911                echo '<button id="' . $uniqueId . '_current" onclick="changelogJumpTo(\'' . $uniqueId . '\', ' . $runningIndex . ')" style="background:#00cc07; border:none; color:#fff; padding:3px 10px; border-radius:3px; cursor:pointer; font-size:10px; font-weight:600; letter-spacing:0.3px; transition:all 0.15s;" onmouseover="this.style.filter=\'brightness(1.2)\'" onmouseout="this.style.filter=\'none\'">Current Release</button>';
21269ccd446eSAtari911                echo '</div>';
2127*7e8ea635SAtari911                echo '<button id="' . $uniqueId . '_next" onclick="changelogNav(\'' . $uniqueId . '\', 1)" style="background:none; border:1px solid ' . $colors['border'] . '; color:' . $colors['text'] . '; width:32px; height:32px; border-radius:50%; cursor:pointer; font-size:16px; display:flex; align-items:center; justify-content:center; transition:all 0.15s;" onmouseover="this.style.borderColor=\'#00cc07\'; this.style.color=\'#00cc07\'" onmouseout="this.style.borderColor=\'' . $colors['border'] . '\'; this.style.color=\'' . $colors['text'] . '\'">›</button>';
21289ccd446eSAtari911                echo '</div>';
21299ccd446eSAtari911
21309ccd446eSAtari911                // Version cards (one per version, only first visible)
21319ccd446eSAtari911                foreach ($versions as $i => $ver) {
21329ccd446eSAtari911                    $display = ($i === 0) ? 'block' : 'none';
2133*7e8ea635SAtari911                    $isRunning = (trim($ver['number']) === $runningVersion);
2134*7e8ea635SAtari911                    $cardBorder = $isRunning ? '2px solid #00cc07' : '1px solid ' . $colors['border'];
2135*7e8ea635SAtari911                    echo '<div class="' . $uniqueId . '_card" id="' . $uniqueId . '_card_' . $i . '" style="display:' . $display . '; padding:10px; background:' . $colors['bg'] . '; border:' . $cardBorder . '; border-left:3px solid #00cc07; border-radius:4px; transition:opacity 0.2s;">';
21369ccd446eSAtari911
21379ccd446eSAtari911                    // Version header
21389ccd446eSAtari911                    echo '<div style="display:flex; align-items:baseline; gap:8px; margin-bottom:8px;">';
2139*7e8ea635SAtari911                    echo '<span style="font-weight:bold; color:#00cc07; font-size:14px;">v' . hsc($ver['number']) . '</span>';
2140*7e8ea635SAtari911                    if ($isRunning) {
2141*7e8ea635SAtari911                        echo '<span style="background:#00cc07; color:#fff; padding:1px 6px; border-radius:3px; font-size:9px; font-weight:700; letter-spacing:0.3px;">RUNNING</span>';
2142*7e8ea635SAtari911                    }
21439ccd446eSAtari911                    if ($ver['date']) {
21449ccd446eSAtari911                        echo '<span style="font-size:11px; color:' . $colors['text'] . '; opacity:0.6;">' . hsc($ver['date']) . '</span>';
21459ccd446eSAtari911                    }
21469ccd446eSAtari911                    echo '</div>';
21479ccd446eSAtari911                    if ($ver['title']) {
21489ccd446eSAtari911                        echo '<div style="font-size:12px; font-weight:600; color:' . $colors['text'] . '; margin-bottom:8px;">' . hsc($ver['title']) . '</div>';
21499ccd446eSAtari911                    }
21509ccd446eSAtari911
21519ccd446eSAtari911                    // Change items
21529ccd446eSAtari911                    if (!empty($ver['items'])) {
21539ccd446eSAtari911                        echo '<div style="font-size:12px; line-height:1.7;">';
21549ccd446eSAtari911                        foreach ($ver['items'] as $item) {
2155*7e8ea635SAtari911                            if ($item['type'] === 'section') {
2156*7e8ea635SAtari911                                echo '<div style="margin:6px 0 2px 0; font-weight:700; color:#00cc07; font-size:11px; letter-spacing:0.3px;">' . hsc($item['desc']) . '</div>';
2157*7e8ea635SAtari911                                continue;
2158*7e8ea635SAtari911                            }
21599ccd446eSAtari911                            $color = '#666'; $icon = '•';
21609ccd446eSAtari911                            $t = $item['type'];
2161*7e8ea635SAtari911                            if ($t === 'Added' || $t === 'New') { $color = '#28a745'; $icon = '✨'; }
2162*7e8ea635SAtari911                            elseif ($t === 'Fixed' || $t === 'Fix' || $t === 'Bug Fix') { $color = '#dc3545'; $icon = '��'; }
2163*7e8ea635SAtari911                            elseif ($t === 'Changed' || $t === 'Change') { $color = '#00cc07'; $icon = '��'; }
2164*7e8ea635SAtari911                            elseif ($t === 'Improved' || $t === 'Enhancement') { $color = '#ff9800'; $icon = '⚡'; }
21659ccd446eSAtari911                            elseif ($t === 'Removed') { $color = '#e91e63'; $icon = '��️'; }
21669ccd446eSAtari911                            elseif ($t === 'Development' || $t === 'Refactored') { $color = '#6c757d'; $icon = '��️'; }
21679ccd446eSAtari911                            elseif ($t === 'Result') { $color = '#2196f3'; $icon = '✅'; }
2168*7e8ea635SAtari911                            else { $color = $colors['text']; $icon = '•'; }
21699ccd446eSAtari911
21709ccd446eSAtari911                            echo '<div style="margin:2px 0; padding-left:4px;">';
21719ccd446eSAtari911                            echo '<span style="color:' . $color . '; font-weight:600;">' . $icon . ' ' . hsc($item['type']) . ':</span> ';
21729ccd446eSAtari911                            echo '<span style="color:' . $colors['text'] . ';">' . hsc($item['desc']) . '</span>';
21739ccd446eSAtari911                            echo '</div>';
21749ccd446eSAtari911                        }
21759ccd446eSAtari911                        echo '</div>';
21769ccd446eSAtari911                    } else {
21779ccd446eSAtari911                        echo '<div style="font-size:11px; color:' . $colors['text'] . '; opacity:0.5; font-style:italic;">No detailed changes recorded</div>';
21789ccd446eSAtari911                    }
21799ccd446eSAtari911
21809ccd446eSAtari911                    echo '</div>';
21819ccd446eSAtari911                }
21829ccd446eSAtari911
21839ccd446eSAtari911                echo '</div>'; // wrap
21849ccd446eSAtari911
21859ccd446eSAtari911                // JavaScript for navigation
21869ccd446eSAtari911                echo '<script>
21879ccd446eSAtari911                (function() {
21889ccd446eSAtari911                    var id = "' . $uniqueId . '";
21899ccd446eSAtari911                    var total = ' . $totalVersions . ';
21909ccd446eSAtari911                    var current = 0;
21919ccd446eSAtari911
2192*7e8ea635SAtari911                    function showCard(idx) {
21939ccd446eSAtari911                        // Hide current
21949ccd446eSAtari911                        var curCard = document.getElementById(id + "_card_" + current);
21959ccd446eSAtari911                        if (curCard) curCard.style.display = "none";
21969ccd446eSAtari911
2197*7e8ea635SAtari911                        // Show target
2198*7e8ea635SAtari911                        current = idx;
21999ccd446eSAtari911                        var nextCard = document.getElementById(id + "_card_" + current);
22009ccd446eSAtari911                        if (nextCard) nextCard.style.display = "block";
22019ccd446eSAtari911
22029ccd446eSAtari911                        // Update counter
22039ccd446eSAtari911                        var counter = document.getElementById(id + "_counter");
22049ccd446eSAtari911                        if (counter) counter.textContent = (current + 1) + " of " + total;
22059ccd446eSAtari911
22069ccd446eSAtari911                        // Update button states
22079ccd446eSAtari911                        var prevBtn = document.getElementById(id + "_prev");
22089ccd446eSAtari911                        var nextBtn = document.getElementById(id + "_next");
22099ccd446eSAtari911                        if (prevBtn) prevBtn.style.opacity = (current === 0) ? "0.3" : "1";
22109ccd446eSAtari911                        if (nextBtn) nextBtn.style.opacity = (current === total - 1) ? "0.3" : "1";
2211*7e8ea635SAtari911                    }
2212*7e8ea635SAtari911
2213*7e8ea635SAtari911                    window.changelogNav = function(uid, dir) {
2214*7e8ea635SAtari911                        if (uid !== id) return;
2215*7e8ea635SAtari911                        var next = current + dir;
2216*7e8ea635SAtari911                        if (next < 0 || next >= total) return;
2217*7e8ea635SAtari911                        showCard(next);
2218*7e8ea635SAtari911                    };
2219*7e8ea635SAtari911
2220*7e8ea635SAtari911                    window.changelogJumpTo = function(uid, idx) {
2221*7e8ea635SAtari911                        if (uid !== id) return;
2222*7e8ea635SAtari911                        if (idx < 0 || idx >= total) return;
2223*7e8ea635SAtari911                        showCard(idx);
22249ccd446eSAtari911                    };
22259ccd446eSAtari911
22269ccd446eSAtari911                    // Initialize button states
22279ccd446eSAtari911                    var prevBtn = document.getElementById(id + "_prev");
22289ccd446eSAtari911                    if (prevBtn) prevBtn.style.opacity = "0.3";
22299ccd446eSAtari911                })();
22309ccd446eSAtari911                </script>';
22319ccd446eSAtari911
22329ccd446eSAtari911            } else {
22339ccd446eSAtari911                echo '<p style="color:#999; font-size:13px; font-style:italic;">No versions found in changelog</p>';
22349ccd446eSAtari911            }
22359ccd446eSAtari911        } else {
22369ccd446eSAtari911            echo '<p style="color:#999; font-size:13px; font-style:italic;">Changelog not available</p>';
22379ccd446eSAtari911        }
22389ccd446eSAtari911
22399ccd446eSAtari911        echo '</div>';
22409ccd446eSAtari911
22419ccd446eSAtari911        // Backup list or manual backup section
22421d05cddcSAtari911        $backupDir = DOKU_PLUGIN;
22431d05cddcSAtari911        $backups = glob($backupDir . 'calendar*.zip');
22441d05cddcSAtari911
22451d05cddcSAtari911        // Filter to only show files that look like backups (not the uploaded plugin files)
22461d05cddcSAtari911        $backups = array_filter($backups, function($file) {
22471d05cddcSAtari911            $name = basename($file);
22481d05cddcSAtari911            // Include files that start with "calendar" but exclude files that are just "calendar.zip" (uploaded plugin)
22491d05cddcSAtari911            return $name !== 'calendar.zip';
22501d05cddcSAtari911        });
22511d05cddcSAtari911
22529ccd446eSAtari911        // Always show backup section (even if no backups yet)
22539ccd446eSAtari911        echo '<div id="backupSection" style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">';
22549ccd446eSAtari911        echo '<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">';
22559ccd446eSAtari911        echo '<h3 style="margin:0; color:#00cc07; font-size:16px;">�� Backups</h3>';
22569ccd446eSAtari911
22579ccd446eSAtari911        // Manual backup button
22589ccd446eSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=update" style="margin:0;">';
22599ccd446eSAtari911        echo '<input type="hidden" name="action" value="create_manual_backup">';
22609ccd446eSAtari911        echo '<button type="submit" onclick="return confirm(\'Create a backup of the current plugin version?\')" style="background:#00cc07; color:white; padding:6px 12px; border:none; border-radius:3px; cursor:pointer; font-size:12px; font-weight:bold;">�� Create Backup Now</button>';
22619ccd446eSAtari911        echo '</form>';
22629ccd446eSAtari911        echo '</div>';
22639ccd446eSAtari911
22641d05cddcSAtari911        if (!empty($backups)) {
22651d05cddcSAtari911            rsort($backups); // Newest first
22669ccd446eSAtari911            echo '<div style="max-height:200px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px; background:' . $colors['bg'] . ';">';
22679ccd446eSAtari911            echo '<table id="backupTable" style="width:100%; border-collapse:collapse; font-size:12px;">';
22681d05cddcSAtari911            echo '<thead style="position:sticky; top:0; background:#e9e9e9;">';
22691d05cddcSAtari911            echo '<tr>';
22709ccd446eSAtari911            echo '<th style="padding:6px; text-align:left; border-bottom:2px solid ' . $colors['border'] . ';">Backup File</th>';
22719ccd446eSAtari911            echo '<th style="padding:6px; text-align:left; border-bottom:2px solid ' . $colors['border'] . ';">Size</th>';
22729ccd446eSAtari911            echo '<th style="padding:6px; text-align:left; border-bottom:2px solid ' . $colors['border'] . ';">Actions</th>';
22731d05cddcSAtari911            echo '</tr></thead><tbody>';
22741d05cddcSAtari911
22751d05cddcSAtari911            foreach ($backups as $backup) {
22761d05cddcSAtari911                $filename = basename($backup);
22771d05cddcSAtari911                $size = $this->formatBytes(filesize($backup));
22781d05cddcSAtari911                echo '<tr style="border-bottom:1px solid #eee;">';
22791d05cddcSAtari911                echo '<td style="padding:6px;"><code style="font-size:11px;">' . hsc($filename) . '</code></td>';
22801d05cddcSAtari911                echo '<td style="padding:6px;">' . $size . '</td>';
22811d05cddcSAtari911                echo '<td style="padding:6px; white-space:nowrap;">';
22821d05cddcSAtari911                echo '<a href="' . DOKU_BASE . 'lib/plugins/' . hsc($filename) . '" download style="color:#00cc07; text-decoration:none; font-size:11px; margin-right:10px;">�� Download</a>';
22831d05cddcSAtari911                echo '<button onclick="renameBackup(\'' . hsc(addslashes($filename)) . '\')" style="background:#f39c12; color:white; border:none; padding:2px 6px; border-radius:2px; cursor:pointer; font-size:10px; margin-right:5px;">✏️ Rename</button>';
22841d05cddcSAtari911                echo '<button onclick="restoreBackup(\'' . hsc(addslashes($filename)) . '\')" style="background:#7b1fa2; color:white; border:none; padding:2px 6px; border-radius:2px; cursor:pointer; font-size:10px; margin-right:5px;">�� Restore</button>';
22851d05cddcSAtari911                echo '<button onclick="deleteBackup(\'' . hsc(addslashes($filename)) . '\')" style="background:#e74c3c; color:white; border:none; padding:2px 6px; border-radius:2px; cursor:pointer; font-size:10px;">��️ Delete</button>';
22861d05cddcSAtari911                echo '</td>';
22871d05cddcSAtari911                echo '</tr>';
22881d05cddcSAtari911            }
22891d05cddcSAtari911
22901d05cddcSAtari911            echo '</tbody></table>';
22911d05cddcSAtari911            echo '</div>';
22929ccd446eSAtari911        } else {
22939ccd446eSAtari911            echo '<p style="color:' . $colors['text'] . '; font-size:13px; margin:8px 0;">No backups yet. Click "Create Backup Now" to create your first backup.</p>';
22941d05cddcSAtari911        }
22959ccd446eSAtari911        echo '</div>';
22961d05cddcSAtari911
22971d05cddcSAtari911        echo '<script>
22981d05cddcSAtari911        function confirmUpload() {
22991d05cddcSAtari911            const fileInput = document.querySelector(\'input[name="plugin_zip"]\');
23001d05cddcSAtari911            if (!fileInput.files[0]) {
23011d05cddcSAtari911                alert("Please select a ZIP file");
23021d05cddcSAtari911                return false;
23031d05cddcSAtari911            }
23041d05cddcSAtari911
23051d05cddcSAtari911            const fileName = fileInput.files[0].name;
23061d05cddcSAtari911            if (!fileName.endsWith(".zip")) {
23071d05cddcSAtari911                alert("Please select a ZIP file");
23081d05cddcSAtari911                return false;
23091d05cddcSAtari911            }
23101d05cddcSAtari911
23111d05cddcSAtari911            return confirm("Upload and install: " + fileName + "?\\n\\nThis will replace all plugin files.\\nYour configuration and data will be preserved.\\n\\nContinue?");
23121d05cddcSAtari911        }
23131d05cddcSAtari911
23141d05cddcSAtari911        function deleteBackup(filename) {
23151d05cddcSAtari911            if (!confirm("Delete backup: " + filename + "?\\n\\nThis cannot be undone!")) {
23161d05cddcSAtari911                return;
23171d05cddcSAtari911            }
23181d05cddcSAtari911
23199ccd446eSAtari911            // Use AJAX to delete without page refresh
23209ccd446eSAtari911            const formData = new FormData();
23219ccd446eSAtari911            formData.append(\'action\', \'delete_backup\');
23229ccd446eSAtari911            formData.append(\'backup_file\', filename);
23231d05cddcSAtari911
23249ccd446eSAtari911            fetch(\'?do=admin&page=calendar&tab=update\', {
23259ccd446eSAtari911                method: \'POST\',
23269ccd446eSAtari911                body: formData
23279ccd446eSAtari911            })
23289ccd446eSAtari911            .then(response => response.text())
23299ccd446eSAtari911            .then(data => {
23309ccd446eSAtari911                // Remove the row from the table
23319ccd446eSAtari911                const rows = document.querySelectorAll(\'tr\');
23329ccd446eSAtari911                rows.forEach(row => {
23339ccd446eSAtari911                    if (row.textContent.includes(filename)) {
23349ccd446eSAtari911                        row.style.transition = \'opacity 0.3s\';
23359ccd446eSAtari911                        row.style.opacity = \'0\';
23369ccd446eSAtari911                        setTimeout(() => {
23379ccd446eSAtari911                            row.remove();
23389ccd446eSAtari911                            // Check if table is now empty
23399ccd446eSAtari911                            const tbody = document.querySelector(\'#backupTable tbody\');
23409ccd446eSAtari911                            if (tbody && tbody.children.length === 0) {
23419ccd446eSAtari911                                const backupSection = document.querySelector(\'#backupSection\');
23429ccd446eSAtari911                                if (backupSection) {
23439ccd446eSAtari911                                    backupSection.style.transition = \'opacity 0.3s\';
23449ccd446eSAtari911                                    backupSection.style.opacity = \'0\';
23459ccd446eSAtari911                                    setTimeout(() => backupSection.remove(), 300);
23469ccd446eSAtari911                                }
23479ccd446eSAtari911                            }
23489ccd446eSAtari911                        }, 300);
23499ccd446eSAtari911                    }
23509ccd446eSAtari911                });
23511d05cddcSAtari911
23529ccd446eSAtari911                // Show success message
23539ccd446eSAtari911                const msg = document.createElement(\'div\');
23549ccd446eSAtari911                msg.style.cssText = \'padding:10px; margin:10px 0; border-left:3px solid #28a745; background:#d4edda; border-radius:3px; max-width:900px; transition:opacity 0.3s;\';
23559ccd446eSAtari911                msg.textContent = \'✓ Backup deleted: \' + filename;
23569ccd446eSAtari911                document.querySelector(\'h2\').after(msg);
23579ccd446eSAtari911                setTimeout(() => {
23589ccd446eSAtari911                    msg.style.opacity = \'0\';
23599ccd446eSAtari911                    setTimeout(() => msg.remove(), 300);
23609ccd446eSAtari911                }, 3000);
23619ccd446eSAtari911            })
23629ccd446eSAtari911            .catch(error => {
23639ccd446eSAtari911                alert(\'Error deleting backup: \' + error);
23649ccd446eSAtari911            });
23651d05cddcSAtari911        }
23661d05cddcSAtari911
23671d05cddcSAtari911        function restoreBackup(filename) {
23681d05cddcSAtari911            if (!confirm("Restore from backup: " + filename + "?\\n\\nThis will replace all current plugin files with the backup version.\\nYour current configuration will be replaced with the backed up configuration.\\n\\nContinue?")) {
23691d05cddcSAtari911                return;
23701d05cddcSAtari911            }
23711d05cddcSAtari911
23721d05cddcSAtari911            const form = document.createElement("form");
23731d05cddcSAtari911            form.method = "POST";
23741d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=update";
23751d05cddcSAtari911
23761d05cddcSAtari911            const actionInput = document.createElement("input");
23771d05cddcSAtari911            actionInput.type = "hidden";
23781d05cddcSAtari911            actionInput.name = "action";
23791d05cddcSAtari911            actionInput.value = "restore_backup";
23801d05cddcSAtari911            form.appendChild(actionInput);
23811d05cddcSAtari911
23821d05cddcSAtari911            const filenameInput = document.createElement("input");
23831d05cddcSAtari911            filenameInput.type = "hidden";
23841d05cddcSAtari911            filenameInput.name = "backup_file";
23851d05cddcSAtari911            filenameInput.value = filename;
23861d05cddcSAtari911            form.appendChild(filenameInput);
23871d05cddcSAtari911
23881d05cddcSAtari911            document.body.appendChild(form);
23891d05cddcSAtari911            form.submit();
23901d05cddcSAtari911        }
23911d05cddcSAtari911
23921d05cddcSAtari911        function renameBackup(filename) {
23931d05cddcSAtari911            const newName = prompt("Enter new backup name (without .zip extension):\\n\\nCurrent: " + filename.replace(/\\.zip$/, ""), filename.replace(/\\.zip$/, ""));
23941d05cddcSAtari911            if (!newName || newName === filename.replace(/\\.zip$/, "")) {
23951d05cddcSAtari911                return;
23961d05cddcSAtari911            }
23971d05cddcSAtari911
23981d05cddcSAtari911            // Add .zip if not present
23991d05cddcSAtari911            const newFilename = newName.endsWith(".zip") ? newName : newName + ".zip";
24001d05cddcSAtari911
24011d05cddcSAtari911            // Basic validation
24021d05cddcSAtari911            if (!/^[a-zA-Z0-9._-]+$/.test(newFilename.replace(/\\.zip$/, ""))) {
24031d05cddcSAtari911                alert("Invalid filename. Use only letters, numbers, dots, dashes, and underscores.");
24041d05cddcSAtari911                return;
24051d05cddcSAtari911            }
24061d05cddcSAtari911
24071d05cddcSAtari911            const form = document.createElement("form");
24081d05cddcSAtari911            form.method = "POST";
24091d05cddcSAtari911            form.action = "?do=admin&page=calendar&tab=update";
24101d05cddcSAtari911
24111d05cddcSAtari911            const actionInput = document.createElement("input");
24121d05cddcSAtari911            actionInput.type = "hidden";
24131d05cddcSAtari911            actionInput.name = "action";
24141d05cddcSAtari911            actionInput.value = "rename_backup";
24151d05cddcSAtari911            form.appendChild(actionInput);
24161d05cddcSAtari911
24171d05cddcSAtari911            const oldNameInput = document.createElement("input");
24181d05cddcSAtari911            oldNameInput.type = "hidden";
24191d05cddcSAtari911            oldNameInput.name = "old_name";
24201d05cddcSAtari911            oldNameInput.value = filename;
24211d05cddcSAtari911            form.appendChild(oldNameInput);
24221d05cddcSAtari911
24231d05cddcSAtari911            const newNameInput = document.createElement("input");
24241d05cddcSAtari911            newNameInput.type = "hidden";
24251d05cddcSAtari911            newNameInput.name = "new_name";
24261d05cddcSAtari911            newNameInput.value = newFilename;
24271d05cddcSAtari911            form.appendChild(newNameInput);
24281d05cddcSAtari911
24291d05cddcSAtari911            document.body.appendChild(form);
24301d05cddcSAtari911            form.submit();
24311d05cddcSAtari911        }
24321d05cddcSAtari911        </script>';
24331d05cddcSAtari911    }
24341d05cddcSAtari911
24351d05cddcSAtari911    private function saveConfig() {
24361d05cddcSAtari911        global $INPUT;
24371d05cddcSAtari911
24381d05cddcSAtari911        // Load existing config to preserve all settings
24391d05cddcSAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
24401d05cddcSAtari911        $existingConfig = [];
24411d05cddcSAtari911        if (file_exists($configFile)) {
24421d05cddcSAtari911            $existingConfig = include $configFile;
24431d05cddcSAtari911        }
24441d05cddcSAtari911
24451d05cddcSAtari911        // Update only the fields from the form - preserve everything else
24461d05cddcSAtari911        $config = $existingConfig;
24471d05cddcSAtari911
24481d05cddcSAtari911        // Update basic fields
24491d05cddcSAtari911        $config['tenant_id'] = $INPUT->str('tenant_id');
24501d05cddcSAtari911        $config['client_id'] = $INPUT->str('client_id');
24511d05cddcSAtari911        $config['client_secret'] = $INPUT->str('client_secret');
24521d05cddcSAtari911        $config['user_email'] = $INPUT->str('user_email');
24531d05cddcSAtari911        $config['timezone'] = $INPUT->str('timezone', 'America/Los_Angeles');
24541d05cddcSAtari911        $config['default_category'] = $INPUT->str('default_category', 'Blue category');
24551d05cddcSAtari911        $config['reminder_minutes'] = $INPUT->int('reminder_minutes', 15);
24561d05cddcSAtari911        $config['sync_completed_tasks'] = $INPUT->bool('sync_completed_tasks');
24571d05cddcSAtari911        $config['delete_outlook_events'] = $INPUT->bool('delete_outlook_events');
24581d05cddcSAtari911        $config['sync_all_namespaces'] = $INPUT->bool('sync_all_namespaces');
24591d05cddcSAtari911        $config['sync_namespaces'] = $INPUT->arr('sync_namespaces');
24604590242dSAtari911        // important_namespaces is managed from the Manage tab, preserve existing value
24614590242dSAtari911        if (!isset($config['important_namespaces'])) {
24624590242dSAtari911            $config['important_namespaces'] = 'important';
24634590242dSAtari911        }
24641d05cddcSAtari911
24651d05cddcSAtari911        // Parse category mapping
24661d05cddcSAtari911        $config['category_mapping'] = [];
24671d05cddcSAtari911        $mappingText = $INPUT->str('category_mapping');
24681d05cddcSAtari911        if ($mappingText) {
24691d05cddcSAtari911            $lines = explode("\n", $mappingText);
24701d05cddcSAtari911            foreach ($lines as $line) {
24711d05cddcSAtari911                $line = trim($line);
24721d05cddcSAtari911                if (empty($line)) continue;
24731d05cddcSAtari911                $parts = explode('=', $line, 2);
24741d05cddcSAtari911                if (count($parts) === 2) {
24751d05cddcSAtari911                    $config['category_mapping'][trim($parts[0])] = trim($parts[1]);
24761d05cddcSAtari911                }
24771d05cddcSAtari911            }
24781d05cddcSAtari911        }
24791d05cddcSAtari911
24801d05cddcSAtari911        // Parse color mapping from dropdown selections
24811d05cddcSAtari911        $config['color_mapping'] = [];
24821d05cddcSAtari911        $colorMappingCount = $INPUT->int('color_mapping_count', 0);
24831d05cddcSAtari911        for ($i = 0; $i < $colorMappingCount; $i++) {
24841d05cddcSAtari911            $hexColor = $INPUT->str('color_hex_' . $i);
24851d05cddcSAtari911            $category = $INPUT->str('color_map_' . $i);
24861d05cddcSAtari911
24871d05cddcSAtari911            if (!empty($hexColor) && !empty($category)) {
24881d05cddcSAtari911                $config['color_mapping'][$hexColor] = $category;
24891d05cddcSAtari911            }
24901d05cddcSAtari911        }
24911d05cddcSAtari911
24921d05cddcSAtari911        // Build file content using return format
24931d05cddcSAtari911        $content = "<?php\n";
24941d05cddcSAtari911        $content .= "/**\n";
24951d05cddcSAtari911        $content .= " * DokuWiki Calendar → Outlook Sync - Configuration\n";
24961d05cddcSAtari911        $content .= " * \n";
24971d05cddcSAtari911        $content .= " * SECURITY: Add this file to .gitignore!\n";
24981d05cddcSAtari911        $content .= " * Never commit credentials to version control.\n";
24991d05cddcSAtari911        $content .= " */\n\n";
25001d05cddcSAtari911        $content .= "return " . var_export($config, true) . ";\n";
25011d05cddcSAtari911
25021d05cddcSAtari911        // Save file
25031d05cddcSAtari911        if (file_put_contents($configFile, $content)) {
25041d05cddcSAtari911            $this->redirect('Configuration saved successfully!', 'success');
25051d05cddcSAtari911        } else {
25061d05cddcSAtari911            $this->redirect('Error: Could not save configuration file', 'error');
25071d05cddcSAtari911        }
25081d05cddcSAtari911    }
25091d05cddcSAtari911
25101d05cddcSAtari911    private function clearCache() {
25111d05cddcSAtari911        // Clear DokuWiki cache
25121d05cddcSAtari911        $cacheDir = DOKU_INC . 'data/cache';
25131d05cddcSAtari911
25141d05cddcSAtari911        if (is_dir($cacheDir)) {
25151d05cddcSAtari911            $this->recursiveDelete($cacheDir, false);
25161d05cddcSAtari911            $this->redirect('Cache cleared successfully!', 'success', 'update');
25171d05cddcSAtari911        } else {
25181d05cddcSAtari911            $this->redirect('Cache directory not found', 'error', 'update');
25191d05cddcSAtari911        }
25201d05cddcSAtari911    }
25211d05cddcSAtari911
25221d05cddcSAtari911    private function recursiveDelete($dir, $deleteRoot = true) {
25231d05cddcSAtari911        if (!is_dir($dir)) return;
25241d05cddcSAtari911
25251d05cddcSAtari911        $files = array_diff(scandir($dir), array('.', '..'));
25261d05cddcSAtari911        foreach ($files as $file) {
25271d05cddcSAtari911            $path = $dir . '/' . $file;
25281d05cddcSAtari911            if (is_dir($path)) {
25291d05cddcSAtari911                $this->recursiveDelete($path, true);
25301d05cddcSAtari911            } else {
25311d05cddcSAtari911                @unlink($path);
25321d05cddcSAtari911            }
25331d05cddcSAtari911        }
25341d05cddcSAtari911
25351d05cddcSAtari911        if ($deleteRoot) {
25361d05cddcSAtari911            @rmdir($dir);
25371d05cddcSAtari911        }
25381d05cddcSAtari911    }
25391d05cddcSAtari911
25401d05cddcSAtari911    private function findRecurringEvents() {
25411d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
25421d05cddcSAtari911        $recurring = [];
25431d05cddcSAtari911        $allEvents = []; // Track all events to detect patterns
2544*7e8ea635SAtari911        $flaggedSeries = []; // Track events with recurring flag by recurringId
25451d05cddcSAtari911
2546*7e8ea635SAtari911        // Helper to process events from a calendar directory
2547*7e8ea635SAtari911        $processCalendarDir = function($calDir, $fallbackNamespace) use (&$allEvents, &$flaggedSeries) {
2548*7e8ea635SAtari911            if (!is_dir($calDir)) return;
2549*7e8ea635SAtari911
2550*7e8ea635SAtari911            foreach (glob($calDir . '/*.json') as $file) {
25511d05cddcSAtari911                $data = json_decode(file_get_contents($file), true);
2552*7e8ea635SAtari911                if (!$data || !is_array($data)) continue;
25531d05cddcSAtari911
25541d05cddcSAtari911                foreach ($data as $dateKey => $events) {
2555*7e8ea635SAtari911                    if (!is_array($events)) continue;
25561d05cddcSAtari911                    foreach ($events as $event) {
2557*7e8ea635SAtari911                        if (!isset($event['title']) || empty(trim($event['title']))) continue;
25581d05cddcSAtari911
2559*7e8ea635SAtari911                        $ns = isset($event['namespace']) ? $event['namespace'] : $fallbackNamespace;
2560*7e8ea635SAtari911
2561*7e8ea635SAtari911                        // If event has recurring flag, group by recurringId
2562*7e8ea635SAtari911                        if (!empty($event['recurring']) && !empty($event['recurringId'])) {
2563*7e8ea635SAtari911                            $rid = $event['recurringId'];
2564*7e8ea635SAtari911                            if (!isset($flaggedSeries[$rid])) {
2565*7e8ea635SAtari911                                $flaggedSeries[$rid] = [
25661d05cddcSAtari911                                    'title' => $event['title'],
2567*7e8ea635SAtari911                                    'namespace' => $ns,
25681d05cddcSAtari911                                    'dates' => [],
25691d05cddcSAtari911                                    'events' => []
25701d05cddcSAtari911                                ];
25711d05cddcSAtari911                            }
2572*7e8ea635SAtari911                            $flaggedSeries[$rid]['dates'][] = $dateKey;
2573*7e8ea635SAtari911                            $flaggedSeries[$rid]['events'][] = $event;
25741d05cddcSAtari911                        }
25751d05cddcSAtari911
2576*7e8ea635SAtari911                        // Also group by title+namespace for pattern detection
2577*7e8ea635SAtari911                        $groupKey = strtolower(trim($event['title'])) . '|' . $ns;
25781d05cddcSAtari911
25791d05cddcSAtari911                        if (!isset($allEvents[$groupKey])) {
25801d05cddcSAtari911                            $allEvents[$groupKey] = [
25811d05cddcSAtari911                                'title' => $event['title'],
2582*7e8ea635SAtari911                                'namespace' => $ns,
25831d05cddcSAtari911                                'dates' => [],
2584*7e8ea635SAtari911                                'events' => [],
2585*7e8ea635SAtari911                                'hasFlag' => false
25861d05cddcSAtari911                            ];
25871d05cddcSAtari911                        }
25881d05cddcSAtari911                        $allEvents[$groupKey]['dates'][] = $dateKey;
25891d05cddcSAtari911                        $allEvents[$groupKey]['events'][] = $event;
2590*7e8ea635SAtari911                        if (!empty($event['recurring'])) {
2591*7e8ea635SAtari911                            $allEvents[$groupKey]['hasFlag'] = true;
25921d05cddcSAtari911                        }
25931d05cddcSAtari911                    }
25941d05cddcSAtari911                }
25951d05cddcSAtari911            }
2596*7e8ea635SAtari911        };
2597*7e8ea635SAtari911
2598*7e8ea635SAtari911        // Check root calendar directory (blank/default namespace)
2599*7e8ea635SAtari911        $processCalendarDir($dataDir . 'calendar', '');
2600*7e8ea635SAtari911
2601*7e8ea635SAtari911        // Scan all namespace directories (including nested)
2602*7e8ea635SAtari911        $this->scanNamespaceDirs($dataDir, $processCalendarDir);
2603*7e8ea635SAtari911
2604*7e8ea635SAtari911        // Deduplicate: remove from allEvents groups that are fully covered by flaggedSeries
2605*7e8ea635SAtari911        $flaggedTitleNs = [];
2606*7e8ea635SAtari911        foreach ($flaggedSeries as $rid => $series) {
2607*7e8ea635SAtari911            $key = strtolower(trim($series['title'])) . '|' . $series['namespace'];
2608*7e8ea635SAtari911            $flaggedTitleNs[$key] = $rid;
2609*7e8ea635SAtari911        }
26101d05cddcSAtari911
2611*7e8ea635SAtari911        // Build results from flaggedSeries first (known recurring)
2612*7e8ea635SAtari911        $seen = [];
2613*7e8ea635SAtari911        foreach ($flaggedSeries as $rid => $series) {
2614*7e8ea635SAtari911            sort($series['dates']);
2615*7e8ea635SAtari911            $dedupDates = array_unique($series['dates']);
2616*7e8ea635SAtari911
2617*7e8ea635SAtari911            $pattern = $this->detectRecurrencePattern($dedupDates);
2618*7e8ea635SAtari911
2619*7e8ea635SAtari911            $recurring[] = [
2620*7e8ea635SAtari911                'baseId' => $rid,
2621*7e8ea635SAtari911                'title' => $series['title'],
2622*7e8ea635SAtari911                'namespace' => $series['namespace'],
2623*7e8ea635SAtari911                'pattern' => $pattern,
2624*7e8ea635SAtari911                'count' => count($dedupDates),
2625*7e8ea635SAtari911                'firstDate' => $dedupDates[0],
2626*7e8ea635SAtari911                'hasFlag' => true
2627*7e8ea635SAtari911            ];
2628*7e8ea635SAtari911            $seen[strtolower(trim($series['title'])) . '|' . $series['namespace']] = true;
2629*7e8ea635SAtari911        }
2630*7e8ea635SAtari911
2631*7e8ea635SAtari911        // Add pattern-detected recurring (3+ occurrences, not already in flaggedSeries)
26321d05cddcSAtari911        foreach ($allEvents as $groupKey => $group) {
2633*7e8ea635SAtari911            if (isset($seen[$groupKey])) continue;
26341d05cddcSAtari911
2635*7e8ea635SAtari911            $dedupDates = array_unique($group['dates']);
2636*7e8ea635SAtari911            sort($dedupDates);
26371d05cddcSAtari911
2638*7e8ea635SAtari911            if (count($dedupDates) < 3) continue;
26391d05cddcSAtari911
2640*7e8ea635SAtari911            $pattern = $this->detectRecurrencePattern($dedupDates);
2641*7e8ea635SAtari911
26421d05cddcSAtari911            $baseId = isset($group['events'][0]['recurringId'])
26431d05cddcSAtari911                ? $group['events'][0]['recurringId']
26441d05cddcSAtari911                : md5($group['title'] . $group['namespace']);
26451d05cddcSAtari911
26461d05cddcSAtari911            $recurring[] = [
26471d05cddcSAtari911                'baseId' => $baseId,
26481d05cddcSAtari911                'title' => $group['title'],
26491d05cddcSAtari911                'namespace' => $group['namespace'],
26501d05cddcSAtari911                'pattern' => $pattern,
2651*7e8ea635SAtari911                'count' => count($dedupDates),
2652*7e8ea635SAtari911                'firstDate' => $dedupDates[0],
2653*7e8ea635SAtari911                'hasFlag' => $group['hasFlag']
26541d05cddcSAtari911            ];
26551d05cddcSAtari911        }
2656*7e8ea635SAtari911
2657*7e8ea635SAtari911        // Sort by title
2658*7e8ea635SAtari911        usort($recurring, function($a, $b) {
2659*7e8ea635SAtari911            return strcasecmp($a['title'], $b['title']);
2660*7e8ea635SAtari911        });
2661*7e8ea635SAtari911
2662*7e8ea635SAtari911        return $recurring;
2663*7e8ea635SAtari911    }
2664*7e8ea635SAtari911
2665*7e8ea635SAtari911    /**
2666*7e8ea635SAtari911     * Recursively scan namespace directories for calendar data
2667*7e8ea635SAtari911     */
2668*7e8ea635SAtari911    private function scanNamespaceDirs($baseDir, $callback) {
2669*7e8ea635SAtari911        foreach (glob($baseDir . '*', GLOB_ONLYDIR) as $nsDir) {
2670*7e8ea635SAtari911            $namespace = basename($nsDir);
2671*7e8ea635SAtari911
2672*7e8ea635SAtari911            // Skip the root 'calendar' dir (already processed)
2673*7e8ea635SAtari911            if ($namespace === 'calendar') continue;
2674*7e8ea635SAtari911
2675*7e8ea635SAtari911            $calendarDir = $nsDir . '/calendar';
2676*7e8ea635SAtari911            if (is_dir($calendarDir)) {
2677*7e8ea635SAtari911                // Derive namespace from path relative to meta dir
2678*7e8ea635SAtari911                $metaDir = DOKU_INC . 'data/meta/';
2679*7e8ea635SAtari911                $relPath = str_replace($metaDir, '', $nsDir);
2680*7e8ea635SAtari911                $ns = str_replace('/', ':', trim($relPath, '/'));
2681*7e8ea635SAtari911                $callback($calendarDir, $ns);
2682*7e8ea635SAtari911            }
2683*7e8ea635SAtari911
2684*7e8ea635SAtari911            // Recurse into subdirectories for nested namespaces
2685*7e8ea635SAtari911            $this->scanNamespaceDirs($nsDir . '/', $callback);
2686*7e8ea635SAtari911        }
26871d05cddcSAtari911    }
26881d05cddcSAtari911
2689*7e8ea635SAtari911    /**
2690*7e8ea635SAtari911     * Detect recurrence pattern from sorted dates using median interval
2691*7e8ea635SAtari911     */
2692*7e8ea635SAtari911    private function detectRecurrencePattern($dates) {
2693*7e8ea635SAtari911        if (count($dates) < 2) return 'Single';
2694*7e8ea635SAtari911
2695*7e8ea635SAtari911        // Calculate all intervals between consecutive dates
2696*7e8ea635SAtari911        $intervals = [];
2697*7e8ea635SAtari911        for ($i = 1; $i < count($dates); $i++) {
2698*7e8ea635SAtari911            try {
2699*7e8ea635SAtari911                $d1 = new DateTime($dates[$i - 1]);
2700*7e8ea635SAtari911                $d2 = new DateTime($dates[$i]);
2701*7e8ea635SAtari911                $intervals[] = $d1->diff($d2)->days;
2702*7e8ea635SAtari911            } catch (Exception $e) {
2703*7e8ea635SAtari911                continue;
2704*7e8ea635SAtari911            }
2705*7e8ea635SAtari911        }
2706*7e8ea635SAtari911
2707*7e8ea635SAtari911        if (empty($intervals)) return 'Custom';
2708*7e8ea635SAtari911
2709*7e8ea635SAtari911        // Use median interval (more robust than first pair)
2710*7e8ea635SAtari911        sort($intervals);
2711*7e8ea635SAtari911        $mid = floor(count($intervals) / 2);
2712*7e8ea635SAtari911        $median = (count($intervals) % 2 === 0)
2713*7e8ea635SAtari911            ? ($intervals[$mid - 1] + $intervals[$mid]) / 2
2714*7e8ea635SAtari911            : $intervals[$mid];
2715*7e8ea635SAtari911
2716*7e8ea635SAtari911        if ($median <= 1) return 'Daily';
2717*7e8ea635SAtari911        if ($median >= 6 && $median <= 8) return 'Weekly';
2718*7e8ea635SAtari911        if ($median >= 13 && $median <= 16) return 'Bi-weekly';
2719*7e8ea635SAtari911        if ($median >= 27 && $median <= 32) return 'Monthly';
2720*7e8ea635SAtari911        if ($median >= 89 && $median <= 93) return 'Quarterly';
2721*7e8ea635SAtari911        if ($median >= 180 && $median <= 186) return 'Semi-annual';
2722*7e8ea635SAtari911        if ($median >= 363 && $median <= 368) return 'Yearly';
2723*7e8ea635SAtari911
2724*7e8ea635SAtari911        return 'Every ~' . round($median) . ' days';
2725*7e8ea635SAtari911    }
2726*7e8ea635SAtari911
2727*7e8ea635SAtari911    /**
2728*7e8ea635SAtari911     * Render the recurring events table HTML
2729*7e8ea635SAtari911     */
2730*7e8ea635SAtari911    private function renderRecurringTable($recurringEvents, $colors) {
2731*7e8ea635SAtari911        if (empty($recurringEvents)) {
2732*7e8ea635SAtari911            echo '<p style="color:' . $colors['text'] . '; font-size:13px; margin:5px 0;">No recurring events found.</p>';
2733*7e8ea635SAtari911            return;
2734*7e8ea635SAtari911        }
2735*7e8ea635SAtari911
2736*7e8ea635SAtari911        // Search bar
2737*7e8ea635SAtari911        echo '<div style="margin-bottom:8px;">';
2738*7e8ea635SAtari911        echo '<input type="text" id="searchRecurring" onkeyup="filterRecurringEvents()" placeholder="�� Search recurring events..." style="width:100%; padding:6px 10px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;">';
2739*7e8ea635SAtari911        echo '</div>';
2740*7e8ea635SAtari911
2741*7e8ea635SAtari911        echo '<style>
2742*7e8ea635SAtari911            .sort-arrow {
2743*7e8ea635SAtari911                color: #999;
2744*7e8ea635SAtari911                font-size: 10px;
2745*7e8ea635SAtari911                margin-left: 3px;
2746*7e8ea635SAtari911                display: inline-block;
2747*7e8ea635SAtari911            }
2748*7e8ea635SAtari911            #recurringTable th:hover {
2749*7e8ea635SAtari911                background: #ddd;
2750*7e8ea635SAtari911            }
2751*7e8ea635SAtari911            #recurringTable th:hover .sort-arrow {
2752*7e8ea635SAtari911                color: #00cc07;
2753*7e8ea635SAtari911            }
2754*7e8ea635SAtari911            .recurring-row-hidden {
2755*7e8ea635SAtari911                display: none;
2756*7e8ea635SAtari911            }
2757*7e8ea635SAtari911        </style>';
2758*7e8ea635SAtari911        echo '<div style="max-height:250px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px;">';
2759*7e8ea635SAtari911        echo '<table id="recurringTable" style="width:100%; border-collapse:collapse; font-size:11px;">';
2760*7e8ea635SAtari911        echo '<thead style="position:sticky; top:0; background:#e9e9e9;">';
2761*7e8ea635SAtari911        echo '<tr>';
2762*7e8ea635SAtari911        echo '<th onclick="sortRecurringTable(0)" style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd; cursor:pointer; user-select:none;">Title <span class="sort-arrow">⇅</span></th>';
2763*7e8ea635SAtari911        echo '<th onclick="sortRecurringTable(1)" style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd; cursor:pointer; user-select:none;">Namespace <span class="sort-arrow">⇅</span></th>';
2764*7e8ea635SAtari911        echo '<th onclick="sortRecurringTable(2)" style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd; cursor:pointer; user-select:none;">Pattern <span class="sort-arrow">⇅</span></th>';
2765*7e8ea635SAtari911        echo '<th onclick="sortRecurringTable(3)" style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd; cursor:pointer; user-select:none;">First <span class="sort-arrow">⇅</span></th>';
2766*7e8ea635SAtari911        echo '<th onclick="sortRecurringTable(4)" style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd; cursor:pointer; user-select:none;">Count <span class="sort-arrow">⇅</span></th>';
2767*7e8ea635SAtari911        echo '<th onclick="sortRecurringTable(5)" style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd; cursor:pointer; user-select:none;">Source <span class="sort-arrow">⇅</span></th>';
2768*7e8ea635SAtari911        echo '<th style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd;">Actions</th>';
2769*7e8ea635SAtari911        echo '</tr></thead><tbody id="recurringTableBody">';
2770*7e8ea635SAtari911
2771*7e8ea635SAtari911        foreach ($recurringEvents as $series) {
2772*7e8ea635SAtari911            $sourceLabel = $series['hasFlag'] ? '��️ Flagged' : '�� Detected';
2773*7e8ea635SAtari911            $sourceColor = $series['hasFlag'] ? '#00cc07' : '#ff9800';
2774*7e8ea635SAtari911            echo '<tr style="border-bottom:1px solid #eee;">';
2775*7e8ea635SAtari911            echo '<td style="padding:4px 6px;">' . hsc($series['title']) . '</td>';
2776*7e8ea635SAtari911            echo '<td style="padding:4px 6px;"><code style="background:#f0f0f0; padding:1px 3px; border-radius:2px; font-size:10px;">' . hsc($series['namespace'] ?: '(default)') . '</code></td>';
2777*7e8ea635SAtari911            echo '<td style="padding:4px 6px;">' . hsc($series['pattern']) . '</td>';
2778*7e8ea635SAtari911            echo '<td style="padding:4px 6px;">' . hsc($series['firstDate']) . '</td>';
2779*7e8ea635SAtari911            echo '<td style="padding:4px 6px;"><strong>' . $series['count'] . '</strong></td>';
2780*7e8ea635SAtari911            echo '<td style="padding:4px 6px;"><span style="color:' . $sourceColor . '; font-size:10px;">' . $sourceLabel . '</span></td>';
2781*7e8ea635SAtari911            echo '<td style="padding:4px 6px; white-space:nowrap;">';
2782*7e8ea635SAtari911            $jsTitle = hsc(addslashes($series['title']));
2783*7e8ea635SAtari911            $jsNs = hsc($series['namespace']);
2784*7e8ea635SAtari911            $jsCount = $series['count'];
2785*7e8ea635SAtari911            $jsFirst = hsc($series['firstDate']);
2786*7e8ea635SAtari911            $jsPattern = hsc($series['pattern']);
2787*7e8ea635SAtari911            $jsHasFlag = $series['hasFlag'] ? 'true' : 'false';
2788*7e8ea635SAtari911            echo '<button onclick="editRecurringSeries(\'' . $jsTitle . '\', \'' . $jsNs . '\')" style="background:#00cc07; color:white; border:none; padding:2px 6px; border-radius:2px; cursor:pointer; font-size:10px; margin-right:2px;" title="Edit title, time, namespace, interval">Edit</button>';
2789*7e8ea635SAtari911            echo '<button onclick="manageRecurringSeries(\'' . $jsTitle . '\', \'' . $jsNs . '\', ' . $jsCount . ', \'' . $jsFirst . '\', \'' . $jsPattern . '\', ' . $jsHasFlag . ')" style="background:#ff9800; color:white; border:none; padding:2px 6px; border-radius:2px; cursor:pointer; font-size:10px; margin-right:2px;" title="Extend, trim, pause, change start">Manage</button>';
2790*7e8ea635SAtari911            echo '<button onclick="deleteRecurringSeries(\'' . $jsTitle . '\', \'' . $jsNs . '\')" style="background:#e74c3c; color:white; border:none; padding:2px 6px; border-radius:2px; cursor:pointer; font-size:10px;" title="Delete all occurrences">Del</button>';
2791*7e8ea635SAtari911            echo '</td>';
2792*7e8ea635SAtari911            echo '</tr>';
2793*7e8ea635SAtari911        }
2794*7e8ea635SAtari911
2795*7e8ea635SAtari911        echo '</tbody></table>';
2796*7e8ea635SAtari911        echo '</div>';
2797*7e8ea635SAtari911        echo '<p style="color:' . $colors['text'] . '; font-size:10px; margin:5px 0 0;">Total: ' . count($recurringEvents) . ' series</p>';
2798*7e8ea635SAtari911    }
2799*7e8ea635SAtari911
2800*7e8ea635SAtari911    /**
2801*7e8ea635SAtari911     * AJAX handler: rescan recurring events and return HTML
2802*7e8ea635SAtari911     */
2803*7e8ea635SAtari911    private function handleCleanupEmptyNamespaces() {
2804*7e8ea635SAtari911        global $INPUT;
2805*7e8ea635SAtari911        $dryRun = $INPUT->bool('dry_run', false);
2806*7e8ea635SAtari911
2807*7e8ea635SAtari911        $metaDir = DOKU_INC . 'data/meta/';
2808*7e8ea635SAtari911        $details = [];
2809*7e8ea635SAtari911        $removedDirs = 0;
2810*7e8ea635SAtari911        $removedCalDirs = 0;
2811*7e8ea635SAtari911
2812*7e8ea635SAtari911        // 1. Find all calendar/ subdirectories anywhere under data/meta/
2813*7e8ea635SAtari911        $allCalDirs = [];
2814*7e8ea635SAtari911        $this->findAllCalendarDirsRecursive($metaDir, $allCalDirs);
2815*7e8ea635SAtari911
2816*7e8ea635SAtari911        // 2. Check each calendar dir for empty JSON files
2817*7e8ea635SAtari911        foreach ($allCalDirs as $calDir) {
2818*7e8ea635SAtari911            $jsonFiles = glob($calDir . '/*.json');
2819*7e8ea635SAtari911            $hasEvents = false;
2820*7e8ea635SAtari911
2821*7e8ea635SAtari911            foreach ($jsonFiles as $jsonFile) {
2822*7e8ea635SAtari911                $data = json_decode(file_get_contents($jsonFile), true);
2823*7e8ea635SAtari911                if ($data && is_array($data)) {
2824*7e8ea635SAtari911                    // Check if any date key has actual events
2825*7e8ea635SAtari911                    foreach ($data as $dateKey => $events) {
2826*7e8ea635SAtari911                        if (is_array($events) && !empty($events)) {
2827*7e8ea635SAtari911                            $hasEvents = true;
2828*7e8ea635SAtari911                            break 2;
2829*7e8ea635SAtari911                        }
2830*7e8ea635SAtari911                    }
2831*7e8ea635SAtari911                    // JSON file has data but all dates are empty — remove it
2832*7e8ea635SAtari911                    if (!$dryRun) unlink($jsonFile);
2833*7e8ea635SAtari911                }
2834*7e8ea635SAtari911            }
2835*7e8ea635SAtari911
2836*7e8ea635SAtari911            // Re-check after cleaning empty JSON files
2837*7e8ea635SAtari911            if (!$dryRun) {
2838*7e8ea635SAtari911                $jsonFiles = glob($calDir . '/*.json');
2839*7e8ea635SAtari911            }
2840*7e8ea635SAtari911
2841*7e8ea635SAtari911            // Derive display name from path
2842*7e8ea635SAtari911            $relPath = str_replace($metaDir, '', $calDir);
2843*7e8ea635SAtari911            $relPath = rtrim(str_replace('/calendar', '', $relPath), '/');
2844*7e8ea635SAtari911            $displayName = $relPath ?: '(root)';
2845*7e8ea635SAtari911
2846*7e8ea635SAtari911            if ($displayName === '(root)') continue; // Never remove root calendar dir
2847*7e8ea635SAtari911
2848*7e8ea635SAtari911            if (!$hasEvents || empty($jsonFiles)) {
2849*7e8ea635SAtari911                $removedCalDirs++;
2850*7e8ea635SAtari911                $details[] = "Remove empty calendar folder: " . $displayName . "/calendar/ (0 events)";
2851*7e8ea635SAtari911
2852*7e8ea635SAtari911                if (!$dryRun) {
2853*7e8ea635SAtari911                    // Remove all remaining files in calendar dir
2854*7e8ea635SAtari911                    foreach (glob($calDir . '/*') as $f) {
2855*7e8ea635SAtari911                        if (is_file($f)) unlink($f);
2856*7e8ea635SAtari911                    }
2857*7e8ea635SAtari911                    @rmdir($calDir);
2858*7e8ea635SAtari911
2859*7e8ea635SAtari911                    // Check if parent namespace dir is now empty too
2860*7e8ea635SAtari911                    $parentDir = dirname($calDir);
2861*7e8ea635SAtari911                    if ($parentDir !== $metaDir && is_dir($parentDir)) {
2862*7e8ea635SAtari911                        $remaining = array_diff(scandir($parentDir), ['.', '..']);
2863*7e8ea635SAtari911                        if (empty($remaining)) {
2864*7e8ea635SAtari911                            @rmdir($parentDir);
2865*7e8ea635SAtari911                            $removedDirs++;
2866*7e8ea635SAtari911                            $details[] = "Removed empty namespace directory: " . $displayName . "/";
2867*7e8ea635SAtari911                        }
2868*7e8ea635SAtari911                    }
2869*7e8ea635SAtari911                }
2870*7e8ea635SAtari911            }
2871*7e8ea635SAtari911        }
2872*7e8ea635SAtari911
2873*7e8ea635SAtari911        // 3. Also scan for namespace dirs that have a calendar/ subdir with 0 json files
2874*7e8ea635SAtari911        //    (already covered above, but also check for namespace dirs without calendar/ at all
2875*7e8ea635SAtari911        //    that are tracked in the event system)
2876*7e8ea635SAtari911
2877*7e8ea635SAtari911        $total = $removedCalDirs + $removedDirs;
2878*7e8ea635SAtari911        $message = $dryRun
2879*7e8ea635SAtari911            ? "Found $total item(s) to clean up"
2880*7e8ea635SAtari911            : "Cleaned up $removedCalDirs empty calendar folder(s)" . ($removedDirs > 0 ? " and $removedDirs empty namespace directory(ies)" : "");
2881*7e8ea635SAtari911
2882*7e8ea635SAtari911        if (!$dryRun) $this->clearStatsCache();
2883*7e8ea635SAtari911
2884*7e8ea635SAtari911        echo json_encode([
2885*7e8ea635SAtari911            'success' => true,
2886*7e8ea635SAtari911            'count' => $total,
2887*7e8ea635SAtari911            'message' => $message,
2888*7e8ea635SAtari911            'details' => $details
2889*7e8ea635SAtari911        ]);
2890*7e8ea635SAtari911    }
2891*7e8ea635SAtari911
2892*7e8ea635SAtari911    /**
2893*7e8ea635SAtari911     * Recursively find all 'calendar' directories under a base path
2894*7e8ea635SAtari911     */
2895*7e8ea635SAtari911    private function findAllCalendarDirsRecursive($baseDir, &$results) {
2896*7e8ea635SAtari911        $entries = glob($baseDir . '*', GLOB_ONLYDIR);
2897*7e8ea635SAtari911        if (!$entries) return;
2898*7e8ea635SAtari911
2899*7e8ea635SAtari911        foreach ($entries as $dir) {
2900*7e8ea635SAtari911            $name = basename($dir);
2901*7e8ea635SAtari911            if ($name === 'calendar') {
2902*7e8ea635SAtari911                $results[] = $dir;
2903*7e8ea635SAtari911            } else {
2904*7e8ea635SAtari911                // Check for calendar subdir
2905*7e8ea635SAtari911                if (is_dir($dir . '/calendar')) {
2906*7e8ea635SAtari911                    $results[] = $dir . '/calendar';
2907*7e8ea635SAtari911                }
2908*7e8ea635SAtari911                // Recurse into subdirectories for nested namespaces
2909*7e8ea635SAtari911                $this->findAllCalendarDirsRecursive($dir . '/', $results);
2910*7e8ea635SAtari911            }
2911*7e8ea635SAtari911        }
2912*7e8ea635SAtari911    }
2913*7e8ea635SAtari911
2914*7e8ea635SAtari911    private function handleTrimAllPastRecurring() {
2915*7e8ea635SAtari911        global $INPUT;
2916*7e8ea635SAtari911        $dryRun = $INPUT->bool('dry_run', false);
2917*7e8ea635SAtari911        $today = date('Y-m-d');
2918*7e8ea635SAtari911        $dataDir = DOKU_INC . 'data/meta/';
2919*7e8ea635SAtari911        $calendarDirs = [];
2920*7e8ea635SAtari911
2921*7e8ea635SAtari911        if (is_dir($dataDir . 'calendar')) {
2922*7e8ea635SAtari911            $calendarDirs[] = $dataDir . 'calendar';
2923*7e8ea635SAtari911        }
2924*7e8ea635SAtari911        $this->findCalendarDirs($dataDir, $calendarDirs);
2925*7e8ea635SAtari911
2926*7e8ea635SAtari911        $removed = 0;
2927*7e8ea635SAtari911
2928*7e8ea635SAtari911        foreach ($calendarDirs as $calDir) {
2929*7e8ea635SAtari911            foreach (glob($calDir . '/*.json') as $file) {
2930*7e8ea635SAtari911                $data = json_decode(file_get_contents($file), true);
2931*7e8ea635SAtari911                if (!$data || !is_array($data)) continue;
2932*7e8ea635SAtari911
2933*7e8ea635SAtari911                $modified = false;
2934*7e8ea635SAtari911                foreach ($data as $dateKey => &$dayEvents) {
2935*7e8ea635SAtari911                    if ($dateKey >= $today) continue;
2936*7e8ea635SAtari911                    if (!is_array($dayEvents)) continue;
2937*7e8ea635SAtari911
2938*7e8ea635SAtari911                    $filtered = [];
2939*7e8ea635SAtari911                    foreach ($dayEvents as $event) {
2940*7e8ea635SAtari911                        if (!empty($event['recurring']) || !empty($event['recurringId'])) {
2941*7e8ea635SAtari911                            $removed++;
2942*7e8ea635SAtari911                            if (!$dryRun) $modified = true;
2943*7e8ea635SAtari911                        } else {
2944*7e8ea635SAtari911                            $filtered[] = $event;
2945*7e8ea635SAtari911                        }
2946*7e8ea635SAtari911                    }
2947*7e8ea635SAtari911                    if (!$dryRun) $dayEvents = $filtered;
2948*7e8ea635SAtari911                }
2949*7e8ea635SAtari911                unset($dayEvents);
2950*7e8ea635SAtari911
2951*7e8ea635SAtari911                if (!$dryRun && $modified) {
2952*7e8ea635SAtari911                    foreach ($data as $dk => $evts) {
2953*7e8ea635SAtari911                        if (empty($evts)) unset($data[$dk]);
2954*7e8ea635SAtari911                    }
2955*7e8ea635SAtari911                    if (empty($data)) {
2956*7e8ea635SAtari911                        unlink($file);
2957*7e8ea635SAtari911                    } else {
2958*7e8ea635SAtari911                        file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
2959*7e8ea635SAtari911                    }
2960*7e8ea635SAtari911                }
2961*7e8ea635SAtari911            }
2962*7e8ea635SAtari911        }
2963*7e8ea635SAtari911
2964*7e8ea635SAtari911        if (!$dryRun) $this->clearStatsCache();
2965*7e8ea635SAtari911        echo json_encode(['success' => true, 'count' => $removed, 'message' => "Removed $removed past recurring occurrences"]);
2966*7e8ea635SAtari911    }
2967*7e8ea635SAtari911
2968*7e8ea635SAtari911    private function handleRescanRecurring() {
2969*7e8ea635SAtari911        $colors = $this->getTemplateColors();
2970*7e8ea635SAtari911        $recurringEvents = $this->findRecurringEvents();
2971*7e8ea635SAtari911
2972*7e8ea635SAtari911        ob_start();
2973*7e8ea635SAtari911        $this->renderRecurringTable($recurringEvents, $colors);
2974*7e8ea635SAtari911        $html = ob_get_clean();
2975*7e8ea635SAtari911
2976*7e8ea635SAtari911        echo json_encode([
2977*7e8ea635SAtari911            'success' => true,
2978*7e8ea635SAtari911            'html' => $html,
2979*7e8ea635SAtari911            'count' => count($recurringEvents)
2980*7e8ea635SAtari911        ]);
2981*7e8ea635SAtari911    }
2982*7e8ea635SAtari911
2983*7e8ea635SAtari911    /**
2984*7e8ea635SAtari911     * Helper: find all events matching a title in a namespace's calendar dir
2985*7e8ea635SAtari911     */
2986*7e8ea635SAtari911    private function getRecurringSeriesEvents($title, $namespace) {
2987*7e8ea635SAtari911        $dataDir = DOKU_INC . 'data/meta/';
2988*7e8ea635SAtari911        if ($namespace !== '') {
2989*7e8ea635SAtari911            $dataDir .= str_replace(':', '/', $namespace) . '/';
2990*7e8ea635SAtari911        }
2991*7e8ea635SAtari911        $dataDir .= 'calendar/';
2992*7e8ea635SAtari911
2993*7e8ea635SAtari911        $events = []; // ['date' => dateKey, 'file' => filepath, 'event' => eventData, 'index' => idx]
2994*7e8ea635SAtari911
2995*7e8ea635SAtari911        if (!is_dir($dataDir)) return $events;
2996*7e8ea635SAtari911
2997*7e8ea635SAtari911        foreach (glob($dataDir . '*.json') as $file) {
2998*7e8ea635SAtari911            $data = json_decode(file_get_contents($file), true);
2999*7e8ea635SAtari911            if (!$data || !is_array($data)) continue;
3000*7e8ea635SAtari911
3001*7e8ea635SAtari911            foreach ($data as $dateKey => $dayEvents) {
3002*7e8ea635SAtari911                if (!is_array($dayEvents)) continue;
3003*7e8ea635SAtari911                foreach ($dayEvents as $idx => $event) {
3004*7e8ea635SAtari911                    if (strtolower(trim($event['title'])) === strtolower(trim($title))) {
3005*7e8ea635SAtari911                        $events[] = [
3006*7e8ea635SAtari911                            'date' => $dateKey,
3007*7e8ea635SAtari911                            'file' => $file,
3008*7e8ea635SAtari911                            'event' => $event,
3009*7e8ea635SAtari911                            'index' => $idx
3010*7e8ea635SAtari911                        ];
3011*7e8ea635SAtari911                    }
3012*7e8ea635SAtari911                }
3013*7e8ea635SAtari911            }
3014*7e8ea635SAtari911        }
3015*7e8ea635SAtari911
3016*7e8ea635SAtari911        // Sort by date
3017*7e8ea635SAtari911        usort($events, function($a, $b) {
3018*7e8ea635SAtari911            return strcmp($a['date'], $b['date']);
3019*7e8ea635SAtari911        });
3020*7e8ea635SAtari911
3021*7e8ea635SAtari911        return $events;
3022*7e8ea635SAtari911    }
3023*7e8ea635SAtari911
3024*7e8ea635SAtari911    /**
3025*7e8ea635SAtari911     * Extend series: add more future occurrences
3026*7e8ea635SAtari911     */
3027*7e8ea635SAtari911    private function handleExtendRecurring() {
3028*7e8ea635SAtari911        global $INPUT;
3029*7e8ea635SAtari911        $title = $INPUT->str('title');
3030*7e8ea635SAtari911        $namespace = $INPUT->str('namespace');
3031*7e8ea635SAtari911        $count = $INPUT->int('count', 4);
3032*7e8ea635SAtari911        $intervalDays = $INPUT->int('interval_days', 7);
3033*7e8ea635SAtari911
3034*7e8ea635SAtari911        $events = $this->getRecurringSeriesEvents($title, $namespace);
3035*7e8ea635SAtari911        if (empty($events)) {
3036*7e8ea635SAtari911            echo json_encode(['success' => false, 'error' => 'Series not found']);
3037*7e8ea635SAtari911            return;
3038*7e8ea635SAtari911        }
3039*7e8ea635SAtari911
3040*7e8ea635SAtari911        // Use last event as template
3041*7e8ea635SAtari911        $lastEvent = end($events);
3042*7e8ea635SAtari911        $lastDate = new DateTime($lastEvent['date']);
3043*7e8ea635SAtari911        $template = $lastEvent['event'];
3044*7e8ea635SAtari911
3045*7e8ea635SAtari911        $dataDir = DOKU_INC . 'data/meta/';
3046*7e8ea635SAtari911        if ($namespace !== '') {
3047*7e8ea635SAtari911            $dataDir .= str_replace(':', '/', $namespace) . '/';
3048*7e8ea635SAtari911        }
3049*7e8ea635SAtari911        $dataDir .= 'calendar/';
3050*7e8ea635SAtari911
3051*7e8ea635SAtari911        if (!is_dir($dataDir)) mkdir($dataDir, 0755, true);
3052*7e8ea635SAtari911
3053*7e8ea635SAtari911        $added = 0;
3054*7e8ea635SAtari911        $baseId = isset($template['recurringId']) ? $template['recurringId'] : md5($title . $namespace);
3055*7e8ea635SAtari911        $maxExistingIdx = 0;
3056*7e8ea635SAtari911        foreach ($events as $e) {
3057*7e8ea635SAtari911            if (isset($e['event']['id']) && preg_match('/-(\d+)$/', $e['event']['id'], $m)) {
3058*7e8ea635SAtari911                $maxExistingIdx = max($maxExistingIdx, (int)$m[1]);
3059*7e8ea635SAtari911            }
3060*7e8ea635SAtari911        }
3061*7e8ea635SAtari911
3062*7e8ea635SAtari911        for ($i = 1; $i <= $count; $i++) {
3063*7e8ea635SAtari911            $newDate = clone $lastDate;
3064*7e8ea635SAtari911            $newDate->modify('+' . ($i * $intervalDays) . ' days');
3065*7e8ea635SAtari911            $dateKey = $newDate->format('Y-m-d');
3066*7e8ea635SAtari911            list($year, $month) = explode('-', $dateKey);
3067*7e8ea635SAtari911
3068*7e8ea635SAtari911            $file = $dataDir . sprintf('%04d-%02d.json', $year, $month);
3069*7e8ea635SAtari911            $fileData = file_exists($file) ? json_decode(file_get_contents($file), true) : [];
3070*7e8ea635SAtari911            if (!is_array($fileData)) $fileData = [];
3071*7e8ea635SAtari911
3072*7e8ea635SAtari911            if (!isset($fileData[$dateKey])) $fileData[$dateKey] = [];
3073*7e8ea635SAtari911
3074*7e8ea635SAtari911            $newEvent = $template;
3075*7e8ea635SAtari911            $newEvent['id'] = $baseId . '-' . ($maxExistingIdx + $i);
3076*7e8ea635SAtari911            $newEvent['recurring'] = true;
3077*7e8ea635SAtari911            $newEvent['recurringId'] = $baseId;
3078*7e8ea635SAtari911            $newEvent['created'] = date('Y-m-d H:i:s');
3079*7e8ea635SAtari911            unset($newEvent['completed']);
3080*7e8ea635SAtari911            $newEvent['completed'] = false;
3081*7e8ea635SAtari911
3082*7e8ea635SAtari911            $fileData[$dateKey][] = $newEvent;
3083*7e8ea635SAtari911            file_put_contents($file, json_encode($fileData, JSON_PRETTY_PRINT));
3084*7e8ea635SAtari911            $added++;
3085*7e8ea635SAtari911        }
3086*7e8ea635SAtari911
3087*7e8ea635SAtari911        $this->clearStatsCache();
3088*7e8ea635SAtari911        echo json_encode(['success' => true, 'message' => "Added $added new occurrences"]);
3089*7e8ea635SAtari911    }
3090*7e8ea635SAtari911
3091*7e8ea635SAtari911    /**
3092*7e8ea635SAtari911     * Trim series: remove past occurrences before a cutoff date
3093*7e8ea635SAtari911     */
3094*7e8ea635SAtari911    private function handleTrimRecurring() {
3095*7e8ea635SAtari911        global $INPUT;
3096*7e8ea635SAtari911        $title = $INPUT->str('title');
3097*7e8ea635SAtari911        $namespace = $INPUT->str('namespace');
3098*7e8ea635SAtari911        $cutoffDate = $INPUT->str('cutoff_date', date('Y-m-d'));
3099*7e8ea635SAtari911
3100*7e8ea635SAtari911        $events = $this->getRecurringSeriesEvents($title, $namespace);
3101*7e8ea635SAtari911        $removed = 0;
3102*7e8ea635SAtari911
3103*7e8ea635SAtari911        foreach ($events as $entry) {
3104*7e8ea635SAtari911            if ($entry['date'] < $cutoffDate) {
3105*7e8ea635SAtari911                // Remove this event from its file
3106*7e8ea635SAtari911                $data = json_decode(file_get_contents($entry['file']), true);
3107*7e8ea635SAtari911                if (!$data || !isset($data[$entry['date']])) continue;
3108*7e8ea635SAtari911
3109*7e8ea635SAtari911                // Find and remove by matching title
3110*7e8ea635SAtari911                foreach ($data[$entry['date']] as $k => $evt) {
3111*7e8ea635SAtari911                    if (strtolower(trim($evt['title'])) === strtolower(trim($title))) {
3112*7e8ea635SAtari911                        unset($data[$entry['date']][$k]);
3113*7e8ea635SAtari911                        $data[$entry['date']] = array_values($data[$entry['date']]);
3114*7e8ea635SAtari911                        $removed++;
3115*7e8ea635SAtari911                        break;
3116*7e8ea635SAtari911                    }
3117*7e8ea635SAtari911                }
3118*7e8ea635SAtari911
3119*7e8ea635SAtari911                // Clean up empty dates
3120*7e8ea635SAtari911                if (empty($data[$entry['date']])) unset($data[$entry['date']]);
3121*7e8ea635SAtari911
3122*7e8ea635SAtari911                if (empty($data)) {
3123*7e8ea635SAtari911                    unlink($entry['file']);
3124*7e8ea635SAtari911                } else {
3125*7e8ea635SAtari911                    file_put_contents($entry['file'], json_encode($data, JSON_PRETTY_PRINT));
3126*7e8ea635SAtari911                }
3127*7e8ea635SAtari911            }
3128*7e8ea635SAtari911        }
3129*7e8ea635SAtari911
3130*7e8ea635SAtari911        $this->clearStatsCache();
3131*7e8ea635SAtari911        echo json_encode(['success' => true, 'message' => "Removed $removed past occurrences before $cutoffDate"]);
3132*7e8ea635SAtari911    }
3133*7e8ea635SAtari911
3134*7e8ea635SAtari911    /**
3135*7e8ea635SAtari911     * Pause series: mark all future occurrences as paused
3136*7e8ea635SAtari911     */
3137*7e8ea635SAtari911    private function handlePauseRecurring() {
3138*7e8ea635SAtari911        global $INPUT;
3139*7e8ea635SAtari911        $title = $INPUT->str('title');
3140*7e8ea635SAtari911        $namespace = $INPUT->str('namespace');
3141*7e8ea635SAtari911        $today = date('Y-m-d');
3142*7e8ea635SAtari911
3143*7e8ea635SAtari911        $events = $this->getRecurringSeriesEvents($title, $namespace);
3144*7e8ea635SAtari911        $paused = 0;
3145*7e8ea635SAtari911
3146*7e8ea635SAtari911        foreach ($events as $entry) {
3147*7e8ea635SAtari911            if ($entry['date'] >= $today) {
3148*7e8ea635SAtari911                $data = json_decode(file_get_contents($entry['file']), true);
3149*7e8ea635SAtari911                if (!$data || !isset($data[$entry['date']])) continue;
3150*7e8ea635SAtari911
3151*7e8ea635SAtari911                foreach ($data[$entry['date']] as $k => &$evt) {
3152*7e8ea635SAtari911                    if (strtolower(trim($evt['title'])) === strtolower(trim($title))) {
3153*7e8ea635SAtari911                        $evt['paused'] = true;
3154*7e8ea635SAtari911                        $evt['title'] = '⏸ ' . preg_replace('/^⏸\s*/', '', $evt['title']);
3155*7e8ea635SAtari911                        $paused++;
3156*7e8ea635SAtari911                        break;
3157*7e8ea635SAtari911                    }
3158*7e8ea635SAtari911                }
3159*7e8ea635SAtari911                unset($evt);
3160*7e8ea635SAtari911
3161*7e8ea635SAtari911                file_put_contents($entry['file'], json_encode($data, JSON_PRETTY_PRINT));
3162*7e8ea635SAtari911            }
3163*7e8ea635SAtari911        }
3164*7e8ea635SAtari911
3165*7e8ea635SAtari911        $this->clearStatsCache();
3166*7e8ea635SAtari911        echo json_encode(['success' => true, 'message' => "Paused $paused future occurrences"]);
3167*7e8ea635SAtari911    }
3168*7e8ea635SAtari911
3169*7e8ea635SAtari911    /**
3170*7e8ea635SAtari911     * Resume series: unmark paused occurrences
3171*7e8ea635SAtari911     */
3172*7e8ea635SAtari911    private function handleResumeRecurring() {
3173*7e8ea635SAtari911        global $INPUT;
3174*7e8ea635SAtari911        $title = $INPUT->str('title');
3175*7e8ea635SAtari911        $namespace = $INPUT->str('namespace');
3176*7e8ea635SAtari911
3177*7e8ea635SAtari911        // Search for both paused and non-paused versions
3178*7e8ea635SAtari911        $dataDir = DOKU_INC . 'data/meta/';
3179*7e8ea635SAtari911        if ($namespace !== '') {
3180*7e8ea635SAtari911            $dataDir .= str_replace(':', '/', $namespace) . '/';
3181*7e8ea635SAtari911        }
3182*7e8ea635SAtari911        $dataDir .= 'calendar/';
3183*7e8ea635SAtari911
3184*7e8ea635SAtari911        $resumed = 0;
3185*7e8ea635SAtari911        $cleanTitle = preg_replace('/^⏸\s*/', '', $title);
3186*7e8ea635SAtari911
3187*7e8ea635SAtari911        if (!is_dir($dataDir)) {
3188*7e8ea635SAtari911            echo json_encode(['success' => false, 'error' => 'Directory not found']);
3189*7e8ea635SAtari911            return;
3190*7e8ea635SAtari911        }
3191*7e8ea635SAtari911
3192*7e8ea635SAtari911        foreach (glob($dataDir . '*.json') as $file) {
3193*7e8ea635SAtari911            $data = json_decode(file_get_contents($file), true);
3194*7e8ea635SAtari911            if (!$data) continue;
3195*7e8ea635SAtari911
3196*7e8ea635SAtari911            $modified = false;
3197*7e8ea635SAtari911            foreach ($data as $dateKey => &$dayEvents) {
3198*7e8ea635SAtari911                foreach ($dayEvents as $k => &$evt) {
3199*7e8ea635SAtari911                    $evtCleanTitle = preg_replace('/^⏸\s*/', '', $evt['title']);
3200*7e8ea635SAtari911                    if (strtolower(trim($evtCleanTitle)) === strtolower(trim($cleanTitle)) &&
3201*7e8ea635SAtari911                        (!empty($evt['paused']) || strpos($evt['title'], '⏸') === 0)) {
3202*7e8ea635SAtari911                        $evt['paused'] = false;
3203*7e8ea635SAtari911                        $evt['title'] = $cleanTitle;
3204*7e8ea635SAtari911                        $resumed++;
3205*7e8ea635SAtari911                        $modified = true;
3206*7e8ea635SAtari911                    }
3207*7e8ea635SAtari911                }
3208*7e8ea635SAtari911                unset($evt);
3209*7e8ea635SAtari911            }
3210*7e8ea635SAtari911            unset($dayEvents);
3211*7e8ea635SAtari911
3212*7e8ea635SAtari911            if ($modified) {
3213*7e8ea635SAtari911                file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
3214*7e8ea635SAtari911            }
3215*7e8ea635SAtari911        }
3216*7e8ea635SAtari911
3217*7e8ea635SAtari911        $this->clearStatsCache();
3218*7e8ea635SAtari911        echo json_encode(['success' => true, 'message' => "Resumed $resumed occurrences"]);
3219*7e8ea635SAtari911    }
3220*7e8ea635SAtari911
3221*7e8ea635SAtari911    /**
3222*7e8ea635SAtari911     * Change start date: shift all occurrences by an offset
3223*7e8ea635SAtari911     */
3224*7e8ea635SAtari911    private function handleChangeStartRecurring() {
3225*7e8ea635SAtari911        global $INPUT;
3226*7e8ea635SAtari911        $title = $INPUT->str('title');
3227*7e8ea635SAtari911        $namespace = $INPUT->str('namespace');
3228*7e8ea635SAtari911        $newStartDate = $INPUT->str('new_start_date');
3229*7e8ea635SAtari911
3230*7e8ea635SAtari911        if (empty($newStartDate)) {
3231*7e8ea635SAtari911            echo json_encode(['success' => false, 'error' => 'No start date provided']);
3232*7e8ea635SAtari911            return;
3233*7e8ea635SAtari911        }
3234*7e8ea635SAtari911
3235*7e8ea635SAtari911        $events = $this->getRecurringSeriesEvents($title, $namespace);
3236*7e8ea635SAtari911        if (empty($events)) {
3237*7e8ea635SAtari911            echo json_encode(['success' => false, 'error' => 'Series not found']);
3238*7e8ea635SAtari911            return;
3239*7e8ea635SAtari911        }
3240*7e8ea635SAtari911
3241*7e8ea635SAtari911        // Calculate offset from old first date to new first date
3242*7e8ea635SAtari911        $oldFirst = new DateTime($events[0]['date']);
3243*7e8ea635SAtari911        $newFirst = new DateTime($newStartDate);
3244*7e8ea635SAtari911        $offsetDays = (int)$oldFirst->diff($newFirst)->format('%r%a');
3245*7e8ea635SAtari911
3246*7e8ea635SAtari911        if ($offsetDays === 0) {
3247*7e8ea635SAtari911            echo json_encode(['success' => true, 'message' => 'Start date unchanged']);
3248*7e8ea635SAtari911            return;
3249*7e8ea635SAtari911        }
3250*7e8ea635SAtari911
3251*7e8ea635SAtari911        $dataDir = DOKU_INC . 'data/meta/';
3252*7e8ea635SAtari911        if ($namespace !== '') {
3253*7e8ea635SAtari911            $dataDir .= str_replace(':', '/', $namespace) . '/';
3254*7e8ea635SAtari911        }
3255*7e8ea635SAtari911        $dataDir .= 'calendar/';
3256*7e8ea635SAtari911
3257*7e8ea635SAtari911        // Collect all events to move
3258*7e8ea635SAtari911        $toMove = [];
3259*7e8ea635SAtari911        foreach ($events as $entry) {
3260*7e8ea635SAtari911            $oldDate = new DateTime($entry['date']);
3261*7e8ea635SAtari911            $newDate = clone $oldDate;
3262*7e8ea635SAtari911            $newDate->modify(($offsetDays > 0 ? '+' : '') . $offsetDays . ' days');
3263*7e8ea635SAtari911
3264*7e8ea635SAtari911            $toMove[] = [
3265*7e8ea635SAtari911                'oldDate' => $entry['date'],
3266*7e8ea635SAtari911                'newDate' => $newDate->format('Y-m-d'),
3267*7e8ea635SAtari911                'event' => $entry['event'],
3268*7e8ea635SAtari911                'file' => $entry['file']
3269*7e8ea635SAtari911            ];
3270*7e8ea635SAtari911        }
3271*7e8ea635SAtari911
3272*7e8ea635SAtari911        // Remove all from old positions
3273*7e8ea635SAtari911        foreach ($toMove as $move) {
3274*7e8ea635SAtari911            $data = json_decode(file_get_contents($move['file']), true);
3275*7e8ea635SAtari911            if (!$data || !isset($data[$move['oldDate']])) continue;
3276*7e8ea635SAtari911
3277*7e8ea635SAtari911            foreach ($data[$move['oldDate']] as $k => $evt) {
3278*7e8ea635SAtari911                if (strtolower(trim($evt['title'])) === strtolower(trim($title))) {
3279*7e8ea635SAtari911                    unset($data[$move['oldDate']][$k]);
3280*7e8ea635SAtari911                    $data[$move['oldDate']] = array_values($data[$move['oldDate']]);
3281*7e8ea635SAtari911                    break;
3282*7e8ea635SAtari911                }
3283*7e8ea635SAtari911            }
3284*7e8ea635SAtari911            if (empty($data[$move['oldDate']])) unset($data[$move['oldDate']]);
3285*7e8ea635SAtari911            if (empty($data)) {
3286*7e8ea635SAtari911                unlink($move['file']);
3287*7e8ea635SAtari911            } else {
3288*7e8ea635SAtari911                file_put_contents($move['file'], json_encode($data, JSON_PRETTY_PRINT));
3289*7e8ea635SAtari911            }
3290*7e8ea635SAtari911        }
3291*7e8ea635SAtari911
3292*7e8ea635SAtari911        // Add to new positions
3293*7e8ea635SAtari911        $moved = 0;
3294*7e8ea635SAtari911        foreach ($toMove as $move) {
3295*7e8ea635SAtari911            list($year, $month) = explode('-', $move['newDate']);
3296*7e8ea635SAtari911            $file = $dataDir . sprintf('%04d-%02d.json', $year, $month);
3297*7e8ea635SAtari911            $data = file_exists($file) ? json_decode(file_get_contents($file), true) : [];
3298*7e8ea635SAtari911            if (!is_array($data)) $data = [];
3299*7e8ea635SAtari911
3300*7e8ea635SAtari911            if (!isset($data[$move['newDate']])) $data[$move['newDate']] = [];
3301*7e8ea635SAtari911            $data[$move['newDate']][] = $move['event'];
3302*7e8ea635SAtari911            file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
3303*7e8ea635SAtari911            $moved++;
3304*7e8ea635SAtari911        }
3305*7e8ea635SAtari911
3306*7e8ea635SAtari911        $dir = $offsetDays > 0 ? 'forward' : 'back';
3307*7e8ea635SAtari911        $this->clearStatsCache();
3308*7e8ea635SAtari911        echo json_encode(['success' => true, 'message' => "Shifted $moved occurrences $dir by " . abs($offsetDays) . " days"]);
3309*7e8ea635SAtari911    }
3310*7e8ea635SAtari911
3311*7e8ea635SAtari911    /**
3312*7e8ea635SAtari911     * Change pattern: re-space all future events with a new interval
3313*7e8ea635SAtari911     */
3314*7e8ea635SAtari911    private function handleChangePatternRecurring() {
3315*7e8ea635SAtari911        global $INPUT;
3316*7e8ea635SAtari911        $title = $INPUT->str('title');
3317*7e8ea635SAtari911        $namespace = $INPUT->str('namespace');
3318*7e8ea635SAtari911        $newIntervalDays = $INPUT->int('interval_days', 7);
3319*7e8ea635SAtari911
3320*7e8ea635SAtari911        $events = $this->getRecurringSeriesEvents($title, $namespace);
3321*7e8ea635SAtari911        $today = date('Y-m-d');
3322*7e8ea635SAtari911
3323*7e8ea635SAtari911        // Split into past and future
3324*7e8ea635SAtari911        $pastEvents = [];
3325*7e8ea635SAtari911        $futureEvents = [];
3326*7e8ea635SAtari911        foreach ($events as $e) {
3327*7e8ea635SAtari911            if ($e['date'] < $today) {
3328*7e8ea635SAtari911                $pastEvents[] = $e;
3329*7e8ea635SAtari911            } else {
3330*7e8ea635SAtari911                $futureEvents[] = $e;
3331*7e8ea635SAtari911            }
3332*7e8ea635SAtari911        }
3333*7e8ea635SAtari911
3334*7e8ea635SAtari911        if (empty($futureEvents)) {
3335*7e8ea635SAtari911            echo json_encode(['success' => false, 'error' => 'No future occurrences to respace']);
3336*7e8ea635SAtari911            return;
3337*7e8ea635SAtari911        }
3338*7e8ea635SAtari911
3339*7e8ea635SAtari911        $dataDir = DOKU_INC . 'data/meta/';
3340*7e8ea635SAtari911        if ($namespace !== '') {
3341*7e8ea635SAtari911            $dataDir .= str_replace(':', '/', $namespace) . '/';
3342*7e8ea635SAtari911        }
3343*7e8ea635SAtari911        $dataDir .= 'calendar/';
3344*7e8ea635SAtari911
3345*7e8ea635SAtari911        // Use first future event as anchor
3346*7e8ea635SAtari911        $anchorDate = new DateTime($futureEvents[0]['date']);
3347*7e8ea635SAtari911
3348*7e8ea635SAtari911        // Remove all future events from files
3349*7e8ea635SAtari911        foreach ($futureEvents as $entry) {
3350*7e8ea635SAtari911            $data = json_decode(file_get_contents($entry['file']), true);
3351*7e8ea635SAtari911            if (!$data || !isset($data[$entry['date']])) continue;
3352*7e8ea635SAtari911
3353*7e8ea635SAtari911            foreach ($data[$entry['date']] as $k => $evt) {
3354*7e8ea635SAtari911                if (strtolower(trim($evt['title'])) === strtolower(trim($title))) {
3355*7e8ea635SAtari911                    unset($data[$entry['date']][$k]);
3356*7e8ea635SAtari911                    $data[$entry['date']] = array_values($data[$entry['date']]);
3357*7e8ea635SAtari911                    break;
3358*7e8ea635SAtari911                }
3359*7e8ea635SAtari911            }
3360*7e8ea635SAtari911            if (empty($data[$entry['date']])) unset($data[$entry['date']]);
3361*7e8ea635SAtari911            if (empty($data)) {
3362*7e8ea635SAtari911                unlink($entry['file']);
3363*7e8ea635SAtari911            } else {
3364*7e8ea635SAtari911                file_put_contents($entry['file'], json_encode($data, JSON_PRETTY_PRINT));
3365*7e8ea635SAtari911            }
3366*7e8ea635SAtari911        }
3367*7e8ea635SAtari911
3368*7e8ea635SAtari911        // Re-create with new spacing
3369*7e8ea635SAtari911        $template = $futureEvents[0]['event'];
3370*7e8ea635SAtari911        $baseId = isset($template['recurringId']) ? $template['recurringId'] : md5($title . $namespace);
3371*7e8ea635SAtari911        $count = count($futureEvents);
3372*7e8ea635SAtari911        $created = 0;
3373*7e8ea635SAtari911
3374*7e8ea635SAtari911        for ($i = 0; $i < $count; $i++) {
3375*7e8ea635SAtari911            $newDate = clone $anchorDate;
3376*7e8ea635SAtari911            $newDate->modify('+' . ($i * $newIntervalDays) . ' days');
3377*7e8ea635SAtari911            $dateKey = $newDate->format('Y-m-d');
3378*7e8ea635SAtari911            list($year, $month) = explode('-', $dateKey);
3379*7e8ea635SAtari911
3380*7e8ea635SAtari911            $file = $dataDir . sprintf('%04d-%02d.json', $year, $month);
3381*7e8ea635SAtari911            $fileData = file_exists($file) ? json_decode(file_get_contents($file), true) : [];
3382*7e8ea635SAtari911            if (!is_array($fileData)) $fileData = [];
3383*7e8ea635SAtari911
3384*7e8ea635SAtari911            if (!isset($fileData[$dateKey])) $fileData[$dateKey] = [];
3385*7e8ea635SAtari911
3386*7e8ea635SAtari911            $newEvent = $template;
3387*7e8ea635SAtari911            $newEvent['id'] = $baseId . '-respace-' . $i;
3388*7e8ea635SAtari911            $newEvent['recurring'] = true;
3389*7e8ea635SAtari911            $newEvent['recurringId'] = $baseId;
3390*7e8ea635SAtari911
3391*7e8ea635SAtari911            $fileData[$dateKey][] = $newEvent;
3392*7e8ea635SAtari911            file_put_contents($file, json_encode($fileData, JSON_PRETTY_PRINT));
3393*7e8ea635SAtari911            $created++;
3394*7e8ea635SAtari911        }
3395*7e8ea635SAtari911
3396*7e8ea635SAtari911        $this->clearStatsCache();
3397*7e8ea635SAtari911        $patternName = $this->intervalToPattern($newIntervalDays);
3398*7e8ea635SAtari911        echo json_encode(['success' => true, 'message' => "Respaced $created future occurrences to $patternName ($newIntervalDays days)"]);
3399*7e8ea635SAtari911    }
3400*7e8ea635SAtari911
3401*7e8ea635SAtari911    private function intervalToPattern($days) {
3402*7e8ea635SAtari911        if ($days == 1) return 'Daily';
3403*7e8ea635SAtari911        if ($days == 7) return 'Weekly';
3404*7e8ea635SAtari911        if ($days == 14) return 'Bi-weekly';
3405*7e8ea635SAtari911        if ($days >= 28 && $days <= 31) return 'Monthly';
3406*7e8ea635SAtari911        if ($days >= 89 && $days <= 93) return 'Quarterly';
3407*7e8ea635SAtari911        if ($days >= 363 && $days <= 368) return 'Yearly';
3408*7e8ea635SAtari911        return "Every $days days";
34091d05cddcSAtari911    }
34101d05cddcSAtari911
34111d05cddcSAtari911    private function getEventsByNamespace() {
34121d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
34131d05cddcSAtari911        $result = [];
34141d05cddcSAtari911
34151d05cddcSAtari911        // Check root calendar directory first (blank/default namespace)
34161d05cddcSAtari911        $rootCalendarDir = $dataDir . 'calendar';
34171d05cddcSAtari911        if (is_dir($rootCalendarDir)) {
34181d05cddcSAtari911            $hasFiles = false;
34191d05cddcSAtari911            $events = [];
34201d05cddcSAtari911
34211d05cddcSAtari911            foreach (glob($rootCalendarDir . '/*.json') as $file) {
34221d05cddcSAtari911                $hasFiles = true;
34231d05cddcSAtari911                $month = basename($file, '.json');
34241d05cddcSAtari911                $data = json_decode(file_get_contents($file), true);
34251d05cddcSAtari911                if (!$data) continue;
34261d05cddcSAtari911
34271d05cddcSAtari911                foreach ($data as $dateKey => $eventList) {
34281d05cddcSAtari911                    foreach ($eventList as $event) {
34291d05cddcSAtari911                        $events[] = [
34301d05cddcSAtari911                            'id' => $event['id'],
34311d05cddcSAtari911                            'title' => $event['title'],
34321d05cddcSAtari911                            'date' => $dateKey,
34331d05cddcSAtari911                            'startTime' => $event['startTime'] ?? null,
34341d05cddcSAtari911                            'month' => $month
34351d05cddcSAtari911                        ];
34361d05cddcSAtari911                    }
34371d05cddcSAtari911                }
34381d05cddcSAtari911            }
34391d05cddcSAtari911
34401d05cddcSAtari911            // Add if it has JSON files (even if empty)
34411d05cddcSAtari911            if ($hasFiles) {
34421d05cddcSAtari911                $result[''] = ['events' => $events];
34431d05cddcSAtari911            }
34441d05cddcSAtari911        }
34451d05cddcSAtari911
34461d05cddcSAtari911        // Recursively scan all namespace directories including sub-namespaces
34471d05cddcSAtari911        $this->scanNamespaceRecursive($dataDir, '', $result);
34481d05cddcSAtari911
34491d05cddcSAtari911        // Sort namespaces, but keep '' (default) first
34501d05cddcSAtari911        uksort($result, function($a, $b) {
34511d05cddcSAtari911            if ($a === '') return -1;
34521d05cddcSAtari911            if ($b === '') return 1;
34531d05cddcSAtari911            return strcmp($a, $b);
34541d05cddcSAtari911        });
34551d05cddcSAtari911
34561d05cddcSAtari911        return $result;
34571d05cddcSAtari911    }
34581d05cddcSAtari911
34591d05cddcSAtari911    private function scanNamespaceRecursive($baseDir, $parentNamespace, &$result) {
34601d05cddcSAtari911        foreach (glob($baseDir . '*', GLOB_ONLYDIR) as $nsDir) {
34611d05cddcSAtari911            $dirName = basename($nsDir);
34621d05cddcSAtari911
34631d05cddcSAtari911            // Skip the root 'calendar' dir
34641d05cddcSAtari911            if ($dirName === 'calendar' && empty($parentNamespace)) continue;
34651d05cddcSAtari911
34661d05cddcSAtari911            // Build namespace path
34671d05cddcSAtari911            $namespace = empty($parentNamespace) ? $dirName : $parentNamespace . ':' . $dirName;
34681d05cddcSAtari911
34691d05cddcSAtari911            // Check for calendar directory
34701d05cddcSAtari911            $calendarDir = $nsDir . '/calendar';
34711d05cddcSAtari911            if (is_dir($calendarDir)) {
34721d05cddcSAtari911                $hasFiles = false;
34731d05cddcSAtari911                $events = [];
34741d05cddcSAtari911
34751d05cddcSAtari911                // Scan all calendar files
34761d05cddcSAtari911                foreach (glob($calendarDir . '/*.json') as $file) {
34771d05cddcSAtari911                    $hasFiles = true;
34781d05cddcSAtari911                    $month = basename($file, '.json');
34791d05cddcSAtari911                    $data = json_decode(file_get_contents($file), true);
34801d05cddcSAtari911                    if (!$data) continue;
34811d05cddcSAtari911
34821d05cddcSAtari911                    foreach ($data as $dateKey => $eventList) {
34831d05cddcSAtari911                        foreach ($eventList as $event) {
34841d05cddcSAtari911                            $events[] = [
34851d05cddcSAtari911                                'id' => $event['id'],
34861d05cddcSAtari911                                'title' => $event['title'],
34871d05cddcSAtari911                                'date' => $dateKey,
34881d05cddcSAtari911                                'startTime' => $event['startTime'] ?? null,
34891d05cddcSAtari911                                'month' => $month
34901d05cddcSAtari911                            ];
34911d05cddcSAtari911                        }
34921d05cddcSAtari911                    }
34931d05cddcSAtari911                }
34941d05cddcSAtari911
34951d05cddcSAtari911                // Add namespace if it has JSON files (even if empty)
34961d05cddcSAtari911                if ($hasFiles) {
34971d05cddcSAtari911                    $result[$namespace] = ['events' => $events];
34981d05cddcSAtari911                }
34991d05cddcSAtari911            }
35001d05cddcSAtari911
35011d05cddcSAtari911            // Recursively scan sub-directories
35021d05cddcSAtari911            $this->scanNamespaceRecursive($nsDir . '/', $namespace, $result);
35031d05cddcSAtari911        }
35041d05cddcSAtari911    }
35051d05cddcSAtari911
35061d05cddcSAtari911    private function getAllNamespaces() {
35071d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
35081d05cddcSAtari911        $namespaces = [];
35091d05cddcSAtari911
35101d05cddcSAtari911        // Check root calendar directory first
35111d05cddcSAtari911        $rootCalendarDir = $dataDir . 'calendar';
35121d05cddcSAtari911        if (is_dir($rootCalendarDir)) {
35131d05cddcSAtari911            $namespaces[] = '';  // Blank/default namespace
35141d05cddcSAtari911        }
35151d05cddcSAtari911
35161d05cddcSAtari911        // Check all other namespace directories
35171d05cddcSAtari911        foreach (glob($dataDir . '*', GLOB_ONLYDIR) as $nsDir) {
35181d05cddcSAtari911            $namespace = basename($nsDir);
35191d05cddcSAtari911
35201d05cddcSAtari911            // Skip the root 'calendar' dir (already added as '')
35211d05cddcSAtari911            if ($namespace === 'calendar') continue;
35221d05cddcSAtari911
35231d05cddcSAtari911            $calendarDir = $nsDir . '/calendar';
35241d05cddcSAtari911            if (is_dir($calendarDir)) {
35251d05cddcSAtari911                $namespaces[] = $namespace;
35261d05cddcSAtari911            }
35271d05cddcSAtari911        }
35281d05cddcSAtari911
35291d05cddcSAtari911        return $namespaces;
35301d05cddcSAtari911    }
35311d05cddcSAtari911
35321d05cddcSAtari911    private function searchEvents($search, $filterNamespace) {
35331d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
35341d05cddcSAtari911        $results = [];
35351d05cddcSAtari911
35361d05cddcSAtari911        $search = strtolower(trim($search));
35371d05cddcSAtari911
35381d05cddcSAtari911        foreach (glob($dataDir . '*', GLOB_ONLYDIR) as $nsDir) {
35391d05cddcSAtari911            $namespace = basename($nsDir);
35401d05cddcSAtari911            $calendarDir = $nsDir . '/calendar';
35411d05cddcSAtari911
35421d05cddcSAtari911            if (!is_dir($calendarDir)) continue;
35431d05cddcSAtari911            if ($filterNamespace !== '' && $namespace !== $filterNamespace) continue;
35441d05cddcSAtari911
35451d05cddcSAtari911            foreach (glob($calendarDir . '/*.json') as $file) {
35461d05cddcSAtari911                $month = basename($file, '.json');
35471d05cddcSAtari911                $data = json_decode(file_get_contents($file), true);
35481d05cddcSAtari911                if (!$data) continue;
35491d05cddcSAtari911
35501d05cddcSAtari911                foreach ($data as $dateKey => $events) {
35511d05cddcSAtari911                    foreach ($events as $event) {
35521d05cddcSAtari911                        if ($search === '' || strpos(strtolower($event['title']), $search) !== false) {
35531d05cddcSAtari911                            $results[] = [
35541d05cddcSAtari911                                'id' => $event['id'],
35551d05cddcSAtari911                                'title' => $event['title'],
35561d05cddcSAtari911                                'date' => $dateKey,
35571d05cddcSAtari911                                'startTime' => $event['startTime'] ?? null,
35581d05cddcSAtari911                                'namespace' => $event['namespace'] ?? '',
35591d05cddcSAtari911                                'month' => $month
35601d05cddcSAtari911                            ];
35611d05cddcSAtari911                        }
35621d05cddcSAtari911                    }
35631d05cddcSAtari911                }
35641d05cddcSAtari911            }
35651d05cddcSAtari911        }
35661d05cddcSAtari911
35671d05cddcSAtari911        return $results;
35681d05cddcSAtari911    }
35691d05cddcSAtari911
35701d05cddcSAtari911    private function deleteRecurringSeries() {
35711d05cddcSAtari911        global $INPUT;
35721d05cddcSAtari911
35731d05cddcSAtari911        $eventTitle = $INPUT->str('event_title');
35741d05cddcSAtari911        $namespace = $INPUT->str('namespace');
35751d05cddcSAtari911
3576*7e8ea635SAtari911        // Collect ALL calendar directories
3577*7e8ea635SAtari911        $dataDir = DOKU_INC . 'data/meta/';
3578*7e8ea635SAtari911        $calendarDirs = [];
3579*7e8ea635SAtari911        if (is_dir($dataDir . 'calendar')) {
3580*7e8ea635SAtari911            $calendarDirs[] = $dataDir . 'calendar';
35811d05cddcSAtari911        }
3582*7e8ea635SAtari911        $this->findCalendarDirs($dataDir, $calendarDirs);
35831d05cddcSAtari911
35841d05cddcSAtari911        $count = 0;
35851d05cddcSAtari911
3586*7e8ea635SAtari911        foreach ($calendarDirs as $calDir) {
3587*7e8ea635SAtari911            foreach (glob($calDir . '/*.json') as $file) {
35881d05cddcSAtari911                $data = json_decode(file_get_contents($file), true);
3589*7e8ea635SAtari911                if (!$data || !is_array($data)) continue;
35901d05cddcSAtari911
35911d05cddcSAtari911                $modified = false;
35921d05cddcSAtari911                foreach ($data as $dateKey => $events) {
35931d05cddcSAtari911                    $filtered = [];
35941d05cddcSAtari911                    foreach ($events as $event) {
3595*7e8ea635SAtari911                        $eventNs = isset($event['namespace']) ? $event['namespace'] : '';
3596*7e8ea635SAtari911                        // Match by title AND namespace field
3597*7e8ea635SAtari911                        if (strtolower(trim($event['title'])) === strtolower(trim($eventTitle)) &&
3598*7e8ea635SAtari911                            strtolower(trim($eventNs)) === strtolower(trim($namespace))) {
35991d05cddcSAtari911                            $count++;
36001d05cddcSAtari911                            $modified = true;
36011d05cddcSAtari911                        } else {
36021d05cddcSAtari911                            $filtered[] = $event;
36031d05cddcSAtari911                        }
36041d05cddcSAtari911                    }
36051d05cddcSAtari911                    $data[$dateKey] = $filtered;
36061d05cddcSAtari911                }
36071d05cddcSAtari911
36081d05cddcSAtari911                if ($modified) {
36099ccd446eSAtari911                    foreach ($data as $dk => $evts) {
36109ccd446eSAtari911                        if (empty($evts)) unset($data[$dk]);
36119ccd446eSAtari911                    }
36129ccd446eSAtari911
36139ccd446eSAtari911                    if (empty($data)) {
36149ccd446eSAtari911                        unlink($file);
36159ccd446eSAtari911                    } else {
36161d05cddcSAtari911                        file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
36171d05cddcSAtari911                    }
36181d05cddcSAtari911                }
36191d05cddcSAtari911            }
36209ccd446eSAtari911        }
36211d05cddcSAtari911
36229ccd446eSAtari911        $this->clearStatsCache();
36231d05cddcSAtari911        $this->redirect("Deleted $count occurrences of recurring event: " . $eventTitle, 'success', 'manage');
36241d05cddcSAtari911    }
36251d05cddcSAtari911
36261d05cddcSAtari911    private function editRecurringSeries() {
36271d05cddcSAtari911        global $INPUT;
36281d05cddcSAtari911
36291d05cddcSAtari911        $oldTitle = $INPUT->str('old_title');
36301d05cddcSAtari911        $oldNamespace = $INPUT->str('old_namespace');
36311d05cddcSAtari911        $newTitle = $INPUT->str('new_title');
36321d05cddcSAtari911        $startTime = $INPUT->str('start_time');
36331d05cddcSAtari911        $endTime = $INPUT->str('end_time');
36341d05cddcSAtari911        $interval = $INPUT->int('interval', 0);
36351d05cddcSAtari911        $newNamespace = $INPUT->str('new_namespace');
36361d05cddcSAtari911
36371d05cddcSAtari911        // Use old namespace if new namespace is empty (keep current)
36381d05cddcSAtari911        if (empty($newNamespace) && !isset($_POST['new_namespace'])) {
36391d05cddcSAtari911            $newNamespace = $oldNamespace;
36401d05cddcSAtari911        }
36411d05cddcSAtari911
3642*7e8ea635SAtari911        // Collect ALL calendar directories to search
3643*7e8ea635SAtari911        $dataDir = DOKU_INC . 'data/meta/';
3644*7e8ea635SAtari911        $calendarDirs = [];
3645*7e8ea635SAtari911
3646*7e8ea635SAtari911        // Root calendar dir
3647*7e8ea635SAtari911        if (is_dir($dataDir . 'calendar')) {
3648*7e8ea635SAtari911            $calendarDirs[] = $dataDir . 'calendar';
36491d05cddcSAtari911        }
36501d05cddcSAtari911
3651*7e8ea635SAtari911        // All namespace dirs
3652*7e8ea635SAtari911        $this->findCalendarDirs($dataDir, $calendarDirs);
36531d05cddcSAtari911
3654*7e8ea635SAtari911        $count = 0;
3655*7e8ea635SAtari911
3656*7e8ea635SAtari911        // Pass 1: Rename title, update time, update namespace field in ALL matching events
3657*7e8ea635SAtari911        foreach ($calendarDirs as $calDir) {
3658*7e8ea635SAtari911            if (is_string($calDir)) {
3659*7e8ea635SAtari911                $dir = $calDir;
3660*7e8ea635SAtari911            } else {
3661*7e8ea635SAtari911                $dir = $calDir['dir'];
3662*7e8ea635SAtari911            }
3663*7e8ea635SAtari911
3664*7e8ea635SAtari911            foreach (glob($dir . '/*.json') as $file) {
36651d05cddcSAtari911                $data = json_decode(file_get_contents($file), true);
3666*7e8ea635SAtari911                if (!$data || !is_array($data)) continue;
36671d05cddcSAtari911
36681d05cddcSAtari911                $modified = false;
3669*7e8ea635SAtari911                foreach ($data as $dateKey => &$dayEvents) {
3670*7e8ea635SAtari911                    if (!is_array($dayEvents)) continue;
3671*7e8ea635SAtari911                    foreach ($dayEvents as $key => &$event) {
3672*7e8ea635SAtari911                        // Match by old title (case-insensitive) AND namespace field
3673*7e8ea635SAtari911                        $eventNs = isset($event['namespace']) ? $event['namespace'] : '';
3674*7e8ea635SAtari911                        if (strtolower(trim($event['title'])) !== strtolower(trim($oldTitle))) continue;
3675*7e8ea635SAtari911                        if (strtolower(trim($eventNs)) !== strtolower(trim($oldNamespace))) continue;
3676*7e8ea635SAtari911
3677*7e8ea635SAtari911                        // Update title
3678*7e8ea635SAtari911                        $event['title'] = $newTitle;
36791d05cddcSAtari911
36801d05cddcSAtari911                        // Update start time if provided
36811d05cddcSAtari911                        if (!empty($startTime)) {
3682*7e8ea635SAtari911                            $event['time'] = $startTime;
36831d05cddcSAtari911                        }
36841d05cddcSAtari911
36851d05cddcSAtari911                        // Update end time if provided
36861d05cddcSAtari911                        if (!empty($endTime)) {
3687*7e8ea635SAtari911                            $event['endTime'] = $endTime;
36881d05cddcSAtari911                        }
36891d05cddcSAtari911
3690*7e8ea635SAtari911                        // Update namespace field
3691*7e8ea635SAtari911                        $event['namespace'] = $newNamespace;
36921d05cddcSAtari911
36931d05cddcSAtari911                        $count++;
36941d05cddcSAtari911                        $modified = true;
36951d05cddcSAtari911                    }
3696*7e8ea635SAtari911                    unset($event);
36971d05cddcSAtari911                }
3698*7e8ea635SAtari911                unset($dayEvents);
36991d05cddcSAtari911
37001d05cddcSAtari911                if ($modified) {
37011d05cddcSAtari911                    file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
37021d05cddcSAtari911                }
37031d05cddcSAtari911            }
3704*7e8ea635SAtari911        }
37051d05cddcSAtari911
3706*7e8ea635SAtari911        // Pass 2: Handle interval changes (respace events from first date)
3707*7e8ea635SAtari911        if ($interval > 0 && $count > 0) {
3708*7e8ea635SAtari911            // Use getRecurringSeriesEvents to find all events with the NEW title
3709*7e8ea635SAtari911            $allEvents = $this->getRecurringSeriesEvents($newTitle, $newNamespace);
37101d05cddcSAtari911
3711*7e8ea635SAtari911            if (count($allEvents) > 1) {
3712*7e8ea635SAtari911                $firstDate = new DateTime($allEvents[0]['date']);
37131d05cddcSAtari911
3714*7e8ea635SAtari911                // Remove all except first, then re-create with new spacing
3715*7e8ea635SAtari911                for ($i = 1; $i < count($allEvents); $i++) {
3716*7e8ea635SAtari911                    $entry = $allEvents[$i];
3717*7e8ea635SAtari911                    $data = json_decode(file_get_contents($entry['file']), true);
3718*7e8ea635SAtari911                    if (!$data || !isset($data[$entry['date']])) continue;
3719*7e8ea635SAtari911
3720*7e8ea635SAtari911                    foreach ($data[$entry['date']] as $k => $evt) {
3721*7e8ea635SAtari911                        if (strtolower(trim($evt['title'])) === strtolower(trim($newTitle))) {
3722*7e8ea635SAtari911                            unset($data[$entry['date']][$k]);
3723*7e8ea635SAtari911                            $data[$entry['date']] = array_values($data[$entry['date']]);
3724*7e8ea635SAtari911                            break;
37251d05cddcSAtari911                        }
37261d05cddcSAtari911                    }
3727*7e8ea635SAtari911                    if (empty($data[$entry['date']])) unset($data[$entry['date']]);
3728*7e8ea635SAtari911                    if (empty($data)) {
3729*7e8ea635SAtari911                        unlink($entry['file']);
3730*7e8ea635SAtari911                    } else {
3731*7e8ea635SAtari911                        file_put_contents($entry['file'], json_encode($data, JSON_PRETTY_PRINT));
37321d05cddcSAtari911                    }
37331d05cddcSAtari911                }
37341d05cddcSAtari911
3735*7e8ea635SAtari911                // Re-create with new interval
3736*7e8ea635SAtari911                $template = $allEvents[0]['event'];
3737*7e8ea635SAtari911                $targetDir = ($newNamespace === '')
3738*7e8ea635SAtari911                    ? DOKU_INC . 'data/meta/calendar'
3739*7e8ea635SAtari911                    : DOKU_INC . 'data/meta/' . str_replace(':', '/', $newNamespace) . '/calendar';
3740*7e8ea635SAtari911                if (!is_dir($targetDir)) mkdir($targetDir, 0755, true);
37411d05cddcSAtari911
3742*7e8ea635SAtari911                $baseId = isset($template['recurringId']) ? $template['recurringId'] : md5($newTitle . $newNamespace);
37431d05cddcSAtari911
3744*7e8ea635SAtari911                for ($i = 1; $i < count($allEvents); $i++) {
3745*7e8ea635SAtari911                    $newDate = clone $firstDate;
3746*7e8ea635SAtari911                    $newDate->modify('+' . ($i * $interval) . ' days');
3747*7e8ea635SAtari911                    $dateKey = $newDate->format('Y-m-d');
3748*7e8ea635SAtari911                    list($year, $month) = explode('-', $dateKey);
37491d05cddcSAtari911
3750*7e8ea635SAtari911                    $file = $targetDir . '/' . sprintf('%04d-%02d.json', $year, $month);
3751*7e8ea635SAtari911                    $fileData = file_exists($file) ? json_decode(file_get_contents($file), true) : [];
3752*7e8ea635SAtari911                    if (!is_array($fileData)) $fileData = [];
3753*7e8ea635SAtari911                    if (!isset($fileData[$dateKey])) $fileData[$dateKey] = [];
37541d05cddcSAtari911
3755*7e8ea635SAtari911                    $newEvent = $template;
3756*7e8ea635SAtari911                    $newEvent['id'] = $baseId . '-respace-' . $i;
3757*7e8ea635SAtari911                    $fileData[$dateKey][] = $newEvent;
3758*7e8ea635SAtari911                    file_put_contents($file, json_encode($fileData, JSON_PRETTY_PRINT));
37591d05cddcSAtari911                }
37601d05cddcSAtari911            }
37611d05cddcSAtari911        }
37621d05cddcSAtari911
37631d05cddcSAtari911        $changes = [];
37641d05cddcSAtari911        if ($oldTitle !== $newTitle) $changes[] = "title";
37651d05cddcSAtari911        if (!empty($startTime) || !empty($endTime)) $changes[] = "time";
37661d05cddcSAtari911        if ($interval > 0) $changes[] = "interval";
37671d05cddcSAtari911        if ($newNamespace !== $oldNamespace) $changes[] = "namespace";
37681d05cddcSAtari911
37691d05cddcSAtari911        $changeStr = !empty($changes) ? " (" . implode(", ", $changes) . ")" : "";
37709ccd446eSAtari911        $this->clearStatsCache();
37711d05cddcSAtari911        $this->redirect("Updated $count occurrences of recurring event$changeStr", 'success', 'manage');
37721d05cddcSAtari911    }
37731d05cddcSAtari911
3774*7e8ea635SAtari911    /**
3775*7e8ea635SAtari911     * Find all calendar directories recursively
3776*7e8ea635SAtari911     */
3777*7e8ea635SAtari911    private function findCalendarDirs($baseDir, &$dirs) {
3778*7e8ea635SAtari911        foreach (glob($baseDir . '*', GLOB_ONLYDIR) as $nsDir) {
3779*7e8ea635SAtari911            $name = basename($nsDir);
3780*7e8ea635SAtari911            if ($name === 'calendar') continue; // Skip root calendar (added separately)
3781*7e8ea635SAtari911
3782*7e8ea635SAtari911            $calDir = $nsDir . '/calendar';
3783*7e8ea635SAtari911            if (is_dir($calDir)) {
3784*7e8ea635SAtari911                $dirs[] = $calDir;
3785*7e8ea635SAtari911            }
3786*7e8ea635SAtari911
3787*7e8ea635SAtari911            // Recurse
3788*7e8ea635SAtari911            $this->findCalendarDirs($nsDir . '/', $dirs);
3789*7e8ea635SAtari911        }
3790*7e8ea635SAtari911    }
3791*7e8ea635SAtari911
37921d05cddcSAtari911    private function moveEvents() {
37931d05cddcSAtari911        global $INPUT;
37941d05cddcSAtari911
37951d05cddcSAtari911        $events = $INPUT->arr('events');
37961d05cddcSAtari911        $targetNamespace = $INPUT->str('target_namespace');
37971d05cddcSAtari911
37981d05cddcSAtari911        if (empty($events)) {
37991d05cddcSAtari911            $this->redirect('No events selected', 'error', 'manage');
38001d05cddcSAtari911        }
38011d05cddcSAtari911
38021d05cddcSAtari911        $moved = 0;
38031d05cddcSAtari911
38041d05cddcSAtari911        foreach ($events as $eventData) {
38051d05cddcSAtari911            list($id, $namespace, $date, $month) = explode('|', $eventData);
38061d05cddcSAtari911
38071d05cddcSAtari911            // Determine old file path
38081d05cddcSAtari911            if ($namespace === '') {
38091d05cddcSAtari911                $oldFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json';
38101d05cddcSAtari911            } else {
38111d05cddcSAtari911                $oldFile = DOKU_INC . 'data/meta/' . $namespace . '/calendar/' . $month . '.json';
38121d05cddcSAtari911            }
38131d05cddcSAtari911
38141d05cddcSAtari911            if (!file_exists($oldFile)) continue;
38151d05cddcSAtari911
38161d05cddcSAtari911            $oldData = json_decode(file_get_contents($oldFile), true);
38171d05cddcSAtari911            if (!$oldData) continue;
38181d05cddcSAtari911
38191d05cddcSAtari911            // Find and remove event from old file
38201d05cddcSAtari911            $event = null;
38219ccd446eSAtari911            if (isset($oldData[$date])) {
38221d05cddcSAtari911                foreach ($oldData[$date] as $key => $evt) {
38231d05cddcSAtari911                    if ($evt['id'] === $id) {
38241d05cddcSAtari911                        $event = $evt;
38251d05cddcSAtari911                        unset($oldData[$date][$key]);
38261d05cddcSAtari911                        $oldData[$date] = array_values($oldData[$date]);
38271d05cddcSAtari911                        break;
38281d05cddcSAtari911                    }
38291d05cddcSAtari911                }
38301d05cddcSAtari911
38319ccd446eSAtari911                // Remove empty date arrays
38329ccd446eSAtari911                if (empty($oldData[$date])) {
38339ccd446eSAtari911                    unset($oldData[$date]);
38349ccd446eSAtari911                }
38359ccd446eSAtari911            }
38369ccd446eSAtari911
38371d05cddcSAtari911            if (!$event) continue;
38381d05cddcSAtari911
38391d05cddcSAtari911            // Save old file
38401d05cddcSAtari911            file_put_contents($oldFile, json_encode($oldData, JSON_PRETTY_PRINT));
38411d05cddcSAtari911
38421d05cddcSAtari911            // Update event namespace
38431d05cddcSAtari911            $event['namespace'] = $targetNamespace;
38441d05cddcSAtari911
38451d05cddcSAtari911            // Determine new file path
38461d05cddcSAtari911            if ($targetNamespace === '') {
38471d05cddcSAtari911                $newFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json';
38481d05cddcSAtari911                $newDir = dirname($newFile);
38491d05cddcSAtari911            } else {
38501d05cddcSAtari911                $newFile = DOKU_INC . 'data/meta/' . $targetNamespace . '/calendar/' . $month . '.json';
38511d05cddcSAtari911                $newDir = dirname($newFile);
38521d05cddcSAtari911            }
38531d05cddcSAtari911
38541d05cddcSAtari911            if (!is_dir($newDir)) {
38551d05cddcSAtari911                mkdir($newDir, 0755, true);
38561d05cddcSAtari911            }
38571d05cddcSAtari911
38581d05cddcSAtari911            $newData = [];
38591d05cddcSAtari911            if (file_exists($newFile)) {
38601d05cddcSAtari911                $newData = json_decode(file_get_contents($newFile), true) ?: [];
38611d05cddcSAtari911            }
38621d05cddcSAtari911
38631d05cddcSAtari911            if (!isset($newData[$date])) {
38641d05cddcSAtari911                $newData[$date] = [];
38651d05cddcSAtari911            }
38661d05cddcSAtari911            $newData[$date][] = $event;
38671d05cddcSAtari911
38681d05cddcSAtari911            file_put_contents($newFile, json_encode($newData, JSON_PRETTY_PRINT));
38691d05cddcSAtari911            $moved++;
38701d05cddcSAtari911        }
38711d05cddcSAtari911
38721d05cddcSAtari911        $displayTarget = $targetNamespace ?: '(default)';
38739ccd446eSAtari911        $this->clearStatsCache();
38741d05cddcSAtari911        $this->redirect("Moved $moved event(s) to namespace: " . $displayTarget, 'success', 'manage');
38751d05cddcSAtari911    }
38761d05cddcSAtari911
38771d05cddcSAtari911    private function moveSingleEvent() {
38781d05cddcSAtari911        global $INPUT;
38791d05cddcSAtari911
38801d05cddcSAtari911        $eventData = $INPUT->str('event');
38811d05cddcSAtari911        $targetNamespace = $INPUT->str('target_namespace');
38821d05cddcSAtari911
38831d05cddcSAtari911        list($id, $namespace, $date, $month) = explode('|', $eventData);
38841d05cddcSAtari911
38851d05cddcSAtari911        // Determine old file path
38861d05cddcSAtari911        if ($namespace === '') {
38871d05cddcSAtari911            $oldFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json';
38881d05cddcSAtari911        } else {
38891d05cddcSAtari911            $oldFile = DOKU_INC . 'data/meta/' . $namespace . '/calendar/' . $month . '.json';
38901d05cddcSAtari911        }
38911d05cddcSAtari911
38921d05cddcSAtari911        if (!file_exists($oldFile)) {
38931d05cddcSAtari911            $this->redirect('Event file not found', 'error', 'manage');
38941d05cddcSAtari911        }
38951d05cddcSAtari911
38961d05cddcSAtari911        $oldData = json_decode(file_get_contents($oldFile), true);
38971d05cddcSAtari911        if (!$oldData) {
38981d05cddcSAtari911            $this->redirect('Could not read event file', 'error', 'manage');
38991d05cddcSAtari911        }
39001d05cddcSAtari911
39011d05cddcSAtari911        // Find and remove event from old file
39021d05cddcSAtari911        $event = null;
39039ccd446eSAtari911        if (isset($oldData[$date])) {
39041d05cddcSAtari911            foreach ($oldData[$date] as $key => $evt) {
39051d05cddcSAtari911                if ($evt['id'] === $id) {
39061d05cddcSAtari911                    $event = $evt;
39071d05cddcSAtari911                    unset($oldData[$date][$key]);
39081d05cddcSAtari911                    $oldData[$date] = array_values($oldData[$date]);
39091d05cddcSAtari911                    break;
39101d05cddcSAtari911                }
39111d05cddcSAtari911            }
39121d05cddcSAtari911
39139ccd446eSAtari911            // Remove empty date arrays
39149ccd446eSAtari911            if (empty($oldData[$date])) {
39159ccd446eSAtari911                unset($oldData[$date]);
39169ccd446eSAtari911            }
39179ccd446eSAtari911        }
39189ccd446eSAtari911
39191d05cddcSAtari911        if (!$event) {
39201d05cddcSAtari911            $this->redirect('Event not found', 'error', 'manage');
39211d05cddcSAtari911        }
39221d05cddcSAtari911
39239ccd446eSAtari911        // Save old file (or delete if empty)
39249ccd446eSAtari911        if (empty($oldData)) {
39259ccd446eSAtari911            unlink($oldFile);
39269ccd446eSAtari911        } else {
39271d05cddcSAtari911            file_put_contents($oldFile, json_encode($oldData, JSON_PRETTY_PRINT));
39289ccd446eSAtari911        }
39291d05cddcSAtari911
39301d05cddcSAtari911        // Update event namespace
39311d05cddcSAtari911        $event['namespace'] = $targetNamespace;
39321d05cddcSAtari911
39331d05cddcSAtari911        // Determine new file path
39341d05cddcSAtari911        if ($targetNamespace === '') {
39351d05cddcSAtari911            $newFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json';
39361d05cddcSAtari911            $newDir = dirname($newFile);
39371d05cddcSAtari911        } else {
39381d05cddcSAtari911            $newFile = DOKU_INC . 'data/meta/' . $targetNamespace . '/calendar/' . $month . '.json';
39391d05cddcSAtari911            $newDir = dirname($newFile);
39401d05cddcSAtari911        }
39411d05cddcSAtari911
39421d05cddcSAtari911        if (!is_dir($newDir)) {
39431d05cddcSAtari911            mkdir($newDir, 0755, true);
39441d05cddcSAtari911        }
39451d05cddcSAtari911
39461d05cddcSAtari911        $newData = [];
39471d05cddcSAtari911        if (file_exists($newFile)) {
39481d05cddcSAtari911            $newData = json_decode(file_get_contents($newFile), true) ?: [];
39491d05cddcSAtari911        }
39501d05cddcSAtari911
39511d05cddcSAtari911        if (!isset($newData[$date])) {
39521d05cddcSAtari911            $newData[$date] = [];
39531d05cddcSAtari911        }
39541d05cddcSAtari911        $newData[$date][] = $event;
39551d05cddcSAtari911
39561d05cddcSAtari911        file_put_contents($newFile, json_encode($newData, JSON_PRETTY_PRINT));
39571d05cddcSAtari911
39581d05cddcSAtari911        $displayTarget = $targetNamespace ?: '(default)';
39599ccd446eSAtari911        $this->clearStatsCache();
39601d05cddcSAtari911        $this->redirect('Moved "' . $event['title'] . '" to ' . $displayTarget, 'success', 'manage');
39611d05cddcSAtari911    }
39621d05cddcSAtari911
39631d05cddcSAtari911    private function createNamespace() {
39641d05cddcSAtari911        global $INPUT;
39651d05cddcSAtari911
39661d05cddcSAtari911        $namespaceName = $INPUT->str('namespace_name');
39671d05cddcSAtari911
39681d05cddcSAtari911        // Validate namespace name
39691d05cddcSAtari911        if (empty($namespaceName)) {
39701d05cddcSAtari911            $this->redirect('Namespace name cannot be empty', 'error', 'manage');
39711d05cddcSAtari911        }
39721d05cddcSAtari911
39731d05cddcSAtari911        if (!preg_match('/^[a-zA-Z0-9_:-]+$/', $namespaceName)) {
39741d05cddcSAtari911            $this->redirect('Invalid namespace name. Use only letters, numbers, underscore, hyphen, and colon.', 'error', 'manage');
39751d05cddcSAtari911        }
39761d05cddcSAtari911
39771d05cddcSAtari911        // Convert namespace to directory path
39781d05cddcSAtari911        $namespacePath = str_replace(':', '/', $namespaceName);
39791d05cddcSAtari911        $calendarDir = DOKU_INC . 'data/meta/' . $namespacePath . '/calendar';
39801d05cddcSAtari911
39811d05cddcSAtari911        // Check if already exists
39821d05cddcSAtari911        if (is_dir($calendarDir)) {
39831d05cddcSAtari911            // Check if it has any JSON files
39841d05cddcSAtari911            $hasFiles = !empty(glob($calendarDir . '/*.json'));
39851d05cddcSAtari911            if ($hasFiles) {
39861d05cddcSAtari911                $this->redirect("Namespace '$namespaceName' already exists with events", 'info', 'manage');
39871d05cddcSAtari911            }
39881d05cddcSAtari911            // If directory exists but empty, continue to create placeholder
39891d05cddcSAtari911        }
39901d05cddcSAtari911
39911d05cddcSAtari911        // Create the directory
39921d05cddcSAtari911        if (!is_dir($calendarDir)) {
39931d05cddcSAtari911            if (!mkdir($calendarDir, 0755, true)) {
39941d05cddcSAtari911                $this->redirect("Failed to create namespace directory", 'error', 'manage');
39951d05cddcSAtari911            }
39961d05cddcSAtari911        }
39971d05cddcSAtari911
39981d05cddcSAtari911        // Create a placeholder JSON file with an empty structure for current month
39991d05cddcSAtari911        // This ensures the namespace appears in the list immediately
40001d05cddcSAtari911        $currentMonth = date('Y-m');
40011d05cddcSAtari911        $placeholderFile = $calendarDir . '/' . $currentMonth . '.json';
40021d05cddcSAtari911
40031d05cddcSAtari911        if (!file_exists($placeholderFile)) {
40041d05cddcSAtari911            file_put_contents($placeholderFile, json_encode([], JSON_PRETTY_PRINT));
40051d05cddcSAtari911        }
40061d05cddcSAtari911
40071d05cddcSAtari911        $this->redirect("Created namespace: $namespaceName", 'success', 'manage');
40081d05cddcSAtari911    }
40091d05cddcSAtari911
40101d05cddcSAtari911    private function deleteNamespace() {
40111d05cddcSAtari911        global $INPUT;
40121d05cddcSAtari911
40131d05cddcSAtari911        $namespace = $INPUT->str('namespace');
40141d05cddcSAtari911
4015*7e8ea635SAtari911        // Validate namespace name to prevent path traversal
4016*7e8ea635SAtari911        if ($namespace !== '' && !preg_match('/^[a-zA-Z0-9_:-]+$/', $namespace)) {
4017*7e8ea635SAtari911            $this->redirect('Invalid namespace name. Use only letters, numbers, underscore, hyphen, and colon.', 'error', 'manage');
4018*7e8ea635SAtari911            return;
4019*7e8ea635SAtari911        }
4020*7e8ea635SAtari911
4021*7e8ea635SAtari911        // Additional safety: ensure no path traversal sequences
4022*7e8ea635SAtari911        if (strpos($namespace, '..') !== false || strpos($namespace, '/') !== false || strpos($namespace, '\\') !== false) {
4023*7e8ea635SAtari911            $this->redirect('Invalid namespace: path traversal not allowed', 'error', 'manage');
4024*7e8ea635SAtari911            return;
4025*7e8ea635SAtari911        }
4026*7e8ea635SAtari911
40271d05cddcSAtari911        // Convert namespace to directory path (e.g., "work:projects" → "work/projects")
40281d05cddcSAtari911        $namespacePath = str_replace(':', '/', $namespace);
40291d05cddcSAtari911
40301d05cddcSAtari911        // Determine calendar directory
40311d05cddcSAtari911        if ($namespace === '') {
40321d05cddcSAtari911            $calendarDir = DOKU_INC . 'data/meta/calendar';
40331d05cddcSAtari911            $namespaceDir = null; // Don't delete root
40341d05cddcSAtari911        } else {
40351d05cddcSAtari911            $calendarDir = DOKU_INC . 'data/meta/' . $namespacePath . '/calendar';
40361d05cddcSAtari911            $namespaceDir = DOKU_INC . 'data/meta/' . $namespacePath;
40371d05cddcSAtari911        }
40381d05cddcSAtari911
40391d05cddcSAtari911        // Check if directory exists
40401d05cddcSAtari911        if (!is_dir($calendarDir)) {
40411d05cddcSAtari911            // Maybe it was never created or already deleted
40421d05cddcSAtari911            $this->redirect("Namespace directory not found: $calendarDir", 'error', 'manage');
40431d05cddcSAtari911            return;
40441d05cddcSAtari911        }
40451d05cddcSAtari911
40461d05cddcSAtari911        $filesDeleted = 0;
40471d05cddcSAtari911        $eventsDeleted = 0;
40481d05cddcSAtari911
40491d05cddcSAtari911        // Delete all calendar JSON files (including empty ones)
40501d05cddcSAtari911        foreach (glob($calendarDir . '/*.json') as $file) {
40511d05cddcSAtari911            $data = json_decode(file_get_contents($file), true);
40521d05cddcSAtari911            if ($data) {
40531d05cddcSAtari911                foreach ($data as $events) {
40541d05cddcSAtari911                    $eventsDeleted += count($events);
40551d05cddcSAtari911                }
40561d05cddcSAtari911            }
40571d05cddcSAtari911            unlink($file);
40581d05cddcSAtari911            $filesDeleted++;
40591d05cddcSAtari911        }
40601d05cddcSAtari911
40611d05cddcSAtari911        // Delete any other files in calendar directory
40621d05cddcSAtari911        foreach (glob($calendarDir . '/*') as $file) {
40631d05cddcSAtari911            if (is_file($file)) {
40641d05cddcSAtari911                unlink($file);
40651d05cddcSAtari911            }
40661d05cddcSAtari911        }
40671d05cddcSAtari911
40681d05cddcSAtari911        // Remove the calendar directory
40691d05cddcSAtari911        if ($namespace !== '') {
40701d05cddcSAtari911            @rmdir($calendarDir);
40711d05cddcSAtari911
40721d05cddcSAtari911            // Try to remove parent directories if they're empty
40731d05cddcSAtari911            // This handles nested namespaces like work:projects:alpha
40741d05cddcSAtari911            $currentDir = dirname($calendarDir);
40751d05cddcSAtari911            $metaDir = DOKU_INC . 'data/meta';
40761d05cddcSAtari911
40771d05cddcSAtari911            while ($currentDir !== $metaDir && $currentDir !== dirname($metaDir)) {
40781d05cddcSAtari911                if (is_dir($currentDir)) {
40791d05cddcSAtari911                    // Check if directory is empty
40801d05cddcSAtari911                    $contents = scandir($currentDir);
40811d05cddcSAtari911                    $isEmpty = count($contents) === 2; // Only . and ..
40821d05cddcSAtari911
40831d05cddcSAtari911                    if ($isEmpty) {
40841d05cddcSAtari911                        @rmdir($currentDir);
40851d05cddcSAtari911                        $currentDir = dirname($currentDir);
40861d05cddcSAtari911                    } else {
40871d05cddcSAtari911                        break; // Directory not empty, stop
40881d05cddcSAtari911                    }
40891d05cddcSAtari911                } else {
40901d05cddcSAtari911                    break;
40911d05cddcSAtari911                }
40921d05cddcSAtari911            }
40931d05cddcSAtari911        }
40941d05cddcSAtari911
40951d05cddcSAtari911        $displayName = $namespace ?: '(default)';
40969ccd446eSAtari911        $this->clearStatsCache();
40971d05cddcSAtari911        $this->redirect("Deleted namespace '$displayName': $eventsDeleted events in $filesDeleted files", 'success', 'manage');
40981d05cddcSAtari911    }
40991d05cddcSAtari911
41009ccd446eSAtari911    private function renameNamespace() {
41019ccd446eSAtari911        global $INPUT;
41029ccd446eSAtari911
41039ccd446eSAtari911        $oldNamespace = $INPUT->str('old_namespace');
41049ccd446eSAtari911        $newNamespace = $INPUT->str('new_namespace');
41059ccd446eSAtari911
4106*7e8ea635SAtari911        // Validate namespace names to prevent path traversal
4107*7e8ea635SAtari911        if ($oldNamespace !== '' && !preg_match('/^[a-zA-Z0-9_:-]+$/', $oldNamespace)) {
4108*7e8ea635SAtari911            $this->redirect('Invalid old namespace name. Use only letters, numbers, underscore, hyphen, and colon.', 'error', 'manage');
4109*7e8ea635SAtari911            return;
4110*7e8ea635SAtari911        }
4111*7e8ea635SAtari911
4112*7e8ea635SAtari911        if ($newNamespace !== '' && !preg_match('/^[a-zA-Z0-9_:-]+$/', $newNamespace)) {
4113*7e8ea635SAtari911            $this->redirect('Invalid new namespace name. Use only letters, numbers, underscore, hyphen, and colon.', 'error', 'manage');
4114*7e8ea635SAtari911            return;
4115*7e8ea635SAtari911        }
4116*7e8ea635SAtari911
4117*7e8ea635SAtari911        // Additional safety: ensure no path traversal sequences
4118*7e8ea635SAtari911        if (strpos($oldNamespace, '..') !== false || strpos($oldNamespace, '/') !== false || strpos($oldNamespace, '\\') !== false ||
4119*7e8ea635SAtari911            strpos($newNamespace, '..') !== false || strpos($newNamespace, '/') !== false || strpos($newNamespace, '\\') !== false) {
4120*7e8ea635SAtari911            $this->redirect('Invalid namespace: path traversal not allowed', 'error', 'manage');
4121*7e8ea635SAtari911            return;
4122*7e8ea635SAtari911        }
4123*7e8ea635SAtari911
41249ccd446eSAtari911        // Validate new namespace name
41259ccd446eSAtari911        if ($newNamespace === '') {
41269ccd446eSAtari911            $this->redirect("Cannot rename to empty namespace", 'error', 'manage');
41279ccd446eSAtari911            return;
41289ccd446eSAtari911        }
41299ccd446eSAtari911
41309ccd446eSAtari911        // Convert namespaces to directory paths
41319ccd446eSAtari911        $oldPath = str_replace(':', '/', $oldNamespace);
41329ccd446eSAtari911        $newPath = str_replace(':', '/', $newNamespace);
41339ccd446eSAtari911
41349ccd446eSAtari911        // Determine source and destination directories
41359ccd446eSAtari911        if ($oldNamespace === '') {
41369ccd446eSAtari911            $sourceDir = DOKU_INC . 'data/meta/calendar';
41379ccd446eSAtari911        } else {
41389ccd446eSAtari911            $sourceDir = DOKU_INC . 'data/meta/' . $oldPath . '/calendar';
41399ccd446eSAtari911        }
41409ccd446eSAtari911
41419ccd446eSAtari911        if ($newNamespace === '') {
41429ccd446eSAtari911            $targetDir = DOKU_INC . 'data/meta/calendar';
41439ccd446eSAtari911        } else {
41449ccd446eSAtari911            $targetDir = DOKU_INC . 'data/meta/' . $newPath . '/calendar';
41459ccd446eSAtari911        }
41469ccd446eSAtari911
41479ccd446eSAtari911        // Check if source exists
41489ccd446eSAtari911        if (!is_dir($sourceDir)) {
41499ccd446eSAtari911            $this->redirect("Source namespace not found: $oldNamespace", 'error', 'manage');
41509ccd446eSAtari911            return;
41519ccd446eSAtari911        }
41529ccd446eSAtari911
41539ccd446eSAtari911        // Check if target already exists
41549ccd446eSAtari911        if (is_dir($targetDir)) {
41559ccd446eSAtari911            $this->redirect("Target namespace already exists: $newNamespace", 'error', 'manage');
41569ccd446eSAtari911            return;
41579ccd446eSAtari911        }
41589ccd446eSAtari911
41599ccd446eSAtari911        // Create target directory
41609ccd446eSAtari911        if (!file_exists(dirname($targetDir))) {
41619ccd446eSAtari911            mkdir(dirname($targetDir), 0755, true);
41629ccd446eSAtari911        }
41639ccd446eSAtari911
41649ccd446eSAtari911        // Rename directory
41659ccd446eSAtari911        if (!rename($sourceDir, $targetDir)) {
41669ccd446eSAtari911            $this->redirect("Failed to rename namespace", 'error', 'manage');
41679ccd446eSAtari911            return;
41689ccd446eSAtari911        }
41699ccd446eSAtari911
41709ccd446eSAtari911        // Update event namespace field in all JSON files
41719ccd446eSAtari911        $eventsUpdated = 0;
41729ccd446eSAtari911        foreach (glob($targetDir . '/*.json') as $file) {
41739ccd446eSAtari911            $data = json_decode(file_get_contents($file), true);
41749ccd446eSAtari911            if ($data) {
41759ccd446eSAtari911                foreach ($data as $date => &$events) {
41769ccd446eSAtari911                    foreach ($events as &$event) {
41779ccd446eSAtari911                        if (isset($event['namespace']) && $event['namespace'] === $oldNamespace) {
41789ccd446eSAtari911                            $event['namespace'] = $newNamespace;
41799ccd446eSAtari911                            $eventsUpdated++;
41809ccd446eSAtari911                        }
41819ccd446eSAtari911                    }
41829ccd446eSAtari911                }
41839ccd446eSAtari911                file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
41849ccd446eSAtari911            }
41859ccd446eSAtari911        }
41869ccd446eSAtari911
41879ccd446eSAtari911        // Clean up old directory structure if empty
41889ccd446eSAtari911        if ($oldNamespace !== '') {
41899ccd446eSAtari911            $currentDir = dirname($sourceDir);
41909ccd446eSAtari911            $metaDir = DOKU_INC . 'data/meta';
41919ccd446eSAtari911
41929ccd446eSAtari911            while ($currentDir !== $metaDir && $currentDir !== dirname($metaDir)) {
41939ccd446eSAtari911                if (is_dir($currentDir)) {
41949ccd446eSAtari911                    $contents = scandir($currentDir);
41959ccd446eSAtari911                    $isEmpty = count($contents) === 2; // Only . and ..
41969ccd446eSAtari911
41979ccd446eSAtari911                    if ($isEmpty) {
41989ccd446eSAtari911                        @rmdir($currentDir);
41999ccd446eSAtari911                        $currentDir = dirname($currentDir);
42009ccd446eSAtari911                    } else {
42019ccd446eSAtari911                        break;
42029ccd446eSAtari911                    }
42039ccd446eSAtari911                } else {
42049ccd446eSAtari911                    break;
42059ccd446eSAtari911                }
42069ccd446eSAtari911            }
42079ccd446eSAtari911        }
42089ccd446eSAtari911
42099ccd446eSAtari911        $this->clearStatsCache();
42109ccd446eSAtari911        $this->redirect("Renamed namespace from '$oldNamespace' to '$newNamespace' ($eventsUpdated events updated)", 'success', 'manage');
42119ccd446eSAtari911    }
42129ccd446eSAtari911
42131d05cddcSAtari911    private function deleteSelectedEvents() {
42141d05cddcSAtari911        global $INPUT;
42151d05cddcSAtari911
42161d05cddcSAtari911        $events = $INPUT->arr('events');
42171d05cddcSAtari911
42181d05cddcSAtari911        if (empty($events)) {
42191d05cddcSAtari911            $this->redirect('No events selected', 'error', 'manage');
42201d05cddcSAtari911        }
42211d05cddcSAtari911
42221d05cddcSAtari911        $deletedCount = 0;
42231d05cddcSAtari911
42241d05cddcSAtari911        foreach ($events as $eventData) {
42251d05cddcSAtari911            list($id, $namespace, $date, $month) = explode('|', $eventData);
42261d05cddcSAtari911
42271d05cddcSAtari911            // Determine file path
42281d05cddcSAtari911            if ($namespace === '') {
42291d05cddcSAtari911                $file = DOKU_INC . 'data/meta/calendar/' . $month . '.json';
42301d05cddcSAtari911            } else {
42311d05cddcSAtari911                $file = DOKU_INC . 'data/meta/' . $namespace . '/calendar/' . $month . '.json';
42321d05cddcSAtari911            }
42331d05cddcSAtari911
42341d05cddcSAtari911            if (!file_exists($file)) continue;
42351d05cddcSAtari911
42361d05cddcSAtari911            $data = json_decode(file_get_contents($file), true);
42371d05cddcSAtari911            if (!$data) continue;
42381d05cddcSAtari911
42391d05cddcSAtari911            // Find and remove event
42401d05cddcSAtari911            if (isset($data[$date])) {
42411d05cddcSAtari911                foreach ($data[$date] as $key => $evt) {
42421d05cddcSAtari911                    if ($evt['id'] === $id) {
42431d05cddcSAtari911                        unset($data[$date][$key]);
42441d05cddcSAtari911                        $data[$date] = array_values($data[$date]);
42451d05cddcSAtari911                        $deletedCount++;
42461d05cddcSAtari911                        break;
42471d05cddcSAtari911                    }
42481d05cddcSAtari911                }
42491d05cddcSAtari911
42501d05cddcSAtari911                // Remove empty date arrays
42511d05cddcSAtari911                if (empty($data[$date])) {
42521d05cddcSAtari911                    unset($data[$date]);
42531d05cddcSAtari911                }
42541d05cddcSAtari911
42551d05cddcSAtari911                // Save file
42561d05cddcSAtari911                file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
42571d05cddcSAtari911            }
42581d05cddcSAtari911        }
42591d05cddcSAtari911
42609ccd446eSAtari911        $this->clearStatsCache();
42611d05cddcSAtari911        $this->redirect("Deleted $deletedCount event(s)", 'success', 'manage');
42621d05cddcSAtari911    }
42631d05cddcSAtari911
42649ccd446eSAtari911    /**
42659ccd446eSAtari911     * Clear the event statistics cache so counts refresh after mutations
42669ccd446eSAtari911     */
42674590242dSAtari911    private function saveImportantNamespaces() {
42684590242dSAtari911        global $INPUT;
42694590242dSAtari911
42704590242dSAtari911        $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
42714590242dSAtari911        $config = [];
42724590242dSAtari911        if (file_exists($configFile)) {
42734590242dSAtari911            $config = include $configFile;
42744590242dSAtari911        }
42754590242dSAtari911
42764590242dSAtari911        $config['important_namespaces'] = $INPUT->str('important_namespaces', 'important');
42774590242dSAtari911
42784590242dSAtari911        $content = "<?php\nreturn " . var_export($config, true) . ";\n";
42794590242dSAtari911        if (file_put_contents($configFile, $content)) {
42804590242dSAtari911            $this->redirect('Important namespaces saved', 'success', 'manage');
42814590242dSAtari911        } else {
42824590242dSAtari911            $this->redirect('Error: Could not save configuration', 'error', 'manage');
42834590242dSAtari911        }
42844590242dSAtari911    }
42854590242dSAtari911
42869ccd446eSAtari911    private function clearStatsCache() {
42879ccd446eSAtari911        $cacheFile = DOKU_PLUGIN . 'calendar/.event_stats_cache';
42889ccd446eSAtari911        if (file_exists($cacheFile)) {
42899ccd446eSAtari911            unlink($cacheFile);
42909ccd446eSAtari911        }
42919ccd446eSAtari911    }
42929ccd446eSAtari911
42931d05cddcSAtari911    private function getCronStatus() {
42941d05cddcSAtari911        // Try to read root's crontab first, then current user
42951d05cddcSAtari911        $output = [];
42961d05cddcSAtari911        exec('sudo crontab -l 2>/dev/null', $output);
42971d05cddcSAtari911
42981d05cddcSAtari911        // If sudo doesn't work, try current user
42991d05cddcSAtari911        if (empty($output)) {
43001d05cddcSAtari911            exec('crontab -l 2>/dev/null', $output);
43011d05cddcSAtari911        }
43021d05cddcSAtari911
43031d05cddcSAtari911        // Also check system crontab files
43041d05cddcSAtari911        if (empty($output)) {
43051d05cddcSAtari911            $cronFiles = [
43061d05cddcSAtari911                '/etc/crontab',
43071d05cddcSAtari911                '/etc/cron.d/calendar',
43081d05cddcSAtari911                '/var/spool/cron/root',
43091d05cddcSAtari911                '/var/spool/cron/crontabs/root'
43101d05cddcSAtari911            ];
43111d05cddcSAtari911
43121d05cddcSAtari911            foreach ($cronFiles as $file) {
43131d05cddcSAtari911                if (file_exists($file) && is_readable($file)) {
43141d05cddcSAtari911                    $content = file_get_contents($file);
43151d05cddcSAtari911                    $output = explode("\n", $content);
43161d05cddcSAtari911                    break;
43171d05cddcSAtari911                }
43181d05cddcSAtari911            }
43191d05cddcSAtari911        }
43201d05cddcSAtari911
43211d05cddcSAtari911        // Look for sync_outlook.php in the cron entries
43221d05cddcSAtari911        foreach ($output as $line) {
43231d05cddcSAtari911            $line = trim($line);
43241d05cddcSAtari911
43251d05cddcSAtari911            // Skip empty lines and comments
43261d05cddcSAtari911            if (empty($line) || $line[0] === '#') continue;
43271d05cddcSAtari911
43281d05cddcSAtari911            // Check if line contains sync_outlook.php
43291d05cddcSAtari911            if (strpos($line, 'sync_outlook.php') !== false) {
43301d05cddcSAtari911                // Parse cron expression
43311d05cddcSAtari911                // Format: minute hour day month weekday [user] command
43321d05cddcSAtari911                $parts = preg_split('/\s+/', $line, 7);
43331d05cddcSAtari911
43341d05cddcSAtari911                if (count($parts) >= 5) {
43351d05cddcSAtari911                    // Determine if this has a user field (system crontab format)
43361d05cddcSAtari911                    $hasUser = (count($parts) >= 6 && !preg_match('/^[\/\*]/', $parts[5]));
43371d05cddcSAtari911                    $offset = $hasUser ? 1 : 0;
43381d05cddcSAtari911
43391d05cddcSAtari911                    $frequency = $this->parseCronExpression($parts[0], $parts[1], $parts[2], $parts[3], $parts[4]);
43401d05cddcSAtari911                    return [
43411d05cddcSAtari911                        'active' => true,
43421d05cddcSAtari911                        'frequency' => $frequency,
43431d05cddcSAtari911                        'expression' => implode(' ', array_slice($parts, 0, 5)),
43441d05cddcSAtari911                        'full_line' => $line
43451d05cddcSAtari911                    ];
43461d05cddcSAtari911                }
43471d05cddcSAtari911            }
43481d05cddcSAtari911        }
43491d05cddcSAtari911
43501d05cddcSAtari911        return ['active' => false, 'frequency' => '', 'expression' => '', 'full_line' => ''];
43511d05cddcSAtari911    }
43521d05cddcSAtari911
43531d05cddcSAtari911    private function parseCronExpression($minute, $hour, $day, $month, $weekday) {
43541d05cddcSAtari911        // Parse minute field
43551d05cddcSAtari911        if ($minute === '*') {
43561d05cddcSAtari911            return 'Runs every minute';
43571d05cddcSAtari911        } elseif (strpos($minute, '*/') === 0) {
43581d05cddcSAtari911            $interval = substr($minute, 2);
43591d05cddcSAtari911            if ($interval == 1) {
43601d05cddcSAtari911                return 'Runs every minute';
43611d05cddcSAtari911            } elseif ($interval == 5) {
43621d05cddcSAtari911                return 'Runs every 5 minutes';
43631d05cddcSAtari911            } elseif ($interval == 8) {
43641d05cddcSAtari911                return 'Runs every 8 minutes';
43651d05cddcSAtari911            } elseif ($interval == 10) {
43661d05cddcSAtari911                return 'Runs every 10 minutes';
43671d05cddcSAtari911            } elseif ($interval == 15) {
43681d05cddcSAtari911                return 'Runs every 15 minutes';
43691d05cddcSAtari911            } elseif ($interval == 30) {
43701d05cddcSAtari911                return 'Runs every 30 minutes';
43711d05cddcSAtari911            } else {
43721d05cddcSAtari911                return "Runs every $interval minutes";
43731d05cddcSAtari911            }
43741d05cddcSAtari911        }
43751d05cddcSAtari911
43761d05cddcSAtari911        // Parse hour field
43771d05cddcSAtari911        if ($hour === '*' && $minute !== '*') {
43781d05cddcSAtari911            return 'Runs hourly';
43791d05cddcSAtari911        } elseif (strpos($hour, '*/') === 0 && $minute !== '*') {
43801d05cddcSAtari911            $interval = substr($hour, 2);
43811d05cddcSAtari911            if ($interval == 1) {
43821d05cddcSAtari911                return 'Runs every hour';
43831d05cddcSAtari911            } else {
43841d05cddcSAtari911                return "Runs every $interval hours";
43851d05cddcSAtari911            }
43861d05cddcSAtari911        }
43871d05cddcSAtari911
43881d05cddcSAtari911        // Parse day field
43891d05cddcSAtari911        if ($day === '*' && $hour !== '*' && $minute !== '*') {
43901d05cddcSAtari911            return 'Runs daily';
43911d05cddcSAtari911        }
43921d05cddcSAtari911
43931d05cddcSAtari911        // Default
43941d05cddcSAtari911        return 'Custom schedule';
43951d05cddcSAtari911    }
43961d05cddcSAtari911
43971d05cddcSAtari911    private function runSync() {
43981d05cddcSAtari911        global $INPUT;
43991d05cddcSAtari911
44001d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
44011d05cddcSAtari911            header('Content-Type: application/json');
44021d05cddcSAtari911
44031d05cddcSAtari911            $syncScript = DOKU_PLUGIN . 'calendar/sync_outlook.php';
44041d05cddcSAtari911            $abortFile = DOKU_PLUGIN . 'calendar/.sync_abort';
44051d05cddcSAtari911
44061d05cddcSAtari911            // Remove any existing abort flag
44071d05cddcSAtari911            if (file_exists($abortFile)) {
44081d05cddcSAtari911                @unlink($abortFile);
44091d05cddcSAtari911            }
44101d05cddcSAtari911
44111d05cddcSAtari911            if (!file_exists($syncScript)) {
44121d05cddcSAtari911                echo json_encode(['success' => false, 'message' => 'Sync script not found at: ' . $syncScript]);
44131d05cddcSAtari911                exit;
44141d05cddcSAtari911            }
44151d05cddcSAtari911
44161d05cddcSAtari911            // Change to plugin directory
44171d05cddcSAtari911            $pluginDir = DOKU_PLUGIN . 'calendar';
44181d05cddcSAtari911            $logFile = $pluginDir . '/sync.log';
44191d05cddcSAtari911
44201d05cddcSAtari911            // Ensure log file exists and is writable
44211d05cddcSAtari911            if (!file_exists($logFile)) {
44221d05cddcSAtari911                @touch($logFile);
44231d05cddcSAtari911                @chmod($logFile, 0666);
44241d05cddcSAtari911            }
44251d05cddcSAtari911
44261d05cddcSAtari911            // Try to log the execution (but don't fail if we can't)
44271d05cddcSAtari911            if (is_writable($logFile)) {
44281d05cddcSAtari911                $tz = new DateTimeZone('America/Los_Angeles');
44291d05cddcSAtari911                $now = new DateTime('now', $tz);
44301d05cddcSAtari911                $timestamp = $now->format('Y-m-d H:i:s');
44311d05cddcSAtari911                @file_put_contents($logFile, "[$timestamp] [ADMIN] Manual sync triggered via admin panel\n", FILE_APPEND);
44321d05cddcSAtari911            }
44331d05cddcSAtari911
44341d05cddcSAtari911            // Find PHP binary - try multiple methods
44351d05cddcSAtari911            $phpPath = $this->findPhpBinary();
44361d05cddcSAtari911
44371d05cddcSAtari911            // Build command
44381d05cddcSAtari911            $command = sprintf(
44391d05cddcSAtari911                'cd %s && %s %s 2>&1',
44401d05cddcSAtari911                escapeshellarg($pluginDir),
44411d05cddcSAtari911                $phpPath,
44421d05cddcSAtari911                escapeshellarg(basename($syncScript))
44431d05cddcSAtari911            );
44441d05cddcSAtari911
44451d05cddcSAtari911            // Execute and capture output
44461d05cddcSAtari911            $output = [];
44471d05cddcSAtari911            $returnCode = 0;
44481d05cddcSAtari911            exec($command, $output, $returnCode);
44491d05cddcSAtari911
44501d05cddcSAtari911            // Check if sync completed
44511d05cddcSAtari911            $lastLines = array_slice($output, -5);
44521d05cddcSAtari911            $completed = false;
44531d05cddcSAtari911            foreach ($lastLines as $line) {
44541d05cddcSAtari911                if (strpos($line, 'Sync Complete') !== false || strpos($line, 'Created:') !== false) {
44551d05cddcSAtari911                    $completed = true;
44561d05cddcSAtari911                    break;
44571d05cddcSAtari911                }
44581d05cddcSAtari911            }
44591d05cddcSAtari911
44601d05cddcSAtari911            if ($returnCode === 0 && $completed) {
44611d05cddcSAtari911                echo json_encode([
44621d05cddcSAtari911                    'success' => true,
44631d05cddcSAtari911                    'message' => 'Sync completed successfully! Check log below.'
44641d05cddcSAtari911                ]);
44651d05cddcSAtari911            } elseif ($returnCode === 0) {
44661d05cddcSAtari911                echo json_encode([
44671d05cddcSAtari911                    'success' => true,
44681d05cddcSAtari911                    'message' => 'Sync started. Check log below for progress.'
44691d05cddcSAtari911                ]);
44701d05cddcSAtari911            } else {
44711d05cddcSAtari911                // Include output for debugging
44721d05cddcSAtari911                $errorMsg = 'Sync failed with error code: ' . $returnCode;
44731d05cddcSAtari911                if (!empty($output)) {
44741d05cddcSAtari911                    $errorMsg .= ' | ' . implode(' | ', array_slice($output, -3));
44751d05cddcSAtari911                }
44761d05cddcSAtari911                echo json_encode([
44771d05cddcSAtari911                    'success' => false,
44781d05cddcSAtari911                    'message' => $errorMsg
44791d05cddcSAtari911                ]);
44801d05cddcSAtari911            }
44811d05cddcSAtari911            exit;
44821d05cddcSAtari911        }
44831d05cddcSAtari911    }
44841d05cddcSAtari911
44851d05cddcSAtari911    private function stopSync() {
44861d05cddcSAtari911        global $INPUT;
44871d05cddcSAtari911
44881d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
44891d05cddcSAtari911            header('Content-Type: application/json');
44901d05cddcSAtari911
44911d05cddcSAtari911            $abortFile = DOKU_PLUGIN . 'calendar/.sync_abort';
44921d05cddcSAtari911
44931d05cddcSAtari911            // Create abort flag file
44941d05cddcSAtari911            if (file_put_contents($abortFile, date('Y-m-d H:i:s')) !== false) {
44951d05cddcSAtari911                echo json_encode([
44961d05cddcSAtari911                    'success' => true,
44971d05cddcSAtari911                    'message' => 'Stop signal sent to sync process'
44981d05cddcSAtari911                ]);
44991d05cddcSAtari911            } else {
45001d05cddcSAtari911                echo json_encode([
45011d05cddcSAtari911                    'success' => false,
45021d05cddcSAtari911                    'message' => 'Failed to create abort flag'
45031d05cddcSAtari911                ]);
45041d05cddcSAtari911            }
45051d05cddcSAtari911            exit;
45061d05cddcSAtari911        }
45071d05cddcSAtari911    }
45081d05cddcSAtari911
45091d05cddcSAtari911    private function uploadUpdate() {
45101d05cddcSAtari911        if (!isset($_FILES['plugin_zip']) || $_FILES['plugin_zip']['error'] !== UPLOAD_ERR_OK) {
45111d05cddcSAtari911            $this->redirect('Upload failed: ' . ($_FILES['plugin_zip']['error'] ?? 'No file uploaded'), 'error', 'update');
45121d05cddcSAtari911            return;
45131d05cddcSAtari911        }
45141d05cddcSAtari911
45151d05cddcSAtari911        $uploadedFile = $_FILES['plugin_zip']['tmp_name'];
45161d05cddcSAtari911        $pluginDir = DOKU_PLUGIN . 'calendar/';
45171d05cddcSAtari911        $backupFirst = isset($_POST['backup_first']);
45181d05cddcSAtari911
45191d05cddcSAtari911        // Check if plugin directory is writable
45201d05cddcSAtari911        if (!is_writable($pluginDir)) {
45211d05cddcSAtari911            $this->redirect('Plugin directory is not writable. Please check permissions: ' . $pluginDir, 'error', 'update');
45221d05cddcSAtari911            return;
45231d05cddcSAtari911        }
45241d05cddcSAtari911
45251d05cddcSAtari911        // Check if parent directory is writable (for backup and temp files)
45261d05cddcSAtari911        if (!is_writable(DOKU_PLUGIN)) {
45271d05cddcSAtari911            $this->redirect('Plugin parent directory is not writable. Please check permissions: ' . DOKU_PLUGIN, 'error', 'update');
45281d05cddcSAtari911            return;
45291d05cddcSAtari911        }
45301d05cddcSAtari911
45311d05cddcSAtari911        // Verify it's a ZIP file
45321d05cddcSAtari911        $finfo = finfo_open(FILEINFO_MIME_TYPE);
45331d05cddcSAtari911        $mimeType = finfo_file($finfo, $uploadedFile);
45341d05cddcSAtari911        finfo_close($finfo);
45351d05cddcSAtari911
45361d05cddcSAtari911        if ($mimeType !== 'application/zip' && $mimeType !== 'application/x-zip-compressed') {
45371d05cddcSAtari911            $this->redirect('Invalid file type. Please upload a ZIP file.', 'error', 'update');
45381d05cddcSAtari911            return;
45391d05cddcSAtari911        }
45401d05cddcSAtari911
45411d05cddcSAtari911        // Create backup if requested
45421d05cddcSAtari911        if ($backupFirst) {
45431d05cddcSAtari911            // Get current version
45441d05cddcSAtari911            $pluginInfo = $pluginDir . 'plugin.info.txt';
45451d05cddcSAtari911            $version = 'unknown';
45461d05cddcSAtari911            if (file_exists($pluginInfo)) {
45471d05cddcSAtari911                $info = confToHash($pluginInfo);
45481d05cddcSAtari911                $version = $info['version'] ?? ($info['date'] ?? 'unknown');
45491d05cddcSAtari911            }
45501d05cddcSAtari911
45511d05cddcSAtari911            $backupName = 'calendar.backup.v' . $version . '.' . date('Y-m-d_H-i-s') . '.zip';
45521d05cddcSAtari911            $backupPath = DOKU_PLUGIN . $backupName;
45531d05cddcSAtari911
45541d05cddcSAtari911            try {
45551d05cddcSAtari911                $zip = new ZipArchive();
45561d05cddcSAtari911                if ($zip->open($backupPath, ZipArchive::CREATE) === TRUE) {
45579ccd446eSAtari911                    $fileCount = $this->addDirectoryToZip($zip, $pluginDir, 'calendar/');
45581d05cddcSAtari911                    $zip->close();
45599ccd446eSAtari911
45609ccd446eSAtari911                    // Verify backup was created and has content
45619ccd446eSAtari911                    if (!file_exists($backupPath)) {
45629ccd446eSAtari911                        $this->redirect('Backup file was not created', 'error', 'update');
45639ccd446eSAtari911                        return;
45649ccd446eSAtari911                    }
45659ccd446eSAtari911
45669ccd446eSAtari911                    $backupSize = filesize($backupPath);
45679ccd446eSAtari911                    if ($backupSize < 1000) { // Backup should be at least 1KB
45689ccd446eSAtari911                        @unlink($backupPath);
45699ccd446eSAtari911                        $this->redirect('Backup file is too small (' . $backupSize . ' bytes). Only ' . $fileCount . ' files were added. Backup aborted.', 'error', 'update');
45709ccd446eSAtari911                        return;
45719ccd446eSAtari911                    }
45729ccd446eSAtari911
45739ccd446eSAtari911                    if ($fileCount < 10) { // Should have at least 10 files
45749ccd446eSAtari911                        @unlink($backupPath);
45759ccd446eSAtari911                        $this->redirect('Backup incomplete: Only ' . $fileCount . ' files were added (expected 30+). Backup aborted.', 'error', 'update');
45769ccd446eSAtari911                        return;
45779ccd446eSAtari911                    }
45781d05cddcSAtari911                } else {
45791d05cddcSAtari911                    $this->redirect('Failed to create backup ZIP file', 'error', 'update');
45801d05cddcSAtari911                    return;
45811d05cddcSAtari911                }
45821d05cddcSAtari911            } catch (Exception $e) {
45839ccd446eSAtari911                if (file_exists($backupPath)) {
45849ccd446eSAtari911                    @unlink($backupPath);
45859ccd446eSAtari911                }
45861d05cddcSAtari911                $this->redirect('Backup failed: ' . $e->getMessage(), 'error', 'update');
45871d05cddcSAtari911                return;
45881d05cddcSAtari911            }
45891d05cddcSAtari911        }
45901d05cddcSAtari911
45911d05cddcSAtari911        // Extract uploaded ZIP
45921d05cddcSAtari911        $zip = new ZipArchive();
45931d05cddcSAtari911        if ($zip->open($uploadedFile) !== TRUE) {
45941d05cddcSAtari911            $this->redirect('Failed to open ZIP file', 'error', 'update');
45951d05cddcSAtari911            return;
45961d05cddcSAtari911        }
45971d05cddcSAtari911
45981d05cddcSAtari911        // Check if ZIP contains calendar folder
45991d05cddcSAtari911        $hasCalendarFolder = false;
46001d05cddcSAtari911        for ($i = 0; $i < $zip->numFiles; $i++) {
46011d05cddcSAtari911            $filename = $zip->getNameIndex($i);
46021d05cddcSAtari911            if (strpos($filename, 'calendar/') === 0) {
46031d05cddcSAtari911                $hasCalendarFolder = true;
46041d05cddcSAtari911                break;
46051d05cddcSAtari911            }
46061d05cddcSAtari911        }
46071d05cddcSAtari911
46081d05cddcSAtari911        // Extract to temp directory first
46091d05cddcSAtari911        $tempDir = DOKU_PLUGIN . 'calendar_update_temp/';
46101d05cddcSAtari911        if (is_dir($tempDir)) {
46111d05cddcSAtari911            $this->deleteDirectory($tempDir);
46121d05cddcSAtari911        }
46131d05cddcSAtari911        mkdir($tempDir);
46141d05cddcSAtari911
46151d05cddcSAtari911        $zip->extractTo($tempDir);
46161d05cddcSAtari911        $zip->close();
46171d05cddcSAtari911
46181d05cddcSAtari911        // Determine source directory
46191d05cddcSAtari911        if ($hasCalendarFolder) {
46201d05cddcSAtari911            $sourceDir = $tempDir . 'calendar/';
46211d05cddcSAtari911        } else {
46221d05cddcSAtari911            $sourceDir = $tempDir;
46231d05cddcSAtari911        }
46241d05cddcSAtari911
46251d05cddcSAtari911        // Preserve configuration files
46261d05cddcSAtari911        $preserveFiles = ['sync_config.php', 'sync_state.json', 'sync.log'];
46271d05cddcSAtari911        $preserved = [];
46281d05cddcSAtari911        foreach ($preserveFiles as $file) {
46291d05cddcSAtari911            $oldFile = $pluginDir . $file;
46301d05cddcSAtari911            if (file_exists($oldFile)) {
46311d05cddcSAtari911                $preserved[$file] = file_get_contents($oldFile);
46321d05cddcSAtari911            }
46331d05cddcSAtari911        }
46341d05cddcSAtari911
46351d05cddcSAtari911        // Delete old plugin files (except data files)
46361d05cddcSAtari911        $this->deleteDirectoryContents($pluginDir, $preserveFiles);
46371d05cddcSAtari911
46381d05cddcSAtari911        // Copy new files
46391d05cddcSAtari911        $this->recursiveCopy($sourceDir, $pluginDir);
46401d05cddcSAtari911
46411d05cddcSAtari911        // Restore preserved files
46421d05cddcSAtari911        foreach ($preserved as $file => $content) {
46431d05cddcSAtari911            file_put_contents($pluginDir . $file, $content);
46441d05cddcSAtari911        }
46451d05cddcSAtari911
46461d05cddcSAtari911        // Update version and date in plugin.info.txt
46471d05cddcSAtari911        $pluginInfo = $pluginDir . 'plugin.info.txt';
46481d05cddcSAtari911        if (file_exists($pluginInfo)) {
46491d05cddcSAtari911            $info = confToHash($pluginInfo);
46501d05cddcSAtari911
46511d05cddcSAtari911            // Get new version from uploaded plugin
46521d05cddcSAtari911            $newVersion = $info['version'] ?? 'unknown';
46531d05cddcSAtari911
46541d05cddcSAtari911            // Update date to current
46551d05cddcSAtari911            $info['date'] = date('Y-m-d');
46561d05cddcSAtari911
46571d05cddcSAtari911            // Write updated info back
46581d05cddcSAtari911            $lines = [];
46591d05cddcSAtari911            foreach ($info as $key => $value) {
46601d05cddcSAtari911                $lines[] = str_pad($key, 8) . ' ' . $value;
46611d05cddcSAtari911            }
46621d05cddcSAtari911            file_put_contents($pluginInfo, implode("\n", $lines) . "\n");
46631d05cddcSAtari911        }
46641d05cddcSAtari911
46651d05cddcSAtari911        // Cleanup temp directory
46661d05cddcSAtari911        $this->deleteDirectory($tempDir);
46671d05cddcSAtari911
46681d05cddcSAtari911        $message = 'Plugin updated successfully!';
46691d05cddcSAtari911        if ($backupFirst) {
46701d05cddcSAtari911            $message .= ' Backup saved as: ' . $backupName;
46711d05cddcSAtari911        }
46721d05cddcSAtari911        $this->redirect($message, 'success', 'update');
46731d05cddcSAtari911    }
46741d05cddcSAtari911
46751d05cddcSAtari911    private function deleteBackup() {
46761d05cddcSAtari911        global $INPUT;
46771d05cddcSAtari911
46781d05cddcSAtari911        $filename = $INPUT->str('backup_file');
46791d05cddcSAtari911
46801d05cddcSAtari911        if (empty($filename)) {
46811d05cddcSAtari911            $this->redirect('No backup file specified', 'error', 'update');
46821d05cddcSAtari911            return;
46831d05cddcSAtari911        }
46841d05cddcSAtari911
46851d05cddcSAtari911        // Security: only allow files starting with "calendar" and ending with .zip, no directory traversal
46861d05cddcSAtari911        if (!preg_match('/^calendar[a-zA-Z0-9._-]*\.zip$/', $filename)) {
46871d05cddcSAtari911            $this->redirect('Invalid backup filename', 'error', 'update');
46881d05cddcSAtari911            return;
46891d05cddcSAtari911        }
46901d05cddcSAtari911
46911d05cddcSAtari911        $backupPath = DOKU_PLUGIN . $filename;
46921d05cddcSAtari911
46931d05cddcSAtari911        if (!file_exists($backupPath)) {
46941d05cddcSAtari911            $this->redirect('Backup file not found', 'error', 'update');
46951d05cddcSAtari911            return;
46961d05cddcSAtari911        }
46971d05cddcSAtari911
46981d05cddcSAtari911        if (@unlink($backupPath)) {
46991d05cddcSAtari911            $this->redirect('Backup deleted: ' . $filename, 'success', 'update');
47001d05cddcSAtari911        } else {
47011d05cddcSAtari911            $this->redirect('Failed to delete backup. Check file permissions.', 'error', 'update');
47021d05cddcSAtari911        }
47031d05cddcSAtari911    }
47041d05cddcSAtari911
47051d05cddcSAtari911    private function renameBackup() {
47061d05cddcSAtari911        global $INPUT;
47071d05cddcSAtari911
47081d05cddcSAtari911        $oldName = $INPUT->str('old_name');
47091d05cddcSAtari911        $newName = $INPUT->str('new_name');
47101d05cddcSAtari911
47111d05cddcSAtari911        if (empty($oldName) || empty($newName)) {
47121d05cddcSAtari911            $this->redirect('Missing filename(s)', 'error', 'update');
47131d05cddcSAtari911            return;
47141d05cddcSAtari911        }
47151d05cddcSAtari911
47161d05cddcSAtari911        // Security: validate filenames
47171d05cddcSAtari911        if (!preg_match('/^[a-zA-Z0-9._-]+\.zip$/', $oldName) || !preg_match('/^[a-zA-Z0-9._-]+\.zip$/', $newName)) {
47181d05cddcSAtari911            $this->redirect('Invalid filename format', 'error', 'update');
47191d05cddcSAtari911            return;
47201d05cddcSAtari911        }
47211d05cddcSAtari911
47221d05cddcSAtari911        $oldPath = DOKU_PLUGIN . $oldName;
47231d05cddcSAtari911        $newPath = DOKU_PLUGIN . $newName;
47241d05cddcSAtari911
47251d05cddcSAtari911        if (!file_exists($oldPath)) {
47261d05cddcSAtari911            $this->redirect('Backup file not found', 'error', 'update');
47271d05cddcSAtari911            return;
47281d05cddcSAtari911        }
47291d05cddcSAtari911
47301d05cddcSAtari911        if (file_exists($newPath)) {
47311d05cddcSAtari911            $this->redirect('A file with the new name already exists', 'error', 'update');
47321d05cddcSAtari911            return;
47331d05cddcSAtari911        }
47341d05cddcSAtari911
47351d05cddcSAtari911        if (@rename($oldPath, $newPath)) {
47361d05cddcSAtari911            $this->redirect('Backup renamed: ' . $oldName . ' → ' . $newName, 'success', 'update');
47371d05cddcSAtari911        } else {
47381d05cddcSAtari911            $this->redirect('Failed to rename backup. Check file permissions.', 'error', 'update');
47391d05cddcSAtari911        }
47401d05cddcSAtari911    }
47411d05cddcSAtari911
47421d05cddcSAtari911    private function restoreBackup() {
47431d05cddcSAtari911        global $INPUT;
47441d05cddcSAtari911
47451d05cddcSAtari911        $filename = $INPUT->str('backup_file');
47461d05cddcSAtari911
47471d05cddcSAtari911        if (empty($filename)) {
47481d05cddcSAtari911            $this->redirect('No backup file specified', 'error', 'update');
47491d05cddcSAtari911            return;
47501d05cddcSAtari911        }
47511d05cddcSAtari911
47521d05cddcSAtari911        // Security: only allow files starting with "calendar" and ending with .zip, no directory traversal
47531d05cddcSAtari911        if (!preg_match('/^calendar[a-zA-Z0-9._-]*\.zip$/', $filename)) {
47541d05cddcSAtari911            $this->redirect('Invalid backup filename', 'error', 'update');
47551d05cddcSAtari911            return;
47561d05cddcSAtari911        }
47571d05cddcSAtari911
47581d05cddcSAtari911        $backupPath = DOKU_PLUGIN . $filename;
47591d05cddcSAtari911        $pluginDir = DOKU_PLUGIN . 'calendar/';
47601d05cddcSAtari911
47611d05cddcSAtari911        if (!file_exists($backupPath)) {
47621d05cddcSAtari911            $this->redirect('Backup file not found', 'error', 'update');
47631d05cddcSAtari911            return;
47641d05cddcSAtari911        }
47651d05cddcSAtari911
47661d05cddcSAtari911        // Check if plugin directory is writable
47671d05cddcSAtari911        if (!is_writable($pluginDir)) {
47681d05cddcSAtari911            $this->redirect('Plugin directory is not writable. Please check permissions.', 'error', 'update');
47691d05cddcSAtari911            return;
47701d05cddcSAtari911        }
47711d05cddcSAtari911
47721d05cddcSAtari911        // Extract backup to temp directory
47731d05cddcSAtari911        $tempDir = DOKU_PLUGIN . 'calendar_restore_temp/';
47741d05cddcSAtari911        if (is_dir($tempDir)) {
47751d05cddcSAtari911            $this->deleteDirectory($tempDir);
47761d05cddcSAtari911        }
47771d05cddcSAtari911        mkdir($tempDir);
47781d05cddcSAtari911
47791d05cddcSAtari911        $zip = new ZipArchive();
47801d05cddcSAtari911        if ($zip->open($backupPath) !== TRUE) {
47811d05cddcSAtari911            $this->redirect('Failed to open backup ZIP file', 'error', 'update');
47821d05cddcSAtari911            return;
47831d05cddcSAtari911        }
47841d05cddcSAtari911
47851d05cddcSAtari911        $zip->extractTo($tempDir);
47861d05cddcSAtari911        $zip->close();
47871d05cddcSAtari911
47881d05cddcSAtari911        // The backup contains a "calendar/" folder
47891d05cddcSAtari911        $sourceDir = $tempDir . 'calendar/';
47901d05cddcSAtari911
47911d05cddcSAtari911        if (!is_dir($sourceDir)) {
47921d05cddcSAtari911            $this->deleteDirectory($tempDir);
47931d05cddcSAtari911            $this->redirect('Invalid backup structure', 'error', 'update');
47941d05cddcSAtari911            return;
47951d05cddcSAtari911        }
47961d05cddcSAtari911
47971d05cddcSAtari911        // Delete current plugin directory contents
47981d05cddcSAtari911        $this->deleteDirectoryContents($pluginDir, []);
47991d05cddcSAtari911
48001d05cddcSAtari911        // Copy backup files to plugin directory
48011d05cddcSAtari911        $this->recursiveCopy($sourceDir, $pluginDir);
48021d05cddcSAtari911
48031d05cddcSAtari911        // Cleanup temp directory
48041d05cddcSAtari911        $this->deleteDirectory($tempDir);
48051d05cddcSAtari911
48061d05cddcSAtari911        $this->redirect('Plugin restored from backup: ' . $filename, 'success', 'update');
48071d05cddcSAtari911    }
48081d05cddcSAtari911
48099ccd446eSAtari911    private function createManualBackup() {
48109ccd446eSAtari911        $pluginDir = DOKU_PLUGIN . 'calendar/';
48119ccd446eSAtari911
48129ccd446eSAtari911        // Check if plugin directory is readable
48139ccd446eSAtari911        if (!is_readable($pluginDir)) {
48149ccd446eSAtari911            $this->redirect('Plugin directory is not readable. Please check permissions.', 'error', 'update');
48159ccd446eSAtari911            return;
48169ccd446eSAtari911        }
48179ccd446eSAtari911
48189ccd446eSAtari911        // Check if parent directory is writable (for saving backup)
48199ccd446eSAtari911        if (!is_writable(DOKU_PLUGIN)) {
48209ccd446eSAtari911            $this->redirect('Plugin parent directory is not writable. Cannot save backup.', 'error', 'update');
48219ccd446eSAtari911            return;
48229ccd446eSAtari911        }
48239ccd446eSAtari911
48249ccd446eSAtari911        // Get current version
48259ccd446eSAtari911        $pluginInfo = $pluginDir . 'plugin.info.txt';
48269ccd446eSAtari911        $version = 'unknown';
48279ccd446eSAtari911        if (file_exists($pluginInfo)) {
48289ccd446eSAtari911            $info = confToHash($pluginInfo);
48299ccd446eSAtari911            $version = $info['version'] ?? ($info['date'] ?? 'unknown');
48309ccd446eSAtari911        }
48319ccd446eSAtari911
48329ccd446eSAtari911        $backupName = 'calendar.backup.v' . $version . '.manual.' . date('Y-m-d_H-i-s') . '.zip';
48339ccd446eSAtari911        $backupPath = DOKU_PLUGIN . $backupName;
48349ccd446eSAtari911
48359ccd446eSAtari911        try {
48369ccd446eSAtari911            $zip = new ZipArchive();
48379ccd446eSAtari911            if ($zip->open($backupPath, ZipArchive::CREATE) === TRUE) {
48389ccd446eSAtari911                $fileCount = $this->addDirectoryToZip($zip, $pluginDir, 'calendar/');
48399ccd446eSAtari911                $zip->close();
48409ccd446eSAtari911
48419ccd446eSAtari911                // Verify backup was created and has content
48429ccd446eSAtari911                if (!file_exists($backupPath)) {
48439ccd446eSAtari911                    $this->redirect('Backup file was not created', 'error', 'update');
48449ccd446eSAtari911                    return;
48459ccd446eSAtari911                }
48469ccd446eSAtari911
48479ccd446eSAtari911                $backupSize = filesize($backupPath);
48489ccd446eSAtari911                if ($backupSize < 1000) { // Backup should be at least 1KB
48499ccd446eSAtari911                    @unlink($backupPath);
48509ccd446eSAtari911                    $this->redirect('Backup file is too small (' . $this->formatBytes($backupSize) . '). Only ' . $fileCount . ' files were added. Backup failed.', 'error', 'update');
48519ccd446eSAtari911                    return;
48529ccd446eSAtari911                }
48539ccd446eSAtari911
48549ccd446eSAtari911                if ($fileCount < 10) { // Should have at least 10 files
48559ccd446eSAtari911                    @unlink($backupPath);
48569ccd446eSAtari911                    $this->redirect('Backup incomplete: Only ' . $fileCount . ' files were added (expected 30+). Backup failed.', 'error', 'update');
48579ccd446eSAtari911                    return;
48589ccd446eSAtari911                }
48599ccd446eSAtari911
48609ccd446eSAtari911                // Success!
48619ccd446eSAtari911                $this->redirect('✓ Manual backup created successfully: ' . $backupName . ' (' . $this->formatBytes($backupSize) . ', ' . $fileCount . ' files)', 'success', 'update');
48629ccd446eSAtari911
48639ccd446eSAtari911            } else {
48649ccd446eSAtari911                $this->redirect('Failed to create backup ZIP file', 'error', 'update');
48659ccd446eSAtari911                return;
48669ccd446eSAtari911            }
48679ccd446eSAtari911        } catch (Exception $e) {
48689ccd446eSAtari911            if (file_exists($backupPath)) {
48699ccd446eSAtari911                @unlink($backupPath);
48709ccd446eSAtari911            }
48719ccd446eSAtari911            $this->redirect('Backup failed: ' . $e->getMessage(), 'error', 'update');
48729ccd446eSAtari911            return;
48739ccd446eSAtari911        }
48749ccd446eSAtari911    }
48759ccd446eSAtari911
48761d05cddcSAtari911    private function addDirectoryToZip($zip, $dir, $zipPath = '') {
48779ccd446eSAtari911        $fileCount = 0;
48789ccd446eSAtari911        $errors = [];
48799ccd446eSAtari911
4880*7e8ea635SAtari911        // Ensure dir has trailing slash
4881*7e8ea635SAtari911        $dir = rtrim($dir, '/') . '/';
4882*7e8ea635SAtari911
48839ccd446eSAtari911        if (!is_dir($dir)) {
48849ccd446eSAtari911            throw new Exception("Directory does not exist: $dir");
48859ccd446eSAtari911        }
48869ccd446eSAtari911
48879ccd446eSAtari911        if (!is_readable($dir)) {
48889ccd446eSAtari911            throw new Exception("Directory is not readable: $dir");
48899ccd446eSAtari911        }
48909ccd446eSAtari911
48911d05cddcSAtari911        try {
4892*7e8ea635SAtari911            // First, add all directories to preserve structure (including empty ones)
4893*7e8ea635SAtari911            $dirs = new RecursiveIteratorIterator(
48941d05cddcSAtari911                new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
4895*7e8ea635SAtari911                RecursiveIteratorIterator::SELF_FIRST  // Process directories before their contents
48961d05cddcSAtari911            );
48971d05cddcSAtari911
4898*7e8ea635SAtari911            foreach ($dirs as $item) {
4899*7e8ea635SAtari911                $itemPath = $item->getRealPath();
4900*7e8ea635SAtari911                if (!$itemPath) continue;
49019ccd446eSAtari911
4902*7e8ea635SAtari911                // Calculate relative path from the source directory
4903*7e8ea635SAtari911                $relativePath = $zipPath . substr($itemPath, strlen($dir));
4904*7e8ea635SAtari911
4905*7e8ea635SAtari911                if ($item->isDir()) {
4906*7e8ea635SAtari911                    // Add directory to ZIP (preserves empty directories and structure)
4907*7e8ea635SAtari911                    $dirInZip = rtrim($relativePath, '/') . '/';
4908*7e8ea635SAtari911                    $zip->addEmptyDir($dirInZip);
4909*7e8ea635SAtari911                } else {
4910*7e8ea635SAtari911                    // Add file to ZIP
4911*7e8ea635SAtari911                    if (is_readable($itemPath)) {
4912*7e8ea635SAtari911                        if ($zip->addFile($itemPath, $relativePath)) {
49139ccd446eSAtari911                            $fileCount++;
49149ccd446eSAtari911                        } else {
4915*7e8ea635SAtari911                            $errors[] = "Failed to add: " . basename($itemPath);
49169ccd446eSAtari911                        }
49179ccd446eSAtari911                    } else {
4918*7e8ea635SAtari911                        $errors[] = "Cannot read: " . basename($itemPath);
49191d05cddcSAtari911                    }
49201d05cddcSAtari911                }
49211d05cddcSAtari911            }
49229ccd446eSAtari911
49239ccd446eSAtari911            // Log any errors but don't fail if we got most files
49249ccd446eSAtari911            if (!empty($errors) && count($errors) < 5) {
49259ccd446eSAtari911                foreach ($errors as $error) {
49269ccd446eSAtari911                    error_log('Calendar plugin backup warning: ' . $error);
49279ccd446eSAtari911                }
49289ccd446eSAtari911            }
49299ccd446eSAtari911
49309ccd446eSAtari911            // If too many errors, fail
49319ccd446eSAtari911            if (count($errors) > 5) {
49329ccd446eSAtari911                throw new Exception("Too many errors adding files to backup: " . implode(', ', array_slice($errors, 0, 5)));
49339ccd446eSAtari911            }
49349ccd446eSAtari911
49351d05cddcSAtari911        } catch (Exception $e) {
49369ccd446eSAtari911            error_log('Calendar plugin backup error: ' . $e->getMessage());
49379ccd446eSAtari911            throw $e;
49381d05cddcSAtari911        }
49399ccd446eSAtari911
49409ccd446eSAtari911        return $fileCount;
49411d05cddcSAtari911    }
49421d05cddcSAtari911
49431d05cddcSAtari911    private function deleteDirectory($dir) {
49441d05cddcSAtari911        if (!is_dir($dir)) return;
49451d05cddcSAtari911
49461d05cddcSAtari911        try {
49471d05cddcSAtari911            $files = new RecursiveIteratorIterator(
49481d05cddcSAtari911                new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
49491d05cddcSAtari911                RecursiveIteratorIterator::CHILD_FIRST
49501d05cddcSAtari911            );
49511d05cddcSAtari911
49521d05cddcSAtari911            foreach ($files as $file) {
49531d05cddcSAtari911                if ($file->isDir()) {
49541d05cddcSAtari911                    @rmdir($file->getRealPath());
49551d05cddcSAtari911                } else {
49561d05cddcSAtari911                    @unlink($file->getRealPath());
49571d05cddcSAtari911                }
49581d05cddcSAtari911            }
49591d05cddcSAtari911
49601d05cddcSAtari911            @rmdir($dir);
49611d05cddcSAtari911        } catch (Exception $e) {
49621d05cddcSAtari911            error_log('Calendar plugin delete directory error: ' . $e->getMessage());
49631d05cddcSAtari911        }
49641d05cddcSAtari911    }
49651d05cddcSAtari911
49661d05cddcSAtari911    private function deleteDirectoryContents($dir, $preserve = []) {
49671d05cddcSAtari911        if (!is_dir($dir)) return;
49681d05cddcSAtari911
49691d05cddcSAtari911        $items = scandir($dir);
49701d05cddcSAtari911        foreach ($items as $item) {
49711d05cddcSAtari911            if ($item === '.' || $item === '..') continue;
49721d05cddcSAtari911            if (in_array($item, $preserve)) continue;
49731d05cddcSAtari911
49741d05cddcSAtari911            $path = $dir . $item;
49751d05cddcSAtari911            if (is_dir($path)) {
49761d05cddcSAtari911                $this->deleteDirectory($path);
49771d05cddcSAtari911            } else {
49781d05cddcSAtari911                unlink($path);
49791d05cddcSAtari911            }
49801d05cddcSAtari911        }
49811d05cddcSAtari911    }
49821d05cddcSAtari911
49831d05cddcSAtari911    private function recursiveCopy($src, $dst) {
4984*7e8ea635SAtari911        if (!is_dir($src)) {
4985*7e8ea635SAtari911            return false;
4986*7e8ea635SAtari911        }
4987*7e8ea635SAtari911
49881d05cddcSAtari911        $dir = opendir($src);
4989*7e8ea635SAtari911        if (!$dir) {
4990*7e8ea635SAtari911            return false;
4991*7e8ea635SAtari911        }
4992*7e8ea635SAtari911
4993*7e8ea635SAtari911        // Create destination directory with proper permissions (0755)
4994*7e8ea635SAtari911        if (!is_dir($dst)) {
4995*7e8ea635SAtari911            mkdir($dst, 0755, true);
4996*7e8ea635SAtari911        }
49971d05cddcSAtari911
49981d05cddcSAtari911        while (($file = readdir($dir)) !== false) {
49991d05cddcSAtari911            if ($file !== '.' && $file !== '..') {
5000*7e8ea635SAtari911                $srcPath = $src . '/' . $file;
5001*7e8ea635SAtari911                $dstPath = $dst . '/' . $file;
5002*7e8ea635SAtari911
5003*7e8ea635SAtari911                if (is_dir($srcPath)) {
5004*7e8ea635SAtari911                    // Recursively copy subdirectory
5005*7e8ea635SAtari911                    $this->recursiveCopy($srcPath, $dstPath);
50061d05cddcSAtari911                } else {
5007*7e8ea635SAtari911                    // Copy file and preserve permissions
5008*7e8ea635SAtari911                    if (copy($srcPath, $dstPath)) {
5009*7e8ea635SAtari911                        // Try to preserve file permissions from source, fallback to 0644
5010*7e8ea635SAtari911                        $perms = @fileperms($srcPath);
5011*7e8ea635SAtari911                        if ($perms !== false) {
5012*7e8ea635SAtari911                            @chmod($dstPath, $perms);
5013*7e8ea635SAtari911                        } else {
5014*7e8ea635SAtari911                            @chmod($dstPath, 0644);
5015*7e8ea635SAtari911                        }
5016*7e8ea635SAtari911                    }
50171d05cddcSAtari911                }
50181d05cddcSAtari911            }
50191d05cddcSAtari911        }
50201d05cddcSAtari911
50211d05cddcSAtari911        closedir($dir);
5022*7e8ea635SAtari911        return true;
50231d05cddcSAtari911    }
50241d05cddcSAtari911
50251d05cddcSAtari911    private function formatBytes($bytes) {
50261d05cddcSAtari911        if ($bytes >= 1073741824) {
50271d05cddcSAtari911            return number_format($bytes / 1073741824, 2) . ' GB';
50281d05cddcSAtari911        } elseif ($bytes >= 1048576) {
50291d05cddcSAtari911            return number_format($bytes / 1048576, 2) . ' MB';
50301d05cddcSAtari911        } elseif ($bytes >= 1024) {
50311d05cddcSAtari911            return number_format($bytes / 1024, 2) . ' KB';
50321d05cddcSAtari911        } else {
50331d05cddcSAtari911            return $bytes . ' bytes';
50341d05cddcSAtari911        }
50351d05cddcSAtari911    }
50361d05cddcSAtari911
50371d05cddcSAtari911    private function findPhpBinary() {
50381d05cddcSAtari911        // Try PHP_BINARY constant first (most reliable if available)
50391d05cddcSAtari911        if (defined('PHP_BINARY') && !empty(PHP_BINARY) && is_executable(PHP_BINARY)) {
50401d05cddcSAtari911            return escapeshellarg(PHP_BINARY);
50411d05cddcSAtari911        }
50421d05cddcSAtari911
50431d05cddcSAtari911        // Try common PHP binary locations
50441d05cddcSAtari911        $possiblePaths = [
50451d05cddcSAtari911            '/usr/bin/php',
50461d05cddcSAtari911            '/usr/bin/php8.1',
50471d05cddcSAtari911            '/usr/bin/php8.2',
50481d05cddcSAtari911            '/usr/bin/php8.3',
50491d05cddcSAtari911            '/usr/bin/php7.4',
50501d05cddcSAtari911            '/usr/local/bin/php',
50511d05cddcSAtari911            'php' // Last resort - rely on PATH
50521d05cddcSAtari911        ];
50531d05cddcSAtari911
50541d05cddcSAtari911        foreach ($possiblePaths as $path) {
50551d05cddcSAtari911            // Test if this PHP binary works
50561d05cddcSAtari911            $testOutput = [];
50571d05cddcSAtari911            $testReturn = 0;
50581d05cddcSAtari911            exec($path . ' -v 2>&1', $testOutput, $testReturn);
50591d05cddcSAtari911
50601d05cddcSAtari911            if ($testReturn === 0) {
50611d05cddcSAtari911                return ($path === 'php') ? 'php' : escapeshellarg($path);
50621d05cddcSAtari911            }
50631d05cddcSAtari911        }
50641d05cddcSAtari911
50651d05cddcSAtari911        // Fallback to 'php' and hope it's in PATH
50661d05cddcSAtari911        return 'php';
50671d05cddcSAtari911    }
50681d05cddcSAtari911
50691d05cddcSAtari911    private function redirect($message, $type = 'success', $tab = null) {
50701d05cddcSAtari911        $url = '?do=admin&page=calendar';
50711d05cddcSAtari911        if ($tab) {
50721d05cddcSAtari911            $url .= '&tab=' . $tab;
50731d05cddcSAtari911        }
50741d05cddcSAtari911        $url .= '&msg=' . urlencode($message) . '&msgtype=' . $type;
50751d05cddcSAtari911        header('Location: ' . $url);
50761d05cddcSAtari911        exit;
50771d05cddcSAtari911    }
50781d05cddcSAtari911
50791d05cddcSAtari911    private function getLog() {
50801d05cddcSAtari911        global $INPUT;
50811d05cddcSAtari911
50821d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
50831d05cddcSAtari911            header('Content-Type: application/json');
50841d05cddcSAtari911
50851d05cddcSAtari911            $logFile = DOKU_PLUGIN . 'calendar/sync.log';
50861d05cddcSAtari911            $log = '';
50871d05cddcSAtari911
50881d05cddcSAtari911            if (file_exists($logFile)) {
50891d05cddcSAtari911                // Get last 500 lines
50901d05cddcSAtari911                $lines = file($logFile);
50911d05cddcSAtari911                if ($lines !== false) {
50921d05cddcSAtari911                    $lines = array_slice($lines, -500);
50931d05cddcSAtari911                    $log = implode('', $lines);
50941d05cddcSAtari911                }
50951d05cddcSAtari911            } else {
50961d05cddcSAtari911                $log = "No log file found. Sync hasn't run yet.";
50971d05cddcSAtari911            }
50981d05cddcSAtari911
50991d05cddcSAtari911            echo json_encode(['log' => $log]);
51001d05cddcSAtari911            exit;
51011d05cddcSAtari911        }
51021d05cddcSAtari911    }
51031d05cddcSAtari911
51041d05cddcSAtari911    private function exportConfig() {
51051d05cddcSAtari911        global $INPUT;
51061d05cddcSAtari911
51071d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
51081d05cddcSAtari911            header('Content-Type: application/json');
51091d05cddcSAtari911
51101d05cddcSAtari911            try {
51111d05cddcSAtari911                $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
51121d05cddcSAtari911
51131d05cddcSAtari911                if (!file_exists($configFile)) {
51141d05cddcSAtari911                    echo json_encode([
51151d05cddcSAtari911                        'success' => false,
51161d05cddcSAtari911                        'message' => 'Config file not found'
51171d05cddcSAtari911                    ]);
51181d05cddcSAtari911                    exit;
51191d05cddcSAtari911                }
51201d05cddcSAtari911
51211d05cddcSAtari911                // Read config file
51221d05cddcSAtari911                $configContent = file_get_contents($configFile);
51231d05cddcSAtari911
51241d05cddcSAtari911                // Generate encryption key from DokuWiki secret
51251d05cddcSAtari911                $key = $this->getEncryptionKey();
51261d05cddcSAtari911
51271d05cddcSAtari911                // Encrypt config
51281d05cddcSAtari911                $encrypted = $this->encryptData($configContent, $key);
51291d05cddcSAtari911
51301d05cddcSAtari911                echo json_encode([
51311d05cddcSAtari911                    'success' => true,
51321d05cddcSAtari911                    'encrypted' => $encrypted,
51331d05cddcSAtari911                    'message' => 'Config exported successfully'
51341d05cddcSAtari911                ]);
51351d05cddcSAtari911                exit;
51361d05cddcSAtari911
51371d05cddcSAtari911            } catch (Exception $e) {
51381d05cddcSAtari911                echo json_encode([
51391d05cddcSAtari911                    'success' => false,
51401d05cddcSAtari911                    'message' => $e->getMessage()
51411d05cddcSAtari911                ]);
51421d05cddcSAtari911                exit;
51431d05cddcSAtari911            }
51441d05cddcSAtari911        }
51451d05cddcSAtari911    }
51461d05cddcSAtari911
51471d05cddcSAtari911    private function importConfig() {
51481d05cddcSAtari911        global $INPUT;
51491d05cddcSAtari911
51501d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
51511d05cddcSAtari911            header('Content-Type: application/json');
51521d05cddcSAtari911
51531d05cddcSAtari911            try {
51541d05cddcSAtari911                $encrypted = $_POST['encrypted_config'] ?? '';
51551d05cddcSAtari911
51561d05cddcSAtari911                if (empty($encrypted)) {
51571d05cddcSAtari911                    echo json_encode([
51581d05cddcSAtari911                        'success' => false,
51591d05cddcSAtari911                        'message' => 'No config data provided'
51601d05cddcSAtari911                    ]);
51611d05cddcSAtari911                    exit;
51621d05cddcSAtari911                }
51631d05cddcSAtari911
51641d05cddcSAtari911                // Generate encryption key from DokuWiki secret
51651d05cddcSAtari911                $key = $this->getEncryptionKey();
51661d05cddcSAtari911
51671d05cddcSAtari911                // Decrypt config
51681d05cddcSAtari911                $configContent = $this->decryptData($encrypted, $key);
51691d05cddcSAtari911
51701d05cddcSAtari911                if ($configContent === false) {
51711d05cddcSAtari911                    echo json_encode([
51721d05cddcSAtari911                        'success' => false,
51731d05cddcSAtari911                        'message' => 'Decryption failed. Invalid key or corrupted file.'
51741d05cddcSAtari911                    ]);
51751d05cddcSAtari911                    exit;
51761d05cddcSAtari911                }
51771d05cddcSAtari911
5178*7e8ea635SAtari911                // Validate PHP config file structure (without using eval)
5179*7e8ea635SAtari911                // Check that it starts with <?php and contains a return statement with array
5180*7e8ea635SAtari911                if (strpos($configContent, '<?php') === false) {
51811d05cddcSAtari911                    echo json_encode([
51821d05cddcSAtari911                        'success' => false,
5183*7e8ea635SAtari911                        'message' => 'Invalid config file: missing PHP opening tag'
5184*7e8ea635SAtari911                    ]);
5185*7e8ea635SAtari911                    exit;
5186*7e8ea635SAtari911                }
5187*7e8ea635SAtari911
5188*7e8ea635SAtari911                // Check for dangerous patterns that shouldn't be in a config file
5189*7e8ea635SAtari911                $dangerousPatterns = [
5190*7e8ea635SAtari911                    '/\b(exec|shell_exec|system|passthru|popen|proc_open)\s*\(/i',
5191*7e8ea635SAtari911                    '/\b(eval|assert|create_function)\s*\(/i',
5192*7e8ea635SAtari911                    '/\b(file_get_contents|file_put_contents|fopen|fwrite|unlink|rmdir)\s*\(/i',
5193*7e8ea635SAtari911                    '/\$_(GET|POST|REQUEST|SERVER|FILES|COOKIE|SESSION)\s*\[/i',
5194*7e8ea635SAtari911                    '/`[^`]+`/',  // Backtick execution
5195*7e8ea635SAtari911                ];
5196*7e8ea635SAtari911
5197*7e8ea635SAtari911                foreach ($dangerousPatterns as $pattern) {
5198*7e8ea635SAtari911                    if (preg_match($pattern, $configContent)) {
5199*7e8ea635SAtari911                        echo json_encode([
5200*7e8ea635SAtari911                            'success' => false,
5201*7e8ea635SAtari911                            'message' => 'Invalid config file: contains prohibited code patterns'
5202*7e8ea635SAtari911                        ]);
5203*7e8ea635SAtari911                        exit;
5204*7e8ea635SAtari911                    }
5205*7e8ea635SAtari911                }
5206*7e8ea635SAtari911
5207*7e8ea635SAtari911                // Verify it looks like a valid config (has return array structure)
5208*7e8ea635SAtari911                if (!preg_match('/return\s*\[/', $configContent)) {
5209*7e8ea635SAtari911                    echo json_encode([
5210*7e8ea635SAtari911                        'success' => false,
5211*7e8ea635SAtari911                        'message' => 'Invalid config file: must contain a return array statement'
52121d05cddcSAtari911                    ]);
52131d05cddcSAtari911                    exit;
52141d05cddcSAtari911                }
52151d05cddcSAtari911
52161d05cddcSAtari911                // Write to config file
52171d05cddcSAtari911                $configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
52181d05cddcSAtari911
52191d05cddcSAtari911                // Backup existing config
52201d05cddcSAtari911                if (file_exists($configFile)) {
52211d05cddcSAtari911                    $backupFile = $configFile . '.backup.' . date('Y-m-d_H-i-s');
52221d05cddcSAtari911                    copy($configFile, $backupFile);
52231d05cddcSAtari911                }
52241d05cddcSAtari911
52251d05cddcSAtari911                // Write new config
52261d05cddcSAtari911                if (file_put_contents($configFile, $configContent) === false) {
52271d05cddcSAtari911                    echo json_encode([
52281d05cddcSAtari911                        'success' => false,
52291d05cddcSAtari911                        'message' => 'Failed to write config file'
52301d05cddcSAtari911                    ]);
52311d05cddcSAtari911                    exit;
52321d05cddcSAtari911                }
52331d05cddcSAtari911
52341d05cddcSAtari911                echo json_encode([
52351d05cddcSAtari911                    'success' => true,
52361d05cddcSAtari911                    'message' => 'Config imported successfully'
52371d05cddcSAtari911                ]);
52381d05cddcSAtari911                exit;
52391d05cddcSAtari911
52401d05cddcSAtari911            } catch (Exception $e) {
52411d05cddcSAtari911                echo json_encode([
52421d05cddcSAtari911                    'success' => false,
52431d05cddcSAtari911                    'message' => $e->getMessage()
52441d05cddcSAtari911                ]);
52451d05cddcSAtari911                exit;
52461d05cddcSAtari911            }
52471d05cddcSAtari911        }
52481d05cddcSAtari911    }
52491d05cddcSAtari911
52501d05cddcSAtari911    private function getEncryptionKey() {
52511d05cddcSAtari911        global $conf;
52521d05cddcSAtari911        // Use DokuWiki's secret as the base for encryption
52531d05cddcSAtari911        // This ensures the key is unique per installation
52541d05cddcSAtari911        return hash('sha256', $conf['secret'] . 'calendar_config_encryption', true);
52551d05cddcSAtari911    }
52561d05cddcSAtari911
52571d05cddcSAtari911    private function encryptData($data, $key) {
52581d05cddcSAtari911        // Use AES-256-CBC encryption
52591d05cddcSAtari911        $ivLength = openssl_cipher_iv_length('aes-256-cbc');
52601d05cddcSAtari911        $iv = openssl_random_pseudo_bytes($ivLength);
52611d05cddcSAtari911
52621d05cddcSAtari911        $encrypted = openssl_encrypt($data, 'aes-256-cbc', $key, 0, $iv);
52631d05cddcSAtari911
52641d05cddcSAtari911        // Combine IV and encrypted data, then base64 encode
52651d05cddcSAtari911        return base64_encode($iv . $encrypted);
52661d05cddcSAtari911    }
52671d05cddcSAtari911
52681d05cddcSAtari911    private function decryptData($encryptedData, $key) {
52691d05cddcSAtari911        // Decode base64
52701d05cddcSAtari911        $data = base64_decode($encryptedData);
52711d05cddcSAtari911
52721d05cddcSAtari911        if ($data === false) {
52731d05cddcSAtari911            return false;
52741d05cddcSAtari911        }
52751d05cddcSAtari911
52761d05cddcSAtari911        // Extract IV and encrypted content
52771d05cddcSAtari911        $ivLength = openssl_cipher_iv_length('aes-256-cbc');
52781d05cddcSAtari911        $iv = substr($data, 0, $ivLength);
52791d05cddcSAtari911        $encrypted = substr($data, $ivLength);
52801d05cddcSAtari911
52811d05cddcSAtari911        // Decrypt
52821d05cddcSAtari911        $decrypted = openssl_decrypt($encrypted, 'aes-256-cbc', $key, 0, $iv);
52831d05cddcSAtari911
52841d05cddcSAtari911        return $decrypted;
52851d05cddcSAtari911    }
52861d05cddcSAtari911
52871d05cddcSAtari911    private function clearLogFile() {
52881d05cddcSAtari911        global $INPUT;
52891d05cddcSAtari911
52901d05cddcSAtari911        if ($INPUT->str('call') === 'ajax') {
52911d05cddcSAtari911            header('Content-Type: application/json');
52921d05cddcSAtari911
52931d05cddcSAtari911            $logFile = DOKU_PLUGIN . 'calendar/sync.log';
52941d05cddcSAtari911
52951d05cddcSAtari911            if (file_exists($logFile)) {
52961d05cddcSAtari911                if (file_put_contents($logFile, '')) {
52971d05cddcSAtari911                    echo json_encode(['success' => true]);
52981d05cddcSAtari911                } else {
52991d05cddcSAtari911                    echo json_encode(['success' => false, 'message' => 'Could not clear log file']);
53001d05cddcSAtari911                }
53011d05cddcSAtari911            } else {
53021d05cddcSAtari911                echo json_encode(['success' => true, 'message' => 'No log file to clear']);
53031d05cddcSAtari911            }
53041d05cddcSAtari911            exit;
53051d05cddcSAtari911        }
53061d05cddcSAtari911    }
53071d05cddcSAtari911
53081d05cddcSAtari911    private function downloadLog() {
53091d05cddcSAtari911        $logFile = DOKU_PLUGIN . 'calendar/sync.log';
53101d05cddcSAtari911
53111d05cddcSAtari911        if (file_exists($logFile)) {
53121d05cddcSAtari911            header('Content-Type: text/plain');
53131d05cddcSAtari911            header('Content-Disposition: attachment; filename="calendar-sync-' . date('Y-m-d-His') . '.log"');
53141d05cddcSAtari911            readfile($logFile);
53151d05cddcSAtari911            exit;
53161d05cddcSAtari911        } else {
53171d05cddcSAtari911            echo 'No log file found';
53181d05cddcSAtari911            exit;
53191d05cddcSAtari911        }
53201d05cddcSAtari911    }
53211d05cddcSAtari911
53221d05cddcSAtari911    private function getEventStatistics() {
53231d05cddcSAtari911        $stats = [
53241d05cddcSAtari911            'total_events' => 0,
53251d05cddcSAtari911            'total_namespaces' => 0,
53261d05cddcSAtari911            'total_files' => 0,
53271d05cddcSAtari911            'total_recurring' => 0,
53281d05cddcSAtari911            'by_namespace' => [],
53291d05cddcSAtari911            'last_scan' => ''
53301d05cddcSAtari911        ];
53311d05cddcSAtari911
53321d05cddcSAtari911        $metaDir = DOKU_INC . 'data/meta/';
53331d05cddcSAtari911        $cacheFile = DOKU_PLUGIN . 'calendar/.event_stats_cache';
53341d05cddcSAtari911
53351d05cddcSAtari911        // Check if we have cached stats (less than 5 minutes old)
53361d05cddcSAtari911        if (file_exists($cacheFile)) {
53371d05cddcSAtari911            $cacheData = json_decode(file_get_contents($cacheFile), true);
53381d05cddcSAtari911            if ($cacheData && (time() - $cacheData['timestamp']) < 300) {
53391d05cddcSAtari911                return $cacheData['stats'];
53401d05cddcSAtari911            }
53411d05cddcSAtari911        }
53421d05cddcSAtari911
53431d05cddcSAtari911        // Scan for events
53441d05cddcSAtari911        $this->scanDirectoryForStats($metaDir, '', $stats);
53451d05cddcSAtari911
53461d05cddcSAtari911        // Count recurring events
53471d05cddcSAtari911        $recurringEvents = $this->findRecurringEvents();
53481d05cddcSAtari911        $stats['total_recurring'] = count($recurringEvents);
53491d05cddcSAtari911
53501d05cddcSAtari911        $stats['total_namespaces'] = count($stats['by_namespace']);
53511d05cddcSAtari911        $stats['last_scan'] = date('Y-m-d H:i:s');
53521d05cddcSAtari911
53531d05cddcSAtari911        // Cache the results
53541d05cddcSAtari911        file_put_contents($cacheFile, json_encode([
53551d05cddcSAtari911            'timestamp' => time(),
53561d05cddcSAtari911            'stats' => $stats
53571d05cddcSAtari911        ]));
53581d05cddcSAtari911
53591d05cddcSAtari911        return $stats;
53601d05cddcSAtari911    }
53611d05cddcSAtari911
53621d05cddcSAtari911    private function scanDirectoryForStats($dir, $namespace, &$stats) {
53631d05cddcSAtari911        if (!is_dir($dir)) return;
53641d05cddcSAtari911
53651d05cddcSAtari911        $items = scandir($dir);
53661d05cddcSAtari911        foreach ($items as $item) {
53671d05cddcSAtari911            if ($item === '.' || $item === '..') continue;
53681d05cddcSAtari911
53691d05cddcSAtari911            $path = $dir . $item;
53701d05cddcSAtari911
53711d05cddcSAtari911            // Check if this is a calendar directory
53721d05cddcSAtari911            if ($item === 'calendar' && is_dir($path)) {
53731d05cddcSAtari911                $jsonFiles = glob($path . '/*.json');
53741d05cddcSAtari911                $eventCount = 0;
53751d05cddcSAtari911
53761d05cddcSAtari911                foreach ($jsonFiles as $file) {
53771d05cddcSAtari911                    $stats['total_files']++;
53781d05cddcSAtari911                    $data = json_decode(file_get_contents($file), true);
53791d05cddcSAtari911                    if ($data) {
53801d05cddcSAtari911                        foreach ($data as $dateEvents) {
53811d05cddcSAtari911                            $eventCount += count($dateEvents);
53821d05cddcSAtari911                        }
53831d05cddcSAtari911                    }
53841d05cddcSAtari911                }
53851d05cddcSAtari911
53861d05cddcSAtari911                $stats['total_events'] += $eventCount;
53871d05cddcSAtari911
53881d05cddcSAtari911                if ($eventCount > 0) {
53891d05cddcSAtari911                    $stats['by_namespace'][$namespace] = [
53901d05cddcSAtari911                        'events' => $eventCount,
53911d05cddcSAtari911                        'files' => count($jsonFiles)
53921d05cddcSAtari911                    ];
53931d05cddcSAtari911                }
53941d05cddcSAtari911            } elseif (is_dir($path)) {
53951d05cddcSAtari911                // Recurse into subdirectories
53961d05cddcSAtari911                $newNamespace = $namespace ? $namespace . ':' . $item : $item;
53971d05cddcSAtari911                $this->scanDirectoryForStats($path . '/', $newNamespace, $stats);
53981d05cddcSAtari911            }
53991d05cddcSAtari911        }
54001d05cddcSAtari911    }
54011d05cddcSAtari911
54021d05cddcSAtari911    private function rescanEvents() {
54031d05cddcSAtari911        // Clear the cache to force a rescan
54049ccd446eSAtari911        $this->clearStatsCache();
54051d05cddcSAtari911
54061d05cddcSAtari911        // Get fresh statistics
54071d05cddcSAtari911        $stats = $this->getEventStatistics();
54081d05cddcSAtari911
54091d05cddcSAtari911        // Build absolute redirect URL
54101d05cddcSAtari911        $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('Events rescanned! Found ' . $stats['total_events'] . ' events in ' . $stats['total_namespaces'] . ' namespaces.') . '&msgtype=success';
54111d05cddcSAtari911
54121d05cddcSAtari911        // Redirect with success message using absolute URL
54131d05cddcSAtari911        header('Location: ' . $redirectUrl, true, 303);
54141d05cddcSAtari911        exit;
54151d05cddcSAtari911    }
54161d05cddcSAtari911
54171d05cddcSAtari911    private function exportAllEvents() {
54181d05cddcSAtari911        $metaDir = DOKU_INC . 'data/meta/';
54191d05cddcSAtari911        $allEvents = [];
54201d05cddcSAtari911
54211d05cddcSAtari911        // Collect all events
54221d05cddcSAtari911        $this->collectAllEvents($metaDir, '', $allEvents);
54231d05cddcSAtari911
54241d05cddcSAtari911        // Create export package
54259ccd446eSAtari911        // Get current version
54269ccd446eSAtari911        $pluginInfo = DOKU_PLUGIN . 'calendar/plugin.info.txt';
54279ccd446eSAtari911        $info = file_exists($pluginInfo) ? confToHash($pluginInfo) : [];
54289ccd446eSAtari911        $currentVersion = isset($info['version']) ? trim($info['version']) : 'unknown';
54299ccd446eSAtari911
54301d05cddcSAtari911        $exportData = [
54311d05cddcSAtari911            'export_date' => date('Y-m-d H:i:s'),
54329ccd446eSAtari911            'version' => $currentVersion,
54331d05cddcSAtari911            'total_events' => 0,
54341d05cddcSAtari911            'namespaces' => []
54351d05cddcSAtari911        ];
54361d05cddcSAtari911
54371d05cddcSAtari911        foreach ($allEvents as $namespace => $files) {
54381d05cddcSAtari911            $exportData['namespaces'][$namespace] = [];
54391d05cddcSAtari911            foreach ($files as $filename => $events) {
54401d05cddcSAtari911                $exportData['namespaces'][$namespace][$filename] = $events;
54411d05cddcSAtari911                foreach ($events as $dateEvents) {
54421d05cddcSAtari911                    $exportData['total_events'] += count($dateEvents);
54431d05cddcSAtari911                }
54441d05cddcSAtari911            }
54451d05cddcSAtari911        }
54461d05cddcSAtari911
54471d05cddcSAtari911        // Send as download
54481d05cddcSAtari911        header('Content-Type: application/json');
54491d05cddcSAtari911        header('Content-Disposition: attachment; filename="calendar-events-export-' . date('Y-m-d-His') . '.json"');
54501d05cddcSAtari911        echo json_encode($exportData, JSON_PRETTY_PRINT);
54511d05cddcSAtari911        exit;
54521d05cddcSAtari911    }
54531d05cddcSAtari911
54541d05cddcSAtari911    private function collectAllEvents($dir, $namespace, &$allEvents) {
54551d05cddcSAtari911        if (!is_dir($dir)) return;
54561d05cddcSAtari911
54571d05cddcSAtari911        $items = scandir($dir);
54581d05cddcSAtari911        foreach ($items as $item) {
54591d05cddcSAtari911            if ($item === '.' || $item === '..') continue;
54601d05cddcSAtari911
54611d05cddcSAtari911            $path = $dir . $item;
54621d05cddcSAtari911
54631d05cddcSAtari911            // Check if this is a calendar directory
54641d05cddcSAtari911            if ($item === 'calendar' && is_dir($path)) {
54651d05cddcSAtari911                $jsonFiles = glob($path . '/*.json');
54661d05cddcSAtari911
54671d05cddcSAtari911                if (!isset($allEvents[$namespace])) {
54681d05cddcSAtari911                    $allEvents[$namespace] = [];
54691d05cddcSAtari911                }
54701d05cddcSAtari911
54711d05cddcSAtari911                foreach ($jsonFiles as $file) {
54721d05cddcSAtari911                    $filename = basename($file);
54731d05cddcSAtari911                    $data = json_decode(file_get_contents($file), true);
54741d05cddcSAtari911                    if ($data) {
54751d05cddcSAtari911                        $allEvents[$namespace][$filename] = $data;
54761d05cddcSAtari911                    }
54771d05cddcSAtari911                }
54781d05cddcSAtari911            } elseif (is_dir($path)) {
54791d05cddcSAtari911                // Recurse into subdirectories
54801d05cddcSAtari911                $newNamespace = $namespace ? $namespace . ':' . $item : $item;
54811d05cddcSAtari911                $this->collectAllEvents($path . '/', $newNamespace, $allEvents);
54821d05cddcSAtari911            }
54831d05cddcSAtari911        }
54841d05cddcSAtari911    }
54851d05cddcSAtari911
54861d05cddcSAtari911    private function importAllEvents() {
54871d05cddcSAtari911        global $INPUT;
54881d05cddcSAtari911
54891d05cddcSAtari911        if (!isset($_FILES['import_file'])) {
54901d05cddcSAtari911            $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('No file uploaded') . '&msgtype=error';
54911d05cddcSAtari911            header('Location: ' . $redirectUrl, true, 303);
54921d05cddcSAtari911            exit;
54931d05cddcSAtari911        }
54941d05cddcSAtari911
54951d05cddcSAtari911        $file = $_FILES['import_file'];
54961d05cddcSAtari911
54971d05cddcSAtari911        if ($file['error'] !== UPLOAD_ERR_OK) {
54981d05cddcSAtari911            $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('Upload error: ' . $file['error']) . '&msgtype=error';
54991d05cddcSAtari911            header('Location: ' . $redirectUrl, true, 303);
55001d05cddcSAtari911            exit;
55011d05cddcSAtari911        }
55021d05cddcSAtari911
55031d05cddcSAtari911        // Read and decode the import file
55041d05cddcSAtari911        $importData = json_decode(file_get_contents($file['tmp_name']), true);
55051d05cddcSAtari911
55061d05cddcSAtari911        if (!$importData || !isset($importData['namespaces'])) {
55071d05cddcSAtari911            $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('Invalid import file format') . '&msgtype=error';
55081d05cddcSAtari911            header('Location: ' . $redirectUrl, true, 303);
55091d05cddcSAtari911            exit;
55101d05cddcSAtari911        }
55111d05cddcSAtari911
55121d05cddcSAtari911        $importedCount = 0;
55131d05cddcSAtari911        $mergedCount = 0;
55141d05cddcSAtari911
55151d05cddcSAtari911        // Import events
55161d05cddcSAtari911        foreach ($importData['namespaces'] as $namespace => $files) {
55171d05cddcSAtari911            $metaDir = DOKU_INC . 'data/meta/';
55181d05cddcSAtari911            if ($namespace) {
55191d05cddcSAtari911                $metaDir .= str_replace(':', '/', $namespace) . '/';
55201d05cddcSAtari911            }
55211d05cddcSAtari911            $calendarDir = $metaDir . 'calendar/';
55221d05cddcSAtari911
55231d05cddcSAtari911            // Create directory if needed
55241d05cddcSAtari911            if (!is_dir($calendarDir)) {
55251d05cddcSAtari911                mkdir($calendarDir, 0755, true);
55261d05cddcSAtari911            }
55271d05cddcSAtari911
55281d05cddcSAtari911            foreach ($files as $filename => $events) {
55291d05cddcSAtari911                $targetFile = $calendarDir . $filename;
55301d05cddcSAtari911
55311d05cddcSAtari911                // If file exists, merge events
55321d05cddcSAtari911                if (file_exists($targetFile)) {
55331d05cddcSAtari911                    $existing = json_decode(file_get_contents($targetFile), true);
55341d05cddcSAtari911                    if ($existing) {
55351d05cddcSAtari911                        foreach ($events as $date => $dateEvents) {
55361d05cddcSAtari911                            if (!isset($existing[$date])) {
55371d05cddcSAtari911                                $existing[$date] = [];
55381d05cddcSAtari911                            }
55391d05cddcSAtari911                            foreach ($dateEvents as $event) {
55401d05cddcSAtari911                                // Check if event with same ID exists
55411d05cddcSAtari911                                $found = false;
55421d05cddcSAtari911                                foreach ($existing[$date] as $existingEvent) {
55431d05cddcSAtari911                                    if ($existingEvent['id'] === $event['id']) {
55441d05cddcSAtari911                                        $found = true;
55451d05cddcSAtari911                                        break;
55461d05cddcSAtari911                                    }
55471d05cddcSAtari911                                }
55481d05cddcSAtari911                                if (!$found) {
55491d05cddcSAtari911                                    $existing[$date][] = $event;
55501d05cddcSAtari911                                    $importedCount++;
55511d05cddcSAtari911                                } else {
55521d05cddcSAtari911                                    $mergedCount++;
55531d05cddcSAtari911                                }
55541d05cddcSAtari911                            }
55551d05cddcSAtari911                        }
55561d05cddcSAtari911                        file_put_contents($targetFile, json_encode($existing, JSON_PRETTY_PRINT));
55571d05cddcSAtari911                    }
55581d05cddcSAtari911                } else {
55591d05cddcSAtari911                    // New file
55601d05cddcSAtari911                    file_put_contents($targetFile, json_encode($events, JSON_PRETTY_PRINT));
55611d05cddcSAtari911                    foreach ($events as $dateEvents) {
55621d05cddcSAtari911                        $importedCount += count($dateEvents);
55631d05cddcSAtari911                    }
55641d05cddcSAtari911                }
55651d05cddcSAtari911            }
55661d05cddcSAtari911        }
55671d05cddcSAtari911
55681d05cddcSAtari911        // Clear cache
55699ccd446eSAtari911        $this->clearStatsCache();
55701d05cddcSAtari911
55711d05cddcSAtari911        $message = "Import complete! Imported $importedCount new events";
55721d05cddcSAtari911        if ($mergedCount > 0) {
55731d05cddcSAtari911            $message .= ", skipped $mergedCount duplicates";
55741d05cddcSAtari911        }
55751d05cddcSAtari911
55761d05cddcSAtari911        $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode($message) . '&msgtype=success';
55771d05cddcSAtari911        header('Location: ' . $redirectUrl, true, 303);
55781d05cddcSAtari911        exit;
55791d05cddcSAtari911    }
55801d05cddcSAtari911
55811d05cddcSAtari911    private function previewCleanup() {
55821d05cddcSAtari911        global $INPUT;
55831d05cddcSAtari911
55841d05cddcSAtari911        $cleanupType = $INPUT->str('cleanup_type', 'age');
55851d05cddcSAtari911        $namespaceFilter = $INPUT->str('namespace_filter', '');
55861d05cddcSAtari911
55871d05cddcSAtari911        // Debug info
55881d05cddcSAtari911        $debug = [];
55891d05cddcSAtari911        $debug['cleanup_type'] = $cleanupType;
55901d05cddcSAtari911        $debug['namespace_filter'] = $namespaceFilter;
55911d05cddcSAtari911        $debug['age_value'] = $INPUT->int('age_value', 6);
55921d05cddcSAtari911        $debug['age_unit'] = $INPUT->str('age_unit', 'months');
55931d05cddcSAtari911        $debug['range_start'] = $INPUT->str('range_start', '');
55941d05cddcSAtari911        $debug['range_end'] = $INPUT->str('range_end', '');
55951d05cddcSAtari911        $debug['delete_completed'] = $INPUT->bool('delete_completed', false);
55961d05cddcSAtari911        $debug['delete_past'] = $INPUT->bool('delete_past', false);
55971d05cddcSAtari911
55981d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
55991d05cddcSAtari911        $debug['data_dir'] = $dataDir;
56001d05cddcSAtari911        $debug['data_dir_exists'] = is_dir($dataDir);
56011d05cddcSAtari911
56021d05cddcSAtari911        $eventsToDelete = $this->findEventsToCleanup($cleanupType, $namespaceFilter);
56031d05cddcSAtari911
56041d05cddcSAtari911        // Merge with scan debug info
56051d05cddcSAtari911        if (isset($this->_cleanupDebug)) {
56061d05cddcSAtari911            $debug = array_merge($debug, $this->_cleanupDebug);
56071d05cddcSAtari911        }
56081d05cddcSAtari911
56091d05cddcSAtari911        // Return JSON for preview with debug info
56101d05cddcSAtari911        header('Content-Type: application/json');
56111d05cddcSAtari911        echo json_encode([
56121d05cddcSAtari911            'count' => count($eventsToDelete),
56131d05cddcSAtari911            'events' => array_slice($eventsToDelete, 0, 50), // Limit to 50 for preview
56141d05cddcSAtari911            'debug' => $debug
56151d05cddcSAtari911        ]);
56161d05cddcSAtari911        exit;
56171d05cddcSAtari911    }
56181d05cddcSAtari911
56191d05cddcSAtari911    private function cleanupEvents() {
56201d05cddcSAtari911        global $INPUT;
56211d05cddcSAtari911
56221d05cddcSAtari911        $cleanupType = $INPUT->str('cleanup_type', 'age');
56231d05cddcSAtari911        $namespaceFilter = $INPUT->str('namespace_filter', '');
56241d05cddcSAtari911
56251d05cddcSAtari911        // Create backup first
56261d05cddcSAtari911        $backupDir = DOKU_PLUGIN . 'calendar/backups/';
56271d05cddcSAtari911        if (!is_dir($backupDir)) {
56281d05cddcSAtari911            mkdir($backupDir, 0755, true);
56291d05cddcSAtari911        }
56301d05cddcSAtari911
56311d05cddcSAtari911        $backupFile = $backupDir . 'before-cleanup-' . date('Y-m-d-His') . '.zip';
56321d05cddcSAtari911        $this->createBackup($backupFile);
56331d05cddcSAtari911
56341d05cddcSAtari911        // Find events to delete
56351d05cddcSAtari911        $eventsToDelete = $this->findEventsToCleanup($cleanupType, $namespaceFilter);
56361d05cddcSAtari911        $deletedCount = 0;
56371d05cddcSAtari911
56381d05cddcSAtari911        // Group by file
56391d05cddcSAtari911        $fileGroups = [];
56401d05cddcSAtari911        foreach ($eventsToDelete as $evt) {
56411d05cddcSAtari911            $fileGroups[$evt['file']][] = $evt;
56421d05cddcSAtari911        }
56431d05cddcSAtari911
56441d05cddcSAtari911        // Delete from each file
56451d05cddcSAtari911        foreach ($fileGroups as $file => $events) {
56461d05cddcSAtari911            if (!file_exists($file)) continue;
56471d05cddcSAtari911
56481d05cddcSAtari911            $json = file_get_contents($file);
56491d05cddcSAtari911            $data = json_decode($json, true);
56501d05cddcSAtari911
56511d05cddcSAtari911            if (!$data) continue;
56521d05cddcSAtari911
56531d05cddcSAtari911            // Remove events
56541d05cddcSAtari911            foreach ($events as $evt) {
56551d05cddcSAtari911                if (isset($data[$evt['date']])) {
56561d05cddcSAtari911                    $data[$evt['date']] = array_filter($data[$evt['date']], function($e) use ($evt) {
56571d05cddcSAtari911                        return $e['id'] !== $evt['id'];
56581d05cddcSAtari911                    });
56591d05cddcSAtari911
56601d05cddcSAtari911                    // Remove date key if empty
56611d05cddcSAtari911                    if (empty($data[$evt['date']])) {
56621d05cddcSAtari911                        unset($data[$evt['date']]);
56631d05cddcSAtari911                    }
56641d05cddcSAtari911
56651d05cddcSAtari911                    $deletedCount++;
56661d05cddcSAtari911                }
56671d05cddcSAtari911            }
56681d05cddcSAtari911
56691d05cddcSAtari911            // Save file or delete if empty
56701d05cddcSAtari911            if (empty($data)) {
56711d05cddcSAtari911                unlink($file);
56721d05cddcSAtari911            } else {
56731d05cddcSAtari911                file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT));
56741d05cddcSAtari911            }
56751d05cddcSAtari911        }
56761d05cddcSAtari911
56771d05cddcSAtari911        // Clear cache
56789ccd446eSAtari911        $this->clearStatsCache();
56791d05cddcSAtari911
56801d05cddcSAtari911        $message = "Cleanup complete! Deleted $deletedCount event(s). Backup created: " . basename($backupFile);
56811d05cddcSAtari911        $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode($message) . '&msgtype=success';
56821d05cddcSAtari911        header('Location: ' . $redirectUrl, true, 303);
56831d05cddcSAtari911        exit;
56841d05cddcSAtari911    }
56851d05cddcSAtari911
56861d05cddcSAtari911    private function findEventsToCleanup($cleanupType, $namespaceFilter) {
56871d05cddcSAtari911        global $INPUT;
56881d05cddcSAtari911
56891d05cddcSAtari911        $eventsToDelete = [];
56901d05cddcSAtari911        $dataDir = DOKU_INC . 'data/meta/';
56911d05cddcSAtari911
56921d05cddcSAtari911        $debug = [];
56931d05cddcSAtari911        $debug['scanned_dirs'] = [];
56941d05cddcSAtari911        $debug['found_files'] = [];
56951d05cddcSAtari911
56961d05cddcSAtari911        // Calculate cutoff date for age-based cleanup
56971d05cddcSAtari911        $cutoffDate = null;
56981d05cddcSAtari911        if ($cleanupType === 'age') {
56991d05cddcSAtari911            $ageValue = $INPUT->int('age_value', 6);
57001d05cddcSAtari911            $ageUnit = $INPUT->str('age_unit', 'months');
57011d05cddcSAtari911
57021d05cddcSAtari911            if ($ageUnit === 'years') {
57031d05cddcSAtari911                $ageValue *= 12; // Convert to months
57041d05cddcSAtari911            }
57051d05cddcSAtari911
57061d05cddcSAtari911            $cutoffDate = date('Y-m-d', strtotime("-$ageValue months"));
57071d05cddcSAtari911            $debug['cutoff_date'] = $cutoffDate;
57081d05cddcSAtari911        }
57091d05cddcSAtari911
57101d05cddcSAtari911        // Get date range for range-based cleanup
57111d05cddcSAtari911        $rangeStart = $cleanupType === 'range' ? $INPUT->str('range_start', '') : null;
57121d05cddcSAtari911        $rangeEnd = $cleanupType === 'range' ? $INPUT->str('range_end', '') : null;
57131d05cddcSAtari911
57141d05cddcSAtari911        // Get status filters
57151d05cddcSAtari911        $deleteCompleted = $cleanupType === 'status' && $INPUT->bool('delete_completed', false);
57161d05cddcSAtari911        $deletePast = $cleanupType === 'status' && $INPUT->bool('delete_past', false);
57171d05cddcSAtari911
57181d05cddcSAtari911        // Check root calendar directory first (blank/default namespace)
57191d05cddcSAtari911        $rootCalendarDir = $dataDir . 'calendar';
57201d05cddcSAtari911        $debug['root_calendar_dir'] = $rootCalendarDir;
57211d05cddcSAtari911        $debug['root_exists'] = is_dir($rootCalendarDir);
57221d05cddcSAtari911
57231d05cddcSAtari911        if (is_dir($rootCalendarDir)) {
57241d05cddcSAtari911            if (!$namespaceFilter || $namespaceFilter === '' || $namespaceFilter === 'default') {
57251d05cddcSAtari911                $debug['scanned_dirs'][] = $rootCalendarDir;
57261d05cddcSAtari911                $files = glob($rootCalendarDir . '/*.json');
57271d05cddcSAtari911                $debug['found_files'] = array_merge($debug['found_files'], $files);
57281d05cddcSAtari911                $this->processCalendarFiles($rootCalendarDir, '', $eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast);
57291d05cddcSAtari911            }
57301d05cddcSAtari911        }
57311d05cddcSAtari911
57321d05cddcSAtari911        // Scan all namespace directories
57331d05cddcSAtari911        $namespaceDirs = glob($dataDir . '*', GLOB_ONLYDIR);
57341d05cddcSAtari911        $debug['namespace_dirs_found'] = $namespaceDirs;
57351d05cddcSAtari911
57361d05cddcSAtari911        foreach ($namespaceDirs as $nsDir) {
57371d05cddcSAtari911            $namespace = basename($nsDir);
57381d05cddcSAtari911
57391d05cddcSAtari911            // Skip the root 'calendar' dir (already processed above)
57401d05cddcSAtari911            if ($namespace === 'calendar') continue;
57411d05cddcSAtari911
57421d05cddcSAtari911            // Check namespace filter
57431d05cddcSAtari911            if ($namespaceFilter && strpos($namespace, $namespaceFilter) === false) {
57441d05cddcSAtari911                continue;
57451d05cddcSAtari911            }
57461d05cddcSAtari911
57471d05cddcSAtari911            $calendarDir = $nsDir . '/calendar';
57481d05cddcSAtari911            $debug['checked_calendar_dirs'][] = $calendarDir;
57491d05cddcSAtari911
57501d05cddcSAtari911            if (!is_dir($calendarDir)) {
57511d05cddcSAtari911                $debug['missing_calendar_dirs'][] = $calendarDir;
57521d05cddcSAtari911                continue;
57531d05cddcSAtari911            }
57541d05cddcSAtari911
57551d05cddcSAtari911            $debug['scanned_dirs'][] = $calendarDir;
57561d05cddcSAtari911            $files = glob($calendarDir . '/*.json');
57571d05cddcSAtari911            $debug['found_files'] = array_merge($debug['found_files'], $files);
57581d05cddcSAtari911            $this->processCalendarFiles($calendarDir, $namespace, $eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast);
57591d05cddcSAtari911        }
57601d05cddcSAtari911
57611d05cddcSAtari911        // Store debug info globally for preview
57621d05cddcSAtari911        $this->_cleanupDebug = $debug;
57631d05cddcSAtari911
57641d05cddcSAtari911        return $eventsToDelete;
57651d05cddcSAtari911    }
57661d05cddcSAtari911
57671d05cddcSAtari911    private function processCalendarFiles($calendarDir, $namespace, &$eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast) {
57681d05cddcSAtari911        foreach (glob($calendarDir . '/*.json') as $file) {
57691d05cddcSAtari911            $json = file_get_contents($file);
57701d05cddcSAtari911            $data = json_decode($json, true);
57711d05cddcSAtari911
57721d05cddcSAtari911            if (!$data) continue;
57731d05cddcSAtari911
57741d05cddcSAtari911            foreach ($data as $date => $dateEvents) {
57751d05cddcSAtari911                foreach ($dateEvents as $event) {
57761d05cddcSAtari911                    $shouldDelete = false;
57771d05cddcSAtari911
57781d05cddcSAtari911                    // Age-based
57791d05cddcSAtari911                    if ($cleanupType === 'age' && $cutoffDate && $date < $cutoffDate) {
57801d05cddcSAtari911                        $shouldDelete = true;
57811d05cddcSAtari911                    }
57821d05cddcSAtari911
57831d05cddcSAtari911                    // Range-based
57841d05cddcSAtari911                    if ($cleanupType === 'range' && $rangeStart && $rangeEnd) {
57851d05cddcSAtari911                        if ($date >= $rangeStart && $date <= $rangeEnd) {
57861d05cddcSAtari911                            $shouldDelete = true;
57871d05cddcSAtari911                        }
57881d05cddcSAtari911                    }
57891d05cddcSAtari911
57901d05cddcSAtari911                    // Status-based
57911d05cddcSAtari911                    if ($cleanupType === 'status') {
57921d05cddcSAtari911                        $isTask = isset($event['isTask']) && $event['isTask'];
57931d05cddcSAtari911                        $isCompleted = isset($event['completed']) && $event['completed'];
57941d05cddcSAtari911                        $isPast = $date < date('Y-m-d');
57951d05cddcSAtari911
57961d05cddcSAtari911                        if ($deleteCompleted && $isTask && $isCompleted) {
57971d05cddcSAtari911                            $shouldDelete = true;
57981d05cddcSAtari911                        }
57991d05cddcSAtari911                        if ($deletePast && !$isTask && $isPast) {
58001d05cddcSAtari911                            $shouldDelete = true;
58011d05cddcSAtari911                        }
58021d05cddcSAtari911                    }
58031d05cddcSAtari911
58041d05cddcSAtari911                    if ($shouldDelete) {
58051d05cddcSAtari911                        $eventsToDelete[] = [
58061d05cddcSAtari911                            'id' => $event['id'],
58071d05cddcSAtari911                            'title' => $event['title'],
58081d05cddcSAtari911                            'date' => $date,
58091d05cddcSAtari911                            'namespace' => $namespace ?: 'default',
58101d05cddcSAtari911                            'file' => $file
58111d05cddcSAtari911                        ];
58121d05cddcSAtari911                    }
58131d05cddcSAtari911                }
58141d05cddcSAtari911            }
58151d05cddcSAtari911        }
58161d05cddcSAtari911    }
58179ccd446eSAtari911
58189ccd446eSAtari911    /**
58199ccd446eSAtari911     * Render Themes tab for sidebar widget theme selection
58209ccd446eSAtari911     */
58219ccd446eSAtari911    private function renderThemesTab($colors = null) {
58229ccd446eSAtari911        global $INPUT;
58239ccd446eSAtari911
58249ccd446eSAtari911        // Use defaults if not provided
58259ccd446eSAtari911        if ($colors === null) {
58269ccd446eSAtari911            $colors = $this->getTemplateColors();
58279ccd446eSAtari911        }
58289ccd446eSAtari911
58299ccd446eSAtari911        // Handle theme save
58309ccd446eSAtari911        if ($INPUT->str('action') === 'save_theme') {
58319ccd446eSAtari911            $theme = $INPUT->str('theme', 'matrix');
58329ccd446eSAtari911            $weekStart = $INPUT->str('week_start', 'monday');
58339ccd446eSAtari911            $this->saveSidebarTheme($theme);
58349ccd446eSAtari911            $this->saveWeekStartDay($weekStart);
58359ccd446eSAtari911            echo '<div style="background:#d4edda; border:1px solid #c3e6cb; color:#155724; padding:12px; border-radius:4px; margin-bottom:20px;">';
58369ccd446eSAtari911            echo '✓ Theme and settings saved successfully! Refresh any page with the sidebar to see changes.';
58379ccd446eSAtari911            echo '</div>';
58389ccd446eSAtari911        }
58399ccd446eSAtari911
58409ccd446eSAtari911        $currentTheme = $this->getSidebarTheme();
58419ccd446eSAtari911        $currentWeekStart = $this->getWeekStartDay();
58429ccd446eSAtari911
58439ccd446eSAtari911        echo '<h2 style="margin:0 0 20px 0; color:' . $colors['text'] . ';">�� Sidebar Widget Settings</h2>';
58449ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; margin-bottom:20px;">Customize the appearance and behavior of the sidebar calendar widget.</p>';
58459ccd446eSAtari911
58469ccd446eSAtari911        echo '<form method="post" action="?do=admin&page=calendar&tab=themes">';
58479ccd446eSAtari911        echo '<input type="hidden" name="action" value="save_theme">';
58489ccd446eSAtari911
58499ccd446eSAtari911        // Week Start Day Section
58509ccd446eSAtari911        echo '<div style="background:' . $colors['bg'] . '; border:1px solid ' . $colors['border'] . '; border-radius:6px; padding:20px; margin-bottom:30px;">';
58519ccd446eSAtari911        echo '<h3 style="margin:0 0 15px 0; color:' . $colors['text'] . '; font-size:16px;">�� Week Start Day</h3>';
58529ccd446eSAtari911        echo '<p style="color:' . $colors['text'] . '; margin-bottom:15px; font-size:13px;">Choose which day the week calendar grid starts with:</p>';
58539ccd446eSAtari911
58549ccd446eSAtari911        echo '<div style="display:flex; gap:15px;">';
58559ccd446eSAtari911        echo '<label style="flex:1; padding:12px; border:2px solid ' . ($currentWeekStart === 'monday' ? '#00cc07' : $colors['border']) . '; border-radius:4px; background:' . ($currentWeekStart === 'monday' ? 'rgba(0, 204, 7, 0.05)' : $colors['bg']) . '; cursor:pointer; display:flex; align-items:center;">';
58569ccd446eSAtari911        echo '<input type="radio" name="week_start" value="monday" ' . ($currentWeekStart === 'monday' ? 'checked' : '') . ' style="margin-right:10px; width:18px; height:18px;">';
58579ccd446eSAtari911        echo '<div>';
58589ccd446eSAtari911        echo '<div style="font-weight:bold; color:' . $colors['text'] . '; margin-bottom:3px;">Monday</div>';
58599ccd446eSAtari911        echo '<div style="font-size:11px; color:' . $colors['text'] . ';">Week starts on Monday (ISO standard)</div>';
58609ccd446eSAtari911        echo '</div>';
58619ccd446eSAtari911        echo '</label>';
58629ccd446eSAtari911
58639ccd446eSAtari911        echo '<label style="flex:1; padding:12px; border:2px solid ' . ($currentWeekStart === 'sunday' ? '#00cc07' : $colors['border']) . '; border-radius:4px; background:' . ($currentWeekStart === 'sunday' ? 'rgba(0, 204, 7, 0.05)' : $colors['bg']) . '; cursor:pointer; display:flex; align-items:center;">';
58649ccd446eSAtari911        echo '<input type="radio" name="week_start" value="sunday" ' . ($currentWeekStart === 'sunday' ? 'checked' : '') . ' style="margin-right:10px; width:18px; height:18px;">';
58659ccd446eSAtari911        echo '<div>';
58669ccd446eSAtari911        echo '<div style="font-weight:bold; color:' . $colors['text'] . '; margin-bottom:3px;">Sunday</div>';
58679ccd446eSAtari911        echo '<div style="font-size:11px; color:' . $colors['text'] . ';">Week starts on Sunday (US/Canada standard)</div>';
58689ccd446eSAtari911        echo '</div>';
58699ccd446eSAtari911        echo '</label>';
58709ccd446eSAtari911        echo '</div>';
58719ccd446eSAtari911        echo '</div>';
58729ccd446eSAtari911
58739ccd446eSAtari911        // Visual Theme Section
58749ccd446eSAtari911        echo '<h3 style="margin:0 0 15px 0; color:' . $colors['text'] . '; font-size:16px;">�� Visual Theme</h3>';
58759ccd446eSAtari911
58769ccd446eSAtari911        // Matrix Theme
58779ccd446eSAtari911        echo '<div style="border:2px solid ' . ($currentTheme === 'matrix' ? '#00cc07' : $colors['border']) . '; border-radius:6px; padding:20px; margin-bottom:20px; background:' . ($currentTheme === 'matrix' ? 'rgba(0, 204, 7, 0.05)' : $colors['bg']) . ';">';
58789ccd446eSAtari911        echo '<label style="display:flex; align-items:center; cursor:pointer;">';
58799ccd446eSAtari911        echo '<input type="radio" name="theme" value="matrix" ' . ($currentTheme === 'matrix' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">';
58809ccd446eSAtari911        echo '<div style="flex:1;">';
58819ccd446eSAtari911        echo '<div style="font-size:18px; font-weight:bold; color:#00cc07; margin-bottom:8px;">�� Matrix Edition</div>';
58829ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Dark green theme with Matrix-style glow effects and neon accents</div>';
58839ccd446eSAtari911        echo '<div style="display:inline-block; background:#242424; border:2px solid #00cc07; padding:8px 12px; border-radius:4px; font-size:11px; font-family:monospace; color:#00cc07; box-shadow:0 0 10px rgba(0, 204, 7, 0.3);">Preview: Matrix Theme</div>';
58849ccd446eSAtari911        echo '</div>';
58859ccd446eSAtari911        echo '</label>';
58869ccd446eSAtari911        echo '</div>';
58879ccd446eSAtari911
58889ccd446eSAtari911        // Purple Theme
58899ccd446eSAtari911        echo '<div style="border:2px solid ' . ($currentTheme === 'purple' ? '#9b59b6' : $colors['border']) . '; border-radius:6px; padding:20px; margin-bottom:20px; background:' . ($currentTheme === 'purple' ? 'rgba(155, 89, 182, 0.05)' : $colors['bg']) . ';">';
58909ccd446eSAtari911        echo '<label style="display:flex; align-items:center; cursor:pointer;">';
58919ccd446eSAtari911        echo '<input type="radio" name="theme" value="purple" ' . ($currentTheme === 'purple' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">';
58929ccd446eSAtari911        echo '<div style="flex:1;">';
58939ccd446eSAtari911        echo '<div style="font-size:18px; font-weight:bold; color:#9b59b6; margin-bottom:8px;">�� Purple Dream</div>';
58949ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Rich purple theme with elegant violet accents and soft glow</div>';
58959ccd446eSAtari911        echo '<div style="display:inline-block; background:#2a2030; border:2px solid #9b59b6; padding:8px 12px; border-radius:4px; font-size:11px; font-family:monospace; color:#b19cd9; box-shadow:0 0 10px rgba(155, 89, 182, 0.3);">Preview: Purple Theme</div>';
58969ccd446eSAtari911        echo '</div>';
58979ccd446eSAtari911        echo '</label>';
58989ccd446eSAtari911        echo '</div>';
58999ccd446eSAtari911
59009ccd446eSAtari911        // Professional Blue Theme
59019ccd446eSAtari911        echo '<div style="border:2px solid ' . ($currentTheme === 'professional' ? '#4a90e2' : $colors['border']) . '; border-radius:6px; padding:20px; margin-bottom:20px; background:' . ($currentTheme === 'professional' ? 'rgba(74, 144, 226, 0.05)' : $colors['bg']) . ';">';
59029ccd446eSAtari911        echo '<label style="display:flex; align-items:center; cursor:pointer;">';
59039ccd446eSAtari911        echo '<input type="radio" name="theme" value="professional" ' . ($currentTheme === 'professional' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">';
59049ccd446eSAtari911        echo '<div style="flex:1;">';
59059ccd446eSAtari911        echo '<div style="font-size:18px; font-weight:bold; color:#4a90e2; margin-bottom:8px;">�� Professional Blue</div>';
59069ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Clean blue and grey theme with modern professional styling, no glow effects</div>';
59079ccd446eSAtari911        echo '<div style="display:inline-block; background:#f5f7fa; border:2px solid #4a90e2; padding:8px 12px; border-radius:4px; font-size:11px; font-family:sans-serif; color:#2c3e50; box-shadow:0 2px 4px rgba(0, 0, 0, 0.1);">Preview: Professional Theme</div>';
59089ccd446eSAtari911        echo '</div>';
59099ccd446eSAtari911        echo '</label>';
59109ccd446eSAtari911        echo '</div>';
59119ccd446eSAtari911
59129ccd446eSAtari911        // Pink Bling Theme
59139ccd446eSAtari911        echo '<div style="border:2px solid ' . ($currentTheme === 'pink' ? '#ff1493' : $colors['border']) . '; border-radius:6px; padding:20px; margin-bottom:20px; background:' . ($currentTheme === 'pink' ? 'rgba(255, 20, 147, 0.05)' : $colors['bg']) . ';">';
59149ccd446eSAtari911        echo '<label style="display:flex; align-items:center; cursor:pointer;">';
59159ccd446eSAtari911        echo '<input type="radio" name="theme" value="pink" ' . ($currentTheme === 'pink' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">';
59169ccd446eSAtari911        echo '<div style="flex:1;">';
59179ccd446eSAtari911        echo '<div style="font-size:18px; font-weight:bold; color:#ff1493; margin-bottom:8px;">�� Pink Bling</div>';
59189ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Glamorous hot pink theme with maximum sparkle, hearts, and diamonds ✨</div>';
59199ccd446eSAtari911        echo '<div style="display:inline-block; background:#1a0d14; border:2px solid #ff1493; padding:8px 12px; border-radius:4px; font-size:11px; font-family:monospace; color:#ff69b4; box-shadow:0 0 12px rgba(255, 20, 147, 0.6);">Preview: Pink Bling Theme ��</div>';
59209ccd446eSAtari911        echo '</div>';
59219ccd446eSAtari911        echo '</label>';
59229ccd446eSAtari911        echo '</div>';
59239ccd446eSAtari911
59249ccd446eSAtari911        // Wiki Default Theme
59259ccd446eSAtari911        echo '<div style="border:2px solid ' . ($currentTheme === 'wiki' ? '#2b73b7' : $colors['border']) . '; border-radius:6px; padding:20px; margin-bottom:20px; background:' . ($currentTheme === 'wiki' ? 'rgba(43, 115, 183, 0.05)' : $colors['bg']) . ';">';
59269ccd446eSAtari911        echo '<label style="display:flex; align-items:center; cursor:pointer;">';
59279ccd446eSAtari911        echo '<input type="radio" name="theme" value="wiki" ' . ($currentTheme === 'wiki' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">';
59289ccd446eSAtari911        echo '<div style="flex:1;">';
59299ccd446eSAtari911        echo '<div style="font-size:18px; font-weight:bold; color:#2b73b7; margin-bottom:8px;">�� Wiki Default</div>';
59309ccd446eSAtari911        echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Automatically matches your DokuWiki template theme using CSS variables - adapts to light and dark themes</div>';
59319ccd446eSAtari911        echo '<div style="display:inline-block; background:#f5f5f5; border:2px solid #ccc; padding:8px 12px; border-radius:4px; font-size:11px; font-family:sans-serif; color:' . $colors['text'] . '; box-shadow:0 1px 2px rgba(0, 0, 0, 0.1);">Preview: Matches Your Wiki Theme</div>';
59329ccd446eSAtari911        echo '</div>';
59339ccd446eSAtari911        echo '</label>';
59349ccd446eSAtari911        echo '</div>';
59359ccd446eSAtari911
59369ccd446eSAtari911        echo '<button type="submit" style="background:#00cc07; color:#fff; border:none; padding:12px 24px; border-radius:4px; font-size:14px; font-weight:bold; cursor:pointer; box-shadow:0 2px 4px rgba(0,0,0,0.2);">Save Settings</button>';
59379ccd446eSAtari911        echo '</form>';
59389ccd446eSAtari911    }
59399ccd446eSAtari911
59409ccd446eSAtari911    /**
59419ccd446eSAtari911     * Get current sidebar theme
59429ccd446eSAtari911     */
59439ccd446eSAtari911    private function getSidebarTheme() {
59449ccd446eSAtari911        $configFile = DOKU_INC . 'data/meta/calendar_theme.txt';
59459ccd446eSAtari911        if (file_exists($configFile)) {
59469ccd446eSAtari911            return trim(file_get_contents($configFile));
59479ccd446eSAtari911        }
59489ccd446eSAtari911        return 'matrix'; // Default
59499ccd446eSAtari911    }
59509ccd446eSAtari911
59519ccd446eSAtari911    /**
59529ccd446eSAtari911     * Save sidebar theme
59539ccd446eSAtari911     */
59549ccd446eSAtari911    private function saveSidebarTheme($theme) {
59559ccd446eSAtari911        $configFile = DOKU_INC . 'data/meta/calendar_theme.txt';
59569ccd446eSAtari911        $validThemes = ['matrix', 'purple', 'professional', 'pink', 'wiki'];
59579ccd446eSAtari911
59589ccd446eSAtari911        if (in_array($theme, $validThemes)) {
59599ccd446eSAtari911            file_put_contents($configFile, $theme);
59609ccd446eSAtari911            return true;
59619ccd446eSAtari911        }
59629ccd446eSAtari911        return false;
59639ccd446eSAtari911    }
59649ccd446eSAtari911
59659ccd446eSAtari911    /**
59669ccd446eSAtari911     * Get week start day
59679ccd446eSAtari911     */
59689ccd446eSAtari911    private function getWeekStartDay() {
59699ccd446eSAtari911        $configFile = DOKU_INC . 'data/meta/calendar_week_start.txt';
59709ccd446eSAtari911        if (file_exists($configFile)) {
59719ccd446eSAtari911            $start = trim(file_get_contents($configFile));
59729ccd446eSAtari911            if (in_array($start, ['monday', 'sunday'])) {
59739ccd446eSAtari911                return $start;
59749ccd446eSAtari911            }
59759ccd446eSAtari911        }
59769ccd446eSAtari911        return 'sunday'; // Default to Sunday (US/Canada standard)
59779ccd446eSAtari911    }
59789ccd446eSAtari911
59799ccd446eSAtari911    /**
59809ccd446eSAtari911     * Save week start day
59819ccd446eSAtari911     */
59829ccd446eSAtari911    private function saveWeekStartDay($weekStart) {
59839ccd446eSAtari911        $configFile = DOKU_INC . 'data/meta/calendar_week_start.txt';
59849ccd446eSAtari911        $validStarts = ['monday', 'sunday'];
59859ccd446eSAtari911
59869ccd446eSAtari911        if (in_array($weekStart, $validStarts)) {
59879ccd446eSAtari911            file_put_contents($configFile, $weekStart);
59889ccd446eSAtari911            return true;
59899ccd446eSAtari911        }
59909ccd446eSAtari911        return false;
59919ccd446eSAtari911    }
59929ccd446eSAtari911
59939ccd446eSAtari911    /**
59949ccd446eSAtari911     * Get colors from DokuWiki template's style.ini file
59959ccd446eSAtari911     */
59969ccd446eSAtari911    private function getTemplateColors() {
59979ccd446eSAtari911        global $conf;
59989ccd446eSAtari911
59999ccd446eSAtari911        // Get current template name
60009ccd446eSAtari911        $template = $conf['template'];
60019ccd446eSAtari911
60029ccd446eSAtari911        // Try multiple possible locations for style.ini
60039ccd446eSAtari911        $possiblePaths = [
60049ccd446eSAtari911            DOKU_INC . 'conf/tpl/' . $template . '/style.ini',
60059ccd446eSAtari911            DOKU_INC . 'lib/tpl/' . $template . '/style.ini',
60069ccd446eSAtari911        ];
60079ccd446eSAtari911
60089ccd446eSAtari911        $styleIni = null;
60099ccd446eSAtari911        foreach ($possiblePaths as $path) {
60109ccd446eSAtari911            if (file_exists($path)) {
60119ccd446eSAtari911                $styleIni = parse_ini_file($path, true);
60129ccd446eSAtari911                break;
60139ccd446eSAtari911            }
60149ccd446eSAtari911        }
60159ccd446eSAtari911
60169ccd446eSAtari911        if (!$styleIni || !isset($styleIni['replacements'])) {
60179ccd446eSAtari911            // Return defaults
60189ccd446eSAtari911            return [
60199ccd446eSAtari911                'bg' => '#fff',
60209ccd446eSAtari911                'bg_alt' => '#e8e8e8',
60219ccd446eSAtari911                'text' => '#333',
60229ccd446eSAtari911                'border' => '#ccc',
60239ccd446eSAtari911                'link' => '#2b73b7',
60249ccd446eSAtari911            ];
60259ccd446eSAtari911        }
60269ccd446eSAtari911
60279ccd446eSAtari911        $r = $styleIni['replacements'];
60289ccd446eSAtari911
60299ccd446eSAtari911        return [
60309ccd446eSAtari911            'bg' => isset($r['__background__']) ? $r['__background__'] : '#fff',
60319ccd446eSAtari911            'bg_alt' => isset($r['__background_alt__']) ? $r['__background_alt__'] : '#e8e8e8',
60329ccd446eSAtari911            'text' => isset($r['__text__']) ? $r['__text__'] : '#333',
60339ccd446eSAtari911            'border' => isset($r['__border__']) ? $r['__border__'] : '#ccc',
60349ccd446eSAtari911            'link' => isset($r['__link__']) ? $r['__link__'] : '#2b73b7',
60359ccd446eSAtari911        ];
60369ccd446eSAtari911    }
60371d05cddcSAtari911}
6038