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 241d05cddcSAtari911 public function handle() { 251d05cddcSAtari911 global $INPUT; 261d05cddcSAtari911 271d05cddcSAtari911 $action = $INPUT->str('action'); 281d05cddcSAtari911 291d05cddcSAtari911 if ($action === 'clear_cache') { 301d05cddcSAtari911 $this->clearCache(); 311d05cddcSAtari911 } elseif ($action === 'save_config') { 321d05cddcSAtari911 $this->saveConfig(); 331d05cddcSAtari911 } elseif ($action === 'delete_recurring_series') { 341d05cddcSAtari911 $this->deleteRecurringSeries(); 351d05cddcSAtari911 } elseif ($action === 'edit_recurring_series') { 361d05cddcSAtari911 $this->editRecurringSeries(); 371d05cddcSAtari911 } elseif ($action === 'move_selected_events') { 381d05cddcSAtari911 $this->moveEvents(); 391d05cddcSAtari911 } elseif ($action === 'move_single_event') { 401d05cddcSAtari911 $this->moveSingleEvent(); 411d05cddcSAtari911 } elseif ($action === 'delete_selected_events') { 421d05cddcSAtari911 $this->deleteSelectedEvents(); 431d05cddcSAtari911 } elseif ($action === 'create_namespace') { 441d05cddcSAtari911 $this->createNamespace(); 451d05cddcSAtari911 } elseif ($action === 'delete_namespace') { 461d05cddcSAtari911 $this->deleteNamespace(); 47*9ccd446eSAtari911 } elseif ($action === 'rename_namespace') { 48*9ccd446eSAtari911 $this->renameNamespace(); 491d05cddcSAtari911 } elseif ($action === 'run_sync') { 501d05cddcSAtari911 $this->runSync(); 511d05cddcSAtari911 } elseif ($action === 'stop_sync') { 521d05cddcSAtari911 $this->stopSync(); 531d05cddcSAtari911 } elseif ($action === 'upload_update') { 541d05cddcSAtari911 $this->uploadUpdate(); 551d05cddcSAtari911 } elseif ($action === 'delete_backup') { 561d05cddcSAtari911 $this->deleteBackup(); 571d05cddcSAtari911 } elseif ($action === 'rename_backup') { 581d05cddcSAtari911 $this->renameBackup(); 591d05cddcSAtari911 } elseif ($action === 'restore_backup') { 601d05cddcSAtari911 $this->restoreBackup(); 61*9ccd446eSAtari911 } elseif ($action === 'create_manual_backup') { 62*9ccd446eSAtari911 $this->createManualBackup(); 631d05cddcSAtari911 } elseif ($action === 'export_config') { 641d05cddcSAtari911 $this->exportConfig(); 651d05cddcSAtari911 } elseif ($action === 'import_config') { 661d05cddcSAtari911 $this->importConfig(); 671d05cddcSAtari911 } elseif ($action === 'get_log') { 681d05cddcSAtari911 $this->getLog(); 691d05cddcSAtari911 } elseif ($action === 'clear_log') { 701d05cddcSAtari911 $this->clearLogFile(); 711d05cddcSAtari911 } elseif ($action === 'download_log') { 721d05cddcSAtari911 $this->downloadLog(); 731d05cddcSAtari911 } elseif ($action === 'rescan_events') { 741d05cddcSAtari911 $this->rescanEvents(); 751d05cddcSAtari911 } elseif ($action === 'export_all_events') { 761d05cddcSAtari911 $this->exportAllEvents(); 771d05cddcSAtari911 } elseif ($action === 'import_all_events') { 781d05cddcSAtari911 $this->importAllEvents(); 791d05cddcSAtari911 } elseif ($action === 'preview_cleanup') { 801d05cddcSAtari911 $this->previewCleanup(); 811d05cddcSAtari911 } elseif ($action === 'cleanup_events') { 821d05cddcSAtari911 $this->cleanupEvents(); 831d05cddcSAtari911 } 841d05cddcSAtari911 } 851d05cddcSAtari911 861d05cddcSAtari911 public function html() { 871d05cddcSAtari911 global $INPUT; 881d05cddcSAtari911 89*9ccd446eSAtari911 // Get current tab - default to 'manage' (Manage Events tab) 90*9ccd446eSAtari911 $tab = $INPUT->str('tab', 'manage'); 911d05cddcSAtari911 92*9ccd446eSAtari911 // Get template colors 93*9ccd446eSAtari911 $colors = $this->getTemplateColors(); 94*9ccd446eSAtari911 $accentColor = '#00cc07'; // Keep calendar plugin accent color 95*9ccd446eSAtari911 96*9ccd446eSAtari911 // Tab navigation (Manage Events, Update Plugin, Outlook Sync, Themes) 97*9ccd446eSAtari911 echo '<div style="border-bottom:2px solid ' . $colors['border'] . '; margin:10px 0 15px 0;">'; 98*9ccd446eSAtari911 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>'; 99*9ccd446eSAtari911 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>'; 100*9ccd446eSAtari911 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>'; 101*9ccd446eSAtari911 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>'; 1021d05cddcSAtari911 echo '</div>'; 1031d05cddcSAtari911 1041d05cddcSAtari911 // Render appropriate tab 1051d05cddcSAtari911 if ($tab === 'config') { 106*9ccd446eSAtari911 $this->renderConfigTab($colors); 1071d05cddcSAtari911 } elseif ($tab === 'manage') { 108*9ccd446eSAtari911 $this->renderManageTab($colors); 109*9ccd446eSAtari911 } elseif ($tab === 'themes') { 110*9ccd446eSAtari911 $this->renderThemesTab($colors); 1111d05cddcSAtari911 } else { 112*9ccd446eSAtari911 $this->renderUpdateTab($colors); 1131d05cddcSAtari911 } 1141d05cddcSAtari911 } 1151d05cddcSAtari911 116*9ccd446eSAtari911 private function renderConfigTab($colors = null) { 1171d05cddcSAtari911 global $INPUT; 1181d05cddcSAtari911 119*9ccd446eSAtari911 // Use defaults if not provided 120*9ccd446eSAtari911 if ($colors === null) { 121*9ccd446eSAtari911 $colors = $this->getTemplateColors(); 122*9ccd446eSAtari911 } 123*9ccd446eSAtari911 1241d05cddcSAtari911 // Load current config 1251d05cddcSAtari911 $configFile = DOKU_PLUGIN . 'calendar/sync_config.php'; 1261d05cddcSAtari911 $config = []; 1271d05cddcSAtari911 if (file_exists($configFile)) { 1281d05cddcSAtari911 $config = include $configFile; 1291d05cddcSAtari911 } 1301d05cddcSAtari911 1311d05cddcSAtari911 // Show message if present 1321d05cddcSAtari911 if ($INPUT->has('msg')) { 1331d05cddcSAtari911 $msg = hsc($INPUT->str('msg')); 1341d05cddcSAtari911 $type = $INPUT->str('msgtype', 'success'); 1351d05cddcSAtari911 $class = ($type === 'success') ? 'msg success' : 'msg error'; 1361d05cddcSAtari911 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;\">"; 1371d05cddcSAtari911 echo $msg; 1381d05cddcSAtari911 echo "</div>"; 1391d05cddcSAtari911 } 1401d05cddcSAtari911 1411d05cddcSAtari911 echo '<h2 style="margin:10px 0; font-size:20px;">Outlook Sync Configuration</h2>'; 1421d05cddcSAtari911 1431d05cddcSAtari911 // Import/Export buttons 1441d05cddcSAtari911 echo '<div style="display:flex; gap:10px; margin-bottom:15px;">'; 1451d05cddcSAtari911 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>'; 1461d05cddcSAtari911 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>'; 1471d05cddcSAtari911 echo '<input type="file" id="importFileInput" accept=".enc" style="display:none;" onchange="importConfig(this)">'; 1481d05cddcSAtari911 echo '<span id="importStatus" style="margin-left:10px; font-size:12px;"></span>'; 1491d05cddcSAtari911 echo '</div>'; 1501d05cddcSAtari911 1511d05cddcSAtari911 echo '<form method="post" action="?do=admin&page=calendar" style="max-width:900px;">'; 1521d05cddcSAtari911 echo '<input type="hidden" name="action" value="save_config">'; 1531d05cddcSAtari911 1541d05cddcSAtari911 // Azure Credentials 155*9ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">'; 1561d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Microsoft Azure App Credentials</h3>'; 157*9ccd446eSAtari911 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>'; 1581d05cddcSAtari911 1591d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Tenant ID</label>'; 160*9ccd446eSAtari911 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;">'; 1611d05cddcSAtari911 1621d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Client ID (Application ID)</label>'; 163*9ccd446eSAtari911 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;">'; 1641d05cddcSAtari911 1651d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Client Secret</label>'; 166*9ccd446eSAtari911 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;">'; 1671d05cddcSAtari911 echo '<p style="color:#999; font-size:0.8em; margin:3px 0 0;">⚠️ Keep this secret safe!</p>'; 1681d05cddcSAtari911 echo '</div>'; 1691d05cddcSAtari911 1701d05cddcSAtari911 // Outlook Settings 171*9ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">'; 1721d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Outlook Settings</h3>'; 1731d05cddcSAtari911 1741d05cddcSAtari911 echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">'; 1751d05cddcSAtari911 1761d05cddcSAtari911 echo '<div>'; 1771d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">User Email</label>'; 178*9ccd446eSAtari911 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;">'; 1791d05cddcSAtari911 echo '</div>'; 1801d05cddcSAtari911 1811d05cddcSAtari911 echo '<div>'; 1821d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Timezone</label>'; 183*9ccd446eSAtari911 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;">'; 1841d05cddcSAtari911 echo '</div>'; 1851d05cddcSAtari911 1861d05cddcSAtari911 echo '<div>'; 1871d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Default Category</label>'; 188*9ccd446eSAtari911 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;">'; 1891d05cddcSAtari911 echo '</div>'; 1901d05cddcSAtari911 1911d05cddcSAtari911 echo '<div>'; 1921d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Reminder (minutes)</label>'; 193*9ccd446eSAtari911 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;">'; 1941d05cddcSAtari911 echo '</div>'; 1951d05cddcSAtari911 1961d05cddcSAtari911 echo '</div>'; // end grid 1971d05cddcSAtari911 echo '</div>'; 1981d05cddcSAtari911 1991d05cddcSAtari911 // Important Namespaces for Sidebar Widget 200*9ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #9b59b6; border-radius:3px;">'; 2011d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#9b59b6; font-size:16px;"> Important Namespaces (Sidebar Widget)</h3>'; 202*9ccd446eSAtari911 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</p>'; 203*9ccd446eSAtari911 echo '<input type="text" name="important_namespaces" value="' . hsc(isset($config['important_namespaces']) ? $config['important_namespaces'] : 'important') . '" style="width:100%; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;" placeholder="important,urgent,priority">'; 204*9ccd446eSAtari911 echo '<p style="color:' . $colors['text'] . '; font-size:10px; margin:4px 0 0;">Comma-separated list of namespace names</p>'; 2051d05cddcSAtari911 echo '</div>'; 2061d05cddcSAtari911 2071d05cddcSAtari911 // Sync Options 208*9ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">'; 2091d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Sync Options</h3>'; 2101d05cddcSAtari911 2111d05cddcSAtari911 $syncCompleted = isset($config['sync_completed_tasks']) ? $config['sync_completed_tasks'] : false; 2121d05cddcSAtari911 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>'; 2131d05cddcSAtari911 2141d05cddcSAtari911 $deleteOutlook = isset($config['delete_outlook_events']) ? $config['delete_outlook_events'] : true; 2151d05cddcSAtari911 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>'; 2161d05cddcSAtari911 2171d05cddcSAtari911 $syncAll = isset($config['sync_all_namespaces']) ? $config['sync_all_namespaces'] : true; 2181d05cddcSAtari911 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>'; 2191d05cddcSAtari911 2201d05cddcSAtari911 // Namespace selection (shown when sync_all is unchecked) 2211d05cddcSAtari911 echo '<div id="namespace_selection" style="margin-top:10px; ' . ($syncAll ? 'display:none;' : '') . '">'; 2221d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin-bottom:5px; font-size:13px;">Select namespaces to sync:</label>'; 2231d05cddcSAtari911 2241d05cddcSAtari911 // Get available namespaces 2251d05cddcSAtari911 $availableNamespaces = $this->getAllNamespaces(); 2261d05cddcSAtari911 $selectedNamespaces = isset($config['sync_namespaces']) ? $config['sync_namespaces'] : []; 2271d05cddcSAtari911 228*9ccd446eSAtari911 echo '<div style="max-height:150px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px; padding:8px; background:' . $colors['bg'] . ';">'; 2291d05cddcSAtari911 echo '<label style="display:block; margin:3px 0;"><input type="checkbox" name="sync_namespaces[]" value=""> (default)</label>'; 2301d05cddcSAtari911 foreach ($availableNamespaces as $ns) { 2311d05cddcSAtari911 if ($ns !== '') { 2321d05cddcSAtari911 $checked = in_array($ns, $selectedNamespaces) ? 'checked' : ''; 2331d05cddcSAtari911 echo '<label style="display:block; margin:3px 0;"><input type="checkbox" name="sync_namespaces[]" value="' . hsc($ns) . '" ' . $checked . '> ' . hsc($ns) . '</label>'; 2341d05cddcSAtari911 } 2351d05cddcSAtari911 } 2361d05cddcSAtari911 echo '</div>'; 2371d05cddcSAtari911 echo '</div>'; 2381d05cddcSAtari911 2391d05cddcSAtari911 echo '<script> 2401d05cddcSAtari911 function toggleNamespaceSelection(checkbox) { 2411d05cddcSAtari911 document.getElementById("namespace_selection").style.display = checkbox.checked ? "none" : "block"; 2421d05cddcSAtari911 } 2431d05cddcSAtari911 </script>'; 2441d05cddcSAtari911 2451d05cddcSAtari911 echo '</div>'; 2461d05cddcSAtari911 2471d05cddcSAtari911 // Namespace and Color Mapping - Side by Side 2481d05cddcSAtari911 echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px; margin:10px 0;">'; 2491d05cddcSAtari911 2501d05cddcSAtari911 // Namespace Mapping 251*9ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; border-left:3px solid #00cc07; border-radius:3px;">'; 2521d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Namespace → Category</h3>'; 253*9ccd446eSAtari911 echo '<p style="color:' . $colors['text'] . '; font-size:0.8em; margin:0 0 5px;">One per line: namespace=Category</p>'; 254*9ccd446eSAtari911 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 personal=Green category">'; 2551d05cddcSAtari911 if (isset($config['category_mapping']) && is_array($config['category_mapping'])) { 2561d05cddcSAtari911 foreach ($config['category_mapping'] as $ns => $cat) { 2571d05cddcSAtari911 echo hsc($ns) . '=' . hsc($cat) . "\n"; 2581d05cddcSAtari911 } 2591d05cddcSAtari911 } 2601d05cddcSAtari911 echo '</textarea>'; 2611d05cddcSAtari911 echo '</div>'; 2621d05cddcSAtari911 2631d05cddcSAtari911 // Color Mapping with Color Picker 264*9ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; border-left:3px solid #00cc07; border-radius:3px;">'; 2651d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;"> Event Color → Category</h3>'; 266*9ccd446eSAtari911 echo '<p style="color:' . $colors['text'] . '; font-size:0.8em; margin:0 0 8px;">Map calendar colors to Outlook categories</p>'; 2671d05cddcSAtari911 2681d05cddcSAtari911 // Define calendar colors and Outlook categories (only the main 6 colors) 2691d05cddcSAtari911 $calendarColors = [ 2701d05cddcSAtari911 '#3498db' => 'Blue', 2711d05cddcSAtari911 '#2ecc71' => 'Green', 2721d05cddcSAtari911 '#e74c3c' => 'Red', 2731d05cddcSAtari911 '#f39c12' => 'Orange', 2741d05cddcSAtari911 '#9b59b6' => 'Purple', 2751d05cddcSAtari911 '#1abc9c' => 'Teal' 2761d05cddcSAtari911 ]; 2771d05cddcSAtari911 2781d05cddcSAtari911 $outlookCategories = [ 2791d05cddcSAtari911 'Blue category', 2801d05cddcSAtari911 'Green category', 2811d05cddcSAtari911 'Orange category', 2821d05cddcSAtari911 'Red category', 2831d05cddcSAtari911 'Yellow category', 2841d05cddcSAtari911 'Purple category' 2851d05cddcSAtari911 ]; 2861d05cddcSAtari911 2871d05cddcSAtari911 // Load existing color mappings 2881d05cddcSAtari911 $existingMappings = isset($config['color_mapping']) && is_array($config['color_mapping']) 2891d05cddcSAtari911 ? $config['color_mapping'] 2901d05cddcSAtari911 : []; 2911d05cddcSAtari911 2921d05cddcSAtari911 // Display color mapping rows 2931d05cddcSAtari911 echo '<div id="colorMappings" style="max-height:200px; overflow-y:auto;">'; 2941d05cddcSAtari911 2951d05cddcSAtari911 $rowIndex = 0; 2961d05cddcSAtari911 foreach ($calendarColors as $hexColor => $colorName) { 2971d05cddcSAtari911 $selectedCategory = isset($existingMappings[$hexColor]) ? $existingMappings[$hexColor] : ''; 2981d05cddcSAtari911 2991d05cddcSAtari911 echo '<div style="display:flex; gap:8px; align-items:center; margin-bottom:6px;">'; 3001d05cddcSAtari911 3011d05cddcSAtari911 // Color preview box 3021d05cddcSAtari911 echo '<div style="width:24px; height:24px; background:' . $hexColor . '; border:2px solid #ddd; border-radius:3px; flex-shrink:0;"></div>'; 3031d05cddcSAtari911 3041d05cddcSAtari911 // Color name 305*9ccd446eSAtari911 echo '<span style="font-size:12px; min-width:90px; color:' . $colors['text'] . ';">' . $colorName . '</span>'; 3061d05cddcSAtari911 3071d05cddcSAtari911 // Arrow 3081d05cddcSAtari911 echo '<span style="color:#999; font-size:12px;">→</span>'; 3091d05cddcSAtari911 3101d05cddcSAtari911 // Outlook category dropdown 311*9ccd446eSAtari911 echo '<select name="color_map_' . $rowIndex . '" style="flex:1; padding:4px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;">'; 3121d05cddcSAtari911 echo '<option value="">-- None --</option>'; 3131d05cddcSAtari911 foreach ($outlookCategories as $category) { 3141d05cddcSAtari911 $selected = ($selectedCategory === $category) ? 'selected' : ''; 3151d05cddcSAtari911 echo '<option value="' . hsc($category) . '" ' . $selected . '>' . hsc($category) . '</option>'; 3161d05cddcSAtari911 } 3171d05cddcSAtari911 echo '</select>'; 3181d05cddcSAtari911 3191d05cddcSAtari911 // Hidden input for the hex color 3201d05cddcSAtari911 echo '<input type="hidden" name="color_hex_' . $rowIndex . '" value="' . $hexColor . '">'; 3211d05cddcSAtari911 3221d05cddcSAtari911 echo '</div>'; 3231d05cddcSAtari911 $rowIndex++; 3241d05cddcSAtari911 } 3251d05cddcSAtari911 3261d05cddcSAtari911 echo '</div>'; 3271d05cddcSAtari911 3281d05cddcSAtari911 // Hidden input to track number of color mappings 3291d05cddcSAtari911 echo '<input type="hidden" name="color_mapping_count" value="' . $rowIndex . '">'; 3301d05cddcSAtari911 3311d05cddcSAtari911 echo '</div>'; 3321d05cddcSAtari911 3331d05cddcSAtari911 echo '</div>'; // end grid 3341d05cddcSAtari911 3351d05cddcSAtari911 // Submit button 3361d05cddcSAtari911 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>'; 3371d05cddcSAtari911 echo '</form>'; 3381d05cddcSAtari911 3391d05cddcSAtari911 // JavaScript for Import/Export 3401d05cddcSAtari911 echo '<script> 3411d05cddcSAtari911 async function exportConfig() { 3421d05cddcSAtari911 try { 3431d05cddcSAtari911 const response = await fetch("?do=admin&page=calendar&action=export_config&call=ajax", { 3441d05cddcSAtari911 method: "POST" 3451d05cddcSAtari911 }); 3461d05cddcSAtari911 const data = await response.json(); 3471d05cddcSAtari911 3481d05cddcSAtari911 if (data.success) { 3491d05cddcSAtari911 // Create download link 3501d05cddcSAtari911 const blob = new Blob([data.encrypted], {type: "application/octet-stream"}); 3511d05cddcSAtari911 const url = URL.createObjectURL(blob); 3521d05cddcSAtari911 const a = document.createElement("a"); 3531d05cddcSAtari911 a.href = url; 3541d05cddcSAtari911 a.download = "sync_config_" + new Date().toISOString().split("T")[0] + ".enc"; 3551d05cddcSAtari911 document.body.appendChild(a); 3561d05cddcSAtari911 a.click(); 3571d05cddcSAtari911 document.body.removeChild(a); 3581d05cddcSAtari911 URL.revokeObjectURL(url); 3591d05cddcSAtari911 3601d05cddcSAtari911 alert("✅ Config exported successfully!\\n\\n⚠️ This file contains encrypted credentials.\\nKeep it secure!"); 3611d05cddcSAtari911 } else { 3621d05cddcSAtari911 alert("❌ Export failed: " + data.message); 3631d05cddcSAtari911 } 3641d05cddcSAtari911 } catch (error) { 3651d05cddcSAtari911 alert("❌ Error: " + error.message); 3661d05cddcSAtari911 } 3671d05cddcSAtari911 } 3681d05cddcSAtari911 3691d05cddcSAtari911 async function importConfig(input) { 3701d05cddcSAtari911 const file = input.files[0]; 3711d05cddcSAtari911 if (!file) return; 3721d05cddcSAtari911 3731d05cddcSAtari911 const status = document.getElementById("importStatus"); 3741d05cddcSAtari911 status.textContent = "⏳ Importing..."; 3751d05cddcSAtari911 status.style.color = "#00cc07"; 3761d05cddcSAtari911 3771d05cddcSAtari911 try { 3781d05cddcSAtari911 const encrypted = await file.text(); 3791d05cddcSAtari911 3801d05cddcSAtari911 const formData = new FormData(); 3811d05cddcSAtari911 formData.append("encrypted_config", encrypted); 3821d05cddcSAtari911 3831d05cddcSAtari911 const response = await fetch("?do=admin&page=calendar&action=import_config&call=ajax", { 3841d05cddcSAtari911 method: "POST", 3851d05cddcSAtari911 body: formData 3861d05cddcSAtari911 }); 3871d05cddcSAtari911 const data = await response.json(); 3881d05cddcSAtari911 3891d05cddcSAtari911 if (data.success) { 3901d05cddcSAtari911 status.textContent = "✅ Import successful! Reloading..."; 3911d05cddcSAtari911 status.style.color = "#28a745"; 3921d05cddcSAtari911 setTimeout(() => { 3931d05cddcSAtari911 window.location.reload(); 3941d05cddcSAtari911 }, 1500); 3951d05cddcSAtari911 } else { 3961d05cddcSAtari911 status.textContent = "❌ Import failed: " + data.message; 3971d05cddcSAtari911 status.style.color = "#dc3545"; 3981d05cddcSAtari911 } 3991d05cddcSAtari911 } catch (error) { 4001d05cddcSAtari911 status.textContent = "❌ Error: " + error.message; 4011d05cddcSAtari911 status.style.color = "#dc3545"; 4021d05cddcSAtari911 } 4031d05cddcSAtari911 4041d05cddcSAtari911 // Reset file input 4051d05cddcSAtari911 input.value = ""; 4061d05cddcSAtari911 } 4071d05cddcSAtari911 </script>'; 4081d05cddcSAtari911 4091d05cddcSAtari911 // Sync Controls Section 410*9ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:15px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">'; 4111d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;"> Sync Controls</h3>'; 4121d05cddcSAtari911 4131d05cddcSAtari911 // Check cron job status 4141d05cddcSAtari911 $cronStatus = $this->getCronStatus(); 4151d05cddcSAtari911 4161d05cddcSAtari911 // Check log file permissions 4171d05cddcSAtari911 $logFile = DOKU_PLUGIN . 'calendar/sync.log'; 4181d05cddcSAtari911 $logWritable = is_writable($logFile) || is_writable(dirname($logFile)); 4191d05cddcSAtari911 4201d05cddcSAtari911 echo '<div style="display:flex; gap:10px; align-items:center; margin-bottom:10px;">'; 4211d05cddcSAtari911 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>'; 4221d05cddcSAtari911 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>'; 4231d05cddcSAtari911 4241d05cddcSAtari911 if ($cronStatus['active']) { 425*9ccd446eSAtari911 echo '<span style="color:' . $colors['text'] . '; font-size:12px;">⏰ ' . hsc($cronStatus['frequency']) . '</span>'; 4261d05cddcSAtari911 } else { 4271d05cddcSAtari911 echo '<span style="color:#999; font-size:12px;">⚠️ No cron job detected</span>'; 4281d05cddcSAtari911 } 4291d05cddcSAtari911 430*9ccd446eSAtari911 echo '<span id="syncStatus" style="color:' . $colors['text'] . '; font-size:12px; margin-left:auto;"></span>'; 4311d05cddcSAtari911 echo '</div>'; 4321d05cddcSAtari911 4331d05cddcSAtari911 // Show permission warning if log not writable 4341d05cddcSAtari911 if (!$logWritable) { 4351d05cddcSAtari911 echo '<div style="background:#fff3e0; border-left:3px solid #ff9800; padding:8px; margin:8px 0; border-radius:3px;">'; 4361d05cddcSAtari911 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>'; 4371d05cddcSAtari911 echo '</div>'; 4381d05cddcSAtari911 } 4391d05cddcSAtari911 4401d05cddcSAtari911 // Show debug info if cron detected 4411d05cddcSAtari911 if ($cronStatus['active'] && !empty($cronStatus['full_line'])) { 4421d05cddcSAtari911 echo '<details style="margin-top:5px;">'; 4431d05cddcSAtari911 echo '<summary style="cursor:pointer; color:#999; font-size:11px;">Show cron details</summary>'; 4441d05cddcSAtari911 echo '<pre style="background:#f0f0f0; padding:8px; border-radius:3px; font-size:10px; margin:5px 0; overflow-x:auto;">' . hsc($cronStatus['full_line']) . '</pre>'; 4451d05cddcSAtari911 echo '</details>'; 4461d05cddcSAtari911 } 4471d05cddcSAtari911 4481d05cddcSAtari911 if (!$cronStatus['active']) { 4491d05cddcSAtari911 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>'; 4501d05cddcSAtari911 } 4511d05cddcSAtari911 4521d05cddcSAtari911 echo '</div>'; 4531d05cddcSAtari911 4541d05cddcSAtari911 // JavaScript for Run Sync Now 4551d05cddcSAtari911 echo '<script> 4561d05cddcSAtari911 let syncAbortController = null; 4571d05cddcSAtari911 4581d05cddcSAtari911 function runSyncNow() { 4591d05cddcSAtari911 const btn = document.getElementById("syncBtn"); 4601d05cddcSAtari911 const stopBtn = document.getElementById("stopBtn"); 4611d05cddcSAtari911 const status = document.getElementById("syncStatus"); 4621d05cddcSAtari911 4631d05cddcSAtari911 btn.disabled = true; 4641d05cddcSAtari911 btn.style.display = "none"; 4651d05cddcSAtari911 stopBtn.style.display = "inline-block"; 4661d05cddcSAtari911 btn.textContent = "⏳ Running..."; 4671d05cddcSAtari911 btn.style.background = "#999"; 4681d05cddcSAtari911 status.textContent = "Starting sync..."; 4691d05cddcSAtari911 status.style.color = "#00cc07"; 4701d05cddcSAtari911 4711d05cddcSAtari911 // Create abort controller for this sync 4721d05cddcSAtari911 syncAbortController = new AbortController(); 4731d05cddcSAtari911 4741d05cddcSAtari911 fetch("?do=admin&page=calendar&action=run_sync&call=ajax", { 4751d05cddcSAtari911 method: "POST", 4761d05cddcSAtari911 signal: syncAbortController.signal 4771d05cddcSAtari911 }) 4781d05cddcSAtari911 .then(response => response.json()) 4791d05cddcSAtari911 .then(data => { 4801d05cddcSAtari911 if (data.success) { 4811d05cddcSAtari911 status.textContent = "✅ " + data.message; 4821d05cddcSAtari911 status.style.color = "#28a745"; 4831d05cddcSAtari911 } else { 4841d05cddcSAtari911 status.textContent = "❌ " + data.message; 4851d05cddcSAtari911 status.style.color = "#dc3545"; 4861d05cddcSAtari911 } 4871d05cddcSAtari911 btn.disabled = false; 4881d05cddcSAtari911 btn.style.display = "inline-block"; 4891d05cddcSAtari911 stopBtn.style.display = "none"; 4901d05cddcSAtari911 btn.textContent = "▶️ Run Sync Now"; 4911d05cddcSAtari911 btn.style.background = "#00cc07"; 4921d05cddcSAtari911 syncAbortController = null; 4931d05cddcSAtari911 4941d05cddcSAtari911 // Clear status after 10 seconds 4951d05cddcSAtari911 setTimeout(() => { 4961d05cddcSAtari911 status.textContent = ""; 4971d05cddcSAtari911 }, 10000); 4981d05cddcSAtari911 }) 4991d05cddcSAtari911 .catch(error => { 5001d05cddcSAtari911 if (error.name === "AbortError") { 5011d05cddcSAtari911 status.textContent = "⏹️ Sync stopped by user"; 5021d05cddcSAtari911 status.style.color = "#ff9800"; 5031d05cddcSAtari911 } else { 5041d05cddcSAtari911 status.textContent = "❌ Error: " + error.message; 5051d05cddcSAtari911 status.style.color = "#dc3545"; 5061d05cddcSAtari911 } 5071d05cddcSAtari911 btn.disabled = false; 5081d05cddcSAtari911 btn.style.display = "inline-block"; 5091d05cddcSAtari911 stopBtn.style.display = "none"; 5101d05cddcSAtari911 btn.textContent = "▶️ Run Sync Now"; 5111d05cddcSAtari911 btn.style.background = "#00cc07"; 5121d05cddcSAtari911 syncAbortController = null; 5131d05cddcSAtari911 }); 5141d05cddcSAtari911 } 5151d05cddcSAtari911 5161d05cddcSAtari911 function stopSyncNow() { 5171d05cddcSAtari911 const status = document.getElementById("syncStatus"); 5181d05cddcSAtari911 5191d05cddcSAtari911 status.textContent = "⏹️ Sending stop signal..."; 5201d05cddcSAtari911 status.style.color = "#ff9800"; 5211d05cddcSAtari911 5221d05cddcSAtari911 // First, send stop signal to server 5231d05cddcSAtari911 fetch("?do=admin&page=calendar&action=stop_sync&call=ajax", { 5241d05cddcSAtari911 method: "POST" 5251d05cddcSAtari911 }) 5261d05cddcSAtari911 .then(response => response.json()) 5271d05cddcSAtari911 .then(data => { 5281d05cddcSAtari911 if (data.success) { 5291d05cddcSAtari911 status.textContent = "⏹️ Stop signal sent - sync will abort soon"; 5301d05cddcSAtari911 status.style.color = "#ff9800"; 5311d05cddcSAtari911 } else { 5321d05cddcSAtari911 status.textContent = "⚠️ " + data.message; 5331d05cddcSAtari911 status.style.color = "#ff9800"; 5341d05cddcSAtari911 } 5351d05cddcSAtari911 }) 5361d05cddcSAtari911 .catch(error => { 5371d05cddcSAtari911 status.textContent = "⚠️ Error sending stop signal: " + error.message; 5381d05cddcSAtari911 status.style.color = "#ff9800"; 5391d05cddcSAtari911 }); 5401d05cddcSAtari911 5411d05cddcSAtari911 // Also abort the fetch request 5421d05cddcSAtari911 if (syncAbortController) { 5431d05cddcSAtari911 syncAbortController.abort(); 5441d05cddcSAtari911 status.textContent = "⏹️ Stopping sync..."; 5451d05cddcSAtari911 status.style.color = "#ff9800"; 5461d05cddcSAtari911 } 5471d05cddcSAtari911 } 5481d05cddcSAtari911 </script>'; 5491d05cddcSAtari911 5501d05cddcSAtari911 // Log Viewer Section - More Compact 551*9ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:15px 0 10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">'; 5521d05cddcSAtari911 echo '<h3 style="margin:0 0 5px 0; color:#00cc07; font-size:16px;"> Live Sync Log</h3>'; 553*9ccd446eSAtari911 echo '<p style="color:' . $colors['text'] . '; font-size:0.8em; margin:0 0 8px;">Updates every 2 seconds</p>'; 5541d05cddcSAtari911 5551d05cddcSAtari911 // Log viewer container 5561d05cddcSAtari911 echo '<div style="background:#1e1e1e; border-radius:5px; overflow:hidden; box-shadow:0 2px 4px rgba(0,0,0,0.3);">'; 5571d05cddcSAtari911 5581d05cddcSAtari911 // Log header - More compact 5591d05cddcSAtari911 echo '<div style="background:#2d2d2d; padding:6px 10px; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #444;">'; 5601d05cddcSAtari911 echo '<span style="color:#00cc07; font-family:monospace; font-weight:bold; font-size:12px;">sync.log</span>'; 5611d05cddcSAtari911 echo '<div>'; 5621d05cddcSAtari911 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>'; 5631d05cddcSAtari911 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>'; 5641d05cddcSAtari911 echo '<button onclick="downloadLog()" style="background:#666; color:white; border:none; padding:4px 8px; border-radius:3px; cursor:pointer; font-size:11px;"> Download</button>'; 5651d05cddcSAtari911 echo '</div>'; 5661d05cddcSAtari911 echo '</div>'; 5671d05cddcSAtari911 5681d05cddcSAtari911 // Log content - Reduced height to 250px 5691d05cddcSAtari911 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>'; 5701d05cddcSAtari911 5711d05cddcSAtari911 echo '</div>'; 5721d05cddcSAtari911 echo '</div>'; 5731d05cddcSAtari911 5741d05cddcSAtari911 // JavaScript for log viewer 5751d05cddcSAtari911 echo '<script> 5761d05cddcSAtari911 let refreshInterval = null; 5771d05cddcSAtari911 let isPaused = false; 5781d05cddcSAtari911 5791d05cddcSAtari911 function refreshLog() { 5801d05cddcSAtari911 if (isPaused) return; 5811d05cddcSAtari911 5821d05cddcSAtari911 fetch("?do=admin&page=calendar&action=get_log&call=ajax") 5831d05cddcSAtari911 .then(response => response.json()) 5841d05cddcSAtari911 .then(data => { 5851d05cddcSAtari911 const logContent = document.getElementById("logContent"); 5861d05cddcSAtari911 if (logContent) { 5871d05cddcSAtari911 logContent.textContent = data.log || "No log data available"; 5881d05cddcSAtari911 logContent.scrollTop = logContent.scrollHeight; 5891d05cddcSAtari911 } 5901d05cddcSAtari911 }) 5911d05cddcSAtari911 .catch(error => { 5921d05cddcSAtari911 console.error("Error fetching log:", error); 5931d05cddcSAtari911 }); 5941d05cddcSAtari911 } 5951d05cddcSAtari911 5961d05cddcSAtari911 function togglePause() { 5971d05cddcSAtari911 isPaused = !isPaused; 5981d05cddcSAtari911 const btn = document.getElementById("pauseBtn"); 5991d05cddcSAtari911 if (isPaused) { 6001d05cddcSAtari911 btn.textContent = "▶ Resume"; 6011d05cddcSAtari911 btn.style.background = "#00cc07"; 6021d05cddcSAtari911 } else { 6031d05cddcSAtari911 btn.textContent = "⏸ Pause"; 6041d05cddcSAtari911 btn.style.background = "#666"; 6051d05cddcSAtari911 refreshLog(); 6061d05cddcSAtari911 } 6071d05cddcSAtari911 } 6081d05cddcSAtari911 6091d05cddcSAtari911 function clearLog() { 6101d05cddcSAtari911 if (!confirm("Clear the sync log file?\\n\\nThis will delete all log entries.")) { 6111d05cddcSAtari911 return; 6121d05cddcSAtari911 } 6131d05cddcSAtari911 6141d05cddcSAtari911 fetch("?do=admin&page=calendar&action=clear_log&call=ajax", { 6151d05cddcSAtari911 method: "POST" 6161d05cddcSAtari911 }) 6171d05cddcSAtari911 .then(response => response.json()) 6181d05cddcSAtari911 .then(data => { 6191d05cddcSAtari911 if (data.success) { 6201d05cddcSAtari911 refreshLog(); 6211d05cddcSAtari911 alert("Log cleared successfully"); 6221d05cddcSAtari911 } else { 6231d05cddcSAtari911 alert("Error clearing log: " + data.message); 6241d05cddcSAtari911 } 6251d05cddcSAtari911 }) 6261d05cddcSAtari911 .catch(error => { 6271d05cddcSAtari911 alert("Error: " + error.message); 6281d05cddcSAtari911 }); 6291d05cddcSAtari911 } 6301d05cddcSAtari911 6311d05cddcSAtari911 function downloadLog() { 6321d05cddcSAtari911 window.location.href = "?do=admin&page=calendar&action=download_log"; 6331d05cddcSAtari911 } 6341d05cddcSAtari911 6351d05cddcSAtari911 // Start auto-refresh 6361d05cddcSAtari911 refreshLog(); 6371d05cddcSAtari911 refreshInterval = setInterval(refreshLog, 2000); 6381d05cddcSAtari911 6391d05cddcSAtari911 // Cleanup on page unload 6401d05cddcSAtari911 window.addEventListener("beforeunload", function() { 6411d05cddcSAtari911 if (refreshInterval) { 6421d05cddcSAtari911 clearInterval(refreshInterval); 6431d05cddcSAtari911 } 6441d05cddcSAtari911 }); 6451d05cddcSAtari911 </script>'; 6461d05cddcSAtari911 } 6471d05cddcSAtari911 648*9ccd446eSAtari911 private function renderManageTab($colors = null) { 6491d05cddcSAtari911 global $INPUT; 6501d05cddcSAtari911 651*9ccd446eSAtari911 // Use defaults if not provided 652*9ccd446eSAtari911 if ($colors === null) { 653*9ccd446eSAtari911 $colors = $this->getTemplateColors(); 654*9ccd446eSAtari911 } 655*9ccd446eSAtari911 6561d05cddcSAtari911 // Show message if present 6571d05cddcSAtari911 if ($INPUT->has('msg')) { 6581d05cddcSAtari911 $msg = hsc($INPUT->str('msg')); 6591d05cddcSAtari911 $type = $INPUT->str('msgtype', 'success'); 6601d05cddcSAtari911 echo "<div style=\"padding:10px; margin:10px 0; border-left:3px solid " . ($type === 'success' ? '#28a745' : '#dc3545') . "; background:" . ($type === 'success' ? '#d4edda' : '#f8d7da') . "; border-radius:3px;\">"; 6611d05cddcSAtari911 echo $msg; 6621d05cddcSAtari911 echo "</div>"; 6631d05cddcSAtari911 } 6641d05cddcSAtari911 6651d05cddcSAtari911 echo '<h2 style="margin:10px 0; font-size:20px;">Manage Calendar Events</h2>'; 6661d05cddcSAtari911 667*9ccd446eSAtari911 // Events Manager Section 668*9ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">'; 6691d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;"> Events Manager</h3>'; 670*9ccd446eSAtari911 echo '<p style="color:' . $colors['text'] . '; font-size:11px; margin:0 0 10px;">Scan, export, and import all calendar events across all namespaces.</p>'; 6711d05cddcSAtari911 6721d05cddcSAtari911 // Get event statistics 6731d05cddcSAtari911 $stats = $this->getEventStatistics(); 6741d05cddcSAtari911 6751d05cddcSAtari911 // Statistics display 676*9ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:10px; border-radius:3px; margin-bottom:10px; border:1px solid ' . $colors['border'] . ';">'; 6771d05cddcSAtari911 echo '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(150px, 1fr)); gap:10px; font-size:12px;">'; 6781d05cddcSAtari911 6791d05cddcSAtari911 echo '<div style="background:#f3e5f5; padding:8px; border-radius:3px; text-align:center;">'; 6801d05cddcSAtari911 echo '<div style="font-size:24px; font-weight:bold; color:#7b1fa2;">' . $stats['total_events'] . '</div>'; 681*9ccd446eSAtari911 echo '<div style="color:' . $colors['text'] . '; font-size:10px;">Total Events</div>'; 6821d05cddcSAtari911 echo '</div>'; 6831d05cddcSAtari911 6841d05cddcSAtari911 echo '<div style="background:#f3e5f5; padding:8px; border-radius:3px; text-align:center;">'; 6851d05cddcSAtari911 echo '<div style="font-size:24px; font-weight:bold; color:#7b1fa2;">' . $stats['total_namespaces'] . '</div>'; 686*9ccd446eSAtari911 echo '<div style="color:' . $colors['text'] . '; font-size:10px;">Namespaces</div>'; 6871d05cddcSAtari911 echo '</div>'; 6881d05cddcSAtari911 6891d05cddcSAtari911 echo '<div style="background:#e8f5e9; padding:8px; border-radius:3px; text-align:center;">'; 6901d05cddcSAtari911 echo '<div style="font-size:24px; font-weight:bold; color:#388e3c;">' . $stats['total_files'] . '</div>'; 691*9ccd446eSAtari911 echo '<div style="color:' . $colors['text'] . '; font-size:10px;">JSON Files</div>'; 6921d05cddcSAtari911 echo '</div>'; 6931d05cddcSAtari911 6941d05cddcSAtari911 echo '<div style="background:#fff3e0; padding:8px; border-radius:3px; text-align:center;">'; 6951d05cddcSAtari911 echo '<div style="font-size:24px; font-weight:bold; color:#f57c00;">' . $stats['total_recurring'] . '</div>'; 696*9ccd446eSAtari911 echo '<div style="color:' . $colors['text'] . '; font-size:10px;">Recurring</div>'; 6971d05cddcSAtari911 echo '</div>'; 6981d05cddcSAtari911 6991d05cddcSAtari911 echo '</div>'; 7001d05cddcSAtari911 7011d05cddcSAtari911 // Last scan time 7021d05cddcSAtari911 if (!empty($stats['last_scan'])) { 703*9ccd446eSAtari911 echo '<div style="margin-top:8px; color:' . $colors['text'] . '; font-size:10px;">Last scanned: ' . hsc($stats['last_scan']) . '</div>'; 7041d05cddcSAtari911 } 7051d05cddcSAtari911 7061d05cddcSAtari911 echo '</div>'; 7071d05cddcSAtari911 7081d05cddcSAtari911 // Action buttons 7091d05cddcSAtari911 echo '<div style="display:flex; gap:10px; flex-wrap:wrap; margin-top:10px;">'; 7101d05cddcSAtari911 7111d05cddcSAtari911 // Rescan button 7121d05cddcSAtari911 echo '<form method="post" action="?do=admin&page=calendar&tab=manage" style="display:inline;">'; 7131d05cddcSAtari911 echo '<input type="hidden" name="action" value="rescan_events">'; 7141d05cddcSAtari911 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;">'; 7151d05cddcSAtari911 echo '<span></span><span>Re-scan Events</span>'; 7161d05cddcSAtari911 echo '</button>'; 7171d05cddcSAtari911 echo '</form>'; 7181d05cddcSAtari911 7191d05cddcSAtari911 // Export button 7201d05cddcSAtari911 echo '<form method="post" action="?do=admin&page=calendar&tab=manage" style="display:inline;">'; 7211d05cddcSAtari911 echo '<input type="hidden" name="action" value="export_all_events">'; 7221d05cddcSAtari911 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;">'; 7231d05cddcSAtari911 echo '<span></span><span>Export All Events</span>'; 7241d05cddcSAtari911 echo '</button>'; 7251d05cddcSAtari911 echo '</form>'; 7261d05cddcSAtari911 7271d05cddcSAtari911 // Import button (with file upload) 7281d05cddcSAtari911 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?\')">'; 7291d05cddcSAtari911 echo '<input type="hidden" name="action" value="import_all_events">'; 7301d05cddcSAtari911 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;">'; 7311d05cddcSAtari911 echo '<span></span><span>Import Events</span>'; 7321d05cddcSAtari911 echo '<input type="file" name="import_file" accept=".json,.zip" required style="display:none;" onchange="this.form.submit()">'; 7331d05cddcSAtari911 echo '</label>'; 7341d05cddcSAtari911 echo '</form>'; 7351d05cddcSAtari911 7361d05cddcSAtari911 echo '</div>'; 7371d05cddcSAtari911 7381d05cddcSAtari911 // Breakdown by namespace 7391d05cddcSAtari911 if (!empty($stats['by_namespace'])) { 7401d05cddcSAtari911 echo '<details style="margin-top:12px;">'; 7411d05cddcSAtari911 echo '<summary style="cursor:pointer; padding:6px; background:#e9e9e9; border-radius:3px; font-size:11px; font-weight:bold;">View Breakdown by Namespace</summary>'; 742*9ccd446eSAtari911 echo '<div style="margin-top:8px; max-height:200px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px;">'; 7431d05cddcSAtari911 echo '<table style="width:100%; border-collapse:collapse; font-size:11px;">'; 7441d05cddcSAtari911 echo '<thead style="position:sticky; top:0; background:#f5f5f5;">'; 7451d05cddcSAtari911 echo '<tr>'; 7461d05cddcSAtari911 echo '<th style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd;">Namespace</th>'; 7471d05cddcSAtari911 echo '<th style="padding:4px 6px; text-align:right; border-bottom:2px solid #ddd;">Events</th>'; 7481d05cddcSAtari911 echo '<th style="padding:4px 6px; text-align:right; border-bottom:2px solid #ddd;">Files</th>'; 7491d05cddcSAtari911 echo '</tr></thead><tbody>'; 7501d05cddcSAtari911 7511d05cddcSAtari911 foreach ($stats['by_namespace'] as $ns => $nsStats) { 7521d05cddcSAtari911 echo '<tr style="border-bottom:1px solid #eee;">'; 7531d05cddcSAtari911 echo '<td style="padding:4px 6px;"><code style="background:#f0f0f0; padding:1px 3px; border-radius:2px; font-size:10px;">' . hsc($ns ?: '(default)') . '</code></td>'; 7541d05cddcSAtari911 echo '<td style="padding:4px 6px; text-align:right;"><strong>' . $nsStats['events'] . '</strong></td>'; 7551d05cddcSAtari911 echo '<td style="padding:4px 6px; text-align:right;">' . $nsStats['files'] . '</td>'; 7561d05cddcSAtari911 echo '</tr>'; 7571d05cddcSAtari911 } 7581d05cddcSAtari911 7591d05cddcSAtari911 echo '</tbody></table>'; 7601d05cddcSAtari911 echo '</div>'; 7611d05cddcSAtari911 echo '</details>'; 7621d05cddcSAtari911 } 7631d05cddcSAtari911 7641d05cddcSAtari911 echo '</div>'; 7651d05cddcSAtari911 766*9ccd446eSAtari911 // Cleanup Events Section 767*9ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">'; 768*9ccd446eSAtari911 echo '<h3 style="margin:0 0 6px 0; color:#00cc07; font-size:16px;"> Cleanup Old Events</h3>'; 769*9ccd446eSAtari911 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>'; 7701d05cddcSAtari911 7711d05cddcSAtari911 echo '<form method="post" action="?do=admin&page=calendar&tab=manage" id="cleanupForm">'; 7721d05cddcSAtari911 echo '<input type="hidden" name="action" value="cleanup_events">'; 7731d05cddcSAtari911 7741d05cddcSAtari911 // Compact options layout 775*9ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:10px; border:1px solid ' . $colors['border'] . '; border-radius:3px; margin-bottom:10px;">'; 7761d05cddcSAtari911 7771d05cddcSAtari911 // Radio buttons in a row 7781d05cddcSAtari911 echo '<div style="display:flex; gap:20px; margin-bottom:10px; padding-bottom:8px; border-bottom:1px solid #f0f0f0;">'; 7791d05cddcSAtari911 echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">'; 7801d05cddcSAtari911 echo '<input type="radio" name="cleanup_type" value="age" checked onchange="updateCleanupOptions()">'; 7811d05cddcSAtari911 echo '<span>By Age</span>'; 7821d05cddcSAtari911 echo '</label>'; 7831d05cddcSAtari911 echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">'; 7841d05cddcSAtari911 echo '<input type="radio" name="cleanup_type" value="status" onchange="updateCleanupOptions()">'; 7851d05cddcSAtari911 echo '<span>By Status</span>'; 7861d05cddcSAtari911 echo '</label>'; 7871d05cddcSAtari911 echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">'; 7881d05cddcSAtari911 echo '<input type="radio" name="cleanup_type" value="range" onchange="updateCleanupOptions()">'; 7891d05cddcSAtari911 echo '<span>By Date Range</span>'; 7901d05cddcSAtari911 echo '</label>'; 7911d05cddcSAtari911 echo '</div>'; 7921d05cddcSAtari911 7931d05cddcSAtari911 // Age options 7941d05cddcSAtari911 echo '<div id="age-options" style="padding:6px 0;">'; 795*9ccd446eSAtari911 echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">Delete events older than:</span>'; 7961d05cddcSAtari911 echo '<select name="age_value" style="width:50px; padding:3px 4px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px; margin-right:4px;">'; 7971d05cddcSAtari911 for ($i = 1; $i <= 24; $i++) { 7981d05cddcSAtari911 $sel = $i === 6 ? ' selected' : ''; 7991d05cddcSAtari911 echo '<option value="' . $i . '"' . $sel . '>' . $i . '</option>'; 8001d05cddcSAtari911 } 8011d05cddcSAtari911 echo '</select>'; 8021d05cddcSAtari911 echo '<select name="age_unit" style="width:80px; padding:3px 4px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px;">'; 8031d05cddcSAtari911 echo '<option value="months" selected>months</option>'; 8041d05cddcSAtari911 echo '<option value="years">years</option>'; 8051d05cddcSAtari911 echo '</select>'; 8061d05cddcSAtari911 echo '</div>'; 8071d05cddcSAtari911 8081d05cddcSAtari911 // Status options 8091d05cddcSAtari911 echo '<div id="status-options" style="padding:6px 0; opacity:0.4;">'; 810*9ccd446eSAtari911 echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">Delete:</span>'; 8111d05cddcSAtari911 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>'; 8121d05cddcSAtari911 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>'; 8131d05cddcSAtari911 echo '</div>'; 8141d05cddcSAtari911 8151d05cddcSAtari911 // Range options 8161d05cddcSAtari911 echo '<div id="range-options" style="padding:6px 0; opacity:0.4;">'; 817*9ccd446eSAtari911 echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">From:</span>'; 8181d05cddcSAtari911 echo '<input type="date" name="range_start" style="padding:3px 6px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px; margin-right:10px;">'; 819*9ccd446eSAtari911 echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">To:</span>'; 8201d05cddcSAtari911 echo '<input type="date" name="range_end" style="padding:3px 6px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px;">'; 8211d05cddcSAtari911 echo '</div>'; 8221d05cddcSAtari911 8231d05cddcSAtari911 echo '</div>'; 8241d05cddcSAtari911 8251d05cddcSAtari911 // Namespace filter - compact 826*9ccd446eSAtari911 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;">'; 8271d05cddcSAtari911 echo '<label style="font-size:11px; font-weight:600; white-space:nowrap; color:#555;">Namespace:</label>'; 8281d05cddcSAtari911 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;">'; 8291d05cddcSAtari911 echo '</div>'; 8301d05cddcSAtari911 8311d05cddcSAtari911 // Action buttons - compact row 8321d05cddcSAtari911 echo '<div style="display:flex; gap:8px; align-items:center;">'; 8331d05cddcSAtari911 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>'; 8341d05cddcSAtari911 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>'; 8351d05cddcSAtari911 echo '<span style="font-size:10px; color:#999;">⚠️ Backup created automatically</span>'; 8361d05cddcSAtari911 echo '</div>'; 8371d05cddcSAtari911 8381d05cddcSAtari911 echo '</form>'; 8391d05cddcSAtari911 8401d05cddcSAtari911 // Preview results area 8411d05cddcSAtari911 echo '<div id="cleanup-preview" style="margin-top:10px; display:none;"></div>'; 8421d05cddcSAtari911 8431d05cddcSAtari911 echo '<script> 8441d05cddcSAtari911 function updateCleanupOptions() { 8451d05cddcSAtari911 const type = document.querySelector(\'input[name="cleanup_type"]:checked\').value; 8461d05cddcSAtari911 8471d05cddcSAtari911 // Show selected, gray out others 8481d05cddcSAtari911 document.getElementById(\'age-options\').style.opacity = type === \'age\' ? \'1\' : \'0.4\'; 8491d05cddcSAtari911 document.getElementById(\'status-options\').style.opacity = type === \'status\' ? \'1\' : \'0.4\'; 8501d05cddcSAtari911 document.getElementById(\'range-options\').style.opacity = type === \'range\' ? \'1\' : \'0.4\'; 8511d05cddcSAtari911 8521d05cddcSAtari911 // Enable/disable inputs 8531d05cddcSAtari911 document.querySelectorAll(\'#age-options select\').forEach(el => el.disabled = type !== \'age\'); 8541d05cddcSAtari911 document.querySelectorAll(\'#status-options input\').forEach(el => el.disabled = type !== \'status\'); 8551d05cddcSAtari911 document.querySelectorAll(\'#range-options input\').forEach(el => el.disabled = type !== \'range\'); 8561d05cddcSAtari911 } 8571d05cddcSAtari911 8581d05cddcSAtari911 function previewCleanup() { 8591d05cddcSAtari911 const form = document.getElementById(\'cleanupForm\'); 8601d05cddcSAtari911 const formData = new FormData(form); 8611d05cddcSAtari911 formData.set(\'action\', \'preview_cleanup\'); 8621d05cddcSAtari911 8631d05cddcSAtari911 const preview = document.getElementById(\'cleanup-preview\'); 864*9ccd446eSAtari911 preview.innerHTML = \'<div style="text-align:center; padding:20px; color:' . $colors['text'] . ';">Loading preview...</div>\'; 8651d05cddcSAtari911 preview.style.display = \'block\'; 8661d05cddcSAtari911 8671d05cddcSAtari911 fetch(\'?do=admin&page=calendar&tab=manage\', { 8681d05cddcSAtari911 method: \'POST\', 8691d05cddcSAtari911 body: new URLSearchParams(formData) 8701d05cddcSAtari911 }) 8711d05cddcSAtari911 .then(r => r.json()) 8721d05cddcSAtari911 .then(data => { 8731d05cddcSAtari911 if (data.count === 0) { 8741d05cddcSAtari911 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>\'; 8751d05cddcSAtari911 8761d05cddcSAtari911 // Show debug info if available 8771d05cddcSAtari911 if (data.debug) { 878*9ccd446eSAtari911 html += \'<details style="margin-top:8px; font-size:11px; color:' . $colors['text'] . ';">\'; 8791d05cddcSAtari911 html += \'<summary style="cursor:pointer;">Debug Info</summary>\'; 8801d05cddcSAtari911 html += \'<pre style="background:#f5f5f5; padding:6px; margin-top:4px; border-radius:3px; overflow-x:auto;">\' + JSON.stringify(data.debug, null, 2) + \'</pre>\'; 8811d05cddcSAtari911 html += \'</details>\'; 8821d05cddcSAtari911 } 8831d05cddcSAtari911 8841d05cddcSAtari911 preview.innerHTML = html; 8851d05cddcSAtari911 } else { 8861d05cddcSAtari911 let html = \'<div style="background:#f8d7da; border:1px solid #f5c6cb; padding:10px; border-radius:3px; font-size:12px; color:#721c24;">\'; 8871d05cddcSAtari911 html += \'<strong>⚠️ Warning:</strong> The following \' + data.count + \' event(s) would be deleted:<br><br>\'; 888*9ccd446eSAtari911 html += \'<div style="max-height:150px; overflow-y:auto; margin-top:6px; background:' . $colors['bg'] . '; padding:6px; border-radius:3px;">\'; 8891d05cddcSAtari911 data.events.forEach(evt => { 8901d05cddcSAtari911 html += \'<div style="padding:3px; border-bottom:1px solid #eee; font-size:11px;">\'; 8911d05cddcSAtari911 html += \'• \' + evt.title + \' (\' + evt.date + \')\'; 8921d05cddcSAtari911 if (evt.namespace) html += \' <span style="background:#e3f2fd; padding:1px 4px; border-radius:2px; font-size:9px;">\' + evt.namespace + \'</span>\'; 8931d05cddcSAtari911 html += \'</div>\'; 8941d05cddcSAtari911 }); 8951d05cddcSAtari911 html += \'</div></div>\'; 8961d05cddcSAtari911 preview.innerHTML = html; 8971d05cddcSAtari911 } 8981d05cddcSAtari911 }) 8991d05cddcSAtari911 .catch(err => { 9001d05cddcSAtari911 preview.innerHTML = \'<div style="background:#f8d7da; border:1px solid #f5c6cb; padding:10px; border-radius:3px; font-size:12px; color:#721c24;">Error loading preview</div>\'; 9011d05cddcSAtari911 }); 9021d05cddcSAtari911 } 9031d05cddcSAtari911 9041d05cddcSAtari911 function confirmCleanup() { 9051d05cddcSAtari911 return confirm(\'Are you sure you want to delete these events? A backup will be created first, but this action cannot be easily undone.\'); 9061d05cddcSAtari911 } 9071d05cddcSAtari911 9081d05cddcSAtari911 updateCleanupOptions(); 9091d05cddcSAtari911 </script>'; 9101d05cddcSAtari911 9111d05cddcSAtari911 echo '</div>'; 9121d05cddcSAtari911 9131d05cddcSAtari911 // Recurring Events Section 914*9ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">'; 9151d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;"> Recurring Events</h3>'; 9161d05cddcSAtari911 9171d05cddcSAtari911 $recurringEvents = $this->findRecurringEvents(); 9181d05cddcSAtari911 9191d05cddcSAtari911 if (empty($recurringEvents)) { 920*9ccd446eSAtari911 echo '<p style="color:' . $colors['text'] . '; font-size:13px; margin:5px 0;">No recurring events found.</p>'; 9211d05cddcSAtari911 } else { 9221d05cddcSAtari911 // Search bar 9231d05cddcSAtari911 echo '<div style="margin-bottom:8px;">'; 924*9ccd446eSAtari911 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;">'; 9251d05cddcSAtari911 echo '</div>'; 9261d05cddcSAtari911 9271d05cddcSAtari911 echo '<style> 9281d05cddcSAtari911 .sort-arrow { 9291d05cddcSAtari911 color: #999; 9301d05cddcSAtari911 font-size: 10px; 9311d05cddcSAtari911 margin-left: 3px; 9321d05cddcSAtari911 display: inline-block; 9331d05cddcSAtari911 } 9341d05cddcSAtari911 #recurringTable th:hover { 9351d05cddcSAtari911 background: #ddd; 9361d05cddcSAtari911 } 9371d05cddcSAtari911 #recurringTable th:hover .sort-arrow { 9381d05cddcSAtari911 color: #00cc07; 9391d05cddcSAtari911 } 9401d05cddcSAtari911 .recurring-row-hidden { 9411d05cddcSAtari911 display: none; 9421d05cddcSAtari911 } 9431d05cddcSAtari911 </style>'; 944*9ccd446eSAtari911 echo '<div style="max-height:250px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px;">'; 9451d05cddcSAtari911 echo '<table id="recurringTable" style="width:100%; border-collapse:collapse; font-size:11px;">'; 9461d05cddcSAtari911 echo '<thead style="position:sticky; top:0; background:#e9e9e9;">'; 9471d05cddcSAtari911 echo '<tr>'; 9481d05cddcSAtari911 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>'; 9491d05cddcSAtari911 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>'; 9501d05cddcSAtari911 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>'; 9511d05cddcSAtari911 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>'; 9521d05cddcSAtari911 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>'; 9531d05cddcSAtari911 echo '<th style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd;">Actions</th>'; 9541d05cddcSAtari911 echo '</tr></thead><tbody id="recurringTableBody">'; 9551d05cddcSAtari911 9561d05cddcSAtari911 foreach ($recurringEvents as $series) { 9571d05cddcSAtari911 echo '<tr style="border-bottom:1px solid #eee;">'; 9581d05cddcSAtari911 echo '<td style="padding:4px 6px;">' . hsc($series['title']) . '</td>'; 9591d05cddcSAtari911 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>'; 9601d05cddcSAtari911 echo '<td style="padding:4px 6px;">' . hsc($series['pattern']) . '</td>'; 9611d05cddcSAtari911 echo '<td style="padding:4px 6px;">' . hsc($series['firstDate']) . '</td>'; 9621d05cddcSAtari911 echo '<td style="padding:4px 6px;"><strong>' . $series['count'] . '</strong></td>'; 9631d05cddcSAtari911 echo '<td style="padding:4px 6px; white-space:nowrap;">'; 9641d05cddcSAtari911 echo '<button onclick="editRecurringSeries(\'' . hsc(addslashes($series['title'])) . '\', \'' . hsc($series['namespace']) . '\')" style="background:#00cc07; color:white; border:none; padding:2px 6px; border-radius:2px; cursor:pointer; font-size:10px; margin-right:2px;">Edit</button>'; 9651d05cddcSAtari911 echo '<button onclick="deleteRecurringSeries(\'' . hsc(addslashes($series['title'])) . '\', \'' . hsc($series['namespace']) . '\')" style="background:#e74c3c; color:white; border:none; padding:2px 6px; border-radius:2px; cursor:pointer; font-size:10px;">Del</button>'; 9661d05cddcSAtari911 echo '</td>'; 9671d05cddcSAtari911 echo '</tr>'; 9681d05cddcSAtari911 } 9691d05cddcSAtari911 9701d05cddcSAtari911 echo '</tbody></table>'; 9711d05cddcSAtari911 echo '</div>'; 972*9ccd446eSAtari911 echo '<p style="color:' . $colors['text'] . '; font-size:10px; margin:5px 0 0;">Total: ' . count($recurringEvents) . ' series</p>'; 9731d05cddcSAtari911 } 9741d05cddcSAtari911 echo '</div>'; 9751d05cddcSAtari911 9761d05cddcSAtari911 // Compact Tree-based Namespace Manager 977*9ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">'; 9781d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;"> Namespace Explorer</h3>'; 979*9ccd446eSAtari911 echo '<p style="color:' . $colors['text'] . '; font-size:11px; margin:0 0 8px;">Select events and move between namespaces. Drag & drop also supported.</p>'; 9801d05cddcSAtari911 9811d05cddcSAtari911 // Search bar 9821d05cddcSAtari911 echo '<div style="margin-bottom:8px;">'; 983*9ccd446eSAtari911 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;">'; 9841d05cddcSAtari911 echo '</div>'; 9851d05cddcSAtari911 9861d05cddcSAtari911 $eventsByNamespace = $this->getEventsByNamespace(); 9871d05cddcSAtari911 9881d05cddcSAtari911 // Control bar 9891d05cddcSAtari911 echo '<form method="post" action="?do=admin&page=calendar&tab=manage" id="moveForm">'; 9901d05cddcSAtari911 echo '<input type="hidden" name="action" value="move_selected_events" id="formAction">'; 9911d05cddcSAtari911 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;">'; 9921d05cddcSAtari911 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>'; 9931d05cddcSAtari911 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>'; 9941d05cddcSAtari911 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>'; 9951d05cddcSAtari911 echo '<span style="margin-left:10px;">Move to:</span>'; 996*9ccd446eSAtari911 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...">'; 9971d05cddcSAtari911 echo '<datalist id="namespaceList">'; 9981d05cddcSAtari911 echo '<option value="">(default)</option>'; 9991d05cddcSAtari911 foreach (array_keys($eventsByNamespace) as $ns) { 10001d05cddcSAtari911 if ($ns !== '') { 10011d05cddcSAtari911 echo '<option value="' . hsc($ns) . '">' . hsc($ns) . '</option>'; 10021d05cddcSAtari911 } 10031d05cddcSAtari911 } 10041d05cddcSAtari911 echo '</datalist>'; 10051d05cddcSAtari911 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>'; 10061d05cddcSAtari911 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>'; 10071d05cddcSAtari911 echo '<span id="selectedCount" style="margin-left:auto; color:#00cc07; font-size:11px;">0 selected</span>'; 10081d05cddcSAtari911 echo '</div>'; 10091d05cddcSAtari911 10101d05cddcSAtari911 echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">'; 10111d05cddcSAtari911 10121d05cddcSAtari911 // Event list with checkboxes 10131d05cddcSAtari911 echo '<div>'; 1014*9ccd446eSAtari911 echo '<div style="max-height:450px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px; background:' . $colors['bg'] . ';">'; 10151d05cddcSAtari911 10161d05cddcSAtari911 foreach ($eventsByNamespace as $namespace => $data) { 10171d05cddcSAtari911 $nsId = 'ns_' . md5($namespace); 10181d05cddcSAtari911 $eventCount = count($data['events']); 10191d05cddcSAtari911 10201d05cddcSAtari911 echo '<div style="border-bottom:1px solid #ddd;">'; 10211d05cddcSAtari911 10221d05cddcSAtari911 // Namespace header - ultra compact 10231d05cddcSAtari911 echo '<div style="background:#f5f5f5; padding:3px 6px; display:flex; justify-content:space-between; align-items:center; font-size:11px;">'; 10241d05cddcSAtari911 echo '<div style="display:flex; align-items:center; gap:4px;">'; 10251d05cddcSAtari911 echo '<span onclick="toggleNamespace(\'' . $nsId . '\')" style="cursor:pointer; width:12px; display:inline-block; font-size:10px;"><span id="' . $nsId . '_arrow">▶</span></span>'; 10261d05cddcSAtari911 echo '<input type="checkbox" onclick="toggleNamespaceSelect(\'' . $nsId . '\')" id="' . $nsId . '_check" style="margin:0; width:12px; height:12px;">'; 10271d05cddcSAtari911 echo '<span onclick="toggleNamespace(\'' . $nsId . '\')" style="cursor:pointer; font-weight:600; font-size:11px;"> ' . hsc($namespace ?: '(default)') . '</span>'; 10281d05cddcSAtari911 echo '</div>'; 10291d05cddcSAtari911 echo '<div style="display:flex; gap:3px; align-items:center;">'; 10301d05cddcSAtari911 echo '<span style="background:#00cc07; color:white; padding:0px 4px; border-radius:6px; font-size:9px; line-height:14px;">' . $eventCount . '</span>'; 1031*9ccd446eSAtari911 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>'; 10321d05cddcSAtari911 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>'; 10331d05cddcSAtari911 echo '</div>'; 10341d05cddcSAtari911 echo '</div>'; 10351d05cddcSAtari911 10361d05cddcSAtari911 // Events - ultra compact 10371d05cddcSAtari911 echo '<div id="' . $nsId . '" style="display:none; max-height:150px; overflow-y:auto;">'; 10381d05cddcSAtari911 foreach ($data['events'] as $event) { 10391d05cddcSAtari911 $eventId = $event['id'] . '|' . $namespace . '|' . $event['date'] . '|' . $event['month']; 10401d05cddcSAtari911 $checkId = 'evt_' . md5($eventId); 10411d05cddcSAtari911 10421d05cddcSAtari911 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\'">'; 10431d05cddcSAtari911 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;">'; 10441d05cddcSAtari911 echo '<div style="flex:1; min-width:0;">'; 10451d05cddcSAtari911 echo '<div style="font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-size:10px;">' . hsc($event['title']) . '</div>'; 10461d05cddcSAtari911 echo '<div style="color:#999; font-size:9px;">' . hsc($event['date']) . ($event['startTime'] ? ' • ' . hsc($event['startTime']) : '') . '</div>'; 10471d05cddcSAtari911 echo '</div>'; 10481d05cddcSAtari911 echo '</div>'; 10491d05cddcSAtari911 } 10501d05cddcSAtari911 echo '</div>'; 10511d05cddcSAtari911 echo '</div>'; 10521d05cddcSAtari911 } 10531d05cddcSAtari911 10541d05cddcSAtari911 echo '</div>'; 10551d05cddcSAtari911 echo '</div>'; 10561d05cddcSAtari911 10571d05cddcSAtari911 // Drop zones - ultra compact 10581d05cddcSAtari911 echo '<div>'; 10591d05cddcSAtari911 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>'; 1060*9ccd446eSAtari911 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'] . ';">'; 10611d05cddcSAtari911 10621d05cddcSAtari911 foreach (array_keys($eventsByNamespace) as $namespace) { 1063*9ccd446eSAtari911 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\'">'; 10641d05cddcSAtari911 echo '<div style="font-size:11px; font-weight:600; color:#00cc07;"> ' . hsc($namespace ?: '(default)') . '</div>'; 10651d05cddcSAtari911 echo '<div style="color:#999; font-size:9px; margin-top:1px;">Drop here</div>'; 10661d05cddcSAtari911 echo '</div>'; 10671d05cddcSAtari911 } 10681d05cddcSAtari911 10691d05cddcSAtari911 echo '</div>'; 10701d05cddcSAtari911 echo '</div>'; 10711d05cddcSAtari911 10721d05cddcSAtari911 echo '</div>'; // end grid 10731d05cddcSAtari911 echo '</form>'; 10741d05cddcSAtari911 10751d05cddcSAtari911 echo '</div>'; 10761d05cddcSAtari911 10771d05cddcSAtari911 // JavaScript 10781d05cddcSAtari911 echo '<script> 10791d05cddcSAtari911 // Table sorting functionality - defined early so onclick handlers work 10801d05cddcSAtari911 let sortDirection = {}; // Track sort direction for each column 10811d05cddcSAtari911 10821d05cddcSAtari911 function sortRecurringTable(columnIndex) { 10831d05cddcSAtari911 const table = document.getElementById("recurringTable"); 10841d05cddcSAtari911 const tbody = document.getElementById("recurringTableBody"); 10851d05cddcSAtari911 1086*9ccd446eSAtari911 if (!table || !tbody) return; 10871d05cddcSAtari911 10881d05cddcSAtari911 const rows = Array.from(tbody.querySelectorAll("tr")); 1089*9ccd446eSAtari911 if (rows.length === 0) return; 10901d05cddcSAtari911 10911d05cddcSAtari911 // Toggle sort direction for this column 10921d05cddcSAtari911 if (!sortDirection[columnIndex]) { 10931d05cddcSAtari911 sortDirection[columnIndex] = "asc"; 10941d05cddcSAtari911 } else { 10951d05cddcSAtari911 sortDirection[columnIndex] = sortDirection[columnIndex] === "asc" ? "desc" : "asc"; 10961d05cddcSAtari911 } 10971d05cddcSAtari911 10981d05cddcSAtari911 const direction = sortDirection[columnIndex]; 10991d05cddcSAtari911 const isNumeric = columnIndex === 4; // Count column 11001d05cddcSAtari911 11011d05cddcSAtari911 // Sort rows 11021d05cddcSAtari911 rows.sort((a, b) => { 11031d05cddcSAtari911 let aValue = a.cells[columnIndex].textContent.trim(); 11041d05cddcSAtari911 let bValue = b.cells[columnIndex].textContent.trim(); 11051d05cddcSAtari911 11061d05cddcSAtari911 // Extract text from code elements for namespace column 11071d05cddcSAtari911 if (columnIndex === 1) { 11081d05cddcSAtari911 const aCode = a.cells[columnIndex].querySelector("code"); 11091d05cddcSAtari911 const bCode = b.cells[columnIndex].querySelector("code"); 11101d05cddcSAtari911 aValue = aCode ? aCode.textContent.trim() : aValue; 11111d05cddcSAtari911 bValue = bCode ? bCode.textContent.trim() : bValue; 11121d05cddcSAtari911 } 11131d05cddcSAtari911 11141d05cddcSAtari911 // Extract number from strong elements for count column 11151d05cddcSAtari911 if (isNumeric) { 11161d05cddcSAtari911 const aStrong = a.cells[columnIndex].querySelector("strong"); 11171d05cddcSAtari911 const bStrong = b.cells[columnIndex].querySelector("strong"); 11181d05cddcSAtari911 aValue = aStrong ? parseInt(aStrong.textContent.trim()) : 0; 11191d05cddcSAtari911 bValue = bStrong ? parseInt(bStrong.textContent.trim()) : 0; 11201d05cddcSAtari911 11211d05cddcSAtari911 return direction === "asc" ? aValue - bValue : bValue - aValue; 11221d05cddcSAtari911 } 11231d05cddcSAtari911 11241d05cddcSAtari911 // String comparison 11251d05cddcSAtari911 if (direction === "asc") { 11261d05cddcSAtari911 return aValue.localeCompare(bValue); 11271d05cddcSAtari911 } else { 11281d05cddcSAtari911 return bValue.localeCompare(aValue); 11291d05cddcSAtari911 } 11301d05cddcSAtari911 }); 11311d05cddcSAtari911 11321d05cddcSAtari911 // Update arrows 11331d05cddcSAtari911 const headers = table.querySelectorAll("th"); 11341d05cddcSAtari911 headers.forEach((header, index) => { 11351d05cddcSAtari911 const arrow = header.querySelector(".sort-arrow"); 11361d05cddcSAtari911 if (arrow) { 11371d05cddcSAtari911 if (index === columnIndex) { 11381d05cddcSAtari911 arrow.textContent = direction === "asc" ? "↑" : "↓"; 11391d05cddcSAtari911 arrow.style.color = "#00cc07"; 11401d05cddcSAtari911 } else { 11411d05cddcSAtari911 arrow.textContent = "⇅"; 11421d05cddcSAtari911 arrow.style.color = "#999"; 11431d05cddcSAtari911 } 11441d05cddcSAtari911 } 11451d05cddcSAtari911 }); 11461d05cddcSAtari911 11471d05cddcSAtari911 // Rebuild tbody 11481d05cddcSAtari911 rows.forEach(row => tbody.appendChild(row)); 11491d05cddcSAtari911 } 11501d05cddcSAtari911 11511d05cddcSAtari911 function filterRecurringEvents() { 11521d05cddcSAtari911 const searchInput = document.getElementById("searchRecurring"); 11531d05cddcSAtari911 const filter = normalizeText(searchInput.value); 11541d05cddcSAtari911 const tbody = document.getElementById("recurringTableBody"); 11551d05cddcSAtari911 const rows = tbody.getElementsByTagName("tr"); 11561d05cddcSAtari911 11571d05cddcSAtari911 for (let i = 0; i < rows.length; i++) { 11581d05cddcSAtari911 const row = rows[i]; 11591d05cddcSAtari911 const titleCell = row.getElementsByTagName("td")[0]; 11601d05cddcSAtari911 11611d05cddcSAtari911 if (titleCell) { 11621d05cddcSAtari911 const titleText = normalizeText(titleCell.textContent || titleCell.innerText); 11631d05cddcSAtari911 11641d05cddcSAtari911 if (titleText.indexOf(filter) > -1) { 11651d05cddcSAtari911 row.classList.remove("recurring-row-hidden"); 11661d05cddcSAtari911 } else { 11671d05cddcSAtari911 row.classList.add("recurring-row-hidden"); 11681d05cddcSAtari911 } 11691d05cddcSAtari911 } 11701d05cddcSAtari911 } 11711d05cddcSAtari911 } 11721d05cddcSAtari911 11731d05cddcSAtari911 function normalizeText(text) { 11741d05cddcSAtari911 // Convert to lowercase 11751d05cddcSAtari911 text = text.toLowerCase(); 11761d05cddcSAtari911 11771d05cddcSAtari911 // Remove apostrophes and quotes 11781d05cddcSAtari911 text = text.replace(/[\'\"]/g, ""); 11791d05cddcSAtari911 11801d05cddcSAtari911 // Replace accented characters with regular ones 11811d05cddcSAtari911 text = text.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); 11821d05cddcSAtari911 11831d05cddcSAtari911 // Remove special characters except spaces and alphanumeric 11841d05cddcSAtari911 text = text.replace(/[^a-z0-9\s]/g, ""); 11851d05cddcSAtari911 11861d05cddcSAtari911 // Collapse multiple spaces 11871d05cddcSAtari911 text = text.replace(/\s+/g, " "); 11881d05cddcSAtari911 11891d05cddcSAtari911 return text.trim(); 11901d05cddcSAtari911 } 11911d05cddcSAtari911 11921d05cddcSAtari911 function filterEvents() { 11931d05cddcSAtari911 const searchText = normalizeText(document.getElementById("searchEvents").value); 11941d05cddcSAtari911 const eventRows = document.querySelectorAll(".event-row"); 11951d05cddcSAtari911 let visibleCount = 0; 11961d05cddcSAtari911 11971d05cddcSAtari911 eventRows.forEach(row => { 11981d05cddcSAtari911 const titleElement = row.querySelector("div div"); 11991d05cddcSAtari911 const originalTitle = titleElement.getAttribute("data-original-title") || titleElement.textContent; 12001d05cddcSAtari911 12011d05cddcSAtari911 // Store original title if not already stored 12021d05cddcSAtari911 if (!titleElement.getAttribute("data-original-title")) { 12031d05cddcSAtari911 titleElement.setAttribute("data-original-title", originalTitle); 12041d05cddcSAtari911 } 12051d05cddcSAtari911 12061d05cddcSAtari911 const normalizedTitle = normalizeText(originalTitle); 12071d05cddcSAtari911 12081d05cddcSAtari911 if (normalizedTitle.includes(searchText) || searchText === "") { 12091d05cddcSAtari911 row.style.display = "flex"; 12101d05cddcSAtari911 visibleCount++; 12111d05cddcSAtari911 } else { 12121d05cddcSAtari911 row.style.display = "none"; 12131d05cddcSAtari911 } 12141d05cddcSAtari911 }); 12151d05cddcSAtari911 12161d05cddcSAtari911 // Update namespace visibility and counts 12171d05cddcSAtari911 document.querySelectorAll("[id^=ns_]").forEach(nsDiv => { 12181d05cddcSAtari911 if (nsDiv.id.endsWith("_arrow") || nsDiv.id.endsWith("_check")) return; 12191d05cddcSAtari911 12201d05cddcSAtari911 const visibleEvents = nsDiv.querySelectorAll(".event-row[style*=\\"display: flex\\"], .event-row:not([style*=\\"display: none\\"])").length; 12211d05cddcSAtari911 const nsId = nsDiv.id; 12221d05cddcSAtari911 const arrow = document.getElementById(nsId + "_arrow"); 12231d05cddcSAtari911 12241d05cddcSAtari911 // Auto-expand namespaces with matches when searching 12251d05cddcSAtari911 if (searchText && visibleEvents > 0) { 12261d05cddcSAtari911 nsDiv.style.display = "block"; 12271d05cddcSAtari911 if (arrow) arrow.textContent = "▼"; 12281d05cddcSAtari911 } 12291d05cddcSAtari911 }); 12301d05cddcSAtari911 } 12311d05cddcSAtari911 12321d05cddcSAtari911 function toggleNamespace(id) { 12331d05cddcSAtari911 const elem = document.getElementById(id); 12341d05cddcSAtari911 const arrow = document.getElementById(id + "_arrow"); 12351d05cddcSAtari911 if (elem.style.display === "none") { 12361d05cddcSAtari911 elem.style.display = "block"; 12371d05cddcSAtari911 arrow.textContent = "▼"; 12381d05cddcSAtari911 } else { 12391d05cddcSAtari911 elem.style.display = "none"; 12401d05cddcSAtari911 arrow.textContent = "▶"; 12411d05cddcSAtari911 } 12421d05cddcSAtari911 } 12431d05cddcSAtari911 12441d05cddcSAtari911 function toggleNamespaceSelect(nsId) { 12451d05cddcSAtari911 const checkbox = document.getElementById(nsId + "_check"); 12461d05cddcSAtari911 const events = document.querySelectorAll("." + nsId + "_events"); 12471d05cddcSAtari911 12481d05cddcSAtari911 // Only select visible events (not hidden by search) 12491d05cddcSAtari911 events.forEach(cb => { 12501d05cddcSAtari911 const eventRow = cb.closest(".event-row"); 12511d05cddcSAtari911 if (eventRow && eventRow.style.display !== "none") { 12521d05cddcSAtari911 cb.checked = checkbox.checked; 12531d05cddcSAtari911 } 12541d05cddcSAtari911 }); 12551d05cddcSAtari911 updateCount(); 12561d05cddcSAtari911 } 12571d05cddcSAtari911 12581d05cddcSAtari911 function selectAll() { 12591d05cddcSAtari911 // Only select visible events 12601d05cddcSAtari911 document.querySelectorAll(".event-checkbox").forEach(cb => { 12611d05cddcSAtari911 const eventRow = cb.closest(".event-row"); 12621d05cddcSAtari911 if (eventRow && eventRow.style.display !== "none") { 12631d05cddcSAtari911 cb.checked = true; 12641d05cddcSAtari911 } 12651d05cddcSAtari911 }); 12661d05cddcSAtari911 // Update namespace checkboxes to indeterminate if partially selected 12671d05cddcSAtari911 document.querySelectorAll("input[id$=_check]").forEach(nsCheckbox => { 12681d05cddcSAtari911 const nsId = nsCheckbox.id.replace("_check", ""); 12691d05cddcSAtari911 const events = document.querySelectorAll("." + nsId + "_events"); 12701d05cddcSAtari911 const visibleEvents = Array.from(events).filter(cb => { 12711d05cddcSAtari911 const row = cb.closest(".event-row"); 12721d05cddcSAtari911 return row && row.style.display !== "none"; 12731d05cddcSAtari911 }); 12741d05cddcSAtari911 const checkedVisible = visibleEvents.filter(cb => cb.checked); 12751d05cddcSAtari911 12761d05cddcSAtari911 if (checkedVisible.length === visibleEvents.length && visibleEvents.length > 0) { 12771d05cddcSAtari911 nsCheckbox.checked = true; 12781d05cddcSAtari911 } else if (checkedVisible.length > 0) { 12791d05cddcSAtari911 nsCheckbox.indeterminate = true; 12801d05cddcSAtari911 } else { 12811d05cddcSAtari911 nsCheckbox.checked = false; 12821d05cddcSAtari911 } 12831d05cddcSAtari911 }); 12841d05cddcSAtari911 updateCount(); 12851d05cddcSAtari911 } 12861d05cddcSAtari911 12871d05cddcSAtari911 function deselectAll() { 12881d05cddcSAtari911 document.querySelectorAll(".event-checkbox").forEach(cb => cb.checked = false); 12891d05cddcSAtari911 document.querySelectorAll("input[id$=_check]").forEach(cb => { 12901d05cddcSAtari911 cb.checked = false; 12911d05cddcSAtari911 cb.indeterminate = false; 12921d05cddcSAtari911 }); 12931d05cddcSAtari911 updateCount(); 12941d05cddcSAtari911 } 12951d05cddcSAtari911 12961d05cddcSAtari911 function deleteSelected() { 12971d05cddcSAtari911 const checkedBoxes = document.querySelectorAll(".event-checkbox:checked"); 12981d05cddcSAtari911 if (checkedBoxes.length === 0) { 12991d05cddcSAtari911 alert("No events selected"); 13001d05cddcSAtari911 return; 13011d05cddcSAtari911 } 13021d05cddcSAtari911 13031d05cddcSAtari911 const count = checkedBoxes.length; 13041d05cddcSAtari911 if (!confirm(`Delete ${count} selected event(s)?\\n\\nThis cannot be undone!`)) { 13051d05cddcSAtari911 return; 13061d05cddcSAtari911 } 13071d05cddcSAtari911 13081d05cddcSAtari911 const form = document.createElement("form"); 13091d05cddcSAtari911 form.method = "POST"; 13101d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=manage"; 13111d05cddcSAtari911 13121d05cddcSAtari911 const actionInput = document.createElement("input"); 13131d05cddcSAtari911 actionInput.type = "hidden"; 13141d05cddcSAtari911 actionInput.name = "action"; 13151d05cddcSAtari911 actionInput.value = "delete_selected_events"; 13161d05cddcSAtari911 form.appendChild(actionInput); 13171d05cddcSAtari911 13181d05cddcSAtari911 checkedBoxes.forEach(cb => { 13191d05cddcSAtari911 const eventInput = document.createElement("input"); 13201d05cddcSAtari911 eventInput.type = "hidden"; 13211d05cddcSAtari911 eventInput.name = "events[]"; 13221d05cddcSAtari911 eventInput.value = cb.value; 13231d05cddcSAtari911 form.appendChild(eventInput); 13241d05cddcSAtari911 }); 13251d05cddcSAtari911 13261d05cddcSAtari911 document.body.appendChild(form); 13271d05cddcSAtari911 form.submit(); 13281d05cddcSAtari911 } 13291d05cddcSAtari911 13301d05cddcSAtari911 function createNewNamespace() { 13311d05cddcSAtari911 const namespaceName = prompt("Enter new namespace name:\\n\\nExamples:\\n- work\\n- personal\\n- projects:alpha\\n- aspen:travel:2025"); 13321d05cddcSAtari911 13331d05cddcSAtari911 if (!namespaceName) { 13341d05cddcSAtari911 return; // Cancelled 13351d05cddcSAtari911 } 13361d05cddcSAtari911 13371d05cddcSAtari911 // Validate namespace name 13381d05cddcSAtari911 if (!/^[a-zA-Z0-9_:-]+$/.test(namespaceName)) { 13391d05cddcSAtari911 alert("Invalid namespace name.\\n\\nUse only letters, numbers, underscore, hyphen, and colon.\\nExample: work:projects:alpha"); 13401d05cddcSAtari911 return; 13411d05cddcSAtari911 } 13421d05cddcSAtari911 13431d05cddcSAtari911 // Submit form to create namespace 13441d05cddcSAtari911 const form = document.createElement("form"); 13451d05cddcSAtari911 form.method = "POST"; 13461d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=manage"; 13471d05cddcSAtari911 13481d05cddcSAtari911 const actionInput = document.createElement("input"); 13491d05cddcSAtari911 actionInput.type = "hidden"; 13501d05cddcSAtari911 actionInput.name = "action"; 13511d05cddcSAtari911 actionInput.value = "create_namespace"; 13521d05cddcSAtari911 form.appendChild(actionInput); 13531d05cddcSAtari911 13541d05cddcSAtari911 const namespaceInput = document.createElement("input"); 13551d05cddcSAtari911 namespaceInput.type = "hidden"; 13561d05cddcSAtari911 namespaceInput.name = "namespace_name"; 13571d05cddcSAtari911 namespaceInput.value = namespaceName; 13581d05cddcSAtari911 form.appendChild(namespaceInput); 13591d05cddcSAtari911 13601d05cddcSAtari911 document.body.appendChild(form); 13611d05cddcSAtari911 form.submit(); 13621d05cddcSAtari911 } 13631d05cddcSAtari911 13641d05cddcSAtari911 function updateCount() { 13651d05cddcSAtari911 const count = document.querySelectorAll(".event-checkbox:checked").length; 13661d05cddcSAtari911 document.getElementById("selectedCount").textContent = count + " selected"; 13671d05cddcSAtari911 } 13681d05cddcSAtari911 13691d05cddcSAtari911 function deleteNamespace(namespace) { 13701d05cddcSAtari911 const displayName = namespace || "(default)"; 13711d05cddcSAtari911 if (!confirm("Delete ENTIRE namespace: " + displayName + "?\\n\\nThis will delete ALL events in this namespace!\\n\\nThis cannot be undone!")) { 13721d05cddcSAtari911 return; 13731d05cddcSAtari911 } 13741d05cddcSAtari911 const form = document.createElement("form"); 13751d05cddcSAtari911 form.method = "POST"; 13761d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=manage"; 13771d05cddcSAtari911 const actionInput = document.createElement("input"); 13781d05cddcSAtari911 actionInput.type = "hidden"; 13791d05cddcSAtari911 actionInput.name = "action"; 13801d05cddcSAtari911 actionInput.value = "delete_namespace"; 13811d05cddcSAtari911 form.appendChild(actionInput); 13821d05cddcSAtari911 const nsInput = document.createElement("input"); 13831d05cddcSAtari911 nsInput.type = "hidden"; 13841d05cddcSAtari911 nsInput.name = "namespace"; 13851d05cddcSAtari911 nsInput.value = namespace; 13861d05cddcSAtari911 form.appendChild(nsInput); 13871d05cddcSAtari911 document.body.appendChild(form); 13881d05cddcSAtari911 form.submit(); 13891d05cddcSAtari911 } 13901d05cddcSAtari911 1391*9ccd446eSAtari911 function renameNamespace(oldNamespace) { 1392*9ccd446eSAtari911 const displayName = oldNamespace || "(default)"; 1393*9ccd446eSAtari911 const newName = prompt("Rename namespace: " + displayName + "\\n\\nEnter new name:", oldNamespace); 1394*9ccd446eSAtari911 if (newName === null || newName === oldNamespace) { 1395*9ccd446eSAtari911 return; // Cancelled or no change 1396*9ccd446eSAtari911 } 1397*9ccd446eSAtari911 const form = document.createElement("form"); 1398*9ccd446eSAtari911 form.method = "POST"; 1399*9ccd446eSAtari911 form.action = "?do=admin&page=calendar&tab=manage"; 1400*9ccd446eSAtari911 const actionInput = document.createElement("input"); 1401*9ccd446eSAtari911 actionInput.type = "hidden"; 1402*9ccd446eSAtari911 actionInput.name = "action"; 1403*9ccd446eSAtari911 actionInput.value = "rename_namespace"; 1404*9ccd446eSAtari911 form.appendChild(actionInput); 1405*9ccd446eSAtari911 const oldInput = document.createElement("input"); 1406*9ccd446eSAtari911 oldInput.type = "hidden"; 1407*9ccd446eSAtari911 oldInput.name = "old_namespace"; 1408*9ccd446eSAtari911 oldInput.value = oldNamespace; 1409*9ccd446eSAtari911 form.appendChild(oldInput); 1410*9ccd446eSAtari911 const newInput = document.createElement("input"); 1411*9ccd446eSAtari911 newInput.type = "hidden"; 1412*9ccd446eSAtari911 newInput.name = "new_namespace"; 1413*9ccd446eSAtari911 newInput.value = newName; 1414*9ccd446eSAtari911 form.appendChild(newInput); 1415*9ccd446eSAtari911 document.body.appendChild(form); 1416*9ccd446eSAtari911 form.submit(); 1417*9ccd446eSAtari911 } 1418*9ccd446eSAtari911 14191d05cddcSAtari911 let draggedEvent = null; 14201d05cddcSAtari911 14211d05cddcSAtari911 function dragStart(event, eventId) { 14221d05cddcSAtari911 const checkbox = event.target.closest(".event-row").querySelector(".event-checkbox"); 14231d05cddcSAtari911 14241d05cddcSAtari911 // If this event is checked, drag all checked events 14251d05cddcSAtari911 const checkedBoxes = document.querySelectorAll(".event-checkbox:checked"); 14261d05cddcSAtari911 if (checkbox && checkbox.checked && checkedBoxes.length > 1) { 14271d05cddcSAtari911 // Dragging multiple selected events 14281d05cddcSAtari911 draggedEvent = "MULTIPLE"; 14291d05cddcSAtari911 event.dataTransfer.setData("text/plain", "MULTIPLE"); 14301d05cddcSAtari911 } else { 14311d05cddcSAtari911 // Dragging single event 14321d05cddcSAtari911 draggedEvent = eventId; 14331d05cddcSAtari911 event.dataTransfer.setData("text/plain", eventId); 14341d05cddcSAtari911 } 14351d05cddcSAtari911 event.dataTransfer.effectAllowed = "move"; 14361d05cddcSAtari911 event.target.style.opacity = "0.5"; 14371d05cddcSAtari911 } 14381d05cddcSAtari911 14391d05cddcSAtari911 function allowDrop(event) { 14401d05cddcSAtari911 event.preventDefault(); 14411d05cddcSAtari911 event.dataTransfer.dropEffect = "move"; 14421d05cddcSAtari911 } 14431d05cddcSAtari911 14441d05cddcSAtari911 function drop(event, targetNamespace) { 14451d05cddcSAtari911 event.preventDefault(); 14461d05cddcSAtari911 14471d05cddcSAtari911 if (draggedEvent === "MULTIPLE") { 14481d05cddcSAtari911 // Move all selected events 14491d05cddcSAtari911 const checkedBoxes = document.querySelectorAll(".event-checkbox:checked"); 14501d05cddcSAtari911 if (checkedBoxes.length === 0) return; 14511d05cddcSAtari911 14521d05cddcSAtari911 const form = document.createElement("form"); 14531d05cddcSAtari911 form.method = "POST"; 14541d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=manage"; 14551d05cddcSAtari911 14561d05cddcSAtari911 const actionInput = document.createElement("input"); 14571d05cddcSAtari911 actionInput.type = "hidden"; 14581d05cddcSAtari911 actionInput.name = "action"; 14591d05cddcSAtari911 actionInput.value = "move_selected_events"; 14601d05cddcSAtari911 form.appendChild(actionInput); 14611d05cddcSAtari911 14621d05cddcSAtari911 checkedBoxes.forEach(cb => { 14631d05cddcSAtari911 const eventInput = document.createElement("input"); 14641d05cddcSAtari911 eventInput.type = "hidden"; 14651d05cddcSAtari911 eventInput.name = "events[]"; 14661d05cddcSAtari911 eventInput.value = cb.value; 14671d05cddcSAtari911 form.appendChild(eventInput); 14681d05cddcSAtari911 }); 14691d05cddcSAtari911 14701d05cddcSAtari911 const targetInput = document.createElement("input"); 14711d05cddcSAtari911 targetInput.type = "hidden"; 14721d05cddcSAtari911 targetInput.name = "target_namespace"; 14731d05cddcSAtari911 targetInput.value = targetNamespace; 14741d05cddcSAtari911 form.appendChild(targetInput); 14751d05cddcSAtari911 14761d05cddcSAtari911 document.body.appendChild(form); 14771d05cddcSAtari911 form.submit(); 14781d05cddcSAtari911 } else { 14791d05cddcSAtari911 // Move single event 14801d05cddcSAtari911 if (!draggedEvent) return; 14811d05cddcSAtari911 const parts = draggedEvent.split("|"); 14821d05cddcSAtari911 const sourceNamespace = parts[1]; 14831d05cddcSAtari911 if (sourceNamespace === targetNamespace) return; 14841d05cddcSAtari911 14851d05cddcSAtari911 const form = document.createElement("form"); 14861d05cddcSAtari911 form.method = "POST"; 14871d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=manage"; 14881d05cddcSAtari911 const actionInput = document.createElement("input"); 14891d05cddcSAtari911 actionInput.type = "hidden"; 14901d05cddcSAtari911 actionInput.name = "action"; 14911d05cddcSAtari911 actionInput.value = "move_single_event"; 14921d05cddcSAtari911 form.appendChild(actionInput); 14931d05cddcSAtari911 const eventInput = document.createElement("input"); 14941d05cddcSAtari911 eventInput.type = "hidden"; 14951d05cddcSAtari911 eventInput.name = "event"; 14961d05cddcSAtari911 eventInput.value = draggedEvent; 14971d05cddcSAtari911 form.appendChild(eventInput); 14981d05cddcSAtari911 const targetInput = document.createElement("input"); 14991d05cddcSAtari911 targetInput.type = "hidden"; 15001d05cddcSAtari911 targetInput.name = "target_namespace"; 15011d05cddcSAtari911 targetInput.value = targetNamespace; 15021d05cddcSAtari911 form.appendChild(targetInput); 15031d05cddcSAtari911 document.body.appendChild(form); 15041d05cddcSAtari911 form.submit(); 15051d05cddcSAtari911 } 15061d05cddcSAtari911 } 15071d05cddcSAtari911 15081d05cddcSAtari911 function editRecurringSeries(title, namespace) { 1509*9ccd446eSAtari911 // Get available namespaces from the namespace explorer 1510*9ccd446eSAtari911 const namespaces = new Set(); 15111d05cddcSAtari911 1512*9ccd446eSAtari911 // Method 1: Try to get from namespace explorer folder names 1513*9ccd446eSAtari911 document.querySelectorAll("[id^=ns_]").forEach(el => { 1514*9ccd446eSAtari911 const nsSpan = el.querySelector("span:nth-child(3)"); 1515*9ccd446eSAtari911 if (nsSpan) { 1516*9ccd446eSAtari911 let nsText = nsSpan.textContent.replace(" ", "").trim(); 1517*9ccd446eSAtari911 if (nsText && nsText !== "(default)") { 1518*9ccd446eSAtari911 namespaces.add(nsText); 1519*9ccd446eSAtari911 } 1520*9ccd446eSAtari911 } 1521*9ccd446eSAtari911 }); 1522*9ccd446eSAtari911 1523*9ccd446eSAtari911 // Method 2: Get from datalist if it exists 1524*9ccd446eSAtari911 document.querySelectorAll("#namespaceList option").forEach(opt => { 1525*9ccd446eSAtari911 if (opt.value && opt.value !== "") { 1526*9ccd446eSAtari911 namespaces.add(opt.value); 1527*9ccd446eSAtari911 } 1528*9ccd446eSAtari911 }); 1529*9ccd446eSAtari911 1530*9ccd446eSAtari911 // Convert to sorted array 1531*9ccd446eSAtari911 const nsArray = Array.from(namespaces).sort(); 1532*9ccd446eSAtari911 1533*9ccd446eSAtari911 // Build options - include current namespace AND all others 1534*9ccd446eSAtari911 let nsOptions = "<option value=\\"\\">(default)</option>"; 1535*9ccd446eSAtari911 1536*9ccd446eSAtari911 // Add current namespace if it\'s not default 1537*9ccd446eSAtari911 if (namespace && namespace !== "") { 1538*9ccd446eSAtari911 nsOptions += "<option value=\\"" + namespace + "\\" selected>" + namespace + " (current)</option>"; 1539*9ccd446eSAtari911 } 1540*9ccd446eSAtari911 1541*9ccd446eSAtari911 // Add all other namespaces 1542*9ccd446eSAtari911 for (const ns of nsArray) { 1543*9ccd446eSAtari911 if (ns !== namespace) { 1544*9ccd446eSAtari911 nsOptions += "<option value=\\"" + ns + "\\">" + ns + "</option>"; 15451d05cddcSAtari911 } 15461d05cddcSAtari911 } 15471d05cddcSAtari911 15481d05cddcSAtari911 // Show edit dialog for recurring events 15491d05cddcSAtari911 const dialog = document.createElement("div"); 15501d05cddcSAtari911 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;"; 15511d05cddcSAtari911 15521d05cddcSAtari911 // Close on clicking background 15531d05cddcSAtari911 dialog.addEventListener("click", function(e) { 15541d05cddcSAtari911 if (e.target === dialog) { 15551d05cddcSAtari911 dialog.remove(); 15561d05cddcSAtari911 } 15571d05cddcSAtari911 }); 15581d05cddcSAtari911 15591d05cddcSAtari911 dialog.innerHTML = ` 1560*9ccd446eSAtari911 <div style="background:' . $colors['bg'] . '; padding:20px; border-radius:8px; min-width:500px; max-width:700px; max-height:90vh; overflow-y:auto;"> 15611d05cddcSAtari911 <h3 style="margin:0 0 15px; color:#00cc07;">Edit Recurring Event</h3> 1562*9ccd446eSAtari911 <p style="margin:0 0 15px; color:' . $colors['text'] . '; font-size:13px;">Changes will apply to ALL occurrences of: <strong>${title}</strong></p> 15631d05cddcSAtari911 15641d05cddcSAtari911 <form id="editRecurringForm" style="display:flex; flex-direction:column; gap:12px;"> 15651d05cddcSAtari911 <div> 15661d05cddcSAtari911 <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">New Title:</label> 1567*9ccd446eSAtari911 <input type="text" name="new_title" value="${title}" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;" required> 15681d05cddcSAtari911 </div> 15691d05cddcSAtari911 15701d05cddcSAtari911 <div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;"> 15711d05cddcSAtari911 <div> 15721d05cddcSAtari911 <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">Start Time:</label> 1573*9ccd446eSAtari911 <input type="time" name="start_time" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;"> 15741d05cddcSAtari911 <small style="color:#999; font-size:11px;">Leave blank to keep current</small> 15751d05cddcSAtari911 </div> 15761d05cddcSAtari911 <div> 15771d05cddcSAtari911 <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">End Time:</label> 1578*9ccd446eSAtari911 <input type="time" name="end_time" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;"> 15791d05cddcSAtari911 <small style="color:#999; font-size:11px;">Leave blank to keep current</small> 15801d05cddcSAtari911 </div> 15811d05cddcSAtari911 </div> 15821d05cddcSAtari911 15831d05cddcSAtari911 <div> 15841d05cddcSAtari911 <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">Interval (days between occurrences):</label> 1585*9ccd446eSAtari911 <select name="interval" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;"> 15861d05cddcSAtari911 <option value="">Keep current interval</option> 15871d05cddcSAtari911 <option value="1">Daily (1 day)</option> 15881d05cddcSAtari911 <option value="7">Weekly (7 days)</option> 15891d05cddcSAtari911 <option value="14">Bi-weekly (14 days)</option> 15901d05cddcSAtari911 <option value="30">Monthly (30 days)</option> 15911d05cddcSAtari911 <option value="365">Yearly (365 days)</option> 15921d05cddcSAtari911 </select> 15931d05cddcSAtari911 </div> 15941d05cddcSAtari911 15951d05cddcSAtari911 <div> 15961d05cddcSAtari911 <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">Move to Namespace:</label> 1597*9ccd446eSAtari911 <select name="new_namespace" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;"> 15981d05cddcSAtari911 ${nsOptions} 15991d05cddcSAtari911 </select> 16001d05cddcSAtari911 </div> 16011d05cddcSAtari911 16021d05cddcSAtari911 <div style="display:flex; gap:10px; margin-top:10px;"> 16031d05cddcSAtari911 <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> 16041d05cddcSAtari911 <button type="button" onclick="closeEditDialog()" style="flex:1; background:#999; color:white; padding:10px; border:none; border-radius:3px; cursor:pointer;">Cancel</button> 16051d05cddcSAtari911 </div> 16061d05cddcSAtari911 </form> 16071d05cddcSAtari911 </div> 16081d05cddcSAtari911 `; 16091d05cddcSAtari911 16101d05cddcSAtari911 document.body.appendChild(dialog); 16111d05cddcSAtari911 16121d05cddcSAtari911 // Add close function to window 16131d05cddcSAtari911 window.closeEditDialog = function() { 16141d05cddcSAtari911 dialog.remove(); 16151d05cddcSAtari911 }; 16161d05cddcSAtari911 16171d05cddcSAtari911 // Handle form submission 16181d05cddcSAtari911 dialog.querySelector("#editRecurringForm").addEventListener("submit", function(e) { 16191d05cddcSAtari911 e.preventDefault(); 16201d05cddcSAtari911 const formData = new FormData(this); 16211d05cddcSAtari911 16221d05cddcSAtari911 // Submit the edit 16231d05cddcSAtari911 const form = document.createElement("form"); 16241d05cddcSAtari911 form.method = "POST"; 16251d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=manage"; 16261d05cddcSAtari911 16271d05cddcSAtari911 const actionInput = document.createElement("input"); 16281d05cddcSAtari911 actionInput.type = "hidden"; 16291d05cddcSAtari911 actionInput.name = "action"; 16301d05cddcSAtari911 actionInput.value = "edit_recurring_series"; 16311d05cddcSAtari911 form.appendChild(actionInput); 16321d05cddcSAtari911 16331d05cddcSAtari911 const oldTitleInput = document.createElement("input"); 16341d05cddcSAtari911 oldTitleInput.type = "hidden"; 16351d05cddcSAtari911 oldTitleInput.name = "old_title"; 16361d05cddcSAtari911 oldTitleInput.value = title; 16371d05cddcSAtari911 form.appendChild(oldTitleInput); 16381d05cddcSAtari911 16391d05cddcSAtari911 const oldNamespaceInput = document.createElement("input"); 16401d05cddcSAtari911 oldNamespaceInput.type = "hidden"; 16411d05cddcSAtari911 oldNamespaceInput.name = "old_namespace"; 16421d05cddcSAtari911 oldNamespaceInput.value = namespace; 16431d05cddcSAtari911 form.appendChild(oldNamespaceInput); 16441d05cddcSAtari911 16451d05cddcSAtari911 // Add all form fields 16461d05cddcSAtari911 for (let [key, value] of formData.entries()) { 16471d05cddcSAtari911 const input = document.createElement("input"); 16481d05cddcSAtari911 input.type = "hidden"; 16491d05cddcSAtari911 input.name = key; 16501d05cddcSAtari911 input.value = value; 16511d05cddcSAtari911 form.appendChild(input); 16521d05cddcSAtari911 } 16531d05cddcSAtari911 16541d05cddcSAtari911 document.body.appendChild(form); 16551d05cddcSAtari911 form.submit(); 16561d05cddcSAtari911 }); 16571d05cddcSAtari911 } 16581d05cddcSAtari911 16591d05cddcSAtari911 function deleteRecurringSeries(title, namespace) { 16601d05cddcSAtari911 const displayNs = namespace || "(default)"; 16611d05cddcSAtari911 if (!confirm("Delete ALL occurrences of: " + title + " (" + displayNs + ")?\\n\\nThis cannot be undone!")) { 16621d05cddcSAtari911 return; 16631d05cddcSAtari911 } 16641d05cddcSAtari911 const form = document.createElement("form"); 16651d05cddcSAtari911 form.method = "POST"; 16661d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=manage"; 16671d05cddcSAtari911 const actionInput = document.createElement("input"); 16681d05cddcSAtari911 actionInput.type = "hidden"; 16691d05cddcSAtari911 actionInput.name = "action"; 16701d05cddcSAtari911 actionInput.value = "delete_recurring_series"; 16711d05cddcSAtari911 form.appendChild(actionInput); 16721d05cddcSAtari911 const titleInput = document.createElement("input"); 16731d05cddcSAtari911 titleInput.type = "hidden"; 16741d05cddcSAtari911 titleInput.name = "event_title"; 16751d05cddcSAtari911 titleInput.value = title; 16761d05cddcSAtari911 form.appendChild(titleInput); 16771d05cddcSAtari911 const namespaceInput = document.createElement("input"); 16781d05cddcSAtari911 namespaceInput.type = "hidden"; 16791d05cddcSAtari911 namespaceInput.name = "namespace"; 16801d05cddcSAtari911 namespaceInput.value = namespace; 16811d05cddcSAtari911 form.appendChild(namespaceInput); 16821d05cddcSAtari911 document.body.appendChild(form); 16831d05cddcSAtari911 form.submit(); 16841d05cddcSAtari911 } 16851d05cddcSAtari911 16861d05cddcSAtari911 document.addEventListener("dragend", function(e) { 16871d05cddcSAtari911 if (e.target.draggable) { 16881d05cddcSAtari911 e.target.style.opacity = "1"; 16891d05cddcSAtari911 } 16901d05cddcSAtari911 }); 16911d05cddcSAtari911 </script>'; 16921d05cddcSAtari911 } 16931d05cddcSAtari911 1694*9ccd446eSAtari911 private function renderUpdateTab($colors = null) { 16951d05cddcSAtari911 global $INPUT; 16961d05cddcSAtari911 1697*9ccd446eSAtari911 // Use defaults if not provided 1698*9ccd446eSAtari911 if ($colors === null) { 1699*9ccd446eSAtari911 $colors = $this->getTemplateColors(); 1700*9ccd446eSAtari911 } 17011d05cddcSAtari911 1702*9ccd446eSAtari911 echo '<h2 style="margin:10px 0; font-size:20px;"> Update Plugin</h2>'; 17031d05cddcSAtari911 17041d05cddcSAtari911 // Show message if present 17051d05cddcSAtari911 if ($INPUT->has('msg')) { 17061d05cddcSAtari911 $msg = hsc($INPUT->str('msg')); 17071d05cddcSAtari911 $type = $INPUT->str('msgtype', 'success'); 17081d05cddcSAtari911 $class = ($type === 'success') ? 'msg success' : 'msg error'; 1709*9ccd446eSAtari911 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;\">"; 17101d05cddcSAtari911 echo $msg; 17111d05cddcSAtari911 echo "</div>"; 17121d05cddcSAtari911 } 17131d05cddcSAtari911 1714*9ccd446eSAtari911 // Show current version FIRST (MOVED TO TOP) 17151d05cddcSAtari911 $pluginInfo = DOKU_PLUGIN . 'calendar/plugin.info.txt'; 17161d05cddcSAtari911 $info = ['version' => 'Unknown', 'date' => 'Unknown', 'name' => 'Calendar Plugin', 'author' => 'Unknown', 'email' => '', 'desc' => '']; 17171d05cddcSAtari911 if (file_exists($pluginInfo)) { 17181d05cddcSAtari911 $info = array_merge($info, confToHash($pluginInfo)); 17191d05cddcSAtari911 } 17201d05cddcSAtari911 1721*9ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">'; 1722*9ccd446eSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;"> Current Version</h3>'; 17231d05cddcSAtari911 echo '<div style="font-size:12px; line-height:1.6;">'; 17241d05cddcSAtari911 echo '<div style="margin:4px 0;"><strong>Version:</strong> ' . hsc($info['version']) . ' (' . hsc($info['date']) . ')</div>'; 17251d05cddcSAtari911 echo '<div style="margin:4px 0;"><strong>Author:</strong> ' . hsc($info['author']) . ($info['email'] ? ' <' . hsc($info['email']) . '>' : '') . '</div>'; 17261d05cddcSAtari911 if ($info['desc']) { 17271d05cddcSAtari911 echo '<div style="margin:4px 0;"><strong>Description:</strong> ' . hsc($info['desc']) . '</div>'; 17281d05cddcSAtari911 } 17291d05cddcSAtari911 echo '<div style="margin:4px 0;"><strong>Location:</strong> <code style="background:#f0f0f0; padding:2px 4px; border-radius:2px;">' . DOKU_PLUGIN . 'calendar/</code></div>'; 17301d05cddcSAtari911 echo '</div>'; 17311d05cddcSAtari911 17321d05cddcSAtari911 // Check permissions 17331d05cddcSAtari911 $pluginDir = DOKU_PLUGIN . 'calendar/'; 17341d05cddcSAtari911 $pluginWritable = is_writable($pluginDir); 17351d05cddcSAtari911 $parentWritable = is_writable(DOKU_PLUGIN); 17361d05cddcSAtari911 1737*9ccd446eSAtari911 echo '<div style="margin-top:8px; padding-top:8px; border-top:1px solid ' . $colors['border'] . ';">'; 17381d05cddcSAtari911 if ($pluginWritable && $parentWritable) { 17391d05cddcSAtari911 echo '<p style="margin:5px 0; font-size:13px; color:#28a745;"><strong>✅ Permissions:</strong> OK - ready to update</p>'; 17401d05cddcSAtari911 } else { 17411d05cddcSAtari911 echo '<p style="margin:5px 0; font-size:13px; color:#dc3545;"><strong>❌ Permissions:</strong> Issues detected</p>'; 17421d05cddcSAtari911 if (!$pluginWritable) { 17431d05cddcSAtari911 echo '<p style="margin:2px 0 2px 20px; font-size:12px; color:#dc3545;">Plugin directory not writable</p>'; 17441d05cddcSAtari911 } 17451d05cddcSAtari911 if (!$parentWritable) { 17461d05cddcSAtari911 echo '<p style="margin:2px 0 2px 20px; font-size:12px; color:#dc3545;">Parent directory not writable</p>'; 17471d05cddcSAtari911 } 1748*9ccd446eSAtari911 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>'; 1749*9ccd446eSAtari911 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>'; 17501d05cddcSAtari911 } 17511d05cddcSAtari911 echo '</div>'; 17521d05cddcSAtari911 17531d05cddcSAtari911 echo '</div>'; 17541d05cddcSAtari911 1755*9ccd446eSAtari911 // Combined upload and notes section (SIDE BY SIDE) 1756*9ccd446eSAtari911 echo '<div style="display:flex; gap:15px; max-width:1200px; margin:10px 0;">'; 17571d05cddcSAtari911 1758*9ccd446eSAtari911 // Left side - Upload form (60% width) 1759*9ccd446eSAtari911 echo '<div style="flex:1; min-width:0; background:' . $colors['bg'] . '; padding:12px; border-left:3px solid #00cc07; border-radius:3px;">'; 1760*9ccd446eSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;"> Upload New Version</h3>'; 1761*9ccd446eSAtari911 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>'; 17621d05cddcSAtari911 17631d05cddcSAtari911 echo '<form method="post" action="?do=admin&page=calendar&tab=update" enctype="multipart/form-data" id="uploadForm">'; 17641d05cddcSAtari911 echo '<input type="hidden" name="action" value="upload_update">'; 17651d05cddcSAtari911 echo '<div style="margin:10px 0;">'; 1766*9ccd446eSAtari911 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%;">'; 17671d05cddcSAtari911 echo '</div>'; 17681d05cddcSAtari911 echo '<div style="margin:10px 0;">'; 17691d05cddcSAtari911 echo '<label style="display:flex; align-items:center; gap:8px; font-size:13px;">'; 17701d05cddcSAtari911 echo '<input type="checkbox" name="backup_first" value="1" checked>'; 17711d05cddcSAtari911 echo '<span>Create backup before updating (Recommended)</span>'; 17721d05cddcSAtari911 echo '</label>'; 17731d05cddcSAtari911 echo '</div>'; 1774*9ccd446eSAtari911 1775*9ccd446eSAtari911 // Buttons side by side 1776*9ccd446eSAtari911 echo '<div style="display:flex; gap:10px; align-items:center; flex-wrap:wrap;">'; 17771d05cddcSAtari911 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>'; 17781d05cddcSAtari911 echo '</form>'; 1779*9ccd446eSAtari911 1780*9ccd446eSAtari911 // Clear Cache button (next to Upload button) 1781*9ccd446eSAtari911 echo '<form method="post" action="?do=admin&page=calendar&tab=update" style="display:inline; margin:0;">'; 1782*9ccd446eSAtari911 echo '<input type="hidden" name="action" value="clear_cache">'; 1783*9ccd446eSAtari911 echo '<input type="hidden" name="tab" value="update">'; 1784*9ccd446eSAtari911 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>'; 1785*9ccd446eSAtari911 echo '</form>'; 17861d05cddcSAtari911 echo '</div>'; 17871d05cddcSAtari911 1788*9ccd446eSAtari911 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>'; 1789*9ccd446eSAtari911 echo '</div>'; 1790*9ccd446eSAtari911 1791*9ccd446eSAtari911 // Right side - Important Notes (40% width) 1792*9ccd446eSAtari911 echo '<div style="flex:0 0 350px; min-width:0; background:#fff3e0; border-left:3px solid #ff9800; padding:12px; border-radius:3px;">'; 17931d05cddcSAtari911 echo '<h4 style="margin:0 0 5px 0; color:#e65100; font-size:14px;">⚠️ Important Notes</h4>'; 1794*9ccd446eSAtari911 echo '<ul style="margin:5px 0; padding-left:20px; font-size:12px; color:#e65100; line-height:1.6;">'; 17951d05cddcSAtari911 echo '<li>This will replace all plugin files</li>'; 17961d05cddcSAtari911 echo '<li>Configuration files (sync_config.php) will be preserved</li>'; 17971d05cddcSAtari911 echo '<li>Event data will not be affected</li>'; 1798*9ccd446eSAtari911 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>'; 17991d05cddcSAtari911 echo '<li>Make sure the ZIP file is a valid calendar plugin</li>'; 18001d05cddcSAtari911 echo '</ul>'; 18011d05cddcSAtari911 echo '</div>'; 18021d05cddcSAtari911 1803*9ccd446eSAtari911 echo '</div>'; // End flex container 1804*9ccd446eSAtari911 1805*9ccd446eSAtari911 // Changelog section - Timeline viewer 1806*9ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #7b1fa2; border-radius:3px; max-width:1200px;">'; 1807*9ccd446eSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#7b1fa2; font-size:16px;"> Version History</h3>'; 1808*9ccd446eSAtari911 1809*9ccd446eSAtari911 $changelogFile = DOKU_PLUGIN . 'calendar/CHANGELOG.md'; 1810*9ccd446eSAtari911 if (file_exists($changelogFile)) { 1811*9ccd446eSAtari911 $changelog = file_get_contents($changelogFile); 1812*9ccd446eSAtari911 1813*9ccd446eSAtari911 // Parse ALL versions into structured data 1814*9ccd446eSAtari911 $lines = explode("\n", $changelog); 1815*9ccd446eSAtari911 $versions = []; 1816*9ccd446eSAtari911 $currentVersion = null; 1817*9ccd446eSAtari911 1818*9ccd446eSAtari911 foreach ($lines as $line) { 1819*9ccd446eSAtari911 $line = trim($line); 1820*9ccd446eSAtari911 1821*9ccd446eSAtari911 // Version header (## Version X.X.X or ## Version X.X.X (date) - title) 1822*9ccd446eSAtari911 if (preg_match('/^## Version (.+?)(?:\s*\(([^)]+)\))?\s*(?:-\s*(.+))?$/', $line, $matches)) { 1823*9ccd446eSAtari911 if ($currentVersion !== null) { 1824*9ccd446eSAtari911 $versions[] = $currentVersion; 1825*9ccd446eSAtari911 } 1826*9ccd446eSAtari911 $currentVersion = [ 1827*9ccd446eSAtari911 'number' => trim($matches[1]), 1828*9ccd446eSAtari911 'date' => isset($matches[2]) ? trim($matches[2]) : '', 1829*9ccd446eSAtari911 'title' => isset($matches[3]) ? trim($matches[3]) : '', 1830*9ccd446eSAtari911 'items' => [] 1831*9ccd446eSAtari911 ]; 1832*9ccd446eSAtari911 } 1833*9ccd446eSAtari911 // List items (- **Type:** description) 1834*9ccd446eSAtari911 elseif ($currentVersion !== null && preg_match('/^- \*\*(.+?):\*\* (.+)$/', $line, $matches)) { 1835*9ccd446eSAtari911 $currentVersion['items'][] = [ 1836*9ccd446eSAtari911 'type' => $matches[1], 1837*9ccd446eSAtari911 'desc' => $matches[2] 1838*9ccd446eSAtari911 ]; 1839*9ccd446eSAtari911 } 1840*9ccd446eSAtari911 } 1841*9ccd446eSAtari911 // Don\'t forget last version 1842*9ccd446eSAtari911 if ($currentVersion !== null) { 1843*9ccd446eSAtari911 $versions[] = $currentVersion; 1844*9ccd446eSAtari911 } 1845*9ccd446eSAtari911 1846*9ccd446eSAtari911 $totalVersions = count($versions); 1847*9ccd446eSAtari911 $uniqueId = 'changelog_' . substr(md5(microtime()), 0, 6); 1848*9ccd446eSAtari911 1849*9ccd446eSAtari911 if ($totalVersions > 0) { 1850*9ccd446eSAtari911 // Timeline navigation bar 1851*9ccd446eSAtari911 echo '<div id="' . $uniqueId . '_wrap" style="position:relative;">'; 1852*9ccd446eSAtari911 1853*9ccd446eSAtari911 // Nav controls 1854*9ccd446eSAtari911 echo '<div style="display:flex; align-items:center; gap:8px; margin-bottom:10px;">'; 1855*9ccd446eSAtari911 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=\'#7b1fa2\'; this.style.color=\'#7b1fa2\'" onmouseout="this.style.borderColor=\'' . $colors['border'] . '\'; this.style.color=\'' . $colors['text'] . '\'">‹</button>'; 1856*9ccd446eSAtari911 echo '<div style="flex:1; text-align:center;">'; 1857*9ccd446eSAtari911 echo '<span id="' . $uniqueId . '_counter" style="font-size:11px; color:' . $colors['text'] . '; opacity:0.7;">1 of ' . $totalVersions . '</span>'; 1858*9ccd446eSAtari911 echo '</div>'; 1859*9ccd446eSAtari911 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=\'#7b1fa2\'; this.style.color=\'#7b1fa2\'" onmouseout="this.style.borderColor=\'' . $colors['border'] . '\'; this.style.color=\'' . $colors['text'] . '\'">›</button>'; 1860*9ccd446eSAtari911 echo '</div>'; 1861*9ccd446eSAtari911 1862*9ccd446eSAtari911 // Version cards (one per version, only first visible) 1863*9ccd446eSAtari911 foreach ($versions as $i => $ver) { 1864*9ccd446eSAtari911 $display = ($i === 0) ? 'block' : 'none'; 1865*9ccd446eSAtari911 echo '<div class="' . $uniqueId . '_card" id="' . $uniqueId . '_card_' . $i . '" style="display:' . $display . '; padding:10px; background:' . $colors['bg'] . '; border:1px solid ' . $colors['border'] . '; border-left:3px solid #7b1fa2; border-radius:4px; transition:opacity 0.2s;">'; 1866*9ccd446eSAtari911 1867*9ccd446eSAtari911 // Version header 1868*9ccd446eSAtari911 echo '<div style="display:flex; align-items:baseline; gap:8px; margin-bottom:8px;">'; 1869*9ccd446eSAtari911 echo '<span style="font-weight:bold; color:#7b1fa2; font-size:14px;">v' . hsc($ver['number']) . '</span>'; 1870*9ccd446eSAtari911 if ($ver['date']) { 1871*9ccd446eSAtari911 echo '<span style="font-size:11px; color:' . $colors['text'] . '; opacity:0.6;">' . hsc($ver['date']) . '</span>'; 1872*9ccd446eSAtari911 } 1873*9ccd446eSAtari911 echo '</div>'; 1874*9ccd446eSAtari911 if ($ver['title']) { 1875*9ccd446eSAtari911 echo '<div style="font-size:12px; font-weight:600; color:' . $colors['text'] . '; margin-bottom:8px;">' . hsc($ver['title']) . '</div>'; 1876*9ccd446eSAtari911 } 1877*9ccd446eSAtari911 1878*9ccd446eSAtari911 // Change items 1879*9ccd446eSAtari911 if (!empty($ver['items'])) { 1880*9ccd446eSAtari911 echo '<div style="font-size:12px; line-height:1.7;">'; 1881*9ccd446eSAtari911 foreach ($ver['items'] as $item) { 1882*9ccd446eSAtari911 $color = '#666'; $icon = '•'; 1883*9ccd446eSAtari911 $t = $item['type']; 1884*9ccd446eSAtari911 if ($t === 'Added') { $color = '#28a745'; $icon = '✨'; } 1885*9ccd446eSAtari911 elseif ($t === 'Fixed') { $color = '#dc3545'; $icon = ''; } 1886*9ccd446eSAtari911 elseif ($t === 'Changed') { $color = '#7b1fa2'; $icon = ''; } 1887*9ccd446eSAtari911 elseif ($t === 'Improved') { $color = '#ff9800'; $icon = '⚡'; } 1888*9ccd446eSAtari911 elseif ($t === 'Removed') { $color = '#e91e63'; $icon = '️'; } 1889*9ccd446eSAtari911 elseif ($t === 'Development' || $t === 'Refactored') { $color = '#6c757d'; $icon = '️'; } 1890*9ccd446eSAtari911 elseif ($t === 'Result') { $color = '#2196f3'; $icon = '✅'; } 1891*9ccd446eSAtari911 1892*9ccd446eSAtari911 echo '<div style="margin:2px 0; padding-left:4px;">'; 1893*9ccd446eSAtari911 echo '<span style="color:' . $color . '; font-weight:600;">' . $icon . ' ' . hsc($item['type']) . ':</span> '; 1894*9ccd446eSAtari911 echo '<span style="color:' . $colors['text'] . ';">' . hsc($item['desc']) . '</span>'; 1895*9ccd446eSAtari911 echo '</div>'; 1896*9ccd446eSAtari911 } 1897*9ccd446eSAtari911 echo '</div>'; 1898*9ccd446eSAtari911 } else { 1899*9ccd446eSAtari911 echo '<div style="font-size:11px; color:' . $colors['text'] . '; opacity:0.5; font-style:italic;">No detailed changes recorded</div>'; 1900*9ccd446eSAtari911 } 1901*9ccd446eSAtari911 1902*9ccd446eSAtari911 echo '</div>'; 1903*9ccd446eSAtari911 } 1904*9ccd446eSAtari911 1905*9ccd446eSAtari911 echo '</div>'; // wrap 1906*9ccd446eSAtari911 1907*9ccd446eSAtari911 // JavaScript for navigation 1908*9ccd446eSAtari911 echo '<script> 1909*9ccd446eSAtari911 (function() { 1910*9ccd446eSAtari911 var id = "' . $uniqueId . '"; 1911*9ccd446eSAtari911 var total = ' . $totalVersions . '; 1912*9ccd446eSAtari911 var current = 0; 1913*9ccd446eSAtari911 1914*9ccd446eSAtari911 window.changelogNav = function(uid, dir) { 1915*9ccd446eSAtari911 if (uid !== id) return; 1916*9ccd446eSAtari911 var next = current + dir; 1917*9ccd446eSAtari911 if (next < 0 || next >= total) return; 1918*9ccd446eSAtari911 1919*9ccd446eSAtari911 // Hide current 1920*9ccd446eSAtari911 var curCard = document.getElementById(id + "_card_" + current); 1921*9ccd446eSAtari911 if (curCard) curCard.style.display = "none"; 1922*9ccd446eSAtari911 1923*9ccd446eSAtari911 // Show next 1924*9ccd446eSAtari911 current = next; 1925*9ccd446eSAtari911 var nextCard = document.getElementById(id + "_card_" + current); 1926*9ccd446eSAtari911 if (nextCard) nextCard.style.display = "block"; 1927*9ccd446eSAtari911 1928*9ccd446eSAtari911 // Update counter 1929*9ccd446eSAtari911 var counter = document.getElementById(id + "_counter"); 1930*9ccd446eSAtari911 if (counter) counter.textContent = (current + 1) + " of " + total; 1931*9ccd446eSAtari911 1932*9ccd446eSAtari911 // Update button states 1933*9ccd446eSAtari911 var prevBtn = document.getElementById(id + "_prev"); 1934*9ccd446eSAtari911 var nextBtn = document.getElementById(id + "_next"); 1935*9ccd446eSAtari911 if (prevBtn) prevBtn.style.opacity = (current === 0) ? "0.3" : "1"; 1936*9ccd446eSAtari911 if (nextBtn) nextBtn.style.opacity = (current === total - 1) ? "0.3" : "1"; 1937*9ccd446eSAtari911 }; 1938*9ccd446eSAtari911 1939*9ccd446eSAtari911 // Initialize button states 1940*9ccd446eSAtari911 var prevBtn = document.getElementById(id + "_prev"); 1941*9ccd446eSAtari911 if (prevBtn) prevBtn.style.opacity = "0.3"; 1942*9ccd446eSAtari911 })(); 1943*9ccd446eSAtari911 </script>'; 1944*9ccd446eSAtari911 1945*9ccd446eSAtari911 } else { 1946*9ccd446eSAtari911 echo '<p style="color:#999; font-size:13px; font-style:italic;">No versions found in changelog</p>'; 1947*9ccd446eSAtari911 } 1948*9ccd446eSAtari911 } else { 1949*9ccd446eSAtari911 echo '<p style="color:#999; font-size:13px; font-style:italic;">Changelog not available</p>'; 1950*9ccd446eSAtari911 } 1951*9ccd446eSAtari911 1952*9ccd446eSAtari911 echo '</div>'; 1953*9ccd446eSAtari911 1954*9ccd446eSAtari911 // Backup list or manual backup section 19551d05cddcSAtari911 $backupDir = DOKU_PLUGIN; 19561d05cddcSAtari911 $backups = glob($backupDir . 'calendar*.zip'); 19571d05cddcSAtari911 19581d05cddcSAtari911 // Filter to only show files that look like backups (not the uploaded plugin files) 19591d05cddcSAtari911 $backups = array_filter($backups, function($file) { 19601d05cddcSAtari911 $name = basename($file); 19611d05cddcSAtari911 // Include files that start with "calendar" but exclude files that are just "calendar.zip" (uploaded plugin) 19621d05cddcSAtari911 return $name !== 'calendar.zip'; 19631d05cddcSAtari911 }); 19641d05cddcSAtari911 1965*9ccd446eSAtari911 // Always show backup section (even if no backups yet) 1966*9ccd446eSAtari911 echo '<div id="backupSection" style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">'; 1967*9ccd446eSAtari911 echo '<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">'; 1968*9ccd446eSAtari911 echo '<h3 style="margin:0; color:#00cc07; font-size:16px;"> Backups</h3>'; 1969*9ccd446eSAtari911 1970*9ccd446eSAtari911 // Manual backup button 1971*9ccd446eSAtari911 echo '<form method="post" action="?do=admin&page=calendar&tab=update" style="margin:0;">'; 1972*9ccd446eSAtari911 echo '<input type="hidden" name="action" value="create_manual_backup">'; 1973*9ccd446eSAtari911 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>'; 1974*9ccd446eSAtari911 echo '</form>'; 1975*9ccd446eSAtari911 echo '</div>'; 1976*9ccd446eSAtari911 19771d05cddcSAtari911 if (!empty($backups)) { 19781d05cddcSAtari911 rsort($backups); // Newest first 1979*9ccd446eSAtari911 echo '<div style="max-height:200px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px; background:' . $colors['bg'] . ';">'; 1980*9ccd446eSAtari911 echo '<table id="backupTable" style="width:100%; border-collapse:collapse; font-size:12px;">'; 19811d05cddcSAtari911 echo '<thead style="position:sticky; top:0; background:#e9e9e9;">'; 19821d05cddcSAtari911 echo '<tr>'; 1983*9ccd446eSAtari911 echo '<th style="padding:6px; text-align:left; border-bottom:2px solid ' . $colors['border'] . ';">Backup File</th>'; 1984*9ccd446eSAtari911 echo '<th style="padding:6px; text-align:left; border-bottom:2px solid ' . $colors['border'] . ';">Size</th>'; 1985*9ccd446eSAtari911 echo '<th style="padding:6px; text-align:left; border-bottom:2px solid ' . $colors['border'] . ';">Actions</th>'; 19861d05cddcSAtari911 echo '</tr></thead><tbody>'; 19871d05cddcSAtari911 19881d05cddcSAtari911 foreach ($backups as $backup) { 19891d05cddcSAtari911 $filename = basename($backup); 19901d05cddcSAtari911 $size = $this->formatBytes(filesize($backup)); 19911d05cddcSAtari911 echo '<tr style="border-bottom:1px solid #eee;">'; 19921d05cddcSAtari911 echo '<td style="padding:6px;"><code style="font-size:11px;">' . hsc($filename) . '</code></td>'; 19931d05cddcSAtari911 echo '<td style="padding:6px;">' . $size . '</td>'; 19941d05cddcSAtari911 echo '<td style="padding:6px; white-space:nowrap;">'; 19951d05cddcSAtari911 echo '<a href="' . DOKU_BASE . 'lib/plugins/' . hsc($filename) . '" download style="color:#00cc07; text-decoration:none; font-size:11px; margin-right:10px;"> Download</a>'; 19961d05cddcSAtari911 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>'; 19971d05cddcSAtari911 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>'; 19981d05cddcSAtari911 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>'; 19991d05cddcSAtari911 echo '</td>'; 20001d05cddcSAtari911 echo '</tr>'; 20011d05cddcSAtari911 } 20021d05cddcSAtari911 20031d05cddcSAtari911 echo '</tbody></table>'; 20041d05cddcSAtari911 echo '</div>'; 2005*9ccd446eSAtari911 } else { 2006*9ccd446eSAtari911 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>'; 20071d05cddcSAtari911 } 2008*9ccd446eSAtari911 echo '</div>'; 20091d05cddcSAtari911 20101d05cddcSAtari911 echo '<script> 20111d05cddcSAtari911 function confirmUpload() { 20121d05cddcSAtari911 const fileInput = document.querySelector(\'input[name="plugin_zip"]\'); 20131d05cddcSAtari911 if (!fileInput.files[0]) { 20141d05cddcSAtari911 alert("Please select a ZIP file"); 20151d05cddcSAtari911 return false; 20161d05cddcSAtari911 } 20171d05cddcSAtari911 20181d05cddcSAtari911 const fileName = fileInput.files[0].name; 20191d05cddcSAtari911 if (!fileName.endsWith(".zip")) { 20201d05cddcSAtari911 alert("Please select a ZIP file"); 20211d05cddcSAtari911 return false; 20221d05cddcSAtari911 } 20231d05cddcSAtari911 20241d05cddcSAtari911 return confirm("Upload and install: " + fileName + "?\\n\\nThis will replace all plugin files.\\nYour configuration and data will be preserved.\\n\\nContinue?"); 20251d05cddcSAtari911 } 20261d05cddcSAtari911 20271d05cddcSAtari911 function deleteBackup(filename) { 20281d05cddcSAtari911 if (!confirm("Delete backup: " + filename + "?\\n\\nThis cannot be undone!")) { 20291d05cddcSAtari911 return; 20301d05cddcSAtari911 } 20311d05cddcSAtari911 2032*9ccd446eSAtari911 // Use AJAX to delete without page refresh 2033*9ccd446eSAtari911 const formData = new FormData(); 2034*9ccd446eSAtari911 formData.append(\'action\', \'delete_backup\'); 2035*9ccd446eSAtari911 formData.append(\'backup_file\', filename); 20361d05cddcSAtari911 2037*9ccd446eSAtari911 fetch(\'?do=admin&page=calendar&tab=update\', { 2038*9ccd446eSAtari911 method: \'POST\', 2039*9ccd446eSAtari911 body: formData 2040*9ccd446eSAtari911 }) 2041*9ccd446eSAtari911 .then(response => response.text()) 2042*9ccd446eSAtari911 .then(data => { 2043*9ccd446eSAtari911 // Remove the row from the table 2044*9ccd446eSAtari911 const rows = document.querySelectorAll(\'tr\'); 2045*9ccd446eSAtari911 rows.forEach(row => { 2046*9ccd446eSAtari911 if (row.textContent.includes(filename)) { 2047*9ccd446eSAtari911 row.style.transition = \'opacity 0.3s\'; 2048*9ccd446eSAtari911 row.style.opacity = \'0\'; 2049*9ccd446eSAtari911 setTimeout(() => { 2050*9ccd446eSAtari911 row.remove(); 2051*9ccd446eSAtari911 // Check if table is now empty 2052*9ccd446eSAtari911 const tbody = document.querySelector(\'#backupTable tbody\'); 2053*9ccd446eSAtari911 if (tbody && tbody.children.length === 0) { 2054*9ccd446eSAtari911 const backupSection = document.querySelector(\'#backupSection\'); 2055*9ccd446eSAtari911 if (backupSection) { 2056*9ccd446eSAtari911 backupSection.style.transition = \'opacity 0.3s\'; 2057*9ccd446eSAtari911 backupSection.style.opacity = \'0\'; 2058*9ccd446eSAtari911 setTimeout(() => backupSection.remove(), 300); 2059*9ccd446eSAtari911 } 2060*9ccd446eSAtari911 } 2061*9ccd446eSAtari911 }, 300); 2062*9ccd446eSAtari911 } 2063*9ccd446eSAtari911 }); 20641d05cddcSAtari911 2065*9ccd446eSAtari911 // Show success message 2066*9ccd446eSAtari911 const msg = document.createElement(\'div\'); 2067*9ccd446eSAtari911 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;\'; 2068*9ccd446eSAtari911 msg.textContent = \'✓ Backup deleted: \' + filename; 2069*9ccd446eSAtari911 document.querySelector(\'h2\').after(msg); 2070*9ccd446eSAtari911 setTimeout(() => { 2071*9ccd446eSAtari911 msg.style.opacity = \'0\'; 2072*9ccd446eSAtari911 setTimeout(() => msg.remove(), 300); 2073*9ccd446eSAtari911 }, 3000); 2074*9ccd446eSAtari911 }) 2075*9ccd446eSAtari911 .catch(error => { 2076*9ccd446eSAtari911 alert(\'Error deleting backup: \' + error); 2077*9ccd446eSAtari911 }); 20781d05cddcSAtari911 } 20791d05cddcSAtari911 20801d05cddcSAtari911 function restoreBackup(filename) { 20811d05cddcSAtari911 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?")) { 20821d05cddcSAtari911 return; 20831d05cddcSAtari911 } 20841d05cddcSAtari911 20851d05cddcSAtari911 const form = document.createElement("form"); 20861d05cddcSAtari911 form.method = "POST"; 20871d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=update"; 20881d05cddcSAtari911 20891d05cddcSAtari911 const actionInput = document.createElement("input"); 20901d05cddcSAtari911 actionInput.type = "hidden"; 20911d05cddcSAtari911 actionInput.name = "action"; 20921d05cddcSAtari911 actionInput.value = "restore_backup"; 20931d05cddcSAtari911 form.appendChild(actionInput); 20941d05cddcSAtari911 20951d05cddcSAtari911 const filenameInput = document.createElement("input"); 20961d05cddcSAtari911 filenameInput.type = "hidden"; 20971d05cddcSAtari911 filenameInput.name = "backup_file"; 20981d05cddcSAtari911 filenameInput.value = filename; 20991d05cddcSAtari911 form.appendChild(filenameInput); 21001d05cddcSAtari911 21011d05cddcSAtari911 document.body.appendChild(form); 21021d05cddcSAtari911 form.submit(); 21031d05cddcSAtari911 } 21041d05cddcSAtari911 21051d05cddcSAtari911 function renameBackup(filename) { 21061d05cddcSAtari911 const newName = prompt("Enter new backup name (without .zip extension):\\n\\nCurrent: " + filename.replace(/\\.zip$/, ""), filename.replace(/\\.zip$/, "")); 21071d05cddcSAtari911 if (!newName || newName === filename.replace(/\\.zip$/, "")) { 21081d05cddcSAtari911 return; 21091d05cddcSAtari911 } 21101d05cddcSAtari911 21111d05cddcSAtari911 // Add .zip if not present 21121d05cddcSAtari911 const newFilename = newName.endsWith(".zip") ? newName : newName + ".zip"; 21131d05cddcSAtari911 21141d05cddcSAtari911 // Basic validation 21151d05cddcSAtari911 if (!/^[a-zA-Z0-9._-]+$/.test(newFilename.replace(/\\.zip$/, ""))) { 21161d05cddcSAtari911 alert("Invalid filename. Use only letters, numbers, dots, dashes, and underscores."); 21171d05cddcSAtari911 return; 21181d05cddcSAtari911 } 21191d05cddcSAtari911 21201d05cddcSAtari911 const form = document.createElement("form"); 21211d05cddcSAtari911 form.method = "POST"; 21221d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=update"; 21231d05cddcSAtari911 21241d05cddcSAtari911 const actionInput = document.createElement("input"); 21251d05cddcSAtari911 actionInput.type = "hidden"; 21261d05cddcSAtari911 actionInput.name = "action"; 21271d05cddcSAtari911 actionInput.value = "rename_backup"; 21281d05cddcSAtari911 form.appendChild(actionInput); 21291d05cddcSAtari911 21301d05cddcSAtari911 const oldNameInput = document.createElement("input"); 21311d05cddcSAtari911 oldNameInput.type = "hidden"; 21321d05cddcSAtari911 oldNameInput.name = "old_name"; 21331d05cddcSAtari911 oldNameInput.value = filename; 21341d05cddcSAtari911 form.appendChild(oldNameInput); 21351d05cddcSAtari911 21361d05cddcSAtari911 const newNameInput = document.createElement("input"); 21371d05cddcSAtari911 newNameInput.type = "hidden"; 21381d05cddcSAtari911 newNameInput.name = "new_name"; 21391d05cddcSAtari911 newNameInput.value = newFilename; 21401d05cddcSAtari911 form.appendChild(newNameInput); 21411d05cddcSAtari911 21421d05cddcSAtari911 document.body.appendChild(form); 21431d05cddcSAtari911 form.submit(); 21441d05cddcSAtari911 } 21451d05cddcSAtari911 </script>'; 21461d05cddcSAtari911 } 21471d05cddcSAtari911 21481d05cddcSAtari911 private function saveConfig() { 21491d05cddcSAtari911 global $INPUT; 21501d05cddcSAtari911 21511d05cddcSAtari911 // Load existing config to preserve all settings 21521d05cddcSAtari911 $configFile = DOKU_PLUGIN . 'calendar/sync_config.php'; 21531d05cddcSAtari911 $existingConfig = []; 21541d05cddcSAtari911 if (file_exists($configFile)) { 21551d05cddcSAtari911 $existingConfig = include $configFile; 21561d05cddcSAtari911 } 21571d05cddcSAtari911 21581d05cddcSAtari911 // Update only the fields from the form - preserve everything else 21591d05cddcSAtari911 $config = $existingConfig; 21601d05cddcSAtari911 21611d05cddcSAtari911 // Update basic fields 21621d05cddcSAtari911 $config['tenant_id'] = $INPUT->str('tenant_id'); 21631d05cddcSAtari911 $config['client_id'] = $INPUT->str('client_id'); 21641d05cddcSAtari911 $config['client_secret'] = $INPUT->str('client_secret'); 21651d05cddcSAtari911 $config['user_email'] = $INPUT->str('user_email'); 21661d05cddcSAtari911 $config['timezone'] = $INPUT->str('timezone', 'America/Los_Angeles'); 21671d05cddcSAtari911 $config['default_category'] = $INPUT->str('default_category', 'Blue category'); 21681d05cddcSAtari911 $config['reminder_minutes'] = $INPUT->int('reminder_minutes', 15); 21691d05cddcSAtari911 $config['sync_completed_tasks'] = $INPUT->bool('sync_completed_tasks'); 21701d05cddcSAtari911 $config['delete_outlook_events'] = $INPUT->bool('delete_outlook_events'); 21711d05cddcSAtari911 $config['sync_all_namespaces'] = $INPUT->bool('sync_all_namespaces'); 21721d05cddcSAtari911 $config['sync_namespaces'] = $INPUT->arr('sync_namespaces'); 21731d05cddcSAtari911 $config['important_namespaces'] = $INPUT->str('important_namespaces', 'important'); 21741d05cddcSAtari911 21751d05cddcSAtari911 // Parse category mapping 21761d05cddcSAtari911 $config['category_mapping'] = []; 21771d05cddcSAtari911 $mappingText = $INPUT->str('category_mapping'); 21781d05cddcSAtari911 if ($mappingText) { 21791d05cddcSAtari911 $lines = explode("\n", $mappingText); 21801d05cddcSAtari911 foreach ($lines as $line) { 21811d05cddcSAtari911 $line = trim($line); 21821d05cddcSAtari911 if (empty($line)) continue; 21831d05cddcSAtari911 $parts = explode('=', $line, 2); 21841d05cddcSAtari911 if (count($parts) === 2) { 21851d05cddcSAtari911 $config['category_mapping'][trim($parts[0])] = trim($parts[1]); 21861d05cddcSAtari911 } 21871d05cddcSAtari911 } 21881d05cddcSAtari911 } 21891d05cddcSAtari911 21901d05cddcSAtari911 // Parse color mapping from dropdown selections 21911d05cddcSAtari911 $config['color_mapping'] = []; 21921d05cddcSAtari911 $colorMappingCount = $INPUT->int('color_mapping_count', 0); 21931d05cddcSAtari911 for ($i = 0; $i < $colorMappingCount; $i++) { 21941d05cddcSAtari911 $hexColor = $INPUT->str('color_hex_' . $i); 21951d05cddcSAtari911 $category = $INPUT->str('color_map_' . $i); 21961d05cddcSAtari911 21971d05cddcSAtari911 if (!empty($hexColor) && !empty($category)) { 21981d05cddcSAtari911 $config['color_mapping'][$hexColor] = $category; 21991d05cddcSAtari911 } 22001d05cddcSAtari911 } 22011d05cddcSAtari911 22021d05cddcSAtari911 // Build file content using return format 22031d05cddcSAtari911 $content = "<?php\n"; 22041d05cddcSAtari911 $content .= "/**\n"; 22051d05cddcSAtari911 $content .= " * DokuWiki Calendar → Outlook Sync - Configuration\n"; 22061d05cddcSAtari911 $content .= " * \n"; 22071d05cddcSAtari911 $content .= " * SECURITY: Add this file to .gitignore!\n"; 22081d05cddcSAtari911 $content .= " * Never commit credentials to version control.\n"; 22091d05cddcSAtari911 $content .= " */\n\n"; 22101d05cddcSAtari911 $content .= "return " . var_export($config, true) . ";\n"; 22111d05cddcSAtari911 22121d05cddcSAtari911 // Save file 22131d05cddcSAtari911 if (file_put_contents($configFile, $content)) { 22141d05cddcSAtari911 $this->redirect('Configuration saved successfully!', 'success'); 22151d05cddcSAtari911 } else { 22161d05cddcSAtari911 $this->redirect('Error: Could not save configuration file', 'error'); 22171d05cddcSAtari911 } 22181d05cddcSAtari911 } 22191d05cddcSAtari911 22201d05cddcSAtari911 private function clearCache() { 22211d05cddcSAtari911 // Clear DokuWiki cache 22221d05cddcSAtari911 $cacheDir = DOKU_INC . 'data/cache'; 22231d05cddcSAtari911 22241d05cddcSAtari911 if (is_dir($cacheDir)) { 22251d05cddcSAtari911 $this->recursiveDelete($cacheDir, false); 22261d05cddcSAtari911 $this->redirect('Cache cleared successfully!', 'success', 'update'); 22271d05cddcSAtari911 } else { 22281d05cddcSAtari911 $this->redirect('Cache directory not found', 'error', 'update'); 22291d05cddcSAtari911 } 22301d05cddcSAtari911 } 22311d05cddcSAtari911 22321d05cddcSAtari911 private function recursiveDelete($dir, $deleteRoot = true) { 22331d05cddcSAtari911 if (!is_dir($dir)) return; 22341d05cddcSAtari911 22351d05cddcSAtari911 $files = array_diff(scandir($dir), array('.', '..')); 22361d05cddcSAtari911 foreach ($files as $file) { 22371d05cddcSAtari911 $path = $dir . '/' . $file; 22381d05cddcSAtari911 if (is_dir($path)) { 22391d05cddcSAtari911 $this->recursiveDelete($path, true); 22401d05cddcSAtari911 } else { 22411d05cddcSAtari911 @unlink($path); 22421d05cddcSAtari911 } 22431d05cddcSAtari911 } 22441d05cddcSAtari911 22451d05cddcSAtari911 if ($deleteRoot) { 22461d05cddcSAtari911 @rmdir($dir); 22471d05cddcSAtari911 } 22481d05cddcSAtari911 } 22491d05cddcSAtari911 22501d05cddcSAtari911 private function findRecurringEvents() { 22511d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 22521d05cddcSAtari911 $recurring = []; 22531d05cddcSAtari911 $allEvents = []; // Track all events to detect patterns 22541d05cddcSAtari911 22551d05cddcSAtari911 // Check root calendar directory first (blank/default namespace) 22561d05cddcSAtari911 $rootCalendarDir = $dataDir . 'calendar'; 22571d05cddcSAtari911 if (is_dir($rootCalendarDir)) { 22581d05cddcSAtari911 foreach (glob($rootCalendarDir . '/*.json') as $file) { 22591d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 22601d05cddcSAtari911 if (!$data) continue; 22611d05cddcSAtari911 22621d05cddcSAtari911 foreach ($data as $dateKey => $events) { 22631d05cddcSAtari911 foreach ($events as $event) { 22641d05cddcSAtari911 // Group by title + namespace (events with same title are likely recurring) 22651d05cddcSAtari911 $groupKey = strtolower(trim($event['title'])) . '_'; 22661d05cddcSAtari911 22671d05cddcSAtari911 if (!isset($allEvents[$groupKey])) { 22681d05cddcSAtari911 $allEvents[$groupKey] = [ 22691d05cddcSAtari911 'title' => $event['title'], 22701d05cddcSAtari911 'namespace' => '', 22711d05cddcSAtari911 'dates' => [], 22721d05cddcSAtari911 'events' => [] 22731d05cddcSAtari911 ]; 22741d05cddcSAtari911 } 22751d05cddcSAtari911 $allEvents[$groupKey]['dates'][] = $dateKey; 22761d05cddcSAtari911 $allEvents[$groupKey]['events'][] = $event; 22771d05cddcSAtari911 } 22781d05cddcSAtari911 } 22791d05cddcSAtari911 } 22801d05cddcSAtari911 } 22811d05cddcSAtari911 22821d05cddcSAtari911 // Scan all namespace directories 22831d05cddcSAtari911 foreach (glob($dataDir . '*', GLOB_ONLYDIR) as $nsDir) { 22841d05cddcSAtari911 $namespace = basename($nsDir); 22851d05cddcSAtari911 22861d05cddcSAtari911 // Skip the root 'calendar' dir (already processed above) 22871d05cddcSAtari911 if ($namespace === 'calendar') continue; 22881d05cddcSAtari911 22891d05cddcSAtari911 $calendarDir = $nsDir . '/calendar'; 22901d05cddcSAtari911 22911d05cddcSAtari911 if (!is_dir($calendarDir)) continue; 22921d05cddcSAtari911 22931d05cddcSAtari911 // Scan all calendar files 22941d05cddcSAtari911 foreach (glob($calendarDir . '/*.json') as $file) { 22951d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 22961d05cddcSAtari911 if (!$data) continue; 22971d05cddcSAtari911 22981d05cddcSAtari911 foreach ($data as $dateKey => $events) { 22991d05cddcSAtari911 foreach ($events as $event) { 23001d05cddcSAtari911 $groupKey = strtolower(trim($event['title'])) . '_' . ($event['namespace'] ?? ''); 23011d05cddcSAtari911 23021d05cddcSAtari911 if (!isset($allEvents[$groupKey])) { 23031d05cddcSAtari911 $allEvents[$groupKey] = [ 23041d05cddcSAtari911 'title' => $event['title'], 23051d05cddcSAtari911 'namespace' => $event['namespace'] ?? '', 23061d05cddcSAtari911 'dates' => [], 23071d05cddcSAtari911 'events' => [] 23081d05cddcSAtari911 ]; 23091d05cddcSAtari911 } 23101d05cddcSAtari911 $allEvents[$groupKey]['dates'][] = $dateKey; 23111d05cddcSAtari911 $allEvents[$groupKey]['events'][] = $event; 23121d05cddcSAtari911 } 23131d05cddcSAtari911 } 23141d05cddcSAtari911 } 23151d05cddcSAtari911 } 23161d05cddcSAtari911 23171d05cddcSAtari911 // Analyze patterns - only include if 3+ occurrences 23181d05cddcSAtari911 foreach ($allEvents as $groupKey => $group) { 23191d05cddcSAtari911 if (count($group['dates']) >= 3) { 23201d05cddcSAtari911 // Sort dates 23211d05cddcSAtari911 sort($group['dates']); 23221d05cddcSAtari911 23231d05cddcSAtari911 // Calculate interval between first and second occurrence 23241d05cddcSAtari911 $date1 = new DateTime($group['dates'][0]); 23251d05cddcSAtari911 $date2 = new DateTime($group['dates'][1]); 23261d05cddcSAtari911 $interval = $date1->diff($date2); 23271d05cddcSAtari911 23281d05cddcSAtari911 // Determine pattern 23291d05cddcSAtari911 $pattern = 'Custom'; 23301d05cddcSAtari911 if ($interval->days == 1) { 23311d05cddcSAtari911 $pattern = 'Daily'; 23321d05cddcSAtari911 } elseif ($interval->days == 7) { 23331d05cddcSAtari911 $pattern = 'Weekly'; 23341d05cddcSAtari911 } elseif ($interval->days >= 14 && $interval->days <= 16) { 23351d05cddcSAtari911 $pattern = 'Bi-weekly'; 23361d05cddcSAtari911 } elseif ($interval->days >= 28 && $interval->days <= 31) { 23371d05cddcSAtari911 $pattern = 'Monthly'; 23381d05cddcSAtari911 } elseif ($interval->days >= 365 && $interval->days <= 366) { 23391d05cddcSAtari911 $pattern = 'Yearly'; 23401d05cddcSAtari911 } 23411d05cddcSAtari911 23421d05cddcSAtari911 // Use first event's ID or create a synthetic one 23431d05cddcSAtari911 $baseId = isset($group['events'][0]['recurringId']) 23441d05cddcSAtari911 ? $group['events'][0]['recurringId'] 23451d05cddcSAtari911 : md5($group['title'] . $group['namespace']); 23461d05cddcSAtari911 23471d05cddcSAtari911 $recurring[] = [ 23481d05cddcSAtari911 'baseId' => $baseId, 23491d05cddcSAtari911 'title' => $group['title'], 23501d05cddcSAtari911 'namespace' => $group['namespace'], 23511d05cddcSAtari911 'pattern' => $pattern, 23521d05cddcSAtari911 'count' => count($group['dates']), 23531d05cddcSAtari911 'firstDate' => $group['dates'][0], 23541d05cddcSAtari911 'interval' => $interval->days 23551d05cddcSAtari911 ]; 23561d05cddcSAtari911 } 23571d05cddcSAtari911 } 23581d05cddcSAtari911 23591d05cddcSAtari911 return $recurring; 23601d05cddcSAtari911 } 23611d05cddcSAtari911 23621d05cddcSAtari911 private function getEventsByNamespace() { 23631d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 23641d05cddcSAtari911 $result = []; 23651d05cddcSAtari911 23661d05cddcSAtari911 // Check root calendar directory first (blank/default namespace) 23671d05cddcSAtari911 $rootCalendarDir = $dataDir . 'calendar'; 23681d05cddcSAtari911 if (is_dir($rootCalendarDir)) { 23691d05cddcSAtari911 $hasFiles = false; 23701d05cddcSAtari911 $events = []; 23711d05cddcSAtari911 23721d05cddcSAtari911 foreach (glob($rootCalendarDir . '/*.json') as $file) { 23731d05cddcSAtari911 $hasFiles = true; 23741d05cddcSAtari911 $month = basename($file, '.json'); 23751d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 23761d05cddcSAtari911 if (!$data) continue; 23771d05cddcSAtari911 23781d05cddcSAtari911 foreach ($data as $dateKey => $eventList) { 23791d05cddcSAtari911 foreach ($eventList as $event) { 23801d05cddcSAtari911 $events[] = [ 23811d05cddcSAtari911 'id' => $event['id'], 23821d05cddcSAtari911 'title' => $event['title'], 23831d05cddcSAtari911 'date' => $dateKey, 23841d05cddcSAtari911 'startTime' => $event['startTime'] ?? null, 23851d05cddcSAtari911 'month' => $month 23861d05cddcSAtari911 ]; 23871d05cddcSAtari911 } 23881d05cddcSAtari911 } 23891d05cddcSAtari911 } 23901d05cddcSAtari911 23911d05cddcSAtari911 // Add if it has JSON files (even if empty) 23921d05cddcSAtari911 if ($hasFiles) { 23931d05cddcSAtari911 $result[''] = ['events' => $events]; 23941d05cddcSAtari911 } 23951d05cddcSAtari911 } 23961d05cddcSAtari911 23971d05cddcSAtari911 // Recursively scan all namespace directories including sub-namespaces 23981d05cddcSAtari911 $this->scanNamespaceRecursive($dataDir, '', $result); 23991d05cddcSAtari911 24001d05cddcSAtari911 // Sort namespaces, but keep '' (default) first 24011d05cddcSAtari911 uksort($result, function($a, $b) { 24021d05cddcSAtari911 if ($a === '') return -1; 24031d05cddcSAtari911 if ($b === '') return 1; 24041d05cddcSAtari911 return strcmp($a, $b); 24051d05cddcSAtari911 }); 24061d05cddcSAtari911 24071d05cddcSAtari911 return $result; 24081d05cddcSAtari911 } 24091d05cddcSAtari911 24101d05cddcSAtari911 private function scanNamespaceRecursive($baseDir, $parentNamespace, &$result) { 24111d05cddcSAtari911 foreach (glob($baseDir . '*', GLOB_ONLYDIR) as $nsDir) { 24121d05cddcSAtari911 $dirName = basename($nsDir); 24131d05cddcSAtari911 24141d05cddcSAtari911 // Skip the root 'calendar' dir 24151d05cddcSAtari911 if ($dirName === 'calendar' && empty($parentNamespace)) continue; 24161d05cddcSAtari911 24171d05cddcSAtari911 // Build namespace path 24181d05cddcSAtari911 $namespace = empty($parentNamespace) ? $dirName : $parentNamespace . ':' . $dirName; 24191d05cddcSAtari911 24201d05cddcSAtari911 // Check for calendar directory 24211d05cddcSAtari911 $calendarDir = $nsDir . '/calendar'; 24221d05cddcSAtari911 if (is_dir($calendarDir)) { 24231d05cddcSAtari911 $hasFiles = false; 24241d05cddcSAtari911 $events = []; 24251d05cddcSAtari911 24261d05cddcSAtari911 // Scan all calendar files 24271d05cddcSAtari911 foreach (glob($calendarDir . '/*.json') as $file) { 24281d05cddcSAtari911 $hasFiles = true; 24291d05cddcSAtari911 $month = basename($file, '.json'); 24301d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 24311d05cddcSAtari911 if (!$data) continue; 24321d05cddcSAtari911 24331d05cddcSAtari911 foreach ($data as $dateKey => $eventList) { 24341d05cddcSAtari911 foreach ($eventList as $event) { 24351d05cddcSAtari911 $events[] = [ 24361d05cddcSAtari911 'id' => $event['id'], 24371d05cddcSAtari911 'title' => $event['title'], 24381d05cddcSAtari911 'date' => $dateKey, 24391d05cddcSAtari911 'startTime' => $event['startTime'] ?? null, 24401d05cddcSAtari911 'month' => $month 24411d05cddcSAtari911 ]; 24421d05cddcSAtari911 } 24431d05cddcSAtari911 } 24441d05cddcSAtari911 } 24451d05cddcSAtari911 24461d05cddcSAtari911 // Add namespace if it has JSON files (even if empty) 24471d05cddcSAtari911 if ($hasFiles) { 24481d05cddcSAtari911 $result[$namespace] = ['events' => $events]; 24491d05cddcSAtari911 } 24501d05cddcSAtari911 } 24511d05cddcSAtari911 24521d05cddcSAtari911 // Recursively scan sub-directories 24531d05cddcSAtari911 $this->scanNamespaceRecursive($nsDir . '/', $namespace, $result); 24541d05cddcSAtari911 } 24551d05cddcSAtari911 } 24561d05cddcSAtari911 24571d05cddcSAtari911 private function getAllNamespaces() { 24581d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 24591d05cddcSAtari911 $namespaces = []; 24601d05cddcSAtari911 24611d05cddcSAtari911 // Check root calendar directory first 24621d05cddcSAtari911 $rootCalendarDir = $dataDir . 'calendar'; 24631d05cddcSAtari911 if (is_dir($rootCalendarDir)) { 24641d05cddcSAtari911 $namespaces[] = ''; // Blank/default namespace 24651d05cddcSAtari911 } 24661d05cddcSAtari911 24671d05cddcSAtari911 // Check all other namespace directories 24681d05cddcSAtari911 foreach (glob($dataDir . '*', GLOB_ONLYDIR) as $nsDir) { 24691d05cddcSAtari911 $namespace = basename($nsDir); 24701d05cddcSAtari911 24711d05cddcSAtari911 // Skip the root 'calendar' dir (already added as '') 24721d05cddcSAtari911 if ($namespace === 'calendar') continue; 24731d05cddcSAtari911 24741d05cddcSAtari911 $calendarDir = $nsDir . '/calendar'; 24751d05cddcSAtari911 if (is_dir($calendarDir)) { 24761d05cddcSAtari911 $namespaces[] = $namespace; 24771d05cddcSAtari911 } 24781d05cddcSAtari911 } 24791d05cddcSAtari911 24801d05cddcSAtari911 return $namespaces; 24811d05cddcSAtari911 } 24821d05cddcSAtari911 24831d05cddcSAtari911 private function searchEvents($search, $filterNamespace) { 24841d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 24851d05cddcSAtari911 $results = []; 24861d05cddcSAtari911 24871d05cddcSAtari911 $search = strtolower(trim($search)); 24881d05cddcSAtari911 24891d05cddcSAtari911 foreach (glob($dataDir . '*', GLOB_ONLYDIR) as $nsDir) { 24901d05cddcSAtari911 $namespace = basename($nsDir); 24911d05cddcSAtari911 $calendarDir = $nsDir . '/calendar'; 24921d05cddcSAtari911 24931d05cddcSAtari911 if (!is_dir($calendarDir)) continue; 24941d05cddcSAtari911 if ($filterNamespace !== '' && $namespace !== $filterNamespace) continue; 24951d05cddcSAtari911 24961d05cddcSAtari911 foreach (glob($calendarDir . '/*.json') as $file) { 24971d05cddcSAtari911 $month = basename($file, '.json'); 24981d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 24991d05cddcSAtari911 if (!$data) continue; 25001d05cddcSAtari911 25011d05cddcSAtari911 foreach ($data as $dateKey => $events) { 25021d05cddcSAtari911 foreach ($events as $event) { 25031d05cddcSAtari911 if ($search === '' || strpos(strtolower($event['title']), $search) !== false) { 25041d05cddcSAtari911 $results[] = [ 25051d05cddcSAtari911 'id' => $event['id'], 25061d05cddcSAtari911 'title' => $event['title'], 25071d05cddcSAtari911 'date' => $dateKey, 25081d05cddcSAtari911 'startTime' => $event['startTime'] ?? null, 25091d05cddcSAtari911 'namespace' => $event['namespace'] ?? '', 25101d05cddcSAtari911 'month' => $month 25111d05cddcSAtari911 ]; 25121d05cddcSAtari911 } 25131d05cddcSAtari911 } 25141d05cddcSAtari911 } 25151d05cddcSAtari911 } 25161d05cddcSAtari911 } 25171d05cddcSAtari911 25181d05cddcSAtari911 return $results; 25191d05cddcSAtari911 } 25201d05cddcSAtari911 25211d05cddcSAtari911 private function deleteRecurringSeries() { 25221d05cddcSAtari911 global $INPUT; 25231d05cddcSAtari911 25241d05cddcSAtari911 $eventTitle = $INPUT->str('event_title'); 25251d05cddcSAtari911 $namespace = $INPUT->str('namespace'); 25261d05cddcSAtari911 25271d05cddcSAtari911 // Determine calendar directory 25281d05cddcSAtari911 if ($namespace === '') { 25291d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/calendar'; 25301d05cddcSAtari911 } else { 25311d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/' . $namespace . '/calendar'; 25321d05cddcSAtari911 } 25331d05cddcSAtari911 25341d05cddcSAtari911 $count = 0; 25351d05cddcSAtari911 25361d05cddcSAtari911 if (is_dir($dataDir)) { 25371d05cddcSAtari911 foreach (glob($dataDir . '/*.json') as $file) { 25381d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 25391d05cddcSAtari911 if (!$data) continue; 25401d05cddcSAtari911 25411d05cddcSAtari911 $modified = false; 25421d05cddcSAtari911 foreach ($data as $dateKey => $events) { 25431d05cddcSAtari911 $filtered = []; 25441d05cddcSAtari911 foreach ($events as $event) { 25451d05cddcSAtari911 // Match by title (case-insensitive) 25461d05cddcSAtari911 if (strtolower(trim($event['title'])) === strtolower(trim($eventTitle))) { 25471d05cddcSAtari911 $count++; 25481d05cddcSAtari911 $modified = true; 25491d05cddcSAtari911 } else { 25501d05cddcSAtari911 $filtered[] = $event; 25511d05cddcSAtari911 } 25521d05cddcSAtari911 } 25531d05cddcSAtari911 $data[$dateKey] = $filtered; 25541d05cddcSAtari911 } 25551d05cddcSAtari911 25561d05cddcSAtari911 if ($modified) { 2557*9ccd446eSAtari911 // Clean up empty date keys 2558*9ccd446eSAtari911 foreach ($data as $dk => $evts) { 2559*9ccd446eSAtari911 if (empty($evts)) unset($data[$dk]); 2560*9ccd446eSAtari911 } 2561*9ccd446eSAtari911 2562*9ccd446eSAtari911 if (empty($data)) { 2563*9ccd446eSAtari911 unlink($file); 2564*9ccd446eSAtari911 } else { 25651d05cddcSAtari911 file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); 25661d05cddcSAtari911 } 25671d05cddcSAtari911 } 25681d05cddcSAtari911 } 2569*9ccd446eSAtari911 } 25701d05cddcSAtari911 2571*9ccd446eSAtari911 $this->clearStatsCache(); 25721d05cddcSAtari911 $this->redirect("Deleted $count occurrences of recurring event: " . $eventTitle, 'success', 'manage'); 25731d05cddcSAtari911 } 25741d05cddcSAtari911 25751d05cddcSAtari911 private function editRecurringSeries() { 25761d05cddcSAtari911 global $INPUT; 25771d05cddcSAtari911 25781d05cddcSAtari911 $oldTitle = $INPUT->str('old_title'); 25791d05cddcSAtari911 $oldNamespace = $INPUT->str('old_namespace'); 25801d05cddcSAtari911 $newTitle = $INPUT->str('new_title'); 25811d05cddcSAtari911 $startTime = $INPUT->str('start_time'); 25821d05cddcSAtari911 $endTime = $INPUT->str('end_time'); 25831d05cddcSAtari911 $interval = $INPUT->int('interval', 0); 25841d05cddcSAtari911 $newNamespace = $INPUT->str('new_namespace'); 25851d05cddcSAtari911 25861d05cddcSAtari911 // Use old namespace if new namespace is empty (keep current) 25871d05cddcSAtari911 if (empty($newNamespace) && !isset($_POST['new_namespace'])) { 25881d05cddcSAtari911 $newNamespace = $oldNamespace; 25891d05cddcSAtari911 } 25901d05cddcSAtari911 25911d05cddcSAtari911 // Determine old calendar directory 25921d05cddcSAtari911 if ($oldNamespace === '') { 25931d05cddcSAtari911 $oldDataDir = DOKU_INC . 'data/meta/calendar'; 25941d05cddcSAtari911 } else { 25951d05cddcSAtari911 $oldDataDir = DOKU_INC . 'data/meta/' . $oldNamespace . '/calendar'; 25961d05cddcSAtari911 } 25971d05cddcSAtari911 25981d05cddcSAtari911 $count = 0; 25991d05cddcSAtari911 $eventsToMove = []; 2600*9ccd446eSAtari911 $firstEventDate = null; 26011d05cddcSAtari911 26021d05cddcSAtari911 if (is_dir($oldDataDir)) { 26031d05cddcSAtari911 foreach (glob($oldDataDir . '/*.json') as $file) { 26041d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 26051d05cddcSAtari911 if (!$data) continue; 26061d05cddcSAtari911 26071d05cddcSAtari911 $modified = false; 26081d05cddcSAtari911 foreach ($data as $dateKey => $events) { 26091d05cddcSAtari911 foreach ($events as $key => $event) { 26101d05cddcSAtari911 // Match by old title (case-insensitive) 26111d05cddcSAtari911 if (strtolower(trim($event['title'])) === strtolower(trim($oldTitle))) { 26121d05cddcSAtari911 // Update the title 26131d05cddcSAtari911 $data[$dateKey][$key]['title'] = $newTitle; 26141d05cddcSAtari911 26151d05cddcSAtari911 // Update start time if provided 26161d05cddcSAtari911 if (!empty($startTime)) { 2617*9ccd446eSAtari911 $data[$dateKey][$key]['time'] = $startTime; 26181d05cddcSAtari911 } 26191d05cddcSAtari911 26201d05cddcSAtari911 // Update end time if provided 26211d05cddcSAtari911 if (!empty($endTime)) { 2622*9ccd446eSAtari911 $data[$dateKey][$key]['endTime'] = $endTime; 26231d05cddcSAtari911 } 26241d05cddcSAtari911 26251d05cddcSAtari911 // Update namespace 26261d05cddcSAtari911 $data[$dateKey][$key]['namespace'] = $newNamespace; 26271d05cddcSAtari911 26281d05cddcSAtari911 // If changing interval, calculate new date 26291d05cddcSAtari911 if ($interval > 0 && $count > 0) { 26301d05cddcSAtari911 // Get the first event date as base 26311d05cddcSAtari911 if (empty($firstEventDate)) { 26321d05cddcSAtari911 $firstEventDate = $dateKey; 26331d05cddcSAtari911 } 26341d05cddcSAtari911 26351d05cddcSAtari911 // Calculate new date based on interval 26361d05cddcSAtari911 $newDate = date('Y-m-d', strtotime($firstEventDate . ' +' . ($count * $interval) . ' days')); 26371d05cddcSAtari911 26381d05cddcSAtari911 // Store for moving 26391d05cddcSAtari911 $eventsToMove[] = [ 26401d05cddcSAtari911 'oldDate' => $dateKey, 26411d05cddcSAtari911 'newDate' => $newDate, 26421d05cddcSAtari911 'event' => $data[$dateKey][$key], 26431d05cddcSAtari911 'key' => $key 26441d05cddcSAtari911 ]; 26451d05cddcSAtari911 } 26461d05cddcSAtari911 26471d05cddcSAtari911 $count++; 26481d05cddcSAtari911 $modified = true; 26491d05cddcSAtari911 } 26501d05cddcSAtari911 } 26511d05cddcSAtari911 } 26521d05cddcSAtari911 26531d05cddcSAtari911 if ($modified) { 26541d05cddcSAtari911 file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); 26551d05cddcSAtari911 } 26561d05cddcSAtari911 } 26571d05cddcSAtari911 26581d05cddcSAtari911 // Handle interval changes by moving events to new dates 26591d05cddcSAtari911 if (!empty($eventsToMove)) { 26601d05cddcSAtari911 // Remove from old dates first 26611d05cddcSAtari911 foreach (glob($oldDataDir . '/*.json') as $file) { 26621d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 26631d05cddcSAtari911 if (!$data) continue; 26641d05cddcSAtari911 26651d05cddcSAtari911 $modified = false; 26661d05cddcSAtari911 foreach ($eventsToMove as $moveData) { 26671d05cddcSAtari911 $oldMonth = substr($moveData['oldDate'], 0, 7); 26681d05cddcSAtari911 $fileMonth = basename($file, '.json'); 26691d05cddcSAtari911 26701d05cddcSAtari911 if ($oldMonth === $fileMonth && isset($data[$moveData['oldDate']])) { 26711d05cddcSAtari911 foreach ($data[$moveData['oldDate']] as $k => $evt) { 26721d05cddcSAtari911 if ($evt['id'] === $moveData['event']['id']) { 26731d05cddcSAtari911 unset($data[$moveData['oldDate']][$k]); 26741d05cddcSAtari911 $data[$moveData['oldDate']] = array_values($data[$moveData['oldDate']]); 26751d05cddcSAtari911 $modified = true; 26761d05cddcSAtari911 } 26771d05cddcSAtari911 } 26781d05cddcSAtari911 } 26791d05cddcSAtari911 } 26801d05cddcSAtari911 26811d05cddcSAtari911 if ($modified) { 26821d05cddcSAtari911 file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); 26831d05cddcSAtari911 } 26841d05cddcSAtari911 } 26851d05cddcSAtari911 26861d05cddcSAtari911 // Add to new dates 26871d05cddcSAtari911 foreach ($eventsToMove as $moveData) { 26881d05cddcSAtari911 $newMonth = substr($moveData['newDate'], 0, 7); 26891d05cddcSAtari911 $targetDir = ($newNamespace === '') ? DOKU_INC . 'data/meta/calendar' : DOKU_INC . 'data/meta/' . $newNamespace . '/calendar'; 26901d05cddcSAtari911 26911d05cddcSAtari911 if (!is_dir($targetDir)) { 26921d05cddcSAtari911 mkdir($targetDir, 0755, true); 26931d05cddcSAtari911 } 26941d05cddcSAtari911 26951d05cddcSAtari911 $targetFile = $targetDir . '/' . $newMonth . '.json'; 26961d05cddcSAtari911 $targetData = file_exists($targetFile) ? json_decode(file_get_contents($targetFile), true) : []; 26971d05cddcSAtari911 26981d05cddcSAtari911 if (!isset($targetData[$moveData['newDate']])) { 26991d05cddcSAtari911 $targetData[$moveData['newDate']] = []; 27001d05cddcSAtari911 } 27011d05cddcSAtari911 27021d05cddcSAtari911 $targetData[$moveData['newDate']][] = $moveData['event']; 27031d05cddcSAtari911 file_put_contents($targetFile, json_encode($targetData, JSON_PRETTY_PRINT)); 27041d05cddcSAtari911 } 27051d05cddcSAtari911 } 27061d05cddcSAtari911 27071d05cddcSAtari911 // Handle namespace change without interval change 27081d05cddcSAtari911 if ($newNamespace !== $oldNamespace && empty($eventsToMove)) { 27091d05cddcSAtari911 foreach (glob($oldDataDir . '/*.json') as $file) { 27101d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 27111d05cddcSAtari911 if (!$data) continue; 27121d05cddcSAtari911 27131d05cddcSAtari911 $month = basename($file, '.json'); 27141d05cddcSAtari911 $targetDir = ($newNamespace === '') ? DOKU_INC . 'data/meta/calendar' : DOKU_INC . 'data/meta/' . $newNamespace . '/calendar'; 27151d05cddcSAtari911 27161d05cddcSAtari911 if (!is_dir($targetDir)) { 27171d05cddcSAtari911 mkdir($targetDir, 0755, true); 27181d05cddcSAtari911 } 27191d05cddcSAtari911 27201d05cddcSAtari911 $targetFile = $targetDir . '/' . $month . '.json'; 27211d05cddcSAtari911 $targetData = file_exists($targetFile) ? json_decode(file_get_contents($targetFile), true) : []; 27221d05cddcSAtari911 27231d05cddcSAtari911 $modified = false; 27241d05cddcSAtari911 foreach ($data as $dateKey => $events) { 27251d05cddcSAtari911 foreach ($events as $k => $event) { 27261d05cddcSAtari911 if (isset($event['namespace']) && $event['namespace'] === $newNamespace && 27271d05cddcSAtari911 strtolower(trim($event['title'])) === strtolower(trim($newTitle))) { 27281d05cddcSAtari911 // Move this event 27291d05cddcSAtari911 if (!isset($targetData[$dateKey])) { 27301d05cddcSAtari911 $targetData[$dateKey] = []; 27311d05cddcSAtari911 } 27321d05cddcSAtari911 $targetData[$dateKey][] = $event; 27331d05cddcSAtari911 unset($data[$dateKey][$k]); 27341d05cddcSAtari911 $data[$dateKey] = array_values($data[$dateKey]); 27351d05cddcSAtari911 $modified = true; 27361d05cddcSAtari911 } 27371d05cddcSAtari911 } 27381d05cddcSAtari911 } 27391d05cddcSAtari911 27401d05cddcSAtari911 if ($modified) { 27411d05cddcSAtari911 file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); 27421d05cddcSAtari911 file_put_contents($targetFile, json_encode($targetData, JSON_PRETTY_PRINT)); 27431d05cddcSAtari911 } 27441d05cddcSAtari911 } 27451d05cddcSAtari911 } 27461d05cddcSAtari911 } 27471d05cddcSAtari911 27481d05cddcSAtari911 $changes = []; 27491d05cddcSAtari911 if ($oldTitle !== $newTitle) $changes[] = "title"; 27501d05cddcSAtari911 if (!empty($startTime) || !empty($endTime)) $changes[] = "time"; 27511d05cddcSAtari911 if ($interval > 0) $changes[] = "interval"; 27521d05cddcSAtari911 if ($newNamespace !== $oldNamespace) $changes[] = "namespace"; 27531d05cddcSAtari911 27541d05cddcSAtari911 $changeStr = !empty($changes) ? " (" . implode(", ", $changes) . ")" : ""; 2755*9ccd446eSAtari911 $this->clearStatsCache(); 27561d05cddcSAtari911 $this->redirect("Updated $count occurrences of recurring event$changeStr", 'success', 'manage'); 27571d05cddcSAtari911 } 27581d05cddcSAtari911 27591d05cddcSAtari911 private function moveEvents() { 27601d05cddcSAtari911 global $INPUT; 27611d05cddcSAtari911 27621d05cddcSAtari911 $events = $INPUT->arr('events'); 27631d05cddcSAtari911 $targetNamespace = $INPUT->str('target_namespace'); 27641d05cddcSAtari911 27651d05cddcSAtari911 if (empty($events)) { 27661d05cddcSAtari911 $this->redirect('No events selected', 'error', 'manage'); 27671d05cddcSAtari911 } 27681d05cddcSAtari911 27691d05cddcSAtari911 $moved = 0; 27701d05cddcSAtari911 27711d05cddcSAtari911 foreach ($events as $eventData) { 27721d05cddcSAtari911 list($id, $namespace, $date, $month) = explode('|', $eventData); 27731d05cddcSAtari911 27741d05cddcSAtari911 // Determine old file path 27751d05cddcSAtari911 if ($namespace === '') { 27761d05cddcSAtari911 $oldFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json'; 27771d05cddcSAtari911 } else { 27781d05cddcSAtari911 $oldFile = DOKU_INC . 'data/meta/' . $namespace . '/calendar/' . $month . '.json'; 27791d05cddcSAtari911 } 27801d05cddcSAtari911 27811d05cddcSAtari911 if (!file_exists($oldFile)) continue; 27821d05cddcSAtari911 27831d05cddcSAtari911 $oldData = json_decode(file_get_contents($oldFile), true); 27841d05cddcSAtari911 if (!$oldData) continue; 27851d05cddcSAtari911 27861d05cddcSAtari911 // Find and remove event from old file 27871d05cddcSAtari911 $event = null; 2788*9ccd446eSAtari911 if (isset($oldData[$date])) { 27891d05cddcSAtari911 foreach ($oldData[$date] as $key => $evt) { 27901d05cddcSAtari911 if ($evt['id'] === $id) { 27911d05cddcSAtari911 $event = $evt; 27921d05cddcSAtari911 unset($oldData[$date][$key]); 27931d05cddcSAtari911 $oldData[$date] = array_values($oldData[$date]); 27941d05cddcSAtari911 break; 27951d05cddcSAtari911 } 27961d05cddcSAtari911 } 27971d05cddcSAtari911 2798*9ccd446eSAtari911 // Remove empty date arrays 2799*9ccd446eSAtari911 if (empty($oldData[$date])) { 2800*9ccd446eSAtari911 unset($oldData[$date]); 2801*9ccd446eSAtari911 } 2802*9ccd446eSAtari911 } 2803*9ccd446eSAtari911 28041d05cddcSAtari911 if (!$event) continue; 28051d05cddcSAtari911 28061d05cddcSAtari911 // Save old file 28071d05cddcSAtari911 file_put_contents($oldFile, json_encode($oldData, JSON_PRETTY_PRINT)); 28081d05cddcSAtari911 28091d05cddcSAtari911 // Update event namespace 28101d05cddcSAtari911 $event['namespace'] = $targetNamespace; 28111d05cddcSAtari911 28121d05cddcSAtari911 // Determine new file path 28131d05cddcSAtari911 if ($targetNamespace === '') { 28141d05cddcSAtari911 $newFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json'; 28151d05cddcSAtari911 $newDir = dirname($newFile); 28161d05cddcSAtari911 } else { 28171d05cddcSAtari911 $newFile = DOKU_INC . 'data/meta/' . $targetNamespace . '/calendar/' . $month . '.json'; 28181d05cddcSAtari911 $newDir = dirname($newFile); 28191d05cddcSAtari911 } 28201d05cddcSAtari911 28211d05cddcSAtari911 if (!is_dir($newDir)) { 28221d05cddcSAtari911 mkdir($newDir, 0755, true); 28231d05cddcSAtari911 } 28241d05cddcSAtari911 28251d05cddcSAtari911 $newData = []; 28261d05cddcSAtari911 if (file_exists($newFile)) { 28271d05cddcSAtari911 $newData = json_decode(file_get_contents($newFile), true) ?: []; 28281d05cddcSAtari911 } 28291d05cddcSAtari911 28301d05cddcSAtari911 if (!isset($newData[$date])) { 28311d05cddcSAtari911 $newData[$date] = []; 28321d05cddcSAtari911 } 28331d05cddcSAtari911 $newData[$date][] = $event; 28341d05cddcSAtari911 28351d05cddcSAtari911 file_put_contents($newFile, json_encode($newData, JSON_PRETTY_PRINT)); 28361d05cddcSAtari911 $moved++; 28371d05cddcSAtari911 } 28381d05cddcSAtari911 28391d05cddcSAtari911 $displayTarget = $targetNamespace ?: '(default)'; 2840*9ccd446eSAtari911 $this->clearStatsCache(); 28411d05cddcSAtari911 $this->redirect("Moved $moved event(s) to namespace: " . $displayTarget, 'success', 'manage'); 28421d05cddcSAtari911 } 28431d05cddcSAtari911 28441d05cddcSAtari911 private function moveSingleEvent() { 28451d05cddcSAtari911 global $INPUT; 28461d05cddcSAtari911 28471d05cddcSAtari911 $eventData = $INPUT->str('event'); 28481d05cddcSAtari911 $targetNamespace = $INPUT->str('target_namespace'); 28491d05cddcSAtari911 28501d05cddcSAtari911 list($id, $namespace, $date, $month) = explode('|', $eventData); 28511d05cddcSAtari911 28521d05cddcSAtari911 // Determine old file path 28531d05cddcSAtari911 if ($namespace === '') { 28541d05cddcSAtari911 $oldFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json'; 28551d05cddcSAtari911 } else { 28561d05cddcSAtari911 $oldFile = DOKU_INC . 'data/meta/' . $namespace . '/calendar/' . $month . '.json'; 28571d05cddcSAtari911 } 28581d05cddcSAtari911 28591d05cddcSAtari911 if (!file_exists($oldFile)) { 28601d05cddcSAtari911 $this->redirect('Event file not found', 'error', 'manage'); 28611d05cddcSAtari911 } 28621d05cddcSAtari911 28631d05cddcSAtari911 $oldData = json_decode(file_get_contents($oldFile), true); 28641d05cddcSAtari911 if (!$oldData) { 28651d05cddcSAtari911 $this->redirect('Could not read event file', 'error', 'manage'); 28661d05cddcSAtari911 } 28671d05cddcSAtari911 28681d05cddcSAtari911 // Find and remove event from old file 28691d05cddcSAtari911 $event = null; 2870*9ccd446eSAtari911 if (isset($oldData[$date])) { 28711d05cddcSAtari911 foreach ($oldData[$date] as $key => $evt) { 28721d05cddcSAtari911 if ($evt['id'] === $id) { 28731d05cddcSAtari911 $event = $evt; 28741d05cddcSAtari911 unset($oldData[$date][$key]); 28751d05cddcSAtari911 $oldData[$date] = array_values($oldData[$date]); 28761d05cddcSAtari911 break; 28771d05cddcSAtari911 } 28781d05cddcSAtari911 } 28791d05cddcSAtari911 2880*9ccd446eSAtari911 // Remove empty date arrays 2881*9ccd446eSAtari911 if (empty($oldData[$date])) { 2882*9ccd446eSAtari911 unset($oldData[$date]); 2883*9ccd446eSAtari911 } 2884*9ccd446eSAtari911 } 2885*9ccd446eSAtari911 28861d05cddcSAtari911 if (!$event) { 28871d05cddcSAtari911 $this->redirect('Event not found', 'error', 'manage'); 28881d05cddcSAtari911 } 28891d05cddcSAtari911 2890*9ccd446eSAtari911 // Save old file (or delete if empty) 2891*9ccd446eSAtari911 if (empty($oldData)) { 2892*9ccd446eSAtari911 unlink($oldFile); 2893*9ccd446eSAtari911 } else { 28941d05cddcSAtari911 file_put_contents($oldFile, json_encode($oldData, JSON_PRETTY_PRINT)); 2895*9ccd446eSAtari911 } 28961d05cddcSAtari911 28971d05cddcSAtari911 // Update event namespace 28981d05cddcSAtari911 $event['namespace'] = $targetNamespace; 28991d05cddcSAtari911 29001d05cddcSAtari911 // Determine new file path 29011d05cddcSAtari911 if ($targetNamespace === '') { 29021d05cddcSAtari911 $newFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json'; 29031d05cddcSAtari911 $newDir = dirname($newFile); 29041d05cddcSAtari911 } else { 29051d05cddcSAtari911 $newFile = DOKU_INC . 'data/meta/' . $targetNamespace . '/calendar/' . $month . '.json'; 29061d05cddcSAtari911 $newDir = dirname($newFile); 29071d05cddcSAtari911 } 29081d05cddcSAtari911 29091d05cddcSAtari911 if (!is_dir($newDir)) { 29101d05cddcSAtari911 mkdir($newDir, 0755, true); 29111d05cddcSAtari911 } 29121d05cddcSAtari911 29131d05cddcSAtari911 $newData = []; 29141d05cddcSAtari911 if (file_exists($newFile)) { 29151d05cddcSAtari911 $newData = json_decode(file_get_contents($newFile), true) ?: []; 29161d05cddcSAtari911 } 29171d05cddcSAtari911 29181d05cddcSAtari911 if (!isset($newData[$date])) { 29191d05cddcSAtari911 $newData[$date] = []; 29201d05cddcSAtari911 } 29211d05cddcSAtari911 $newData[$date][] = $event; 29221d05cddcSAtari911 29231d05cddcSAtari911 file_put_contents($newFile, json_encode($newData, JSON_PRETTY_PRINT)); 29241d05cddcSAtari911 29251d05cddcSAtari911 $displayTarget = $targetNamespace ?: '(default)'; 2926*9ccd446eSAtari911 $this->clearStatsCache(); 29271d05cddcSAtari911 $this->redirect('Moved "' . $event['title'] . '" to ' . $displayTarget, 'success', 'manage'); 29281d05cddcSAtari911 } 29291d05cddcSAtari911 29301d05cddcSAtari911 private function createNamespace() { 29311d05cddcSAtari911 global $INPUT; 29321d05cddcSAtari911 29331d05cddcSAtari911 $namespaceName = $INPUT->str('namespace_name'); 29341d05cddcSAtari911 29351d05cddcSAtari911 // Validate namespace name 29361d05cddcSAtari911 if (empty($namespaceName)) { 29371d05cddcSAtari911 $this->redirect('Namespace name cannot be empty', 'error', 'manage'); 29381d05cddcSAtari911 } 29391d05cddcSAtari911 29401d05cddcSAtari911 if (!preg_match('/^[a-zA-Z0-9_:-]+$/', $namespaceName)) { 29411d05cddcSAtari911 $this->redirect('Invalid namespace name. Use only letters, numbers, underscore, hyphen, and colon.', 'error', 'manage'); 29421d05cddcSAtari911 } 29431d05cddcSAtari911 29441d05cddcSAtari911 // Convert namespace to directory path 29451d05cddcSAtari911 $namespacePath = str_replace(':', '/', $namespaceName); 29461d05cddcSAtari911 $calendarDir = DOKU_INC . 'data/meta/' . $namespacePath . '/calendar'; 29471d05cddcSAtari911 29481d05cddcSAtari911 // Check if already exists 29491d05cddcSAtari911 if (is_dir($calendarDir)) { 29501d05cddcSAtari911 // Check if it has any JSON files 29511d05cddcSAtari911 $hasFiles = !empty(glob($calendarDir . '/*.json')); 29521d05cddcSAtari911 if ($hasFiles) { 29531d05cddcSAtari911 $this->redirect("Namespace '$namespaceName' already exists with events", 'info', 'manage'); 29541d05cddcSAtari911 } 29551d05cddcSAtari911 // If directory exists but empty, continue to create placeholder 29561d05cddcSAtari911 } 29571d05cddcSAtari911 29581d05cddcSAtari911 // Create the directory 29591d05cddcSAtari911 if (!is_dir($calendarDir)) { 29601d05cddcSAtari911 if (!mkdir($calendarDir, 0755, true)) { 29611d05cddcSAtari911 $this->redirect("Failed to create namespace directory", 'error', 'manage'); 29621d05cddcSAtari911 } 29631d05cddcSAtari911 } 29641d05cddcSAtari911 29651d05cddcSAtari911 // Create a placeholder JSON file with an empty structure for current month 29661d05cddcSAtari911 // This ensures the namespace appears in the list immediately 29671d05cddcSAtari911 $currentMonth = date('Y-m'); 29681d05cddcSAtari911 $placeholderFile = $calendarDir . '/' . $currentMonth . '.json'; 29691d05cddcSAtari911 29701d05cddcSAtari911 if (!file_exists($placeholderFile)) { 29711d05cddcSAtari911 file_put_contents($placeholderFile, json_encode([], JSON_PRETTY_PRINT)); 29721d05cddcSAtari911 } 29731d05cddcSAtari911 29741d05cddcSAtari911 $this->redirect("Created namespace: $namespaceName", 'success', 'manage'); 29751d05cddcSAtari911 } 29761d05cddcSAtari911 29771d05cddcSAtari911 private function deleteNamespace() { 29781d05cddcSAtari911 global $INPUT; 29791d05cddcSAtari911 29801d05cddcSAtari911 $namespace = $INPUT->str('namespace'); 29811d05cddcSAtari911 29821d05cddcSAtari911 // Convert namespace to directory path (e.g., "work:projects" → "work/projects") 29831d05cddcSAtari911 $namespacePath = str_replace(':', '/', $namespace); 29841d05cddcSAtari911 29851d05cddcSAtari911 // Determine calendar directory 29861d05cddcSAtari911 if ($namespace === '') { 29871d05cddcSAtari911 $calendarDir = DOKU_INC . 'data/meta/calendar'; 29881d05cddcSAtari911 $namespaceDir = null; // Don't delete root 29891d05cddcSAtari911 } else { 29901d05cddcSAtari911 $calendarDir = DOKU_INC . 'data/meta/' . $namespacePath . '/calendar'; 29911d05cddcSAtari911 $namespaceDir = DOKU_INC . 'data/meta/' . $namespacePath; 29921d05cddcSAtari911 } 29931d05cddcSAtari911 29941d05cddcSAtari911 // Check if directory exists 29951d05cddcSAtari911 if (!is_dir($calendarDir)) { 29961d05cddcSAtari911 // Maybe it was never created or already deleted 29971d05cddcSAtari911 $this->redirect("Namespace directory not found: $calendarDir", 'error', 'manage'); 29981d05cddcSAtari911 return; 29991d05cddcSAtari911 } 30001d05cddcSAtari911 30011d05cddcSAtari911 $filesDeleted = 0; 30021d05cddcSAtari911 $eventsDeleted = 0; 30031d05cddcSAtari911 30041d05cddcSAtari911 // Delete all calendar JSON files (including empty ones) 30051d05cddcSAtari911 foreach (glob($calendarDir . '/*.json') as $file) { 30061d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 30071d05cddcSAtari911 if ($data) { 30081d05cddcSAtari911 foreach ($data as $events) { 30091d05cddcSAtari911 $eventsDeleted += count($events); 30101d05cddcSAtari911 } 30111d05cddcSAtari911 } 30121d05cddcSAtari911 unlink($file); 30131d05cddcSAtari911 $filesDeleted++; 30141d05cddcSAtari911 } 30151d05cddcSAtari911 30161d05cddcSAtari911 // Delete any other files in calendar directory 30171d05cddcSAtari911 foreach (glob($calendarDir . '/*') as $file) { 30181d05cddcSAtari911 if (is_file($file)) { 30191d05cddcSAtari911 unlink($file); 30201d05cddcSAtari911 } 30211d05cddcSAtari911 } 30221d05cddcSAtari911 30231d05cddcSAtari911 // Remove the calendar directory 30241d05cddcSAtari911 if ($namespace !== '') { 30251d05cddcSAtari911 @rmdir($calendarDir); 30261d05cddcSAtari911 30271d05cddcSAtari911 // Try to remove parent directories if they're empty 30281d05cddcSAtari911 // This handles nested namespaces like work:projects:alpha 30291d05cddcSAtari911 $currentDir = dirname($calendarDir); 30301d05cddcSAtari911 $metaDir = DOKU_INC . 'data/meta'; 30311d05cddcSAtari911 30321d05cddcSAtari911 while ($currentDir !== $metaDir && $currentDir !== dirname($metaDir)) { 30331d05cddcSAtari911 if (is_dir($currentDir)) { 30341d05cddcSAtari911 // Check if directory is empty 30351d05cddcSAtari911 $contents = scandir($currentDir); 30361d05cddcSAtari911 $isEmpty = count($contents) === 2; // Only . and .. 30371d05cddcSAtari911 30381d05cddcSAtari911 if ($isEmpty) { 30391d05cddcSAtari911 @rmdir($currentDir); 30401d05cddcSAtari911 $currentDir = dirname($currentDir); 30411d05cddcSAtari911 } else { 30421d05cddcSAtari911 break; // Directory not empty, stop 30431d05cddcSAtari911 } 30441d05cddcSAtari911 } else { 30451d05cddcSAtari911 break; 30461d05cddcSAtari911 } 30471d05cddcSAtari911 } 30481d05cddcSAtari911 } 30491d05cddcSAtari911 30501d05cddcSAtari911 $displayName = $namespace ?: '(default)'; 3051*9ccd446eSAtari911 $this->clearStatsCache(); 30521d05cddcSAtari911 $this->redirect("Deleted namespace '$displayName': $eventsDeleted events in $filesDeleted files", 'success', 'manage'); 30531d05cddcSAtari911 } 30541d05cddcSAtari911 3055*9ccd446eSAtari911 private function renameNamespace() { 3056*9ccd446eSAtari911 global $INPUT; 3057*9ccd446eSAtari911 3058*9ccd446eSAtari911 $oldNamespace = $INPUT->str('old_namespace'); 3059*9ccd446eSAtari911 $newNamespace = $INPUT->str('new_namespace'); 3060*9ccd446eSAtari911 3061*9ccd446eSAtari911 // Validate new namespace name 3062*9ccd446eSAtari911 if ($newNamespace === '') { 3063*9ccd446eSAtari911 $this->redirect("Cannot rename to empty namespace", 'error', 'manage'); 3064*9ccd446eSAtari911 return; 3065*9ccd446eSAtari911 } 3066*9ccd446eSAtari911 3067*9ccd446eSAtari911 // Convert namespaces to directory paths 3068*9ccd446eSAtari911 $oldPath = str_replace(':', '/', $oldNamespace); 3069*9ccd446eSAtari911 $newPath = str_replace(':', '/', $newNamespace); 3070*9ccd446eSAtari911 3071*9ccd446eSAtari911 // Determine source and destination directories 3072*9ccd446eSAtari911 if ($oldNamespace === '') { 3073*9ccd446eSAtari911 $sourceDir = DOKU_INC . 'data/meta/calendar'; 3074*9ccd446eSAtari911 } else { 3075*9ccd446eSAtari911 $sourceDir = DOKU_INC . 'data/meta/' . $oldPath . '/calendar'; 3076*9ccd446eSAtari911 } 3077*9ccd446eSAtari911 3078*9ccd446eSAtari911 if ($newNamespace === '') { 3079*9ccd446eSAtari911 $targetDir = DOKU_INC . 'data/meta/calendar'; 3080*9ccd446eSAtari911 } else { 3081*9ccd446eSAtari911 $targetDir = DOKU_INC . 'data/meta/' . $newPath . '/calendar'; 3082*9ccd446eSAtari911 } 3083*9ccd446eSAtari911 3084*9ccd446eSAtari911 // Check if source exists 3085*9ccd446eSAtari911 if (!is_dir($sourceDir)) { 3086*9ccd446eSAtari911 $this->redirect("Source namespace not found: $oldNamespace", 'error', 'manage'); 3087*9ccd446eSAtari911 return; 3088*9ccd446eSAtari911 } 3089*9ccd446eSAtari911 3090*9ccd446eSAtari911 // Check if target already exists 3091*9ccd446eSAtari911 if (is_dir($targetDir)) { 3092*9ccd446eSAtari911 $this->redirect("Target namespace already exists: $newNamespace", 'error', 'manage'); 3093*9ccd446eSAtari911 return; 3094*9ccd446eSAtari911 } 3095*9ccd446eSAtari911 3096*9ccd446eSAtari911 // Create target directory 3097*9ccd446eSAtari911 if (!file_exists(dirname($targetDir))) { 3098*9ccd446eSAtari911 mkdir(dirname($targetDir), 0755, true); 3099*9ccd446eSAtari911 } 3100*9ccd446eSAtari911 3101*9ccd446eSAtari911 // Rename directory 3102*9ccd446eSAtari911 if (!rename($sourceDir, $targetDir)) { 3103*9ccd446eSAtari911 $this->redirect("Failed to rename namespace", 'error', 'manage'); 3104*9ccd446eSAtari911 return; 3105*9ccd446eSAtari911 } 3106*9ccd446eSAtari911 3107*9ccd446eSAtari911 // Update event namespace field in all JSON files 3108*9ccd446eSAtari911 $eventsUpdated = 0; 3109*9ccd446eSAtari911 foreach (glob($targetDir . '/*.json') as $file) { 3110*9ccd446eSAtari911 $data = json_decode(file_get_contents($file), true); 3111*9ccd446eSAtari911 if ($data) { 3112*9ccd446eSAtari911 foreach ($data as $date => &$events) { 3113*9ccd446eSAtari911 foreach ($events as &$event) { 3114*9ccd446eSAtari911 if (isset($event['namespace']) && $event['namespace'] === $oldNamespace) { 3115*9ccd446eSAtari911 $event['namespace'] = $newNamespace; 3116*9ccd446eSAtari911 $eventsUpdated++; 3117*9ccd446eSAtari911 } 3118*9ccd446eSAtari911 } 3119*9ccd446eSAtari911 } 3120*9ccd446eSAtari911 file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); 3121*9ccd446eSAtari911 } 3122*9ccd446eSAtari911 } 3123*9ccd446eSAtari911 3124*9ccd446eSAtari911 // Clean up old directory structure if empty 3125*9ccd446eSAtari911 if ($oldNamespace !== '') { 3126*9ccd446eSAtari911 $currentDir = dirname($sourceDir); 3127*9ccd446eSAtari911 $metaDir = DOKU_INC . 'data/meta'; 3128*9ccd446eSAtari911 3129*9ccd446eSAtari911 while ($currentDir !== $metaDir && $currentDir !== dirname($metaDir)) { 3130*9ccd446eSAtari911 if (is_dir($currentDir)) { 3131*9ccd446eSAtari911 $contents = scandir($currentDir); 3132*9ccd446eSAtari911 $isEmpty = count($contents) === 2; // Only . and .. 3133*9ccd446eSAtari911 3134*9ccd446eSAtari911 if ($isEmpty) { 3135*9ccd446eSAtari911 @rmdir($currentDir); 3136*9ccd446eSAtari911 $currentDir = dirname($currentDir); 3137*9ccd446eSAtari911 } else { 3138*9ccd446eSAtari911 break; 3139*9ccd446eSAtari911 } 3140*9ccd446eSAtari911 } else { 3141*9ccd446eSAtari911 break; 3142*9ccd446eSAtari911 } 3143*9ccd446eSAtari911 } 3144*9ccd446eSAtari911 } 3145*9ccd446eSAtari911 3146*9ccd446eSAtari911 $this->clearStatsCache(); 3147*9ccd446eSAtari911 $this->redirect("Renamed namespace from '$oldNamespace' to '$newNamespace' ($eventsUpdated events updated)", 'success', 'manage'); 3148*9ccd446eSAtari911 } 3149*9ccd446eSAtari911 31501d05cddcSAtari911 private function deleteSelectedEvents() { 31511d05cddcSAtari911 global $INPUT; 31521d05cddcSAtari911 31531d05cddcSAtari911 $events = $INPUT->arr('events'); 31541d05cddcSAtari911 31551d05cddcSAtari911 if (empty($events)) { 31561d05cddcSAtari911 $this->redirect('No events selected', 'error', 'manage'); 31571d05cddcSAtari911 } 31581d05cddcSAtari911 31591d05cddcSAtari911 $deletedCount = 0; 31601d05cddcSAtari911 31611d05cddcSAtari911 foreach ($events as $eventData) { 31621d05cddcSAtari911 list($id, $namespace, $date, $month) = explode('|', $eventData); 31631d05cddcSAtari911 31641d05cddcSAtari911 // Determine file path 31651d05cddcSAtari911 if ($namespace === '') { 31661d05cddcSAtari911 $file = DOKU_INC . 'data/meta/calendar/' . $month . '.json'; 31671d05cddcSAtari911 } else { 31681d05cddcSAtari911 $file = DOKU_INC . 'data/meta/' . $namespace . '/calendar/' . $month . '.json'; 31691d05cddcSAtari911 } 31701d05cddcSAtari911 31711d05cddcSAtari911 if (!file_exists($file)) continue; 31721d05cddcSAtari911 31731d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 31741d05cddcSAtari911 if (!$data) continue; 31751d05cddcSAtari911 31761d05cddcSAtari911 // Find and remove event 31771d05cddcSAtari911 if (isset($data[$date])) { 31781d05cddcSAtari911 foreach ($data[$date] as $key => $evt) { 31791d05cddcSAtari911 if ($evt['id'] === $id) { 31801d05cddcSAtari911 unset($data[$date][$key]); 31811d05cddcSAtari911 $data[$date] = array_values($data[$date]); 31821d05cddcSAtari911 $deletedCount++; 31831d05cddcSAtari911 break; 31841d05cddcSAtari911 } 31851d05cddcSAtari911 } 31861d05cddcSAtari911 31871d05cddcSAtari911 // Remove empty date arrays 31881d05cddcSAtari911 if (empty($data[$date])) { 31891d05cddcSAtari911 unset($data[$date]); 31901d05cddcSAtari911 } 31911d05cddcSAtari911 31921d05cddcSAtari911 // Save file 31931d05cddcSAtari911 file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); 31941d05cddcSAtari911 } 31951d05cddcSAtari911 } 31961d05cddcSAtari911 3197*9ccd446eSAtari911 $this->clearStatsCache(); 31981d05cddcSAtari911 $this->redirect("Deleted $deletedCount event(s)", 'success', 'manage'); 31991d05cddcSAtari911 } 32001d05cddcSAtari911 3201*9ccd446eSAtari911 /** 3202*9ccd446eSAtari911 * Clear the event statistics cache so counts refresh after mutations 3203*9ccd446eSAtari911 */ 3204*9ccd446eSAtari911 private function clearStatsCache() { 3205*9ccd446eSAtari911 $cacheFile = DOKU_PLUGIN . 'calendar/.event_stats_cache'; 3206*9ccd446eSAtari911 if (file_exists($cacheFile)) { 3207*9ccd446eSAtari911 unlink($cacheFile); 3208*9ccd446eSAtari911 } 3209*9ccd446eSAtari911 } 3210*9ccd446eSAtari911 32111d05cddcSAtari911 private function getCronStatus() { 32121d05cddcSAtari911 // Try to read root's crontab first, then current user 32131d05cddcSAtari911 $output = []; 32141d05cddcSAtari911 exec('sudo crontab -l 2>/dev/null', $output); 32151d05cddcSAtari911 32161d05cddcSAtari911 // If sudo doesn't work, try current user 32171d05cddcSAtari911 if (empty($output)) { 32181d05cddcSAtari911 exec('crontab -l 2>/dev/null', $output); 32191d05cddcSAtari911 } 32201d05cddcSAtari911 32211d05cddcSAtari911 // Also check system crontab files 32221d05cddcSAtari911 if (empty($output)) { 32231d05cddcSAtari911 $cronFiles = [ 32241d05cddcSAtari911 '/etc/crontab', 32251d05cddcSAtari911 '/etc/cron.d/calendar', 32261d05cddcSAtari911 '/var/spool/cron/root', 32271d05cddcSAtari911 '/var/spool/cron/crontabs/root' 32281d05cddcSAtari911 ]; 32291d05cddcSAtari911 32301d05cddcSAtari911 foreach ($cronFiles as $file) { 32311d05cddcSAtari911 if (file_exists($file) && is_readable($file)) { 32321d05cddcSAtari911 $content = file_get_contents($file); 32331d05cddcSAtari911 $output = explode("\n", $content); 32341d05cddcSAtari911 break; 32351d05cddcSAtari911 } 32361d05cddcSAtari911 } 32371d05cddcSAtari911 } 32381d05cddcSAtari911 32391d05cddcSAtari911 // Look for sync_outlook.php in the cron entries 32401d05cddcSAtari911 foreach ($output as $line) { 32411d05cddcSAtari911 $line = trim($line); 32421d05cddcSAtari911 32431d05cddcSAtari911 // Skip empty lines and comments 32441d05cddcSAtari911 if (empty($line) || $line[0] === '#') continue; 32451d05cddcSAtari911 32461d05cddcSAtari911 // Check if line contains sync_outlook.php 32471d05cddcSAtari911 if (strpos($line, 'sync_outlook.php') !== false) { 32481d05cddcSAtari911 // Parse cron expression 32491d05cddcSAtari911 // Format: minute hour day month weekday [user] command 32501d05cddcSAtari911 $parts = preg_split('/\s+/', $line, 7); 32511d05cddcSAtari911 32521d05cddcSAtari911 if (count($parts) >= 5) { 32531d05cddcSAtari911 // Determine if this has a user field (system crontab format) 32541d05cddcSAtari911 $hasUser = (count($parts) >= 6 && !preg_match('/^[\/\*]/', $parts[5])); 32551d05cddcSAtari911 $offset = $hasUser ? 1 : 0; 32561d05cddcSAtari911 32571d05cddcSAtari911 $frequency = $this->parseCronExpression($parts[0], $parts[1], $parts[2], $parts[3], $parts[4]); 32581d05cddcSAtari911 return [ 32591d05cddcSAtari911 'active' => true, 32601d05cddcSAtari911 'frequency' => $frequency, 32611d05cddcSAtari911 'expression' => implode(' ', array_slice($parts, 0, 5)), 32621d05cddcSAtari911 'full_line' => $line 32631d05cddcSAtari911 ]; 32641d05cddcSAtari911 } 32651d05cddcSAtari911 } 32661d05cddcSAtari911 } 32671d05cddcSAtari911 32681d05cddcSAtari911 return ['active' => false, 'frequency' => '', 'expression' => '', 'full_line' => '']; 32691d05cddcSAtari911 } 32701d05cddcSAtari911 32711d05cddcSAtari911 private function parseCronExpression($minute, $hour, $day, $month, $weekday) { 32721d05cddcSAtari911 // Parse minute field 32731d05cddcSAtari911 if ($minute === '*') { 32741d05cddcSAtari911 return 'Runs every minute'; 32751d05cddcSAtari911 } elseif (strpos($minute, '*/') === 0) { 32761d05cddcSAtari911 $interval = substr($minute, 2); 32771d05cddcSAtari911 if ($interval == 1) { 32781d05cddcSAtari911 return 'Runs every minute'; 32791d05cddcSAtari911 } elseif ($interval == 5) { 32801d05cddcSAtari911 return 'Runs every 5 minutes'; 32811d05cddcSAtari911 } elseif ($interval == 8) { 32821d05cddcSAtari911 return 'Runs every 8 minutes'; 32831d05cddcSAtari911 } elseif ($interval == 10) { 32841d05cddcSAtari911 return 'Runs every 10 minutes'; 32851d05cddcSAtari911 } elseif ($interval == 15) { 32861d05cddcSAtari911 return 'Runs every 15 minutes'; 32871d05cddcSAtari911 } elseif ($interval == 30) { 32881d05cddcSAtari911 return 'Runs every 30 minutes'; 32891d05cddcSAtari911 } else { 32901d05cddcSAtari911 return "Runs every $interval minutes"; 32911d05cddcSAtari911 } 32921d05cddcSAtari911 } 32931d05cddcSAtari911 32941d05cddcSAtari911 // Parse hour field 32951d05cddcSAtari911 if ($hour === '*' && $minute !== '*') { 32961d05cddcSAtari911 return 'Runs hourly'; 32971d05cddcSAtari911 } elseif (strpos($hour, '*/') === 0 && $minute !== '*') { 32981d05cddcSAtari911 $interval = substr($hour, 2); 32991d05cddcSAtari911 if ($interval == 1) { 33001d05cddcSAtari911 return 'Runs every hour'; 33011d05cddcSAtari911 } else { 33021d05cddcSAtari911 return "Runs every $interval hours"; 33031d05cddcSAtari911 } 33041d05cddcSAtari911 } 33051d05cddcSAtari911 33061d05cddcSAtari911 // Parse day field 33071d05cddcSAtari911 if ($day === '*' && $hour !== '*' && $minute !== '*') { 33081d05cddcSAtari911 return 'Runs daily'; 33091d05cddcSAtari911 } 33101d05cddcSAtari911 33111d05cddcSAtari911 // Default 33121d05cddcSAtari911 return 'Custom schedule'; 33131d05cddcSAtari911 } 33141d05cddcSAtari911 33151d05cddcSAtari911 private function runSync() { 33161d05cddcSAtari911 global $INPUT; 33171d05cddcSAtari911 33181d05cddcSAtari911 if ($INPUT->str('call') === 'ajax') { 33191d05cddcSAtari911 header('Content-Type: application/json'); 33201d05cddcSAtari911 33211d05cddcSAtari911 $syncScript = DOKU_PLUGIN . 'calendar/sync_outlook.php'; 33221d05cddcSAtari911 $abortFile = DOKU_PLUGIN . 'calendar/.sync_abort'; 33231d05cddcSAtari911 33241d05cddcSAtari911 // Remove any existing abort flag 33251d05cddcSAtari911 if (file_exists($abortFile)) { 33261d05cddcSAtari911 @unlink($abortFile); 33271d05cddcSAtari911 } 33281d05cddcSAtari911 33291d05cddcSAtari911 if (!file_exists($syncScript)) { 33301d05cddcSAtari911 echo json_encode(['success' => false, 'message' => 'Sync script not found at: ' . $syncScript]); 33311d05cddcSAtari911 exit; 33321d05cddcSAtari911 } 33331d05cddcSAtari911 33341d05cddcSAtari911 // Change to plugin directory 33351d05cddcSAtari911 $pluginDir = DOKU_PLUGIN . 'calendar'; 33361d05cddcSAtari911 $logFile = $pluginDir . '/sync.log'; 33371d05cddcSAtari911 33381d05cddcSAtari911 // Ensure log file exists and is writable 33391d05cddcSAtari911 if (!file_exists($logFile)) { 33401d05cddcSAtari911 @touch($logFile); 33411d05cddcSAtari911 @chmod($logFile, 0666); 33421d05cddcSAtari911 } 33431d05cddcSAtari911 33441d05cddcSAtari911 // Try to log the execution (but don't fail if we can't) 33451d05cddcSAtari911 if (is_writable($logFile)) { 33461d05cddcSAtari911 $tz = new DateTimeZone('America/Los_Angeles'); 33471d05cddcSAtari911 $now = new DateTime('now', $tz); 33481d05cddcSAtari911 $timestamp = $now->format('Y-m-d H:i:s'); 33491d05cddcSAtari911 @file_put_contents($logFile, "[$timestamp] [ADMIN] Manual sync triggered via admin panel\n", FILE_APPEND); 33501d05cddcSAtari911 } 33511d05cddcSAtari911 33521d05cddcSAtari911 // Find PHP binary - try multiple methods 33531d05cddcSAtari911 $phpPath = $this->findPhpBinary(); 33541d05cddcSAtari911 33551d05cddcSAtari911 // Build command 33561d05cddcSAtari911 $command = sprintf( 33571d05cddcSAtari911 'cd %s && %s %s 2>&1', 33581d05cddcSAtari911 escapeshellarg($pluginDir), 33591d05cddcSAtari911 $phpPath, 33601d05cddcSAtari911 escapeshellarg(basename($syncScript)) 33611d05cddcSAtari911 ); 33621d05cddcSAtari911 33631d05cddcSAtari911 // Execute and capture output 33641d05cddcSAtari911 $output = []; 33651d05cddcSAtari911 $returnCode = 0; 33661d05cddcSAtari911 exec($command, $output, $returnCode); 33671d05cddcSAtari911 33681d05cddcSAtari911 // Check if sync completed 33691d05cddcSAtari911 $lastLines = array_slice($output, -5); 33701d05cddcSAtari911 $completed = false; 33711d05cddcSAtari911 foreach ($lastLines as $line) { 33721d05cddcSAtari911 if (strpos($line, 'Sync Complete') !== false || strpos($line, 'Created:') !== false) { 33731d05cddcSAtari911 $completed = true; 33741d05cddcSAtari911 break; 33751d05cddcSAtari911 } 33761d05cddcSAtari911 } 33771d05cddcSAtari911 33781d05cddcSAtari911 if ($returnCode === 0 && $completed) { 33791d05cddcSAtari911 echo json_encode([ 33801d05cddcSAtari911 'success' => true, 33811d05cddcSAtari911 'message' => 'Sync completed successfully! Check log below.' 33821d05cddcSAtari911 ]); 33831d05cddcSAtari911 } elseif ($returnCode === 0) { 33841d05cddcSAtari911 echo json_encode([ 33851d05cddcSAtari911 'success' => true, 33861d05cddcSAtari911 'message' => 'Sync started. Check log below for progress.' 33871d05cddcSAtari911 ]); 33881d05cddcSAtari911 } else { 33891d05cddcSAtari911 // Include output for debugging 33901d05cddcSAtari911 $errorMsg = 'Sync failed with error code: ' . $returnCode; 33911d05cddcSAtari911 if (!empty($output)) { 33921d05cddcSAtari911 $errorMsg .= ' | ' . implode(' | ', array_slice($output, -3)); 33931d05cddcSAtari911 } 33941d05cddcSAtari911 echo json_encode([ 33951d05cddcSAtari911 'success' => false, 33961d05cddcSAtari911 'message' => $errorMsg 33971d05cddcSAtari911 ]); 33981d05cddcSAtari911 } 33991d05cddcSAtari911 exit; 34001d05cddcSAtari911 } 34011d05cddcSAtari911 } 34021d05cddcSAtari911 34031d05cddcSAtari911 private function stopSync() { 34041d05cddcSAtari911 global $INPUT; 34051d05cddcSAtari911 34061d05cddcSAtari911 if ($INPUT->str('call') === 'ajax') { 34071d05cddcSAtari911 header('Content-Type: application/json'); 34081d05cddcSAtari911 34091d05cddcSAtari911 $abortFile = DOKU_PLUGIN . 'calendar/.sync_abort'; 34101d05cddcSAtari911 34111d05cddcSAtari911 // Create abort flag file 34121d05cddcSAtari911 if (file_put_contents($abortFile, date('Y-m-d H:i:s')) !== false) { 34131d05cddcSAtari911 echo json_encode([ 34141d05cddcSAtari911 'success' => true, 34151d05cddcSAtari911 'message' => 'Stop signal sent to sync process' 34161d05cddcSAtari911 ]); 34171d05cddcSAtari911 } else { 34181d05cddcSAtari911 echo json_encode([ 34191d05cddcSAtari911 'success' => false, 34201d05cddcSAtari911 'message' => 'Failed to create abort flag' 34211d05cddcSAtari911 ]); 34221d05cddcSAtari911 } 34231d05cddcSAtari911 exit; 34241d05cddcSAtari911 } 34251d05cddcSAtari911 } 34261d05cddcSAtari911 34271d05cddcSAtari911 private function uploadUpdate() { 34281d05cddcSAtari911 if (!isset($_FILES['plugin_zip']) || $_FILES['plugin_zip']['error'] !== UPLOAD_ERR_OK) { 34291d05cddcSAtari911 $this->redirect('Upload failed: ' . ($_FILES['plugin_zip']['error'] ?? 'No file uploaded'), 'error', 'update'); 34301d05cddcSAtari911 return; 34311d05cddcSAtari911 } 34321d05cddcSAtari911 34331d05cddcSAtari911 $uploadedFile = $_FILES['plugin_zip']['tmp_name']; 34341d05cddcSAtari911 $pluginDir = DOKU_PLUGIN . 'calendar/'; 34351d05cddcSAtari911 $backupFirst = isset($_POST['backup_first']); 34361d05cddcSAtari911 34371d05cddcSAtari911 // Check if plugin directory is writable 34381d05cddcSAtari911 if (!is_writable($pluginDir)) { 34391d05cddcSAtari911 $this->redirect('Plugin directory is not writable. Please check permissions: ' . $pluginDir, 'error', 'update'); 34401d05cddcSAtari911 return; 34411d05cddcSAtari911 } 34421d05cddcSAtari911 34431d05cddcSAtari911 // Check if parent directory is writable (for backup and temp files) 34441d05cddcSAtari911 if (!is_writable(DOKU_PLUGIN)) { 34451d05cddcSAtari911 $this->redirect('Plugin parent directory is not writable. Please check permissions: ' . DOKU_PLUGIN, 'error', 'update'); 34461d05cddcSAtari911 return; 34471d05cddcSAtari911 } 34481d05cddcSAtari911 34491d05cddcSAtari911 // Verify it's a ZIP file 34501d05cddcSAtari911 $finfo = finfo_open(FILEINFO_MIME_TYPE); 34511d05cddcSAtari911 $mimeType = finfo_file($finfo, $uploadedFile); 34521d05cddcSAtari911 finfo_close($finfo); 34531d05cddcSAtari911 34541d05cddcSAtari911 if ($mimeType !== 'application/zip' && $mimeType !== 'application/x-zip-compressed') { 34551d05cddcSAtari911 $this->redirect('Invalid file type. Please upload a ZIP file.', 'error', 'update'); 34561d05cddcSAtari911 return; 34571d05cddcSAtari911 } 34581d05cddcSAtari911 34591d05cddcSAtari911 // Create backup if requested 34601d05cddcSAtari911 if ($backupFirst) { 34611d05cddcSAtari911 // Get current version 34621d05cddcSAtari911 $pluginInfo = $pluginDir . 'plugin.info.txt'; 34631d05cddcSAtari911 $version = 'unknown'; 34641d05cddcSAtari911 if (file_exists($pluginInfo)) { 34651d05cddcSAtari911 $info = confToHash($pluginInfo); 34661d05cddcSAtari911 $version = $info['version'] ?? ($info['date'] ?? 'unknown'); 34671d05cddcSAtari911 } 34681d05cddcSAtari911 34691d05cddcSAtari911 $backupName = 'calendar.backup.v' . $version . '.' . date('Y-m-d_H-i-s') . '.zip'; 34701d05cddcSAtari911 $backupPath = DOKU_PLUGIN . $backupName; 34711d05cddcSAtari911 34721d05cddcSAtari911 try { 34731d05cddcSAtari911 $zip = new ZipArchive(); 34741d05cddcSAtari911 if ($zip->open($backupPath, ZipArchive::CREATE) === TRUE) { 3475*9ccd446eSAtari911 $fileCount = $this->addDirectoryToZip($zip, $pluginDir, 'calendar/'); 34761d05cddcSAtari911 $zip->close(); 3477*9ccd446eSAtari911 3478*9ccd446eSAtari911 // Verify backup was created and has content 3479*9ccd446eSAtari911 if (!file_exists($backupPath)) { 3480*9ccd446eSAtari911 $this->redirect('Backup file was not created', 'error', 'update'); 3481*9ccd446eSAtari911 return; 3482*9ccd446eSAtari911 } 3483*9ccd446eSAtari911 3484*9ccd446eSAtari911 $backupSize = filesize($backupPath); 3485*9ccd446eSAtari911 if ($backupSize < 1000) { // Backup should be at least 1KB 3486*9ccd446eSAtari911 @unlink($backupPath); 3487*9ccd446eSAtari911 $this->redirect('Backup file is too small (' . $backupSize . ' bytes). Only ' . $fileCount . ' files were added. Backup aborted.', 'error', 'update'); 3488*9ccd446eSAtari911 return; 3489*9ccd446eSAtari911 } 3490*9ccd446eSAtari911 3491*9ccd446eSAtari911 if ($fileCount < 10) { // Should have at least 10 files 3492*9ccd446eSAtari911 @unlink($backupPath); 3493*9ccd446eSAtari911 $this->redirect('Backup incomplete: Only ' . $fileCount . ' files were added (expected 30+). Backup aborted.', 'error', 'update'); 3494*9ccd446eSAtari911 return; 3495*9ccd446eSAtari911 } 34961d05cddcSAtari911 } else { 34971d05cddcSAtari911 $this->redirect('Failed to create backup ZIP file', 'error', 'update'); 34981d05cddcSAtari911 return; 34991d05cddcSAtari911 } 35001d05cddcSAtari911 } catch (Exception $e) { 3501*9ccd446eSAtari911 if (file_exists($backupPath)) { 3502*9ccd446eSAtari911 @unlink($backupPath); 3503*9ccd446eSAtari911 } 35041d05cddcSAtari911 $this->redirect('Backup failed: ' . $e->getMessage(), 'error', 'update'); 35051d05cddcSAtari911 return; 35061d05cddcSAtari911 } 35071d05cddcSAtari911 } 35081d05cddcSAtari911 35091d05cddcSAtari911 // Extract uploaded ZIP 35101d05cddcSAtari911 $zip = new ZipArchive(); 35111d05cddcSAtari911 if ($zip->open($uploadedFile) !== TRUE) { 35121d05cddcSAtari911 $this->redirect('Failed to open ZIP file', 'error', 'update'); 35131d05cddcSAtari911 return; 35141d05cddcSAtari911 } 35151d05cddcSAtari911 35161d05cddcSAtari911 // Check if ZIP contains calendar folder 35171d05cddcSAtari911 $hasCalendarFolder = false; 35181d05cddcSAtari911 for ($i = 0; $i < $zip->numFiles; $i++) { 35191d05cddcSAtari911 $filename = $zip->getNameIndex($i); 35201d05cddcSAtari911 if (strpos($filename, 'calendar/') === 0) { 35211d05cddcSAtari911 $hasCalendarFolder = true; 35221d05cddcSAtari911 break; 35231d05cddcSAtari911 } 35241d05cddcSAtari911 } 35251d05cddcSAtari911 35261d05cddcSAtari911 // Extract to temp directory first 35271d05cddcSAtari911 $tempDir = DOKU_PLUGIN . 'calendar_update_temp/'; 35281d05cddcSAtari911 if (is_dir($tempDir)) { 35291d05cddcSAtari911 $this->deleteDirectory($tempDir); 35301d05cddcSAtari911 } 35311d05cddcSAtari911 mkdir($tempDir); 35321d05cddcSAtari911 35331d05cddcSAtari911 $zip->extractTo($tempDir); 35341d05cddcSAtari911 $zip->close(); 35351d05cddcSAtari911 35361d05cddcSAtari911 // Determine source directory 35371d05cddcSAtari911 if ($hasCalendarFolder) { 35381d05cddcSAtari911 $sourceDir = $tempDir . 'calendar/'; 35391d05cddcSAtari911 } else { 35401d05cddcSAtari911 $sourceDir = $tempDir; 35411d05cddcSAtari911 } 35421d05cddcSAtari911 35431d05cddcSAtari911 // Preserve configuration files 35441d05cddcSAtari911 $preserveFiles = ['sync_config.php', 'sync_state.json', 'sync.log']; 35451d05cddcSAtari911 $preserved = []; 35461d05cddcSAtari911 foreach ($preserveFiles as $file) { 35471d05cddcSAtari911 $oldFile = $pluginDir . $file; 35481d05cddcSAtari911 if (file_exists($oldFile)) { 35491d05cddcSAtari911 $preserved[$file] = file_get_contents($oldFile); 35501d05cddcSAtari911 } 35511d05cddcSAtari911 } 35521d05cddcSAtari911 35531d05cddcSAtari911 // Delete old plugin files (except data files) 35541d05cddcSAtari911 $this->deleteDirectoryContents($pluginDir, $preserveFiles); 35551d05cddcSAtari911 35561d05cddcSAtari911 // Copy new files 35571d05cddcSAtari911 $this->recursiveCopy($sourceDir, $pluginDir); 35581d05cddcSAtari911 35591d05cddcSAtari911 // Restore preserved files 35601d05cddcSAtari911 foreach ($preserved as $file => $content) { 35611d05cddcSAtari911 file_put_contents($pluginDir . $file, $content); 35621d05cddcSAtari911 } 35631d05cddcSAtari911 35641d05cddcSAtari911 // Update version and date in plugin.info.txt 35651d05cddcSAtari911 $pluginInfo = $pluginDir . 'plugin.info.txt'; 35661d05cddcSAtari911 if (file_exists($pluginInfo)) { 35671d05cddcSAtari911 $info = confToHash($pluginInfo); 35681d05cddcSAtari911 35691d05cddcSAtari911 // Get new version from uploaded plugin 35701d05cddcSAtari911 $newVersion = $info['version'] ?? 'unknown'; 35711d05cddcSAtari911 35721d05cddcSAtari911 // Update date to current 35731d05cddcSAtari911 $info['date'] = date('Y-m-d'); 35741d05cddcSAtari911 35751d05cddcSAtari911 // Write updated info back 35761d05cddcSAtari911 $lines = []; 35771d05cddcSAtari911 foreach ($info as $key => $value) { 35781d05cddcSAtari911 $lines[] = str_pad($key, 8) . ' ' . $value; 35791d05cddcSAtari911 } 35801d05cddcSAtari911 file_put_contents($pluginInfo, implode("\n", $lines) . "\n"); 35811d05cddcSAtari911 } 35821d05cddcSAtari911 35831d05cddcSAtari911 // Cleanup temp directory 35841d05cddcSAtari911 $this->deleteDirectory($tempDir); 35851d05cddcSAtari911 35861d05cddcSAtari911 $message = 'Plugin updated successfully!'; 35871d05cddcSAtari911 if ($backupFirst) { 35881d05cddcSAtari911 $message .= ' Backup saved as: ' . $backupName; 35891d05cddcSAtari911 } 35901d05cddcSAtari911 $this->redirect($message, 'success', 'update'); 35911d05cddcSAtari911 } 35921d05cddcSAtari911 35931d05cddcSAtari911 private function deleteBackup() { 35941d05cddcSAtari911 global $INPUT; 35951d05cddcSAtari911 35961d05cddcSAtari911 $filename = $INPUT->str('backup_file'); 35971d05cddcSAtari911 35981d05cddcSAtari911 if (empty($filename)) { 35991d05cddcSAtari911 $this->redirect('No backup file specified', 'error', 'update'); 36001d05cddcSAtari911 return; 36011d05cddcSAtari911 } 36021d05cddcSAtari911 36031d05cddcSAtari911 // Security: only allow files starting with "calendar" and ending with .zip, no directory traversal 36041d05cddcSAtari911 if (!preg_match('/^calendar[a-zA-Z0-9._-]*\.zip$/', $filename)) { 36051d05cddcSAtari911 $this->redirect('Invalid backup filename', 'error', 'update'); 36061d05cddcSAtari911 return; 36071d05cddcSAtari911 } 36081d05cddcSAtari911 36091d05cddcSAtari911 $backupPath = DOKU_PLUGIN . $filename; 36101d05cddcSAtari911 36111d05cddcSAtari911 if (!file_exists($backupPath)) { 36121d05cddcSAtari911 $this->redirect('Backup file not found', 'error', 'update'); 36131d05cddcSAtari911 return; 36141d05cddcSAtari911 } 36151d05cddcSAtari911 36161d05cddcSAtari911 if (@unlink($backupPath)) { 36171d05cddcSAtari911 $this->redirect('Backup deleted: ' . $filename, 'success', 'update'); 36181d05cddcSAtari911 } else { 36191d05cddcSAtari911 $this->redirect('Failed to delete backup. Check file permissions.', 'error', 'update'); 36201d05cddcSAtari911 } 36211d05cddcSAtari911 } 36221d05cddcSAtari911 36231d05cddcSAtari911 private function renameBackup() { 36241d05cddcSAtari911 global $INPUT; 36251d05cddcSAtari911 36261d05cddcSAtari911 $oldName = $INPUT->str('old_name'); 36271d05cddcSAtari911 $newName = $INPUT->str('new_name'); 36281d05cddcSAtari911 36291d05cddcSAtari911 if (empty($oldName) || empty($newName)) { 36301d05cddcSAtari911 $this->redirect('Missing filename(s)', 'error', 'update'); 36311d05cddcSAtari911 return; 36321d05cddcSAtari911 } 36331d05cddcSAtari911 36341d05cddcSAtari911 // Security: validate filenames 36351d05cddcSAtari911 if (!preg_match('/^[a-zA-Z0-9._-]+\.zip$/', $oldName) || !preg_match('/^[a-zA-Z0-9._-]+\.zip$/', $newName)) { 36361d05cddcSAtari911 $this->redirect('Invalid filename format', 'error', 'update'); 36371d05cddcSAtari911 return; 36381d05cddcSAtari911 } 36391d05cddcSAtari911 36401d05cddcSAtari911 $oldPath = DOKU_PLUGIN . $oldName; 36411d05cddcSAtari911 $newPath = DOKU_PLUGIN . $newName; 36421d05cddcSAtari911 36431d05cddcSAtari911 if (!file_exists($oldPath)) { 36441d05cddcSAtari911 $this->redirect('Backup file not found', 'error', 'update'); 36451d05cddcSAtari911 return; 36461d05cddcSAtari911 } 36471d05cddcSAtari911 36481d05cddcSAtari911 if (file_exists($newPath)) { 36491d05cddcSAtari911 $this->redirect('A file with the new name already exists', 'error', 'update'); 36501d05cddcSAtari911 return; 36511d05cddcSAtari911 } 36521d05cddcSAtari911 36531d05cddcSAtari911 if (@rename($oldPath, $newPath)) { 36541d05cddcSAtari911 $this->redirect('Backup renamed: ' . $oldName . ' → ' . $newName, 'success', 'update'); 36551d05cddcSAtari911 } else { 36561d05cddcSAtari911 $this->redirect('Failed to rename backup. Check file permissions.', 'error', 'update'); 36571d05cddcSAtari911 } 36581d05cddcSAtari911 } 36591d05cddcSAtari911 36601d05cddcSAtari911 private function restoreBackup() { 36611d05cddcSAtari911 global $INPUT; 36621d05cddcSAtari911 36631d05cddcSAtari911 $filename = $INPUT->str('backup_file'); 36641d05cddcSAtari911 36651d05cddcSAtari911 if (empty($filename)) { 36661d05cddcSAtari911 $this->redirect('No backup file specified', 'error', 'update'); 36671d05cddcSAtari911 return; 36681d05cddcSAtari911 } 36691d05cddcSAtari911 36701d05cddcSAtari911 // Security: only allow files starting with "calendar" and ending with .zip, no directory traversal 36711d05cddcSAtari911 if (!preg_match('/^calendar[a-zA-Z0-9._-]*\.zip$/', $filename)) { 36721d05cddcSAtari911 $this->redirect('Invalid backup filename', 'error', 'update'); 36731d05cddcSAtari911 return; 36741d05cddcSAtari911 } 36751d05cddcSAtari911 36761d05cddcSAtari911 $backupPath = DOKU_PLUGIN . $filename; 36771d05cddcSAtari911 $pluginDir = DOKU_PLUGIN . 'calendar/'; 36781d05cddcSAtari911 36791d05cddcSAtari911 if (!file_exists($backupPath)) { 36801d05cddcSAtari911 $this->redirect('Backup file not found', 'error', 'update'); 36811d05cddcSAtari911 return; 36821d05cddcSAtari911 } 36831d05cddcSAtari911 36841d05cddcSAtari911 // Check if plugin directory is writable 36851d05cddcSAtari911 if (!is_writable($pluginDir)) { 36861d05cddcSAtari911 $this->redirect('Plugin directory is not writable. Please check permissions.', 'error', 'update'); 36871d05cddcSAtari911 return; 36881d05cddcSAtari911 } 36891d05cddcSAtari911 36901d05cddcSAtari911 // Extract backup to temp directory 36911d05cddcSAtari911 $tempDir = DOKU_PLUGIN . 'calendar_restore_temp/'; 36921d05cddcSAtari911 if (is_dir($tempDir)) { 36931d05cddcSAtari911 $this->deleteDirectory($tempDir); 36941d05cddcSAtari911 } 36951d05cddcSAtari911 mkdir($tempDir); 36961d05cddcSAtari911 36971d05cddcSAtari911 $zip = new ZipArchive(); 36981d05cddcSAtari911 if ($zip->open($backupPath) !== TRUE) { 36991d05cddcSAtari911 $this->redirect('Failed to open backup ZIP file', 'error', 'update'); 37001d05cddcSAtari911 return; 37011d05cddcSAtari911 } 37021d05cddcSAtari911 37031d05cddcSAtari911 $zip->extractTo($tempDir); 37041d05cddcSAtari911 $zip->close(); 37051d05cddcSAtari911 37061d05cddcSAtari911 // The backup contains a "calendar/" folder 37071d05cddcSAtari911 $sourceDir = $tempDir . 'calendar/'; 37081d05cddcSAtari911 37091d05cddcSAtari911 if (!is_dir($sourceDir)) { 37101d05cddcSAtari911 $this->deleteDirectory($tempDir); 37111d05cddcSAtari911 $this->redirect('Invalid backup structure', 'error', 'update'); 37121d05cddcSAtari911 return; 37131d05cddcSAtari911 } 37141d05cddcSAtari911 37151d05cddcSAtari911 // Delete current plugin directory contents 37161d05cddcSAtari911 $this->deleteDirectoryContents($pluginDir, []); 37171d05cddcSAtari911 37181d05cddcSAtari911 // Copy backup files to plugin directory 37191d05cddcSAtari911 $this->recursiveCopy($sourceDir, $pluginDir); 37201d05cddcSAtari911 37211d05cddcSAtari911 // Cleanup temp directory 37221d05cddcSAtari911 $this->deleteDirectory($tempDir); 37231d05cddcSAtari911 37241d05cddcSAtari911 $this->redirect('Plugin restored from backup: ' . $filename, 'success', 'update'); 37251d05cddcSAtari911 } 37261d05cddcSAtari911 3727*9ccd446eSAtari911 private function createManualBackup() { 3728*9ccd446eSAtari911 $pluginDir = DOKU_PLUGIN . 'calendar/'; 3729*9ccd446eSAtari911 3730*9ccd446eSAtari911 // Check if plugin directory is readable 3731*9ccd446eSAtari911 if (!is_readable($pluginDir)) { 3732*9ccd446eSAtari911 $this->redirect('Plugin directory is not readable. Please check permissions.', 'error', 'update'); 3733*9ccd446eSAtari911 return; 3734*9ccd446eSAtari911 } 3735*9ccd446eSAtari911 3736*9ccd446eSAtari911 // Check if parent directory is writable (for saving backup) 3737*9ccd446eSAtari911 if (!is_writable(DOKU_PLUGIN)) { 3738*9ccd446eSAtari911 $this->redirect('Plugin parent directory is not writable. Cannot save backup.', 'error', 'update'); 3739*9ccd446eSAtari911 return; 3740*9ccd446eSAtari911 } 3741*9ccd446eSAtari911 3742*9ccd446eSAtari911 // Get current version 3743*9ccd446eSAtari911 $pluginInfo = $pluginDir . 'plugin.info.txt'; 3744*9ccd446eSAtari911 $version = 'unknown'; 3745*9ccd446eSAtari911 if (file_exists($pluginInfo)) { 3746*9ccd446eSAtari911 $info = confToHash($pluginInfo); 3747*9ccd446eSAtari911 $version = $info['version'] ?? ($info['date'] ?? 'unknown'); 3748*9ccd446eSAtari911 } 3749*9ccd446eSAtari911 3750*9ccd446eSAtari911 $backupName = 'calendar.backup.v' . $version . '.manual.' . date('Y-m-d_H-i-s') . '.zip'; 3751*9ccd446eSAtari911 $backupPath = DOKU_PLUGIN . $backupName; 3752*9ccd446eSAtari911 3753*9ccd446eSAtari911 try { 3754*9ccd446eSAtari911 $zip = new ZipArchive(); 3755*9ccd446eSAtari911 if ($zip->open($backupPath, ZipArchive::CREATE) === TRUE) { 3756*9ccd446eSAtari911 $fileCount = $this->addDirectoryToZip($zip, $pluginDir, 'calendar/'); 3757*9ccd446eSAtari911 $zip->close(); 3758*9ccd446eSAtari911 3759*9ccd446eSAtari911 // Verify backup was created and has content 3760*9ccd446eSAtari911 if (!file_exists($backupPath)) { 3761*9ccd446eSAtari911 $this->redirect('Backup file was not created', 'error', 'update'); 3762*9ccd446eSAtari911 return; 3763*9ccd446eSAtari911 } 3764*9ccd446eSAtari911 3765*9ccd446eSAtari911 $backupSize = filesize($backupPath); 3766*9ccd446eSAtari911 if ($backupSize < 1000) { // Backup should be at least 1KB 3767*9ccd446eSAtari911 @unlink($backupPath); 3768*9ccd446eSAtari911 $this->redirect('Backup file is too small (' . $this->formatBytes($backupSize) . '). Only ' . $fileCount . ' files were added. Backup failed.', 'error', 'update'); 3769*9ccd446eSAtari911 return; 3770*9ccd446eSAtari911 } 3771*9ccd446eSAtari911 3772*9ccd446eSAtari911 if ($fileCount < 10) { // Should have at least 10 files 3773*9ccd446eSAtari911 @unlink($backupPath); 3774*9ccd446eSAtari911 $this->redirect('Backup incomplete: Only ' . $fileCount . ' files were added (expected 30+). Backup failed.', 'error', 'update'); 3775*9ccd446eSAtari911 return; 3776*9ccd446eSAtari911 } 3777*9ccd446eSAtari911 3778*9ccd446eSAtari911 // Success! 3779*9ccd446eSAtari911 $this->redirect('✓ Manual backup created successfully: ' . $backupName . ' (' . $this->formatBytes($backupSize) . ', ' . $fileCount . ' files)', 'success', 'update'); 3780*9ccd446eSAtari911 3781*9ccd446eSAtari911 } else { 3782*9ccd446eSAtari911 $this->redirect('Failed to create backup ZIP file', 'error', 'update'); 3783*9ccd446eSAtari911 return; 3784*9ccd446eSAtari911 } 3785*9ccd446eSAtari911 } catch (Exception $e) { 3786*9ccd446eSAtari911 if (file_exists($backupPath)) { 3787*9ccd446eSAtari911 @unlink($backupPath); 3788*9ccd446eSAtari911 } 3789*9ccd446eSAtari911 $this->redirect('Backup failed: ' . $e->getMessage(), 'error', 'update'); 3790*9ccd446eSAtari911 return; 3791*9ccd446eSAtari911 } 3792*9ccd446eSAtari911 } 3793*9ccd446eSAtari911 37941d05cddcSAtari911 private function addDirectoryToZip($zip, $dir, $zipPath = '') { 3795*9ccd446eSAtari911 $fileCount = 0; 3796*9ccd446eSAtari911 $errors = []; 3797*9ccd446eSAtari911 3798*9ccd446eSAtari911 if (!is_dir($dir)) { 3799*9ccd446eSAtari911 throw new Exception("Directory does not exist: $dir"); 3800*9ccd446eSAtari911 } 3801*9ccd446eSAtari911 3802*9ccd446eSAtari911 if (!is_readable($dir)) { 3803*9ccd446eSAtari911 throw new Exception("Directory is not readable: $dir"); 3804*9ccd446eSAtari911 } 3805*9ccd446eSAtari911 38061d05cddcSAtari911 try { 38071d05cddcSAtari911 $files = new RecursiveIteratorIterator( 38081d05cddcSAtari911 new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), 38091d05cddcSAtari911 RecursiveIteratorIterator::LEAVES_ONLY 38101d05cddcSAtari911 ); 38111d05cddcSAtari911 38121d05cddcSAtari911 foreach ($files as $file) { 38131d05cddcSAtari911 if (!$file->isDir()) { 38141d05cddcSAtari911 $filePath = $file->getRealPath(); 38151d05cddcSAtari911 if ($filePath && is_readable($filePath)) { 38161d05cddcSAtari911 $relativePath = $zipPath . substr($filePath, strlen($dir)); 3817*9ccd446eSAtari911 3818*9ccd446eSAtari911 if ($zip->addFile($filePath, $relativePath)) { 3819*9ccd446eSAtari911 $fileCount++; 3820*9ccd446eSAtari911 } else { 3821*9ccd446eSAtari911 $errors[] = "Failed to add: " . basename($filePath); 3822*9ccd446eSAtari911 } 3823*9ccd446eSAtari911 } else { 3824*9ccd446eSAtari911 $errors[] = "Cannot read: " . ($filePath ? basename($filePath) : 'unknown'); 38251d05cddcSAtari911 } 38261d05cddcSAtari911 } 38271d05cddcSAtari911 } 3828*9ccd446eSAtari911 3829*9ccd446eSAtari911 // Log any errors but don't fail if we got most files 3830*9ccd446eSAtari911 if (!empty($errors) && count($errors) < 5) { 3831*9ccd446eSAtari911 foreach ($errors as $error) { 3832*9ccd446eSAtari911 error_log('Calendar plugin backup warning: ' . $error); 3833*9ccd446eSAtari911 } 3834*9ccd446eSAtari911 } 3835*9ccd446eSAtari911 3836*9ccd446eSAtari911 // If too many errors, fail 3837*9ccd446eSAtari911 if (count($errors) > 5) { 3838*9ccd446eSAtari911 throw new Exception("Too many errors adding files to backup: " . implode(', ', array_slice($errors, 0, 5))); 3839*9ccd446eSAtari911 } 3840*9ccd446eSAtari911 38411d05cddcSAtari911 } catch (Exception $e) { 3842*9ccd446eSAtari911 error_log('Calendar plugin backup error: ' . $e->getMessage()); 3843*9ccd446eSAtari911 throw $e; 38441d05cddcSAtari911 } 3845*9ccd446eSAtari911 3846*9ccd446eSAtari911 return $fileCount; 38471d05cddcSAtari911 } 38481d05cddcSAtari911 38491d05cddcSAtari911 private function deleteDirectory($dir) { 38501d05cddcSAtari911 if (!is_dir($dir)) return; 38511d05cddcSAtari911 38521d05cddcSAtari911 try { 38531d05cddcSAtari911 $files = new RecursiveIteratorIterator( 38541d05cddcSAtari911 new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), 38551d05cddcSAtari911 RecursiveIteratorIterator::CHILD_FIRST 38561d05cddcSAtari911 ); 38571d05cddcSAtari911 38581d05cddcSAtari911 foreach ($files as $file) { 38591d05cddcSAtari911 if ($file->isDir()) { 38601d05cddcSAtari911 @rmdir($file->getRealPath()); 38611d05cddcSAtari911 } else { 38621d05cddcSAtari911 @unlink($file->getRealPath()); 38631d05cddcSAtari911 } 38641d05cddcSAtari911 } 38651d05cddcSAtari911 38661d05cddcSAtari911 @rmdir($dir); 38671d05cddcSAtari911 } catch (Exception $e) { 38681d05cddcSAtari911 error_log('Calendar plugin delete directory error: ' . $e->getMessage()); 38691d05cddcSAtari911 } 38701d05cddcSAtari911 } 38711d05cddcSAtari911 38721d05cddcSAtari911 private function deleteDirectoryContents($dir, $preserve = []) { 38731d05cddcSAtari911 if (!is_dir($dir)) return; 38741d05cddcSAtari911 38751d05cddcSAtari911 $items = scandir($dir); 38761d05cddcSAtari911 foreach ($items as $item) { 38771d05cddcSAtari911 if ($item === '.' || $item === '..') continue; 38781d05cddcSAtari911 if (in_array($item, $preserve)) continue; 38791d05cddcSAtari911 38801d05cddcSAtari911 $path = $dir . $item; 38811d05cddcSAtari911 if (is_dir($path)) { 38821d05cddcSAtari911 $this->deleteDirectory($path); 38831d05cddcSAtari911 } else { 38841d05cddcSAtari911 unlink($path); 38851d05cddcSAtari911 } 38861d05cddcSAtari911 } 38871d05cddcSAtari911 } 38881d05cddcSAtari911 38891d05cddcSAtari911 private function recursiveCopy($src, $dst) { 38901d05cddcSAtari911 $dir = opendir($src); 38911d05cddcSAtari911 @mkdir($dst); 38921d05cddcSAtari911 38931d05cddcSAtari911 while (($file = readdir($dir)) !== false) { 38941d05cddcSAtari911 if ($file !== '.' && $file !== '..') { 38951d05cddcSAtari911 if (is_dir($src . '/' . $file)) { 38961d05cddcSAtari911 $this->recursiveCopy($src . '/' . $file, $dst . '/' . $file); 38971d05cddcSAtari911 } else { 38981d05cddcSAtari911 copy($src . '/' . $file, $dst . '/' . $file); 38991d05cddcSAtari911 } 39001d05cddcSAtari911 } 39011d05cddcSAtari911 } 39021d05cddcSAtari911 39031d05cddcSAtari911 closedir($dir); 39041d05cddcSAtari911 } 39051d05cddcSAtari911 39061d05cddcSAtari911 private function formatBytes($bytes) { 39071d05cddcSAtari911 if ($bytes >= 1073741824) { 39081d05cddcSAtari911 return number_format($bytes / 1073741824, 2) . ' GB'; 39091d05cddcSAtari911 } elseif ($bytes >= 1048576) { 39101d05cddcSAtari911 return number_format($bytes / 1048576, 2) . ' MB'; 39111d05cddcSAtari911 } elseif ($bytes >= 1024) { 39121d05cddcSAtari911 return number_format($bytes / 1024, 2) . ' KB'; 39131d05cddcSAtari911 } else { 39141d05cddcSAtari911 return $bytes . ' bytes'; 39151d05cddcSAtari911 } 39161d05cddcSAtari911 } 39171d05cddcSAtari911 39181d05cddcSAtari911 private function findPhpBinary() { 39191d05cddcSAtari911 // Try PHP_BINARY constant first (most reliable if available) 39201d05cddcSAtari911 if (defined('PHP_BINARY') && !empty(PHP_BINARY) && is_executable(PHP_BINARY)) { 39211d05cddcSAtari911 return escapeshellarg(PHP_BINARY); 39221d05cddcSAtari911 } 39231d05cddcSAtari911 39241d05cddcSAtari911 // Try common PHP binary locations 39251d05cddcSAtari911 $possiblePaths = [ 39261d05cddcSAtari911 '/usr/bin/php', 39271d05cddcSAtari911 '/usr/bin/php8.1', 39281d05cddcSAtari911 '/usr/bin/php8.2', 39291d05cddcSAtari911 '/usr/bin/php8.3', 39301d05cddcSAtari911 '/usr/bin/php7.4', 39311d05cddcSAtari911 '/usr/local/bin/php', 39321d05cddcSAtari911 'php' // Last resort - rely on PATH 39331d05cddcSAtari911 ]; 39341d05cddcSAtari911 39351d05cddcSAtari911 foreach ($possiblePaths as $path) { 39361d05cddcSAtari911 // Test if this PHP binary works 39371d05cddcSAtari911 $testOutput = []; 39381d05cddcSAtari911 $testReturn = 0; 39391d05cddcSAtari911 exec($path . ' -v 2>&1', $testOutput, $testReturn); 39401d05cddcSAtari911 39411d05cddcSAtari911 if ($testReturn === 0) { 39421d05cddcSAtari911 return ($path === 'php') ? 'php' : escapeshellarg($path); 39431d05cddcSAtari911 } 39441d05cddcSAtari911 } 39451d05cddcSAtari911 39461d05cddcSAtari911 // Fallback to 'php' and hope it's in PATH 39471d05cddcSAtari911 return 'php'; 39481d05cddcSAtari911 } 39491d05cddcSAtari911 39501d05cddcSAtari911 private function redirect($message, $type = 'success', $tab = null) { 39511d05cddcSAtari911 $url = '?do=admin&page=calendar'; 39521d05cddcSAtari911 if ($tab) { 39531d05cddcSAtari911 $url .= '&tab=' . $tab; 39541d05cddcSAtari911 } 39551d05cddcSAtari911 $url .= '&msg=' . urlencode($message) . '&msgtype=' . $type; 39561d05cddcSAtari911 header('Location: ' . $url); 39571d05cddcSAtari911 exit; 39581d05cddcSAtari911 } 39591d05cddcSAtari911 39601d05cddcSAtari911 private function getLog() { 39611d05cddcSAtari911 global $INPUT; 39621d05cddcSAtari911 39631d05cddcSAtari911 if ($INPUT->str('call') === 'ajax') { 39641d05cddcSAtari911 header('Content-Type: application/json'); 39651d05cddcSAtari911 39661d05cddcSAtari911 $logFile = DOKU_PLUGIN . 'calendar/sync.log'; 39671d05cddcSAtari911 $log = ''; 39681d05cddcSAtari911 39691d05cddcSAtari911 if (file_exists($logFile)) { 39701d05cddcSAtari911 // Get last 500 lines 39711d05cddcSAtari911 $lines = file($logFile); 39721d05cddcSAtari911 if ($lines !== false) { 39731d05cddcSAtari911 $lines = array_slice($lines, -500); 39741d05cddcSAtari911 $log = implode('', $lines); 39751d05cddcSAtari911 } 39761d05cddcSAtari911 } else { 39771d05cddcSAtari911 $log = "No log file found. Sync hasn't run yet."; 39781d05cddcSAtari911 } 39791d05cddcSAtari911 39801d05cddcSAtari911 echo json_encode(['log' => $log]); 39811d05cddcSAtari911 exit; 39821d05cddcSAtari911 } 39831d05cddcSAtari911 } 39841d05cddcSAtari911 39851d05cddcSAtari911 private function exportConfig() { 39861d05cddcSAtari911 global $INPUT; 39871d05cddcSAtari911 39881d05cddcSAtari911 if ($INPUT->str('call') === 'ajax') { 39891d05cddcSAtari911 header('Content-Type: application/json'); 39901d05cddcSAtari911 39911d05cddcSAtari911 try { 39921d05cddcSAtari911 $configFile = DOKU_PLUGIN . 'calendar/sync_config.php'; 39931d05cddcSAtari911 39941d05cddcSAtari911 if (!file_exists($configFile)) { 39951d05cddcSAtari911 echo json_encode([ 39961d05cddcSAtari911 'success' => false, 39971d05cddcSAtari911 'message' => 'Config file not found' 39981d05cddcSAtari911 ]); 39991d05cddcSAtari911 exit; 40001d05cddcSAtari911 } 40011d05cddcSAtari911 40021d05cddcSAtari911 // Read config file 40031d05cddcSAtari911 $configContent = file_get_contents($configFile); 40041d05cddcSAtari911 40051d05cddcSAtari911 // Generate encryption key from DokuWiki secret 40061d05cddcSAtari911 $key = $this->getEncryptionKey(); 40071d05cddcSAtari911 40081d05cddcSAtari911 // Encrypt config 40091d05cddcSAtari911 $encrypted = $this->encryptData($configContent, $key); 40101d05cddcSAtari911 40111d05cddcSAtari911 echo json_encode([ 40121d05cddcSAtari911 'success' => true, 40131d05cddcSAtari911 'encrypted' => $encrypted, 40141d05cddcSAtari911 'message' => 'Config exported successfully' 40151d05cddcSAtari911 ]); 40161d05cddcSAtari911 exit; 40171d05cddcSAtari911 40181d05cddcSAtari911 } catch (Exception $e) { 40191d05cddcSAtari911 echo json_encode([ 40201d05cddcSAtari911 'success' => false, 40211d05cddcSAtari911 'message' => $e->getMessage() 40221d05cddcSAtari911 ]); 40231d05cddcSAtari911 exit; 40241d05cddcSAtari911 } 40251d05cddcSAtari911 } 40261d05cddcSAtari911 } 40271d05cddcSAtari911 40281d05cddcSAtari911 private function importConfig() { 40291d05cddcSAtari911 global $INPUT; 40301d05cddcSAtari911 40311d05cddcSAtari911 if ($INPUT->str('call') === 'ajax') { 40321d05cddcSAtari911 header('Content-Type: application/json'); 40331d05cddcSAtari911 40341d05cddcSAtari911 try { 40351d05cddcSAtari911 $encrypted = $_POST['encrypted_config'] ?? ''; 40361d05cddcSAtari911 40371d05cddcSAtari911 if (empty($encrypted)) { 40381d05cddcSAtari911 echo json_encode([ 40391d05cddcSAtari911 'success' => false, 40401d05cddcSAtari911 'message' => 'No config data provided' 40411d05cddcSAtari911 ]); 40421d05cddcSAtari911 exit; 40431d05cddcSAtari911 } 40441d05cddcSAtari911 40451d05cddcSAtari911 // Generate encryption key from DokuWiki secret 40461d05cddcSAtari911 $key = $this->getEncryptionKey(); 40471d05cddcSAtari911 40481d05cddcSAtari911 // Decrypt config 40491d05cddcSAtari911 $configContent = $this->decryptData($encrypted, $key); 40501d05cddcSAtari911 40511d05cddcSAtari911 if ($configContent === false) { 40521d05cddcSAtari911 echo json_encode([ 40531d05cddcSAtari911 'success' => false, 40541d05cddcSAtari911 'message' => 'Decryption failed. Invalid key or corrupted file.' 40551d05cddcSAtari911 ]); 40561d05cddcSAtari911 exit; 40571d05cddcSAtari911 } 40581d05cddcSAtari911 40591d05cddcSAtari911 // Validate PHP syntax 40601d05cddcSAtari911 $valid = @eval('?>' . $configContent); 40611d05cddcSAtari911 if ($valid === false) { 40621d05cddcSAtari911 echo json_encode([ 40631d05cddcSAtari911 'success' => false, 40641d05cddcSAtari911 'message' => 'Invalid config file format' 40651d05cddcSAtari911 ]); 40661d05cddcSAtari911 exit; 40671d05cddcSAtari911 } 40681d05cddcSAtari911 40691d05cddcSAtari911 // Write to config file 40701d05cddcSAtari911 $configFile = DOKU_PLUGIN . 'calendar/sync_config.php'; 40711d05cddcSAtari911 40721d05cddcSAtari911 // Backup existing config 40731d05cddcSAtari911 if (file_exists($configFile)) { 40741d05cddcSAtari911 $backupFile = $configFile . '.backup.' . date('Y-m-d_H-i-s'); 40751d05cddcSAtari911 copy($configFile, $backupFile); 40761d05cddcSAtari911 } 40771d05cddcSAtari911 40781d05cddcSAtari911 // Write new config 40791d05cddcSAtari911 if (file_put_contents($configFile, $configContent) === false) { 40801d05cddcSAtari911 echo json_encode([ 40811d05cddcSAtari911 'success' => false, 40821d05cddcSAtari911 'message' => 'Failed to write config file' 40831d05cddcSAtari911 ]); 40841d05cddcSAtari911 exit; 40851d05cddcSAtari911 } 40861d05cddcSAtari911 40871d05cddcSAtari911 echo json_encode([ 40881d05cddcSAtari911 'success' => true, 40891d05cddcSAtari911 'message' => 'Config imported successfully' 40901d05cddcSAtari911 ]); 40911d05cddcSAtari911 exit; 40921d05cddcSAtari911 40931d05cddcSAtari911 } catch (Exception $e) { 40941d05cddcSAtari911 echo json_encode([ 40951d05cddcSAtari911 'success' => false, 40961d05cddcSAtari911 'message' => $e->getMessage() 40971d05cddcSAtari911 ]); 40981d05cddcSAtari911 exit; 40991d05cddcSAtari911 } 41001d05cddcSAtari911 } 41011d05cddcSAtari911 } 41021d05cddcSAtari911 41031d05cddcSAtari911 private function getEncryptionKey() { 41041d05cddcSAtari911 global $conf; 41051d05cddcSAtari911 // Use DokuWiki's secret as the base for encryption 41061d05cddcSAtari911 // This ensures the key is unique per installation 41071d05cddcSAtari911 return hash('sha256', $conf['secret'] . 'calendar_config_encryption', true); 41081d05cddcSAtari911 } 41091d05cddcSAtari911 41101d05cddcSAtari911 private function encryptData($data, $key) { 41111d05cddcSAtari911 // Use AES-256-CBC encryption 41121d05cddcSAtari911 $ivLength = openssl_cipher_iv_length('aes-256-cbc'); 41131d05cddcSAtari911 $iv = openssl_random_pseudo_bytes($ivLength); 41141d05cddcSAtari911 41151d05cddcSAtari911 $encrypted = openssl_encrypt($data, 'aes-256-cbc', $key, 0, $iv); 41161d05cddcSAtari911 41171d05cddcSAtari911 // Combine IV and encrypted data, then base64 encode 41181d05cddcSAtari911 return base64_encode($iv . $encrypted); 41191d05cddcSAtari911 } 41201d05cddcSAtari911 41211d05cddcSAtari911 private function decryptData($encryptedData, $key) { 41221d05cddcSAtari911 // Decode base64 41231d05cddcSAtari911 $data = base64_decode($encryptedData); 41241d05cddcSAtari911 41251d05cddcSAtari911 if ($data === false) { 41261d05cddcSAtari911 return false; 41271d05cddcSAtari911 } 41281d05cddcSAtari911 41291d05cddcSAtari911 // Extract IV and encrypted content 41301d05cddcSAtari911 $ivLength = openssl_cipher_iv_length('aes-256-cbc'); 41311d05cddcSAtari911 $iv = substr($data, 0, $ivLength); 41321d05cddcSAtari911 $encrypted = substr($data, $ivLength); 41331d05cddcSAtari911 41341d05cddcSAtari911 // Decrypt 41351d05cddcSAtari911 $decrypted = openssl_decrypt($encrypted, 'aes-256-cbc', $key, 0, $iv); 41361d05cddcSAtari911 41371d05cddcSAtari911 return $decrypted; 41381d05cddcSAtari911 } 41391d05cddcSAtari911 41401d05cddcSAtari911 private function clearLogFile() { 41411d05cddcSAtari911 global $INPUT; 41421d05cddcSAtari911 41431d05cddcSAtari911 if ($INPUT->str('call') === 'ajax') { 41441d05cddcSAtari911 header('Content-Type: application/json'); 41451d05cddcSAtari911 41461d05cddcSAtari911 $logFile = DOKU_PLUGIN . 'calendar/sync.log'; 41471d05cddcSAtari911 41481d05cddcSAtari911 if (file_exists($logFile)) { 41491d05cddcSAtari911 if (file_put_contents($logFile, '')) { 41501d05cddcSAtari911 echo json_encode(['success' => true]); 41511d05cddcSAtari911 } else { 41521d05cddcSAtari911 echo json_encode(['success' => false, 'message' => 'Could not clear log file']); 41531d05cddcSAtari911 } 41541d05cddcSAtari911 } else { 41551d05cddcSAtari911 echo json_encode(['success' => true, 'message' => 'No log file to clear']); 41561d05cddcSAtari911 } 41571d05cddcSAtari911 exit; 41581d05cddcSAtari911 } 41591d05cddcSAtari911 } 41601d05cddcSAtari911 41611d05cddcSAtari911 private function downloadLog() { 41621d05cddcSAtari911 $logFile = DOKU_PLUGIN . 'calendar/sync.log'; 41631d05cddcSAtari911 41641d05cddcSAtari911 if (file_exists($logFile)) { 41651d05cddcSAtari911 header('Content-Type: text/plain'); 41661d05cddcSAtari911 header('Content-Disposition: attachment; filename="calendar-sync-' . date('Y-m-d-His') . '.log"'); 41671d05cddcSAtari911 readfile($logFile); 41681d05cddcSAtari911 exit; 41691d05cddcSAtari911 } else { 41701d05cddcSAtari911 echo 'No log file found'; 41711d05cddcSAtari911 exit; 41721d05cddcSAtari911 } 41731d05cddcSAtari911 } 41741d05cddcSAtari911 41751d05cddcSAtari911 private function getEventStatistics() { 41761d05cddcSAtari911 $stats = [ 41771d05cddcSAtari911 'total_events' => 0, 41781d05cddcSAtari911 'total_namespaces' => 0, 41791d05cddcSAtari911 'total_files' => 0, 41801d05cddcSAtari911 'total_recurring' => 0, 41811d05cddcSAtari911 'by_namespace' => [], 41821d05cddcSAtari911 'last_scan' => '' 41831d05cddcSAtari911 ]; 41841d05cddcSAtari911 41851d05cddcSAtari911 $metaDir = DOKU_INC . 'data/meta/'; 41861d05cddcSAtari911 $cacheFile = DOKU_PLUGIN . 'calendar/.event_stats_cache'; 41871d05cddcSAtari911 41881d05cddcSAtari911 // Check if we have cached stats (less than 5 minutes old) 41891d05cddcSAtari911 if (file_exists($cacheFile)) { 41901d05cddcSAtari911 $cacheData = json_decode(file_get_contents($cacheFile), true); 41911d05cddcSAtari911 if ($cacheData && (time() - $cacheData['timestamp']) < 300) { 41921d05cddcSAtari911 return $cacheData['stats']; 41931d05cddcSAtari911 } 41941d05cddcSAtari911 } 41951d05cddcSAtari911 41961d05cddcSAtari911 // Scan for events 41971d05cddcSAtari911 $this->scanDirectoryForStats($metaDir, '', $stats); 41981d05cddcSAtari911 41991d05cddcSAtari911 // Count recurring events 42001d05cddcSAtari911 $recurringEvents = $this->findRecurringEvents(); 42011d05cddcSAtari911 $stats['total_recurring'] = count($recurringEvents); 42021d05cddcSAtari911 42031d05cddcSAtari911 $stats['total_namespaces'] = count($stats['by_namespace']); 42041d05cddcSAtari911 $stats['last_scan'] = date('Y-m-d H:i:s'); 42051d05cddcSAtari911 42061d05cddcSAtari911 // Cache the results 42071d05cddcSAtari911 file_put_contents($cacheFile, json_encode([ 42081d05cddcSAtari911 'timestamp' => time(), 42091d05cddcSAtari911 'stats' => $stats 42101d05cddcSAtari911 ])); 42111d05cddcSAtari911 42121d05cddcSAtari911 return $stats; 42131d05cddcSAtari911 } 42141d05cddcSAtari911 42151d05cddcSAtari911 private function scanDirectoryForStats($dir, $namespace, &$stats) { 42161d05cddcSAtari911 if (!is_dir($dir)) return; 42171d05cddcSAtari911 42181d05cddcSAtari911 $items = scandir($dir); 42191d05cddcSAtari911 foreach ($items as $item) { 42201d05cddcSAtari911 if ($item === '.' || $item === '..') continue; 42211d05cddcSAtari911 42221d05cddcSAtari911 $path = $dir . $item; 42231d05cddcSAtari911 42241d05cddcSAtari911 // Check if this is a calendar directory 42251d05cddcSAtari911 if ($item === 'calendar' && is_dir($path)) { 42261d05cddcSAtari911 $jsonFiles = glob($path . '/*.json'); 42271d05cddcSAtari911 $eventCount = 0; 42281d05cddcSAtari911 42291d05cddcSAtari911 foreach ($jsonFiles as $file) { 42301d05cddcSAtari911 $stats['total_files']++; 42311d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 42321d05cddcSAtari911 if ($data) { 42331d05cddcSAtari911 foreach ($data as $dateEvents) { 42341d05cddcSAtari911 $eventCount += count($dateEvents); 42351d05cddcSAtari911 } 42361d05cddcSAtari911 } 42371d05cddcSAtari911 } 42381d05cddcSAtari911 42391d05cddcSAtari911 $stats['total_events'] += $eventCount; 42401d05cddcSAtari911 42411d05cddcSAtari911 if ($eventCount > 0) { 42421d05cddcSAtari911 $stats['by_namespace'][$namespace] = [ 42431d05cddcSAtari911 'events' => $eventCount, 42441d05cddcSAtari911 'files' => count($jsonFiles) 42451d05cddcSAtari911 ]; 42461d05cddcSAtari911 } 42471d05cddcSAtari911 } elseif (is_dir($path)) { 42481d05cddcSAtari911 // Recurse into subdirectories 42491d05cddcSAtari911 $newNamespace = $namespace ? $namespace . ':' . $item : $item; 42501d05cddcSAtari911 $this->scanDirectoryForStats($path . '/', $newNamespace, $stats); 42511d05cddcSAtari911 } 42521d05cddcSAtari911 } 42531d05cddcSAtari911 } 42541d05cddcSAtari911 42551d05cddcSAtari911 private function rescanEvents() { 42561d05cddcSAtari911 // Clear the cache to force a rescan 4257*9ccd446eSAtari911 $this->clearStatsCache(); 42581d05cddcSAtari911 42591d05cddcSAtari911 // Get fresh statistics 42601d05cddcSAtari911 $stats = $this->getEventStatistics(); 42611d05cddcSAtari911 42621d05cddcSAtari911 // Build absolute redirect URL 42631d05cddcSAtari911 $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'; 42641d05cddcSAtari911 42651d05cddcSAtari911 // Redirect with success message using absolute URL 42661d05cddcSAtari911 header('Location: ' . $redirectUrl, true, 303); 42671d05cddcSAtari911 exit; 42681d05cddcSAtari911 } 42691d05cddcSAtari911 42701d05cddcSAtari911 private function exportAllEvents() { 42711d05cddcSAtari911 $metaDir = DOKU_INC . 'data/meta/'; 42721d05cddcSAtari911 $allEvents = []; 42731d05cddcSAtari911 42741d05cddcSAtari911 // Collect all events 42751d05cddcSAtari911 $this->collectAllEvents($metaDir, '', $allEvents); 42761d05cddcSAtari911 42771d05cddcSAtari911 // Create export package 4278*9ccd446eSAtari911 // Get current version 4279*9ccd446eSAtari911 $pluginInfo = DOKU_PLUGIN . 'calendar/plugin.info.txt'; 4280*9ccd446eSAtari911 $info = file_exists($pluginInfo) ? confToHash($pluginInfo) : []; 4281*9ccd446eSAtari911 $currentVersion = isset($info['version']) ? trim($info['version']) : 'unknown'; 4282*9ccd446eSAtari911 42831d05cddcSAtari911 $exportData = [ 42841d05cddcSAtari911 'export_date' => date('Y-m-d H:i:s'), 4285*9ccd446eSAtari911 'version' => $currentVersion, 42861d05cddcSAtari911 'total_events' => 0, 42871d05cddcSAtari911 'namespaces' => [] 42881d05cddcSAtari911 ]; 42891d05cddcSAtari911 42901d05cddcSAtari911 foreach ($allEvents as $namespace => $files) { 42911d05cddcSAtari911 $exportData['namespaces'][$namespace] = []; 42921d05cddcSAtari911 foreach ($files as $filename => $events) { 42931d05cddcSAtari911 $exportData['namespaces'][$namespace][$filename] = $events; 42941d05cddcSAtari911 foreach ($events as $dateEvents) { 42951d05cddcSAtari911 $exportData['total_events'] += count($dateEvents); 42961d05cddcSAtari911 } 42971d05cddcSAtari911 } 42981d05cddcSAtari911 } 42991d05cddcSAtari911 43001d05cddcSAtari911 // Send as download 43011d05cddcSAtari911 header('Content-Type: application/json'); 43021d05cddcSAtari911 header('Content-Disposition: attachment; filename="calendar-events-export-' . date('Y-m-d-His') . '.json"'); 43031d05cddcSAtari911 echo json_encode($exportData, JSON_PRETTY_PRINT); 43041d05cddcSAtari911 exit; 43051d05cddcSAtari911 } 43061d05cddcSAtari911 43071d05cddcSAtari911 private function collectAllEvents($dir, $namespace, &$allEvents) { 43081d05cddcSAtari911 if (!is_dir($dir)) return; 43091d05cddcSAtari911 43101d05cddcSAtari911 $items = scandir($dir); 43111d05cddcSAtari911 foreach ($items as $item) { 43121d05cddcSAtari911 if ($item === '.' || $item === '..') continue; 43131d05cddcSAtari911 43141d05cddcSAtari911 $path = $dir . $item; 43151d05cddcSAtari911 43161d05cddcSAtari911 // Check if this is a calendar directory 43171d05cddcSAtari911 if ($item === 'calendar' && is_dir($path)) { 43181d05cddcSAtari911 $jsonFiles = glob($path . '/*.json'); 43191d05cddcSAtari911 43201d05cddcSAtari911 if (!isset($allEvents[$namespace])) { 43211d05cddcSAtari911 $allEvents[$namespace] = []; 43221d05cddcSAtari911 } 43231d05cddcSAtari911 43241d05cddcSAtari911 foreach ($jsonFiles as $file) { 43251d05cddcSAtari911 $filename = basename($file); 43261d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 43271d05cddcSAtari911 if ($data) { 43281d05cddcSAtari911 $allEvents[$namespace][$filename] = $data; 43291d05cddcSAtari911 } 43301d05cddcSAtari911 } 43311d05cddcSAtari911 } elseif (is_dir($path)) { 43321d05cddcSAtari911 // Recurse into subdirectories 43331d05cddcSAtari911 $newNamespace = $namespace ? $namespace . ':' . $item : $item; 43341d05cddcSAtari911 $this->collectAllEvents($path . '/', $newNamespace, $allEvents); 43351d05cddcSAtari911 } 43361d05cddcSAtari911 } 43371d05cddcSAtari911 } 43381d05cddcSAtari911 43391d05cddcSAtari911 private function importAllEvents() { 43401d05cddcSAtari911 global $INPUT; 43411d05cddcSAtari911 43421d05cddcSAtari911 if (!isset($_FILES['import_file'])) { 43431d05cddcSAtari911 $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('No file uploaded') . '&msgtype=error'; 43441d05cddcSAtari911 header('Location: ' . $redirectUrl, true, 303); 43451d05cddcSAtari911 exit; 43461d05cddcSAtari911 } 43471d05cddcSAtari911 43481d05cddcSAtari911 $file = $_FILES['import_file']; 43491d05cddcSAtari911 43501d05cddcSAtari911 if ($file['error'] !== UPLOAD_ERR_OK) { 43511d05cddcSAtari911 $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('Upload error: ' . $file['error']) . '&msgtype=error'; 43521d05cddcSAtari911 header('Location: ' . $redirectUrl, true, 303); 43531d05cddcSAtari911 exit; 43541d05cddcSAtari911 } 43551d05cddcSAtari911 43561d05cddcSAtari911 // Read and decode the import file 43571d05cddcSAtari911 $importData = json_decode(file_get_contents($file['tmp_name']), true); 43581d05cddcSAtari911 43591d05cddcSAtari911 if (!$importData || !isset($importData['namespaces'])) { 43601d05cddcSAtari911 $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('Invalid import file format') . '&msgtype=error'; 43611d05cddcSAtari911 header('Location: ' . $redirectUrl, true, 303); 43621d05cddcSAtari911 exit; 43631d05cddcSAtari911 } 43641d05cddcSAtari911 43651d05cddcSAtari911 $importedCount = 0; 43661d05cddcSAtari911 $mergedCount = 0; 43671d05cddcSAtari911 43681d05cddcSAtari911 // Import events 43691d05cddcSAtari911 foreach ($importData['namespaces'] as $namespace => $files) { 43701d05cddcSAtari911 $metaDir = DOKU_INC . 'data/meta/'; 43711d05cddcSAtari911 if ($namespace) { 43721d05cddcSAtari911 $metaDir .= str_replace(':', '/', $namespace) . '/'; 43731d05cddcSAtari911 } 43741d05cddcSAtari911 $calendarDir = $metaDir . 'calendar/'; 43751d05cddcSAtari911 43761d05cddcSAtari911 // Create directory if needed 43771d05cddcSAtari911 if (!is_dir($calendarDir)) { 43781d05cddcSAtari911 mkdir($calendarDir, 0755, true); 43791d05cddcSAtari911 } 43801d05cddcSAtari911 43811d05cddcSAtari911 foreach ($files as $filename => $events) { 43821d05cddcSAtari911 $targetFile = $calendarDir . $filename; 43831d05cddcSAtari911 43841d05cddcSAtari911 // If file exists, merge events 43851d05cddcSAtari911 if (file_exists($targetFile)) { 43861d05cddcSAtari911 $existing = json_decode(file_get_contents($targetFile), true); 43871d05cddcSAtari911 if ($existing) { 43881d05cddcSAtari911 foreach ($events as $date => $dateEvents) { 43891d05cddcSAtari911 if (!isset($existing[$date])) { 43901d05cddcSAtari911 $existing[$date] = []; 43911d05cddcSAtari911 } 43921d05cddcSAtari911 foreach ($dateEvents as $event) { 43931d05cddcSAtari911 // Check if event with same ID exists 43941d05cddcSAtari911 $found = false; 43951d05cddcSAtari911 foreach ($existing[$date] as $existingEvent) { 43961d05cddcSAtari911 if ($existingEvent['id'] === $event['id']) { 43971d05cddcSAtari911 $found = true; 43981d05cddcSAtari911 break; 43991d05cddcSAtari911 } 44001d05cddcSAtari911 } 44011d05cddcSAtari911 if (!$found) { 44021d05cddcSAtari911 $existing[$date][] = $event; 44031d05cddcSAtari911 $importedCount++; 44041d05cddcSAtari911 } else { 44051d05cddcSAtari911 $mergedCount++; 44061d05cddcSAtari911 } 44071d05cddcSAtari911 } 44081d05cddcSAtari911 } 44091d05cddcSAtari911 file_put_contents($targetFile, json_encode($existing, JSON_PRETTY_PRINT)); 44101d05cddcSAtari911 } 44111d05cddcSAtari911 } else { 44121d05cddcSAtari911 // New file 44131d05cddcSAtari911 file_put_contents($targetFile, json_encode($events, JSON_PRETTY_PRINT)); 44141d05cddcSAtari911 foreach ($events as $dateEvents) { 44151d05cddcSAtari911 $importedCount += count($dateEvents); 44161d05cddcSAtari911 } 44171d05cddcSAtari911 } 44181d05cddcSAtari911 } 44191d05cddcSAtari911 } 44201d05cddcSAtari911 44211d05cddcSAtari911 // Clear cache 4422*9ccd446eSAtari911 $this->clearStatsCache(); 44231d05cddcSAtari911 44241d05cddcSAtari911 $message = "Import complete! Imported $importedCount new events"; 44251d05cddcSAtari911 if ($mergedCount > 0) { 44261d05cddcSAtari911 $message .= ", skipped $mergedCount duplicates"; 44271d05cddcSAtari911 } 44281d05cddcSAtari911 44291d05cddcSAtari911 $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode($message) . '&msgtype=success'; 44301d05cddcSAtari911 header('Location: ' . $redirectUrl, true, 303); 44311d05cddcSAtari911 exit; 44321d05cddcSAtari911 } 44331d05cddcSAtari911 44341d05cddcSAtari911 private function previewCleanup() { 44351d05cddcSAtari911 global $INPUT; 44361d05cddcSAtari911 44371d05cddcSAtari911 $cleanupType = $INPUT->str('cleanup_type', 'age'); 44381d05cddcSAtari911 $namespaceFilter = $INPUT->str('namespace_filter', ''); 44391d05cddcSAtari911 44401d05cddcSAtari911 // Debug info 44411d05cddcSAtari911 $debug = []; 44421d05cddcSAtari911 $debug['cleanup_type'] = $cleanupType; 44431d05cddcSAtari911 $debug['namespace_filter'] = $namespaceFilter; 44441d05cddcSAtari911 $debug['age_value'] = $INPUT->int('age_value', 6); 44451d05cddcSAtari911 $debug['age_unit'] = $INPUT->str('age_unit', 'months'); 44461d05cddcSAtari911 $debug['range_start'] = $INPUT->str('range_start', ''); 44471d05cddcSAtari911 $debug['range_end'] = $INPUT->str('range_end', ''); 44481d05cddcSAtari911 $debug['delete_completed'] = $INPUT->bool('delete_completed', false); 44491d05cddcSAtari911 $debug['delete_past'] = $INPUT->bool('delete_past', false); 44501d05cddcSAtari911 44511d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 44521d05cddcSAtari911 $debug['data_dir'] = $dataDir; 44531d05cddcSAtari911 $debug['data_dir_exists'] = is_dir($dataDir); 44541d05cddcSAtari911 44551d05cddcSAtari911 $eventsToDelete = $this->findEventsToCleanup($cleanupType, $namespaceFilter); 44561d05cddcSAtari911 44571d05cddcSAtari911 // Merge with scan debug info 44581d05cddcSAtari911 if (isset($this->_cleanupDebug)) { 44591d05cddcSAtari911 $debug = array_merge($debug, $this->_cleanupDebug); 44601d05cddcSAtari911 } 44611d05cddcSAtari911 44621d05cddcSAtari911 // Return JSON for preview with debug info 44631d05cddcSAtari911 header('Content-Type: application/json'); 44641d05cddcSAtari911 echo json_encode([ 44651d05cddcSAtari911 'count' => count($eventsToDelete), 44661d05cddcSAtari911 'events' => array_slice($eventsToDelete, 0, 50), // Limit to 50 for preview 44671d05cddcSAtari911 'debug' => $debug 44681d05cddcSAtari911 ]); 44691d05cddcSAtari911 exit; 44701d05cddcSAtari911 } 44711d05cddcSAtari911 44721d05cddcSAtari911 private function cleanupEvents() { 44731d05cddcSAtari911 global $INPUT; 44741d05cddcSAtari911 44751d05cddcSAtari911 $cleanupType = $INPUT->str('cleanup_type', 'age'); 44761d05cddcSAtari911 $namespaceFilter = $INPUT->str('namespace_filter', ''); 44771d05cddcSAtari911 44781d05cddcSAtari911 // Create backup first 44791d05cddcSAtari911 $backupDir = DOKU_PLUGIN . 'calendar/backups/'; 44801d05cddcSAtari911 if (!is_dir($backupDir)) { 44811d05cddcSAtari911 mkdir($backupDir, 0755, true); 44821d05cddcSAtari911 } 44831d05cddcSAtari911 44841d05cddcSAtari911 $backupFile = $backupDir . 'before-cleanup-' . date('Y-m-d-His') . '.zip'; 44851d05cddcSAtari911 $this->createBackup($backupFile); 44861d05cddcSAtari911 44871d05cddcSAtari911 // Find events to delete 44881d05cddcSAtari911 $eventsToDelete = $this->findEventsToCleanup($cleanupType, $namespaceFilter); 44891d05cddcSAtari911 $deletedCount = 0; 44901d05cddcSAtari911 44911d05cddcSAtari911 // Group by file 44921d05cddcSAtari911 $fileGroups = []; 44931d05cddcSAtari911 foreach ($eventsToDelete as $evt) { 44941d05cddcSAtari911 $fileGroups[$evt['file']][] = $evt; 44951d05cddcSAtari911 } 44961d05cddcSAtari911 44971d05cddcSAtari911 // Delete from each file 44981d05cddcSAtari911 foreach ($fileGroups as $file => $events) { 44991d05cddcSAtari911 if (!file_exists($file)) continue; 45001d05cddcSAtari911 45011d05cddcSAtari911 $json = file_get_contents($file); 45021d05cddcSAtari911 $data = json_decode($json, true); 45031d05cddcSAtari911 45041d05cddcSAtari911 if (!$data) continue; 45051d05cddcSAtari911 45061d05cddcSAtari911 // Remove events 45071d05cddcSAtari911 foreach ($events as $evt) { 45081d05cddcSAtari911 if (isset($data[$evt['date']])) { 45091d05cddcSAtari911 $data[$evt['date']] = array_filter($data[$evt['date']], function($e) use ($evt) { 45101d05cddcSAtari911 return $e['id'] !== $evt['id']; 45111d05cddcSAtari911 }); 45121d05cddcSAtari911 45131d05cddcSAtari911 // Remove date key if empty 45141d05cddcSAtari911 if (empty($data[$evt['date']])) { 45151d05cddcSAtari911 unset($data[$evt['date']]); 45161d05cddcSAtari911 } 45171d05cddcSAtari911 45181d05cddcSAtari911 $deletedCount++; 45191d05cddcSAtari911 } 45201d05cddcSAtari911 } 45211d05cddcSAtari911 45221d05cddcSAtari911 // Save file or delete if empty 45231d05cddcSAtari911 if (empty($data)) { 45241d05cddcSAtari911 unlink($file); 45251d05cddcSAtari911 } else { 45261d05cddcSAtari911 file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); 45271d05cddcSAtari911 } 45281d05cddcSAtari911 } 45291d05cddcSAtari911 45301d05cddcSAtari911 // Clear cache 4531*9ccd446eSAtari911 $this->clearStatsCache(); 45321d05cddcSAtari911 45331d05cddcSAtari911 $message = "Cleanup complete! Deleted $deletedCount event(s). Backup created: " . basename($backupFile); 45341d05cddcSAtari911 $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode($message) . '&msgtype=success'; 45351d05cddcSAtari911 header('Location: ' . $redirectUrl, true, 303); 45361d05cddcSAtari911 exit; 45371d05cddcSAtari911 } 45381d05cddcSAtari911 45391d05cddcSAtari911 private function findEventsToCleanup($cleanupType, $namespaceFilter) { 45401d05cddcSAtari911 global $INPUT; 45411d05cddcSAtari911 45421d05cddcSAtari911 $eventsToDelete = []; 45431d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 45441d05cddcSAtari911 45451d05cddcSAtari911 $debug = []; 45461d05cddcSAtari911 $debug['scanned_dirs'] = []; 45471d05cddcSAtari911 $debug['found_files'] = []; 45481d05cddcSAtari911 45491d05cddcSAtari911 // Calculate cutoff date for age-based cleanup 45501d05cddcSAtari911 $cutoffDate = null; 45511d05cddcSAtari911 if ($cleanupType === 'age') { 45521d05cddcSAtari911 $ageValue = $INPUT->int('age_value', 6); 45531d05cddcSAtari911 $ageUnit = $INPUT->str('age_unit', 'months'); 45541d05cddcSAtari911 45551d05cddcSAtari911 if ($ageUnit === 'years') { 45561d05cddcSAtari911 $ageValue *= 12; // Convert to months 45571d05cddcSAtari911 } 45581d05cddcSAtari911 45591d05cddcSAtari911 $cutoffDate = date('Y-m-d', strtotime("-$ageValue months")); 45601d05cddcSAtari911 $debug['cutoff_date'] = $cutoffDate; 45611d05cddcSAtari911 } 45621d05cddcSAtari911 45631d05cddcSAtari911 // Get date range for range-based cleanup 45641d05cddcSAtari911 $rangeStart = $cleanupType === 'range' ? $INPUT->str('range_start', '') : null; 45651d05cddcSAtari911 $rangeEnd = $cleanupType === 'range' ? $INPUT->str('range_end', '') : null; 45661d05cddcSAtari911 45671d05cddcSAtari911 // Get status filters 45681d05cddcSAtari911 $deleteCompleted = $cleanupType === 'status' && $INPUT->bool('delete_completed', false); 45691d05cddcSAtari911 $deletePast = $cleanupType === 'status' && $INPUT->bool('delete_past', false); 45701d05cddcSAtari911 45711d05cddcSAtari911 // Check root calendar directory first (blank/default namespace) 45721d05cddcSAtari911 $rootCalendarDir = $dataDir . 'calendar'; 45731d05cddcSAtari911 $debug['root_calendar_dir'] = $rootCalendarDir; 45741d05cddcSAtari911 $debug['root_exists'] = is_dir($rootCalendarDir); 45751d05cddcSAtari911 45761d05cddcSAtari911 if (is_dir($rootCalendarDir)) { 45771d05cddcSAtari911 if (!$namespaceFilter || $namespaceFilter === '' || $namespaceFilter === 'default') { 45781d05cddcSAtari911 $debug['scanned_dirs'][] = $rootCalendarDir; 45791d05cddcSAtari911 $files = glob($rootCalendarDir . '/*.json'); 45801d05cddcSAtari911 $debug['found_files'] = array_merge($debug['found_files'], $files); 45811d05cddcSAtari911 $this->processCalendarFiles($rootCalendarDir, '', $eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast); 45821d05cddcSAtari911 } 45831d05cddcSAtari911 } 45841d05cddcSAtari911 45851d05cddcSAtari911 // Scan all namespace directories 45861d05cddcSAtari911 $namespaceDirs = glob($dataDir . '*', GLOB_ONLYDIR); 45871d05cddcSAtari911 $debug['namespace_dirs_found'] = $namespaceDirs; 45881d05cddcSAtari911 45891d05cddcSAtari911 foreach ($namespaceDirs as $nsDir) { 45901d05cddcSAtari911 $namespace = basename($nsDir); 45911d05cddcSAtari911 45921d05cddcSAtari911 // Skip the root 'calendar' dir (already processed above) 45931d05cddcSAtari911 if ($namespace === 'calendar') continue; 45941d05cddcSAtari911 45951d05cddcSAtari911 // Check namespace filter 45961d05cddcSAtari911 if ($namespaceFilter && strpos($namespace, $namespaceFilter) === false) { 45971d05cddcSAtari911 continue; 45981d05cddcSAtari911 } 45991d05cddcSAtari911 46001d05cddcSAtari911 $calendarDir = $nsDir . '/calendar'; 46011d05cddcSAtari911 $debug['checked_calendar_dirs'][] = $calendarDir; 46021d05cddcSAtari911 46031d05cddcSAtari911 if (!is_dir($calendarDir)) { 46041d05cddcSAtari911 $debug['missing_calendar_dirs'][] = $calendarDir; 46051d05cddcSAtari911 continue; 46061d05cddcSAtari911 } 46071d05cddcSAtari911 46081d05cddcSAtari911 $debug['scanned_dirs'][] = $calendarDir; 46091d05cddcSAtari911 $files = glob($calendarDir . '/*.json'); 46101d05cddcSAtari911 $debug['found_files'] = array_merge($debug['found_files'], $files); 46111d05cddcSAtari911 $this->processCalendarFiles($calendarDir, $namespace, $eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast); 46121d05cddcSAtari911 } 46131d05cddcSAtari911 46141d05cddcSAtari911 // Store debug info globally for preview 46151d05cddcSAtari911 $this->_cleanupDebug = $debug; 46161d05cddcSAtari911 46171d05cddcSAtari911 return $eventsToDelete; 46181d05cddcSAtari911 } 46191d05cddcSAtari911 46201d05cddcSAtari911 private function processCalendarFiles($calendarDir, $namespace, &$eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast) { 46211d05cddcSAtari911 foreach (glob($calendarDir . '/*.json') as $file) { 46221d05cddcSAtari911 $json = file_get_contents($file); 46231d05cddcSAtari911 $data = json_decode($json, true); 46241d05cddcSAtari911 46251d05cddcSAtari911 if (!$data) continue; 46261d05cddcSAtari911 46271d05cddcSAtari911 foreach ($data as $date => $dateEvents) { 46281d05cddcSAtari911 foreach ($dateEvents as $event) { 46291d05cddcSAtari911 $shouldDelete = false; 46301d05cddcSAtari911 46311d05cddcSAtari911 // Age-based 46321d05cddcSAtari911 if ($cleanupType === 'age' && $cutoffDate && $date < $cutoffDate) { 46331d05cddcSAtari911 $shouldDelete = true; 46341d05cddcSAtari911 } 46351d05cddcSAtari911 46361d05cddcSAtari911 // Range-based 46371d05cddcSAtari911 if ($cleanupType === 'range' && $rangeStart && $rangeEnd) { 46381d05cddcSAtari911 if ($date >= $rangeStart && $date <= $rangeEnd) { 46391d05cddcSAtari911 $shouldDelete = true; 46401d05cddcSAtari911 } 46411d05cddcSAtari911 } 46421d05cddcSAtari911 46431d05cddcSAtari911 // Status-based 46441d05cddcSAtari911 if ($cleanupType === 'status') { 46451d05cddcSAtari911 $isTask = isset($event['isTask']) && $event['isTask']; 46461d05cddcSAtari911 $isCompleted = isset($event['completed']) && $event['completed']; 46471d05cddcSAtari911 $isPast = $date < date('Y-m-d'); 46481d05cddcSAtari911 46491d05cddcSAtari911 if ($deleteCompleted && $isTask && $isCompleted) { 46501d05cddcSAtari911 $shouldDelete = true; 46511d05cddcSAtari911 } 46521d05cddcSAtari911 if ($deletePast && !$isTask && $isPast) { 46531d05cddcSAtari911 $shouldDelete = true; 46541d05cddcSAtari911 } 46551d05cddcSAtari911 } 46561d05cddcSAtari911 46571d05cddcSAtari911 if ($shouldDelete) { 46581d05cddcSAtari911 $eventsToDelete[] = [ 46591d05cddcSAtari911 'id' => $event['id'], 46601d05cddcSAtari911 'title' => $event['title'], 46611d05cddcSAtari911 'date' => $date, 46621d05cddcSAtari911 'namespace' => $namespace ?: 'default', 46631d05cddcSAtari911 'file' => $file 46641d05cddcSAtari911 ]; 46651d05cddcSAtari911 } 46661d05cddcSAtari911 } 46671d05cddcSAtari911 } 46681d05cddcSAtari911 } 46691d05cddcSAtari911 } 4670*9ccd446eSAtari911 4671*9ccd446eSAtari911 /** 4672*9ccd446eSAtari911 * Render Themes tab for sidebar widget theme selection 4673*9ccd446eSAtari911 */ 4674*9ccd446eSAtari911 private function renderThemesTab($colors = null) { 4675*9ccd446eSAtari911 global $INPUT; 4676*9ccd446eSAtari911 4677*9ccd446eSAtari911 // Use defaults if not provided 4678*9ccd446eSAtari911 if ($colors === null) { 4679*9ccd446eSAtari911 $colors = $this->getTemplateColors(); 4680*9ccd446eSAtari911 } 4681*9ccd446eSAtari911 4682*9ccd446eSAtari911 // Handle theme save 4683*9ccd446eSAtari911 if ($INPUT->str('action') === 'save_theme') { 4684*9ccd446eSAtari911 $theme = $INPUT->str('theme', 'matrix'); 4685*9ccd446eSAtari911 $weekStart = $INPUT->str('week_start', 'monday'); 4686*9ccd446eSAtari911 $this->saveSidebarTheme($theme); 4687*9ccd446eSAtari911 $this->saveWeekStartDay($weekStart); 4688*9ccd446eSAtari911 echo '<div style="background:#d4edda; border:1px solid #c3e6cb; color:#155724; padding:12px; border-radius:4px; margin-bottom:20px;">'; 4689*9ccd446eSAtari911 echo '✓ Theme and settings saved successfully! Refresh any page with the sidebar to see changes.'; 4690*9ccd446eSAtari911 echo '</div>'; 4691*9ccd446eSAtari911 } 4692*9ccd446eSAtari911 4693*9ccd446eSAtari911 $currentTheme = $this->getSidebarTheme(); 4694*9ccd446eSAtari911 $currentWeekStart = $this->getWeekStartDay(); 4695*9ccd446eSAtari911 4696*9ccd446eSAtari911 echo '<h2 style="margin:0 0 20px 0; color:' . $colors['text'] . ';"> Sidebar Widget Settings</h2>'; 4697*9ccd446eSAtari911 echo '<p style="color:' . $colors['text'] . '; margin-bottom:20px;">Customize the appearance and behavior of the sidebar calendar widget.</p>'; 4698*9ccd446eSAtari911 4699*9ccd446eSAtari911 echo '<form method="post" action="?do=admin&page=calendar&tab=themes">'; 4700*9ccd446eSAtari911 echo '<input type="hidden" name="action" value="save_theme">'; 4701*9ccd446eSAtari911 4702*9ccd446eSAtari911 // Week Start Day Section 4703*9ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; border:1px solid ' . $colors['border'] . '; border-radius:6px; padding:20px; margin-bottom:30px;">'; 4704*9ccd446eSAtari911 echo '<h3 style="margin:0 0 15px 0; color:' . $colors['text'] . '; font-size:16px;"> Week Start Day</h3>'; 4705*9ccd446eSAtari911 echo '<p style="color:' . $colors['text'] . '; margin-bottom:15px; font-size:13px;">Choose which day the week calendar grid starts with:</p>'; 4706*9ccd446eSAtari911 4707*9ccd446eSAtari911 echo '<div style="display:flex; gap:15px;">'; 4708*9ccd446eSAtari911 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;">'; 4709*9ccd446eSAtari911 echo '<input type="radio" name="week_start" value="monday" ' . ($currentWeekStart === 'monday' ? 'checked' : '') . ' style="margin-right:10px; width:18px; height:18px;">'; 4710*9ccd446eSAtari911 echo '<div>'; 4711*9ccd446eSAtari911 echo '<div style="font-weight:bold; color:' . $colors['text'] . '; margin-bottom:3px;">Monday</div>'; 4712*9ccd446eSAtari911 echo '<div style="font-size:11px; color:' . $colors['text'] . ';">Week starts on Monday (ISO standard)</div>'; 4713*9ccd446eSAtari911 echo '</div>'; 4714*9ccd446eSAtari911 echo '</label>'; 4715*9ccd446eSAtari911 4716*9ccd446eSAtari911 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;">'; 4717*9ccd446eSAtari911 echo '<input type="radio" name="week_start" value="sunday" ' . ($currentWeekStart === 'sunday' ? 'checked' : '') . ' style="margin-right:10px; width:18px; height:18px;">'; 4718*9ccd446eSAtari911 echo '<div>'; 4719*9ccd446eSAtari911 echo '<div style="font-weight:bold; color:' . $colors['text'] . '; margin-bottom:3px;">Sunday</div>'; 4720*9ccd446eSAtari911 echo '<div style="font-size:11px; color:' . $colors['text'] . ';">Week starts on Sunday (US/Canada standard)</div>'; 4721*9ccd446eSAtari911 echo '</div>'; 4722*9ccd446eSAtari911 echo '</label>'; 4723*9ccd446eSAtari911 echo '</div>'; 4724*9ccd446eSAtari911 echo '</div>'; 4725*9ccd446eSAtari911 4726*9ccd446eSAtari911 // Visual Theme Section 4727*9ccd446eSAtari911 echo '<h3 style="margin:0 0 15px 0; color:' . $colors['text'] . '; font-size:16px;"> Visual Theme</h3>'; 4728*9ccd446eSAtari911 4729*9ccd446eSAtari911 // Matrix Theme 4730*9ccd446eSAtari911 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']) . ';">'; 4731*9ccd446eSAtari911 echo '<label style="display:flex; align-items:center; cursor:pointer;">'; 4732*9ccd446eSAtari911 echo '<input type="radio" name="theme" value="matrix" ' . ($currentTheme === 'matrix' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">'; 4733*9ccd446eSAtari911 echo '<div style="flex:1;">'; 4734*9ccd446eSAtari911 echo '<div style="font-size:18px; font-weight:bold; color:#00cc07; margin-bottom:8px;"> Matrix Edition</div>'; 4735*9ccd446eSAtari911 echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Dark green theme with Matrix-style glow effects and neon accents</div>'; 4736*9ccd446eSAtari911 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>'; 4737*9ccd446eSAtari911 echo '</div>'; 4738*9ccd446eSAtari911 echo '</label>'; 4739*9ccd446eSAtari911 echo '</div>'; 4740*9ccd446eSAtari911 4741*9ccd446eSAtari911 // Purple Theme 4742*9ccd446eSAtari911 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']) . ';">'; 4743*9ccd446eSAtari911 echo '<label style="display:flex; align-items:center; cursor:pointer;">'; 4744*9ccd446eSAtari911 echo '<input type="radio" name="theme" value="purple" ' . ($currentTheme === 'purple' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">'; 4745*9ccd446eSAtari911 echo '<div style="flex:1;">'; 4746*9ccd446eSAtari911 echo '<div style="font-size:18px; font-weight:bold; color:#9b59b6; margin-bottom:8px;"> Purple Dream</div>'; 4747*9ccd446eSAtari911 echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Rich purple theme with elegant violet accents and soft glow</div>'; 4748*9ccd446eSAtari911 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>'; 4749*9ccd446eSAtari911 echo '</div>'; 4750*9ccd446eSAtari911 echo '</label>'; 4751*9ccd446eSAtari911 echo '</div>'; 4752*9ccd446eSAtari911 4753*9ccd446eSAtari911 // Professional Blue Theme 4754*9ccd446eSAtari911 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']) . ';">'; 4755*9ccd446eSAtari911 echo '<label style="display:flex; align-items:center; cursor:pointer;">'; 4756*9ccd446eSAtari911 echo '<input type="radio" name="theme" value="professional" ' . ($currentTheme === 'professional' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">'; 4757*9ccd446eSAtari911 echo '<div style="flex:1;">'; 4758*9ccd446eSAtari911 echo '<div style="font-size:18px; font-weight:bold; color:#4a90e2; margin-bottom:8px;"> Professional Blue</div>'; 4759*9ccd446eSAtari911 echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Clean blue and grey theme with modern professional styling, no glow effects</div>'; 4760*9ccd446eSAtari911 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>'; 4761*9ccd446eSAtari911 echo '</div>'; 4762*9ccd446eSAtari911 echo '</label>'; 4763*9ccd446eSAtari911 echo '</div>'; 4764*9ccd446eSAtari911 4765*9ccd446eSAtari911 // Pink Bling Theme 4766*9ccd446eSAtari911 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']) . ';">'; 4767*9ccd446eSAtari911 echo '<label style="display:flex; align-items:center; cursor:pointer;">'; 4768*9ccd446eSAtari911 echo '<input type="radio" name="theme" value="pink" ' . ($currentTheme === 'pink' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">'; 4769*9ccd446eSAtari911 echo '<div style="flex:1;">'; 4770*9ccd446eSAtari911 echo '<div style="font-size:18px; font-weight:bold; color:#ff1493; margin-bottom:8px;"> Pink Bling</div>'; 4771*9ccd446eSAtari911 echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Glamorous hot pink theme with maximum sparkle, hearts, and diamonds ✨</div>'; 4772*9ccd446eSAtari911 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>'; 4773*9ccd446eSAtari911 echo '</div>'; 4774*9ccd446eSAtari911 echo '</label>'; 4775*9ccd446eSAtari911 echo '</div>'; 4776*9ccd446eSAtari911 4777*9ccd446eSAtari911 // Wiki Default Theme 4778*9ccd446eSAtari911 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']) . ';">'; 4779*9ccd446eSAtari911 echo '<label style="display:flex; align-items:center; cursor:pointer;">'; 4780*9ccd446eSAtari911 echo '<input type="radio" name="theme" value="wiki" ' . ($currentTheme === 'wiki' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">'; 4781*9ccd446eSAtari911 echo '<div style="flex:1;">'; 4782*9ccd446eSAtari911 echo '<div style="font-size:18px; font-weight:bold; color:#2b73b7; margin-bottom:8px;"> Wiki Default</div>'; 4783*9ccd446eSAtari911 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>'; 4784*9ccd446eSAtari911 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>'; 4785*9ccd446eSAtari911 echo '</div>'; 4786*9ccd446eSAtari911 echo '</label>'; 4787*9ccd446eSAtari911 echo '</div>'; 4788*9ccd446eSAtari911 4789*9ccd446eSAtari911 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>'; 4790*9ccd446eSAtari911 echo '</form>'; 4791*9ccd446eSAtari911 } 4792*9ccd446eSAtari911 4793*9ccd446eSAtari911 /** 4794*9ccd446eSAtari911 * Get current sidebar theme 4795*9ccd446eSAtari911 */ 4796*9ccd446eSAtari911 private function getSidebarTheme() { 4797*9ccd446eSAtari911 $configFile = DOKU_INC . 'data/meta/calendar_theme.txt'; 4798*9ccd446eSAtari911 if (file_exists($configFile)) { 4799*9ccd446eSAtari911 return trim(file_get_contents($configFile)); 4800*9ccd446eSAtari911 } 4801*9ccd446eSAtari911 return 'matrix'; // Default 4802*9ccd446eSAtari911 } 4803*9ccd446eSAtari911 4804*9ccd446eSAtari911 /** 4805*9ccd446eSAtari911 * Save sidebar theme 4806*9ccd446eSAtari911 */ 4807*9ccd446eSAtari911 private function saveSidebarTheme($theme) { 4808*9ccd446eSAtari911 $configFile = DOKU_INC . 'data/meta/calendar_theme.txt'; 4809*9ccd446eSAtari911 $validThemes = ['matrix', 'purple', 'professional', 'pink', 'wiki']; 4810*9ccd446eSAtari911 4811*9ccd446eSAtari911 if (in_array($theme, $validThemes)) { 4812*9ccd446eSAtari911 file_put_contents($configFile, $theme); 4813*9ccd446eSAtari911 return true; 4814*9ccd446eSAtari911 } 4815*9ccd446eSAtari911 return false; 4816*9ccd446eSAtari911 } 4817*9ccd446eSAtari911 4818*9ccd446eSAtari911 /** 4819*9ccd446eSAtari911 * Get week start day 4820*9ccd446eSAtari911 */ 4821*9ccd446eSAtari911 private function getWeekStartDay() { 4822*9ccd446eSAtari911 $configFile = DOKU_INC . 'data/meta/calendar_week_start.txt'; 4823*9ccd446eSAtari911 if (file_exists($configFile)) { 4824*9ccd446eSAtari911 $start = trim(file_get_contents($configFile)); 4825*9ccd446eSAtari911 if (in_array($start, ['monday', 'sunday'])) { 4826*9ccd446eSAtari911 return $start; 4827*9ccd446eSAtari911 } 4828*9ccd446eSAtari911 } 4829*9ccd446eSAtari911 return 'sunday'; // Default to Sunday (US/Canada standard) 4830*9ccd446eSAtari911 } 4831*9ccd446eSAtari911 4832*9ccd446eSAtari911 /** 4833*9ccd446eSAtari911 * Save week start day 4834*9ccd446eSAtari911 */ 4835*9ccd446eSAtari911 private function saveWeekStartDay($weekStart) { 4836*9ccd446eSAtari911 $configFile = DOKU_INC . 'data/meta/calendar_week_start.txt'; 4837*9ccd446eSAtari911 $validStarts = ['monday', 'sunday']; 4838*9ccd446eSAtari911 4839*9ccd446eSAtari911 if (in_array($weekStart, $validStarts)) { 4840*9ccd446eSAtari911 file_put_contents($configFile, $weekStart); 4841*9ccd446eSAtari911 return true; 4842*9ccd446eSAtari911 } 4843*9ccd446eSAtari911 return false; 4844*9ccd446eSAtari911 } 4845*9ccd446eSAtari911 4846*9ccd446eSAtari911 /** 4847*9ccd446eSAtari911 * Get colors from DokuWiki template's style.ini file 4848*9ccd446eSAtari911 */ 4849*9ccd446eSAtari911 private function getTemplateColors() { 4850*9ccd446eSAtari911 global $conf; 4851*9ccd446eSAtari911 4852*9ccd446eSAtari911 // Get current template name 4853*9ccd446eSAtari911 $template = $conf['template']; 4854*9ccd446eSAtari911 4855*9ccd446eSAtari911 // Try multiple possible locations for style.ini 4856*9ccd446eSAtari911 $possiblePaths = [ 4857*9ccd446eSAtari911 DOKU_INC . 'conf/tpl/' . $template . '/style.ini', 4858*9ccd446eSAtari911 DOKU_INC . 'lib/tpl/' . $template . '/style.ini', 4859*9ccd446eSAtari911 ]; 4860*9ccd446eSAtari911 4861*9ccd446eSAtari911 $styleIni = null; 4862*9ccd446eSAtari911 foreach ($possiblePaths as $path) { 4863*9ccd446eSAtari911 if (file_exists($path)) { 4864*9ccd446eSAtari911 $styleIni = parse_ini_file($path, true); 4865*9ccd446eSAtari911 break; 4866*9ccd446eSAtari911 } 4867*9ccd446eSAtari911 } 4868*9ccd446eSAtari911 4869*9ccd446eSAtari911 if (!$styleIni || !isset($styleIni['replacements'])) { 4870*9ccd446eSAtari911 // Return defaults 4871*9ccd446eSAtari911 return [ 4872*9ccd446eSAtari911 'bg' => '#fff', 4873*9ccd446eSAtari911 'bg_alt' => '#e8e8e8', 4874*9ccd446eSAtari911 'text' => '#333', 4875*9ccd446eSAtari911 'border' => '#ccc', 4876*9ccd446eSAtari911 'link' => '#2b73b7', 4877*9ccd446eSAtari911 ]; 4878*9ccd446eSAtari911 } 4879*9ccd446eSAtari911 4880*9ccd446eSAtari911 $r = $styleIni['replacements']; 4881*9ccd446eSAtari911 4882*9ccd446eSAtari911 return [ 4883*9ccd446eSAtari911 'bg' => isset($r['__background__']) ? $r['__background__'] : '#fff', 4884*9ccd446eSAtari911 'bg_alt' => isset($r['__background_alt__']) ? $r['__background_alt__'] : '#e8e8e8', 4885*9ccd446eSAtari911 'text' => isset($r['__text__']) ? $r['__text__'] : '#333', 4886*9ccd446eSAtari911 'border' => isset($r['__border__']) ? $r['__border__'] : '#ccc', 4887*9ccd446eSAtari911 'link' => isset($r['__link__']) ? $r['__link__'] : '#2b73b7', 4888*9ccd446eSAtari911 ]; 4889*9ccd446eSAtari911 } 48901d05cddcSAtari911} 4891