feat: Update MailToTaskImporter to handle attachment MIME types and improve filename handling; add CLAUDE.md for project guidance; create TODO.md for PDF attachment issue
This commit is contained in:
@@ -11,7 +11,8 @@
|
||||
"Bash(grep:*)",
|
||||
"WebFetch(domain:storefront.developers.shoper.pl)",
|
||||
"WebFetch(domain:developers.shoper.pl)",
|
||||
"WebFetch(domain:pomoc.home.pl)"
|
||||
"WebFetch(domain:pomoc.home.pl)",
|
||||
"Bash(cd:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
18
.vscode/ftp-kr.sync.cache.json
vendored
18
.vscode/ftp-kr.sync.cache.json
vendored
@@ -121,8 +121,8 @@
|
||||
},
|
||||
"class.Tasks.php": {
|
||||
"type": "-",
|
||||
"size": 23567,
|
||||
"lmtime": 1771241116941,
|
||||
"size": 24179,
|
||||
"lmtime": 1771336367894,
|
||||
"modified": false
|
||||
},
|
||||
"class.Users.php": {
|
||||
@@ -251,8 +251,8 @@
|
||||
".claude": {
|
||||
"settings.local.json": {
|
||||
"type": "-",
|
||||
"size": 301,
|
||||
"lmtime": 1771236164946,
|
||||
"size": 449,
|
||||
"lmtime": 1771336223831,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
@@ -323,9 +323,9 @@
|
||||
},
|
||||
"main_view.php": {
|
||||
"type": "-",
|
||||
"size": 42781,
|
||||
"lmtime": 1770583657535,
|
||||
"modified": true
|
||||
"size": 42784,
|
||||
"lmtime": 1771336223833,
|
||||
"modified": false
|
||||
},
|
||||
"task_edit.php": {
|
||||
"type": "-",
|
||||
@@ -335,8 +335,8 @@
|
||||
},
|
||||
"task_popup.php": {
|
||||
"type": "-",
|
||||
"size": 20929,
|
||||
"lmtime": 1771241165284,
|
||||
"size": 22706,
|
||||
"lmtime": 1771336355877,
|
||||
"modified": false
|
||||
},
|
||||
"task_single.php": {
|
||||
|
||||
100
CLAUDE.md
Normal file
100
CLAUDE.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
CRM PRO is a Polish-language CRM application for task/project management, client management, finances, and work time tracking. Built with PHP + MySQL, Bootstrap, jQuery, and PHP templating.
|
||||
|
||||
## Running the Application
|
||||
|
||||
- **Web entry point:** `index.php` — routes via `?module=<module>&action=<action>` query params
|
||||
- **AJAX endpoint:** `ajax.php`
|
||||
- **REST API:** `api.php`
|
||||
- **Background jobs:** `cron.php` (email import, recursive tasks, reminders)
|
||||
- **Tests:** `php tests/run.php` (custom lightweight test runner, no PHPUnit)
|
||||
- **SCSS:** compiled via VS Code Live Sass Compile extension
|
||||
|
||||
## Architecture
|
||||
|
||||
### Layered structure with ongoing DDD migration
|
||||
|
||||
```
|
||||
autoload/
|
||||
├── Controllers/ # NEW: PSR-4 namespaced, camelCase methods
|
||||
├── controls/ # LEGACY: snake_case methods, being gradually replaced
|
||||
├── factory/ # Data access + business logic (legacy, being replaced by Domain)
|
||||
├── Domain/ # NEW: Repository pattern, single-responsibility classes
|
||||
│ ├── Tasks/ # WorkTimeRepository, TaskAttachmentRepository, MailToTaskImporter
|
||||
│ ├── Crm/ # ClientRepository
|
||||
│ ├── Finances/ # FinanceRepository
|
||||
│ └── Users/ # UserRepository
|
||||
├── view/ # View rendering layer
|
||||
├── class.S.php # Global static utility (sessions, request params, email, hashing)
|
||||
├── class.Tpl.php # Template engine: Tpl::view('path', $data)
|
||||
├── class.DbModel.php # Simple ActiveRecord wrapper
|
||||
└── class.Html.php # HTML form helper components
|
||||
templates/ # PHP templates organized by module
|
||||
templates_user/ # Custom user template overrides
|
||||
```
|
||||
|
||||
### Routing (`controls\Site::route()`)
|
||||
|
||||
1. Takes `module` and `action` GET params
|
||||
2. Tries `\Controllers\{Module}Controller::{camelCaseAction}()` first
|
||||
3. Falls back to `\controls\{Module}::{snake_case_action}()`
|
||||
|
||||
### Autoloading
|
||||
|
||||
Custom `spl_autoload_register` in `index.php`: maps `Namespace\Class` → `autoload/Namespace/Class.php`, falling back to `autoload/Namespace/class.Class.php`.
|
||||
|
||||
### Database access
|
||||
|
||||
- **Medoo** (`$mdb` global) — primary query builder for SELECT/INSERT/UPDATE/DELETE
|
||||
- **RedBean** (`\R`) — ORM used for some entity operations
|
||||
- Both configured in `index.php` from `config.php` credentials
|
||||
|
||||
### Key globals
|
||||
|
||||
- `$mdb` — Medoo database instance
|
||||
- `$user` — current session user array (`\S::get_session('user')`)
|
||||
- `$settings` — merged app settings from `config.php` + DB `settings` table
|
||||
- `\S::get('param')` — safe request parameter access
|
||||
|
||||
## Refactoring Status (see REFACTORING_PLAN.md)
|
||||
|
||||
- **Stage 1 (DONE):** Tasks/WorkTime migrated to `Domain\Tasks\WorkTimeRepository`
|
||||
- **Stage 2 (IN PROGRESS):** Controller standardization — `TasksController` partially migrated
|
||||
- **Stage 3 (DONE):** UI cleanup for work time billing
|
||||
- **Stage 4 (NEXT):** Finance domain extraction
|
||||
- **Stage 5 (NEXT):** View layer standardization
|
||||
|
||||
### Migration rules
|
||||
|
||||
- No big-bang rewrites — one functional area per commit
|
||||
- New code goes in `Domain/` (repositories) and `Controllers/` (camelCase)
|
||||
- Legacy `controls/` and `factory/` kept as adapters until full migration
|
||||
- Every migrated method must have at least one test in `tests/`
|
||||
|
||||
## Coding Conventions
|
||||
|
||||
- **New controllers:** `Controllers\{Module}Controller` with camelCase methods
|
||||
- **New domain code:** `Domain\{Module}\{Name}Repository` with constructor-injected `$mdb`
|
||||
- **Legacy code:** `controls\{Module}` and `factory\{Module}` with snake_case methods
|
||||
- **Templates:** rendered via `\Tpl::view('module/template', $data_array)`, XSS protection via `\Tpl::secureHTML()`
|
||||
- **UI language:** Polish (labels, comments, database content)
|
||||
- **File naming:** new classes `ClassName.php`, legacy classes `class.ClassName.php`
|
||||
|
||||
## Key Database Tables
|
||||
|
||||
- `tasks`, `tasks_work`, `tasks_attachments`, `task_user`, `task_action` — task management
|
||||
- `crm_client` — client records
|
||||
- `finance_operations`, `finance_categories` — finances
|
||||
- `users`, `users_permissions` — auth and RBAC
|
||||
- `tasks_filtrs` — saved user filters
|
||||
|
||||
## Authentication
|
||||
|
||||
- Email + password with PHP sessions, IP validation, cookie-based remember-me
|
||||
- Permission checks via `\controls\Users::permissions($user_id, $module)`
|
||||
- Admin (user ID 1) can impersonate other users
|
||||
1
TODO.md
Normal file
1
TODO.md
Normal file
@@ -0,0 +1 @@
|
||||
1. W wiadomości tworzonej z maila pliki PDF dodały się jako att_6995b8ad9d4567. Trzeba to poprawić.
|
||||
@@ -204,7 +204,18 @@ class MailToTaskImporter
|
||||
// Zapisz normalne załączniki
|
||||
foreach ( $content['attachments'] as $attachment )
|
||||
{
|
||||
$this -> attachments -> uploadFromContent( $task_id, self::TASK_USER_ID, $attachment['name'], $attachment['content'] );
|
||||
$att_name = $attachment['name'];
|
||||
$att_mime = isset( $attachment['mime'] ) ? $attachment['mime'] : '';
|
||||
|
||||
// Jeśli nazwa nie ma rozszerzenia, dodaj je z MIME type
|
||||
if ( pathinfo( $att_name, PATHINFO_EXTENSION ) === '' && $att_mime !== '' )
|
||||
{
|
||||
$mime_ext = $this -> extensionFromMime( $att_mime );
|
||||
if ( $mime_ext !== '' )
|
||||
$att_name .= '.' . $mime_ext;
|
||||
}
|
||||
|
||||
$this -> attachments -> uploadFromContent( $task_id, self::TASK_USER_ID, $att_name, $attachment['content'] );
|
||||
}
|
||||
|
||||
$import_status = 'imported';
|
||||
@@ -466,7 +477,8 @@ class MailToTaskImporter
|
||||
|
||||
$attachments[] = [
|
||||
'name' => $part['name'],
|
||||
'content' => $part['body']
|
||||
'content' => $part['body'],
|
||||
'mime' => isset( $part['mime'] ) ? $part['mime'] : 'application/octet-stream'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -535,6 +547,14 @@ class MailToTaskImporter
|
||||
|
||||
$name = $this -> decodeHeaderValue( $name );
|
||||
|
||||
// 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 ) === '' ) )
|
||||
{
|
||||
$mime_name = $this -> extractNameFromRawMime( $imap, $message_no, $part_number );
|
||||
if ( $mime_name !== '' )
|
||||
$name = $mime_name;
|
||||
}
|
||||
|
||||
$disposition = isset( $part -> disposition ) ? strtoupper( trim( (string)$part -> disposition ) ) : '';
|
||||
$content_id = '';
|
||||
if ( isset( $part -> id ) and trim( (string)$part -> id ) !== '' )
|
||||
@@ -578,7 +598,113 @@ class MailToTaskImporter
|
||||
}
|
||||
}
|
||||
|
||||
return $params;
|
||||
return $this -> reassembleRfc2231Params( $params );
|
||||
}
|
||||
|
||||
private function reassembleRfc2231Params( array $params )
|
||||
{
|
||||
$normal = [];
|
||||
$rfc2231 = [];
|
||||
|
||||
foreach ( $params as $key => $value )
|
||||
{
|
||||
if ( preg_match( '/^([^*]+)\*(\d+)?\*?$/', $key, $m ) )
|
||||
{
|
||||
$base = $m[1];
|
||||
$index = isset( $m[2] ) && $m[2] !== '' ? (int)$m[2] : 0;
|
||||
$rfc2231[$base][$index] = $value;
|
||||
}
|
||||
else
|
||||
$normal[$key] = $value;
|
||||
}
|
||||
|
||||
foreach ( $rfc2231 as $base => $parts )
|
||||
{
|
||||
ksort( $parts, SORT_NUMERIC );
|
||||
$combined = implode( '', $parts );
|
||||
|
||||
if ( preg_match( "/^([^']*)'([^']*)'(.+)$/s", $combined, $enc ) )
|
||||
{
|
||||
$charset = strtoupper( $enc[1] );
|
||||
$decoded = rawurldecode( $enc[3] );
|
||||
if ( $charset !== '' && $charset !== 'UTF-8' && function_exists( 'mb_convert_encoding' ) )
|
||||
$decoded = @mb_convert_encoding( $decoded, 'UTF-8', $charset );
|
||||
$combined = $decoded;
|
||||
}
|
||||
|
||||
if ( $combined !== '' || !isset( $normal[$base] ) )
|
||||
$normal[$base] = $combined;
|
||||
}
|
||||
|
||||
return $normal;
|
||||
}
|
||||
|
||||
private function extractNameFromRawMime( $imap, $message_no, $part_number )
|
||||
{
|
||||
$headers = (string)@imap_fetchmime( $imap, $message_no, $part_number, FT_PEEK );
|
||||
if ( trim( $headers ) === '' )
|
||||
return '';
|
||||
|
||||
// Szukaj filename w Content-Disposition, potem name w Content-Type
|
||||
foreach ( [ 'filename', 'name' ] as $param_name )
|
||||
{
|
||||
// RFC 2231 z kontynuacją: param*0*=...; param*1*=...
|
||||
$rfc2231_parts = [];
|
||||
if ( preg_match_all( '/' . preg_quote( $param_name, '/' ) . '\*(\d+)\*?\s*=\s*([^\r\n;]+)/i', $headers, $matches, PREG_SET_ORDER ) )
|
||||
{
|
||||
foreach ( $matches as $m )
|
||||
$rfc2231_parts[(int)$m[1]] = trim( $m[2], " \t\"'" );
|
||||
|
||||
if ( !empty( $rfc2231_parts ) )
|
||||
{
|
||||
ksort( $rfc2231_parts, SORT_NUMERIC );
|
||||
$combined = implode( '', $rfc2231_parts );
|
||||
if ( preg_match( "/^([^']*)'([^']*)'(.+)$/s", $combined, $enc ) )
|
||||
{
|
||||
$charset = strtoupper( $enc[1] );
|
||||
$decoded = rawurldecode( $enc[3] );
|
||||
if ( $charset !== '' && $charset !== 'UTF-8' && function_exists( 'mb_convert_encoding' ) )
|
||||
$decoded = @mb_convert_encoding( $decoded, 'UTF-8', $charset );
|
||||
$combined = $decoded;
|
||||
}
|
||||
if ( trim( $combined ) !== '' && pathinfo( $combined, PATHINFO_EXTENSION ) !== '' )
|
||||
return trim( $combined );
|
||||
}
|
||||
}
|
||||
|
||||
// RFC 2231 prosty: param*=charset'lang'value
|
||||
if ( preg_match( '/' . preg_quote( $param_name, '/' ) . '\*\s*=\s*([^\r\n;]+)/i', $headers, $m ) )
|
||||
{
|
||||
$val = trim( $m[1], " \t\"'" );
|
||||
if ( preg_match( "/^([^']*)'([^']*)'(.+)$/s", $val, $enc ) )
|
||||
{
|
||||
$charset = strtoupper( $enc[1] );
|
||||
$decoded = rawurldecode( $enc[3] );
|
||||
if ( $charset !== '' && $charset !== 'UTF-8' && function_exists( 'mb_convert_encoding' ) )
|
||||
$decoded = @mb_convert_encoding( $decoded, 'UTF-8', $charset );
|
||||
$val = $decoded;
|
||||
}
|
||||
if ( trim( $val ) !== '' && pathinfo( $val, PATHINFO_EXTENSION ) !== '' )
|
||||
return 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 ( 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 );
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function decodePartBody( $raw, $encoding )
|
||||
@@ -1048,8 +1174,32 @@ class MailToTaskImporter
|
||||
}
|
||||
|
||||
private function guessExtensionFromMime( $mime )
|
||||
{
|
||||
$ext = $this -> extensionFromMime( $mime );
|
||||
return $ext !== '' ? $ext : 'png';
|
||||
}
|
||||
|
||||
private function extensionFromMime( $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',
|
||||
@@ -1060,7 +1210,7 @@ class MailToTaskImporter
|
||||
];
|
||||
|
||||
$mime = strtolower( trim( (string)$mime ) );
|
||||
return isset( $map[$mime] ) ? $map[$mime] : 'png';
|
||||
return isset( $map[$mime] ) ? $map[$mime] : '';
|
||||
}
|
||||
|
||||
private function replaceCidReferences( $html, array $cid_to_url )
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
<? if ( is_array( $this -> task_attachments ) and count( $this -> task_attachments ) ):?>
|
||||
<? foreach ( $this -> task_attachments as $attachment ):?>
|
||||
<li>
|
||||
<a href="<?= $attachment['url'];?>" target="_blank" rel="noopener noreferrer" class="attachment-link">
|
||||
<a href="<?= $attachment['url'];?>" target="_blank" rel="noopener noreferrer" class="attachment-link" download="<?= htmlspecialchars( $attachment['title_effective'] );?>">
|
||||
<?= htmlspecialchars( $attachment['title_effective'] );?>
|
||||
</a>
|
||||
<small>(<?= $attachment['size_human'];?>)</small>
|
||||
|
||||
Reference in New Issue
Block a user