PHP Classes

File: docs/proposals/FileAreas_Proposal.md

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   docs/proposals/FileAreas_Proposal.md   Download  
File: docs/proposals/FileAreas_Proposal.md
Role: Auxiliary data
Content type: text/markdown
Description: Auxiliary data
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 100,084 bytes
 

Contents

Class file image Download

File Area System Proposal

DRAFT DOCUMENT: This proposal is a draft that was generated by AI and may not have been reviewed for accuracy. Implementation details may change based on technical constraints, security considerations, and user feedback.

Executive Summary

This proposal outlines a comprehensive file area system for BinktermPHP that enables users to upload, download, and share files through both the web interface and Fidonet networking. The system includes virus scanning, credit-based economy integration, sysop approval workflows, and future support for Fidonet file requests (FREQ).

Table of Contents

  1. Overview
  2. Design Decisions
  3. Architecture
  4. Database Schema
  5. Core Components
  6. API Endpoints
  7. User Interface
  8. Integration Pointsthe system
  9. Security Considerations
  10. Implementation Roadmap
  11. Future Enhancements

Overview

The file area system extends BinktermPHP's capabilities beyond message-based communication to include file distribution, similar to traditional BBS file bases. This feature enables:

  • User File Sharing: Upload and download files through the web interface
  • Fidonet File Distribution: Receive and distribute files via binkp protocol
  • Private File Storage: Users receive netmail attachments in their private file area
  • Credit Economy: Reward uploads and charge for downloads (public files only)
  • Quality Control: Sysop approval queue for user-uploaded content
  • Security: Optional ClamAV virus scanning for all files
  • Multi-Network Support: File areas per network domain
  • Future FREQ Support: Framework for Fidonet file request protocol

Use Cases

  1. Software Distribution: Share utilities, tools, and applications
  2. Document Libraries: Technical documentation, guides, tutorials
  3. Media Sharing: Images, music, videos (subject to size limits)
  4. Archive Preservation: Historical BBS files, vintage software
  5. Network Collaboration: Share files between Fidonet nodes
  6. User Contributions: Allow users to upload content for community benefit
  7. Private File Storage: Users receive files via netmail attachments in their private file area

Design Decisions

Based on stakeholder input and architectural review, the following design decisions have been made:

1. Credits Integration

Decision: Integrate with existing credit system Rationale: Incentivizes quality uploads and prevents abuse of downloads Implementation: - Charge configurable credits per download - Award configurable credits for approved uploads - Use existing UserCredit class for consistency

2. Storage Structure

Decision: Organize by file area tag (data/files/{area_tag}/{filename}) Rationale: Mirrors Fidonet file area structure, simplifies backup/management Benefits: - Easy to locate files for specific areas - Supports area-level operations (archive, delete, backup) - Aligns with Fidonet conventions

3. Metadata Tracking

Decision: Store both short and long descriptions plus download statistics Rationale: Follows BBS tradition, provides user-friendly browsing Fields: - Short description (255 chars) - Quick summary - Long description (unlimited) - Detailed information - Download count, last download date - Uploader information

4. File Request Support

Decision: Plan for both binkp FREQ and netmail-based requests Rationale: Maximum compatibility with Fidonet ecosystem Phases: - Phase 1: Database schema and queue system (this proposal) - Phase 2: Binkp M_GET frame handling - Phase 3: Netmail command parsing

5. Virus Detection Strategy

Decision: Flag infected files with visual indicator, don't auto-delete Rationale: Allows sysop review of false positives, maintains audit trail Workflow: - Scan uploads with ClamAV (optional) - Mark infected files with status='quarantined' - Move to .quarantine/ directory - Display prominent warning in approval queue - Sysop makes final decision

6. Size Limits

Decision: Per-area configurable maximum file size Rationale: Flexibility for different area purposes Examples: - Text files area: 1 MB limit - General files: 10 MB limit - Large archives: 100 MB limit - Sysop-only area: Unlimited

7. Duplicate Handling

Decision: Reject duplicates with error message (no auto-rename) Rationale: Prevents confusion, maintains file integrity Implementation: - Calculate SHA256 hash of uploaded file - Check for existing file with same hash in same area - Return error if duplicate found - UNIQUE constraint: (file_area_id, file_hash)

8. File Type Control

Decision: Configurable blacklist per area (e.g., .exe, .bat) Rationale: Security and content control flexibility Features: - Whitelist mode: allowed_extensions (e.g., .txt,.zip,.pdf) - Blacklist mode: blocked_extensions (e.g., .exe,.bat,.com) - Admin can configure per-area

9. Private File Storage

Decision: Use is_private flag instead of creating private file areas per user Rationale: Simpler implementation, reuses existing infrastructure Implementation: - Set is_private = TRUE on file record - Set file_area_id = NULL (not associated with public area) - Store in data/files/private/{user_id}/ directory - Access controlled by uploaded_by_user_id ownership - No approval workflow (auto-approved) - No credit costs for downloading own files Benefits: - Single files table for both public and private files - Reuses virus scanning, download handlers, storage logic - Simple access control (owner check) - Easy to implement file transfer from private ? public

Architecture

Architecture Pattern

The file area system follows existing BinktermPHP patterns for consistency:

| Component | Pattern Source | Similarity | |-----------|----------------|------------| | Database Schema | echoareas table | Multi-network support, subscriptions, domains | | Approval Queue | crashmail_queue | Pending status, admin review, approval/rejection | | Admin UI | templates/echoareas.twig | List view, modals, color coding | | Queue Management | templates/admin/crashmail_queue.twig | Pending items, approve/reject actions | | API Endpoints | routes/admin-routes.php | RESTful design, admin authentication | | Credit Integration | Crashmail debit system | UserCredit class usage |

System Flow Diagram

???????????????????
?  User Upload    ?
?   (Web Form)    ?
???????????????????
         ?
         ?
???????????????????????????????
?  FileUploadHandler          ?
?  - Validate size/extension  ?
?  - Calculate SHA256 hash    ?
?  - Check duplicates         ?
?  - Scan virus (optional)    ?
???????????????????????????????
         ?
         ?
???????????????????????????????
?  Store to disk              ?
?  data/files/{tag}/{file}    ?
???????????????????????????????
         ?
         ?
???????????????????????????????
?  Create database record     ?
?  status = 'pending'         ?
???????????????????????????????
         ?
         ?
???????????????????????????????
?  Admin Approval Queue       ?
?  (FileApprovalController)   ?
???????????????????????????????
         ?
    ???????????
    ?         ?
?????????? ???????????
?Approve ? ? Reject  ?
?????????? ???????????
    ?           ?
    ?           ?
?????????? ????????????
?Award   ? ? Notify   ?
?Credits ? ? User     ?
?????????? ????????????
    ?
    ?
???????????????????????????????
?  File available for         ?
?  download                   ?
???????????????????????????????

???????????????????
? User Download   ?
???????????????????
         ?
         ?
???????????????????????????????
?  FileDownloadHandler        ?
?  - Check subscription       ?
?  - Check credits            ?
?  - Deduct credits           ?
?  - Log download             ?
???????????????????????????????
         ?
         ?
???????????????????????????????
?  Stream file to user        ?
???????????????????????????????

Database Schema

Migration File

Table: file_areas

File distribution areas configuration (similar to echoareas).

CREATE TABLE file_areas (
    id SERIAL PRIMARY KEY,
    tag VARCHAR(255) NOT NULL,
    description TEXT,
    domain VARCHAR(255) NOT NULL DEFAULT 'fidonet',
    is_local BOOLEAN DEFAULT FALSE,
    is_active BOOLEAN DEFAULT TRUE,
    is_sysop_only BOOLEAN DEFAULT FALSE,
    color VARCHAR(7) DEFAULT '#007bff',

    -- File-specific settings
    max_file_size BIGINT DEFAULT 10485760, -- 10MB default
    allowed_extensions TEXT, -- Comma-separated: .txt,.zip,.pdf
    blocked_extensions TEXT, -- Comma-separated: .exe,.bat,.com
    download_credit_cost INTEGER DEFAULT 0,
    upload_credit_reward INTEGER DEFAULT 0,
    require_approval BOOLEAN DEFAULT TRUE,
    scan_virus BOOLEAN DEFAULT TRUE,

    -- Statistics
    file_count INTEGER DEFAULT 0,
    total_size BIGINT DEFAULT 0,
    download_count INTEGER DEFAULT 0,

    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT unique_tag_domain UNIQUE(tag, domain)
);

Key Fields: - tag: File area identifier (e.g., GENERAL_FILES, SOFTWARE) - domain: Network domain for multi-network support - is_local: If true, files not transmitted to uplinks - max_file_size: Maximum file size in bytes - require_approval: If false, uploads auto-approved - scan_virus: Enable ClamAV scanning for this area

Table: files

File metadata and tracking.

CREATE TABLE files (
    id SERIAL PRIMARY KEY,
    file_area_id INTEGER REFERENCES file_areas(id) ON DELETE CASCADE,

    -- File information
    filename VARCHAR(255) NOT NULL,
    filesize BIGINT NOT NULL,
    file_hash VARCHAR(64) NOT NULL, -- SHA256
    storage_path TEXT NOT NULL,

    -- Upload information
    uploaded_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
    uploaded_from_address VARCHAR(255), -- Fidonet address (e.g., 1:123/456)
    source_type VARCHAR(20) DEFAULT 'user', -- 'user', 'fidonet', 'freq', 'netmail_attachment'

    -- Privacy
    is_private BOOLEAN DEFAULT FALSE, -- If true, only uploaded_by_user_id can access

    -- Descriptions
    short_description VARCHAR(255),
    long_description TEXT,

    -- Approval workflow (not used for private files)
    status VARCHAR(20) DEFAULT 'pending', -- pending, approved, rejected, quarantined
    approved_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
    approved_at TIMESTAMP,
    rejection_reason TEXT,

    -- Virus scanning
    virus_scanned BOOLEAN DEFAULT FALSE,
    virus_scan_result VARCHAR(20), -- clean, infected, error, skipped
    virus_signature VARCHAR(255), -- Name of detected virus
    virus_scanned_at TIMESTAMP,

    -- Download statistics
    download_count INTEGER DEFAULT 0,
    last_downloaded_at TIMESTAMP,

    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT unique_file_hash_per_area UNIQUE(file_area_id, file_hash)
);

