- Implemented tests for various scenarios of attachment name normalization in MailToTaskImporter. - Covered cases for technical names, named files, missing extensions, and content-based detection. - Added tests for extracting names from MIME headers, including handling of Content-Description.
356 lines
11 KiB
PHP
356 lines
11 KiB
PHP
<?php
|
|
namespace Domain\Tasks;
|
|
|
|
class TaskAttachmentRepository
|
|
{
|
|
private $mdb;
|
|
private $upload_dir;
|
|
private $upload_url;
|
|
private $table_ready = false;
|
|
|
|
public function __construct( $mdb = null, $upload_dir = null, $upload_url = '/upload/task_attachments' )
|
|
{
|
|
if ( $mdb )
|
|
$this -> mdb = $mdb;
|
|
else
|
|
{
|
|
global $mdb;
|
|
$this -> mdb = $mdb;
|
|
}
|
|
|
|
$this -> upload_dir = $upload_dir ? rtrim( $upload_dir, '/\\' ) : getcwd() . DIRECTORY_SEPARATOR . 'upload' . DIRECTORY_SEPARATOR . 'task_attachments';
|
|
$this -> upload_url = rtrim( $upload_url, '/' );
|
|
}
|
|
|
|
public function listByTaskId( $task_id )
|
|
{
|
|
$this -> ensureStorage();
|
|
$rows = $this -> mdb -> select( 'tasks_attachments', '*', [
|
|
'AND' => [ 'task_id' => (int)$task_id, 'deleted' => 0 ],
|
|
'ORDER' => [ 'id' => 'DESC' ]
|
|
] );
|
|
|
|
if ( !is_array( $rows ) )
|
|
return [];
|
|
|
|
foreach ( $rows as &$row )
|
|
{
|
|
$row['title_effective'] = self::effectiveTitleWithExtension(
|
|
isset( $row['title'] ) ? $row['title'] : null,
|
|
isset( $row['original_name'] ) ? $row['original_name'] : '',
|
|
isset( $row['file_ext'] ) ? $row['file_ext'] : '',
|
|
isset( $row['stored_name'] ) ? $row['stored_name'] : ''
|
|
);
|
|
$row['url'] = $this -> buildPublicUrl( $row['relative_path'], $row['stored_name'] );
|
|
$row['size_human'] = $this -> formatSize( (int)$row['file_size'] );
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
public function upload( $task_id, $user_id, array $file )
|
|
{
|
|
$this -> ensureStorage();
|
|
|
|
if ( !isset( $file['error'] ) or $file['error'] !== UPLOAD_ERR_OK )
|
|
return [ 'status' => 'error', 'msg' => 'Nie udalo sie wgrac pliku.' ];
|
|
|
|
if ( !isset( $file['tmp_name'] ) or !is_uploaded_file( $file['tmp_name'] ) )
|
|
return [ 'status' => 'error', 'msg' => 'Nieprawidlowy plik.' ];
|
|
|
|
$original_name = trim( (string)$file['name'] );
|
|
if ( $original_name === '' )
|
|
return [ 'status' => 'error', 'msg' => 'Brak nazwy pliku.' ];
|
|
|
|
$safe_original_name = self::sanitizeFileName( $original_name );
|
|
$ext = strtolower( pathinfo( $safe_original_name, PATHINFO_EXTENSION ) );
|
|
if ( $ext === '' && isset( $file['tmp_name'] ) && is_string( $file['tmp_name'] ) && file_exists( $file['tmp_name'] ) )
|
|
{
|
|
$detected_ext = self::detectExtensionFromContent( @file_get_contents( $file['tmp_name'] ) );
|
|
if ( $detected_ext !== '' )
|
|
{
|
|
$ext = $detected_ext;
|
|
$safe_original_name .= '.' . $ext;
|
|
}
|
|
}
|
|
$stored_name = uniqid( 'att_', true ) . ( $ext ? '.' . $ext : '' );
|
|
|
|
$relative_path = date( 'Y' ) . '/' . date( 'm' );
|
|
$target_dir = $this -> upload_dir . DIRECTORY_SEPARATOR . str_replace( '/', DIRECTORY_SEPARATOR, $relative_path );
|
|
if ( !is_dir( $target_dir ) )
|
|
@mkdir( $target_dir, 0777, true );
|
|
|
|
$target_file = $target_dir . DIRECTORY_SEPARATOR . $stored_name;
|
|
if ( !move_uploaded_file( $file['tmp_name'], $target_file ) )
|
|
return [ 'status' => 'error', 'msg' => 'Nie udalo sie zapisac pliku.' ];
|
|
|
|
return $this -> storeMeta( $task_id, $user_id, $safe_original_name, $stored_name, $relative_path, $ext, isset( $file['size'] ) ? (int)$file['size'] : 0 );
|
|
}
|
|
|
|
public function uploadFromContent( $task_id, $user_id, $original_name, $content )
|
|
{
|
|
$this -> ensureStorage();
|
|
|
|
$original_name = trim( (string)$original_name );
|
|
if ( $original_name === '' )
|
|
$original_name = 'zalacznik';
|
|
|
|
$safe_original_name = self::sanitizeFileName( $original_name );
|
|
$ext = strtolower( pathinfo( $safe_original_name, PATHINFO_EXTENSION ) );
|
|
if ( $ext === '' )
|
|
{
|
|
$detected_ext = self::detectExtensionFromContent( (string)$content );
|
|
if ( $detected_ext !== '' )
|
|
{
|
|
$ext = $detected_ext;
|
|
$safe_original_name .= '.' . $ext;
|
|
}
|
|
}
|
|
$stored_name = uniqid( 'att_', true ) . ( $ext ? '.' . $ext : '' );
|
|
|
|
$relative_path = date( 'Y' ) . '/' . date( 'm' );
|
|
$target_dir = $this -> upload_dir . DIRECTORY_SEPARATOR . str_replace( '/', DIRECTORY_SEPARATOR, $relative_path );
|
|
if ( !is_dir( $target_dir ) )
|
|
@mkdir( $target_dir, 0777, true );
|
|
|
|
$target_file = $target_dir . DIRECTORY_SEPARATOR . $stored_name;
|
|
if ( file_put_contents( $target_file, (string)$content ) === false )
|
|
return [ 'status' => 'error', 'msg' => 'Nie udalo sie zapisac pliku.' ];
|
|
|
|
return $this -> storeMeta( $task_id, $user_id, $safe_original_name, $stored_name, $relative_path, $ext, strlen( (string)$content ) );
|
|
}
|
|
|
|
public function rename( $attachment_id, $title )
|
|
{
|
|
$this -> ensureStorage();
|
|
$title = trim( (string)$title );
|
|
|
|
return $this -> mdb -> update( 'tasks_attachments', [
|
|
'title' => $title !== '' ? $title : null
|
|
], [
|
|
'AND' => [ 'id' => (int)$attachment_id, 'deleted' => 0 ]
|
|
] );
|
|
}
|
|
|
|
public function delete( $attachment_id, $deleted_by_user_id = null )
|
|
{
|
|
$this -> ensureStorage();
|
|
|
|
$attachment = $this -> mdb -> get( 'tasks_attachments', '*', [
|
|
'AND' => [ 'id' => (int)$attachment_id, 'deleted' => 0 ]
|
|
] );
|
|
|
|
if ( !$attachment )
|
|
return false;
|
|
|
|
$file_path = $this -> upload_dir . DIRECTORY_SEPARATOR .
|
|
str_replace( '/', DIRECTORY_SEPARATOR, $attachment['relative_path'] ) .
|
|
DIRECTORY_SEPARATOR . $attachment['stored_name'];
|
|
|
|
if ( file_exists( $file_path ) )
|
|
@unlink( $file_path );
|
|
|
|
return $this -> mdb -> update( 'tasks_attachments', [
|
|
'deleted' => 1,
|
|
'deleted_by' => $deleted_by_user_id ? (int)$deleted_by_user_id : null,
|
|
'date_delete' => date( 'Y-m-d H:i:s' )
|
|
], [ 'id' => (int)$attachment_id ] );
|
|
}
|
|
|
|
public function purgeByTaskId( $task_id )
|
|
{
|
|
$this -> ensureStorage();
|
|
|
|
$rows = $this -> mdb -> select( 'tasks_attachments', '*', [
|
|
'task_id' => (int)$task_id
|
|
] );
|
|
|
|
if ( !is_array( $rows ) )
|
|
return false;
|
|
|
|
foreach ( $rows as $attachment )
|
|
{
|
|
$file_path = $this -> resolveFilePath( $attachment );
|
|
if ( $file_path !== '' and file_exists( $file_path ) )
|
|
@unlink( $file_path );
|
|
}
|
|
|
|
$this -> mdb -> delete( 'tasks_attachments', [ 'task_id' => (int)$task_id ] );
|
|
return true;
|
|
}
|
|
|
|
public static function effectiveTitle( $title, $fallback )
|
|
{
|
|
$title = trim( (string)$title );
|
|
return $title !== '' ? $title : (string)$fallback;
|
|
}
|
|
|
|
public static function effectiveTitleWithExtension( $title, $fallback, $file_ext = '', $stored_name = '' )
|
|
{
|
|
$title = trim( (string)$title );
|
|
$fallback = trim( (string)$fallback );
|
|
|
|
if ( self::isGenericAttachmentTitle( $title ) && $fallback !== '' && !self::isGenericAttachmentTitle( $fallback ) )
|
|
$effective = $fallback;
|
|
else
|
|
$effective = self::effectiveTitle( $title, $fallback );
|
|
|
|
if ( pathinfo( $effective, PATHINFO_EXTENSION ) !== '' )
|
|
return $effective;
|
|
|
|
$file_ext = strtolower( trim( (string)$file_ext ) );
|
|
if ( $file_ext === '' )
|
|
$file_ext = strtolower( pathinfo( (string)$stored_name, PATHINFO_EXTENSION ) );
|
|
|
|
if ( $file_ext !== '' )
|
|
return $effective . '.' . $file_ext;
|
|
|
|
return $effective;
|
|
}
|
|
|
|
private static function isGenericAttachmentTitle( $name )
|
|
{
|
|
$name = strtolower( trim( (string)$name ) );
|
|
if ( $name === '' )
|
|
return false;
|
|
|
|
return (bool)preg_match( '/^zalacznik(?:\.[a-z0-9]{2,6})?$/i', $name );
|
|
}
|
|
|
|
public static function sanitizeFileName( $name )
|
|
{
|
|
$name = (string)$name;
|
|
if ( $name !== '' && !preg_match( '//u', $name ) )
|
|
{
|
|
if ( function_exists( 'mb_convert_encoding' ) )
|
|
{
|
|
$converted = @mb_convert_encoding( $name, 'UTF-8', 'Windows-1250,ISO-8859-2,Windows-1252,ISO-8859-1' );
|
|
if ( is_string( $converted ) && $converted !== '' )
|
|
$name = $converted;
|
|
}
|
|
|
|
if ( !preg_match( '//u', $name ) && function_exists( 'iconv' ) )
|
|
{
|
|
$converted = @iconv( 'Windows-1250', 'UTF-8//IGNORE', $name );
|
|
if ( is_string( $converted ) && $converted !== '' )
|
|
$name = $converted;
|
|
}
|
|
}
|
|
|
|
$name = preg_replace( '/[^\p{L}\p{N}\s\.\-_]+/u', '_', (string)$name );
|
|
$name = preg_replace( '/\s+/', '_', $name );
|
|
$name = trim( $name, '._-' );
|
|
return $name !== '' ? $name : 'zalacznik';
|
|
}
|
|
|
|
public static function detectExtensionFromContent( $content )
|
|
{
|
|
if ( !is_string( $content ) || $content === '' )
|
|
return '';
|
|
|
|
$sample = substr( $content, 0, 4096 );
|
|
|
|
if ( strpos( $sample, '%PDF-' ) !== false )
|
|
return 'pdf';
|
|
|
|
if ( substr( $sample, 0, 8 ) === "\x89PNG\r\n\x1A\n" )
|
|
return 'png';
|
|
|
|
if ( substr( $sample, 0, 3 ) === "\xFF\xD8\xFF" )
|
|
return 'jpg';
|
|
|
|
if ( substr( $sample, 0, 6 ) === 'GIF87a' || substr( $sample, 0, 6 ) === 'GIF89a' )
|
|
return 'gif';
|
|
|
|
if ( substr( $sample, 0, 2 ) === 'PK' )
|
|
return 'zip';
|
|
|
|
return '';
|
|
}
|
|
|
|
private function ensureStorage()
|
|
{
|
|
if ( !is_dir( $this -> upload_dir ) )
|
|
@mkdir( $this -> upload_dir, 0777, true );
|
|
|
|
if ( !$this -> table_ready )
|
|
{
|
|
$this -> ensureTable();
|
|
$this -> table_ready = true;
|
|
}
|
|
}
|
|
|
|
private function ensureTable()
|
|
{
|
|
$this -> mdb -> query(
|
|
'CREATE TABLE IF NOT EXISTS `tasks_attachments` (
|
|
`id` INT NOT NULL AUTO_INCREMENT,
|
|
`task_id` INT NOT NULL,
|
|
`user_id` INT NOT NULL,
|
|
`title` VARCHAR(255) NULL,
|
|
`original_name` VARCHAR(255) NOT NULL,
|
|
`stored_name` VARCHAR(255) NOT NULL,
|
|
`relative_path` VARCHAR(255) NOT NULL,
|
|
`file_ext` VARCHAR(20) NULL,
|
|
`file_size` INT UNSIGNED NOT NULL DEFAULT 0,
|
|
`date_add` DATETIME NOT NULL,
|
|
`deleted` TINYINT(1) NOT NULL DEFAULT 0,
|
|
`deleted_by` INT NULL,
|
|
`date_delete` DATETIME NULL,
|
|
PRIMARY KEY (`id`),
|
|
INDEX `idx_tasks_attachments_task` (`task_id`),
|
|
INDEX `idx_tasks_attachments_deleted` (`deleted`)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8'
|
|
);
|
|
}
|
|
|
|
private function storeMeta( $task_id, $user_id, $safe_original_name, $stored_name, $relative_path, $ext, $size )
|
|
{
|
|
$insert = $this -> mdb -> insert( 'tasks_attachments', [
|
|
'task_id' => (int)$task_id,
|
|
'user_id' => (int)$user_id,
|
|
'title' => null,
|
|
'original_name' => $safe_original_name,
|
|
'stored_name' => $stored_name,
|
|
'relative_path' => $relative_path,
|
|
'file_ext' => $ext,
|
|
'file_size' => (int)$size,
|
|
'date_add' => date( 'Y-m-d H:i:s' ),
|
|
'deleted' => 0
|
|
] );
|
|
|
|
if ( !$insert )
|
|
return [ 'status' => 'error', 'msg' => 'Nie udalo sie zapisac zalacznika w bazie.' ];
|
|
|
|
$attachment_id = $this -> mdb -> id();
|
|
return [ 'status' => 'success', 'id' => (int)$attachment_id, 'relative_path' => $relative_path, 'stored_name' => $stored_name ];
|
|
}
|
|
|
|
private function buildPublicUrl( $relative_path, $stored_name )
|
|
{
|
|
return $this -> upload_url . '/' . trim( $relative_path, '/' ) . '/' . rawurlencode( $stored_name );
|
|
}
|
|
|
|
private function formatSize( $bytes )
|
|
{
|
|
if ( $bytes < 1024 )
|
|
return $bytes . ' B';
|
|
|
|
$kb = $bytes / 1024;
|
|
if ( $kb < 1024 )
|
|
return number_format( $kb, 1, '.', '' ) . ' KB';
|
|
|
|
return number_format( $kb / 1024, 1, '.', '' ) . ' MB';
|
|
}
|
|
|
|
private function resolveFilePath( array $attachment )
|
|
{
|
|
if ( !isset( $attachment['relative_path'] ) or !isset( $attachment['stored_name'] ) )
|
|
return '';
|
|
|
|
return $this -> upload_dir . DIRECTORY_SEPARATOR .
|
|
str_replace( '/', DIRECTORY_SEPARATOR, (string)$attachment['relative_path'] ) .
|
|
DIRECTORY_SEPARATOR . (string)$attachment['stored_name'];
|
|
}
|
|
}
|