1*1d05cddcSAtari911<?php 2*1d05cddcSAtari911/** 3*1d05cddcSAtari911 * Calendar Plugin - Admin Interface 4*1d05cddcSAtari911 * Clean rewrite - Configuration only 5*1d05cddcSAtari911 * Version: 3.3 6*1d05cddcSAtari911 */ 7*1d05cddcSAtari911 8*1d05cddcSAtari911if(!defined('DOKU_INC')) die(); 9*1d05cddcSAtari911 10*1d05cddcSAtari911class admin_plugin_calendar extends DokuWiki_Admin_Plugin { 11*1d05cddcSAtari911 12*1d05cddcSAtari911 public function getMenuText($language) { 13*1d05cddcSAtari911 return 'Calendar Management'; 14*1d05cddcSAtari911 } 15*1d05cddcSAtari911 16*1d05cddcSAtari911 public function getMenuSort() { 17*1d05cddcSAtari911 return 100; 18*1d05cddcSAtari911 } 19*1d05cddcSAtari911 20*1d05cddcSAtari911 public function forAdminOnly() { 21*1d05cddcSAtari911 return true; 22*1d05cddcSAtari911 } 23*1d05cddcSAtari911 24*1d05cddcSAtari911 public function handle() { 25*1d05cddcSAtari911 global $INPUT; 26*1d05cddcSAtari911 27*1d05cddcSAtari911 $action = $INPUT->str('action'); 28*1d05cddcSAtari911 29*1d05cddcSAtari911 if ($action === 'clear_cache') { 30*1d05cddcSAtari911 $this->clearCache(); 31*1d05cddcSAtari911 } elseif ($action === 'save_config') { 32*1d05cddcSAtari911 $this->saveConfig(); 33*1d05cddcSAtari911 } elseif ($action === 'delete_recurring_series') { 34*1d05cddcSAtari911 $this->deleteRecurringSeries(); 35*1d05cddcSAtari911 } elseif ($action === 'edit_recurring_series') { 36*1d05cddcSAtari911 $this->editRecurringSeries(); 37*1d05cddcSAtari911 } elseif ($action === 'move_events') { 38*1d05cddcSAtari911 $this->moveEvents(); 39*1d05cddcSAtari911 } elseif ($action === 'move_selected_events') { 40*1d05cddcSAtari911 $this->moveEvents(); 41*1d05cddcSAtari911 } elseif ($action === 'move_single_event') { 42*1d05cddcSAtari911 $this->moveSingleEvent(); 43*1d05cddcSAtari911 } elseif ($action === 'delete_selected_events') { 44*1d05cddcSAtari911 $this->deleteSelectedEvents(); 45*1d05cddcSAtari911 } elseif ($action === 'create_namespace') { 46*1d05cddcSAtari911 $this->createNamespace(); 47*1d05cddcSAtari911 } elseif ($action === 'delete_namespace') { 48*1d05cddcSAtari911 $this->deleteNamespace(); 49*1d05cddcSAtari911 } elseif ($action === 'run_sync') { 50*1d05cddcSAtari911 $this->runSync(); 51*1d05cddcSAtari911 } elseif ($action === 'stop_sync') { 52*1d05cddcSAtari911 $this->stopSync(); 53*1d05cddcSAtari911 } elseif ($action === 'upload_update') { 54*1d05cddcSAtari911 $this->uploadUpdate(); 55*1d05cddcSAtari911 } elseif ($action === 'delete_backup') { 56*1d05cddcSAtari911 $this->deleteBackup(); 57*1d05cddcSAtari911 } elseif ($action === 'rename_backup') { 58*1d05cddcSAtari911 $this->renameBackup(); 59*1d05cddcSAtari911 } elseif ($action === 'restore_backup') { 60*1d05cddcSAtari911 $this->restoreBackup(); 61*1d05cddcSAtari911 } elseif ($action === 'export_config') { 62*1d05cddcSAtari911 $this->exportConfig(); 63*1d05cddcSAtari911 } elseif ($action === 'import_config') { 64*1d05cddcSAtari911 $this->importConfig(); 65*1d05cddcSAtari911 } elseif ($action === 'get_log') { 66*1d05cddcSAtari911 $this->getLog(); 67*1d05cddcSAtari911 } elseif ($action === 'clear_log') { 68*1d05cddcSAtari911 $this->clearLogFile(); 69*1d05cddcSAtari911 } elseif ($action === 'download_log') { 70*1d05cddcSAtari911 $this->downloadLog(); 71*1d05cddcSAtari911 } elseif ($action === 'rescan_events') { 72*1d05cddcSAtari911 $this->rescanEvents(); 73*1d05cddcSAtari911 } elseif ($action === 'export_all_events') { 74*1d05cddcSAtari911 $this->exportAllEvents(); 75*1d05cddcSAtari911 } elseif ($action === 'import_all_events') { 76*1d05cddcSAtari911 $this->importAllEvents(); 77*1d05cddcSAtari911 } elseif ($action === 'preview_cleanup') { 78*1d05cddcSAtari911 $this->previewCleanup(); 79*1d05cddcSAtari911 } elseif ($action === 'cleanup_events') { 80*1d05cddcSAtari911 $this->cleanupEvents(); 81*1d05cddcSAtari911 } 82*1d05cddcSAtari911 } 83*1d05cddcSAtari911 84*1d05cddcSAtari911 public function html() { 85*1d05cddcSAtari911 global $INPUT; 86*1d05cddcSAtari911 87*1d05cddcSAtari911 // Get current tab - default to 'update' (Update Plugin tab) 88*1d05cddcSAtari911 $tab = $INPUT->str('tab', 'update'); 89*1d05cddcSAtari911 90*1d05cddcSAtari911 // Tab navigation 91*1d05cddcSAtari911 echo '<div style="border-bottom:2px solid #ddd; margin:10px 0 15px 0;">'; 92*1d05cddcSAtari911 echo '<a href="?do=admin&page=calendar&tab=update" style="display:inline-block; padding:8px 16px; text-decoration:none; color:' . ($tab === 'update' ? '#00cc07' : '#333') . '; border-bottom:3px solid ' . ($tab === 'update' ? '#00cc07' : 'transparent') . '; font-weight:' . ($tab === 'update' ? 'bold' : 'normal') . ';"> Update Plugin</a>'; 93*1d05cddcSAtari911 echo '<a href="?do=admin&page=calendar&tab=config" style="display:inline-block; padding:8px 16px; text-decoration:none; color:' . ($tab === 'config' ? '#00cc07' : '#333') . '; border-bottom:3px solid ' . ($tab === 'config' ? '#00cc07' : 'transparent') . '; font-weight:' . ($tab === 'config' ? 'bold' : 'normal') . ';">⚙️ Outlook Sync</a>'; 94*1d05cddcSAtari911 echo '<a href="?do=admin&page=calendar&tab=manage" style="display:inline-block; padding:8px 16px; text-decoration:none; color:' . ($tab === 'manage' ? '#00cc07' : '#333') . '; border-bottom:3px solid ' . ($tab === 'manage' ? '#00cc07' : 'transparent') . '; font-weight:' . ($tab === 'manage' ? 'bold' : 'normal') . ';"> Manage Events</a>'; 95*1d05cddcSAtari911 echo '</div>'; 96*1d05cddcSAtari911 97*1d05cddcSAtari911 // Render appropriate tab 98*1d05cddcSAtari911 if ($tab === 'config') { 99*1d05cddcSAtari911 $this->renderConfigTab(); 100*1d05cddcSAtari911 } elseif ($tab === 'manage') { 101*1d05cddcSAtari911 $this->renderManageTab(); 102*1d05cddcSAtari911 } else { 103*1d05cddcSAtari911 $this->renderUpdateTab(); 104*1d05cddcSAtari911 } 105*1d05cddcSAtari911 } 106*1d05cddcSAtari911 107*1d05cddcSAtari911 private function renderConfigTab() { 108*1d05cddcSAtari911 global $INPUT; 109*1d05cddcSAtari911 110*1d05cddcSAtari911 // Load current config 111*1d05cddcSAtari911 $configFile = DOKU_PLUGIN . 'calendar/sync_config.php'; 112*1d05cddcSAtari911 $config = []; 113*1d05cddcSAtari911 if (file_exists($configFile)) { 114*1d05cddcSAtari911 $config = include $configFile; 115*1d05cddcSAtari911 } 116*1d05cddcSAtari911 117*1d05cddcSAtari911 // Show message if present 118*1d05cddcSAtari911 if ($INPUT->has('msg')) { 119*1d05cddcSAtari911 $msg = hsc($INPUT->str('msg')); 120*1d05cddcSAtari911 $type = $INPUT->str('msgtype', 'success'); 121*1d05cddcSAtari911 $class = ($type === 'success') ? 'msg success' : 'msg error'; 122*1d05cddcSAtari911 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;\">"; 123*1d05cddcSAtari911 echo $msg; 124*1d05cddcSAtari911 echo "</div>"; 125*1d05cddcSAtari911 } 126*1d05cddcSAtari911 127*1d05cddcSAtari911 echo '<h2 style="margin:10px 0; font-size:20px;">Outlook Sync Configuration</h2>'; 128*1d05cddcSAtari911 129*1d05cddcSAtari911 // Import/Export buttons 130*1d05cddcSAtari911 echo '<div style="display:flex; gap:10px; margin-bottom:15px;">'; 131*1d05cddcSAtari911 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>'; 132*1d05cddcSAtari911 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>'; 133*1d05cddcSAtari911 echo '<input type="file" id="importFileInput" accept=".enc" style="display:none;" onchange="importConfig(this)">'; 134*1d05cddcSAtari911 echo '<span id="importStatus" style="margin-left:10px; font-size:12px;"></span>'; 135*1d05cddcSAtari911 echo '</div>'; 136*1d05cddcSAtari911 137*1d05cddcSAtari911 echo '<form method="post" action="?do=admin&page=calendar" style="max-width:900px;">'; 138*1d05cddcSAtari911 echo '<input type="hidden" name="action" value="save_config">'; 139*1d05cddcSAtari911 140*1d05cddcSAtari911 // Azure Credentials 141*1d05cddcSAtari911 echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">'; 142*1d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Microsoft Azure App Credentials</h3>'; 143*1d05cddcSAtari911 echo '<p style="color:#666; 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>'; 144*1d05cddcSAtari911 145*1d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Tenant ID</label>'; 146*1d05cddcSAtari911 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 #ddd; border-radius:3px; font-size:13px;">'; 147*1d05cddcSAtari911 148*1d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Client ID (Application ID)</label>'; 149*1d05cddcSAtari911 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 #ddd; border-radius:3px; font-size:13px;">'; 150*1d05cddcSAtari911 151*1d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin:8px 0 3px; font-size:13px;">Client Secret</label>'; 152*1d05cddcSAtari911 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 #ddd; border-radius:3px; font-size:13px;">'; 153*1d05cddcSAtari911 echo '<p style="color:#999; font-size:0.8em; margin:3px 0 0;">⚠️ Keep this secret safe!</p>'; 154*1d05cddcSAtari911 echo '</div>'; 155*1d05cddcSAtari911 156*1d05cddcSAtari911 // Outlook Settings 157*1d05cddcSAtari911 echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">'; 158*1d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Outlook Settings</h3>'; 159*1d05cddcSAtari911 160*1d05cddcSAtari911 echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">'; 161*1d05cddcSAtari911 162*1d05cddcSAtari911 echo '<div>'; 163*1d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">User Email</label>'; 164*1d05cddcSAtari911 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 #ddd; border-radius:3px; font-size:13px;">'; 165*1d05cddcSAtari911 echo '</div>'; 166*1d05cddcSAtari911 167*1d05cddcSAtari911 echo '<div>'; 168*1d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Timezone</label>'; 169*1d05cddcSAtari911 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 #ddd; border-radius:3px; font-size:13px;">'; 170*1d05cddcSAtari911 echo '</div>'; 171*1d05cddcSAtari911 172*1d05cddcSAtari911 echo '<div>'; 173*1d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Default Category</label>'; 174*1d05cddcSAtari911 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 #ddd; border-radius:3px; font-size:13px;">'; 175*1d05cddcSAtari911 echo '</div>'; 176*1d05cddcSAtari911 177*1d05cddcSAtari911 echo '<div>'; 178*1d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin:0 0 3px; font-size:13px;">Reminder (minutes)</label>'; 179*1d05cddcSAtari911 echo '<input type="number" name="reminder_minutes" value="' . hsc($config['reminder_minutes'] ?? 15) . '" placeholder="15" style="width:100%; padding:6px; border:1px solid #ddd; border-radius:3px; font-size:13px;">'; 180*1d05cddcSAtari911 echo '</div>'; 181*1d05cddcSAtari911 182*1d05cddcSAtari911 echo '</div>'; // end grid 183*1d05cddcSAtari911 echo '</div>'; 184*1d05cddcSAtari911 185*1d05cddcSAtari911 // Important Namespaces for Sidebar Widget 186*1d05cddcSAtari911 echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #9b59b6; border-radius:3px;">'; 187*1d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#9b59b6; font-size:16px;"> Important Namespaces (Sidebar Widget)</h3>'; 188*1d05cddcSAtari911 echo '<p style="color:#666; font-size:11px; margin:0 0 8px;">Events from these namespaces will be highlighted in purple in the sidebar widget</p>'; 189*1d05cddcSAtari911 echo '<input type="text" name="important_namespaces" value="' . hsc(isset($config['important_namespaces']) ? $config['important_namespaces'] : 'important') . '" style="width:100%; padding:6px; border:1px solid #ddd; border-radius:3px; font-size:12px;" placeholder="important,urgent,priority">'; 190*1d05cddcSAtari911 echo '<p style="color:#666; font-size:10px; margin:4px 0 0;">Comma-separated list of namespace names</p>'; 191*1d05cddcSAtari911 echo '</div>'; 192*1d05cddcSAtari911 193*1d05cddcSAtari911 // Sync Options 194*1d05cddcSAtari911 echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px;">'; 195*1d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Sync Options</h3>'; 196*1d05cddcSAtari911 197*1d05cddcSAtari911 $syncCompleted = isset($config['sync_completed_tasks']) ? $config['sync_completed_tasks'] : false; 198*1d05cddcSAtari911 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>'; 199*1d05cddcSAtari911 200*1d05cddcSAtari911 $deleteOutlook = isset($config['delete_outlook_events']) ? $config['delete_outlook_events'] : true; 201*1d05cddcSAtari911 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>'; 202*1d05cddcSAtari911 203*1d05cddcSAtari911 $syncAll = isset($config['sync_all_namespaces']) ? $config['sync_all_namespaces'] : true; 204*1d05cddcSAtari911 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>'; 205*1d05cddcSAtari911 206*1d05cddcSAtari911 // Namespace selection (shown when sync_all is unchecked) 207*1d05cddcSAtari911 echo '<div id="namespace_selection" style="margin-top:10px; ' . ($syncAll ? 'display:none;' : '') . '">'; 208*1d05cddcSAtari911 echo '<label style="display:block; font-weight:bold; margin-bottom:5px; font-size:13px;">Select namespaces to sync:</label>'; 209*1d05cddcSAtari911 210*1d05cddcSAtari911 // Get available namespaces 211*1d05cddcSAtari911 $availableNamespaces = $this->getAllNamespaces(); 212*1d05cddcSAtari911 $selectedNamespaces = isset($config['sync_namespaces']) ? $config['sync_namespaces'] : []; 213*1d05cddcSAtari911 214*1d05cddcSAtari911 echo '<div style="max-height:150px; overflow-y:auto; border:1px solid #ddd; border-radius:3px; padding:8px; background:white;">'; 215*1d05cddcSAtari911 echo '<label style="display:block; margin:3px 0;"><input type="checkbox" name="sync_namespaces[]" value=""> (default)</label>'; 216*1d05cddcSAtari911 foreach ($availableNamespaces as $ns) { 217*1d05cddcSAtari911 if ($ns !== '') { 218*1d05cddcSAtari911 $checked = in_array($ns, $selectedNamespaces) ? 'checked' : ''; 219*1d05cddcSAtari911 echo '<label style="display:block; margin:3px 0;"><input type="checkbox" name="sync_namespaces[]" value="' . hsc($ns) . '" ' . $checked . '> ' . hsc($ns) . '</label>'; 220*1d05cddcSAtari911 } 221*1d05cddcSAtari911 } 222*1d05cddcSAtari911 echo '</div>'; 223*1d05cddcSAtari911 echo '</div>'; 224*1d05cddcSAtari911 225*1d05cddcSAtari911 echo '<script> 226*1d05cddcSAtari911 function toggleNamespaceSelection(checkbox) { 227*1d05cddcSAtari911 document.getElementById("namespace_selection").style.display = checkbox.checked ? "none" : "block"; 228*1d05cddcSAtari911 } 229*1d05cddcSAtari911 </script>'; 230*1d05cddcSAtari911 231*1d05cddcSAtari911 echo '</div>'; 232*1d05cddcSAtari911 233*1d05cddcSAtari911 // Namespace and Color Mapping - Side by Side 234*1d05cddcSAtari911 echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px; margin:10px 0;">'; 235*1d05cddcSAtari911 236*1d05cddcSAtari911 // Namespace Mapping 237*1d05cddcSAtari911 echo '<div style="background:#f9f9f9; padding:12px; border-left:3px solid #00cc07; border-radius:3px;">'; 238*1d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Namespace → Category</h3>'; 239*1d05cddcSAtari911 echo '<p style="color:#666; font-size:0.8em; margin:0 0 5px;">One per line: namespace=Category</p>'; 240*1d05cddcSAtari911 echo '<textarea name="category_mapping" rows="6" style="width:100%; padding:6px; border:1px solid #ddd; border-radius:3px; font-family:monospace; font-size:12px; resize:vertical;" placeholder="work=Blue category personal=Green category">'; 241*1d05cddcSAtari911 if (isset($config['category_mapping']) && is_array($config['category_mapping'])) { 242*1d05cddcSAtari911 foreach ($config['category_mapping'] as $ns => $cat) { 243*1d05cddcSAtari911 echo hsc($ns) . '=' . hsc($cat) . "\n"; 244*1d05cddcSAtari911 } 245*1d05cddcSAtari911 } 246*1d05cddcSAtari911 echo '</textarea>'; 247*1d05cddcSAtari911 echo '</div>'; 248*1d05cddcSAtari911 249*1d05cddcSAtari911 // Color Mapping with Color Picker 250*1d05cddcSAtari911 echo '<div style="background:#f9f9f9; padding:12px; border-left:3px solid #00cc07; border-radius:3px;">'; 251*1d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;"> Event Color → Category</h3>'; 252*1d05cddcSAtari911 echo '<p style="color:#666; font-size:0.8em; margin:0 0 8px;">Map calendar colors to Outlook categories</p>'; 253*1d05cddcSAtari911 254*1d05cddcSAtari911 // Define calendar colors and Outlook categories (only the main 6 colors) 255*1d05cddcSAtari911 $calendarColors = [ 256*1d05cddcSAtari911 '#3498db' => 'Blue', 257*1d05cddcSAtari911 '#2ecc71' => 'Green', 258*1d05cddcSAtari911 '#e74c3c' => 'Red', 259*1d05cddcSAtari911 '#f39c12' => 'Orange', 260*1d05cddcSAtari911 '#9b59b6' => 'Purple', 261*1d05cddcSAtari911 '#1abc9c' => 'Teal' 262*1d05cddcSAtari911 ]; 263*1d05cddcSAtari911 264*1d05cddcSAtari911 $outlookCategories = [ 265*1d05cddcSAtari911 'Blue category', 266*1d05cddcSAtari911 'Green category', 267*1d05cddcSAtari911 'Orange category', 268*1d05cddcSAtari911 'Red category', 269*1d05cddcSAtari911 'Yellow category', 270*1d05cddcSAtari911 'Purple category' 271*1d05cddcSAtari911 ]; 272*1d05cddcSAtari911 273*1d05cddcSAtari911 // Load existing color mappings 274*1d05cddcSAtari911 $existingMappings = isset($config['color_mapping']) && is_array($config['color_mapping']) 275*1d05cddcSAtari911 ? $config['color_mapping'] 276*1d05cddcSAtari911 : []; 277*1d05cddcSAtari911 278*1d05cddcSAtari911 // Display color mapping rows 279*1d05cddcSAtari911 echo '<div id="colorMappings" style="max-height:200px; overflow-y:auto;">'; 280*1d05cddcSAtari911 281*1d05cddcSAtari911 $rowIndex = 0; 282*1d05cddcSAtari911 foreach ($calendarColors as $hexColor => $colorName) { 283*1d05cddcSAtari911 $selectedCategory = isset($existingMappings[$hexColor]) ? $existingMappings[$hexColor] : ''; 284*1d05cddcSAtari911 285*1d05cddcSAtari911 echo '<div style="display:flex; gap:8px; align-items:center; margin-bottom:6px;">'; 286*1d05cddcSAtari911 287*1d05cddcSAtari911 // Color preview box 288*1d05cddcSAtari911 echo '<div style="width:24px; height:24px; background:' . $hexColor . '; border:2px solid #ddd; border-radius:3px; flex-shrink:0;"></div>'; 289*1d05cddcSAtari911 290*1d05cddcSAtari911 // Color name 291*1d05cddcSAtari911 echo '<span style="font-size:12px; min-width:90px; color:#666;">' . $colorName . '</span>'; 292*1d05cddcSAtari911 293*1d05cddcSAtari911 // Arrow 294*1d05cddcSAtari911 echo '<span style="color:#999; font-size:12px;">→</span>'; 295*1d05cddcSAtari911 296*1d05cddcSAtari911 // Outlook category dropdown 297*1d05cddcSAtari911 echo '<select name="color_map_' . $rowIndex . '" style="flex:1; padding:4px; border:1px solid #ddd; border-radius:3px; font-size:12px;">'; 298*1d05cddcSAtari911 echo '<option value="">-- None --</option>'; 299*1d05cddcSAtari911 foreach ($outlookCategories as $category) { 300*1d05cddcSAtari911 $selected = ($selectedCategory === $category) ? 'selected' : ''; 301*1d05cddcSAtari911 echo '<option value="' . hsc($category) . '" ' . $selected . '>' . hsc($category) . '</option>'; 302*1d05cddcSAtari911 } 303*1d05cddcSAtari911 echo '</select>'; 304*1d05cddcSAtari911 305*1d05cddcSAtari911 // Hidden input for the hex color 306*1d05cddcSAtari911 echo '<input type="hidden" name="color_hex_' . $rowIndex . '" value="' . $hexColor . '">'; 307*1d05cddcSAtari911 308*1d05cddcSAtari911 echo '</div>'; 309*1d05cddcSAtari911 $rowIndex++; 310*1d05cddcSAtari911 } 311*1d05cddcSAtari911 312*1d05cddcSAtari911 echo '</div>'; 313*1d05cddcSAtari911 314*1d05cddcSAtari911 // Hidden input to track number of color mappings 315*1d05cddcSAtari911 echo '<input type="hidden" name="color_mapping_count" value="' . $rowIndex . '">'; 316*1d05cddcSAtari911 317*1d05cddcSAtari911 echo '</div>'; 318*1d05cddcSAtari911 319*1d05cddcSAtari911 echo '</div>'; // end grid 320*1d05cddcSAtari911 321*1d05cddcSAtari911 // Submit button 322*1d05cddcSAtari911 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>'; 323*1d05cddcSAtari911 echo '</form>'; 324*1d05cddcSAtari911 325*1d05cddcSAtari911 // JavaScript for Import/Export 326*1d05cddcSAtari911 echo '<script> 327*1d05cddcSAtari911 async function exportConfig() { 328*1d05cddcSAtari911 try { 329*1d05cddcSAtari911 const response = await fetch("?do=admin&page=calendar&action=export_config&call=ajax", { 330*1d05cddcSAtari911 method: "POST" 331*1d05cddcSAtari911 }); 332*1d05cddcSAtari911 const data = await response.json(); 333*1d05cddcSAtari911 334*1d05cddcSAtari911 if (data.success) { 335*1d05cddcSAtari911 // Create download link 336*1d05cddcSAtari911 const blob = new Blob([data.encrypted], {type: "application/octet-stream"}); 337*1d05cddcSAtari911 const url = URL.createObjectURL(blob); 338*1d05cddcSAtari911 const a = document.createElement("a"); 339*1d05cddcSAtari911 a.href = url; 340*1d05cddcSAtari911 a.download = "sync_config_" + new Date().toISOString().split("T")[0] + ".enc"; 341*1d05cddcSAtari911 document.body.appendChild(a); 342*1d05cddcSAtari911 a.click(); 343*1d05cddcSAtari911 document.body.removeChild(a); 344*1d05cddcSAtari911 URL.revokeObjectURL(url); 345*1d05cddcSAtari911 346*1d05cddcSAtari911 alert("✅ Config exported successfully!\\n\\n⚠️ This file contains encrypted credentials.\\nKeep it secure!"); 347*1d05cddcSAtari911 } else { 348*1d05cddcSAtari911 alert("❌ Export failed: " + data.message); 349*1d05cddcSAtari911 } 350*1d05cddcSAtari911 } catch (error) { 351*1d05cddcSAtari911 alert("❌ Error: " + error.message); 352*1d05cddcSAtari911 } 353*1d05cddcSAtari911 } 354*1d05cddcSAtari911 355*1d05cddcSAtari911 async function importConfig(input) { 356*1d05cddcSAtari911 const file = input.files[0]; 357*1d05cddcSAtari911 if (!file) return; 358*1d05cddcSAtari911 359*1d05cddcSAtari911 const status = document.getElementById("importStatus"); 360*1d05cddcSAtari911 status.textContent = "⏳ Importing..."; 361*1d05cddcSAtari911 status.style.color = "#00cc07"; 362*1d05cddcSAtari911 363*1d05cddcSAtari911 try { 364*1d05cddcSAtari911 const encrypted = await file.text(); 365*1d05cddcSAtari911 366*1d05cddcSAtari911 const formData = new FormData(); 367*1d05cddcSAtari911 formData.append("encrypted_config", encrypted); 368*1d05cddcSAtari911 369*1d05cddcSAtari911 const response = await fetch("?do=admin&page=calendar&action=import_config&call=ajax", { 370*1d05cddcSAtari911 method: "POST", 371*1d05cddcSAtari911 body: formData 372*1d05cddcSAtari911 }); 373*1d05cddcSAtari911 const data = await response.json(); 374*1d05cddcSAtari911 375*1d05cddcSAtari911 if (data.success) { 376*1d05cddcSAtari911 status.textContent = "✅ Import successful! Reloading..."; 377*1d05cddcSAtari911 status.style.color = "#28a745"; 378*1d05cddcSAtari911 setTimeout(() => { 379*1d05cddcSAtari911 window.location.reload(); 380*1d05cddcSAtari911 }, 1500); 381*1d05cddcSAtari911 } else { 382*1d05cddcSAtari911 status.textContent = "❌ Import failed: " + data.message; 383*1d05cddcSAtari911 status.style.color = "#dc3545"; 384*1d05cddcSAtari911 } 385*1d05cddcSAtari911 } catch (error) { 386*1d05cddcSAtari911 status.textContent = "❌ Error: " + error.message; 387*1d05cddcSAtari911 status.style.color = "#dc3545"; 388*1d05cddcSAtari911 } 389*1d05cddcSAtari911 390*1d05cddcSAtari911 // Reset file input 391*1d05cddcSAtari911 input.value = ""; 392*1d05cddcSAtari911 } 393*1d05cddcSAtari911 </script>'; 394*1d05cddcSAtari911 395*1d05cddcSAtari911 // Sync Controls Section 396*1d05cddcSAtari911 echo '<div style="background:#f9f9f9; padding:12px; margin:15px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">'; 397*1d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;"> Sync Controls</h3>'; 398*1d05cddcSAtari911 399*1d05cddcSAtari911 // Check cron job status 400*1d05cddcSAtari911 $cronStatus = $this->getCronStatus(); 401*1d05cddcSAtari911 402*1d05cddcSAtari911 // Check log file permissions 403*1d05cddcSAtari911 $logFile = DOKU_PLUGIN . 'calendar/sync.log'; 404*1d05cddcSAtari911 $logWritable = is_writable($logFile) || is_writable(dirname($logFile)); 405*1d05cddcSAtari911 406*1d05cddcSAtari911 echo '<div style="display:flex; gap:10px; align-items:center; margin-bottom:10px;">'; 407*1d05cddcSAtari911 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>'; 408*1d05cddcSAtari911 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>'; 409*1d05cddcSAtari911 410*1d05cddcSAtari911 if ($cronStatus['active']) { 411*1d05cddcSAtari911 echo '<span style="color:#666; font-size:12px;">⏰ ' . hsc($cronStatus['frequency']) . '</span>'; 412*1d05cddcSAtari911 } else { 413*1d05cddcSAtari911 echo '<span style="color:#999; font-size:12px;">⚠️ No cron job detected</span>'; 414*1d05cddcSAtari911 } 415*1d05cddcSAtari911 416*1d05cddcSAtari911 echo '<span id="syncStatus" style="color:#666; font-size:12px; margin-left:auto;"></span>'; 417*1d05cddcSAtari911 echo '</div>'; 418*1d05cddcSAtari911 419*1d05cddcSAtari911 // Show permission warning if log not writable 420*1d05cddcSAtari911 if (!$logWritable) { 421*1d05cddcSAtari911 echo '<div style="background:#fff3e0; border-left:3px solid #ff9800; padding:8px; margin:8px 0; border-radius:3px;">'; 422*1d05cddcSAtari911 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>'; 423*1d05cddcSAtari911 echo '</div>'; 424*1d05cddcSAtari911 } 425*1d05cddcSAtari911 426*1d05cddcSAtari911 // Show debug info if cron detected 427*1d05cddcSAtari911 if ($cronStatus['active'] && !empty($cronStatus['full_line'])) { 428*1d05cddcSAtari911 echo '<details style="margin-top:5px;">'; 429*1d05cddcSAtari911 echo '<summary style="cursor:pointer; color:#999; font-size:11px;">Show cron details</summary>'; 430*1d05cddcSAtari911 echo '<pre style="background:#f0f0f0; padding:8px; border-radius:3px; font-size:10px; margin:5px 0; overflow-x:auto;">' . hsc($cronStatus['full_line']) . '</pre>'; 431*1d05cddcSAtari911 echo '</details>'; 432*1d05cddcSAtari911 } 433*1d05cddcSAtari911 434*1d05cddcSAtari911 if (!$cronStatus['active']) { 435*1d05cddcSAtari911 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>'; 436*1d05cddcSAtari911 } 437*1d05cddcSAtari911 438*1d05cddcSAtari911 echo '</div>'; 439*1d05cddcSAtari911 440*1d05cddcSAtari911 // JavaScript for Run Sync Now 441*1d05cddcSAtari911 echo '<script> 442*1d05cddcSAtari911 let syncAbortController = null; 443*1d05cddcSAtari911 444*1d05cddcSAtari911 function runSyncNow() { 445*1d05cddcSAtari911 const btn = document.getElementById("syncBtn"); 446*1d05cddcSAtari911 const stopBtn = document.getElementById("stopBtn"); 447*1d05cddcSAtari911 const status = document.getElementById("syncStatus"); 448*1d05cddcSAtari911 449*1d05cddcSAtari911 btn.disabled = true; 450*1d05cddcSAtari911 btn.style.display = "none"; 451*1d05cddcSAtari911 stopBtn.style.display = "inline-block"; 452*1d05cddcSAtari911 btn.textContent = "⏳ Running..."; 453*1d05cddcSAtari911 btn.style.background = "#999"; 454*1d05cddcSAtari911 status.textContent = "Starting sync..."; 455*1d05cddcSAtari911 status.style.color = "#00cc07"; 456*1d05cddcSAtari911 457*1d05cddcSAtari911 // Create abort controller for this sync 458*1d05cddcSAtari911 syncAbortController = new AbortController(); 459*1d05cddcSAtari911 460*1d05cddcSAtari911 fetch("?do=admin&page=calendar&action=run_sync&call=ajax", { 461*1d05cddcSAtari911 method: "POST", 462*1d05cddcSAtari911 signal: syncAbortController.signal 463*1d05cddcSAtari911 }) 464*1d05cddcSAtari911 .then(response => response.json()) 465*1d05cddcSAtari911 .then(data => { 466*1d05cddcSAtari911 if (data.success) { 467*1d05cddcSAtari911 status.textContent = "✅ " + data.message; 468*1d05cddcSAtari911 status.style.color = "#28a745"; 469*1d05cddcSAtari911 } else { 470*1d05cddcSAtari911 status.textContent = "❌ " + data.message; 471*1d05cddcSAtari911 status.style.color = "#dc3545"; 472*1d05cddcSAtari911 } 473*1d05cddcSAtari911 btn.disabled = false; 474*1d05cddcSAtari911 btn.style.display = "inline-block"; 475*1d05cddcSAtari911 stopBtn.style.display = "none"; 476*1d05cddcSAtari911 btn.textContent = "▶️ Run Sync Now"; 477*1d05cddcSAtari911 btn.style.background = "#00cc07"; 478*1d05cddcSAtari911 syncAbortController = null; 479*1d05cddcSAtari911 480*1d05cddcSAtari911 // Clear status after 10 seconds 481*1d05cddcSAtari911 setTimeout(() => { 482*1d05cddcSAtari911 status.textContent = ""; 483*1d05cddcSAtari911 }, 10000); 484*1d05cddcSAtari911 }) 485*1d05cddcSAtari911 .catch(error => { 486*1d05cddcSAtari911 if (error.name === "AbortError") { 487*1d05cddcSAtari911 status.textContent = "⏹️ Sync stopped by user"; 488*1d05cddcSAtari911 status.style.color = "#ff9800"; 489*1d05cddcSAtari911 } else { 490*1d05cddcSAtari911 status.textContent = "❌ Error: " + error.message; 491*1d05cddcSAtari911 status.style.color = "#dc3545"; 492*1d05cddcSAtari911 } 493*1d05cddcSAtari911 btn.disabled = false; 494*1d05cddcSAtari911 btn.style.display = "inline-block"; 495*1d05cddcSAtari911 stopBtn.style.display = "none"; 496*1d05cddcSAtari911 btn.textContent = "▶️ Run Sync Now"; 497*1d05cddcSAtari911 btn.style.background = "#00cc07"; 498*1d05cddcSAtari911 syncAbortController = null; 499*1d05cddcSAtari911 }); 500*1d05cddcSAtari911 } 501*1d05cddcSAtari911 502*1d05cddcSAtari911 function stopSyncNow() { 503*1d05cddcSAtari911 const status = document.getElementById("syncStatus"); 504*1d05cddcSAtari911 505*1d05cddcSAtari911 status.textContent = "⏹️ Sending stop signal..."; 506*1d05cddcSAtari911 status.style.color = "#ff9800"; 507*1d05cddcSAtari911 508*1d05cddcSAtari911 // First, send stop signal to server 509*1d05cddcSAtari911 fetch("?do=admin&page=calendar&action=stop_sync&call=ajax", { 510*1d05cddcSAtari911 method: "POST" 511*1d05cddcSAtari911 }) 512*1d05cddcSAtari911 .then(response => response.json()) 513*1d05cddcSAtari911 .then(data => { 514*1d05cddcSAtari911 if (data.success) { 515*1d05cddcSAtari911 status.textContent = "⏹️ Stop signal sent - sync will abort soon"; 516*1d05cddcSAtari911 status.style.color = "#ff9800"; 517*1d05cddcSAtari911 } else { 518*1d05cddcSAtari911 status.textContent = "⚠️ " + data.message; 519*1d05cddcSAtari911 status.style.color = "#ff9800"; 520*1d05cddcSAtari911 } 521*1d05cddcSAtari911 }) 522*1d05cddcSAtari911 .catch(error => { 523*1d05cddcSAtari911 status.textContent = "⚠️ Error sending stop signal: " + error.message; 524*1d05cddcSAtari911 status.style.color = "#ff9800"; 525*1d05cddcSAtari911 }); 526*1d05cddcSAtari911 527*1d05cddcSAtari911 // Also abort the fetch request 528*1d05cddcSAtari911 if (syncAbortController) { 529*1d05cddcSAtari911 syncAbortController.abort(); 530*1d05cddcSAtari911 status.textContent = "⏹️ Stopping sync..."; 531*1d05cddcSAtari911 status.style.color = "#ff9800"; 532*1d05cddcSAtari911 } 533*1d05cddcSAtari911 } 534*1d05cddcSAtari911 </script>'; 535*1d05cddcSAtari911 536*1d05cddcSAtari911 // Log Viewer Section - More Compact 537*1d05cddcSAtari911 echo '<div style="background:#f9f9f9; padding:12px; margin:15px 0 10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">'; 538*1d05cddcSAtari911 echo '<h3 style="margin:0 0 5px 0; color:#00cc07; font-size:16px;"> Live Sync Log</h3>'; 539*1d05cddcSAtari911 echo '<p style="color:#666; font-size:0.8em; margin:0 0 8px;">Updates every 2 seconds</p>'; 540*1d05cddcSAtari911 541*1d05cddcSAtari911 // Log viewer container 542*1d05cddcSAtari911 echo '<div style="background:#1e1e1e; border-radius:5px; overflow:hidden; box-shadow:0 2px 4px rgba(0,0,0,0.3);">'; 543*1d05cddcSAtari911 544*1d05cddcSAtari911 // Log header - More compact 545*1d05cddcSAtari911 echo '<div style="background:#2d2d2d; padding:6px 10px; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid #444;">'; 546*1d05cddcSAtari911 echo '<span style="color:#00cc07; font-family:monospace; font-weight:bold; font-size:12px;">sync.log</span>'; 547*1d05cddcSAtari911 echo '<div>'; 548*1d05cddcSAtari911 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>'; 549*1d05cddcSAtari911 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>'; 550*1d05cddcSAtari911 echo '<button onclick="downloadLog()" style="background:#666; color:white; border:none; padding:4px 8px; border-radius:3px; cursor:pointer; font-size:11px;"> Download</button>'; 551*1d05cddcSAtari911 echo '</div>'; 552*1d05cddcSAtari911 echo '</div>'; 553*1d05cddcSAtari911 554*1d05cddcSAtari911 // Log content - Reduced height to 250px 555*1d05cddcSAtari911 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>'; 556*1d05cddcSAtari911 557*1d05cddcSAtari911 echo '</div>'; 558*1d05cddcSAtari911 echo '</div>'; 559*1d05cddcSAtari911 560*1d05cddcSAtari911 // JavaScript for log viewer 561*1d05cddcSAtari911 echo '<script> 562*1d05cddcSAtari911 let refreshInterval = null; 563*1d05cddcSAtari911 let isPaused = false; 564*1d05cddcSAtari911 565*1d05cddcSAtari911 function refreshLog() { 566*1d05cddcSAtari911 if (isPaused) return; 567*1d05cddcSAtari911 568*1d05cddcSAtari911 fetch("?do=admin&page=calendar&action=get_log&call=ajax") 569*1d05cddcSAtari911 .then(response => response.json()) 570*1d05cddcSAtari911 .then(data => { 571*1d05cddcSAtari911 const logContent = document.getElementById("logContent"); 572*1d05cddcSAtari911 if (logContent) { 573*1d05cddcSAtari911 logContent.textContent = data.log || "No log data available"; 574*1d05cddcSAtari911 logContent.scrollTop = logContent.scrollHeight; 575*1d05cddcSAtari911 } 576*1d05cddcSAtari911 }) 577*1d05cddcSAtari911 .catch(error => { 578*1d05cddcSAtari911 console.error("Error fetching log:", error); 579*1d05cddcSAtari911 }); 580*1d05cddcSAtari911 } 581*1d05cddcSAtari911 582*1d05cddcSAtari911 function togglePause() { 583*1d05cddcSAtari911 isPaused = !isPaused; 584*1d05cddcSAtari911 const btn = document.getElementById("pauseBtn"); 585*1d05cddcSAtari911 if (isPaused) { 586*1d05cddcSAtari911 btn.textContent = "▶ Resume"; 587*1d05cddcSAtari911 btn.style.background = "#00cc07"; 588*1d05cddcSAtari911 } else { 589*1d05cddcSAtari911 btn.textContent = "⏸ Pause"; 590*1d05cddcSAtari911 btn.style.background = "#666"; 591*1d05cddcSAtari911 refreshLog(); 592*1d05cddcSAtari911 } 593*1d05cddcSAtari911 } 594*1d05cddcSAtari911 595*1d05cddcSAtari911 function clearLog() { 596*1d05cddcSAtari911 if (!confirm("Clear the sync log file?\\n\\nThis will delete all log entries.")) { 597*1d05cddcSAtari911 return; 598*1d05cddcSAtari911 } 599*1d05cddcSAtari911 600*1d05cddcSAtari911 fetch("?do=admin&page=calendar&action=clear_log&call=ajax", { 601*1d05cddcSAtari911 method: "POST" 602*1d05cddcSAtari911 }) 603*1d05cddcSAtari911 .then(response => response.json()) 604*1d05cddcSAtari911 .then(data => { 605*1d05cddcSAtari911 if (data.success) { 606*1d05cddcSAtari911 refreshLog(); 607*1d05cddcSAtari911 alert("Log cleared successfully"); 608*1d05cddcSAtari911 } else { 609*1d05cddcSAtari911 alert("Error clearing log: " + data.message); 610*1d05cddcSAtari911 } 611*1d05cddcSAtari911 }) 612*1d05cddcSAtari911 .catch(error => { 613*1d05cddcSAtari911 alert("Error: " + error.message); 614*1d05cddcSAtari911 }); 615*1d05cddcSAtari911 } 616*1d05cddcSAtari911 617*1d05cddcSAtari911 function downloadLog() { 618*1d05cddcSAtari911 window.location.href = "?do=admin&page=calendar&action=download_log"; 619*1d05cddcSAtari911 } 620*1d05cddcSAtari911 621*1d05cddcSAtari911 // Start auto-refresh 622*1d05cddcSAtari911 refreshLog(); 623*1d05cddcSAtari911 refreshInterval = setInterval(refreshLog, 2000); 624*1d05cddcSAtari911 625*1d05cddcSAtari911 // Cleanup on page unload 626*1d05cddcSAtari911 window.addEventListener("beforeunload", function() { 627*1d05cddcSAtari911 if (refreshInterval) { 628*1d05cddcSAtari911 clearInterval(refreshInterval); 629*1d05cddcSAtari911 } 630*1d05cddcSAtari911 }); 631*1d05cddcSAtari911 </script>'; 632*1d05cddcSAtari911 } 633*1d05cddcSAtari911 634*1d05cddcSAtari911 private function renderManageTab() { 635*1d05cddcSAtari911 global $INPUT; 636*1d05cddcSAtari911 637*1d05cddcSAtari911 // Show message if present 638*1d05cddcSAtari911 if ($INPUT->has('msg')) { 639*1d05cddcSAtari911 $msg = hsc($INPUT->str('msg')); 640*1d05cddcSAtari911 $type = $INPUT->str('msgtype', 'success'); 641*1d05cddcSAtari911 echo "<div style=\"padding:10px; margin:10px 0; border-left:3px solid " . ($type === 'success' ? '#28a745' : '#dc3545') . "; background:" . ($type === 'success' ? '#d4edda' : '#f8d7da') . "; border-radius:3px;\">"; 642*1d05cddcSAtari911 echo $msg; 643*1d05cddcSAtari911 echo "</div>"; 644*1d05cddcSAtari911 } 645*1d05cddcSAtari911 646*1d05cddcSAtari911 echo '<h2 style="margin:10px 0; font-size:20px;">Manage Calendar Events</h2>'; 647*1d05cddcSAtari911 648*1d05cddcSAtari911 // Events Manager Section - NEW! 649*1d05cddcSAtari911 echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">'; 650*1d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;"> Events Manager</h3>'; 651*1d05cddcSAtari911 echo '<p style="color:#666; font-size:11px; margin:0 0 10px;">Scan, export, and import all calendar events across all namespaces.</p>'; 652*1d05cddcSAtari911 653*1d05cddcSAtari911 // Get event statistics 654*1d05cddcSAtari911 $stats = $this->getEventStatistics(); 655*1d05cddcSAtari911 656*1d05cddcSAtari911 // Statistics display 657*1d05cddcSAtari911 echo '<div style="background:white; padding:10px; border-radius:3px; margin-bottom:10px; border:1px solid #ddd;">'; 658*1d05cddcSAtari911 echo '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(150px, 1fr)); gap:10px; font-size:12px;">'; 659*1d05cddcSAtari911 660*1d05cddcSAtari911 echo '<div style="background:#f3e5f5; padding:8px; border-radius:3px; text-align:center;">'; 661*1d05cddcSAtari911 echo '<div style="font-size:24px; font-weight:bold; color:#7b1fa2;">' . $stats['total_events'] . '</div>'; 662*1d05cddcSAtari911 echo '<div style="color:#666; font-size:10px;">Total Events</div>'; 663*1d05cddcSAtari911 echo '</div>'; 664*1d05cddcSAtari911 665*1d05cddcSAtari911 echo '<div style="background:#f3e5f5; padding:8px; border-radius:3px; text-align:center;">'; 666*1d05cddcSAtari911 echo '<div style="font-size:24px; font-weight:bold; color:#7b1fa2;">' . $stats['total_namespaces'] . '</div>'; 667*1d05cddcSAtari911 echo '<div style="color:#666; font-size:10px;">Namespaces</div>'; 668*1d05cddcSAtari911 echo '</div>'; 669*1d05cddcSAtari911 670*1d05cddcSAtari911 echo '<div style="background:#e8f5e9; padding:8px; border-radius:3px; text-align:center;">'; 671*1d05cddcSAtari911 echo '<div style="font-size:24px; font-weight:bold; color:#388e3c;">' . $stats['total_files'] . '</div>'; 672*1d05cddcSAtari911 echo '<div style="color:#666; font-size:10px;">JSON Files</div>'; 673*1d05cddcSAtari911 echo '</div>'; 674*1d05cddcSAtari911 675*1d05cddcSAtari911 echo '<div style="background:#fff3e0; padding:8px; border-radius:3px; text-align:center;">'; 676*1d05cddcSAtari911 echo '<div style="font-size:24px; font-weight:bold; color:#f57c00;">' . $stats['total_recurring'] . '</div>'; 677*1d05cddcSAtari911 echo '<div style="color:#666; font-size:10px;">Recurring</div>'; 678*1d05cddcSAtari911 echo '</div>'; 679*1d05cddcSAtari911 680*1d05cddcSAtari911 echo '</div>'; 681*1d05cddcSAtari911 682*1d05cddcSAtari911 // Last scan time 683*1d05cddcSAtari911 if (!empty($stats['last_scan'])) { 684*1d05cddcSAtari911 echo '<div style="margin-top:8px; color:#666; font-size:10px;">Last scanned: ' . hsc($stats['last_scan']) . '</div>'; 685*1d05cddcSAtari911 } 686*1d05cddcSAtari911 687*1d05cddcSAtari911 echo '</div>'; 688*1d05cddcSAtari911 689*1d05cddcSAtari911 // Action buttons 690*1d05cddcSAtari911 echo '<div style="display:flex; gap:10px; flex-wrap:wrap; margin-top:10px;">'; 691*1d05cddcSAtari911 692*1d05cddcSAtari911 // Rescan button 693*1d05cddcSAtari911 echo '<form method="post" action="?do=admin&page=calendar&tab=manage" style="display:inline;">'; 694*1d05cddcSAtari911 echo '<input type="hidden" name="action" value="rescan_events">'; 695*1d05cddcSAtari911 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;">'; 696*1d05cddcSAtari911 echo '<span></span><span>Re-scan Events</span>'; 697*1d05cddcSAtari911 echo '</button>'; 698*1d05cddcSAtari911 echo '</form>'; 699*1d05cddcSAtari911 700*1d05cddcSAtari911 // Export button 701*1d05cddcSAtari911 echo '<form method="post" action="?do=admin&page=calendar&tab=manage" style="display:inline;">'; 702*1d05cddcSAtari911 echo '<input type="hidden" name="action" value="export_all_events">'; 703*1d05cddcSAtari911 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;">'; 704*1d05cddcSAtari911 echo '<span></span><span>Export All Events</span>'; 705*1d05cddcSAtari911 echo '</button>'; 706*1d05cddcSAtari911 echo '</form>'; 707*1d05cddcSAtari911 708*1d05cddcSAtari911 // Import button (with file upload) 709*1d05cddcSAtari911 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?\')">'; 710*1d05cddcSAtari911 echo '<input type="hidden" name="action" value="import_all_events">'; 711*1d05cddcSAtari911 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;">'; 712*1d05cddcSAtari911 echo '<span></span><span>Import Events</span>'; 713*1d05cddcSAtari911 echo '<input type="file" name="import_file" accept=".json,.zip" required style="display:none;" onchange="this.form.submit()">'; 714*1d05cddcSAtari911 echo '</label>'; 715*1d05cddcSAtari911 echo '</form>'; 716*1d05cddcSAtari911 717*1d05cddcSAtari911 echo '</div>'; 718*1d05cddcSAtari911 719*1d05cddcSAtari911 // Breakdown by namespace 720*1d05cddcSAtari911 if (!empty($stats['by_namespace'])) { 721*1d05cddcSAtari911 echo '<details style="margin-top:12px;">'; 722*1d05cddcSAtari911 echo '<summary style="cursor:pointer; padding:6px; background:#e9e9e9; border-radius:3px; font-size:11px; font-weight:bold;">View Breakdown by Namespace</summary>'; 723*1d05cddcSAtari911 echo '<div style="margin-top:8px; max-height:200px; overflow-y:auto; border:1px solid #ddd; border-radius:3px;">'; 724*1d05cddcSAtari911 echo '<table style="width:100%; border-collapse:collapse; font-size:11px;">'; 725*1d05cddcSAtari911 echo '<thead style="position:sticky; top:0; background:#f5f5f5;">'; 726*1d05cddcSAtari911 echo '<tr>'; 727*1d05cddcSAtari911 echo '<th style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd;">Namespace</th>'; 728*1d05cddcSAtari911 echo '<th style="padding:4px 6px; text-align:right; border-bottom:2px solid #ddd;">Events</th>'; 729*1d05cddcSAtari911 echo '<th style="padding:4px 6px; text-align:right; border-bottom:2px solid #ddd;">Files</th>'; 730*1d05cddcSAtari911 echo '</tr></thead><tbody>'; 731*1d05cddcSAtari911 732*1d05cddcSAtari911 foreach ($stats['by_namespace'] as $ns => $nsStats) { 733*1d05cddcSAtari911 echo '<tr style="border-bottom:1px solid #eee;">'; 734*1d05cddcSAtari911 echo '<td style="padding:4px 6px;"><code style="background:#f0f0f0; padding:1px 3px; border-radius:2px; font-size:10px;">' . hsc($ns ?: '(default)') . '</code></td>'; 735*1d05cddcSAtari911 echo '<td style="padding:4px 6px; text-align:right;"><strong>' . $nsStats['events'] . '</strong></td>'; 736*1d05cddcSAtari911 echo '<td style="padding:4px 6px; text-align:right;">' . $nsStats['files'] . '</td>'; 737*1d05cddcSAtari911 echo '</tr>'; 738*1d05cddcSAtari911 } 739*1d05cddcSAtari911 740*1d05cddcSAtari911 echo '</tbody></table>'; 741*1d05cddcSAtari911 echo '</div>'; 742*1d05cddcSAtari911 echo '</details>'; 743*1d05cddcSAtari911 } 744*1d05cddcSAtari911 745*1d05cddcSAtari911 echo '</div>'; 746*1d05cddcSAtari911 747*1d05cddcSAtari911 // Cleanup Events Section - Redesigned for compact, sleek look 748*1d05cddcSAtari911 echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #ff9800; border-radius:3px; max-width:1200px;">'; 749*1d05cddcSAtari911 echo '<h3 style="margin:0 0 6px 0; color:#f57c00; font-size:16px;"> Cleanup Old Events</h3>'; 750*1d05cddcSAtari911 echo '<p style="color:#666; font-size:11px; margin:0 0 12px;">Delete events based on criteria below. Automatic backup created before deletion.</p>'; 751*1d05cddcSAtari911 752*1d05cddcSAtari911 echo '<form method="post" action="?do=admin&page=calendar&tab=manage" id="cleanupForm">'; 753*1d05cddcSAtari911 echo '<input type="hidden" name="action" value="cleanup_events">'; 754*1d05cddcSAtari911 755*1d05cddcSAtari911 // Compact options layout 756*1d05cddcSAtari911 echo '<div style="background:white; padding:10px; border:1px solid #e0e0e0; border-radius:3px; margin-bottom:10px;">'; 757*1d05cddcSAtari911 758*1d05cddcSAtari911 // Radio buttons in a row 759*1d05cddcSAtari911 echo '<div style="display:flex; gap:20px; margin-bottom:10px; padding-bottom:8px; border-bottom:1px solid #f0f0f0;">'; 760*1d05cddcSAtari911 echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">'; 761*1d05cddcSAtari911 echo '<input type="radio" name="cleanup_type" value="age" checked onchange="updateCleanupOptions()">'; 762*1d05cddcSAtari911 echo '<span>By Age</span>'; 763*1d05cddcSAtari911 echo '</label>'; 764*1d05cddcSAtari911 echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">'; 765*1d05cddcSAtari911 echo '<input type="radio" name="cleanup_type" value="status" onchange="updateCleanupOptions()">'; 766*1d05cddcSAtari911 echo '<span>By Status</span>'; 767*1d05cddcSAtari911 echo '</label>'; 768*1d05cddcSAtari911 echo '<label style="cursor:pointer; font-size:12px; font-weight:600; display:flex; align-items:center; gap:4px;">'; 769*1d05cddcSAtari911 echo '<input type="radio" name="cleanup_type" value="range" onchange="updateCleanupOptions()">'; 770*1d05cddcSAtari911 echo '<span>By Date Range</span>'; 771*1d05cddcSAtari911 echo '</label>'; 772*1d05cddcSAtari911 echo '</div>'; 773*1d05cddcSAtari911 774*1d05cddcSAtari911 // Age options 775*1d05cddcSAtari911 echo '<div id="age-options" style="padding:6px 0;">'; 776*1d05cddcSAtari911 echo '<span style="font-size:11px; color:#666; margin-right:8px;">Delete events older than:</span>'; 777*1d05cddcSAtari911 echo '<select name="age_value" style="width:50px; padding:3px 4px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px; margin-right:4px;">'; 778*1d05cddcSAtari911 for ($i = 1; $i <= 24; $i++) { 779*1d05cddcSAtari911 $sel = $i === 6 ? ' selected' : ''; 780*1d05cddcSAtari911 echo '<option value="' . $i . '"' . $sel . '>' . $i . '</option>'; 781*1d05cddcSAtari911 } 782*1d05cddcSAtari911 echo '</select>'; 783*1d05cddcSAtari911 echo '<select name="age_unit" style="width:80px; padding:3px 4px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px;">'; 784*1d05cddcSAtari911 echo '<option value="months" selected>months</option>'; 785*1d05cddcSAtari911 echo '<option value="years">years</option>'; 786*1d05cddcSAtari911 echo '</select>'; 787*1d05cddcSAtari911 echo '</div>'; 788*1d05cddcSAtari911 789*1d05cddcSAtari911 // Status options 790*1d05cddcSAtari911 echo '<div id="status-options" style="padding:6px 0; opacity:0.4;">'; 791*1d05cddcSAtari911 echo '<span style="font-size:11px; color:#666; margin-right:8px;">Delete:</span>'; 792*1d05cddcSAtari911 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>'; 793*1d05cddcSAtari911 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>'; 794*1d05cddcSAtari911 echo '</div>'; 795*1d05cddcSAtari911 796*1d05cddcSAtari911 // Range options 797*1d05cddcSAtari911 echo '<div id="range-options" style="padding:6px 0; opacity:0.4;">'; 798*1d05cddcSAtari911 echo '<span style="font-size:11px; color:#666; margin-right:8px;">From:</span>'; 799*1d05cddcSAtari911 echo '<input type="date" name="range_start" style="padding:3px 6px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px; margin-right:10px;">'; 800*1d05cddcSAtari911 echo '<span style="font-size:11px; color:#666; margin-right:8px;">To:</span>'; 801*1d05cddcSAtari911 echo '<input type="date" name="range_end" style="padding:3px 6px; font-size:11px; border:1px solid #d0d0d0; border-radius:3px;">'; 802*1d05cddcSAtari911 echo '</div>'; 803*1d05cddcSAtari911 804*1d05cddcSAtari911 echo '</div>'; 805*1d05cddcSAtari911 806*1d05cddcSAtari911 // Namespace filter - compact 807*1d05cddcSAtari911 echo '<div style="background:white; padding:8px 10px; border:1px solid #e0e0e0; border-radius:3px; margin-bottom:10px; display:flex; align-items:center; gap:8px;">'; 808*1d05cddcSAtari911 echo '<label style="font-size:11px; font-weight:600; white-space:nowrap; color:#555;">Namespace:</label>'; 809*1d05cddcSAtari911 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;">'; 810*1d05cddcSAtari911 echo '</div>'; 811*1d05cddcSAtari911 812*1d05cddcSAtari911 // Action buttons - compact row 813*1d05cddcSAtari911 echo '<div style="display:flex; gap:8px; align-items:center;">'; 814*1d05cddcSAtari911 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>'; 815*1d05cddcSAtari911 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>'; 816*1d05cddcSAtari911 echo '<span style="font-size:10px; color:#999;">⚠️ Backup created automatically</span>'; 817*1d05cddcSAtari911 echo '</div>'; 818*1d05cddcSAtari911 819*1d05cddcSAtari911 echo '</form>'; 820*1d05cddcSAtari911 821*1d05cddcSAtari911 // Preview results area 822*1d05cddcSAtari911 echo '<div id="cleanup-preview" style="margin-top:10px; display:none;"></div>'; 823*1d05cddcSAtari911 824*1d05cddcSAtari911 echo '<script> 825*1d05cddcSAtari911 function updateCleanupOptions() { 826*1d05cddcSAtari911 const type = document.querySelector(\'input[name="cleanup_type"]:checked\').value; 827*1d05cddcSAtari911 828*1d05cddcSAtari911 // Show selected, gray out others 829*1d05cddcSAtari911 document.getElementById(\'age-options\').style.opacity = type === \'age\' ? \'1\' : \'0.4\'; 830*1d05cddcSAtari911 document.getElementById(\'status-options\').style.opacity = type === \'status\' ? \'1\' : \'0.4\'; 831*1d05cddcSAtari911 document.getElementById(\'range-options\').style.opacity = type === \'range\' ? \'1\' : \'0.4\'; 832*1d05cddcSAtari911 833*1d05cddcSAtari911 // Enable/disable inputs 834*1d05cddcSAtari911 document.querySelectorAll(\'#age-options select\').forEach(el => el.disabled = type !== \'age\'); 835*1d05cddcSAtari911 document.querySelectorAll(\'#status-options input\').forEach(el => el.disabled = type !== \'status\'); 836*1d05cddcSAtari911 document.querySelectorAll(\'#range-options input\').forEach(el => el.disabled = type !== \'range\'); 837*1d05cddcSAtari911 } 838*1d05cddcSAtari911 839*1d05cddcSAtari911 function previewCleanup() { 840*1d05cddcSAtari911 const form = document.getElementById(\'cleanupForm\'); 841*1d05cddcSAtari911 const formData = new FormData(form); 842*1d05cddcSAtari911 formData.set(\'action\', \'preview_cleanup\'); 843*1d05cddcSAtari911 844*1d05cddcSAtari911 const preview = document.getElementById(\'cleanup-preview\'); 845*1d05cddcSAtari911 preview.innerHTML = \'<div style="text-align:center; padding:20px; color:#666;">Loading preview...</div>\'; 846*1d05cddcSAtari911 preview.style.display = \'block\'; 847*1d05cddcSAtari911 848*1d05cddcSAtari911 fetch(\'?do=admin&page=calendar&tab=manage\', { 849*1d05cddcSAtari911 method: \'POST\', 850*1d05cddcSAtari911 body: new URLSearchParams(formData) 851*1d05cddcSAtari911 }) 852*1d05cddcSAtari911 .then(r => r.json()) 853*1d05cddcSAtari911 .then(data => { 854*1d05cddcSAtari911 if (data.count === 0) { 855*1d05cddcSAtari911 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>\'; 856*1d05cddcSAtari911 857*1d05cddcSAtari911 // Show debug info if available 858*1d05cddcSAtari911 if (data.debug) { 859*1d05cddcSAtari911 html += \'<details style="margin-top:8px; font-size:11px; color:#666;">\'; 860*1d05cddcSAtari911 html += \'<summary style="cursor:pointer;">Debug Info</summary>\'; 861*1d05cddcSAtari911 html += \'<pre style="background:#f5f5f5; padding:6px; margin-top:4px; border-radius:3px; overflow-x:auto;">\' + JSON.stringify(data.debug, null, 2) + \'</pre>\'; 862*1d05cddcSAtari911 html += \'</details>\'; 863*1d05cddcSAtari911 } 864*1d05cddcSAtari911 865*1d05cddcSAtari911 preview.innerHTML = html; 866*1d05cddcSAtari911 } else { 867*1d05cddcSAtari911 let html = \'<div style="background:#f8d7da; border:1px solid #f5c6cb; padding:10px; border-radius:3px; font-size:12px; color:#721c24;">\'; 868*1d05cddcSAtari911 html += \'<strong>⚠️ Warning:</strong> The following \' + data.count + \' event(s) would be deleted:<br><br>\'; 869*1d05cddcSAtari911 html += \'<div style="max-height:150px; overflow-y:auto; margin-top:6px; background:white; padding:6px; border-radius:3px;">\'; 870*1d05cddcSAtari911 data.events.forEach(evt => { 871*1d05cddcSAtari911 html += \'<div style="padding:3px; border-bottom:1px solid #eee; font-size:11px;">\'; 872*1d05cddcSAtari911 html += \'• \' + evt.title + \' (\' + evt.date + \')\'; 873*1d05cddcSAtari911 if (evt.namespace) html += \' <span style="background:#e3f2fd; padding:1px 4px; border-radius:2px; font-size:9px;">\' + evt.namespace + \'</span>\'; 874*1d05cddcSAtari911 html += \'</div>\'; 875*1d05cddcSAtari911 }); 876*1d05cddcSAtari911 html += \'</div></div>\'; 877*1d05cddcSAtari911 preview.innerHTML = html; 878*1d05cddcSAtari911 } 879*1d05cddcSAtari911 }) 880*1d05cddcSAtari911 .catch(err => { 881*1d05cddcSAtari911 preview.innerHTML = \'<div style="background:#f8d7da; border:1px solid #f5c6cb; padding:10px; border-radius:3px; font-size:12px; color:#721c24;">Error loading preview</div>\'; 882*1d05cddcSAtari911 }); 883*1d05cddcSAtari911 } 884*1d05cddcSAtari911 885*1d05cddcSAtari911 function confirmCleanup() { 886*1d05cddcSAtari911 return confirm(\'Are you sure you want to delete these events? A backup will be created first, but this action cannot be easily undone.\'); 887*1d05cddcSAtari911 } 888*1d05cddcSAtari911 889*1d05cddcSAtari911 updateCleanupOptions(); 890*1d05cddcSAtari911 </script>'; 891*1d05cddcSAtari911 892*1d05cddcSAtari911 echo '</div>'; 893*1d05cddcSAtari911 894*1d05cddcSAtari911 // Recurring Events Section 895*1d05cddcSAtari911 echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">'; 896*1d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;"> Recurring Events</h3>'; 897*1d05cddcSAtari911 898*1d05cddcSAtari911 $recurringEvents = $this->findRecurringEvents(); 899*1d05cddcSAtari911 900*1d05cddcSAtari911 if (empty($recurringEvents)) { 901*1d05cddcSAtari911 echo '<p style="color:#666; font-size:13px; margin:5px 0;">No recurring events found.</p>'; 902*1d05cddcSAtari911 } else { 903*1d05cddcSAtari911 // Search bar 904*1d05cddcSAtari911 echo '<div style="margin-bottom:8px;">'; 905*1d05cddcSAtari911 echo '<input type="text" id="searchRecurring" onkeyup="filterRecurringEvents()" placeholder=" Search recurring events..." style="width:100%; padding:6px 10px; border:1px solid #ddd; border-radius:3px; font-size:12px;">'; 906*1d05cddcSAtari911 echo '</div>'; 907*1d05cddcSAtari911 908*1d05cddcSAtari911 echo '<style> 909*1d05cddcSAtari911 .sort-arrow { 910*1d05cddcSAtari911 color: #999; 911*1d05cddcSAtari911 font-size: 10px; 912*1d05cddcSAtari911 margin-left: 3px; 913*1d05cddcSAtari911 display: inline-block; 914*1d05cddcSAtari911 } 915*1d05cddcSAtari911 #recurringTable th:hover { 916*1d05cddcSAtari911 background: #ddd; 917*1d05cddcSAtari911 } 918*1d05cddcSAtari911 #recurringTable th:hover .sort-arrow { 919*1d05cddcSAtari911 color: #00cc07; 920*1d05cddcSAtari911 } 921*1d05cddcSAtari911 .recurring-row-hidden { 922*1d05cddcSAtari911 display: none; 923*1d05cddcSAtari911 } 924*1d05cddcSAtari911 </style>'; 925*1d05cddcSAtari911 echo '<div style="max-height:250px; overflow-y:auto; border:1px solid #ddd; border-radius:3px;">'; 926*1d05cddcSAtari911 echo '<table id="recurringTable" style="width:100%; border-collapse:collapse; font-size:11px;">'; 927*1d05cddcSAtari911 echo '<thead style="position:sticky; top:0; background:#e9e9e9;">'; 928*1d05cddcSAtari911 echo '<tr>'; 929*1d05cddcSAtari911 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>'; 930*1d05cddcSAtari911 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>'; 931*1d05cddcSAtari911 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>'; 932*1d05cddcSAtari911 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>'; 933*1d05cddcSAtari911 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>'; 934*1d05cddcSAtari911 echo '<th style="padding:4px 6px; text-align:left; border-bottom:2px solid #ddd;">Actions</th>'; 935*1d05cddcSAtari911 echo '</tr></thead><tbody id="recurringTableBody">'; 936*1d05cddcSAtari911 937*1d05cddcSAtari911 foreach ($recurringEvents as $series) { 938*1d05cddcSAtari911 echo '<tr style="border-bottom:1px solid #eee;">'; 939*1d05cddcSAtari911 echo '<td style="padding:4px 6px;">' . hsc($series['title']) . '</td>'; 940*1d05cddcSAtari911 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>'; 941*1d05cddcSAtari911 echo '<td style="padding:4px 6px;">' . hsc($series['pattern']) . '</td>'; 942*1d05cddcSAtari911 echo '<td style="padding:4px 6px;">' . hsc($series['firstDate']) . '</td>'; 943*1d05cddcSAtari911 echo '<td style="padding:4px 6px;"><strong>' . $series['count'] . '</strong></td>'; 944*1d05cddcSAtari911 echo '<td style="padding:4px 6px; white-space:nowrap;">'; 945*1d05cddcSAtari911 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>'; 946*1d05cddcSAtari911 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>'; 947*1d05cddcSAtari911 echo '</td>'; 948*1d05cddcSAtari911 echo '</tr>'; 949*1d05cddcSAtari911 } 950*1d05cddcSAtari911 951*1d05cddcSAtari911 echo '</tbody></table>'; 952*1d05cddcSAtari911 echo '</div>'; 953*1d05cddcSAtari911 echo '<p style="color:#666; font-size:10px; margin:5px 0 0;">Total: ' . count($recurringEvents) . ' series</p>'; 954*1d05cddcSAtari911 } 955*1d05cddcSAtari911 echo '</div>'; 956*1d05cddcSAtari911 957*1d05cddcSAtari911 // Compact Tree-based Namespace Manager 958*1d05cddcSAtari911 echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:1200px;">'; 959*1d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;"> Namespace Explorer</h3>'; 960*1d05cddcSAtari911 echo '<p style="color:#666; font-size:11px; margin:0 0 8px;">Select events and move between namespaces. Drag & drop also supported.</p>'; 961*1d05cddcSAtari911 962*1d05cddcSAtari911 // Search bar 963*1d05cddcSAtari911 echo '<div style="margin-bottom:8px;">'; 964*1d05cddcSAtari911 echo '<input type="text" id="searchEvents" onkeyup="filterEvents()" placeholder=" Search events by title..." style="width:100%; padding:6px 10px; border:1px solid #ddd; border-radius:3px; font-size:12px;">'; 965*1d05cddcSAtari911 echo '</div>'; 966*1d05cddcSAtari911 967*1d05cddcSAtari911 $eventsByNamespace = $this->getEventsByNamespace(); 968*1d05cddcSAtari911 969*1d05cddcSAtari911 // Control bar 970*1d05cddcSAtari911 echo '<form method="post" action="?do=admin&page=calendar&tab=manage" id="moveForm">'; 971*1d05cddcSAtari911 echo '<input type="hidden" name="action" value="move_selected_events" id="formAction">'; 972*1d05cddcSAtari911 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;">'; 973*1d05cddcSAtari911 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>'; 974*1d05cddcSAtari911 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>'; 975*1d05cddcSAtari911 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>'; 976*1d05cddcSAtari911 echo '<span style="margin-left:10px;">Move to:</span>'; 977*1d05cddcSAtari911 echo '<input list="namespaceList" name="target_namespace" required style="padding:3px 6px; border:1px solid #ddd; border-radius:2px; font-size:11px; min-width:150px;" placeholder="Type or select...">'; 978*1d05cddcSAtari911 echo '<datalist id="namespaceList">'; 979*1d05cddcSAtari911 echo '<option value="">(default)</option>'; 980*1d05cddcSAtari911 foreach (array_keys($eventsByNamespace) as $ns) { 981*1d05cddcSAtari911 if ($ns !== '') { 982*1d05cddcSAtari911 echo '<option value="' . hsc($ns) . '">' . hsc($ns) . '</option>'; 983*1d05cddcSAtari911 } 984*1d05cddcSAtari911 } 985*1d05cddcSAtari911 echo '</datalist>'; 986*1d05cddcSAtari911 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>'; 987*1d05cddcSAtari911 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>'; 988*1d05cddcSAtari911 echo '<span id="selectedCount" style="margin-left:auto; color:#00cc07; font-size:11px;">0 selected</span>'; 989*1d05cddcSAtari911 echo '</div>'; 990*1d05cddcSAtari911 991*1d05cddcSAtari911 echo '<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">'; 992*1d05cddcSAtari911 993*1d05cddcSAtari911 // Event list with checkboxes 994*1d05cddcSAtari911 echo '<div>'; 995*1d05cddcSAtari911 echo '<div style="max-height:450px; overflow-y:auto; border:1px solid #ddd; border-radius:3px; background:white;">'; 996*1d05cddcSAtari911 997*1d05cddcSAtari911 foreach ($eventsByNamespace as $namespace => $data) { 998*1d05cddcSAtari911 $nsId = 'ns_' . md5($namespace); 999*1d05cddcSAtari911 $eventCount = count($data['events']); 1000*1d05cddcSAtari911 1001*1d05cddcSAtari911 echo '<div style="border-bottom:1px solid #ddd;">'; 1002*1d05cddcSAtari911 1003*1d05cddcSAtari911 // Namespace header - ultra compact 1004*1d05cddcSAtari911 echo '<div style="background:#f5f5f5; padding:3px 6px; display:flex; justify-content:space-between; align-items:center; font-size:11px;">'; 1005*1d05cddcSAtari911 echo '<div style="display:flex; align-items:center; gap:4px;">'; 1006*1d05cddcSAtari911 echo '<span onclick="toggleNamespace(\'' . $nsId . '\')" style="cursor:pointer; width:12px; display:inline-block; font-size:10px;"><span id="' . $nsId . '_arrow">▶</span></span>'; 1007*1d05cddcSAtari911 echo '<input type="checkbox" onclick="toggleNamespaceSelect(\'' . $nsId . '\')" id="' . $nsId . '_check" style="margin:0; width:12px; height:12px;">'; 1008*1d05cddcSAtari911 echo '<span onclick="toggleNamespace(\'' . $nsId . '\')" style="cursor:pointer; font-weight:600; font-size:11px;"> ' . hsc($namespace ?: '(default)') . '</span>'; 1009*1d05cddcSAtari911 echo '</div>'; 1010*1d05cddcSAtari911 echo '<div style="display:flex; gap:3px; align-items:center;">'; 1011*1d05cddcSAtari911 echo '<span style="background:#00cc07; color:white; padding:0px 4px; border-radius:6px; font-size:9px; line-height:14px;">' . $eventCount . '</span>'; 1012*1d05cddcSAtari911 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>'; 1013*1d05cddcSAtari911 echo '</div>'; 1014*1d05cddcSAtari911 echo '</div>'; 1015*1d05cddcSAtari911 1016*1d05cddcSAtari911 // Events - ultra compact 1017*1d05cddcSAtari911 echo '<div id="' . $nsId . '" style="display:none; max-height:150px; overflow-y:auto;">'; 1018*1d05cddcSAtari911 foreach ($data['events'] as $event) { 1019*1d05cddcSAtari911 $eventId = $event['id'] . '|' . $namespace . '|' . $event['date'] . '|' . $event['month']; 1020*1d05cddcSAtari911 $checkId = 'evt_' . md5($eventId); 1021*1d05cddcSAtari911 1022*1d05cddcSAtari911 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\'">'; 1023*1d05cddcSAtari911 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;">'; 1024*1d05cddcSAtari911 echo '<div style="flex:1; min-width:0;">'; 1025*1d05cddcSAtari911 echo '<div style="font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; font-size:10px;">' . hsc($event['title']) . '</div>'; 1026*1d05cddcSAtari911 echo '<div style="color:#999; font-size:9px;">' . hsc($event['date']) . ($event['startTime'] ? ' • ' . hsc($event['startTime']) : '') . '</div>'; 1027*1d05cddcSAtari911 echo '</div>'; 1028*1d05cddcSAtari911 echo '</div>'; 1029*1d05cddcSAtari911 } 1030*1d05cddcSAtari911 echo '</div>'; 1031*1d05cddcSAtari911 echo '</div>'; 1032*1d05cddcSAtari911 } 1033*1d05cddcSAtari911 1034*1d05cddcSAtari911 echo '</div>'; 1035*1d05cddcSAtari911 echo '</div>'; 1036*1d05cddcSAtari911 1037*1d05cddcSAtari911 // Drop zones - ultra compact 1038*1d05cddcSAtari911 echo '<div>'; 1039*1d05cddcSAtari911 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>'; 1040*1d05cddcSAtari911 echo '<div style="border:1px solid #ddd; border-top:none; border-radius:0 0 3px 3px; max-height:450px; overflow-y:auto; background:white;">'; 1041*1d05cddcSAtari911 1042*1d05cddcSAtari911 foreach (array_keys($eventsByNamespace) as $namespace) { 1043*1d05cddcSAtari911 echo '<div ondrop="drop(event, \'' . hsc($namespace) . '\')" ondragover="allowDrop(event)" style="padding:5px 6px; border-bottom:1px solid #eee; background:white; min-height:28px;" onmouseover="this.style.background=\'#f0fff0\'" onmouseout="this.style.background=\'white\'">'; 1044*1d05cddcSAtari911 echo '<div style="font-size:11px; font-weight:600; color:#00cc07;"> ' . hsc($namespace ?: '(default)') . '</div>'; 1045*1d05cddcSAtari911 echo '<div style="color:#999; font-size:9px; margin-top:1px;">Drop here</div>'; 1046*1d05cddcSAtari911 echo '</div>'; 1047*1d05cddcSAtari911 } 1048*1d05cddcSAtari911 1049*1d05cddcSAtari911 echo '</div>'; 1050*1d05cddcSAtari911 echo '</div>'; 1051*1d05cddcSAtari911 1052*1d05cddcSAtari911 echo '</div>'; // end grid 1053*1d05cddcSAtari911 echo '</form>'; 1054*1d05cddcSAtari911 1055*1d05cddcSAtari911 echo '</div>'; 1056*1d05cddcSAtari911 1057*1d05cddcSAtari911 // JavaScript 1058*1d05cddcSAtari911 echo '<script> 1059*1d05cddcSAtari911 // Table sorting functionality - defined early so onclick handlers work 1060*1d05cddcSAtari911 let sortDirection = {}; // Track sort direction for each column 1061*1d05cddcSAtari911 1062*1d05cddcSAtari911 function sortRecurringTable(columnIndex) { 1063*1d05cddcSAtari911 console.log("sortRecurringTable called with column:", columnIndex); 1064*1d05cddcSAtari911 const table = document.getElementById("recurringTable"); 1065*1d05cddcSAtari911 const tbody = document.getElementById("recurringTableBody"); 1066*1d05cddcSAtari911 console.log("Table:", table, "Tbody:", tbody); 1067*1d05cddcSAtari911 1068*1d05cddcSAtari911 if (!table || !tbody) { 1069*1d05cddcSAtari911 console.error("Table or tbody not found!"); 1070*1d05cddcSAtari911 return; 1071*1d05cddcSAtari911 } 1072*1d05cddcSAtari911 1073*1d05cddcSAtari911 const rows = Array.from(tbody.querySelectorAll("tr")); 1074*1d05cddcSAtari911 console.log("Rows found:", rows.length); 1075*1d05cddcSAtari911 1076*1d05cddcSAtari911 if (rows.length === 0) { 1077*1d05cddcSAtari911 console.warn("No rows to sort"); 1078*1d05cddcSAtari911 return; 1079*1d05cddcSAtari911 } 1080*1d05cddcSAtari911 1081*1d05cddcSAtari911 // Toggle sort direction for this column 1082*1d05cddcSAtari911 if (!sortDirection[columnIndex]) { 1083*1d05cddcSAtari911 sortDirection[columnIndex] = "asc"; 1084*1d05cddcSAtari911 } else { 1085*1d05cddcSAtari911 sortDirection[columnIndex] = sortDirection[columnIndex] === "asc" ? "desc" : "asc"; 1086*1d05cddcSAtari911 } 1087*1d05cddcSAtari911 1088*1d05cddcSAtari911 const direction = sortDirection[columnIndex]; 1089*1d05cddcSAtari911 console.log("Sorting column", columnIndex, "in", direction, "order"); 1090*1d05cddcSAtari911 const isNumeric = columnIndex === 4; // Count column 1091*1d05cddcSAtari911 1092*1d05cddcSAtari911 // Sort rows 1093*1d05cddcSAtari911 rows.sort((a, b) => { 1094*1d05cddcSAtari911 let aValue = a.cells[columnIndex].textContent.trim(); 1095*1d05cddcSAtari911 let bValue = b.cells[columnIndex].textContent.trim(); 1096*1d05cddcSAtari911 1097*1d05cddcSAtari911 // Extract text from code elements for namespace column 1098*1d05cddcSAtari911 if (columnIndex === 1) { 1099*1d05cddcSAtari911 const aCode = a.cells[columnIndex].querySelector("code"); 1100*1d05cddcSAtari911 const bCode = b.cells[columnIndex].querySelector("code"); 1101*1d05cddcSAtari911 aValue = aCode ? aCode.textContent.trim() : aValue; 1102*1d05cddcSAtari911 bValue = bCode ? bCode.textContent.trim() : bValue; 1103*1d05cddcSAtari911 } 1104*1d05cddcSAtari911 1105*1d05cddcSAtari911 // Extract number from strong elements for count column 1106*1d05cddcSAtari911 if (isNumeric) { 1107*1d05cddcSAtari911 const aStrong = a.cells[columnIndex].querySelector("strong"); 1108*1d05cddcSAtari911 const bStrong = b.cells[columnIndex].querySelector("strong"); 1109*1d05cddcSAtari911 aValue = aStrong ? parseInt(aStrong.textContent.trim()) : 0; 1110*1d05cddcSAtari911 bValue = bStrong ? parseInt(bStrong.textContent.trim()) : 0; 1111*1d05cddcSAtari911 1112*1d05cddcSAtari911 return direction === "asc" ? aValue - bValue : bValue - aValue; 1113*1d05cddcSAtari911 } 1114*1d05cddcSAtari911 1115*1d05cddcSAtari911 // String comparison 1116*1d05cddcSAtari911 if (direction === "asc") { 1117*1d05cddcSAtari911 return aValue.localeCompare(bValue); 1118*1d05cddcSAtari911 } else { 1119*1d05cddcSAtari911 return bValue.localeCompare(aValue); 1120*1d05cddcSAtari911 } 1121*1d05cddcSAtari911 }); 1122*1d05cddcSAtari911 1123*1d05cddcSAtari911 // Update arrows 1124*1d05cddcSAtari911 const headers = table.querySelectorAll("th"); 1125*1d05cddcSAtari911 headers.forEach((header, index) => { 1126*1d05cddcSAtari911 const arrow = header.querySelector(".sort-arrow"); 1127*1d05cddcSAtari911 if (arrow) { 1128*1d05cddcSAtari911 if (index === columnIndex) { 1129*1d05cddcSAtari911 arrow.textContent = direction === "asc" ? "↑" : "↓"; 1130*1d05cddcSAtari911 arrow.style.color = "#00cc07"; 1131*1d05cddcSAtari911 } else { 1132*1d05cddcSAtari911 arrow.textContent = "⇅"; 1133*1d05cddcSAtari911 arrow.style.color = "#999"; 1134*1d05cddcSAtari911 } 1135*1d05cddcSAtari911 } 1136*1d05cddcSAtari911 }); 1137*1d05cddcSAtari911 1138*1d05cddcSAtari911 // Rebuild tbody 1139*1d05cddcSAtari911 rows.forEach(row => tbody.appendChild(row)); 1140*1d05cddcSAtari911 } 1141*1d05cddcSAtari911 1142*1d05cddcSAtari911 function filterRecurringEvents() { 1143*1d05cddcSAtari911 const searchInput = document.getElementById("searchRecurring"); 1144*1d05cddcSAtari911 const filter = normalizeText(searchInput.value); 1145*1d05cddcSAtari911 const tbody = document.getElementById("recurringTableBody"); 1146*1d05cddcSAtari911 const rows = tbody.getElementsByTagName("tr"); 1147*1d05cddcSAtari911 1148*1d05cddcSAtari911 for (let i = 0; i < rows.length; i++) { 1149*1d05cddcSAtari911 const row = rows[i]; 1150*1d05cddcSAtari911 const titleCell = row.getElementsByTagName("td")[0]; 1151*1d05cddcSAtari911 1152*1d05cddcSAtari911 if (titleCell) { 1153*1d05cddcSAtari911 const titleText = normalizeText(titleCell.textContent || titleCell.innerText); 1154*1d05cddcSAtari911 1155*1d05cddcSAtari911 if (titleText.indexOf(filter) > -1) { 1156*1d05cddcSAtari911 row.classList.remove("recurring-row-hidden"); 1157*1d05cddcSAtari911 } else { 1158*1d05cddcSAtari911 row.classList.add("recurring-row-hidden"); 1159*1d05cddcSAtari911 } 1160*1d05cddcSAtari911 } 1161*1d05cddcSAtari911 } 1162*1d05cddcSAtari911 } 1163*1d05cddcSAtari911 1164*1d05cddcSAtari911 function normalizeText(text) { 1165*1d05cddcSAtari911 // Convert to lowercase 1166*1d05cddcSAtari911 text = text.toLowerCase(); 1167*1d05cddcSAtari911 1168*1d05cddcSAtari911 // Remove apostrophes and quotes 1169*1d05cddcSAtari911 text = text.replace(/[\'\"]/g, ""); 1170*1d05cddcSAtari911 1171*1d05cddcSAtari911 // Replace accented characters with regular ones 1172*1d05cddcSAtari911 text = text.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); 1173*1d05cddcSAtari911 1174*1d05cddcSAtari911 // Remove special characters except spaces and alphanumeric 1175*1d05cddcSAtari911 text = text.replace(/[^a-z0-9\s]/g, ""); 1176*1d05cddcSAtari911 1177*1d05cddcSAtari911 // Collapse multiple spaces 1178*1d05cddcSAtari911 text = text.replace(/\s+/g, " "); 1179*1d05cddcSAtari911 1180*1d05cddcSAtari911 return text.trim(); 1181*1d05cddcSAtari911 } 1182*1d05cddcSAtari911 1183*1d05cddcSAtari911 function filterEvents() { 1184*1d05cddcSAtari911 const searchText = normalizeText(document.getElementById("searchEvents").value); 1185*1d05cddcSAtari911 const eventRows = document.querySelectorAll(".event-row"); 1186*1d05cddcSAtari911 let visibleCount = 0; 1187*1d05cddcSAtari911 1188*1d05cddcSAtari911 eventRows.forEach(row => { 1189*1d05cddcSAtari911 const titleElement = row.querySelector("div div"); 1190*1d05cddcSAtari911 const originalTitle = titleElement.getAttribute("data-original-title") || titleElement.textContent; 1191*1d05cddcSAtari911 1192*1d05cddcSAtari911 // Store original title if not already stored 1193*1d05cddcSAtari911 if (!titleElement.getAttribute("data-original-title")) { 1194*1d05cddcSAtari911 titleElement.setAttribute("data-original-title", originalTitle); 1195*1d05cddcSAtari911 } 1196*1d05cddcSAtari911 1197*1d05cddcSAtari911 const normalizedTitle = normalizeText(originalTitle); 1198*1d05cddcSAtari911 1199*1d05cddcSAtari911 if (normalizedTitle.includes(searchText) || searchText === "") { 1200*1d05cddcSAtari911 row.style.display = "flex"; 1201*1d05cddcSAtari911 visibleCount++; 1202*1d05cddcSAtari911 } else { 1203*1d05cddcSAtari911 row.style.display = "none"; 1204*1d05cddcSAtari911 } 1205*1d05cddcSAtari911 }); 1206*1d05cddcSAtari911 1207*1d05cddcSAtari911 // Update namespace visibility and counts 1208*1d05cddcSAtari911 document.querySelectorAll("[id^=ns_]").forEach(nsDiv => { 1209*1d05cddcSAtari911 if (nsDiv.id.endsWith("_arrow") || nsDiv.id.endsWith("_check")) return; 1210*1d05cddcSAtari911 1211*1d05cddcSAtari911 const visibleEvents = nsDiv.querySelectorAll(".event-row[style*=\\"display: flex\\"], .event-row:not([style*=\\"display: none\\"])").length; 1212*1d05cddcSAtari911 const nsId = nsDiv.id; 1213*1d05cddcSAtari911 const arrow = document.getElementById(nsId + "_arrow"); 1214*1d05cddcSAtari911 1215*1d05cddcSAtari911 // Auto-expand namespaces with matches when searching 1216*1d05cddcSAtari911 if (searchText && visibleEvents > 0) { 1217*1d05cddcSAtari911 nsDiv.style.display = "block"; 1218*1d05cddcSAtari911 if (arrow) arrow.textContent = "▼"; 1219*1d05cddcSAtari911 } 1220*1d05cddcSAtari911 }); 1221*1d05cddcSAtari911 } 1222*1d05cddcSAtari911 1223*1d05cddcSAtari911 function toggleNamespace(id) { 1224*1d05cddcSAtari911 const elem = document.getElementById(id); 1225*1d05cddcSAtari911 const arrow = document.getElementById(id + "_arrow"); 1226*1d05cddcSAtari911 if (elem.style.display === "none") { 1227*1d05cddcSAtari911 elem.style.display = "block"; 1228*1d05cddcSAtari911 arrow.textContent = "▼"; 1229*1d05cddcSAtari911 } else { 1230*1d05cddcSAtari911 elem.style.display = "none"; 1231*1d05cddcSAtari911 arrow.textContent = "▶"; 1232*1d05cddcSAtari911 } 1233*1d05cddcSAtari911 } 1234*1d05cddcSAtari911 1235*1d05cddcSAtari911 function toggleNamespaceSelect(nsId) { 1236*1d05cddcSAtari911 const checkbox = document.getElementById(nsId + "_check"); 1237*1d05cddcSAtari911 const events = document.querySelectorAll("." + nsId + "_events"); 1238*1d05cddcSAtari911 1239*1d05cddcSAtari911 // Only select visible events (not hidden by search) 1240*1d05cddcSAtari911 events.forEach(cb => { 1241*1d05cddcSAtari911 const eventRow = cb.closest(".event-row"); 1242*1d05cddcSAtari911 if (eventRow && eventRow.style.display !== "none") { 1243*1d05cddcSAtari911 cb.checked = checkbox.checked; 1244*1d05cddcSAtari911 } 1245*1d05cddcSAtari911 }); 1246*1d05cddcSAtari911 updateCount(); 1247*1d05cddcSAtari911 } 1248*1d05cddcSAtari911 1249*1d05cddcSAtari911 function selectAll() { 1250*1d05cddcSAtari911 // Only select visible events 1251*1d05cddcSAtari911 document.querySelectorAll(".event-checkbox").forEach(cb => { 1252*1d05cddcSAtari911 const eventRow = cb.closest(".event-row"); 1253*1d05cddcSAtari911 if (eventRow && eventRow.style.display !== "none") { 1254*1d05cddcSAtari911 cb.checked = true; 1255*1d05cddcSAtari911 } 1256*1d05cddcSAtari911 }); 1257*1d05cddcSAtari911 // Update namespace checkboxes to indeterminate if partially selected 1258*1d05cddcSAtari911 document.querySelectorAll("input[id$=_check]").forEach(nsCheckbox => { 1259*1d05cddcSAtari911 const nsId = nsCheckbox.id.replace("_check", ""); 1260*1d05cddcSAtari911 const events = document.querySelectorAll("." + nsId + "_events"); 1261*1d05cddcSAtari911 const visibleEvents = Array.from(events).filter(cb => { 1262*1d05cddcSAtari911 const row = cb.closest(".event-row"); 1263*1d05cddcSAtari911 return row && row.style.display !== "none"; 1264*1d05cddcSAtari911 }); 1265*1d05cddcSAtari911 const checkedVisible = visibleEvents.filter(cb => cb.checked); 1266*1d05cddcSAtari911 1267*1d05cddcSAtari911 if (checkedVisible.length === visibleEvents.length && visibleEvents.length > 0) { 1268*1d05cddcSAtari911 nsCheckbox.checked = true; 1269*1d05cddcSAtari911 } else if (checkedVisible.length > 0) { 1270*1d05cddcSAtari911 nsCheckbox.indeterminate = true; 1271*1d05cddcSAtari911 } else { 1272*1d05cddcSAtari911 nsCheckbox.checked = false; 1273*1d05cddcSAtari911 } 1274*1d05cddcSAtari911 }); 1275*1d05cddcSAtari911 updateCount(); 1276*1d05cddcSAtari911 } 1277*1d05cddcSAtari911 1278*1d05cddcSAtari911 function deselectAll() { 1279*1d05cddcSAtari911 document.querySelectorAll(".event-checkbox").forEach(cb => cb.checked = false); 1280*1d05cddcSAtari911 document.querySelectorAll("input[id$=_check]").forEach(cb => { 1281*1d05cddcSAtari911 cb.checked = false; 1282*1d05cddcSAtari911 cb.indeterminate = false; 1283*1d05cddcSAtari911 }); 1284*1d05cddcSAtari911 updateCount(); 1285*1d05cddcSAtari911 } 1286*1d05cddcSAtari911 1287*1d05cddcSAtari911 function deleteSelected() { 1288*1d05cddcSAtari911 const checkedBoxes = document.querySelectorAll(".event-checkbox:checked"); 1289*1d05cddcSAtari911 if (checkedBoxes.length === 0) { 1290*1d05cddcSAtari911 alert("No events selected"); 1291*1d05cddcSAtari911 return; 1292*1d05cddcSAtari911 } 1293*1d05cddcSAtari911 1294*1d05cddcSAtari911 const count = checkedBoxes.length; 1295*1d05cddcSAtari911 if (!confirm(`Delete ${count} selected event(s)?\\n\\nThis cannot be undone!`)) { 1296*1d05cddcSAtari911 return; 1297*1d05cddcSAtari911 } 1298*1d05cddcSAtari911 1299*1d05cddcSAtari911 const form = document.createElement("form"); 1300*1d05cddcSAtari911 form.method = "POST"; 1301*1d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=manage"; 1302*1d05cddcSAtari911 1303*1d05cddcSAtari911 const actionInput = document.createElement("input"); 1304*1d05cddcSAtari911 actionInput.type = "hidden"; 1305*1d05cddcSAtari911 actionInput.name = "action"; 1306*1d05cddcSAtari911 actionInput.value = "delete_selected_events"; 1307*1d05cddcSAtari911 form.appendChild(actionInput); 1308*1d05cddcSAtari911 1309*1d05cddcSAtari911 checkedBoxes.forEach(cb => { 1310*1d05cddcSAtari911 const eventInput = document.createElement("input"); 1311*1d05cddcSAtari911 eventInput.type = "hidden"; 1312*1d05cddcSAtari911 eventInput.name = "events[]"; 1313*1d05cddcSAtari911 eventInput.value = cb.value; 1314*1d05cddcSAtari911 form.appendChild(eventInput); 1315*1d05cddcSAtari911 }); 1316*1d05cddcSAtari911 1317*1d05cddcSAtari911 document.body.appendChild(form); 1318*1d05cddcSAtari911 form.submit(); 1319*1d05cddcSAtari911 } 1320*1d05cddcSAtari911 1321*1d05cddcSAtari911 function createNewNamespace() { 1322*1d05cddcSAtari911 const namespaceName = prompt("Enter new namespace name:\\n\\nExamples:\\n- work\\n- personal\\n- projects:alpha\\n- aspen:travel:2025"); 1323*1d05cddcSAtari911 1324*1d05cddcSAtari911 if (!namespaceName) { 1325*1d05cddcSAtari911 return; // Cancelled 1326*1d05cddcSAtari911 } 1327*1d05cddcSAtari911 1328*1d05cddcSAtari911 // Validate namespace name 1329*1d05cddcSAtari911 if (!/^[a-zA-Z0-9_:-]+$/.test(namespaceName)) { 1330*1d05cddcSAtari911 alert("Invalid namespace name.\\n\\nUse only letters, numbers, underscore, hyphen, and colon.\\nExample: work:projects:alpha"); 1331*1d05cddcSAtari911 return; 1332*1d05cddcSAtari911 } 1333*1d05cddcSAtari911 1334*1d05cddcSAtari911 // Submit form to create namespace 1335*1d05cddcSAtari911 const form = document.createElement("form"); 1336*1d05cddcSAtari911 form.method = "POST"; 1337*1d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=manage"; 1338*1d05cddcSAtari911 1339*1d05cddcSAtari911 const actionInput = document.createElement("input"); 1340*1d05cddcSAtari911 actionInput.type = "hidden"; 1341*1d05cddcSAtari911 actionInput.name = "action"; 1342*1d05cddcSAtari911 actionInput.value = "create_namespace"; 1343*1d05cddcSAtari911 form.appendChild(actionInput); 1344*1d05cddcSAtari911 1345*1d05cddcSAtari911 const namespaceInput = document.createElement("input"); 1346*1d05cddcSAtari911 namespaceInput.type = "hidden"; 1347*1d05cddcSAtari911 namespaceInput.name = "namespace_name"; 1348*1d05cddcSAtari911 namespaceInput.value = namespaceName; 1349*1d05cddcSAtari911 form.appendChild(namespaceInput); 1350*1d05cddcSAtari911 1351*1d05cddcSAtari911 document.body.appendChild(form); 1352*1d05cddcSAtari911 form.submit(); 1353*1d05cddcSAtari911 } 1354*1d05cddcSAtari911 1355*1d05cddcSAtari911 function updateCount() { 1356*1d05cddcSAtari911 const count = document.querySelectorAll(".event-checkbox:checked").length; 1357*1d05cddcSAtari911 document.getElementById("selectedCount").textContent = count + " selected"; 1358*1d05cddcSAtari911 } 1359*1d05cddcSAtari911 1360*1d05cddcSAtari911 function deleteNamespace(namespace) { 1361*1d05cddcSAtari911 const displayName = namespace || "(default)"; 1362*1d05cddcSAtari911 if (!confirm("Delete ENTIRE namespace: " + displayName + "?\\n\\nThis will delete ALL events in this namespace!\\n\\nThis cannot be undone!")) { 1363*1d05cddcSAtari911 return; 1364*1d05cddcSAtari911 } 1365*1d05cddcSAtari911 const form = document.createElement("form"); 1366*1d05cddcSAtari911 form.method = "POST"; 1367*1d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=manage"; 1368*1d05cddcSAtari911 const actionInput = document.createElement("input"); 1369*1d05cddcSAtari911 actionInput.type = "hidden"; 1370*1d05cddcSAtari911 actionInput.name = "action"; 1371*1d05cddcSAtari911 actionInput.value = "delete_namespace"; 1372*1d05cddcSAtari911 form.appendChild(actionInput); 1373*1d05cddcSAtari911 const nsInput = document.createElement("input"); 1374*1d05cddcSAtari911 nsInput.type = "hidden"; 1375*1d05cddcSAtari911 nsInput.name = "namespace"; 1376*1d05cddcSAtari911 nsInput.value = namespace; 1377*1d05cddcSAtari911 form.appendChild(nsInput); 1378*1d05cddcSAtari911 document.body.appendChild(form); 1379*1d05cddcSAtari911 form.submit(); 1380*1d05cddcSAtari911 } 1381*1d05cddcSAtari911 1382*1d05cddcSAtari911 let draggedEvent = null; 1383*1d05cddcSAtari911 1384*1d05cddcSAtari911 function dragStart(event, eventId) { 1385*1d05cddcSAtari911 const checkbox = event.target.closest(".event-row").querySelector(".event-checkbox"); 1386*1d05cddcSAtari911 1387*1d05cddcSAtari911 // If this event is checked, drag all checked events 1388*1d05cddcSAtari911 const checkedBoxes = document.querySelectorAll(".event-checkbox:checked"); 1389*1d05cddcSAtari911 if (checkbox && checkbox.checked && checkedBoxes.length > 1) { 1390*1d05cddcSAtari911 // Dragging multiple selected events 1391*1d05cddcSAtari911 draggedEvent = "MULTIPLE"; 1392*1d05cddcSAtari911 event.dataTransfer.setData("text/plain", "MULTIPLE"); 1393*1d05cddcSAtari911 } else { 1394*1d05cddcSAtari911 // Dragging single event 1395*1d05cddcSAtari911 draggedEvent = eventId; 1396*1d05cddcSAtari911 event.dataTransfer.setData("text/plain", eventId); 1397*1d05cddcSAtari911 } 1398*1d05cddcSAtari911 event.dataTransfer.effectAllowed = "move"; 1399*1d05cddcSAtari911 event.target.style.opacity = "0.5"; 1400*1d05cddcSAtari911 } 1401*1d05cddcSAtari911 1402*1d05cddcSAtari911 function allowDrop(event) { 1403*1d05cddcSAtari911 event.preventDefault(); 1404*1d05cddcSAtari911 event.dataTransfer.dropEffect = "move"; 1405*1d05cddcSAtari911 } 1406*1d05cddcSAtari911 1407*1d05cddcSAtari911 function drop(event, targetNamespace) { 1408*1d05cddcSAtari911 event.preventDefault(); 1409*1d05cddcSAtari911 1410*1d05cddcSAtari911 if (draggedEvent === "MULTIPLE") { 1411*1d05cddcSAtari911 // Move all selected events 1412*1d05cddcSAtari911 const checkedBoxes = document.querySelectorAll(".event-checkbox:checked"); 1413*1d05cddcSAtari911 if (checkedBoxes.length === 0) return; 1414*1d05cddcSAtari911 1415*1d05cddcSAtari911 const form = document.createElement("form"); 1416*1d05cddcSAtari911 form.method = "POST"; 1417*1d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=manage"; 1418*1d05cddcSAtari911 1419*1d05cddcSAtari911 const actionInput = document.createElement("input"); 1420*1d05cddcSAtari911 actionInput.type = "hidden"; 1421*1d05cddcSAtari911 actionInput.name = "action"; 1422*1d05cddcSAtari911 actionInput.value = "move_selected_events"; 1423*1d05cddcSAtari911 form.appendChild(actionInput); 1424*1d05cddcSAtari911 1425*1d05cddcSAtari911 checkedBoxes.forEach(cb => { 1426*1d05cddcSAtari911 const eventInput = document.createElement("input"); 1427*1d05cddcSAtari911 eventInput.type = "hidden"; 1428*1d05cddcSAtari911 eventInput.name = "events[]"; 1429*1d05cddcSAtari911 eventInput.value = cb.value; 1430*1d05cddcSAtari911 form.appendChild(eventInput); 1431*1d05cddcSAtari911 }); 1432*1d05cddcSAtari911 1433*1d05cddcSAtari911 const targetInput = document.createElement("input"); 1434*1d05cddcSAtari911 targetInput.type = "hidden"; 1435*1d05cddcSAtari911 targetInput.name = "target_namespace"; 1436*1d05cddcSAtari911 targetInput.value = targetNamespace; 1437*1d05cddcSAtari911 form.appendChild(targetInput); 1438*1d05cddcSAtari911 1439*1d05cddcSAtari911 document.body.appendChild(form); 1440*1d05cddcSAtari911 form.submit(); 1441*1d05cddcSAtari911 } else { 1442*1d05cddcSAtari911 // Move single event 1443*1d05cddcSAtari911 if (!draggedEvent) return; 1444*1d05cddcSAtari911 const parts = draggedEvent.split("|"); 1445*1d05cddcSAtari911 const sourceNamespace = parts[1]; 1446*1d05cddcSAtari911 if (sourceNamespace === targetNamespace) return; 1447*1d05cddcSAtari911 1448*1d05cddcSAtari911 const form = document.createElement("form"); 1449*1d05cddcSAtari911 form.method = "POST"; 1450*1d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=manage"; 1451*1d05cddcSAtari911 const actionInput = document.createElement("input"); 1452*1d05cddcSAtari911 actionInput.type = "hidden"; 1453*1d05cddcSAtari911 actionInput.name = "action"; 1454*1d05cddcSAtari911 actionInput.value = "move_single_event"; 1455*1d05cddcSAtari911 form.appendChild(actionInput); 1456*1d05cddcSAtari911 const eventInput = document.createElement("input"); 1457*1d05cddcSAtari911 eventInput.type = "hidden"; 1458*1d05cddcSAtari911 eventInput.name = "event"; 1459*1d05cddcSAtari911 eventInput.value = draggedEvent; 1460*1d05cddcSAtari911 form.appendChild(eventInput); 1461*1d05cddcSAtari911 const targetInput = document.createElement("input"); 1462*1d05cddcSAtari911 targetInput.type = "hidden"; 1463*1d05cddcSAtari911 targetInput.name = "target_namespace"; 1464*1d05cddcSAtari911 targetInput.value = targetNamespace; 1465*1d05cddcSAtari911 form.appendChild(targetInput); 1466*1d05cddcSAtari911 document.body.appendChild(form); 1467*1d05cddcSAtari911 form.submit(); 1468*1d05cddcSAtari911 } 1469*1d05cddcSAtari911 } 1470*1d05cddcSAtari911 1471*1d05cddcSAtari911 function editRecurringSeries(title, namespace) { 1472*1d05cddcSAtari911 // Get available namespaces 1473*1d05cddcSAtari911 const namespaces = Array.from(document.querySelectorAll("[id^=ns_]")) 1474*1d05cddcSAtari911 .map(el => { 1475*1d05cddcSAtari911 const match = el.id.match(/^ns_[a-f0-9]+$/); 1476*1d05cddcSAtari911 if (!match) return null; 1477*1d05cddcSAtari911 const nsSpan = el.querySelector("span:nth-child(3)"); 1478*1d05cddcSAtari911 if (!nsSpan) return null; 1479*1d05cddcSAtari911 return nsSpan.textContent.replace(" ", "").replace("(default)", "").trim(); 1480*1d05cddcSAtari911 }) 1481*1d05cddcSAtari911 .filter((ns, idx, arr) => ns && arr.indexOf(ns) === idx); 1482*1d05cddcSAtari911 1483*1d05cddcSAtari911 let nsOptions = `<option value="">Keep current (${namespace || "(default)"})</option>`; 1484*1d05cddcSAtari911 nsOptions += `<option value="">(default)</option>`; 1485*1d05cddcSAtari911 for (const ns of namespaces) { 1486*1d05cddcSAtari911 if (ns && ns !== "(default)" && ns !== namespace) { 1487*1d05cddcSAtari911 nsOptions += `<option value="${ns}">${ns}</option>`; 1488*1d05cddcSAtari911 } 1489*1d05cddcSAtari911 } 1490*1d05cddcSAtari911 1491*1d05cddcSAtari911 // Show edit dialog for recurring events 1492*1d05cddcSAtari911 const dialog = document.createElement("div"); 1493*1d05cddcSAtari911 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;"; 1494*1d05cddcSAtari911 1495*1d05cddcSAtari911 // Close on clicking background 1496*1d05cddcSAtari911 dialog.addEventListener("click", function(e) { 1497*1d05cddcSAtari911 if (e.target === dialog) { 1498*1d05cddcSAtari911 dialog.remove(); 1499*1d05cddcSAtari911 } 1500*1d05cddcSAtari911 }); 1501*1d05cddcSAtari911 1502*1d05cddcSAtari911 dialog.innerHTML = ` 1503*1d05cddcSAtari911 <div style="background:white; padding:20px; border-radius:8px; min-width:500px; max-width:700px; max-height:90vh; overflow-y:auto;"> 1504*1d05cddcSAtari911 <h3 style="margin:0 0 15px; color:#00cc07;">Edit Recurring Event</h3> 1505*1d05cddcSAtari911 <p style="margin:0 0 15px; color:#666; font-size:13px;">Changes will apply to ALL occurrences of: <strong>${title}</strong></p> 1506*1d05cddcSAtari911 1507*1d05cddcSAtari911 <form id="editRecurringForm" style="display:flex; flex-direction:column; gap:12px;"> 1508*1d05cddcSAtari911 <div> 1509*1d05cddcSAtari911 <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">New Title:</label> 1510*1d05cddcSAtari911 <input type="text" name="new_title" value="${title}" style="width:100%; padding:8px; border:1px solid #ddd; border-radius:3px;" required> 1511*1d05cddcSAtari911 </div> 1512*1d05cddcSAtari911 1513*1d05cddcSAtari911 <div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;"> 1514*1d05cddcSAtari911 <div> 1515*1d05cddcSAtari911 <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">Start Time:</label> 1516*1d05cddcSAtari911 <input type="time" name="start_time" style="width:100%; padding:8px; border:1px solid #ddd; border-radius:3px;"> 1517*1d05cddcSAtari911 <small style="color:#999; font-size:11px;">Leave blank to keep current</small> 1518*1d05cddcSAtari911 </div> 1519*1d05cddcSAtari911 <div> 1520*1d05cddcSAtari911 <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">End Time:</label> 1521*1d05cddcSAtari911 <input type="time" name="end_time" style="width:100%; padding:8px; border:1px solid #ddd; border-radius:3px;"> 1522*1d05cddcSAtari911 <small style="color:#999; font-size:11px;">Leave blank to keep current</small> 1523*1d05cddcSAtari911 </div> 1524*1d05cddcSAtari911 </div> 1525*1d05cddcSAtari911 1526*1d05cddcSAtari911 <div> 1527*1d05cddcSAtari911 <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">Interval (days between occurrences):</label> 1528*1d05cddcSAtari911 <select name="interval" style="width:100%; padding:8px; border:1px solid #ddd; border-radius:3px;"> 1529*1d05cddcSAtari911 <option value="">Keep current interval</option> 1530*1d05cddcSAtari911 <option value="1">Daily (1 day)</option> 1531*1d05cddcSAtari911 <option value="7">Weekly (7 days)</option> 1532*1d05cddcSAtari911 <option value="14">Bi-weekly (14 days)</option> 1533*1d05cddcSAtari911 <option value="30">Monthly (30 days)</option> 1534*1d05cddcSAtari911 <option value="365">Yearly (365 days)</option> 1535*1d05cddcSAtari911 </select> 1536*1d05cddcSAtari911 </div> 1537*1d05cddcSAtari911 1538*1d05cddcSAtari911 <div> 1539*1d05cddcSAtari911 <label style="display:block; font-weight:bold; margin-bottom:4px; font-size:13px;">Move to Namespace:</label> 1540*1d05cddcSAtari911 <select name="new_namespace" style="width:100%; padding:8px; border:1px solid #ddd; border-radius:3px;"> 1541*1d05cddcSAtari911 ${nsOptions} 1542*1d05cddcSAtari911 </select> 1543*1d05cddcSAtari911 </div> 1544*1d05cddcSAtari911 1545*1d05cddcSAtari911 <div style="display:flex; gap:10px; margin-top:10px;"> 1546*1d05cddcSAtari911 <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> 1547*1d05cddcSAtari911 <button type="button" onclick="closeEditDialog()" style="flex:1; background:#999; color:white; padding:10px; border:none; border-radius:3px; cursor:pointer;">Cancel</button> 1548*1d05cddcSAtari911 </div> 1549*1d05cddcSAtari911 </form> 1550*1d05cddcSAtari911 </div> 1551*1d05cddcSAtari911 `; 1552*1d05cddcSAtari911 1553*1d05cddcSAtari911 document.body.appendChild(dialog); 1554*1d05cddcSAtari911 1555*1d05cddcSAtari911 // Add close function to window 1556*1d05cddcSAtari911 window.closeEditDialog = function() { 1557*1d05cddcSAtari911 dialog.remove(); 1558*1d05cddcSAtari911 }; 1559*1d05cddcSAtari911 1560*1d05cddcSAtari911 // Handle form submission 1561*1d05cddcSAtari911 dialog.querySelector("#editRecurringForm").addEventListener("submit", function(e) { 1562*1d05cddcSAtari911 e.preventDefault(); 1563*1d05cddcSAtari911 const formData = new FormData(this); 1564*1d05cddcSAtari911 1565*1d05cddcSAtari911 // Submit the edit 1566*1d05cddcSAtari911 const form = document.createElement("form"); 1567*1d05cddcSAtari911 form.method = "POST"; 1568*1d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=manage"; 1569*1d05cddcSAtari911 1570*1d05cddcSAtari911 const actionInput = document.createElement("input"); 1571*1d05cddcSAtari911 actionInput.type = "hidden"; 1572*1d05cddcSAtari911 actionInput.name = "action"; 1573*1d05cddcSAtari911 actionInput.value = "edit_recurring_series"; 1574*1d05cddcSAtari911 form.appendChild(actionInput); 1575*1d05cddcSAtari911 1576*1d05cddcSAtari911 const oldTitleInput = document.createElement("input"); 1577*1d05cddcSAtari911 oldTitleInput.type = "hidden"; 1578*1d05cddcSAtari911 oldTitleInput.name = "old_title"; 1579*1d05cddcSAtari911 oldTitleInput.value = title; 1580*1d05cddcSAtari911 form.appendChild(oldTitleInput); 1581*1d05cddcSAtari911 1582*1d05cddcSAtari911 const oldNamespaceInput = document.createElement("input"); 1583*1d05cddcSAtari911 oldNamespaceInput.type = "hidden"; 1584*1d05cddcSAtari911 oldNamespaceInput.name = "old_namespace"; 1585*1d05cddcSAtari911 oldNamespaceInput.value = namespace; 1586*1d05cddcSAtari911 form.appendChild(oldNamespaceInput); 1587*1d05cddcSAtari911 1588*1d05cddcSAtari911 // Add all form fields 1589*1d05cddcSAtari911 for (let [key, value] of formData.entries()) { 1590*1d05cddcSAtari911 const input = document.createElement("input"); 1591*1d05cddcSAtari911 input.type = "hidden"; 1592*1d05cddcSAtari911 input.name = key; 1593*1d05cddcSAtari911 input.value = value; 1594*1d05cddcSAtari911 form.appendChild(input); 1595*1d05cddcSAtari911 } 1596*1d05cddcSAtari911 1597*1d05cddcSAtari911 document.body.appendChild(form); 1598*1d05cddcSAtari911 form.submit(); 1599*1d05cddcSAtari911 }); 1600*1d05cddcSAtari911 } 1601*1d05cddcSAtari911 1602*1d05cddcSAtari911 function deleteRecurringSeries(title, namespace) { 1603*1d05cddcSAtari911 const displayNs = namespace || "(default)"; 1604*1d05cddcSAtari911 if (!confirm("Delete ALL occurrences of: " + title + " (" + displayNs + ")?\\n\\nThis cannot be undone!")) { 1605*1d05cddcSAtari911 return; 1606*1d05cddcSAtari911 } 1607*1d05cddcSAtari911 const form = document.createElement("form"); 1608*1d05cddcSAtari911 form.method = "POST"; 1609*1d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=manage"; 1610*1d05cddcSAtari911 const actionInput = document.createElement("input"); 1611*1d05cddcSAtari911 actionInput.type = "hidden"; 1612*1d05cddcSAtari911 actionInput.name = "action"; 1613*1d05cddcSAtari911 actionInput.value = "delete_recurring_series"; 1614*1d05cddcSAtari911 form.appendChild(actionInput); 1615*1d05cddcSAtari911 const titleInput = document.createElement("input"); 1616*1d05cddcSAtari911 titleInput.type = "hidden"; 1617*1d05cddcSAtari911 titleInput.name = "event_title"; 1618*1d05cddcSAtari911 titleInput.value = title; 1619*1d05cddcSAtari911 form.appendChild(titleInput); 1620*1d05cddcSAtari911 const namespaceInput = document.createElement("input"); 1621*1d05cddcSAtari911 namespaceInput.type = "hidden"; 1622*1d05cddcSAtari911 namespaceInput.name = "namespace"; 1623*1d05cddcSAtari911 namespaceInput.value = namespace; 1624*1d05cddcSAtari911 form.appendChild(namespaceInput); 1625*1d05cddcSAtari911 document.body.appendChild(form); 1626*1d05cddcSAtari911 form.submit(); 1627*1d05cddcSAtari911 } 1628*1d05cddcSAtari911 1629*1d05cddcSAtari911 document.addEventListener("dragend", function(e) { 1630*1d05cddcSAtari911 if (e.target.draggable) { 1631*1d05cddcSAtari911 e.target.style.opacity = "1"; 1632*1d05cddcSAtari911 } 1633*1d05cddcSAtari911 }); 1634*1d05cddcSAtari911 </script>'; 1635*1d05cddcSAtari911 } 1636*1d05cddcSAtari911 1637*1d05cddcSAtari911 private function renderUpdateTab() { 1638*1d05cddcSAtari911 global $INPUT; 1639*1d05cddcSAtari911 1640*1d05cddcSAtari911 echo '<h2 style="margin:10px 0; font-size:20px;"> Update Plugin</h2>'; 1641*1d05cddcSAtari911 1642*1d05cddcSAtari911 // Clear Cache button 1643*1d05cddcSAtari911 echo '<div style="margin-bottom:15px;">'; 1644*1d05cddcSAtari911 echo '<form method="post" action="?do=admin&page=calendar&tab=update" style="display:inline; margin:0;">'; 1645*1d05cddcSAtari911 echo '<input type="hidden" name="action" value="clear_cache">'; 1646*1d05cddcSAtari911 echo '<input type="hidden" name="tab" value="update">'; 1647*1d05cddcSAtari911 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; box-shadow:0 2px 4px rgba(0,0,0,0.1);">️ Clear Cache</button>'; 1648*1d05cddcSAtari911 echo '</form>'; 1649*1d05cddcSAtari911 echo '<p style="margin:8px 0 0 0; font-size:12px; color:#666;">Clear the DokuWiki cache if changes aren\'t appearing or after updating the plugin.</p>'; 1650*1d05cddcSAtari911 echo '</div>'; 1651*1d05cddcSAtari911 1652*1d05cddcSAtari911 // Show message if present 1653*1d05cddcSAtari911 if ($INPUT->has('msg')) { 1654*1d05cddcSAtari911 $msg = hsc($INPUT->str('msg')); 1655*1d05cddcSAtari911 $type = $INPUT->str('msgtype', 'success'); 1656*1d05cddcSAtari911 $class = ($type === 'success') ? 'msg success' : 'msg error'; 1657*1d05cddcSAtari911 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:900px;\">"; 1658*1d05cddcSAtari911 echo $msg; 1659*1d05cddcSAtari911 echo "</div>"; 1660*1d05cddcSAtari911 } 1661*1d05cddcSAtari911 1662*1d05cddcSAtari911 // Show current version 1663*1d05cddcSAtari911 $pluginInfo = DOKU_PLUGIN . 'calendar/plugin.info.txt'; 1664*1d05cddcSAtari911 $info = ['version' => 'Unknown', 'date' => 'Unknown', 'name' => 'Calendar Plugin', 'author' => 'Unknown', 'email' => '', 'desc' => '']; 1665*1d05cddcSAtari911 if (file_exists($pluginInfo)) { 1666*1d05cddcSAtari911 $info = array_merge($info, confToHash($pluginInfo)); 1667*1d05cddcSAtari911 } 1668*1d05cddcSAtari911 1669*1d05cddcSAtari911 echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">'; 1670*1d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">' . hsc($info['name']) . '</h3>'; 1671*1d05cddcSAtari911 echo '<div style="font-size:12px; line-height:1.6;">'; 1672*1d05cddcSAtari911 echo '<div style="margin:4px 0;"><strong>Version:</strong> ' . hsc($info['version']) . ' (' . hsc($info['date']) . ')</div>'; 1673*1d05cddcSAtari911 echo '<div style="margin:4px 0;"><strong>Author:</strong> ' . hsc($info['author']) . ($info['email'] ? ' <' . hsc($info['email']) . '>' : '') . '</div>'; 1674*1d05cddcSAtari911 if ($info['desc']) { 1675*1d05cddcSAtari911 echo '<div style="margin:4px 0;"><strong>Description:</strong> ' . hsc($info['desc']) . '</div>'; 1676*1d05cddcSAtari911 } 1677*1d05cddcSAtari911 echo '<div style="margin:4px 0;"><strong>Location:</strong> <code style="background:#f0f0f0; padding:2px 4px; border-radius:2px;">' . DOKU_PLUGIN . 'calendar/</code></div>'; 1678*1d05cddcSAtari911 echo '</div>'; 1679*1d05cddcSAtari911 1680*1d05cddcSAtari911 // Check permissions 1681*1d05cddcSAtari911 $pluginDir = DOKU_PLUGIN . 'calendar/'; 1682*1d05cddcSAtari911 $pluginWritable = is_writable($pluginDir); 1683*1d05cddcSAtari911 $parentWritable = is_writable(DOKU_PLUGIN); 1684*1d05cddcSAtari911 1685*1d05cddcSAtari911 echo '<div style="margin-top:8px; padding-top:8px; border-top:1px solid #ddd;">'; 1686*1d05cddcSAtari911 if ($pluginWritable && $parentWritable) { 1687*1d05cddcSAtari911 echo '<p style="margin:5px 0; font-size:13px; color:#28a745;"><strong>✅ Permissions:</strong> OK - ready to update</p>'; 1688*1d05cddcSAtari911 } else { 1689*1d05cddcSAtari911 echo '<p style="margin:5px 0; font-size:13px; color:#dc3545;"><strong>❌ Permissions:</strong> Issues detected</p>'; 1690*1d05cddcSAtari911 if (!$pluginWritable) { 1691*1d05cddcSAtari911 echo '<p style="margin:2px 0 2px 20px; font-size:12px; color:#dc3545;">Plugin directory not writable</p>'; 1692*1d05cddcSAtari911 } 1693*1d05cddcSAtari911 if (!$parentWritable) { 1694*1d05cddcSAtari911 echo '<p style="margin:2px 0 2px 20px; font-size:12px; color:#dc3545;">Parent directory not writable</p>'; 1695*1d05cddcSAtari911 } 1696*1d05cddcSAtari911 echo '<p style="margin:5px 0; font-size:12px; color:#666;">Fix with: <code style="background:#f0f0f0; padding:2px 4px; border-radius:2px;">chmod -R 755 ' . DOKU_PLUGIN . 'calendar/</code></p>'; 1697*1d05cddcSAtari911 echo '<p style="margin:2px 0; font-size:12px; color:#666;">Or: <code style="background:#f0f0f0; padding:2px 4px; border-radius:2px;">chown -R www-data:www-data ' . DOKU_PLUGIN . 'calendar/</code></p>'; 1698*1d05cddcSAtari911 } 1699*1d05cddcSAtari911 echo '</div>'; 1700*1d05cddcSAtari911 1701*1d05cddcSAtari911 echo '</div>'; 1702*1d05cddcSAtari911 1703*1d05cddcSAtari911 // Changelog section 1704*1d05cddcSAtari911 echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #7b1fa2; border-radius:3px; max-width:900px;">'; 1705*1d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#7b1fa2; font-size:16px;"> Recent Changes</h3>'; 1706*1d05cddcSAtari911 1707*1d05cddcSAtari911 $changelogFile = DOKU_PLUGIN . 'calendar/CHANGELOG.md'; 1708*1d05cddcSAtari911 if (file_exists($changelogFile)) { 1709*1d05cddcSAtari911 $changelog = file_get_contents($changelogFile); 1710*1d05cddcSAtari911 1711*1d05cddcSAtari911 // Parse markdown and show last 10 versions 1712*1d05cddcSAtari911 $lines = explode("\n", $changelog); 1713*1d05cddcSAtari911 $versionsShown = 0; 1714*1d05cddcSAtari911 $maxVersions = 10; 1715*1d05cddcSAtari911 $inVersion = false; 1716*1d05cddcSAtari911 $changelogHtml = '<div style="font-size:12px; line-height:1.7; max-height:100px; overflow-y:auto; padding-right:10px;">'; 1717*1d05cddcSAtari911 1718*1d05cddcSAtari911 foreach ($lines as $line) { 1719*1d05cddcSAtari911 $line = trim($line); 1720*1d05cddcSAtari911 1721*1d05cddcSAtari911 // Version header (## Version X.X.X) 1722*1d05cddcSAtari911 if (preg_match('/^## Version (.+)$/', $line, $matches)) { 1723*1d05cddcSAtari911 if ($versionsShown >= $maxVersions) break; 1724*1d05cddcSAtari911 $versionsShown++; 1725*1d05cddcSAtari911 $inVersion = true; 1726*1d05cddcSAtari911 $changelogHtml .= '<div style="margin-top:' . ($versionsShown > 1 ? '16px' : '0') . '; padding:8px; background:#fff; border-radius:3px; border-left:3px solid #00cc07;">'; 1727*1d05cddcSAtari911 $changelogHtml .= '<div style="font-weight:bold; color:#00cc07; margin-bottom:6px;">️ ' . hsc($matches[1]) . '</div>'; 1728*1d05cddcSAtari911 } 1729*1d05cddcSAtari911 // List items (- **Added:** text) 1730*1d05cddcSAtari911 elseif (preg_match('/^- \*\*(.+?):\*\* (.+)$/', $line, $matches)) { 1731*1d05cddcSAtari911 $type = $matches[1]; 1732*1d05cddcSAtari911 $description = $matches[2]; 1733*1d05cddcSAtari911 1734*1d05cddcSAtari911 // Color-code by type 1735*1d05cddcSAtari911 $color = '#666'; 1736*1d05cddcSAtari911 $icon = '•'; 1737*1d05cddcSAtari911 if ($type === 'Added') { $color = '#28a745'; $icon = '✨'; } 1738*1d05cddcSAtari911 elseif ($type === 'Fixed') { $color = '#dc3545'; $icon = ''; } 1739*1d05cddcSAtari911 elseif ($type === 'Changed') { $color = '#7b1fa2'; $icon = ''; } 1740*1d05cddcSAtari911 elseif ($type === 'Improved') { $color = '#ff9800'; $icon = '⚡'; } 1741*1d05cddcSAtari911 elseif ($type === 'Development') { $color = '#6c757d'; $icon = '️'; } 1742*1d05cddcSAtari911 1743*1d05cddcSAtari911 $changelogHtml .= '<div style="margin:3px 0 3px 10px; color:' . $color . ';">'; 1744*1d05cddcSAtari911 $changelogHtml .= '<strong>' . $icon . ' ' . hsc($type) . ':</strong> <span style="color:#333;">' . hsc($description) . '</span>'; 1745*1d05cddcSAtari911 $changelogHtml .= '</div>'; 1746*1d05cddcSAtari911 } 1747*1d05cddcSAtari911 // Close version block on empty line after items 1748*1d05cddcSAtari911 elseif ($inVersion && $line === '' && $versionsShown > 0) { 1749*1d05cddcSAtari911 $changelogHtml .= '</div>'; 1750*1d05cddcSAtari911 $inVersion = false; 1751*1d05cddcSAtari911 } 1752*1d05cddcSAtari911 } 1753*1d05cddcSAtari911 1754*1d05cddcSAtari911 // Close last version if still open 1755*1d05cddcSAtari911 if ($inVersion) { 1756*1d05cddcSAtari911 $changelogHtml .= '</div>'; 1757*1d05cddcSAtari911 } 1758*1d05cddcSAtari911 1759*1d05cddcSAtari911 $changelogHtml .= '</div>'; 1760*1d05cddcSAtari911 1761*1d05cddcSAtari911 echo $changelogHtml; 1762*1d05cddcSAtari911 } else { 1763*1d05cddcSAtari911 echo '<p style="color:#999; font-size:13px; font-style:italic;">Changelog not available</p>'; 1764*1d05cddcSAtari911 } 1765*1d05cddcSAtari911 1766*1d05cddcSAtari911 echo '</div>'; 1767*1d05cddcSAtari911 1768*1d05cddcSAtari911 // Upload form 1769*1d05cddcSAtari911 echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">'; 1770*1d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;">Upload New Version</h3>'; 1771*1d05cddcSAtari911 echo '<p style="color:#666; font-size:13px; margin:0 0 10px;">Upload a calendar plugin ZIP file to update. Your configuration will be preserved.</p>'; 1772*1d05cddcSAtari911 1773*1d05cddcSAtari911 echo '<form method="post" action="?do=admin&page=calendar&tab=update" enctype="multipart/form-data" id="uploadForm">'; 1774*1d05cddcSAtari911 echo '<input type="hidden" name="action" value="upload_update">'; 1775*1d05cddcSAtari911 echo '<div style="margin:10px 0;">'; 1776*1d05cddcSAtari911 echo '<input type="file" name="plugin_zip" accept=".zip" required style="padding:8px; border:1px solid #ddd; border-radius:3px; font-size:13px;">'; 1777*1d05cddcSAtari911 echo '</div>'; 1778*1d05cddcSAtari911 echo '<div style="margin:10px 0;">'; 1779*1d05cddcSAtari911 echo '<label style="display:flex; align-items:center; gap:8px; font-size:13px;">'; 1780*1d05cddcSAtari911 echo '<input type="checkbox" name="backup_first" value="1" checked>'; 1781*1d05cddcSAtari911 echo '<span>Create backup before updating (Recommended)</span>'; 1782*1d05cddcSAtari911 echo '</label>'; 1783*1d05cddcSAtari911 echo '</div>'; 1784*1d05cddcSAtari911 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>'; 1785*1d05cddcSAtari911 echo '</form>'; 1786*1d05cddcSAtari911 echo '</div>'; 1787*1d05cddcSAtari911 1788*1d05cddcSAtari911 // Warning box 1789*1d05cddcSAtari911 echo '<div style="background:#fff3e0; border-left:3px solid #ff9800; padding:12px; margin:10px 0; border-radius:3px; max-width:900px;">'; 1790*1d05cddcSAtari911 echo '<h4 style="margin:0 0 5px 0; color:#e65100; font-size:14px;">⚠️ Important Notes</h4>'; 1791*1d05cddcSAtari911 echo '<ul style="margin:5px 0; padding-left:20px; font-size:12px; color:#e65100;">'; 1792*1d05cddcSAtari911 echo '<li>This will replace all plugin files</li>'; 1793*1d05cddcSAtari911 echo '<li>Configuration files (sync_config.php) will be preserved</li>'; 1794*1d05cddcSAtari911 echo '<li>Event data will not be affected</li>'; 1795*1d05cddcSAtari911 echo '<li>Backup will be saved to: <code>calendar.backup.vX.X.X.YYYY-MM-DD_HH-MM-SS.zip</code></li>'; 1796*1d05cddcSAtari911 echo '<li>Make sure the ZIP file is a valid calendar plugin</li>'; 1797*1d05cddcSAtari911 echo '</ul>'; 1798*1d05cddcSAtari911 echo '</div>'; 1799*1d05cddcSAtari911 1800*1d05cddcSAtari911 // Backup list 1801*1d05cddcSAtari911 $backupDir = DOKU_PLUGIN; 1802*1d05cddcSAtari911 $backups = glob($backupDir . 'calendar*.zip'); 1803*1d05cddcSAtari911 1804*1d05cddcSAtari911 // Filter to only show files that look like backups (not the uploaded plugin files) 1805*1d05cddcSAtari911 $backups = array_filter($backups, function($file) { 1806*1d05cddcSAtari911 $name = basename($file); 1807*1d05cddcSAtari911 // Include files that start with "calendar" but exclude files that are just "calendar.zip" (uploaded plugin) 1808*1d05cddcSAtari911 return $name !== 'calendar.zip'; 1809*1d05cddcSAtari911 }); 1810*1d05cddcSAtari911 1811*1d05cddcSAtari911 if (!empty($backups)) { 1812*1d05cddcSAtari911 rsort($backups); // Newest first 1813*1d05cddcSAtari911 echo '<div style="background:#f9f9f9; padding:12px; margin:10px 0; border-left:3px solid #00cc07; border-radius:3px; max-width:900px;">'; 1814*1d05cddcSAtari911 echo '<h3 style="margin:0 0 8px 0; color:#00cc07; font-size:16px;"> Available Backups</h3>'; 1815*1d05cddcSAtari911 echo '<div style="max-height:200px; overflow-y:auto; border:1px solid #ddd; border-radius:3px; background:white;">'; 1816*1d05cddcSAtari911 echo '<table style="width:100%; border-collapse:collapse; font-size:12px;">'; 1817*1d05cddcSAtari911 echo '<thead style="position:sticky; top:0; background:#e9e9e9;">'; 1818*1d05cddcSAtari911 echo '<tr>'; 1819*1d05cddcSAtari911 echo '<th style="padding:6px; text-align:left; border-bottom:2px solid #ddd;">Backup File</th>'; 1820*1d05cddcSAtari911 echo '<th style="padding:6px; text-align:left; border-bottom:2px solid #ddd;">Size</th>'; 1821*1d05cddcSAtari911 echo '<th style="padding:6px; text-align:left; border-bottom:2px solid #ddd;">Actions</th>'; 1822*1d05cddcSAtari911 echo '</tr></thead><tbody>'; 1823*1d05cddcSAtari911 1824*1d05cddcSAtari911 foreach ($backups as $backup) { 1825*1d05cddcSAtari911 $filename = basename($backup); 1826*1d05cddcSAtari911 $size = $this->formatBytes(filesize($backup)); 1827*1d05cddcSAtari911 echo '<tr style="border-bottom:1px solid #eee;">'; 1828*1d05cddcSAtari911 echo '<td style="padding:6px;"><code style="font-size:11px;">' . hsc($filename) . '</code></td>'; 1829*1d05cddcSAtari911 echo '<td style="padding:6px;">' . $size . '</td>'; 1830*1d05cddcSAtari911 echo '<td style="padding:6px; white-space:nowrap;">'; 1831*1d05cddcSAtari911 echo '<a href="' . DOKU_BASE . 'lib/plugins/' . hsc($filename) . '" download style="color:#00cc07; text-decoration:none; font-size:11px; margin-right:10px;"> Download</a>'; 1832*1d05cddcSAtari911 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>'; 1833*1d05cddcSAtari911 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>'; 1834*1d05cddcSAtari911 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>'; 1835*1d05cddcSAtari911 echo '</td>'; 1836*1d05cddcSAtari911 echo '</tr>'; 1837*1d05cddcSAtari911 } 1838*1d05cddcSAtari911 1839*1d05cddcSAtari911 echo '</tbody></table>'; 1840*1d05cddcSAtari911 echo '</div>'; 1841*1d05cddcSAtari911 echo '</div>'; 1842*1d05cddcSAtari911 } 1843*1d05cddcSAtari911 1844*1d05cddcSAtari911 echo '<script> 1845*1d05cddcSAtari911 function confirmUpload() { 1846*1d05cddcSAtari911 const fileInput = document.querySelector(\'input[name="plugin_zip"]\'); 1847*1d05cddcSAtari911 if (!fileInput.files[0]) { 1848*1d05cddcSAtari911 alert("Please select a ZIP file"); 1849*1d05cddcSAtari911 return false; 1850*1d05cddcSAtari911 } 1851*1d05cddcSAtari911 1852*1d05cddcSAtari911 const fileName = fileInput.files[0].name; 1853*1d05cddcSAtari911 if (!fileName.endsWith(".zip")) { 1854*1d05cddcSAtari911 alert("Please select a ZIP file"); 1855*1d05cddcSAtari911 return false; 1856*1d05cddcSAtari911 } 1857*1d05cddcSAtari911 1858*1d05cddcSAtari911 return confirm("Upload and install: " + fileName + "?\\n\\nThis will replace all plugin files.\\nYour configuration and data will be preserved.\\n\\nContinue?"); 1859*1d05cddcSAtari911 } 1860*1d05cddcSAtari911 1861*1d05cddcSAtari911 function deleteBackup(filename) { 1862*1d05cddcSAtari911 if (!confirm("Delete backup: " + filename + "?\\n\\nThis cannot be undone!")) { 1863*1d05cddcSAtari911 return; 1864*1d05cddcSAtari911 } 1865*1d05cddcSAtari911 1866*1d05cddcSAtari911 const form = document.createElement("form"); 1867*1d05cddcSAtari911 form.method = "POST"; 1868*1d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=update"; 1869*1d05cddcSAtari911 1870*1d05cddcSAtari911 const actionInput = document.createElement("input"); 1871*1d05cddcSAtari911 actionInput.type = "hidden"; 1872*1d05cddcSAtari911 actionInput.name = "action"; 1873*1d05cddcSAtari911 actionInput.value = "delete_backup"; 1874*1d05cddcSAtari911 form.appendChild(actionInput); 1875*1d05cddcSAtari911 1876*1d05cddcSAtari911 const filenameInput = document.createElement("input"); 1877*1d05cddcSAtari911 filenameInput.type = "hidden"; 1878*1d05cddcSAtari911 filenameInput.name = "backup_file"; 1879*1d05cddcSAtari911 filenameInput.value = filename; 1880*1d05cddcSAtari911 form.appendChild(filenameInput); 1881*1d05cddcSAtari911 1882*1d05cddcSAtari911 document.body.appendChild(form); 1883*1d05cddcSAtari911 form.submit(); 1884*1d05cddcSAtari911 } 1885*1d05cddcSAtari911 1886*1d05cddcSAtari911 function restoreBackup(filename) { 1887*1d05cddcSAtari911 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?")) { 1888*1d05cddcSAtari911 return; 1889*1d05cddcSAtari911 } 1890*1d05cddcSAtari911 1891*1d05cddcSAtari911 const form = document.createElement("form"); 1892*1d05cddcSAtari911 form.method = "POST"; 1893*1d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=update"; 1894*1d05cddcSAtari911 1895*1d05cddcSAtari911 const actionInput = document.createElement("input"); 1896*1d05cddcSAtari911 actionInput.type = "hidden"; 1897*1d05cddcSAtari911 actionInput.name = "action"; 1898*1d05cddcSAtari911 actionInput.value = "restore_backup"; 1899*1d05cddcSAtari911 form.appendChild(actionInput); 1900*1d05cddcSAtari911 1901*1d05cddcSAtari911 const filenameInput = document.createElement("input"); 1902*1d05cddcSAtari911 filenameInput.type = "hidden"; 1903*1d05cddcSAtari911 filenameInput.name = "backup_file"; 1904*1d05cddcSAtari911 filenameInput.value = filename; 1905*1d05cddcSAtari911 form.appendChild(filenameInput); 1906*1d05cddcSAtari911 1907*1d05cddcSAtari911 document.body.appendChild(form); 1908*1d05cddcSAtari911 form.submit(); 1909*1d05cddcSAtari911 } 1910*1d05cddcSAtari911 1911*1d05cddcSAtari911 function renameBackup(filename) { 1912*1d05cddcSAtari911 const newName = prompt("Enter new backup name (without .zip extension):\\n\\nCurrent: " + filename.replace(/\\.zip$/, ""), filename.replace(/\\.zip$/, "")); 1913*1d05cddcSAtari911 if (!newName || newName === filename.replace(/\\.zip$/, "")) { 1914*1d05cddcSAtari911 return; 1915*1d05cddcSAtari911 } 1916*1d05cddcSAtari911 1917*1d05cddcSAtari911 // Add .zip if not present 1918*1d05cddcSAtari911 const newFilename = newName.endsWith(".zip") ? newName : newName + ".zip"; 1919*1d05cddcSAtari911 1920*1d05cddcSAtari911 // Basic validation 1921*1d05cddcSAtari911 if (!/^[a-zA-Z0-9._-]+$/.test(newFilename.replace(/\\.zip$/, ""))) { 1922*1d05cddcSAtari911 alert("Invalid filename. Use only letters, numbers, dots, dashes, and underscores."); 1923*1d05cddcSAtari911 return; 1924*1d05cddcSAtari911 } 1925*1d05cddcSAtari911 1926*1d05cddcSAtari911 const form = document.createElement("form"); 1927*1d05cddcSAtari911 form.method = "POST"; 1928*1d05cddcSAtari911 form.action = "?do=admin&page=calendar&tab=update"; 1929*1d05cddcSAtari911 1930*1d05cddcSAtari911 const actionInput = document.createElement("input"); 1931*1d05cddcSAtari911 actionInput.type = "hidden"; 1932*1d05cddcSAtari911 actionInput.name = "action"; 1933*1d05cddcSAtari911 actionInput.value = "rename_backup"; 1934*1d05cddcSAtari911 form.appendChild(actionInput); 1935*1d05cddcSAtari911 1936*1d05cddcSAtari911 const oldNameInput = document.createElement("input"); 1937*1d05cddcSAtari911 oldNameInput.type = "hidden"; 1938*1d05cddcSAtari911 oldNameInput.name = "old_name"; 1939*1d05cddcSAtari911 oldNameInput.value = filename; 1940*1d05cddcSAtari911 form.appendChild(oldNameInput); 1941*1d05cddcSAtari911 1942*1d05cddcSAtari911 const newNameInput = document.createElement("input"); 1943*1d05cddcSAtari911 newNameInput.type = "hidden"; 1944*1d05cddcSAtari911 newNameInput.name = "new_name"; 1945*1d05cddcSAtari911 newNameInput.value = newFilename; 1946*1d05cddcSAtari911 form.appendChild(newNameInput); 1947*1d05cddcSAtari911 1948*1d05cddcSAtari911 document.body.appendChild(form); 1949*1d05cddcSAtari911 form.submit(); 1950*1d05cddcSAtari911 } 1951*1d05cddcSAtari911 </script>'; 1952*1d05cddcSAtari911 } 1953*1d05cddcSAtari911 1954*1d05cddcSAtari911 private function saveConfig() { 1955*1d05cddcSAtari911 global $INPUT; 1956*1d05cddcSAtari911 1957*1d05cddcSAtari911 // Load existing config to preserve all settings 1958*1d05cddcSAtari911 $configFile = DOKU_PLUGIN . 'calendar/sync_config.php'; 1959*1d05cddcSAtari911 $existingConfig = []; 1960*1d05cddcSAtari911 if (file_exists($configFile)) { 1961*1d05cddcSAtari911 $existingConfig = include $configFile; 1962*1d05cddcSAtari911 } 1963*1d05cddcSAtari911 1964*1d05cddcSAtari911 // Update only the fields from the form - preserve everything else 1965*1d05cddcSAtari911 $config = $existingConfig; 1966*1d05cddcSAtari911 1967*1d05cddcSAtari911 // Update basic fields 1968*1d05cddcSAtari911 $config['tenant_id'] = $INPUT->str('tenant_id'); 1969*1d05cddcSAtari911 $config['client_id'] = $INPUT->str('client_id'); 1970*1d05cddcSAtari911 $config['client_secret'] = $INPUT->str('client_secret'); 1971*1d05cddcSAtari911 $config['user_email'] = $INPUT->str('user_email'); 1972*1d05cddcSAtari911 $config['timezone'] = $INPUT->str('timezone', 'America/Los_Angeles'); 1973*1d05cddcSAtari911 $config['default_category'] = $INPUT->str('default_category', 'Blue category'); 1974*1d05cddcSAtari911 $config['reminder_minutes'] = $INPUT->int('reminder_minutes', 15); 1975*1d05cddcSAtari911 $config['sync_completed_tasks'] = $INPUT->bool('sync_completed_tasks'); 1976*1d05cddcSAtari911 $config['delete_outlook_events'] = $INPUT->bool('delete_outlook_events'); 1977*1d05cddcSAtari911 $config['sync_all_namespaces'] = $INPUT->bool('sync_all_namespaces'); 1978*1d05cddcSAtari911 $config['sync_namespaces'] = $INPUT->arr('sync_namespaces'); 1979*1d05cddcSAtari911 $config['important_namespaces'] = $INPUT->str('important_namespaces', 'important'); 1980*1d05cddcSAtari911 1981*1d05cddcSAtari911 // Parse category mapping 1982*1d05cddcSAtari911 $config['category_mapping'] = []; 1983*1d05cddcSAtari911 $mappingText = $INPUT->str('category_mapping'); 1984*1d05cddcSAtari911 if ($mappingText) { 1985*1d05cddcSAtari911 $lines = explode("\n", $mappingText); 1986*1d05cddcSAtari911 foreach ($lines as $line) { 1987*1d05cddcSAtari911 $line = trim($line); 1988*1d05cddcSAtari911 if (empty($line)) continue; 1989*1d05cddcSAtari911 $parts = explode('=', $line, 2); 1990*1d05cddcSAtari911 if (count($parts) === 2) { 1991*1d05cddcSAtari911 $config['category_mapping'][trim($parts[0])] = trim($parts[1]); 1992*1d05cddcSAtari911 } 1993*1d05cddcSAtari911 } 1994*1d05cddcSAtari911 } 1995*1d05cddcSAtari911 1996*1d05cddcSAtari911 // Parse color mapping from dropdown selections 1997*1d05cddcSAtari911 $config['color_mapping'] = []; 1998*1d05cddcSAtari911 $colorMappingCount = $INPUT->int('color_mapping_count', 0); 1999*1d05cddcSAtari911 for ($i = 0; $i < $colorMappingCount; $i++) { 2000*1d05cddcSAtari911 $hexColor = $INPUT->str('color_hex_' . $i); 2001*1d05cddcSAtari911 $category = $INPUT->str('color_map_' . $i); 2002*1d05cddcSAtari911 2003*1d05cddcSAtari911 if (!empty($hexColor) && !empty($category)) { 2004*1d05cddcSAtari911 $config['color_mapping'][$hexColor] = $category; 2005*1d05cddcSAtari911 } 2006*1d05cddcSAtari911 } 2007*1d05cddcSAtari911 2008*1d05cddcSAtari911 // Build file content using return format 2009*1d05cddcSAtari911 $content = "<?php\n"; 2010*1d05cddcSAtari911 $content .= "/**\n"; 2011*1d05cddcSAtari911 $content .= " * DokuWiki Calendar → Outlook Sync - Configuration\n"; 2012*1d05cddcSAtari911 $content .= " * \n"; 2013*1d05cddcSAtari911 $content .= " * SECURITY: Add this file to .gitignore!\n"; 2014*1d05cddcSAtari911 $content .= " * Never commit credentials to version control.\n"; 2015*1d05cddcSAtari911 $content .= " */\n\n"; 2016*1d05cddcSAtari911 $content .= "return " . var_export($config, true) . ";\n"; 2017*1d05cddcSAtari911 2018*1d05cddcSAtari911 // Save file 2019*1d05cddcSAtari911 if (file_put_contents($configFile, $content)) { 2020*1d05cddcSAtari911 $this->redirect('Configuration saved successfully!', 'success'); 2021*1d05cddcSAtari911 } else { 2022*1d05cddcSAtari911 $this->redirect('Error: Could not save configuration file', 'error'); 2023*1d05cddcSAtari911 } 2024*1d05cddcSAtari911 } 2025*1d05cddcSAtari911 2026*1d05cddcSAtari911 private function clearCache() { 2027*1d05cddcSAtari911 // Clear DokuWiki cache 2028*1d05cddcSAtari911 $cacheDir = DOKU_INC . 'data/cache'; 2029*1d05cddcSAtari911 2030*1d05cddcSAtari911 if (is_dir($cacheDir)) { 2031*1d05cddcSAtari911 $this->recursiveDelete($cacheDir, false); 2032*1d05cddcSAtari911 $this->redirect('Cache cleared successfully!', 'success', 'update'); 2033*1d05cddcSAtari911 } else { 2034*1d05cddcSAtari911 $this->redirect('Cache directory not found', 'error', 'update'); 2035*1d05cddcSAtari911 } 2036*1d05cddcSAtari911 } 2037*1d05cddcSAtari911 2038*1d05cddcSAtari911 private function recursiveDelete($dir, $deleteRoot = true) { 2039*1d05cddcSAtari911 if (!is_dir($dir)) return; 2040*1d05cddcSAtari911 2041*1d05cddcSAtari911 $files = array_diff(scandir($dir), array('.', '..')); 2042*1d05cddcSAtari911 foreach ($files as $file) { 2043*1d05cddcSAtari911 $path = $dir . '/' . $file; 2044*1d05cddcSAtari911 if (is_dir($path)) { 2045*1d05cddcSAtari911 $this->recursiveDelete($path, true); 2046*1d05cddcSAtari911 } else { 2047*1d05cddcSAtari911 @unlink($path); 2048*1d05cddcSAtari911 } 2049*1d05cddcSAtari911 } 2050*1d05cddcSAtari911 2051*1d05cddcSAtari911 if ($deleteRoot) { 2052*1d05cddcSAtari911 @rmdir($dir); 2053*1d05cddcSAtari911 } 2054*1d05cddcSAtari911 } 2055*1d05cddcSAtari911 2056*1d05cddcSAtari911 private function findRecurringEvents() { 2057*1d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 2058*1d05cddcSAtari911 $recurring = []; 2059*1d05cddcSAtari911 $allEvents = []; // Track all events to detect patterns 2060*1d05cddcSAtari911 2061*1d05cddcSAtari911 // Check root calendar directory first (blank/default namespace) 2062*1d05cddcSAtari911 $rootCalendarDir = $dataDir . 'calendar'; 2063*1d05cddcSAtari911 if (is_dir($rootCalendarDir)) { 2064*1d05cddcSAtari911 foreach (glob($rootCalendarDir . '/*.json') as $file) { 2065*1d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 2066*1d05cddcSAtari911 if (!$data) continue; 2067*1d05cddcSAtari911 2068*1d05cddcSAtari911 foreach ($data as $dateKey => $events) { 2069*1d05cddcSAtari911 foreach ($events as $event) { 2070*1d05cddcSAtari911 // Group by title + namespace (events with same title are likely recurring) 2071*1d05cddcSAtari911 $groupKey = strtolower(trim($event['title'])) . '_'; 2072*1d05cddcSAtari911 2073*1d05cddcSAtari911 if (!isset($allEvents[$groupKey])) { 2074*1d05cddcSAtari911 $allEvents[$groupKey] = [ 2075*1d05cddcSAtari911 'title' => $event['title'], 2076*1d05cddcSAtari911 'namespace' => '', 2077*1d05cddcSAtari911 'dates' => [], 2078*1d05cddcSAtari911 'events' => [] 2079*1d05cddcSAtari911 ]; 2080*1d05cddcSAtari911 } 2081*1d05cddcSAtari911 $allEvents[$groupKey]['dates'][] = $dateKey; 2082*1d05cddcSAtari911 $allEvents[$groupKey]['events'][] = $event; 2083*1d05cddcSAtari911 } 2084*1d05cddcSAtari911 } 2085*1d05cddcSAtari911 } 2086*1d05cddcSAtari911 } 2087*1d05cddcSAtari911 2088*1d05cddcSAtari911 // Scan all namespace directories 2089*1d05cddcSAtari911 foreach (glob($dataDir . '*', GLOB_ONLYDIR) as $nsDir) { 2090*1d05cddcSAtari911 $namespace = basename($nsDir); 2091*1d05cddcSAtari911 2092*1d05cddcSAtari911 // Skip the root 'calendar' dir (already processed above) 2093*1d05cddcSAtari911 if ($namespace === 'calendar') continue; 2094*1d05cddcSAtari911 2095*1d05cddcSAtari911 $calendarDir = $nsDir . '/calendar'; 2096*1d05cddcSAtari911 2097*1d05cddcSAtari911 if (!is_dir($calendarDir)) continue; 2098*1d05cddcSAtari911 2099*1d05cddcSAtari911 // Scan all calendar files 2100*1d05cddcSAtari911 foreach (glob($calendarDir . '/*.json') as $file) { 2101*1d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 2102*1d05cddcSAtari911 if (!$data) continue; 2103*1d05cddcSAtari911 2104*1d05cddcSAtari911 foreach ($data as $dateKey => $events) { 2105*1d05cddcSAtari911 foreach ($events as $event) { 2106*1d05cddcSAtari911 $groupKey = strtolower(trim($event['title'])) . '_' . ($event['namespace'] ?? ''); 2107*1d05cddcSAtari911 2108*1d05cddcSAtari911 if (!isset($allEvents[$groupKey])) { 2109*1d05cddcSAtari911 $allEvents[$groupKey] = [ 2110*1d05cddcSAtari911 'title' => $event['title'], 2111*1d05cddcSAtari911 'namespace' => $event['namespace'] ?? '', 2112*1d05cddcSAtari911 'dates' => [], 2113*1d05cddcSAtari911 'events' => [] 2114*1d05cddcSAtari911 ]; 2115*1d05cddcSAtari911 } 2116*1d05cddcSAtari911 $allEvents[$groupKey]['dates'][] = $dateKey; 2117*1d05cddcSAtari911 $allEvents[$groupKey]['events'][] = $event; 2118*1d05cddcSAtari911 } 2119*1d05cddcSAtari911 } 2120*1d05cddcSAtari911 } 2121*1d05cddcSAtari911 } 2122*1d05cddcSAtari911 2123*1d05cddcSAtari911 // Analyze patterns - only include if 3+ occurrences 2124*1d05cddcSAtari911 foreach ($allEvents as $groupKey => $group) { 2125*1d05cddcSAtari911 if (count($group['dates']) >= 3) { 2126*1d05cddcSAtari911 // Sort dates 2127*1d05cddcSAtari911 sort($group['dates']); 2128*1d05cddcSAtari911 2129*1d05cddcSAtari911 // Calculate interval between first and second occurrence 2130*1d05cddcSAtari911 $date1 = new DateTime($group['dates'][0]); 2131*1d05cddcSAtari911 $date2 = new DateTime($group['dates'][1]); 2132*1d05cddcSAtari911 $interval = $date1->diff($date2); 2133*1d05cddcSAtari911 2134*1d05cddcSAtari911 // Determine pattern 2135*1d05cddcSAtari911 $pattern = 'Custom'; 2136*1d05cddcSAtari911 if ($interval->days == 1) { 2137*1d05cddcSAtari911 $pattern = 'Daily'; 2138*1d05cddcSAtari911 } elseif ($interval->days == 7) { 2139*1d05cddcSAtari911 $pattern = 'Weekly'; 2140*1d05cddcSAtari911 } elseif ($interval->days >= 14 && $interval->days <= 16) { 2141*1d05cddcSAtari911 $pattern = 'Bi-weekly'; 2142*1d05cddcSAtari911 } elseif ($interval->days >= 28 && $interval->days <= 31) { 2143*1d05cddcSAtari911 $pattern = 'Monthly'; 2144*1d05cddcSAtari911 } elseif ($interval->days >= 365 && $interval->days <= 366) { 2145*1d05cddcSAtari911 $pattern = 'Yearly'; 2146*1d05cddcSAtari911 } 2147*1d05cddcSAtari911 2148*1d05cddcSAtari911 // Use first event's ID or create a synthetic one 2149*1d05cddcSAtari911 $baseId = isset($group['events'][0]['recurringId']) 2150*1d05cddcSAtari911 ? $group['events'][0]['recurringId'] 2151*1d05cddcSAtari911 : md5($group['title'] . $group['namespace']); 2152*1d05cddcSAtari911 2153*1d05cddcSAtari911 $recurring[] = [ 2154*1d05cddcSAtari911 'baseId' => $baseId, 2155*1d05cddcSAtari911 'title' => $group['title'], 2156*1d05cddcSAtari911 'namespace' => $group['namespace'], 2157*1d05cddcSAtari911 'pattern' => $pattern, 2158*1d05cddcSAtari911 'count' => count($group['dates']), 2159*1d05cddcSAtari911 'firstDate' => $group['dates'][0], 2160*1d05cddcSAtari911 'interval' => $interval->days 2161*1d05cddcSAtari911 ]; 2162*1d05cddcSAtari911 } 2163*1d05cddcSAtari911 } 2164*1d05cddcSAtari911 2165*1d05cddcSAtari911 return $recurring; 2166*1d05cddcSAtari911 } 2167*1d05cddcSAtari911 2168*1d05cddcSAtari911 private function getEventsByNamespace() { 2169*1d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 2170*1d05cddcSAtari911 $result = []; 2171*1d05cddcSAtari911 2172*1d05cddcSAtari911 // Check root calendar directory first (blank/default namespace) 2173*1d05cddcSAtari911 $rootCalendarDir = $dataDir . 'calendar'; 2174*1d05cddcSAtari911 if (is_dir($rootCalendarDir)) { 2175*1d05cddcSAtari911 $hasFiles = false; 2176*1d05cddcSAtari911 $events = []; 2177*1d05cddcSAtari911 2178*1d05cddcSAtari911 foreach (glob($rootCalendarDir . '/*.json') as $file) { 2179*1d05cddcSAtari911 $hasFiles = true; 2180*1d05cddcSAtari911 $month = basename($file, '.json'); 2181*1d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 2182*1d05cddcSAtari911 if (!$data) continue; 2183*1d05cddcSAtari911 2184*1d05cddcSAtari911 foreach ($data as $dateKey => $eventList) { 2185*1d05cddcSAtari911 foreach ($eventList as $event) { 2186*1d05cddcSAtari911 $events[] = [ 2187*1d05cddcSAtari911 'id' => $event['id'], 2188*1d05cddcSAtari911 'title' => $event['title'], 2189*1d05cddcSAtari911 'date' => $dateKey, 2190*1d05cddcSAtari911 'startTime' => $event['startTime'] ?? null, 2191*1d05cddcSAtari911 'month' => $month 2192*1d05cddcSAtari911 ]; 2193*1d05cddcSAtari911 } 2194*1d05cddcSAtari911 } 2195*1d05cddcSAtari911 } 2196*1d05cddcSAtari911 2197*1d05cddcSAtari911 // Add if it has JSON files (even if empty) 2198*1d05cddcSAtari911 if ($hasFiles) { 2199*1d05cddcSAtari911 $result[''] = ['events' => $events]; 2200*1d05cddcSAtari911 } 2201*1d05cddcSAtari911 } 2202*1d05cddcSAtari911 2203*1d05cddcSAtari911 // Recursively scan all namespace directories including sub-namespaces 2204*1d05cddcSAtari911 $this->scanNamespaceRecursive($dataDir, '', $result); 2205*1d05cddcSAtari911 2206*1d05cddcSAtari911 // Sort namespaces, but keep '' (default) first 2207*1d05cddcSAtari911 uksort($result, function($a, $b) { 2208*1d05cddcSAtari911 if ($a === '') return -1; 2209*1d05cddcSAtari911 if ($b === '') return 1; 2210*1d05cddcSAtari911 return strcmp($a, $b); 2211*1d05cddcSAtari911 }); 2212*1d05cddcSAtari911 2213*1d05cddcSAtari911 return $result; 2214*1d05cddcSAtari911 } 2215*1d05cddcSAtari911 2216*1d05cddcSAtari911 private function scanNamespaceRecursive($baseDir, $parentNamespace, &$result) { 2217*1d05cddcSAtari911 foreach (glob($baseDir . '*', GLOB_ONLYDIR) as $nsDir) { 2218*1d05cddcSAtari911 $dirName = basename($nsDir); 2219*1d05cddcSAtari911 2220*1d05cddcSAtari911 // Skip the root 'calendar' dir 2221*1d05cddcSAtari911 if ($dirName === 'calendar' && empty($parentNamespace)) continue; 2222*1d05cddcSAtari911 2223*1d05cddcSAtari911 // Build namespace path 2224*1d05cddcSAtari911 $namespace = empty($parentNamespace) ? $dirName : $parentNamespace . ':' . $dirName; 2225*1d05cddcSAtari911 2226*1d05cddcSAtari911 // Check for calendar directory 2227*1d05cddcSAtari911 $calendarDir = $nsDir . '/calendar'; 2228*1d05cddcSAtari911 if (is_dir($calendarDir)) { 2229*1d05cddcSAtari911 $hasFiles = false; 2230*1d05cddcSAtari911 $events = []; 2231*1d05cddcSAtari911 2232*1d05cddcSAtari911 // Scan all calendar files 2233*1d05cddcSAtari911 foreach (glob($calendarDir . '/*.json') as $file) { 2234*1d05cddcSAtari911 $hasFiles = true; 2235*1d05cddcSAtari911 $month = basename($file, '.json'); 2236*1d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 2237*1d05cddcSAtari911 if (!$data) continue; 2238*1d05cddcSAtari911 2239*1d05cddcSAtari911 foreach ($data as $dateKey => $eventList) { 2240*1d05cddcSAtari911 foreach ($eventList as $event) { 2241*1d05cddcSAtari911 $events[] = [ 2242*1d05cddcSAtari911 'id' => $event['id'], 2243*1d05cddcSAtari911 'title' => $event['title'], 2244*1d05cddcSAtari911 'date' => $dateKey, 2245*1d05cddcSAtari911 'startTime' => $event['startTime'] ?? null, 2246*1d05cddcSAtari911 'month' => $month 2247*1d05cddcSAtari911 ]; 2248*1d05cddcSAtari911 } 2249*1d05cddcSAtari911 } 2250*1d05cddcSAtari911 } 2251*1d05cddcSAtari911 2252*1d05cddcSAtari911 // Add namespace if it has JSON files (even if empty) 2253*1d05cddcSAtari911 if ($hasFiles) { 2254*1d05cddcSAtari911 $result[$namespace] = ['events' => $events]; 2255*1d05cddcSAtari911 } 2256*1d05cddcSAtari911 } 2257*1d05cddcSAtari911 2258*1d05cddcSAtari911 // Recursively scan sub-directories 2259*1d05cddcSAtari911 $this->scanNamespaceRecursive($nsDir . '/', $namespace, $result); 2260*1d05cddcSAtari911 } 2261*1d05cddcSAtari911 } 2262*1d05cddcSAtari911 2263*1d05cddcSAtari911 private function getAllNamespaces() { 2264*1d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 2265*1d05cddcSAtari911 $namespaces = []; 2266*1d05cddcSAtari911 2267*1d05cddcSAtari911 // Check root calendar directory first 2268*1d05cddcSAtari911 $rootCalendarDir = $dataDir . 'calendar'; 2269*1d05cddcSAtari911 if (is_dir($rootCalendarDir)) { 2270*1d05cddcSAtari911 $namespaces[] = ''; // Blank/default namespace 2271*1d05cddcSAtari911 } 2272*1d05cddcSAtari911 2273*1d05cddcSAtari911 // Check all other namespace directories 2274*1d05cddcSAtari911 foreach (glob($dataDir . '*', GLOB_ONLYDIR) as $nsDir) { 2275*1d05cddcSAtari911 $namespace = basename($nsDir); 2276*1d05cddcSAtari911 2277*1d05cddcSAtari911 // Skip the root 'calendar' dir (already added as '') 2278*1d05cddcSAtari911 if ($namespace === 'calendar') continue; 2279*1d05cddcSAtari911 2280*1d05cddcSAtari911 $calendarDir = $nsDir . '/calendar'; 2281*1d05cddcSAtari911 if (is_dir($calendarDir)) { 2282*1d05cddcSAtari911 $namespaces[] = $namespace; 2283*1d05cddcSAtari911 } 2284*1d05cddcSAtari911 } 2285*1d05cddcSAtari911 2286*1d05cddcSAtari911 return $namespaces; 2287*1d05cddcSAtari911 } 2288*1d05cddcSAtari911 2289*1d05cddcSAtari911 private function searchEvents($search, $filterNamespace) { 2290*1d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 2291*1d05cddcSAtari911 $results = []; 2292*1d05cddcSAtari911 2293*1d05cddcSAtari911 $search = strtolower(trim($search)); 2294*1d05cddcSAtari911 2295*1d05cddcSAtari911 foreach (glob($dataDir . '*', GLOB_ONLYDIR) as $nsDir) { 2296*1d05cddcSAtari911 $namespace = basename($nsDir); 2297*1d05cddcSAtari911 $calendarDir = $nsDir . '/calendar'; 2298*1d05cddcSAtari911 2299*1d05cddcSAtari911 if (!is_dir($calendarDir)) continue; 2300*1d05cddcSAtari911 if ($filterNamespace !== '' && $namespace !== $filterNamespace) continue; 2301*1d05cddcSAtari911 2302*1d05cddcSAtari911 foreach (glob($calendarDir . '/*.json') as $file) { 2303*1d05cddcSAtari911 $month = basename($file, '.json'); 2304*1d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 2305*1d05cddcSAtari911 if (!$data) continue; 2306*1d05cddcSAtari911 2307*1d05cddcSAtari911 foreach ($data as $dateKey => $events) { 2308*1d05cddcSAtari911 foreach ($events as $event) { 2309*1d05cddcSAtari911 if ($search === '' || strpos(strtolower($event['title']), $search) !== false) { 2310*1d05cddcSAtari911 $results[] = [ 2311*1d05cddcSAtari911 'id' => $event['id'], 2312*1d05cddcSAtari911 'title' => $event['title'], 2313*1d05cddcSAtari911 'date' => $dateKey, 2314*1d05cddcSAtari911 'startTime' => $event['startTime'] ?? null, 2315*1d05cddcSAtari911 'namespace' => $event['namespace'] ?? '', 2316*1d05cddcSAtari911 'month' => $month 2317*1d05cddcSAtari911 ]; 2318*1d05cddcSAtari911 } 2319*1d05cddcSAtari911 } 2320*1d05cddcSAtari911 } 2321*1d05cddcSAtari911 } 2322*1d05cddcSAtari911 } 2323*1d05cddcSAtari911 2324*1d05cddcSAtari911 return $results; 2325*1d05cddcSAtari911 } 2326*1d05cddcSAtari911 2327*1d05cddcSAtari911 private function deleteRecurringSeries() { 2328*1d05cddcSAtari911 global $INPUT; 2329*1d05cddcSAtari911 2330*1d05cddcSAtari911 $eventTitle = $INPUT->str('event_title'); 2331*1d05cddcSAtari911 $namespace = $INPUT->str('namespace'); 2332*1d05cddcSAtari911 2333*1d05cddcSAtari911 // Determine calendar directory 2334*1d05cddcSAtari911 if ($namespace === '') { 2335*1d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/calendar'; 2336*1d05cddcSAtari911 } else { 2337*1d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/' . $namespace . '/calendar'; 2338*1d05cddcSAtari911 } 2339*1d05cddcSAtari911 2340*1d05cddcSAtari911 $count = 0; 2341*1d05cddcSAtari911 2342*1d05cddcSAtari911 if (is_dir($dataDir)) { 2343*1d05cddcSAtari911 foreach (glob($dataDir . '/*.json') as $file) { 2344*1d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 2345*1d05cddcSAtari911 if (!$data) continue; 2346*1d05cddcSAtari911 2347*1d05cddcSAtari911 $modified = false; 2348*1d05cddcSAtari911 foreach ($data as $dateKey => $events) { 2349*1d05cddcSAtari911 $filtered = []; 2350*1d05cddcSAtari911 foreach ($events as $event) { 2351*1d05cddcSAtari911 // Match by title (case-insensitive) 2352*1d05cddcSAtari911 if (strtolower(trim($event['title'])) === strtolower(trim($eventTitle))) { 2353*1d05cddcSAtari911 $count++; 2354*1d05cddcSAtari911 $modified = true; 2355*1d05cddcSAtari911 } else { 2356*1d05cddcSAtari911 $filtered[] = $event; 2357*1d05cddcSAtari911 } 2358*1d05cddcSAtari911 } 2359*1d05cddcSAtari911 $data[$dateKey] = $filtered; 2360*1d05cddcSAtari911 } 2361*1d05cddcSAtari911 2362*1d05cddcSAtari911 if ($modified) { 2363*1d05cddcSAtari911 file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); 2364*1d05cddcSAtari911 } 2365*1d05cddcSAtari911 } 2366*1d05cddcSAtari911 } 2367*1d05cddcSAtari911 2368*1d05cddcSAtari911 $this->redirect("Deleted $count occurrences of recurring event: " . $eventTitle, 'success', 'manage'); 2369*1d05cddcSAtari911 } 2370*1d05cddcSAtari911 2371*1d05cddcSAtari911 private function editRecurringSeries() { 2372*1d05cddcSAtari911 global $INPUT; 2373*1d05cddcSAtari911 2374*1d05cddcSAtari911 $oldTitle = $INPUT->str('old_title'); 2375*1d05cddcSAtari911 $oldNamespace = $INPUT->str('old_namespace'); 2376*1d05cddcSAtari911 $newTitle = $INPUT->str('new_title'); 2377*1d05cddcSAtari911 $startTime = $INPUT->str('start_time'); 2378*1d05cddcSAtari911 $endTime = $INPUT->str('end_time'); 2379*1d05cddcSAtari911 $interval = $INPUT->int('interval', 0); 2380*1d05cddcSAtari911 $newNamespace = $INPUT->str('new_namespace'); 2381*1d05cddcSAtari911 2382*1d05cddcSAtari911 // Use old namespace if new namespace is empty (keep current) 2383*1d05cddcSAtari911 if (empty($newNamespace) && !isset($_POST['new_namespace'])) { 2384*1d05cddcSAtari911 $newNamespace = $oldNamespace; 2385*1d05cddcSAtari911 } 2386*1d05cddcSAtari911 2387*1d05cddcSAtari911 // Determine old calendar directory 2388*1d05cddcSAtari911 if ($oldNamespace === '') { 2389*1d05cddcSAtari911 $oldDataDir = DOKU_INC . 'data/meta/calendar'; 2390*1d05cddcSAtari911 } else { 2391*1d05cddcSAtari911 $oldDataDir = DOKU_INC . 'data/meta/' . $oldNamespace . '/calendar'; 2392*1d05cddcSAtari911 } 2393*1d05cddcSAtari911 2394*1d05cddcSAtari911 $count = 0; 2395*1d05cddcSAtari911 $eventsToMove = []; 2396*1d05cddcSAtari911 2397*1d05cddcSAtari911 if (is_dir($oldDataDir)) { 2398*1d05cddcSAtari911 foreach (glob($oldDataDir . '/*.json') as $file) { 2399*1d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 2400*1d05cddcSAtari911 if (!$data) continue; 2401*1d05cddcSAtari911 2402*1d05cddcSAtari911 $modified = false; 2403*1d05cddcSAtari911 foreach ($data as $dateKey => $events) { 2404*1d05cddcSAtari911 foreach ($events as $key => $event) { 2405*1d05cddcSAtari911 // Match by old title (case-insensitive) 2406*1d05cddcSAtari911 if (strtolower(trim($event['title'])) === strtolower(trim($oldTitle))) { 2407*1d05cddcSAtari911 // Update the title 2408*1d05cddcSAtari911 $data[$dateKey][$key]['title'] = $newTitle; 2409*1d05cddcSAtari911 2410*1d05cddcSAtari911 // Update start time if provided 2411*1d05cddcSAtari911 if (!empty($startTime)) { 2412*1d05cddcSAtari911 $data[$dateKey][$key]['start'] = $startTime; 2413*1d05cddcSAtari911 } 2414*1d05cddcSAtari911 2415*1d05cddcSAtari911 // Update end time if provided 2416*1d05cddcSAtari911 if (!empty($endTime)) { 2417*1d05cddcSAtari911 $data[$dateKey][$key]['end'] = $endTime; 2418*1d05cddcSAtari911 } 2419*1d05cddcSAtari911 2420*1d05cddcSAtari911 // Update namespace 2421*1d05cddcSAtari911 $data[$dateKey][$key]['namespace'] = $newNamespace; 2422*1d05cddcSAtari911 2423*1d05cddcSAtari911 // If changing interval, calculate new date 2424*1d05cddcSAtari911 if ($interval > 0 && $count > 0) { 2425*1d05cddcSAtari911 // Get the first event date as base 2426*1d05cddcSAtari911 if (empty($firstEventDate)) { 2427*1d05cddcSAtari911 $firstEventDate = $dateKey; 2428*1d05cddcSAtari911 } 2429*1d05cddcSAtari911 2430*1d05cddcSAtari911 // Calculate new date based on interval 2431*1d05cddcSAtari911 $newDate = date('Y-m-d', strtotime($firstEventDate . ' +' . ($count * $interval) . ' days')); 2432*1d05cddcSAtari911 2433*1d05cddcSAtari911 // Store for moving 2434*1d05cddcSAtari911 $eventsToMove[] = [ 2435*1d05cddcSAtari911 'oldDate' => $dateKey, 2436*1d05cddcSAtari911 'newDate' => $newDate, 2437*1d05cddcSAtari911 'event' => $data[$dateKey][$key], 2438*1d05cddcSAtari911 'key' => $key 2439*1d05cddcSAtari911 ]; 2440*1d05cddcSAtari911 } 2441*1d05cddcSAtari911 2442*1d05cddcSAtari911 $count++; 2443*1d05cddcSAtari911 $modified = true; 2444*1d05cddcSAtari911 } 2445*1d05cddcSAtari911 } 2446*1d05cddcSAtari911 } 2447*1d05cddcSAtari911 2448*1d05cddcSAtari911 if ($modified) { 2449*1d05cddcSAtari911 file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); 2450*1d05cddcSAtari911 } 2451*1d05cddcSAtari911 } 2452*1d05cddcSAtari911 2453*1d05cddcSAtari911 // Handle interval changes by moving events to new dates 2454*1d05cddcSAtari911 if (!empty($eventsToMove)) { 2455*1d05cddcSAtari911 // Remove from old dates first 2456*1d05cddcSAtari911 foreach (glob($oldDataDir . '/*.json') as $file) { 2457*1d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 2458*1d05cddcSAtari911 if (!$data) continue; 2459*1d05cddcSAtari911 2460*1d05cddcSAtari911 $modified = false; 2461*1d05cddcSAtari911 foreach ($eventsToMove as $moveData) { 2462*1d05cddcSAtari911 $oldMonth = substr($moveData['oldDate'], 0, 7); 2463*1d05cddcSAtari911 $fileMonth = basename($file, '.json'); 2464*1d05cddcSAtari911 2465*1d05cddcSAtari911 if ($oldMonth === $fileMonth && isset($data[$moveData['oldDate']])) { 2466*1d05cddcSAtari911 foreach ($data[$moveData['oldDate']] as $k => $evt) { 2467*1d05cddcSAtari911 if ($evt['id'] === $moveData['event']['id']) { 2468*1d05cddcSAtari911 unset($data[$moveData['oldDate']][$k]); 2469*1d05cddcSAtari911 $data[$moveData['oldDate']] = array_values($data[$moveData['oldDate']]); 2470*1d05cddcSAtari911 $modified = true; 2471*1d05cddcSAtari911 } 2472*1d05cddcSAtari911 } 2473*1d05cddcSAtari911 } 2474*1d05cddcSAtari911 } 2475*1d05cddcSAtari911 2476*1d05cddcSAtari911 if ($modified) { 2477*1d05cddcSAtari911 file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); 2478*1d05cddcSAtari911 } 2479*1d05cddcSAtari911 } 2480*1d05cddcSAtari911 2481*1d05cddcSAtari911 // Add to new dates 2482*1d05cddcSAtari911 foreach ($eventsToMove as $moveData) { 2483*1d05cddcSAtari911 $newMonth = substr($moveData['newDate'], 0, 7); 2484*1d05cddcSAtari911 $targetDir = ($newNamespace === '') ? DOKU_INC . 'data/meta/calendar' : DOKU_INC . 'data/meta/' . $newNamespace . '/calendar'; 2485*1d05cddcSAtari911 2486*1d05cddcSAtari911 if (!is_dir($targetDir)) { 2487*1d05cddcSAtari911 mkdir($targetDir, 0755, true); 2488*1d05cddcSAtari911 } 2489*1d05cddcSAtari911 2490*1d05cddcSAtari911 $targetFile = $targetDir . '/' . $newMonth . '.json'; 2491*1d05cddcSAtari911 $targetData = file_exists($targetFile) ? json_decode(file_get_contents($targetFile), true) : []; 2492*1d05cddcSAtari911 2493*1d05cddcSAtari911 if (!isset($targetData[$moveData['newDate']])) { 2494*1d05cddcSAtari911 $targetData[$moveData['newDate']] = []; 2495*1d05cddcSAtari911 } 2496*1d05cddcSAtari911 2497*1d05cddcSAtari911 $targetData[$moveData['newDate']][] = $moveData['event']; 2498*1d05cddcSAtari911 file_put_contents($targetFile, json_encode($targetData, JSON_PRETTY_PRINT)); 2499*1d05cddcSAtari911 } 2500*1d05cddcSAtari911 } 2501*1d05cddcSAtari911 2502*1d05cddcSAtari911 // Handle namespace change without interval change 2503*1d05cddcSAtari911 if ($newNamespace !== $oldNamespace && empty($eventsToMove)) { 2504*1d05cddcSAtari911 foreach (glob($oldDataDir . '/*.json') as $file) { 2505*1d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 2506*1d05cddcSAtari911 if (!$data) continue; 2507*1d05cddcSAtari911 2508*1d05cddcSAtari911 $month = basename($file, '.json'); 2509*1d05cddcSAtari911 $targetDir = ($newNamespace === '') ? DOKU_INC . 'data/meta/calendar' : DOKU_INC . 'data/meta/' . $newNamespace . '/calendar'; 2510*1d05cddcSAtari911 2511*1d05cddcSAtari911 if (!is_dir($targetDir)) { 2512*1d05cddcSAtari911 mkdir($targetDir, 0755, true); 2513*1d05cddcSAtari911 } 2514*1d05cddcSAtari911 2515*1d05cddcSAtari911 $targetFile = $targetDir . '/' . $month . '.json'; 2516*1d05cddcSAtari911 $targetData = file_exists($targetFile) ? json_decode(file_get_contents($targetFile), true) : []; 2517*1d05cddcSAtari911 2518*1d05cddcSAtari911 $modified = false; 2519*1d05cddcSAtari911 foreach ($data as $dateKey => $events) { 2520*1d05cddcSAtari911 foreach ($events as $k => $event) { 2521*1d05cddcSAtari911 if (isset($event['namespace']) && $event['namespace'] === $newNamespace && 2522*1d05cddcSAtari911 strtolower(trim($event['title'])) === strtolower(trim($newTitle))) { 2523*1d05cddcSAtari911 // Move this event 2524*1d05cddcSAtari911 if (!isset($targetData[$dateKey])) { 2525*1d05cddcSAtari911 $targetData[$dateKey] = []; 2526*1d05cddcSAtari911 } 2527*1d05cddcSAtari911 $targetData[$dateKey][] = $event; 2528*1d05cddcSAtari911 unset($data[$dateKey][$k]); 2529*1d05cddcSAtari911 $data[$dateKey] = array_values($data[$dateKey]); 2530*1d05cddcSAtari911 $modified = true; 2531*1d05cddcSAtari911 } 2532*1d05cddcSAtari911 } 2533*1d05cddcSAtari911 } 2534*1d05cddcSAtari911 2535*1d05cddcSAtari911 if ($modified) { 2536*1d05cddcSAtari911 file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); 2537*1d05cddcSAtari911 file_put_contents($targetFile, json_encode($targetData, JSON_PRETTY_PRINT)); 2538*1d05cddcSAtari911 } 2539*1d05cddcSAtari911 } 2540*1d05cddcSAtari911 } 2541*1d05cddcSAtari911 } 2542*1d05cddcSAtari911 2543*1d05cddcSAtari911 $changes = []; 2544*1d05cddcSAtari911 if ($oldTitle !== $newTitle) $changes[] = "title"; 2545*1d05cddcSAtari911 if (!empty($startTime) || !empty($endTime)) $changes[] = "time"; 2546*1d05cddcSAtari911 if ($interval > 0) $changes[] = "interval"; 2547*1d05cddcSAtari911 if ($newNamespace !== $oldNamespace) $changes[] = "namespace"; 2548*1d05cddcSAtari911 2549*1d05cddcSAtari911 $changeStr = !empty($changes) ? " (" . implode(", ", $changes) . ")" : ""; 2550*1d05cddcSAtari911 $this->redirect("Updated $count occurrences of recurring event$changeStr", 'success', 'manage'); 2551*1d05cddcSAtari911 } 2552*1d05cddcSAtari911 2553*1d05cddcSAtari911 private function moveEvents() { 2554*1d05cddcSAtari911 global $INPUT; 2555*1d05cddcSAtari911 2556*1d05cddcSAtari911 $events = $INPUT->arr('events'); 2557*1d05cddcSAtari911 $targetNamespace = $INPUT->str('target_namespace'); 2558*1d05cddcSAtari911 2559*1d05cddcSAtari911 if (empty($events)) { 2560*1d05cddcSAtari911 $this->redirect('No events selected', 'error', 'manage'); 2561*1d05cddcSAtari911 } 2562*1d05cddcSAtari911 2563*1d05cddcSAtari911 $moved = 0; 2564*1d05cddcSAtari911 2565*1d05cddcSAtari911 foreach ($events as $eventData) { 2566*1d05cddcSAtari911 list($id, $namespace, $date, $month) = explode('|', $eventData); 2567*1d05cddcSAtari911 2568*1d05cddcSAtari911 // Determine old file path 2569*1d05cddcSAtari911 if ($namespace === '') { 2570*1d05cddcSAtari911 $oldFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json'; 2571*1d05cddcSAtari911 } else { 2572*1d05cddcSAtari911 $oldFile = DOKU_INC . 'data/meta/' . $namespace . '/calendar/' . $month . '.json'; 2573*1d05cddcSAtari911 } 2574*1d05cddcSAtari911 2575*1d05cddcSAtari911 if (!file_exists($oldFile)) continue; 2576*1d05cddcSAtari911 2577*1d05cddcSAtari911 $oldData = json_decode(file_get_contents($oldFile), true); 2578*1d05cddcSAtari911 if (!$oldData) continue; 2579*1d05cddcSAtari911 2580*1d05cddcSAtari911 // Find and remove event from old file 2581*1d05cddcSAtari911 $event = null; 2582*1d05cddcSAtari911 foreach ($oldData[$date] as $key => $evt) { 2583*1d05cddcSAtari911 if ($evt['id'] === $id) { 2584*1d05cddcSAtari911 $event = $evt; 2585*1d05cddcSAtari911 unset($oldData[$date][$key]); 2586*1d05cddcSAtari911 $oldData[$date] = array_values($oldData[$date]); 2587*1d05cddcSAtari911 break; 2588*1d05cddcSAtari911 } 2589*1d05cddcSAtari911 } 2590*1d05cddcSAtari911 2591*1d05cddcSAtari911 if (!$event) continue; 2592*1d05cddcSAtari911 2593*1d05cddcSAtari911 // Save old file 2594*1d05cddcSAtari911 file_put_contents($oldFile, json_encode($oldData, JSON_PRETTY_PRINT)); 2595*1d05cddcSAtari911 2596*1d05cddcSAtari911 // Update event namespace 2597*1d05cddcSAtari911 $event['namespace'] = $targetNamespace; 2598*1d05cddcSAtari911 2599*1d05cddcSAtari911 // Determine new file path 2600*1d05cddcSAtari911 if ($targetNamespace === '') { 2601*1d05cddcSAtari911 $newFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json'; 2602*1d05cddcSAtari911 $newDir = dirname($newFile); 2603*1d05cddcSAtari911 } else { 2604*1d05cddcSAtari911 $newFile = DOKU_INC . 'data/meta/' . $targetNamespace . '/calendar/' . $month . '.json'; 2605*1d05cddcSAtari911 $newDir = dirname($newFile); 2606*1d05cddcSAtari911 } 2607*1d05cddcSAtari911 2608*1d05cddcSAtari911 if (!is_dir($newDir)) { 2609*1d05cddcSAtari911 mkdir($newDir, 0755, true); 2610*1d05cddcSAtari911 } 2611*1d05cddcSAtari911 2612*1d05cddcSAtari911 $newData = []; 2613*1d05cddcSAtari911 if (file_exists($newFile)) { 2614*1d05cddcSAtari911 $newData = json_decode(file_get_contents($newFile), true) ?: []; 2615*1d05cddcSAtari911 } 2616*1d05cddcSAtari911 2617*1d05cddcSAtari911 if (!isset($newData[$date])) { 2618*1d05cddcSAtari911 $newData[$date] = []; 2619*1d05cddcSAtari911 } 2620*1d05cddcSAtari911 $newData[$date][] = $event; 2621*1d05cddcSAtari911 2622*1d05cddcSAtari911 file_put_contents($newFile, json_encode($newData, JSON_PRETTY_PRINT)); 2623*1d05cddcSAtari911 $moved++; 2624*1d05cddcSAtari911 } 2625*1d05cddcSAtari911 2626*1d05cddcSAtari911 $displayTarget = $targetNamespace ?: '(default)'; 2627*1d05cddcSAtari911 $this->redirect("Moved $moved event(s) to namespace: " . $displayTarget, 'success', 'manage'); 2628*1d05cddcSAtari911 } 2629*1d05cddcSAtari911 2630*1d05cddcSAtari911 private function moveSingleEvent() { 2631*1d05cddcSAtari911 global $INPUT; 2632*1d05cddcSAtari911 2633*1d05cddcSAtari911 $eventData = $INPUT->str('event'); 2634*1d05cddcSAtari911 $targetNamespace = $INPUT->str('target_namespace'); 2635*1d05cddcSAtari911 2636*1d05cddcSAtari911 list($id, $namespace, $date, $month) = explode('|', $eventData); 2637*1d05cddcSAtari911 2638*1d05cddcSAtari911 // Determine old file path 2639*1d05cddcSAtari911 if ($namespace === '') { 2640*1d05cddcSAtari911 $oldFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json'; 2641*1d05cddcSAtari911 } else { 2642*1d05cddcSAtari911 $oldFile = DOKU_INC . 'data/meta/' . $namespace . '/calendar/' . $month . '.json'; 2643*1d05cddcSAtari911 } 2644*1d05cddcSAtari911 2645*1d05cddcSAtari911 if (!file_exists($oldFile)) { 2646*1d05cddcSAtari911 $this->redirect('Event file not found', 'error', 'manage'); 2647*1d05cddcSAtari911 } 2648*1d05cddcSAtari911 2649*1d05cddcSAtari911 $oldData = json_decode(file_get_contents($oldFile), true); 2650*1d05cddcSAtari911 if (!$oldData) { 2651*1d05cddcSAtari911 $this->redirect('Could not read event file', 'error', 'manage'); 2652*1d05cddcSAtari911 } 2653*1d05cddcSAtari911 2654*1d05cddcSAtari911 // Find and remove event from old file 2655*1d05cddcSAtari911 $event = null; 2656*1d05cddcSAtari911 foreach ($oldData[$date] as $key => $evt) { 2657*1d05cddcSAtari911 if ($evt['id'] === $id) { 2658*1d05cddcSAtari911 $event = $evt; 2659*1d05cddcSAtari911 unset($oldData[$date][$key]); 2660*1d05cddcSAtari911 $oldData[$date] = array_values($oldData[$date]); 2661*1d05cddcSAtari911 break; 2662*1d05cddcSAtari911 } 2663*1d05cddcSAtari911 } 2664*1d05cddcSAtari911 2665*1d05cddcSAtari911 if (!$event) { 2666*1d05cddcSAtari911 $this->redirect('Event not found', 'error', 'manage'); 2667*1d05cddcSAtari911 } 2668*1d05cddcSAtari911 2669*1d05cddcSAtari911 // Save old file 2670*1d05cddcSAtari911 file_put_contents($oldFile, json_encode($oldData, JSON_PRETTY_PRINT)); 2671*1d05cddcSAtari911 2672*1d05cddcSAtari911 // Update event namespace 2673*1d05cddcSAtari911 $event['namespace'] = $targetNamespace; 2674*1d05cddcSAtari911 2675*1d05cddcSAtari911 // Determine new file path 2676*1d05cddcSAtari911 if ($targetNamespace === '') { 2677*1d05cddcSAtari911 $newFile = DOKU_INC . 'data/meta/calendar/' . $month . '.json'; 2678*1d05cddcSAtari911 $newDir = dirname($newFile); 2679*1d05cddcSAtari911 } else { 2680*1d05cddcSAtari911 $newFile = DOKU_INC . 'data/meta/' . $targetNamespace . '/calendar/' . $month . '.json'; 2681*1d05cddcSAtari911 $newDir = dirname($newFile); 2682*1d05cddcSAtari911 } 2683*1d05cddcSAtari911 2684*1d05cddcSAtari911 if (!is_dir($newDir)) { 2685*1d05cddcSAtari911 mkdir($newDir, 0755, true); 2686*1d05cddcSAtari911 } 2687*1d05cddcSAtari911 2688*1d05cddcSAtari911 $newData = []; 2689*1d05cddcSAtari911 if (file_exists($newFile)) { 2690*1d05cddcSAtari911 $newData = json_decode(file_get_contents($newFile), true) ?: []; 2691*1d05cddcSAtari911 } 2692*1d05cddcSAtari911 2693*1d05cddcSAtari911 if (!isset($newData[$date])) { 2694*1d05cddcSAtari911 $newData[$date] = []; 2695*1d05cddcSAtari911 } 2696*1d05cddcSAtari911 $newData[$date][] = $event; 2697*1d05cddcSAtari911 2698*1d05cddcSAtari911 file_put_contents($newFile, json_encode($newData, JSON_PRETTY_PRINT)); 2699*1d05cddcSAtari911 2700*1d05cddcSAtari911 $displayTarget = $targetNamespace ?: '(default)'; 2701*1d05cddcSAtari911 $this->redirect('Moved "' . $event['title'] . '" to ' . $displayTarget, 'success', 'manage'); 2702*1d05cddcSAtari911 } 2703*1d05cddcSAtari911 2704*1d05cddcSAtari911 private function createNamespace() { 2705*1d05cddcSAtari911 global $INPUT; 2706*1d05cddcSAtari911 2707*1d05cddcSAtari911 $namespaceName = $INPUT->str('namespace_name'); 2708*1d05cddcSAtari911 2709*1d05cddcSAtari911 // Validate namespace name 2710*1d05cddcSAtari911 if (empty($namespaceName)) { 2711*1d05cddcSAtari911 $this->redirect('Namespace name cannot be empty', 'error', 'manage'); 2712*1d05cddcSAtari911 } 2713*1d05cddcSAtari911 2714*1d05cddcSAtari911 if (!preg_match('/^[a-zA-Z0-9_:-]+$/', $namespaceName)) { 2715*1d05cddcSAtari911 $this->redirect('Invalid namespace name. Use only letters, numbers, underscore, hyphen, and colon.', 'error', 'manage'); 2716*1d05cddcSAtari911 } 2717*1d05cddcSAtari911 2718*1d05cddcSAtari911 // Convert namespace to directory path 2719*1d05cddcSAtari911 $namespacePath = str_replace(':', '/', $namespaceName); 2720*1d05cddcSAtari911 $calendarDir = DOKU_INC . 'data/meta/' . $namespacePath . '/calendar'; 2721*1d05cddcSAtari911 2722*1d05cddcSAtari911 // Check if already exists 2723*1d05cddcSAtari911 if (is_dir($calendarDir)) { 2724*1d05cddcSAtari911 // Check if it has any JSON files 2725*1d05cddcSAtari911 $hasFiles = !empty(glob($calendarDir . '/*.json')); 2726*1d05cddcSAtari911 if ($hasFiles) { 2727*1d05cddcSAtari911 $this->redirect("Namespace '$namespaceName' already exists with events", 'info', 'manage'); 2728*1d05cddcSAtari911 } 2729*1d05cddcSAtari911 // If directory exists but empty, continue to create placeholder 2730*1d05cddcSAtari911 } 2731*1d05cddcSAtari911 2732*1d05cddcSAtari911 // Create the directory 2733*1d05cddcSAtari911 if (!is_dir($calendarDir)) { 2734*1d05cddcSAtari911 if (!mkdir($calendarDir, 0755, true)) { 2735*1d05cddcSAtari911 $this->redirect("Failed to create namespace directory", 'error', 'manage'); 2736*1d05cddcSAtari911 } 2737*1d05cddcSAtari911 } 2738*1d05cddcSAtari911 2739*1d05cddcSAtari911 // Create a placeholder JSON file with an empty structure for current month 2740*1d05cddcSAtari911 // This ensures the namespace appears in the list immediately 2741*1d05cddcSAtari911 $currentMonth = date('Y-m'); 2742*1d05cddcSAtari911 $placeholderFile = $calendarDir . '/' . $currentMonth . '.json'; 2743*1d05cddcSAtari911 2744*1d05cddcSAtari911 if (!file_exists($placeholderFile)) { 2745*1d05cddcSAtari911 file_put_contents($placeholderFile, json_encode([], JSON_PRETTY_PRINT)); 2746*1d05cddcSAtari911 } 2747*1d05cddcSAtari911 2748*1d05cddcSAtari911 $this->redirect("Created namespace: $namespaceName", 'success', 'manage'); 2749*1d05cddcSAtari911 } 2750*1d05cddcSAtari911 2751*1d05cddcSAtari911 private function deleteNamespace() { 2752*1d05cddcSAtari911 global $INPUT; 2753*1d05cddcSAtari911 2754*1d05cddcSAtari911 $namespace = $INPUT->str('namespace'); 2755*1d05cddcSAtari911 2756*1d05cddcSAtari911 // Convert namespace to directory path (e.g., "work:projects" → "work/projects") 2757*1d05cddcSAtari911 $namespacePath = str_replace(':', '/', $namespace); 2758*1d05cddcSAtari911 2759*1d05cddcSAtari911 // Determine calendar directory 2760*1d05cddcSAtari911 if ($namespace === '') { 2761*1d05cddcSAtari911 $calendarDir = DOKU_INC . 'data/meta/calendar'; 2762*1d05cddcSAtari911 $namespaceDir = null; // Don't delete root 2763*1d05cddcSAtari911 } else { 2764*1d05cddcSAtari911 $calendarDir = DOKU_INC . 'data/meta/' . $namespacePath . '/calendar'; 2765*1d05cddcSAtari911 $namespaceDir = DOKU_INC . 'data/meta/' . $namespacePath; 2766*1d05cddcSAtari911 } 2767*1d05cddcSAtari911 2768*1d05cddcSAtari911 // Check if directory exists 2769*1d05cddcSAtari911 if (!is_dir($calendarDir)) { 2770*1d05cddcSAtari911 // Maybe it was never created or already deleted 2771*1d05cddcSAtari911 $this->redirect("Namespace directory not found: $calendarDir", 'error', 'manage'); 2772*1d05cddcSAtari911 return; 2773*1d05cddcSAtari911 } 2774*1d05cddcSAtari911 2775*1d05cddcSAtari911 $filesDeleted = 0; 2776*1d05cddcSAtari911 $eventsDeleted = 0; 2777*1d05cddcSAtari911 2778*1d05cddcSAtari911 // Delete all calendar JSON files (including empty ones) 2779*1d05cddcSAtari911 foreach (glob($calendarDir . '/*.json') as $file) { 2780*1d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 2781*1d05cddcSAtari911 if ($data) { 2782*1d05cddcSAtari911 foreach ($data as $events) { 2783*1d05cddcSAtari911 $eventsDeleted += count($events); 2784*1d05cddcSAtari911 } 2785*1d05cddcSAtari911 } 2786*1d05cddcSAtari911 unlink($file); 2787*1d05cddcSAtari911 $filesDeleted++; 2788*1d05cddcSAtari911 } 2789*1d05cddcSAtari911 2790*1d05cddcSAtari911 // Delete any other files in calendar directory 2791*1d05cddcSAtari911 foreach (glob($calendarDir . '/*') as $file) { 2792*1d05cddcSAtari911 if (is_file($file)) { 2793*1d05cddcSAtari911 unlink($file); 2794*1d05cddcSAtari911 } 2795*1d05cddcSAtari911 } 2796*1d05cddcSAtari911 2797*1d05cddcSAtari911 // Remove the calendar directory 2798*1d05cddcSAtari911 if ($namespace !== '') { 2799*1d05cddcSAtari911 @rmdir($calendarDir); 2800*1d05cddcSAtari911 2801*1d05cddcSAtari911 // Try to remove parent directories if they're empty 2802*1d05cddcSAtari911 // This handles nested namespaces like work:projects:alpha 2803*1d05cddcSAtari911 $currentDir = dirname($calendarDir); 2804*1d05cddcSAtari911 $metaDir = DOKU_INC . 'data/meta'; 2805*1d05cddcSAtari911 2806*1d05cddcSAtari911 while ($currentDir !== $metaDir && $currentDir !== dirname($metaDir)) { 2807*1d05cddcSAtari911 if (is_dir($currentDir)) { 2808*1d05cddcSAtari911 // Check if directory is empty 2809*1d05cddcSAtari911 $contents = scandir($currentDir); 2810*1d05cddcSAtari911 $isEmpty = count($contents) === 2; // Only . and .. 2811*1d05cddcSAtari911 2812*1d05cddcSAtari911 if ($isEmpty) { 2813*1d05cddcSAtari911 @rmdir($currentDir); 2814*1d05cddcSAtari911 $currentDir = dirname($currentDir); 2815*1d05cddcSAtari911 } else { 2816*1d05cddcSAtari911 break; // Directory not empty, stop 2817*1d05cddcSAtari911 } 2818*1d05cddcSAtari911 } else { 2819*1d05cddcSAtari911 break; 2820*1d05cddcSAtari911 } 2821*1d05cddcSAtari911 } 2822*1d05cddcSAtari911 } 2823*1d05cddcSAtari911 2824*1d05cddcSAtari911 $displayName = $namespace ?: '(default)'; 2825*1d05cddcSAtari911 $this->redirect("Deleted namespace '$displayName': $eventsDeleted events in $filesDeleted files", 'success', 'manage'); 2826*1d05cddcSAtari911 } 2827*1d05cddcSAtari911 2828*1d05cddcSAtari911 private function deleteSelectedEvents() { 2829*1d05cddcSAtari911 global $INPUT; 2830*1d05cddcSAtari911 2831*1d05cddcSAtari911 $events = $INPUT->arr('events'); 2832*1d05cddcSAtari911 2833*1d05cddcSAtari911 if (empty($events)) { 2834*1d05cddcSAtari911 $this->redirect('No events selected', 'error', 'manage'); 2835*1d05cddcSAtari911 } 2836*1d05cddcSAtari911 2837*1d05cddcSAtari911 $deletedCount = 0; 2838*1d05cddcSAtari911 2839*1d05cddcSAtari911 foreach ($events as $eventData) { 2840*1d05cddcSAtari911 list($id, $namespace, $date, $month) = explode('|', $eventData); 2841*1d05cddcSAtari911 2842*1d05cddcSAtari911 // Determine file path 2843*1d05cddcSAtari911 if ($namespace === '') { 2844*1d05cddcSAtari911 $file = DOKU_INC . 'data/meta/calendar/' . $month . '.json'; 2845*1d05cddcSAtari911 } else { 2846*1d05cddcSAtari911 $file = DOKU_INC . 'data/meta/' . $namespace . '/calendar/' . $month . '.json'; 2847*1d05cddcSAtari911 } 2848*1d05cddcSAtari911 2849*1d05cddcSAtari911 if (!file_exists($file)) continue; 2850*1d05cddcSAtari911 2851*1d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 2852*1d05cddcSAtari911 if (!$data) continue; 2853*1d05cddcSAtari911 2854*1d05cddcSAtari911 // Find and remove event 2855*1d05cddcSAtari911 if (isset($data[$date])) { 2856*1d05cddcSAtari911 foreach ($data[$date] as $key => $evt) { 2857*1d05cddcSAtari911 if ($evt['id'] === $id) { 2858*1d05cddcSAtari911 unset($data[$date][$key]); 2859*1d05cddcSAtari911 $data[$date] = array_values($data[$date]); 2860*1d05cddcSAtari911 $deletedCount++; 2861*1d05cddcSAtari911 break; 2862*1d05cddcSAtari911 } 2863*1d05cddcSAtari911 } 2864*1d05cddcSAtari911 2865*1d05cddcSAtari911 // Remove empty date arrays 2866*1d05cddcSAtari911 if (empty($data[$date])) { 2867*1d05cddcSAtari911 unset($data[$date]); 2868*1d05cddcSAtari911 } 2869*1d05cddcSAtari911 2870*1d05cddcSAtari911 // Save file 2871*1d05cddcSAtari911 file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); 2872*1d05cddcSAtari911 } 2873*1d05cddcSAtari911 } 2874*1d05cddcSAtari911 2875*1d05cddcSAtari911 $this->redirect("Deleted $deletedCount event(s)", 'success', 'manage'); 2876*1d05cddcSAtari911 } 2877*1d05cddcSAtari911 2878*1d05cddcSAtari911 private function getCronStatus() { 2879*1d05cddcSAtari911 // Try to read root's crontab first, then current user 2880*1d05cddcSAtari911 $output = []; 2881*1d05cddcSAtari911 exec('sudo crontab -l 2>/dev/null', $output); 2882*1d05cddcSAtari911 2883*1d05cddcSAtari911 // If sudo doesn't work, try current user 2884*1d05cddcSAtari911 if (empty($output)) { 2885*1d05cddcSAtari911 exec('crontab -l 2>/dev/null', $output); 2886*1d05cddcSAtari911 } 2887*1d05cddcSAtari911 2888*1d05cddcSAtari911 // Also check system crontab files 2889*1d05cddcSAtari911 if (empty($output)) { 2890*1d05cddcSAtari911 $cronFiles = [ 2891*1d05cddcSAtari911 '/etc/crontab', 2892*1d05cddcSAtari911 '/etc/cron.d/calendar', 2893*1d05cddcSAtari911 '/var/spool/cron/root', 2894*1d05cddcSAtari911 '/var/spool/cron/crontabs/root' 2895*1d05cddcSAtari911 ]; 2896*1d05cddcSAtari911 2897*1d05cddcSAtari911 foreach ($cronFiles as $file) { 2898*1d05cddcSAtari911 if (file_exists($file) && is_readable($file)) { 2899*1d05cddcSAtari911 $content = file_get_contents($file); 2900*1d05cddcSAtari911 $output = explode("\n", $content); 2901*1d05cddcSAtari911 break; 2902*1d05cddcSAtari911 } 2903*1d05cddcSAtari911 } 2904*1d05cddcSAtari911 } 2905*1d05cddcSAtari911 2906*1d05cddcSAtari911 // Look for sync_outlook.php in the cron entries 2907*1d05cddcSAtari911 foreach ($output as $line) { 2908*1d05cddcSAtari911 $line = trim($line); 2909*1d05cddcSAtari911 2910*1d05cddcSAtari911 // Skip empty lines and comments 2911*1d05cddcSAtari911 if (empty($line) || $line[0] === '#') continue; 2912*1d05cddcSAtari911 2913*1d05cddcSAtari911 // Check if line contains sync_outlook.php 2914*1d05cddcSAtari911 if (strpos($line, 'sync_outlook.php') !== false) { 2915*1d05cddcSAtari911 // Parse cron expression 2916*1d05cddcSAtari911 // Format: minute hour day month weekday [user] command 2917*1d05cddcSAtari911 $parts = preg_split('/\s+/', $line, 7); 2918*1d05cddcSAtari911 2919*1d05cddcSAtari911 if (count($parts) >= 5) { 2920*1d05cddcSAtari911 // Determine if this has a user field (system crontab format) 2921*1d05cddcSAtari911 $hasUser = (count($parts) >= 6 && !preg_match('/^[\/\*]/', $parts[5])); 2922*1d05cddcSAtari911 $offset = $hasUser ? 1 : 0; 2923*1d05cddcSAtari911 2924*1d05cddcSAtari911 $frequency = $this->parseCronExpression($parts[0], $parts[1], $parts[2], $parts[3], $parts[4]); 2925*1d05cddcSAtari911 return [ 2926*1d05cddcSAtari911 'active' => true, 2927*1d05cddcSAtari911 'frequency' => $frequency, 2928*1d05cddcSAtari911 'expression' => implode(' ', array_slice($parts, 0, 5)), 2929*1d05cddcSAtari911 'full_line' => $line 2930*1d05cddcSAtari911 ]; 2931*1d05cddcSAtari911 } 2932*1d05cddcSAtari911 } 2933*1d05cddcSAtari911 } 2934*1d05cddcSAtari911 2935*1d05cddcSAtari911 return ['active' => false, 'frequency' => '', 'expression' => '', 'full_line' => '']; 2936*1d05cddcSAtari911 } 2937*1d05cddcSAtari911 2938*1d05cddcSAtari911 private function parseCronExpression($minute, $hour, $day, $month, $weekday) { 2939*1d05cddcSAtari911 // Parse minute field 2940*1d05cddcSAtari911 if ($minute === '*') { 2941*1d05cddcSAtari911 return 'Runs every minute'; 2942*1d05cddcSAtari911 } elseif (strpos($minute, '*/') === 0) { 2943*1d05cddcSAtari911 $interval = substr($minute, 2); 2944*1d05cddcSAtari911 if ($interval == 1) { 2945*1d05cddcSAtari911 return 'Runs every minute'; 2946*1d05cddcSAtari911 } elseif ($interval == 5) { 2947*1d05cddcSAtari911 return 'Runs every 5 minutes'; 2948*1d05cddcSAtari911 } elseif ($interval == 8) { 2949*1d05cddcSAtari911 return 'Runs every 8 minutes'; 2950*1d05cddcSAtari911 } elseif ($interval == 10) { 2951*1d05cddcSAtari911 return 'Runs every 10 minutes'; 2952*1d05cddcSAtari911 } elseif ($interval == 15) { 2953*1d05cddcSAtari911 return 'Runs every 15 minutes'; 2954*1d05cddcSAtari911 } elseif ($interval == 30) { 2955*1d05cddcSAtari911 return 'Runs every 30 minutes'; 2956*1d05cddcSAtari911 } else { 2957*1d05cddcSAtari911 return "Runs every $interval minutes"; 2958*1d05cddcSAtari911 } 2959*1d05cddcSAtari911 } 2960*1d05cddcSAtari911 2961*1d05cddcSAtari911 // Parse hour field 2962*1d05cddcSAtari911 if ($hour === '*' && $minute !== '*') { 2963*1d05cddcSAtari911 return 'Runs hourly'; 2964*1d05cddcSAtari911 } elseif (strpos($hour, '*/') === 0 && $minute !== '*') { 2965*1d05cddcSAtari911 $interval = substr($hour, 2); 2966*1d05cddcSAtari911 if ($interval == 1) { 2967*1d05cddcSAtari911 return 'Runs every hour'; 2968*1d05cddcSAtari911 } else { 2969*1d05cddcSAtari911 return "Runs every $interval hours"; 2970*1d05cddcSAtari911 } 2971*1d05cddcSAtari911 } 2972*1d05cddcSAtari911 2973*1d05cddcSAtari911 // Parse day field 2974*1d05cddcSAtari911 if ($day === '*' && $hour !== '*' && $minute !== '*') { 2975*1d05cddcSAtari911 return 'Runs daily'; 2976*1d05cddcSAtari911 } 2977*1d05cddcSAtari911 2978*1d05cddcSAtari911 // Default 2979*1d05cddcSAtari911 return 'Custom schedule'; 2980*1d05cddcSAtari911 } 2981*1d05cddcSAtari911 2982*1d05cddcSAtari911 private function runSync() { 2983*1d05cddcSAtari911 global $INPUT; 2984*1d05cddcSAtari911 2985*1d05cddcSAtari911 if ($INPUT->str('call') === 'ajax') { 2986*1d05cddcSAtari911 header('Content-Type: application/json'); 2987*1d05cddcSAtari911 2988*1d05cddcSAtari911 $syncScript = DOKU_PLUGIN . 'calendar/sync_outlook.php'; 2989*1d05cddcSAtari911 $abortFile = DOKU_PLUGIN . 'calendar/.sync_abort'; 2990*1d05cddcSAtari911 2991*1d05cddcSAtari911 // Remove any existing abort flag 2992*1d05cddcSAtari911 if (file_exists($abortFile)) { 2993*1d05cddcSAtari911 @unlink($abortFile); 2994*1d05cddcSAtari911 } 2995*1d05cddcSAtari911 2996*1d05cddcSAtari911 if (!file_exists($syncScript)) { 2997*1d05cddcSAtari911 echo json_encode(['success' => false, 'message' => 'Sync script not found at: ' . $syncScript]); 2998*1d05cddcSAtari911 exit; 2999*1d05cddcSAtari911 } 3000*1d05cddcSAtari911 3001*1d05cddcSAtari911 // Change to plugin directory 3002*1d05cddcSAtari911 $pluginDir = DOKU_PLUGIN . 'calendar'; 3003*1d05cddcSAtari911 $logFile = $pluginDir . '/sync.log'; 3004*1d05cddcSAtari911 3005*1d05cddcSAtari911 // Ensure log file exists and is writable 3006*1d05cddcSAtari911 if (!file_exists($logFile)) { 3007*1d05cddcSAtari911 @touch($logFile); 3008*1d05cddcSAtari911 @chmod($logFile, 0666); 3009*1d05cddcSAtari911 } 3010*1d05cddcSAtari911 3011*1d05cddcSAtari911 // Try to log the execution (but don't fail if we can't) 3012*1d05cddcSAtari911 if (is_writable($logFile)) { 3013*1d05cddcSAtari911 $tz = new DateTimeZone('America/Los_Angeles'); 3014*1d05cddcSAtari911 $now = new DateTime('now', $tz); 3015*1d05cddcSAtari911 $timestamp = $now->format('Y-m-d H:i:s'); 3016*1d05cddcSAtari911 @file_put_contents($logFile, "[$timestamp] [ADMIN] Manual sync triggered via admin panel\n", FILE_APPEND); 3017*1d05cddcSAtari911 } 3018*1d05cddcSAtari911 3019*1d05cddcSAtari911 // Find PHP binary - try multiple methods 3020*1d05cddcSAtari911 $phpPath = $this->findPhpBinary(); 3021*1d05cddcSAtari911 3022*1d05cddcSAtari911 // Build command 3023*1d05cddcSAtari911 $command = sprintf( 3024*1d05cddcSAtari911 'cd %s && %s %s 2>&1', 3025*1d05cddcSAtari911 escapeshellarg($pluginDir), 3026*1d05cddcSAtari911 $phpPath, 3027*1d05cddcSAtari911 escapeshellarg(basename($syncScript)) 3028*1d05cddcSAtari911 ); 3029*1d05cddcSAtari911 3030*1d05cddcSAtari911 // Execute and capture output 3031*1d05cddcSAtari911 $output = []; 3032*1d05cddcSAtari911 $returnCode = 0; 3033*1d05cddcSAtari911 exec($command, $output, $returnCode); 3034*1d05cddcSAtari911 3035*1d05cddcSAtari911 // Check if sync completed 3036*1d05cddcSAtari911 $lastLines = array_slice($output, -5); 3037*1d05cddcSAtari911 $completed = false; 3038*1d05cddcSAtari911 foreach ($lastLines as $line) { 3039*1d05cddcSAtari911 if (strpos($line, 'Sync Complete') !== false || strpos($line, 'Created:') !== false) { 3040*1d05cddcSAtari911 $completed = true; 3041*1d05cddcSAtari911 break; 3042*1d05cddcSAtari911 } 3043*1d05cddcSAtari911 } 3044*1d05cddcSAtari911 3045*1d05cddcSAtari911 if ($returnCode === 0 && $completed) { 3046*1d05cddcSAtari911 echo json_encode([ 3047*1d05cddcSAtari911 'success' => true, 3048*1d05cddcSAtari911 'message' => 'Sync completed successfully! Check log below.' 3049*1d05cddcSAtari911 ]); 3050*1d05cddcSAtari911 } elseif ($returnCode === 0) { 3051*1d05cddcSAtari911 echo json_encode([ 3052*1d05cddcSAtari911 'success' => true, 3053*1d05cddcSAtari911 'message' => 'Sync started. Check log below for progress.' 3054*1d05cddcSAtari911 ]); 3055*1d05cddcSAtari911 } else { 3056*1d05cddcSAtari911 // Include output for debugging 3057*1d05cddcSAtari911 $errorMsg = 'Sync failed with error code: ' . $returnCode; 3058*1d05cddcSAtari911 if (!empty($output)) { 3059*1d05cddcSAtari911 $errorMsg .= ' | ' . implode(' | ', array_slice($output, -3)); 3060*1d05cddcSAtari911 } 3061*1d05cddcSAtari911 echo json_encode([ 3062*1d05cddcSAtari911 'success' => false, 3063*1d05cddcSAtari911 'message' => $errorMsg 3064*1d05cddcSAtari911 ]); 3065*1d05cddcSAtari911 } 3066*1d05cddcSAtari911 exit; 3067*1d05cddcSAtari911 } 3068*1d05cddcSAtari911 } 3069*1d05cddcSAtari911 3070*1d05cddcSAtari911 private function stopSync() { 3071*1d05cddcSAtari911 global $INPUT; 3072*1d05cddcSAtari911 3073*1d05cddcSAtari911 if ($INPUT->str('call') === 'ajax') { 3074*1d05cddcSAtari911 header('Content-Type: application/json'); 3075*1d05cddcSAtari911 3076*1d05cddcSAtari911 $abortFile = DOKU_PLUGIN . 'calendar/.sync_abort'; 3077*1d05cddcSAtari911 3078*1d05cddcSAtari911 // Create abort flag file 3079*1d05cddcSAtari911 if (file_put_contents($abortFile, date('Y-m-d H:i:s')) !== false) { 3080*1d05cddcSAtari911 echo json_encode([ 3081*1d05cddcSAtari911 'success' => true, 3082*1d05cddcSAtari911 'message' => 'Stop signal sent to sync process' 3083*1d05cddcSAtari911 ]); 3084*1d05cddcSAtari911 } else { 3085*1d05cddcSAtari911 echo json_encode([ 3086*1d05cddcSAtari911 'success' => false, 3087*1d05cddcSAtari911 'message' => 'Failed to create abort flag' 3088*1d05cddcSAtari911 ]); 3089*1d05cddcSAtari911 } 3090*1d05cddcSAtari911 exit; 3091*1d05cddcSAtari911 } 3092*1d05cddcSAtari911 } 3093*1d05cddcSAtari911 3094*1d05cddcSAtari911 private function uploadUpdate() { 3095*1d05cddcSAtari911 if (!isset($_FILES['plugin_zip']) || $_FILES['plugin_zip']['error'] !== UPLOAD_ERR_OK) { 3096*1d05cddcSAtari911 $this->redirect('Upload failed: ' . ($_FILES['plugin_zip']['error'] ?? 'No file uploaded'), 'error', 'update'); 3097*1d05cddcSAtari911 return; 3098*1d05cddcSAtari911 } 3099*1d05cddcSAtari911 3100*1d05cddcSAtari911 $uploadedFile = $_FILES['plugin_zip']['tmp_name']; 3101*1d05cddcSAtari911 $pluginDir = DOKU_PLUGIN . 'calendar/'; 3102*1d05cddcSAtari911 $backupFirst = isset($_POST['backup_first']); 3103*1d05cddcSAtari911 3104*1d05cddcSAtari911 // Check if plugin directory is writable 3105*1d05cddcSAtari911 if (!is_writable($pluginDir)) { 3106*1d05cddcSAtari911 $this->redirect('Plugin directory is not writable. Please check permissions: ' . $pluginDir, 'error', 'update'); 3107*1d05cddcSAtari911 return; 3108*1d05cddcSAtari911 } 3109*1d05cddcSAtari911 3110*1d05cddcSAtari911 // Check if parent directory is writable (for backup and temp files) 3111*1d05cddcSAtari911 if (!is_writable(DOKU_PLUGIN)) { 3112*1d05cddcSAtari911 $this->redirect('Plugin parent directory is not writable. Please check permissions: ' . DOKU_PLUGIN, 'error', 'update'); 3113*1d05cddcSAtari911 return; 3114*1d05cddcSAtari911 } 3115*1d05cddcSAtari911 3116*1d05cddcSAtari911 // Verify it's a ZIP file 3117*1d05cddcSAtari911 $finfo = finfo_open(FILEINFO_MIME_TYPE); 3118*1d05cddcSAtari911 $mimeType = finfo_file($finfo, $uploadedFile); 3119*1d05cddcSAtari911 finfo_close($finfo); 3120*1d05cddcSAtari911 3121*1d05cddcSAtari911 if ($mimeType !== 'application/zip' && $mimeType !== 'application/x-zip-compressed') { 3122*1d05cddcSAtari911 $this->redirect('Invalid file type. Please upload a ZIP file.', 'error', 'update'); 3123*1d05cddcSAtari911 return; 3124*1d05cddcSAtari911 } 3125*1d05cddcSAtari911 3126*1d05cddcSAtari911 // Create backup if requested 3127*1d05cddcSAtari911 if ($backupFirst) { 3128*1d05cddcSAtari911 // Get current version 3129*1d05cddcSAtari911 $pluginInfo = $pluginDir . 'plugin.info.txt'; 3130*1d05cddcSAtari911 $version = 'unknown'; 3131*1d05cddcSAtari911 if (file_exists($pluginInfo)) { 3132*1d05cddcSAtari911 $info = confToHash($pluginInfo); 3133*1d05cddcSAtari911 $version = $info['version'] ?? ($info['date'] ?? 'unknown'); 3134*1d05cddcSAtari911 } 3135*1d05cddcSAtari911 3136*1d05cddcSAtari911 $backupName = 'calendar.backup.v' . $version . '.' . date('Y-m-d_H-i-s') . '.zip'; 3137*1d05cddcSAtari911 $backupPath = DOKU_PLUGIN . $backupName; 3138*1d05cddcSAtari911 3139*1d05cddcSAtari911 try { 3140*1d05cddcSAtari911 $zip = new ZipArchive(); 3141*1d05cddcSAtari911 if ($zip->open($backupPath, ZipArchive::CREATE) === TRUE) { 3142*1d05cddcSAtari911 $this->addDirectoryToZip($zip, $pluginDir, 'calendar/'); 3143*1d05cddcSAtari911 $zip->close(); 3144*1d05cddcSAtari911 } else { 3145*1d05cddcSAtari911 $this->redirect('Failed to create backup ZIP file', 'error', 'update'); 3146*1d05cddcSAtari911 return; 3147*1d05cddcSAtari911 } 3148*1d05cddcSAtari911 } catch (Exception $e) { 3149*1d05cddcSAtari911 $this->redirect('Backup failed: ' . $e->getMessage(), 'error', 'update'); 3150*1d05cddcSAtari911 return; 3151*1d05cddcSAtari911 } 3152*1d05cddcSAtari911 } 3153*1d05cddcSAtari911 3154*1d05cddcSAtari911 // Extract uploaded ZIP 3155*1d05cddcSAtari911 $zip = new ZipArchive(); 3156*1d05cddcSAtari911 if ($zip->open($uploadedFile) !== TRUE) { 3157*1d05cddcSAtari911 $this->redirect('Failed to open ZIP file', 'error', 'update'); 3158*1d05cddcSAtari911 return; 3159*1d05cddcSAtari911 } 3160*1d05cddcSAtari911 3161*1d05cddcSAtari911 // Check if ZIP contains calendar folder 3162*1d05cddcSAtari911 $hasCalendarFolder = false; 3163*1d05cddcSAtari911 for ($i = 0; $i < $zip->numFiles; $i++) { 3164*1d05cddcSAtari911 $filename = $zip->getNameIndex($i); 3165*1d05cddcSAtari911 if (strpos($filename, 'calendar/') === 0) { 3166*1d05cddcSAtari911 $hasCalendarFolder = true; 3167*1d05cddcSAtari911 break; 3168*1d05cddcSAtari911 } 3169*1d05cddcSAtari911 } 3170*1d05cddcSAtari911 3171*1d05cddcSAtari911 // Extract to temp directory first 3172*1d05cddcSAtari911 $tempDir = DOKU_PLUGIN . 'calendar_update_temp/'; 3173*1d05cddcSAtari911 if (is_dir($tempDir)) { 3174*1d05cddcSAtari911 $this->deleteDirectory($tempDir); 3175*1d05cddcSAtari911 } 3176*1d05cddcSAtari911 mkdir($tempDir); 3177*1d05cddcSAtari911 3178*1d05cddcSAtari911 $zip->extractTo($tempDir); 3179*1d05cddcSAtari911 $zip->close(); 3180*1d05cddcSAtari911 3181*1d05cddcSAtari911 // Determine source directory 3182*1d05cddcSAtari911 if ($hasCalendarFolder) { 3183*1d05cddcSAtari911 $sourceDir = $tempDir . 'calendar/'; 3184*1d05cddcSAtari911 } else { 3185*1d05cddcSAtari911 $sourceDir = $tempDir; 3186*1d05cddcSAtari911 } 3187*1d05cddcSAtari911 3188*1d05cddcSAtari911 // Preserve configuration files 3189*1d05cddcSAtari911 $preserveFiles = ['sync_config.php', 'sync_state.json', 'sync.log']; 3190*1d05cddcSAtari911 $preserved = []; 3191*1d05cddcSAtari911 foreach ($preserveFiles as $file) { 3192*1d05cddcSAtari911 $oldFile = $pluginDir . $file; 3193*1d05cddcSAtari911 if (file_exists($oldFile)) { 3194*1d05cddcSAtari911 $preserved[$file] = file_get_contents($oldFile); 3195*1d05cddcSAtari911 } 3196*1d05cddcSAtari911 } 3197*1d05cddcSAtari911 3198*1d05cddcSAtari911 // Delete old plugin files (except data files) 3199*1d05cddcSAtari911 $this->deleteDirectoryContents($pluginDir, $preserveFiles); 3200*1d05cddcSAtari911 3201*1d05cddcSAtari911 // Copy new files 3202*1d05cddcSAtari911 $this->recursiveCopy($sourceDir, $pluginDir); 3203*1d05cddcSAtari911 3204*1d05cddcSAtari911 // Restore preserved files 3205*1d05cddcSAtari911 foreach ($preserved as $file => $content) { 3206*1d05cddcSAtari911 file_put_contents($pluginDir . $file, $content); 3207*1d05cddcSAtari911 } 3208*1d05cddcSAtari911 3209*1d05cddcSAtari911 // Update version and date in plugin.info.txt 3210*1d05cddcSAtari911 $pluginInfo = $pluginDir . 'plugin.info.txt'; 3211*1d05cddcSAtari911 if (file_exists($pluginInfo)) { 3212*1d05cddcSAtari911 $info = confToHash($pluginInfo); 3213*1d05cddcSAtari911 3214*1d05cddcSAtari911 // Get new version from uploaded plugin 3215*1d05cddcSAtari911 $newVersion = $info['version'] ?? 'unknown'; 3216*1d05cddcSAtari911 3217*1d05cddcSAtari911 // Update date to current 3218*1d05cddcSAtari911 $info['date'] = date('Y-m-d'); 3219*1d05cddcSAtari911 3220*1d05cddcSAtari911 // Write updated info back 3221*1d05cddcSAtari911 $lines = []; 3222*1d05cddcSAtari911 foreach ($info as $key => $value) { 3223*1d05cddcSAtari911 $lines[] = str_pad($key, 8) . ' ' . $value; 3224*1d05cddcSAtari911 } 3225*1d05cddcSAtari911 file_put_contents($pluginInfo, implode("\n", $lines) . "\n"); 3226*1d05cddcSAtari911 } 3227*1d05cddcSAtari911 3228*1d05cddcSAtari911 // Cleanup temp directory 3229*1d05cddcSAtari911 $this->deleteDirectory($tempDir); 3230*1d05cddcSAtari911 3231*1d05cddcSAtari911 $message = 'Plugin updated successfully!'; 3232*1d05cddcSAtari911 if ($backupFirst) { 3233*1d05cddcSAtari911 $message .= ' Backup saved as: ' . $backupName; 3234*1d05cddcSAtari911 } 3235*1d05cddcSAtari911 $this->redirect($message, 'success', 'update'); 3236*1d05cddcSAtari911 } 3237*1d05cddcSAtari911 3238*1d05cddcSAtari911 private function deleteBackup() { 3239*1d05cddcSAtari911 global $INPUT; 3240*1d05cddcSAtari911 3241*1d05cddcSAtari911 $filename = $INPUT->str('backup_file'); 3242*1d05cddcSAtari911 3243*1d05cddcSAtari911 if (empty($filename)) { 3244*1d05cddcSAtari911 $this->redirect('No backup file specified', 'error', 'update'); 3245*1d05cddcSAtari911 return; 3246*1d05cddcSAtari911 } 3247*1d05cddcSAtari911 3248*1d05cddcSAtari911 // Security: only allow files starting with "calendar" and ending with .zip, no directory traversal 3249*1d05cddcSAtari911 if (!preg_match('/^calendar[a-zA-Z0-9._-]*\.zip$/', $filename)) { 3250*1d05cddcSAtari911 $this->redirect('Invalid backup filename', 'error', 'update'); 3251*1d05cddcSAtari911 return; 3252*1d05cddcSAtari911 } 3253*1d05cddcSAtari911 3254*1d05cddcSAtari911 $backupPath = DOKU_PLUGIN . $filename; 3255*1d05cddcSAtari911 3256*1d05cddcSAtari911 if (!file_exists($backupPath)) { 3257*1d05cddcSAtari911 $this->redirect('Backup file not found', 'error', 'update'); 3258*1d05cddcSAtari911 return; 3259*1d05cddcSAtari911 } 3260*1d05cddcSAtari911 3261*1d05cddcSAtari911 if (@unlink($backupPath)) { 3262*1d05cddcSAtari911 $this->redirect('Backup deleted: ' . $filename, 'success', 'update'); 3263*1d05cddcSAtari911 } else { 3264*1d05cddcSAtari911 $this->redirect('Failed to delete backup. Check file permissions.', 'error', 'update'); 3265*1d05cddcSAtari911 } 3266*1d05cddcSAtari911 } 3267*1d05cddcSAtari911 3268*1d05cddcSAtari911 private function renameBackup() { 3269*1d05cddcSAtari911 global $INPUT; 3270*1d05cddcSAtari911 3271*1d05cddcSAtari911 $oldName = $INPUT->str('old_name'); 3272*1d05cddcSAtari911 $newName = $INPUT->str('new_name'); 3273*1d05cddcSAtari911 3274*1d05cddcSAtari911 if (empty($oldName) || empty($newName)) { 3275*1d05cddcSAtari911 $this->redirect('Missing filename(s)', 'error', 'update'); 3276*1d05cddcSAtari911 return; 3277*1d05cddcSAtari911 } 3278*1d05cddcSAtari911 3279*1d05cddcSAtari911 // Security: validate filenames 3280*1d05cddcSAtari911 if (!preg_match('/^[a-zA-Z0-9._-]+\.zip$/', $oldName) || !preg_match('/^[a-zA-Z0-9._-]+\.zip$/', $newName)) { 3281*1d05cddcSAtari911 $this->redirect('Invalid filename format', 'error', 'update'); 3282*1d05cddcSAtari911 return; 3283*1d05cddcSAtari911 } 3284*1d05cddcSAtari911 3285*1d05cddcSAtari911 $oldPath = DOKU_PLUGIN . $oldName; 3286*1d05cddcSAtari911 $newPath = DOKU_PLUGIN . $newName; 3287*1d05cddcSAtari911 3288*1d05cddcSAtari911 if (!file_exists($oldPath)) { 3289*1d05cddcSAtari911 $this->redirect('Backup file not found', 'error', 'update'); 3290*1d05cddcSAtari911 return; 3291*1d05cddcSAtari911 } 3292*1d05cddcSAtari911 3293*1d05cddcSAtari911 if (file_exists($newPath)) { 3294*1d05cddcSAtari911 $this->redirect('A file with the new name already exists', 'error', 'update'); 3295*1d05cddcSAtari911 return; 3296*1d05cddcSAtari911 } 3297*1d05cddcSAtari911 3298*1d05cddcSAtari911 if (@rename($oldPath, $newPath)) { 3299*1d05cddcSAtari911 $this->redirect('Backup renamed: ' . $oldName . ' → ' . $newName, 'success', 'update'); 3300*1d05cddcSAtari911 } else { 3301*1d05cddcSAtari911 $this->redirect('Failed to rename backup. Check file permissions.', 'error', 'update'); 3302*1d05cddcSAtari911 } 3303*1d05cddcSAtari911 } 3304*1d05cddcSAtari911 3305*1d05cddcSAtari911 private function restoreBackup() { 3306*1d05cddcSAtari911 global $INPUT; 3307*1d05cddcSAtari911 3308*1d05cddcSAtari911 $filename = $INPUT->str('backup_file'); 3309*1d05cddcSAtari911 3310*1d05cddcSAtari911 if (empty($filename)) { 3311*1d05cddcSAtari911 $this->redirect('No backup file specified', 'error', 'update'); 3312*1d05cddcSAtari911 return; 3313*1d05cddcSAtari911 } 3314*1d05cddcSAtari911 3315*1d05cddcSAtari911 // Security: only allow files starting with "calendar" and ending with .zip, no directory traversal 3316*1d05cddcSAtari911 if (!preg_match('/^calendar[a-zA-Z0-9._-]*\.zip$/', $filename)) { 3317*1d05cddcSAtari911 $this->redirect('Invalid backup filename', 'error', 'update'); 3318*1d05cddcSAtari911 return; 3319*1d05cddcSAtari911 } 3320*1d05cddcSAtari911 3321*1d05cddcSAtari911 $backupPath = DOKU_PLUGIN . $filename; 3322*1d05cddcSAtari911 $pluginDir = DOKU_PLUGIN . 'calendar/'; 3323*1d05cddcSAtari911 3324*1d05cddcSAtari911 if (!file_exists($backupPath)) { 3325*1d05cddcSAtari911 $this->redirect('Backup file not found', 'error', 'update'); 3326*1d05cddcSAtari911 return; 3327*1d05cddcSAtari911 } 3328*1d05cddcSAtari911 3329*1d05cddcSAtari911 // Check if plugin directory is writable 3330*1d05cddcSAtari911 if (!is_writable($pluginDir)) { 3331*1d05cddcSAtari911 $this->redirect('Plugin directory is not writable. Please check permissions.', 'error', 'update'); 3332*1d05cddcSAtari911 return; 3333*1d05cddcSAtari911 } 3334*1d05cddcSAtari911 3335*1d05cddcSAtari911 // Extract backup to temp directory 3336*1d05cddcSAtari911 $tempDir = DOKU_PLUGIN . 'calendar_restore_temp/'; 3337*1d05cddcSAtari911 if (is_dir($tempDir)) { 3338*1d05cddcSAtari911 $this->deleteDirectory($tempDir); 3339*1d05cddcSAtari911 } 3340*1d05cddcSAtari911 mkdir($tempDir); 3341*1d05cddcSAtari911 3342*1d05cddcSAtari911 $zip = new ZipArchive(); 3343*1d05cddcSAtari911 if ($zip->open($backupPath) !== TRUE) { 3344*1d05cddcSAtari911 $this->redirect('Failed to open backup ZIP file', 'error', 'update'); 3345*1d05cddcSAtari911 return; 3346*1d05cddcSAtari911 } 3347*1d05cddcSAtari911 3348*1d05cddcSAtari911 $zip->extractTo($tempDir); 3349*1d05cddcSAtari911 $zip->close(); 3350*1d05cddcSAtari911 3351*1d05cddcSAtari911 // The backup contains a "calendar/" folder 3352*1d05cddcSAtari911 $sourceDir = $tempDir . 'calendar/'; 3353*1d05cddcSAtari911 3354*1d05cddcSAtari911 if (!is_dir($sourceDir)) { 3355*1d05cddcSAtari911 $this->deleteDirectory($tempDir); 3356*1d05cddcSAtari911 $this->redirect('Invalid backup structure', 'error', 'update'); 3357*1d05cddcSAtari911 return; 3358*1d05cddcSAtari911 } 3359*1d05cddcSAtari911 3360*1d05cddcSAtari911 // Delete current plugin directory contents 3361*1d05cddcSAtari911 $this->deleteDirectoryContents($pluginDir, []); 3362*1d05cddcSAtari911 3363*1d05cddcSAtari911 // Copy backup files to plugin directory 3364*1d05cddcSAtari911 $this->recursiveCopy($sourceDir, $pluginDir); 3365*1d05cddcSAtari911 3366*1d05cddcSAtari911 // Cleanup temp directory 3367*1d05cddcSAtari911 $this->deleteDirectory($tempDir); 3368*1d05cddcSAtari911 3369*1d05cddcSAtari911 $this->redirect('Plugin restored from backup: ' . $filename, 'success', 'update'); 3370*1d05cddcSAtari911 } 3371*1d05cddcSAtari911 3372*1d05cddcSAtari911 private function addDirectoryToZip($zip, $dir, $zipPath = '') { 3373*1d05cddcSAtari911 try { 3374*1d05cddcSAtari911 $files = new RecursiveIteratorIterator( 3375*1d05cddcSAtari911 new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), 3376*1d05cddcSAtari911 RecursiveIteratorIterator::LEAVES_ONLY 3377*1d05cddcSAtari911 ); 3378*1d05cddcSAtari911 3379*1d05cddcSAtari911 foreach ($files as $file) { 3380*1d05cddcSAtari911 if (!$file->isDir()) { 3381*1d05cddcSAtari911 $filePath = $file->getRealPath(); 3382*1d05cddcSAtari911 if ($filePath && is_readable($filePath)) { 3383*1d05cddcSAtari911 $relativePath = $zipPath . substr($filePath, strlen($dir)); 3384*1d05cddcSAtari911 $zip->addFile($filePath, $relativePath); 3385*1d05cddcSAtari911 } 3386*1d05cddcSAtari911 } 3387*1d05cddcSAtari911 } 3388*1d05cddcSAtari911 } catch (Exception $e) { 3389*1d05cddcSAtari911 // Log error but continue - some files might not be readable 3390*1d05cddcSAtari911 error_log('Calendar plugin backup warning: ' . $e->getMessage()); 3391*1d05cddcSAtari911 } 3392*1d05cddcSAtari911 } 3393*1d05cddcSAtari911 3394*1d05cddcSAtari911 private function deleteDirectory($dir) { 3395*1d05cddcSAtari911 if (!is_dir($dir)) return; 3396*1d05cddcSAtari911 3397*1d05cddcSAtari911 try { 3398*1d05cddcSAtari911 $files = new RecursiveIteratorIterator( 3399*1d05cddcSAtari911 new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), 3400*1d05cddcSAtari911 RecursiveIteratorIterator::CHILD_FIRST 3401*1d05cddcSAtari911 ); 3402*1d05cddcSAtari911 3403*1d05cddcSAtari911 foreach ($files as $file) { 3404*1d05cddcSAtari911 if ($file->isDir()) { 3405*1d05cddcSAtari911 @rmdir($file->getRealPath()); 3406*1d05cddcSAtari911 } else { 3407*1d05cddcSAtari911 @unlink($file->getRealPath()); 3408*1d05cddcSAtari911 } 3409*1d05cddcSAtari911 } 3410*1d05cddcSAtari911 3411*1d05cddcSAtari911 @rmdir($dir); 3412*1d05cddcSAtari911 } catch (Exception $e) { 3413*1d05cddcSAtari911 error_log('Calendar plugin delete directory error: ' . $e->getMessage()); 3414*1d05cddcSAtari911 } 3415*1d05cddcSAtari911 } 3416*1d05cddcSAtari911 3417*1d05cddcSAtari911 private function deleteDirectoryContents($dir, $preserve = []) { 3418*1d05cddcSAtari911 if (!is_dir($dir)) return; 3419*1d05cddcSAtari911 3420*1d05cddcSAtari911 $items = scandir($dir); 3421*1d05cddcSAtari911 foreach ($items as $item) { 3422*1d05cddcSAtari911 if ($item === '.' || $item === '..') continue; 3423*1d05cddcSAtari911 if (in_array($item, $preserve)) continue; 3424*1d05cddcSAtari911 3425*1d05cddcSAtari911 $path = $dir . $item; 3426*1d05cddcSAtari911 if (is_dir($path)) { 3427*1d05cddcSAtari911 $this->deleteDirectory($path); 3428*1d05cddcSAtari911 } else { 3429*1d05cddcSAtari911 unlink($path); 3430*1d05cddcSAtari911 } 3431*1d05cddcSAtari911 } 3432*1d05cddcSAtari911 } 3433*1d05cddcSAtari911 3434*1d05cddcSAtari911 private function recursiveCopy($src, $dst) { 3435*1d05cddcSAtari911 $dir = opendir($src); 3436*1d05cddcSAtari911 @mkdir($dst); 3437*1d05cddcSAtari911 3438*1d05cddcSAtari911 while (($file = readdir($dir)) !== false) { 3439*1d05cddcSAtari911 if ($file !== '.' && $file !== '..') { 3440*1d05cddcSAtari911 if (is_dir($src . '/' . $file)) { 3441*1d05cddcSAtari911 $this->recursiveCopy($src . '/' . $file, $dst . '/' . $file); 3442*1d05cddcSAtari911 } else { 3443*1d05cddcSAtari911 copy($src . '/' . $file, $dst . '/' . $file); 3444*1d05cddcSAtari911 } 3445*1d05cddcSAtari911 } 3446*1d05cddcSAtari911 } 3447*1d05cddcSAtari911 3448*1d05cddcSAtari911 closedir($dir); 3449*1d05cddcSAtari911 } 3450*1d05cddcSAtari911 3451*1d05cddcSAtari911 private function formatBytes($bytes) { 3452*1d05cddcSAtari911 if ($bytes >= 1073741824) { 3453*1d05cddcSAtari911 return number_format($bytes / 1073741824, 2) . ' GB'; 3454*1d05cddcSAtari911 } elseif ($bytes >= 1048576) { 3455*1d05cddcSAtari911 return number_format($bytes / 1048576, 2) . ' MB'; 3456*1d05cddcSAtari911 } elseif ($bytes >= 1024) { 3457*1d05cddcSAtari911 return number_format($bytes / 1024, 2) . ' KB'; 3458*1d05cddcSAtari911 } else { 3459*1d05cddcSAtari911 return $bytes . ' bytes'; 3460*1d05cddcSAtari911 } 3461*1d05cddcSAtari911 } 3462*1d05cddcSAtari911 3463*1d05cddcSAtari911 private function findPhpBinary() { 3464*1d05cddcSAtari911 // Try PHP_BINARY constant first (most reliable if available) 3465*1d05cddcSAtari911 if (defined('PHP_BINARY') && !empty(PHP_BINARY) && is_executable(PHP_BINARY)) { 3466*1d05cddcSAtari911 return escapeshellarg(PHP_BINARY); 3467*1d05cddcSAtari911 } 3468*1d05cddcSAtari911 3469*1d05cddcSAtari911 // Try common PHP binary locations 3470*1d05cddcSAtari911 $possiblePaths = [ 3471*1d05cddcSAtari911 '/usr/bin/php', 3472*1d05cddcSAtari911 '/usr/bin/php8.1', 3473*1d05cddcSAtari911 '/usr/bin/php8.2', 3474*1d05cddcSAtari911 '/usr/bin/php8.3', 3475*1d05cddcSAtari911 '/usr/bin/php7.4', 3476*1d05cddcSAtari911 '/usr/local/bin/php', 3477*1d05cddcSAtari911 'php' // Last resort - rely on PATH 3478*1d05cddcSAtari911 ]; 3479*1d05cddcSAtari911 3480*1d05cddcSAtari911 foreach ($possiblePaths as $path) { 3481*1d05cddcSAtari911 // Test if this PHP binary works 3482*1d05cddcSAtari911 $testOutput = []; 3483*1d05cddcSAtari911 $testReturn = 0; 3484*1d05cddcSAtari911 exec($path . ' -v 2>&1', $testOutput, $testReturn); 3485*1d05cddcSAtari911 3486*1d05cddcSAtari911 if ($testReturn === 0) { 3487*1d05cddcSAtari911 return ($path === 'php') ? 'php' : escapeshellarg($path); 3488*1d05cddcSAtari911 } 3489*1d05cddcSAtari911 } 3490*1d05cddcSAtari911 3491*1d05cddcSAtari911 // Fallback to 'php' and hope it's in PATH 3492*1d05cddcSAtari911 return 'php'; 3493*1d05cddcSAtari911 } 3494*1d05cddcSAtari911 3495*1d05cddcSAtari911 private function redirect($message, $type = 'success', $tab = null) { 3496*1d05cddcSAtari911 $url = '?do=admin&page=calendar'; 3497*1d05cddcSAtari911 if ($tab) { 3498*1d05cddcSAtari911 $url .= '&tab=' . $tab; 3499*1d05cddcSAtari911 } 3500*1d05cddcSAtari911 $url .= '&msg=' . urlencode($message) . '&msgtype=' . $type; 3501*1d05cddcSAtari911 header('Location: ' . $url); 3502*1d05cddcSAtari911 exit; 3503*1d05cddcSAtari911 } 3504*1d05cddcSAtari911 3505*1d05cddcSAtari911 private function getLog() { 3506*1d05cddcSAtari911 global $INPUT; 3507*1d05cddcSAtari911 3508*1d05cddcSAtari911 if ($INPUT->str('call') === 'ajax') { 3509*1d05cddcSAtari911 header('Content-Type: application/json'); 3510*1d05cddcSAtari911 3511*1d05cddcSAtari911 $logFile = DOKU_PLUGIN . 'calendar/sync.log'; 3512*1d05cddcSAtari911 $log = ''; 3513*1d05cddcSAtari911 3514*1d05cddcSAtari911 if (file_exists($logFile)) { 3515*1d05cddcSAtari911 // Get last 500 lines 3516*1d05cddcSAtari911 $lines = file($logFile); 3517*1d05cddcSAtari911 if ($lines !== false) { 3518*1d05cddcSAtari911 $lines = array_slice($lines, -500); 3519*1d05cddcSAtari911 $log = implode('', $lines); 3520*1d05cddcSAtari911 } 3521*1d05cddcSAtari911 } else { 3522*1d05cddcSAtari911 $log = "No log file found. Sync hasn't run yet."; 3523*1d05cddcSAtari911 } 3524*1d05cddcSAtari911 3525*1d05cddcSAtari911 echo json_encode(['log' => $log]); 3526*1d05cddcSAtari911 exit; 3527*1d05cddcSAtari911 } 3528*1d05cddcSAtari911 } 3529*1d05cddcSAtari911 3530*1d05cddcSAtari911 private function exportConfig() { 3531*1d05cddcSAtari911 global $INPUT; 3532*1d05cddcSAtari911 3533*1d05cddcSAtari911 if ($INPUT->str('call') === 'ajax') { 3534*1d05cddcSAtari911 header('Content-Type: application/json'); 3535*1d05cddcSAtari911 3536*1d05cddcSAtari911 try { 3537*1d05cddcSAtari911 $configFile = DOKU_PLUGIN . 'calendar/sync_config.php'; 3538*1d05cddcSAtari911 3539*1d05cddcSAtari911 if (!file_exists($configFile)) { 3540*1d05cddcSAtari911 echo json_encode([ 3541*1d05cddcSAtari911 'success' => false, 3542*1d05cddcSAtari911 'message' => 'Config file not found' 3543*1d05cddcSAtari911 ]); 3544*1d05cddcSAtari911 exit; 3545*1d05cddcSAtari911 } 3546*1d05cddcSAtari911 3547*1d05cddcSAtari911 // Read config file 3548*1d05cddcSAtari911 $configContent = file_get_contents($configFile); 3549*1d05cddcSAtari911 3550*1d05cddcSAtari911 // Generate encryption key from DokuWiki secret 3551*1d05cddcSAtari911 $key = $this->getEncryptionKey(); 3552*1d05cddcSAtari911 3553*1d05cddcSAtari911 // Encrypt config 3554*1d05cddcSAtari911 $encrypted = $this->encryptData($configContent, $key); 3555*1d05cddcSAtari911 3556*1d05cddcSAtari911 echo json_encode([ 3557*1d05cddcSAtari911 'success' => true, 3558*1d05cddcSAtari911 'encrypted' => $encrypted, 3559*1d05cddcSAtari911 'message' => 'Config exported successfully' 3560*1d05cddcSAtari911 ]); 3561*1d05cddcSAtari911 exit; 3562*1d05cddcSAtari911 3563*1d05cddcSAtari911 } catch (Exception $e) { 3564*1d05cddcSAtari911 echo json_encode([ 3565*1d05cddcSAtari911 'success' => false, 3566*1d05cddcSAtari911 'message' => $e->getMessage() 3567*1d05cddcSAtari911 ]); 3568*1d05cddcSAtari911 exit; 3569*1d05cddcSAtari911 } 3570*1d05cddcSAtari911 } 3571*1d05cddcSAtari911 } 3572*1d05cddcSAtari911 3573*1d05cddcSAtari911 private function importConfig() { 3574*1d05cddcSAtari911 global $INPUT; 3575*1d05cddcSAtari911 3576*1d05cddcSAtari911 if ($INPUT->str('call') === 'ajax') { 3577*1d05cddcSAtari911 header('Content-Type: application/json'); 3578*1d05cddcSAtari911 3579*1d05cddcSAtari911 try { 3580*1d05cddcSAtari911 $encrypted = $_POST['encrypted_config'] ?? ''; 3581*1d05cddcSAtari911 3582*1d05cddcSAtari911 if (empty($encrypted)) { 3583*1d05cddcSAtari911 echo json_encode([ 3584*1d05cddcSAtari911 'success' => false, 3585*1d05cddcSAtari911 'message' => 'No config data provided' 3586*1d05cddcSAtari911 ]); 3587*1d05cddcSAtari911 exit; 3588*1d05cddcSAtari911 } 3589*1d05cddcSAtari911 3590*1d05cddcSAtari911 // Generate encryption key from DokuWiki secret 3591*1d05cddcSAtari911 $key = $this->getEncryptionKey(); 3592*1d05cddcSAtari911 3593*1d05cddcSAtari911 // Decrypt config 3594*1d05cddcSAtari911 $configContent = $this->decryptData($encrypted, $key); 3595*1d05cddcSAtari911 3596*1d05cddcSAtari911 if ($configContent === false) { 3597*1d05cddcSAtari911 echo json_encode([ 3598*1d05cddcSAtari911 'success' => false, 3599*1d05cddcSAtari911 'message' => 'Decryption failed. Invalid key or corrupted file.' 3600*1d05cddcSAtari911 ]); 3601*1d05cddcSAtari911 exit; 3602*1d05cddcSAtari911 } 3603*1d05cddcSAtari911 3604*1d05cddcSAtari911 // Validate PHP syntax 3605*1d05cddcSAtari911 $valid = @eval('?>' . $configContent); 3606*1d05cddcSAtari911 if ($valid === false) { 3607*1d05cddcSAtari911 echo json_encode([ 3608*1d05cddcSAtari911 'success' => false, 3609*1d05cddcSAtari911 'message' => 'Invalid config file format' 3610*1d05cddcSAtari911 ]); 3611*1d05cddcSAtari911 exit; 3612*1d05cddcSAtari911 } 3613*1d05cddcSAtari911 3614*1d05cddcSAtari911 // Write to config file 3615*1d05cddcSAtari911 $configFile = DOKU_PLUGIN . 'calendar/sync_config.php'; 3616*1d05cddcSAtari911 3617*1d05cddcSAtari911 // Backup existing config 3618*1d05cddcSAtari911 if (file_exists($configFile)) { 3619*1d05cddcSAtari911 $backupFile = $configFile . '.backup.' . date('Y-m-d_H-i-s'); 3620*1d05cddcSAtari911 copy($configFile, $backupFile); 3621*1d05cddcSAtari911 } 3622*1d05cddcSAtari911 3623*1d05cddcSAtari911 // Write new config 3624*1d05cddcSAtari911 if (file_put_contents($configFile, $configContent) === false) { 3625*1d05cddcSAtari911 echo json_encode([ 3626*1d05cddcSAtari911 'success' => false, 3627*1d05cddcSAtari911 'message' => 'Failed to write config file' 3628*1d05cddcSAtari911 ]); 3629*1d05cddcSAtari911 exit; 3630*1d05cddcSAtari911 } 3631*1d05cddcSAtari911 3632*1d05cddcSAtari911 echo json_encode([ 3633*1d05cddcSAtari911 'success' => true, 3634*1d05cddcSAtari911 'message' => 'Config imported successfully' 3635*1d05cddcSAtari911 ]); 3636*1d05cddcSAtari911 exit; 3637*1d05cddcSAtari911 3638*1d05cddcSAtari911 } catch (Exception $e) { 3639*1d05cddcSAtari911 echo json_encode([ 3640*1d05cddcSAtari911 'success' => false, 3641*1d05cddcSAtari911 'message' => $e->getMessage() 3642*1d05cddcSAtari911 ]); 3643*1d05cddcSAtari911 exit; 3644*1d05cddcSAtari911 } 3645*1d05cddcSAtari911 } 3646*1d05cddcSAtari911 } 3647*1d05cddcSAtari911 3648*1d05cddcSAtari911 private function getEncryptionKey() { 3649*1d05cddcSAtari911 global $conf; 3650*1d05cddcSAtari911 // Use DokuWiki's secret as the base for encryption 3651*1d05cddcSAtari911 // This ensures the key is unique per installation 3652*1d05cddcSAtari911 return hash('sha256', $conf['secret'] . 'calendar_config_encryption', true); 3653*1d05cddcSAtari911 } 3654*1d05cddcSAtari911 3655*1d05cddcSAtari911 private function encryptData($data, $key) { 3656*1d05cddcSAtari911 // Use AES-256-CBC encryption 3657*1d05cddcSAtari911 $ivLength = openssl_cipher_iv_length('aes-256-cbc'); 3658*1d05cddcSAtari911 $iv = openssl_random_pseudo_bytes($ivLength); 3659*1d05cddcSAtari911 3660*1d05cddcSAtari911 $encrypted = openssl_encrypt($data, 'aes-256-cbc', $key, 0, $iv); 3661*1d05cddcSAtari911 3662*1d05cddcSAtari911 // Combine IV and encrypted data, then base64 encode 3663*1d05cddcSAtari911 return base64_encode($iv . $encrypted); 3664*1d05cddcSAtari911 } 3665*1d05cddcSAtari911 3666*1d05cddcSAtari911 private function decryptData($encryptedData, $key) { 3667*1d05cddcSAtari911 // Decode base64 3668*1d05cddcSAtari911 $data = base64_decode($encryptedData); 3669*1d05cddcSAtari911 3670*1d05cddcSAtari911 if ($data === false) { 3671*1d05cddcSAtari911 return false; 3672*1d05cddcSAtari911 } 3673*1d05cddcSAtari911 3674*1d05cddcSAtari911 // Extract IV and encrypted content 3675*1d05cddcSAtari911 $ivLength = openssl_cipher_iv_length('aes-256-cbc'); 3676*1d05cddcSAtari911 $iv = substr($data, 0, $ivLength); 3677*1d05cddcSAtari911 $encrypted = substr($data, $ivLength); 3678*1d05cddcSAtari911 3679*1d05cddcSAtari911 // Decrypt 3680*1d05cddcSAtari911 $decrypted = openssl_decrypt($encrypted, 'aes-256-cbc', $key, 0, $iv); 3681*1d05cddcSAtari911 3682*1d05cddcSAtari911 return $decrypted; 3683*1d05cddcSAtari911 } 3684*1d05cddcSAtari911 3685*1d05cddcSAtari911 private function clearLogFile() { 3686*1d05cddcSAtari911 global $INPUT; 3687*1d05cddcSAtari911 3688*1d05cddcSAtari911 if ($INPUT->str('call') === 'ajax') { 3689*1d05cddcSAtari911 header('Content-Type: application/json'); 3690*1d05cddcSAtari911 3691*1d05cddcSAtari911 $logFile = DOKU_PLUGIN . 'calendar/sync.log'; 3692*1d05cddcSAtari911 3693*1d05cddcSAtari911 if (file_exists($logFile)) { 3694*1d05cddcSAtari911 if (file_put_contents($logFile, '')) { 3695*1d05cddcSAtari911 echo json_encode(['success' => true]); 3696*1d05cddcSAtari911 } else { 3697*1d05cddcSAtari911 echo json_encode(['success' => false, 'message' => 'Could not clear log file']); 3698*1d05cddcSAtari911 } 3699*1d05cddcSAtari911 } else { 3700*1d05cddcSAtari911 echo json_encode(['success' => true, 'message' => 'No log file to clear']); 3701*1d05cddcSAtari911 } 3702*1d05cddcSAtari911 exit; 3703*1d05cddcSAtari911 } 3704*1d05cddcSAtari911 } 3705*1d05cddcSAtari911 3706*1d05cddcSAtari911 private function downloadLog() { 3707*1d05cddcSAtari911 $logFile = DOKU_PLUGIN . 'calendar/sync.log'; 3708*1d05cddcSAtari911 3709*1d05cddcSAtari911 if (file_exists($logFile)) { 3710*1d05cddcSAtari911 header('Content-Type: text/plain'); 3711*1d05cddcSAtari911 header('Content-Disposition: attachment; filename="calendar-sync-' . date('Y-m-d-His') . '.log"'); 3712*1d05cddcSAtari911 readfile($logFile); 3713*1d05cddcSAtari911 exit; 3714*1d05cddcSAtari911 } else { 3715*1d05cddcSAtari911 echo 'No log file found'; 3716*1d05cddcSAtari911 exit; 3717*1d05cddcSAtari911 } 3718*1d05cddcSAtari911 } 3719*1d05cddcSAtari911 3720*1d05cddcSAtari911 private function getEventStatistics() { 3721*1d05cddcSAtari911 $stats = [ 3722*1d05cddcSAtari911 'total_events' => 0, 3723*1d05cddcSAtari911 'total_namespaces' => 0, 3724*1d05cddcSAtari911 'total_files' => 0, 3725*1d05cddcSAtari911 'total_recurring' => 0, 3726*1d05cddcSAtari911 'by_namespace' => [], 3727*1d05cddcSAtari911 'last_scan' => '' 3728*1d05cddcSAtari911 ]; 3729*1d05cddcSAtari911 3730*1d05cddcSAtari911 $metaDir = DOKU_INC . 'data/meta/'; 3731*1d05cddcSAtari911 $cacheFile = DOKU_PLUGIN . 'calendar/.event_stats_cache'; 3732*1d05cddcSAtari911 3733*1d05cddcSAtari911 // Check if we have cached stats (less than 5 minutes old) 3734*1d05cddcSAtari911 if (file_exists($cacheFile)) { 3735*1d05cddcSAtari911 $cacheData = json_decode(file_get_contents($cacheFile), true); 3736*1d05cddcSAtari911 if ($cacheData && (time() - $cacheData['timestamp']) < 300) { 3737*1d05cddcSAtari911 return $cacheData['stats']; 3738*1d05cddcSAtari911 } 3739*1d05cddcSAtari911 } 3740*1d05cddcSAtari911 3741*1d05cddcSAtari911 // Scan for events 3742*1d05cddcSAtari911 $this->scanDirectoryForStats($metaDir, '', $stats); 3743*1d05cddcSAtari911 3744*1d05cddcSAtari911 // Count recurring events 3745*1d05cddcSAtari911 $recurringEvents = $this->findRecurringEvents(); 3746*1d05cddcSAtari911 $stats['total_recurring'] = count($recurringEvents); 3747*1d05cddcSAtari911 3748*1d05cddcSAtari911 $stats['total_namespaces'] = count($stats['by_namespace']); 3749*1d05cddcSAtari911 $stats['last_scan'] = date('Y-m-d H:i:s'); 3750*1d05cddcSAtari911 3751*1d05cddcSAtari911 // Cache the results 3752*1d05cddcSAtari911 file_put_contents($cacheFile, json_encode([ 3753*1d05cddcSAtari911 'timestamp' => time(), 3754*1d05cddcSAtari911 'stats' => $stats 3755*1d05cddcSAtari911 ])); 3756*1d05cddcSAtari911 3757*1d05cddcSAtari911 return $stats; 3758*1d05cddcSAtari911 } 3759*1d05cddcSAtari911 3760*1d05cddcSAtari911 private function scanDirectoryForStats($dir, $namespace, &$stats) { 3761*1d05cddcSAtari911 if (!is_dir($dir)) return; 3762*1d05cddcSAtari911 3763*1d05cddcSAtari911 $items = scandir($dir); 3764*1d05cddcSAtari911 foreach ($items as $item) { 3765*1d05cddcSAtari911 if ($item === '.' || $item === '..') continue; 3766*1d05cddcSAtari911 3767*1d05cddcSAtari911 $path = $dir . $item; 3768*1d05cddcSAtari911 3769*1d05cddcSAtari911 // Check if this is a calendar directory 3770*1d05cddcSAtari911 if ($item === 'calendar' && is_dir($path)) { 3771*1d05cddcSAtari911 $jsonFiles = glob($path . '/*.json'); 3772*1d05cddcSAtari911 $eventCount = 0; 3773*1d05cddcSAtari911 3774*1d05cddcSAtari911 foreach ($jsonFiles as $file) { 3775*1d05cddcSAtari911 $stats['total_files']++; 3776*1d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 3777*1d05cddcSAtari911 if ($data) { 3778*1d05cddcSAtari911 foreach ($data as $dateEvents) { 3779*1d05cddcSAtari911 $eventCount += count($dateEvents); 3780*1d05cddcSAtari911 } 3781*1d05cddcSAtari911 } 3782*1d05cddcSAtari911 } 3783*1d05cddcSAtari911 3784*1d05cddcSAtari911 $stats['total_events'] += $eventCount; 3785*1d05cddcSAtari911 3786*1d05cddcSAtari911 if ($eventCount > 0) { 3787*1d05cddcSAtari911 $stats['by_namespace'][$namespace] = [ 3788*1d05cddcSAtari911 'events' => $eventCount, 3789*1d05cddcSAtari911 'files' => count($jsonFiles) 3790*1d05cddcSAtari911 ]; 3791*1d05cddcSAtari911 } 3792*1d05cddcSAtari911 } elseif (is_dir($path)) { 3793*1d05cddcSAtari911 // Recurse into subdirectories 3794*1d05cddcSAtari911 $newNamespace = $namespace ? $namespace . ':' . $item : $item; 3795*1d05cddcSAtari911 $this->scanDirectoryForStats($path . '/', $newNamespace, $stats); 3796*1d05cddcSAtari911 } 3797*1d05cddcSAtari911 } 3798*1d05cddcSAtari911 } 3799*1d05cddcSAtari911 3800*1d05cddcSAtari911 private function rescanEvents() { 3801*1d05cddcSAtari911 // Clear the cache to force a rescan 3802*1d05cddcSAtari911 $cacheFile = DOKU_PLUGIN . 'calendar/.event_stats_cache'; 3803*1d05cddcSAtari911 if (file_exists($cacheFile)) { 3804*1d05cddcSAtari911 unlink($cacheFile); 3805*1d05cddcSAtari911 } 3806*1d05cddcSAtari911 3807*1d05cddcSAtari911 // Get fresh statistics 3808*1d05cddcSAtari911 $stats = $this->getEventStatistics(); 3809*1d05cddcSAtari911 3810*1d05cddcSAtari911 // Build absolute redirect URL 3811*1d05cddcSAtari911 $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'; 3812*1d05cddcSAtari911 3813*1d05cddcSAtari911 // Redirect with success message using absolute URL 3814*1d05cddcSAtari911 header('Location: ' . $redirectUrl, true, 303); 3815*1d05cddcSAtari911 exit; 3816*1d05cddcSAtari911 } 3817*1d05cddcSAtari911 3818*1d05cddcSAtari911 private function exportAllEvents() { 3819*1d05cddcSAtari911 $metaDir = DOKU_INC . 'data/meta/'; 3820*1d05cddcSAtari911 $allEvents = []; 3821*1d05cddcSAtari911 3822*1d05cddcSAtari911 // Collect all events 3823*1d05cddcSAtari911 $this->collectAllEvents($metaDir, '', $allEvents); 3824*1d05cddcSAtari911 3825*1d05cddcSAtari911 // Create export package 3826*1d05cddcSAtari911 $exportData = [ 3827*1d05cddcSAtari911 'export_date' => date('Y-m-d H:i:s'), 3828*1d05cddcSAtari911 'version' => '3.4.6', 3829*1d05cddcSAtari911 'total_events' => 0, 3830*1d05cddcSAtari911 'namespaces' => [] 3831*1d05cddcSAtari911 ]; 3832*1d05cddcSAtari911 3833*1d05cddcSAtari911 foreach ($allEvents as $namespace => $files) { 3834*1d05cddcSAtari911 $exportData['namespaces'][$namespace] = []; 3835*1d05cddcSAtari911 foreach ($files as $filename => $events) { 3836*1d05cddcSAtari911 $exportData['namespaces'][$namespace][$filename] = $events; 3837*1d05cddcSAtari911 foreach ($events as $dateEvents) { 3838*1d05cddcSAtari911 $exportData['total_events'] += count($dateEvents); 3839*1d05cddcSAtari911 } 3840*1d05cddcSAtari911 } 3841*1d05cddcSAtari911 } 3842*1d05cddcSAtari911 3843*1d05cddcSAtari911 // Send as download 3844*1d05cddcSAtari911 header('Content-Type: application/json'); 3845*1d05cddcSAtari911 header('Content-Disposition: attachment; filename="calendar-events-export-' . date('Y-m-d-His') . '.json"'); 3846*1d05cddcSAtari911 echo json_encode($exportData, JSON_PRETTY_PRINT); 3847*1d05cddcSAtari911 exit; 3848*1d05cddcSAtari911 } 3849*1d05cddcSAtari911 3850*1d05cddcSAtari911 private function collectAllEvents($dir, $namespace, &$allEvents) { 3851*1d05cddcSAtari911 if (!is_dir($dir)) return; 3852*1d05cddcSAtari911 3853*1d05cddcSAtari911 $items = scandir($dir); 3854*1d05cddcSAtari911 foreach ($items as $item) { 3855*1d05cddcSAtari911 if ($item === '.' || $item === '..') continue; 3856*1d05cddcSAtari911 3857*1d05cddcSAtari911 $path = $dir . $item; 3858*1d05cddcSAtari911 3859*1d05cddcSAtari911 // Check if this is a calendar directory 3860*1d05cddcSAtari911 if ($item === 'calendar' && is_dir($path)) { 3861*1d05cddcSAtari911 $jsonFiles = glob($path . '/*.json'); 3862*1d05cddcSAtari911 3863*1d05cddcSAtari911 if (!isset($allEvents[$namespace])) { 3864*1d05cddcSAtari911 $allEvents[$namespace] = []; 3865*1d05cddcSAtari911 } 3866*1d05cddcSAtari911 3867*1d05cddcSAtari911 foreach ($jsonFiles as $file) { 3868*1d05cddcSAtari911 $filename = basename($file); 3869*1d05cddcSAtari911 $data = json_decode(file_get_contents($file), true); 3870*1d05cddcSAtari911 if ($data) { 3871*1d05cddcSAtari911 $allEvents[$namespace][$filename] = $data; 3872*1d05cddcSAtari911 } 3873*1d05cddcSAtari911 } 3874*1d05cddcSAtari911 } elseif (is_dir($path)) { 3875*1d05cddcSAtari911 // Recurse into subdirectories 3876*1d05cddcSAtari911 $newNamespace = $namespace ? $namespace . ':' . $item : $item; 3877*1d05cddcSAtari911 $this->collectAllEvents($path . '/', $newNamespace, $allEvents); 3878*1d05cddcSAtari911 } 3879*1d05cddcSAtari911 } 3880*1d05cddcSAtari911 } 3881*1d05cddcSAtari911 3882*1d05cddcSAtari911 private function importAllEvents() { 3883*1d05cddcSAtari911 global $INPUT; 3884*1d05cddcSAtari911 3885*1d05cddcSAtari911 if (!isset($_FILES['import_file'])) { 3886*1d05cddcSAtari911 $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('No file uploaded') . '&msgtype=error'; 3887*1d05cddcSAtari911 header('Location: ' . $redirectUrl, true, 303); 3888*1d05cddcSAtari911 exit; 3889*1d05cddcSAtari911 } 3890*1d05cddcSAtari911 3891*1d05cddcSAtari911 $file = $_FILES['import_file']; 3892*1d05cddcSAtari911 3893*1d05cddcSAtari911 if ($file['error'] !== UPLOAD_ERR_OK) { 3894*1d05cddcSAtari911 $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('Upload error: ' . $file['error']) . '&msgtype=error'; 3895*1d05cddcSAtari911 header('Location: ' . $redirectUrl, true, 303); 3896*1d05cddcSAtari911 exit; 3897*1d05cddcSAtari911 } 3898*1d05cddcSAtari911 3899*1d05cddcSAtari911 // Read and decode the import file 3900*1d05cddcSAtari911 $importData = json_decode(file_get_contents($file['tmp_name']), true); 3901*1d05cddcSAtari911 3902*1d05cddcSAtari911 if (!$importData || !isset($importData['namespaces'])) { 3903*1d05cddcSAtari911 $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode('Invalid import file format') . '&msgtype=error'; 3904*1d05cddcSAtari911 header('Location: ' . $redirectUrl, true, 303); 3905*1d05cddcSAtari911 exit; 3906*1d05cddcSAtari911 } 3907*1d05cddcSAtari911 3908*1d05cddcSAtari911 $importedCount = 0; 3909*1d05cddcSAtari911 $mergedCount = 0; 3910*1d05cddcSAtari911 3911*1d05cddcSAtari911 // Import events 3912*1d05cddcSAtari911 foreach ($importData['namespaces'] as $namespace => $files) { 3913*1d05cddcSAtari911 $metaDir = DOKU_INC . 'data/meta/'; 3914*1d05cddcSAtari911 if ($namespace) { 3915*1d05cddcSAtari911 $metaDir .= str_replace(':', '/', $namespace) . '/'; 3916*1d05cddcSAtari911 } 3917*1d05cddcSAtari911 $calendarDir = $metaDir . 'calendar/'; 3918*1d05cddcSAtari911 3919*1d05cddcSAtari911 // Create directory if needed 3920*1d05cddcSAtari911 if (!is_dir($calendarDir)) { 3921*1d05cddcSAtari911 mkdir($calendarDir, 0755, true); 3922*1d05cddcSAtari911 } 3923*1d05cddcSAtari911 3924*1d05cddcSAtari911 foreach ($files as $filename => $events) { 3925*1d05cddcSAtari911 $targetFile = $calendarDir . $filename; 3926*1d05cddcSAtari911 3927*1d05cddcSAtari911 // If file exists, merge events 3928*1d05cddcSAtari911 if (file_exists($targetFile)) { 3929*1d05cddcSAtari911 $existing = json_decode(file_get_contents($targetFile), true); 3930*1d05cddcSAtari911 if ($existing) { 3931*1d05cddcSAtari911 foreach ($events as $date => $dateEvents) { 3932*1d05cddcSAtari911 if (!isset($existing[$date])) { 3933*1d05cddcSAtari911 $existing[$date] = []; 3934*1d05cddcSAtari911 } 3935*1d05cddcSAtari911 foreach ($dateEvents as $event) { 3936*1d05cddcSAtari911 // Check if event with same ID exists 3937*1d05cddcSAtari911 $found = false; 3938*1d05cddcSAtari911 foreach ($existing[$date] as $existingEvent) { 3939*1d05cddcSAtari911 if ($existingEvent['id'] === $event['id']) { 3940*1d05cddcSAtari911 $found = true; 3941*1d05cddcSAtari911 break; 3942*1d05cddcSAtari911 } 3943*1d05cddcSAtari911 } 3944*1d05cddcSAtari911 if (!$found) { 3945*1d05cddcSAtari911 $existing[$date][] = $event; 3946*1d05cddcSAtari911 $importedCount++; 3947*1d05cddcSAtari911 } else { 3948*1d05cddcSAtari911 $mergedCount++; 3949*1d05cddcSAtari911 } 3950*1d05cddcSAtari911 } 3951*1d05cddcSAtari911 } 3952*1d05cddcSAtari911 file_put_contents($targetFile, json_encode($existing, JSON_PRETTY_PRINT)); 3953*1d05cddcSAtari911 } 3954*1d05cddcSAtari911 } else { 3955*1d05cddcSAtari911 // New file 3956*1d05cddcSAtari911 file_put_contents($targetFile, json_encode($events, JSON_PRETTY_PRINT)); 3957*1d05cddcSAtari911 foreach ($events as $dateEvents) { 3958*1d05cddcSAtari911 $importedCount += count($dateEvents); 3959*1d05cddcSAtari911 } 3960*1d05cddcSAtari911 } 3961*1d05cddcSAtari911 } 3962*1d05cddcSAtari911 } 3963*1d05cddcSAtari911 3964*1d05cddcSAtari911 // Clear cache 3965*1d05cddcSAtari911 $cacheFile = DOKU_PLUGIN . 'calendar/.event_stats_cache'; 3966*1d05cddcSAtari911 if (file_exists($cacheFile)) { 3967*1d05cddcSAtari911 unlink($cacheFile); 3968*1d05cddcSAtari911 } 3969*1d05cddcSAtari911 3970*1d05cddcSAtari911 $message = "Import complete! Imported $importedCount new events"; 3971*1d05cddcSAtari911 if ($mergedCount > 0) { 3972*1d05cddcSAtari911 $message .= ", skipped $mergedCount duplicates"; 3973*1d05cddcSAtari911 } 3974*1d05cddcSAtari911 3975*1d05cddcSAtari911 $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode($message) . '&msgtype=success'; 3976*1d05cddcSAtari911 header('Location: ' . $redirectUrl, true, 303); 3977*1d05cddcSAtari911 exit; 3978*1d05cddcSAtari911 } 3979*1d05cddcSAtari911 3980*1d05cddcSAtari911 private function previewCleanup() { 3981*1d05cddcSAtari911 global $INPUT; 3982*1d05cddcSAtari911 3983*1d05cddcSAtari911 $cleanupType = $INPUT->str('cleanup_type', 'age'); 3984*1d05cddcSAtari911 $namespaceFilter = $INPUT->str('namespace_filter', ''); 3985*1d05cddcSAtari911 3986*1d05cddcSAtari911 // Debug info 3987*1d05cddcSAtari911 $debug = []; 3988*1d05cddcSAtari911 $debug['cleanup_type'] = $cleanupType; 3989*1d05cddcSAtari911 $debug['namespace_filter'] = $namespaceFilter; 3990*1d05cddcSAtari911 $debug['age_value'] = $INPUT->int('age_value', 6); 3991*1d05cddcSAtari911 $debug['age_unit'] = $INPUT->str('age_unit', 'months'); 3992*1d05cddcSAtari911 $debug['range_start'] = $INPUT->str('range_start', ''); 3993*1d05cddcSAtari911 $debug['range_end'] = $INPUT->str('range_end', ''); 3994*1d05cddcSAtari911 $debug['delete_completed'] = $INPUT->bool('delete_completed', false); 3995*1d05cddcSAtari911 $debug['delete_past'] = $INPUT->bool('delete_past', false); 3996*1d05cddcSAtari911 3997*1d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 3998*1d05cddcSAtari911 $debug['data_dir'] = $dataDir; 3999*1d05cddcSAtari911 $debug['data_dir_exists'] = is_dir($dataDir); 4000*1d05cddcSAtari911 4001*1d05cddcSAtari911 $eventsToDelete = $this->findEventsToCleanup($cleanupType, $namespaceFilter); 4002*1d05cddcSAtari911 4003*1d05cddcSAtari911 // Merge with scan debug info 4004*1d05cddcSAtari911 if (isset($this->_cleanupDebug)) { 4005*1d05cddcSAtari911 $debug = array_merge($debug, $this->_cleanupDebug); 4006*1d05cddcSAtari911 } 4007*1d05cddcSAtari911 4008*1d05cddcSAtari911 // Return JSON for preview with debug info 4009*1d05cddcSAtari911 header('Content-Type: application/json'); 4010*1d05cddcSAtari911 echo json_encode([ 4011*1d05cddcSAtari911 'count' => count($eventsToDelete), 4012*1d05cddcSAtari911 'events' => array_slice($eventsToDelete, 0, 50), // Limit to 50 for preview 4013*1d05cddcSAtari911 'debug' => $debug 4014*1d05cddcSAtari911 ]); 4015*1d05cddcSAtari911 exit; 4016*1d05cddcSAtari911 } 4017*1d05cddcSAtari911 4018*1d05cddcSAtari911 private function cleanupEvents() { 4019*1d05cddcSAtari911 global $INPUT; 4020*1d05cddcSAtari911 4021*1d05cddcSAtari911 $cleanupType = $INPUT->str('cleanup_type', 'age'); 4022*1d05cddcSAtari911 $namespaceFilter = $INPUT->str('namespace_filter', ''); 4023*1d05cddcSAtari911 4024*1d05cddcSAtari911 // Create backup first 4025*1d05cddcSAtari911 $backupDir = DOKU_PLUGIN . 'calendar/backups/'; 4026*1d05cddcSAtari911 if (!is_dir($backupDir)) { 4027*1d05cddcSAtari911 mkdir($backupDir, 0755, true); 4028*1d05cddcSAtari911 } 4029*1d05cddcSAtari911 4030*1d05cddcSAtari911 $backupFile = $backupDir . 'before-cleanup-' . date('Y-m-d-His') . '.zip'; 4031*1d05cddcSAtari911 $this->createBackup($backupFile); 4032*1d05cddcSAtari911 4033*1d05cddcSAtari911 // Find events to delete 4034*1d05cddcSAtari911 $eventsToDelete = $this->findEventsToCleanup($cleanupType, $namespaceFilter); 4035*1d05cddcSAtari911 $deletedCount = 0; 4036*1d05cddcSAtari911 4037*1d05cddcSAtari911 // Group by file 4038*1d05cddcSAtari911 $fileGroups = []; 4039*1d05cddcSAtari911 foreach ($eventsToDelete as $evt) { 4040*1d05cddcSAtari911 $fileGroups[$evt['file']][] = $evt; 4041*1d05cddcSAtari911 } 4042*1d05cddcSAtari911 4043*1d05cddcSAtari911 // Delete from each file 4044*1d05cddcSAtari911 foreach ($fileGroups as $file => $events) { 4045*1d05cddcSAtari911 if (!file_exists($file)) continue; 4046*1d05cddcSAtari911 4047*1d05cddcSAtari911 $json = file_get_contents($file); 4048*1d05cddcSAtari911 $data = json_decode($json, true); 4049*1d05cddcSAtari911 4050*1d05cddcSAtari911 if (!$data) continue; 4051*1d05cddcSAtari911 4052*1d05cddcSAtari911 // Remove events 4053*1d05cddcSAtari911 foreach ($events as $evt) { 4054*1d05cddcSAtari911 if (isset($data[$evt['date']])) { 4055*1d05cddcSAtari911 $data[$evt['date']] = array_filter($data[$evt['date']], function($e) use ($evt) { 4056*1d05cddcSAtari911 return $e['id'] !== $evt['id']; 4057*1d05cddcSAtari911 }); 4058*1d05cddcSAtari911 4059*1d05cddcSAtari911 // Remove date key if empty 4060*1d05cddcSAtari911 if (empty($data[$evt['date']])) { 4061*1d05cddcSAtari911 unset($data[$evt['date']]); 4062*1d05cddcSAtari911 } 4063*1d05cddcSAtari911 4064*1d05cddcSAtari911 $deletedCount++; 4065*1d05cddcSAtari911 } 4066*1d05cddcSAtari911 } 4067*1d05cddcSAtari911 4068*1d05cddcSAtari911 // Save file or delete if empty 4069*1d05cddcSAtari911 if (empty($data)) { 4070*1d05cddcSAtari911 unlink($file); 4071*1d05cddcSAtari911 } else { 4072*1d05cddcSAtari911 file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT)); 4073*1d05cddcSAtari911 } 4074*1d05cddcSAtari911 } 4075*1d05cddcSAtari911 4076*1d05cddcSAtari911 // Clear cache 4077*1d05cddcSAtari911 $cacheFile = DOKU_PLUGIN . 'calendar/.event_stats_cache'; 4078*1d05cddcSAtari911 if (file_exists($cacheFile)) { 4079*1d05cddcSAtari911 unlink($cacheFile); 4080*1d05cddcSAtari911 } 4081*1d05cddcSAtari911 4082*1d05cddcSAtari911 $message = "Cleanup complete! Deleted $deletedCount event(s). Backup created: " . basename($backupFile); 4083*1d05cddcSAtari911 $redirectUrl = DOKU_URL . 'doku.php?do=admin&page=calendar&tab=manage&msg=' . urlencode($message) . '&msgtype=success'; 4084*1d05cddcSAtari911 header('Location: ' . $redirectUrl, true, 303); 4085*1d05cddcSAtari911 exit; 4086*1d05cddcSAtari911 } 4087*1d05cddcSAtari911 4088*1d05cddcSAtari911 private function findEventsToCleanup($cleanupType, $namespaceFilter) { 4089*1d05cddcSAtari911 global $INPUT; 4090*1d05cddcSAtari911 4091*1d05cddcSAtari911 $eventsToDelete = []; 4092*1d05cddcSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 4093*1d05cddcSAtari911 4094*1d05cddcSAtari911 $debug = []; 4095*1d05cddcSAtari911 $debug['scanned_dirs'] = []; 4096*1d05cddcSAtari911 $debug['found_files'] = []; 4097*1d05cddcSAtari911 4098*1d05cddcSAtari911 // Calculate cutoff date for age-based cleanup 4099*1d05cddcSAtari911 $cutoffDate = null; 4100*1d05cddcSAtari911 if ($cleanupType === 'age') { 4101*1d05cddcSAtari911 $ageValue = $INPUT->int('age_value', 6); 4102*1d05cddcSAtari911 $ageUnit = $INPUT->str('age_unit', 'months'); 4103*1d05cddcSAtari911 4104*1d05cddcSAtari911 if ($ageUnit === 'years') { 4105*1d05cddcSAtari911 $ageValue *= 12; // Convert to months 4106*1d05cddcSAtari911 } 4107*1d05cddcSAtari911 4108*1d05cddcSAtari911 $cutoffDate = date('Y-m-d', strtotime("-$ageValue months")); 4109*1d05cddcSAtari911 $debug['cutoff_date'] = $cutoffDate; 4110*1d05cddcSAtari911 } 4111*1d05cddcSAtari911 4112*1d05cddcSAtari911 // Get date range for range-based cleanup 4113*1d05cddcSAtari911 $rangeStart = $cleanupType === 'range' ? $INPUT->str('range_start', '') : null; 4114*1d05cddcSAtari911 $rangeEnd = $cleanupType === 'range' ? $INPUT->str('range_end', '') : null; 4115*1d05cddcSAtari911 4116*1d05cddcSAtari911 // Get status filters 4117*1d05cddcSAtari911 $deleteCompleted = $cleanupType === 'status' && $INPUT->bool('delete_completed', false); 4118*1d05cddcSAtari911 $deletePast = $cleanupType === 'status' && $INPUT->bool('delete_past', false); 4119*1d05cddcSAtari911 4120*1d05cddcSAtari911 // Check root calendar directory first (blank/default namespace) 4121*1d05cddcSAtari911 $rootCalendarDir = $dataDir . 'calendar'; 4122*1d05cddcSAtari911 $debug['root_calendar_dir'] = $rootCalendarDir; 4123*1d05cddcSAtari911 $debug['root_exists'] = is_dir($rootCalendarDir); 4124*1d05cddcSAtari911 4125*1d05cddcSAtari911 if (is_dir($rootCalendarDir)) { 4126*1d05cddcSAtari911 if (!$namespaceFilter || $namespaceFilter === '' || $namespaceFilter === 'default') { 4127*1d05cddcSAtari911 $debug['scanned_dirs'][] = $rootCalendarDir; 4128*1d05cddcSAtari911 $files = glob($rootCalendarDir . '/*.json'); 4129*1d05cddcSAtari911 $debug['found_files'] = array_merge($debug['found_files'], $files); 4130*1d05cddcSAtari911 $this->processCalendarFiles($rootCalendarDir, '', $eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast); 4131*1d05cddcSAtari911 } 4132*1d05cddcSAtari911 } 4133*1d05cddcSAtari911 4134*1d05cddcSAtari911 // Scan all namespace directories 4135*1d05cddcSAtari911 $namespaceDirs = glob($dataDir . '*', GLOB_ONLYDIR); 4136*1d05cddcSAtari911 $debug['namespace_dirs_found'] = $namespaceDirs; 4137*1d05cddcSAtari911 4138*1d05cddcSAtari911 foreach ($namespaceDirs as $nsDir) { 4139*1d05cddcSAtari911 $namespace = basename($nsDir); 4140*1d05cddcSAtari911 4141*1d05cddcSAtari911 // Skip the root 'calendar' dir (already processed above) 4142*1d05cddcSAtari911 if ($namespace === 'calendar') continue; 4143*1d05cddcSAtari911 4144*1d05cddcSAtari911 // Check namespace filter 4145*1d05cddcSAtari911 if ($namespaceFilter && strpos($namespace, $namespaceFilter) === false) { 4146*1d05cddcSAtari911 continue; 4147*1d05cddcSAtari911 } 4148*1d05cddcSAtari911 4149*1d05cddcSAtari911 $calendarDir = $nsDir . '/calendar'; 4150*1d05cddcSAtari911 $debug['checked_calendar_dirs'][] = $calendarDir; 4151*1d05cddcSAtari911 4152*1d05cddcSAtari911 if (!is_dir($calendarDir)) { 4153*1d05cddcSAtari911 $debug['missing_calendar_dirs'][] = $calendarDir; 4154*1d05cddcSAtari911 continue; 4155*1d05cddcSAtari911 } 4156*1d05cddcSAtari911 4157*1d05cddcSAtari911 $debug['scanned_dirs'][] = $calendarDir; 4158*1d05cddcSAtari911 $files = glob($calendarDir . '/*.json'); 4159*1d05cddcSAtari911 $debug['found_files'] = array_merge($debug['found_files'], $files); 4160*1d05cddcSAtari911 $this->processCalendarFiles($calendarDir, $namespace, $eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast); 4161*1d05cddcSAtari911 } 4162*1d05cddcSAtari911 4163*1d05cddcSAtari911 // Store debug info globally for preview 4164*1d05cddcSAtari911 $this->_cleanupDebug = $debug; 4165*1d05cddcSAtari911 4166*1d05cddcSAtari911 return $eventsToDelete; 4167*1d05cddcSAtari911 } 4168*1d05cddcSAtari911 4169*1d05cddcSAtari911 private function processCalendarFiles($calendarDir, $namespace, &$eventsToDelete, $cleanupType, $cutoffDate, $rangeStart, $rangeEnd, $deleteCompleted, $deletePast) { 4170*1d05cddcSAtari911 foreach (glob($calendarDir . '/*.json') as $file) { 4171*1d05cddcSAtari911 $json = file_get_contents($file); 4172*1d05cddcSAtari911 $data = json_decode($json, true); 4173*1d05cddcSAtari911 4174*1d05cddcSAtari911 if (!$data) continue; 4175*1d05cddcSAtari911 4176*1d05cddcSAtari911 foreach ($data as $date => $dateEvents) { 4177*1d05cddcSAtari911 foreach ($dateEvents as $event) { 4178*1d05cddcSAtari911 $shouldDelete = false; 4179*1d05cddcSAtari911 4180*1d05cddcSAtari911 // Age-based 4181*1d05cddcSAtari911 if ($cleanupType === 'age' && $cutoffDate && $date < $cutoffDate) { 4182*1d05cddcSAtari911 $shouldDelete = true; 4183*1d05cddcSAtari911 } 4184*1d05cddcSAtari911 4185*1d05cddcSAtari911 // Range-based 4186*1d05cddcSAtari911 if ($cleanupType === 'range' && $rangeStart && $rangeEnd) { 4187*1d05cddcSAtari911 if ($date >= $rangeStart && $date <= $rangeEnd) { 4188*1d05cddcSAtari911 $shouldDelete = true; 4189*1d05cddcSAtari911 } 4190*1d05cddcSAtari911 } 4191*1d05cddcSAtari911 4192*1d05cddcSAtari911 // Status-based 4193*1d05cddcSAtari911 if ($cleanupType === 'status') { 4194*1d05cddcSAtari911 $isTask = isset($event['isTask']) && $event['isTask']; 4195*1d05cddcSAtari911 $isCompleted = isset($event['completed']) && $event['completed']; 4196*1d05cddcSAtari911 $isPast = $date < date('Y-m-d'); 4197*1d05cddcSAtari911 4198*1d05cddcSAtari911 if ($deleteCompleted && $isTask && $isCompleted) { 4199*1d05cddcSAtari911 $shouldDelete = true; 4200*1d05cddcSAtari911 } 4201*1d05cddcSAtari911 if ($deletePast && !$isTask && $isPast) { 4202*1d05cddcSAtari911 $shouldDelete = true; 4203*1d05cddcSAtari911 } 4204*1d05cddcSAtari911 } 4205*1d05cddcSAtari911 4206*1d05cddcSAtari911 if ($shouldDelete) { 4207*1d05cddcSAtari911 $eventsToDelete[] = [ 4208*1d05cddcSAtari911 'id' => $event['id'], 4209*1d05cddcSAtari911 'title' => $event['title'], 4210*1d05cddcSAtari911 'date' => $date, 4211*1d05cddcSAtari911 'namespace' => $namespace ?: 'default', 4212*1d05cddcSAtari911 'file' => $file 4213*1d05cddcSAtari911 ]; 4214*1d05cddcSAtari911 } 4215*1d05cddcSAtari911 } 4216*1d05cddcSAtari911 } 4217*1d05cddcSAtari911 } 4218*1d05cddcSAtari911 } 4219*1d05cddcSAtari911} 4220