Merge branch 'main' of https://git.project-pro.pl/Project-Pro/crmPRO
This commit is contained in:
1
.vscode/ftp-kr.diff._tmp_mail_name_check.php
vendored
Normal file
1
.vscode/ftp-kr.diff._tmp_mail_name_check.php
vendored
Normal file
@@ -0,0 +1 @@
|
||||
c:\visual studio code\projekty\crmPRO\_tmp_mail_name_check.php
|
||||
BIN
VIDOK_Instrukcja Montażu I Uruchomienia - Wyroby Elektryczne.pdf
Normal file
BIN
VIDOK_Instrukcja Montażu I Uruchomienia - Wyroby Elektryczne.pdf
Normal file
Binary file not shown.
@@ -204,7 +204,11 @@ class MailToTaskImporter
|
||||
// Zapisz normalne załączniki
|
||||
foreach ( $content['attachments'] as $attachment )
|
||||
{
|
||||
$att_name = $attachment['name'];
|
||||
$att_name = self::normalizeImportedAttachmentName(
|
||||
isset( $attachment['name'] ) ? $attachment['name'] : '',
|
||||
isset( $attachment['mime'] ) ? $attachment['mime'] : '',
|
||||
isset( $attachment['content'] ) ? $attachment['content'] : null
|
||||
);
|
||||
$att_mime = isset( $attachment['mime'] ) ? $attachment['mime'] : '';
|
||||
|
||||
// Jeśli nazwa nie ma rozszerzenia, dodaj je z MIME type
|
||||
@@ -359,17 +363,82 @@ class MailToTaskImporter
|
||||
if ( $value === '' )
|
||||
return '';
|
||||
|
||||
if ( !function_exists( 'imap_mime_header_decode' ) )
|
||||
return trim( $this -> convertTextToUtf8( $value ) );
|
||||
|
||||
$chunks = @imap_mime_header_decode( $value );
|
||||
if ( !is_array( $chunks ) or !count( $chunks ) )
|
||||
return trim( $value );
|
||||
return trim( $this -> convertTextToUtf8( $value ) );
|
||||
|
||||
$decoded = '';
|
||||
foreach ( $chunks as $chunk )
|
||||
$decoded .= isset( $chunk -> text ) ? $chunk -> text : '';
|
||||
{
|
||||
$chunk_text = isset( $chunk -> text ) ? (string)$chunk -> text : '';
|
||||
$chunk_charset = isset( $chunk -> charset ) ? (string)$chunk -> charset : '';
|
||||
$decoded .= $this -> convertTextToUtf8( $chunk_text, $chunk_charset );
|
||||
}
|
||||
|
||||
return trim( $decoded );
|
||||
}
|
||||
|
||||
private function convertTextToUtf8( $text, $source_charset = '' )
|
||||
{
|
||||
$text = (string)$text;
|
||||
if ( $text === '' )
|
||||
return '';
|
||||
|
||||
if ( preg_match( '//u', $text ) )
|
||||
return $text;
|
||||
|
||||
$source_charset = strtoupper( trim( (string)$source_charset ) );
|
||||
$aliases = [
|
||||
'DEFAULT' => '',
|
||||
'UNKNOWN-8BIT' => '',
|
||||
'X-UNKNOWN' => '',
|
||||
'UTF8' => 'UTF-8',
|
||||
'UTF-8' => 'UTF-8',
|
||||
'ISO-8859-2' => 'ISO-8859-2',
|
||||
'ISO8859-2' => 'ISO-8859-2',
|
||||
'WINDOWS-1250' => 'Windows-1250',
|
||||
'CP1250' => 'Windows-1250',
|
||||
'WINDOWS-1252' => 'Windows-1252',
|
||||
'CP1252' => 'Windows-1252'
|
||||
];
|
||||
|
||||
$candidates = [];
|
||||
if ( isset( $aliases[$source_charset] ) && $aliases[$source_charset] !== '' )
|
||||
$candidates[] = $aliases[$source_charset];
|
||||
|
||||
$candidates = array_merge( $candidates, [ 'Windows-1250', 'ISO-8859-2', 'Windows-1252', 'ISO-8859-1' ] );
|
||||
$candidates = array_values( array_unique( $candidates ) );
|
||||
|
||||
foreach ( $candidates as $encoding )
|
||||
{
|
||||
if ( function_exists( 'mb_convert_encoding' ) )
|
||||
{
|
||||
$converted = @mb_convert_encoding( $text, 'UTF-8', $encoding );
|
||||
if ( is_string( $converted ) && $converted !== '' && preg_match( '//u', $converted ) )
|
||||
return $converted;
|
||||
}
|
||||
|
||||
if ( function_exists( 'iconv' ) )
|
||||
{
|
||||
$converted = @iconv( $encoding, 'UTF-8//IGNORE', $text );
|
||||
if ( is_string( $converted ) && $converted !== '' && preg_match( '//u', $converted ) )
|
||||
return $converted;
|
||||
}
|
||||
}
|
||||
|
||||
if ( function_exists( 'mb_convert_encoding' ) )
|
||||
{
|
||||
$converted = @mb_convert_encoding( $text, 'UTF-8', 'UTF-8' );
|
||||
if ( is_string( $converted ) && $converted !== '' )
|
||||
return $converted;
|
||||
}
|
||||
|
||||
return @iconv( 'UTF-8', 'UTF-8//IGNORE', $text ) ?: $text;
|
||||
}
|
||||
|
||||
private function messageKey( $header, $sender, $subject, $message_no )
|
||||
{
|
||||
$message_id = '';
|
||||
@@ -434,7 +503,7 @@ class MailToTaskImporter
|
||||
$structure = @imap_fetchstructure( $imap, $message_no );
|
||||
|
||||
$parts = [];
|
||||
$this -> flattenParts( $imap, $message_no, $structure, '', $parts );
|
||||
$this -> flattenParts( $imap, $message_no, $structure, '', $parts, [] );
|
||||
|
||||
$plain = '';
|
||||
$html = '';
|
||||
@@ -472,7 +541,9 @@ class MailToTaskImporter
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( !$this -> shouldImportAttachment( $part, $cid_refs ) )
|
||||
$should_import = $this -> shouldImportAttachment( $part, $cid_refs );
|
||||
|
||||
if ( !$should_import )
|
||||
continue;
|
||||
|
||||
$attachments[] = [
|
||||
@@ -493,7 +564,7 @@ class MailToTaskImporter
|
||||
];
|
||||
}
|
||||
|
||||
private function flattenParts( $imap, $message_no, $structure, $prefix, array &$parts )
|
||||
private function flattenParts( $imap, $message_no, $structure, $prefix, array &$parts, array $parent_context = [] )
|
||||
{
|
||||
if ( !$structure )
|
||||
{
|
||||
@@ -510,21 +581,24 @@ class MailToTaskImporter
|
||||
if ( !isset( $structure -> parts ) or !is_array( $structure -> parts ) or !count( $structure -> parts ) )
|
||||
{
|
||||
$part_number = $prefix !== '' ? $prefix : null;
|
||||
$parts[] = $this -> parseSinglePart( $imap, $message_no, $structure, $part_number );
|
||||
$parts[] = $this -> parseSinglePart( $imap, $message_no, $structure, $part_number, $parent_context );
|
||||
return;
|
||||
}
|
||||
|
||||
$current_context = $this -> mergePartContexts( $parent_context, $this -> extractPartContext( $structure ) );
|
||||
|
||||
foreach ( $structure -> parts as $index => $subpart )
|
||||
{
|
||||
$part_number = $prefix === '' ? (string)( $index + 1 ) : $prefix . '.' . ( $index + 1 );
|
||||
$child_context = $this -> mergePartContexts( $current_context, $this -> extractPartContext( $subpart ) );
|
||||
if ( isset( $subpart -> parts ) and is_array( $subpart -> parts ) and count( $subpart -> parts ) )
|
||||
$this -> flattenParts( $imap, $message_no, $subpart, $part_number, $parts );
|
||||
$this -> flattenParts( $imap, $message_no, $subpart, $part_number, $parts, $child_context );
|
||||
else
|
||||
$parts[] = $this -> parseSinglePart( $imap, $message_no, $subpart, $part_number );
|
||||
$parts[] = $this -> parseSinglePart( $imap, $message_no, $subpart, $part_number, $child_context );
|
||||
}
|
||||
}
|
||||
|
||||
private function parseSinglePart( $imap, $message_no, $part, $part_number )
|
||||
private function parseSinglePart( $imap, $message_no, $part, $part_number, array $context = [] )
|
||||
{
|
||||
$raw = '';
|
||||
if ( $part_number === null )
|
||||
@@ -539,31 +613,44 @@ class MailToTaskImporter
|
||||
|
||||
$params = $this -> partParams( $part );
|
||||
|
||||
$name = '';
|
||||
if ( isset( $params['filename'] ) )
|
||||
$name = $params['filename'];
|
||||
elseif ( isset( $params['name'] ) )
|
||||
$name = $params['name'];
|
||||
$filename_param = isset( $params['filename'] ) ? $this -> decodeHeaderValue( $params['filename'] ) : '';
|
||||
$name_param = isset( $params['name'] ) ? $this -> decodeHeaderValue( $params['name'] ) : '';
|
||||
$description_param = isset( $params['description'] ) ? $this -> decodeHeaderValue( $params['description'] ) : '';
|
||||
$name = $this -> pickBetterAttachmentName( $filename_param, $name_param );
|
||||
$name = $this -> pickBetterAttachmentName( $name, $description_param );
|
||||
|
||||
$name = $this -> decodeHeaderValue( $name );
|
||||
if ( isset( $part -> description ) && trim( (string)$part -> description ) !== '' )
|
||||
$name = $this -> pickBetterAttachmentName( $name, $this -> decodeHeaderValue( (string)$part -> description ) );
|
||||
|
||||
// Fallback: jeśli brak nazwy lub brak rozszerzenia, spróbuj z surowych nagłówków MIME
|
||||
if ( $part_number !== null && ( trim( $name ) === '' || pathinfo( $name, PATHINFO_EXTENSION ) === '' ) )
|
||||
if ( isset( $context['name'] ) && trim( (string)$context['name'] ) !== '' )
|
||||
$name = $this -> pickBetterAttachmentName( $name, (string)$context['name'] );
|
||||
|
||||
// Fallback: jeśli brak nazwy, brak rozszerzenia lub nazwa techniczna, spróbuj z surowych nagłówków MIME
|
||||
if ( $part_number !== null && ( trim( $name ) === '' || pathinfo( $name, PATHINFO_EXTENSION ) === '' || self::isTechnicalAttachmentName( $name ) ) )
|
||||
{
|
||||
$mime_name = $this -> extractNameFromRawMime( $imap, $message_no, $part_number );
|
||||
if ( $mime_name !== '' )
|
||||
$name = $mime_name;
|
||||
}
|
||||
|
||||
$name = self::normalizeImportedAttachmentName( $name, $mime, $decoded );
|
||||
|
||||
$disposition = isset( $part -> disposition ) ? strtoupper( trim( (string)$part -> disposition ) ) : '';
|
||||
if ( $disposition === '' && isset( $context['disposition'] ) )
|
||||
$disposition = strtoupper( trim( (string)$context['disposition'] ) );
|
||||
|
||||
$content_id = '';
|
||||
if ( isset( $part -> id ) and trim( (string)$part -> id ) !== '' )
|
||||
$content_id = $this -> normalizeContentId( (string)$part -> id );
|
||||
elseif ( isset( $params['content-id'] ) and trim( (string)$params['content-id'] ) !== '' )
|
||||
$content_id = $this -> normalizeContentId( (string)$params['content-id'] );
|
||||
elseif ( isset( $context['content_id'] ) )
|
||||
$content_id = $this -> normalizeContentId( (string)$context['content_id'] );
|
||||
|
||||
$is_inline = $disposition === 'INLINE' or $content_id !== '';
|
||||
$is_attachment = trim( $name ) !== '' or $disposition === 'ATTACHMENT';
|
||||
$is_attachment = trim( $name ) !== ''
|
||||
or $disposition === 'ATTACHMENT'
|
||||
or $this -> isLikelyBinaryAttachmentWithoutName( $mime, $decoded, $disposition, $content_id );
|
||||
|
||||
return [
|
||||
'mime' => $mime,
|
||||
@@ -641,10 +728,22 @@ class MailToTaskImporter
|
||||
|
||||
private function extractNameFromRawMime( $imap, $message_no, $part_number )
|
||||
{
|
||||
$headers = (string)@imap_fetchmime( $imap, $message_no, $part_number, FT_PEEK );
|
||||
$headers = $this -> fetchRawMimeHeaders( $imap, $message_no, $part_number );
|
||||
|
||||
if ( trim( $headers ) === '' )
|
||||
return '';
|
||||
|
||||
return $this -> extractNameFromMimeHeaders( $headers );
|
||||
}
|
||||
|
||||
private function extractNameFromMimeHeaders( $headers )
|
||||
{
|
||||
$headers = (string)$headers;
|
||||
if ( trim( $headers ) === '' )
|
||||
return '';
|
||||
|
||||
$best_name = '';
|
||||
|
||||
// Szukaj filename w Content-Disposition, potem name w Content-Type
|
||||
foreach ( [ 'filename', 'name' ] as $param_name )
|
||||
{
|
||||
@@ -667,8 +766,8 @@ class MailToTaskImporter
|
||||
$decoded = @mb_convert_encoding( $decoded, 'UTF-8', $charset );
|
||||
$combined = $decoded;
|
||||
}
|
||||
if ( trim( $combined ) !== '' && pathinfo( $combined, PATHINFO_EXTENSION ) !== '' )
|
||||
return trim( $combined );
|
||||
if ( trim( $combined ) !== '' )
|
||||
$best_name = $this -> pickBetterAttachmentName( $best_name, trim( $combined ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -684,27 +783,109 @@ class MailToTaskImporter
|
||||
$decoded = @mb_convert_encoding( $decoded, 'UTF-8', $charset );
|
||||
$val = $decoded;
|
||||
}
|
||||
if ( trim( $val ) !== '' && pathinfo( $val, PATHINFO_EXTENSION ) !== '' )
|
||||
return trim( $val );
|
||||
if ( trim( $val ) !== '' )
|
||||
$best_name = $this -> pickBetterAttachmentName( $best_name, trim( $val ) );
|
||||
}
|
||||
|
||||
// Standardowy: param="value" lub param=value
|
||||
if ( preg_match( '/' . preg_quote( $param_name, '/' ) . '\s*=\s*"([^"]+)"/i', $headers, $m ) )
|
||||
{
|
||||
$val = $this -> decodeHeaderValue( trim( $m[1] ) );
|
||||
if ( trim( $val ) !== '' && pathinfo( $val, PATHINFO_EXTENSION ) !== '' )
|
||||
return trim( $val );
|
||||
if ( trim( $val ) !== '' )
|
||||
$best_name = $this -> pickBetterAttachmentName( $best_name, trim( $val ) );
|
||||
}
|
||||
|
||||
if ( preg_match( '/' . preg_quote( $param_name, '/' ) . '\s*=\s*([^\s;]+)/i', $headers, $m ) )
|
||||
{
|
||||
$val = $this -> decodeHeaderValue( trim( $m[1], " \t\"'" ) );
|
||||
if ( trim( $val ) !== '' && pathinfo( $val, PATHINFO_EXTENSION ) !== '' )
|
||||
return trim( $val );
|
||||
if ( trim( $val ) !== '' )
|
||||
$best_name = $this -> pickBetterAttachmentName( $best_name, trim( $val ) );
|
||||
}
|
||||
|
||||
// Wartość bez cudzysłowów, ale ze spacjami aż do końca linii lub średnika
|
||||
if ( preg_match( '/' . preg_quote( $param_name, '/' ) . '\s*=\s*([^;\r\n]+)/i', $headers, $m ) )
|
||||
{
|
||||
$val = $this -> decodeHeaderValue( trim( $m[1], " \t\"'" ) );
|
||||
if ( trim( $val ) !== '' )
|
||||
$best_name = $this -> pickBetterAttachmentName( $best_name, trim( $val ) );
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
if ( preg_match( '/^\s*content-description\s*:\s*(.+)$/im', $headers, $m ) )
|
||||
{
|
||||
$val = $this -> decodeHeaderValue( trim( $m[1] ) );
|
||||
if ( trim( $val ) !== '' )
|
||||
$best_name = $this -> pickBetterAttachmentName( $best_name, trim( $val ) );
|
||||
}
|
||||
|
||||
return trim( $best_name );
|
||||
}
|
||||
|
||||
public function debugExtractNameFromMimeHeaders( $headers )
|
||||
{
|
||||
return $this -> extractNameFromMimeHeaders( $headers );
|
||||
}
|
||||
|
||||
private function extractPartContext( $part )
|
||||
{
|
||||
if ( !$part )
|
||||
return [];
|
||||
|
||||
$params = $this -> partParams( $part );
|
||||
$filename_param = isset( $params['filename'] ) ? $this -> decodeHeaderValue( $params['filename'] ) : '';
|
||||
$name_param = isset( $params['name'] ) ? $this -> decodeHeaderValue( $params['name'] ) : '';
|
||||
$description_param = isset( $params['description'] ) ? $this -> decodeHeaderValue( $params['description'] ) : '';
|
||||
|
||||
$name = $this -> pickBetterAttachmentName( $filename_param, $name_param );
|
||||
$name = $this -> pickBetterAttachmentName( $name, $description_param );
|
||||
if ( isset( $part -> description ) && trim( (string)$part -> description ) !== '' )
|
||||
$name = $this -> pickBetterAttachmentName( $name, $this -> decodeHeaderValue( (string)$part -> description ) );
|
||||
$disposition = isset( $part -> disposition ) ? strtoupper( trim( (string)$part -> disposition ) ) : '';
|
||||
|
||||
$content_id = '';
|
||||
if ( isset( $part -> id ) and trim( (string)$part -> id ) !== '' )
|
||||
$content_id = $this -> normalizeContentId( (string)$part -> id );
|
||||
elseif ( isset( $params['content-id'] ) and trim( (string)$params['content-id'] ) !== '' )
|
||||
$content_id = $this -> normalizeContentId( (string)$params['content-id'] );
|
||||
|
||||
return [
|
||||
'name' => $name,
|
||||
'disposition' => $disposition,
|
||||
'content_id' => $content_id
|
||||
];
|
||||
}
|
||||
|
||||
private function mergePartContexts( array $parent_context, array $child_context )
|
||||
{
|
||||
$merged = $parent_context;
|
||||
|
||||
$parent_name = isset( $parent_context['name'] ) ? (string)$parent_context['name'] : '';
|
||||
$child_name = isset( $child_context['name'] ) ? (string)$child_context['name'] : '';
|
||||
$merged['name'] = $this -> pickBetterAttachmentName( $parent_name, $child_name );
|
||||
|
||||
if ( isset( $child_context['disposition'] ) && trim( (string)$child_context['disposition'] ) !== '' )
|
||||
$merged['disposition'] = (string)$child_context['disposition'];
|
||||
elseif ( isset( $parent_context['disposition'] ) )
|
||||
$merged['disposition'] = (string)$parent_context['disposition'];
|
||||
|
||||
if ( isset( $child_context['content_id'] ) && trim( (string)$child_context['content_id'] ) !== '' )
|
||||
$merged['content_id'] = (string)$child_context['content_id'];
|
||||
elseif ( isset( $parent_context['content_id'] ) )
|
||||
$merged['content_id'] = (string)$parent_context['content_id'];
|
||||
|
||||
return $merged;
|
||||
}
|
||||
|
||||
private function fetchRawMimeHeaders( $imap, $message_no, $part_number )
|
||||
{
|
||||
if ( $part_number === null )
|
||||
return '';
|
||||
|
||||
$headers = (string)@imap_fetchmime( $imap, $message_no, $part_number, FT_PEEK );
|
||||
if ( trim( $headers ) === '' )
|
||||
$headers = (string)@imap_fetchbody( $imap, $message_no, $part_number . '.MIME', FT_PEEK );
|
||||
|
||||
return (string)$headers;
|
||||
}
|
||||
|
||||
private function decodePartBody( $raw, $encoding )
|
||||
@@ -838,12 +1019,10 @@ class MailToTaskImporter
|
||||
private function shouldImportAttachment( array $part, array $cid_refs )
|
||||
{
|
||||
$name = trim( isset( $part['name'] ) ? (string)$part['name'] : '' );
|
||||
if ( $name === '' )
|
||||
return false;
|
||||
|
||||
$mime = strtolower( isset( $part['mime'] ) ? (string)$part['mime'] : '' );
|
||||
$disposition = strtoupper( isset( $part['disposition'] ) ? (string)$part['disposition'] : '' );
|
||||
$content_id = $this -> normalizeContentId( isset( $part['content_id'] ) ? (string)$part['content_id'] : '' );
|
||||
$body = isset( $part['body'] ) ? (string)$part['body'] : '';
|
||||
|
||||
if ( !empty( $part['is_inline'] ) )
|
||||
return false;
|
||||
@@ -857,7 +1036,51 @@ class MailToTaskImporter
|
||||
if ( strpos( $mime, 'image/' ) === 0 and $disposition !== 'ATTACHMENT' )
|
||||
return false;
|
||||
|
||||
return true;
|
||||
if ( $name !== '' )
|
||||
return true;
|
||||
|
||||
if ( $disposition === 'ATTACHMENT' )
|
||||
return true;
|
||||
|
||||
if ( $mime === 'application/pdf' or $mime === 'application/x-pdf' )
|
||||
return true;
|
||||
|
||||
if ( $mime === 'application/octet-stream' and self::contentLooksLikePdf( $body ) )
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function isLikelyBinaryAttachmentWithoutName( $mime, $content, $disposition, $content_id )
|
||||
{
|
||||
$mime = strtolower( trim( (string)$mime ) );
|
||||
$disposition = strtoupper( trim( (string)$disposition ) );
|
||||
$content_id = trim( (string)$content_id );
|
||||
|
||||
if ( strpos( $mime, 'text/' ) === 0 )
|
||||
return false;
|
||||
|
||||
if ( $content_id !== '' )
|
||||
return false;
|
||||
|
||||
if ( strpos( $mime, 'image/' ) === 0 && $disposition !== 'ATTACHMENT' )
|
||||
return false;
|
||||
|
||||
if ( $mime === 'application/pdf' || $mime === 'application/x-pdf' )
|
||||
return true;
|
||||
|
||||
if ( $mime === 'application/octet-stream' && self::contentLooksLikePdf( $content ) )
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static function contentLooksLikePdf( $content )
|
||||
{
|
||||
if ( !is_string( $content ) || $content === '' )
|
||||
return false;
|
||||
|
||||
return strpos( substr( $content, 0, 1024 ), '%PDF-' ) !== false;
|
||||
}
|
||||
|
||||
private function extractReferencedCidValues( $html )
|
||||
@@ -1213,6 +1436,136 @@ class MailToTaskImporter
|
||||
return isset( $map[$mime] ) ? $map[$mime] : '';
|
||||
}
|
||||
|
||||
private function pickBetterAttachmentName( $primary, $secondary )
|
||||
{
|
||||
$primary = trim( (string)$primary );
|
||||
$secondary = trim( (string)$secondary );
|
||||
|
||||
if ( $primary === '' )
|
||||
return $secondary;
|
||||
|
||||
if ( $secondary === '' )
|
||||
return $primary;
|
||||
|
||||
$primary_score = self::attachmentNameScore( $primary );
|
||||
$secondary_score = self::attachmentNameScore( $secondary );
|
||||
|
||||
return $secondary_score > $primary_score ? $secondary : $primary;
|
||||
}
|
||||
|
||||
public static function normalizeImportedAttachmentName( $name, $mime = '', $content = null )
|
||||
{
|
||||
$name = trim( (string)$name );
|
||||
$technical_ext = '';
|
||||
|
||||
if ( self::isTechnicalAttachmentName( $name ) )
|
||||
{
|
||||
$technical_ext = strtolower( pathinfo( $name, PATHINFO_EXTENSION ) );
|
||||
$name = '';
|
||||
}
|
||||
|
||||
if ( $name === '' )
|
||||
$name = 'zalacznik';
|
||||
|
||||
$ext = strtolower( pathinfo( $name, PATHINFO_EXTENSION ) );
|
||||
if ( $ext === '' )
|
||||
{
|
||||
$mime_ext = self::extensionFromMimeStatic( $mime );
|
||||
if ( $mime_ext === '' && $technical_ext !== '' )
|
||||
$mime_ext = $technical_ext;
|
||||
if ( $mime_ext === '' )
|
||||
$mime_ext = self::extensionFromContent( $content );
|
||||
if ( $mime_ext !== '' )
|
||||
$name .= '.' . $mime_ext;
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
private static function attachmentNameScore( $name )
|
||||
{
|
||||
$name = trim( (string)$name );
|
||||
if ( $name === '' )
|
||||
return -1;
|
||||
|
||||
$score = 1;
|
||||
if ( pathinfo( $name, PATHINFO_EXTENSION ) !== '' )
|
||||
$score += 2;
|
||||
|
||||
if ( self::isTechnicalAttachmentName( $name ) )
|
||||
$score -= 3;
|
||||
|
||||
return $score;
|
||||
}
|
||||
|
||||
private static function isTechnicalAttachmentName( $name )
|
||||
{
|
||||
$name = trim( (string)$name );
|
||||
if ( $name === '' )
|
||||
return false;
|
||||
|
||||
return (bool)preg_match( '/^att_[a-f0-9]{8,}(?:\.[a-z0-9]{2,6})?$/i', $name );
|
||||
}
|
||||
|
||||
private static function extensionFromMimeStatic( $mime )
|
||||
{
|
||||
$map = [
|
||||
'application/pdf' => 'pdf',
|
||||
'application/zip' => 'zip',
|
||||
'application/x-rar-compressed' => 'rar',
|
||||
'application/vnd.rar' => 'rar',
|
||||
'application/x-7z-compressed' => '7z',
|
||||
'application/msword' => 'doc',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
|
||||
'application/vnd.ms-excel' => 'xls',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
|
||||
'application/vnd.ms-powerpoint' => 'ppt',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
|
||||
'application/xml' => 'xml',
|
||||
'application/json' => 'json',
|
||||
'application/rtf' => 'rtf',
|
||||
'application/octet-stream' => '',
|
||||
'text/plain' => 'txt',
|
||||
'text/html' => 'html',
|
||||
'text/csv' => 'csv',
|
||||
'image/jpeg' => 'jpg',
|
||||
'image/png' => 'png',
|
||||
'image/gif' => 'gif',
|
||||
'image/webp' => 'webp',
|
||||
'image/bmp' => 'bmp',
|
||||
'image/svg+xml' => 'svg',
|
||||
'image/tiff' => 'tiff'
|
||||
];
|
||||
|
||||
$mime = strtolower( trim( (string)$mime ) );
|
||||
return isset( $map[$mime] ) ? $map[$mime] : '';
|
||||
}
|
||||
|
||||
private static function extensionFromContent( $content )
|
||||
{
|
||||
if ( !is_string( $content ) || $content === '' )
|
||||
return '';
|
||||
|
||||
$prefix = substr( $content, 0, 16 );
|
||||
|
||||
if ( substr( $prefix, 0, 4 ) === '%PDF' )
|
||||
return 'pdf';
|
||||
|
||||
if ( substr( $prefix, 0, 2 ) === 'PK' )
|
||||
return 'zip';
|
||||
|
||||
if ( substr( $prefix, 0, 3 ) === "\xFF\xD8\xFF" )
|
||||
return 'jpg';
|
||||
|
||||
if ( substr( $prefix, 0, 8 ) === "\x89PNG\r\n\x1A\n" )
|
||||
return 'png';
|
||||
|
||||
if ( substr( $prefix, 0, 6 ) === 'GIF87a' || substr( $prefix, 0, 6 ) === 'GIF89a' )
|
||||
return 'gif';
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function replaceCidReferences( $html, array $cid_to_url )
|
||||
{
|
||||
if ( trim( (string)$html ) === '' || empty( $cid_to_url ) )
|
||||
|
||||
@@ -35,7 +35,12 @@ class TaskAttachmentRepository
|
||||
|
||||
foreach ( $rows as &$row )
|
||||
{
|
||||
$row['title_effective'] = self::effectiveTitle( $row['title'], $row['original_name'] );
|
||||
$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'] );
|
||||
}
|
||||
@@ -59,6 +64,15 @@ class TaskAttachmentRepository
|
||||
|
||||
$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' );
|
||||
@@ -83,6 +97,15 @@ class TaskAttachmentRepository
|
||||
|
||||
$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' );
|
||||
@@ -162,14 +185,89 @@ class TaskAttachmentRepository
|
||||
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 ) )
|
||||
|
||||
85
tests/Domain/Tasks/MailToTaskImporterTest.php
Normal file
85
tests/Domain/Tasks/MailToTaskImporterTest.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../../../autoload/Domain/Tasks/MailToTaskImporter.php';
|
||||
|
||||
use Domain\Tasks\MailToTaskImporter;
|
||||
|
||||
function run_mail_to_task_importer_tests()
|
||||
{
|
||||
$importer = new MailToTaskImporter( null );
|
||||
|
||||
$normalized_pdf = MailToTaskImporter::normalizeImportedAttachmentName( 'att_6995b8ad9d4567', 'application/pdf' );
|
||||
assert_true(
|
||||
$normalized_pdf === 'zalacznik.pdf',
|
||||
'Expected technical att_* name to be normalized to readable fallback with MIME extension.'
|
||||
);
|
||||
|
||||
$normalized_named = MailToTaskImporter::normalizeImportedAttachmentName( 'faktura_01_2026.pdf', 'application/pdf' );
|
||||
assert_true(
|
||||
$normalized_named === 'faktura_01_2026.pdf',
|
||||
'Expected normal file name to stay unchanged.'
|
||||
);
|
||||
|
||||
$normalized_without_ext = MailToTaskImporter::normalizeImportedAttachmentName( 'umowa_final', 'application/pdf' );
|
||||
assert_true(
|
||||
$normalized_without_ext === 'umowa_final.pdf',
|
||||
'Expected missing extension to be appended from MIME type.'
|
||||
);
|
||||
|
||||
$normalized_from_content = MailToTaskImporter::normalizeImportedAttachmentName(
|
||||
'att_6995b8ad9d4567',
|
||||
'application/octet-stream',
|
||||
"%PDF-1.7\nexample"
|
||||
);
|
||||
assert_true(
|
||||
$normalized_from_content === 'zalacznik.pdf',
|
||||
'Expected PDF extension to be detected from file content when MIME is generic.'
|
||||
);
|
||||
|
||||
$normalized_technical_with_ext = MailToTaskImporter::normalizeImportedAttachmentName(
|
||||
'att_6995b8ad9d4567.pdf',
|
||||
'application/octet-stream'
|
||||
);
|
||||
assert_true(
|
||||
$normalized_technical_with_ext === 'zalacznik.pdf',
|
||||
'Expected technical name with extension to preserve extension in normalized fallback.'
|
||||
);
|
||||
|
||||
$normalized_original_should_stay = MailToTaskImporter::normalizeImportedAttachmentName(
|
||||
'VIDOK_Instrukcja Montażu I Uruchomienia - Wyroby Elektryczne.pdf',
|
||||
'application/octet-stream'
|
||||
);
|
||||
assert_true(
|
||||
$normalized_original_should_stay === 'VIDOK_Instrukcja Montażu I Uruchomienia - Wyroby Elektryczne.pdf',
|
||||
'Expected original non-technical PDF name to be preserved as-is.'
|
||||
);
|
||||
|
||||
$raw_headers = "Content-Type: application/octet-stream; name=VIDOK_Instrukcja Montażu I Uruchomienia - Wyroby Elektryczne.pdf\r\n"
|
||||
. "Content-Disposition: attachment; filename=att_6995b8ad9d4567\r\n";
|
||||
|
||||
$parsed_name = $importer -> debugExtractNameFromMimeHeaders( $raw_headers );
|
||||
assert_true(
|
||||
$parsed_name === 'VIDOK_Instrukcja Montażu I Uruchomienia - Wyroby Elektryczne.pdf',
|
||||
'Expected parser to keep full unquoted file name with spaces and prefer it over technical att_* name.'
|
||||
);
|
||||
|
||||
$raw_headers_with_description = "Content-Type: application/octet-stream\r\n"
|
||||
. "Content-Disposition: attachment; filename=att_6995b8ad9d4567\r\n"
|
||||
. "Content-Description: VIDOK_Instrukcja Montażu I Uruchomienia - Wyroby Elektryczne.pdf\r\n";
|
||||
|
||||
$parsed_name_from_description = $importer -> debugExtractNameFromMimeHeaders( $raw_headers_with_description );
|
||||
assert_true(
|
||||
$parsed_name_from_description === 'VIDOK_Instrukcja Montażu I Uruchomienia - Wyroby Elektryczne.pdf',
|
||||
'Expected parser to use Content-Description when filename is only technical att_*.'
|
||||
);
|
||||
|
||||
$normalized_octet_pdf = MailToTaskImporter::normalizeImportedAttachmentName(
|
||||
'',
|
||||
'application/octet-stream',
|
||||
"%PDF-1.6\n..."
|
||||
);
|
||||
assert_true(
|
||||
$normalized_octet_pdf === 'zalacznik.pdf',
|
||||
'Expected unnamed octet-stream with PDF signature to get pdf extension.'
|
||||
);
|
||||
}
|
||||
@@ -25,4 +25,24 @@ function run_task_attachment_repository_tests()
|
||||
&& substr( $sanitized, -4 ) === '.pdf',
|
||||
'Expected sanitized file name to remove unsupported characters and keep extension.'
|
||||
);
|
||||
|
||||
assert_true(
|
||||
TaskAttachmentRepository::detectExtensionFromContent( "%PDF-1.7\nexample" ) === 'pdf',
|
||||
'Expected PDF extension to be detected from binary content.'
|
||||
);
|
||||
|
||||
assert_true(
|
||||
TaskAttachmentRepository::effectiveTitleWithExtension( '', 'zalacznik', 'pdf', '' ) === 'zalacznik.pdf',
|
||||
'Expected display title fallback to include file extension.'
|
||||
);
|
||||
|
||||
assert_true(
|
||||
TaskAttachmentRepository::effectiveTitleWithExtension(
|
||||
'zalacznik.pdf',
|
||||
'VIDOK_Instrukcja Montażu I Uruchomienia - Wyroby Elektryczne.pdf',
|
||||
'pdf',
|
||||
''
|
||||
) === 'VIDOK_Instrukcja Montażu I Uruchomienia - Wyroby Elektryczne.pdf',
|
||||
'Expected generic title to be ignored when original file name is available.'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
require_once __DIR__ . '/Domain/Tasks/WorkTimeRepositoryTest.php';
|
||||
require_once __DIR__ . '/Domain/Tasks/TaskAttachmentRepositoryTest.php';
|
||||
require_once __DIR__ . '/Domain/Tasks/MailToTaskImporterTest.php';
|
||||
require_once __DIR__ . '/Domain/Users/UserRepositoryTest.php';
|
||||
require_once __DIR__ . '/Controllers/TasksControllerTest.php';
|
||||
require_once __DIR__ . '/Controllers/UsersControllerTest.php';
|
||||
@@ -9,6 +10,7 @@ require_once __DIR__ . '/Controllers/UsersControllerTest.php';
|
||||
$tests = [
|
||||
'run_work_time_repository_tests',
|
||||
'run_task_attachment_repository_tests',
|
||||
'run_mail_to_task_importer_tests',
|
||||
'run_user_repository_tests',
|
||||
'run_tasks_controller_tests',
|
||||
'run_users_controller_tests'
|
||||
|
||||
Reference in New Issue
Block a user