Key Fields: - file_area_id: Can be NULL for private files (not associated with public area) - file_hash: SHA256 hash for duplicate detection - source_type: Track origin (user upload, fidonet, netmail_attachment, freq) - is_private: If TRUE, only the owning user can access (bypasses approval workflow) - status: Workflow state (pending ? approved/rejected/quarantined) - auto-set to 'approved' for private files - virus_scan_result: ClamAV scan outcome

Private Files Behavior: - Private files have is_private = TRUE and file_area_id = NULL - Stored in user-specific directory: data/files/private/{user_id}/ - No approval workflow (auto-approved upon receipt) - Only accessible by the owning user (via uploaded_by_user_id) - Created when files are received as netmail attachments - No credit costs for downloading your own private files - Still subject to virus scanning for security

Table: user_file_area_subscriptions

Access control for file areas.

CREATE TABLE user_file_area_subscriptions (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    file_area_id INTEGER NOT NULL REFERENCES file_areas(id) ON DELETE CASCADE,
    subscription_type VARCHAR(20) DEFAULT 'read_write', -- read_only, read_write, upload_only
    is_active BOOLEAN DEFAULT TRUE,
    subscribed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT unique_user_file_area UNIQUE(user_id, file_area_id)
);

Subscription Types: - read_write: Browse and upload (most common) - read_only: Browse and download only - upload_only: Upload only (rare, for contribution areas)

Table: file_downloads

Download history and audit trail.

