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(); 479ccd446eSAtari911 } elseif ($action === 'rename_namespace') { 489ccd446eSAtari911 $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(); 619ccd446eSAtari911 } elseif ($action === 'create_manual_backup') { 629ccd446eSAtari911 $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(); 83*4590242dSAtari911 } elseif ($action === 'save_important_namespaces') { 84*4590242dSAtari911 $this->saveImportantNamespaces(); 851d05cddcSAtari911 } 861d05cddcSAtari911 } 871d05cddcSAtari911 881d05cddcSAtari911 public function html() { 891d05cddcSAtari911 global $INPUT; 901d05cddcSAtari911 919ccd446eSAtari911 // Get current tab - default to 'manage' (Manage Events tab) 929ccd446eSAtari911 $tab = $INPUT->str('tab', 'manage'); 931d05cddcSAtari911 949ccd446eSAtari911 // Get template colors 959ccd446eSAtari911 $colors = $this->getTemplateColors(); 969ccd446eSAtari911 $accentColor = '#00cc07'; // Keep calendar plugin accent color 979ccd446eSAtari911 989ccd446eSAtari911 // Tab navigation (Manage Events, Update Plugin, Outlook Sync, Themes) 999ccd446eSAtari911 echo '<div style="border-bottom:2px solid ' . $colors['border'] . '; margin:10px 0 15px 0;">'; 1009ccd446eSAtari911 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>'; 1019ccd446eSAtari911 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>'; 1029ccd446eSAtari911 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>'; 1039ccd446eSAtari911 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>'; 1041d05cddcSAtari911 echo '</div>'; 1051d05cddcSAtari911 1061d05cddcSAtari911 // Render appropriate tab 1071d05cddcSAtari911 if ($tab === 'config') { 1089ccd446eSAtari911 $this->renderConfigTab($colors); 1091d05cddcSAtari911 } elseif ($tab === 'manage') { 1109ccd446eSAtari911 $this->renderManageTab($colors); 1119ccd446eSAtari911 } elseif ($tab === 'themes') { 1129ccd446eSAtari911 $this->renderThemesTab($colors); 1131d05cddcSAtari911 } else { 1149ccd446eSAtari911 $this->renderUpdateTab($colors); 1151d05cddcSAtari911 } 1161d05cddcSAtari911 } 1171d05cddcSAtari911 1189ccd446eSAtari911 private function renderConfigTab($colors = null) { 1191d05cddcSAtari911 global $INPUT; 1201d05cddcSAtari911 1219ccd446eSAtari911 // Use defaults if not provided 1229ccd446eSAtari911 if ($colors === null) { 1239ccd446eSAtari911 $colors = $this->getTemplateColors(); 1249ccd446eSAtari911 } 1259ccd446eSAtari911 1261d05cddcSAtari911 // Load current config 1271d05cddcSAtari911 $configFile = DOKU_PLUGIN . 'calendar/sync_config.php'; 1281d05cddcSAtari911 $config = []; 1291d05cddcSAtari911 if (file_exists($configFile)) { 1301d05cddcSAtari911 $config = include $configFile; 1311d05cddcSAtari911 } 1321d05cddcSAtari911 1331d05cddcSAtari911 // Show message if present 1341d05cddcSAtari911 if ($INPUT->has('msg')) { 1351d05cddcSAtari911 $msg = hsc($INPUT->str('msg')); 1361d05cddcSAtari911 $type = $INPUT->str('msgtype', 'success'); 1371d05cddcSAtari911 $class = ($type === 'success') ? 'msg success' : 'msg error'; 1381d05cddcSAtari911 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;\">"; 1391d05cddcSAtari911 echo $msg; 1401d05cddcSAtari911 echo "</div>"; 1411d05cddcSAtari911 } 1421d05cddcSAtari911 1431d05cddcSAtari911 echo '<h2 style="margin:10px 0; font-size:20px;">Outlook Sync Configuration</h2>'; 1441d05cddcSAtari911 1451d05cddcSAtari911 // Import/Export buttons 1461d05cddcSAtari911 echo '<div style="display:flex; gap:10px; margin-bottom:15px;">'; 1471d05cddcSAtari911 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>'; 1481d05cddcSAtari911 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>'; 1491d05cddcSAtari911 echo '<input type="file" id="importFileInput" accept=".enc" style="display:none;" onchange="importConfig(this)">'; 1501d05cddcSAtari911 echo '<span id="importStatus" style="margin-left:10px; font-size:12px;"></span>'; 1511d05cddcSAtari911 echo '</div>'; 1521d05cddcSAtari911 1531d05cddcSAtari911 echo '<form method="post" action="?do=admin&page=calendar" style="max-width:900px;">'; 1541d05cddcSAtari911 echo '<input type="hidden" name="action" value="save_config">'; 1551d05cddcSAtari911 1561d05cddcSAtari911 // Azure Credentials 1579ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">'; 1581d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Microsoft Azure App Credentials</h3>'; 1599ccd446eSAtari911 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>'; 1601d05cddcSAtari911 1611d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Tenant ID</label>'; 1629ccd446eSAtari911 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;">'; 1631d05cddcSAtari911 1641d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Client ID (Application ID)</label>'; 1659ccd446eSAtari911 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;">'; 1661d05cddcSAtari911 1671d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Client Secret</label>'; 1689ccd446eSAtari911 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;">'; 1691d05cddcSAtari911 echo '<p style="color:#999; font-size:0.8em; margin:3px 0 0;">⚠️ Keep this secret safe!</p>'; 1701d05cddcSAtari911 echo '</div>'; 1711d05cddcSAtari911 1721d05cddcSAtari911 // Outlook Settings 1739ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">'; 1741d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Outlook Settings</h3>'; 1751d05cddcSAtari911 1761d05cddcSAtari911 echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">'; 1771d05cddcSAtari911 1781d05cddcSAtari911 echo '<div>'; 1791d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">User Email</label>'; 1809ccd446eSAtari911 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;">'; 1811d05cddcSAtari911 echo '</div>'; 1821d05cddcSAtari911 1831d05cddcSAtari911 echo '<div>'; 1841d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Timezone</label>'; 1859ccd446eSAtari911 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;">'; 1861d05cddcSAtari911 echo '</div>'; 1871d05cddcSAtari911 1881d05cddcSAtari911 echo '<div>'; 1891d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Default Category</label>'; 1909ccd446eSAtari911 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;">'; 1911d05cddcSAtari911 echo '</div>'; 1921d05cddcSAtari911 1931d05cddcSAtari911 echo '<div>'; 1941d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Reminder (minutes)</label>'; 1959ccd446eSAtari911 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;">'; 1961d05cddcSAtari911 echo '</div>'; 1971d05cddcSAtari911 1981d05cddcSAtari911 echo '</div>'; // end grid 1991d05cddcSAtari911 echo '</div>'; 2001d05cddcSAtari911 2011d05cddcSAtari911 // Sync Options 2029ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">'; 2031d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Sync Options</h3>'; 2041d05cddcSAtari911 2051d05cddcSAtari911 $syncCompleted = isset($config['sync_completed_tasks']) ? $config['sync_completed_tasks'] : false; 2061d05cddcSAtari911 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>'; 2071d05cddcSAtari911 2081d05cddcSAtari911 $deleteOutlook = isset($config['delete_outlook_events']) ? $config['delete_outlook_events'] : true; 2091d05cddcSAtari911 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>'; 2101d05cddcSAtari911 2111d05cddcSAtari911 $syncAll = isset($config['sync_all_namespaces']) ? $config['sync_all_namespaces'] : true; 2121d05cddcSAtari911 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>'; 2131d05cddcSAtari911 2141d05cddcSAtari911 // Namespace selection (shown when sync_all is unchecked) 2151d05cddcSAtari911 echo '<div id="namespace_selection" style="margin-top:10px; ' . ($syncAll ? 'display:none;' : '') . '">'; 2161d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin-bottom:5px; font-size:13px;">Select namespaces to sync:</label>'; 2171d05cddcSAtari911 2181d05cddcSAtari911 // Get available namespaces 2191d05cddcSAtari911 $availableNamespaces = $this->getAllNamespaces(); 2201d05cddcSAtari911 $selectedNamespaces = isset($config['sync_namespaces']) ? $config['sync_namespaces'] : []; 2211d05cddcSAtari911 2229ccd446eSAtari911 echo '<div style="max-height:150px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px; padding:8px; background:' . $colors['bg'] . ';">'; 2231d05cddcSAtari911 echo '<label style="display:block; margin:3px 0;"><input type="checkbox" name="sync_namespaces[]" value=""> (default)</label>'; 2241d05cddcSAtari911 foreach ($availableNamespaces as $ns) { 2251d05cddcSAtari911 if ($ns !== '') { 2261d05cddcSAtari911 $checked = in_array($ns, $selectedNamespaces) ? 'checked' : ''; 2271d05cddcSAtari911 echo '<label style="display:block; margin:3px 0;"><input type="checkbox" name="sync_namespaces[]" value="' . hsc($ns) . '" ' . $checked . '> ' . hsc($ns) . '</label>'; 2281d05cddcSAtari911 } 2291d05cddcSAtari911 } 2301d05cddcSAtari911 echo '</div>'; 2311d05cddcSAtari911 echo '</div>'; 2321d05cddcSAtari911 2331d05cddcSAtari911 echo '<script> 2341d05cddcSAtari911 function toggleNamespaceSelection(checkbox) { 2351d05cddcSAtari911 document.getElementById("namespace_selection").style.display = checkbox.checked ? "none" : "block"; 2361d05cddcSAtari911 } 2371d05cddcSAtari911 </script>'; 2381d05cddcSAtari911 2391d05cddcSAtari911 echo '</div>'; 2401d05cddcSAtari911 2411d05cddcSAtari911 // Namespace and Color Mapping - Side by Side 2421d05cddcSAtari911 echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px; margin:10px 0;">'; 2431d05cddcSAtari911 2441d05cddcSAtari911 // Namespace Mapping 2459ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; border-left:3px solid #00cc07; border-radius:3px;">'; 2461d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Namespace → Category</h3>'; 2479ccd446eSAtari911 echo '<p style="color:' . $colors['text'] . '; font-size:0.8em; margin:0 0 5px;">One per line: namespace=Category</p>'; 2489ccd446eSAtari911 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">'; 2491d05cddcSAtari911 if (isset($config['category_mapping']) && is_array($config['category_mapping'])) { 2501d05cddcSAtari911 foreach ($config['category_mapping'] as $ns => $cat) { 2511d05cddcSAtari911 echo hsc($ns) . '=' . hsc($cat) . "\n"; 2521d05cddcSAtari911 } 2531d05cddcSAtari911 } 2541d05cddcSAtari911 echo '</textarea>'; 2551d05cddcSAtari911 echo '</div>'; 2561d05cddcSAtari911 2571d05cddcSAtari911 // Color Mapping with Color Picker 2589ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; border-left:3px solid #00cc07; border-radius:3px;">'; 2591d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;"> Event Color → Category</h3>'; 2609ccd446eSAtari911 echo '<p style="color:' . $colors['text'] . '; font-size:0.8em; margin:0 0 8px;">Map calendar colors to Outlook categories</p>'; 2611d05cddcSAtari911 2621d05cddcSAtari911 // Define calendar colors and Outlook categories (only the main 6 colors) 2631d05cddcSAtari911 $calendarColors = [ 2641d05cddcSAtari911 '#3498db' => 'Blue', 2651d05cddcSAtari911 '#2ecc71' => 'Green', 2661d05cddcSAtari911 '#e74c3c' => 'Red', 2671d05cddcSAtari911 '#f39c12' => 'Orange', 2681d05cddcSAtari911 '#9b59b6' => 'Purple', 2691d05cddcSAtari911 '#1abc9c' => 'Teal' 2701d05cddcSAtari911 ]; 2711d05cddcSAtari911 2721d05cddcSAtari911 $outlookCategories = [ 2731d05cddcSAtari911 'Blue category', 2741d05cddcSAtari911 'Green category', 2751d05cddcSAtari911 'Orange category', 2761d05cddcSAtari911 'Red category', 2771d05cddcSAtari911 'Yellow category', 2781d05cddcSAtari911 'Purple category' 2791d05cddcSAtari911 ]; 2801d05cddcSAtari911 2811d05cddcSAtari911 // Load existing color mappings 2821d05cddcSAtari911 $existingMappings = isset($config['color_mapping']) && is_array($config['color_mapping']) 2831d05cddcSAtari911 ? $config['color_mapping'] 2841d05cddcSAtari911 : []; 2851d05cddcSAtari911 2861d05cddcSAtari911 // Display color mapping rows 2871d05cddcSAtari911 echo '<div id="colorMappings" style="max-height:200px; overflow-y:auto;">'; 2881d05cddcSAtari911 2891d05cddcSAtari911 $rowIndex = 0; 2901d05cddcSAtari911 foreach ($calendarColors as $hexColor => $colorName) { 2911d05cddcSAtari911 $selectedCategory = isset($existingMappings[$hexColor]) ? $existingMappings[$hexColor] : ''; 2921d05cddcSAtari911 2931d05cddcSAtari911 echo '<div style="display:flex; gap:8px; align-items:center; margin-bottom:6px;">'; 2941d05cddcSAtari911 2951d05cddcSAtari911 // Color preview box 2961d05cddcSAtari911 echo '<div style="width:24px; height:24px; background:' . $hexColor . '; border:2px solid #ddd; border-radius:3px; flex-shrink:0;"></div>'; 2971d05cddcSAtari911 2981d05cddcSAtari911 // Color name 2999ccd446eSAtari911 echo '<span style="font-size:12px; min-width:90px; color:' . $colors['text'] . ';">' . $colorName . '</span>'; 3001d05cddcSAtari911 3011d05cddcSAtari911 // Arrow 3021d05cddcSAtari911 echo '<span style="color:#999; font-size:12px;">→</span>'; 3031d05cddcSAtari911 3041d05cddcSAtari911 // Outlook category dropdown 3059ccd446eSAtari911 echo '<select name="color_map_' . $rowIndex . '" style="flex:1; padding:4px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;">'; 3061d05cddcSAtari911 echo '<option value="">-- None --</option>'; 3071d05cddcSAtari911 foreach ($outlookCategories as $category) { 3081d05cddcSAtari911 $selected = ($selectedCategory === $category) ? 'selected' : ''; 3091d05cddcSAtari911 echo '<option value="' . hsc($category) . '" ' . $selected . '>' . hsc($category) . '</option>'; 3101d05cddcSAtari911 } 3111d05cddcSAtari911 echo '</select>'; 3121d05cddcSAtari911 3131d05cddcSAtari911 // Hidden input for the hex color 3141d05cddcSAtari911 echo '<input type="hidden" name="color_hex_' . $rowIndex . '" value="' . $hexColor . '">'; 3151d05cddcSAtari911 3161d05cddcSAtari911 echo '</div>'; 3171d05cddcSAtari911 $rowIndex++; 3181d05cddcSAtari911 } 3191d05cddcSAtari911 3201d05cddcSAtari911 echo '</div>'; 3211d05cddcSAtari911 3221d05cddcSAtari911 // Hidden input to track number of color mappings 3231d05cddcSAtari911 echo '<input type="hidden" name="color_mapping_count" value="' . $rowIndex . '">'; 3241d05cddcSAtari911 3251d05cddcSAtari911 echo '</div>'; 3261d05cddcSAtari911 3271d05cddcSAtari911 echo '</div>'; // end grid 3281d05cddcSAtari911 3291d05cddcSAtari911 // Submit button 3301d05cddcSAtari911 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>'; 3311d05cddcSAtari911 echo '</form>'; 3321d05cddcSAtari911 3331d05cddcSAtari911 // JavaScript for Import/Export 3341d05cddcSAtari911 echo '<script> 3351d05cddcSAtari911 async function exportConfig() { 3361d05cddcSAtari911 try { 3371d05cddcSAtari911 const response = await fetch("?do=admin&page=calendar&action=export_config&call=ajax", { 3381d05cddcSAtari911 method: "POST" 3391d05cddcSAtari911 }); 3401d05cddcSAtari911 const data = await response.json(); 3411d05cddcSAtari911 3421d05cddcSAtari911 if (data.success) { 3431d05cddcSAtari911 // Create download link 3441d05cddcSAtari911 const blob = new Blob([data.encrypted], {type: "application/octet-stream"}); 3451d05cddcSAtari911 const url = URL.createObjectURL(blob); 3461d05cddcSAtari911 const a = document.createElement("a"); 3471d05cddcSAtari911 a.href = url; 3481d05cddcSAtari911 a.download = "sync_config_" + new Date().toISOString().split("T")[0] + ".enc"; 3491d05cddcSAtari911 document.body.appendChild(a); 3501d05cddcSAtari911 a.click(); 3511d05cddcSAtari911 document.body.removeChild(a); 3521d05cddcSAtari911 URL.revokeObjectURL(url); 3531d05cddcSAtari911 3541d05cddcSAtari911 alert("✅ Config exported successfully!\\n\\n⚠️ This file contains encrypted credentials.\\nKeep it secure!"); 3551d05cddcSAtari911 } else { 3561d05cddcSAtari911 alert("❌ Export failed: " + data.message); 3571d05cddcSAtari911 } 3581d05cddcSAtari911 } catch (error) { 3591d05cddcSAtari911 alert("❌ Error: " + error.message); 3601d05cddcSAtari911 } 3611d05cddcSAtari911 } 3621d05cddcSAtari911 3631d05cddcSAtari911 async function importConfig(input) { 3641d05cddcSAtari911 const file = input.files[0]; 3651d05cddcSAtari911 if (!file) return; 3661d05cddcSAtari911 3671d05cddcSAtari911 const status = document.getElementById("importStatus"); 3681d05cddcSAtari911 status.textContent = "⏳ Importing..."; 3691d05cddcSAtari911 status.style.color = "#00cc07"; 3701d05cddcSAtari911 3711d05cddcSAtari911 try { 3721d05cddcSAtari911 const encrypted = await file.text(); 3731d05cddcSAtari911 3741d05cddcSAtari911 const formData = new FormData(); 3751d05cddcSAtari911 formData.append("encrypted_config", encrypted); 3761d05cddcSAtari911 3771d05cddcSAtari911 const response = await fetch("?do=admin&page=calendar&action=import_config&call=ajax", { 3781d05cddcSAtari911 method: "POST", 3791d05cddcSAtari911 body: formData 3801d05cddcSAtari911 }); 3811d05cddcSAtari911 const data = await response.json(); 3821d05cddcSAtari911 3831d05cddcSAtari911 if (data.success) { 3841d05cddcSAtari911 status.textContent = "✅ Import successful! Reloading..."; 3851d05cddcSAtari911 status.style.color = "#28a745"; 3861d05cddcSAtari911 setTimeout(() => { 3871d05cddcSAtari911 window.location.reload(); 3881d05cddcSAtari911 }, 1500); 3891d05cddcSAtari911 } else { 3901d05cddcSAtari911 status.textContent = "❌ Import failed: " + data.message; 3911d05cddcSAtari911 status.style.color = "#dc3545"; 3921d05cddcSAtari911 } 3931d05cddcSAtari911 } catch (error) { 3941d05cddcSAtari911 status.textContent = "❌ Error: " + error.message; 3951d05cddcSAtari911 status.style.color = "#dc3545"; 3961d05cddcSAtari911 } 3971d05cddcSAtari911 3981d05cddcSAtari911 // Reset file input 3991d05cddcSAtari911 input.value = ""; 4001d05cddcSAtari911 } 4011d05cddcSAtari911 </script>'; 4021d05cddcSAtari911 4031d05cddcSAtari911 // Sync Controls Section 4049ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:15px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">'; 4051d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;"> Sync Controls</h3>'; 4061d05cddcSAtari911 4071d05cddcSAtari911 // Check cron job status 4081d05cddcSAtari911 $cronStatus = $this->getCronStatus(); 4091d05cddcSAtari911 4101d05cddcSAtari911 // Check log file permissions 4111d05cddcSAtari911 $logFile = DOKU_PLUGIN . 'calendar/sync.log'; 4121d05cddcSAtari911 $logWritable = is_writable($logFile) || is_writable(dirname($logFile)); 4131d05cddcSAtari911 4141d05cddcSAtari911 echo '<div style="display:flex; gap:10px; align-items:center; margin-bottom:10px;">'; 4151d05cddcSAtari911 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>'; 4161d05cddcSAtari911 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>'; 4171d05cddcSAtari911 4181d05cddcSAtari911 if ($cronStatus['active']) { 4199ccd446eSAtari911 echo '<span style="color:' . $colors['text'] . '; font-size:12px;">⏰ ' . hsc($cronStatus['frequency']) . '</span>'; 4201d05cddcSAtari911 } else { 4211d05cddcSAtari911 echo '<span style="color:#999; font-size:12px;">⚠️ No cron job detected</span>'; 4221d05cddcSAtari911 } 4231d05cddcSAtari911 4249ccd446eSAtari911 echo '<span id="syncStatus" style="color:' . $colors['text'] . '; font-size:12px; margin-left:auto;"></span>'; 4251d05cddcSAtari911 echo '</div>'; 4261d05cddcSAtari911 4271d05cddcSAtari911 // Show permission warning if log not writable 4281d05cddcSAtari911 if (!$logWritable) { 4291d05cddcSAtari911 echo '<div style="background:#fff3e0; border-left:3px solid #ff9800; padding:8px; margin:8px 0; border-radius:3px;">'; 4301d05cddcSAtari911 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>'; 4311d05cddcSAtari911 echo '</div>'; 4321d05cddcSAtari911 } 4331d05cddcSAtari911 4341d05cddcSAtari911 // Show debug info if cron detected 4351d05cddcSAtari911 if ($cronStatus['active'] && !empty($cronStatus['full_line'])) { 4361d05cddcSAtari911 echo '<details style="margin-top:5px;">'; 4371d05cddcSAtari911 echo '<summary style="cursor:pointer; color:#999; font-size:11px;">Show cron details</summary>'; 4381d05cddcSAtari911 echo '<pre style="background:#f0f0f0; padding:8px; border-radius:3px; font-size:10px; margin:5px 0; overflow-x:auto;">' . hsc($cronStatus['full_line']) . '</pre>'; 4391d05cddcSAtari911 echo '</details>'; 4401d05cddcSAtari911 } 4411d05cddcSAtari911 4421d05cddcSAtari911 if (!$cronStatus['active']) { 4431d05cddcSAtari911 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>'; 4441d05cddcSAtari911 } 4451d05cddcSAtari911 4461d05cddcSAtari911 echo '</div>'; 4471d05cddcSAtari911 4481d05cddcSAtari911 // JavaScript for Run Sync Now 4491d05cddcSAtari911 echo '<script> 4501d05cddcSAtari911 let syncAbortController = null; 4511d05cddcSAtari911 4521d05cddcSAtari911 function runSyncNow() { 4531d05cddcSAtari911 const btn = document.getElementById("syncBtn"); 4541d05cddcSAtari911 const stopBtn = document.getElementById("stopBtn"); 4551d05cddcSAtari911 const status = document.getElementById("syncStatus"); 4561d05cddcSAtari911 4571d05cddcSAtari911 btn.disabled = true; 4581d05cddcSAtari911 btn.style.display = "none"; 4591d05cddcSAtari911 stopBtn.style.display = "inline-block"; 4601d05cddcSAtari911 btn.textContent = "⏳ Running..."; 4611d05cddcSAtari911 btn.style.background = "#999"; 4621d05cddcSAtari911 status.textContent = "Starting sync..."; 4631d05cddcSAtari911 status.style.color = "#00cc07"; 4641d05cddcSAtari911 4651d05cddcSAtari911 // Create abort controller for this sync 4661d05cddcSAtari911 syncAbortController = new AbortController(); 4671d05cddcSAtari911 4681d05cddcSAtari911 fetch("?do=admin&page=calendar&action=run_sync&call=ajax", { 4691d05cddcSAtari911 method: "POST", 4701d05cddcSAtari911 signal: syncAbortController.signal 4711d05cddcSAtari911 }) 4721d05cddcSAtari911 .then(response => response.json()) 4731d05cddcSAtari911 .then(data => { 4741d05cddcSAtari911 if (data.success) { 4751d05cddcSAtari911 status.textContent = "✅ " + data.message; 4761d05cddcSAtari911 status.style.color = "#28a745"; 4771d05cddcSAtari911 } else { 4781d05cddcSAtari911 status.textContent = "❌ " + data.message; 4791d05cddcSAtari911 status.style.color = "#dc3545"; 4801d05cddcSAtari911 } 4811d05cddcSAtari911 btn.disabled = false; 4821d05cddcSAtari911 btn.style.display = "inline-block"; 4831d05cddcSAtari911 stopBtn.style.display = "none"; 4841d05cddcSAtari911 btn.textContent = "▶️ Run Sync Now"; 4851d05cddcSAtari911 btn.style.background = "#00cc07"; 4861d05cddcSAtari911 syncAbortController = null; 4871d05cddcSAtari911 4881d05cddcSAtari911 // Clear status after 10 seconds 4891d05cddcSAtari911 setTimeout(() => { 4901d05cddcSAtari911 status.textContent = ""; 4911d05cddcSAtari911 }, 10000); 4921d05cddcSAtari911 }) 4931d05cddcSAtari911 .catch(error => { 4941d05cddcSAtari911 if (error.name === "AbortError") { 4951d05cddcSAtari911 status.textContent = "⏹️ Sync stopped by user"; 4961d05cddcSAtari911 status.style.color = "#ff9800"; 4971d05cddcSAtari911 } else { 4981d05cddcSAtari911 status.textContent = "❌ Error: " + error.message; 4991d05cddcSAtari911 status.style.color = "#dc3545"; 5001d05cddcSAtari911 } 5011d05cddcSAtari911 btn.disabled = false; 5021d05cddcSAtari911 btn.style.display = "inline-block"; 5031d05cddcSAtari911 stopBtn.style.display = "none"; 5041d05cddcSAtari911 btn.textContent = "▶️ Run Sync Now"; 5051d05cddcSAtari911 btn.style.background = "#00cc07"; 5061d05cddcSAtari911 syncAbortController = null; 5071d05cddcSAtari911 }); 5081d05cddcSAtari911 } 5091d05cddcSAtari911 5101d05cddcSAtari911 function stopSyncNow() { 5111d05cddcSAtari911 const status = document.getElementById("syncStatus"); 5121d05cddcSAtari911 5131d05cddcSAtari911 status.textContent = "⏹️ Sending stop signal..."; 5141d05cddcSAtari911 status.style.color = "#ff9800"; 5151d05cddcSAtari911 5161d05cddcSAtari911 // First, send stop signal to server 5171d05cddcSAtari911 fetch("?do=admin&page=calendar&action=stop_sync&call=ajax", { 5181d05cddcSAtari911 method: "POST" 5191d05cddcSAtari911 }) 5201d05cddcSAtari911 .then(response => response.json()) 5211d05cddcSAtari911 .then(data => { 5221d05cddcSAtari911 if (data.success) { 5231d05cddcSAtari911 status.textContent = "⏹️ Stop signal sent - sync will abort soon"; 5241d05cddcSAtari911 status.style.color = "#ff9800"; 5251d05cddcSAtari911 } else { 5261d05cddcSAtari911 status.textContent = "⚠️ " + data.message; 5271d05cddcSAtari911 status.style.color = "#ff9800"; 5281d05cddcSAtari911 } 5291d05cddcSAtari911 }) 5301d05cddcSAtari911 .catch(error => { 5311d05cddcSAtari911 status.textContent = "⚠️ Error sending stop signal: " + error.message; 5321d05cddcSAtari911 status.style.color = "#ff9800"; 5331d05cddcSAtari911 }); 5341d05cddcSAtari911 5351d05cddcSAtari911 // Also abort the fetch request 5361d05cddcSAtari911 if (syncAbortController) { 5371d05cddcSAtari911 syncAbortController.abort(); 5381d05cddcSAtari911 status.textContent = "⏹️ Stopping sync..."; 5391d05cddcSAtari911 status.style.color = "#ff9800"; 5401d05cddcSAtari911 } 5411d05cddcSAtari911 } 5421d05cddcSAtari911 </script>'; 5431d05cddcSAtari911 5441d05cddcSAtari911 // Log Viewer Section - More Compact 5459ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:15px 0 10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">'; 5461d05cddcSAtari911 echo '<h3 style="margin:0 0 5px 0; color:#00cc07; font-size:16px;"> Live Sync Log</h3>'; 5479ccd446eSAtari911 echo '<p style="color:' . $colors['text'] . '; font-size:0.8em; margin:0 0 8px;">Updates every 2 seconds</p>'; 5481d05cddcSAtari911 5491d05cddcSAtari911 // Log viewer container 5501d05cddcSAtari911 echo '<div style="background:#1e1e1e; border-radius:5px; overflow:hidden; box-shadow:0 2px 4px rgba(0,0,0,0.3);">'; 5511d05cddcSAtari911 5521d05cddcSAtari911 // Log header - More compact 5531d05cddcSAtari911 echo '<div style="background:#2d2d2d; padding:6px 10px; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #444;">'; 5541d05cddcSAtari911 echo '<span style="color:#00cc07; font-family:monospace; font-weight:bold; font-size:12px;">sync.log</span>'; 5551d05cddcSAtari911 echo '<div>'; 5561d05cddcSAtari911 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>'; 5571d05cddcSAtari911 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>'; 5581d05cddcSAtari911 echo '<button onclick="downloadLog()" style="background:#666; color:white; border:none; padding:4px 8px; border-radius:3px; cursor:pointer; font-size:11px;"> Download</button>'; 5591d05cddcSAtari911 echo '</div>'; 5601d05cddcSAtari911 echo '</div>'; 5611d05cddcSAtari911 5621d05cddcSAtari911 // Log content - Reduced height to 250px 5631d05cddcSAtari911 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>'; 5641d05cddcSAtari911 5651d05cddcSAtari911 echo '</div>'; 5661d05cddcSAtari911 echo '</div>'; 5671d05cddcSAtari911 5681d05cddcSAtari911 // JavaScript for log viewer 5691d05cddcSAtari911 echo '<script> 5701d05cddcSAtari911 let refreshInterval = null; 5711d05cddcSAtari911 let isPaused = false; 5721d05cddcSAtari911 5731d05cddcSAtari911 function refreshLog() { 5741d05cddcSAtari911 if (isPaused) return; 5751d05cddcSAtari911 5761d05cddcSAtari911 fetch("?do=admin&page=calendar&action=get_log&call=ajax") 5771d05cddcSAtari911 .then(response => response.json()) 5781d05cddcSAtari911 .then(data => { 5791d05cddcSAtari911 const logContent = document.getElementById("logContent"); 5801d05cddcSAtari911 if (logContent) { 5811d05cddcSAtari911 logContent.textContent = data.log || "No log data available"; 5821d05cddcSAtari911 logContent.scrollTop = logContent.scrollHeight; 5831d05cddcSAtari911 } 5841d05cddcSAtari911 }) 5851d05cddcSAtari911 .catch(error => { 5861d05cddcSAtari911 console.error("Error fetching log:", error); 5871d05cddcSAtari911 }); 5881d05cddcSAtari911 } 5891d05cddcSAtari911 5901d05cddcSAtari911 function togglePause() { 5911d05cddcSAtari911 isPaused = !isPaused; 5921d05cddcSAtari911 const btn = document.getElementById("pauseBtn"); 5931d05cddcSAtari911 if (isPaused) { 5941d05cddcSAtari911 btn.textContent = "▶ Resume"; 5951d05cddcSAtari911 btn.style.background = "#00cc07"; 5961d05cddcSAtari911 } else { 5971d05cddcSAtari911 btn.textContent = "⏸ Pause"; 5981d05cddcSAtari911 btn.style.background = "#666"; 5991d05cddcSAtari911 refreshLog(); 6001d05cddcSAtari911 } 6011d05cddcSAtari911 } 6021d05cddcSAtari911 6031d05cddcSAtari911 function clearLog() { 6041d05cddcSAtari911 if (!confirm("Clear the sync log file?\\n\\nThis will delete all log entries.")) { 6051d05cddcSAtari911 return; 6061d05cddcSAtari911 } 6071d05cddcSAtari911 6081d05cddcSAtari911 fetch("?do=admin&page=calendar&action=clear_log&call=ajax", { 6091d05cddcSAtari911 method: "POST" 6101d05cddcSAtari911 }) 6111d05cddcSAtari911 .then(response => response.json()) 6121d05cddcSAtari911 .then(data => { 6131d05cddcSAtari911 if (data.success) { 6141d05cddcSAtari911 refreshLog(); 6151d05cddcSAtari911 alert("Log cleared successfully"); 6161d05cddcSAtari911 } else { 6171d05cddcSAtari911 alert("Error clearing log: " + data.message); 6181d05cddcSAtari911 } 6191d05cddcSAtari911 }) 6201d05cddcSAtari911 .catch(error => { 6211d05cddcSAtari911 alert("Error: " + error.message); 6221d05cddcSAtari911 }); 6231d05cddcSAtari911 } 6241d05cddcSAtari911 6251d05cddcSAtari911 function downloadLog() { 6261d05cddcSAtari911 window.location.href = "?do=admin&page=calendar&action=download_log"; 6271d05cddcSAtari911 } 6281d05cddcSAtari911 6291d05cddcSAtari911 // Start auto-refresh 6301d05cddcSAtari911 refreshLog(); 6311d05cddcSAtari911 refreshInterval = setInterval(refreshLog, 2000); 6321d05cddcSAtari911 6331d05cddcSAtari911 // Cleanup on page unload 6341d05cddcSAtari911 window.addEventListener("beforeunload", function() { 6351d05cddcSAtari911 if (refreshInterval) { 6361d05cddcSAtari911 clearInterval(refreshInterval); 6371d05cddcSAtari911 } 6381d05cddcSAtari911 }); 6391d05cddcSAtari911 </script>'; 6401d05cddcSAtari911 } 6411d05cddcSAtari911 6429ccd446eSAtari911 private function renderManageTab($colors = null) { 6431d05cddcSAtari911 global $INPUT; 6441d05cddcSAtari911 6459ccd446eSAtari911 // Use defaults if not provided 6469ccd446eSAtari911 if ($colors === null) { 6479ccd446eSAtari911 $colors = $this->getTemplateColors(); 6489ccd446eSAtari911 } 6499ccd446eSAtari911 6501d05cddcSAtari911 // Show message if present 6511d05cddcSAtari911 if ($INPUT->has('msg')) { 6521d05cddcSAtari911 $msg = hsc($INPUT->str('msg')); 6531d05cddcSAtari911 $type = $INPUT->str('msgtype', 'success'); 6541d05cddcSAtari911 echo "<div style=\"padding:10px; margin:10px 0; border-left:3px solid " . ($type === 'success' ? '#28a745' : '#dc3545') . "; background:" . ($type === 'success' ? '#d4edda' : '#f8d7da') . "; border-radius:3px;\">"; 6551d05cddcSAtari911 echo $msg; 6561d05cddcSAtari911 echo "</div>"; 6571d05cddcSAtari911 } 6581d05cddcSAtari911 6591d05cddcSAtari911 echo '<h2 style="margin:10px 0; font-size:20px;">Manage Calendar Events</h2>'; 6601d05cddcSAtari911 6619ccd446eSAtari911 // Events Manager Section 6629ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">'; 6631d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;"> Events Manager</h3>'; 6649ccd446eSAtari911 echo '<p style="color:' . $colors['text'] . '; font-size:11px; margin:0 0 10px;">Scan, export, and import all calendar events across all namespaces.</p>'; 6651d05cddcSAtari911 6661d05cddcSAtari911 // Get event statistics 6671d05cddcSAtari911 $stats = $this->getEventStatistics(); 6681d05cddcSAtari911 6691d05cddcSAtari911 // Statistics display 6709ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:10px; border-radius:3px; margin-bottom:10px; border:1px solid ' . $colors['border'] . ';">'; 6711d05cddcSAtari911 echo '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(150px, 1fr)); gap:10px; font-size:12px;">'; 6721d05cddcSAtari911 6731d05cddcSAtari911 echo '<div style="background:#f3e5f5; padding:8px; border-radius:3px; text-align:center;">'; 6741d05cddcSAtari911 echo '<div style="font-size:24px; font-weight:bold; color:#7b1fa2;">' . $stats['total_events'] . '</div>'; 6759ccd446eSAtari911 echo '<div style="color:' . $colors['text'] . '; font-size:10px;">Total Events</div>'; 6761d05cddcSAtari911 echo '</div>'; 6771d05cddcSAtari911 6781d05cddcSAtari911 echo '<div style="background:#f3e5f5; padding:8px; border-radius:3px; text-align:center;">'; 6791d05cddcSAtari911 echo '<div style="font-size:24px; font-weight:bold; color:#7b1fa2;">' . $stats['total_namespaces'] . '</div>'; 6809ccd446eSAtari911 echo '<div style="color:' . $colors['text'] . '; font-size:10px;">Namespaces</div>'; 6811d05cddcSAtari911 echo '</div>'; 6821d05cddcSAtari911 6831d05cddcSAtari911 echo '<div style="background:#e8f5e9; padding:8px; border-radius:3px; text-align:center;">'; 6841d05cddcSAtari911 echo '<div style="font-size:24px; font-weight:bold; color:#388e3c;">' . $stats['total_files'] . '</div>'; 6859ccd446eSAtari911 echo '<div style="color:' . $colors['text'] . '; font-size:10px;">JSON Files</div>'; 6861d05cddcSAtari911 echo '</div>'; 6871d05cddcSAtari911 6881d05cddcSAtari911 echo '<div style="background:#fff3e0; padding:8px; border-radius:3px; text-align:center;">'; 6891d05cddcSAtari911 echo '<div style="font-size:24px; font-weight:bold; color:#f57c00;">' . $stats['total_recurring'] . '</div>'; 6909ccd446eSAtari911 echo '<div style="color:' . $colors['text'] . '; font-size:10px;">Recurring</div>'; 6911d05cddcSAtari911 echo '</div>'; 6921d05cddcSAtari911 6931d05cddcSAtari911 echo '</div>'; 6941d05cddcSAtari911 6951d05cddcSAtari911 // Last scan time 6961d05cddcSAtari911 if (!empty($stats['last_scan'])) { 6979ccd446eSAtari911 echo '<div style="margin-top:8px; color:' . $colors['text'] . '; font-size:10px;">Last scanned: ' . hsc($stats['last_scan']) . '</div>'; 6981d05cddcSAtari911 } 6991d05cddcSAtari911 7001d05cddcSAtari911 echo '</div>'; 7011d05cddcSAtari911 7021d05cddcSAtari911 // Action buttons 7031d05cddcSAtari911 echo '<div style="display:flex; gap:10px; flex-wrap:wrap; margin-top:10px;">'; 7041d05cddcSAtari911 7051d05cddcSAtari911 // Rescan button 7061d05cddcSAtari911 echo '<form method="post" action="?do=admin&page=calendar&tab=manage" style="display:inline;">'; 7071d05cddcSAtari911 echo '<input type="hidden" name="action" value="rescan_events">'; 7081d05cddcSAtari911 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;">'; 7091d05cddcSAtari911 echo '<span></span><span>Re-scan Events</span>'; 7101d05cddcSAtari911 echo '</button>'; 7111d05cddcSAtari911 echo '</form>'; 7121d05cddcSAtari911 7131d05cddcSAtari911 // Export button 7141d05cddcSAtari911 echo '<form method="post" action="?do=admin&page=calendar&tab=manage" style="display:inline;">'; 7151d05cddcSAtari911 echo '<input type="hidden" name="action" value="export_all_events">'; 7161d05cddcSAtari911 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;">'; 7171d05cddcSAtari911 echo '<span></span><span>Export All Events</span>'; 7181d05cddcSAtari911 echo '</button>'; 7191d05cddcSAtari911 echo '</form>'; 7201d05cddcSAtari911 7211d05cddcSAtari911 // Import button (with file upload) 7221d05cddcSAtari911 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?\')">'; 7231d05cddcSAtari911 echo '<input type="hidden" name="action" value="import_all_events">'; 7241d05cddcSAtari911 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;">'; 7251d05cddcSAtari911 echo '<span></span><span>Import Events</span>'; 7261d05cddcSAtari911 echo '<input type="file" name="import_file" accept=".json,.zip" required style="display:none;" onchange="this.form.submit()">'; 7271d05cddcSAtari911 echo '</label>'; 7281d05cddcSAtari911 echo '</form>'; 7291d05cddcSAtari911 7301d05cddcSAtari911 echo '</div>'; 7311d05cddcSAtari911 7321d05cddcSAtari911 // Breakdown by namespace 7331d05cddcSAtari911 if (!empty($stats['by_namespace'])) { 7341d05cddcSAtari911 echo '<details style="margin-top:12px;">'; 7351d05cddcSAtari911 echo '<summary style="cursor:pointer; padding:6px; background:#e9e9e9; border-radius:3px; font-size:11px; font-weight:bold;">View Breakdown by Namespace</summary>'; 7369ccd446eSAtari911 echo '<div style="margin-top:8px; max-height:200px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px;">'; 7371d05cddcSAtari911 echo '<table style="width:100%; border-collapse:collapse; font-size:11px;">'; 7381d05cddcSAtari911 echo '<thead style="position:sticky; top:0; background:#f5f5f5;">'; 7391d05cddcSAtari911 echo '<tr>'; 7401d05cddcSAtari911 echo '<th style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd;">Namespace</th>'; 7411d05cddcSAtari911 echo '<th style="padding:4px 6px; text-align:right; border-bottom:2px solid #ddd;">Events</th>'; 7421d05cddcSAtari911 echo '<th style="padding:4px 6px; text-align:right; border-bottom:2px solid #ddd;">Files</th>'; 7431d05cddcSAtari911 echo '</tr></thead><tbody>'; 7441d05cddcSAtari911 7451d05cddcSAtari911 foreach ($stats['by_namespace'] as $ns => $nsStats) { 7461d05cddcSAtari911 echo '<tr style="border-bottom:1px solid #eee;">'; 7471d05cddcSAtari911 echo '<td style="padding:4px 6px;"><code style="background:#f0f0f0; padding:1px 3px; border-radius:2px; font-size:10px;">' . hsc($ns ?: '(default)') . '</code></td>'; 7481d05cddcSAtari911 echo '<td style="padding:4px 6px; text-align:right;"><strong>' . $nsStats['events'] . '</strong></td>'; 7491d05cddcSAtari911 echo '<td style="padding:4px 6px; text-align:right;">' . $nsStats['files'] . '</td>'; 7501d05cddcSAtari911 echo '</tr>'; 7511d05cddcSAtari911 } 7521d05cddcSAtari911 7531d05cddcSAtari911 echo '</tbody></table>'; 7541d05cddcSAtari911 echo '</div>'; 7551d05cddcSAtari911 echo '</details>'; 7561d05cddcSAtari911 } 7571d05cddcSAtari911 7581d05cddcSAtari911 echo '</div>'; 7591d05cddcSAtari911 760*4590242dSAtari911 // Important Namespaces Section 761*4590242dSAtari911 $configFile = DOKU_PLUGIN . 'calendar/sync_config.php'; 762*4590242dSAtari911 $importantConfig = []; 763*4590242dSAtari911 if (file_exists($configFile)) { 764*4590242dSAtari911 $importantConfig = include $configFile; 765*4590242dSAtari911 } 766*4590242dSAtari911 $importantNsValue = isset($importantConfig['important_namespaces']) ? $importantConfig['important_namespaces'] : 'important'; 767*4590242dSAtari911 768*4590242dSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">'; 769*4590242dSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;"> Important Namespaces (Sidebar Widget)</h3>'; 770*4590242dSAtari911 echo '<p style="color:' . $colors['text'] . '; font-size:11px; margin:0 0 8px;">Events from these namespaces will be highlighted in purple in the sidebar widget\'s "Important Events" section.</p>'; 771*4590242dSAtari911 echo '<form method="post" action="?do=admin&page=calendar&tab=manage" style="display:flex; gap:8px; align-items:center;">'; 772*4590242dSAtari911 echo '<input type="hidden" name="action" value="save_important_namespaces">'; 773*4590242dSAtari911 echo '<input type="text" name="important_namespaces" value="' . hsc($importantNsValue) . '" style="flex:1; padding:6px; border:1px solid ' . $colors['border'] . '; border-radius:3px; font-size:12px;" placeholder="important,urgent,priority">'; 774*4590242dSAtari911 echo '<button type="submit" style="background:#00cc07; color:white; padding:6px 16px; border:none; border-radius:3px; cursor:pointer; font-size:12px; font-weight:bold; white-space:nowrap;">Save</button>'; 775*4590242dSAtari911 echo '</form>'; 776*4590242dSAtari911 echo '<p style="color:' . $colors['text'] . '; font-size:10px; margin:4px 0 0;">Comma-separated list of namespace names</p>'; 777*4590242dSAtari911 echo '</div>'; 778*4590242dSAtari911 7799ccd446eSAtari911 // Cleanup Events Section 7809ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">'; 7819ccd446eSAtari911 echo '<h3 style="margin:0 0 6px 0; color:#00cc07; font-size:16px;"> Cleanup Old Events</h3>'; 7829ccd446eSAtari911 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>'; 7831d05cddcSAtari911 7841d05cddcSAtari911 echo '<form method="post" action="?do=admin&page=calendar&tab=manage" id="cleanupForm">'; 7851d05cddcSAtari911 echo '<input type="hidden" name="action" value="cleanup_events">'; 7861d05cddcSAtari911 7871d05cddcSAtari911 // Compact options layout 7889ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:10px; border:1px solid ' . $colors['border'] . '; border-radius:3px; margin-bottom:10px;">'; 7891d05cddcSAtari911 7901d05cddcSAtari911 // Radio buttons in a row 7911d05cddcSAtari911 echo '<div style="display:flex; gap:20px; margin-bottom:10px; padding-bottom:8px; border-bottom:1px solid #f0f0f0;">'; 7921d05cddcSAtari911 echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">'; 7931d05cddcSAtari911 echo '<input type="radio" name="cleanup_type" value="age" checked onchange="updateCleanupOptions()">'; 7941d05cddcSAtari911 echo '<span>By Age</span>'; 7951d05cddcSAtari911 echo '</label>'; 7961d05cddcSAtari911 echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">'; 7971d05cddcSAtari911 echo '<input type="radio" name="cleanup_type" value="status" onchange="updateCleanupOptions()">'; 7981d05cddcSAtari911 echo '<span>By Status</span>'; 7991d05cddcSAtari911 echo '</label>'; 8001d05cddcSAtari911 echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">'; 8011d05cddcSAtari911 echo '<input type="radio" name="cleanup_type" value="range" onchange="updateCleanupOptions()">'; 8021d05cddcSAtari911 echo '<span>By Date Range</span>'; 8031d05cddcSAtari911 echo '</label>'; 8041d05cddcSAtari911 echo '</div>'; 8051d05cddcSAtari911 8061d05cddcSAtari911 // Age options 8071d05cddcSAtari911 echo '<div id="age-options" style="padding:6px 0;">'; 8089ccd446eSAtari911 echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">Delete events older than:</span>'; 8091d05cddcSAtari911 echo '<select name="age_value" style="width:50px; padding:3px 4px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px; margin-right:4px;">'; 8101d05cddcSAtari911 for ($i = 1; $i <= 24; $i++) { 8111d05cddcSAtari911 $sel = $i === 6 ? ' selected' : ''; 8121d05cddcSAtari911 echo '<option value="' . $i . '"' . $sel . '>' . $i . '</option>'; 8131d05cddcSAtari911 } 8141d05cddcSAtari911 echo '</select>'; 8151d05cddcSAtari911 echo '<select name="age_unit" style="width:80px; padding:3px 4px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px;">'; 8161d05cddcSAtari911 echo '<option value="months" selected>months</option>'; 8171d05cddcSAtari911 echo '<option value="years">years</option>'; 8181d05cddcSAtari911 echo '</select>'; 8191d05cddcSAtari911 echo '</div>'; 8201d05cddcSAtari911 8211d05cddcSAtari911 // Status options 8221d05cddcSAtari911 echo '<div id="status-options" style="padding:6px 0; opacity:0.4;">'; 8239ccd446eSAtari911 echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">Delete:</span>'; 8241d05cddcSAtari911 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>'; 8251d05cddcSAtari911 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>'; 8261d05cddcSAtari911 echo '</div>'; 8271d05cddcSAtari911 8281d05cddcSAtari911 // Range options 8291d05cddcSAtari911 echo '<div id="range-options" style="padding:6px 0; opacity:0.4;">'; 8309ccd446eSAtari911 echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">From:</span>'; 8311d05cddcSAtari911 echo '<input type="date" name="range_start" style="padding:3px 6px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px; margin-right:10px;">'; 8329ccd446eSAtari911 echo '<span style="font-size:11px; color:' . $colors['text'] . '; margin-right:8px;">To:</span>'; 8331d05cddcSAtari911 echo '<input type="date" name="range_end" style="padding:3px 6px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px;">'; 8341d05cddcSAtari911 echo '</div>'; 8351d05cddcSAtari911 8361d05cddcSAtari911 echo '</div>'; 8371d05cddcSAtari911 8381d05cddcSAtari911 // Namespace filter - compact 8399ccd446eSAtari911 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;">'; 8401d05cddcSAtari911 echo '<label style="font-size:11px; font-weight:600; white-space:nowrap; color:#555;">Namespace:</label>'; 8411d05cddcSAtari911 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;">'; 8421d05cddcSAtari911 echo '</div>'; 8431d05cddcSAtari911 8441d05cddcSAtari911 // Action buttons - compact row 8451d05cddcSAtari911 echo '<div style="display:flex; gap:8px; align-items:center;">'; 8461d05cddcSAtari911 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>'; 8471d05cddcSAtari911 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>'; 8481d05cddcSAtari911 echo '<span style="font-size:10px; color:#999;">⚠️ Backup created automatically</span>'; 8491d05cddcSAtari911 echo '</div>'; 8501d05cddcSAtari911 8511d05cddcSAtari911 echo '</form>'; 8521d05cddcSAtari911 8531d05cddcSAtari911 // Preview results area 8541d05cddcSAtari911 echo '<div id="cleanup-preview" style="margin-top:10px; display:none;"></div>'; 8551d05cddcSAtari911 8561d05cddcSAtari911 echo '<script> 8571d05cddcSAtari911 function updateCleanupOptions() { 8581d05cddcSAtari911 const type = document.querySelector(\'input[name="cleanup_type"]:checked\').value; 8591d05cddcSAtari911 8601d05cddcSAtari911 // Show selected, gray out others 8611d05cddcSAtari911 document.getElementById(\'age-options\').style.opacity = type === \'age\' ? \'1\' : \'0.4\'; 8621d05cddcSAtari911 document.getElementById(\'status-options\').style.opacity = type === \'status\' ? \'1\' : \'0.4\'; 8631d05cddcSAtari911 document.getElementById(\'range-options\').style.opacity = type === \'range\' ? \'1\' : \'0.4\'; 8641d05cddcSAtari911 8651d05cddcSAtari911 // Enable/disable inputs 8661d05cddcSAtari911 document.querySelectorAll(\'#age-options select\').forEach(el => el.disabled = type !== \'age\'); 8671d05cddcSAtari911 document.querySelectorAll(\'#status-options input\').forEach(el => el.disabled = type !== \'status\'); 8681d05cddcSAtari911 document.querySelectorAll(\'#range-options input\').forEach(el => el.disabled = type !== \'range\'); 8691d05cddcSAtari911 } 8701d05cddcSAtari911 8711d05cddcSAtari911 function previewCleanup() { 8721d05cddcSAtari911 const form = document.getElementById(\'cleanupForm\'); 8731d05cddcSAtari911 const formData = new FormData(form); 8741d05cddcSAtari911 formData.set(\'action\', \'preview_cleanup\'); 8751d05cddcSAtari911 8761d05cddcSAtari911 const preview = document.getElementById(\'cleanup-preview\'); 8779ccd446eSAtari911 preview.innerHTML = \'<div style="text-align:center; padding:20px; color:' . $colors['text'] . ';">Loading preview...</div>\'; 8781d05cddcSAtari911 preview.style.display = \'block\'; 8791d05cddcSAtari911 8801d05cddcSAtari911 fetch(\'?do=admin&page=calendar&tab=manage\', { 8811d05cddcSAtari911 method: \'POST\', 8821d05cddcSAtari911 body: new URLSearchParams(formData) 8831d05cddcSAtari911 }) 8841d05cddcSAtari911 .then(r => r.json()) 8851d05cddcSAtari911 .then(data => { 8861d05cddcSAtari911 if (data.count === 0) { 8871d05cddcSAtari911 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>\'; 8881d05cddcSAtari911 8891d05cddcSAtari911 // Show debug info if available 8901d05cddcSAtari911 if (data.debug) { 8919ccd446eSAtari911 html += \'<details style="margin-top:8px; font-size:11px; color:' . $colors['text'] . ';">\'; 8921d05cddcSAtari911 html += \'<summary style="cursor:pointer;">Debug Info</summary>\'; 8931d05cddcSAtari911 html += \'<pre style="background:#f5f5f5; padding:6px; margin-top:4px; border-radius:3px; overflow-x:auto;">\' + JSON.stringify(data.debug, null, 2) + \'</pre>\'; 8941d05cddcSAtari911 html += \'</details>\'; 8951d05cddcSAtari911 } 8961d05cddcSAtari911 8971d05cddcSAtari911 preview.innerHTML = html; 8981d05cddcSAtari911 } else { 8991d05cddcSAtari911 let html = \'<div style="background:#f8d7da; border:1px solid #f5c6cb; padding:10px; border-radius:3px; font-size:12px; color:#721c24;">\'; 9001d05cddcSAtari911 html += \'<strong>⚠️ Warning:</strong> The following \' + data.count + \' event(s) would be deleted:<br><br>\'; 9019ccd446eSAtari911 html += \'<div style="max-height:150px; overflow-y:auto; margin-top:6px; background:' . $colors['bg'] . '; padding:6px; border-radius:3px;">\'; 9021d05cddcSAtari911 data.events.forEach(evt => { 9031d05cddcSAtari911 html += \'<div style="padding:3px; border-bottom:1px solid #eee; font-size:11px;">\'; 9041d05cddcSAtari911 html += \'• \' + evt.title + \' (\' + evt.date + \')\'; 9051d05cddcSAtari911 if (evt.namespace) html += \' <span style="background:#e3f2fd; padding:1px 4px; border-radius:2px; font-size:9px;">\' + evt.namespace + \'</span>\'; 9061d05cddcSAtari911 html += \'</div>\'; 9071d05cddcSAtari911 }); 9081d05cddcSAtari911 html += \'</div></div>\'; 9091d05cddcSAtari911 preview.innerHTML = html; 9101d05cddcSAtari911 } 9111d05cddcSAtari911 }) 9121d05cddcSAtari911 .catch(err => { 9131d05cddcSAtari911 preview.innerHTML = \'<div style="background:#f8d7da; border:1px solid #f5c6cb; padding:10px; border-radius:3px; font-size:12px; color:#721c24;">Error loading preview</div>\'; 9141d05cddcSAtari911 }); 9151d05cddcSAtari911 } 9161d05cddcSAtari911 9171d05cddcSAtari911 function confirmCleanup() { 9181d05cddcSAtari911 return confirm(\'Are you sure you want to delete these events? A backup will be created first, but this action cannot be easily undone.\'); 9191d05cddcSAtari911 } 9201d05cddcSAtari911 9211d05cddcSAtari911 updateCleanupOptions(); 9221d05cddcSAtari911 </script>'; 9231d05cddcSAtari911 9241d05cddcSAtari911 echo '</div>'; 9251d05cddcSAtari911 9261d05cddcSAtari911 // Recurring Events Section 9279ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">'; 9281d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;"> Recurring Events</h3>'; 9291d05cddcSAtari911 9301d05cddcSAtari911 $recurringEvents = $this->findRecurringEvents(); 9311d05cddcSAtari911 9321d05cddcSAtari911 if (empty($recurringEvents)) { 9339ccd446eSAtari911 echo '<p style="color:' . $colors['text'] . '; font-size:13px; margin:5px 0;">No recurring events found.</p>'; 9341d05cddcSAtari911 } else { 9351d05cddcSAtari911 // Search bar 9361d05cddcSAtari911 echo '<div style="margin-bottom:8px;">'; 9379ccd446eSAtari911 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;">'; 9381d05cddcSAtari911 echo '</div>'; 9391d05cddcSAtari911 9401d05cddcSAtari911 echo '<style> 9411d05cddcSAtari911 .sort-arrow { 9421d05cddcSAtari911 color: #999; 9431d05cddcSAtari911 font-size: 10px; 9441d05cddcSAtari911 margin-left: 3px; 9451d05cddcSAtari911 display: inline-block; 9461d05cddcSAtari911 } 9471d05cddcSAtari911 #recurringTable th:hover { 9481d05cddcSAtari911 background: #ddd; 9491d05cddcSAtari911 } 9501d05cddcSAtari911 #recurringTable th:hover .sort-arrow { 9511d05cddcSAtari911 color: #00cc07; 9521d05cddcSAtari911 } 9531d05cddcSAtari911 .recurring-row-hidden { 9541d05cddcSAtari911 display: none; 9551d05cddcSAtari911 } 9561d05cddcSAtari911 </style>'; 9579ccd446eSAtari911 echo '<div style="max-height:250px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px;">'; 9581d05cddcSAtari911 echo '<table id="recurringTable" style="width:100%; border-collapse:collapse; font-size:11px;">'; 9591d05cddcSAtari911 echo '<thead style="position:sticky; top:0; background:#e9e9e9;">'; 9601d05cddcSAtari911 echo '<tr>'; 9611d05cddcSAtari911 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>'; 9621d05cddcSAtari911 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>'; 9631d05cddcSAtari911 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>'; 9641d05cddcSAtari911 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>'; 9651d05cddcSAtari911 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>'; 9661d05cddcSAtari911 echo '<th style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd;">Actions</th>'; 9671d05cddcSAtari911 echo '</tr></thead><tbody id="recurringTableBody">'; 9681d05cddcSAtari911 9691d05cddcSAtari911 foreach ($recurringEvents as $series) { 9701d05cddcSAtari911 echo '<tr style="border-bottom:1px solid #eee;">'; 9711d05cddcSAtari911 echo '<td style="padding:4px 6px;">' . hsc($series['title']) . '</td>'; 9721d05cddcSAtari911 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>'; 9731d05cddcSAtari911 echo '<td style="padding:4px 6px;">' . hsc($series['pattern']) . '</td>'; 9741d05cddcSAtari911 echo '<td style="padding:4px 6px;">' . hsc($series['firstDate']) . '</td>'; 9751d05cddcSAtari911 echo '<td style="padding:4px 6px;"><strong>' . $series['count'] . '</strong></td>'; 9761d05cddcSAtari911 echo '<td style="padding:4px 6px; white-space:nowrap;">'; 9771d05cddcSAtari911 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>'; 9781d05cddcSAtari911 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>'; 9791d05cddcSAtari911 echo '</td>'; 9801d05cddcSAtari911 echo '</tr>'; 9811d05cddcSAtari911 } 9821d05cddcSAtari911 9831d05cddcSAtari911 echo '</tbody></table>'; 9841d05cddcSAtari911 echo '</div>'; 9859ccd446eSAtari911 echo '<p style="color:' . $colors['text'] . '; font-size:10px; margin:5px 0 0;">Total: ' . count($recurringEvents) . ' series</p>'; 9861d05cddcSAtari911 } 9871d05cddcSAtari911 echo '</div>'; 9881d05cddcSAtari911 9891d05cddcSAtari911 // Compact Tree-based Namespace Manager 9909ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">'; 9911d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;"> Namespace Explorer</h3>'; 9929ccd446eSAtari911 echo '<p style="color:' . $colors['text'] . '; font-size:11px; margin:0 0 8px;">Select events and move between namespaces. Drag & drop also supported.</p>'; 9931d05cddcSAtari911 9941d05cddcSAtari911 // Search bar 9951d05cddcSAtari911 echo '<div style="margin-bottom:8px;">'; 9969ccd446eSAtari911 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;">'; 9971d05cddcSAtari911 echo '</div>'; 9981d05cddcSAtari911 9991d05cddcSAtari911 $eventsByNamespace = $this->getEventsByNamespace(); 10001d05cddcSAtari911 10011d05cddcSAtari911 // Control bar 10021d05cddcSAtari911 echo '<form method="post" action="?do=admin&page=calendar&tab=manage" id="moveForm">'; 10031d05cddcSAtari911 echo '<input type="hidden" name="action" value="move_selected_events" id="formAction">'; 10041d05cddcSAtari911 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;">'; 10051d05cddcSAtari911 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>'; 10061d05cddcSAtari911 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>'; 10071d05cddcSAtari911 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>'; 10081d05cddcSAtari911 echo '<span style="margin-left:10px;">Move to:</span>'; 10099ccd446eSAtari911 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...">'; 10101d05cddcSAtari911 echo '<datalist id="namespaceList">'; 10111d05cddcSAtari911 echo '<option value="">(default)</option>'; 10121d05cddcSAtari911 foreach (array_keys($eventsByNamespace) as $ns) { 10131d05cddcSAtari911 if ($ns !== '') { 10141d05cddcSAtari911 echo '<option value="' . hsc($ns) . '">' . hsc($ns) . '</option>'; 10151d05cddcSAtari911 } 10161d05cddcSAtari911 } 10171d05cddcSAtari911 echo '</datalist>'; 10181d05cddcSAtari911 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>'; 10191d05cddcSAtari911 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>'; 10201d05cddcSAtari911 echo '<span id="selectedCount" style="margin-left:auto; color:#00cc07; font-size:11px;">0 selected</span>'; 10211d05cddcSAtari911 echo '</div>'; 10221d05cddcSAtari911 10231d05cddcSAtari911 echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">'; 10241d05cddcSAtari911 10251d05cddcSAtari911 // Event list with checkboxes 10261d05cddcSAtari911 echo '<div>'; 10279ccd446eSAtari911 echo '<div style="max-height:450px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px; background:' . $colors['bg'] . ';">'; 10281d05cddcSAtari911 10291d05cddcSAtari911 foreach ($eventsByNamespace as $namespace => $data) { 10301d05cddcSAtari911 $nsId = 'ns_' . md5($namespace); 10311d05cddcSAtari911 $eventCount = count($data['events']); 10321d05cddcSAtari911 10331d05cddcSAtari911 echo '<div style="border-bottom:1px solid #ddd;">'; 10341d05cddcSAtari911 10351d05cddcSAtari911 // Namespace header - ultra compact 10361d05cddcSAtari911 echo '<div style="background:#f5f5f5; padding:3px 6px; display:flex; justify-content:space-between; align-items:center; font-size:11px;">'; 10371d05cddcSAtari911 echo '<div style="display:flex; align-items:center; gap:4px;">'; 10381d05cddcSAtari911 echo '<span onclick="toggleNamespace(\'' . $nsId . '\')" style="cursor:pointer; width:12px; display:inline-block; font-size:10px;"><span id="' . $nsId . '_arrow">▶</span></span>'; 10391d05cddcSAtari911 echo '<input type="checkbox" onclick="toggleNamespaceSelect(\'' . $nsId . '\')" id="' . $nsId . '_check" style="margin:0; width:12px; height:12px;">'; 10401d05cddcSAtari911 echo '<span onclick="toggleNamespace(\'' . $nsId . '\')" style="cursor:pointer; font-weight:600; font-size:11px;"> ' . hsc($namespace ?: '(default)') . '</span>'; 10411d05cddcSAtari911 echo '</div>'; 10421d05cddcSAtari911 echo '<div style="display:flex; gap:3px; align-items:center;">'; 10431d05cddcSAtari911 echo '<span style="background:#00cc07; color:white; padding:0px 4px; border-radius:6px; font-size:9px; line-height:14px;">' . $eventCount . '</span>'; 10449ccd446eSAtari911 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>'; 10451d05cddcSAtari911 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>'; 10461d05cddcSAtari911 echo '</div>'; 10471d05cddcSAtari911 echo '</div>'; 10481d05cddcSAtari911 10491d05cddcSAtari911 // Events - ultra compact 10501d05cddcSAtari911 echo '<div id="' . $nsId . '" style="display:none; max-height:150px; overflow-y:auto;">'; 10511d05cddcSAtari911 foreach ($data['events'] as $event) { 10521d05cddcSAtari911 $eventId = $event['id'] . '|' . $namespace . '|' . $event['date'] . '|' . $event['month']; 10531d05cddcSAtari911 $checkId = 'evt_' . md5($eventId); 10541d05cddcSAtari911 10551d05cddcSAtari911 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\'">'; 10561d05cddcSAtari911 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;">'; 10571d05cddcSAtari911 echo '<div style="flex:1; min-width:0;">'; 10581d05cddcSAtari911 echo '<div style="font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-size:10px;">' . hsc($event['title']) . '</div>'; 10591d05cddcSAtari911 echo '<div style="color:#999; font-size:9px;">' . hsc($event['date']) . ($event['startTime'] ? ' • ' . hsc($event['startTime']) : '') . '</div>'; 10601d05cddcSAtari911 echo '</div>'; 10611d05cddcSAtari911 echo '</div>'; 10621d05cddcSAtari911 } 10631d05cddcSAtari911 echo '</div>'; 10641d05cddcSAtari911 echo '</div>'; 10651d05cddcSAtari911 } 10661d05cddcSAtari911 10671d05cddcSAtari911 echo '</div>'; 10681d05cddcSAtari911 echo '</div>'; 10691d05cddcSAtari911 10701d05cddcSAtari911 // Drop zones - ultra compact 10711d05cddcSAtari911 echo '<div>'; 10721d05cddcSAtari911 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>'; 10739ccd446eSAtari911 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'] . ';">'; 10741d05cddcSAtari911 10751d05cddcSAtari911 foreach (array_keys($eventsByNamespace) as $namespace) { 10769ccd446eSAtari911 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\'">'; 10771d05cddcSAtari911 echo '<div style="font-size:11px; font-weight:600; color:#00cc07;"> ' . hsc($namespace ?: '(default)') . '</div>'; 10781d05cddcSAtari911 echo '<div style="color:#999; font-size:9px; margin-top:1px;">Drop here</div>'; 10791d05cddcSAtari911 echo '</div>'; 10801d05cddcSAtari911 } 10811d05cddcSAtari911 10821d05cddcSAtari911 echo '</div>'; 10831d05cddcSAtari911 echo '</div>'; 10841d05cddcSAtari911 10851d05cddcSAtari911 echo '</div>'; // end grid 10861d05cddcSAtari911 echo '</form>'; 10871d05cddcSAtari911 10881d05cddcSAtari911 echo '</div>'; 10891d05cddcSAtari911 10901d05cddcSAtari911 // JavaScript 10911d05cddcSAtari911 echo '<script> 10921d05cddcSAtari911 // Table sorting functionality - defined early so onclick handlers work 10931d05cddcSAtari911 let sortDirection = {}; // Track sort direction for each column 10941d05cddcSAtari911 10951d05cddcSAtari911 function sortRecurringTable(columnIndex) { 10961d05cddcSAtari911 const table = document.getElementById("recurringTable"); 10971d05cddcSAtari911 const tbody = document.getElementById("recurringTableBody"); 10981d05cddcSAtari911 10999ccd446eSAtari911 if (!table || !tbody) return; 11001d05cddcSAtari911 11011d05cddcSAtari911 const rows = Array.from(tbody.querySelectorAll("tr")); 11029ccd446eSAtari911 if (rows.length === 0) return; 11031d05cddcSAtari911 11041d05cddcSAtari911 // Toggle sort direction for this column 11051d05cddcSAtari911 if (!sortDirection[columnIndex]) { 11061d05cddcSAtari911 sortDirection[columnIndex] = "asc"; 11071d05cddcSAtari911 } else { 11081d05cddcSAtari911 sortDirection[columnIndex] = sortDirection[columnIndex] === "asc" ? "desc" : "asc"; 11091d05cddcSAtari911 } 11101d05cddcSAtari911 11111d05cddcSAtari911 const direction = sortDirection[columnIndex]; 11121d05cddcSAtari911 const isNumeric = columnIndex === 4; // Count column 11131d05cddcSAtari911 11141d05cddcSAtari911 // Sort rows 11151d05cddcSAtari911 rows.sort((a, b) => { 11161d05cddcSAtari911 let aValue = a.cells[columnIndex].textContent.trim(); 11171d05cddcSAtari911 let bValue = b.cells[columnIndex].textContent.trim(); 11181d05cddcSAtari911 11191d05cddcSAtari911 // Extract text from code elements for namespace column 11201d05cddcSAtari911 if (columnIndex === 1) { 11211d05cddcSAtari911 const aCode = a.cells[columnIndex].querySelector("code"); 11221d05cddcSAtari911 const bCode = b.cells[columnIndex].querySelector("code"); 11231d05cddcSAtari911 aValue = aCode ? aCode.textContent.trim() : aValue; 11241d05cddcSAtari911 bValue = bCode ? bCode.textContent.trim() : bValue; 11251d05cddcSAtari911 } 11261d05cddcSAtari911 11271d05cddcSAtari911 // Extract number from strong elements for count column 11281d05cddcSAtari911 if (isNumeric) { 11291d05cddcSAtari911 const aStrong = a.cells[columnIndex].querySelector("strong"); 11301d05cddcSAtari911 const bStrong = b.cells[columnIndex].querySelector("strong"); 11311d05cddcSAtari911 aValue = aStrong ? parseInt(aStrong.textContent.trim()) : 0; 11321d05cddcSAtari911 bValue = bStrong ? parseInt(bStrong.textContent.trim()) : 0; 11331d05cddcSAtari911 11341d05cddcSAtari911 return direction === "asc" ? aValue - bValue : bValue - aValue; 11351d05cddcSAtari911 } 11361d05cddcSAtari911 11371d05cddcSAtari911 // String comparison 11381d05cddcSAtari911 if (direction === "asc") { 11391d05cddcSAtari911 return aValue.localeCompare(bValue); 11401d05cddcSAtari911 } else { 11411d05cddcSAtari911 return bValue.localeCompare(aValue); 11421d05cddcSAtari911 } 11431d05cddcSAtari911 }); 11441d05cddcSAtari911 11451d05cddcSAtari911 // Update arrows 11461d05cddcSAtari911 const headers = table.querySelectorAll("th"); 11471d05cddcSAtari911 headers.forEach((header, index) => { 11481d05cddcSAtari911 const arrow = header.querySelector(".sort-arrow"); 11491d05cddcSAtari911 if (arrow) { 11501d05cddcSAtari911 if (index === columnIndex) { 11511d05cddcSAtari911 arrow.textContent = direction === "asc" ? "↑" : "↓"; 11521d05cddcSAtari911 arrow.style.color = "#00cc07"; 11531d05cddcSAtari911 } else { 11541d05cddcSAtari911 arrow.textContent = "⇅"; 11551d05cddcSAtari911 arrow.style.color = "#999"; 11561d05cddcSAtari911 } 11571d05cddcSAtari911 } 11581d05cddcSAtari911 }); 11591d05cddcSAtari911 11601d05cddcSAtari911 // Rebuild tbody 11611d05cddcSAtari911 rows.forEach(row => tbody.appendChild(row)); 11621d05cddcSAtari911 } 11631d05cddcSAtari911 11641d05cddcSAtari911 function filterRecurringEvents() { 11651d05cddcSAtari911 const searchInput = document.getElementById("searchRecurring"); 11661d05cddcSAtari911 const filter = normalizeText(searchInput.value); 11671d05cddcSAtari911 const tbody = document.getElementById("recurringTableBody"); 11681d05cddcSAtari911 const rows = tbody.getElementsByTagName("tr"); 11691d05cddcSAtari911 11701d05cddcSAtari911 for (let i = 0; i < rows.length; i++) { 11711d05cddcSAtari911 const row = rows[i]; 11721d05cddcSAtari911 const titleCell = row.getElementsByTagName("td")[0]; 11731d05cddcSAtari911 11741d05cddcSAtari911 if (titleCell) { 11751d05cddcSAtari911 const titleText = normalizeText(titleCell.textContent || titleCell.innerText); 11761d05cddcSAtari911 11771d05cddcSAtari911 if (titleText.indexOf(filter) > -1) { 11781d05cddcSAtari911 row.classList.remove("recurring-row-hidden"); 11791d05cddcSAtari911 } else { 11801d05cddcSAtari911 row.classList.add("recurring-row-hidden"); 11811d05cddcSAtari911 } 11821d05cddcSAtari911 } 11831d05cddcSAtari911 } 11841d05cddcSAtari911 } 11851d05cddcSAtari911 11861d05cddcSAtari911 function normalizeText(text) { 11871d05cddcSAtari911 // Convert to lowercase 11881d05cddcSAtari911 text = text.toLowerCase(); 11891d05cddcSAtari911 11901d05cddcSAtari911 // Remove apostrophes and quotes 11911d05cddcSAtari911 text = text.replace(/[\'\"]/g, ""); 11921d05cddcSAtari911 11931d05cddcSAtari911 // Replace accented characters with regular ones 11941d05cddcSAtari911 text = text.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); 11951d05cddcSAtari911 11961d05cddcSAtari911 // Remove special characters except spaces and alphanumeric 11971d05cddcSAtari911 text = text.replace(/[^a-z0-9\s]/g, ""); 11981d05cddcSAtari911 11991d05cddcSAtari911 // Collapse multiple spaces 12001d05cddcSAtari911 text = text.replace(/\s+/g, " "); 12011d05cddcSAtari911 12021d05cddcSAtari911 return text.trim(); 12031d05cddcSAtari911 } 12041d05cddcSAtari911 12051d05cddcSAtari911 function filterEvents() { 12061d05cddcSAtari911 const searchText = normalizeText(document.getElementById("searchEvents").value); 12071d05cddcSAtari911 const eventRows = document.querySelectorAll(".event-row"); 12081d05cddcSAtari911 let visibleCount = 0; 12091d05cddcSAtari911 12101d05cddcSAtari911 eventRows.forEach(row => { 12111d05cddcSAtari911 const titleElement = row.querySelector("div div"); 12121d05cddcSAtari911 const originalTitle = titleElement.getAttribute("data-original-title") || titleElement.textContent; 12131d05cddcSAtari911 12141d05cddcSAtari911 // Store original title if not already stored 12151d05cddcSAtari911 if (!titleElement.getAttribute("data-original-title")) { 12161d05cddcSAtari911 titleElement.setAttribute("data-original-title", originalTitle); 12171d05cddcSAtari911 } 12181d05cddcSAtari911 12191d05cddcSAtari911 const normalizedTitle = normalizeText(originalTitle); 12201d05cddcSAtari911 12211d05cddcSAtari911 if (normalizedTitle.includes(searchText) || searchText === "") { 12221d05cddcSAtari911 row.style.display = "flex"; 12231d05cddcSAtari911 visibleCount++; 12241d05cddcSAtari911 } else { 12251d05cddcSAtari911 row.style.display = "none"; 12261d05cddcSAtari911 } 12271d05cddcSAtari911 }); 12281d05cddcSAtari911 12291d05cddcSAtari911 // Update namespace visibility and counts 12301d05cddcSAtari911 document.querySelectorAll("[id^=ns_]").forEach(nsDiv => { 12311d05cddcSAtari911 if (nsDiv.id.endsWith("_arrow") || nsDiv.id.endsWith("_check")) return; 12321d05cddcSAtari911 12331d05cddcSAtari911 const visibleEvents = nsDiv.querySelectorAll(".event-row[style*=\\"display: flex\\"], .event-row:not([style*=\\"display: none\\"])").length; 12341d05cddcSAtari911 const nsId = nsDiv.id; 12351d05cddcSAtari911 const arrow = document.getElementById(nsId + "_arrow"); 12361d05cddcSAtari911 12371d05cddcSAtari911 // Auto-expand namespaces with matches when searching 12381d05cddcSAtari911 if (searchText && visibleEvents > 0) { 12391d05cddcSAtari911 nsDiv.style.display = "block"; 12401d05cddcSAtari911 if (arrow) arrow.textContent = "▼"; 12411d05cddcSAtari911 } 12421d05cddcSAtari911 }); 12431d05cddcSAtari911 } 12441d05cddcSAtari911 12451d05cddcSAtari911 function toggleNamespace(id) { 12461d05cddcSAtari911 const elem = document.getElementById(id); 12471d05cddcSAtari911 const arrow = document.getElementById(id + "_arrow"); 12481d05cddcSAtari911 if (elem.style.display === "none") { 12491d05cddcSAtari911 elem.style.display = "block"; 12501d05cddcSAtari911 arrow.textContent = "▼"; 12511d05cddcSAtari911 } else { 12521d05cddcSAtari911 elem.style.display = "none"; 12531d05cddcSAtari911 arrow.textContent = "▶"; 12541d05cddcSAtari911 } 12551d05cddcSAtari911 } 12561d05cddcSAtari911 12571d05cddcSAtari911 function toggleNamespaceSelect(nsId) { 12581d05cddcSAtari911 const checkbox = document.getElementById(nsId + "_check"); 12591d05cddcSAtari911 const events = document.querySelectorAll("." + nsId + "_events"); 12601d05cddcSAtari911 12611d05cddcSAtari911 // Only select visible events (not hidden by search) 12621d05cddcSAtari911 events.forEach(cb => { 12631d05cddcSAtari911 const eventRow = cb.closest(".event-row"); 12641d05cddcSAtari911 if (eventRow && eventRow.style.display !== "none") { 12651d05cddcSAtari911 cb.checked = checkbox.checked; 12661d05cddcSAtari911 } 12671d05cddcSAtari911 }); 12681d05cddcSAtari911 updateCount(); 12691d05cddcSAtari911 } 12701d05cddcSAtari911 12711d05cddcSAtari911 function selectAll() { 12721d05cddcSAtari911 // Only select visible events 12731d05cddcSAtari911 document.querySelectorAll(".event-checkbox").forEach(cb => { 12741d05cddcSAtari911 const eventRow = cb.closest(".event-row"); 12751d05cddcSAtari911 if (eventRow && eventRow.style.display !== "none") { 12761d05cddcSAtari911 cb.checked = true; 12771d05cddcSAtari911 } 12781d05cddcSAtari911 }); 12791d05cddcSAtari911 // Update namespace checkboxes to indeterminate if partially selected 12801d05cddcSAtari911 document.querySelectorAll("input[id$=_check]").forEach(nsCheckbox => { 12811d05cddcSAtari911 const nsId = nsCheckbox.id.replace("_check", ""); 12821d05cddcSAtari911 const events = document.querySelectorAll("." + nsId + "_events"); 12831d05cddcSAtari911 const visibleEvents = Array.from(events).filter(cb => { 12841d05cddcSAtari911 const row = cb.closest(".event-row"); 12851d05cddcSAtari911 return row && row.style.display !== "none"; 12861d05cddcSAtari911 }); 12871d05cddcSAtari911 const checkedVisible = visibleEvents.filter(cb => cb.checked); 12881d05cddcSAtari911 12891d05cddcSAtari911 if (checkedVisible.length === visibleEvents.length && visibleEvents.length > 0) { 12901d05cddcSAtari911 nsCheckbox.checked = true; 12911d05cddcSAtari911 } else if (checkedVisible.length > 0) { 12921d05cddcSAtari911 nsCheckbox.indeterminate = true; 12931d05cddcSAtari911 } else { 12941d05cddcSAtari911 nsCheckbox.checked = false; 12951d05cddcSAtari911 } 12961d05cddcSAtari911 }); 12971d05cddcSAtari911 updateCount(); 12981d05cddcSAtari911 } 12991d05cddcSAtari911 13001d05cddcSAtari911 function deselectAll() { 13011d05cddcSAtari911 document.querySelectorAll(".event-checkbox").forEach(cb => cb.checked = false); 13021d05cddcSAtari911 document.querySelectorAll("input[id$=_check]").forEach(cb => { 13031d05cddcSAtari911 cb.checked = false; 13041d05cddcSAtari911 cb.indeterminate = false; 13051d05cddcSAtari911 }); 13061d05cddcSAtari911 updateCount(); 13071d05cddcSAtari911 } 13081d05cddcSAtari911 13091d05cddcSAtari911 function deleteSelected() { 13101d05cddcSAtari911 const checkedBoxes = document.querySelectorAll(".event-checkbox:checked"); 13111d05cddcSAtari911 if (checkedBoxes.length === 0) { 13121d05cddcSAtari911 alert("No events selected"); 13131d05cddcSAtari911 return; 13141d05cddcSAtari911 } 13151d05cddcSAtari911 13161d05cddcSAtari911 const count = checkedBoxes.length; 13171d05cddcSAtari911 if (!confirm(`Delete ${count} selected event(s)?\\n\\nThis cannot be undone!`)) { 13181d05cddcSAtari911 return; 13191d05cddcSAtari911 } 13201d05cddcSAtari911 13211d05cddcSAtari911 const form = document.createElement("form"); 13221d05cddcSAtari911 form.method = "POST"; 13231d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=manage"; 13241d05cddcSAtari911 13251d05cddcSAtari911 const actionInput = document.createElement("input"); 13261d05cddcSAtari911 actionInput.type = "hidden"; 13271d05cddcSAtari911 actionInput.name = "action"; 13281d05cddcSAtari911 actionInput.value = "delete_selected_events"; 13291d05cddcSAtari911 form.appendChild(actionInput); 13301d05cddcSAtari911 13311d05cddcSAtari911 checkedBoxes.forEach(cb => { 13321d05cddcSAtari911 const eventInput = document.createElement("input"); 13331d05cddcSAtari911 eventInput.type = "hidden"; 13341d05cddcSAtari911 eventInput.name = "events[]"; 13351d05cddcSAtari911 eventInput.value = cb.value; 13361d05cddcSAtari911 form.appendChild(eventInput); 13371d05cddcSAtari911 }); 13381d05cddcSAtari911 13391d05cddcSAtari911 document.body.appendChild(form); 13401d05cddcSAtari911 form.submit(); 13411d05cddcSAtari911 } 13421d05cddcSAtari911 13431d05cddcSAtari911 function createNewNamespace() { 13441d05cddcSAtari911 const namespaceName = prompt("Enter new namespace name:\\n\\nExamples:\\n- work\\n- personal\\n- projects:alpha\\n- aspen:travel:2025"); 13451d05cddcSAtari911 13461d05cddcSAtari911 if (!namespaceName) { 13471d05cddcSAtari911 return; // Cancelled 13481d05cddcSAtari911 } 13491d05cddcSAtari911 13501d05cddcSAtari911 // Validate namespace name 13511d05cddcSAtari911 if (!/^[a-zA-Z0-9_:-]+$/.test(namespaceName)) { 13521d05cddcSAtari911 alert("Invalid namespace name.\\n\\nUse only letters, numbers, underscore, hyphen, and colon.\\nExample: work:projects:alpha"); 13531d05cddcSAtari911 return; 13541d05cddcSAtari911 } 13551d05cddcSAtari911 13561d05cddcSAtari911 // Submit form to create namespace 13571d05cddcSAtari911 const form = document.createElement("form"); 13581d05cddcSAtari911 form.method = "POST"; 13591d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=manage"; 13601d05cddcSAtari911 13611d05cddcSAtari911 const actionInput = document.createElement("input"); 13621d05cddcSAtari911 actionInput.type = "hidden"; 13631d05cddcSAtari911 actionInput.name = "action"; 13641d05cddcSAtari911 actionInput.value = "create_namespace"; 13651d05cddcSAtari911 form.appendChild(actionInput); 13661d05cddcSAtari911 13671d05cddcSAtari911 const namespaceInput = document.createElement("input"); 13681d05cddcSAtari911 namespaceInput.type = "hidden"; 13691d05cddcSAtari911 namespaceInput.name = "namespace_name"; 13701d05cddcSAtari911 namespaceInput.value = namespaceName; 13711d05cddcSAtari911 form.appendChild(namespaceInput); 13721d05cddcSAtari911 13731d05cddcSAtari911 document.body.appendChild(form); 13741d05cddcSAtari911 form.submit(); 13751d05cddcSAtari911 } 13761d05cddcSAtari911 13771d05cddcSAtari911 function updateCount() { 13781d05cddcSAtari911 const count = document.querySelectorAll(".event-checkbox:checked").length; 13791d05cddcSAtari911 document.getElementById("selectedCount").textContent = count + " selected"; 13801d05cddcSAtari911 } 13811d05cddcSAtari911 13821d05cddcSAtari911 function deleteNamespace(namespace) { 13831d05cddcSAtari911 const displayName = namespace || "(default)"; 13841d05cddcSAtari911 if (!confirm("Delete ENTIRE namespace: " + displayName + "?\\n\\nThis will delete ALL events in this namespace!\\n\\nThis cannot be undone!")) { 13851d05cddcSAtari911 return; 13861d05cddcSAtari911 } 13871d05cddcSAtari911 const form = document.createElement("form"); 13881d05cddcSAtari911 form.method = "POST"; 13891d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=manage"; 13901d05cddcSAtari911 const actionInput = document.createElement("input"); 13911d05cddcSAtari911 actionInput.type = "hidden"; 13921d05cddcSAtari911 actionInput.name = "action"; 13931d05cddcSAtari911 actionInput.value = "delete_namespace"; 13941d05cddcSAtari911 form.appendChild(actionInput); 13951d05cddcSAtari911 const nsInput = document.createElement("input"); 13961d05cddcSAtari911 nsInput.type = "hidden"; 13971d05cddcSAtari911 nsInput.name = "namespace"; 13981d05cddcSAtari911 nsInput.value = namespace; 13991d05cddcSAtari911 form.appendChild(nsInput); 14001d05cddcSAtari911 document.body.appendChild(form); 14011d05cddcSAtari911 form.submit(); 14021d05cddcSAtari911 } 14031d05cddcSAtari911 14049ccd446eSAtari911 function renameNamespace(oldNamespace) { 14059ccd446eSAtari911 const displayName = oldNamespace || "(default)"; 14069ccd446eSAtari911 const newName = prompt("Rename namespace: " + displayName + "\\n\\nEnter new name:", oldNamespace); 14079ccd446eSAtari911 if (newName === null || newName === oldNamespace) { 14089ccd446eSAtari911 return; // Cancelled or no change 14099ccd446eSAtari911 } 14109ccd446eSAtari911 const form = document.createElement("form"); 14119ccd446eSAtari911 form.method = "POST"; 14129ccd446eSAtari911 form.action = "?do=admin&page=calendar&tab=manage"; 14139ccd446eSAtari911 const actionInput = document.createElement("input"); 14149ccd446eSAtari911 actionInput.type = "hidden"; 14159ccd446eSAtari911 actionInput.name = "action"; 14169ccd446eSAtari911 actionInput.value = "rename_namespace"; 14179ccd446eSAtari911 form.appendChild(actionInput); 14189ccd446eSAtari911 const oldInput = document.createElement("input"); 14199ccd446eSAtari911 oldInput.type = "hidden"; 14209ccd446eSAtari911 oldInput.name = "old_namespace"; 14219ccd446eSAtari911 oldInput.value = oldNamespace; 14229ccd446eSAtari911 form.appendChild(oldInput); 14239ccd446eSAtari911 const newInput = document.createElement("input"); 14249ccd446eSAtari911 newInput.type = "hidden"; 14259ccd446eSAtari911 newInput.name = "new_namespace"; 14269ccd446eSAtari911 newInput.value = newName; 14279ccd446eSAtari911 form.appendChild(newInput); 14289ccd446eSAtari911 document.body.appendChild(form); 14299ccd446eSAtari911 form.submit(); 14309ccd446eSAtari911 } 14319ccd446eSAtari911 14321d05cddcSAtari911 let draggedEvent = null; 14331d05cddcSAtari911 14341d05cddcSAtari911 function dragStart(event, eventId) { 14351d05cddcSAtari911 const checkbox = event.target.closest(".event-row").querySelector(".event-checkbox"); 14361d05cddcSAtari911 14371d05cddcSAtari911 // If this event is checked, drag all checked events 14381d05cddcSAtari911 const checkedBoxes = document.querySelectorAll(".event-checkbox:checked"); 14391d05cddcSAtari911 if (checkbox && checkbox.checked && checkedBoxes.length > 1) { 14401d05cddcSAtari911 // Dragging multiple selected events 14411d05cddcSAtari911 draggedEvent = "MULTIPLE"; 14421d05cddcSAtari911 event.dataTransfer.setData("text/plain", "MULTIPLE"); 14431d05cddcSAtari911 } else { 14441d05cddcSAtari911 // Dragging single event 14451d05cddcSAtari911 draggedEvent = eventId; 14461d05cddcSAtari911 event.dataTransfer.setData("text/plain", eventId); 14471d05cddcSAtari911 } 14481d05cddcSAtari911 event.dataTransfer.effectAllowed = "move"; 14491d05cddcSAtari911 event.target.style.opacity = "0.5"; 14501d05cddcSAtari911 } 14511d05cddcSAtari911 14521d05cddcSAtari911 function allowDrop(event) { 14531d05cddcSAtari911 event.preventDefault(); 14541d05cddcSAtari911 event.dataTransfer.dropEffect = "move"; 14551d05cddcSAtari911 } 14561d05cddcSAtari911 14571d05cddcSAtari911 function drop(event, targetNamespace) { 14581d05cddcSAtari911 event.preventDefault(); 14591d05cddcSAtari911 14601d05cddcSAtari911 if (draggedEvent === "MULTIPLE") { 14611d05cddcSAtari911 // Move all selected events 14621d05cddcSAtari911 const checkedBoxes = document.querySelectorAll(".event-checkbox:checked"); 14631d05cddcSAtari911 if (checkedBoxes.length === 0) return; 14641d05cddcSAtari911 14651d05cddcSAtari911 const form = document.createElement("form"); 14661d05cddcSAtari911 form.method = "POST"; 14671d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=manage"; 14681d05cddcSAtari911 14691d05cddcSAtari911 const actionInput = document.createElement("input"); 14701d05cddcSAtari911 actionInput.type = "hidden"; 14711d05cddcSAtari911 actionInput.name = "action"; 14721d05cddcSAtari911 actionInput.value = "move_selected_events"; 14731d05cddcSAtari911 form.appendChild(actionInput); 14741d05cddcSAtari911 14751d05cddcSAtari911 checkedBoxes.forEach(cb => { 14761d05cddcSAtari911 const eventInput = document.createElement("input"); 14771d05cddcSAtari911 eventInput.type = "hidden"; 14781d05cddcSAtari911 eventInput.name = "events[]"; 14791d05cddcSAtari911 eventInput.value = cb.value; 14801d05cddcSAtari911 form.appendChild(eventInput); 14811d05cddcSAtari911 }); 14821d05cddcSAtari911 14831d05cddcSAtari911 const targetInput = document.createElement("input"); 14841d05cddcSAtari911 targetInput.type = "hidden"; 14851d05cddcSAtari911 targetInput.name = "target_namespace"; 14861d05cddcSAtari911 targetInput.value = targetNamespace; 14871d05cddcSAtari911 form.appendChild(targetInput); 14881d05cddcSAtari911 14891d05cddcSAtari911 document.body.appendChild(form); 14901d05cddcSAtari911 form.submit(); 14911d05cddcSAtari911 } else { 14921d05cddcSAtari911 // Move single event 14931d05cddcSAtari911 if (!draggedEvent) return; 14941d05cddcSAtari911 const parts = draggedEvent.split("|"); 14951d05cddcSAtari911 const sourceNamespace = parts[1]; 14961d05cddcSAtari911 if (sourceNamespace === targetNamespace) return; 14971d05cddcSAtari911 14981d05cddcSAtari911 const form = document.createElement("form"); 14991d05cddcSAtari911 form.method = "POST"; 15001d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=manage"; 15011d05cddcSAtari911 const actionInput = document.createElement("input"); 15021d05cddcSAtari911 actionInput.type = "hidden"; 15031d05cddcSAtari911 actionInput.name = "action"; 15041d05cddcSAtari911 actionInput.value = "move_single_event"; 15051d05cddcSAtari911 form.appendChild(actionInput); 15061d05cddcSAtari911 const eventInput = document.createElement("input"); 15071d05cddcSAtari911 eventInput.type = "hidden"; 15081d05cddcSAtari911 eventInput.name = "event"; 15091d05cddcSAtari911 eventInput.value = draggedEvent; 15101d05cddcSAtari911 form.appendChild(eventInput); 15111d05cddcSAtari911 const targetInput = document.createElement("input"); 15121d05cddcSAtari911 targetInput.type = "hidden"; 15131d05cddcSAtari911 targetInput.name = "target_namespace"; 15141d05cddcSAtari911 targetInput.value = targetNamespace; 15151d05cddcSAtari911 form.appendChild(targetInput); 15161d05cddcSAtari911 document.body.appendChild(form); 15171d05cddcSAtari911 form.submit(); 15181d05cddcSAtari911 } 15191d05cddcSAtari911 } 15201d05cddcSAtari911 15211d05cddcSAtari911 function editRecurringSeries(title, namespace) { 15229ccd446eSAtari911 // Get available namespaces from the namespace explorer 15239ccd446eSAtari911 const namespaces = new Set(); 15241d05cddcSAtari911 15259ccd446eSAtari911 // Method 1: Try to get from namespace explorer folder names 15269ccd446eSAtari911 document.querySelectorAll("[id^=ns_]").forEach(el => { 15279ccd446eSAtari911 const nsSpan = el.querySelector("span:nth-child(3)"); 15289ccd446eSAtari911 if (nsSpan) { 15299ccd446eSAtari911 let nsText = nsSpan.textContent.replace(" ", "").trim(); 15309ccd446eSAtari911 if (nsText && nsText !== "(default)") { 15319ccd446eSAtari911 namespaces.add(nsText); 15329ccd446eSAtari911 } 15339ccd446eSAtari911 } 15349ccd446eSAtari911 }); 15359ccd446eSAtari911 15369ccd446eSAtari911 // Method 2: Get from datalist if it exists 15379ccd446eSAtari911 document.querySelectorAll("#namespaceList option").forEach(opt => { 15389ccd446eSAtari911 if (opt.value && opt.value !== "") { 15399ccd446eSAtari911 namespaces.add(opt.value); 15409ccd446eSAtari911 } 15419ccd446eSAtari911 }); 15429ccd446eSAtari911 15439ccd446eSAtari911 // Convert to sorted array 15449ccd446eSAtari911 const nsArray = Array.from(namespaces).sort(); 15459ccd446eSAtari911 15469ccd446eSAtari911 // Build options - include current namespace AND all others 15479ccd446eSAtari911 let nsOptions = "<option value=\\"\\">(default)</option>"; 15489ccd446eSAtari911 15499ccd446eSAtari911 // Add current namespace if it\'s not default 15509ccd446eSAtari911 if (namespace && namespace !== "") { 15519ccd446eSAtari911 nsOptions += "<option value=\\"" + namespace + "\\" selected>" + namespace + " (current)</option>"; 15529ccd446eSAtari911 } 15539ccd446eSAtari911 15549ccd446eSAtari911 // Add all other namespaces 15559ccd446eSAtari911 for (const ns of nsArray) { 15569ccd446eSAtari911 if (ns !== namespace) { 15579ccd446eSAtari911 nsOptions += "<option value=\\"" + ns + "\\">" + ns + "</option>"; 15581d05cddcSAtari911 } 15591d05cddcSAtari911 } 15601d05cddcSAtari911 15611d05cddcSAtari911 // Show edit dialog for recurring events 15621d05cddcSAtari911 const dialog = document.createElement("div"); 15631d05cddcSAtari911 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;"; 15641d05cddcSAtari911 15651d05cddcSAtari911 // Close on clicking background 15661d05cddcSAtari911 dialog.addEventListener("click", function(e) { 15671d05cddcSAtari911 if (e.target === dialog) { 15681d05cddcSAtari911 dialog.remove(); 15691d05cddcSAtari911 } 15701d05cddcSAtari911 }); 15711d05cddcSAtari911 15721d05cddcSAtari911 dialog.innerHTML = ` 15739ccd446eSAtari911 <div style="background:' . $colors['bg'] . '; padding:20px; border-radius:8px; min-width:500px; max-width:700px; max-height:90vh; overflow-y:auto;"> 15741d05cddcSAtari911 <h3 style="margin:0 0 15px; color:#00cc07;">Edit Recurring Event</h3> 15759ccd446eSAtari911 <p style="margin:0 0 15px; color:' . $colors['text'] . '; font-size:13px;">Changes will apply to ALL occurrences of: <strong>${title}</strong></p> 15761d05cddcSAtari911 15771d05cddcSAtari911 <form id="editRecurringForm" style="display:flex; flex-direction:column; gap:12px;"> 15781d05cddcSAtari911 <div> 15791d05cddcSAtari911 <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">New Title:</label> 15809ccd446eSAtari911 <input type="text" name="new_title" value="${title}" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;" required> 15811d05cddcSAtari911 </div> 15821d05cddcSAtari911 15831d05cddcSAtari911 <div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;"> 15841d05cddcSAtari911 <div> 15851d05cddcSAtari911 <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">Start Time:</label> 15869ccd446eSAtari911 <input type="time" name="start_time" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;"> 15871d05cddcSAtari911 <small style="color:#999; font-size:11px;">Leave blank to keep current</small> 15881d05cddcSAtari911 </div> 15891d05cddcSAtari911 <div> 15901d05cddcSAtari911 <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">End Time:</label> 15919ccd446eSAtari911 <input type="time" name="end_time" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;"> 15921d05cddcSAtari911 <small style="color:#999; font-size:11px;">Leave blank to keep current</small> 15931d05cddcSAtari911 </div> 15941d05cddcSAtari911 </div> 15951d05cddcSAtari911 15961d05cddcSAtari911 <div> 15971d05cddcSAtari911 <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">Interval (days between occurrences):</label> 15989ccd446eSAtari911 <select name="interval" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;"> 15991d05cddcSAtari911 <option value="">Keep current interval</option> 16001d05cddcSAtari911 <option value="1">Daily (1 day)</option> 16011d05cddcSAtari911 <option value="7">Weekly (7 days)</option> 16021d05cddcSAtari911 <option value="14">Bi-weekly (14 days)</option> 16031d05cddcSAtari911 <option value="30">Monthly (30 days)</option> 16041d05cddcSAtari911 <option value="365">Yearly (365 days)</option> 16051d05cddcSAtari911 </select> 16061d05cddcSAtari911 </div> 16071d05cddcSAtari911 16081d05cddcSAtari911 <div> 16091d05cddcSAtari911 <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">Move to Namespace:</label> 16109ccd446eSAtari911 <select name="new_namespace" style="width:100%; padding:8px; border:1px solid ' . $colors['border'] . '; border-radius:3px;"> 16111d05cddcSAtari911 ${nsOptions} 16121d05cddcSAtari911 </select> 16131d05cddcSAtari911 </div> 16141d05cddcSAtari911 16151d05cddcSAtari911 <div style="display:flex; gap:10px; margin-top:10px;"> 16161d05cddcSAtari911 <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> 16171d05cddcSAtari911 <button type="button" onclick="closeEditDialog()" style="flex:1; background:#999; color:white; padding:10px; border:none; border-radius:3px; cursor:pointer;">Cancel</button> 16181d05cddcSAtari911 </div> 16191d05cddcSAtari911 </form> 16201d05cddcSAtari911 </div> 16211d05cddcSAtari911 `; 16221d05cddcSAtari911 16231d05cddcSAtari911 document.body.appendChild(dialog); 16241d05cddcSAtari911 16251d05cddcSAtari911 // Add close function to window 16261d05cddcSAtari911 window.closeEditDialog = function() { 16271d05cddcSAtari911 dialog.remove(); 16281d05cddcSAtari911 }; 16291d05cddcSAtari911 16301d05cddcSAtari911 // Handle form submission 16311d05cddcSAtari911 dialog.querySelector("#editRecurringForm").addEventListener("submit", function(e) { 16321d05cddcSAtari911 e.preventDefault(); 16331d05cddcSAtari911 const formData = new FormData(this); 16341d05cddcSAtari911 16351d05cddcSAtari911 // Submit the edit 16361d05cddcSAtari911 const form = document.createElement("form"); 16371d05cddcSAtari911 form.method = "POST"; 16381d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=manage"; 16391d05cddcSAtari911 16401d05cddcSAtari911 const actionInput = document.createElement("input"); 16411d05cddcSAtari911 actionInput.type = "hidden"; 16421d05cddcSAtari911 actionInput.name = "action"; 16431d05cddcSAtari911 actionInput.value = "edit_recurring_series"; 16441d05cddcSAtari911 form.appendChild(actionInput); 16451d05cddcSAtari911 16461d05cddcSAtari911 const oldTitleInput = document.createElement("input"); 16471d05cddcSAtari911 oldTitleInput.type = "hidden"; 16481d05cddcSAtari911 oldTitleInput.name = "old_title"; 16491d05cddcSAtari911 oldTitleInput.value = title; 16501d05cddcSAtari911 form.appendChild(oldTitleInput); 16511d05cddcSAtari911 16521d05cddcSAtari911 const oldNamespaceInput = document.createElement("input"); 16531d05cddcSAtari911 oldNamespaceInput.type = "hidden"; 16541d05cddcSAtari911 oldNamespaceInput.name = "old_namespace"; 16551d05cddcSAtari911 oldNamespaceInput.value = namespace; 16561d05cddcSAtari911 form.appendChild(oldNamespaceInput); 16571d05cddcSAtari911 16581d05cddcSAtari911 // Add all form fields 16591d05cddcSAtari911 for (let [key, value] of formData.entries()) { 16601d05cddcSAtari911 const input = document.createElement("input"); 16611d05cddcSAtari911 input.type = "hidden"; 16621d05cddcSAtari911 input.name = key; 16631d05cddcSAtari911 input.value = value; 16641d05cddcSAtari911 form.appendChild(input); 16651d05cddcSAtari911 } 16661d05cddcSAtari911 16671d05cddcSAtari911 document.body.appendChild(form); 16681d05cddcSAtari911 form.submit(); 16691d05cddcSAtari911 }); 16701d05cddcSAtari911 } 16711d05cddcSAtari911 16721d05cddcSAtari911 function deleteRecurringSeries(title, namespace) { 16731d05cddcSAtari911 const displayNs = namespace || "(default)"; 16741d05cddcSAtari911 if (!confirm("Delete ALL occurrences of: " + title + " (" + displayNs + ")?\\n\\nThis cannot be undone!")) { 16751d05cddcSAtari911 return; 16761d05cddcSAtari911 } 16771d05cddcSAtari911 const form = document.createElement("form"); 16781d05cddcSAtari911 form.method = "POST"; 16791d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=manage"; 16801d05cddcSAtari911 const actionInput = document.createElement("input"); 16811d05cddcSAtari911 actionInput.type = "hidden"; 16821d05cddcSAtari911 actionInput.name = "action"; 16831d05cddcSAtari911 actionInput.value = "delete_recurring_series"; 16841d05cddcSAtari911 form.appendChild(actionInput); 16851d05cddcSAtari911 const titleInput = document.createElement("input"); 16861d05cddcSAtari911 titleInput.type = "hidden"; 16871d05cddcSAtari911 titleInput.name = "event_title"; 16881d05cddcSAtari911 titleInput.value = title; 16891d05cddcSAtari911 form.appendChild(titleInput); 16901d05cddcSAtari911 const namespaceInput = document.createElement("input"); 16911d05cddcSAtari911 namespaceInput.type = "hidden"; 16921d05cddcSAtari911 namespaceInput.name = "namespace"; 16931d05cddcSAtari911 namespaceInput.value = namespace; 16941d05cddcSAtari911 form.appendChild(namespaceInput); 16951d05cddcSAtari911 document.body.appendChild(form); 16961d05cddcSAtari911 form.submit(); 16971d05cddcSAtari911 } 16981d05cddcSAtari911 16991d05cddcSAtari911 document.addEventListener("dragend", function(e) { 17001d05cddcSAtari911 if (e.target.draggable) { 17011d05cddcSAtari911 e.target.style.opacity = "1"; 17021d05cddcSAtari911 } 17031d05cddcSAtari911 }); 17041d05cddcSAtari911 </script>'; 17051d05cddcSAtari911 } 17061d05cddcSAtari911 17079ccd446eSAtari911 private function renderUpdateTab($colors = null) { 17081d05cddcSAtari911 global $INPUT; 17091d05cddcSAtari911 17109ccd446eSAtari911 // Use defaults if not provided 17119ccd446eSAtari911 if ($colors === null) { 17129ccd446eSAtari911 $colors = $this->getTemplateColors(); 17139ccd446eSAtari911 } 17141d05cddcSAtari911 17159ccd446eSAtari911 echo '<h2 style="margin:10px 0; font-size:20px;"> Update Plugin</h2>'; 17161d05cddcSAtari911 17171d05cddcSAtari911 // Show message if present 17181d05cddcSAtari911 if ($INPUT->has('msg')) { 17191d05cddcSAtari911 $msg = hsc($INPUT->str('msg')); 17201d05cddcSAtari911 $type = $INPUT->str('msgtype', 'success'); 17211d05cddcSAtari911 $class = ($type === 'success') ? 'msg success' : 'msg error'; 17229ccd446eSAtari911 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;\">"; 17231d05cddcSAtari911 echo $msg; 17241d05cddcSAtari911 echo "</div>"; 17251d05cddcSAtari911 } 17261d05cddcSAtari911 17279ccd446eSAtari911 // Show current version FIRST (MOVED TO TOP) 17281d05cddcSAtari911 $pluginInfo = DOKU_PLUGIN . 'calendar/plugin.info.txt'; 17291d05cddcSAtari911 $info = ['version' => 'Unknown', 'date' => 'Unknown', 'name' => 'Calendar Plugin', 'author' => 'Unknown', 'email' => '', 'desc' => '']; 17301d05cddcSAtari911 if (file_exists($pluginInfo)) { 17311d05cddcSAtari911 $info = array_merge($info, confToHash($pluginInfo)); 17321d05cddcSAtari911 } 17331d05cddcSAtari911 17349ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">'; 17359ccd446eSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;"> Current Version</h3>'; 17361d05cddcSAtari911 echo '<div style="font-size:12px; line-height:1.6;">'; 17371d05cddcSAtari911 echo '<div style="margin:4px 0;"><strong>Version:</strong> ' . hsc($info['version']) . ' (' . hsc($info['date']) . ')</div>'; 17381d05cddcSAtari911 echo '<div style="margin:4px 0;"><strong>Author:</strong> ' . hsc($info['author']) . ($info['email'] ? ' <' . hsc($info['email']) . '>' : '') . '</div>'; 17391d05cddcSAtari911 if ($info['desc']) { 17401d05cddcSAtari911 echo '<div style="margin:4px 0;"><strong>Description:</strong> ' . hsc($info['desc']) . '</div>'; 17411d05cddcSAtari911 } 17421d05cddcSAtari911 echo '<div style="margin:4px 0;"><strong>Location:</strong> <code style="background:#f0f0f0; padding:2px 4px; border-radius:2px;">' . DOKU_PLUGIN . 'calendar/</code></div>'; 17431d05cddcSAtari911 echo '</div>'; 17441d05cddcSAtari911 17451d05cddcSAtari911 // Check permissions 17461d05cddcSAtari911 $pluginDir = DOKU_PLUGIN . 'calendar/'; 17471d05cddcSAtari911 $pluginWritable = is_writable($pluginDir); 17481d05cddcSAtari911 $parentWritable = is_writable(DOKU_PLUGIN); 17491d05cddcSAtari911 17509ccd446eSAtari911 echo '<div style="margin-top:8px; padding-top:8px; border-top:1px solid ' . $colors['border'] . ';">'; 17511d05cddcSAtari911 if ($pluginWritable && $parentWritable) { 17521d05cddcSAtari911 echo '<p style="margin:5px 0; font-size:13px; color:#28a745;"><strong>✅ Permissions:</strong> OK - ready to update</p>'; 17531d05cddcSAtari911 } else { 17541d05cddcSAtari911 echo '<p style="margin:5px 0; font-size:13px; color:#dc3545;"><strong>❌ Permissions:</strong> Issues detected</p>'; 17551d05cddcSAtari911 if (!$pluginWritable) { 17561d05cddcSAtari911 echo '<p style="margin:2px 0 2px 20px; font-size:12px; color:#dc3545;">Plugin directory not writable</p>'; 17571d05cddcSAtari911 } 17581d05cddcSAtari911 if (!$parentWritable) { 17591d05cddcSAtari911 echo '<p style="margin:2px 0 2px 20px; font-size:12px; color:#dc3545;">Parent directory not writable</p>'; 17601d05cddcSAtari911 } 17619ccd446eSAtari911 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>'; 17629ccd446eSAtari911 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>'; 17631d05cddcSAtari911 } 17641d05cddcSAtari911 echo '</div>'; 17651d05cddcSAtari911 17661d05cddcSAtari911 echo '</div>'; 17671d05cddcSAtari911 17689ccd446eSAtari911 // Combined upload and notes section (SIDE BY SIDE) 17699ccd446eSAtari911 echo '<div style="display:flex; gap:15px; max-width:1200px; margin:10px 0;">'; 17701d05cddcSAtari911 17719ccd446eSAtari911 // Left side - Upload form (60% width) 17729ccd446eSAtari911 echo '<div style="flex:1; min-width:0; background:' . $colors['bg'] . '; padding:12px; border-left:3px solid #00cc07; border-radius:3px;">'; 17739ccd446eSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;"> Upload New Version</h3>'; 17749ccd446eSAtari911 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>'; 17751d05cddcSAtari911 17761d05cddcSAtari911 echo '<form method="post" action="?do=admin&page=calendar&tab=update" enctype="multipart/form-data" id="uploadForm">'; 17771d05cddcSAtari911 echo '<input type="hidden" name="action" value="upload_update">'; 17781d05cddcSAtari911 echo '<div style="margin:10px 0;">'; 17799ccd446eSAtari911 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%;">'; 17801d05cddcSAtari911 echo '</div>'; 17811d05cddcSAtari911 echo '<div style="margin:10px 0;">'; 17821d05cddcSAtari911 echo '<label style="display:flex; align-items:center; gap:8px; font-size:13px;">'; 17831d05cddcSAtari911 echo '<input type="checkbox" name="backup_first" value="1" checked>'; 17841d05cddcSAtari911 echo '<span>Create backup before updating (Recommended)</span>'; 17851d05cddcSAtari911 echo '</label>'; 17861d05cddcSAtari911 echo '</div>'; 17879ccd446eSAtari911 17889ccd446eSAtari911 // Buttons side by side 17899ccd446eSAtari911 echo '<div style="display:flex; gap:10px; align-items:center; flex-wrap:wrap;">'; 17901d05cddcSAtari911 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>'; 17911d05cddcSAtari911 echo '</form>'; 17929ccd446eSAtari911 17939ccd446eSAtari911 // Clear Cache button (next to Upload button) 17949ccd446eSAtari911 echo '<form method="post" action="?do=admin&page=calendar&tab=update" style="display:inline; margin:0;">'; 17959ccd446eSAtari911 echo '<input type="hidden" name="action" value="clear_cache">'; 17969ccd446eSAtari911 echo '<input type="hidden" name="tab" value="update">'; 17979ccd446eSAtari911 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>'; 17989ccd446eSAtari911 echo '</form>'; 17991d05cddcSAtari911 echo '</div>'; 18001d05cddcSAtari911 18019ccd446eSAtari911 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>'; 18029ccd446eSAtari911 echo '</div>'; 18039ccd446eSAtari911 18049ccd446eSAtari911 // Right side - Important Notes (40% width) 18059ccd446eSAtari911 echo '<div style="flex:0 0 350px; min-width:0; background:#fff3e0; border-left:3px solid #ff9800; padding:12px; border-radius:3px;">'; 18061d05cddcSAtari911 echo '<h4 style="margin:0 0 5px 0; color:#e65100; font-size:14px;">⚠️ Important Notes</h4>'; 18079ccd446eSAtari911 echo '<ul style="margin:5px 0; padding-left:20px; font-size:12px; color:#e65100; line-height:1.6;">'; 18081d05cddcSAtari911 echo '<li>This will replace all plugin files</li>'; 18091d05cddcSAtari911 echo '<li>Configuration files (sync_config.php) will be preserved</li>'; 18101d05cddcSAtari911 echo '<li>Event data will not be affected</li>'; 18119ccd446eSAtari911 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>'; 18121d05cddcSAtari911 echo '<li>Make sure the ZIP file is a valid calendar plugin</li>'; 18131d05cddcSAtari911 echo '</ul>'; 18141d05cddcSAtari911 echo '</div>'; 18151d05cddcSAtari911 18169ccd446eSAtari911 echo '</div>'; // End flex container 18179ccd446eSAtari911 18189ccd446eSAtari911 // Changelog section - Timeline viewer 18199ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #7b1fa2; border-radius:3px; max-width:1200px;">'; 18209ccd446eSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#7b1fa2; font-size:16px;"> Version History</h3>'; 18219ccd446eSAtari911 18229ccd446eSAtari911 $changelogFile = DOKU_PLUGIN . 'calendar/CHANGELOG.md'; 18239ccd446eSAtari911 if (file_exists($changelogFile)) { 18249ccd446eSAtari911 $changelog = file_get_contents($changelogFile); 18259ccd446eSAtari911 18269ccd446eSAtari911 // Parse ALL versions into structured data 18279ccd446eSAtari911 $lines = explode("\n", $changelog); 18289ccd446eSAtari911 $versions = []; 18299ccd446eSAtari911 $currentVersion = null; 18309ccd446eSAtari911 18319ccd446eSAtari911 foreach ($lines as $line) { 18329ccd446eSAtari911 $line = trim($line); 18339ccd446eSAtari911 18349ccd446eSAtari911 // Version header (## Version X.X.X or ## Version X.X.X (date) - title) 18359ccd446eSAtari911 if (preg_match('/^## Version (.+?)(?:\s*\(([^)]+)\))?\s*(?:-\s*(.+))?$/', $line, $matches)) { 18369ccd446eSAtari911 if ($currentVersion !== null) { 18379ccd446eSAtari911 $versions[] = $currentVersion; 18389ccd446eSAtari911 } 18399ccd446eSAtari911 $currentVersion = [ 18409ccd446eSAtari911 'number' => trim($matches[1]), 18419ccd446eSAtari911 'date' => isset($matches[2]) ? trim($matches[2]) : '', 18429ccd446eSAtari911 'title' => isset($matches[3]) ? trim($matches[3]) : '', 18439ccd446eSAtari911 'items' => [] 18449ccd446eSAtari911 ]; 18459ccd446eSAtari911 } 18469ccd446eSAtari911 // List items (- **Type:** description) 18479ccd446eSAtari911 elseif ($currentVersion !== null && preg_match('/^- \*\*(.+?):\*\* (.+)$/', $line, $matches)) { 18489ccd446eSAtari911 $currentVersion['items'][] = [ 18499ccd446eSAtari911 'type' => $matches[1], 18509ccd446eSAtari911 'desc' => $matches[2] 18519ccd446eSAtari911 ]; 18529ccd446eSAtari911 } 18539ccd446eSAtari911 } 18549ccd446eSAtari911 // Don\'t forget last version 18559ccd446eSAtari911 if ($currentVersion !== null) { 18569ccd446eSAtari911 $versions[] = $currentVersion; 18579ccd446eSAtari911 } 18589ccd446eSAtari911 18599ccd446eSAtari911 $totalVersions = count($versions); 18609ccd446eSAtari911 $uniqueId = 'changelog_' . substr(md5(microtime()), 0, 6); 18619ccd446eSAtari911 18629ccd446eSAtari911 if ($totalVersions > 0) { 18639ccd446eSAtari911 // Timeline navigation bar 18649ccd446eSAtari911 echo '<div id="' . $uniqueId . '_wrap" style="position:relative;">'; 18659ccd446eSAtari911 18669ccd446eSAtari911 // Nav controls 18679ccd446eSAtari911 echo '<div style="display:flex; align-items:center; gap:8px; margin-bottom:10px;">'; 18689ccd446eSAtari911 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>'; 18699ccd446eSAtari911 echo '<div style="flex:1; text-align:center;">'; 18709ccd446eSAtari911 echo '<span id="' . $uniqueId . '_counter" style="font-size:11px; color:' . $colors['text'] . '; opacity:0.7;">1 of ' . $totalVersions . '</span>'; 18719ccd446eSAtari911 echo '</div>'; 18729ccd446eSAtari911 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>'; 18739ccd446eSAtari911 echo '</div>'; 18749ccd446eSAtari911 18759ccd446eSAtari911 // Version cards (one per version, only first visible) 18769ccd446eSAtari911 foreach ($versions as $i => $ver) { 18779ccd446eSAtari911 $display = ($i === 0) ? 'block' : 'none'; 18789ccd446eSAtari911 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;">'; 18799ccd446eSAtari911 18809ccd446eSAtari911 // Version header 18819ccd446eSAtari911 echo '<div style="display:flex; align-items:baseline; gap:8px; margin-bottom:8px;">'; 18829ccd446eSAtari911 echo '<span style="font-weight:bold; color:#7b1fa2; font-size:14px;">v' . hsc($ver['number']) . '</span>'; 18839ccd446eSAtari911 if ($ver['date']) { 18849ccd446eSAtari911 echo '<span style="font-size:11px; color:' . $colors['text'] . '; opacity:0.6;">' . hsc($ver['date']) . '</span>'; 18859ccd446eSAtari911 } 18869ccd446eSAtari911 echo '</div>'; 18879ccd446eSAtari911 if ($ver['title']) { 18889ccd446eSAtari911 echo '<div style="font-size:12px; font-weight:600; color:' . $colors['text'] . '; margin-bottom:8px;">' . hsc($ver['title']) . '</div>'; 18899ccd446eSAtari911 } 18909ccd446eSAtari911 18919ccd446eSAtari911 // Change items 18929ccd446eSAtari911 if (!empty($ver['items'])) { 18939ccd446eSAtari911 echo '<div style="font-size:12px; line-height:1.7;">'; 18949ccd446eSAtari911 foreach ($ver['items'] as $item) { 18959ccd446eSAtari911 $color = '#666'; $icon = '•'; 18969ccd446eSAtari911 $t = $item['type']; 18979ccd446eSAtari911 if ($t === 'Added') { $color = '#28a745'; $icon = '✨'; } 18989ccd446eSAtari911 elseif ($t === 'Fixed') { $color = '#dc3545'; $icon = ''; } 18999ccd446eSAtari911 elseif ($t === 'Changed') { $color = '#7b1fa2'; $icon = ''; } 19009ccd446eSAtari911 elseif ($t === 'Improved') { $color = '#ff9800'; $icon = '⚡'; } 19019ccd446eSAtari911 elseif ($t === 'Removed') { $color = '#e91e63'; $icon = '️'; } 19029ccd446eSAtari911 elseif ($t === 'Development' || $t === 'Refactored') { $color = '#6c757d'; $icon = '️'; } 19039ccd446eSAtari911 elseif ($t === 'Result') { $color = '#2196f3'; $icon = '✅'; } 19049ccd446eSAtari911 19059ccd446eSAtari911 echo '<div style="margin:2px 0; padding-left:4px;">'; 19069ccd446eSAtari911 echo '<span style="color:' . $color . '; font-weight:600;">' . $icon . ' ' . hsc($item['type']) . ':</span> '; 19079ccd446eSAtari911 echo '<span style="color:' . $colors['text'] . ';">' . hsc($item['desc']) . '</span>'; 19089ccd446eSAtari911 echo '</div>'; 19099ccd446eSAtari911 } 19109ccd446eSAtari911 echo '</div>'; 19119ccd446eSAtari911 } else { 19129ccd446eSAtari911 echo '<div style="font-size:11px; color:' . $colors['text'] . '; opacity:0.5; font-style:italic;">No detailed changes recorded</div>'; 19139ccd446eSAtari911 } 19149ccd446eSAtari911 19159ccd446eSAtari911 echo '</div>'; 19169ccd446eSAtari911 } 19179ccd446eSAtari911 19189ccd446eSAtari911 echo '</div>'; // wrap 19199ccd446eSAtari911 19209ccd446eSAtari911 // JavaScript for navigation 19219ccd446eSAtari911 echo '<script> 19229ccd446eSAtari911 (function() { 19239ccd446eSAtari911 var id = "' . $uniqueId . '"; 19249ccd446eSAtari911 var total = ' . $totalVersions . '; 19259ccd446eSAtari911 var current = 0; 19269ccd446eSAtari911 19279ccd446eSAtari911 window.changelogNav = function(uid, dir) { 19289ccd446eSAtari911 if (uid !== id) return; 19299ccd446eSAtari911 var next = current + dir; 19309ccd446eSAtari911 if (next < 0 || next >= total) return; 19319ccd446eSAtari911 19329ccd446eSAtari911 // Hide current 19339ccd446eSAtari911 var curCard = document.getElementById(id + "_card_" + current); 19349ccd446eSAtari911 if (curCard) curCard.style.display = "none"; 19359ccd446eSAtari911 19369ccd446eSAtari911 // Show next 19379ccd446eSAtari911 current = next; 19389ccd446eSAtari911 var nextCard = document.getElementById(id + "_card_" + current); 19399ccd446eSAtari911 if (nextCard) nextCard.style.display = "block"; 19409ccd446eSAtari911 19419ccd446eSAtari911 // Update counter 19429ccd446eSAtari911 var counter = document.getElementById(id + "_counter"); 19439ccd446eSAtari911 if (counter) counter.textContent = (current + 1) + " of " + total; 19449ccd446eSAtari911 19459ccd446eSAtari911 // Update button states 19469ccd446eSAtari911 var prevBtn = document.getElementById(id + "_prev"); 19479ccd446eSAtari911 var nextBtn = document.getElementById(id + "_next"); 19489ccd446eSAtari911 if (prevBtn) prevBtn.style.opacity = (current === 0) ? "0.3" : "1"; 19499ccd446eSAtari911 if (nextBtn) nextBtn.style.opacity = (current === total - 1) ? "0.3" : "1"; 19509ccd446eSAtari911 }; 19519ccd446eSAtari911 19529ccd446eSAtari911 // Initialize button states 19539ccd446eSAtari911 var prevBtn = document.getElementById(id + "_prev"); 19549ccd446eSAtari911 if (prevBtn) prevBtn.style.opacity = "0.3"; 19559ccd446eSAtari911 })(); 19569ccd446eSAtari911 </script>'; 19579ccd446eSAtari911 19589ccd446eSAtari911 } else { 19599ccd446eSAtari911 echo '<p style="color:#999; font-size:13px; font-style:italic;">No versions found in changelog</p>'; 19609ccd446eSAtari911 } 19619ccd446eSAtari911 } else { 19629ccd446eSAtari911 echo '<p style="color:#999; font-size:13px; font-style:italic;">Changelog not available</p>'; 19639ccd446eSAtari911 } 19649ccd446eSAtari911 19659ccd446eSAtari911 echo '</div>'; 19669ccd446eSAtari911 19679ccd446eSAtari911 // Backup list or manual backup section 19681d05cddcSAtari911 $backupDir = DOKU_PLUGIN; 19691d05cddcSAtari911 $backups = glob($backupDir . 'calendar*.zip'); 19701d05cddcSAtari911 19711d05cddcSAtari911 // Filter to only show files that look like backups (not the uploaded plugin files) 19721d05cddcSAtari911 $backups = array_filter($backups, function($file) { 19731d05cddcSAtari911 $name = basename($file); 19741d05cddcSAtari911 // Include files that start with "calendar" but exclude files that are just "calendar.zip" (uploaded plugin) 19751d05cddcSAtari911 return $name !== 'calendar.zip'; 19761d05cddcSAtari911 }); 19771d05cddcSAtari911 19789ccd446eSAtari911 // Always show backup section (even if no backups yet) 19799ccd446eSAtari911 echo '<div id="backupSection" style="background:' . $colors['bg'] . '; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">'; 19809ccd446eSAtari911 echo '<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:8px;">'; 19819ccd446eSAtari911 echo '<h3 style="margin:0; color:#00cc07; font-size:16px;"> Backups</h3>'; 19829ccd446eSAtari911 19839ccd446eSAtari911 // Manual backup button 19849ccd446eSAtari911 echo '<form method="post" action="?do=admin&page=calendar&tab=update" style="margin:0;">'; 19859ccd446eSAtari911 echo '<input type="hidden" name="action" value="create_manual_backup">'; 19869ccd446eSAtari911 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>'; 19879ccd446eSAtari911 echo '</form>'; 19889ccd446eSAtari911 echo '</div>'; 19899ccd446eSAtari911 19901d05cddcSAtari911 if (!empty($backups)) { 19911d05cddcSAtari911 rsort($backups); // Newest first 19929ccd446eSAtari911 echo '<div style="max-height:200px; overflow-y:auto; border:1px solid ' . $colors['border'] . '; border-radius:3px; background:' . $colors['bg'] . ';">'; 19939ccd446eSAtari911 echo '<table id="backupTable" style="width:100%; border-collapse:collapse; font-size:12px;">'; 19941d05cddcSAtari911 echo '<thead style="position:sticky; top:0; background:#e9e9e9;">'; 19951d05cddcSAtari911 echo '<tr>'; 19969ccd446eSAtari911 echo '<th style="padding:6px; text-align:left; border-bottom:2px solid ' . $colors['border'] . ';">Backup File</th>'; 19979ccd446eSAtari911 echo '<th style="padding:6px; text-align:left; border-bottom:2px solid ' . $colors['border'] . ';">Size</th>'; 19989ccd446eSAtari911 echo '<th style="padding:6px; text-align:left; border-bottom:2px solid ' . $colors['border'] . ';">Actions</th>'; 19991d05cddcSAtari911 echo '</tr></thead><tbody>'; 20001d05cddcSAtari911 20011d05cddcSAtari911 foreach ($backups as $backup) { 20021d05cddcSAtari911 $filename = basename($backup); 20031d05cddcSAtari911 $size = $this->formatBytes(filesize($backup)); 20041d05cddcSAtari911 echo '<tr style="border-bottom:1px solid #eee;">'; 20051d05cddcSAtari911 echo '<td style="padding:6px;"><code style="font-size:11px;">' . hsc($filename) . '</code></td>'; 20061d05cddcSAtari911 echo '<td style="padding:6px;">' . $size . '</td>'; 20071d05cddcSAtari911 echo '<td style="padding:6px; white-space:nowrap;">'; 20081d05cddcSAtari911 echo '<a href="' . DOKU_BASE . 'lib/plugins/' . hsc($filename) . '" download style="color:#00cc07; text-decoration:none; font-size:11px; margin-right:10px;"> Download</a>'; 20091d05cddcSAtari911 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>'; 20101d05cddcSAtari911 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>'; 20111d05cddcSAtari911 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>'; 20121d05cddcSAtari911 echo '</td>'; 20131d05cddcSAtari911 echo '</tr>'; 20141d05cddcSAtari911 } 20151d05cddcSAtari911 20161d05cddcSAtari911 echo '</tbody></table>'; 20171d05cddcSAtari911 echo '</div>'; 20189ccd446eSAtari911 } else { 20199ccd446eSAtari911 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>'; 20201d05cddcSAtari911 } 20219ccd446eSAtari911 echo '</div>'; 20221d05cddcSAtari911 20231d05cddcSAtari911 echo '<script> 20241d05cddcSAtari911 function confirmUpload() { 20251d05cddcSAtari911 const fileInput = document.querySelector(\'input[name="plugin_zip"]\'); 20261d05cddcSAtari911 if (!fileInput.files[0]) { 20271d05cddcSAtari911 alert("Please select a ZIP file"); 20281d05cddcSAtari911 return false; 20291d05cddcSAtari911 } 20301d05cddcSAtari911 20311d05cddcSAtari911 const fileName = fileInput.files[0].name; 20321d05cddcSAtari911 if (!fileName.endsWith(".zip")) { 20331d05cddcSAtari911 alert("Please select a ZIP file"); 20341d05cddcSAtari911 return false; 20351d05cddcSAtari911 } 20361d05cddcSAtari911 20371d05cddcSAtari911 return confirm("Upload and install: " + fileName + "?\\n\\nThis will replace all plugin files.\\nYour configuration and data will be preserved.\\n\\nContinue?"); 20381d05cddcSAtari911 } 20391d05cddcSAtari911 20401d05cddcSAtari911 function deleteBackup(filename) { 20411d05cddcSAtari911 if (!confirm("Delete backup: " + filename + "?\\n\\nThis cannot be undone!")) { 20421d05cddcSAtari911 return; 20431d05cddcSAtari911 } 20441d05cddcSAtari911 20459ccd446eSAtari911 // Use AJAX to delete without page refresh 20469ccd446eSAtari911 const formData = new FormData(); 20479ccd446eSAtari911 formData.append(\'action\', \'delete_backup\'); 20489ccd446eSAtari911 formData.append(\'backup_file\', filename); 20491d05cddcSAtari911 20509ccd446eSAtari911 fetch(\'?do=admin&page=calendar&tab=update\', { 20519ccd446eSAtari911 method: \'POST\', 20529ccd446eSAtari911 body: formData 20539ccd446eSAtari911 }) 20549ccd446eSAtari911 .then(response => response.text()) 20559ccd446eSAtari911 .then(data => { 20569ccd446eSAtari911 // Remove the row from the table 20579ccd446eSAtari911 const rows = document.querySelectorAll(\'tr\'); 20589ccd446eSAtari911 rows.forEach(row => { 20599ccd446eSAtari911 if (row.textContent.includes(filename)) { 20609ccd446eSAtari911 row.style.transition = \'opacity 0.3s\'; 20619ccd446eSAtari911 row.style.opacity = \'0\'; 20629ccd446eSAtari911 setTimeout(() => { 20639ccd446eSAtari911 row.remove(); 20649ccd446eSAtari911 // Check if table is now empty 20659ccd446eSAtari911 const tbody = document.querySelector(\'#backupTable tbody\'); 20669ccd446eSAtari911 if (tbody && tbody.children.length === 0) { 20679ccd446eSAtari911 const backupSection = document.querySelector(\'#backupSection\'); 20689ccd446eSAtari911 if (backupSection) { 20699ccd446eSAtari911 backupSection.style.transition = \'opacity 0.3s\'; 20709ccd446eSAtari911 backupSection.style.opacity = \'0\'; 20719ccd446eSAtari911 setTimeout(() => backupSection.remove(), 300); 20729ccd446eSAtari911 } 20739ccd446eSAtari911 } 20749ccd446eSAtari911 }, 300); 20759ccd446eSAtari911 } 20769ccd446eSAtari911 }); 20771d05cddcSAtari911 20789ccd446eSAtari911 // Show success message 20799ccd446eSAtari911 const msg = document.createElement(\'div\'); 20809ccd446eSAtari911 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;\'; 20819ccd446eSAtari911 msg.textContent = \'✓ Backup deleted: \' + filename; 20829ccd446eSAtari911 document.querySelector(\'h2\').after(msg); 20839ccd446eSAtari911 setTimeout(() => { 20849ccd446eSAtari911 msg.style.opacity = \'0\'; 20859ccd446eSAtari911 setTimeout(() => msg.remove(), 300); 20869ccd446eSAtari911 }, 3000); 20879ccd446eSAtari911 }) 20889ccd446eSAtari911 .catch(error => { 20899ccd446eSAtari911 alert(\'Error deleting backup: \' + error); 20909ccd446eSAtari911 }); 20911d05cddcSAtari911 } 20921d05cddcSAtari911 20931d05cddcSAtari911 function restoreBackup(filename) { 20941d05cddcSAtari911 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?")) { 20951d05cddcSAtari911 return; 20961d05cddcSAtari911 } 20971d05cddcSAtari911 20981d05cddcSAtari911 const form = document.createElement("form"); 20991d05cddcSAtari911 form.method = "POST"; 21001d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=update"; 21011d05cddcSAtari911 21021d05cddcSAtari911 const actionInput = document.createElement("input"); 21031d05cddcSAtari911 actionInput.type = "hidden"; 21041d05cddcSAtari911 actionInput.name = "action"; 21051d05cddcSAtari911 actionInput.value = "restore_backup"; 21061d05cddcSAtari911 form.appendChild(actionInput); 21071d05cddcSAtari911 21081d05cddcSAtari911 const filenameInput = document.createElement("input"); 21091d05cddcSAtari911 filenameInput.type = "hidden"; 21101d05cddcSAtari911 filenameInput.name = "backup_file"; 21111d05cddcSAtari911 filenameInput.value = filename; 21121d05cddcSAtari911 form.appendChild(filenameInput); 21131d05cddcSAtari911 21141d05cddcSAtari911 document.body.appendChild(form); 21151d05cddcSAtari911 form.submit(); 21161d05cddcSAtari911 } 21171d05cddcSAtari911 21181d05cddcSAtari911 function renameBackup(filename) { 21191d05cddcSAtari911 const newName = prompt("Enter new backup name (without .zip extension):\\n\\nCurrent: " + filename.replace(/\\.zip$/, ""), filename.replace(/\\.zip$/, "")); 21201d05cddcSAtari911 if (!newName || newName === filename.replace(/\\.zip$/, "")) { 21211d05cddcSAtari911 return; 21221d05cddcSAtari911 } 21231d05cddcSAtari911 21241d05cddcSAtari911 // Add .zip if not present 21251d05cddcSAtari911 const newFilename = newName.endsWith(".zip") ? newName : newName + ".zip"; 21261d05cddcSAtari911 21271d05cddcSAtari911 // Basic validation 21281d05cddcSAtari911 if (!/^[a-zA-Z0-9._-]+$/.test(newFilename.replace(/\\.zip$/, ""))) { 21291d05cddcSAtari911 alert("Invalid filename. Use only letters, numbers, dots, dashes, and underscores."); 21301d05cddcSAtari911 return; 21311d05cddcSAtari911 } 21321d05cddcSAtari911 21331d05cddcSAtari911 const form = document.createElement("form"); 21341d05cddcSAtari911 form.method = "POST"; 21351d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=update"; 21361d05cddcSAtari911 21371d05cddcSAtari911 const actionInput = document.createElement("input"); 21381d05cddcSAtari911 actionInput.type = "hidden"; 21391d05cddcSAtari911 actionInput.name = "action"; 21401d05cddcSAtari911 actionInput.value = "rename_backup"; 21411d05cddcSAtari911 form.appendChild(actionInput); 21421d05cddcSAtari911 21431d05cddcSAtari911 const oldNameInput = document.createElement("input"); 21441d05cddcSAtari911 oldNameInput.type = "hidden"; 21451d05cddcSAtari911 oldNameInput.name = "old_name"; 21461d05cddcSAtari911 oldNameInput.value = filename; 21471d05cddcSAtari911 form.appendChild(oldNameInput); 21481d05cddcSAtari911 21491d05cddcSAtari911 const newNameInput = document.createElement("input"); 21501d05cddcSAtari911 newNameInput.type = "hidden"; 21511d05cddcSAtari911 newNameInput.name = "new_name"; 21521d05cddcSAtari911 newNameInput.value = newFilename; 21531d05cddcSAtari911 form.appendChild(newNameInput); 21541d05cddcSAtari911 21551d05cddcSAtari911 document.body.appendChild(form); 21561d05cddcSAtari911 form.submit(); 21571d05cddcSAtari911 } 21581d05cddcSAtari911 </script>'; 21591d05cddcSAtari911 } 21601d05cddcSAtari911 21611d05cddcSAtari911 private function saveConfig() { 21621d05cddcSAtari911 global $INPUT; 21631d05cddcSAtari911 21641d05cddcSAtari911 // Load existing config to preserve all settings 21651d05cddcSAtari911 $configFile = DOKU_PLUGIN . 'calendar/sync_config.php'; 21661d05cddcSAtari911 $existingConfig = []; 21671d05cddcSAtari911 if (file_exists($configFile)) { 21681d05cddcSAtari911 $existingConfig = include $configFile; 21691d05cddcSAtari911 } 21701d05cddcSAtari911 21711d05cddcSAtari911 // Update only the fields from the form - preserve everything else 21721d05cddcSAtari911 $config = $existingConfig; 21731d05cddcSAtari911 21741d05cddcSAtari911 // Update basic fields 21751d05cddcSAtari911 $config['tenant_id'] = $INPUT->str('tenant_id'); 21761d05cddcSAtari911 $config['client_id'] = $INPUT->str('client_id'); 21771d05cddcSAtari911 $config['client_secret'] = $INPUT->str('client_secret'); 21781d05cddcSAtari911 $config['user_email'] = $INPUT->str('user_email'); 21791d05cddcSAtari911 $config['timezone'] = $INPUT->str('timezone', 'America/Los_Angeles'); 21801d05cddcSAtari911 $config['default_category'] = $INPUT->str('default_category', 'Blue category'); 21811d05cddcSAtari911 $config['reminder_minutes'] = $INPUT->int('reminder_minutes', 15); 21821d05cddcSAtari911 $config['sync_completed_tasks'] = $INPUT->bool('sync_completed_tasks'); 21831d05cddcSAtari911 $config['delete_outlook_events'] = $INPUT->bool('delete_outlook_events'); 21841d05cddcSAtari911 $config['sync_all_namespaces'] = $INPUT->bool('sync_all_namespaces'); 21851d05cddcSAtari911 $config['sync_namespaces'] = $INPUT->arr('sync_namespaces'); 2186*4590242dSAtari911 // important_namespaces is managed from the Manage tab, preserve existing value 2187*4590242dSAtari911 if (!isset($config['important_namespaces'])) { 2188*4590242dSAtari911 $config['important_namespaces'] = 'important'; 2189*4590242dSAtari911 } 21901d05cddcSAtari911 21911d05cddcSAtari911 // Parse category mapping 21921d05cddcSAtari911 $config['category_mapping'] = []; 21931d05cddcSAtari911 $mappingText = $INPUT->str('category_mapping'); 21941d05cddcSAtari911 if ($mappingText) { 21951d05cddcSAtari911 $lines = explode("\n", $mappingText); 21961d05cddcSAtari911 foreach ($lines as $line) { 21971d05cddcSAtari911 $line = trim($line); 21981d05cddcSAtari911 if (empty($line)) continue; 21991d05cddcSAtari911 $parts = explode('=', $line, 2); 22001d05cddcSAtari911 if (count($parts) === 2) { 22011d05cddcSAtari911 $config['category_mapping'][trim($parts[0])] = trim($parts[1]); 22021d05cddcSAtari911 } 22031d05cddcSAtari911 } 22041d05cddcSAtari911 } 22051d05cddcSAtari911 22061d05cddcSAtari911 // Parse color mapping from dropdown selections 22071d05cddcSAtari911 $config['color_mapping'] = []; 22081d05cddcSAtari911 $colorMappingCount = $INPUT->int('color_mapping_count', 0); 22091d05cddcSAtari911 for ($i = 0; $i < $colorMappingCount; $i++) { 22101d05cddcSAtari911 $hexColor = $INPUT->str('color_hex_' . $i); 22111d05cddcSAtari911 $category = $INPUT->str('color_map_' . $i); 22121d05cddcSAtari911 22131d05cddcSAtari911 if (!empty($hexColor) && !empty($category)) { 22141d05cddcSAtari911 $config['color_mapping'][$hexColor] = $category; 22151d05cddcSAtari911 } 22161d05cddcSAtari911 } 22171d05cddcSAtari911 22181d05cddcSAtari911 // Build file content using return format 22191d05cddcSAtari911 $content = "<?php\n"; 22201d05cddcSAtari911 $content .= "/**\n"; 22211d05cddcSAtari911 $content .= " * DokuWiki Calendar → Outlook Sync - Configuration\n"; 22221d05cddcSAtari911 $content .= " * \n"; 22231d05cddcSAtari911 $content .= " * SECURITY: Add this file to .gitignore!\n"; 22241d05cddcSAtari911 $content .= " * Never commit credentials to version control.\n"; 22251d05cddcSAtari911 $content .= " */\n\n"; 22261d05cddcSAtari911 $content .= "return " . var_export($config, true) . ";\n"; 22271d05cddcSAtari911 22281d05cddcSAtari911 // Save file 22291d05cddcSAtari911 if (file_put_contents($configFile, $content)) { 22301d05cddcSAtari911 $this->redirect('Configuration saved successfully!', 'success'); 22311d05cddcSAtari911 } else { 22321d05cddcSAtari911 $this->redirect('Error: Could not save configuration file', 'error'); 22331d05cddcSAtari911 } 22341d05cddcSAtari911 } 22351d05cddcSAtari911 22361d05cddcSAtari911 private function clearCache() { 22371d05cddcSAtari911 // Clear DokuWiki cache 22381d05cddcSAtari911 $cacheDir = DOKU_INC . 'data/cache'; 22391d05cddcSAtari911 22401d05cddcSAtari911 if (is_dir($cacheDir)) { 22411d05cddcSAtari911 $this->recursiveDelete($cacheDir, false); 22421d05cddcSAtari911 $this->redirect('Cache cleared successfully!', 'success', 'update'); 22431d05cddcSAtari911 } else { 22441d05cddcSAtari911 $this->redirect('Cache directory not found', 'error', 'update'); 22451d05cddcSAtari911 } 22461d05cddcSAtari911 } 22471d05cddcSAtari911 22481d05cddcSAtari911 private function recursiveDelete($dir, $deleteRoot = true) { 22491d05cddcSAtari911 if (!is_dir($dir)) return; 22501d05cddcSAtari911 22511d05cddcSAtari911 $files = array_diff(scandir($dir), array('.', '..')); 22521d05cddcSAtari911 foreach ($files as $file) { 22531d05cddcSAtari911 $path = $dir . '/' . $file; 22541d05cddcSAtari911 if (is_dir($path)) { 22551d05cddcSAtari911 $this->recursiveDelete($path, true); 22561d05cddcSAtari911 } else { 22571d05cddcSAtari911 @unlink($path); 22581d05cddcSAtari911 } 22591d05cddcSAtari911 } 22601d05cddcSAtari911 22611d05cddcSAtari911 if ($deleteRoot) { 22621d05cddcSAtari911 @rmdir($dir); 22631d05cddcSAtari911 } 22641d05cddcSAtari911 } 22651d05cddcSAtari911 22661d05cddcSAtari911 private function findRecurringEvents() { 22671d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 22681d05cddcSAtari911 $recurring = []; 22691d05cddcSAtari911 $allEvents = []; // Track all events to detect patterns 22701d05cddcSAtari911 22711d05cddcSAtari911 // Check root calendar directory first (blank/default namespace) 22721d05cddcSAtari911 $rootCalendarDir = $dataDir . 'calendar'; 22731d05cddcSAtari911 if (is_dir($rootCalendarDir)) { 22741d05cddcSAtari911 foreach (glob($rootCalendarDir . '/*.json') as $file) { 22751d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 22761d05cddcSAtari911 if (!$data) continue; 22771d05cddcSAtari911 22781d05cddcSAtari911 foreach ($data as $dateKey => $events) { 22791d05cddcSAtari911 foreach ($events as $event) { 22801d05cddcSAtari911 // Group by title + namespace (events with same title are likely recurring) 22811d05cddcSAtari911 $groupKey = strtolower(trim($event['title'])) . '_'; 22821d05cddcSAtari911 22831d05cddcSAtari911 if (!isset($allEvents[$groupKey])) { 22841d05cddcSAtari911 $allEvents[$groupKey] = [ 22851d05cddcSAtari911 'title' => $event['title'], 22861d05cddcSAtari911 'namespace' => '', 22871d05cddcSAtari911 'dates' => [], 22881d05cddcSAtari911 'events' => [] 22891d05cddcSAtari911 ]; 22901d05cddcSAtari911 } 22911d05cddcSAtari911 $allEvents[$groupKey]['dates'][] = $dateKey; 22921d05cddcSAtari911 $allEvents[$groupKey]['events'][] = $event; 22931d05cddcSAtari911 } 22941d05cddcSAtari911 } 22951d05cddcSAtari911 } 22961d05cddcSAtari911 } 22971d05cddcSAtari911 22981d05cddcSAtari911 // Scan all namespace directories 22991d05cddcSAtari911 foreach (glob($dataDir . '*', GLOB_ONLYDIR) as $nsDir) { 23001d05cddcSAtari911 $namespace = basename($nsDir); 23011d05cddcSAtari911 23021d05cddcSAtari911 // Skip the root 'calendar' dir (already processed above) 23031d05cddcSAtari911 if ($namespace === 'calendar') continue; 23041d05cddcSAtari911 23051d05cddcSAtari911 $calendarDir = $nsDir . '/calendar'; 23061d05cddcSAtari911 23071d05cddcSAtari911 if (!is_dir($calendarDir)) continue; 23081d05cddcSAtari911 23091d05cddcSAtari911 // Scan all calendar files 23101d05cddcSAtari911 foreach (glob($calendarDir . '/*.json') as $file) { 23111d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 23121d05cddcSAtari911 if (!$data) continue; 23131d05cddcSAtari911 23141d05cddcSAtari911 foreach ($data as $dateKey => $events) { 23151d05cddcSAtari911 foreach ($events as $event) { 23161d05cddcSAtari911 $groupKey = strtolower(trim($event['title'])) . '_' . ($event['namespace'] ?? ''); 23171d05cddcSAtari911 23181d05cddcSAtari911 if (!isset($allEvents[$groupKey])) { 23191d05cddcSAtari911 $allEvents[$groupKey] = [ 23201d05cddcSAtari911 'title' => $event['title'], 23211d05cddcSAtari911 'namespace' => $event['namespace'] ?? '', 23221d05cddcSAtari911 'dates' => [], 23231d05cddcSAtari911 'events' => [] 23241d05cddcSAtari911 ]; 23251d05cddcSAtari911 } 23261d05cddcSAtari911 $allEvents[$groupKey]['dates'][] = $dateKey; 23271d05cddcSAtari911 $allEvents[$groupKey]['events'][] = $event; 23281d05cddcSAtari911 } 23291d05cddcSAtari911 } 23301d05cddcSAtari911 } 23311d05cddcSAtari911 } 23321d05cddcSAtari911 23331d05cddcSAtari911 // Analyze patterns - only include if 3+ occurrences 23341d05cddcSAtari911 foreach ($allEvents as $groupKey => $group) { 23351d05cddcSAtari911 if (count($group['dates']) >= 3) { 23361d05cddcSAtari911 // Sort dates 23371d05cddcSAtari911 sort($group['dates']); 23381d05cddcSAtari911 23391d05cddcSAtari911 // Calculate interval between first and second occurrence 23401d05cddcSAtari911 $date1 = new DateTime($group['dates'][0]); 23411d05cddcSAtari911 $date2 = new DateTime($group['dates'][1]); 23421d05cddcSAtari911 $interval = $date1->diff($date2); 23431d05cddcSAtari911 23441d05cddcSAtari911 // Determine pattern 23451d05cddcSAtari911 $pattern = 'Custom'; 23461d05cddcSAtari911 if ($interval->days == 1) { 23471d05cddcSAtari911 $pattern = 'Daily'; 23481d05cddcSAtari911 } elseif ($interval->days == 7) { 23491d05cddcSAtari911 $pattern = 'Weekly'; 23501d05cddcSAtari911 } elseif ($interval->days >= 14 && $interval->days <= 16) { 23511d05cddcSAtari911 $pattern = 'Bi-weekly'; 23521d05cddcSAtari911 } elseif ($interval->days >= 28 && $interval->days <= 31) { 23531d05cddcSAtari911 $pattern = 'Monthly'; 23541d05cddcSAtari911 } elseif ($interval->days >= 365 && $interval->days <= 366) { 23551d05cddcSAtari911 $pattern = 'Yearly'; 23561d05cddcSAtari911 } 23571d05cddcSAtari911 23581d05cddcSAtari911 // Use first event's ID or create a synthetic one 23591d05cddcSAtari911 $baseId = isset($group['events'][0]['recurringId']) 23601d05cddcSAtari911 ? $group['events'][0]['recurringId'] 23611d05cddcSAtari911 : md5($group['title'] . $group['namespace']); 23621d05cddcSAtari911 23631d05cddcSAtari911 $recurring[] = [ 23641d05cddcSAtari911 'baseId' => $baseId, 23651d05cddcSAtari911 'title' => $group['title'], 23661d05cddcSAtari911 'namespace' => $group['namespace'], 23671d05cddcSAtari911 'pattern' => $pattern, 23681d05cddcSAtari911 'count' => count($group['dates']), 23691d05cddcSAtari911 'firstDate' => $group['dates'][0], 23701d05cddcSAtari911 'interval' => $interval->days 23711d05cddcSAtari911 ]; 23721d05cddcSAtari911 } 23731d05cddcSAtari911 } 23741d05cddcSAtari911 23751d05cddcSAtari911 return $recurring; 23761d05cddcSAtari911 } 23771d05cddcSAtari911 23781d05cddcSAtari911 private function getEventsByNamespace() { 23791d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 23801d05cddcSAtari911 $result = []; 23811d05cddcSAtari911 23821d05cddcSAtari911 // Check root calendar directory first (blank/default namespace) 23831d05cddcSAtari911 $rootCalendarDir = $dataDir . 'calendar'; 23841d05cddcSAtari911 if (is_dir($rootCalendarDir)) { 23851d05cddcSAtari911 $hasFiles = false; 23861d05cddcSAtari911 $events = []; 23871d05cddcSAtari911 23881d05cddcSAtari911 foreach (glob($rootCalendarDir . '/*.json') as $file) { 23891d05cddcSAtari911 $hasFiles = true; 23901d05cddcSAtari911 $month = basename($file, '.json'); 23911d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 23921d05cddcSAtari911 if (!$data) continue; 23931d05cddcSAtari911 23941d05cddcSAtari911 foreach ($data as $dateKey => $eventList) { 23951d05cddcSAtari911 foreach ($eventList as $event) { 23961d05cddcSAtari911 $events[] = [ 23971d05cddcSAtari911 'id' => $event['id'], 23981d05cddcSAtari911 'title' => $event['title'], 23991d05cddcSAtari911 'date' => $dateKey, 24001d05cddcSAtari911 'startTime' => $event['startTime'] ?? null, 24011d05cddcSAtari911 'month' => $month 24021d05cddcSAtari911 ]; 24031d05cddcSAtari911 } 24041d05cddcSAtari911 } 24051d05cddcSAtari911 } 24061d05cddcSAtari911 24071d05cddcSAtari911 // Add if it has JSON files (even if empty) 24081d05cddcSAtari911 if ($hasFiles) { 24091d05cddcSAtari911 $result[''] = ['events' => $events]; 24101d05cddcSAtari911 } 24111d05cddcSAtari911 } 24121d05cddcSAtari911 24131d05cddcSAtari911 // Recursively scan all namespace directories including sub-namespaces 24141d05cddcSAtari911 $this->scanNamespaceRecursive($dataDir, '', $result); 24151d05cddcSAtari911 24161d05cddcSAtari911 // Sort namespaces, but keep '' (default) first 24171d05cddcSAtari911 uksort($result, function($a, $b) { 24181d05cddcSAtari911 if ($a === '') return -1; 24191d05cddcSAtari911 if ($b === '') return 1; 24201d05cddcSAtari911 return strcmp($a, $b); 24211d05cddcSAtari911 }); 24221d05cddcSAtari911 24231d05cddcSAtari911 return $result; 24241d05cddcSAtari911 } 24251d05cddcSAtari911 24261d05cddcSAtari911 private function scanNamespaceRecursive($baseDir, $parentNamespace, &$result) { 24271d05cddcSAtari911 foreach (glob($baseDir . '*', GLOB_ONLYDIR) as $nsDir) { 24281d05cddcSAtari911 $dirName = basename($nsDir); 24291d05cddcSAtari911 24301d05cddcSAtari911 // Skip the root 'calendar' dir 24311d05cddcSAtari911 if ($dirName === 'calendar' && empty($parentNamespace)) continue; 24321d05cddcSAtari911 24331d05cddcSAtari911 // Build namespace path 24341d05cddcSAtari911 $namespace = empty($parentNamespace) ? $dirName : $parentNamespace . ':' . $dirName; 24351d05cddcSAtari911 24361d05cddcSAtari911 // Check for calendar directory 24371d05cddcSAtari911 $calendarDir = $nsDir . '/calendar'; 24381d05cddcSAtari911 if (is_dir($calendarDir)) { 24391d05cddcSAtari911 $hasFiles = false; 24401d05cddcSAtari911 $events = []; 24411d05cddcSAtari911 24421d05cddcSAtari911 // Scan all calendar files 24431d05cddcSAtari911 foreach (glob($calendarDir . '/*.json') as $file) { 24441d05cddcSAtari911 $hasFiles = true; 24451d05cddcSAtari911 $month = basename($file, '.json'); 24461d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 24471d05cddcSAtari911 if (!$data) continue; 24481d05cddcSAtari911 24491d05cddcSAtari911 foreach ($data as $dateKey => $eventList) { 24501d05cddcSAtari911 foreach ($eventList as $event) { 24511d05cddcSAtari911 $events[] = [ 24521d05cddcSAtari911 'id' => $event['id'], 24531d05cddcSAtari911 'title' => $event['title'], 24541d05cddcSAtari911 'date' => $dateKey, 24551d05cddcSAtari911 'startTime' => $event['startTime'] ?? null, 24561d05cddcSAtari911 'month' => $month 24571d05cddcSAtari911 ]; 24581d05cddcSAtari911 } 24591d05cddcSAtari911 } 24601d05cddcSAtari911 } 24611d05cddcSAtari911 24621d05cddcSAtari911 // Add namespace if it has JSON files (even if empty) 24631d05cddcSAtari911 if ($hasFiles) { 24641d05cddcSAtari911 $result[$namespace] = ['events' => $events]; 24651d05cddcSAtari911 } 24661d05cddcSAtari911 } 24671d05cddcSAtari911 24681d05cddcSAtari911 // Recursively scan sub-directories 24691d05cddcSAtari911 $this->scanNamespaceRecursive($nsDir . '/', $namespace, $result); 24701d05cddcSAtari911 } 24711d05cddcSAtari911 } 24721d05cddcSAtari911 24731d05cddcSAtari911 private function getAllNamespaces() { 24741d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 24751d05cddcSAtari911 $namespaces = []; 24761d05cddcSAtari911 24771d05cddcSAtari911 // Check root calendar directory first 24781d05cddcSAtari911 $rootCalendarDir = $dataDir . 'calendar'; 24791d05cddcSAtari911 if (is_dir($rootCalendarDir)) { 24801d05cddcSAtari911 $namespaces[] = ''; // Blank/default namespace 24811d05cddcSAtari911 } 24821d05cddcSAtari911 24831d05cddcSAtari911 // Check all other namespace directories 24841d05cddcSAtari911 foreach (glob($dataDir . '*', GLOB_ONLYDIR) as $nsDir) { 24851d05cddcSAtari911 $namespace = basename($nsDir); 24861d05cddcSAtari911 24871d05cddcSAtari911 // Skip the root 'calendar' dir (already added as '') 24881d05cddcSAtari911 if ($namespace === 'calendar') continue; 24891d05cddcSAtari911 24901d05cddcSAtari911 $calendarDir = $nsDir . '/calendar'; 24911d05cddcSAtari911 if (is_dir($calendarDir)) { 24921d05cddcSAtari911 $namespaces[] = $namespace; 24931d05cddcSAtari911 } 24941d05cddcSAtari911 } 24951d05cddcSAtari911 24961d05cddcSAtari911 return $namespaces; 24971d05cddcSAtari911 } 24981d05cddcSAtari911 24991d05cddcSAtari911 private function searchEvents($search, $filterNamespace) { 25001d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 25011d05cddcSAtari911 $results = []; 25021d05cddcSAtari911 25031d05cddcSAtari911 $search = strtolower(trim($search)); 25041d05cddcSAtari911 25051d05cddcSAtari911 foreach (glob($dataDir . '*', GLOB_ONLYDIR) as $nsDir) { 25061d05cddcSAtari911 $namespace = basename($nsDir); 25071d05cddcSAtari911 $calendarDir = $nsDir . '/calendar'; 25081d05cddcSAtari911 25091d05cddcSAtari911 if (!is_dir($calendarDir)) continue; 25101d05cddcSAtari911 if ($filterNamespace !== '' && $namespace !== $filterNamespace) continue; 25111d05cddcSAtari911 25121d05cddcSAtari911 foreach (glob($calendarDir . '/*.json') as $file) { 25131d05cddcSAtari911 $month = basename($file, '.json'); 25141d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 25151d05cddcSAtari911 if (!$data) continue; 25161d05cddcSAtari911 25171d05cddcSAtari911 foreach ($data as $dateKey => $events) { 25181d05cddcSAtari911 foreach ($events as $event) { 25191d05cddcSAtari911 if ($search === '' || strpos(strtolower($event['title']), $search) !== false) { 25201d05cddcSAtari911 $results[] = [ 25211d05cddcSAtari911 'id' => $event['id'], 25221d05cddcSAtari911 'title' => $event['title'], 25231d05cddcSAtari911 'date' => $dateKey, 25241d05cddcSAtari911 'startTime' => $event['startTime'] ?? null, 25251d05cddcSAtari911 'namespace' => $event['namespace'] ?? '', 25261d05cddcSAtari911 'month' => $month 25271d05cddcSAtari911 ]; 25281d05cddcSAtari911 } 25291d05cddcSAtari911 } 25301d05cddcSAtari911 } 25311d05cddcSAtari911 } 25321d05cddcSAtari911 } 25331d05cddcSAtari911 25341d05cddcSAtari911 return $results; 25351d05cddcSAtari911 } 25361d05cddcSAtari911 25371d05cddcSAtari911 private function deleteRecurringSeries() { 25381d05cddcSAtari911 global $INPUT; 25391d05cddcSAtari911 25401d05cddcSAtari911 $eventTitle = $INPUT->str('event_title'); 25411d05cddcSAtari911 $namespace = $INPUT->str('namespace'); 25421d05cddcSAtari911 25431d05cddcSAtari911 // Determine calendar directory 25441d05cddcSAtari911 if ($namespace === '') { 25451d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/calendar'; 25461d05cddcSAtari911 } else { 25471d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/' . $namespace . '/calendar'; 25481d05cddcSAtari911 } 25491d05cddcSAtari911 25501d05cddcSAtari911 $count = 0; 25511d05cddcSAtari911 25521d05cddcSAtari911 if (is_dir($dataDir)) { 25531d05cddcSAtari911 foreach (glob($dataDir . '/*.json') as $file) { 25541d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 25551d05cddcSAtari911 if (!$data) continue; 25561d05cddcSAtari911 25571d05cddcSAtari911 $modified = false; 25581d05cddcSAtari911 foreach ($data as $dateKey => $events) { 25591d05cddcSAtari911 $filtered = []; 25601d05cddcSAtari911 foreach ($events as $event) { 25611d05cddcSAtari911 // Match by title (case-insensitive) 25621d05cddcSAtari911 if (strtolower(trim($event['title'])) === strtolower(trim($eventTitle))) { 25631d05cddcSAtari911 $count++; 25641d05cddcSAtari911 $modified = true; 25651d05cddcSAtari911 } else { 25661d05cddcSAtari911 $filtered[] = $event; 25671d05cddcSAtari911 } 25681d05cddcSAtari911 } 25691d05cddcSAtari911 $data[$dateKey] = $filtered; 25701d05cddcSAtari911 } 25711d05cddcSAtari911 25721d05cddcSAtari911 if ($modified) { 25739ccd446eSAtari911 // Clean up empty date keys 25749ccd446eSAtari911 foreach ($data as $dk => $evts) { 25759ccd446eSAtari911 if (empty($evts)) unset($data[$dk]); 25769ccd446eSAtari911 } 25779ccd446eSAtari911 25789ccd446eSAtari911 if (empty($data)) { 25799ccd446eSAtari911 unlink($file); 25809ccd446eSAtari911 } else { 25811d05cddcSAtari911 file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); 25821d05cddcSAtari911 } 25831d05cddcSAtari911 } 25841d05cddcSAtari911 } 25859ccd446eSAtari911 } 25861d05cddcSAtari911 25879ccd446eSAtari911 $this->clearStatsCache(); 25881d05cddcSAtari911 $this->redirect("Deleted $count occurrences of recurring event: " . $eventTitle, 'success', 'manage'); 25891d05cddcSAtari911 } 25901d05cddcSAtari911 25911d05cddcSAtari911 private function editRecurringSeries() { 25921d05cddcSAtari911 global $INPUT; 25931d05cddcSAtari911 25941d05cddcSAtari911 $oldTitle = $INPUT->str('old_title'); 25951d05cddcSAtari911 $oldNamespace = $INPUT->str('old_namespace'); 25961d05cddcSAtari911 $newTitle = $INPUT->str('new_title'); 25971d05cddcSAtari911 $startTime = $INPUT->str('start_time'); 25981d05cddcSAtari911 $endTime = $INPUT->str('end_time'); 25991d05cddcSAtari911 $interval = $INPUT->int('interval', 0); 26001d05cddcSAtari911 $newNamespace = $INPUT->str('new_namespace'); 26011d05cddcSAtari911 26021d05cddcSAtari911 // Use old namespace if new namespace is empty (keep current) 26031d05cddcSAtari911 if (empty($newNamespace) && !isset($_POST['new_namespace'])) { 26041d05cddcSAtari911 $newNamespace = $oldNamespace; 26051d05cddcSAtari911 } 26061d05cddcSAtari911 26071d05cddcSAtari911 // Determine old calendar directory 26081d05cddcSAtari911 if ($oldNamespace === '') { 26091d05cddcSAtari911 $oldDataDir = DOKU_INC . 'data/meta/calendar'; 26101d05cddcSAtari911 } else { 26111d05cddcSAtari911 $oldDataDir = DOKU_INC . 'data/meta/' . $oldNamespace . '/calendar'; 26121d05cddcSAtari911 } 26131d05cddcSAtari911 26141d05cddcSAtari911 $count = 0; 26151d05cddcSAtari911 $eventsToMove = []; 26169ccd446eSAtari911 $firstEventDate = null; 26171d05cddcSAtari911 26181d05cddcSAtari911 if (is_dir($oldDataDir)) { 26191d05cddcSAtari911 foreach (glob($oldDataDir . '/*.json') as $file) { 26201d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 26211d05cddcSAtari911 if (!$data) continue; 26221d05cddcSAtari911 26231d05cddcSAtari911 $modified = false; 26241d05cddcSAtari911 foreach ($data as $dateKey => $events) { 26251d05cddcSAtari911 foreach ($events as $key => $event) { 26261d05cddcSAtari911 // Match by old title (case-insensitive) 26271d05cddcSAtari911 if (strtolower(trim($event['title'])) === strtolower(trim($oldTitle))) { 26281d05cddcSAtari911 // Update the title 26291d05cddcSAtari911 $data[$dateKey][$key]['title'] = $newTitle; 26301d05cddcSAtari911 26311d05cddcSAtari911 // Update start time if provided 26321d05cddcSAtari911 if (!empty($startTime)) { 26339ccd446eSAtari911 $data[$dateKey][$key]['time'] = $startTime; 26341d05cddcSAtari911 } 26351d05cddcSAtari911 26361d05cddcSAtari911 // Update end time if provided 26371d05cddcSAtari911 if (!empty($endTime)) { 26389ccd446eSAtari911 $data[$dateKey][$key]['endTime'] = $endTime; 26391d05cddcSAtari911 } 26401d05cddcSAtari911 26411d05cddcSAtari911 // Update namespace 26421d05cddcSAtari911 $data[$dateKey][$key]['namespace'] = $newNamespace; 26431d05cddcSAtari911 26441d05cddcSAtari911 // If changing interval, calculate new date 26451d05cddcSAtari911 if ($interval > 0 && $count > 0) { 26461d05cddcSAtari911 // Get the first event date as base 26471d05cddcSAtari911 if (empty($firstEventDate)) { 26481d05cddcSAtari911 $firstEventDate = $dateKey; 26491d05cddcSAtari911 } 26501d05cddcSAtari911 26511d05cddcSAtari911 // Calculate new date based on interval 26521d05cddcSAtari911 $newDate = date('Y-m-d', strtotime($firstEventDate . ' +' . ($count * $interval) . ' days')); 26531d05cddcSAtari911 26541d05cddcSAtari911 // Store for moving 26551d05cddcSAtari911 $eventsToMove[] = [ 26561d05cddcSAtari911 'oldDate' => $dateKey, 26571d05cddcSAtari911 'newDate' => $newDate, 26581d05cddcSAtari911 'event' => $data[$dateKey][$key], 26591d05cddcSAtari911 'key' => $key 26601d05cddcSAtari911 ]; 26611d05cddcSAtari911 } 26621d05cddcSAtari911 26631d05cddcSAtari911 $count++; 26641d05cddcSAtari911 $modified = true; 26651d05cddcSAtari911 } 26661d05cddcSAtari911 } 26671d05cddcSAtari911 } 26681d05cddcSAtari911 26691d05cddcSAtari911 if ($modified) { 26701d05cddcSAtari911 file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); 26711d05cddcSAtari911 } 26721d05cddcSAtari911 } 26731d05cddcSAtari911 26741d05cddcSAtari911 // Handle interval changes by moving events to new dates 26751d05cddcSAtari911 if (!empty($eventsToMove)) { 26761d05cddcSAtari911 // Remove from old dates first 26771d05cddcSAtari911 foreach (glob($oldDataDir . '/*.json') as $file) { 26781d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 26791d05cddcSAtari911 if (!$data) continue; 26801d05cddcSAtari911 26811d05cddcSAtari911 $modified = false; 26821d05cddcSAtari911 foreach ($eventsToMove as $moveData) { 26831d05cddcSAtari911 $oldMonth = substr($moveData['oldDate'], 0, 7); 26841d05cddcSAtari911 $fileMonth = basename($file, '.json'); 26851d05cddcSAtari911 26861d05cddcSAtari911 if ($oldMonth === $fileMonth && isset($data[$moveData['oldDate']])) { 26871d05cddcSAtari911 foreach ($data[$moveData['oldDate']] as $k => $evt) { 26881d05cddcSAtari911 if ($evt['id'] === $moveData['event']['id']) { 26891d05cddcSAtari911 unset($data[$moveData['oldDate']][$k]); 26901d05cddcSAtari911 $data[$moveData['oldDate']] = array_values($data[$moveData['oldDate']]); 26911d05cddcSAtari911 $modified = true; 26921d05cddcSAtari911 } 26931d05cddcSAtari911 } 26941d05cddcSAtari911 } 26951d05cddcSAtari911 } 26961d05cddcSAtari911 26971d05cddcSAtari911 if ($modified) { 26981d05cddcSAtari911 file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); 26991d05cddcSAtari911 } 27001d05cddcSAtari911 } 27011d05cddcSAtari911 27021d05cddcSAtari911 // Add to new dates 27031d05cddcSAtari911 foreach ($eventsToMove as $moveData) { 27041d05cddcSAtari911 $newMonth = substr($moveData['newDate'], 0, 7); 27051d05cddcSAtari911 $targetDir = ($newNamespace === '') ? DOKU_INC . 'data/meta/calendar' : DOKU_INC . 'data/meta/' . $newNamespace . '/calendar'; 27061d05cddcSAtari911 27071d05cddcSAtari911 if (!is_dir($targetDir)) { 27081d05cddcSAtari911 mkdir($targetDir, 0755, true); 27091d05cddcSAtari911 } 27101d05cddcSAtari911 27111d05cddcSAtari911 $targetFile = $targetDir . '/' . $newMonth . '.json'; 27121d05cddcSAtari911 $targetData = file_exists($targetFile) ? json_decode(file_get_contents($targetFile), true) : []; 27131d05cddcSAtari911 27141d05cddcSAtari911 if (!isset($targetData[$moveData['newDate']])) { 27151d05cddcSAtari911 $targetData[$moveData['newDate']] = []; 27161d05cddcSAtari911 } 27171d05cddcSAtari911 27181d05cddcSAtari911 $targetData[$moveData['newDate']][] = $moveData['event']; 27191d05cddcSAtari911 file_put_contents($targetFile, json_encode($targetData, JSON_PRETTY_PRINT)); 27201d05cddcSAtari911 } 27211d05cddcSAtari911 } 27221d05cddcSAtari911 27231d05cddcSAtari911 // Handle namespace change without interval change 27241d05cddcSAtari911 if ($newNamespace !== $oldNamespace && empty($eventsToMove)) { 27251d05cddcSAtari911 foreach (glob($oldDataDir . '/*.json') as $file) { 27261d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 27271d05cddcSAtari911 if (!$data) continue; 27281d05cddcSAtari911 27291d05cddcSAtari911 $month = basename($file, '.json'); 27301d05cddcSAtari911 $targetDir = ($newNamespace === '') ? DOKU_INC . 'data/meta/calendar' : DOKU_INC . 'data/meta/' . $newNamespace . '/calendar'; 27311d05cddcSAtari911 27321d05cddcSAtari911 if (!is_dir($targetDir)) { 27331d05cddcSAtari911 mkdir($targetDir, 0755, true); 27341d05cddcSAtari911 } 27351d05cddcSAtari911 27361d05cddcSAtari911 $targetFile = $targetDir . '/' . $month . '.json'; 27371d05cddcSAtari911 $targetData = file_exists($targetFile) ? json_decode(file_get_contents($targetFile), true) : []; 27381d05cddcSAtari911 27391d05cddcSAtari911 $modified = false; 27401d05cddcSAtari911 foreach ($data as $dateKey => $events) { 27411d05cddcSAtari911 foreach ($events as $k => $event) { 27421d05cddcSAtari911 if (isset($event['namespace']) && $event['namespace'] === $newNamespace && 27431d05cddcSAtari911 strtolower(trim($event['title'])) === strtolower(trim($newTitle))) { 27441d05cddcSAtari911 // Move this event 27451d05cddcSAtari911 if (!isset($targetData[$dateKey])) { 27461d05cddcSAtari911 $targetData[$dateKey] = []; 27471d05cddcSAtari911 } 27481d05cddcSAtari911 $targetData[$dateKey][] = $event; 27491d05cddcSAtari911 unset($data[$dateKey][$k]); 27501d05cddcSAtari911 $data[$dateKey] = array_values($data[$dateKey]); 27511d05cddcSAtari911 $modified = true; 27521d05cddcSAtari911 } 27531d05cddcSAtari911 } 27541d05cddcSAtari911 } 27551d05cddcSAtari911 27561d05cddcSAtari911 if ($modified) { 27571d05cddcSAtari911 file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); 27581d05cddcSAtari911 file_put_contents($targetFile, json_encode($targetData, JSON_PRETTY_PRINT)); 27591d05cddcSAtari911 } 27601d05cddcSAtari911 } 27611d05cddcSAtari911 } 27621d05cddcSAtari911 } 27631d05cddcSAtari911 27641d05cddcSAtari911 $changes = []; 27651d05cddcSAtari911 if ($oldTitle !== $newTitle) $changes[] = "title"; 27661d05cddcSAtari911 if (!empty($startTime) || !empty($endTime)) $changes[] = "time"; 27671d05cddcSAtari911 if ($interval > 0) $changes[] = "interval"; 27681d05cddcSAtari911 if ($newNamespace !== $oldNamespace) $changes[] = "namespace"; 27691d05cddcSAtari911 27701d05cddcSAtari911 $changeStr = !empty($changes) ? " (" . implode(", ", $changes) . ")" : ""; 27719ccd446eSAtari911 $this->clearStatsCache(); 27721d05cddcSAtari911 $this->redirect("Updated $count occurrences of recurring event$changeStr", 'success', 'manage'); 27731d05cddcSAtari911 } 27741d05cddcSAtari911 27751d05cddcSAtari911 private function moveEvents() { 27761d05cddcSAtari911 global $INPUT; 27771d05cddcSAtari911 27781d05cddcSAtari911 $events = $INPUT->arr('events'); 27791d05cddcSAtari911 $targetNamespace = $INPUT->str('target_namespace'); 27801d05cddcSAtari911 27811d05cddcSAtari911 if (empty($events)) { 27821d05cddcSAtari911 $this->redirect('No events selected', 'error', 'manage'); 27831d05cddcSAtari911 } 27841d05cddcSAtari911 27851d05cddcSAtari911 $moved = 0; 27861d05cddcSAtari911 27871d05cddcSAtari911 foreach ($events as $eventData) { 27881d05cddcSAtari911 list($id, $namespace, $date, $month) = explode('|', $eventData); 27891d05cddcSAtari911 27901d05cddcSAtari911 // Determine old file path 27911d05cddcSAtari911 if ($namespace === '') { 27921d05cddcSAtari911 $oldFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json'; 27931d05cddcSAtari911 } else { 27941d05cddcSAtari911 $oldFile = DOKU_INC . 'data/meta/' . $namespace . '/calendar/' . $month . '.json'; 27951d05cddcSAtari911 } 27961d05cddcSAtari911 27971d05cddcSAtari911 if (!file_exists($oldFile)) continue; 27981d05cddcSAtari911 27991d05cddcSAtari911 $oldData = json_decode(file_get_contents($oldFile), true); 28001d05cddcSAtari911 if (!$oldData) continue; 28011d05cddcSAtari911 28021d05cddcSAtari911 // Find and remove event from old file 28031d05cddcSAtari911 $event = null; 28049ccd446eSAtari911 if (isset($oldData[$date])) { 28051d05cddcSAtari911 foreach ($oldData[$date] as $key => $evt) { 28061d05cddcSAtari911 if ($evt['id'] === $id) { 28071d05cddcSAtari911 $event = $evt; 28081d05cddcSAtari911 unset($oldData[$date][$key]); 28091d05cddcSAtari911 $oldData[$date] = array_values($oldData[$date]); 28101d05cddcSAtari911 break; 28111d05cddcSAtari911 } 28121d05cddcSAtari911 } 28131d05cddcSAtari911 28149ccd446eSAtari911 // Remove empty date arrays 28159ccd446eSAtari911 if (empty($oldData[$date])) { 28169ccd446eSAtari911 unset($oldData[$date]); 28179ccd446eSAtari911 } 28189ccd446eSAtari911 } 28199ccd446eSAtari911 28201d05cddcSAtari911 if (!$event) continue; 28211d05cddcSAtari911 28221d05cddcSAtari911 // Save old file 28231d05cddcSAtari911 file_put_contents($oldFile, json_encode($oldData, JSON_PRETTY_PRINT)); 28241d05cddcSAtari911 28251d05cddcSAtari911 // Update event namespace 28261d05cddcSAtari911 $event['namespace'] = $targetNamespace; 28271d05cddcSAtari911 28281d05cddcSAtari911 // Determine new file path 28291d05cddcSAtari911 if ($targetNamespace === '') { 28301d05cddcSAtari911 $newFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json'; 28311d05cddcSAtari911 $newDir = dirname($newFile); 28321d05cddcSAtari911 } else { 28331d05cddcSAtari911 $newFile = DOKU_INC . 'data/meta/' . $targetNamespace . '/calendar/' . $month . '.json'; 28341d05cddcSAtari911 $newDir = dirname($newFile); 28351d05cddcSAtari911 } 28361d05cddcSAtari911 28371d05cddcSAtari911 if (!is_dir($newDir)) { 28381d05cddcSAtari911 mkdir($newDir, 0755, true); 28391d05cddcSAtari911 } 28401d05cddcSAtari911 28411d05cddcSAtari911 $newData = []; 28421d05cddcSAtari911 if (file_exists($newFile)) { 28431d05cddcSAtari911 $newData = json_decode(file_get_contents($newFile), true) ?: []; 28441d05cddcSAtari911 } 28451d05cddcSAtari911 28461d05cddcSAtari911 if (!isset($newData[$date])) { 28471d05cddcSAtari911 $newData[$date] = []; 28481d05cddcSAtari911 } 28491d05cddcSAtari911 $newData[$date][] = $event; 28501d05cddcSAtari911 28511d05cddcSAtari911 file_put_contents($newFile, json_encode($newData, JSON_PRETTY_PRINT)); 28521d05cddcSAtari911 $moved++; 28531d05cddcSAtari911 } 28541d05cddcSAtari911 28551d05cddcSAtari911 $displayTarget = $targetNamespace ?: '(default)'; 28569ccd446eSAtari911 $this->clearStatsCache(); 28571d05cddcSAtari911 $this->redirect("Moved $moved event(s) to namespace: " . $displayTarget, 'success', 'manage'); 28581d05cddcSAtari911 } 28591d05cddcSAtari911 28601d05cddcSAtari911 private function moveSingleEvent() { 28611d05cddcSAtari911 global $INPUT; 28621d05cddcSAtari911 28631d05cddcSAtari911 $eventData = $INPUT->str('event'); 28641d05cddcSAtari911 $targetNamespace = $INPUT->str('target_namespace'); 28651d05cddcSAtari911 28661d05cddcSAtari911 list($id, $namespace, $date, $month) = explode('|', $eventData); 28671d05cddcSAtari911 28681d05cddcSAtari911 // Determine old file path 28691d05cddcSAtari911 if ($namespace === '') { 28701d05cddcSAtari911 $oldFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json'; 28711d05cddcSAtari911 } else { 28721d05cddcSAtari911 $oldFile = DOKU_INC . 'data/meta/' . $namespace . '/calendar/' . $month . '.json'; 28731d05cddcSAtari911 } 28741d05cddcSAtari911 28751d05cddcSAtari911 if (!file_exists($oldFile)) { 28761d05cddcSAtari911 $this->redirect('Event file not found', 'error', 'manage'); 28771d05cddcSAtari911 } 28781d05cddcSAtari911 28791d05cddcSAtari911 $oldData = json_decode(file_get_contents($oldFile), true); 28801d05cddcSAtari911 if (!$oldData) { 28811d05cddcSAtari911 $this->redirect('Could not read event file', 'error', 'manage'); 28821d05cddcSAtari911 } 28831d05cddcSAtari911 28841d05cddcSAtari911 // Find and remove event from old file 28851d05cddcSAtari911 $event = null; 28869ccd446eSAtari911 if (isset($oldData[$date])) { 28871d05cddcSAtari911 foreach ($oldData[$date] as $key => $evt) { 28881d05cddcSAtari911 if ($evt['id'] === $id) { 28891d05cddcSAtari911 $event = $evt; 28901d05cddcSAtari911 unset($oldData[$date][$key]); 28911d05cddcSAtari911 $oldData[$date] = array_values($oldData[$date]); 28921d05cddcSAtari911 break; 28931d05cddcSAtari911 } 28941d05cddcSAtari911 } 28951d05cddcSAtari911 28969ccd446eSAtari911 // Remove empty date arrays 28979ccd446eSAtari911 if (empty($oldData[$date])) { 28989ccd446eSAtari911 unset($oldData[$date]); 28999ccd446eSAtari911 } 29009ccd446eSAtari911 } 29019ccd446eSAtari911 29021d05cddcSAtari911 if (!$event) { 29031d05cddcSAtari911 $this->redirect('Event not found', 'error', 'manage'); 29041d05cddcSAtari911 } 29051d05cddcSAtari911 29069ccd446eSAtari911 // Save old file (or delete if empty) 29079ccd446eSAtari911 if (empty($oldData)) { 29089ccd446eSAtari911 unlink($oldFile); 29099ccd446eSAtari911 } else { 29101d05cddcSAtari911 file_put_contents($oldFile, json_encode($oldData, JSON_PRETTY_PRINT)); 29119ccd446eSAtari911 } 29121d05cddcSAtari911 29131d05cddcSAtari911 // Update event namespace 29141d05cddcSAtari911 $event['namespace'] = $targetNamespace; 29151d05cddcSAtari911 29161d05cddcSAtari911 // Determine new file path 29171d05cddcSAtari911 if ($targetNamespace === '') { 29181d05cddcSAtari911 $newFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json'; 29191d05cddcSAtari911 $newDir = dirname($newFile); 29201d05cddcSAtari911 } else { 29211d05cddcSAtari911 $newFile = DOKU_INC . 'data/meta/' . $targetNamespace . '/calendar/' . $month . '.json'; 29221d05cddcSAtari911 $newDir = dirname($newFile); 29231d05cddcSAtari911 } 29241d05cddcSAtari911 29251d05cddcSAtari911 if (!is_dir($newDir)) { 29261d05cddcSAtari911 mkdir($newDir, 0755, true); 29271d05cddcSAtari911 } 29281d05cddcSAtari911 29291d05cddcSAtari911 $newData = []; 29301d05cddcSAtari911 if (file_exists($newFile)) { 29311d05cddcSAtari911 $newData = json_decode(file_get_contents($newFile), true) ?: []; 29321d05cddcSAtari911 } 29331d05cddcSAtari911 29341d05cddcSAtari911 if (!isset($newData[$date])) { 29351d05cddcSAtari911 $newData[$date] = []; 29361d05cddcSAtari911 } 29371d05cddcSAtari911 $newData[$date][] = $event; 29381d05cddcSAtari911 29391d05cddcSAtari911 file_put_contents($newFile, json_encode($newData, JSON_PRETTY_PRINT)); 29401d05cddcSAtari911 29411d05cddcSAtari911 $displayTarget = $targetNamespace ?: '(default)'; 29429ccd446eSAtari911 $this->clearStatsCache(); 29431d05cddcSAtari911 $this->redirect('Moved "' . $event['title'] . '" to ' . $displayTarget, 'success', 'manage'); 29441d05cddcSAtari911 } 29451d05cddcSAtari911 29461d05cddcSAtari911 private function createNamespace() { 29471d05cddcSAtari911 global $INPUT; 29481d05cddcSAtari911 29491d05cddcSAtari911 $namespaceName = $INPUT->str('namespace_name'); 29501d05cddcSAtari911 29511d05cddcSAtari911 // Validate namespace name 29521d05cddcSAtari911 if (empty($namespaceName)) { 29531d05cddcSAtari911 $this->redirect('Namespace name cannot be empty', 'error', 'manage'); 29541d05cddcSAtari911 } 29551d05cddcSAtari911 29561d05cddcSAtari911 if (!preg_match('/^[a-zA-Z0-9_:-]+$/', $namespaceName)) { 29571d05cddcSAtari911 $this->redirect('Invalid namespace name. Use only letters, numbers, underscore, hyphen, and colon.', 'error', 'manage'); 29581d05cddcSAtari911 } 29591d05cddcSAtari911 29601d05cddcSAtari911 // Convert namespace to directory path 29611d05cddcSAtari911 $namespacePath = str_replace(':', '/', $namespaceName); 29621d05cddcSAtari911 $calendarDir = DOKU_INC . 'data/meta/' . $namespacePath . '/calendar'; 29631d05cddcSAtari911 29641d05cddcSAtari911 // Check if already exists 29651d05cddcSAtari911 if (is_dir($calendarDir)) { 29661d05cddcSAtari911 // Check if it has any JSON files 29671d05cddcSAtari911 $hasFiles = !empty(glob($calendarDir . '/*.json')); 29681d05cddcSAtari911 if ($hasFiles) { 29691d05cddcSAtari911 $this->redirect("Namespace '$namespaceName' already exists with events", 'info', 'manage'); 29701d05cddcSAtari911 } 29711d05cddcSAtari911 // If directory exists but empty, continue to create placeholder 29721d05cddcSAtari911 } 29731d05cddcSAtari911 29741d05cddcSAtari911 // Create the directory 29751d05cddcSAtari911 if (!is_dir($calendarDir)) { 29761d05cddcSAtari911 if (!mkdir($calendarDir, 0755, true)) { 29771d05cddcSAtari911 $this->redirect("Failed to create namespace directory", 'error', 'manage'); 29781d05cddcSAtari911 } 29791d05cddcSAtari911 } 29801d05cddcSAtari911 29811d05cddcSAtari911 // Create a placeholder JSON file with an empty structure for current month 29821d05cddcSAtari911 // This ensures the namespace appears in the list immediately 29831d05cddcSAtari911 $currentMonth = date('Y-m'); 29841d05cddcSAtari911 $placeholderFile = $calendarDir . '/' . $currentMonth . '.json'; 29851d05cddcSAtari911 29861d05cddcSAtari911 if (!file_exists($placeholderFile)) { 29871d05cddcSAtari911 file_put_contents($placeholderFile, json_encode([], JSON_PRETTY_PRINT)); 29881d05cddcSAtari911 } 29891d05cddcSAtari911 29901d05cddcSAtari911 $this->redirect("Created namespace: $namespaceName", 'success', 'manage'); 29911d05cddcSAtari911 } 29921d05cddcSAtari911 29931d05cddcSAtari911 private function deleteNamespace() { 29941d05cddcSAtari911 global $INPUT; 29951d05cddcSAtari911 29961d05cddcSAtari911 $namespace = $INPUT->str('namespace'); 29971d05cddcSAtari911 29981d05cddcSAtari911 // Convert namespace to directory path (e.g., "work:projects" → "work/projects") 29991d05cddcSAtari911 $namespacePath = str_replace(':', '/', $namespace); 30001d05cddcSAtari911 30011d05cddcSAtari911 // Determine calendar directory 30021d05cddcSAtari911 if ($namespace === '') { 30031d05cddcSAtari911 $calendarDir = DOKU_INC . 'data/meta/calendar'; 30041d05cddcSAtari911 $namespaceDir = null; // Don't delete root 30051d05cddcSAtari911 } else { 30061d05cddcSAtari911 $calendarDir = DOKU_INC . 'data/meta/' . $namespacePath . '/calendar'; 30071d05cddcSAtari911 $namespaceDir = DOKU_INC . 'data/meta/' . $namespacePath; 30081d05cddcSAtari911 } 30091d05cddcSAtari911 30101d05cddcSAtari911 // Check if directory exists 30111d05cddcSAtari911 if (!is_dir($calendarDir)) { 30121d05cddcSAtari911 // Maybe it was never created or already deleted 30131d05cddcSAtari911 $this->redirect("Namespace directory not found: $calendarDir", 'error', 'manage'); 30141d05cddcSAtari911 return; 30151d05cddcSAtari911 } 30161d05cddcSAtari911 30171d05cddcSAtari911 $filesDeleted = 0; 30181d05cddcSAtari911 $eventsDeleted = 0; 30191d05cddcSAtari911 30201d05cddcSAtari911 // Delete all calendar JSON files (including empty ones) 30211d05cddcSAtari911 foreach (glob($calendarDir . '/*.json') as $file) { 30221d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 30231d05cddcSAtari911 if ($data) { 30241d05cddcSAtari911 foreach ($data as $events) { 30251d05cddcSAtari911 $eventsDeleted += count($events); 30261d05cddcSAtari911 } 30271d05cddcSAtari911 } 30281d05cddcSAtari911 unlink($file); 30291d05cddcSAtari911 $filesDeleted++; 30301d05cddcSAtari911 } 30311d05cddcSAtari911 30321d05cddcSAtari911 // Delete any other files in calendar directory 30331d05cddcSAtari911 foreach (glob($calendarDir . '/*') as $file) { 30341d05cddcSAtari911 if (is_file($file)) { 30351d05cddcSAtari911 unlink($file); 30361d05cddcSAtari911 } 30371d05cddcSAtari911 } 30381d05cddcSAtari911 30391d05cddcSAtari911 // Remove the calendar directory 30401d05cddcSAtari911 if ($namespace !== '') { 30411d05cddcSAtari911 @rmdir($calendarDir); 30421d05cddcSAtari911 30431d05cddcSAtari911 // Try to remove parent directories if they're empty 30441d05cddcSAtari911 // This handles nested namespaces like work:projects:alpha 30451d05cddcSAtari911 $currentDir = dirname($calendarDir); 30461d05cddcSAtari911 $metaDir = DOKU_INC . 'data/meta'; 30471d05cddcSAtari911 30481d05cddcSAtari911 while ($currentDir !== $metaDir && $currentDir !== dirname($metaDir)) { 30491d05cddcSAtari911 if (is_dir($currentDir)) { 30501d05cddcSAtari911 // Check if directory is empty 30511d05cddcSAtari911 $contents = scandir($currentDir); 30521d05cddcSAtari911 $isEmpty = count($contents) === 2; // Only . and .. 30531d05cddcSAtari911 30541d05cddcSAtari911 if ($isEmpty) { 30551d05cddcSAtari911 @rmdir($currentDir); 30561d05cddcSAtari911 $currentDir = dirname($currentDir); 30571d05cddcSAtari911 } else { 30581d05cddcSAtari911 break; // Directory not empty, stop 30591d05cddcSAtari911 } 30601d05cddcSAtari911 } else { 30611d05cddcSAtari911 break; 30621d05cddcSAtari911 } 30631d05cddcSAtari911 } 30641d05cddcSAtari911 } 30651d05cddcSAtari911 30661d05cddcSAtari911 $displayName = $namespace ?: '(default)'; 30679ccd446eSAtari911 $this->clearStatsCache(); 30681d05cddcSAtari911 $this->redirect("Deleted namespace '$displayName': $eventsDeleted events in $filesDeleted files", 'success', 'manage'); 30691d05cddcSAtari911 } 30701d05cddcSAtari911 30719ccd446eSAtari911 private function renameNamespace() { 30729ccd446eSAtari911 global $INPUT; 30739ccd446eSAtari911 30749ccd446eSAtari911 $oldNamespace = $INPUT->str('old_namespace'); 30759ccd446eSAtari911 $newNamespace = $INPUT->str('new_namespace'); 30769ccd446eSAtari911 30779ccd446eSAtari911 // Validate new namespace name 30789ccd446eSAtari911 if ($newNamespace === '') { 30799ccd446eSAtari911 $this->redirect("Cannot rename to empty namespace", 'error', 'manage'); 30809ccd446eSAtari911 return; 30819ccd446eSAtari911 } 30829ccd446eSAtari911 30839ccd446eSAtari911 // Convert namespaces to directory paths 30849ccd446eSAtari911 $oldPath = str_replace(':', '/', $oldNamespace); 30859ccd446eSAtari911 $newPath = str_replace(':', '/', $newNamespace); 30869ccd446eSAtari911 30879ccd446eSAtari911 // Determine source and destination directories 30889ccd446eSAtari911 if ($oldNamespace === '') { 30899ccd446eSAtari911 $sourceDir = DOKU_INC . 'data/meta/calendar'; 30909ccd446eSAtari911 } else { 30919ccd446eSAtari911 $sourceDir = DOKU_INC . 'data/meta/' . $oldPath . '/calendar'; 30929ccd446eSAtari911 } 30939ccd446eSAtari911 30949ccd446eSAtari911 if ($newNamespace === '') { 30959ccd446eSAtari911 $targetDir = DOKU_INC . 'data/meta/calendar'; 30969ccd446eSAtari911 } else { 30979ccd446eSAtari911 $targetDir = DOKU_INC . 'data/meta/' . $newPath . '/calendar'; 30989ccd446eSAtari911 } 30999ccd446eSAtari911 31009ccd446eSAtari911 // Check if source exists 31019ccd446eSAtari911 if (!is_dir($sourceDir)) { 31029ccd446eSAtari911 $this->redirect("Source namespace not found: $oldNamespace", 'error', 'manage'); 31039ccd446eSAtari911 return; 31049ccd446eSAtari911 } 31059ccd446eSAtari911 31069ccd446eSAtari911 // Check if target already exists 31079ccd446eSAtari911 if (is_dir($targetDir)) { 31089ccd446eSAtari911 $this->redirect("Target namespace already exists: $newNamespace", 'error', 'manage'); 31099ccd446eSAtari911 return; 31109ccd446eSAtari911 } 31119ccd446eSAtari911 31129ccd446eSAtari911 // Create target directory 31139ccd446eSAtari911 if (!file_exists(dirname($targetDir))) { 31149ccd446eSAtari911 mkdir(dirname($targetDir), 0755, true); 31159ccd446eSAtari911 } 31169ccd446eSAtari911 31179ccd446eSAtari911 // Rename directory 31189ccd446eSAtari911 if (!rename($sourceDir, $targetDir)) { 31199ccd446eSAtari911 $this->redirect("Failed to rename namespace", 'error', 'manage'); 31209ccd446eSAtari911 return; 31219ccd446eSAtari911 } 31229ccd446eSAtari911 31239ccd446eSAtari911 // Update event namespace field in all JSON files 31249ccd446eSAtari911 $eventsUpdated = 0; 31259ccd446eSAtari911 foreach (glob($targetDir . '/*.json') as $file) { 31269ccd446eSAtari911 $data = json_decode(file_get_contents($file), true); 31279ccd446eSAtari911 if ($data) { 31289ccd446eSAtari911 foreach ($data as $date => &$events) { 31299ccd446eSAtari911 foreach ($events as &$event) { 31309ccd446eSAtari911 if (isset($event['namespace']) && $event['namespace'] === $oldNamespace) { 31319ccd446eSAtari911 $event['namespace'] = $newNamespace; 31329ccd446eSAtari911 $eventsUpdated++; 31339ccd446eSAtari911 } 31349ccd446eSAtari911 } 31359ccd446eSAtari911 } 31369ccd446eSAtari911 file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); 31379ccd446eSAtari911 } 31389ccd446eSAtari911 } 31399ccd446eSAtari911 31409ccd446eSAtari911 // Clean up old directory structure if empty 31419ccd446eSAtari911 if ($oldNamespace !== '') { 31429ccd446eSAtari911 $currentDir = dirname($sourceDir); 31439ccd446eSAtari911 $metaDir = DOKU_INC . 'data/meta'; 31449ccd446eSAtari911 31459ccd446eSAtari911 while ($currentDir !== $metaDir && $currentDir !== dirname($metaDir)) { 31469ccd446eSAtari911 if (is_dir($currentDir)) { 31479ccd446eSAtari911 $contents = scandir($currentDir); 31489ccd446eSAtari911 $isEmpty = count($contents) === 2; // Only . and .. 31499ccd446eSAtari911 31509ccd446eSAtari911 if ($isEmpty) { 31519ccd446eSAtari911 @rmdir($currentDir); 31529ccd446eSAtari911 $currentDir = dirname($currentDir); 31539ccd446eSAtari911 } else { 31549ccd446eSAtari911 break; 31559ccd446eSAtari911 } 31569ccd446eSAtari911 } else { 31579ccd446eSAtari911 break; 31589ccd446eSAtari911 } 31599ccd446eSAtari911 } 31609ccd446eSAtari911 } 31619ccd446eSAtari911 31629ccd446eSAtari911 $this->clearStatsCache(); 31639ccd446eSAtari911 $this->redirect("Renamed namespace from '$oldNamespace' to '$newNamespace' ($eventsUpdated events updated)", 'success', 'manage'); 31649ccd446eSAtari911 } 31659ccd446eSAtari911 31661d05cddcSAtari911 private function deleteSelectedEvents() { 31671d05cddcSAtari911 global $INPUT; 31681d05cddcSAtari911 31691d05cddcSAtari911 $events = $INPUT->arr('events'); 31701d05cddcSAtari911 31711d05cddcSAtari911 if (empty($events)) { 31721d05cddcSAtari911 $this->redirect('No events selected', 'error', 'manage'); 31731d05cddcSAtari911 } 31741d05cddcSAtari911 31751d05cddcSAtari911 $deletedCount = 0; 31761d05cddcSAtari911 31771d05cddcSAtari911 foreach ($events as $eventData) { 31781d05cddcSAtari911 list($id, $namespace, $date, $month) = explode('|', $eventData); 31791d05cddcSAtari911 31801d05cddcSAtari911 // Determine file path 31811d05cddcSAtari911 if ($namespace === '') { 31821d05cddcSAtari911 $file = DOKU_INC . 'data/meta/calendar/' . $month . '.json'; 31831d05cddcSAtari911 } else { 31841d05cddcSAtari911 $file = DOKU_INC . 'data/meta/' . $namespace . '/calendar/' . $month . '.json'; 31851d05cddcSAtari911 } 31861d05cddcSAtari911 31871d05cddcSAtari911 if (!file_exists($file)) continue; 31881d05cddcSAtari911 31891d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 31901d05cddcSAtari911 if (!$data) continue; 31911d05cddcSAtari911 31921d05cddcSAtari911 // Find and remove event 31931d05cddcSAtari911 if (isset($data[$date])) { 31941d05cddcSAtari911 foreach ($data[$date] as $key => $evt) { 31951d05cddcSAtari911 if ($evt['id'] === $id) { 31961d05cddcSAtari911 unset($data[$date][$key]); 31971d05cddcSAtari911 $data[$date] = array_values($data[$date]); 31981d05cddcSAtari911 $deletedCount++; 31991d05cddcSAtari911 break; 32001d05cddcSAtari911 } 32011d05cddcSAtari911 } 32021d05cddcSAtari911 32031d05cddcSAtari911 // Remove empty date arrays 32041d05cddcSAtari911 if (empty($data[$date])) { 32051d05cddcSAtari911 unset($data[$date]); 32061d05cddcSAtari911 } 32071d05cddcSAtari911 32081d05cddcSAtari911 // Save file 32091d05cddcSAtari911 file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); 32101d05cddcSAtari911 } 32111d05cddcSAtari911 } 32121d05cddcSAtari911 32139ccd446eSAtari911 $this->clearStatsCache(); 32141d05cddcSAtari911 $this->redirect("Deleted $deletedCount event(s)", 'success', 'manage'); 32151d05cddcSAtari911 } 32161d05cddcSAtari911 32179ccd446eSAtari911 /** 32189ccd446eSAtari911 * Clear the event statistics cache so counts refresh after mutations 32199ccd446eSAtari911 */ 3220*4590242dSAtari911 private function saveImportantNamespaces() { 3221*4590242dSAtari911 global $INPUT; 3222*4590242dSAtari911 3223*4590242dSAtari911 $configFile = DOKU_PLUGIN . 'calendar/sync_config.php'; 3224*4590242dSAtari911 $config = []; 3225*4590242dSAtari911 if (file_exists($configFile)) { 3226*4590242dSAtari911 $config = include $configFile; 3227*4590242dSAtari911 } 3228*4590242dSAtari911 3229*4590242dSAtari911 $config['important_namespaces'] = $INPUT->str('important_namespaces', 'important'); 3230*4590242dSAtari911 3231*4590242dSAtari911 $content = "<?php\nreturn " . var_export($config, true) . ";\n"; 3232*4590242dSAtari911 if (file_put_contents($configFile, $content)) { 3233*4590242dSAtari911 $this->redirect('Important namespaces saved', 'success', 'manage'); 3234*4590242dSAtari911 } else { 3235*4590242dSAtari911 $this->redirect('Error: Could not save configuration', 'error', 'manage'); 3236*4590242dSAtari911 } 3237*4590242dSAtari911 } 3238*4590242dSAtari911 32399ccd446eSAtari911 private function clearStatsCache() { 32409ccd446eSAtari911 $cacheFile = DOKU_PLUGIN . 'calendar/.event_stats_cache'; 32419ccd446eSAtari911 if (file_exists($cacheFile)) { 32429ccd446eSAtari911 unlink($cacheFile); 32439ccd446eSAtari911 } 32449ccd446eSAtari911 } 32459ccd446eSAtari911 32461d05cddcSAtari911 private function getCronStatus() { 32471d05cddcSAtari911 // Try to read root's crontab first, then current user 32481d05cddcSAtari911 $output = []; 32491d05cddcSAtari911 exec('sudo crontab -l 2>/dev/null', $output); 32501d05cddcSAtari911 32511d05cddcSAtari911 // If sudo doesn't work, try current user 32521d05cddcSAtari911 if (empty($output)) { 32531d05cddcSAtari911 exec('crontab -l 2>/dev/null', $output); 32541d05cddcSAtari911 } 32551d05cddcSAtari911 32561d05cddcSAtari911 // Also check system crontab files 32571d05cddcSAtari911 if (empty($output)) { 32581d05cddcSAtari911 $cronFiles = [ 32591d05cddcSAtari911 '/etc/crontab', 32601d05cddcSAtari911 '/etc/cron.d/calendar', 32611d05cddcSAtari911 '/var/spool/cron/root', 32621d05cddcSAtari911 '/var/spool/cron/crontabs/root' 32631d05cddcSAtari911 ]; 32641d05cddcSAtari911 32651d05cddcSAtari911 foreach ($cronFiles as $file) { 32661d05cddcSAtari911 if (file_exists($file) && is_readable($file)) { 32671d05cddcSAtari911 $content = file_get_contents($file); 32681d05cddcSAtari911 $output = explode("\n", $content); 32691d05cddcSAtari911 break; 32701d05cddcSAtari911 } 32711d05cddcSAtari911 } 32721d05cddcSAtari911 } 32731d05cddcSAtari911 32741d05cddcSAtari911 // Look for sync_outlook.php in the cron entries 32751d05cddcSAtari911 foreach ($output as $line) { 32761d05cddcSAtari911 $line = trim($line); 32771d05cddcSAtari911 32781d05cddcSAtari911 // Skip empty lines and comments 32791d05cddcSAtari911 if (empty($line) || $line[0] === '#') continue; 32801d05cddcSAtari911 32811d05cddcSAtari911 // Check if line contains sync_outlook.php 32821d05cddcSAtari911 if (strpos($line, 'sync_outlook.php') !== false) { 32831d05cddcSAtari911 // Parse cron expression 32841d05cddcSAtari911 // Format: minute hour day month weekday [user] command 32851d05cddcSAtari911 $parts = preg_split('/\s+/', $line, 7); 32861d05cddcSAtari911 32871d05cddcSAtari911 if (count($parts) >= 5) { 32881d05cddcSAtari911 // Determine if this has a user field (system crontab format) 32891d05cddcSAtari911 $hasUser = (count($parts) >= 6 && !preg_match('/^[\/\*]/', $parts[5])); 32901d05cddcSAtari911 $offset = $hasUser ? 1 : 0; 32911d05cddcSAtari911 32921d05cddcSAtari911 $frequency = $this->parseCronExpression($parts[0], $parts[1], $parts[2], $parts[3], $parts[4]); 32931d05cddcSAtari911 return [ 32941d05cddcSAtari911 'active' => true, 32951d05cddcSAtari911 'frequency' => $frequency, 32961d05cddcSAtari911 'expression' => implode(' ', array_slice($parts, 0, 5)), 32971d05cddcSAtari911 'full_line' => $line 32981d05cddcSAtari911 ]; 32991d05cddcSAtari911 } 33001d05cddcSAtari911 } 33011d05cddcSAtari911 } 33021d05cddcSAtari911 33031d05cddcSAtari911 return ['active' => false, 'frequency' => '', 'expression' => '', 'full_line' => '']; 33041d05cddcSAtari911 } 33051d05cddcSAtari911 33061d05cddcSAtari911 private function parseCronExpression($minute, $hour, $day, $month, $weekday) { 33071d05cddcSAtari911 // Parse minute field 33081d05cddcSAtari911 if ($minute === '*') { 33091d05cddcSAtari911 return 'Runs every minute'; 33101d05cddcSAtari911 } elseif (strpos($minute, '*/') === 0) { 33111d05cddcSAtari911 $interval = substr($minute, 2); 33121d05cddcSAtari911 if ($interval == 1) { 33131d05cddcSAtari911 return 'Runs every minute'; 33141d05cddcSAtari911 } elseif ($interval == 5) { 33151d05cddcSAtari911 return 'Runs every 5 minutes'; 33161d05cddcSAtari911 } elseif ($interval == 8) { 33171d05cddcSAtari911 return 'Runs every 8 minutes'; 33181d05cddcSAtari911 } elseif ($interval == 10) { 33191d05cddcSAtari911 return 'Runs every 10 minutes'; 33201d05cddcSAtari911 } elseif ($interval == 15) { 33211d05cddcSAtari911 return 'Runs every 15 minutes'; 33221d05cddcSAtari911 } elseif ($interval == 30) { 33231d05cddcSAtari911 return 'Runs every 30 minutes'; 33241d05cddcSAtari911 } else { 33251d05cddcSAtari911 return "Runs every $interval minutes"; 33261d05cddcSAtari911 } 33271d05cddcSAtari911 } 33281d05cddcSAtari911 33291d05cddcSAtari911 // Parse hour field 33301d05cddcSAtari911 if ($hour === '*' && $minute !== '*') { 33311d05cddcSAtari911 return 'Runs hourly'; 33321d05cddcSAtari911 } elseif (strpos($hour, '*/') === 0 && $minute !== '*') { 33331d05cddcSAtari911 $interval = substr($hour, 2); 33341d05cddcSAtari911 if ($interval == 1) { 33351d05cddcSAtari911 return 'Runs every hour'; 33361d05cddcSAtari911 } else { 33371d05cddcSAtari911 return "Runs every $interval hours"; 33381d05cddcSAtari911 } 33391d05cddcSAtari911 } 33401d05cddcSAtari911 33411d05cddcSAtari911 // Parse day field 33421d05cddcSAtari911 if ($day === '*' && $hour !== '*' && $minute !== '*') { 33431d05cddcSAtari911 return 'Runs daily'; 33441d05cddcSAtari911 } 33451d05cddcSAtari911 33461d05cddcSAtari911 // Default 33471d05cddcSAtari911 return 'Custom schedule'; 33481d05cddcSAtari911 } 33491d05cddcSAtari911 33501d05cddcSAtari911 private function runSync() { 33511d05cddcSAtari911 global $INPUT; 33521d05cddcSAtari911 33531d05cddcSAtari911 if ($INPUT->str('call') === 'ajax') { 33541d05cddcSAtari911 header('Content-Type: application/json'); 33551d05cddcSAtari911 33561d05cddcSAtari911 $syncScript = DOKU_PLUGIN . 'calendar/sync_outlook.php'; 33571d05cddcSAtari911 $abortFile = DOKU_PLUGIN . 'calendar/.sync_abort'; 33581d05cddcSAtari911 33591d05cddcSAtari911 // Remove any existing abort flag 33601d05cddcSAtari911 if (file_exists($abortFile)) { 33611d05cddcSAtari911 @unlink($abortFile); 33621d05cddcSAtari911 } 33631d05cddcSAtari911 33641d05cddcSAtari911 if (!file_exists($syncScript)) { 33651d05cddcSAtari911 echo json_encode(['success' => false, 'message' => 'Sync script not found at: ' . $syncScript]); 33661d05cddcSAtari911 exit; 33671d05cddcSAtari911 } 33681d05cddcSAtari911 33691d05cddcSAtari911 // Change to plugin directory 33701d05cddcSAtari911 $pluginDir = DOKU_PLUGIN . 'calendar'; 33711d05cddcSAtari911 $logFile = $pluginDir . '/sync.log'; 33721d05cddcSAtari911 33731d05cddcSAtari911 // Ensure log file exists and is writable 33741d05cddcSAtari911 if (!file_exists($logFile)) { 33751d05cddcSAtari911 @touch($logFile); 33761d05cddcSAtari911 @chmod($logFile, 0666); 33771d05cddcSAtari911 } 33781d05cddcSAtari911 33791d05cddcSAtari911 // Try to log the execution (but don't fail if we can't) 33801d05cddcSAtari911 if (is_writable($logFile)) { 33811d05cddcSAtari911 $tz = new DateTimeZone('America/Los_Angeles'); 33821d05cddcSAtari911 $now = new DateTime('now', $tz); 33831d05cddcSAtari911 $timestamp = $now->format('Y-m-d H:i:s'); 33841d05cddcSAtari911 @file_put_contents($logFile, "[$timestamp] [ADMIN] Manual sync triggered via admin panel\n", FILE_APPEND); 33851d05cddcSAtari911 } 33861d05cddcSAtari911 33871d05cddcSAtari911 // Find PHP binary - try multiple methods 33881d05cddcSAtari911 $phpPath = $this->findPhpBinary(); 33891d05cddcSAtari911 33901d05cddcSAtari911 // Build command 33911d05cddcSAtari911 $command = sprintf( 33921d05cddcSAtari911 'cd %s && %s %s 2>&1', 33931d05cddcSAtari911 escapeshellarg($pluginDir), 33941d05cddcSAtari911 $phpPath, 33951d05cddcSAtari911 escapeshellarg(basename($syncScript)) 33961d05cddcSAtari911 ); 33971d05cddcSAtari911 33981d05cddcSAtari911 // Execute and capture output 33991d05cddcSAtari911 $output = []; 34001d05cddcSAtari911 $returnCode = 0; 34011d05cddcSAtari911 exec($command, $output, $returnCode); 34021d05cddcSAtari911 34031d05cddcSAtari911 // Check if sync completed 34041d05cddcSAtari911 $lastLines = array_slice($output, -5); 34051d05cddcSAtari911 $completed = false; 34061d05cddcSAtari911 foreach ($lastLines as $line) { 34071d05cddcSAtari911 if (strpos($line, 'Sync Complete') !== false || strpos($line, 'Created:') !== false) { 34081d05cddcSAtari911 $completed = true; 34091d05cddcSAtari911 break; 34101d05cddcSAtari911 } 34111d05cddcSAtari911 } 34121d05cddcSAtari911 34131d05cddcSAtari911 if ($returnCode === 0 && $completed) { 34141d05cddcSAtari911 echo json_encode([ 34151d05cddcSAtari911 'success' => true, 34161d05cddcSAtari911 'message' => 'Sync completed successfully! Check log below.' 34171d05cddcSAtari911 ]); 34181d05cddcSAtari911 } elseif ($returnCode === 0) { 34191d05cddcSAtari911 echo json_encode([ 34201d05cddcSAtari911 'success' => true, 34211d05cddcSAtari911 'message' => 'Sync started. Check log below for progress.' 34221d05cddcSAtari911 ]); 34231d05cddcSAtari911 } else { 34241d05cddcSAtari911 // Include output for debugging 34251d05cddcSAtari911 $errorMsg = 'Sync failed with error code: ' . $returnCode; 34261d05cddcSAtari911 if (!empty($output)) { 34271d05cddcSAtari911 $errorMsg .= ' | ' . implode(' | ', array_slice($output, -3)); 34281d05cddcSAtari911 } 34291d05cddcSAtari911 echo json_encode([ 34301d05cddcSAtari911 'success' => false, 34311d05cddcSAtari911 'message' => $errorMsg 34321d05cddcSAtari911 ]); 34331d05cddcSAtari911 } 34341d05cddcSAtari911 exit; 34351d05cddcSAtari911 } 34361d05cddcSAtari911 } 34371d05cddcSAtari911 34381d05cddcSAtari911 private function stopSync() { 34391d05cddcSAtari911 global $INPUT; 34401d05cddcSAtari911 34411d05cddcSAtari911 if ($INPUT->str('call') === 'ajax') { 34421d05cddcSAtari911 header('Content-Type: application/json'); 34431d05cddcSAtari911 34441d05cddcSAtari911 $abortFile = DOKU_PLUGIN . 'calendar/.sync_abort'; 34451d05cddcSAtari911 34461d05cddcSAtari911 // Create abort flag file 34471d05cddcSAtari911 if (file_put_contents($abortFile, date('Y-m-d H:i:s')) !== false) { 34481d05cddcSAtari911 echo json_encode([ 34491d05cddcSAtari911 'success' => true, 34501d05cddcSAtari911 'message' => 'Stop signal sent to sync process' 34511d05cddcSAtari911 ]); 34521d05cddcSAtari911 } else { 34531d05cddcSAtari911 echo json_encode([ 34541d05cddcSAtari911 'success' => false, 34551d05cddcSAtari911 'message' => 'Failed to create abort flag' 34561d05cddcSAtari911 ]); 34571d05cddcSAtari911 } 34581d05cddcSAtari911 exit; 34591d05cddcSAtari911 } 34601d05cddcSAtari911 } 34611d05cddcSAtari911 34621d05cddcSAtari911 private function uploadUpdate() { 34631d05cddcSAtari911 if (!isset($_FILES['plugin_zip']) || $_FILES['plugin_zip']['error'] !== UPLOAD_ERR_OK) { 34641d05cddcSAtari911 $this->redirect('Upload failed: ' . ($_FILES['plugin_zip']['error'] ?? 'No file uploaded'), 'error', 'update'); 34651d05cddcSAtari911 return; 34661d05cddcSAtari911 } 34671d05cddcSAtari911 34681d05cddcSAtari911 $uploadedFile = $_FILES['plugin_zip']['tmp_name']; 34691d05cddcSAtari911 $pluginDir = DOKU_PLUGIN . 'calendar/'; 34701d05cddcSAtari911 $backupFirst = isset($_POST['backup_first']); 34711d05cddcSAtari911 34721d05cddcSAtari911 // Check if plugin directory is writable 34731d05cddcSAtari911 if (!is_writable($pluginDir)) { 34741d05cddcSAtari911 $this->redirect('Plugin directory is not writable. Please check permissions: ' . $pluginDir, 'error', 'update'); 34751d05cddcSAtari911 return; 34761d05cddcSAtari911 } 34771d05cddcSAtari911 34781d05cddcSAtari911 // Check if parent directory is writable (for backup and temp files) 34791d05cddcSAtari911 if (!is_writable(DOKU_PLUGIN)) { 34801d05cddcSAtari911 $this->redirect('Plugin parent directory is not writable. Please check permissions: ' . DOKU_PLUGIN, 'error', 'update'); 34811d05cddcSAtari911 return; 34821d05cddcSAtari911 } 34831d05cddcSAtari911 34841d05cddcSAtari911 // Verify it's a ZIP file 34851d05cddcSAtari911 $finfo = finfo_open(FILEINFO_MIME_TYPE); 34861d05cddcSAtari911 $mimeType = finfo_file($finfo, $uploadedFile); 34871d05cddcSAtari911 finfo_close($finfo); 34881d05cddcSAtari911 34891d05cddcSAtari911 if ($mimeType !== 'application/zip' && $mimeType !== 'application/x-zip-compressed') { 34901d05cddcSAtari911 $this->redirect('Invalid file type. Please upload a ZIP file.', 'error', 'update'); 34911d05cddcSAtari911 return; 34921d05cddcSAtari911 } 34931d05cddcSAtari911 34941d05cddcSAtari911 // Create backup if requested 34951d05cddcSAtari911 if ($backupFirst) { 34961d05cddcSAtari911 // Get current version 34971d05cddcSAtari911 $pluginInfo = $pluginDir . 'plugin.info.txt'; 34981d05cddcSAtari911 $version = 'unknown'; 34991d05cddcSAtari911 if (file_exists($pluginInfo)) { 35001d05cddcSAtari911 $info = confToHash($pluginInfo); 35011d05cddcSAtari911 $version = $info['version'] ?? ($info['date'] ?? 'unknown'); 35021d05cddcSAtari911 } 35031d05cddcSAtari911 35041d05cddcSAtari911 $backupName = 'calendar.backup.v' . $version . '.' . date('Y-m-d_H-i-s') . '.zip'; 35051d05cddcSAtari911 $backupPath = DOKU_PLUGIN . $backupName; 35061d05cddcSAtari911 35071d05cddcSAtari911 try { 35081d05cddcSAtari911 $zip = new ZipArchive(); 35091d05cddcSAtari911 if ($zip->open($backupPath, ZipArchive::CREATE) === TRUE) { 35109ccd446eSAtari911 $fileCount = $this->addDirectoryToZip($zip, $pluginDir, 'calendar/'); 35111d05cddcSAtari911 $zip->close(); 35129ccd446eSAtari911 35139ccd446eSAtari911 // Verify backup was created and has content 35149ccd446eSAtari911 if (!file_exists($backupPath)) { 35159ccd446eSAtari911 $this->redirect('Backup file was not created', 'error', 'update'); 35169ccd446eSAtari911 return; 35179ccd446eSAtari911 } 35189ccd446eSAtari911 35199ccd446eSAtari911 $backupSize = filesize($backupPath); 35209ccd446eSAtari911 if ($backupSize < 1000) { // Backup should be at least 1KB 35219ccd446eSAtari911 @unlink($backupPath); 35229ccd446eSAtari911 $this->redirect('Backup file is too small (' . $backupSize . ' bytes). Only ' . $fileCount . ' files were added. Backup aborted.', 'error', 'update'); 35239ccd446eSAtari911 return; 35249ccd446eSAtari911 } 35259ccd446eSAtari911 35269ccd446eSAtari911 if ($fileCount < 10) { // Should have at least 10 files 35279ccd446eSAtari911 @unlink($backupPath); 35289ccd446eSAtari911 $this->redirect('Backup incomplete: Only ' . $fileCount . ' files were added (expected 30+). Backup aborted.', 'error', 'update'); 35299ccd446eSAtari911 return; 35309ccd446eSAtari911 } 35311d05cddcSAtari911 } else { 35321d05cddcSAtari911 $this->redirect('Failed to create backup ZIP file', 'error', 'update'); 35331d05cddcSAtari911 return; 35341d05cddcSAtari911 } 35351d05cddcSAtari911 } catch (Exception $e) { 35369ccd446eSAtari911 if (file_exists($backupPath)) { 35379ccd446eSAtari911 @unlink($backupPath); 35389ccd446eSAtari911 } 35391d05cddcSAtari911 $this->redirect('Backup failed: ' . $e->getMessage(), 'error', 'update'); 35401d05cddcSAtari911 return; 35411d05cddcSAtari911 } 35421d05cddcSAtari911 } 35431d05cddcSAtari911 35441d05cddcSAtari911 // Extract uploaded ZIP 35451d05cddcSAtari911 $zip = new ZipArchive(); 35461d05cddcSAtari911 if ($zip->open($uploadedFile) !== TRUE) { 35471d05cddcSAtari911 $this->redirect('Failed to open ZIP file', 'error', 'update'); 35481d05cddcSAtari911 return; 35491d05cddcSAtari911 } 35501d05cddcSAtari911 35511d05cddcSAtari911 // Check if ZIP contains calendar folder 35521d05cddcSAtari911 $hasCalendarFolder = false; 35531d05cddcSAtari911 for ($i = 0; $i < $zip->numFiles; $i++) { 35541d05cddcSAtari911 $filename = $zip->getNameIndex($i); 35551d05cddcSAtari911 if (strpos($filename, 'calendar/') === 0) { 35561d05cddcSAtari911 $hasCalendarFolder = true; 35571d05cddcSAtari911 break; 35581d05cddcSAtari911 } 35591d05cddcSAtari911 } 35601d05cddcSAtari911 35611d05cddcSAtari911 // Extract to temp directory first 35621d05cddcSAtari911 $tempDir = DOKU_PLUGIN . 'calendar_update_temp/'; 35631d05cddcSAtari911 if (is_dir($tempDir)) { 35641d05cddcSAtari911 $this->deleteDirectory($tempDir); 35651d05cddcSAtari911 } 35661d05cddcSAtari911 mkdir($tempDir); 35671d05cddcSAtari911 35681d05cddcSAtari911 $zip->extractTo($tempDir); 35691d05cddcSAtari911 $zip->close(); 35701d05cddcSAtari911 35711d05cddcSAtari911 // Determine source directory 35721d05cddcSAtari911 if ($hasCalendarFolder) { 35731d05cddcSAtari911 $sourceDir = $tempDir . 'calendar/'; 35741d05cddcSAtari911 } else { 35751d05cddcSAtari911 $sourceDir = $tempDir; 35761d05cddcSAtari911 } 35771d05cddcSAtari911 35781d05cddcSAtari911 // Preserve configuration files 35791d05cddcSAtari911 $preserveFiles = ['sync_config.php', 'sync_state.json', 'sync.log']; 35801d05cddcSAtari911 $preserved = []; 35811d05cddcSAtari911 foreach ($preserveFiles as $file) { 35821d05cddcSAtari911 $oldFile = $pluginDir . $file; 35831d05cddcSAtari911 if (file_exists($oldFile)) { 35841d05cddcSAtari911 $preserved[$file] = file_get_contents($oldFile); 35851d05cddcSAtari911 } 35861d05cddcSAtari911 } 35871d05cddcSAtari911 35881d05cddcSAtari911 // Delete old plugin files (except data files) 35891d05cddcSAtari911 $this->deleteDirectoryContents($pluginDir, $preserveFiles); 35901d05cddcSAtari911 35911d05cddcSAtari911 // Copy new files 35921d05cddcSAtari911 $this->recursiveCopy($sourceDir, $pluginDir); 35931d05cddcSAtari911 35941d05cddcSAtari911 // Restore preserved files 35951d05cddcSAtari911 foreach ($preserved as $file => $content) { 35961d05cddcSAtari911 file_put_contents($pluginDir . $file, $content); 35971d05cddcSAtari911 } 35981d05cddcSAtari911 35991d05cddcSAtari911 // Update version and date in plugin.info.txt 36001d05cddcSAtari911 $pluginInfo = $pluginDir . 'plugin.info.txt'; 36011d05cddcSAtari911 if (file_exists($pluginInfo)) { 36021d05cddcSAtari911 $info = confToHash($pluginInfo); 36031d05cddcSAtari911 36041d05cddcSAtari911 // Get new version from uploaded plugin 36051d05cddcSAtari911 $newVersion = $info['version'] ?? 'unknown'; 36061d05cddcSAtari911 36071d05cddcSAtari911 // Update date to current 36081d05cddcSAtari911 $info['date'] = date('Y-m-d'); 36091d05cddcSAtari911 36101d05cddcSAtari911 // Write updated info back 36111d05cddcSAtari911 $lines = []; 36121d05cddcSAtari911 foreach ($info as $key => $value) { 36131d05cddcSAtari911 $lines[] = str_pad($key, 8) . ' ' . $value; 36141d05cddcSAtari911 } 36151d05cddcSAtari911 file_put_contents($pluginInfo, implode("\n", $lines) . "\n"); 36161d05cddcSAtari911 } 36171d05cddcSAtari911 36181d05cddcSAtari911 // Cleanup temp directory 36191d05cddcSAtari911 $this->deleteDirectory($tempDir); 36201d05cddcSAtari911 36211d05cddcSAtari911 $message = 'Plugin updated successfully!'; 36221d05cddcSAtari911 if ($backupFirst) { 36231d05cddcSAtari911 $message .= ' Backup saved as: ' . $backupName; 36241d05cddcSAtari911 } 36251d05cddcSAtari911 $this->redirect($message, 'success', 'update'); 36261d05cddcSAtari911 } 36271d05cddcSAtari911 36281d05cddcSAtari911 private function deleteBackup() { 36291d05cddcSAtari911 global $INPUT; 36301d05cddcSAtari911 36311d05cddcSAtari911 $filename = $INPUT->str('backup_file'); 36321d05cddcSAtari911 36331d05cddcSAtari911 if (empty($filename)) { 36341d05cddcSAtari911 $this->redirect('No backup file specified', 'error', 'update'); 36351d05cddcSAtari911 return; 36361d05cddcSAtari911 } 36371d05cddcSAtari911 36381d05cddcSAtari911 // Security: only allow files starting with "calendar" and ending with .zip, no directory traversal 36391d05cddcSAtari911 if (!preg_match('/^calendar[a-zA-Z0-9._-]*\.zip$/', $filename)) { 36401d05cddcSAtari911 $this->redirect('Invalid backup filename', 'error', 'update'); 36411d05cddcSAtari911 return; 36421d05cddcSAtari911 } 36431d05cddcSAtari911 36441d05cddcSAtari911 $backupPath = DOKU_PLUGIN . $filename; 36451d05cddcSAtari911 36461d05cddcSAtari911 if (!file_exists($backupPath)) { 36471d05cddcSAtari911 $this->redirect('Backup file not found', 'error', 'update'); 36481d05cddcSAtari911 return; 36491d05cddcSAtari911 } 36501d05cddcSAtari911 36511d05cddcSAtari911 if (@unlink($backupPath)) { 36521d05cddcSAtari911 $this->redirect('Backup deleted: ' . $filename, 'success', 'update'); 36531d05cddcSAtari911 } else { 36541d05cddcSAtari911 $this->redirect('Failed to delete backup. Check file permissions.', 'error', 'update'); 36551d05cddcSAtari911 } 36561d05cddcSAtari911 } 36571d05cddcSAtari911 36581d05cddcSAtari911 private function renameBackup() { 36591d05cddcSAtari911 global $INPUT; 36601d05cddcSAtari911 36611d05cddcSAtari911 $oldName = $INPUT->str('old_name'); 36621d05cddcSAtari911 $newName = $INPUT->str('new_name'); 36631d05cddcSAtari911 36641d05cddcSAtari911 if (empty($oldName) || empty($newName)) { 36651d05cddcSAtari911 $this->redirect('Missing filename(s)', 'error', 'update'); 36661d05cddcSAtari911 return; 36671d05cddcSAtari911 } 36681d05cddcSAtari911 36691d05cddcSAtari911 // Security: validate filenames 36701d05cddcSAtari911 if (!preg_match('/^[a-zA-Z0-9._-]+\.zip$/', $oldName) || !preg_match('/^[a-zA-Z0-9._-]+\.zip$/', $newName)) { 36711d05cddcSAtari911 $this->redirect('Invalid filename format', 'error', 'update'); 36721d05cddcSAtari911 return; 36731d05cddcSAtari911 } 36741d05cddcSAtari911 36751d05cddcSAtari911 $oldPath = DOKU_PLUGIN . $oldName; 36761d05cddcSAtari911 $newPath = DOKU_PLUGIN . $newName; 36771d05cddcSAtari911 36781d05cddcSAtari911 if (!file_exists($oldPath)) { 36791d05cddcSAtari911 $this->redirect('Backup file not found', 'error', 'update'); 36801d05cddcSAtari911 return; 36811d05cddcSAtari911 } 36821d05cddcSAtari911 36831d05cddcSAtari911 if (file_exists($newPath)) { 36841d05cddcSAtari911 $this->redirect('A file with the new name already exists', 'error', 'update'); 36851d05cddcSAtari911 return; 36861d05cddcSAtari911 } 36871d05cddcSAtari911 36881d05cddcSAtari911 if (@rename($oldPath, $newPath)) { 36891d05cddcSAtari911 $this->redirect('Backup renamed: ' . $oldName . ' → ' . $newName, 'success', 'update'); 36901d05cddcSAtari911 } else { 36911d05cddcSAtari911 $this->redirect('Failed to rename backup. Check file permissions.', 'error', 'update'); 36921d05cddcSAtari911 } 36931d05cddcSAtari911 } 36941d05cddcSAtari911 36951d05cddcSAtari911 private function restoreBackup() { 36961d05cddcSAtari911 global $INPUT; 36971d05cddcSAtari911 36981d05cddcSAtari911 $filename = $INPUT->str('backup_file'); 36991d05cddcSAtari911 37001d05cddcSAtari911 if (empty($filename)) { 37011d05cddcSAtari911 $this->redirect('No backup file specified', 'error', 'update'); 37021d05cddcSAtari911 return; 37031d05cddcSAtari911 } 37041d05cddcSAtari911 37051d05cddcSAtari911 // Security: only allow files starting with "calendar" and ending with .zip, no directory traversal 37061d05cddcSAtari911 if (!preg_match('/^calendar[a-zA-Z0-9._-]*\.zip$/', $filename)) { 37071d05cddcSAtari911 $this->redirect('Invalid backup filename', 'error', 'update'); 37081d05cddcSAtari911 return; 37091d05cddcSAtari911 } 37101d05cddcSAtari911 37111d05cddcSAtari911 $backupPath = DOKU_PLUGIN . $filename; 37121d05cddcSAtari911 $pluginDir = DOKU_PLUGIN . 'calendar/'; 37131d05cddcSAtari911 37141d05cddcSAtari911 if (!file_exists($backupPath)) { 37151d05cddcSAtari911 $this->redirect('Backup file not found', 'error', 'update'); 37161d05cddcSAtari911 return; 37171d05cddcSAtari911 } 37181d05cddcSAtari911 37191d05cddcSAtari911 // Check if plugin directory is writable 37201d05cddcSAtari911 if (!is_writable($pluginDir)) { 37211d05cddcSAtari911 $this->redirect('Plugin directory is not writable. Please check permissions.', 'error', 'update'); 37221d05cddcSAtari911 return; 37231d05cddcSAtari911 } 37241d05cddcSAtari911 37251d05cddcSAtari911 // Extract backup to temp directory 37261d05cddcSAtari911 $tempDir = DOKU_PLUGIN . 'calendar_restore_temp/'; 37271d05cddcSAtari911 if (is_dir($tempDir)) { 37281d05cddcSAtari911 $this->deleteDirectory($tempDir); 37291d05cddcSAtari911 } 37301d05cddcSAtari911 mkdir($tempDir); 37311d05cddcSAtari911 37321d05cddcSAtari911 $zip = new ZipArchive(); 37331d05cddcSAtari911 if ($zip->open($backupPath) !== TRUE) { 37341d05cddcSAtari911 $this->redirect('Failed to open backup ZIP file', 'error', 'update'); 37351d05cddcSAtari911 return; 37361d05cddcSAtari911 } 37371d05cddcSAtari911 37381d05cddcSAtari911 $zip->extractTo($tempDir); 37391d05cddcSAtari911 $zip->close(); 37401d05cddcSAtari911 37411d05cddcSAtari911 // The backup contains a "calendar/" folder 37421d05cddcSAtari911 $sourceDir = $tempDir . 'calendar/'; 37431d05cddcSAtari911 37441d05cddcSAtari911 if (!is_dir($sourceDir)) { 37451d05cddcSAtari911 $this->deleteDirectory($tempDir); 37461d05cddcSAtari911 $this->redirect('Invalid backup structure', 'error', 'update'); 37471d05cddcSAtari911 return; 37481d05cddcSAtari911 } 37491d05cddcSAtari911 37501d05cddcSAtari911 // Delete current plugin directory contents 37511d05cddcSAtari911 $this->deleteDirectoryContents($pluginDir, []); 37521d05cddcSAtari911 37531d05cddcSAtari911 // Copy backup files to plugin directory 37541d05cddcSAtari911 $this->recursiveCopy($sourceDir, $pluginDir); 37551d05cddcSAtari911 37561d05cddcSAtari911 // Cleanup temp directory 37571d05cddcSAtari911 $this->deleteDirectory($tempDir); 37581d05cddcSAtari911 37591d05cddcSAtari911 $this->redirect('Plugin restored from backup: ' . $filename, 'success', 'update'); 37601d05cddcSAtari911 } 37611d05cddcSAtari911 37629ccd446eSAtari911 private function createManualBackup() { 37639ccd446eSAtari911 $pluginDir = DOKU_PLUGIN . 'calendar/'; 37649ccd446eSAtari911 37659ccd446eSAtari911 // Check if plugin directory is readable 37669ccd446eSAtari911 if (!is_readable($pluginDir)) { 37679ccd446eSAtari911 $this->redirect('Plugin directory is not readable. Please check permissions.', 'error', 'update'); 37689ccd446eSAtari911 return; 37699ccd446eSAtari911 } 37709ccd446eSAtari911 37719ccd446eSAtari911 // Check if parent directory is writable (for saving backup) 37729ccd446eSAtari911 if (!is_writable(DOKU_PLUGIN)) { 37739ccd446eSAtari911 $this->redirect('Plugin parent directory is not writable. Cannot save backup.', 'error', 'update'); 37749ccd446eSAtari911 return; 37759ccd446eSAtari911 } 37769ccd446eSAtari911 37779ccd446eSAtari911 // Get current version 37789ccd446eSAtari911 $pluginInfo = $pluginDir . 'plugin.info.txt'; 37799ccd446eSAtari911 $version = 'unknown'; 37809ccd446eSAtari911 if (file_exists($pluginInfo)) { 37819ccd446eSAtari911 $info = confToHash($pluginInfo); 37829ccd446eSAtari911 $version = $info['version'] ?? ($info['date'] ?? 'unknown'); 37839ccd446eSAtari911 } 37849ccd446eSAtari911 37859ccd446eSAtari911 $backupName = 'calendar.backup.v' . $version . '.manual.' . date('Y-m-d_H-i-s') . '.zip'; 37869ccd446eSAtari911 $backupPath = DOKU_PLUGIN . $backupName; 37879ccd446eSAtari911 37889ccd446eSAtari911 try { 37899ccd446eSAtari911 $zip = new ZipArchive(); 37909ccd446eSAtari911 if ($zip->open($backupPath, ZipArchive::CREATE) === TRUE) { 37919ccd446eSAtari911 $fileCount = $this->addDirectoryToZip($zip, $pluginDir, 'calendar/'); 37929ccd446eSAtari911 $zip->close(); 37939ccd446eSAtari911 37949ccd446eSAtari911 // Verify backup was created and has content 37959ccd446eSAtari911 if (!file_exists($backupPath)) { 37969ccd446eSAtari911 $this->redirect('Backup file was not created', 'error', 'update'); 37979ccd446eSAtari911 return; 37989ccd446eSAtari911 } 37999ccd446eSAtari911 38009ccd446eSAtari911 $backupSize = filesize($backupPath); 38019ccd446eSAtari911 if ($backupSize < 1000) { // Backup should be at least 1KB 38029ccd446eSAtari911 @unlink($backupPath); 38039ccd446eSAtari911 $this->redirect('Backup file is too small (' . $this->formatBytes($backupSize) . '). Only ' . $fileCount . ' files were added. Backup failed.', 'error', 'update'); 38049ccd446eSAtari911 return; 38059ccd446eSAtari911 } 38069ccd446eSAtari911 38079ccd446eSAtari911 if ($fileCount < 10) { // Should have at least 10 files 38089ccd446eSAtari911 @unlink($backupPath); 38099ccd446eSAtari911 $this->redirect('Backup incomplete: Only ' . $fileCount . ' files were added (expected 30+). Backup failed.', 'error', 'update'); 38109ccd446eSAtari911 return; 38119ccd446eSAtari911 } 38129ccd446eSAtari911 38139ccd446eSAtari911 // Success! 38149ccd446eSAtari911 $this->redirect('✓ Manual backup created successfully: ' . $backupName . ' (' . $this->formatBytes($backupSize) . ', ' . $fileCount . ' files)', 'success', 'update'); 38159ccd446eSAtari911 38169ccd446eSAtari911 } else { 38179ccd446eSAtari911 $this->redirect('Failed to create backup ZIP file', 'error', 'update'); 38189ccd446eSAtari911 return; 38199ccd446eSAtari911 } 38209ccd446eSAtari911 } catch (Exception $e) { 38219ccd446eSAtari911 if (file_exists($backupPath)) { 38229ccd446eSAtari911 @unlink($backupPath); 38239ccd446eSAtari911 } 38249ccd446eSAtari911 $this->redirect('Backup failed: ' . $e->getMessage(), 'error', 'update'); 38259ccd446eSAtari911 return; 38269ccd446eSAtari911 } 38279ccd446eSAtari911 } 38289ccd446eSAtari911 38291d05cddcSAtari911 private function addDirectoryToZip($zip, $dir, $zipPath = '') { 38309ccd446eSAtari911 $fileCount = 0; 38319ccd446eSAtari911 $errors = []; 38329ccd446eSAtari911 38339ccd446eSAtari911 if (!is_dir($dir)) { 38349ccd446eSAtari911 throw new Exception("Directory does not exist: $dir"); 38359ccd446eSAtari911 } 38369ccd446eSAtari911 38379ccd446eSAtari911 if (!is_readable($dir)) { 38389ccd446eSAtari911 throw new Exception("Directory is not readable: $dir"); 38399ccd446eSAtari911 } 38409ccd446eSAtari911 38411d05cddcSAtari911 try { 38421d05cddcSAtari911 $files = new RecursiveIteratorIterator( 38431d05cddcSAtari911 new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), 38441d05cddcSAtari911 RecursiveIteratorIterator::LEAVES_ONLY 38451d05cddcSAtari911 ); 38461d05cddcSAtari911 38471d05cddcSAtari911 foreach ($files as $file) { 38481d05cddcSAtari911 if (!$file->isDir()) { 38491d05cddcSAtari911 $filePath = $file->getRealPath(); 38501d05cddcSAtari911 if ($filePath && is_readable($filePath)) { 38511d05cddcSAtari911 $relativePath = $zipPath . substr($filePath, strlen($dir)); 38529ccd446eSAtari911 38539ccd446eSAtari911 if ($zip->addFile($filePath, $relativePath)) { 38549ccd446eSAtari911 $fileCount++; 38559ccd446eSAtari911 } else { 38569ccd446eSAtari911 $errors[] = "Failed to add: " . basename($filePath); 38579ccd446eSAtari911 } 38589ccd446eSAtari911 } else { 38599ccd446eSAtari911 $errors[] = "Cannot read: " . ($filePath ? basename($filePath) : 'unknown'); 38601d05cddcSAtari911 } 38611d05cddcSAtari911 } 38621d05cddcSAtari911 } 38639ccd446eSAtari911 38649ccd446eSAtari911 // Log any errors but don't fail if we got most files 38659ccd446eSAtari911 if (!empty($errors) && count($errors) < 5) { 38669ccd446eSAtari911 foreach ($errors as $error) { 38679ccd446eSAtari911 error_log('Calendar plugin backup warning: ' . $error); 38689ccd446eSAtari911 } 38699ccd446eSAtari911 } 38709ccd446eSAtari911 38719ccd446eSAtari911 // If too many errors, fail 38729ccd446eSAtari911 if (count($errors) > 5) { 38739ccd446eSAtari911 throw new Exception("Too many errors adding files to backup: " . implode(', ', array_slice($errors, 0, 5))); 38749ccd446eSAtari911 } 38759ccd446eSAtari911 38761d05cddcSAtari911 } catch (Exception $e) { 38779ccd446eSAtari911 error_log('Calendar plugin backup error: ' . $e->getMessage()); 38789ccd446eSAtari911 throw $e; 38791d05cddcSAtari911 } 38809ccd446eSAtari911 38819ccd446eSAtari911 return $fileCount; 38821d05cddcSAtari911 } 38831d05cddcSAtari911 38841d05cddcSAtari911 private function deleteDirectory($dir) { 38851d05cddcSAtari911 if (!is_dir($dir)) return; 38861d05cddcSAtari911 38871d05cddcSAtari911 try { 38881d05cddcSAtari911 $files = new RecursiveIteratorIterator( 38891d05cddcSAtari911 new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), 38901d05cddcSAtari911 RecursiveIteratorIterator::CHILD_FIRST 38911d05cddcSAtari911 ); 38921d05cddcSAtari911 38931d05cddcSAtari911 foreach ($files as $file) { 38941d05cddcSAtari911 if ($file->isDir()) { 38951d05cddcSAtari911 @rmdir($file->getRealPath()); 38961d05cddcSAtari911 } else { 38971d05cddcSAtari911 @unlink($file->getRealPath()); 38981d05cddcSAtari911 } 38991d05cddcSAtari911 } 39001d05cddcSAtari911 39011d05cddcSAtari911 @rmdir($dir); 39021d05cddcSAtari911 } catch (Exception $e) { 39031d05cddcSAtari911 error_log('Calendar plugin delete directory error: ' . $e->getMessage()); 39041d05cddcSAtari911 } 39051d05cddcSAtari911 } 39061d05cddcSAtari911 39071d05cddcSAtari911 private function deleteDirectoryContents($dir, $preserve = []) { 39081d05cddcSAtari911 if (!is_dir($dir)) return; 39091d05cddcSAtari911 39101d05cddcSAtari911 $items = scandir($dir); 39111d05cddcSAtari911 foreach ($items as $item) { 39121d05cddcSAtari911 if ($item === '.' || $item === '..') continue; 39131d05cddcSAtari911 if (in_array($item, $preserve)) continue; 39141d05cddcSAtari911 39151d05cddcSAtari911 $path = $dir . $item; 39161d05cddcSAtari911 if (is_dir($path)) { 39171d05cddcSAtari911 $this->deleteDirectory($path); 39181d05cddcSAtari911 } else { 39191d05cddcSAtari911 unlink($path); 39201d05cddcSAtari911 } 39211d05cddcSAtari911 } 39221d05cddcSAtari911 } 39231d05cddcSAtari911 39241d05cddcSAtari911 private function recursiveCopy($src, $dst) { 39251d05cddcSAtari911 $dir = opendir($src); 39261d05cddcSAtari911 @mkdir($dst); 39271d05cddcSAtari911 39281d05cddcSAtari911 while (($file = readdir($dir)) !== false) { 39291d05cddcSAtari911 if ($file !== '.' && $file !== '..') { 39301d05cddcSAtari911 if (is_dir($src . '/' . $file)) { 39311d05cddcSAtari911 $this->recursiveCopy($src . '/' . $file, $dst . '/' . $file); 39321d05cddcSAtari911 } else { 39331d05cddcSAtari911 copy($src . '/' . $file, $dst . '/' . $file); 39341d05cddcSAtari911 } 39351d05cddcSAtari911 } 39361d05cddcSAtari911 } 39371d05cddcSAtari911 39381d05cddcSAtari911 closedir($dir); 39391d05cddcSAtari911 } 39401d05cddcSAtari911 39411d05cddcSAtari911 private function formatBytes($bytes) { 39421d05cddcSAtari911 if ($bytes >= 1073741824) { 39431d05cddcSAtari911 return number_format($bytes / 1073741824, 2) . ' GB'; 39441d05cddcSAtari911 } elseif ($bytes >= 1048576) { 39451d05cddcSAtari911 return number_format($bytes / 1048576, 2) . ' MB'; 39461d05cddcSAtari911 } elseif ($bytes >= 1024) { 39471d05cddcSAtari911 return number_format($bytes / 1024, 2) . ' KB'; 39481d05cddcSAtari911 } else { 39491d05cddcSAtari911 return $bytes . ' bytes'; 39501d05cddcSAtari911 } 39511d05cddcSAtari911 } 39521d05cddcSAtari911 39531d05cddcSAtari911 private function findPhpBinary() { 39541d05cddcSAtari911 // Try PHP_BINARY constant first (most reliable if available) 39551d05cddcSAtari911 if (defined('PHP_BINARY') && !empty(PHP_BINARY) && is_executable(PHP_BINARY)) { 39561d05cddcSAtari911 return escapeshellarg(PHP_BINARY); 39571d05cddcSAtari911 } 39581d05cddcSAtari911 39591d05cddcSAtari911 // Try common PHP binary locations 39601d05cddcSAtari911 $possiblePaths = [ 39611d05cddcSAtari911 '/usr/bin/php', 39621d05cddcSAtari911 '/usr/bin/php8.1', 39631d05cddcSAtari911 '/usr/bin/php8.2', 39641d05cddcSAtari911 '/usr/bin/php8.3', 39651d05cddcSAtari911 '/usr/bin/php7.4', 39661d05cddcSAtari911 '/usr/local/bin/php', 39671d05cddcSAtari911 'php' // Last resort - rely on PATH 39681d05cddcSAtari911 ]; 39691d05cddcSAtari911 39701d05cddcSAtari911 foreach ($possiblePaths as $path) { 39711d05cddcSAtari911 // Test if this PHP binary works 39721d05cddcSAtari911 $testOutput = []; 39731d05cddcSAtari911 $testReturn = 0; 39741d05cddcSAtari911 exec($path . ' -v 2>&1', $testOutput, $testReturn); 39751d05cddcSAtari911 39761d05cddcSAtari911 if ($testReturn === 0) { 39771d05cddcSAtari911 return ($path === 'php') ? 'php' : escapeshellarg($path); 39781d05cddcSAtari911 } 39791d05cddcSAtari911 } 39801d05cddcSAtari911 39811d05cddcSAtari911 // Fallback to 'php' and hope it's in PATH 39821d05cddcSAtari911 return 'php'; 39831d05cddcSAtari911 } 39841d05cddcSAtari911 39851d05cddcSAtari911 private function redirect($message, $type = 'success', $tab = null) { 39861d05cddcSAtari911 $url = '?do=admin&page=calendar'; 39871d05cddcSAtari911 if ($tab) { 39881d05cddcSAtari911 $url .= '&tab=' . $tab; 39891d05cddcSAtari911 } 39901d05cddcSAtari911 $url .= '&msg=' . urlencode($message) . '&msgtype=' . $type; 39911d05cddcSAtari911 header('Location: ' . $url); 39921d05cddcSAtari911 exit; 39931d05cddcSAtari911 } 39941d05cddcSAtari911 39951d05cddcSAtari911 private function getLog() { 39961d05cddcSAtari911 global $INPUT; 39971d05cddcSAtari911 39981d05cddcSAtari911 if ($INPUT->str('call') === 'ajax') { 39991d05cddcSAtari911 header('Content-Type: application/json'); 40001d05cddcSAtari911 40011d05cddcSAtari911 $logFile = DOKU_PLUGIN . 'calendar/sync.log'; 40021d05cddcSAtari911 $log = ''; 40031d05cddcSAtari911 40041d05cddcSAtari911 if (file_exists($logFile)) { 40051d05cddcSAtari911 // Get last 500 lines 40061d05cddcSAtari911 $lines = file($logFile); 40071d05cddcSAtari911 if ($lines !== false) { 40081d05cddcSAtari911 $lines = array_slice($lines, -500); 40091d05cddcSAtari911 $log = implode('', $lines); 40101d05cddcSAtari911 } 40111d05cddcSAtari911 } else { 40121d05cddcSAtari911 $log = "No log file found. Sync hasn't run yet."; 40131d05cddcSAtari911 } 40141d05cddcSAtari911 40151d05cddcSAtari911 echo json_encode(['log' => $log]); 40161d05cddcSAtari911 exit; 40171d05cddcSAtari911 } 40181d05cddcSAtari911 } 40191d05cddcSAtari911 40201d05cddcSAtari911 private function exportConfig() { 40211d05cddcSAtari911 global $INPUT; 40221d05cddcSAtari911 40231d05cddcSAtari911 if ($INPUT->str('call') === 'ajax') { 40241d05cddcSAtari911 header('Content-Type: application/json'); 40251d05cddcSAtari911 40261d05cddcSAtari911 try { 40271d05cddcSAtari911 $configFile = DOKU_PLUGIN . 'calendar/sync_config.php'; 40281d05cddcSAtari911 40291d05cddcSAtari911 if (!file_exists($configFile)) { 40301d05cddcSAtari911 echo json_encode([ 40311d05cddcSAtari911 'success' => false, 40321d05cddcSAtari911 'message' => 'Config file not found' 40331d05cddcSAtari911 ]); 40341d05cddcSAtari911 exit; 40351d05cddcSAtari911 } 40361d05cddcSAtari911 40371d05cddcSAtari911 // Read config file 40381d05cddcSAtari911 $configContent = file_get_contents($configFile); 40391d05cddcSAtari911 40401d05cddcSAtari911 // Generate encryption key from DokuWiki secret 40411d05cddcSAtari911 $key = $this->getEncryptionKey(); 40421d05cddcSAtari911 40431d05cddcSAtari911 // Encrypt config 40441d05cddcSAtari911 $encrypted = $this->encryptData($configContent, $key); 40451d05cddcSAtari911 40461d05cddcSAtari911 echo json_encode([ 40471d05cddcSAtari911 'success' => true, 40481d05cddcSAtari911 'encrypted' => $encrypted, 40491d05cddcSAtari911 'message' => 'Config exported successfully' 40501d05cddcSAtari911 ]); 40511d05cddcSAtari911 exit; 40521d05cddcSAtari911 40531d05cddcSAtari911 } catch (Exception $e) { 40541d05cddcSAtari911 echo json_encode([ 40551d05cddcSAtari911 'success' => false, 40561d05cddcSAtari911 'message' => $e->getMessage() 40571d05cddcSAtari911 ]); 40581d05cddcSAtari911 exit; 40591d05cddcSAtari911 } 40601d05cddcSAtari911 } 40611d05cddcSAtari911 } 40621d05cddcSAtari911 40631d05cddcSAtari911 private function importConfig() { 40641d05cddcSAtari911 global $INPUT; 40651d05cddcSAtari911 40661d05cddcSAtari911 if ($INPUT->str('call') === 'ajax') { 40671d05cddcSAtari911 header('Content-Type: application/json'); 40681d05cddcSAtari911 40691d05cddcSAtari911 try { 40701d05cddcSAtari911 $encrypted = $_POST['encrypted_config'] ?? ''; 40711d05cddcSAtari911 40721d05cddcSAtari911 if (empty($encrypted)) { 40731d05cddcSAtari911 echo json_encode([ 40741d05cddcSAtari911 'success' => false, 40751d05cddcSAtari911 'message' => 'No config data provided' 40761d05cddcSAtari911 ]); 40771d05cddcSAtari911 exit; 40781d05cddcSAtari911 } 40791d05cddcSAtari911 40801d05cddcSAtari911 // Generate encryption key from DokuWiki secret 40811d05cddcSAtari911 $key = $this->getEncryptionKey(); 40821d05cddcSAtari911 40831d05cddcSAtari911 // Decrypt config 40841d05cddcSAtari911 $configContent = $this->decryptData($encrypted, $key); 40851d05cddcSAtari911 40861d05cddcSAtari911 if ($configContent === false) { 40871d05cddcSAtari911 echo json_encode([ 40881d05cddcSAtari911 'success' => false, 40891d05cddcSAtari911 'message' => 'Decryption failed. Invalid key or corrupted file.' 40901d05cddcSAtari911 ]); 40911d05cddcSAtari911 exit; 40921d05cddcSAtari911 } 40931d05cddcSAtari911 40941d05cddcSAtari911 // Validate PHP syntax 40951d05cddcSAtari911 $valid = @eval('?>' . $configContent); 40961d05cddcSAtari911 if ($valid === false) { 40971d05cddcSAtari911 echo json_encode([ 40981d05cddcSAtari911 'success' => false, 40991d05cddcSAtari911 'message' => 'Invalid config file format' 41001d05cddcSAtari911 ]); 41011d05cddcSAtari911 exit; 41021d05cddcSAtari911 } 41031d05cddcSAtari911 41041d05cddcSAtari911 // Write to config file 41051d05cddcSAtari911 $configFile = DOKU_PLUGIN . 'calendar/sync_config.php'; 41061d05cddcSAtari911 41071d05cddcSAtari911 // Backup existing config 41081d05cddcSAtari911 if (file_exists($configFile)) { 41091d05cddcSAtari911 $backupFile = $configFile . '.backup.' . date('Y-m-d_H-i-s'); 41101d05cddcSAtari911 copy($configFile, $backupFile); 41111d05cddcSAtari911 } 41121d05cddcSAtari911 41131d05cddcSAtari911 // Write new config 41141d05cddcSAtari911 if (file_put_contents($configFile, $configContent) === false) { 41151d05cddcSAtari911 echo json_encode([ 41161d05cddcSAtari911 'success' => false, 41171d05cddcSAtari911 'message' => 'Failed to write config file' 41181d05cddcSAtari911 ]); 41191d05cddcSAtari911 exit; 41201d05cddcSAtari911 } 41211d05cddcSAtari911 41221d05cddcSAtari911 echo json_encode([ 41231d05cddcSAtari911 'success' => true, 41241d05cddcSAtari911 'message' => 'Config imported successfully' 41251d05cddcSAtari911 ]); 41261d05cddcSAtari911 exit; 41271d05cddcSAtari911 41281d05cddcSAtari911 } catch (Exception $e) { 41291d05cddcSAtari911 echo json_encode([ 41301d05cddcSAtari911 'success' => false, 41311d05cddcSAtari911 'message' => $e->getMessage() 41321d05cddcSAtari911 ]); 41331d05cddcSAtari911 exit; 41341d05cddcSAtari911 } 41351d05cddcSAtari911 } 41361d05cddcSAtari911 } 41371d05cddcSAtari911 41381d05cddcSAtari911 private function getEncryptionKey() { 41391d05cddcSAtari911 global $conf; 41401d05cddcSAtari911 // Use DokuWiki's secret as the base for encryption 41411d05cddcSAtari911 // This ensures the key is unique per installation 41421d05cddcSAtari911 return hash('sha256', $conf['secret'] . 'calendar_config_encryption', true); 41431d05cddcSAtari911 } 41441d05cddcSAtari911 41451d05cddcSAtari911 private function encryptData($data, $key) { 41461d05cddcSAtari911 // Use AES-256-CBC encryption 41471d05cddcSAtari911 $ivLength = openssl_cipher_iv_length('aes-256-cbc'); 41481d05cddcSAtari911 $iv = openssl_random_pseudo_bytes($ivLength); 41491d05cddcSAtari911 41501d05cddcSAtari911 $encrypted = openssl_encrypt($data, 'aes-256-cbc', $key, 0, $iv); 41511d05cddcSAtari911 41521d05cddcSAtari911 // Combine IV and encrypted data, then base64 encode 41531d05cddcSAtari911 return base64_encode($iv . $encrypted); 41541d05cddcSAtari911 } 41551d05cddcSAtari911 41561d05cddcSAtari911 private function decryptData($encryptedData, $key) { 41571d05cddcSAtari911 // Decode base64 41581d05cddcSAtari911 $data = base64_decode($encryptedData); 41591d05cddcSAtari911 41601d05cddcSAtari911 if ($data === false) { 41611d05cddcSAtari911 return false; 41621d05cddcSAtari911 } 41631d05cddcSAtari911 41641d05cddcSAtari911 // Extract IV and encrypted content 41651d05cddcSAtari911 $ivLength = openssl_cipher_iv_length('aes-256-cbc'); 41661d05cddcSAtari911 $iv = substr($data, 0, $ivLength); 41671d05cddcSAtari911 $encrypted = substr($data, $ivLength); 41681d05cddcSAtari911 41691d05cddcSAtari911 // Decrypt 41701d05cddcSAtari911 $decrypted = openssl_decrypt($encrypted, 'aes-256-cbc', $key, 0, $iv); 41711d05cddcSAtari911 41721d05cddcSAtari911 return $decrypted; 41731d05cddcSAtari911 } 41741d05cddcSAtari911 41751d05cddcSAtari911 private function clearLogFile() { 41761d05cddcSAtari911 global $INPUT; 41771d05cddcSAtari911 41781d05cddcSAtari911 if ($INPUT->str('call') === 'ajax') { 41791d05cddcSAtari911 header('Content-Type: application/json'); 41801d05cddcSAtari911 41811d05cddcSAtari911 $logFile = DOKU_PLUGIN . 'calendar/sync.log'; 41821d05cddcSAtari911 41831d05cddcSAtari911 if (file_exists($logFile)) { 41841d05cddcSAtari911 if (file_put_contents($logFile, '')) { 41851d05cddcSAtari911 echo json_encode(['success' => true]); 41861d05cddcSAtari911 } else { 41871d05cddcSAtari911 echo json_encode(['success' => false, 'message' => 'Could not clear log file']); 41881d05cddcSAtari911 } 41891d05cddcSAtari911 } else { 41901d05cddcSAtari911 echo json_encode(['success' => true, 'message' => 'No log file to clear']); 41911d05cddcSAtari911 } 41921d05cddcSAtari911 exit; 41931d05cddcSAtari911 } 41941d05cddcSAtari911 } 41951d05cddcSAtari911 41961d05cddcSAtari911 private function downloadLog() { 41971d05cddcSAtari911 $logFile = DOKU_PLUGIN . 'calendar/sync.log'; 41981d05cddcSAtari911 41991d05cddcSAtari911 if (file_exists($logFile)) { 42001d05cddcSAtari911 header('Content-Type: text/plain'); 42011d05cddcSAtari911 header('Content-Disposition: attachment; filename="calendar-sync-' . date('Y-m-d-His') . '.log"'); 42021d05cddcSAtari911 readfile($logFile); 42031d05cddcSAtari911 exit; 42041d05cddcSAtari911 } else { 42051d05cddcSAtari911 echo 'No log file found'; 42061d05cddcSAtari911 exit; 42071d05cddcSAtari911 } 42081d05cddcSAtari911 } 42091d05cddcSAtari911 42101d05cddcSAtari911 private function getEventStatistics() { 42111d05cddcSAtari911 $stats = [ 42121d05cddcSAtari911 'total_events' => 0, 42131d05cddcSAtari911 'total_namespaces' => 0, 42141d05cddcSAtari911 'total_files' => 0, 42151d05cddcSAtari911 'total_recurring' => 0, 42161d05cddcSAtari911 'by_namespace' => [], 42171d05cddcSAtari911 'last_scan' => '' 42181d05cddcSAtari911 ]; 42191d05cddcSAtari911 42201d05cddcSAtari911 $metaDir = DOKU_INC . 'data/meta/'; 42211d05cddcSAtari911 $cacheFile = DOKU_PLUGIN . 'calendar/.event_stats_cache'; 42221d05cddcSAtari911 42231d05cddcSAtari911 // Check if we have cached stats (less than 5 minutes old) 42241d05cddcSAtari911 if (file_exists($cacheFile)) { 42251d05cddcSAtari911 $cacheData = json_decode(file_get_contents($cacheFile), true); 42261d05cddcSAtari911 if ($cacheData && (time() - $cacheData['timestamp']) < 300) { 42271d05cddcSAtari911 return $cacheData['stats']; 42281d05cddcSAtari911 } 42291d05cddcSAtari911 } 42301d05cddcSAtari911 42311d05cddcSAtari911 // Scan for events 42321d05cddcSAtari911 $this->scanDirectoryForStats($metaDir, '', $stats); 42331d05cddcSAtari911 42341d05cddcSAtari911 // Count recurring events 42351d05cddcSAtari911 $recurringEvents = $this->findRecurringEvents(); 42361d05cddcSAtari911 $stats['total_recurring'] = count($recurringEvents); 42371d05cddcSAtari911 42381d05cddcSAtari911 $stats['total_namespaces'] = count($stats['by_namespace']); 42391d05cddcSAtari911 $stats['last_scan'] = date('Y-m-d H:i:s'); 42401d05cddcSAtari911 42411d05cddcSAtari911 // Cache the results 42421d05cddcSAtari911 file_put_contents($cacheFile, json_encode([ 42431d05cddcSAtari911 'timestamp' => time(), 42441d05cddcSAtari911 'stats' => $stats 42451d05cddcSAtari911 ])); 42461d05cddcSAtari911 42471d05cddcSAtari911 return $stats; 42481d05cddcSAtari911 } 42491d05cddcSAtari911 42501d05cddcSAtari911 private function scanDirectoryForStats($dir, $namespace, &$stats) { 42511d05cddcSAtari911 if (!is_dir($dir)) return; 42521d05cddcSAtari911 42531d05cddcSAtari911 $items = scandir($dir); 42541d05cddcSAtari911 foreach ($items as $item) { 42551d05cddcSAtari911 if ($item === '.' || $item === '..') continue; 42561d05cddcSAtari911 42571d05cddcSAtari911 $path = $dir . $item; 42581d05cddcSAtari911 42591d05cddcSAtari911 // Check if this is a calendar directory 42601d05cddcSAtari911 if ($item === 'calendar' && is_dir($path)) { 42611d05cddcSAtari911 $jsonFiles = glob($path . '/*.json'); 42621d05cddcSAtari911 $eventCount = 0; 42631d05cddcSAtari911 42641d05cddcSAtari911 foreach ($jsonFiles as $file) { 42651d05cddcSAtari911 $stats['total_files']++; 42661d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 42671d05cddcSAtari911 if ($data) { 42681d05cddcSAtari911 foreach ($data as $dateEvents) { 42691d05cddcSAtari911 $eventCount += count($dateEvents); 42701d05cddcSAtari911 } 42711d05cddcSAtari911 } 42721d05cddcSAtari911 } 42731d05cddcSAtari911 42741d05cddcSAtari911 $stats['total_events'] += $eventCount; 42751d05cddcSAtari911 42761d05cddcSAtari911 if ($eventCount > 0) { 42771d05cddcSAtari911 $stats['by_namespace'][$namespace] = [ 42781d05cddcSAtari911 'events' => $eventCount, 42791d05cddcSAtari911 'files' => count($jsonFiles) 42801d05cddcSAtari911 ]; 42811d05cddcSAtari911 } 42821d05cddcSAtari911 } elseif (is_dir($path)) { 42831d05cddcSAtari911 // Recurse into subdirectories 42841d05cddcSAtari911 $newNamespace = $namespace ? $namespace . ':' . $item : $item; 42851d05cddcSAtari911 $this->scanDirectoryForStats($path . '/', $newNamespace, $stats); 42861d05cddcSAtari911 } 42871d05cddcSAtari911 } 42881d05cddcSAtari911 } 42891d05cddcSAtari911 42901d05cddcSAtari911 private function rescanEvents() { 42911d05cddcSAtari911 // Clear the cache to force a rescan 42929ccd446eSAtari911 $this->clearStatsCache(); 42931d05cddcSAtari911 42941d05cddcSAtari911 // Get fresh statistics 42951d05cddcSAtari911 $stats = $this->getEventStatistics(); 42961d05cddcSAtari911 42971d05cddcSAtari911 // Build absolute redirect URL 42981d05cddcSAtari911 $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'; 42991d05cddcSAtari911 43001d05cddcSAtari911 // Redirect with success message using absolute URL 43011d05cddcSAtari911 header('Location: ' . $redirectUrl, true, 303); 43021d05cddcSAtari911 exit; 43031d05cddcSAtari911 } 43041d05cddcSAtari911 43051d05cddcSAtari911 private function exportAllEvents() { 43061d05cddcSAtari911 $metaDir = DOKU_INC . 'data/meta/'; 43071d05cddcSAtari911 $allEvents = []; 43081d05cddcSAtari911 43091d05cddcSAtari911 // Collect all events 43101d05cddcSAtari911 $this->collectAllEvents($metaDir, '', $allEvents); 43111d05cddcSAtari911 43121d05cddcSAtari911 // Create export package 43139ccd446eSAtari911 // Get current version 43149ccd446eSAtari911 $pluginInfo = DOKU_PLUGIN . 'calendar/plugin.info.txt'; 43159ccd446eSAtari911 $info = file_exists($pluginInfo) ? confToHash($pluginInfo) : []; 43169ccd446eSAtari911 $currentVersion = isset($info['version']) ? trim($info['version']) : 'unknown'; 43179ccd446eSAtari911 43181d05cddcSAtari911 $exportData = [ 43191d05cddcSAtari911 'export_date' => date('Y-m-d H:i:s'), 43209ccd446eSAtari911 'version' => $currentVersion, 43211d05cddcSAtari911 'total_events' => 0, 43221d05cddcSAtari911 'namespaces' => [] 43231d05cddcSAtari911 ]; 43241d05cddcSAtari911 43251d05cddcSAtari911 foreach ($allEvents as $namespace => $files) { 43261d05cddcSAtari911 $exportData['namespaces'][$namespace] = []; 43271d05cddcSAtari911 foreach ($files as $filename => $events) { 43281d05cddcSAtari911 $exportData['namespaces'][$namespace][$filename] = $events; 43291d05cddcSAtari911 foreach ($events as $dateEvents) { 43301d05cddcSAtari911 $exportData['total_events'] += count($dateEvents); 43311d05cddcSAtari911 } 43321d05cddcSAtari911 } 43331d05cddcSAtari911 } 43341d05cddcSAtari911 43351d05cddcSAtari911 // Send as download 43361d05cddcSAtari911 header('Content-Type: application/json'); 43371d05cddcSAtari911 header('Content-Disposition: attachment; filename="calendar-events-export-' . date('Y-m-d-His') . '.json"'); 43381d05cddcSAtari911 echo json_encode($exportData, JSON_PRETTY_PRINT); 43391d05cddcSAtari911 exit; 43401d05cddcSAtari911 } 43411d05cddcSAtari911 43421d05cddcSAtari911 private function collectAllEvents($dir, $namespace, &$allEvents) { 43431d05cddcSAtari911 if (!is_dir($dir)) return; 43441d05cddcSAtari911 43451d05cddcSAtari911 $items = scandir($dir); 43461d05cddcSAtari911 foreach ($items as $item) { 43471d05cddcSAtari911 if ($item === '.' || $item === '..') continue; 43481d05cddcSAtari911 43491d05cddcSAtari911 $path = $dir . $item; 43501d05cddcSAtari911 43511d05cddcSAtari911 // Check if this is a calendar directory 43521d05cddcSAtari911 if ($item === 'calendar' && is_dir($path)) { 43531d05cddcSAtari911 $jsonFiles = glob($path . '/*.json'); 43541d05cddcSAtari911 43551d05cddcSAtari911 if (!isset($allEvents[$namespace])) { 43561d05cddcSAtari911 $allEvents[$namespace] = []; 43571d05cddcSAtari911 } 43581d05cddcSAtari911 43591d05cddcSAtari911 foreach ($jsonFiles as $file) { 43601d05cddcSAtari911 $filename = basename($file); 43611d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 43621d05cddcSAtari911 if ($data) { 43631d05cddcSAtari911 $allEvents[$namespace][$filename] = $data; 43641d05cddcSAtari911 } 43651d05cddcSAtari911 } 43661d05cddcSAtari911 } elseif (is_dir($path)) { 43671d05cddcSAtari911 // Recurse into subdirectories 43681d05cddcSAtari911 $newNamespace = $namespace ? $namespace . ':' . $item : $item; 43691d05cddcSAtari911 $this->collectAllEvents($path . '/', $newNamespace, $allEvents); 43701d05cddcSAtari911 } 43711d05cddcSAtari911 } 43721d05cddcSAtari911 } 43731d05cddcSAtari911 43741d05cddcSAtari911 private function importAllEvents() { 43751d05cddcSAtari911 global $INPUT; 43761d05cddcSAtari911 43771d05cddcSAtari911 if (!isset($_FILES['import_file'])) { 43781d05cddcSAtari911 $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('No file uploaded') . '&msgtype=error'; 43791d05cddcSAtari911 header('Location: ' . $redirectUrl, true, 303); 43801d05cddcSAtari911 exit; 43811d05cddcSAtari911 } 43821d05cddcSAtari911 43831d05cddcSAtari911 $file = $_FILES['import_file']; 43841d05cddcSAtari911 43851d05cddcSAtari911 if ($file['error'] !== UPLOAD_ERR_OK) { 43861d05cddcSAtari911 $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('Upload error: ' . $file['error']) . '&msgtype=error'; 43871d05cddcSAtari911 header('Location: ' . $redirectUrl, true, 303); 43881d05cddcSAtari911 exit; 43891d05cddcSAtari911 } 43901d05cddcSAtari911 43911d05cddcSAtari911 // Read and decode the import file 43921d05cddcSAtari911 $importData = json_decode(file_get_contents($file['tmp_name']), true); 43931d05cddcSAtari911 43941d05cddcSAtari911 if (!$importData || !isset($importData['namespaces'])) { 43951d05cddcSAtari911 $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('Invalid import file format') . '&msgtype=error'; 43961d05cddcSAtari911 header('Location: ' . $redirectUrl, true, 303); 43971d05cddcSAtari911 exit; 43981d05cddcSAtari911 } 43991d05cddcSAtari911 44001d05cddcSAtari911 $importedCount = 0; 44011d05cddcSAtari911 $mergedCount = 0; 44021d05cddcSAtari911 44031d05cddcSAtari911 // Import events 44041d05cddcSAtari911 foreach ($importData['namespaces'] as $namespace => $files) { 44051d05cddcSAtari911 $metaDir = DOKU_INC . 'data/meta/'; 44061d05cddcSAtari911 if ($namespace) { 44071d05cddcSAtari911 $metaDir .= str_replace(':', '/', $namespace) . '/'; 44081d05cddcSAtari911 } 44091d05cddcSAtari911 $calendarDir = $metaDir . 'calendar/'; 44101d05cddcSAtari911 44111d05cddcSAtari911 // Create directory if needed 44121d05cddcSAtari911 if (!is_dir($calendarDir)) { 44131d05cddcSAtari911 mkdir($calendarDir, 0755, true); 44141d05cddcSAtari911 } 44151d05cddcSAtari911 44161d05cddcSAtari911 foreach ($files as $filename => $events) { 44171d05cddcSAtari911 $targetFile = $calendarDir . $filename; 44181d05cddcSAtari911 44191d05cddcSAtari911 // If file exists, merge events 44201d05cddcSAtari911 if (file_exists($targetFile)) { 44211d05cddcSAtari911 $existing = json_decode(file_get_contents($targetFile), true); 44221d05cddcSAtari911 if ($existing) { 44231d05cddcSAtari911 foreach ($events as $date => $dateEvents) { 44241d05cddcSAtari911 if (!isset($existing[$date])) { 44251d05cddcSAtari911 $existing[$date] = []; 44261d05cddcSAtari911 } 44271d05cddcSAtari911 foreach ($dateEvents as $event) { 44281d05cddcSAtari911 // Check if event with same ID exists 44291d05cddcSAtari911 $found = false; 44301d05cddcSAtari911 foreach ($existing[$date] as $existingEvent) { 44311d05cddcSAtari911 if ($existingEvent['id'] === $event['id']) { 44321d05cddcSAtari911 $found = true; 44331d05cddcSAtari911 break; 44341d05cddcSAtari911 } 44351d05cddcSAtari911 } 44361d05cddcSAtari911 if (!$found) { 44371d05cddcSAtari911 $existing[$date][] = $event; 44381d05cddcSAtari911 $importedCount++; 44391d05cddcSAtari911 } else { 44401d05cddcSAtari911 $mergedCount++; 44411d05cddcSAtari911 } 44421d05cddcSAtari911 } 44431d05cddcSAtari911 } 44441d05cddcSAtari911 file_put_contents($targetFile, json_encode($existing, JSON_PRETTY_PRINT)); 44451d05cddcSAtari911 } 44461d05cddcSAtari911 } else { 44471d05cddcSAtari911 // New file 44481d05cddcSAtari911 file_put_contents($targetFile, json_encode($events, JSON_PRETTY_PRINT)); 44491d05cddcSAtari911 foreach ($events as $dateEvents) { 44501d05cddcSAtari911 $importedCount += count($dateEvents); 44511d05cddcSAtari911 } 44521d05cddcSAtari911 } 44531d05cddcSAtari911 } 44541d05cddcSAtari911 } 44551d05cddcSAtari911 44561d05cddcSAtari911 // Clear cache 44579ccd446eSAtari911 $this->clearStatsCache(); 44581d05cddcSAtari911 44591d05cddcSAtari911 $message = "Import complete! Imported $importedCount new events"; 44601d05cddcSAtari911 if ($mergedCount > 0) { 44611d05cddcSAtari911 $message .= ", skipped $mergedCount duplicates"; 44621d05cddcSAtari911 } 44631d05cddcSAtari911 44641d05cddcSAtari911 $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode($message) . '&msgtype=success'; 44651d05cddcSAtari911 header('Location: ' . $redirectUrl, true, 303); 44661d05cddcSAtari911 exit; 44671d05cddcSAtari911 } 44681d05cddcSAtari911 44691d05cddcSAtari911 private function previewCleanup() { 44701d05cddcSAtari911 global $INPUT; 44711d05cddcSAtari911 44721d05cddcSAtari911 $cleanupType = $INPUT->str('cleanup_type', 'age'); 44731d05cddcSAtari911 $namespaceFilter = $INPUT->str('namespace_filter', ''); 44741d05cddcSAtari911 44751d05cddcSAtari911 // Debug info 44761d05cddcSAtari911 $debug = []; 44771d05cddcSAtari911 $debug['cleanup_type'] = $cleanupType; 44781d05cddcSAtari911 $debug['namespace_filter'] = $namespaceFilter; 44791d05cddcSAtari911 $debug['age_value'] = $INPUT->int('age_value', 6); 44801d05cddcSAtari911 $debug['age_unit'] = $INPUT->str('age_unit', 'months'); 44811d05cddcSAtari911 $debug['range_start'] = $INPUT->str('range_start', ''); 44821d05cddcSAtari911 $debug['range_end'] = $INPUT->str('range_end', ''); 44831d05cddcSAtari911 $debug['delete_completed'] = $INPUT->bool('delete_completed', false); 44841d05cddcSAtari911 $debug['delete_past'] = $INPUT->bool('delete_past', false); 44851d05cddcSAtari911 44861d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 44871d05cddcSAtari911 $debug['data_dir'] = $dataDir; 44881d05cddcSAtari911 $debug['data_dir_exists'] = is_dir($dataDir); 44891d05cddcSAtari911 44901d05cddcSAtari911 $eventsToDelete = $this->findEventsToCleanup($cleanupType, $namespaceFilter); 44911d05cddcSAtari911 44921d05cddcSAtari911 // Merge with scan debug info 44931d05cddcSAtari911 if (isset($this->_cleanupDebug)) { 44941d05cddcSAtari911 $debug = array_merge($debug, $this->_cleanupDebug); 44951d05cddcSAtari911 } 44961d05cddcSAtari911 44971d05cddcSAtari911 // Return JSON for preview with debug info 44981d05cddcSAtari911 header('Content-Type: application/json'); 44991d05cddcSAtari911 echo json_encode([ 45001d05cddcSAtari911 'count' => count($eventsToDelete), 45011d05cddcSAtari911 'events' => array_slice($eventsToDelete, 0, 50), // Limit to 50 for preview 45021d05cddcSAtari911 'debug' => $debug 45031d05cddcSAtari911 ]); 45041d05cddcSAtari911 exit; 45051d05cddcSAtari911 } 45061d05cddcSAtari911 45071d05cddcSAtari911 private function cleanupEvents() { 45081d05cddcSAtari911 global $INPUT; 45091d05cddcSAtari911 45101d05cddcSAtari911 $cleanupType = $INPUT->str('cleanup_type', 'age'); 45111d05cddcSAtari911 $namespaceFilter = $INPUT->str('namespace_filter', ''); 45121d05cddcSAtari911 45131d05cddcSAtari911 // Create backup first 45141d05cddcSAtari911 $backupDir = DOKU_PLUGIN . 'calendar/backups/'; 45151d05cddcSAtari911 if (!is_dir($backupDir)) { 45161d05cddcSAtari911 mkdir($backupDir, 0755, true); 45171d05cddcSAtari911 } 45181d05cddcSAtari911 45191d05cddcSAtari911 $backupFile = $backupDir . 'before-cleanup-' . date('Y-m-d-His') . '.zip'; 45201d05cddcSAtari911 $this->createBackup($backupFile); 45211d05cddcSAtari911 45221d05cddcSAtari911 // Find events to delete 45231d05cddcSAtari911 $eventsToDelete = $this->findEventsToCleanup($cleanupType, $namespaceFilter); 45241d05cddcSAtari911 $deletedCount = 0; 45251d05cddcSAtari911 45261d05cddcSAtari911 // Group by file 45271d05cddcSAtari911 $fileGroups = []; 45281d05cddcSAtari911 foreach ($eventsToDelete as $evt) { 45291d05cddcSAtari911 $fileGroups[$evt['file']][] = $evt; 45301d05cddcSAtari911 } 45311d05cddcSAtari911 45321d05cddcSAtari911 // Delete from each file 45331d05cddcSAtari911 foreach ($fileGroups as $file => $events) { 45341d05cddcSAtari911 if (!file_exists($file)) continue; 45351d05cddcSAtari911 45361d05cddcSAtari911 $json = file_get_contents($file); 45371d05cddcSAtari911 $data = json_decode($json, true); 45381d05cddcSAtari911 45391d05cddcSAtari911 if (!$data) continue; 45401d05cddcSAtari911 45411d05cddcSAtari911 // Remove events 45421d05cddcSAtari911 foreach ($events as $evt) { 45431d05cddcSAtari911 if (isset($data[$evt['date']])) { 45441d05cddcSAtari911 $data[$evt['date']] = array_filter($data[$evt['date']], function($e) use ($evt) { 45451d05cddcSAtari911 return $e['id'] !== $evt['id']; 45461d05cddcSAtari911 }); 45471d05cddcSAtari911 45481d05cddcSAtari911 // Remove date key if empty 45491d05cddcSAtari911 if (empty($data[$evt['date']])) { 45501d05cddcSAtari911 unset($data[$evt['date']]); 45511d05cddcSAtari911 } 45521d05cddcSAtari911 45531d05cddcSAtari911 $deletedCount++; 45541d05cddcSAtari911 } 45551d05cddcSAtari911 } 45561d05cddcSAtari911 45571d05cddcSAtari911 // Save file or delete if empty 45581d05cddcSAtari911 if (empty($data)) { 45591d05cddcSAtari911 unlink($file); 45601d05cddcSAtari911 } else { 45611d05cddcSAtari911 file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); 45621d05cddcSAtari911 } 45631d05cddcSAtari911 } 45641d05cddcSAtari911 45651d05cddcSAtari911 // Clear cache 45669ccd446eSAtari911 $this->clearStatsCache(); 45671d05cddcSAtari911 45681d05cddcSAtari911 $message = "Cleanup complete! Deleted $deletedCount event(s). Backup created: " . basename($backupFile); 45691d05cddcSAtari911 $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode($message) . '&msgtype=success'; 45701d05cddcSAtari911 header('Location: ' . $redirectUrl, true, 303); 45711d05cddcSAtari911 exit; 45721d05cddcSAtari911 } 45731d05cddcSAtari911 45741d05cddcSAtari911 private function findEventsToCleanup($cleanupType, $namespaceFilter) { 45751d05cddcSAtari911 global $INPUT; 45761d05cddcSAtari911 45771d05cddcSAtari911 $eventsToDelete = []; 45781d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 45791d05cddcSAtari911 45801d05cddcSAtari911 $debug = []; 45811d05cddcSAtari911 $debug['scanned_dirs'] = []; 45821d05cddcSAtari911 $debug['found_files'] = []; 45831d05cddcSAtari911 45841d05cddcSAtari911 // Calculate cutoff date for age-based cleanup 45851d05cddcSAtari911 $cutoffDate = null; 45861d05cddcSAtari911 if ($cleanupType === 'age') { 45871d05cddcSAtari911 $ageValue = $INPUT->int('age_value', 6); 45881d05cddcSAtari911 $ageUnit = $INPUT->str('age_unit', 'months'); 45891d05cddcSAtari911 45901d05cddcSAtari911 if ($ageUnit === 'years') { 45911d05cddcSAtari911 $ageValue *= 12; // Convert to months 45921d05cddcSAtari911 } 45931d05cddcSAtari911 45941d05cddcSAtari911 $cutoffDate = date('Y-m-d', strtotime("-$ageValue months")); 45951d05cddcSAtari911 $debug['cutoff_date'] = $cutoffDate; 45961d05cddcSAtari911 } 45971d05cddcSAtari911 45981d05cddcSAtari911 // Get date range for range-based cleanup 45991d05cddcSAtari911 $rangeStart = $cleanupType === 'range' ? $INPUT->str('range_start', '') : null; 46001d05cddcSAtari911 $rangeEnd = $cleanupType === 'range' ? $INPUT->str('range_end', '') : null; 46011d05cddcSAtari911 46021d05cddcSAtari911 // Get status filters 46031d05cddcSAtari911 $deleteCompleted = $cleanupType === 'status' && $INPUT->bool('delete_completed', false); 46041d05cddcSAtari911 $deletePast = $cleanupType === 'status' && $INPUT->bool('delete_past', false); 46051d05cddcSAtari911 46061d05cddcSAtari911 // Check root calendar directory first (blank/default namespace) 46071d05cddcSAtari911 $rootCalendarDir = $dataDir . 'calendar'; 46081d05cddcSAtari911 $debug['root_calendar_dir'] = $rootCalendarDir; 46091d05cddcSAtari911 $debug['root_exists'] = is_dir($rootCalendarDir); 46101d05cddcSAtari911 46111d05cddcSAtari911 if (is_dir($rootCalendarDir)) { 46121d05cddcSAtari911 if (!$namespaceFilter || $namespaceFilter === '' || $namespaceFilter === 'default') { 46131d05cddcSAtari911 $debug['scanned_dirs'][] = $rootCalendarDir; 46141d05cddcSAtari911 $files = glob($rootCalendarDir . '/*.json'); 46151d05cddcSAtari911 $debug['found_files'] = array_merge($debug['found_files'], $files); 46161d05cddcSAtari911 $this->processCalendarFiles($rootCalendarDir, '', $eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast); 46171d05cddcSAtari911 } 46181d05cddcSAtari911 } 46191d05cddcSAtari911 46201d05cddcSAtari911 // Scan all namespace directories 46211d05cddcSAtari911 $namespaceDirs = glob($dataDir . '*', GLOB_ONLYDIR); 46221d05cddcSAtari911 $debug['namespace_dirs_found'] = $namespaceDirs; 46231d05cddcSAtari911 46241d05cddcSAtari911 foreach ($namespaceDirs as $nsDir) { 46251d05cddcSAtari911 $namespace = basename($nsDir); 46261d05cddcSAtari911 46271d05cddcSAtari911 // Skip the root 'calendar' dir (already processed above) 46281d05cddcSAtari911 if ($namespace === 'calendar') continue; 46291d05cddcSAtari911 46301d05cddcSAtari911 // Check namespace filter 46311d05cddcSAtari911 if ($namespaceFilter && strpos($namespace, $namespaceFilter) === false) { 46321d05cddcSAtari911 continue; 46331d05cddcSAtari911 } 46341d05cddcSAtari911 46351d05cddcSAtari911 $calendarDir = $nsDir . '/calendar'; 46361d05cddcSAtari911 $debug['checked_calendar_dirs'][] = $calendarDir; 46371d05cddcSAtari911 46381d05cddcSAtari911 if (!is_dir($calendarDir)) { 46391d05cddcSAtari911 $debug['missing_calendar_dirs'][] = $calendarDir; 46401d05cddcSAtari911 continue; 46411d05cddcSAtari911 } 46421d05cddcSAtari911 46431d05cddcSAtari911 $debug['scanned_dirs'][] = $calendarDir; 46441d05cddcSAtari911 $files = glob($calendarDir . '/*.json'); 46451d05cddcSAtari911 $debug['found_files'] = array_merge($debug['found_files'], $files); 46461d05cddcSAtari911 $this->processCalendarFiles($calendarDir, $namespace, $eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast); 46471d05cddcSAtari911 } 46481d05cddcSAtari911 46491d05cddcSAtari911 // Store debug info globally for preview 46501d05cddcSAtari911 $this->_cleanupDebug = $debug; 46511d05cddcSAtari911 46521d05cddcSAtari911 return $eventsToDelete; 46531d05cddcSAtari911 } 46541d05cddcSAtari911 46551d05cddcSAtari911 private function processCalendarFiles($calendarDir, $namespace, &$eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast) { 46561d05cddcSAtari911 foreach (glob($calendarDir . '/*.json') as $file) { 46571d05cddcSAtari911 $json = file_get_contents($file); 46581d05cddcSAtari911 $data = json_decode($json, true); 46591d05cddcSAtari911 46601d05cddcSAtari911 if (!$data) continue; 46611d05cddcSAtari911 46621d05cddcSAtari911 foreach ($data as $date => $dateEvents) { 46631d05cddcSAtari911 foreach ($dateEvents as $event) { 46641d05cddcSAtari911 $shouldDelete = false; 46651d05cddcSAtari911 46661d05cddcSAtari911 // Age-based 46671d05cddcSAtari911 if ($cleanupType === 'age' && $cutoffDate && $date < $cutoffDate) { 46681d05cddcSAtari911 $shouldDelete = true; 46691d05cddcSAtari911 } 46701d05cddcSAtari911 46711d05cddcSAtari911 // Range-based 46721d05cddcSAtari911 if ($cleanupType === 'range' && $rangeStart && $rangeEnd) { 46731d05cddcSAtari911 if ($date >= $rangeStart && $date <= $rangeEnd) { 46741d05cddcSAtari911 $shouldDelete = true; 46751d05cddcSAtari911 } 46761d05cddcSAtari911 } 46771d05cddcSAtari911 46781d05cddcSAtari911 // Status-based 46791d05cddcSAtari911 if ($cleanupType === 'status') { 46801d05cddcSAtari911 $isTask = isset($event['isTask']) && $event['isTask']; 46811d05cddcSAtari911 $isCompleted = isset($event['completed']) && $event['completed']; 46821d05cddcSAtari911 $isPast = $date < date('Y-m-d'); 46831d05cddcSAtari911 46841d05cddcSAtari911 if ($deleteCompleted && $isTask && $isCompleted) { 46851d05cddcSAtari911 $shouldDelete = true; 46861d05cddcSAtari911 } 46871d05cddcSAtari911 if ($deletePast && !$isTask && $isPast) { 46881d05cddcSAtari911 $shouldDelete = true; 46891d05cddcSAtari911 } 46901d05cddcSAtari911 } 46911d05cddcSAtari911 46921d05cddcSAtari911 if ($shouldDelete) { 46931d05cddcSAtari911 $eventsToDelete[] = [ 46941d05cddcSAtari911 'id' => $event['id'], 46951d05cddcSAtari911 'title' => $event['title'], 46961d05cddcSAtari911 'date' => $date, 46971d05cddcSAtari911 'namespace' => $namespace ?: 'default', 46981d05cddcSAtari911 'file' => $file 46991d05cddcSAtari911 ]; 47001d05cddcSAtari911 } 47011d05cddcSAtari911 } 47021d05cddcSAtari911 } 47031d05cddcSAtari911 } 47041d05cddcSAtari911 } 47059ccd446eSAtari911 47069ccd446eSAtari911 /** 47079ccd446eSAtari911 * Render Themes tab for sidebar widget theme selection 47089ccd446eSAtari911 */ 47099ccd446eSAtari911 private function renderThemesTab($colors = null) { 47109ccd446eSAtari911 global $INPUT; 47119ccd446eSAtari911 47129ccd446eSAtari911 // Use defaults if not provided 47139ccd446eSAtari911 if ($colors === null) { 47149ccd446eSAtari911 $colors = $this->getTemplateColors(); 47159ccd446eSAtari911 } 47169ccd446eSAtari911 47179ccd446eSAtari911 // Handle theme save 47189ccd446eSAtari911 if ($INPUT->str('action') === 'save_theme') { 47199ccd446eSAtari911 $theme = $INPUT->str('theme', 'matrix'); 47209ccd446eSAtari911 $weekStart = $INPUT->str('week_start', 'monday'); 47219ccd446eSAtari911 $this->saveSidebarTheme($theme); 47229ccd446eSAtari911 $this->saveWeekStartDay($weekStart); 47239ccd446eSAtari911 echo '<div style="background:#d4edda; border:1px solid #c3e6cb; color:#155724; padding:12px; border-radius:4px; margin-bottom:20px;">'; 47249ccd446eSAtari911 echo '✓ Theme and settings saved successfully! Refresh any page with the sidebar to see changes.'; 47259ccd446eSAtari911 echo '</div>'; 47269ccd446eSAtari911 } 47279ccd446eSAtari911 47289ccd446eSAtari911 $currentTheme = $this->getSidebarTheme(); 47299ccd446eSAtari911 $currentWeekStart = $this->getWeekStartDay(); 47309ccd446eSAtari911 47319ccd446eSAtari911 echo '<h2 style="margin:0 0 20px 0; color:' . $colors['text'] . ';"> Sidebar Widget Settings</h2>'; 47329ccd446eSAtari911 echo '<p style="color:' . $colors['text'] . '; margin-bottom:20px;">Customize the appearance and behavior of the sidebar calendar widget.</p>'; 47339ccd446eSAtari911 47349ccd446eSAtari911 echo '<form method="post" action="?do=admin&page=calendar&tab=themes">'; 47359ccd446eSAtari911 echo '<input type="hidden" name="action" value="save_theme">'; 47369ccd446eSAtari911 47379ccd446eSAtari911 // Week Start Day Section 47389ccd446eSAtari911 echo '<div style="background:' . $colors['bg'] . '; border:1px solid ' . $colors['border'] . '; border-radius:6px; padding:20px; margin-bottom:30px;">'; 47399ccd446eSAtari911 echo '<h3 style="margin:0 0 15px 0; color:' . $colors['text'] . '; font-size:16px;"> Week Start Day</h3>'; 47409ccd446eSAtari911 echo '<p style="color:' . $colors['text'] . '; margin-bottom:15px; font-size:13px;">Choose which day the week calendar grid starts with:</p>'; 47419ccd446eSAtari911 47429ccd446eSAtari911 echo '<div style="display:flex; gap:15px;">'; 47439ccd446eSAtari911 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;">'; 47449ccd446eSAtari911 echo '<input type="radio" name="week_start" value="monday" ' . ($currentWeekStart === 'monday' ? 'checked' : '') . ' style="margin-right:10px; width:18px; height:18px;">'; 47459ccd446eSAtari911 echo '<div>'; 47469ccd446eSAtari911 echo '<div style="font-weight:bold; color:' . $colors['text'] . '; margin-bottom:3px;">Monday</div>'; 47479ccd446eSAtari911 echo '<div style="font-size:11px; color:' . $colors['text'] . ';">Week starts on Monday (ISO standard)</div>'; 47489ccd446eSAtari911 echo '</div>'; 47499ccd446eSAtari911 echo '</label>'; 47509ccd446eSAtari911 47519ccd446eSAtari911 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;">'; 47529ccd446eSAtari911 echo '<input type="radio" name="week_start" value="sunday" ' . ($currentWeekStart === 'sunday' ? 'checked' : '') . ' style="margin-right:10px; width:18px; height:18px;">'; 47539ccd446eSAtari911 echo '<div>'; 47549ccd446eSAtari911 echo '<div style="font-weight:bold; color:' . $colors['text'] . '; margin-bottom:3px;">Sunday</div>'; 47559ccd446eSAtari911 echo '<div style="font-size:11px; color:' . $colors['text'] . ';">Week starts on Sunday (US/Canada standard)</div>'; 47569ccd446eSAtari911 echo '</div>'; 47579ccd446eSAtari911 echo '</label>'; 47589ccd446eSAtari911 echo '</div>'; 47599ccd446eSAtari911 echo '</div>'; 47609ccd446eSAtari911 47619ccd446eSAtari911 // Visual Theme Section 47629ccd446eSAtari911 echo '<h3 style="margin:0 0 15px 0; color:' . $colors['text'] . '; font-size:16px;"> Visual Theme</h3>'; 47639ccd446eSAtari911 47649ccd446eSAtari911 // Matrix Theme 47659ccd446eSAtari911 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']) . ';">'; 47669ccd446eSAtari911 echo '<label style="display:flex; align-items:center; cursor:pointer;">'; 47679ccd446eSAtari911 echo '<input type="radio" name="theme" value="matrix" ' . ($currentTheme === 'matrix' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">'; 47689ccd446eSAtari911 echo '<div style="flex:1;">'; 47699ccd446eSAtari911 echo '<div style="font-size:18px; font-weight:bold; color:#00cc07; margin-bottom:8px;"> Matrix Edition</div>'; 47709ccd446eSAtari911 echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Dark green theme with Matrix-style glow effects and neon accents</div>'; 47719ccd446eSAtari911 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>'; 47729ccd446eSAtari911 echo '</div>'; 47739ccd446eSAtari911 echo '</label>'; 47749ccd446eSAtari911 echo '</div>'; 47759ccd446eSAtari911 47769ccd446eSAtari911 // Purple Theme 47779ccd446eSAtari911 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']) . ';">'; 47789ccd446eSAtari911 echo '<label style="display:flex; align-items:center; cursor:pointer;">'; 47799ccd446eSAtari911 echo '<input type="radio" name="theme" value="purple" ' . ($currentTheme === 'purple' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">'; 47809ccd446eSAtari911 echo '<div style="flex:1;">'; 47819ccd446eSAtari911 echo '<div style="font-size:18px; font-weight:bold; color:#9b59b6; margin-bottom:8px;"> Purple Dream</div>'; 47829ccd446eSAtari911 echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Rich purple theme with elegant violet accents and soft glow</div>'; 47839ccd446eSAtari911 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>'; 47849ccd446eSAtari911 echo '</div>'; 47859ccd446eSAtari911 echo '</label>'; 47869ccd446eSAtari911 echo '</div>'; 47879ccd446eSAtari911 47889ccd446eSAtari911 // Professional Blue Theme 47899ccd446eSAtari911 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']) . ';">'; 47909ccd446eSAtari911 echo '<label style="display:flex; align-items:center; cursor:pointer;">'; 47919ccd446eSAtari911 echo '<input type="radio" name="theme" value="professional" ' . ($currentTheme === 'professional' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">'; 47929ccd446eSAtari911 echo '<div style="flex:1;">'; 47939ccd446eSAtari911 echo '<div style="font-size:18px; font-weight:bold; color:#4a90e2; margin-bottom:8px;"> Professional Blue</div>'; 47949ccd446eSAtari911 echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Clean blue and grey theme with modern professional styling, no glow effects</div>'; 47959ccd446eSAtari911 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>'; 47969ccd446eSAtari911 echo '</div>'; 47979ccd446eSAtari911 echo '</label>'; 47989ccd446eSAtari911 echo '</div>'; 47999ccd446eSAtari911 48009ccd446eSAtari911 // Pink Bling Theme 48019ccd446eSAtari911 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']) . ';">'; 48029ccd446eSAtari911 echo '<label style="display:flex; align-items:center; cursor:pointer;">'; 48039ccd446eSAtari911 echo '<input type="radio" name="theme" value="pink" ' . ($currentTheme === 'pink' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">'; 48049ccd446eSAtari911 echo '<div style="flex:1;">'; 48059ccd446eSAtari911 echo '<div style="font-size:18px; font-weight:bold; color:#ff1493; margin-bottom:8px;"> Pink Bling</div>'; 48069ccd446eSAtari911 echo '<div style="color:' . $colors['text'] . '; margin-bottom:12px;">Glamorous hot pink theme with maximum sparkle, hearts, and diamonds ✨</div>'; 48079ccd446eSAtari911 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>'; 48089ccd446eSAtari911 echo '</div>'; 48099ccd446eSAtari911 echo '</label>'; 48109ccd446eSAtari911 echo '</div>'; 48119ccd446eSAtari911 48129ccd446eSAtari911 // Wiki Default Theme 48139ccd446eSAtari911 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']) . ';">'; 48149ccd446eSAtari911 echo '<label style="display:flex; align-items:center; cursor:pointer;">'; 48159ccd446eSAtari911 echo '<input type="radio" name="theme" value="wiki" ' . ($currentTheme === 'wiki' ? 'checked' : '') . ' style="margin-right:12px; width:20px; height:20px;">'; 48169ccd446eSAtari911 echo '<div style="flex:1;">'; 48179ccd446eSAtari911 echo '<div style="font-size:18px; font-weight:bold; color:#2b73b7; margin-bottom:8px;"> Wiki Default</div>'; 48189ccd446eSAtari911 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>'; 48199ccd446eSAtari911 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>'; 48209ccd446eSAtari911 echo '</div>'; 48219ccd446eSAtari911 echo '</label>'; 48229ccd446eSAtari911 echo '</div>'; 48239ccd446eSAtari911 48249ccd446eSAtari911 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>'; 48259ccd446eSAtari911 echo '</form>'; 48269ccd446eSAtari911 } 48279ccd446eSAtari911 48289ccd446eSAtari911 /** 48299ccd446eSAtari911 * Get current sidebar theme 48309ccd446eSAtari911 */ 48319ccd446eSAtari911 private function getSidebarTheme() { 48329ccd446eSAtari911 $configFile = DOKU_INC . 'data/meta/calendar_theme.txt'; 48339ccd446eSAtari911 if (file_exists($configFile)) { 48349ccd446eSAtari911 return trim(file_get_contents($configFile)); 48359ccd446eSAtari911 } 48369ccd446eSAtari911 return 'matrix'; // Default 48379ccd446eSAtari911 } 48389ccd446eSAtari911 48399ccd446eSAtari911 /** 48409ccd446eSAtari911 * Save sidebar theme 48419ccd446eSAtari911 */ 48429ccd446eSAtari911 private function saveSidebarTheme($theme) { 48439ccd446eSAtari911 $configFile = DOKU_INC . 'data/meta/calendar_theme.txt'; 48449ccd446eSAtari911 $validThemes = ['matrix', 'purple', 'professional', 'pink', 'wiki']; 48459ccd446eSAtari911 48469ccd446eSAtari911 if (in_array($theme, $validThemes)) { 48479ccd446eSAtari911 file_put_contents($configFile, $theme); 48489ccd446eSAtari911 return true; 48499ccd446eSAtari911 } 48509ccd446eSAtari911 return false; 48519ccd446eSAtari911 } 48529ccd446eSAtari911 48539ccd446eSAtari911 /** 48549ccd446eSAtari911 * Get week start day 48559ccd446eSAtari911 */ 48569ccd446eSAtari911 private function getWeekStartDay() { 48579ccd446eSAtari911 $configFile = DOKU_INC . 'data/meta/calendar_week_start.txt'; 48589ccd446eSAtari911 if (file_exists($configFile)) { 48599ccd446eSAtari911 $start = trim(file_get_contents($configFile)); 48609ccd446eSAtari911 if (in_array($start, ['monday', 'sunday'])) { 48619ccd446eSAtari911 return $start; 48629ccd446eSAtari911 } 48639ccd446eSAtari911 } 48649ccd446eSAtari911 return 'sunday'; // Default to Sunday (US/Canada standard) 48659ccd446eSAtari911 } 48669ccd446eSAtari911 48679ccd446eSAtari911 /** 48689ccd446eSAtari911 * Save week start day 48699ccd446eSAtari911 */ 48709ccd446eSAtari911 private function saveWeekStartDay($weekStart) { 48719ccd446eSAtari911 $configFile = DOKU_INC . 'data/meta/calendar_week_start.txt'; 48729ccd446eSAtari911 $validStarts = ['monday', 'sunday']; 48739ccd446eSAtari911 48749ccd446eSAtari911 if (in_array($weekStart, $validStarts)) { 48759ccd446eSAtari911 file_put_contents($configFile, $weekStart); 48769ccd446eSAtari911 return true; 48779ccd446eSAtari911 } 48789ccd446eSAtari911 return false; 48799ccd446eSAtari911 } 48809ccd446eSAtari911 48819ccd446eSAtari911 /** 48829ccd446eSAtari911 * Get colors from DokuWiki template's style.ini file 48839ccd446eSAtari911 */ 48849ccd446eSAtari911 private function getTemplateColors() { 48859ccd446eSAtari911 global $conf; 48869ccd446eSAtari911 48879ccd446eSAtari911 // Get current template name 48889ccd446eSAtari911 $template = $conf['template']; 48899ccd446eSAtari911 48909ccd446eSAtari911 // Try multiple possible locations for style.ini 48919ccd446eSAtari911 $possiblePaths = [ 48929ccd446eSAtari911 DOKU_INC . 'conf/tpl/' . $template . '/style.ini', 48939ccd446eSAtari911 DOKU_INC . 'lib/tpl/' . $template . '/style.ini', 48949ccd446eSAtari911 ]; 48959ccd446eSAtari911 48969ccd446eSAtari911 $styleIni = null; 48979ccd446eSAtari911 foreach ($possiblePaths as $path) { 48989ccd446eSAtari911 if (file_exists($path)) { 48999ccd446eSAtari911 $styleIni = parse_ini_file($path, true); 49009ccd446eSAtari911 break; 49019ccd446eSAtari911 } 49029ccd446eSAtari911 } 49039ccd446eSAtari911 49049ccd446eSAtari911 if (!$styleIni || !isset($styleIni['replacements'])) { 49059ccd446eSAtari911 // Return defaults 49069ccd446eSAtari911 return [ 49079ccd446eSAtari911 'bg' => '#fff', 49089ccd446eSAtari911 'bg_alt' => '#e8e8e8', 49099ccd446eSAtari911 'text' => '#333', 49109ccd446eSAtari911 'border' => '#ccc', 49119ccd446eSAtari911 'link' => '#2b73b7', 49129ccd446eSAtari911 ]; 49139ccd446eSAtari911 } 49149ccd446eSAtari911 49159ccd446eSAtari911 $r = $styleIni['replacements']; 49169ccd446eSAtari911 49179ccd446eSAtari911 return [ 49189ccd446eSAtari911 'bg' => isset($r['__background__']) ? $r['__background__'] : '#fff', 49199ccd446eSAtari911 'bg_alt' => isset($r['__background_alt__']) ? $r['__background_alt__'] : '#e8e8e8', 49209ccd446eSAtari911 'text' => isset($r['__text__']) ? $r['__text__'] : '#333', 49219ccd446eSAtari911 'border' => isset($r['__border__']) ? $r['__border__'] : '#ccc', 49229ccd446eSAtari911 'link' => isset($r['__link__']) ? $r['__link__'] : '#2b73b7', 49239ccd446eSAtari911 ]; 49249ccd446eSAtari911 } 49251d05cddcSAtari911} 4926