ILIAS  release_9 Revision v9.13-25-g2c18ec4c24f
class.ilFileDataMail.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
22 
31 {
32  public string $mail_path;
36  protected ilDBInterface $db;
37  protected ILIAS $ilias;
38 
39  public function __construct(public int $user_id = 0)
40  {
41  global $DIC;
42 
43  if (!defined('MAILPATH')) {
44  define('MAILPATH', 'mail');
45  }
47  $this->mail_path = $this->getPath() . "/" . MAILPATH;
48  $this->ilias = $DIC['ilias'];
49  $this->checkReadWrite();
50 
51  $this->db = $DIC->database();
52  $this->tmpDirectory = $DIC->filesystem()->temp();
53  $this->storageDirectory = $DIC->filesystem()->storage();
54 
56  }
57 
58  public function initDirectory(): bool
59  {
60  if (is_writable($this->getPath())
61  && mkdir($this->getPath() . '/' . MAILPATH)
62  && chmod($this->getPath() . '/' . MAILPATH, 0755)) {
63  $this->mail_path = $this->getPath() . '/' . MAILPATH;
64  return true;
65  }
66  return false;
67  }
68 
69  public function getUploadLimit(): int
70  {
72  }
73 
74  public function getAttachmentsTotalSizeLimit(): ?float
75  {
76  $max_size = $this->ilias->getSetting('mail_maxsize_attach', '');
77  if ($max_size === '') {
78  return null;
79  }
80 
81  return (float) $this->ilias->getSetting('mail_maxsize_attach', '0') * 1024;
82  }
83 
84  public function getMailPath(): string
85  {
86  return $this->mail_path;
87  }
88 
89  public function getAbsoluteAttachmentPoolPathPrefix(): string
90  {
91  return $this->mail_path . '/' . $this->user_id . '_';
92  }
93 
98  public function getAttachmentPathAndFilenameByMd5Hash(string $md5FileHash, int $mailId): array
99  {
100  $res = $this->db->queryF(
101  "SELECT path FROM mail_attachment WHERE mail_id = %s",
102  ['integer'],
103  [$mailId]
104  );
105 
106  if (1 !== $this->db->numRows($res)) {
107  throw new OutOfBoundsException();
108  }
109 
110  $row = $this->db->fetchAssoc($res);
111 
112  $relativePath = $row['path'];
113  $path = $this->getMailPath() . '/' . $row['path'];
114 
115  $files = ilFileUtils::getDir($path);
116  foreach ($files as $file) {
117  if ($file['type'] === 'file' && md5($file['entry']) === $md5FileHash) {
118  return [
119  'path' => $this->getMailPath() . '/' . $relativePath . '/' . $file['entry'],
120  'filename' => $file['entry'],
121  ];
122  }
123  }
124 
125  throw new OutOfBoundsException();
126  }
127 
128 
129  private function getAttachmentPathByMailId(int $mailId): string
130  {
131  $query = $this->db->query(
132  "SELECT path FROM mail_attachment WHERE mail_id = " . $this->db->quote($mailId, 'integer')
133  );
134 
135  while ($row = $this->db->fetchObject($query)) {
136  return $row->path;
137  }
138 
139  return '';
140  }
141 
142  public function getAttachmentPath(string $a_filename, int $a_mail_id): string
143  {
144  $path = $this->getMailPath() . '/' . $this->getAttachmentPathByMailId($a_mail_id) . '/' . $a_filename;
145 
146  if (is_readable($path)) {
147  return $path;
148  }
149 
150  return '';
151  }
152 
158  public function adoptAttachments(array $a_attachments, int $a_mail_id): string
159  {
160  foreach ($a_attachments as $file) {
161  $path = $this->getAttachmentPath($file, $a_mail_id);
162  if (!copy($path, $this->getMailPath() . '/' . $this->user_id . '_' . $file)) {
163  return 'ERROR: ' . $this->getMailPath() . '/' . $this->user_id . '_' . $file . ' cannot be created';
164  }
165  }
166 
167  return '';
168  }
169 
170  public function checkReadWrite(): bool
171  {
172  if (is_writable($this->mail_path) && is_readable($this->mail_path)) {
173  return true;
174  }
175 
176  $this->ilias->raiseError(
177  "Mail directory is not readable/writable by webserver: " .
178  $this->mail_path,
179  $this->ilias->error_obj->FATAL
180  );
181 
182  return false;
183  }
184 
188  public function getUserFilesData(): array
189  {
190  return $this->getUnsentFiles();
191  }
192 
196  private function getUnsentFiles(): array
197  {
198  $files = [];
199 
200  $iter = new RegexIterator(new DirectoryIterator($this->mail_path), "/^{$this->user_id}_(.+)$/");
201  foreach ($iter as $file) {
203  if (!$file->isFile()) {
204  continue;
205  }
206 
207  [$uid, $rest] = explode('_', $file->getFilename(), 2);
208  if ($uid === (string) $this->user_id) {
209  $files[] = [
210  'name' => $rest,
211  'size' => $file->getSize(),
212  'ctime' => $file->getCTime(),
213  ];
214  }
215  }
216 
217  return $files;
218  }
219 
220  public function storeAsAttachment(string $a_filename, string $a_content): string
221  {
222  if (strlen($a_content) >= $this->getUploadLimit()) {
223  throw new DomainException(
224  sprintf(
225  'Mail upload limit reached for user with id %s',
226  $this->user_id
227  )
228  );
229  }
230 
231  $name = ilFileUtils::_sanitizeFilemame($a_filename);
232  $this->rotateFiles($this->getMailPath() . '/' . $this->user_id . '_' . $name);
233 
234  $abs_path = $this->getMailPath() . '/' . $this->user_id . '_' . $name;
235 
236  $fp = fopen($abs_path, 'wb+');
237  if (!is_resource($fp)) {
238  throw new RuntimeException(
239  sprintf(
240  'Could not read file: %s',
241  $abs_path
242  )
243  );
244  }
245 
246  if (fwrite($fp, $a_content) === false) {
247  fclose($fp);
248  throw new RuntimeException(
249  sprintf(
250  'Could not write file: %s',
251  $abs_path
252  )
253  );
254  }
255 
256  fclose($fp);
257 
258  return $name;
259  }
260 
264  public function storeUploadedFile(array $file): string
265  {
266  $sanitized_filename = ilFileUtils::_sanitizeFilemame($file['name']);
267 
268  $this->rotateFiles($this->getMailPath() . '/' . $this->user_id . '_' . $sanitized_filename);
269 
271  $file['tmp_name'],
272  $sanitized_filename,
273  $this->getMailPath() . '/' . $this->user_id . '_' . $sanitized_filename
274  );
275 
276  return $sanitized_filename;
277  }
278 
282  public function copyAttachmentFile(string $a_abs_path, string $a_new_name): bool
283  {
284  @copy($a_abs_path, $this->getMailPath() . "/" . $this->user_id . "_" . $a_new_name);
285 
286  return true;
287  }
288 
289  private function rotateFiles(string $a_path): bool
290  {
291  if (is_file($a_path)) {
292  $this->rotateFiles($a_path . ".old");
293  return ilFileUtils::rename($a_path, $a_path . '.old');
294  }
295 
296  return true;
297  }
298 
303  public function unlinkFiles(array $a_filenames): string
304  {
305  foreach ($a_filenames as $file) {
306  if (!$this->unlinkFile($file)) {
307  return $file;
308  }
309  }
310 
311  return '';
312  }
313 
314  public function unlinkFile(string $a_filename): bool
315  {
316  if (is_file($this->mail_path . '/' . basename($this->user_id . '_' . $a_filename))) {
317  return unlink($this->mail_path . '/' . basename($this->user_id . '_' . $a_filename));
318  }
319 
320  return false;
321  }
322 
327  public function getAbsoluteAttachmentPoolPathByFilename(string $fileName): string
328  {
329  return $this->getAbsoluteAttachmentPoolPathPrefix() . $fileName;
330  }
331 
337  public function saveFiles(int $a_mail_id, array $a_attachments): void
338  {
339  if (!is_numeric($a_mail_id) || $a_mail_id < 1) {
340  throw new InvalidArgumentException('The passed mail_id must be a valid integer!');
341  }
342 
343  foreach ($a_attachments as $attachment) {
344  $this->saveFile($a_mail_id, $attachment);
345  }
346  }
347 
348  public static function getStorage(int $a_mail_id, int $a_usr_id): ilFSStorageMail
349  {
350  static $fsstorage_cache = [];
351 
352  $fsstorage_cache[$a_mail_id][$a_usr_id] = new ilFSStorageMail($a_mail_id, $a_usr_id);
353 
354  return $fsstorage_cache[$a_mail_id][$a_usr_id];
355  }
356 
360  public function saveFile(int $a_mail_id, string $a_attachment): bool
361  {
362  $oStorage = self::getStorage($a_mail_id, $this->user_id);
363  $oStorage->create();
364  $storage_directory = $oStorage->getAbsolutePath();
365 
366  if (!is_dir($storage_directory)) {
367  return false;
368  }
369 
370  return copy(
371  $this->mail_path . '/' . $this->user_id . '_' . $a_attachment,
372  $storage_directory . '/' . $a_attachment
373  );
374  }
375 
379  public function checkFilesExist(array $a_files): bool
380  {
381  if ($a_files !== []) {
382  foreach ($a_files as $file) {
383  if (!is_file($this->mail_path . '/' . $this->user_id . '_' . $file)) {
384  return false;
385  }
386  }
387  return true;
388  }
389  return true;
390  }
391 
392  public function assignAttachmentsToDirectory(int $a_mail_id, int $a_sent_mail_id): void
393  {
394  global $ilDB;
395 
396  $oStorage = self::getStorage($a_sent_mail_id, $this->user_id);
397  $ilDB->manipulateF(
398  '
399  INSERT INTO mail_attachment
400  ( mail_id, path) VALUES (%s, %s)',
401  ['integer', 'text'],
402  [$a_mail_id, $oStorage->getRelativePathExMailDirectory()]
403  );
404  }
405 
406  public function deassignAttachmentFromDirectory(int $a_mail_id): bool
407  {
408  global $ilDB;
409  // IF IT'S THE LAST MAIL CONTAINING THESE ATTACHMENTS => DELETE ATTACHMENTS
410  $res = $ilDB->query(
411  'SELECT path FROM mail_attachment WHERE mail_id = ' .
412  $ilDB->quote($a_mail_id, 'integer')
413  );
414 
415  $path = '';
416  while ($row = $res->fetchRow(ilDBConstants::FETCHMODE_OBJECT)) {
417  $path = (string) $row->path;
418  }
419 
420  if ($path !== '') {
421  $res = $ilDB->query(
422  'SELECT COUNT(mail_id) count_mail_id FROM mail_attachment WHERE path = ' .
423  $ilDB->quote($path, 'text')
424  ) ;
425 
426  $cnt_mail_id = 0;
427  while ($row = $res->fetchRow(ilDBConstants::FETCHMODE_OBJECT)) {
428  $cnt_mail_id = (int) $row->count_mail_id;
429  }
430 
431  if ($cnt_mail_id === 1) {
433  }
434  }
435 
436  $ilDB->manipulateF(
437  'DELETE FROM mail_attachment WHERE mail_id = %s',
438  ['integer'],
439  [$a_mail_id]
440  );
441 
442  return true;
443  }
444 
445  private function deleteAttachmentDirectory(string $a_rel_path): void
446  {
447  ilFileUtils::delDir($this->mail_path . "/" . $a_rel_path);
448  }
449 
450  protected function initAttachmentMaxUploadSize(): void
451  {
454  // Copy of ilFileInputGUI: begin
455  // get the value for the maximal uploadable filesize from the php.ini (if available)
456  $umf = ini_get("upload_max_filesize");
457  // get the value for the maximal post data from the php.ini (if available)
458  $pms = ini_get("post_max_size");
459 
460  //convert from short-string representation to "real" bytes
461  $multiplier_a = ["K" => 1024, "M" => 1024 * 1024, "G" => 1024 * 1024 * 1024];
462 
463  $umf_parts = preg_split(
464  "/(\d+)([K|G|M])/",
465  (string) $umf,
466  -1,
467  PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
468  );
469  $pms_parts = preg_split(
470  "/(\d+)([K|G|M])/",
471  (string) $pms,
472  -1,
473  PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
474  );
475 
476  if ((is_countable($umf_parts) ? count($umf_parts) : 0) === 2) {
477  $umf = (float) $umf_parts[0] * $multiplier_a[$umf_parts[1]];
478  }
479  if ((is_countable($pms_parts) ? count($pms_parts) : 0) === 2) {
480  $pms = (float) $pms_parts[0] * $multiplier_a[$pms_parts[1]];
481  }
482 
483  // use the smaller one as limit
484  $max_filesize = min($umf, $pms);
485 
486  if (!$max_filesize) {
487  $max_filesize = max($umf, $pms);
488  }
489 
490  $this->mail_max_upload_file_size = (int) $max_filesize;
491  }
492 
493  public function onUserDelete(): void
494  {
498  global $ilDB;
499 
500  // Delete uploaded mail files which are not attached to any message
501  try {
502  $iter = new RegexIterator(
503  new DirectoryIterator($this->getMailPath()),
504  '/^' . $this->user_id . '_/'
505  );
506  foreach ($iter as $file) {
511  if ($file->isFile()) {
512  @unlink($file->getPathname());
513  }
514  }
515  } catch (Exception) {
516  }
517 
518  // Select all files attached to messages which are not shared (... = 1) with other messages anymore
519  $query = '
520  SELECT DISTINCT(ma1.path)
521  FROM mail_attachment ma1
522  INNER JOIN mail
523  ON mail.mail_id = ma1.mail_id
524  WHERE mail.user_id = %s
525  AND (SELECT COUNT(tmp.path) FROM mail_attachment tmp WHERE tmp.path = ma1.path) = 1
526  ';
527  $res = $ilDB->queryF(
528  $query,
529  ['integer'],
530  [$this->user_id]
531  );
532  while ($row = $ilDB->fetchAssoc($res)) {
533  try {
534  $path = $this->getMailPath() . DIRECTORY_SEPARATOR . $row['path'];
535  $iter = new RecursiveIteratorIterator(
537  RecursiveIteratorIterator::CHILD_FIRST
538  );
539  foreach ($iter as $file) {
544  if ($file->isDir()) {
545  @rmdir($file->getPathname());
546  } else {
547  @unlink($file->getPathname());
548  }
549  }
550  @rmdir($path);
551  } catch (Exception) {
552  }
553  }
554 
555  // Delete each mail attachment rows assigned to a message of the deleted user.
556  $ilDB->manipulateF(
557  '
558  DELETE
559  FROM mail_attachment
560  WHERE EXISTS(
561  SELECT mail.mail_id
562  FROM mail
563  WHERE mail.user_id = %s AND mail.mail_id = mail_attachment.mail_id
564  )
565  ',
566  ['integer'],
567  [$this->user_id]
568  );
569  }
570 
577  public function deliverAttachmentsAsZip(
578  string $basename,
579  int $mailId,
580  array $files = [],
581  bool $isDraft = false
582  ): void {
583  $path = '';
584  if (!$isDraft) {
585  $path = $this->getAttachmentPathByMailId($mailId);
586  if ($path === '') {
587  throw new ilMailException('mail_download_zip_no_attachments');
588  }
589  }
590 
591  $downloadFilename = ilFileUtils::getASCIIFilename($basename);
592  if ($downloadFilename === '') {
593  $downloadFilename = 'attachments';
594  }
595 
596  $processingDirectory = ilFileUtils::ilTempnam();
597  $relativeProcessingDirectory = basename($processingDirectory);
598 
599  $absoluteZipDirectory = $processingDirectory . '/' . $downloadFilename;
600  $relativeZipDirectory = $relativeProcessingDirectory . '/' . $downloadFilename;
601 
602  $this->tmpDirectory->createDir($relativeZipDirectory);
603 
604  foreach ($files as $fileName) {
605  if ($isDraft) {
606  $source = str_replace(
607  $this->mail_path,
608  MAILPATH,
610  );
611  } else {
612  $source = MAILPATH . '/' . $path . '/' . $fileName;
613  }
614 
615  $source = str_replace('//', '/', $source);
616  if (!$this->storageDirectory->has($source)) {
617  continue;
618  }
619 
620  $target = $relativeZipDirectory . '/' . $fileName;
621 
622  $stream = $this->storageDirectory->readStream($source);
623  $this->tmpDirectory->writeStream($target, $stream);
624  }
625 
626  $pathToZipFile = $processingDirectory . '/' . $downloadFilename . '.zip';
627  ilFileUtils::zip($absoluteZipDirectory, $pathToZipFile);
628 
629  $this->tmpDirectory->deleteDir($relativeZipDirectory);
630 
632  $processingDirectory . '/' . $downloadFilename . '.zip',
633  ilFileUtils::getValidFilename($downloadFilename . '.zip')
634  );
635  }
636 }
getAttachmentPathAndFilenameByMd5Hash(string $md5FileHash, int $mailId)
$res
Definition: ltiservices.php:69
Filesystem $storageDirectory
storeAsAttachment(string $a_filename, string $a_content)
This class handles all operations on files (attachments) in directory ilias_data/mail.
Class ChatMainBarProvider .
adoptAttachments(array $a_attachments, int $a_mail_id)
Adopt attachments (in case of forwarding a mail)
static getValidFilename(string $a_filename)
const MAILPATH
Definition: constants.php:50
copyAttachmentFile(string $a_abs_path, string $a_new_name)
Copy files in mail directory.
static getASCIIFilename(string $a_filename)
getAttachmentPathByMailId(int $mailId)
global $DIC
Definition: feed.php:28
static getStorage(int $a_mail_id, int $a_usr_id)
unlinkFile(string $a_filename)
__construct(VocabulariesInterface $vocabularies)
static deliverFileAttached(string $path_to_file, ?string $download_file_name=null, ?string $mime_type=null, bool $delete_file=false)
assignAttachmentsToDirectory(int $a_mail_id, int $a_sent_mail_id)
rotateFiles(string $a_path)
getAbsoluteAttachmentPoolPathByFilename(string $fileName)
Resolves a path for a passed filename in regards of a user&#39;s mail attachment pool, meaning attachments not being sent.
deassignAttachmentFromDirectory(int $a_mail_id)
static delDir(string $a_dir, bool $a_clean_only=false)
removes a dir and all its content (subdirs and files) recursively
deliverAttachmentsAsZip(string $basename, int $mailId, array $files=[], bool $isDraft=false)
unlinkFiles(array $a_filenames)
header include for all ilias files.
static getDir(string $a_dir, bool $a_rec=false, ?string $a_sub_dir="")
get directory
static moveUploadedFile(string $a_file, string $a_name, string $a_target, bool $a_raise_errors=true, string $a_mode="move_uploaded")
move uploaded file
static _sanitizeFilemame(string $a_filename)
static ilTempnam(?string $a_temp_path=null)
Returns a unique and non existing Path for e temporary file or directory.
static zip(string $a_dir, string $a_file, bool $compress_content=false)
storeUploadedFile(array $file)
__construct(public int $user_id=0)
static rename(string $a_source, string $a_target)
saveFiles(int $a_mail_id, array $a_attachments)
Saves all attachment files in a specific mail directory .../mail/<calculated_path>/mail_<mail_id>_<us...
getAttachmentPath(string $a_filename, int $a_mail_id)
checkFilesExist(array $a_files)
saveFile(int $a_mail_id, string $a_attachment)
Save attachment file in a specific mail directory .../mail/<calculated_path>/mail_<mail_id>_<user_id>...
string $path
deleteAttachmentDirectory(string $a_rel_path)