$params */ public function actor(): string { return $this->session->getUser()?->getUID() ?? (PHP_SAPI === 'cli' ? 'occ' : 'unknown'); } public function securityEvent(string $event, array $params = [], bool $notify = true): void { $actor = $this->actor(); $params = ['actor' => $actor] + $params; $this->events->dispatchTyped(new \OCP\Log\Audit\CriticalActionPerformedEvent( 'NG Backup: %s by %s ' . json_encode(array_diff_key($params, ['actor' => 1]), JSON_UNESCAPED_SLASHES), ['event' => $event, 'actor' => $actor])); if ($notify) { $this->notifyAdmins('security_event', $event . '-' . time(), ['event' => $event] + $params); } } public function runFailed(Run $run, string $targetName): void { $this->notifyAdmins('run_failed', 'run-' . $run->getId(), ['kind' => $run->getKind(), 'target' => $targetName, 'error' => mb_strimwidth((string)$run->getError(), 0, 300, '…')]); } /** A backup that finished but is missing unreadable files, or holds files that changed while read. */ public function runWarnings(Run $run, string $targetName, int $unreadable, int $changing, array $examples): void { $this->notifyAdmins('run_warnings', 'run-' . $run->getId(), ['target' => $targetName, 'unreadable' => (string)$unreadable, 'changing' => (string)$changing, 'examples' => mb_strimwidth(implode(', ', array_merge($examples['unreadable'], $examples['changing'])), 0, 300, '…')]); } /** * A backup that finished but left out files or folders because of their name. * * @param list $examples */ public function runBadNames(Run $run, string $targetName, int $count, array $examples): void { $this->notifyAdmins('run_bad_names', 'run-' . $run->getId(), ['target' => $targetName, 'count' => (string)$count, 'examples' => mb_strimwidth(implode(', ', $examples), 0, 300, '…')]); } /** * Remember how much is stored on a location (for the settings page) and warn administrators * once a day when it is at 80% of its limit or more. */ public function storedBytes(Target $target, int $bytes): void { $this->appConfig->setValueString(Application::APP_ID, 'stored_' . $target->getId(), (string)$bytes); $limit = $target->getMaxBytes(); if ($limit === null || $limit <= 0 || $bytes < 0.8 * $limit) { return; } $key = 'quota_notified_' . $target->getId(); if ($this->appConfig->getValueString(Application::APP_ID, $key, '') === date('Y-m-d')) { return; } $this->appConfig->setValueString(Application::APP_ID, $key, date('Y-m-d')); $this->notifyAdmins('quota_warning', 'quota-' . $target->getId() . '-' . date('Y-m-d'), [ 'target' => $target->getName(), 'percent' => (string)round($bytes / $limit * 100), 'used' => self::gib($bytes), 'limit' => self::gib($limit)]); } private static function gib(int $bytes): string { $g = $bytes / 1073741824; return sprintf($g < 10 ? '%.2f GiB' : '%.1f GiB', $g); } public function lastStoredBytes(Target $target): ?int { $v = $this->appConfig->getValueString(Application::APP_ID, 'stored_' . $target->getId(), ''); return $v === '' ? null : (int)$v; } /** * Whether it is time to remind administrators of a location without a schedule: once a week * while another location is still backed up, every day when none is. An hour of margin, so * that the moment of the cron run does not push a reminder to the next day. */ public static function offReminderDue(int $last, int $now, bool $othersActive): bool { return $now - $last >= ($othersActive ? 7 * 86400 : 86400) - 3600; } public function checkOff(Target $target, bool $othersActive): void { if ((string)$target->getSchedule() !== '') { return; } $key = 'off_notified_' . $target->getId(); $now = time(); $last = max((int)$target->getScheduleSince(), $this->appConfig->getValueInt(Application::APP_ID, $key, 0)); if (!self::offReminderDue($last, $now, $othersActive)) { return; } $this->appConfig->setValueInt(Application::APP_ID, $key, $now); $this->notifyAdmins('schedule_off', 'off-' . $target->getId() . '-' . date('Y-m-d', $now), ['target' => $target->getName(), 'others' => $othersActive ? '1' : '0']); } /** At most one notification per location per day. */ public function checkStale(Target $target): void { if ((string)$target->getSchedule() === '') { return; // no schedule is a choice, as for a location that is only connected now and then } $last =$this->runs->lastFinished($target->getId(), BackupService::KIND_FULL); $lastTime = $last?->getFinishedAt() ?? $target->getCreatedAt(); if (time() - $lastTime < self::STALE_AFTER || $this->runs->findRunning($target->getId(), BackupService::KIND_FULL) !== []) { return; } $key = 'stale_notified_' . $target->getId(); if ($this->appConfig->getValueString(Application::APP_ID, $key, '') === date('Y-m-d')) { return; } $this->appConfig->setValueString(Application::APP_ID, $key, date('Y-m-d')); $this->notifyAdmins('backup_stale', 'stale-' . $target->getId() . '-' . date('Y-m-d'), ['target' => $target->getName(), 'since' => $last ? date('Y-m-d H:i', $lastTime) : '']); } private function notifyAdmins(string $subject, string $objectId, array $params): void { foreach ($this->groups->get('admin')?->getUsers() ?? [] as $admin) { $n = $this->notifications->createNotification(); $n->setApp(Application::APP_ID) ->setUser($admin->getUID()) ->setDateTime(new \DateTime()) ->setObject($subject, $objectId) ->setSubject($subject, $params); $this->notifications->notify($n); } } }