Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist

This commit is contained in:
FreeScout
2026-09-27 03:41:36 -07:00
7 changed files with 109 additions and 53 deletions

View File

@@ -19,6 +19,8 @@ class Attachment extends Model
const DIRECTORY = 'attachment';
const DISK = 'local_app';
const MIME_TYPE_MAX_LENGTH = 127;
// This token type was used for backward compatibility for some time
@@ -133,7 +135,7 @@ class Attachment extends Model
$file_info = self::saveFileToDisk($attachment, $file_name, $content, $uploaded_file);
$attachment->file_dir = $file_info['file_dir'];
$attachment->size = Storage::size($file_info['file_path']);
$attachment->size = self::getDisk()->size($file_info['file_path']);
$attachment->save();
return $attachment;
@@ -154,15 +156,15 @@ class Attachment extends Model
do {
$i++;
$file_path = self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir.$i.DIRECTORY_SEPARATOR.$file_name;
} while (Storage::exists($file_path));
} while (self::getDisk()->exists($file_path));
$file_dir .= $i.DIRECTORY_SEPARATOR;
try {
if ($uploaded_file) {
$uploaded_file->storeAs(self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir, $file_name);
$uploaded_file->storeAs(self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir, $file_name, self::getDiskName());
} else {
Storage::put($file_path, $content);
self::getDisk()->put($file_path, $content);
}
} catch (\Exception $e) {
\Helper::logException($e, '[Attachment::saveFileToDisk()]');
@@ -268,7 +270,7 @@ class Attachment extends Model
// URL must contain only forward slashes.
$file_path = str_replace(DIRECTORY_SEPARATOR, '/', $file_path);
$file_url = Storage::disk('local')->url($file_path);
$file_url = self::getDisk()->url($file_path);
// Fix percents.
// https://github.com/freescout-helpdesk/freescout/issues/3530
@@ -298,7 +300,7 @@ class Attachment extends Model
public function download($view = false, $headers = [])
{
// #533
//return $this->getDisk()->download($this->getStorageFilePath(), \Str::ascii($this->file_name));
//return self::getDisk()->download($this->getStorageFilePath(), \Str::ascii($this->file_name));
if ($view) {
$headers['Content-Disposition'] = '';
}
@@ -311,7 +313,7 @@ class Attachment extends Model
// Cache attachments in browser - 1 month.
$headers['Cache-Control'] = 'max-age=2592000';
return Storage::download($this->getStorageFilePath(), $file_name, $headers);
return self::getDisk()->download($this->getStorageFilePath(), $file_name, $headers);
}
/**
@@ -338,7 +340,7 @@ class Attachment extends Model
public function getLocalFilePath($full = true)
{
if ($full) {
return Storage::path(self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name);
return self::getDisk()->path(self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name);
} else {
return DIRECTORY_SEPARATOR.'storage'.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name;
}
@@ -349,7 +351,7 @@ class Attachment extends Model
*/
public function fileExists()
{
return Storage::exists(self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name);
return self::getDisk()->exists(self::DIRECTORY.DIRECTORY_SEPARATOR.$this->file_dir.$this->file_name);
}
public static function formatBytes($size, $precision = 0)
@@ -400,7 +402,7 @@ class Attachment extends Model
{
// Delete from disk
foreach ($attachments as $attachment) {
Storage::delete($attachment->getStorageFilePath());
self::getDisk()->delete($attachment->getStorageFilePath());
}
// Delete from DB
@@ -480,7 +482,13 @@ class Attachment extends Model
public function getFileContents()
{
return Storage::get($this->getStorageFilePath());
try {
return self::getDisk()->get($this->getStorageFilePath());
} catch (\Exception $e) {
// File not found at path:...
\Helper::logException($e, '[Attachment::getFileContents()]');
return '';
}
}
/**
@@ -490,6 +498,25 @@ class Attachment extends Model
*/
public function getFileStream()
{
return Storage::readStream($this->getStorageFilePath());
try {
return self::getDisk()->readStream($this->getStorageFilePath());
} catch (\Exception $e) {
\Helper::logException($e, '[Attachment::getFileStream()]');
return null;
}
}
public static function getDiskName()
{
if (\Helper::isLocalStorage()) {
return self::DISK;
} else {
return config('filesystems.default');
}
}
public static function getDisk()
{
return Storage::disk(self::getDiskName());
}
}

View File

@@ -1955,52 +1955,74 @@ class Conversation extends Model
/**
* Get information on viewers for conversation table.
* Returns users in "users" key with the current user excluded: replying users first, then viewing users.
*/
public static function getViewersInfo($conversations, $fields = ['id', 'first_name', 'last_name'], $exclude_user_ids = [])
{
$viewers_cache = \Cache::get('conv_view');
$viewers = [];
$user_ids = [];
// Skip stale records not yet removed by freescout:check-conv-viewers.
// Dates have 'Y-m-d H:i:s' format, so they can be compared as strings.
// https://github.com/freescout-help-desk/freescout/pull/5670
$min_date = \Carbon\Carbon::now()->subSeconds(30)->toDateTimeString();
foreach ($conversations as $conversation) {
$first_user_id = null;
if (!empty($viewers_cache[$conversation->id])) {
// Get replying viewers
foreach ($viewers_cache[$conversation->id] as $user_id => $viewer) {
if (!$first_user_id) {
$first_user_id = $user_id;
}
if (!empty($viewer['r']) && !in_array($user_id, $exclude_user_ids)) {
$viewers[$conversation->id] = [
'user' => null,
'user_id' => $user_id,
'replying' => true,
];
$user_ids[] = $user_id;
break;
}
if (empty($viewers_cache[$conversation->id]) || !is_array($viewers_cache[$conversation->id])) {
continue;
}
$replying = [];
$viewing = [];
foreach ($viewers_cache[$conversation->id] as $user_id => $viewer) {
if (in_array($user_id, $exclude_user_ids)
|| empty($viewer['t'])
|| $viewer['t'] < $min_date
) {
continue;
}
// Get first non-replying viewer
if (empty($viewers[$conversation->id]) && !in_array($user_id, $exclude_user_ids)) {
$viewers[$conversation->id] = [
'user' => null,
'user_id' => $first_user_id,
'replying' => false,
];
$user_ids[] = $first_user_id;
$item = [
'user' => null,
'user_id' => $user_id,
'replying' => !empty($viewer['r']),
];
if ($item['replying']) {
$replying[] = $item;
} else {
$viewing[] = $item;
}
$user_ids[] = $user_id;
}
// Replying viewers go first.
$list = array_merge($replying, $viewing);
if ($list) {
// First viewer is kept on the top level for backward compatibility.
$viewers[$conversation->id] = $list[0];
$viewers[$conversation->id]['users'] = $list;
}
}
// Get all viewing users in one query
if ($user_ids) {
$user_ids = array_unique($user_ids);
$users = User::select($fields)->whereIn('id', $user_ids)->get();
$users = User::select($fields)->whereIn('id', $user_ids)->get()->keyBy('id');
foreach ($viewers as $i => $viewer) {
foreach ($users as $user) {
if ($user->id == $viewer['user_id']) {
$viewers[$i]['user'] = $user;
foreach ($viewers as $conversation_id => $viewer) {
foreach ($viewer['users'] as $i => $item) {
if (!empty($users[$item['user_id']])) {
$viewers[$conversation_id]['users'][$i]['user'] = $users[$item['user_id']];
} else {
// User not found in DB.
unset($viewers[$conversation_id]['users'][$i]);
}
}
// Remove not found users.
$viewers[$conversation_id]['users'] = array_values($viewers[$conversation_id]['users']);
if (!$viewers[$conversation_id]['users']) {
unset($viewers[$conversation_id]);
continue;
}
$first_user = $viewers[$conversation_id]['users'][0];
$viewers[$conversation_id]['user'] = $first_user['user'];
$viewers[$conversation_id]['user_id'] = $first_user['user_id'];
$viewers[$conversation_id]['replying'] = $first_user['replying'];
}
}
return $viewers;

View File

@@ -845,7 +845,7 @@ class ConversationsController extends Controller
}
// Check max. message size.
if (!$response['msg']) {
if (!$response['msg'] && !$is_note) {
$max_message_size = (int)config('app.max_message_size');
if ($max_message_size) {
@@ -857,13 +857,17 @@ class ConversationsController extends Controller
$attachments_ids = $this->decodeAttachmentsIds($attachments_ids);
if (count($attachments_ids)) {
$attachments_to_check = Attachment::select('size')->whereIn('id', $attachments_ids)->get();
foreach ($attachments_to_check as $attachment) {
$attachments_query = Attachment::select('size')->whereIn('id', $attachments_ids);
// Skip embedded images.
if (!\Eventy::filter('attachments.embedded.check_size', false)) {
$attachments_query->where('embedded', false);
}
foreach ($attachments_query->get() as $attachment) {
$message_size += (int)$attachment->size;
}
}
if ($message_size > $max_message_size*1024*1024) {
if ($message_size*1.37 > $max_message_size*1024*1024) {
$response['msg'] = __('Message is too large — :info. Please shorten your message or remove some attachments.', ['info' => __('Max. Message Size').': '.$max_message_size.' MB']);
}
}

View File

@@ -217,7 +217,7 @@ class OpenController extends Controller
}
// Check attachment name.
if (trim($attachment->file_name) != trim($file_name)) {
if (mb_strtolower(trim($attachment->file_name)) != mb_strtolower(trim($file_name))) {
return \Helper::denyAccess();
}

View File

@@ -18,7 +18,7 @@ return [
| or any other location as required by the application or its packages.
*/
'version' => '1.8.242',
'version' => '1.8.243',
/*
|--------------------------------------------------------------------------
@@ -318,10 +318,11 @@ return [
| to avoid sending multiple duplicate emails to other recipients.
|
| https://github.com/freescout-helpdesk/freescout/issues/870#issuecomment-786477909
| https://github.com/freescout-help-desk/freescout/issues/5666
|
|-------------------------------------------------------------------------
*/
'no_retry_mail_errors' => env('APP_NO_RETRY_MAIL_ERRORS', '(no valid recipients|does not comply with RFC|message file too big|malformed address)'),
'no_retry_mail_errors' => env('APP_NO_RETRY_MAIL_ERRORS', '(no valid recipients|does not comply with RFC|message file too big|malformed address|message size|message too large|size exceeded)'),
/*
|--------------------------------------------------------------------------

View File

@@ -43,6 +43,7 @@ return [
'disks' => [
// /storage/app/public
'local' => [
'driver' => 'local',
'root' => storage_path('app/public'),
@@ -55,15 +56,16 @@ return [
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
],
],*/
// To store file in the storage/app folder
'private' => [
// To store files in the storage/app folder not available from browser:
// /storage/app
'local_app' => [
'driver' => 'local',
'root' => storage_path('app'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
],*/
],
's3' => [
'driver' => 's3',

View File

@@ -136,7 +136,7 @@
@foreach ($conversations as $conversation)
<tr class="conv-row @action('conversations_table.row_class', $conversation) @if ($conversation->isActive()) conv-active @endif @if ($conversation->isSpam()) conv-spam @endif" data-conversation_id="{{ $conversation->id }}">
@if (empty($no_checkboxes))<td class="conv-current">@if (!empty($viewers[$conversation->id]))
<div class="viewer-badge @if (!empty($viewers[$conversation->id]['replying'])) viewer-replying @endif" data-toggle="tooltip" title="@if (!empty($viewers[$conversation->id]['replying'])){{ __(':user is replying', ['user' => $viewers[$conversation->id]['user']->getFullName()]) }}@else{{ __(':user is viewing', ['user' => $viewers[$conversation->id]['user']->getFullName()]) }}@endif"><div>
<div class="viewer-badge @if (!empty($viewers[$conversation->id]['replying'])) viewer-replying @endif" data-toggle="tooltip" data-html="true" title="{{ implode('<br/>', array_map(function ($viewer) { return e(__($viewer['replying'] ? ':user is replying' : ':user is viewing', ['user' => $viewer['user']->getFullName()])); }, $viewers[$conversation->id]['users'])) }}"><div>
@endif</td>@else<td class="conv-current"></td>@endif
@if (empty($no_checkboxes))
<td class="conv-cb">