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']; } }