diff --git a/app/Http/Controllers/ConversationsController.php b/app/Http/Controllers/ConversationsController.php index b40e12ba..691205dc 100755 --- a/app/Http/Controllers/ConversationsController.php +++ b/app/Http/Controllers/ConversationsController.php @@ -753,6 +753,13 @@ class ConversationsController extends Controller } } + $body = $request->body; + + // Replace base64 images with attachment URLs in case text + // was copy and pasted into the editor. + // https://github.com/freescout-helpdesk/freescout/issues/3057 + $body = Thread::replaceBase64ImagesWithAttachments($body); + // List of emails. $to_array = []; if ($is_forward) { @@ -789,7 +796,7 @@ class ConversationsController extends Controller $conversation = new Conversation(); $conversation->type = $type; $conversation->subject = $request->subject; - $conversation->setPreview($request->body); + $conversation->setPreview($body); $conversation->mailbox_id = $request->mailbox_id; $conversation->created_by_user_id = auth()->user()->id; $conversation->source_via = Conversation::PERSON_USER; @@ -937,7 +944,7 @@ class ConversationsController extends Controller $conversation->threads_count++; // We need to set preview here as when conversation is created from draft, // ThreadObserver::created() method is not called. - $conversation->setPreview($request->body); + $conversation->setPreview($body); } $conversation->save(); @@ -993,7 +1000,7 @@ class ConversationsController extends Controller $thread->created_by_user_id = auth()->user()->id; $thread->edited_by_user_id = null; $thread->edited_at = null; - $thread->body = $request->body; + $thread->body = $body; if ($is_create && !$is_multiple && count($to_array) > 1) { $thread->setTo($to_array); } else { diff --git a/app/Misc/Helper.php b/app/Misc/Helper.php index 5e278a8c..8c73058b 100644 --- a/app/Misc/Helper.php +++ b/app/Misc/Helper.php @@ -1331,7 +1331,7 @@ class Helper $links = array(); // Extract existing links and tags - $value = preg_replace_callback('~(.*?|<.*?>)~i', function ($match) use (&$links) { return '<' . array_push($links, $match[1]) . '>'; }, $value ?? ''); + $value = preg_replace_callback('~(.*?|<.*?>)~i', function ($match) use (&$links) { return '<' . array_push($links, $match[1]) . '>'; }, $value ?? '') ?: $value; $value = $value ?? ''; @@ -1339,14 +1339,17 @@ class Helper foreach ((array)$protocols as $protocol) { switch ($protocol) { case 'http': - case 'https': $value = preg_replace_callback('~(?:(https?)://([^\s<]+)|(www\.[^\s<]+?\.[^\s<]+))(?$protocol://$link") . '>'; }, $value); break; - case 'mail': $value = preg_replace_callback('~([^\s<>]+?@[^\s<]+?\.[^\s<]+)(?{$match[1]}") . '>'; }, $value); break; - default: $value = preg_replace_callback('~' . preg_quote($protocol, '~') . '://([^\s<]+?)(?$protocol://{$match[1]}") . '>'; }, $value); break; + case 'https': $value = preg_replace_callback('~(?:(https?)://([^\s<]+)|(www\.[^\s<]+?\.[^\s<]+))(?$protocol://$link") . '>'; }, $value) ?: $value; + break; + case 'mail': $value = preg_replace_callback('~([^\s<>]+?@[^\s<]+?\.[^\s<]+)(?{$match[1]}") . '>'; }, $value) ?: $value; + break; + default: $value = preg_replace_callback('~' . preg_quote($protocol, '~') . '://([^\s<]+?)(?$protocol://{$match[1]}") . '>'; }, $value) ?: $value; + break; } } // Insert all link - return preg_replace_callback('/<(\d+)>/', function ($match) use (&$links) { return $links[$match[1] - 1]; }, $value ?? ''); + return preg_replace_callback('/<(\d+)>/', function ($match) use (&$links) { return $links[$match[1] - 1]; }, $value ?? '') ?: $value; } /** @@ -1873,4 +1876,14 @@ class Helper $then = mb_substr($string, 1, null, $encoding); return mb_strtoupper($first_char, $encoding) . $then; } + + /** + * This is needed to allow using regexes for large texts. + */ + public static function setPcreBacktrackLimit() + { + if ((int)ini_get('pcre.backtrack_limit') <= 1000000) { + ini_set('pcre.backtrack_limit', 1000000000); + } + } } diff --git a/app/Thread.php b/app/Thread.php index 3940f16d..36811d1c 100644 --- a/app/Thread.php +++ b/app/Thread.php @@ -308,11 +308,12 @@ class Thread extends Model // Change "background:" to "background-color:". // https://github.com/freescout-helpdesk/freescout/issues/2560 - $body = preg_replace("/(<[^<>]+style=[\"'][^\"']*)background: *([^;() ]+;)/", '$1background-color:$2', $body); + // Keep in mind that with large texts preg_replace() may return null. + $body = preg_replace("/(<[^<>]+style=[\"'][^\"']*)background: *([^;() ]+;)/", '$1background-color:$2', $body) ?: $body; // Cut out "collapse" class as it hides elements. - $body = preg_replace("/(<[^<>\r\n]+class=([\"'][^\"']* |[\"']))(collapse|hidden)([\"' ])/", '$1$4', $body); - + $body = preg_replace("/(<[^<>\r\n]+class=([\"'][^\"']* |[\"']))(collapse|hidden)([\"' ])/", '$1$4', $body) ?: $body; + return \Helper::purifyHtml($body); } @@ -352,7 +353,7 @@ class Thread extends Model return sprintf('target="_blank" %s', array_shift($m2)); }, $tpl); - }, $body); + }, $body) ?: $body; return $body; } @@ -1440,4 +1441,36 @@ class Thread extends Model { return $this->type == self::TYPE_MESSAGE; } + + public static function replaceBase64ImagesWithAttachments($body, $user_id = null) + { + \Helper::setPcreBacktrackLimit(); + + $body = preg_replace_callback("#(]+src=[\"'])data:image/([^;]+);base64,([^\"']+)([\"'])#", + function ($match) { + $attachment = null; + $data = base64_decode($match[3]); + + if ($data) { + $attachment = Attachment::create( + $file_name = number_format(microtime(true), 4, '', '').'.'.$match[2], + $mime_type = 'image/'.$match[2], + $type = Attachment::TYPE_IMAGE, + $data, + $uploaded_file = null, + $embedded = true, + $thread_id = null, + $user_id = \Auth::id() + ); + } + if ($attachment) { + return $match[1].$attachment->url().$match[4]; + } else { + return $match[0]; + } + }, $body + ); + + return $body; + } }