19declare(strict_types=1);
56 return 'Migrate Mail Attachments to IRSS';
85 $this->statistics = [];
86 $started = microtime(
true);
91 }
catch (Throwable
$e) {
92 $this->
error(
'Migration aborted', [
'step' => $this->step_number],
$e);
97 if ($this->statistics !== []) {
98 $this->
info(
'Step finished', array_merge(
99 [
'step' => $this->step_number,
'seconds' => round(microtime(
true) - $started, 2)],
115 $mail_path = rtrim($this->helper->getClientDataDir(),
'/') .
'/mail';
118 $absolute_path = $mail_path .
'/' . $relative_path;
119 if (trim($relative_path,
'/. ') ===
'' || str_contains($relative_path,
'..')) {
120 $this->
skipPath($relative_path,
'Invalid directory name');
125 if (!is_dir($absolute_path)) {
126 $this->
skipPath($relative_path,
'Directory does not exist');
132 if ($filenames === []) {
133 $this->
skipPath($relative_path,
'No file of the directory could be stored');
140 if ($rcid ===
null) {
141 $this->
warning(
'Directory was not migrated because not every file could be stored', [
142 'path' => $relative_path,
143 'files' =>
count($filenames),
153 $this->
count(
'directories_migrated');
164 string $absolute_path,
168 $collection = $this->helper->getCollectionBuilder()->new($owner_id);
170 foreach ($filenames as $file_name) {
171 $resource_id = $this->helper->movePathToStorage(
172 $absolute_path .
'/' . $file_name,
175 static fn(
string $name):
string => md5($name),
183 $copies[] = $resource_id;
184 $collection->add($resource_id);
187 if ($collection->count() !==
count($filenames)
188 || !$this->helper->getCollectionBuilder()->store($collection)) {
194 return $collection->getIdentification();
204 foreach ($resource_ids as $resource_id) {
205 $this->helper->getManager()->remove($resource_id, $this->helper->getStakeholder());
215 foreach (
new \DirectoryIterator($absolute_path) as $file_info) {
216 if (!$file_info->isFile()) {
219 $filenames[] = $file_info->getFilename();
228 foreach ($this->listDirectoryFiles($absolute_path) as $file_name) {
229 unlink($absolute_path .
'/' . $file_name);
231 rmdir($absolute_path);
239 $db = $this->helper->getDatabase();
240 $db->setLimit(self::PATHS_PER_STEP);
242 'SELECT ma.path FROM mail_attachment ma
243 LEFT JOIN mail m ON m.mail_id = ma.mail_id
244 WHERE (ma.rcid IS NULL OR ma.rcid = "")
245 AND ma.path IS NOT NULL AND ma.path != ""
247 ORDER BY MAX(m.send_time) DESC'
251 while ($row =
$db->fetchAssoc(
$res)) {
252 $paths[] = (string) $row[
'path'];
258 private function skipPath(
string $relative_path,
string $reason): void
260 $mail_ids = $this->mailIdsOfPath($relative_path);
261 $this->markPathAsSkipped($relative_path);
262 $this->count(
'directories_skipped');
264 $this->warning(
'Directory skipped: ' . $reason, [
265 'path' => $relative_path,
266 'mails' => count($mail_ids),
267 'mail_ids' => array_slice($mail_ids, 0, 20),
276 $db = $this->helper->getDatabase();
278 'SELECT mail_id FROM mail_attachment WHERE path = %s',
284 while ($row =
$db->fetchAssoc(
$res)) {
285 $mail_ids[] = (
int) $row[
'mail_id'];
301 $db = $this->helper->getDatabase();
302 $db->setLimit(self::MAILS_PER_STEP);
304 'SELECT m.mail_id, m.user_id, m.attachments, fo.m_type FROM mail m
305 LEFT JOIN mail_attachment ma ON ma.mail_id = m.mail_id
306 LEFT JOIN mail_obj_data fo ON fo.obj_id = m.folder_id
307 WHERE ma.mail_id IS NULL
309 ORDER BY m.send_time DESC, m.mail_id DESC'
312 $mail_path = rtrim($this->helper->getClientDataDir(),
'/') .
'/mail';
313 while ($row =
$db->fetchAssoc(
$res)) {
314 $this->migrateSerializedMail(
315 (
int) $row[
'mail_id'],
316 (
int) $row[
'user_id'],
317 (
string) $row[
'attachments'],
320 $row[
'm_type'] ??
null,
331 string $raw_attachments,
333 bool $references_pool
336 if ($attachments ===
null || !$attachments->isLegacy()) {
337 if ($attachments ===
null) {
338 $this->warning(
'Attachments of mail could not be read and have been removed', [
339 'mail_id' => $mail_id,
340 'value' => substr($raw_attachments, 0, 200),
343 $this->clearMailAttachmentsColumn($mail_id);
349 if (!$references_pool) {
350 $this->warning(
'Mail outside drafts and outbox has no attachment directory, its attachments have been removed', [
351 'mail_id' => $mail_id,
352 'files' => $attachments->legacyFilenames(),
354 $this->clearMailAttachmentsColumn($mail_id);
359 $rcid = $this->migratePoolFilenamesToCollection($attachments->legacyFilenames(),
$user_id, $mail_path, $mail_id);
360 if ($rcid ===
null) {
361 $this->warning(
'None of the pool files referenced by the mail exists, its attachments have been removed', [
362 'mail_id' => $mail_id,
363 'files' => $attachments->legacyFilenames(),
365 $this->clearMailAttachmentsColumn($mail_id);
370 $db = $this->helper->getDatabase();
372 'INSERT INTO mail_attachment (mail_id, path, rcid) VALUES (%s, %s, %s)',
374 [$mail_id,
'', $rcid->serialize()]
381 $this->count(
'drafts_migrated');
396 $collection = $this->helper->getCollectionBuilder()->new(
$user_id);
401 $this->warning(
'Pool file referenced by the mail does not exist', [
402 'mail_id' => $mail_id,
410 $resource_id = $this->helper->movePathToStorage(
419 $this->markConfidential($resource_id);
420 $collection->add($resource_id);
424 if ($collection->count() === 0) {
428 if (!$this->helper->getCollectionBuilder()->store($collection)) {
429 $this->discardResources(array_values($collection->getResourceIdentifications()));
434 return $collection->getIdentification();
439 $this->helper->getManager()->setConfidential($identification,
true);
444 foreach ($this->helper->getCollectionBuilder()->get($rcid)->getResourceIdentifications() as $identification) {
445 $this->markConfidential($identification);
451 $this->helper->getDatabase()->update(
464 $db = $this->helper->getDatabase();
466 $path_count = (
int) $db->fetchAssoc(
468 'SELECT COUNT(DISTINCT path) cnt FROM mail_attachment
469 WHERE (rcid IS NULL OR rcid = "")
470 AND path IS NOT NULL AND path != ""'
474 $mail_count = (
int) $db->fetchAssoc(
476 'SELECT COUNT(m.mail_id) cnt FROM mail m
477 LEFT JOIN mail_attachment ma ON ma.mail_id = m.mail_id
478 WHERE ma.mail_id IS NULL
484 (
int) ceil($path_count / self::PATHS_PER_STEP),
485 (
int) ceil($mail_count / self::MAILS_PER_STEP)
488 $this->
info(
'Remaining', [
489 'directories' => $path_count,
490 'drafts' => $mail_count,
491 'steps' => $remaining,
499 $db = $this->helper->getDatabase();
502 'SELECT m.sender_id FROM mail_attachment ma
503 INNER JOIN mail m ON m.mail_id = ma.mail_id
505 ORDER BY m.send_time ASC',
510 $row = $db->fetchAssoc(
$res);
511 if (is_array($row) && (
int) $row[
'sender_id'] > 0) {
512 return (
int) $row[
'sender_id'];
520 $this->helper->getDatabase()->manipulateF(
521 'UPDATE mail_attachment SET rcid = %s WHERE path = %s',
529 $mail_ids = $this->mailIdsOfPath($relative_path);
530 if ($mail_ids === []) {
534 $db = $this->helper->getDatabase();
544 $this->helper->getDatabase()->manipulateF(
545 'UPDATE mail_attachment SET rcid = %s WHERE path = %s',
547 [self::SKIPPED_MARKER, $relative_path]
553 $io = $environment->
getResource(Environment::RESOURCE_ADMIN_INTERACTION);
558 private function count(
string $key,
int $amount = 1): void
560 $this->statistics[$key] = ($this->statistics[$key] ?? 0) + $amount;
568 $this->report(
'INFO',
$message, $context);
576 $this->report(
'WARNING',
$message, $context);
582 private function error(
string $message, array $context = [], ?Throwable $exception =
null): void
584 if ($exception !==
null) {
585 $context[
'exception'] = $exception::class .
': ' . $exception->getMessage() .
586 ' (' . $exception->getFile() .
':' . $exception->getLine() .
')';
589 $this->report(
'ERROR',
$message, $context);
597 $this->io?->inform(sprintf(
598 '[%s] Mail attachment migration %s: %s%s',
603 ?
' ' . json_encode($context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR)
Class ResourceCollectionIdentification.
Class ResourceIdentification.
This class attempt to achieve a set of database update steps.
Class ilResourceStorageMigrationHelper.
static getPreconditions()
const SYSTEM_USER_ID
This file contains constants for PHPStan analyis, see: https://phpstan.org/config-reference#constants...
This defines ways in which objectives may interact with admins during the setup.
An environment holds resources to be used in the setup process.
getResource(string $id)
Consumers of this method should check if the result is what they expect, e.g.
A migration is a potentially long lasting operation that can be broken into discrete steps.
foreach($_POST as $key=> $value) $res