# fluent-support/2.4.5/app/Models/Attachment.php

Fluent Support – Helpdesk &amp; Customer Support Ticket System, version 2.4.5. 149 lines.

- Page: https://pluginprobe.com/plugins/fluent-support/2.4.5/code/app/Models/Attachment.php
- Raw: https://pluginprobe.com/plugins/fluent-support/2.4.5/raw/app/Models/Attachment.php
- Modified: 2026-10-09T15:31:16+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/fluent-support/2.4.5/code/app/Models/Attachment.php#L10-L20`.

```php
<?php

namespace FluentSupport\App\Models;

use FluentSupport\App\Services\Helper;

class Attachment extends Model
{
    protected $table = 'fs_attachments';

    protected $hidden = ['full_url', 'file_path'];

    protected $appends = ['secureUrl'];

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'ticket_id',
        'person_id',
        'conversation_id',
        'file_type',
        'file_path',
        'full_url',
        'title',
        'driver',
        'file_size',
        'status',
        'settings'
    ];

    public static function boot()
    {
        parent::boot();

        static::creating(function ($model) {
            $uid = wp_generate_uuid4();
            $model->file_hash = md5($uid . wp_rand(0, 1000));
        });
    }

    /**
     * Delete the stored files for these attachments (local, or via the remote
     * driver hook). DB rows are left to the caller.
     *
     * @param \FluentSupport\Framework\Database\Orm\Collection|array $attachments
     * @param int|null $ticketId  When provided, removes the ticket_{id} upload directory.
     */
    public static function purgeAttachments($attachments, $ticketId = null)
    {
        foreach ($attachments as $attachment) {
            if ($attachment->driver && $attachment->driver !== 'local') {
                do_action('fluent_support/delete_remote_attachment_' . $attachment->driver, $attachment);
            } elseif ($attachment->file_path && file_exists($attachment->file_path)) {
                wp_delete_file($attachment->file_path);
            }
        }

        if ($ticketId) {
            $uploadDir = wp_upload_dir();
            $ticketDir = $uploadDir['basedir'] . '/' . FLUENT_SUPPORT_UPLOAD_DIR . '/ticket_' . $ticketId;
            if (is_dir($ticketDir)) {
                if (!class_exists('\WP_Filesystem_Direct')) {
                    require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
                    require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
                }
                (new \WP_Filesystem_Direct(false))->rmdir($ticketDir, true);
            }
        }
    }

    public function setSettingsAttribute($settings)
    {
        $this->attributes['settings'] = \maybe_serialize($settings);
    }

    public function getSettingsAttribute($settings)
    {
        return Helper::safeUnserialize($settings);
    }

    /**
     * Signed download URL.
     *
     * @return string
     */
    public function getSecureUrlAttribute()
    {
        return $this->signedUrl(self::signedUrlExpiry());
    }

    /**
     * Download URL valid until $expires.
     *
     * @param int $expires Unix timestamp.
     * @return string
     */
    public function signedUrl($expires)
    {
        return add_query_arg([
            'fst_file'    => $this->file_hash,
            'expires'     => (int) $expires,
            'secure_sign' => self::signature($this->id, (int) $expires)
        ], site_url('/index.php'));
    }

    /**
     * Expiry for a newly signed URL. Rounded to a TTL boundary so the URL stays
     * the same for a while (browser caching), and always at least one TTL ahead.
     * A URL signed just before a boundary used to die within seconds.
     *
     * @return int Unix timestamp.
     */
    public static function signedUrlExpiry()
    {
        $ttl = max(MINUTE_IN_SECONDS * 5, (int) apply_filters('fluent_support/attachment_signed_url_ttl', HOUR_IN_SECONDS));

        return (intdiv(time(), $ttl) + 2) * $ttl;
    }

    /**
     * Signature for an expiring URL. The 'v2|' prefix keeps it apart from legacy
     * signatures: those signed "id|YmdH", and without the prefix an old hourly
     * signature would pass as one that expires in 2034 (2026100614 read as a timestamp).
     *
     * @param int $attachmentId
     * @param int $expires Unix timestamp.
     * @return string
     */
    public static function signature($attachmentId, $expires)
    {
        return hash_hmac('sha256', 'v2|' . $attachmentId . '|' . (int) $expires, wp_salt('secure_auth'));
    }

    /**
     * Signature of a pre-2.4.1 URL, valid for one UTC hour.
     *
     * @param int $attachmentId
     * @param string $hourWindow gmdate('YmdH')
     * @return string
     */
    public static function legacySignature($attachmentId, $hourWindow)
    {
        return hash_hmac('sha256', $attachmentId . '|' . $hourWindow, wp_salt('secure_auth'));
    }
}

```
