nilsteampassnet /
TeamPass
This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include, or for example
via PHP's auto-loading mechanism.
| 1 | <?php |
||
| 2 | /** |
||
| 3 | * Teampass - a collaborative passwords manager. |
||
| 4 | * --- |
||
| 5 | * This file is part of the TeamPass project. |
||
| 6 | * |
||
| 7 | * TeamPass is free software: you can redistribute it and/or modify it |
||
| 8 | * under the terms of the GNU General Public License as published by |
||
| 9 | * the Free Software Foundation, version 3 of the License. |
||
| 10 | * |
||
| 11 | * TeamPass is distributed in the hope that it will be useful, |
||
| 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
| 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
| 14 | * GNU General Public License for more details. |
||
| 15 | * |
||
| 16 | * You should have received a copy of the GNU General Public License |
||
| 17 | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
||
| 18 | * |
||
| 19 | * Certain components of this file may be under different licenses. For |
||
| 20 | * details, see the `licenses` directory or individual file headers. |
||
| 21 | * --- |
||
| 22 | * @file background_tasks___handler.php |
||
| 23 | * @author Nils Laumaillé ([email protected]) |
||
| 24 | * @copyright 2009-2025 Teampass.net |
||
| 25 | * @license GPL-3.0 |
||
| 26 | * @see https://www.teampass.net |
||
| 27 | */ |
||
| 28 | |||
| 29 | use Symfony\Component\Process\Process; |
||
| 30 | use TeampassClasses\ConfigManager\ConfigManager; |
||
| 31 | |||
| 32 | require_once __DIR__.'/../sources/main.functions.php'; |
||
| 33 | require_once __DIR__ . '/taskLogger.php'; |
||
| 34 | |||
| 35 | class BackgroundTasksHandler { |
||
| 36 | private $settings; |
||
| 37 | private $logger; |
||
| 38 | private $maxParallelTasks; |
||
| 39 | private $maxExecutionTime; |
||
| 40 | private $batchSize; |
||
| 41 | private $maxTimeBeforeRemoval; |
||
| 42 | |||
| 43 | public function __construct(array $settings) { |
||
| 44 | $this->settings = $settings; |
||
| 45 | $this->logger = new TaskLogger($settings, LOG_TASKS_FILE); |
||
| 46 | $this->maxParallelTasks = $settings['max_parallel_tasks'] ?? 2; |
||
| 47 | $this->maxExecutionTime = $settings['task_maximum_run_time'] ?? 600; |
||
| 48 | $this->batchSize = $settings['task_batch_size'] ?? 50; |
||
| 49 | $this->maxTimeBeforeRemoval = isset($settings['history_duration']) ? ($settings['history_duration'] * 24 * 3600) : (15 * 24 * 3600); |
||
| 50 | } |
||
| 51 | |||
| 52 | /** |
||
| 53 | * Main function to process background tasks |
||
| 54 | */ |
||
| 55 | public function processBackgroundTasks() { |
||
| 56 | // Prevent multiple concurrent executions |
||
| 57 | if (!$this->acquireProcessLock()) { |
||
| 58 | if (LOG_TASKS=== true) $this->logger->log('Process already running', 'INFO'); |
||
| 59 | return false; |
||
| 60 | } |
||
| 61 | |||
| 62 | try { |
||
| 63 | $this->cleanupStaleTasks(); |
||
| 64 | $this->processTaskBatches(); |
||
| 65 | $this->performMaintenanceTasks(); |
||
| 66 | } catch (Exception $e) { |
||
| 67 | if (LOG_TASKS=== true) $this->logger->log('Task processing error: ' . $e->getMessage(), 'ERROR'); |
||
| 68 | } finally { |
||
| 69 | $this->releaseProcessLock(); |
||
| 70 | } |
||
| 71 | } |
||
| 72 | |||
| 73 | /** |
||
| 74 | * Acquire a lock to prevent multiple instances of this script from running simultaneously. |
||
| 75 | * @return bool |
||
| 76 | */ |
||
| 77 | private function acquireProcessLock(): bool { |
||
| 78 | $lockFile = empty(TASKS_LOCK_FILE) ? __DIR__.'/../files/teampass_background_tasks.lock' : TASKS_LOCK_FILE; |
||
| 79 | |||
| 80 | $fp = fopen($lockFile, 'w'); |
||
| 81 | |||
| 82 | if (!flock($fp, LOCK_EX | LOCK_NB)) { |
||
| 83 | return false; |
||
| 84 | } |
||
| 85 | |||
| 86 | fwrite($fp, (string)getmypid()); |
||
| 87 | return true; |
||
| 88 | } |
||
| 89 | |||
| 90 | /** |
||
| 91 | * Release the lock file. |
||
| 92 | */ |
||
| 93 | private function releaseProcessLock() { |
||
| 94 | $lockFile = empty(TASKS_LOCK_FILE) ? __DIR__.'/../files/teampass_background_tasks.lock' : TASKS_LOCK_FILE; |
||
| 95 | if (file_exists($lockFile)) { |
||
| 96 | unlink($lockFile); |
||
| 97 | } |
||
| 98 | } |
||
| 99 | |||
| 100 | /** |
||
| 101 | * Cleanup stale tasks that have been running for too long or are marked as failed. |
||
| 102 | */ |
||
| 103 | private function cleanupStaleTasks() { |
||
| 104 | // Mark tasks as failed if they've been running too long |
||
| 105 | DB::query( |
||
| 106 | 'UPDATE ' . prefixTable('background_tasks') . ' |
||
| 107 | SET is_in_progress = -1, |
||
| 108 | finished_at = %i, |
||
| 109 | status = "failed", |
||
| 110 | error_message = "Task exceeded maximum execution time of '.$this->maxExecutionTime.' seconds" |
||
| 111 | WHERE is_in_progress = 1 |
||
| 112 | AND updated_at < %i', |
||
| 113 | time(), |
||
| 114 | time() - $this->maxExecutionTime |
||
| 115 | ); |
||
| 116 | |||
| 117 | // Remove very old failed tasks |
||
| 118 | DB::query( |
||
| 119 | 'DELETE t, st FROM ' . prefixTable('background_tasks') . ' t |
||
| 120 | INNER JOIN ' . prefixTable('background_subtasks') . ' st ON (t.increment_id = st.task_id) |
||
| 121 | WHERE t.finished_at < %i |
||
| 122 | AND t.status = %s', |
||
| 123 | time() - $this->maxTimeBeforeRemoval, |
||
| 124 | "failed" |
||
| 125 | ); |
||
| 126 | } |
||
| 127 | |||
| 128 | /** |
||
| 129 | * Process batches of tasks. |
||
| 130 | * This method fetches tasks from the database and processes them in parallel. |
||
| 131 | */ |
||
| 132 | private function processTaskBatches() { |
||
| 133 | $runningTasks = $this->countRunningTasks(); |
||
| 134 | |||
| 135 | // Check if the maximum number of parallel tasks is reached |
||
| 136 | if ($runningTasks >= $this->maxParallelTasks) { |
||
| 137 | if (LOG_TASKS=== true) $this->logger->log('Wait ... '.$runningTasks.' out of '.$this->maxParallelTasks.' are already running ', 'INFO'); |
||
| 138 | return; |
||
| 139 | } |
||
| 140 | |||
| 141 | $availableSlotsCount = $this->maxParallelTasks - $runningTasks; |
||
| 142 | |||
| 143 | // Fetch next batch of tasks |
||
| 144 | $tasks = DB::query( |
||
| 145 | 'SELECT increment_id, process_type, arguments |
||
| 146 | FROM ' . prefixTable('background_tasks') . ' |
||
| 147 | WHERE is_in_progress = 0 |
||
| 148 | AND (finished_at IS NULL OR finished_at = "") |
||
| 149 | ORDER BY increment_id ASC |
||
| 150 | LIMIT %i', |
||
| 151 | min($this->batchSize, $availableSlotsCount) |
||
| 152 | ); |
||
| 153 | |||
| 154 | foreach ($tasks as $task) { |
||
| 155 | if (LOG_TASKS=== true) $this->logger->log('Launching '.$task['increment_id'], 'INFO'); |
||
| 156 | $this->processIndividualTask($task); |
||
| 157 | } |
||
| 158 | } |
||
| 159 | |||
| 160 | /** |
||
| 161 | * Process an individual task. |
||
| 162 | * This method updates the task status in the database and starts a new process for the task. |
||
| 163 | * @param array $task The task to process. |
||
| 164 | */ |
||
| 165 | private function processIndividualTask(array $task) { |
||
| 166 | if (LOG_TASKS=== true) $this->logger->log('Starting task: ' . print_r($task, true), 'INFO'); |
||
| 167 | |||
| 168 | // Store progress in the database |
||
| 169 | DB::update( |
||
| 170 | prefixTable('background_tasks'), |
||
| 171 | [ |
||
| 172 | 'is_in_progress' => 1, |
||
| 173 | 'started_at' => time(), |
||
| 174 | 'updated_at' => time(), |
||
| 175 | 'status' => 'in_progress' |
||
| 176 | ], |
||
| 177 | 'increment_id = %i', |
||
| 178 | $task['increment_id'] |
||
| 179 | ); |
||
| 180 | |||
| 181 | // Prepare process |
||
| 182 | $process = new Process([ |
||
| 183 | PHP_BINARY, |
||
| 184 | __DIR__ . '/background_tasks___worker.php', |
||
| 185 | $task['increment_id'], |
||
| 186 | $task['process_type'], |
||
| 187 | $task['arguments'] |
||
| 188 | ]); |
||
| 189 | |||
| 190 | // Launch process |
||
| 191 | try{ |
||
| 192 | $process->setTimeout($this->maxExecutionTime); |
||
| 193 | $process->mustRun(); |
||
| 194 | |||
| 195 | } catch (Exception $e) { |
||
| 196 | if (LOG_TASKS=== true) $this->logger->log('Error launching task: ' . $e->getMessage(), 'ERROR'); |
||
| 197 | DB::update( |
||
| 198 | prefixTable('background_tasks'), |
||
| 199 | [ |
||
| 200 | 'is_in_progress' => -1, |
||
| 201 | 'finished_at' => time(), |
||
| 202 | 'status' => 'failed', |
||
| 203 | 'error_message' => is_null($e->getMessage()) ? 'Unknown error' : $e->getMessage() |
||
|
0 ignored issues
–
show
introduced
by
Loading history...
|
|||
| 204 | ], |
||
| 205 | 'increment_id = %i', |
||
| 206 | $task['increment_id'] |
||
| 207 | ); |
||
| 208 | } |
||
| 209 | } |
||
| 210 | |||
| 211 | /** |
||
| 212 | * Count the number of currently running tasks. |
||
| 213 | * @return int The number of running tasks. |
||
| 214 | */ |
||
| 215 | private function countRunningTasks(): int { |
||
| 216 | return DB::queryFirstField( |
||
| 217 | 'SELECT COUNT(*) |
||
| 218 | FROM ' . prefixTable('background_tasks') . ' |
||
| 219 | WHERE is_in_progress = 1' |
||
| 220 | ); |
||
| 221 | } |
||
| 222 | |||
| 223 | /** |
||
| 224 | * Perform maintenance tasks. |
||
| 225 | * This method cleans up old items, expired tokens, and finished tasks. |
||
| 226 | */ |
||
| 227 | private function performMaintenanceTasks() { |
||
| 228 | $this->cleanMultipleItemsEdition(); |
||
| 229 | $this->handleItemTokensExpiration(); |
||
| 230 | $this->cleanOldFinishedTasks(); |
||
| 231 | } |
||
| 232 | |||
| 233 | /** |
||
| 234 | * Clean up multiple items edition. |
||
| 235 | * This method removes duplicate entries in the items_edition table. |
||
| 236 | */ |
||
| 237 | private function cleanMultipleItemsEdition() { |
||
| 238 | DB::query( |
||
| 239 | 'DELETE i1 FROM ' . prefixTable('items_edition') . ' i1 |
||
| 240 | JOIN ( |
||
| 241 | SELECT user_id, item_id, MIN(timestamp) AS oldest_timestamp |
||
| 242 | FROM ' . prefixTable('items_edition') . ' |
||
| 243 | GROUP BY user_id, item_id |
||
| 244 | ) i2 ON i1.user_id = i2.user_id AND i1.item_id = i2.item_id |
||
| 245 | WHERE i1.timestamp > i2.oldest_timestamp' |
||
| 246 | ); |
||
| 247 | } |
||
| 248 | |||
| 249 | /** |
||
| 250 | * Handle item tokens expiration. |
||
| 251 | * This method removes expired tokens from the items_edition table. |
||
| 252 | */ |
||
| 253 | private function handleItemTokensExpiration() { |
||
| 254 | DB::query( |
||
| 255 | 'DELETE FROM ' . prefixTable('items_edition') . ' |
||
| 256 | WHERE timestamp < %i', |
||
| 257 | time() - ($this->settings['delay_item_edition'] * 60 ?: EDITION_LOCK_PERIOD) |
||
| 258 | ); |
||
| 259 | } |
||
| 260 | |||
| 261 | /** |
||
| 262 | * Clean up old finished tasks. |
||
| 263 | * This method removes tasks that have been completed for too long. |
||
| 264 | */ |
||
| 265 | private function cleanOldFinishedTasks() { |
||
| 266 | // Timestamp cutoff for removal |
||
| 267 | $cutoffTimestamp = time() - $this->maxTimeBeforeRemoval; |
||
| 268 | |||
| 269 | // 1. Get all finished tasks older than the cutoff timestamp |
||
| 270 | // and that are not in progress |
||
| 271 | $tasks = DB::query( |
||
| 272 | 'SELECT increment_id FROM ' . prefixTable('background_tasks') . ' |
||
| 273 | WHERE status = %s AND is_in_progress = %i AND finished_at < %s', |
||
| 274 | 'completed', |
||
| 275 | -1, |
||
| 276 | $cutoffTimestamp |
||
| 277 | ); |
||
| 278 | |||
| 279 | if (empty($tasks)) { |
||
| 280 | return; |
||
| 281 | } |
||
| 282 | |||
| 283 | $taskIds = array_column($tasks, 'increment_id'); |
||
| 284 | |||
| 285 | // 2. Delete all subtasks related to these tasks |
||
| 286 | DB::query( |
||
| 287 | 'DELETE FROM ' . prefixTable('background_subtasks') . ' |
||
| 288 | WHERE task_id IN %ls', |
||
| 289 | $taskIds |
||
| 290 | ); |
||
| 291 | |||
| 292 | // 3. Delete the tasks themselves |
||
| 293 | DB::query( |
||
| 294 | 'DELETE FROM ' . prefixTable('background_tasks') . ' |
||
| 295 | WHERE increment_id IN %ls', |
||
| 296 | $taskIds |
||
| 297 | ); |
||
| 298 | |||
| 299 | if (LOG_TASKS=== true) $this->logger->log('Old finished tasks cleaned: ' . count($taskIds), 'INFO'); |
||
| 300 | } |
||
| 301 | } |
||
| 302 | |||
| 303 | |||
| 304 | |||
| 305 | // Main execution |
||
| 306 | try { |
||
| 307 | $configManager = new ConfigManager(); |
||
| 308 | $settings = $configManager->getAllSettings(); |
||
| 309 | |||
| 310 | $tasksHandler = new BackgroundTasksHandler($settings); |
||
| 311 | $tasksHandler->processBackgroundTasks(); |
||
| 312 | } catch (Exception $e) { |
||
| 313 | error_log('Teampass Background Tasks Error: ' . $e->getMessage()); |
||
| 314 | } |