CREATE TABLE file_downloads (
    id SERIAL PRIMARY KEY,
    file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
    user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
    credits_charged INTEGER DEFAULT 0,
    ip_address INET,
    user_agent TEXT,
    downloaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Purpose: - Track who downloaded what and when - Audit trail for credit charges - Statistics for popular files - Security monitoring

Table: file_requests

File request queue (FREQ) - future implementation.

CREATE TABLE file_requests (
    id SERIAL PRIMARY KEY,
    request_type VARCHAR(20) NOT NULL, -- 'binkp_freq', 'netmail_command'
    requested_filename VARCHAR(255) NOT NULL,
    requesting_address VARCHAR(255), -- Fidonet address
    requesting_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,

    status VARCHAR(20) DEFAULT 'pending', -- pending, matched, sent, failed
    matched_file_id INTEGER REFERENCES files(id) ON DELETE SET NULL,

    password VARCHAR(255), -- FREQ password (if required)
    error_message TEXT,

    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    processed_at TIMESTAMP,
    sent_at TIMESTAMP
);

Future Use: - Queue incoming FREQ requests - Match requests to available files - Track fulfillment status

Indexes

-- Performance indexes
CREATE INDEX idx_files_file_area_id ON files(file_area_id);
CREATE INDEX idx_files_status ON files(status);
CREATE INDEX idx_files_hash ON files(file_hash);
CREATE INDEX idx_files_uploaded_by ON files(uploaded_by_user_id);
CREATE INDEX idx_files_virus_scan ON files(virus_scanned, virus_scan_result);

CREATE INDEX idx_subscriptions_user_id ON user_file_area_subscriptions(user_id);
CREATE INDEX idx_subscriptions_file_area_id ON user_file_area_subscriptions(file_area_id);

CREATE INDEX idx_downloads_file_id ON file_downloads(file_id);
CREATE INDEX idx_downloads_user_id ON file_downloads(user_id);
CREATE INDEX idx_downloads_date ON file_downloads(downloaded_at);

CREATE INDEX idx_requests_status ON file_requests(status);
CREATE INDEX idx_requests_address ON file_requests(requesting_address);

Default Data

-- Create default file areas
INSERT INTO file_areas (tag, description, domain, is_local, color, max_file_size, download_credit_cost, upload_credit_reward) VALUES
('GENERAL_FILES', 'General purpose file area for miscellaneous files', 'fidonet', TRUE, '#007bff', 10485760, 10, 25),
('LOCALTEST_FILES', 'Testing area for local file uploads', 'fidonet', TRUE, '#6c757d', 5242880, 0, 0);

Core Components

1. FileAreaManager

File: src/FileAreaManager.php Pattern: Similar to EchoareaSubscriptionManager.php Purpose: Business logic for file area management and subscriptions

Key Methods

class FileAreaManager
{
    /
     * Get all file areas user is subscribed to
     */
    public function getUserSubscribedFileAreas(int $userId): array;

    /
     * Subscribe user to file area
     */
    public function subscribeUser(int $userId, int $fileAreaId, string $type = 'read_write'): bool;

    /
     * Unsubscribe user from file area
     */
    public function unsubscribeUser(int $userId, int $fileAreaId): bool;

    /
     * Check if user has access to file area
     */
    public function isUserSubscribed(int $userId, int $fileAreaId): bool;

    /
     * Get file area details with statistics
     */
    public function getFileAreaWithStats(int $fileAreaId): ?array;

    /
     * Get all file areas with statistics (admin)
     */
    public function getFileAreasWithStats(string $domain = 'fidonet'): array;

    /
     * Create new file area (admin)
     */
    public function createFileArea(array $data): int;

    /
     * Update file area (admin)
     */
    public function updateFileArea(int $id, array $data): bool;

    /
     * Delete file area (admin)
     */
    public function deleteFileArea(int $id): bool;

    /
     * Update file area statistics
     */
    public function updateStats(int $fileAreaId): void;
}

Responsibilities

  • Subscription management
  • Access control checks
  • CRUD operations for file areas
  • Statistics aggregation

2. FileUploadHandler

File: src/FileUploadHandler.php Pattern: Similar to packet processing in BinkdProcessor.php Purpose: Process and validate file uploads from users and Fidonet

Key Methods

class FileUploadHandler
{
    /
     * Handle user upload from web interface
     */
    public function handleUserUpload(
        int $fileAreaId,
        array $uploadedFile,
        int $userId,
        array $metadata
    ): array;

    /
     * Handle file from Fidonet (binkp)
     */
    public function handleFidonetFile(
        int $fileAreaId,
        string $filePath,
        string $fromAddress,
        array $metadata
    ): array;

    /
     * Validate file against area rules
     */
    protected function validateFile(array $fileArea, string $filePath): array;

    /
     * Calculate SHA256 hash
     */
    protected function calculateFileHash(string $filePath): string;

    /
     * Check for duplicate files
     */
    protected function isDuplicate(int $fileAreaId, string $hash): bool;

    /
     * Move file to storage location
     */
    protected function moveToStorage(string $tempPath, string $areaTag, string $filename): string;

    /
     * Create database record
     */
    protected function storeFileRecord(array $data): int;

    /
     * Trigger virus scan
     */
    protected function scanForViruses(string $filePath): array;
}

Upload Workflow

  1. Validation: - Check file size ? `max_file_size` - Validate extension against whitelist/blacklist - Calculate SHA256 hash - Check for duplicate hash in area
  2. Virus Scanning (if enabled): - Call `VirusScanner::scanFile()` - If infected: status='quarantined', move to `.quarantine/` - If clean: continue to storage
  3. Storage: - Move from temp to `data/files/{area_tag}/{filename}` - Set appropriate permissions (644)
  4. Database Record: - Insert into `files` table - Set status='pending' (user) or 'approved' (Fidonet if auto-approve enabled) - Record uploader, source, descriptions
  5. Logging: - Log upload via error_log or dedicated file logger - Log admin action if auto-approved

Return: ['success' => true, 'file_id' => 123] or ['success' => false, 'error' => 'message']

3. FileDownloadHandler

File: src/FileDownloadHandler.php Pattern: Similar to credit deduction in crashmail Purpose: Handle file downloads with credit integration

Key Methods

class FileDownloadHandler
{
    /
     * Initiate download process
     */
    public function initiateDownload(int $fileId, int $userId): array;

    /
     * Check user has access to file area
     */
    protected function checkAccess(int $fileId, int $userId): bool;

    /
     * Check user has sufficient credits
     */
    protected function checkCredits(int $fileId, int $userId): bool;

    /
     * Deduct credits from user balance
     */
    protected function deductCredits(int $fileId, int $userId): bool;

    /
     * Record download in history
     */
    protected function recordDownload(int $fileId, int $userId, int $creditsCharged): void;

    /
     * Update file statistics
     */
    protected function updateDownloadStats(int $fileId): void;

    /
     * Stream file to user
     */
    public function streamFile(int $fileId): void;
}

Download Workflow

  1. Access Check: - Verify user subscribed to file area - Check file status = 'approved' - Verify file exists on disk
  2. Credit Check: - Get `download_credit_cost` from file area - Check user balance via `UserCredit` class - If insufficient: return error
  3. Credit Deduction: - Call `UserCredit::debit($userId, $cost, $description)` - Transaction description: "Downloaded: {filename}"
  4. Download Logging: - Insert into `file_downloads` table - Record: file_id, user_id, credits_charged, IP, user agent, timestamp
  5. Statistics Update: - Increment `files.download_count` - Update `files.last_downloaded_at` - Increment `file_areas.download_count`
  6. File Streaming: - Set headers: Content-Type, Content-Disposition, Content-Length - Stream file in chunks (memory efficient) - Support resume/partial content (HTTP 206)

Error Handling: - Insufficient credits: Return error, no charge - File missing: Log error, refund if charged - Download interrupted: No refund (user initiated)

4. VirusScanner

File: src/VirusScanner.php Purpose: Optional ClamAV integration for virus detection

Key Methods

class VirusScanner
{
    /
     * Check if virus scanning is enabled
     */
    public static function isEnabled(): bool;

    /
     * Scan file for viruses
     */
    public static function scanFile(string $filePath): array;

    /
     * Update virus scan result in database
     */
    public static function updateVirusScanResult(int $fileId, array $result): void;

    /
     * Get scanner status/version
     */
    public static function getStatus(): array;
}

Configuration

Environment variables in .env:

CLAMAV_ENABLED=false
CLAMAV_PATH=/usr/bin/clamdscan
CLAMAV_SOCKET=/var/run/clamav/clamd.ctl
CLAMAV_TIMEOUT=30

Scan Results

Return array structure:

[
    'scanned' => true,
    'result' => 'clean|infected|error|skipped',
    'signature' => 'Win.Trojan.Eicar-1', // If infected
    'error' => 'Scanner timeout', // If error
    'scanned_at' => '2025-01-30 12:34:56'
]

Result Values: - clean: No virus detected - infected: Virus found (signature in 'signature' field) - error: Scan failed (error in 'error' field) - skipped: Scanning disabled

Implementation

public static function scanFile(string $filePath): array
{
    if (!self::isEnabled()) {
        return ['scanned' => false, 'result' => 'skipped'];
    }

    $clamPath = getenv('CLAMAV_PATH') ?: '/usr/bin/clamdscan';
    $timeout = intval(getenv('CLAMAV_TIMEOUT') ?: 30);

    $command = escapeshellarg($clamPath) . ' --no-summary ' . escapeshellarg($filePath);

    exec($command, $output, $returnCode);

    // Return code 0 = clean, 1 = infected, 2 = error
    switch ($returnCode) {
        case 0:
            return ['scanned' => true, 'result' => 'clean', 'scanned_at' => date('Y-m-d H:i:s')];
        case 1:
            // Parse signature from output
            $signature = self::parseSignature($output);
            return [
                'scanned' => true,
                'result' => 'infected',
                'signature' => $signature,
                'scanned_at' => date('Y-m-d H:i:s')
            ];
        default:
            return [
                'scanned' => true,
                'result' => 'error',
                'error' => implode("\n", $output),
                'scanned_at' => date('Y-m-d H:i:s')
            ];
    }
}

Testing: - Use EICAR test file for infected file testing - ClamAV must be installed and running (clamd daemon)

5. FileApprovalController

File: src/FileApprovalController.php Pattern: Similar to crashmail queue management Purpose: Admin approval workflow for user uploads

Key Methods

class FileApprovalController
{
    /
     * Get pending files for approval
     */
    public function getPendingFiles(?int $fileAreaId = null, int $limit = 50): array;

    /
     * Approve file
     */
    public function approveFile(int $fileId, int $adminUserId): bool;

    /
     * Reject file
     */
    public function rejectFile(int $fileId, int $adminUserId, string $reason): bool;

    /
     * Get approval statistics
     */
    public function getApprovalStats(): array;

    /
     * Get file details for approval review
     */
    public function getFileDetails(int $fileId): ?array;
}

Approval Workflow

Approve Action: 1. Update files table: - Set status='approved' - Set approved_by_user_id=$adminUserId - Set approved_at=NOW()

  1. Award Credits: - If source_type='user' and uploaded_by_user_id exists - Get upload_credit_reward from file area - Call `UserCredit::credit($userId, $amount, "File approved: {filename}", $adminUserId, UserCredit::TYPE_SYSTEM_REWARD)`
  2. Update Statistics: - Increment `file_areas.file_count` - Add to `file_areas.total_size`
  3. Log Action: - `AdminActionLogger::logAction($adminUserId, 'file_approved', ['file_id' => $fileId, 'filename' => $filename])`
  4. Notify User (optional): - Send netmail notification - Or web notification

Reject Action: 1. Update files table: - Set status='rejected' - Set rejection_reason - Set approved_by_user_id=$adminUserId (who rejected) - Set approved_at=NOW() (when rejected)

  1. File Handling Options: - Move to `.quarantine/` directory - Or delete from disk - Configurable via admin UI
  2. Log Action: - `AdminActionLogger::logAction($adminUserId, 'file_rejected', ['file_id' => $fileId, 'reason' => $reason])`
  3. Notify User: - Send rejection reason via netmail or web notification

Statistics

Return array:

[
    'pending_count' => 15,
    'approved_today' => 8,
    'rejected_today' => 2,
    'quarantined_count' => 3,
    'total_pending_size' => 125829120 // bytes
]

6. FileAreaController

File: src/FileAreaController.php Purpose: Web/API controller for user and admin endpoints

User Methods

class FileAreaController
{
    /
     * List file areas user has access to
     */
    public function handleListFileAreas(Request $request): Response;

    /
     * Browse files in a specific area
     */
    public function handleBrowseArea(Request $request, int $fileAreaId): Response;

    /
     * Upload file to area
     */
    public function handleUploadFile(Request $request, int $fileAreaId): Response;

    /
     * Download file
     */
    public function handleDownloadFile(Request $request, int $fileId): Response;

    /
     * Get file details
     */
    public function handleFileDetails(Request $request, int $fileId): Response;

    /
     * Subscribe to file area
     */
    public function handleSubscribe(Request $request, int $fileAreaId): Response;

    /
     * Unsubscribe from file area
     */
    public function handleUnsubscribe(Request $request, int $fileAreaId): Response;

    /
     * List user's private files (netmail attachments)
     */
    public function handleListPrivateFiles(Request $request): Response;

    /
     * Delete user's private file
     */
    public function handleDeletePrivateFile(Request $request, int $fileId): Response;
}

Admin Methods

/
 * File area management page
 */
public function handleAdminFileAreas(Request $request): Response;

/
 * Approval queue page
 */
public function handleApprovalQueue(Request $request): Response;

/
 * CRUD operations (via API)
 */
public function handleCreateFileArea(Request $request): Response;
public function handleUpdateFileArea(Request $request, int $id): Response;
public function handleDeleteFileArea(Request $request, int $id): Response;

/
 * Approval actions (via API)
 */
public function handleApproveFile(Request $request, int $fileId): Response;
public function handleRejectFile(Request $request, int $fileId): Response;

Private File Methods

/
 * List user's private files
 */
public function handleListPrivateFiles(Request $request): Response
{
    $userId = $request->user['id'];

    $db = Database::getInstance()->getPdo();
    $stmt = $db->prepare("
        SELECT id, filename, filesize, short_description, long_description,
               uploaded_from_address, virus_scan_result, created_at,
               download_count
        FROM files
        WHERE is_private = TRUE
          AND uploaded_by_user_id = ?
          AND status != 'deleted'
        ORDER BY created_at DESC
    ");
    $stmt->execute([$userId]);
    $files = $stmt->fetchAll(\PDO::FETCH_ASSOC);

    // Calculate storage usage
    $totalSize = array_sum(array_column($files, 'filesize'));
    $storageQuota = intval(getenv('FILES_PRIVATE_STORAGE_QUOTA') ?: 104857600);

    return $this->json([
        'success' => true,
        'files' => $files,
        'storage' => [
            'used' => $totalSize,
            'quota' => $storageQuota,
            'percent' => round(($totalSize / $storageQuota) * 100, 1)
        ]
    ]);
}

/
 * Delete user's private file
 */
public function handleDeletePrivateFile(Request $request, int $fileId): Response
{
    $userId = $request->user['id'];

    $db = Database::getInstance()->getPdo();

    // Verify ownership
    $stmt = $db->prepare("
        SELECT id, storage_path, is_private, uploaded_by_user_id
        FROM files
        WHERE id = ?
    ");
    $stmt->execute([$fileId]);
    $file = $stmt->fetch(\PDO::FETCH_ASSOC);

    if (!$file) {
        return $this->json(['success' => false, 'error' => 'File not found'], 404);
    }

    if (!$file['is_private'] || $file['uploaded_by_user_id'] != $userId) {
        return $this->json(['success' => false, 'error' => 'Access denied'], 403);
    }

    // Delete from disk
    if (file_exists($file['storage_path'])) {
        unlink($file['storage_path']);
    }

    // Delete from database
    $stmt = $db->prepare("DELETE FROM files WHERE id = ?");
    $stmt->execute([$fileId]);

    return $this->json([
        'success' => true,
        'message' => 'File deleted successfully'
    ]);
}

7. TicFileProcessor

File: src/TicFileProcessor.php Purpose: Process incoming TIC files and handle file distribution

Key Methods

class TicFileProcessor
{
    /
     * Process TIC file received via binkp
     */
    public function processTicFile(string $ticPath, string $filePath): array;

    /
     * Parse TIC file content
     */
    protected function parseTicFile(string $ticPath): array;

    /
     * Validate file against TIC metadata
     */
    protected function validateFile(array $ticData, string $filePath): bool;

    /
     * Calculate CRC32 checksum
     */
    protected function calculateCrc32(string $filePath): string;

    /
     * Store file in file area
     */
    protected function storeFile(array $ticData, string $filePath): int;

    /
     * Update TIC path and seenby lines
     */
    protected function updateTicRouting(array &$ticData, string $ourAddress): void;

    /
     * Forward TIC to downstream nodes
     */
    protected function forwardTic(array $ticData, int $fileId): void;

    /
     * Create TIC file for outbound
     */
    public function createTicFile(int $fileId, string $destAddress): string;

    /
     * Check if file area is subscribed
     */
    protected function isAreaSubscribed(string $areaTag): bool;
}

TIC File Format

Standard TIC Fields: - Area - File area tag (required) - File - Filename (required) - Desc - Short description (required) - From - Origin address (required) - Path - Routing path (required) - Seenby - Distribution tracking (required) - Size - File size in bytes - Crc - CRC32 checksum (8 hex digits) - Created - Creation program - Date - Creation timestamp - Pw - Password (optional) - Replaces - File being replaced (optional) - LDesc - Long description (multi-line, optional)

Processing Workflow

public function processTicFile(string $ticPath, string $filePath): array
{
    try {
        // Parse TIC file
        $ticData = $this->parseTicFile($ticPath);

        // Check if we're subscribed to this area
        if (!$this->isAreaSubscribed($ticData['Area'])) {
            return [
                'success' => false,
                'error' => "Not subscribed to file area: {$ticData['Area']}"
            ];
        }

        // Validate file
        if (!$this->validateFile($ticData, $filePath)) {
            return [
                'success' => false,
                'error' => 'File validation failed (size/CRC mismatch)'
            ];
        }

        // Store file
        $fileId = $this->storeFile($ticData, $filePath);

        // Update routing
        $this->updateTicRouting($ticData, $this->getOurAddress());

        // Forward to downstream nodes if configured
        $this->forwardTic($ticData, $fileId);

        return [
            'success' => true,
            'file_id' => $fileId,
            'area' => $ticData['Area']
        ];

    } catch (\Exception $e) {
        error_log("TIC processing error: " . $e->getMessage());
        return [
            'success' => false,
            'error' => $e->getMessage()
        ];
    }
}

protected function parseTicFile(string $ticPath): array
{
    $content = file_get_contents($ticPath);
    $lines = explode("\n", $content);

    $ticData = [
        'Path' => [],
        'Seenby' => [],
        'LDesc' => []
    ];

    foreach ($lines as $line) {
        $line = trim($line);
        if (empty($line)) continue;

        // Parse key value pairs
        if (preg_match('/^(\w+)\s+(.+)$/', $line, $matches)) {
            $key = $matches[1];
            $value = trim($matches[2]);

            if ($key === 'Path' || $key === 'Seenby') {
                $ticData[$key][] = $value;
            } elseif ($key === 'LDesc') {
                $ticData['LDesc'][] = $value;
            } else {
                $ticData[$key] = $value;
            }
        }
    }

    // Validate required fields
    $required = ['Area', 'File', 'From'];
    foreach ($required as $field) {
        if (!isset($ticData[$field])) {
            throw new \Exception("Missing required TIC field: $field");
        }
    }

    return $ticData;
}

protected function validateFile(array $ticData, string $filePath): bool
{
    // Validate size
    if (isset($ticData['Size'])) {
        $actualSize = filesize($filePath);
        if ($actualSize != intval($ticData['Size'])) {
            error_log("TIC size mismatch: expected {$ticData['Size']}, got $actualSize");
            return false;
        }
    }

    // Validate CRC32
    if (isset($ticData['Crc'])) {
        $actualCrc = $this->calculateCrc32($filePath);
        if (strtoupper($actualCrc) !== strtoupper($ticData['Crc'])) {
            error_log("TIC CRC mismatch: expected {$ticData['Crc']}, got $actualCrc");
            return false;
        }
    }

    return true;
}

protected function calculateCrc32(string $filePath): string
{
    $hash = hash_file('crc32b', $filePath);
    return strtoupper($hash);
}

8. FileRequestHandler

File: src/FileRequestHandler.php Purpose: Stub for future FREQ implementation

Planned Methods (Phase 2+)

class FileRequestHandler
{
    /
     * Create file request in queue
     */
    public function createRequest(array $requestData): int;

    /
     * Process binkp FREQ (M_GET frame)
     */
    public function handleBinkpFreq(string $address, string $filename, ?string $password = null): array;

    /
     * Process netmail file request command
     */
    public function handleNetmailFreq(int $netmailId): array;

    /
     * Match request to available file
     */
    protected function matchFile(string $filename): ?int;

    /
     * Verify FREQ password
     */
    protected function verifyFreqPassword(int $fileAreaId, string $password): bool;

    /
     * Queue file for transmission
     */
    protected function queueFileTransmission(int $fileId, string $address): bool;
}

Note: This is a placeholder for future implementation. Phase 1 focuses on core upload/download functionality.

API Endpoints

User Endpoints

Add to routes/api-routes.php:

// File area browsing
$router->get('/api/file-areas',
    [FileAreaController::class, 'handleListFileAreas']);

$router->get('/api/file-areas/{id}/files',
    [FileAreaController::class, 'handleBrowseArea']);

$router->get('/api/files/{id}',
    [FileAreaController::class, 'handleFileDetails']);

// File upload
$router->post('/api/file-areas/{id}/upload',
    [FileAreaController::class, 'handleUploadFile']);

// File download
$router->get('/api/files/{id}/download',
    [FileAreaController::class, 'handleDownloadFile']);

// Subscriptions
$router->post('/api/file-areas/{id}/subscribe',
    [FileAreaController::class, 'handleSubscribe']);

$router->post('/api/file-areas/{id}/unsubscribe',
    [FileAreaController::class, 'handleUnsubscribe']);

// Private files
$router->get('/api/files/private',
    [FileAreaController::class, 'handleListPrivateFiles']);

$router->delete('/api/files/private/{id}',
    [FileAreaController::class, 'handleDeletePrivateFile']);

Admin Endpoints

Add to routes/admin-routes.php:

// File area management pages
$router->get('/admin/file-areas',
    [FileAreaController::class, 'handleAdminFileAreas']);

$router->get('/admin/file-approval',
    [FileAreaController::class, 'handleApprovalQueue']);

// File area CRUD API
$router->get('/admin/api/file-areas',
    [FileAreaController::class, 'handleListFileAreasAdmin']);

$router->post('/admin/api/file-areas',
    [FileAreaController::class, 'handleCreateFileArea']);

$router->put('/admin/api/file-areas/{id}',
    [FileAreaController::class, 'handleUpdateFileArea']);

$router->delete('/admin/api/file-areas/{id}',
    [FileAreaController::class, 'handleDeleteFileArea']);

// Approval queue API
$router->get('/admin/api/files/pending',
    [FileAreaController::class, 'handleGetPendingFiles']);

$router->post('/admin/api/files/{id}/approve',
    [FileAreaController::class, 'handleApproveFile']);

$router->post('/admin/api/files/{id}/reject',
    [FileAreaController::class, 'handleRejectFile']);

$router->get('/admin/api/files/stats',
    [FileAreaController::class, 'handleApprovalStats']);

Request/Response Examples

Upload File (POST /api/file-areas/1/upload)

Request:

POST /api/file-areas/1/upload HTTP/1.1
Content-Type: multipart/form-data

------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="example.zip"
Content-Type: application/zip

[binary data]
------WebKitFormBoundary
Content-Disposition: form-data; name="short_description"

Useful utility for X
------WebKitFormBoundary
Content-Disposition: form-data; name="long_description"

This is a detailed description of the utility...
------WebKitFormBoundary--

Response (Success):

{
    "success": true,
    "file_id": 42,
    "message": "File uploaded successfully and awaiting approval",
    "status": "pending"
}

Response (Error):

{
    "success": false,
    "error": "File exceeds maximum size of 10MB"
}

Download File (GET /api/files/42/download)

Response (Success):

HTTP/1.1 200 OK
Content-Type: application/zip
Content-Disposition: attachment; filename="example.zip"
Content-Length: 1024000

[binary file stream]

Response (Insufficient Credits):

{
    "success": false,
    "error": "Insufficient credits. This download costs 20 credits, you have 15."
}

Approve File (POST /admin/api/files/42/approve)

Request:

{
    "admin_user_id": 1
}

Response:

{
    "success": true,
    "message": "File approved successfully",
    "credits_awarded": 25
}

User Interface

Admin Templates

1. File Areas Management (templates/admin/file_areas.twig)

Pattern: Similar to templates/echoareas.twig

Features: - List all file areas with statistics - Color-coded area tags - Add/Edit modal dialogs - Delete confirmation - Real-time updates via AJAX

Layout:

???????????????????????????????????????????????????????????
? File Areas Management                    [+ Add Area]   ?
???????????????????????????????????????????????????????????
? Filter: [All ?] [Active] [Inactive]                    ?
???????????????????????????????????????????????????????????
? TAG              ? DESC        ? FILES ? SIZE  ? DL    ?
??????????????????????????????????????????????????????????
? GENERAL_FILES    ? General...  ? 145   ? 2.1GB ? 1,234 ?
? ?                ? [Edit] [Del]?       ?       ?       ?
??????????????????????????????????????????????????????????
? SOFTWARE         ? Software... ? 78    ? 890MB ? 567   ?
? ?                ? [Edit] [Del]?       ?       ?       ?
???????????????????????????????????????????????????????????

Add/Edit Modal:

???????????????????????????????????????????
? Add File Area                      [×]  ?
???????????????????????????????????????????
? Tag:         [________________]         ?
? Description: [________________]         ?
? Domain:      [fidonet ?]                ?
? Color:       [#007bff] [?]            ?
?                                         ?
? File Settings:                          ?
? Max Size:    [10] MB                    ?
? Allowed Ext: [.txt,.zip,.pdf]          ?
? Blocked Ext: [.exe,.bat,.com]          ?
?                                         ?
? Credits:                                ?
? Download Cost:  [10] credits            ?
? Upload Reward:  [25] credits            ?
?                                         ?
? Options:                                ?
? ? Active                                ?
? ? Local Only                            ?
? ? Sysop Only                            ?
? ? Require Approval                      ?
? ? Scan for Viruses                      ?
?                                         ?
?         [Cancel]  [Save Area]           ?
???????????????????????????????????????????

2. File Approval Queue (templates/admin/file_approval.twig)

Pattern: Similar to templates/admin/crashmail_queue.twig

Features: - List pending files - Virus scan results prominently displayed - File preview (images) - Approve/Reject actions - Statistics sidebar

Layout:

???????????????????????????????????????????????????????????????????
? File Approval Queue                                             ?
???????????????????????????????????????????????????????????????????
? ???????????????????                                             ?
? ? Statistics      ?                                             ?
? ? Pending: 15     ?                                             ?
? ? Approved: 8     ?                                             ?
? ? Rejected: 2     ?                                             ?
? ???????????????????                                             ?
???????????????????????????????????????????????????????????????????
? Filter by Area: [All ?]    Status: [All ?]                     ?
???????????????????????????????????????????????????????????????????
? ?????????????????????????????????????????????????????????????   ?
? ? ? example.zip (2.4 MB)                                   ?   ?
? ? Area: GENERAL_FILES                                       ?   ?
? ? Uploaded by: JohnDoe (user_id: 42) on 2025-01-30 10:23  ?   ?
? ? From: 1:123/456.0                                        ?   ?
? ?                                                           ?   ?
? ? ? Virus Scan: CLEAN (Scanned: 2025-01-30 10:23)        ?   ?
? ?                                                           ?   ?
? ? Short: Useful utility for X                              ?   ?
? ? Long: This is a detailed description...                  ?   ?
? ?                                                           ?   ?
? ?         [? Approve]  [? Reject]                          ?   ?
? ?????????????????????????????????????????????????????????????   ?
?                                                                 ?
? ?????????????????????????????????????????????????????????????   ?
? ? ? suspicious.exe (142 KB)                                ?   ?
? ? Area: SOFTWARE                                            ?   ?
? ? Uploaded by: JaneSmith (user_id: 99) on 2025-01-30 11:45?   ?
? ?                                                           ?   ?
? ? ??  Virus Scan: INFECTED - Win.Trojan.Generic           ?   ?
? ?     Signature: Win.Trojan.Generic                        ?   ?
? ?                                                           ?   ?
? ? Short: Productivity tool                                 ?   ?
? ?                                                           ?   ?
? ?         [? Approve]  [? Reject]                          ?   ?
? ?????????????????????????????????????????????????????????????   ?
???????????????????????????????????????????????????????????????????

Virus Scan Display: - Green checkmark + "CLEAN" for clean files - Red warning + "INFECTED" with signature for infected - Yellow warning + "ERROR" if scan failed - Gray "SKIPPED" if scanning disabled

Reject Modal:

???????????????????????????????????????????
? Reject File                        [×]  ?
???????????????????????????????????????????
? Filename: suspicious.exe                ?
?                                         ?
? Reason for rejection:                   ?
? ??????????????????????????????????????? ?
? ? File contains malware detected by   ? ?
? ? virus scanner                       ? ?
? ?                                     ? ?
? ??????????????????????????????????????? ?
?                                         ?
? Action:                                 ?
? ? Move to quarantine                    ?
? ? Delete from disk                      ?
?                                         ?
?         [Cancel]  [Reject File]         ?
???????????????????????????????????????????

User Templates

3. File Browsing (templates/file_browse.twig)

Features: - Tabbed interface for file areas - File list with icons and descriptions - Download buttons with credit cost display - Upload button - Search and filter

Layout:

???????????????????????????????????????????????????????????????????
? File Areas                                                      ?
???????????????????????????????????????????????????????????????????
? [GENERAL_FILES] [SOFTWARE] [DOCS]                              ?
???????????????????????????????????????????????????????????????????
? Search: [__________]  Sort: [Name ?]        [? Upload File]   ?
???????????????????????????????????????????????????????????????????
? Your Balance: 150 credits                                       ?
???????????????????????????????????????????????????????????????????
? ? example.zip (2.4 MB) - Downloaded 145 times                 ?
?    Useful utility for X                                        ?
?    [? Download (10 credits)]                                   ?
???????????????????????????????????????????????????????????????????
? ? another-file.txt (24 KB) - Downloaded 67 times              ?
?    Text documentation for Y                                    ?
?    [? Download (5 credits)]                                    ?
???????????????????????????????????????????????????????????????????
? ? large-archive.7z (45 MB) - Downloaded 12 times              ?
?    Complete archive of Z                                       ?
?    [? Download (50 credits)] ?? Large file                    ?
???????????????????????????????????????????????????????????????????

File Icons: - ? Generic file - ? Archive (.zip, .rar, .7z) - ? Text file (.txt, .md) - ?? Image (.jpg, .png, .gif) - ? Audio (.mp3, .wav) - ? Video (.mp4, .avi)

4. File Upload (templates/file_upload.twig)

Features: - Drag-and-drop file picker - Description fields - Progress bar - Upload status

Layout:

???????????????????????????????????????????????????????????????????
? Upload File                                                     ?
???????????????????????????????????????????????????????????????????
? File Area: [GENERAL_FILES ?]                                   ?
?                                                                 ?
? ?????????????????????????????????????????????????????????????   ?
? ?                                                           ?   ?
? ?         ? Drag and drop file here                       ?   ?
? ?              or click to browse                          ?   ?
? ?                                                           ?   ?
? ?         Max size: 10 MB                                  ?   ?
? ?         Allowed: .txt, .zip, .pdf                        ?   ?
? ?                                                           ?   ?
? ?????????????????????????????????????????????????????????????   ?
?                                                                 ?
? Short Description (255 chars):                                 ?
? [_________________________________________________________]     ?
?                                                                 ?
? Long Description:                                              ?
? ???????????????????????????????????????????????????????????     ?
? ?                                                         ?     ?
? ?                                                         ?     ?
? ?                                                         ?     ?
? ???????????????????????????????????????????????????????????     ?
?                                                                 ?
? Reward: 25 credits upon approval                               ?
?                                                                 ?
?                                    [Cancel]  [Upload File]     ?
???????????????????????????????????????????????????????????????????

Upload Progress:

???????????????????????????????????????????
? Uploading: example.zip                  ?
? ?????????????????????????? 65%         ?
? 1.5 MB / 2.4 MB                         ?
???????????????????????????????????????????

Success Message:

???????????????????????????????????????????
? ? Upload Successful                    ?
?                                         ?
? Your file has been uploaded and is      ?
? awaiting approval by a sysop.           ?
?                                         ?
? You will receive 25 credits once        ?
? approved.                               ?
?                                         ?
?              [OK]                       ?
???????????????????????????????????????????

5. Private Files (templates/file_private.twig)

Features: - List user's private files (received via netmail) - Download private files (no credit cost) - Delete unwanted files - Show file origin (sender address/name) - Virus scan status

Layout:

???????????????????????????????????????????????????????????????????
? My Private Files                                                ?
???????????????????????????????????????????????????????????????????
? Files received via netmail are stored here.                    ?
? Storage used: 15.2 MB / 100 MB                                  ?
???????????????????????????????????????????????????????????????????
? ? document.pdf (2.4 MB)                                        ?
?    Received from: John Doe (1:123/456)                         ?
?    Date: 2025-01-30 10:23                                      ?
?    Subject: RE: Project files                                  ?
?    ? Virus Scan: CLEAN                                        ?
?    [? Download] [?? Delete]                                    ?
???????????????????????????????????????????????????????????????????
? ? archive.zip (5.1 MB)                                         ?
?    Received from: Jane Smith (1:234/567)                       ?
?    Date: 2025-01-29 14:15                                      ?
?    Subject: Files you requested                                ?
?    ? Virus Scan: CLEAN                                        ?
?    [? Download] [?? Delete]                                    ?
???????????????????????????????????????????????????????????????????
? ? infected.exe (142 KB)                                        ?
?    Received from: Unknown (1:999/999)                          ?
?    Date: 2025-01-28 08:30                                      ?
?    ??  Virus Scan: QUARANTINED - Win.Trojan.Generic           ?
?    This file cannot be downloaded (infected)                   ?
?    [?? Delete]                                                 ?
???????????????????????????????????????????????????????????????????

Features: - Read-only list (files added automatically via netmail) - No upload button (files come from netmail only) - Free downloads (no credit cost for own files) - Delete option to manage storage - Shows sender information from netmail - Quarantined files clearly marked and not downloadable

Integration Points

1. BinkpSession.php Integration

File: src/Binkp/Protocol/BinkpSession.php Location: handleFileFrame() method (around line 743)

Current Behavior: - Receives M_FILE frame from binkp session - Saves all files to inbound directory - Only processes .pkt files

New Behavior: - Process .pkt files (existing behavior) - Detect .TIC files and their associated data files - Process TIC files via TicFileProcessor - Store files in appropriate file areas

Implementation:

protected function handleFileFrame($filename, $fileData)
{
    $inboundPath = $this->config->getInboundPath();
    $tempPath = $inboundPath . '/' . $filename;

    // Save to temp location first
    file_put_contents($tempPath, $fileData);

    if (str_ends_with(strtolower($filename), '.pkt')) {
        // Existing packet processing
        $this->logger->info("Received packet: $filename");
        // ... existing packet handling code
    } elseif (str_ends_with(strtolower($filename), '.tic')) {
        // NEW: TIC file processing
        $this->logger->info("Received TIC file: $filename from {$this->remoteAddress}");
        $this->pendingTicFiles[$filename] = $tempPath;
        // TIC processing happens after associated file is received
    } else {
        // Regular file - check if there's a pending TIC for it
        $ticFile = $filename . '.tic';
        if (isset($this->pendingTicFiles[$ticFile])) {
            // Process TIC + file together
            try {
                $ticProcessor = new \BinktermPHP\TicFileProcessor();
                $result = $ticProcessor->processTicFile(
                    $this->pendingTicFiles[$ticFile],
                    $tempPath
                );

                if ($result['success']) {
                    $this->logger->info("TIC processed: {$result['area']} - $filename (file_id={$result['file_id']})");
                    // Clean up TIC file
                    unlink($this->pendingTicFiles[$ticFile]);
                    unset($this->pendingTicFiles[$ticFile]);
                } else {
                    $this->logger->error("TIC processing failed: {$result['error']}");
                }

            } catch (\Exception $e) {
                $this->logger->error("TIC handling exception: " . $e->getMessage());
            }
        } else {
            // File without TIC - log warning and skip
            $this->logger->warning("Received file without TIC: $filename");
        }
    }
}

Session Variables:

class BinkpSession
{
    // Add to class properties
    private array $pendingTicFiles = [];

    // At end of session, process any orphaned TIC files
    protected function cleanup()
    {
        foreach ($this->pendingTicFiles as $ticFile => $ticPath) {
            $this->logger->warning("TIC file received without data file: $ticFile");
            unlink($ticPath);
        }
        $this->pendingTicFiles = [];
    }
}

2. BinkdProcessor.php Integration

File: src/BinkdProcessor.php

Purpose: Process files from inbound directory

New Method:

/
 * Process non-packet files from inbound directory
 */
public function processInboundFiles(): void
{
    $allFiles = glob($this->inboundPath . '/*');
    if ($allFiles === false) {
        return;
    }

    // Filter out .pkt files (already handled by processInbound)
    $pktFiles = glob($this->inboundPath . '/*.pkt') ?: [];
    $otherFiles = array_diff($allFiles, $pktFiles);

    foreach ($otherFiles as $filePath) {
        if (!is_file($filePath) || is_dir($filePath)) {
            continue;
        }

        try {
            $this->processFileAreaFile($filePath);
        } catch (\Exception $e) {
            error_log("Error processing file {$filePath}: " . $e->getMessage());
        }
    }
}

/
 * Process a single file area file
 */
protected function processFileAreaFile(string $filePath): void
{
    $filename = basename($filePath);

    // Determine file area (simple implementation)
    $db = Database::getInstance()->getPdo();
    $stmt = $db->prepare("SELECT id FROM file_areas WHERE tag = ? AND is_active = TRUE LIMIT 1");
    $stmt->execute(['GENERAL_FILES']);
    $area = $stmt->fetch(\PDO::FETCH_ASSOC);

    if (!$area) {
        error_log("No active file area found for: $filename");
        return;
    }

    $fileHandler = new FileUploadHandler();
    $result = $fileHandler->handleFidonetFile(
        $area['id'],
        $filePath,
        'unknown', // Address not available in batch processing
        ['domain' => 'fidonet']
    );

    if ($result['success']) {
        error_log("File processed successfully: $filename (file_id={$result['file_id']})");
        // Remove from inbound after successful processing
        unlink($filePath);
    } else {
        error_log("File processing failed: $filename - {$result['error']}");
    }
}

Integration Point:

Modify the main processing loop or create a separate cron job:

// In scripts/binkp_poll.php or similar
$processor = new BinktermPHP\BinkdProcessor();
$processor->processInbound(); // Existing packet processing
$processor->processInboundFiles(); // NEW: File area processing

3. UserCredit Integration

Purpose: Award credits for approved uploads, charge for downloads

Download Credit Deduction

File: src/FileDownloadHandler.php

use BinktermPHP\UserCredit;

protected function deductCredits(int $fileId, int $userId): bool
{
    $file = $this->getFileDetails($fileId);
    $fileArea = $this->getFileArea($file['file_area_id']);

    $cost = $fileArea['download_credit_cost'];

    if ($cost <= 0) {
        return true; // Free download
    }

    return UserCredit::debit(
        $userId,
        $cost,
        "Downloaded: {$file['filename']}",
        null, // No source user
        UserCredit::TYPE_PAYMENT
    );
}

Upload Credit Reward

File: src/FileApprovalController.php

use BinktermPHP\UserCredit;

public function approveFile(int $fileId, int $adminUserId): bool
{
    $file = $this->getFileDetails($fileId);

    // Only award credits for user uploads (not Fidonet)
    if ($file['source_type'] === 'user' && $file['uploaded_by_user_id']) {
        $fileArea = $this->getFileArea($file['file_area_id']);
        $reward = $fileArea['upload_credit_reward'];

        if ($reward > 0) {
            UserCredit::credit(
                $file['uploaded_by_user_id'],
                $reward,
                "File approved: {$file['filename']}",
                $adminUserId,
                UserCredit::TYPE_SYSTEM_REWARD
            );
        }
    }

    // Update file status
    $db = Database::getInstance()->getPdo();
    $stmt = $db->prepare("
        UPDATE files
        SET status = 'approved',
            approved_by_user_id = ?,
            approved_at = NOW(),
            updated_at = NOW()
        WHERE id = ?
    ");
    $stmt->execute([$adminUserId, $fileId]);

    // Update file area stats
    $this->updateFileAreaStats($file['file_area_id']);

    return true;
}

4. Netmail File Attachment Integration

File: Netmail packet processing (location TBD based on current netmail handling) Purpose: Extract file attachments from netmail and store in user's private area

Implementation

When processing incoming netmail packets, detect file attachments and handle them:

/
 * Process netmail with file attachments
 */
protected function processNetmailAttachments(array $netmail, array $attachedFiles): void
{
    // Get recipient user
    $db = Database::getInstance()->getPdo();
    $stmt = $db->prepare("SELECT id FROM users WHERE ftn_address = ? OR real_name = ?");
    $stmt->execute([$netmail['to_address'], $netmail['to_name']]);
    $recipient = $stmt->fetch(\PDO::FETCH_ASSOC);

    if (!$recipient) {
        error_log("Cannot store attachments: recipient not found for {$netmail['to_name']}");
        return;
    }

    $fileHandler = new FileUploadHandler();

    foreach ($attachedFiles as $filePath) {
        $filename = basename($filePath);

        // Store in user's private area
        $result = $fileHandler->handleNetmailAttachment(
            $recipient['id'],
            $filePath,
            $netmail['from_address'],
            [
                'netmail_subject' => $netmail['subject'],
                'from_name' => $netmail['from_name']
            ]
        );

        if ($result['success']) {
            error_log("Netmail attachment stored: $filename (file_id={$result['file_id']})");

            // Optionally: Add note to netmail body about received file
            $this->addAttachmentNote($netmail['id'], $filename, $result['file_id']);
        } else {
            error_log("Failed to store netmail attachment: $filename - {$result['error']}");
        }
    }
}

/
 * Add note to netmail about received file attachment
 */
protected function addAttachmentNote(int $netmailId, string $filename, int $fileId): void
{
    // Could append to message body or add system message
    $note = "\n\n--- File Attachment ---\nFile: $filename\nStored in your private files. View at: /files/private\n";

    $db = Database::getInstance()->getPdo();
    $stmt = $db->prepare("UPDATE netmail SET body = body || ? WHERE id = ?");
    $stmt->execute([$note, $netmailId]);
}

FileUploadHandler New Method

Add to FileUploadHandler class:

/
 * Handle file received as netmail attachment
 *
 * @param int $userId Owner of the file
 * @param string $filePath Path to temporary file
 * @param string $fromAddress Sender's Fidonet address
 * @param array $metadata Additional metadata
 * @return array Result array
 */
public function handleNetmailAttachment(
    int $userId,
    string $filePath,
    string $fromAddress,
    array $metadata = []
): array {
    try {
        $filename = basename($filePath);
        $filesize = filesize($filePath);

        // Calculate hash
        $fileHash = $this->calculateFileHash($filePath);

        // Scan for viruses
        $scanResult = $this->scanForViruses($filePath);
        if ($scanResult['result'] === 'infected') {
            // Quarantine infected files even in private area
            $this->moveToQuarantine($filePath, $userId, $filename);

            return [
                'success' => false,
                'error' => 'File infected with virus: ' . $scanResult['signature'],
                'quarantined' => true
            ];
        }

        // Move to private storage
        $storagePath = $this->moveToPrivateStorage($filePath, $userId, $filename);

        // Create database record
        $fileId = $this->storePrivateFileRecord([
            'filename' => $filename,
            'filesize' => $filesize,
            'file_hash' => $fileHash,
            'storage_path' => $storagePath,
            'uploaded_by_user_id' => $userId,
            'uploaded_from_address' => $fromAddress,
            'source_type' => 'netmail_attachment',
            'is_private' => true,
            'status' => 'approved', // Auto-approved
            'short_description' => $metadata['netmail_subject'] ?? '',
            'long_description' => "Received from {$metadata['from_name']} ({$fromAddress})",
            'virus_scanned' => $scanResult['scanned'],
            'virus_scan_result' => $scanResult['result'],
            'virus_scanned_at' => $scanResult['scanned_at'] ?? null
        ]);

        return [
            'success' => true,
            'file_id' => $fileId,
            'storage_path' => $storagePath
        ];

    } catch (\Exception $e) {
        error_log("Netmail attachment handling error: " . $e->getMessage());
        return [
            'success' => false,
            'error' => $e->getMessage()
        ];
    }
}

/
 * Move file to user's private storage directory
 */
protected function moveToPrivateStorage(string $tempPath, int $userId, string $filename): string
{
    $filename = $this->sanitizeFilename($filename);

    // Build storage path: data/files/private/{user_id}/
    $baseDir = realpath(__DIR__ . '/../data/files/private');
    $userDir = $baseDir . '/' . $userId;

    // Create user directory if needed
    if (!is_dir($userDir)) {
        mkdir($userDir, 0755, true);
    }

    // Handle filename conflicts
    $storagePath = $userDir . '/' . $filename;
    $counter = 1;
    while (file_exists($storagePath)) {
        $pathInfo = pathinfo($filename);
        $newFilename = $pathInfo['filename'] . '_' . $counter . '.' . $pathInfo['extension'];
        $storagePath = $userDir . '/' . $newFilename;
        $counter++;
    }

    // Move file
    if (!rename($tempPath, $storagePath)) {
        throw new \Exception("Failed to move file to private storage");
    }

    // Set permissions
    chmod($storagePath, 0644);

    return $storagePath;
}

/
 * Store private file record in database
 */
protected function storePrivateFileRecord(array $data): int
{
    $db = Database::getInstance()->getPdo();

    $stmt = $db->prepare("
        INSERT INTO files (
            file_area_id, filename, filesize, file_hash, storage_path,
            uploaded_by_user_id, uploaded_from_address, source_type,
            is_private, status, short_description, long_description,
            virus_scanned, virus_scan_result, virus_scanned_at,
            created_at, updated_at
        ) VALUES (
            NULL, ?, ?, ?, ?,
            ?, ?, ?,
            TRUE, 'approved', ?, ?,
            ?, ?, ?,
            NOW(), NOW()
        ) RETURNING id
    ");

    $stmt->execute([
        $data['filename'],
        $data['filesize'],
        $data['file_hash'],
        $data['storage_path'],
        $data['uploaded_by_user_id'],
        $data['uploaded_from_address'],
        $data['source_type'],
        $data['short_description'],
        $data['long_description'],
        $data['virus_scanned'],
        $data['virus_scan_result'],
        $data['virus_scanned_at']
    ]);

    $result = $stmt->fetch(\PDO::FETCH_ASSOC);
    return $result['id'];
}

5. AdminActionLogger Integration

Purpose: Audit trail for all file area admin actions

Actions to Log:

use BinktermPHP\AdminActionLogger;

// File area created
AdminActionLogger::logAction($userId, 'file_area_created', [
    'tag' => $tag,
    'domain' => $domain,
    'max_file_size' => $maxFileSize
]);

// File area updated
AdminActionLogger::logAction($userId, 'file_area_updated', [
    'file_area_id' => $id,
    'tag' => $tag,
    'changes' => $changedFields
]);

// File area deleted
AdminActionLogger::logAction($userId, 'file_area_deleted', [
    'file_area_id' => $id,
    'tag' => $tag
]);

// File approved
AdminActionLogger::logAction($adminUserId, 'file_approved', [
    'file_id' => $fileId,
    'filename' => $filename,
    'uploader_id' => $uploaderId,
    'credits_awarded' => $creditsAwarded
]);

// File rejected
AdminActionLogger::logAction($adminUserId, 'file_rejected', [
    'file_id' => $fileId,
    'filename' => $filename,
    'uploader_id' => $uploaderId,
    'reason' => $rejectionReason
]);

// File quarantined (virus detected)
AdminActionLogger::logAction($adminUserId, 'file_quarantined', [
    'file_id' => $fileId,
    'filename' => $filename,
    'virus_signature' => $signature
]);

Security Considerations

1. File Extension Blacklist

Implementation:

protected function validateExtension(string $filename, array $fileArea): bool
{
    $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));

    // Check blacklist
    if (!empty($fileArea['blocked_extensions'])) {
        $blocked = array_map('trim', explode(',', $fileArea['blocked_extensions']));
        $blocked = array_map('strtolower', $blocked);

        if (in_array('.' . $extension, $blocked) || in_array($extension, $blocked)) {
            return false;
        }
    }

    // Check whitelist (if configured)
    if (!empty($fileArea['allowed_extensions'])) {
        $allowed = array_map('trim', explode(',', $fileArea['allowed_extensions']));
        $allowed = array_map('strtolower', $allowed);

        if (!in_array('.' . $extension, $allowed) && !in_array($extension, $allowed)) {
            return false;
        }
    }

    return true;
}

Default Blacklist: - Executables: .exe, .bat, .com, .cmd, .scr, .pif - Scripts: .vbs, .js, .wsf, .hta - System files: .sys, .dll, .drv

2. Path Traversal Prevention

Implementation:

protected function sanitizeFilename(string $filename): string
{
    // Remove path components
    $filename = basename($filename);

    // Remove dangerous characters
    $filename = preg_replace('/[^a-zA-Z0-9._-]/', '_', $filename);

    // Prevent hidden files
    $filename = ltrim($filename, '.');

    // Ensure not empty
    if (empty($filename)) {
        $filename = 'file_' . bin2hex(random_bytes(8));
    }

    return $filename;
}

protected function moveToStorage(string $tempPath, string $areaTag, string $filename): string
{
    // Sanitize area tag
    $areaTag = preg_replace('/[^a-zA-Z0-9_-]/', '', $areaTag);

    // Sanitize filename
    $filename = $this->sanitizeFilename($filename);

    // Build safe storage path
    $storagePath = realpath(__DIR__ . '/../data/files') . '/' . $areaTag . 'FileAreas_Proposal.md/' . $filename;

    // Verify path is within allowed directory
    $allowedBase = realpath(__DIR__ . '/../data/files');
    if (!str_starts_with($storagePath, $allowedBase)) {
        throw new \Exception("Invalid storage path detected");
    }

    // Create directory if needed
    $areaDir = $allowedBase . '/' . $areaTag;
    if (!is_dir($areaDir)) {
        mkdir($areaDir, 0755, true);
    }

    // Move file
    if (!rename($tempPath, $storagePath)) {
        throw new \Exception("Failed to move file to storage");
    }

    // Set permissions
    chmod($storagePath, 0644);

    return $storagePath;
}

3. Access Control

Access Check with Private File Support:

protected function checkAccess(int $fileId, int $userId): bool
{
    $db = Database::getInstance()->getPdo();

    $stmt = $db->prepare("
        SELECT f.id, f.is_private, f.uploaded_by_user_id,
               f.file_area_id, fa.is_sysop_only
        FROM files f
        LEFT JOIN file_areas fa ON f.file_area_id = fa.id
        WHERE f.id = ? AND f.status = 'approved'
    ");
    $stmt->execute([$fileId]);
    $result = $stmt->fetch(\PDO::FETCH_ASSOC);

    if (!$result) {
        return false; // File not found or not approved
    }

    // Private files: only owner can access
    if ($result['is_private']) {
        return $result['uploaded_by_user_id'] == $userId;
    }

    // Public files: check subscriptions and sysop restrictions
    if (!$result['file_area_id']) {
        return false; // Public file must have area
    }

    // Check if sysop-only and user is not sysop
    if ($result['is_sysop_only']) {
        $userStmt = $db->prepare("SELECT is_sysop FROM users WHERE id = ?");
        $userStmt->execute([$userId]);
        $user = $userStmt->fetch(\PDO::FETCH_ASSOC);

        if (!$user || !$user['is_sysop']) {
            return false;
        }
    }

    // Check subscription
    $subStmt = $db->prepare("
        SELECT id FROM user_file_area_subscriptions
        WHERE user_id = ? AND file_area_id = ?
        AND is_active = TRUE
    ");
    $subStmt->execute([$userId, $result['file_area_id']]);

    return $subStmt->fetch() !== false;
}

4. Credit Balance Checks

Pre-Download Validation with Private File Support:

protected function checkCredits(int $fileId, int $userId): array
{
    $file = $this->getFileDetails($fileId);

    // Private files are always free for the owner
    if ($file['is_private']) {
        return ['sufficient' => true, 'cost' => 0, 'balance' => null];
    }

    $fileArea = $this->getFileArea($file['file_area_id']);
    $cost = $fileArea['download_credit_cost'];

    if ($cost <= 0) {
        return ['sufficient' => true, 'cost' => 0, 'balance' => null];
    }

    $balance = UserCredit::getBalance($userId);

    return [
        'sufficient' => $balance >= $cost,
        'cost' => $cost,
        'balance' => $balance,
        'shortage' => max(0, $cost - $balance)
    ];
}

5. Virus Scanning

See VirusScanner component above.

Key Security Points: - Scan before making file available - Quarantine infected files immediately - Don't auto-delete (allow sysop review) - Update virus definitions regularly - Timeout protection (30 seconds default)

6. Audit Logging

All Critical Actions Logged: - File uploads (user, file, size, timestamp) - File downloads (user, file, credits, IP) - Admin approvals/rejections - File area changes - Virus detections

Example Log Queries:

-- Recent file uploads
SELECT f.filename, u.username, f.created_at
FROM files f
LEFT JOIN users u ON f.uploaded_by_user_id = u.id
ORDER BY f.created_at DESC
LIMIT 50;

-- Download history for file
SELECT u.username, fd.downloaded_at, fd.ip_address
FROM file_downloads fd
JOIN users u ON fd.user_id = u.id
WHERE fd.file_id = ?
ORDER BY fd.downloaded_at DESC;

-- Virus detections
SELECT f.filename, f.virus_signature, f.virus_scanned_at
FROM files f
WHERE f.virus_scan_result = 'infected'
ORDER BY f.virus_scanned_at DESC;

Implementation Roadmap

Phase 1: Foundation (Week 1)

Tasks: 1. Create database migration 2. Run migration and verify tables created 3. Create directory structure (data/files/, .quarantine/, etc.). Place .gitkeep in. Update setup.php to make these world writable just as data/outbound is 4. Create FileAreaManager class 5. Create FileUploadHandler class (basic validation) 6. Create FileDownloadHandler class (basic download) 7. Unit tests for core classes

Deliverables: - Working database schema - Core class functionality - Basic upload/download flow

Phase 2: Web Interface (Week 2)

Tasks: 1. Create API endpoints (user routes) 2. Create admin API endpoints 3. Create FileAreaController 4. Create admin templates (file_areas.twig, file_approval.twig) 5. Create user templates (file_browse.twig, file_upload.twig) 6. Create JavaScript files (file_areas.js, file_approval.js, file_browse.js) 7. CSS styling for file area pages

Deliverables: - Working admin interface - Working user interface - AJAX-based interactions

Phase 3: Advanced Features (Week 3)

Tasks: 1. Implement VirusScanner class 2. Configure ClamAV integration 3. Implement FileApprovalController 4. Add credit integration (UserCredit) 5. Add admin action logging (AdminActionLogger) 6. File area subscription management 7. Download statistics and tracking

Deliverables: - Virus scanning functional - Credit economy integrated - Approval workflow complete - Statistics tracking

Phase 4: Fidonet Integration (Week 4)

Tasks: 1. Modify BinkpSession.php for file handling 2. Modify BinkdProcessor.php for batch processing 3. Implement file area routing logic 4. Test receiving files via binkp 5. Auto-approval for Fidonet sources 6. File area routing table (optional) 7. Implement netmail attachment extraction 8. Private file storage for netmail attachments 9. Create private files UI template

Deliverables: - Files received via binkp processed correctly - Auto-approval for trusted nodes - Routing logic functional - Netmail attachments stored in user private areas - Users can browse and download their private files

Phase 5: Testing & Documentation (Week 5)

Tasks: 1. End-to-end testing (see test plan below) 2. Security audit (penetration testing) 3. Performance testing (large files, many users) 4. Documentation: UPGRADING_1.9.0.md 5. Documentation: Update FAQ.md 6. Bug fixes and refinements

Deliverables: - Complete test coverage - Documentation complete - Production-ready system

Phase 6: Future Enhancements (Post-Release)

Planned Features: 1. Binkp FREQ protocol (M_GET frame) 2. Netmail file request commands 3. File area statistics dashboard 4. Automatic file aging/archival 5. File tagging and categories 6. Thumbnail generation for images 7. File versioning system 8. Bandwidth quotas per user 9. File area mirroring between nodes 10. Advanced routing rules

Testing Plan

Unit Tests

FileAreaManager Tests: - Create/update/delete file areas - Subscription management - Access control checks - Statistics aggregation

FileUploadHandler Tests: - File size validation - Extension validation - Duplicate detection (hash) - Path traversal prevention - Filename sanitization

FileDownloadHandler Tests: - Access control - Credit checks - Credit deduction - Download logging - File streaming

VirusScanner Tests: - Clean file detection - Infected file detection (EICAR) - Error handling - Timeout handling

Integration Tests

Upload Flow: 1. User uploads valid file ? status='pending' 2. Virus scan runs ? result='clean' 3. File stored in correct directory 4. Database record created 5. Admin sees file in approval queue

Approval Flow: 1. Admin approves file 2. User awarded credits 3. File status='approved' 4. File appears in browse list 5. Download statistics initialized

Download Flow: 1. User initiates download 2. Access checked 3. Credits checked and deducted 4. Download logged 5. Statistics updated 6. File streamed to user

Fidonet Flow: 1. File received via binkp 2. Routed to correct file area 3. Auto-approved (if configured) 4. Available for download immediately

End-to-End Tests

Test Scenario 1: User Upload to Approval to Download

  1. Login as regular user
  2. Navigate to file upload page
  3. Select GENERAL_FILES area
  4. Upload test.txt (1 KB)
  5. Add descriptions
  6. Submit upload
  7. Verify status: "Awaiting approval"
  8. Logout
  9. Login as admin
  10. Navigate to approval queue
  11. See test.txt in pending list
  12. Review file details
  13. Approve file
  14. Verify user awarded credits
  15. Logout
  16. Login as different user (with credits)
  17. Navigate to file browse
  18. See test.txt in GENERAL_FILES
  19. Click download
  20. Verify credit deduction
  21. Verify file downloaded correctly

Expected Results: - Upload successful - Approval workflow correct - Credits awarded/deducted correctly - File download successful - All statistics updated

Test Scenario 2: Virus Detection

  1. Login as user
  2. Upload EICAR test file
  3. Verify virus scan detects infection
  4. Verify file status='quarantined'
  5. Verify file moved to .quarantine/
  6. Login as admin
  7. See prominent virus warning in approval queue
  8. Review virus signature
  9. Reject file with reason
  10. Verify file not available for download

Expected Results: - Virus detected correctly - File quarantined - Admin notified - Rejection logged

Test Scenario 3: Insufficient Credits

  1. Login as user with 10 credits
  2. Attempt to download file costing 20 credits
  3. Verify error: "Insufficient credits"
  4. Verify no download
  5. Verify no credit deduction
  6. Verify download not logged

Expected Results: - Download blocked - Clear error message - No credit charge - No false statistics

Test Scenario 4: Duplicate Detection

  1. Upload file: example.zip
  2. Approve and publish
  3. Attempt to upload same file again (same hash)
  4. Verify error: "Duplicate file exists"
  5. Verify no database record created
  6. Verify no duplicate file on disk

Expected Results: - Duplicate detected - Upload rejected - No duplicate storage

Test Scenario 5: Access Control

  1. Create sysop-only file area
  2. Upload file to that area
  3. Login as regular user
  4. Verify area not visible in browse list
  5. Attempt direct URL access to file
  6. Verify access denied
  7. Login as sysop
  8. Verify area visible
  9. Verify download allowed

Expected Results: - Regular users blocked - Sysops have access - No security bypass

Test Scenario 6: Private File Delivery via Netmail

  1. User sends netmail with file attachment to user "alice"
  2. Netmail processed by system
  3. File attachment extracted
  4. Virus scan performed
  5. File stored in alice's private area
  6. Verify file record created with is_private=TRUE
  7. Verify storage path: `data/files/private/{alice_user_id}/filename`
  8. Login as alice
  9. Navigate to "My Private Files"
  10. See attached file listed
  11. Verify sender information displayed
  12. Download file
  13. Verify no credit charge
  14. Verify download successful
  15. Login as different user (bob)
  16. Attempt to access alice's private file via direct URL
  17. Verify access denied

Expected Results: - Netmail attachment extracted correctly - File stored in private area - Only owner can access - No credit charge for owner - Other users blocked from access

Test Scenario 7: Private File Deletion

  1. Login as user with private files
  2. Navigate to "My Private Files"
  3. Select file to delete
  4. Click delete button
  5. Confirm deletion
  6. Verify file removed from list
  7. Verify file deleted from disk
  8. Verify database record removed/marked deleted
  9. Verify storage quota updated

Expected Results: - File deleted successfully - Storage space reclaimed - Database cleaned up

Test Scenario 8: Private File Virus Quarantine

  1. User receives netmail with infected file (EICAR test)
  2. System extracts attachment
  3. Virus scan detects infection
  4. File quarantined (not placed in private area)
  5. Database record created with status='quarantined'
  6. Login as recipient user
  7. Navigate to "My Private Files"
  8. See quarantined file with warning
  9. Verify download button disabled
  10. Verify prominent virus warning displayed
  11. Option to delete quarantined file

Expected Results: - Infected file quarantined - User notified of virus - Download prevented - Clear security warning

Future Enhancements

Phase 2: File Request (FREQ) Support

Binkp FREQ Protocol: - Implement M_GET frame handling in BinkpSession - Match requested filenames to file_areas - Queue files for transmission - Support password-protected FREQ - Implement file request limits (rate limiting)

Netmail File Requests: - Parse netmail body for FREQ commands - Support commands: REQUEST, FREQ, SENDFILE - Respond with availability status - Queue files for next binkp session

Database Extensions:

-- Already created in Phase 1
CREATE TABLE file_requests (
    ...
);

-- Add routing rules
CREATE TABLE file_freq_rules (
    id SERIAL PRIMARY KEY,
    file_area_id INTEGER REFERENCES file_areas(id),
    require_password BOOLEAN DEFAULT FALSE,
    password_hash VARCHAR(255),
    max_requests_per_day INTEGER DEFAULT 10,
    max_file_size BIGINT,
    allowed_addresses TEXT -- Comma-separated patterns
);

Phase 3: Statistics Dashboard

Features: - Top downloaded files - Most active uploaders - File area popularity - Credit economy statistics - Virus detection trends - Storage usage graphs

Implementation: - New template: templates/admin/file_statistics.twig - Chart.js for visualizations - API endpoint: /admin/api/file-stats

Phase 4: File Aging & Archival

Features: - Auto-archive files not downloaded in X days - Move old files to archive area - Delete files older than X days (configurable) - Sysop notifications before deletion

Configuration:

// Per file area
'auto_archive_days' => 180, // Move to archive after 180 days
'auto_delete_days' => 365,  // Delete after 1 year
'notify_before_delete' => true

Phase 5: File Tagging & Categories

Features: - User-defined tags (e.g., "utility", "game", "documentation") - Category browsing - Tag-based search - Tag clouds

Database:

CREATE TABLE file_tags (
    id SERIAL PRIMARY KEY,
    tag_name VARCHAR(50) UNIQUE NOT NULL
);

CREATE TABLE file_tag_assignments (
    file_id INTEGER REFERENCES files(id) ON DELETE CASCADE,
    tag_id INTEGER REFERENCES file_tags(id) ON DELETE CASCADE,
    PRIMARY KEY (file_id, tag_id)
);

Phase 6: Thumbnail Generation

Features: - Auto-generate thumbnails for images - Preview for videos (first frame) - PDF preview (first page) - Archive content listing (.zip, .rar)

Implementation: - GD library or ImageMagick for image processing - Store thumbnails in data/files/.thumbnails/ - Lazy loading in file browse interface

Phase 7: File Versioning

Features: - Upload new version of existing file - Keep version history - Download specific versions - Compare versions

Database:

ALTER TABLE files ADD COLUMN version INTEGER DEFAULT 1;
ALTER TABLE files ADD COLUMN parent_file_id INTEGER REFERENCES files(id);

Phase 8: Bandwidth Quotas

Features: - Daily/weekly/monthly download limits per user - Quota tracking - Reset schedules - Quota exceeded notifications

Database:

CREATE TABLE user_bandwidth_quotas (
    user_id INTEGER PRIMARY KEY REFERENCES users(id),
    daily_limit BIGINT DEFAULT 104857600, -- 100 MB
    weekly_limit BIGINT,
    monthly_limit BIGINT,
    current_period_usage BIGINT DEFAULT 0,
    period_start TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Phase 9: File Area Mirroring

Features: - Designate file areas for mirroring - Automatically send new files to linked nodes - Receive mirrored files from uplinks - Conflict resolution

Configuration:

CREATE TABLE file_area_mirrors (
    id SERIAL PRIMARY KEY,
    file_area_id INTEGER REFERENCES file_areas(id),
    remote_address VARCHAR(255), -- Fidonet address
    direction VARCHAR(10), -- 'send', 'receive', 'both'
    is_active BOOLEAN DEFAULT TRUE
);

Conclusion

This proposal outlines a comprehensive file area system for BinktermPHP that:

  1. Enhances User Experience: Modern web interface for file sharing
  2. Maintains BBS Tradition: Classic file base features with credit economy
  3. Ensures Security: Virus scanning, approval workflows, access control
  4. Integrates Seamlessly: Works with existing code patterns and architecture
  5. Supports Growth: Extensible design for future FREQ and advanced features

Key Benefits: - ? Multi-network file distribution - ? Private file storage for netmail attachments - ? Credit-based economy integration (public files) - ? Virus protection with ClamAV for all files - ? Sysop approval workflow (public files) - ? Download statistics and tracking - ? Fidonet file reception via binkp - ? Future FREQ protocol support - ? Scalable storage architecture - ? Simple ownership-based access control for private files

Implementation Timeline: 5 weeks for core features, ongoing enhancements

Risk Mitigation: - Follows existing code patterns (low integration risk) - Incremental rollout (Phase 1 ? 2 ? 3...) - Extensive testing plan - Security audit before production

Appendix: Configuration Examples

.env Configuration

# File Area Settings
FILES_MAX_UPLOAD_SIZE=10485760          # 10MB default
FILES_AUTO_APPROVE_FIDONET=true         # Auto-approve from trusted nodes
FILES_QUARANTINE_INFECTED=true          # Move infected to .quarantine/
FILES_DEFAULT_DOWNLOAD_COST=10          # Default credits per download
FILES_DEFAULT_UPLOAD_REWARD=25          # Default credits per approved upload

# Private File Settings
FILES_PRIVATE_STORAGE_QUOTA=104857600   # 100MB per user default
FILES_PRIVATE_MAX_FILE_SIZE=10485760    # 10MB max size for netmail attachments
FILES_PRIVATE_SCAN_VIRUSES=true         # Scan private files for viruses

# ClamAV Virus Scanning (optional)
CLAMAV_ENABLED=false
CLAMAV_PATH=/usr/bin/clamdscan
CLAMAV_SOCKET=/var/run/clamav/clamd.ctl
CLAMAV_TIMEOUT=30

# File Area Routing (future)
FILES_AUTO_ROUTE_ENABLED=false
FILES_ROUTING_CONFIG=/path/to/routing.json

TIC File Configuration

File areas use the TIC (File Catalog) system for Fidonet file distribution. Configuration similar to binkp.json for uplink file area subscriptions.

File: config/fileareas.json

{
    "file_areas": [
        {
            "tag": "COOKING_FILES",
            "description": "Cooking recipes and food-related files",
            "domain": "fidonet",
            "uplinks": ["1:123/456"],
            "forward_to": [],
            "is_passive": false
        },
        {
            "tag": "BBS_ADS",
            "description": "BBS advertisements and ANSI art",
            "domain": "fidonet",
            "uplinks": ["1:123/456"],
            "forward_to": ["1:234/567"],
            "is_passive": false
        }
    ],
    "tic_settings": {
        "inbound_path": "data/inbound/tic",
        "validate_crc": true,
        "validate_size": true,
        "require_password": false,
        "replace_existing": false
    }
}

TIC File Format (received with files):

Area COOKING_FILES
File recipe.txt
Desc A great recipe for chocolate cake
From 1:123/456
Path 1:123/456 1234567890
Seenby 1:123/456
Created by BinktermPHP v1.9.0
Date Wed, 30 Jan 2025 12:34:56 -0500
Size 2048
Crc A3B5C7D9

Processing Flow: 1. Receive .TIC + file via binkp 2. Parse TIC metadata 3. Validate CRC and size 4. Look up file area by tag 5. Store file in area 6. Update Path/Seenby 7. Forward to downstream nodes if configured

Appendix: SQL Schema Summary

Tables Created: 1. file_areas - File distribution areas (8 rows expected initially) 2. files - File metadata (grows over time) 3. user_file_area_subscriptions - Access control (per user/area) 4. file_downloads - Download history (audit trail) 5. file_requests - FREQ queue (future use)

Total Storage Estimate: - Database: ~100 MB for 10,000 files - Files: Varies (configurable limits) - Thumbnails: ~50 MB for 10,000 images (future)

Document Version: 1.1 Last Updated: 2025-01-30 Status: DRAFT - Pending Review