Fix code quality violations and enhance ROUTE-EXISTS-01 rule
Implement JQHTML function cache ID system and fix bundle compilation Implement underscore prefix for system tables Fix JS syntax linter to support decorators and grant exception to Task system SPA: Update planning docs and wishlists with remaining features SPA: Document Navigation API abandonment and future enhancements Implement SPA browser integration with History API (Phase 1) Convert contacts view page to SPA action Convert clients pages to SPA actions and document conversion procedure SPA: Merge GET parameters and update documentation Implement SPA route URL generation in JavaScript and PHP Implement SPA bootstrap controller architecture Add SPA routing manual page (rsx:man spa) Add SPA routing documentation to CLAUDE.md Phase 4 Complete: Client-side SPA routing implementation Update get_routes() consumers for unified route structure Complete SPA Phase 3: PHP-side route type detection and is_spa flag Restore unified routes structure and Manifest_Query class Refactor route indexing and add SPA infrastructure Phase 3 Complete: SPA route registration in manifest Implement SPA Phase 2: Extract router code and test decorators Rename Jqhtml_Component to Component and complete SPA foundation setup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
229
app/RSpade/Core/Files/CLAUDE.md
Executable file
229
app/RSpade/Core/Files/CLAUDE.md
Executable file
@@ -0,0 +1,229 @@
|
||||
# File Attachments System
|
||||
|
||||
## Overview
|
||||
|
||||
The RSpade file attachment system provides secure, session-based file uploads with automatic thumbnail generation and polymorphic model associations.
|
||||
|
||||
## Upload Flow
|
||||
|
||||
**Security Model**: Files upload UNATTACHED → validate → assign via API
|
||||
|
||||
1. User uploads file to `/_upload` endpoint
|
||||
2. File saved with `session_id`, no model association
|
||||
3. Returns unique `key` to frontend
|
||||
4. Frontend calls API endpoint with key
|
||||
5. Backend validates ownership and assigns to model
|
||||
|
||||
## Security Implementation
|
||||
|
||||
**Session-based validation** prevents cross-user file assignment:
|
||||
- Files get `session_id` on upload
|
||||
- `can_user_assign_this_file()` validates:
|
||||
- File not already assigned
|
||||
- Same site_id as user's session
|
||||
- Same session_id (prevents cross-user assignment)
|
||||
- User-provided `fileable_*` params ignored during upload
|
||||
|
||||
## Attachment API
|
||||
|
||||
### File_Attachment_Model Methods
|
||||
|
||||
```php
|
||||
// Find attachment by key
|
||||
$attachment = File_Attachment_Model::find_by_key($key);
|
||||
|
||||
// Validate user can assign
|
||||
if ($attachment->can_user_assign_this_file()) {
|
||||
// Single file attachment (replaces existing)
|
||||
$attachment->attach_to($user, 'profile_photo');
|
||||
|
||||
// Multiple file attachment (adds to collection)
|
||||
$attachment->add_to($project, 'documents');
|
||||
}
|
||||
|
||||
// Remove assignment
|
||||
$attachment->detach();
|
||||
|
||||
// Check assignment status
|
||||
if ($attachment->is_attached()) {
|
||||
// File is assigned to a model
|
||||
}
|
||||
```
|
||||
|
||||
### Model Helper Methods
|
||||
|
||||
All models extending `Rsx_Model_Abstract` have attachment helpers:
|
||||
|
||||
```php
|
||||
// Get single attachment
|
||||
$photo = $user->get_attachment('profile_photo');
|
||||
|
||||
// Get multiple attachments
|
||||
$docs = $project->get_attachments('documents');
|
||||
|
||||
// Count attachments
|
||||
$count = $project->count_attachments('documents');
|
||||
|
||||
// Check if has attachments
|
||||
if ($user->has_attachment('profile_photo')) {
|
||||
// User has profile photo
|
||||
}
|
||||
```
|
||||
|
||||
## Display URLs
|
||||
|
||||
```php
|
||||
// Thumbnail with specific dimensions
|
||||
$photo->get_thumbnail_url('cover', 128, 128);
|
||||
|
||||
// Full file URL
|
||||
$photo->get_url();
|
||||
|
||||
// Force download URL
|
||||
$photo->get_download_url();
|
||||
|
||||
// Get file metadata
|
||||
$size = $photo->file_size;
|
||||
$mime = $photo->mime_type;
|
||||
$name = $photo->original_filename;
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
|
||||
- `/_upload` - File upload endpoint
|
||||
- `/_download/:key` - Force download file
|
||||
- `/_thumbnail/:key/:type/:w/:h` - Generated thumbnail
|
||||
- `/_file/:key` - Direct file access
|
||||
|
||||
## Thumbnail System
|
||||
|
||||
Thumbnails are generated on-demand and cached:
|
||||
|
||||
**Preset types** (defined in config):
|
||||
- `cover` - Cover image aspect ratio
|
||||
- `square` - 1:1 aspect ratio
|
||||
- `landscape` - 16:9 aspect ratio
|
||||
|
||||
**Dynamic thumbnails**:
|
||||
- Limited to `max_dynamic_size` (default 2000px)
|
||||
- Cached for performance
|
||||
- Automatic cleanup via scheduled task
|
||||
|
||||
## Controller Implementation Pattern
|
||||
|
||||
```php
|
||||
#[Ajax_Endpoint]
|
||||
public static function save_with_photo(Request $request, array $params = [])
|
||||
{
|
||||
// Validate required fields
|
||||
if (empty($params['name'])) {
|
||||
return response_form_error('Validation failed', [
|
||||
'name' => 'Name is required'
|
||||
]);
|
||||
}
|
||||
|
||||
// Save model
|
||||
$user = new User_Model();
|
||||
$user->name = $params['name'];
|
||||
$user->save();
|
||||
|
||||
// Attach photo if provided
|
||||
if (!empty($params['photo_key'])) {
|
||||
$photo = File_Attachment_Model::find_by_key($params['photo_key']);
|
||||
|
||||
if (!$photo || !$photo->can_user_assign_this_file()) {
|
||||
return response_form_error('Invalid file', [
|
||||
'photo' => 'File not found or access denied'
|
||||
]);
|
||||
}
|
||||
|
||||
$photo->attach_to($user, 'profile_photo');
|
||||
}
|
||||
|
||||
return ['user_id' => $user->id];
|
||||
}
|
||||
```
|
||||
|
||||
## Frontend Upload Component
|
||||
|
||||
```javascript
|
||||
// Using Rsx_File_Upload component
|
||||
<Rsx_File_Upload
|
||||
$id="photo_upload"
|
||||
$accept="image/*"
|
||||
$max_size="5242880"
|
||||
/>
|
||||
|
||||
// Get uploaded file key
|
||||
const key = this.$id('photo_upload').component().get_file_key();
|
||||
|
||||
// Submit with form
|
||||
const data = {
|
||||
name: this.$id('name').val(),
|
||||
photo_key: key
|
||||
};
|
||||
await Controller.save_with_photo(data);
|
||||
```
|
||||
|
||||
## Database Schema
|
||||
|
||||
```sql
|
||||
file_attachments
|
||||
├── id (bigint)
|
||||
├── key (varchar 64, unique)
|
||||
├── site_id (bigint)
|
||||
├── session_id (bigint)
|
||||
├── fileable_type (varchar 255, nullable)
|
||||
├── fileable_id (bigint, nullable)
|
||||
├── fileable_key (varchar 255, nullable)
|
||||
├── storage_path (varchar 500)
|
||||
├── original_filename (varchar 500)
|
||||
├── mime_type (varchar 255)
|
||||
├── file_size (bigint)
|
||||
├── metadata (json)
|
||||
└── timestamps
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
In `/system/config/rsx.php`:
|
||||
|
||||
```php
|
||||
'attachments' => [
|
||||
'upload_dir' => storage_path('rsx-attachments'),
|
||||
'max_upload_size' => 10 * 1024 * 1024, // 10MB
|
||||
'allowed_extensions' => ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx'],
|
||||
],
|
||||
|
||||
'thumbnails' => [
|
||||
'presets' => [
|
||||
'cover' => ['width' => 800, 'height' => 600],
|
||||
'square' => ['width' => 300, 'height' => 300],
|
||||
],
|
||||
'max_dynamic_size' => 2000,
|
||||
'quotas' => [
|
||||
'preset_max_bytes' => 500 * 1024 * 1024, // 500MB
|
||||
'dynamic_max_bytes' => 100 * 1024 * 1024, // 100MB
|
||||
],
|
||||
],
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Never trust client-provided fileable_* params** during upload
|
||||
2. **Always validate ownership** before assignment
|
||||
3. **Use polymorphic associations** for flexibility
|
||||
4. **Implement access control** in download endpoints
|
||||
5. **Sanitize filenames** to prevent directory traversal
|
||||
6. **Validate MIME types** server-side
|
||||
7. **Set appropriate upload size limits**
|
||||
8. **Use scheduled cleanup** for orphaned files
|
||||
|
||||
## Scheduled Cleanup
|
||||
|
||||
Orphaned files (uploaded but never assigned) are cleaned automatically:
|
||||
- Files older than 24 hours without assignment
|
||||
- Runs daily via scheduled task
|
||||
- Preserves actively used files
|
||||
|
||||
See also: `php artisan rsx:man file_upload`
|
||||
Reference in New Issue
Block a user