Compare commits

...

817 Commits

Author SHA1 Message Date
FreeScout
a02cc2878e Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-07-21 03:24:07 -07:00
FreeScout
1426336439 v1.8.187 2025-07-21 03:23:26 -07:00
FreeScout
d3263c2429 Fix Helper::decrypt() function - closes #4929 2025-07-21 03:19:38 -07:00
FreeScout
4594a539a2 Optimize MailHelper::getHeader() 2025-07-20 23:12:33 -07:00
FreeScout
cf18c3850e Update Romanian language 2025-07-20 00:25:31 -07:00
FreeScout
53f6100f2c Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-07-19 04:29:38 -07:00
FreeScout
26a98823f9 v1.8.186 2025-07-19 04:28:38 -07:00
FreeScout
5fcc7de0a9 Add Romanian language 2025-07-19 04:26:32 -07:00
FreeScout
8814df7cfa Conversation threading for Outlook - closes #4922 2025-07-17 22:52:33 -07:00
FreeScout
b06e8928d2 Add allowed_tags parameter to Helper::stripDangerousTags() 2025-07-17 21:37:52 -07:00
FreeScout
304c0d4bca Add reply separator - closes #4764 2025-07-17 12:18:23 -07:00
FreeScout
62768d2112 Added actions to system status and tools pages - closes #4920 2025-07-17 12:14:46 -07:00
FreeScout
dedf9efe54 Add conversation.before_threads action #4917 from Lars-/master
Add conversation.before_threads action
2025-07-14 13:27:36 +02:00
Lars Jansen
09aafd50b2 feat: added conversation.before_threads action 2025-07-14 13:03:17 +02:00
FreeScout
9669c57f1d Strip also OBJECT tag in Helper::stripDangerousTags() 2025-07-14 02:12:56 -07:00
FreeScout
e2de65f3f3 Do not unserialize data by default in Helper::decrypt() function 2025-07-13 23:31:34 -07:00
FreeScout
1794763bbb Do not unserialize data by default in Helper::decrypt() function 2025-07-10 08:37:12 -07:00
FreeScout
a4e9e89518 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-07-05 02:36:31 -07:00
FreeScout
4038bbe6b5 v1.8.185 2025-07-05 02:36:18 -07:00
FreeScout
09558dab75 In cCat mMde expand Show Details block if it contains invalid fields 2025-07-04 23:17:10 -07:00
FreeScout
0298d7f3c6 Add start new chat option to mailbox - closes #4902 2025-07-04 22:48:05 -07:00
FreeScout
a58360926a Allow to prefill multiple emails via GET parameter - closes #4896 2025-06-29 10:58:13 -07:00
FreeScout
b3f1868b42 Fix group keys error in Translation manager 2025-06-26 11:18:31 -07:00
FreeScout
86b30fe336 Add Arabic language 2025-06-25 03:50:54 -07:00
FreeScout
e4f471ef28 Update Russian translation 2025-06-25 03:27:36 -07:00
FreeScout
e85c3be9ff Show error on POP3 failed authentication 2025-06-24 23:13:00 -07:00
FreeScout
b3e64dcfb1 PHP 8.4 compatibility - closes #4881 2025-06-23 22:58:10 -07:00
FreeScout
c0ae0ca567 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-06-22 05:38:20 -07:00
FreeScout
5638e46fab v1.8.184 2025-06-22 05:38:07 -07:00
FreeScout
b5b79fd298 Update Czech translation 2025-06-21 23:20:29 -07:00
FreeScout
806e094cd5 Fix mb_convert_encoding() error in decodeBoundary() on fethcing 2025-06-21 23:01:58 -07:00
FreeScout
a454c0cd57 Fix too many redirects issue after assigning a conversation in some cases - closes #4009 2025-06-20 00:04:02 -07:00
FreeScout
98a94c3e6b Add missing schedule options for fetch command #4873 from sulemaanhamza/master
Add missing schedule options for fetch command
2025-06-19 14:59:24 +02:00
suleman
b1b24f3114 Add missing schedule options for fetch command 2025-06-19 15:47:50 +05:00
FreeScout
570e3b9932 Fixes in Laravel Translation Manager - CVE-2025-49130 2025-06-17 11:39:20 -07:00
FreeScout
fb2fd080fe Add vendor files to dist 2025-06-13 23:18:43 -07:00
FreeScout
6a27f13f3c Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-06-13 23:17:55 -07:00
FreeScout
382a938b14 v1.8.183 2025-06-13 23:17:38 -07:00
FreeScout
e1abd03937 PHP 8.4 compatibility - closes #4848 2025-06-10 10:23:21 -07:00
FreeScout
3e05686342 Fix Call to a member function getFullName() on null on fetching - closes #4845 2025-06-10 03:42:18 -07:00
FreeScout
9aa8299720 Remove blockquote gmail_quote from the list of reply separators 2025-06-05 11:12:41 -07:00
FreeScout
fbc0376443 Set RFC822.eml name for attached .MSG and .EML files - closes #4738 2025-06-03 05:33:50 -07:00
FreeScout
7a0f0d64e0 Do not send email notifications if mail server send limit reached - closes #4806 2025-06-02 23:27:55 -07:00
FreeScout
ca675b0815 Add vendor files to dist 2025-06-02 23:00:31 -07:00
FreeScout
a3e6826b2f Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-06-02 22:59:34 -07:00
FreeScout
395318b568 v1.8.182 2025-06-02 22:59:17 -07:00
FreeScout
d6483f895c Fix showing Delete button in bulk actions - closes #4819 2025-06-01 11:24:56 -07:00
FreeScout
b740d55260 Fix saving their profiles by support agents 2025-05-30 00:00:08 -07:00
FreeScout
da62f64f34 Add hook to ReplyToCustomer.php - closes #4815 2025-05-29 11:54:42 -07:00
FreeScout
0a9a51c73c Fix error on uploading user photo - closes #4812 2025-05-28 11:02:36 -07:00
FreeScout
fcc7437ffa Fix PHP 8.4 error in Swift/TransportException.php - closes #4809 2025-05-28 03:55:24 -07:00
FreeScout
18fc0bc54d Fix null parameter issue in imap_rfc822_parse_headers() - closes #4807 2025-05-27 10:39:45 -07:00
FreeScout
95fe52218e Add __h() function 2025-05-27 06:40:53 -07:00
FreeScout
fb346f2e2e Add vendor files to dist 2025-05-27 00:01:08 -07:00
FreeScout
b0804c6dca Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-05-26 23:59:31 -07:00
FreeScout
7442fbb522 v1.8.181 2025-05-26 23:58:44 -07:00
FreeScout
54b9548f8c Add __j() function 2025-05-26 23:56:10 -07:00
FreeScout
8810c48f97 Show translated messages when activating or deactivating modules 2025-05-25 10:02:48 -07:00
FreeScout
4fd28b3038 Escape quotes when publishing translations 2025-05-25 09:48:57 -07:00
FreeScout
c624e8e781 Add md size to modal dialogs 2025-05-25 09:02:40 -07:00
FreeScout
81bf63f8b0 Suppress Duplicate entry error when fetching emails - closes #4795 2025-05-23 06:20:11 -07:00
FreeScout
5dd7a2effb Do not trim values enclosed in quotes in sanitiseVariableValue() - closes #4714 2025-05-23 05:54:52 -07:00
FreeScout
8484785a17 Take into account APP_SHOW_ONLY_ASSIGNED_CONVERSATIONS option when deleting conversation 2025-05-23 05:36:57 -07:00
FreeScout
3f5bb2841f Eliminate possibility of race condition when deleting a user 2025-05-23 03:01:10 -07:00
FreeScout
508dda1685 Strip tags in User::setData() method 2025-05-23 02:38:50 -07:00
FreeScout
9dc18b4b52 Fix error on creating mailbox - closes #4800 2025-05-23 02:02:25 -07:00
FreeScout
9879e29c20 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-05-22 19:29:49 -07:00
FreeScout
59a2975cc0 v1.8.180 2025-05-22 19:29:37 -07:00
FreeScout
cd98d8b559 Add fetch_emails.separate_reply.preprocess_body filter - closes #4791 2025-05-22 19:27:31 -07:00
FreeScout
305f45a03d Sanitize name field when creating mailbox 2025-05-22 12:08:29 -07:00
FreeScout
7718d8e906 Strip dangerous tags in /system/ajax-html/job_details/ modal 2025-05-22 11:57:56 -07:00
FreeScout
540b3cba6e Strip dangerous tags when publishing translations 2025-05-22 11:23:00 -07:00
FreeScout
b5574ea37b Set same_site back to null in session config - closes #4769 2025-05-22 11:03:15 -07:00
FreeScout
ac13321407 Make setCookie() function crossbrowser in main.js - closes #4793 2025-05-22 10:59:55 -07:00
FreeScout
47c516ef9a Escape company name displayed in the Welcome to Company flash message 2025-05-21 11:39:56 -07:00
FreeScout
20034ab376 Escape customer name in the customer updated flash message 2025-05-21 11:31:17 -07:00
FreeScout
4fb8b7f8d7 Remove also unclosed restricted tags in Helper::stripDangerousTags() function 2025-05-21 03:21:09 -07:00
FreeScout
38316da137 Limit fields which can be updated during updating customer 2025-05-20 23:10:05 -07:00
FreeScout
507be36664 Limit fields which can be updated in OpenController userSetupSave() function 2025-05-20 11:16:30 -07:00
FreeScout
8e3c87ac55 Check language name when deleting language from Translate page 2025-05-20 02:55:45 -07:00
FreeScout
d2048f59a8 Limit fields which can be updated during user creation 2025-05-20 02:45:50 -07:00
FreeScout
119a2772ec Limit fields which can be updated on mailbox Settings and Auto Reply pages 2025-05-20 02:40:12 -07:00
FreeScout
1628567cfc Do not allow regular users change other users emails when PERM_EDIT_USERS enabled 2025-05-19 03:54:12 -07:00
FreeScout
f0b8e63897 Fix fix UserPolicy update method 2025-05-19 03:47:19 -07:00
FreeScout
6eb8bdad2e Do not allow to change user password in profileSave() function 2025-05-19 03:38:55 -07:00
FreeScout
d797aacb9a Fix CSP issue when embedding iframe - closes #4777 2025-05-19 03:01:15 -07:00
FreeScout
e804038b4e Fix German translation - closes #4776 2025-05-18 22:35:14 -07:00
FreeScout
1f154ce039 Take into account APP_LIMIT_USER_CUSTOMER_VISIBILITY option on customer profile page 2025-05-18 11:15:43 -07:00
FreeScout
eb0be440ac Take into account APP_SHOW_ONLY_ASSIGNED_CONVERSATIONS option on customer Conversation tab 2025-05-18 04:28:38 -07:00
FreeScout
cfdcdaf092 Take into account APP_SHOW_ONLY_ASSIGNED_CONVERSATIONS option when following or unfollowing conversation 2025-05-18 04:19:24 -07:00
FreeScout
43dafd9130 Take into account APP_SHOW_ONLY_ASSIGNED_CONVERSATIONS option when changing conversation subject 2025-05-18 04:17:27 -07:00
FreeScout
87cdb65d6b Take into account APP_SHOW_ONLY_ASSIGNED_CONVERSATIONS option in conversation update policy 2025-05-18 04:11:52 -07:00
FreeScout
0a8fe89a92 Add vendor files to dist 2025-05-18 03:44:23 -07:00
FreeScout
d9f96d4e03 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-05-18 03:43:34 -07:00
FreeScout
ce607ff111 v1.8.179 2025-05-18 03:43:15 -07:00
FreeScout
2552a2b842 Check from_thread_id parameter access permissions when creating new conversations 2025-05-17 11:17:14 -07:00
FreeScout
01c91d2086 Check access permissions when muting mailbox notifications via Ajax 2025-05-17 06:52:19 -07:00
FreeScout
e136660e8d Add phtml and phar to the list of restricted file upload extensions 2025-05-16 23:22:22 -07:00
FreeScout
f411f8cf5a Fix CVE-2024-52301 issue in laravel/framework 2025-05-16 11:39:58 -07:00
FreeScout
da7898446f Add extra reply separator - closes #4764 2025-05-16 06:08:45 -07:00
FreeScout
5620c25830 Fix array_merge error in Option::set() - closes #4759 2025-05-15 09:19:58 -07:00
FreeScout
fa3f6c8c63 Allow external styles and fonts in the CSP tag - closes #4755 2025-05-15 04:47:22 -07:00
FreeScout
7a2f6d1e12 Adjust CSP tag to display external images - closes #4754 2025-05-15 04:01:39 -07:00
FreeScout
eab9771102 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-05-14 23:34:52 -07:00
FreeScout
8267be397f v1.8.178 2025-05-14 23:34:35 -07:00
FreeScout
4e82464f48 Set SameSite to None for session cookie and others - closes #4750 2025-05-14 23:32:20 -07:00
FreeScout
f03810e380 Do not use serialize() function in Option::set() 2025-05-14 22:38:44 -07:00
FreeScout
fb33d672a2 Improve sanitizing php_path in tools.php 2025-05-14 11:44:36 -07:00
FreeScout
311319199e Do not use serialize() function in Option::set() 2025-05-14 11:00:29 -07:00
FreeScout
17421bae24 Fix VerifyCsrfToken::handle() function - closes #4752 2025-05-14 06:45:01 -07:00
FreeScout
f9f491c453 Fix TokenMismatchException not found - closes #4753 2025-05-14 06:38:18 -07:00
FreeScout
d7d7a020d3 Do not allow to create translations with random language code 2025-05-14 04:22:02 -07:00
FreeScout
f7548a7076 Do not user serialize() function in Option::set() 2025-05-14 04:05:17 -07:00
FreeScout
018bb1f982 Remove new line symbols in Helper::setEnvFileVar() 2025-05-14 02:43:05 -07:00
FreeScout
916fe1102f Adjust default-src in CSP tag - closes #4749 2025-05-14 02:22:06 -07:00
FreeScout
9070732d2c Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-05-13 12:03:41 -07:00
FreeScout
4f8d998068 v1.8.177 2025-05-13 12:03:20 -07:00
FreeScout
8b07e919ad Rollback CSP tag changes - closes #4749 2025-05-13 12:01:56 -07:00
FreeScout
e4b21c9797 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-05-13 11:07:45 -07:00
FreeScout
0fb7bb3aa5 v1.8.176 2025-05-13 11:07:26 -07:00
FreeScout
823816517e Add new reply separator - closes #4629 2025-05-12 22:49:19 -07:00
FreeScout
5f8a7feb2a Adjust reply separators - closes #4629 2025-05-12 06:36:38 -07:00
FreeScout
9102717053 Adjust reply separators - closes #4629 2025-05-12 04:15:55 -07:00
FreeScout
2cc4fb3c5c Set X-Content-Type-Options header and remove unwanted headers 2025-05-09 11:30:41 -07:00
FreeScout
9ab1dae25e Do not set XSRF-TOKEN cookie as it is not used 2025-05-09 11:17:42 -07:00
FreeScout
10cafafeef Set SameSite to Lax by default in setCookie function in main.js 2025-05-09 10:53:35 -07:00
FreeScout
0f1d3fa9fc Set SameSite attribute to lax for session cookie 2025-05-09 10:49:59 -07:00
FreeScout
694403e0ce Add form-action to CSP meta tag 2025-05-09 06:05:48 -07:00
FreeScout
8274a96d7c Add default-src to CSP meta tag 2025-05-09 06:01:06 -07:00
FreeScout
c980d52ff0 Do not stop fetching when encountering an email without Message-ID - closes #4740 2025-05-08 07:34:43 -07:00
FreeScout
0b6a314f01 Take in account possible spaces when handling forwarded emails - closes #4733 2025-05-04 06:47:47 -07:00
FreeScout
03992b2061 Add action to append customer profile tab #4725 from simivar/patch-2
Add action to append customer profile tab
2025-04-29 07:51:51 +02:00
Krystian Marcisz
44bf284fe3 feat: add action to append profile tab
Adds a new actions, `customers.profile_tabs.append`, for easy appending of new tabs in the Customer's profile
2025-04-28 22:32:44 +02:00
FreeScout
1ced45029e Add vendor files to dist 2025-04-25 23:57:06 -07:00
FreeScout
639c08c1f3 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-04-25 23:56:32 -07:00
FreeScout
9e0ef60e00 v1.8.175 2025-04-25 23:56:22 -07:00
FreeScout
fd4b334d8a Webklex - Fix Date parsing on non-standard format from Aqua Mail 2025-04-25 04:00:41 -07:00
FreeScout
a8a2bded0a Webklex - Fix case sensitivity of folder attribute parsing: NoSelect, NoInferiors 2025-04-25 03:50:28 -07:00
FreeScout
ca003cecb2 Webklex - Fix Get folders list in hierarchical order 2025-04-25 03:45:42 -07:00
FreeScout
7ad231787b Webklex - When using the chunk function, some messages do not have an element with index 0 2025-04-25 03:43:35 -07:00
FreeScout
17cfded1f0 Added extra user actions and filters - closes #4715 2025-04-23 03:50:35 -07:00
FreeScout
7a2f965951 Added extra user actions and filters #4715 from Lars-/feature/notifications
Added extra user actions and filters
2025-04-23 12:48:19 +02:00
FreeScout
22ef2b48d1 Update email.user_notification.subject Eventy filter #4713 from avenjamin/master
Update email.user_notification.subject Eventy filter
2025-04-23 12:40:34 +02:00
Lars Jansen
4ab35da9cf chore: changed action to use less parameters 2025-04-23 12:09:42 +02:00
Lars Jansen
4b6171c9cd chore: changed modifications to actions and filters 2025-04-23 12:05:42 +02:00
Ben Perry
174b7dcf36 Update email.user_notification.subject Eventy filter
Added `$this->conversation` parameter to allow the Ticket Module to override the email subject.
2025-04-23 15:09:01 +10:00
FreeScout
a086151b2f Prevent zooming editor input in iOS browser - #4698 2025-04-12 20:51:29 -07:00
FreeScout
de4d6ed2fc PHP 8.4 compatibility - closes #4695 2025-04-09 04:38:41 -07:00
FreeScout
f142f0e575 Add mail fallback capabilities to mail vars #4693 from caseproof/add/mail-vars-fallback
Add mail fallback capabilities to mail vars
2025-04-08 08:27:41 +02:00
Thomas Patrick Levy
affa9325df feat: add mail vars fallback 2025-04-07 17:06:53 -07:00
Thomas Patrick Levy
56e8aa2669 Merge branch 'master' of github.com:freescout-help-desk/freescout 2025-04-07 16:17:08 -07:00
FreeScout
1839a43359 Add vendor files to dist 2025-04-04 05:38:45 -07:00
FreeScout
64d21bcecf Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-04-04 05:37:46 -07:00
FreeScout
2db9233ba1 v1.8.174 2025-04-04 05:37:32 -07:00
FreeScout
d154d31ec2 Fix UnclosedFormControlIsInvalid error in the editor - closes #4671 2025-04-03 23:23:08 -07:00
FreeScout
9fe476b742 Remove RLM characters when sanitizing file names - closes #2123 2025-04-03 23:02:46 -07:00
FreeScout
f91396f833 Improve PopProtocol.php 2025-04-02 23:08:34 -07:00
FreeScout
beabd14cae Update Readme 2025-04-01 23:59:04 -07:00
FreeScout
a2d81deff9 Fix retrieving IMAP folders containing single quotes in names - closes #4682 2025-04-01 23:57:25 -07:00
FreeScout
0e83d360b5 Remove unprintable characters and invalid unicode characters from uploaded file names - closes #4681 2025-04-01 05:30:33 -07:00
FreeScout
f05fe74f4e Do not hide attachments after editing notes or messages - closes #4678 2025-03-31 05:49:45 -07:00
FreeScout
cc87962474 Fix error when job payload is empty on Status page - closes #4673 2025-03-31 04:51:37 -07:00
FreeScout
10c32901d8 Add Hungarian language 2025-03-31 01:23:15 -07:00
FreeScout
d1dffe8f60 Take into account user time zone and time format in Customer viewed text - closes #4674 2025-03-30 03:20:39 -07:00
FreeScout
04ec0b09fe Timezone fix for Kolkata #4676 from vtq221/master
Timezone fix for Kolkata
2025-03-30 09:38:14 +02:00
vtq
3651fcdcc2 Timzone fix for Kolkata 2025-03-29 10:01:50 +11:00
FreeScout
feda4104b0 PHP 8.4 compatibility - closes #4630 2025-03-09 04:56:17 -07:00
FreeScout
8bf8c929fb Change Helper::IN_LIMIT to 999 to avoid issues on MariaDB - closes #4623 2025-03-07 07:07:20 -08:00
FreeScout
7f824c685e Show Link modal dialog faster - closes #4622 2025-03-05 05:16:51 -08:00
FreeScout
7f2f83d32e Show modal dialogs faster - closes #4622 2025-03-04 23:54:03 -08:00
FreeScout
b13e0fefb0 Show empty link URL when inserting link - closes #4622 2025-03-04 23:45:19 -08:00
FreeScout
dbeba27553 Trigger thread.before_save_from_request hook for each thread when sending to multiple recipients 2025-03-02 05:37:56 -08:00
FreeScout
90cd9061b4 Fix Call to a member function query() on null when testing fetching - closes #4614 2025-02-28 06:19:39 -08:00
FreeScout
7991cb39a5 Fix Call to a member function query() on null when testing fetching - closes #4614 2025-02-28 06:15:39 -08:00
FreeScout
31c72b8092 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-02-28 00:15:15 -08:00
FreeScout
5e56a473e7 v1.8.173 2025-02-28 00:14:38 -08:00
FreeScout
497e481d42 Allow to specify module.json in module latestVersionNumberUrl parameter - closes #4611 2025-02-27 23:41:39 -08:00
FreeScout
ade91429e8 Allow to spcify path to GitHub zip archive in module latestVersionZipUrl parameter - closes #4611 2025-02-26 22:45:27 -08:00
FreeScout
b9effba813 Show customer ZIP in the customer sidebar - closes #4606 2025-02-26 06:05:35 -08:00
FreeScout
0b1d72d68e Use keyMap Enter insertParagraph in editor as it insert divs #4607 from Som-Energia/fix-editor-insert-paragraph
Use keyMap Enter->insertParagraph in editor as it insert divs
2025-02-26 14:54:34 +01:00
Àngel Fernández
6c5ad6997d Use keyMap Enter->InserParagraht in editor as it insert divs 2025-02-26 12:56:29 +01:00
FreeScout
4df0c3196f Fix loadHTML() error on fetching - closes #4600 2025-02-25 09:53:01 -08:00
FreeScout
b8f82a15a9 Add vendor files to dist 2025-02-21 00:49:59 -08:00
FreeScout
792cb95718 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-02-21 00:49:02 -08:00
FreeScout
cebd754983 v1.8.172 2025-02-21 00:48:51 -08:00
FreeScout
e69a4105a1 Handling boundary split into several parts in email headers - closes #4567 2025-02-20 23:32:24 -08:00
FreeScout
1ff8dce33c Use ajax search in the customer merge select 2025-02-18 21:26:00 -08:00
FreeScout
5b74800cb4 Use ajax search in the customer merge select 2025-02-18 08:49:42 -08:00
FreeScout
199bfc5dca PHP 8.4 compatibility - closes #4588 2025-02-18 01:09:04 -08:00
FreeScout
5ea7d041b2 Merge customers - closes #2188 2025-02-17 23:59:51 -08:00
FreeScout
ad183694bf Handling boundary split into several parts in email headers - closes #4567 2025-02-17 00:18:04 -08:00
FreeScout
d77fa741c8 PHP 8.4 compatibility 2025-02-14 21:14:13 -08:00
FreeScout
ec4cf9a457 Add vendor files to dist 2025-02-14 03:33:22 -08:00
FreeScout
f7e20304b2 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-02-14 03:32:28 -08:00
FreeScout
f51651e45a v1.8.171 2025-02-14 03:32:07 -08:00
FreeScout
d179d96bde Update Swedish translations #4578 from timhultstrand/master
Update Swedish translations
2025-02-14 06:25:13 +01:00
timhultstrand
c7498f288e Update Swedish translations 2025-02-13 21:51:19 +01:00
FreeScout
f691fe2d7f Show which attachments are embedded in freescout:parse-eml command - closes #4575 2025-02-13 03:22:14 -08:00
FreeScout
6f1c286af3 Fix Undefined variable $protonmail_quote_pos on fetching - closes #4568 2025-02-11 04:02:45 -08:00
FreeScout
b43e300b7c PHP 8.4 compatibility 2025-02-11 04:01:00 -08:00
FreeScout
a23f64d539 Allow to enable persistent connections for MySQL and PostgreSQL #4566 from RisingOpsNinja/db-persistent-connections
Allow to enable persistent connections for MySQL and PostgreSQL
2025-02-10 15:07:08 +01:00
RisingOpsNinja
dcfa6d4f9c Allow to enable persistent connections for MySQL and Postgresql
Request a persistent db connection, rather than creating a new connection for every request.
2025-02-10 12:37:08 +01:00
FreeScout
791bc1d643 PHP 8.4 compatibility 2025-02-08 23:10:37 -08:00
FreeScout
bb178de5d2 Fix searching conversations and customers on PostgreSQL - closes #4559 2025-02-08 07:48:15 -08:00
FreeScout
33e000af6f PHP 8.4 compatibility 2025-02-08 06:00:08 -08:00
FreeScout
8c1c4c1a03 PHP 8.4 compatibility 2025-02-07 09:19:23 -08:00
FreeScout
35b628376b Allow to set PDO::PGSQL_ATTR_DISABLE_PREPARES via environment variable #4558 from RisingOpsNinja/master
Allow to set PDO::PGSQL_ATTR_DISABLE_PREPARES via environment variable
2025-02-07 16:52:33 +01:00
RisingOpsNinja
79bc595e14 Allow to set PDO PGSQL_ATTR_DISABLE_PREPARES via environment variable 2025-02-07 16:28:28 +01:00
FreeScout
d9ada9c6ec Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-02-07 05:22:47 -08:00
FreeScout
ee361fd95a v1.8.170 2025-02-07 05:22:38 -08:00
FreeScout
3d517b4434 Fix empty body issue caused by incrorrect proton reply fix - closes #4556 2025-02-07 05:21:52 -08:00
FreeScout
d95ab52c4d Add vendor files to dist 2025-02-06 23:40:39 -08:00
FreeScout
4d889e31e4 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-02-06 23:39:40 -08:00
FreeScout
dabc022c4f v1.8.169 2025-02-06 23:39:10 -08:00
FreeScout
3c709fa3ba Fix deleting attachments when creating a new conversation from existing thread - closes #4432 2025-02-06 22:10:06 -08:00
FreeScout
95c2a1a2f4 Disable routes caching as it does not increase speed and increases memory consumption - closes #4544 2025-02-04 09:32:17 -08:00
FreeScout
bab2fc5fb8 Enable routes caching - closes #4544 2025-02-04 01:55:55 -08:00
FreeScout
cf94762736 Update Gmail reply separator 2025-02-02 22:23:58 -08:00
FreeScout
94269e1dea Fix separating replies from Proton emails - closes #4537 2025-02-02 22:20:46 -08:00
FreeScout
34b910bbba Fix getFullName() on null in Conversation.php - closes #4540 2025-01-31 08:42:39 -08:00
FreeScout
fbcd1797f0 Remove services.php and packages.php in /bootstrap/cache/ when clearing cache - closes #4536 2025-01-31 03:20:21 -08:00
FreeScout
cb0e560ed8 Remove services.php and packages.php in /bootstrap/cache/ when clearing cache - closes #4536 2025-01-31 03:11:06 -08:00
FreeScout
589ac69594 Increase mailbox imap_sent_folder field max length to 50 - closes #4533 2025-01-30 03:42:23 -08:00
FreeScout
e4dfd3cb2e Revert editor blockquote fix - closes #3811 2025-01-29 06:16:20 -08:00
FreeScout
490475fe56 Add searching by user fullname for conversations and customers #4523 from caseproof/add/user-fullname-search
Add searching by user fullname for conversations and customers
2025-01-29 06:35:01 +01:00
Thomas Patrick Levy
65014d72b5 feat: add searching by user fullname for conversations and customers 2025-01-28 11:34:08 -07:00
FreeScout
2dd4260103 PHP 8.4 compatibility 2025-01-28 04:45:01 -08:00
FreeScout
7c00262494 Make encoding check case-insensitive in Header.php - closes #4519 2025-01-28 04:31:56 -08:00
FreeScout
4ab9e0beb8 PHP 8.4 compatibility 2025-01-27 22:10:12 -08:00
FreeScout
6608107d70 Change PHP packages installation order to avoid installing apache - closes #4514 2025-01-25 23:02:51 -08:00
FreeScout
cf08353ebc Update German translation 2025-01-24 22:25:40 -08:00
FreeScout
5c73a68d12 PHP 8.4 compatibility 2025-01-24 22:01:38 -08:00
FreeScout
ef70f04594 PHP 8.4 compatibility 2025-01-24 21:53:19 -08:00
FreeScout
ec83995838 Add vendor files to dist 2025-01-24 05:58:27 -08:00
FreeScout
e71de79923 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-01-24 05:57:46 -08:00
FreeScout
20188aecb1 v1.8.168 2025-01-24 05:56:49 -08:00
FreeScout
b2abca9af1 Fix empty message issue - closes #4508 2025-01-24 05:51:55 -08:00
FreeScout
29491f663f Fix accents and special characters removed from new customer names - closes #4506 2025-01-23 22:58:43 -08:00
FreeScout
2d821be5e1 PHP 8.4 compatibility - closes #4501 2025-01-23 09:40:31 -08:00
FreeScout
08b8e1cf1d Fix Undefined variable $e in Header.php - closes #4505 2025-01-23 09:06:19 -08:00
FreeScout
e1a493acd8 Add vendor files to dist 2025-01-22 20:16:21 -08:00
FreeScout
c8c0db5cb9 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-01-22 20:14:50 -08:00
FreeScout
417bf07f69 v1.8.167 2025-01-22 20:14:41 -08:00
FreeScout
17b8eeae96 Fix Call to undefined function str_starts_with() on fetching with PHP 7.x - closes #4499 2025-01-22 20:12:56 -08:00
FreeScout
d4a92e9bb9 PHP 8.4 compatibility 2025-01-22 06:17:23 -08:00
FreeScout
020b9e7c18 Exclude Modules folder from composer classmap - closes #4496 2025-01-21 10:15:45 -08:00
FreeScout
9b0cec6b2b Add vendor files to dist 2025-01-21 03:27:14 -08:00
FreeScout
736e931529 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-01-21 03:25:26 -08:00
FreeScout
e84fd891bf v1.8.166 2025-01-21 03:25:14 -08:00
FreeScout
888701e066 Rollback PHPUnit to v9.5.28 as v11.2.6 does not work with PHP 7.4 2025-01-21 02:18:54 -08:00
FreeScout
b59144e704 Update GitHub tests 2025-01-21 02:12:32 -08:00
FreeScout
eae50903f2 Update GitHub tests 2025-01-21 02:05:19 -08:00
FreeScout
66e19c9b2c PHPUnit v11.2.6 2025-01-21 00:50:49 -08:00
FreeScout
dcdb7e68cf PHP 8.4 compatibility 2025-01-20 22:55:21 -08:00
FreeScout
a363fb264c Add latest PHP versions to GitHub tests 2025-01-20 22:41:15 -08:00
FreeScout
877813cb73 Fix Unexpected ClientManager in FixtureWebklexMessage.php 2025-01-20 22:38:38 -08:00
FreeScout
0ced99564d Replace password with asterisks in PopProtocol.php logs 2025-01-20 22:27:34 -08:00
FreeScout
d6db40d563 Webklex/php-imap: send NOOP connection check once in second 2025-01-20 22:16:04 -08:00
FreeScout
4591dec171 Update composer.json scripts 2025-01-20 21:55:13 -08:00
FreeScout
d8b7723a7c Few tests for Webklex/php-imap 2025-01-20 21:15:26 -08:00
FreeScout
c7576c52e4 Do not require --mailbox parameter in freescout:parse-eml command 2025-01-20 10:44:06 -08:00
FreeScout
678cd45198 Improve MailHelper::decodeSubject() function 2025-01-20 08:07:53 -08:00
FreeScout
4b1afadf92 Remove blockquote from the list of reply separators - closes #4460 2025-01-20 05:11:59 -08:00
FreeScout
0422b29321 Webklex/php-imap: Fixed attachments with sspecial symbols in filename 2025-01-20 01:39:47 -08:00
FreeScout
9989bf02d3 Webklex/php-imap: send NOOP connection check once in second 2025-01-19 23:49:18 -08:00
FreeScout
c35b7ff3c0 Webklex/php-imap: Decode partially encoded address names 2025-01-19 23:18:22 -08:00
FreeScout
38e22ef69c Update comment for IMAP BODY[TEXT] 2025-01-19 22:58:43 -08:00
FreeScout
d2b7a87bbb Webklex/php-imap: Boundary parsing fixed and improved to support more formats 2025-01-19 22:56:27 -08:00
FreeScout
3208a8c1d9 Webklex/php-imap: Address parsing improved and extended to include more cases 2025-01-19 22:41:40 -08:00
FreeScout
ab8309bdab Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-01-18 01:53:52 -08:00
FreeScout
12584db7b5 v1.8.165 2025-01-18 01:53:39 -08:00
FreeScout
6dac6eb1df Make \Webklex\PHPIMAP\Header::rfc822_parse_headers() function static 2025-01-17 21:01:59 -08:00
FreeScout
3a33fd4628 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-01-17 20:45:37 -08:00
FreeScout
e1b9d711d3 v1.8.164 2025-01-17 20:45:28 -08:00
FreeScout
9e1b53d410 Fix accessing array offset on null in Header.php - closes #4488 2025-01-17 09:20:08 -08:00
FreeScout
ac43d4628d Fix Waiting Since not showing - closes #4486 2025-01-17 08:44:52 -08:00
FreeScout
33a7d06032 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-01-17 02:00:23 -08:00
FreeScout
a110039125 v1.8.163 2025-01-17 02:00:02 -08:00
FreeScout
dd654551c1 Fix Waiting Since not showing - closes #4486 2025-01-17 01:36:34 -08:00
FreeScout
74048295fa Fix user notification subject missing - closes #4485 2025-01-17 01:06:30 -08:00
FreeScout
e5cfdf58af Webklex/php-imap: Better connection check for IMAP 2025-01-17 00:25:01 -08:00
FreeScout
1595be8c0d Webklex/php-imap: Query - Chunked - Resolved infinite loop when start chunk > 1 2025-01-16 23:48:41 -08:00
FreeScout
925f9f4bc8 Webklex/php-imap: Make the space optional after a comma separator 2025-01-16 23:43:52 -08:00
FreeScout
a3f49ca25c Webklex/php-imap: Fixed date issue if timezone is UT and a 2 digit year 2025-01-16 23:42:08 -08:00
FreeScout
177e54f1f2 Add vendor files to dist 2025-01-16 23:28:40 -08:00
FreeScout
7efa8f5a1c Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-01-16 23:26:40 -08:00
FreeScout
18a5efcc98 v1.8.162 2025-01-16 23:25:36 -08:00
FreeScout
da99de9a44 Helper::jsonEncodeSafe() function - closes #4471 2025-01-16 22:39:36 -08:00
FreeScout
2c8caef9a6 PHP 8.4 compatibility 2025-01-16 21:29:27 -08:00
FreeScout
63b435ac42 Add Asia/Colombo timezone to timezone options - closes #4483 2025-01-16 21:28:06 -08:00
FreeScout
898f1d3266 Allow to preserve transparency in Helper::resizeImage() - closes #4480 2025-01-16 00:49:27 -08:00
FreeScout
c92d52d055 Added filters allowing module customisation of email reply and user notification templates and data. 2025-01-16 00:12:02 -08:00
FreeScout
486e777210 Added filters allowing module customisation of email reply and user notification templates #4477 from vtq221/master
Added filters allowing module customisation of email reply and user notification templates
2025-01-16 09:10:46 +01:00
FreeScout
ef4bbef17e Implement POP3 protocol without PHP IMAP extension functions 2025-01-15 23:57:08 -08:00
FreeScout
54226692a0 Fix Call to a member function getFullName() on null when some user has been deleted from DB - closes #4467 2025-01-15 23:52:30 -08:00
FreeScout
0d5944b95a PHP 8.4 compatibility 2025-01-15 05:50:36 -08:00
FreeScout
4627ec463a Replace fread() with fgets() in Webklex to avoid possible infinite loops 2025-01-14 22:15:48 -08:00
FreeScout
ca0bc6ef2a Avoid PHP IMAP extension functions 2025-01-14 21:26:56 -08:00
FreeScout
e26f7c5e84 Update Swedish language 2025-01-14 21:21:58 -08:00
FreeScout
e2617dbbd8 PHP 8.4 compatibility 2025-01-14 09:41:27 -08:00
FreeScout
185d660f8b Avoid PHP IMAP extension functions 2025-01-14 09:40:42 -08:00
vtq
a264424afb Added filters allowing module customisation of email reply and user notification templates and data. 2025-01-14 23:58:26 +11:00
FreeScout
50f0f25e0b Avoid PHP IMAP extension functions 2025-01-14 02:36:01 -08:00
FreeScout
976b926d2d PHP 8.4 compatibility 2025-01-14 00:55:41 -08:00
FreeScout
b26fb0e13a Add vendor files to dist 2025-01-13 03:00:56 -08:00
FreeScout
fa46d45662 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2025-01-13 03:00:07 -08:00
FreeScout
c56d315eb5 v1.8.161 2025-01-13 02:59:24 -08:00
FreeScout
222ffece76 Do not remove customer phone when creating a phone conversation without specifying customer phone number - closes #4464 2025-01-13 02:25:06 -08:00
FreeScout
cb8d99fe5a Fix deleting attachments when creating a new conversation from existing thread - closes #4432 2025-01-13 02:10:18 -08:00
FreeScout
c771001d9f Review CVE-2024-6484 and CVE-2024-6485 in the Bootstrap JS 2025-01-13 01:51:31 -08:00
FreeScout
7d7f1e708e Allow to get out of quote block in the editor - closes #3811 2025-01-01 23:30:37 -08:00
FreeScout
978138df1a Fix incorrect year returned by User::dateFormat() - closes #4443 2024-12-30 22:02:57 -08:00
FreeScout
095af77472 Fix Implicit conversion from float to int error in Guzzle - closes #4439 2024-12-20 21:03:16 -08:00
FreeScout
5eaa427f09 Fix Call to a member function getFullName() on null in Thread.php - closes #4438 2024-12-20 08:42:14 -08:00
FreeScout
178ac6d19b Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-12-20 08:28:52 -08:00
FreeScout
18fb794c75 v1.8.160 2024-12-20 08:28:42 -08:00
FreeScout
1148a35663 Fix preserving attachments when creating a new conversation from existing thread - closes #4432 2024-12-19 01:30:52 -08:00
FreeScout
043b7dd5ce Allow class attribute for tables in purifier config - closes #4429 2024-12-17 23:54:27 -08:00
FreeScout
13bf22065f Improve parts boundary detection in Webklex/php-imap - closes #4417 2024-12-12 04:17:40 -08:00
FreeScout
0e14e087a5 Make Insert Variable dropdown accessible via screenreaders - closes #4412 2024-12-10 23:31:26 -08:00
FreeScout
d41fac7a7f Fix OnOffswitch checkboxes on screenreaders - closes #4412 2024-12-10 21:24:03 -08:00
FreeScout
6abf8cde9e Action toolbar changes and added a new filter - #4396 2024-12-10 01:58:56 -08:00
FreeScout
17b81de9e1 Action toolbar changes and added a new filter #4396 from Lars-/master
Action toolbar changes and added a new filter
2024-12-10 12:50:49 +03:00
Lars Jansen
066871b1a3 chore: changed filter hook name 2024-12-10 10:39:36 +01:00
FreeScout
2f2e010796 Add aria-label to the Notifications dropdown in navigation menu - #4412 2024-12-09 22:52:31 -08:00
FreeScout
1ab7806356 Add getLocale() function to main.js 2024-12-09 08:33:05 -08:00
Lars Jansen
88c00d5a02 fix: delete button 2024-12-09 13:59:30 +01:00
FreeScout
b887113b50 New hooks: listeners.failed_login and fetch_emails.set_seen_flag #4407 from vtq221/master
New hooks: listeners.failed_login and fetch_emails.set_seen_flag
2024-12-07 13:29:52 +01:00
FreeScout
51dbe56f3f Improve Helper::strSplitKeepWords() function with Chinese symbols 2024-12-07 04:26:23 -08:00
vtq
f4fea342ed Added filter (filter set seen flag for messages) and action (send action on failed login) to enable for use in module development (email customization and failed login alerting/firewall). 2024-12-07 19:06:22 +11:00
FreeScout
01bfd5c971 Update readme 2024-12-06 09:23:44 -08:00
FreeScout
2ccc2cbd66 Update readme 2024-12-05 08:12:49 -08:00
Lars Jansen
143d96af6b feat: added filter on mailboxes in move_conv popup 2024-12-04 11:56:11 +01:00
Lars Jansen
93391f826c feat: make toolbar and dropdown buttons dynamic so they can be rearranged more easily 2024-12-04 11:56:00 +01:00
FreeScout
0790a5114f Update Dutch translation 2024-12-02 02:29:02 -08:00
FreeScout
11058803c7 Add extra phrase to no_retry_mail_errors list - closes #870 2024-12-02 00:57:59 -08:00
FreeScout
df0ab1add9 Fix vars.js broken when custom translation contains quotes - closes #4369 2024-11-29 22:21:21 -08:00
FreeScout
b564d20627 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-11-29 20:45:28 -08:00
FreeScout
3562298031 v1.8.159 2024-11-29 20:45:00 -08:00
FreeScout
7706d52e63 Fix text bolding issue - closes #4367 2024-11-29 06:31:14 -08:00
FreeScout
ff2e92f1ff Add layout.title.name hook 2024-11-29 06:18:39 -08:00
FreeScout
4b2f385e43 Fix Spanish translation #4376 from anxodio/patch-2
Fix Spanish translation
2024-11-28 17:17:59 +01:00
Àngel Fernández
a15c29bea0 Fix bad translation
The translation says the opposite than the original intention
2024-11-28 16:10:32 +01:00
FreeScout
a0594bf6d9 Fix Undefined array key 2 error on fetching - closes #4372 2024-11-28 04:43:24 -08:00
FreeScout
6e0a213003 Fix text bolding issue - closes #4367 2024-11-28 04:30:42 -08:00
FreeScout
8024c72db5 Optimize UpdateFolderCounters Job #4374 from mahavirn/optimize-update-folder-count-job
Optimize UpdateFolderCounters Job
2024-11-28 11:50:10 +03:00
Mahavir Nahata
ecc9ea9ab9 Optimize UpdateFolderCounters Job 2024-11-28 13:38:06 +05:30
FreeScout
b3dbad9a6e Show more detailed errors on fetching - closes #4372 2024-11-27 04:09:16 -08:00
FreeScout
ccdbd265c8 Show more detailed errors on fetching - closes #4372 2024-11-27 01:09:34 -08:00
FreeScout
a0659a8971 Add Precedence:list to the list of no auto-reply send headers - closes #4363 2024-11-24 23:01:19 -08:00
FreeScout
ef4d37f308 Fix text bolding issue - closes #4367 2024-11-24 22:08:36 -08:00
FreeScout
a49fd0ad33 Clear cache manually in update.sh to avoid post-update issues - closes #4366 2024-11-24 02:06:17 -08:00
FreeScout
190d2daece Fix Argument 1 (lastErrors) must be of type array in Carbon 2024-11-23 20:58:26 -08:00
FreeScout
bfbfeb2e66 Add vendor files to dist 2024-11-22 00:37:13 -08:00
FreeScout
a11df91aa4 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-11-22 00:34:55 -08:00
FreeScout
4d51348487 v1.8.158 2024-11-22 00:34:40 -08:00
FreeScout
0e7e1a54f4 Fix an issue with the SPAN tag being added in the editor - closes #4354 2024-11-17 09:35:13 -08:00
FreeScout
b7938df35b Do not check In-Reply-To header when processing @fwd instruction - closes #4348 2024-11-15 02:24:52 -08:00
FreeScout
b622bbb284 Remove Webklex/laravel-imap library 2024-11-15 02:14:50 -08:00
FreeScout
009c51e245 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-11-14 23:36:24 -08:00
FreeScout
21fe296303 v1.8.157 2024-11-14 23:36:13 -08:00
FreeScout
2a0200f46d Fix table wizard in the editor - closes #4345 2024-11-14 00:15:41 -08:00
FreeScout
f5971fb24f Adjust placeholder in translations - closes #4346 2024-11-13 23:39:12 -08:00
FreeScout
76c550ff99 Croatian translation #4346 from shunkica/hr-translation
Croatian translation
2024-11-14 08:23:39 +01:00
shunkica
2d1fec3cb8 Update hr.json 2024-11-13 23:11:37 +01:00
shunkica
097f10ec18 Update croatian translations 2024-11-13 23:06:59 +01:00
FreeScout
b63bad55ac Allow to set APP_FETCHING_BUNCH_SIZE env parameter - closes #4343 2024-11-13 05:26:10 -08:00
FreeScout
94c1c01480 Do not reset Default Redirect settings for users when saving Mailbox Permissions - closes #4339 2024-11-12 05:02:27 -08:00
FreeScout
21053200f0 Add conversations.table_sorting hook - closes #4338 2024-11-12 02:01:40 -08:00
FreeScout
8d65bdf0bf Fix @fwd forwarding when styles are present in the email body - closes #4333 2024-11-11 04:23:28 -08:00
FreeScout
e2e3682ca8 Update readme 2024-11-11 00:55:31 -08:00
FreeScout
fc7f4bfc35 Fix moving emails to a folder on fetching on mail servers not supporting MOVE command - closes #4313 2024-11-07 21:14:55 -08:00
FreeScout
e2360f2146 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-11-04 08:24:58 -08:00
FreeScout
910edcaea2 v1.8.156 2024-11-04 08:24:43 -08:00
FreeScout
ffcce029f5 Fix Undefined variable customer in thread.blade.php - closes #4321 2024-11-04 08:22:41 -08:00
FreeScout
b6987ddb90 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-11-01 21:45:43 -07:00
FreeScout
cbeffb850d v1.8.155 2024-11-01 21:45:35 -07:00
FreeScout
bbcba6e266 Allow to hide from support agents customers from non-visible mailboxes - closes #4316 2024-11-01 10:13:41 -07:00
FreeScout
9a6429ea97 Allow to hide from support agents customers from non-visible mailboxes - closes #4316 2024-11-01 00:35:14 -07:00
FreeScout
52e5ecf905 Update German translation 2024-10-29 03:03:18 -07:00
FreeScout
2912422a31 Fix typo in user profile.blade.php 2024-10-27 10:55:21 -07:00
FreeScout
e17da2b6c0 Update Japanese translation 2024-10-26 07:16:11 -07:00
FreeScout
b276cea24a Fix error in Thread->getFromHeader() 2024-10-26 02:55:53 -07:00
FreeScout
e7ec7e7364 Set CURLOPT_FOLLOWLOCATION in Helper::getRemoteFileContents() 2024-10-26 02:52:51 -07:00
FreeScout
46add74e4f Show From address in the conversation when From and Reply-To headers are different in the email - closes #4287 2024-10-18 01:38:33 -07:00
FreeScout
4e8cd07303 Fix decoding Japanese symbols in the email body in gb2312 encoding - closes #4282 2024-10-16 22:50:08 -07:00
FreeScout
5db8906a86 Add hooks: conversation.convinfo.prepend and conversation.convinfo.before_nav - closes #4279 2024-10-08 22:46:52 -07:00
FreeScout
927afe44c7 Add Delivered-To:Autoresponder to the list of autoresponder headers 2024-10-04 11:07:41 -07:00
FreeScout
30b07a82c7 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-10-04 04:34:21 -07:00
FreeScout
c80ce3d4f8 v1.8.154 2024-10-04 04:34:11 -07:00
FreeScout
980035fecc Improve the email search by Message-Id in Show Original window - closes #4266 2024-10-04 04:31:47 -07:00
FreeScout
6af389655c Check if user is deleted on Permissions and Notifications pages 2024-10-03 00:52:20 -07:00
FreeScout
eb5c8fac12 Create User::deleteUser() method 2024-10-03 00:26:33 -07:00
FreeScout
3f90a9bad3 Improve connection sleep time selection when fetching emails to avoid connection setup failed error - closes #4227 2024-10-02 03:53:10 -07:00
FreeScout
0280e4f30d Fix SMTP authentication not working in some cases due to setAuthMode() used in Swiftmailer - closes #4250 2024-10-02 03:34:58 -07:00
FreeScout
b92e502fb0 Fix null given error in getMessageNumber() - closes #4257 2024-09-27 07:00:31 -07:00
FreeScout
8cb8a180b7 Fix null given error in getMessageNumber() - closes #4257 2024-09-27 06:58:39 -07:00
FreeScout
5c9ab337ea Add vendor files to dist 2024-09-22 07:01:39 -07:00
FreeScout
f6e18818cd Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-09-22 07:00:04 -07:00
FreeScout
880205fc59 v1.8.153 2024-09-22 06:33:42 -07:00
FreeScout
c073511378 Update German translation 2024-09-22 06:17:35 -07:00
FreeScout
275454d479 Add CSRF check to the Delete all files link in Manage App Logs 2024-09-20 05:57:16 -07:00
FreeScout
a96b31fb99 Avoid process list trancation when checking running commands - closes #4237 2024-09-20 02:58:03 -07:00
FreeScout
a19bfd421c Improve connection sleep time selection when fetching emails to avoid connection setup failed error - closes #4227 2024-09-12 23:53:52 -07:00
FreeScout
fbae84ba3c Improve connection sleep time selection when fetching emails to avoid connection setup failed error - closes #4227 2024-09-12 22:30:16 -07:00
FreeScout
e63904a53b Add X-Autoresponder to the list of auto-responder headers - closes #4226 2024-09-12 00:19:38 -07:00
FreeScout
5a989ec257 Improve @fwd detection when forwarding emails - closes #2940 2024-09-11 10:12:58 -07:00
FreeScout
b90f3d7204 Change /bin/sh to /bin/bash in update.sh - closes #4208 2024-09-03 00:54:17 -07:00
FreeScout
03c234edec Fix an issue with emails being empty when fetching from iCloud - closes #4202 2024-08-29 00:42:12 -07:00
FreeScout
af2b1254e8 Update Polish translation 2024-08-28 23:52:21 -07:00
FreeScout
f218fe17d0 Add vendor files to dist 2024-08-27 23:15:39 -07:00
FreeScout
a2979a4406 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-08-27 23:15:16 -07:00
FreeScout
e4dd3b4ad1 v1.8.152 2024-08-27 23:15:04 -07:00
FreeScout
aa713a3a57 Fix an issue with plain text emails having empty body - closes #4181 2024-08-27 23:13:45 -07:00
FreeScout
adc9921108 Add vendor files to dist 2024-08-27 04:52:34 -07:00
FreeScout
f326b14776 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-08-27 04:52:00 -07:00
FreeScout
afaf1f53ab v1.8.151 2024-08-27 04:50:57 -07:00
FreeScout
23bb50eb86 Fix out of memory on Status page when there are many background jobs - closes #4197 2024-08-27 03:58:19 -07:00
FreeScout
39824c876d Fix Undefined array key 1 error on fetching emails - closes #4195 2024-08-26 22:22:06 -07:00
FreeScout
ca1a7d0900 Fix non-multipart emails contain headers in the bodies - closes #4181 2024-08-24 03:02:23 -07:00
FreeScout
351eff9b80 Fix checking POP3 connection when PHP Request Shutdown: Unexpected characters at end of address error occurs 2024-08-23 22:48:46 -07:00
FreeScout
e037011c55 Added Kazakh language 2024-08-16 09:10:07 -07:00
FreeScout
c0ce07a2b9 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-08-15 20:10:13 -07:00
FreeScout
94f11a9155 v1.8.150 2024-08-15 20:09:47 -07:00
FreeScout
dff234ffbe Allow to disable quotes around SINCE date in IMAP search query via APP_SINCE_WITHOUT_QUOTES_ON_FETCHING env parameter - closes #4175 2024-08-15 20:07:55 -07:00
FreeScout
223b4d12dc Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-08-14 09:51:38 -07:00
FreeScout
b33fb5e1d3 v1.8.149 2024-08-14 09:51:26 -07:00
FreeScout
3511e7acb2 Fix configuring MS365 SMTP via oAuth - closes #4179 2024-08-13 22:23:07 -07:00
FreeScout
be4a1234c3 Improve Webklex/php-imap memory usage 2024-08-13 11:24:45 -07:00
FreeScout
823014d4e9 Call parseRawBody() in Webklex Message if body has not been parsed yet 2024-08-13 11:08:16 -07:00
FreeScout
310d3290fe Do not add quotes around the date in IMAP SINCE query - closes #4175 2024-08-11 03:44:03 -07:00
FreeScout
07765cf8ff Populate variables in custom Mailbox names inside emails sent to customers - closes #4169 2024-08-08 05:00:40 -07:00
FreeScout
c4a6585cc7 Add vendor files to dist 2024-08-08 04:17:10 -07:00
FreeScout
d99d991650 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-08-08 04:16:35 -07:00
FreeScout
6e8682e693 v1.8.148 2024-08-08 04:15:59 -07:00
FreeScout
87eab76574 Set MS365 SMTP credentials automatically only when outlook.office365.com SMTP host is set - closes #4151 2024-08-08 04:00:35 -07:00
FreeScout
a011948149 Make toSql() comparison compatible with PostgreSQL - closes #4167 2024-08-07 03:56:31 -07:00
FreeScout
b9391d87dc Leave other emails unread when an exception happens during fetching of one of them - closes #4159 2024-08-02 23:32:49 -07:00
FreeScout
7e7f0a0d10 Leave other emails unread when an exception happens during fetching of one of them - closes #4159 2024-08-01 23:34:38 -07:00
FreeScout
8e62d70986 Correctly processs invalid dates when fetching emails - closes #4159 2024-08-01 21:22:33 -07:00
FreeScout
02e5fa8535 Correctly process invalid mail Reply-To headers - closes #4158 2024-08-01 21:01:42 -07:00
FreeScout
2dc8d061e1 Prefill SMTP username and password for MS365 if those fields are empty - closes #4151 2024-07-30 22:21:34 -07:00
FreeScout
efca2acbe2 Log Helper::getRemoteFileContents() errors 2024-07-22 09:50:01 -07:00
FreeScout
029e5c8a1f Execute hooks when performing conversations bulk delete to Deleted folder - closes #4141 2024-07-22 06:30:26 -07:00
FreeScout
7383b95864 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-07-21 06:19:34 -07:00
FreeScout
5be99f03f3 v1.8.147 2024-07-21 06:19:27 -07:00
FreeScout
da522d8d58 Fix Disconnect link for MS365 in Fetching settings - closes #4136 2024-07-18 02:30:49 -07:00
FreeScout
60ac03df04 Improve App Key check in tools.php 2024-07-17 09:38:38 -07:00
FreeScout
5a22011fe6 Improve sanitizing Path to PHP in tools.php 2024-07-17 09:37:21 -07:00
FreeScout
09f4908def Fix Call to a member function getFirstName() on null in notification.blade.php - closes #4132 2024-07-16 22:17:20 -07:00
FreeScout
8172db9cdb Improve sanitizing Path to PHP in tools.php 2024-07-16 22:12:57 -07:00
FreeScout
05e7e1e739 Improve sanitizing Path to PHP in tools.php 2024-07-16 07:03:27 -07:00
FreeScout
b2c72775dd Merge pull request #4130 from IchHabeHunger54/master
Add conversation->changeSubject() and conversation.subject_changed action
2024-07-16 13:55:52 +03:00
Neustifter, David
9eb7457097 Add Conversation->changeSubject() and conversation.subject_changed action 2024-07-16 12:47:07 +02:00
FreeScout
0b7d1dc06f Allow padding-left and simiar CSS properties when purifying HTML 2024-07-11 00:53:42 -07:00
FreeScout
2f9d1bf92e Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-07-08 01:44:11 -07:00
FreeScout
de405fe60f v1.8.146 2024-07-08 01:44:01 -07:00
FreeScout
9bb3ee9bc1 Use only forward slashes in attachment URLs - closes #4106 2024-07-04 21:04:52 -07:00
FreeScout
5ab6a42808 Fallback to getPathname() for uploaded files - closes #4105 2024-07-04 21:03:14 -07:00
FreeScout
9b5286efca Fallback to getPathname() for uploaded files - closes #41054105 2024-07-04 20:59:32 -07:00
FreeScout
78e1067c07 Do not duplicate recipient in conversation CC field - closes #4112 2024-07-03 08:24:55 -07:00
FreeScout
c19c4ee217 Fix conversation draft not visible in Drafts after undoing new conversation - closes #4112 2024-07-03 07:55:25 -07:00
FreeScout
5ef26eaf32 Fix error message when can not fetch specific email vai MailHelper::fetchMessage() - closes #4097 2024-07-03 04:37:07 -07:00
FreeScout
16843c9d6a Fix Datatables sort_desc.png not found - closes #4107 2024-07-03 00:45:22 -07:00
FreeScout
a21f5c9d53 Merge branch 'master' of github.com:freescout-helpdesk/freescout 2024-07-02 12:02:26 -07:00
FreeScout
97cea6cdef Fix Undefined array key auth_mode on sending emails - closes #4103 2024-07-02 12:02:09 -07:00
FreeScout
12c6552259 Merge pull request #4099 from gabllux/master
Handle null values on outOauthEnabled() - closes #4094
2024-07-02 20:54:56 +02:00
gabllux
04aaeb4cc1 Handle null values on outOauthEnabled() - closes https://github.com/freescout-help-desk/freescout/issues/4094 2024-06-25 21:33:01 +02:00
FreeScout
7c3e57aa57 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-06-19 11:39:48 -07:00
FreeScout
f90ba3d6d6 v1.8.145 2024-06-19 11:39:29 -07:00
FreeScout
4fe2a9333e Improve SMTP oAuth implementation - closes #4084 2024-06-19 09:47:03 -07:00
FreeScout
bd59fa5ef5 Mention posix_isatty() function in \Helper::checkRequiredFunctions() 2024-06-19 04:24:00 -07:00
FreeScout
989c729834 Add created_at index to threads table - closes #4069 2024-06-18 02:26:10 -07:00
FreeScout
a57a424067 Update readme 2024-06-18 00:46:15 -07:00
FreeScout
755e4f0cd7 Add vendor files to dist 2024-06-17 22:12:38 -07:00
FreeScout
a2a0539316 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-06-17 22:11:43 -07:00
FreeScout
e8975d04d6 v1.8.144 2024-06-17 22:11:34 -07:00
FreeScout
d6eac44275 Show more detailed errors when IMAP authentication fails 2024-06-17 10:54:23 -07:00
FreeScout
ea39e24727 Rename STARTTLS Encryption method in Fetching settings 2024-06-17 06:31:18 -07:00
FreeScout
5a290b3ad3 Allow to enable debug in Status - Tools - Fetching section 2024-06-16 02:21:06 -07:00
FreeScout
ce523c51e7 Show debug log when testing fetching 2024-06-16 02:05:42 -07:00
FreeScout
5bcde678d1 Update repo URL in self-updater 2024-06-15 08:30:44 -07:00
FreeScout
98e74e53f2 Update repo link 2024-06-14 00:36:27 -07:00
FreeScout
bde2da820c Update repo URL in self-updater 2024-06-14 00:06:10 -07:00
FreeScout
4b2dd40d99 Update also inactive modules via freescout:module-update command 2024-06-13 03:22:17 -07:00
FreeScout
4d7f8d1f50 Microsoft Exchange oAuth for SMTP - closes #4072 2024-06-12 22:21:03 -07:00
FreeScout
8510ee8224 Microsoft Exchange oAuth for SMTP - closes #4072 2024-06-12 20:45:58 -07:00
FreeScout
7e8e514e7b Microsoft Exchange oAuth for SMTP - closes #4072 2024-06-12 11:45:08 -07:00
FreeScout
fb3b0d3a49 Update readme 2024-06-11 22:18:50 -07:00
FreeScout
aaacc6d3fa Fix Call to undefined function get_current_user() error on Status page - closes #4067 2024-06-10 22:31:29 -07:00
FreeScout
3c199f697c Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-06-10 10:25:57 -07:00
FreeScout
5c01c3793c v1.8.143 2024-06-10 10:25:45 -07:00
FreeScout
0ff901ef32 Fix Undefined variable $customer in thread.php - closes #4066 2024-06-10 10:24:36 -07:00
FreeScout
08a5d5f6ff Add MailHelper::parseEml() function 2024-06-10 04:42:42 -07:00
FreeScout
1fd00def95 Allow to call \Webklex\PHPIMAP\Message::make() without specifying uid 2024-06-10 01:27:24 -07:00
FreeScout
21a73fa5e6 Add vendor files to dist 2024-06-09 09:37:24 -07:00
FreeScout
c9b2c3cc36 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-06-09 09:36:57 -07:00
FreeScout
4ee876c134 v1.8.142 2024-06-09 09:36:48 -07:00
FreeScout
c1c713ef15 Show From for emails received from customers with multiple emails - closes #4059 2024-06-06 11:52:59 -07:00
FreeScout
b926cd5cf2 Do not show Mailbox is empty error when fetching via POP3 - closes #4060 2024-06-06 11:44:21 -07:00
FreeScout
325343c2cf Fix sending a new conversation to the customer with multiple emails - closes #4057 2024-06-05 04:00:26 -07:00
FreeScout
d9502ff348 Fix error in module:list command - closes #4056 2024-06-05 00:00:37 -07:00
FreeScout
585f8f49a7 Sort modules list in the Deactivate License dialog - closes #4055 2024-06-03 22:45:54 -07:00
FreeScout
505db999ad Fix mb_convert_encoding() invalid encoding error on fetching - closes #4051 2024-06-01 08:55:33 -07:00
FreeScout
d402dd5164 Catch mb_convert_encoding() error on fetching - closes #4051 2024-06-01 08:36:57 -07:00
FreeScout
a93cc2a08f Improve Gmail reply separation on fetching 2024-06-01 03:34:03 -07:00
FreeScout
0ac8685c63 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-05-30 04:55:43 -07:00
FreeScout
754b2fc40c v1.8.141 2024-05-30 04:55:26 -07:00
FreeScout
c32a68ed93 Remove All from current mailbox link from conversation page - closes #4041 2024-05-27 01:30:51 -07:00
FreeScout
c269311ef5 Skip auto-replies sent to the email notification on behalf of a user - #4035 2024-05-27 01:24:35 -07:00
FreeScout
04c28a9637 Disable Check Connection button on changing mailbox Fetching settings 2024-05-22 22:33:24 -07:00
FreeScout
6830a78e23 Show exclamation icon in mailbox connection settings tabs 2024-05-22 22:04:29 -07:00
FreeScout
3a4921344b Show exclamation icon in mailbox connection settings tabs 2024-05-22 22:02:03 -07:00
FreeScout
063b77f3df Fix IMAP folders not being retrieved in some cases 2024-05-22 21:50:26 -07:00
FreeScout
61e5f1fde5 Fix fetching emails forwarded using @fwd command - closes #4036 2024-05-21 11:36:15 -07:00
FreeScout
4f253bfab1 Fix reteiving the original message in Show Original window for moved conversations 2024-05-21 10:33:16 -07:00
FreeScout
c88ca44ff1 Find prev_thead iteratively looking in-reply-to and references #4026 from Som-Energia/master
Find prev_thead iteratively looking in-reply-to and references
2024-05-16 09:01:22 +03:00
FreeScout
2d871f2759 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-05-15 22:57:28 -07:00
FreeScout
2a0bac500c v1.8.140 2024-05-15 22:57:20 -07:00
FreeScout
cedf4de2ba Update TestWebklex command 2024-05-15 22:46:16 -07:00
Àngel Fernández
d9e1a7ae98 Find prev_thead iteratively looking in-reply-to and references 2024-05-15 18:15:31 +02:00
FreeScout
80118d579d Update general_help_request.md 2024-05-15 07:13:47 -07:00
FreeScout
0299c8be99 Switch to the new fetching library 2024-05-10 22:46:33 -07:00
FreeScout
70a7ec54ea Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-05-10 22:15:09 -07:00
FreeScout
4876d37287 v1.8.139 2024-05-10 22:14:53 -07:00
FreeScout
9894ca78f0 app.csp_custom env parameter 2024-05-09 06:34:05 -07:00
FreeScout
99a4b4b4e1 Remove unwanted tags from HTML when editing threads - GHSA-985r-6qfc-hg8m 2024-05-06 00:52:36 -07:00
FreeScout
1169f5bae9 Strip unwanted tags from mailbox Auto Reply 2024-05-06 00:16:53 -07:00
FreeScout
a59619c153 Strip unwanted tags from mailbox signature when saving it 2024-05-06 00:14:00 -07:00
FreeScout
c4932c4c8e Fix CC and BCC fields when cloning a conversation - closes #4011 2024-05-05 09:44:35 -07:00
FreeScout
cb31699a6e Update Polish translation 2024-05-05 00:08:47 -07:00
FreeScout
41e6e10241 Update Polish translation 2024-05-05 00:07:28 -07:00
FreeScout
ce5c85f592 Remove FORM tags from HTML when editing threads - GHSA-985r-6qfc-hg8m 2024-05-04 23:15:05 -07:00
FreeScout
2614514bc6 Do not allow Prototype Pollution via getQueryParam() function - GHSA-rx6j-4c33-9h3r 2024-05-04 22:59:35 -07:00
FreeScout
a50c261f13 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-04-26 06:41:18 -07:00
FreeScout
b474193f97 v1.8.138 2024-04-26 06:41:10 -07:00
FreeScout
27a4f2e8ff Fix cron URL link - closes #4000 2024-04-26 03:53:37 -07:00
FreeScout
17024f814a Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-04-22 09:35:48 -07:00
FreeScout
d18cc23149 v1.8.137 2024-04-22 09:35:36 -07:00
FreeScout
047111ffbb Fix Unknown hashing algorithm crc32c error in Webklex/php-imap library - closes #3991 2024-04-22 06:51:49 -07:00
FreeScout
8c5dbb4197 Fetch emails sent to multiple mailboxes on a per mailbox basis - closes #3941 2024-04-22 01:36:46 -07:00
FreeScout
7a6cbab8af Do not show closed conversations in non-standard folders - closes #3988 2024-04-20 23:17:20 -07:00
FreeScout
add6f179a4 Add folder.only_assigned_condition hook - closes #701 2024-04-20 22:27:13 -07:00
FreeScout
29d8c41395 Fix browser push notifications sent not to all users - closes #3880 2024-04-19 07:31:00 -07:00
FreeScout
0ecfa248ee Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-04-18 23:35:34 -07:00
FreeScout
748c7c5de1 v1.8.136 2024-04-18 23:34:12 -07:00
FreeScout
2b5be510b7 Update Dutch translation 2024-04-16 06:34:24 -07:00
FreeScout
99ef4e6899 Do not run scheduled commands when running \Artisan::call() function - closes #3970 2024-04-15 07:17:25 -07:00
FreeScout
59b86a882e Fix attachments not being shown when re-opening a new conversation draft - closes #3957 2024-04-12 22:12:10 -07:00
FreeScout
a6a729189c Show system date and time on Status page instead of current user time 2024-04-12 22:03:48 -07:00
FreeScout
7829e9a3ba Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-04-12 21:53:28 -07:00
FreeScout
a2e27f3ced v1.8.135 2024-04-12 21:53:13 -07:00
FreeScout
9175ce3646 Fix Parse error in Attachment.php - closes #3965 2024-04-12 21:52:35 -07:00
FreeScout
0dbdf401eb Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-04-12 11:20:10 -07:00
FreeScout
db84078093 v1.8.134 2024-04-12 11:19:57 -07:00
FreeScout
a0ab8069d2 Fix Header::decode() is not supported error in Webklex/php-imap - closes #3960 2024-04-12 11:18:45 -07:00
FreeScout
ea068dc23e Update Readme 2024-04-11 23:33:16 -07:00
FreeScout
26756cc687 Add vendor files to dist 2024-04-11 23:17:54 -07:00
FreeScout
171a4e3b48 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-04-11 23:17:21 -07:00
FreeScout
bfd5aef4d9 v1.8.133 2024-04-11 23:17:11 -07:00
FreeScout
b3c2a48eec Fix an error when new conversation draft is being discarded by other user - closes #3951 2024-04-11 23:15:26 -07:00
FreeScout
848cc9e43f Fix To field not appearing after discarding forwarding draft - closes #3950 2024-04-10 23:10:27 -07:00
FreeScout
f61230ff35 Update Polish translation 2024-04-10 22:54:15 -07:00
FreeScout
268740a142 Fix an issue with removing attachments when replying 2024-04-10 06:40:48 -07:00
FreeScout
abf8d32ff5 When forwarding a conversation do not include attachments from drafts 2024-04-10 06:39:15 -07:00
FreeScout
b769aec7e8 Fix attachments not being sent when forwarding a conversation 2024-04-10 06:29:10 -07:00
FreeScout
84832f011a Fix attachments being duplicated when discarding forwarding draft - closes #3949 2024-04-10 06:28:34 -07:00
FreeScout
b1dc766ffb Make updates in Webklex/php-imap library 2024-04-10 05:35:06 -07:00
FreeScout
3274b35468 Do not mark thread as containing attachments when only embedded images are present in the email 2024-04-09 07:06:47 -07:00
FreeScout
ad76e2d3ce Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-04-09 03:19:17 -07:00
FreeScout
aec78d147a v1.8.132 2024-04-09 03:19:03 -07:00
FreeScout
9333111a99 Fix some emails shown incorrectly - closes #3907 2024-04-08 11:21:52 -07:00
FreeScout
1f0d192fd5 Fix Call to a member function getFullName() on null error in notification template - closes #3942 2024-04-07 09:27:04 -07:00
FreeScout
21f04259cb Fix Call to a member function getFullName() on null error in notification template - closes #3942 2024-04-07 09:10:29 -07:00
FreeScout
a62eb20f26 Update ru.json 2024-04-06 23:16:01 -07:00
FreeScout
b813a4af9d Update ru.json #3945 from ViktorOn/master
Update ru.json
2024-04-07 08:51:13 +03:00
Vik
09ca411903 Update ru.json
Lots of edits
2024-04-07 00:37:14 +03:00
FreeScout
59c256f8cc Fix duplicated folder names when getting IMAP folders - closes #3937 2024-04-05 07:15:56 -07:00
FreeScout
c0e2a8a6fb Fix Passing null error in symfony/http-foundation/Request.php - closes #3940 2024-04-05 04:23:26 -07:00
FreeScout
c15278d3c6 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-04-04 22:21:07 -07:00
FreeScout
7cf3c67432 v1.8.131 2024-04-04 22:20:55 -07:00
FreeScout
1ef3796709 Fix attachment IDs when checking max message size - closes #3939 2024-04-04 11:24:33 -07:00
FreeScout
d3527f2aee Allow limiting command freescout:fetch-emails to specific mailboxes #3932 from ohader/fetch-emails-mailbox
Allow limiting command freescout:fetch-emails to specific mailboxes
2024-04-03 21:10:58 +03:00
Oliver Hader
31b3dff98e Allow limiting command freescout:fetch-emails to specific mailboxes
This change introduces an optional command option `mailboxes`, which
can be used to limit fetching messages to particular mailbox ids.

The following example fetches all messages of the last 500 days
for the specific mailboxes 5 and 7 only. By omitting the new
`mailboxes` option, any message of any mailbox would have been fetched.

```
php artisan freescout:fetch-emails --mailboxes=5,7 --unseen=0 --days=500
```
2024-04-03 20:06:51 +02:00
FreeScout
1b9cf02f1a Fix saving into Sent IMAP folder message with attachments - closes #3934 2024-04-03 10:06:38 -07:00
FreeScout
a9963c2f43 Fix saving into Sent IMAP folder message with attachments - closes #3934 2024-04-03 01:19:22 -07:00
FreeScout
b9bdf92065 Update Polish translation 2024-04-01 23:05:26 -07:00
FreeScout
b66b8f1880 Fix To and From headers when saving outgoing emails to the Sent IMAP folder - closes #3900 2024-04-01 23:04:01 -07:00
FreeScout
356b211754 Allow fetching seen/read messages via scheduler #3931 from ohader/schedule-fetch-unseen
Allow fetching seen/read messages via scheduler
2024-04-02 08:48:26 +03:00
FreeScout
833389c1fc Fix retrieving IMAP subfolders - closes #3933 2024-04-01 22:46:13 -07:00
Oliver Hader
f5e30cdff3 Allow fetching seen/read messages via scheduler
Usually the `schedule:run` command invokes `freescout:fetch-emails`,
but only fetches unseen (not read by anybody else, yet). This change
allows to configure this behavior and to fetch seen/read messages.

The new option `app.fetch_unseen` can be configured via `.env`:

```
APP_FETCH_UNSEEN=0
```
2024-04-01 14:54:58 +02:00
FreeScout
50f6cd743a Make params optional in Helper::setGuzzleDefaultOptions() #3926 from Lars-/master
Make params optional in Helper::setGuzzleDefaultOptions()
2024-03-30 08:10:49 +03:00
Lars Jansen
c4c6e861b5 fix: optional params in setGuzzleDefaultOptions 2024-03-30 06:02:34 +01:00
FreeScout
cac293b75b Use encrypted attachment IDs when uploading attachments 2024-03-29 09:37:56 -07:00
FreeScout
2c39ba9d9e Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-03-29 07:36:35 -07:00
FreeScout
a1602fa57a v1.8.130 2024-03-29 07:36:25 -07:00
FreeScout
f6f1adc9f4 Use encrypted attachment IDs when uploading attachments 2024-03-29 07:35:38 -07:00
FreeScout
9fbbba817e Fix conv_type_custom error - closes #3923 2024-03-29 07:34:49 -07:00
FreeScout
d973a8a2ca Update Japanese translation 2024-03-29 06:41:48 -07:00
FreeScout
70cc044e57 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-03-29 06:36:58 -07:00
FreeScout
f7498e5ba2 v1.8.129 2024-03-29 06:36:06 -07:00
FreeScout
e103599984 Helper encrypt and decypt with password functions 2024-03-29 06:34:40 -07:00
FreeScout
40abe0b311 Update Japanese translation 2024-03-28 23:02:19 -07:00
FreeScout
8b9e514ce9 Add Custom conversation type #3918 from Lars-/master
Add Custom conversation type
2024-03-28 11:26:34 +03:00
Lars Jansen
582f3a6ae4 feat: new conversation type: custom 2024-03-28 09:08:41 +01:00
Lars Jansen
5589c0a80f Merge remote-tracking branch 'Github/master' 2024-03-28 09:08:10 +01:00
FreeScout
71c67ad637 Re-sort strings in translation files 2024-03-26 23:07:15 -07:00
FreeScout
1c7aff0d5b Fix deleting conversations after sorting them - closes #3901 2024-03-25 23:50:32 -07:00
FreeScout
9b359abcce Add sender name to From field when saving email to the Sent folder - closes #3900 2024-03-25 00:24:28 -07:00
FreeScout
67de1109fe Add vendor files to dist 2024-03-22 00:11:37 -07:00
FreeScout
3489567cde Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-03-22 00:11:04 -07:00
FreeScout
d69e6c3501 v1.8.128 2024-03-22 00:10:55 -07:00
FreeScout
01dec5160e Exclude duplicate INBOX. folders when retreiving folders 2024-03-21 23:40:54 -07:00
FreeScout
90c80c9946 Fix removing comments when purifying HTML - closes #3894 2024-03-21 02:48:15 -07:00
FreeScout
0b9ca610b7 Fix Passing null to parameter error in HTMLPurifier - closes #3893 2024-03-20 23:07:07 -07:00
FreeScout
2a2782bc95 Fix conversations bulk actions when page parameter is set in the URL 2024-03-19 23:18:02 -07:00
FreeScout
3b6b02ed19 Fix conversation bulk actions after sorting conversations - closes #3884 2024-03-19 23:08:38 -07:00
FreeScout
443ae7f40b Fix long fetch command being killed - closes #3844 2024-03-19 06:45:05 -07:00
FreeScout
4c91ac44e1 Sanitize Path to PHP in tools.php 2024-03-19 04:47:43 -07:00
FreeScout
42dc98fa45 Strip script tags from mailbox signature 2024-03-19 03:15:37 -07:00
FreeScout
e88bec86fb Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-03-18 00:25:56 -07:00
FreeScout
20c8ef7383 v1.8.127 2024-03-18 00:25:48 -07:00
FreeScout
ff09db6244 Upgrade DataTables to 1.13.11 2024-03-18 00:11:45 -07:00
FreeScout
c15a00d603 Properly process situation when an attachment can not be saved to the disc - closes #3871 2024-03-13 07:14:24 -07:00
FreeScout
b18cf31bdc Show extended editor toolbar when editing threads - closes #3866 2024-03-12 00:37:51 -07:00
FreeScout
3ee1a96bde Allow <center> element when purifying thread content 2024-03-12 00:27:39 -07:00
FreeScout
8c470d0e4e Remove [if mso] from thread content when purifying 2024-03-12 00:26:43 -07:00
FreeScout
0826102f6e Preserve elements inside [if mso] when purifying thread content 2024-03-12 00:13:27 -07:00
FreeScout
2ff52a0359 Use latest PHP version for linting #3864 from fulldecent/patch-15
Use latest PHP version for linting
2024-03-12 09:54:41 +03:00
William Entriken
8d70999498 Use latest PHP version for linting 2024-03-10 13:25:44 -04:00
FreeScout
f6a4581dad Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-03-08 06:49:51 -08:00
FreeScout
2d74977ed3 v1.8.126 2024-03-08 06:49:39 -08:00
FreeScout
8b06724b67 Fix an issue with updating modules 2024-03-08 06:48:51 -08:00
FreeScout
c4755fa920 Disable Undo reply feature for chat conversations in Chat Mode - closes #3856 2024-03-08 06:17:44 -08:00
FreeScout
36f146a71d Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-03-08 01:17:16 -08:00
FreeScout
a5515d8520 v1.8.125 2024-03-08 01:16:49 -08:00
FreeScout
33514c6e25 Fixed an issue with opening Closed folder - closes #3858 2024-03-08 01:16:14 -08:00
FreeScout
d8a582aaa5 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-03-08 00:23:41 -08:00
FreeScout
a428d33964 v1.8.124 2024-03-08 00:23:25 -08:00
FreeScout
33639a8955 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-03-08 00:09:52 -08:00
FreeScout
4021f07aec v1.8.123 2024-03-08 00:09:43 -08:00
FreeScout
67bd456b63 Update Readme 2024-03-07 22:08:26 -08:00
FreeScout
78cc5877a3 Update readme 2024-03-07 10:43:17 -08:00
FreeScout
c4bc505e03 Accessibility of exact date in conversation table - closes #3829 2024-03-07 10:41:43 -08:00
FreeScout
ad372590a9 Allowing updates for third party modules #3851 from Lars-/master
Allowing updates for third party modules
2024-03-07 13:35:03 +03:00
Lars Jansen
2771d526be chore: added comments 2024-03-07 11:24:04 +01:00
Lars Jansen
263ec4d266 fix: code styling 2024-03-07 11:03:46 +01:00
FreeScout
f86876328e Accessibility of exact date in conversation table - closes #3829 2024-03-06 21:55:33 -08:00
Lars Jansen
e47167b17e feat: allow updates for external modules 2024-03-05 20:46:43 +01:00
Lars Jansen
5c32cf69e6 fix: typo 2024-03-05 15:39:31 +01:00
Lars Jansen
fd46017c45 feat: add author to module card if available 2024-03-05 15:39:26 +01:00
FreeScout
95d3cca660 Do not save SMTP username and password in Send Log status message 2024-03-05 06:39:24 -08:00
FreeScout
1fcb5560f0 Fix Passing null to parameter error in Helper::stripTags() - closes #3849 2024-03-05 01:30:31 -08:00
FreeScout
06b3dc27d2 Fix creating conversations by a user seeing only assigned conversations - closes #3843 2024-03-04 05:36:57 -08:00
FreeScout
d2b683368c If user can see only assigned conversations send him notifications only for assigned to him conversations - closes #3843 2024-03-02 21:32:19 -08:00
FreeScout
6b397ccd3f Fix creating conversations by a user seeing only assigned conversations - closes #3843 2024-03-02 21:09:50 -08:00
FreeScout
32006fe8e1 Fix adding notes to Spam conversations 2024-03-02 04:58:52 -08:00
FreeScout
911e157322 Fix showing only assigned to the user tickets - closes #3843 2024-03-02 04:44:23 -08:00
FreeScout
100b4062bf Fix Numeric value out of range error in search on PostgreSQL - closes #3831 2024-02-27 23:04:44 -08:00
FreeScout
b27e2b7a1c Update Czech translation 2024-02-24 06:50:52 -08:00
FreeScout
7085009667 Update Spanish translation 2024-02-24 06:24:34 -08:00
FreeScout
4b6ceab070 Fix word plus colon mistakenly converted to URL in editor - closes #3822 2024-02-24 03:54:38 -08:00
FreeScout
49af472617 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-02-23 06:52:13 -08:00
FreeScout
d7aeb0d3b3 v1.8.122 2024-02-23 06:52:03 -08:00
FreeScout
35f8b775f9 Fix an issue with minify.config.php - closes #3818 2024-02-23 06:49:37 -08:00
FreeScout
079df0d6a6 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-02-23 03:26:42 -08:00
FreeScout
36442cb3a2 v1.8.121 2024-02-23 03:26:32 -08:00
FreeScout
217cd9183e Add Turkish language 2024-02-20 03:12:36 -08:00
FreeScout
e9c2eaebce Fix showing scaled image in threads - closes #3798 2024-02-20 03:00:48 -08:00
FreeScout
954c790827 Update it.json #3755 from PeopleInside/patch-1
Update it.json
2024-02-20 11:53:55 +03:00
FreeScout
2d2b476688 Remove APP_URL from minify.config.php to avoid CORS issues on custom domains 2024-02-20 00:21:20 -08:00
FreeScout
ab58217cab Improve sorting elements on mobile devices - closes #3800 2024-02-18 22:29:13 -08:00
FreeScout
6f0b05c1f6 Improve searching customers by phone number when creating Phone conversations - closes #3786 2024-02-18 08:54:17 -08:00
FreeScout
58d35098c0 Allow to set session.same_site via SESSION_SAME_SITE env parameter - closes #3023 2024-02-18 05:54:38 -08:00
FreeScout
44e71e58e6 Remove 255 symbols limit from mailbox Alises 2024-02-18 05:36:30 -08:00
FreeScout
0e516bc407 Allow style attribute for tr in HTML purifier - closes #3799 2024-02-18 05:28:06 -08:00
Marco Borla
9fc7b9d755 Update it.json
Fix Soggetto is Oggetto.

Subject = Oggetto
2024-02-02 10:46:40 +01:00
FreeScout
e1ff8fb520 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-02-01 17:35:42 -08:00
FreeScout
adf308ac9f v1.8.120 2024-02-01 17:35:31 -08:00
FreeScout
7b6abe858c Fix an issue with multiple emails being sent to customers - closes #3632 2024-01-31 23:17:48 -08:00
FreeScout
6937346e5a Show .parsley-required in red color 2024-01-31 23:10:08 -08:00
FreeScout
fc1a354072 Update Italian translation #3739 from floatingpurr/fix-transalation
✨ Update Italian translation
2024-01-26 19:41:15 +03:00
Andrea Mignone
82464e4129 ✨ Modify Italian translation 2024-01-26 15:29:04 +01:00
FreeScout
1a126ad743 Add parameter to open tracking URL 2024-01-26 06:01:43 -08:00
FreeScout
9558194bf7 Fix redeclaration of let DragDropTouch on Translate page - closes #3717 2024-01-26 00:51:00 -08:00
FreeScout
8aa2f0c018 Improve contrast on on unread notifications 2024-01-24 22:26:11 -08:00
FreeScout
d6de310363 Improve contrast on on unread notifications #3728 from mokkin/patch-1
Improve contrast on on unread notifications
2024-01-25 09:17:59 +03:00
mokkin
1fdddb25ec improve contrast on on unread notifications
fixes https://github.com/freescout-helpdesk/freescout/issues/3693
2024-01-23 17:15:15 +01:00
FreeScout
a78f6f5ce3 Add vendor files to dist 2024-01-23 02:01:04 -08:00
FreeScout
2528727d81 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-01-23 02:00:40 -08:00
FreeScout
97bb4dd956 v1.8.119 2024-01-23 02:00:23 -08:00
FreeScout
3c6819c9e7 Fix scrolling conversations list on mobile devices - closes #3724 2024-01-23 01:59:33 -08:00
FreeScout
bb2fc15c07 Fix error in HTMLPurifier/Length.php - closes #3721 2024-01-23 00:45:39 -08:00
FreeScout
fb129df215 Fix Bootstrap submenu after applying DragDropTouch Polyfill - closes #3708 2024-01-22 01:05:29 -08:00
FreeScout
050ccb921e Fix Trailing data error on PostgreSQL - closes #3702 2024-01-20 22:05:39 -08:00
FreeScout
9b9aae4c0e Add vendor files to dist 2024-01-19 23:40:40 -08:00
FreeScout
263569b163 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-01-19 23:40:11 -08:00
FreeScout
2702232eea v1.8.118 2024-01-19 23:40:02 -08:00
FreeScout
8f802b4d38 Update install.sh 2024-01-19 23:38:24 -08:00
FreeScout
00b4aabb32 Update Slovak translation #3692 from jozefrebjak/sk-lang-update
Update Slovak translation
2024-01-20 10:31:24 +03:00
Jozef Rebjak
8ea84dca94 lang: update slovak translation 2024-01-16 15:02:22 +01:00
FreeScout
bc45b779ef Fix Use of undefined constant FT_UID in Webklex/laravel-imap - closes #2604 2024-01-15 19:06:51 -08:00
FreeScout
c72286623f Fix Use of undefined constant FT_UID in Webklex/laravel-imap - closes #2604 2024-01-11 21:56:49 -08:00
FreeScout
1d3c97fae5 Do not show Delete button in bulk actions if user does not have corresponding permission - closes #3680 2024-01-11 10:35:19 -08:00
FreeScout
17e22b8b93 Improve HTML purification - closes #3664 2024-01-11 10:24:46 -08:00
FreeScout
e32714ebaf Make html5sortable.js work on mobile devices - closes #3671 2024-01-09 23:33:53 -08:00
FreeScout
acd81548e6 Make summernote compatible with jQuery v3.6.0 - closes #3666 2024-01-08 09:23:07 -08:00
FreeScout
2d71860fa1 Fix counter not aligned in Dashboard - closes #3665 2024-01-08 04:16:19 -08:00
FreeScout
6930924cc1 Update German translation 2024-01-06 21:02:28 -08:00
FreeScout
e5ce253daf Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-01-06 04:08:07 -08:00
FreeScout
b9cfe0e300 v1.8.117 2024-01-06 04:07:56 -08:00
FreeScout
af91c8c9e3 Fix Call to undefined method getHashedRelySeparator() 2024-01-06 04:06:14 -08:00
FreeScout
08e50e3643 Localize calendar 2024-01-06 03:55:51 -08:00
FreeScout
885001ec97 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-01-05 20:47:49 -08:00
FreeScout
b2dae09cf1 Improve fetching email in Show Original window - closes #3658 2024-01-05 10:08:21 -08:00
FreeScout
c55d94d969 Add alternative reply separation method - closes #3648 2024-01-05 08:35:15 -08:00
FreeScout
7d035f5604 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2024-01-05 08:20:36 -08:00
FreeScout
a5f49080a7 v1.8.116 2024-01-05 08:20:21 -08:00
FreeScout
8a52a85ca3 Replace shell_exec() with Helper::shellExec() 2024-01-05 03:28:30 -08:00
FreeScout
2bfdc99f89 Add alternative reply separation method - closes #3648 2024-01-04 02:03:36 -08:00
FreeScout
e5da621736 Copy attachments when creating a new conversation from some message - closes #3647 2024-01-03 23:57:07 -08:00
FreeScout
359f6c455c Add alternative reply separation method - closes #3648 2024-01-03 22:22:59 -08:00
FreeScout
e929af5c99 Upgrade Bootstrap to v3.4.1 2024-01-03 03:17:36 -08:00
FreeScout
191e61400d Upgrade jQuery to v3.6.0 2024-01-03 03:15:52 -08:00
FreeScout
d311b40faa Allow to filter conversation by Assignee - closes #3641 2024-01-02 03:31:34 -08:00
FreeScout
7c7fc41b2c Add America/Santiago timezone - closes #3639 2023-12-31 07:48:11 -08:00
FreeScout
5ab0f84d41 Refine the Italian translation #3634 from floatingpurr/fix-translation
Refine the Italian translation
2023-12-27 20:44:16 +03:00
Andrea Mignone
5917b190eb Refine and make consistent Italian translation 2023-12-27 18:40:18 +01:00
FreeScout
828c7b814c Show SMTP communication details when testing mail sending - closes #3626 2023-12-27 02:12:06 -08:00
FreeScout
6d3f1bbee8 Show SMTP communication details when testing mail sending - closes #3626 2023-12-27 01:04:03 -08:00
FreeScout
85954ab7cc Add vendor files to dist 2023-12-26 21:49:22 -08:00
FreeScout
822b2f4ae7 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-12-26 21:48:38 -08:00
FreeScout
ed6911a8dc v1.8.115 2023-12-26 21:30:05 -08:00
FreeScout
d8cfe15373 Improve linkify algorithm 2023-12-23 01:50:22 -08:00
FreeScout
dc4d6d2671 Add StartTLS encryption option for Fetching Email when Webklex/php-imap is used - closes #3624 2023-12-22 03:27:03 -08:00
FreeScout
eb6b30864f Improve the message shown when IMAP folder can not be found during fetching 2023-12-22 00:25:26 -08:00
FreeScout
1618bb1266 Allow to fetch emails from nested folders - closes #3623 2023-12-21 23:11:39 -08:00
FreeScout
54e5aa28bd Do not set customer last name when it was manually removed - closes #3622 2023-12-21 00:55:22 -08:00
FreeScout
4e72e2ea50 Fix null error in locale_options template - closes #3620 2023-12-20 03:41:08 -08:00
FreeScout
134608625f Fix Packets out of order error - closes #3619 2023-12-20 03:34:55 -08:00
FreeScout
11fbaa6be8 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-12-19 21:59:34 -08:00
FreeScout
377dd08c96 v1.8.114 2023-12-19 21:59:03 -08:00
FreeScout
6a9dfe6dd1 Fix email send loop - closes #3607 2023-12-19 21:30:02 -08:00
FreeScout
59da4e4f99 Fix email send loop - closes #3607 2023-12-19 09:37:58 -08:00
FreeScout
6d917f82d7 Add as CC a customer who was in CC and replied to the conversation - #3613 2023-12-16 21:15:02 -08:00
FreeScout
15780467eb Do not strip HTML tags in plain text emails - closes #3614 2023-12-16 04:09:15 -08:00
FreeScout
1a53871517 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-12-14 23:58:28 -08:00
FreeScout
ef0af3a494 v1.8.113 2023-12-14 23:58:15 -08:00
FreeScout
ab854befd1 Helper::sanitizeDatepickerDatetime() function 2023-12-14 21:58:12 -08:00
FreeScout
3d522c4f2e Show notice on System Status page is CloudFlare is used 2023-12-13 09:32:18 -08:00
FreeScout
7908da3721 Allow to translate English texts - closes #3599 2023-12-13 06:00:12 -08:00
FreeScout
f9a3354741 Fix user reply to a notification being incorrectly cropped - closes #3580 2023-12-12 09:57:03 -08:00
FreeScout
fbf4d3b355 Improve SendReplyToCustomer background jobs monitoring - closes #3590 2023-12-12 03:43:31 -08:00
FreeScout
f1839bcfbf Fix user reply to a notification being incorrectly cropped - closes #3580 2023-12-11 10:23:17 -08:00
FreeScout
895af2d7ad Make sure thread_id is null for an attachment when linking attachment to a thread 2023-12-11 06:09:38 -08:00
FreeScout
b5510da291 Do not remove double dots from attachment names - closes #3592 2023-12-09 10:40:25 -08:00
FreeScout
3083738d00 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-12-08 02:45:34 -08:00
FreeScout
ea84fe82dd v1.8.112 2023-12-08 02:45:08 -08:00
FreeScout
ea8b7507e4 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-12-07 23:35:26 -08:00
FreeScout
7dfd170983 Allow .PEM insertion for MySQL connections #3582 from crixle/master
Allow .PEM insertion for MySQL connections
2023-12-06 11:53:58 +03:00
Crixle
06c7e709e6 Allow .PEM insertion for MySQL connections
Required for PaaS DB servers that require SSL.
2023-12-05 17:22:06 -05:00
FreeScout
e0f10670d7 Improve linkify algorithm - closes #3402 2023-12-03 02:27:03 -08:00
FreeScout
b33d2dff4a Fix Call to undefined function shell_exec() on Status page - closes #3570 2023-12-02 21:32:27 -08:00
FreeScout
2f1b9ebe0c conversation.is_user_assignee hook - closes #3568 2023-12-02 04:18:52 -08:00
FreeScout
9b024bda31 Do not change customer when CCed email replies to a conversation - closes #3551 2023-12-02 00:46:47 -08:00
FreeScout
606dcf17c9 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-12-01 21:20:17 -08:00
FreeScout
2fb0c29705 v1.8.110 2023-12-01 21:20:07 -08:00
FreeScout
d673a7006b Fix Select None button - closes #3546 2023-11-24 21:11:49 -08:00
FreeScout
fbe3a795df Merge multiple conversations at once - closes #3516 2023-11-23 23:04:51 -08:00
FreeScout
0c614c3f7e Make error message in logs more descriptive when SendReplyToCustomer job can not save outgoing reply to the IMAP folder 2023-11-22 23:52:36 -08:00
FreeScout
ce4bebb8b4 Autosave new conversation draft when choosing recipient - closes #3538 2023-11-22 21:23:15 -08:00
FreeScout
a8b4a581e4 Update Readme 2023-11-21 23:21:39 -08:00
FreeScout
7d5cc80898 Update readme 2023-11-21 23:18:19 -08:00
FreeScout
7fa6003156 Update Portuguese translation 2023-11-21 21:47:51 -08:00
FreeScout
87cdff17b8 Improve linkify algorithm - closes #3402 2023-11-19 23:06:52 -08:00
FreeScout
4fbe3a3340 Allow to select multiple conversations using SHIFT button - closes #1312 2023-11-19 22:02:36 -08:00
FreeScout
c2a8894d9f Dynamically load customer info when creating new conversation - closes #2402 2023-11-18 04:02:19 -08:00
FreeScout
4bb7e224d7 Show customer info when opening new conversation draft - closes #2402 2023-11-18 01:02:00 -08:00
FreeScout
c24efff92f Check pcntl_signal() function on System Status page in the console PHP 2023-11-17 22:28:01 -08:00
FreeScout
9dc73c29ff Set min-height for .container 2023-11-17 22:16:21 -08:00
FreeScout
c2d599b546 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-11-17 21:20:09 -08:00
FreeScout
574d1d1d69 v1.8.109 2023-11-17 21:19:57 -08:00
FreeScout
f3ff95cc57 Allow to set SMTP timeout via MAIL_SMTP_TIMEOUT .env parameter 2023-11-17 05:30:47 -08:00
FreeScout
4a2f4fdadc Allow to set SMTP timeout via MAIL_TIMEOUT .env parameter 2023-11-16 21:56:58 -08:00
FreeScout
c2bd39a65f Mark email as read when saving to IMAP Folder with Webklex/PHPIMAP - closes #3533 2023-11-16 10:54:52 -08:00
FreeScout
529b141f1b Fix URLs for attachments with percents - closes #3530 2023-11-15 21:16:46 -08:00
FreeScout
de643ee51b Remove max length limitation from mailbox Aliases field - closes #3525 2023-11-13 22:01:59 -08:00
FreeScout
1dd59bc2b6 Fix displaying SVG images in the uploaded folder on Apache - closes #3522 2023-11-12 22:45:55 -08:00
FreeScout
8b80bd4119 Allow to set table prefix via .env DB_TABLE_PREFIX parameter - closes #3523 2023-11-12 22:23:13 -08:00
FreeScout
0ca0ea0e93 Check pcntl_signal() function on System Status page - closes #3515 2023-11-12 22:18:54 -08:00
FreeScout
4f15b2f273 Allow to retry thread sending from conversation - closes #3517 2023-11-09 05:31:47 -08:00
FreeScout
63e2384907 PHP IMAP: Only explode the tag line if it contains a space - closes #3503 2023-11-08 09:21:41 -08:00
FreeScout
e9b66ff7fd Allow to pass custom message to \Helper::denyAccess() 2023-11-08 09:21:15 -08:00
FreeScout
0eff48267f Fix cicle between two tickets with older newer buttons - closes #3506 2023-11-07 22:04:53 -08:00
FreeScout
4c9f247cf6 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-11-06 23:50:12 -08:00
FreeScout
5aa7552232 v1.8.108 2023-11-06 23:49:59 -08:00
FreeScout
0dd1a7308a Fixed saving outgoing replies to the IMAP Sent folder with non-ASCII name 2023-11-06 23:48:51 -08:00
FreeScout
315aeafd24 Fix fetching emails from non-ascii imap folders - closes #3502 2023-11-06 23:33:47 -08:00
FreeScout
74fd2dcf5e Fix Passing null to parameter error in User.php - closes #3501 2023-11-06 05:20:33 -08:00
FreeScout
da66ab2fc6 Add html5sortable.js script 2023-11-04 21:49:58 -07:00
FreeScout
4835fba6dc Fix an issue with phone customers always shown in the search - closes #3499 2023-11-04 05:02:48 -07:00
FreeScout
c28b663f56 Merge branch 'master' of github.com:freescout-helpdesk/freescout into dist 2023-11-03 23:48:43 -07:00
FreeScout
cc2ece53ee v1.8.107 2023-11-03 23:48:35 -07:00
FreeScout
ccc23e6e52 Update install.sh 2023-11-03 23:47:50 -07:00
FreeScout
cee5c16aa1 Adjust installer styles 2023-11-03 23:34:05 -07:00
FreeScout
84db98515d Make install.sh script compatible with Debian 2023-11-03 23:33:48 -07:00
FreeScout
4a24df3395 Fix installing helpdesk without HTTPS via install.sh script 2023-11-03 23:33:10 -07:00
FreeScout
f21054f17f Helper::sqlSanitizeString() function 2023-11-03 03:13:10 -07:00
FreeScout
e12824008f Update general_help_request.md 2023-11-01 10:45:00 -07:00
FreeScout
0c24e04b8c Improve font color sanitizing when purifying thread content - closes #3492 2023-11-01 10:23:33 -07:00
FreeScout
e66aae1209 Set max length for attribtues before saving a user to avoid String data right truncated error on PostgreSQL - closes #3489 2023-11-01 06:07:03 -07:00
FreeScout
e652a1eced Set system timezone by default for users created outside of web interface - closes #3490 2023-10-31 23:20:44 -07:00
FreeScout
bbfbd43105 Fix incorrect redirect when replying to the last active conversation - closes #3486 2023-10-31 07:10:05 -07:00
FreeScout
62bc9643fd .link-grey-blue style 2023-10-31 07:08:24 -07:00
FreeScout
a36bbb2ffa Update Polish translation 2023-10-31 00:31:28 -07:00
FreeScout
c58e2403ca Add dashboard.heading_append hook 2023-10-28 22:07:23 -07:00
FreeScout
926327156d Dynamically expand Logs Monitoring settings in Alerts 2023-10-27 22:19:32 -07:00
449 changed files with 26836 additions and 35022 deletions

View File

@@ -3,18 +3,23 @@ name: General Help Request
about: Create a general help request
---
<!--
* If you have some error the first thing you should do is to check "Manage » Logs » App Logs"! If there are any errors there - make sure to provide them in the issue.
* Have you read FAQ? https://github.com/freescout-helpdesk/freescout/wiki/FAQ
* If you have "Whoops something went wrong" error see: https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide#11-troubleshooting
* Before creating an issue try to disable all non-official modules and remove all custom code from your FreeScout and see if the issue still persists.
* Feature requests for modules should be posted here: https://freescout.net/request-feature/
* Docker questions should go here: https://github.com/tiredofit/docker-freescout/issues
* Create separate issues for different topics - no need to mix all your problems into one issue.
* Questions on issues with payments for modules should be sent by email: https://freescout.net/contact-us/
* Don't forget that you are not the project manager. Try to be humble, polite, friendly and positive. Otherwise your account may be banned and it can't be undone.
* Mentioning how much you've spent on modules or donated is the way to be banned.
* Don't forget that you are not the project manager. Try to be humble, polite, friendly and positive. Otherwise your account may be banned (and it can't be undone).
* Mentioning how much you've spent on modules is also the way to be banned.
Still here? Well clean this out and go ahead :)
-->
PHP version:
FreeScout version:
FreeScout version:
Database: MySQL / PostgreSQL
Are you using CloudFlare: Yes / No
Are you using non-official modules: Yes / No

View File

@@ -13,7 +13,7 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.1
php-version: 8.3
tools: phpcs
- name: Run check

View File

@@ -32,7 +32,7 @@ jobs:
strategy:
matrix:
php: ['7.3', '7.4', '8.0', '8.1', '8.2']
php: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4']
steps:
- uses: actions/checkout@v3
@@ -54,6 +54,7 @@ jobs:
DB_PORT: ${{ job.services.postgres.ports[5432] }}
- name: Run PHP tests
if: ${{ matrix.php < 8.4 }}
run: php${{ matrix.php }} ./vendor/bin/phpunit
env:
DB_PORT: ${{ job.services.postgres.ports[5432] }}

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['7.3', '7.4', '8.0', '8.1', '8.2']
php: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4']
steps:
- uses: actions/checkout@v3
@@ -42,4 +42,5 @@ jobs:
php${{ matrix.php }} artisan db:seed --force -n --database=testing
- name: Run PHP tests
if: ${{ matrix.php < 8.4 }}
run: php${{ matrix.php }} ./vendor/bin/phpunit

3
.gitignore vendored
View File

@@ -33,4 +33,5 @@ Thumbs.db
/tools
.well-known
/resources/lang/module.*
.phpunit.result.cache
.phpunit.result.cache
/.phpunit.cache

View File

@@ -2,10 +2,10 @@
<div align="center">
<img src="https://raw.githubusercontent.com/freescout-helpdesk/freescout/master/public/img/logo-300.png" width="180" height="180" />
<img src="https://raw.githubusercontent.com/freescout-help-desk/freescout/master/public/img/logo-300.png" width="180" height="180" />
<br/><br/>
[![PHP version](https://freescout-helpdesk.github.io/img/badges/PHP-7.1%2B-blue.svg)](https://github.com/freescout-helpdesk/freescout#requirements) [![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Ffreescout-helpdesk%2Ffreescout&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)](https://hits.seeyoufarm.com)
[![PHP version](https://freescout-helpdesk.github.io/img/badges/PHP-7.1%2B-blue.svg)](https://github.com/freescout-help-desk/freescout#requirements)
</div>
@@ -36,9 +36,11 @@ If you want to support the project feel free to **star this repository**. It hel
* No limitations on the number of users, tickets, mailboxes, etc.
* 100% Mobile-friendly.
* Multilingual: English, Chinese, Croatian, Czech, Danish, Dutch, Finnish, French, German, Italian, Japanese, Korean, Norwegian, Persian, Polish, Portuguese, Russian, Spanish, Slovak, Swedish.
* Multilingual: English, Chinese, Croatian, Czech, Danish, Dutch, Finnish, French, German, Hungarian, Italian, Japanese, Kazakh, Korean, Norwegian, Persian, Polish, Portuguese, Romanian, Russian, Spanish, Slovak, Swedish, Turkish.
* Seamless email integration.
* Supports modern Microsoft Exchange authentication.
* Fully supports screen readers (for visually impaired).
* Built with strong focus on [security](https://freescout.net/security).
* Web installer & updater.
* Starred conversations.
* Forwarding conversations.
@@ -57,6 +59,7 @@ If you want to support the project feel free to **star this repository**. It hel
* Open tracking.
* Editing threads.
* Search.
* And more…
Need anything else? Suggest features [here](https://freescout.net/request-feature/).
@@ -66,19 +69,21 @@ Mobile apps support the same functionality and modules as the web version of you
<a href="https://freescout.net/android-app/" target="_blank" rel="nofollow"><img alt="Android App" src="https://freescout-helpdesk.github.io/img/apps/android.png" width="200px" /></a> <a href="https://freescout.net/ios-app/" target="_blank" rel="nofollow"><img alt="iOS App" src="https://freescout-helpdesk.github.io/img/apps/ios.png?v=1" width="200px" /></a>
[MacOS Menu Bar App](https://github.com/jonalaniz/scouter)
## Requirements
FreeScout is a pure PHP/MySQL application, so it can be easily deployed even on a shared hosting.
FreeScout is a pure PHP/MySQL application, so it can be easily deployed even on a [shared hosting](https://github.com/freescout-help-desk/freescout/wiki/Choosing-a-Server).
* Nginx / Apache / IIS
* PHP 7.1 - 8.2
* PHP 7.1 - 8.x
* MySQL 5.0+ / MariaDB 5.0+ / PostgreSQL
There is no minimum system requirements (CPU / RAM) – FreeScout will run on any system.
There are no minimum system requirements (CPU / RAM) – FreeScout will run on any system.
## Installation
[Installation Guide](https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide)
[Installation Guide](https://github.com/freescout-help-desk/freescout/wiki/Installation-Guide)
Images & one-click installs:
@@ -86,7 +91,7 @@ Images & one-click installs:
* [Softaculous](http://www.softaculous.com/apps/customersupport/FreeScout) (cPanel, Plesk, ISPmanager, H-Sphere, DirectAdmin, InterWorx)
* [Fantastico](http://ff3.netenberg.com/visitors/scripts/freescout/view) (cPanel, DirectAdmin, ISP Manager, ISP Config)
* [Cloudron](https://cloudron.io/store/net.freescout.cloudronapp.html)
* [Ubuntu](https://github.com/freescout-helpdesk/freescout/wiki/Installation-Guide#interactive-installation-bash-script-ubuntu) (bash script)
* [Ubuntu](https://github.com/freescout-help-desk/freescout/wiki/Installation-Guide#interactive-installation-bash-script-ubuntu) (bash script)
## Cloud Hosted
@@ -108,14 +113,14 @@ Images & one-click installs:
Don't miss news, updates and new modules!
[Email Newsletter](https://freescout.net/subscribe/) | [Facebook](https://freescout.net/facebook/) | [Twitter](https://freescout.net/twitter/) | [YouTube](https://freescout.net/youtube/) | [RSS](https://freescout.net/feed/)
[Email Newsletter](https://freescout.net/subscribe/) | [Facebook](https://freescout.net/facebook/) | [Twitter](https://freescout.net/twitter/) | [YouTube](https://freescout.net/youtube/) | [Telegram](https://freescout.net/telegram/) | [RSS](https://freescout.net/feed/)
## Contributing
* [Support the project by leaving a feedback](https://github.com/freescout-helpdesk/freescout/issues/288)
* [Development Guide](https://github.com/freescout-helpdesk/freescout/wiki/Development-Guide)
* [Todo list](https://github.com/freescout-helpdesk/freescout/labels/help%20wanted)
* [Translate](https://github.com/freescout-helpdesk/freescout/wiki/Translate)
* [Support the project by leaving a feedback](https://github.com/freescout-help-desk/freescout/issues/288)
* [Development Guide](https://github.com/freescout-help-desk/freescout/wiki/Development-Guide)
* [Todo list](https://github.com/freescout-help-desk/freescout/labels/help%20wanted)
* [Translate](https://github.com/freescout-help-desk/freescout/wiki/Translate)
## Screenshots

View File

@@ -150,10 +150,14 @@ class Attachment extends Model
$file_dir .= $i.DIRECTORY_SEPARATOR;
if ($uploaded_file) {
$uploaded_file->storeAs(self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir, $file_name, ['disk' => self::DISK]);
} else {
Storage::disk(self::DISK)->put($file_path, $content);
try {
if ($uploaded_file) {
$uploaded_file->storeAs(self::DIRECTORY.DIRECTORY_SEPARATOR.$file_dir, $file_name, ['disk' => self::DISK]);
} else {
Storage::disk(self::DISK)->put($file_path, $content);
}
} catch (\Exception $e) {
\Helper::logException($e, '[Attachment::saveFileToDisk()]');
}
\Helper::sanitizeUploadedFileData($file_path, \Helper::getPrivateStorage(), $content);
@@ -248,7 +252,18 @@ class Attachment extends Model
*/
public function url()
{
return Storage::url($this->getStorageFilePath()).'?id='.$this->id.'&token='.$this->getToken();
$file_path = $this->getStorageFilePath();
// URL must contain only forward slashes.
$file_path = str_replace(DIRECTORY_SEPARATOR, '/', $file_path);
$file_url = Storage::url($file_path);
// Fix percents.
// https://github.com/freescout-helpdesk/freescout/issues/3530
$file_url = str_replace('%', '%25', $file_url);
return $file_url.'?id='.$this->id.'&token='.$this->getToken();
}
/**
@@ -412,9 +427,8 @@ class Attachment extends Model
public function duplicate($thread_id = null)
{
$new_attachment = $this->replicate();
if ($thread_id) {
$new_attachment->thread_id = $thread_id;
}
$new_attachment->thread_id = $thread_id;
$new_attachment->save();

View File

@@ -39,7 +39,7 @@ class CleanTmp extends Command
*/
public function handle()
{
shell_exec('find '.\Helper::getTempDir().' -mtime +7 -type f -name '.\Helper::getTempFilePrefix().'* -exec rm -r -f {} \;');
\Helper::shellExec('find '.\Helper::getTempDir().' -mtime +7 -type f -name '.\Helper::getTempFilePrefix().'* -exec rm -r -f {} \;');
$this->comment("Done");
}

View File

@@ -39,6 +39,13 @@ class ClearCache extends Command
{
$this->call('clear-compiled');
$this->call('cache:clear');
// Remove files from /bootstrap/cache folder.
// https://github.com/freescout-help-desk/freescout/issues/4536
$files = new \Illuminate\Filesystem\Filesystem;
$files->delete($this->laravel->getCachedServicesPath());
$files->delete($this->laravel->getCachedPackagesPath());
$this->call('view:clear');
if ($this->option('doNotCacheConfig')) {
$this->call('config:clear');
@@ -50,6 +57,16 @@ class ClearCache extends Command
opcache_invalidate(app()->getCachedConfigPath());
}
}
// In FreeScout routes caching does not increase performance
// but it increases memory consumption and may lead to problems
// during application updating or installing/updating modules.
// try {
// $this->call('route:cache');
// } catch (\Exception $e) {
// // Do nothing.
// }
// Regenerate vars to get new data from .env
if (!$this->option('doNotGenerateVars')) {
$this->call('freescout:generate-vars');

View File

@@ -18,12 +18,14 @@ use App\Subscription;
use App\Thread;
use App\User;
use Illuminate\Console\Command;
use Webklex\IMAP\Client;
//use Webklex\IMAP\Client;
class FetchEmails extends Command
{
const FWD_AS_CUSTOMER_COMMAND = '@fwd';
const MAX_SLEEP = 500000;
/**
* The name and signature of the console command.
*
@@ -31,7 +33,7 @@ class FetchEmails extends Command
*
* @var string
*/
protected $signature = 'freescout:fetch-emails {--days=3} {--unseen=1} {--identifier=dummy}';
protected $signature = 'freescout:fetch-emails {--days=3} {--unseen=1} {--debug=0} {--identifier=dummy} {--mailboxes=0}';
/**
* The console command description.
@@ -78,11 +80,14 @@ class FetchEmails extends Command
{
$now = time();
$successfully = true;
$debug = $this->option('debug');
Option::set('fetch_emails_last_run', $now);
$this->line('['.date('Y-m-d H:i:s').'] Fetching '.($this->option('unseen') ? 'UNREAD' : 'ALL').' emails for the last '.$this->option('days').' days.');
if ($debug) {
\Config::set('imap.options.debug', true);
}
$this->extra_import = [];
$this->line('['.date('Y-m-d H:i:s').'] Fetching '.($this->option('unseen') ? 'UNREAD' : 'ALL').' emails for the last '.$this->option('days').' days.');
if (Mailbox::getInProtocols() === Mailbox::$in_protocols) {
$this->mailboxes = Mailbox::get();
@@ -97,39 +102,84 @@ class FetchEmails extends Command
// Microseconds: 1 second = 1 000 000 microseconds.
$sleep = 20000;
// Fetches specific mailboxes only, in case the corresponding id is greater than zero.
$mailboxIds = array_filter(
array_map(
'intval',
explode(',', $this->option('mailboxes'))
),
function ($mailboxId) {
return $mailboxId > 0;
}
);
foreach ($this->mailboxes as $mailbox) {
if (!$mailbox->isInActive()) {
continue;
}
if ($mailboxIds !== [] && !in_array($mailbox->id, $mailboxIds, true)) {
continue;
}
$sleep += 20000;
if ($sleep > 500000) {
$sleep = 500000;
if ($sleep > self::MAX_SLEEP) {
$sleep = self::MAX_SLEEP;
}
$this->info('['.date('Y-m-d H:i:s').'] Mailbox: '.$mailbox->name);
$this->mailbox = $mailbox;
$this->extra_import = [];
$debug_log = '';
try {
$this->fetch($mailbox);
$debug_log = $this->executeFetch($mailbox, $debug);
} catch (\Exception $e) {
$successfully = false;
$this->logError('Error: '.$e->getMessage().'; File: '.$e->getFile().' ('.$e->getLine().')').')';
// If mail server starts to block the connection
// (when there are many mailboxes for example),
// we increase connection sleep time and retry after sleep.
// https://github.com/freescout-help-desk/freescout/issues/4227
if (trim($e->getMessage()) == 'connection setup failed') {
$sleep += 500000;
usleep(self::MAX_SLEEP);
try {
$debug_log = $this->executeFetch($mailbox, $debug);
} catch (\Exception $e) {
$successfully = false;
$this->logError('Error: '.$e->getMessage().'; File: '.$e->getFile().' ('.$e->getLine().')').')';
}
} else {
$successfully = false;
// When emails are fetched via POP3 we have to come through all the
// emails and check dates in their headers - it may take some time.
// So there can be situations when more than one fetching imports same email.
if (!\Str::startsWith($e->getMessage(), 'SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry')
) {
$this->logError('Error: '.$e->getMessage().'; File: '.$e->getFile().' ('.$e->getLine().')').')';
}
}
}
if ($debug && $debug_log) {
$this->line($debug_log);
}
// Import emails sent to several mailboxes at once.
if (count($this->extra_import)) {
$this->line('['.date('Y-m-d H:i:s').'] Importing emails sent to several mailboxes at once: '.count($this->extra_import));
foreach ($this->extra_import as $i => $extra_import) {
$this->line('['.date('Y-m-d H:i:s').'] '.($i+1).') '.$extra_import['message']->getSubject());
$this->processMessage($extra_import['message'], $extra_import['message_id'], $extra_import['mailbox'], [], true);
}
}
usleep($sleep);
}
// Import emails sent to several mailboxes at once.
if (count($this->extra_import)) {
$this->line('['.date('Y-m-d H:i:s').'] Importing emails sent to several mailboxes at once: '.count($this->extra_import));
foreach ($this->extra_import as $i => $extra_import) {
$this->line('['.date('Y-m-d H:i:s').'] '.($i+1).') '.$extra_import['message']->getSubject());
$this->processMessage($extra_import['message'], $extra_import['message_id'], $extra_import['mailbox'], [], true);
}
}
if ($successfully && count($this->mailboxes)) {
Option::set('fetch_emails_last_successful_run', $now);
}
@@ -145,14 +195,45 @@ class FetchEmails extends Command
$this->mailboxes = [];
}
public function executeFetch($mailbox, $debug)
{
$debug_log = '';
if ($debug) {
ob_start();
}
$this->fetch($mailbox);
if ($debug) {
$debug_log = ob_get_contents();
ob_end_clean();
}
return $debug_log;
}
public function fetch($mailbox)
{
$no_charset = false;
$client = \MailHelper::getMailboxClient($mailbox);
// Connect to the Server
$client->connect();
// Connect to the Server.
try {
$client->connect();
} catch (\Exception $e) {
$error = $e->getMessage();
// POP3 uses LegacyProtocol.php
// https://github.com/freescout-helpdesk/freescout/issues/4060
if ($error && \Str::startsWith($error, 'Mailbox is empty')) {
$this->line('['.date('Y-m-d H:i:s').'] Fetched: 0');
return;
} else {
throw $e;
}
}
$folders = [];
@@ -166,11 +247,13 @@ class FetchEmails extends Command
$folder = \MailHelper::getImapFolder($client, $folder_name);
} catch (\Exception $e) {
// Just log error and continue.
$this->error('['.date('Y-m-d H:i:s').'] Could not get mailbox IMAP folder: '.$folder_name);
$this->error('['.date('Y-m-d H:i:s').'] IMAP folder ('.$folder_name.') not found on the mail server: '.$e->getMessage());
}
if ($folder) {
$folders[] = $folder;
} else {
$this->line('['.date('Y-m-d H:i:s').'] IMAP folder not found on the mail server: '.$folder_name);
}
}
// try {
@@ -184,8 +267,9 @@ class FetchEmails extends Command
$this->line('['.date('Y-m-d H:i:s').'] Fetching: '.($unseen ? 'UNREAD' : 'ALL'));
}
$page_size = (int)config('app.fetching_bunch_size');
foreach ($folders as $folder) {
$this->line('['.date('Y-m-d H:i:s').'] Folder: '.$folder->name);
$this->line('['.date('Y-m-d H:i:s').'] Folder: '.($folder->full_name ?? $folder->name));
// Requesting emails by bunches allows to fetch large amounts of emails
// without problems with memory.
@@ -203,7 +287,7 @@ class FetchEmails extends Command
if ($no_charset) {
$messages_query->setCharset(null);
}
$messages_query->limit(self::PAGE_SIZE, $page);
$messages_query->limit($page_size, $page);
$messages = $messages_query->get();
@@ -211,7 +295,7 @@ class FetchEmails extends Command
$last_error = $client->getLastError();
}
} catch (\Exception $e) {
$last_error = $e->getMessage();
$last_error = $e->getMessage().'; File: '.$e->getFile().' ('.$e->getLine().')'.')';
}
if ($last_error && stristr($last_error, 'The specified charset is not supported')) {
@@ -256,7 +340,7 @@ class FetchEmails extends Command
$this->processMessage($message, $message_id, $dest_mailbox, $this->mailboxes);
}
$page++;
} while (count($messages) == self::PAGE_SIZE);
} while (count($messages) == $page_size);
}
$client->disconnect();
@@ -367,7 +451,7 @@ class FetchEmails extends Command
$user_id = null;
$user = null; // for user reply only
$message_from_customer = true;
$in_reply_to = $message->getInReplyTo();
$in_reply_to = trim($message->getInReplyTo() ?? '', '<>');
$references = $message->getReferences();
$attachments = $message->getAttachments();
$html_body = '';
@@ -376,19 +460,21 @@ class FetchEmails extends Command
$is_bounce = false;
// Determine previous Message-ID
$prev_message_id = '';
$prev_message_ids = array();
if ($references && !is_array($references)) {
$references = array_filter(preg_split('/[, <>]/', $references));
}
if ($in_reply_to) {
$prev_message_id = trim($in_reply_to, '<>');
} elseif ($references) {
if (!is_array($references)) {
$references = array_filter(preg_split('/[, <>]/', $references));
}
// Find first non-empty reference
$prev_message_ids[] = $in_reply_to;
}
if ($references) {
// Find non-empty references
if (is_array($references)) {
foreach ($references as $reference) {
if (!empty(trim($reference))) {
$prev_message_id = trim($reference);
break;
$prev_message_ids[] = trim($reference);
}
}
}
@@ -403,13 +489,10 @@ class FetchEmails extends Command
];
// Try to get previous message ID from marker in body.
if (!$prev_message_id || !preg_match('/^('.implode('|', $reply_prefixes).')\-(\d+)\-/', $prev_message_id)) {
$html_body = $message->getHTMLBody(false);
$marker_message_id = \MailHelper::fetchMessageMarkerValue($html_body);
if ($marker_message_id) {
$prev_message_id = $marker_message_id;
}
$html_body = $message->getHTMLBody(false);
$marker_message_id = \MailHelper::fetchMessageMarkerValue($html_body);
if ($marker_message_id) {
$prev_message_ids[] = $marker_message_id;
}
// Bounce detection.
@@ -478,51 +561,45 @@ class FetchEmails extends Command
}
}
// Is it a message from Customer or User replied to the notification
preg_match('/^'.\MailHelper::MESSAGE_ID_PREFIX_NOTIFICATION."\-(\d+)\-(\d+)\-/", $prev_message_id, $m);
# Try to get the thread traversing the possible prev_message_ids
foreach ($prev_message_ids as $prev_message_id) {
// Is it a message from Customer or User replied to the notification
preg_match('/^'.\MailHelper::MESSAGE_ID_PREFIX_NOTIFICATION."\-(\d+)\-(\d+)\-/", $prev_message_id, $m);
if (!$is_bounce && !empty($m[1]) && !empty($m[2])) {
// Reply from User to the notification
$prev_thread = Thread::find($m[1]);
$user_id = $m[2];
$user = User::find($user_id);
$message_from_customer = false;
$is_reply = true;
if (!$is_bounce && !empty($m[1]) && !empty($m[2])) {
// Reply from User to the notification
$prev_thread = Thread::find($m[1]);
$user_id = $m[2];
$user = User::find($user_id);
$message_from_customer = false;
$is_reply = true;
if (!$user) {
$this->logError('User not found: '.$user_id);
$this->setSeen($message, $mailbox);
return;
}
$this->line('['.date('Y-m-d H:i:s').'] Message from: User');
} else {
// Message from Customer or User replied to his reply to notification
$this->line('['.date('Y-m-d H:i:s').'] Message from: Customer');
if (!$user) {
$this->logError('User not found: '.$user_id);
$this->setSeen($message, $mailbox);
return;
}
// Skip auto-replies sent to the email notification on behalf of a user.
// https://github.com/freescout-helpdesk/freescout/issues/4035
if (\MailHelper::isAutoResponder($message_header)) {
$this->logError('Skipping an auto-reply to the email notification');
$this->setSeen($message, $mailbox);
return;
}
$this->line('['.date('Y-m-d H:i:s').'] Message from: User');
} else {
// Message from Customer or User replied to his reply to notification
$this->line('['.date('Y-m-d H:i:s').'] Message from: Customer');
if (!$is_bounce) {
if ($prev_message_id) {
$prev_thread_id = '';
if (!$is_bounce) {
if ($prev_message_id) {
$prev_thread_id = '';
// Customer replied to the email from user
preg_match('/^'.\MailHelper::MESSAGE_ID_PREFIX_REPLY_TO_CUSTOMER."\-(\d+)\-([a-z0-9]+)@/", $prev_message_id, $m);
// Simply checking thread_id from message_id was causing an issue when
// customer was sending a message from FreeScout - the message was
// connected to the wrong conversation.
if (!empty($m[1]) && !empty($m[2])) {
$message_id_hash = $m[2];
if (strlen($message_id_hash) == 16) {
if ($message_id_hash == \MailHelper::getMessageIdHash($m[1])) {
$prev_thread_id = $m[1];
}
} else {
// Backward compatibility.
$prev_thread_id = $m[1];
}
}
// Customer replied to the auto reply
if (!$prev_thread_id) {
preg_match('/^'.\MailHelper::MESSAGE_ID_PREFIX_AUTO_REPLY."\-(\d+)\-([a-z0-9]+)@/", $prev_message_id, $m);
// Customer replied to the email from user
preg_match('/^'.\MailHelper::MESSAGE_ID_PREFIX_REPLY_TO_CUSTOMER."\-(\d+)\-([a-z0-9]+)@/", $prev_message_id, $m);
// Simply checking thread_id from message_id was causing an issue when
// customer was sending a message from FreeScout - the message was
// connected to the wrong conversation.
if (!empty($m[1]) && !empty($m[2])) {
$message_id_hash = $m[2];
if (strlen($message_id_hash) == 16) {
@@ -534,29 +611,48 @@ class FetchEmails extends Command
$prev_thread_id = $m[1];
}
}
}
if ($prev_thread_id) {
$prev_thread = Thread::find($prev_thread_id);
} else {
// Customer replied to his own message
$prev_thread = Thread::where('message_id', $prev_message_id)->first();
}
// Customer replied to the auto reply
if (!$prev_thread_id) {
preg_match('/^'.\MailHelper::MESSAGE_ID_PREFIX_AUTO_REPLY."\-(\d+)\-([a-z0-9]+)@/", $prev_message_id, $m);
if (!empty($m[1]) && !empty($m[2])) {
$message_id_hash = $m[2];
if (strlen($message_id_hash) == 16) {
if ($message_id_hash == \MailHelper::getMessageIdHash($m[1])) {
$prev_thread_id = $m[1];
}
} else {
// Backward compatibility.
$prev_thread_id = $m[1];
}
}
}
// Reply from user to his reply to the notification
if (!$prev_thread
&& ($prev_thread = Thread::where('message_id', $prev_message_id)->first())
&& $prev_thread->created_by_user_id
&& $prev_thread->created_by_user->hasEmail($from)
) {
$user_id = $user->id;
$message_from_customer = false;
$is_reply = true;
if ($prev_thread_id) {
$prev_thread = Thread::find($prev_thread_id);
} else {
// Customer replied to his own message
$prev_thread = Thread::where('message_id', $prev_message_id)->first();
}
// Reply from user to his reply to the notification
if (!$prev_thread
&& ($prev_thread = Thread::where('message_id', $prev_message_id)->first())
&& $prev_thread->created_by_user_id
&& $prev_thread->created_by_user->hasEmail($from)
) {
$user_id = $user->id;
$message_from_customer = false;
$is_reply = true;
}
}
}
if (!empty($prev_thread)) {
$is_reply = true;
}
}
# If a thread is found, we keep it and break
if (!empty($prev_thread)) {
$is_reply = true;
break;
}
}
@@ -593,12 +689,17 @@ class FetchEmails extends Command
// Get body and do not replace :cid with images base64
$html_body = $message->getHTMLBody(false);
}
$is_html = true;
if ($html_body) {
$body = $this->separateReply($html_body, true, $is_reply);
$body = $html_body;
} else {
$body = $message->getTextBody();
$body = $this->separateReply($body, false, $is_reply);
$is_html = false;
$body = $message->getTextBody() ?? '';
$body = htmlspecialchars($body);
}
// We have to fetch absolutely all emails, even with empty body.
// if (!$body) {
// $this->logError('Message body is empty');
@@ -620,23 +721,26 @@ class FetchEmails extends Command
// It will always return an empty value as it's Bcc.
$bcc = $this->formatEmailList($message->getBcc());
// If existing user forwarded customer's email to the mailbox
// we are creating a new conversation as if it was sent by the customer.
if ($in_reply_to
if (// Some mail clients to not add "In-Reply-To" header when forwarding emails.
// https://github.com/freescout-help-desk/freescout/issues/4348
//$in_reply_to
// We should use body here, as entire HTML may contain
// email looking things.
//&& ($fwd_body = $html_body ?: $message->getTextBody())
&& $body
$body
//&& preg_match("/^(".implode('|', \MailHelper::$fwd_prefixes)."):(.*)/i", $subject, $m)
// F:, FW:, FWD:, WG:, De:
&& preg_match("/^[[:alpha:]]{1,3}:(.*)/i", $subject, $m)
&& preg_match("/^[[:alpha:]]{1,3}\s*:(.*)/i", $subject, $m)
// It can be just "Fwd:"
//&& !empty($m[1])
&& !$user_id && !$is_reply && !$prev_thread
// Only if the email has been sent to one mailbox.
&& count($to) == 1 && count($cc) == 0
&& preg_match("/^[\s]*".self::FWD_AS_CUSTOMER_COMMAND."/su", trim(strip_tags($body)))
// We need to replace also any potential <style></style> tags.
&& preg_match("/^[\s]*".self::FWD_AS_CUSTOMER_COMMAND."/su", strtolower(trim(\Helper::stripTags($body))))
) {
// Try to get "From:" from body.
$original_sender = $this->getOriginalSenderFromFwd($body);
@@ -657,6 +761,12 @@ class FetchEmails extends Command
}
}
// separateReply() function may distort original HTML if email
// is mentioned as <test@example.org> and it will interpret it as a tag.
// https://github.com/freescout-helpdesk/freescout/issues/4036
$body = $this->separateReply($body, $is_html, $is_reply, !$message_from_customer, (($message_from_customer && $prev_thread) ? $prev_thread->getMessageId($mailbox) : ''));
// Create customers
$emails = array_merge(
$this->attrToArray($message->getFrom()),
@@ -800,7 +910,7 @@ class FetchEmails extends Command
// https://github.com/freescout-helpdesk/freescout/issues/2672
$body = preg_replace("/[\"']cid:/", '!', $body);
// Cut out the command, otherwise it will be recognized as an email.
$body = preg_replace("/".self::FWD_AS_CUSTOMER_COMMAND."([\s<]+)/su", '$1', $body);
$body = preg_replace("/".self::FWD_AS_CUSTOMER_COMMAND."([\s<]+)/isu", '$1', $body);
// Looks like email texts may appear in attributes:
// https://github.com/freescout-helpdesk/freescout/issues/276
@@ -813,6 +923,7 @@ class FetchEmails extends Command
$email = $b[1] ?? '';
// https://github.com/freescout-helpdesk/freescout/issues/2517
$email = preg_replace("#.*&lt(.*)&gt.*#", "$1", $email);
return Email::sanitizeEmail($email);
}
@@ -902,6 +1013,7 @@ class FetchEmails extends Command
$now = date('Y-m-d H:i:s');
}
$conv_cc = $cc;
$prev_conv_cc = $conv_cc;
// Customers are created before with email and name
$customer = Customer::create($from);
@@ -911,6 +1023,7 @@ class FetchEmails extends Command
// If reply came from another customer: change customer, add original as CC.
// If FreeScout will not change the customer, the reply will be shown
// as coming from the original customer (not the real sender) and cause confusion.
// Below after events are fired we roll customer back.
if ($conversation->customer_id != $customer->id) {
$prev_customer_id = $conversation->customer_id;
$prev_customer_email = $conversation->customer_email;
@@ -1005,11 +1118,17 @@ class FetchEmails extends Command
$body_changed = false;
$saved_attachments = $this->saveAttachments($attachments, $thread->id);
if ($saved_attachments) {
$thread->has_attachments = true;
// After attachments saved to the disk we can replace cids in body (for PLAIN and HTML body)
$thread->body = $this->replaceCidsWithAttachmentUrls($thread->body, $saved_attachments, $conversation, $prev_has_attachments);
$body_changed = true;
foreach ($saved_attachments as $saved_attachment) {
if (!$saved_attachment['attachment']->embedded) {
$thread->has_attachments = true;
break;
}
}
}
$new_body = Thread::replaceBase64ImagesWithAttachments($thread->body);
@@ -1043,8 +1162,16 @@ class FetchEmails extends Command
}
// Conversation customer changed
// if ($prev_customer_id) {
// event(new ConversationCustomerChanged($conversation, $prev_customer_id, $prev_customer_email, null, $customer));
// }
// Return original customer back.
if ($prev_customer_id) {
event(new ConversationCustomerChanged($conversation, $prev_customer_id, $prev_customer_email, null, $customer));
$conversation->customer_id = $prev_customer_id;
$conversation->customer_email = $prev_customer_email;
$conversation->setCc(array_merge($prev_conv_cc, array_diff($to, $mailbox->getEmails())));
$conversation->save();
}
return $thread;
@@ -1143,11 +1270,16 @@ class FetchEmails extends Command
$body_changed = false;
$saved_attachments = $this->saveAttachments($attachments, $thread->id);
if ($saved_attachments) {
$thread->has_attachments = true;
// After attachments saved to the disk we can replace cids in body (for PLAIN and HTML body)
$thread->body = $this->replaceCidsWithAttachmentUrls($thread->body, $saved_attachments, $conversation, $prev_has_attachments);
$body_changed = true;
foreach ($saved_attachments as $saved_attachment) {
if (!$saved_attachment['attachment']->embedded) {
$thread->has_attachments = true;
break;
}
}
}
$new_body = Thread::replaceBase64ImagesWithAttachments($thread->body);
@@ -1203,7 +1335,7 @@ class FetchEmails extends Command
// Fix for Webklex/laravel-imap.
// https://github.com/freescout-helpdesk/freescout/issues/2782
if (\Str::startsWith($name, '=?')) {
$name_decoded = \imap_utf8($name);
$name_decoded = \MailHelper::imapUtf8($name);
if ($name_decoded) {
return $name_decoded;
@@ -1220,7 +1352,7 @@ class FetchEmails extends Command
*
* @return string
*/
public function separateReply($body, $is_html, $is_reply)
public function separateReply($body, $is_html, $is_reply, $user_reply_to_notification = false, $prev_message_id = '')
{
$cmp_reply_length_desc = function ($a, $b) {
if (mb_strlen($a) == mb_strlen($b)) {
@@ -1232,8 +1364,22 @@ class FetchEmails extends Command
$result = '';
// Can fix broken HTML, remove sensitive parts and etc.
$body = \Eventy::filter('fetch_emails.separate_reply.preprocess_body', $body);
if ($is_html) {
// Extract body content from HTML
// Proton has it's own unique way of placing replies:
// https://github.com/freescout-help-desk/freescout/issues/4537#issuecomment-2629836738
if ($is_reply
&& ($protonmail_quote_pos = mb_strpos($body, '<div class="protonmail_quote">'))
&& ($html_pos = mb_strpos($body, '<html'))
&& $protonmail_quote_pos < $html_pos
) {
$body = mb_substr($body, 0, $protonmail_quote_pos);
}
// Split by <html>
$htmls = [];
preg_match_all("/<html[^>]*>(.*?)<\/html>/is", $body, $htmls);
@@ -1247,7 +1393,7 @@ class FetchEmails extends Command
libxml_use_internal_errors(true);
//$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
//$dom->loadHTML(\Helper::mbConvertEncodingHtmlEntities($html));
$dom->loadHTML(\Symfony\Polyfill\Mbstring\Mbstring::mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
$dom->loadHTML(\Symfony\Polyfill\Mbstring\Mbstring::mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8') ?: $html);
libxml_use_internal_errors(false);
$bodies = $dom->getElementsByTagName('body');
if ($bodies->length == 1) {
@@ -1270,12 +1416,31 @@ class FetchEmails extends Command
if ($is_reply) {
// Check all separators and choose the shortest reply
$reply_bodies = [];
$reply_separators = Mail::$alternative_reply_separators;
if (!empty($this->mailbox->before_reply)) {
$reply_separators[] = $this->mailbox->before_reply;
}
// If user replied to the email notification use only predefined reply separator.
// https://github.com/freescout-helpdesk/freescout/issues/3580
if ($user_reply_to_notification && strstr($result, \MailHelper::REPLY_SEPARATOR_NOTIFICATION)) {
$reply_separators = [\MailHelper::REPLY_SEPARATOR_NOTIFICATION];
}
// Try to separate customer reply using hashed reply separator.
// In this case some extra text may appear below customer's reply:
// On Thu, Jan 4, 2024 at 8:41 AM John Doe | Demo <test@example.org> wrote:
if (config('app.alternative_reply_separation')) {
if (!$user_reply_to_notification && $prev_message_id) {
$hashed_reply_separator = \MailHelper::getHashedReplySeparator($prev_message_id);
if (strstr($result, $hashed_reply_separator)) {
$reply_separators = [$hashed_reply_separator];
}
}
}
foreach ($reply_separators as $reply_separator) {
if (\Str::startsWith($reply_separator, 'regex:')) {
$regex = preg_replace("/^regex:/", '', $reply_separator);
@@ -1284,7 +1449,7 @@ class FetchEmails extends Command
$parts = explode($reply_separator, $result);
}
if (count($parts) > 1) {
// Check if past contains any real text.
// Check if part contains any real text.
$text = \Helper::htmlToText($parts[0]);
$text = trim($text);
$text = preg_replace('/^\s+/mu', '', $text);
@@ -1474,7 +1639,8 @@ class FetchEmails extends Command
public function setSeen($message, $mailbox)
{
$message->setFlag(['Seen']);
$flag = \Eventy::filter('fetch_emails.set_seen_flag', ['Seen'], $message, $mailbox);
$message->setFlag($flag);
\Eventy::action('fetch_emails.after_set_seen', $message, $mailbox, $this);
}
}

View File

@@ -53,8 +53,17 @@ class GenerateVars extends Command
$content = view('js/vars', $params)->render();
//$filesystem->put($file_path, $content);
// Save vars only if content changed
// Escape quotes in json values.
// https://github.com/freescout-help-desk/freescout/issues/4369
$content = preg_replace_callback(
"#(:[ ]*\")(.*)(\"[,\r\n])#",
function($v) {
return $v[1].str_replace('"', '\"', $v[2]).$v[3];
},
$content
);
// Save vars only if content has changed.
try {
if (\Storage::exists('js/vars.js')) {
$old_content = \Storage::get('js/vars.js');

View File

@@ -69,10 +69,11 @@ class ModuleUpdate extends Command
// Detect if new version is available.
foreach ($installed_modules as $module) {
if ($module->getAlias() != $dir_module['alias'] || !$module->active()) {
if ($module->getAlias() != $dir_module['alias'] /*|| !$module->active()*/) {
continue;
}
if (!empty($dir_module['version']) && version_compare($dir_module['version'], $module->get('version'), '>')) {
$update_result = \App\Module::updateModule($dir_module['alias']);
$this->info('['.$update_result['module_name'].' Module'.']');
@@ -88,12 +89,75 @@ class ModuleUpdate extends Command
if (trim($update_result['output'])) {
$this->line(preg_replace("#\n#", "\n> ", '> '.trim($update_result['output'])));
}
$counter++;
}
}
}
// Update custom modules.
// Loop through each installed module.
foreach ($installed_modules as $module) {
// Skip if the module is an official one
if (\App\Module::isOfficial($module->get('authorUrl'))) {
continue;
}
// Get the URL for the latest version of the module
$latest_version_number_url = $module->get('latestVersionUrl');
if (! $latest_version_number_url) {
continue;
}
// Create a new Guzzle HTTP client
$client = new \GuzzleHttp\Client();
try {
// Send a GET request to the latest version URL
$response = $client->request('GET', $latest_version_number_url, \Helper::setGuzzleDefaultOptions());
// Get the latest version number from the response body
$latest_version = trim((string) $response->getBody());
if (empty($latest_version)) {
continue;
} else {
// Get the current version of the module
$current_version = $module->get('version');
}
} catch (\Exception $e) {
// If there's an exception, skip to the next iteration
continue;
}
// If the latest version is greater than the current version
if (version_compare($latest_version, $current_version, '>')) {
// Update the module
$update_result = \App\Module::updateModule($module->getAlias());
// Print the module name and status
$this->info('[' . $update_result['module_name'] . ' Module' . ']');
if ($update_result['status'] == 'success') {
// If the update was successful, print the success message
$this->line($update_result['msg_success']);
} else {
// If the update failed, print the error message
$msg = $update_result['msg'];
if ($update_result['download_msg']) {
$msg .= ' (' . $update_result['download_msg'] . ')';
}
$this->error('ERROR: ' . $msg);
}
// If there's any output from the update, print it
if (trim($update_result['output'])) {
$this->line(preg_replace("#\n#", "\n> ", '> ' . trim($update_result['output'])));
}
// Increment the counter
$counter ++;
}
}
if ($module_alias && !$found) {
$this->error('Module with the following alias not found: '.$module_alias);
} elseif (!$counter) {

View File

@@ -0,0 +1,126 @@
<?php
namespace App\Console\Commands;
use App\Mailbox;
use Webklex\PHPIMAP\IMAP;
use Webklex\PHPIMAP\Message;
use Illuminate\Console\Command;
class ParseEml extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'freescout:parse-eml';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Parse EML file';
/**
* Current mailbox.
*
* Used to process emails sent to multiple mailboxes.
*/
public $mailbox;
/**
* Used to process emails sent to multiple mailboxes.
*/
public $mailboxes;
public $extra_import = [];
/**
* Page size when requesting emails from mail server.
*/
const PAGE_SIZE = 300;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$email = file_get_contents(storage_path('logs/email.eml'));
if (!str_contains($email, "\r\n")){
$email = str_replace("\n", "\r\n", $email);
}
// $raw_header = substr($email, 0, strpos($email, "\r\n\r\n"));
// $raw_body = substr($email, strlen($raw_header)+4);
// $mailbox = Mailbox::find($this->option('mailbox'));
// //\Config::set('app.new_fetching_library', 'true');
// $client = \MailHelper::getMailboxClient($mailbox);
// $client->openFolder("INBOX");
// $message = Message::make(/*$this->option('uid')*/null, null, $client, $raw_header, $raw_body, [/*0 => "\\Seen"*/], IMAP::ST_UID);
$manager = new \Webklex\PHPIMAP\ClientManager([
// 'options' => [
// "debug" => $_ENV["LIVE_MAILBOX_DEBUG"] ?? false,
// ],
// 'accounts' => [
// 'default' => [
// 'host' => getenv("LIVE_MAILBOX_HOST"),
// 'port' => getenv("LIVE_MAILBOX_PORT"),
// 'encryption' => getenv("LIVE_MAILBOX_ENCRYPTION"),
// 'validate_cert' => getenv("LIVE_MAILBOX_VALIDATE_CERT"),
// 'username' => getenv("LIVE_MAILBOX_USERNAME"),
// 'password' => getenv("LIVE_MAILBOX_PASSWORD"),
// 'protocol' => 'imap', //might also use imap, [pop3 or nntp (untested)]
// ],
// ],
]);
$message = \Webklex\PHPIMAP\Message::fromString($email);
$this->line('Headers: ');
$this->info($message->getHeader()->raw);
$this->line('From: ');
$this->info(json_encode($message->getFrom()[0] ?? [], JSON_UNESCAPED_UNICODE));
$this->line('Reply-To: ');
$this->info(json_encode($message->getReplyTo()[0] ?? [], JSON_UNESCAPED_UNICODE));
$this->line('In-Reply-To: ');
$this->info($message->getInReplyTo());
$this->line('References: ');
$this->info(json_encode(array_values(array_filter(preg_split('/[, <>]/', $message->getReferences() ?? ''))), JSON_UNESCAPED_UNICODE));
$this->line('Date: ');
$this->info($message->getDate());
$this->line('Subject: ');
$this->info($message->getSubject());
$this->line('Text Body: ');
$this->info($message->getTextBody());
$this->line('HTML Body: ');
$html_body = $message->getHTMLBody(false) ?? '';
$this->info($html_body);
$attachments = $message->getAttachments();
if (count($attachments)) {
$this->line('Attachments: ');
foreach ($attachments as $attachment) {
$this->info('— '.$attachment->getName().(strstr($html_body, 'cid:'.$attachment->id) ? ' (embedded)' : ''));
}
}
}
}

View File

@@ -43,7 +43,7 @@ class SendMonitor extends Command
// Get SendReplyToCustomer jobs.
$pending_jobs = \App\Job::where('queue', 'emails')
->where('payload', 'like', '{"displayName":"App\\\\\\\\Jobs\\\\\\\\SendReplyToCustomer"%')
->where('created_at', '<', time() - self::CHECK_PERIOD)
->where('available_at', '<', time() - self::CHECK_PERIOD)
->exists();
// Check failed_jobs.

View File

@@ -1,102 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Mailbox;
use Webklex\PHPIMAP\IMAP;
use Webklex\PHPIMAP\Message;
use Illuminate\Console\Command;
class TestWebklex extends Command
{
/**
* The name and signature of the console command.
*
* --mailbox Any mailbox able to connect via IMAP to its mail server.
* --uid Pass any UID from the Webklex/PHP-IMAP fetching output.
*
* @var string
*/
protected $signature = 'freescout:test-webklex {--mailbox=2} {--uid=914}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Test Webklex/php-imap library';
/**
* Current mailbox.
*
* Used to process emails sent to multiple mailboxes.
*/
public $mailbox;
/**
* Used to process emails sent to multiple mailboxes.
*/
public $mailboxes;
public $extra_import = [];
/**
* Page size when requesting emails from mail server.
*/
const PAGE_SIZE = 300;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$email = file_get_contents(storage_path('logs/test_webklex.eml'));
if (!str_contains($email, "\r\n")){
$email = str_replace("\n", "\r\n", $email);
}
$raw_header = substr($email, 0, strpos($email, "\r\n\r\n"));
$raw_body = substr($email, strlen($raw_header)+8);
$mailbox = Mailbox::find($this->option('mailbox'));
\Config::set('app.new_fetching_library', 'true');
$client = \MailHelper::getMailboxClient($mailbox);
$client->openFolder("INBOX");
$message = Message::make($this->option('uid'), null, $client, $raw_header, $raw_body, [0 => "\\Seen"], IMAP::ST_UID);
$this->line('Headers: ');
$this->info($message->getHeader()->raw);
$this->line('From: ');
$this->info(json_encode($message->getFrom()[0] ?? [], JSON_UNESCAPED_UNICODE));
$this->line('Subject: ');
$this->info($message->getSubject());
$this->line('Text Body: ');
$this->info($message->getTextBody());
$this->line('HTML Body: ');
$this->info($message->getHTMLBody(false));
$attachments = $message->getAttachments();
if (count($attachments)) {
$this->line('Attachments: ');
foreach ($attachments as $attachment) {
$this->info($attachment->getName());
}
}
}
}

View File

@@ -32,7 +32,10 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
// Keep in mind that this function is also called on clearing cache.
// https://github.com/freescout-helpdesk/freescout/issues/3970
if (!$this->isScheduleRun() && !\Helper::isRoute('system.cron')) {
return;
}
// Remove failed jobs
$schedule->command('queue:flush')
@@ -100,23 +103,30 @@ class Kernel extends ConsoleKernel
}
}
$fetch_unseen = (int)config('app.fetch_unseen');
$fetch_command_identifier = \Helper::getWorkerIdentifier('freescout:fetch-emails');
$fetch_command_name = 'freescout:fetch-emails --identifier='.$fetch_command_identifier;
$fetch_command_name = 'freescout:fetch-emails'
. ' --identifier='.$fetch_command_identifier
. ' --unseen='.$fetch_unseen;
// Kill fetch commands running for too long.
// In shedule:run this code is executed every time $schedule->command() in this function is executed.
if ($this->isScheduleRun() && function_exists('shell_exec')) {
if (function_exists('shell_exec')) {
$fetch_command_pids = \Helper::getRunningProcesses($fetch_command_identifier);
// The name of the command here must be exactly the same as below!
// Otherwise long fetching will be killed and won't run longer than 1 mintue.
$mutex_name = $schedule->command($fetch_command_name)
->skip(function () {
return true;
})
->mutexName();
// $schedule->command() creates a new mutex, so we need to store mutex name in cache.
// $mutex_name = $schedule->command($fetch_command_name)
// ->everyMinute() - this also need to be set
// ->skip(function () {
// return true;
// })
// ->mutexName();
// If there is no cache mutext but there are running fetch commands
$mutex_name = \Cache::get('fetch_mutex_name') ?? '';
// If there is no cache mutext but there are running fetch commands
// it means the mutex had expired after self::FETCH_MAX_EXECUTION_TIME
// and the existing command(s) is running longer than self::FETCH_MAX_EXECUTION_TIME.
if (count($fetch_command_pids) > 0 && !\Cache::get($mutex_name)) {
@@ -138,15 +148,21 @@ class Kernel extends ConsoleKernel
// Fetch emails from mailboxes
$fetch_command = $schedule->command($fetch_command_name)
// withoutOverlapping() option creates a mutex in the cache
// withoutOverlapping() option creates a mutex in the cache
// which by default expires in 24 hours.
// So we are passing an 'expiresAt' parameter to withoutOverlapping() to
// prevent fetching from not being executed when fetching command by some reason
// does not remove the mutex from the cache.
// does not remove the mutex from the cache.
->withoutOverlapping($expiresAt = self::FETCH_MAX_EXECUTION_TIME /* minutes */)
->sendOutputTo(storage_path().'/logs/fetch-emails.log');
switch (config('app.fetch_schedule')) {
case Mail::FETCH_SCHEDULE_EVERY_TWO_MINUTES:
$fetch_command->cron('*/2 * * * *');
break;
case Mail::FETCH_SCHEDULE_EVERY_THREE_MINUTES:
$fetch_command->cron('*/3 * * * *');
break;
case Mail::FETCH_SCHEDULE_EVERY_FIVE_MINUTES:
$fetch_command->everyFiveMinutes();
break;
@@ -166,6 +182,7 @@ class Kernel extends ConsoleKernel
$fetch_command->everyMinute();
break;
}
\Cache::put('fetch_mutex_name', $fetch_command->mutexName(), self::FETCH_MAX_EXECUTION_TIME);
$schedule = \Eventy::filter('schedule', $schedule);
@@ -187,10 +204,10 @@ class Kernel extends ConsoleKernel
// $schedule->command('queue:work') command below has withoutOverlapping() option,
// which works via special mutex stored in the cache preventing several 'queue:work' to work at the same time.
// So when the cache is cleared the mutex indicating that the 'queue:work' is running is removed,
// and the second 'queue:work' command is launched by cron. When `artisan schedule:run` is executed it sees
// and the second 'queue:work' command is launched by cron. When `artisan schedule:run` is executed it sees
// that there are two 'queue:work' processes running and kills them.
// After one minute 'queue:work' is executed by cron via `artisan schedule:run` and works in the background.
if ($this->isScheduleRun() && function_exists('shell_exec')) {
if (function_exists('shell_exec')) {
$running_commands = \Helper::getRunningProcesses();
if (count($running_commands) > 1) {
@@ -202,7 +219,7 @@ class Kernel extends ConsoleKernel
sleep(1);
// Check processes again.
$worker_pids = \Helper::getRunningProcesses();
if (count($worker_pids) > 1) {
// Current process also has to be killed, as otherwise it "stucks"
// $current_pid = getmypid();
@@ -240,12 +257,12 @@ class Kernel extends ConsoleKernel
}
/**
* This function is needed because every time $schedule->command() is executed
* This function is needed because every time $schedule->command() is executed
* the schedule() is executed also.
*/
public function isScheduleRun()
{
if (!\Helper::isConsole()) {
if (\Helper::isConsole()) {
return true;
} else {
return !empty($_SERVER['argv']) && in_array('schedule:run', $_SERVER['argv']);

View File

@@ -57,11 +57,13 @@ class Conversation extends Model
const TYPE_EMAIL = 1;
const TYPE_PHONE = 2;
const TYPE_CHAT = 3;
const TYPE_CUSTOM = 4;
public static $types = [
self::TYPE_EMAIL => 'email',
self::TYPE_PHONE => 'phone',
self::TYPE_CHAT => 'chat',
self::TYPE_CUSTOM => 'custom',
];
/**
@@ -669,7 +671,7 @@ class Conversation extends Model
*
* @return Conversation
*/
public function getNearby($mode = 'closest', $folder_id = null)
public function getNearby($mode = 'closest', $folder_id = null, $status = null, $prev_if_no_next = false)
{
$conversation = null;
@@ -684,21 +686,31 @@ class Conversation extends Model
$query = \Eventy::filter('conversation.get_nearby_query', $query, $this, $mode, $folder);
$status_applied = \Eventy::filter('conversation.get_nearby_status', false, $query, $status, $this, $folder);
if (!$status_applied && $status) {
$query->where('status', $status);
}
$order_bys = $folder->getOrderByArray();
// Next.
if ($mode != 'prev') {
// Try to get next conversation
$query_next = $query;
$query_next = clone $query;
foreach ($order_bys as $order_by) {
foreach ($order_by as $field => $sort_order) {
if (!$this->$field) {
continue;
}
$field_value = $this->$field;
if ($field == 'status' && $status !== null) {
$field_value = $status;
}
if ($sort_order == 'asc') {
$query_next->where($field, '>=', $this->$field);
$query_next->where($field, '>=', $field_value);
} else {
$query_next->where($field, '<=', $this->$field);
$query_next->where($field, '<=', $field_value);
}
$query_next->orderBy($field, $sort_order);
}
@@ -706,7 +718,8 @@ class Conversation extends Model
$conversation = $query_next->first();
}
if ($conversation || $mode == 'next') {
// https://github.com/freescout-helpdesk/freescout/issues/3486
if ($conversation || ($mode == 'next' && !$prev_if_no_next)) {
return $conversation;
}
@@ -717,10 +730,14 @@ class Conversation extends Model
if (!$this->$field) {
continue;
}
$field_value = $this->$field;
if ($field == 'status' && $status !== null) {
$field_value = $status;
}
if ($sort_order == 'asc') {
$query_prev->where($field, '<=', $this->$field);
$query_prev->where($field, '<=', $field_value);
} else {
$query_prev->where($field, '>=', $this->$field);
$query_prev->where($field, '>=', $field_value);
}
$query_prev->orderBy($field, $sort_order == 'asc' ? 'desc' : 'asc');
}
@@ -732,9 +749,9 @@ class Conversation extends Model
/**
* Get URL of the next conversation.
*/
public function urlNext($folder_id = null)
public function urlNext($folder_id = null, $status = null, $prev_if_no_next = false)
{
$next_conversation = $this->getNearby('next', $folder_id);
$next_conversation = $this->getNearby('next', $folder_id, $status, $prev_if_no_next);
if ($next_conversation) {
$url = $next_conversation->url();
} else {
@@ -782,6 +799,11 @@ class Conversation extends Model
if (!$mailbox) {
$mailbox = $this->mailbox;
if ($mailbox->id != $this->mailbox_id) {
$this->load('mailbox');
$mailbox = $this->mailbox;
}
}
// Find folder
@@ -1065,8 +1087,10 @@ class Conversation extends Model
} else {
return __('me');
}
} else {
} elseif ($this->user) {
return $this->user->getFullName();
} else {
return '';
}
}
@@ -1077,11 +1101,13 @@ class Conversation extends Model
{
// Get conversations from personal folder
if ($folder->type == Folder::TYPE_MINE) {
$query_conversations = self::where('user_id', $user_id)
->where('mailbox_id', $folder->mailbox_id)
$query_conversations = self::where('mailbox_id', $folder->mailbox_id)
->whereIn('status', [self::STATUS_ACTIVE, self::STATUS_PENDING])
->where('state', self::STATE_PUBLISHED);
// Applied below.
//where('user_id', $user_id)
// Assigned - do not show my conversations.
} elseif ($folder->type == Folder::TYPE_ASSIGNED) {
$query_conversations = $folder->conversations()
@@ -1113,18 +1139,31 @@ class Conversation extends Model
$query_conversations = $folder->conversations()->where('state', self::STATE_PUBLISHED);
}
$assignee_condition_applied = false;
// If show only assigned to the current user conversations.
if (!\Helper::isConsole()
&& $user_id
&& $user = auth()->user()
) {
if ($user->id == $user_id
&& $user->hasManageMailboxPermission($folder->mailbox_id, Mailbox::ACCESS_PERM_ASSIGNED)
) {
$query_conversations->where('user_id', '=', $user_id);
if ($user->id == $user_id && $user->canSeeOnlyAssignedConversations()) {
if ($folder->type != Folder::TYPE_DRAFTS) {
$assignee_condition_applied = \Eventy::filter('folder.only_assigned_condition', false, $query_conversations, $user_id);
if (!$assignee_condition_applied) {
$query_conversations->where('user_id', '=', $user_id);
$assignee_condition_applied = true;
}
} else {
$query_conversations->where('user_id', '=', $user_id)
->orWhere('created_by_user_id', '=', $user_id);
}
}
}
if ($folder->type == Folder::TYPE_MINE && !$assignee_condition_applied) {
$query_conversations->where('user_id', $user_id);
}
return \Eventy::filter('folder.conversations_query', $query_conversations, $folder, $user_id);
}
@@ -1134,9 +1173,12 @@ class Conversation extends Model
*/
public function getSignatureProcessed($data = [], $escape = false)
{
$replaced_text = $this->replaceTextVars( $this->mailbox->signature, $data, $escape );
$replaced_text = $this->replaceTextVars($this->mailbox->signature, $data, $escape);
return \Eventy::filter( 'conversation.signature_processed', $replaced_text, $this, $data, $escape );
// https://github.com/freescout-helpdesk/freescout/security/advisories/GHSA-fffc-phh8-5h4v
$replaced_text = \Helper::stripDangerousTags($replaced_text);
return \Eventy::filter('conversation.signature_processed', $replaced_text, $this, $data, $escape);
}
/**
@@ -1238,6 +1280,8 @@ class Conversation extends Model
}
}
// Remember original mailbox ID.
$this->setMeta('orig_mailbox_id', $this->mailbox_id);
// We don't know how to replace $this->mailbox object.
$this->mailbox_id = $mailbox->id;
// Check assignee.
@@ -1630,7 +1674,7 @@ class Conversation extends Model
}
/**
* Get emails which are excluded from CC and BCC.
* Get emails which should be excluded from CC and BCC.
*/
public function getExcludeArray($mailbox = null)
{
@@ -1661,6 +1705,14 @@ class Conversation extends Model
return ($this->type == self::TYPE_PHONE);
}
/**
* Is it as custom conversation.
*/
public function isCustom()
{
return ($this->type == self::TYPE_CUSTOM);
}
/**
* Is it as chat conversation.
*/
@@ -1722,6 +1774,16 @@ class Conversation extends Model
return $viewers;
}
public function changeSubject($new_subject, $user = null)
{
$prev_subject = $this->subject;
$this->subject = $new_subject;
$this->save();
\Eventy::action('conversation.subject_changed', $this, $user, $prev_subject);
}
public function changeState($new_state, $user = null)
{
if (!array_key_exists($new_state, self::$states)) {
@@ -1796,9 +1858,9 @@ class Conversation extends Model
\Eventy::action('conversation.user_changed', $this, $user, $prev_user_id);
}
public function deleteToFolder($user)
public function deleteToFolder($user, $update_folders_counters = true)
{
$folder_id = $this->getCurrentFolder();
//$folder_id = $this->getCurrentFolder();
$prev_state = $this->state;
$this->state = Conversation::STATE_DELETED;
@@ -1824,8 +1886,10 @@ class Conversation extends Model
// Remove conversation from drafts folder.
$this->removeFromFolder(Folder::TYPE_DRAFTS);
// Recalculate only old and new folders
$this->mailbox->updateFoldersCounters();
// Recalculate only old and new folders.
if ($update_folders_counters) {
$this->mailbox->updateFoldersCounters();
}
\Eventy::action('conversation.deleted', $this, $user);
\Eventy::action('conversation.state_changed', $this, $user, $prev_state);
@@ -2164,6 +2228,8 @@ class Conversation extends Model
'order' => 'desc',
];
$result = \Eventy::filter('conversations.table_sorting', $result);
if (
!empty($request->sorting['sort_by']) && !empty($request->sorting['order']) &&
in_array($request->sorting['sort_by'], ['subject', 'number', 'date']) &&
@@ -2176,7 +2242,7 @@ class Conversation extends Model
return $result;
}
public static function search($q, $filters, $user = null, $query_conversations = null)
public static function search($q, $filters, $user = null, $query_conversations = null, $group_by = [])
{
$mailbox_ids = [];
@@ -2189,7 +2255,7 @@ class Conversation extends Model
// https://github.com/laravel/framework/issues/21242
// https://github.com/laravel/framework/pull/27675
$query_conversations->groupby('conversations.id');
$query_conversations->groupBy(array_merge(['conversations.id'], $group_by));
if (!empty($filters['mailbox'])) {
// Check if the user has access to the mailbox.
@@ -2213,12 +2279,18 @@ class Conversation extends Model
if ($q) {
$query_conversations->where(function ($query) use ($like, $filters, $q, $like_op) {
// It needs to be sanitized to avoid "Numeric value out of range" on PostgreSQL.
$q_int = (int)$q;
$q_int = $q_int > \Helper::DB_INT_MAX ? \Helper::DB_INT_MAX : $q_int;
$query->where('conversations.subject', $like_op, $like)
->orWhere('conversations.customer_email', $like_op, $like)
->orWhere('conversations.'.self::numberFieldName(), (int)$q)
->orWhere('conversations.id', (int)$q)
->orWhere('conversations.'.self::numberFieldName(), $q_int)
->orWhere('conversations.id', $q_int)
->orWhere('customers.first_name', $like_op, $like)
->orWhere('customers.last_name', $like_op, $like)
->orWhere(\Helper::isPgSql() ? \DB::raw('(customers.first_name || \' \' || customers.last_name)') : \DB::raw('CONCAT(customers.first_name, " ", customers.last_name)'), $like_op, $like)
->orWhere('threads.body', $like_op, $like)
->orWhere('threads.from', $like_op, $like)
->orWhere('threads.to', $like_op, $like)
@@ -2293,15 +2365,15 @@ class Conversation extends Model
$query_conversations->where('conversations.created_at', '<=', date('Y-m-d 23:59:59', strtotime($filters['before'])));
}
// Join tables if needed
// Join tables if needed.
$query_sql = $query_conversations->toSql();
if (!strstr($query_sql, '`threads`.`conversation_id`')) {
if (!self::queryContainsStr($query_sql, '`threads`.`conversation_id`')) {
$query_conversations->join('threads', function ($join) {
$join->on('conversations.id', '=', 'threads.conversation_id');
});
}
if (!strstr($query_sql, '`customers`.`id`')) {
if (!self::queryContainsStr($query_sql, '`customers`.`id`')) {
$query_conversations->leftJoin('customers', 'conversations.customer_id', '=' ,'customers.id');
}
@@ -2408,4 +2480,34 @@ class Conversation extends Model
return $chats;
}
public static function queryContainsStr($query_str, $substr)
{
$regex = preg_quote($substr);
$regex = str_replace('`', '[`"]', $regex);
return preg_match('#'.$regex.'#', $query_str);
}
public function userHasAccessToMailbox($user_id)
{
return MailboxUser::where('mailbox_id', $this->mailbox_id)
->where('user_id', $user_id)
->exists();
}
public function chatShouldStartNew($mailbox = null)
{
if (!$mailbox) {
$mailbox = $this->mailbox;
}
if (!empty($mailbox->meta['chat_start_new'])
&& ($this->status == Conversation::STATUS_CLOSED
|| $this->state == Conversation::STATE_DELETED)
) {
return true;
} else {
return false;
}
}
}

View File

@@ -454,7 +454,7 @@ class Customer extends Model
*
* @var [type]
*/
protected $fillable = ['first_name', 'last_name', 'company', 'job_title', 'address', 'city', 'state', 'zip', 'country', 'photo_url', 'age', 'gender', 'notes', 'channel', 'channel_id', 'social_profiles'];
protected $fillable = ['first_name', 'last_name', 'company', 'job_title', 'address', 'city', 'state', 'zip', 'country', 'photo_url', 'notes', 'channel', 'channel_id', 'social_profiles'];
/**
* Fields stored as JSON.
@@ -501,6 +501,14 @@ class Customer extends Model
return Email::select('email')->where('customer_id', $customer_id)->pluck('email');
}
/**
* Check if the customer has an email address among his emails.
*/
public function hasEmail($email_address)
{
return $this->emails_cached()->where('email', Email::sanitizeEmail($email_address))->exists();
}
/**
* Get customer full name.
*
@@ -1013,6 +1021,14 @@ class Customer extends Model
$result = true;
} else {
// Update empty fields.
// Do not set last name if first name is already set (and vise versa).
if (!empty($this->first_name) && !empty($data['last_name'])) {
unset($data['last_name']);
}
if (!empty($this->last_name) && !empty($data['first_name'])) {
unset($data['first_name']);
}
foreach ($data as $key => $value) {
if (in_array($key, $this->fillable) && empty($this->$key)) {
$this->$key = $value;
@@ -1271,6 +1287,84 @@ class Customer extends Model
return Storage::url(self::PHOTO_DIRECTORY.DIRECTORY_SEPARATOR.$file_name);
}
/**
* Merge customerю
*/
public function mergeWith(Customer $customer2)
{
$user = auth()->user();
$customer2->conversations()->update(['customer_id' => $this->id]);
// do {
// $conversations = $customer2->conversations()->limit(1000);
// foreach ($conversations as $conversation) {
// $conversation->changeCustomer($customer_email, $this, $user);
// }
// } while (count($conversations) > 0);
// Move emails.
$customer2->emails()->update(['customer_id' => $this->id]);
// Merge attributes
// if (in_array('phone', $keepAttributes) && !$this->phone) {
// $this->phone = $customerToMerge->phone;
// }
foreach ($this->fillable as $attr_name) {
// Skip some attribues.
if (in_array($attr_name, ['channel', 'channel_id'])) {
continue;
}
if (!$this->$attr_name) {
$this->$attr_name = $customer2->$attr_name;
}
}
// Merge Phones.
$phones = self::mergeTypeValueLists($this->getPhones(), $customer2->getPhones());
if (count($phones) != count($this->getPhones())) {
$this->setPhones($phones);
}
// Merge websites.
$websites = array_merge($this->getWebsites(), $customer2->getWebsites());
$this->setWebsites(array_unique($websites));
// Merge social profiles.
$social = self::mergeTypeValueLists($this->getSocialProfiles(), $customer2->getSocialProfiles());
if (count($social) != count($this->getSocialProfiles())) {
$this->setSocialProfiles($social);
}
$this->save();
\Eventy::action('customer.merged', $this, $customer2, $user);
$customer2->delete();
}
public static function mergeTypeValueLists($list1, $list2)
{
foreach ($list2 as $data2) {
if (empty($data2['type']) || empty($data2['value'])) {
continue;
}
$exists = false;
foreach ($list1 as $data) {
if (!empty($data['type']) && !empty($data['value'])
&& $data['type'] == $data2['type'] && $data['value'] == $data2['value']
) {
$exists = true;
break;
}
}
if (!$exists) {
$list1[] = $data2;
}
}
return $list1;
}
/**
* Resize and save photo.
*/
@@ -1529,5 +1623,17 @@ class Customer extends Model
return $customers;
}
}
/**
* Get dummy customer.
*/
public static function getDummyCustomer()
{
$customer = new self();
$customer->first_name = __('Customer');
$customer->last_name = '';
return $customer;
}
}

View File

@@ -28,4 +28,9 @@ class FailedJob extends Model
{
return \App\Job::getPayloadCommand($this->getPayloadDecoded());
}
public static function retry($job_id)
{
\Artisan::call('queue:retry', ['id' => $job_id]);
}
}

View File

@@ -224,7 +224,9 @@ class Folder extends Model
public function updateCounters()
{
if (config('app.update_folder_counters_in_background')) {
\App\Jobs\UpdateFolderCounters::dispatch($this);
if(!\Illuminate\Support\Facades\Cache::has("folder_update_lock_{$this->id}")) {
\App\Jobs\UpdateFolderCounters::dispatch($this);
}
} else {
$this->updateCountersNow();
}

View File

@@ -199,6 +199,25 @@ class ConversationsController extends Controller
}
}
$threads = $conversation->threads()->orderBy('created_at', 'desc')->get();
// Get To for new conversation.
$new_conv_to = [];
if (empty($threads[0]) || empty($threads[0]->to)) {
// Before new conversation To field was stored in $conversation->customer_email.
$emails = Conversation::sanitizeEmails($conversation->customer_email);
// Get customers info for emails.
if (count($emails)) {
$new_conv_to = Customer::emailsToCustomers($emails);
}
} else {
$new_conv_to = Customer::emailsToCustomers($threads[0]->getToArray());
}
if (empty($customer) && count($new_conv_to) == 1) {
$customer = Customer::getByEmail(array_key_first($new_conv_to));
}
// Previous conversations
$prev_conversations = [];
if ($customer) {
@@ -217,7 +236,24 @@ class ConversationsController extends Controller
$template = 'conversations/create';
}
// CC.
$exclude_array = $conversation->getExcludeArray($mailbox);
$cc = $conversation->getCcArray($exclude_array);
// If last reply came from customer who was mentioned in CC before,
// we need to add this customer as CC.
// https://github.com/freescout-helpdesk/freescout/issues/3613
foreach ($threads as $thread) {
if ($thread->isUserMessage() && !$thread->isDraft()) {
break;
}
if ($thread->isCustomerMessage()) {
if ($thread->customer_id != $conversation->customer_id) {
$cc[] = $thread->from;
}
break;
}
}
// Get data for creating a phone conversation.
$name = [];
@@ -282,8 +318,6 @@ class ConversationsController extends Controller
\Eventy::action('conversation.view.start', $conversation, $request);
$threads = $conversation->threads()->orderBy('created_at', 'desc')->get();
// Mailbox aliases.
$from_aliases = $conversation->mailbox->getAliases(true, true);
$from_alias = '';
@@ -324,19 +358,6 @@ class ConversationsController extends Controller
}
}
// Get To for new conversation draft.
$new_conv_to = [];
if (empty($threads[0]) || empty($threads[0]->to)) {
// Before new conversation To field was stored in $conversation->customer_email.
$emails = Conversation::sanitizeEmails($conversation->customer_email);
// Get customers info for emails.
if (count($emails)) {
$new_conv_to = Customer::emailsToCustomers($emails);
}
} else {
$new_conv_to = Customer::emailsToCustomers($threads[0]->getToArray());
}
return view($template, [
'conversation' => $conversation,
'mailbox' => $conversation->mailbox,
@@ -348,7 +369,7 @@ class ConversationsController extends Controller
'to' => $new_conv_to,
'to_customers' => $to_customers,
'prev_conversations' => $prev_conversations,
'cc' => $conversation->getCcArray($exclude_array),
'cc' => $cc,
'bcc' => [], //$conversation->getBccArray($exclude_array),
// Data for creating a phone conversation.
'name' => $name,
@@ -382,13 +403,14 @@ class ConversationsController extends Controller
// Create conversation from thread
$thread = null;
$attachments = [];
if (!empty($request->from_thread_id)) {
$orig_thread = Thread::find($request->from_thread_id);
if ($orig_thread) {
if ($orig_thread && auth()->user()->can('view', $orig_thread->conversation)) {
$subject = $orig_thread->conversation->subject;
$subject = preg_replace('/^Fwd:/i', 'Re: ', $subject);
$thread = new \App\Thread();
$thread = new Thread();
$thread->body = $orig_thread->body;
// If this is a forwarded message, try to fetch From
preg_match_all("/From:[^<\n]+<([^<\n]+)>/m", html_entity_decode(strip_tags($thread->body)), $m);
@@ -401,16 +423,33 @@ class ConversationsController extends Controller
}
}
}
// Clone attachments.
$orig_attachments = Attachment::where('thread_id', $orig_thread->id)->get();
if (count($orig_attachments)) {
$conversation->has_attachments = true;
$thread->has_attachments = true;
foreach ($orig_attachments as $attachment) {
$attachments[] = $attachment->duplicate();
}
}
}
}
$to = [];
// Prefill some values.
$prefill_to = \App\Email::sanitizeEmail($request->get('to'));
if ($prefill_to) {
$to = [$prefill_to => $prefill_to];
if ($request->get('to')) {
$prefill_to_emails = explode(',', $request->get('to'));
foreach ($prefill_to_emails as $prefill_to_email) {
$prefill_to_email = \App\Email::sanitizeEmail($prefill_to_email);
if ($prefill_to_email) {
$to[$prefill_to_email] = $prefill_to_email;
}
}
}
$conversation->subject = $subject;
return view('conversations/create', [
@@ -422,6 +461,7 @@ class ConversationsController extends Controller
'after_send' => $after_send,
'to' => $to,
'from_aliases' => $mailbox->getAliases(true, true),
'attachments' => $attachments,
]);
}
@@ -469,7 +509,6 @@ class ConversationsController extends Controller
$conversation->user_id = $orign_conv->user_id;
$conversation->updateFolder();
$conversation->save();
$thread = Thread::createExtended([
'conversation_id' => $orig_thread->conversation_id,
@@ -481,8 +520,8 @@ class ConversationsController extends Controller
'headers' => $orig_thread->headers,
'from' => $orig_thread->from,
'to' => $orig_thread->to,
'cc' => $orig_thread->cc,
'bcc' => $orig_thread->bcc,
'cc' => $orig_thread->getCcArray(),
'bcc' => $orig_thread->getBccArray(),
//'attachments' => $attachments,
'has_attachments' => $orig_thread->has_attachments,
'message_id' => "clone".crc32(microtime()).'-'.$orig_thread->message_id,
@@ -682,6 +721,11 @@ class ConversationsController extends Controller
$is_phone = true;
}
$is_custom = false;
if ($type == Conversation::TYPE_CUSTOM) {
$is_custom = true;
}
$is_create = false;
if (!empty($request->is_create)) {
//if ($new || ($from_draft && $conversation->threads_count == 1)) {
@@ -701,7 +745,7 @@ class ConversationsController extends Controller
// If reply is being created from draft, there is already thread created
$thread = null;
$from_draft = false;
if ((!$is_note || $is_phone) && !$response['msg'] && !empty($request->thread_id)) {
if (( ! $is_note || $is_phone || $is_custom ) && ! $response['msg'] && ! empty($request->thread_id)) {
$thread = Thread::find($request->thread_id);
if ($thread && (!$conversation || $thread->conversation_id != $conversation->id)) {
$response['msg'] = __('Incorrect thread');
@@ -727,7 +771,7 @@ class ConversationsController extends Controller
'cc' => 'nullable|array',
'bcc' => 'nullable|array',
]);
} else {
} elseif ($type === Conversation::TYPE_PHONE) {
// Phone conversation.
$validator = Validator::make($request->all(), [
'name' => 'required|string',
@@ -736,6 +780,13 @@ class ConversationsController extends Controller
'phone' => 'nullable|string',
'to_email' => 'nullable|string',
]);
} elseif ($type === Conversation::TYPE_CUSTOM) {
$validation_rules = \Eventy::filter('conversation.custom.validation_rules', [
'body' => 'required|string',
'cc' => 'nullable|array',
'bcc' => 'nullable|array',
], $request);
$validator = Validator::make($request->all(), $validation_rules);
}
} else {
$validator = Validator::make($request->all(), [
@@ -769,7 +820,7 @@ class ConversationsController extends Controller
$to_array = Conversation::sanitizeEmails($request->to);
}
// Check To
if (!$response['msg'] && $new && !$is_phone) {
if (! $response['msg'] && $new && ! $is_phone && ! $is_custom) {
if (!$to_array) {
$response['msg'] .= __('Incorrect recipients');
}
@@ -777,6 +828,7 @@ class ConversationsController extends Controller
// Check max. message size.
if (!$response['msg']) {
$max_message_size = (int)config('app.max_message_size');
if ($max_message_size) {
// Todo: take into account conversation history.
@@ -784,6 +836,7 @@ class ConversationsController extends Controller
// Calculate attachments size.
$attachments_ids = array_merge($request->attachments ?? [], $request->embeds ?? []);
$attachments_ids = $this->decodeAttachmentsIds($attachments_ids);
if (count($attachments_ids)) {
$attachments_to_check = Attachment::select('size')->whereIn('id', $attachments_ids)->get();
@@ -815,6 +868,11 @@ class ConversationsController extends Controller
$now = date('Y-m-d H:i:s');
$status_changed = false;
$user_changed = false;
// Chat conversations in chat mode can not be undone.
$can_undo = true;
$request_status = (int)$request->status;
if ($new) {
// New conversation
$conversation = new Conversation();
@@ -827,7 +885,7 @@ class ConversationsController extends Controller
$conversation->source_type = Conversation::SOURCE_TYPE_WEB;
} else {
// Reply or note
if ((int) $request->status != (int) $conversation->status) {
if ($request_status && $request_status != (int)$conversation->status) {
$status_changed = true;
}
if (!empty($request->subject)) {
@@ -865,14 +923,16 @@ class ConversationsController extends Controller
$customer_email = $phone_customer_data['customer_email'];
$customer = $phone_customer_data['customer'];
if (!$conversation->customer_id) {
if (! $conversation->customer_id) {
$conversation->customer_id = $customer->id;
}
} elseif ($is_custom) {
// No customer for custom conversations.
} else {
// Email or reply to a phone conversation.
if (!empty($to_array)) {
$customer_email = $to_array[0];
} elseif (!$conversation->customer_email
} elseif (!$conversation->customer_email
&& ($conversation->isEmail() || $conversation->isPhone())
&& $conversation->customer_id
&& $conversation->customer
@@ -894,7 +954,7 @@ class ConversationsController extends Controller
$prev_status = $conversation->status;
$conversation->status = $request->status;
$conversation->status = $request_status ?: $conversation->status;
if (($prev_status != $conversation->status || $is_create)
&& $conversation->status == Conversation::STATUS_CLOSED
@@ -944,13 +1004,18 @@ class ConversationsController extends Controller
if (!$is_note && !$is_forward) {
// Save extra recipients to CC
$cc = Conversation::sanitizeEmails($request->cc);
if ($is_create && !$is_multiple && count($to_array) > 1) {
$conversation->setCc(array_merge(Conversation::sanitizeEmails($request->cc), $to_array));
// First recipient becomes To, others - go to CC.
$remaining_to = array_diff($to_array, [$to]);
$cc = array_diff($cc, [$to]);
$cc = array_merge($cc, $remaining_to);
$conversation->setCc($cc);
} else {
if (!$is_multiple) {
$conversation->setCc(array_merge(Conversation::sanitizeEmails($request->cc), [$to]));
$conversation->setCc(array_diff($cc, [$to]));
} else {
$conversation->setCc(Conversation::sanitizeEmails($request->cc));
$conversation->setCc($cc);
}
}
$conversation->setBcc($request->bcc);
@@ -1018,9 +1083,11 @@ class ConversationsController extends Controller
$thread->first = true;
}
$thread->user_id = $conversation->user_id;
$thread->status = $request->status;
$thread->status = $request_status ?? $conversation->status;
$thread->state = Thread::STATE_PUBLISHED;
$thread->customer_id = $customer->id;
if (!$is_custom) {
$thread->customer_id = $customer->id;
}
$thread->created_by_user_id = auth()->user()->id;
$thread->edited_by_user_id = null;
$thread->edited_at = null;
@@ -1159,7 +1226,9 @@ class ConversationsController extends Controller
}
}
} else {
Attachment::whereIn('id', $attachments_info['attachments'])->update(['thread_id' => $thread->id]);
Attachment::whereIn('id', $attachments_info['attachments'])
->where('thread_id', null)
->update(['thread_id' => $thread->id]);
}
}
@@ -1170,13 +1239,17 @@ class ConversationsController extends Controller
$user->followConversation($conversation->id);
}
if ($conversation->isChat() && \Helper::isChatMode()) {
$can_undo = false;
}
// When user creates a new conversation it may be saved as draft first.
if ($is_create) {
// New conversation.
event(new UserCreatedConversation($conversation, $thread));
\Eventy::action('conversation.created_by_user_can_undo', $conversation, $thread);
// After Conversation::UNDO_TIMOUT period trigger final event.
\Helper::backgroundAction('conversation.created_by_user', [$conversation, $thread], now()->addSeconds(Conversation::UNDO_TIMOUT));
\Helper::backgroundAction('conversation.created_by_user', [$conversation, $thread], now()->addSeconds($this->getUndoTimeout($can_undo)));
} elseif ($is_forward) {
// Forward.
// Notifications to users not sent.
@@ -1199,7 +1272,7 @@ class ConversationsController extends Controller
event(new UserReplied($conversation, $thread));
\Eventy::action('conversation.user_replied_can_undo', $conversation, $thread);
// After Conversation::UNDO_TIMOUT period trigger final event.
\Helper::backgroundAction('conversation.user_replied', [$conversation, $thread], now()->addSeconds(Conversation::UNDO_TIMOUT));
\Helper::backgroundAction('conversation.user_replied', [$conversation, $thread], now()->addSeconds($this->getUndoTimeout($can_undo)));
}
// Send new conversation separately to each customer.
@@ -1243,6 +1316,9 @@ class ConversationsController extends Controller
// Reload the conversation, otherwise Thread observer will be
// increasing threads_count for the first conversation.
$thread_copy->load('conversation');
\Eventy::action('thread.before_save_from_request', $thread_copy, $request);
$thread_copy->push();
// Copy attachments.
@@ -1258,7 +1334,7 @@ class ConversationsController extends Controller
event(new UserCreatedConversation($conversation_copy, $thread_copy));
\Eventy::action('conversation.created_by_user_can_undo', $conversation_copy, $thread_copy);
// After Conversation::UNDO_TIMOUT period trigger final event.
\Helper::backgroundAction('conversation.created_by_user', [$conversation_copy, $thread_copy], now()->addSeconds(Conversation::UNDO_TIMOUT));
\Helper::backgroundAction('conversation.created_by_user', [$conversation_copy, $thread_copy], now()->addSeconds($this->getUndoTimeout($can_undo)));
}
}
@@ -1277,6 +1353,14 @@ class ConversationsController extends Controller
} else {
$flash_text = '<strong>'.__('Conversation created').'</strong>';
}
} elseif ($is_custom) {
$flash_type = 'warning';
$identifier = \Eventy::filter('conversation.custom.identifier', __('Custom conversation'), $request);
if ($show_view_link) {
$flash_text = __(':%tag_start%' . $identifier . ' added:%tag_end% :%view_start%View:%a_end%', $flash_vars);
} else {
$flash_text = '<strong>'.__('%identifier% added',['%identifier%'=>$identifier]).'</strong>';
}
} elseif ($is_note) {
$flash_type = 'warning';
if ($show_view_link) {
@@ -1293,7 +1377,9 @@ class ConversationsController extends Controller
}
}
\Session::flash('flash_'.$flash_type.'_floating', $flash_text);
if ($can_undo) {
\Session::flash('flash_'.$flash_type.'_floating', $flash_text);
}
}
break;
@@ -1310,7 +1396,7 @@ class ConversationsController extends Controller
$new = true;
if (!$response['msg'] && !empty($request->conversation_id)) {
$conversation = Conversation::find($request->conversation_id);
if ($conversation && !$user->can('view', $conversation)) {
if ($conversation && !$user->can('view', $conversation) && !$user->hasManageMailboxPermission($request->mailbox_id, Mailbox::ACCESS_PERM_ASSIGNED)) {
$response['msg'] = __('Not enough permissions');
} else {
$new = false;
@@ -1322,6 +1408,12 @@ class ConversationsController extends Controller
$is_create = true;
}
// If new conversation draft has been discarded (by some other user for example).
// https://github.com/freescout-helpdesk/freescout/issues/3951
if (!$response['msg'] && $is_create && !$conversation) {
$new = true;
}
$thread = null;
$new_thread = true;
if (!$response['msg'] && !empty($request->thread_id)) {
@@ -1521,7 +1613,9 @@ class ConversationsController extends Controller
// Set thread_id for uploaded attachments
if ($attachments_info['attachments']) {
Attachment::whereIn('id', $attachments_info['attachments'])->update(['thread_id' => $thread->id]);
Attachment::whereIn('id', $attachments_info['attachments'])
->where('thread_id', null)
->update(['thread_id' => $thread->id]);
}
// Update folder counter.
@@ -1624,7 +1718,7 @@ class ConversationsController extends Controller
$attachments = [];
foreach ($thread->attachments as $attachment) {
$attachments[] = [
'id' => $attachment->id,
'id' => encrypt($attachment->id),
'name' => $attachment->file_name,
'size' => $attachment->size,
'url' => $attachment->url(),
@@ -1663,16 +1757,16 @@ class ConversationsController extends Controller
if ($conversation->has_attachments) {
foreach ($conversation->threads as $thread) {
if ($thread->has_attachments) {
if ($thread->has_attachments && (!$thread->isDraft() || count($conversation->threads) == 1)) {
foreach ($thread->attachments as $attachment) {
if ($request->is_forwarding == 'true') {
$attachment_copy = $attachment->duplicate($thread->id);
$attachment_copy = $attachment->duplicate();
} else {
$attachment_copy = $attachment;
}
$attachments[] = [
'id' => $attachment_copy->id,
'id' => encrypt($attachment_copy->id),
'name' => $attachment_copy->file_name,
'size' => $attachment_copy->size,
'url' => $attachment_copy->url(),
@@ -1717,6 +1811,14 @@ class ConversationsController extends Controller
// Conversations navigation
case 'conversations_pagination':
if (!empty($request->filter)) {
// Filter conversations by Assigned To column in Search.
if (!empty($request->params['user_id']) && !empty($request->filter['f'])) {
$filter = $request->filter ?? [];
$filter['f']['assigned'] = (int)$request->params['user_id'];
$request->merge(['filter' => $filter]);
}
$response = $this->ajaxConversationsFilter($request, $response, $user);
} else {
$response = $this->ajaxConversationsPagination($request, $response, $user);
@@ -1867,6 +1969,8 @@ class ConversationsController extends Controller
}
if (!$response['msg']) {
$thread->body = \Helper::stripDangerousTags($thread->body);
$data = [
'thread' => $thread
];
@@ -2002,27 +2106,7 @@ class ConversationsController extends Controller
if ($conversation->state != Conversation::STATE_DELETED) {
// Move to Deleted folder.
$conversation->state = Conversation::STATE_DELETED;
$conversation->user_updated_at = date('Y-m-d H:i:s');
$conversation->updateFolder();
$conversation->save();
// Create lineitem thread
$thread = new Thread();
$thread->conversation_id = $conversation->id;
$thread->user_id = $conversation->user_id;
$thread->type = Thread::TYPE_LINEITEM;
$thread->state = Thread::STATE_PUBLISHED;
$thread->status = Thread::STATUS_NOCHANGE;
$thread->action_type = Thread::ACTION_TYPE_DELETED_TICKET;
$thread->source_via = Thread::PERSON_USER;
$thread->source_type = Thread::SOURCE_TYPE_WEB;
$thread->customer_id = $conversation->customer_id;
$thread->created_by_user_id = $user->id;
$thread->save();
// Remove conversation from drafts folder.
$conversation->removeFromFolder(Folder::TYPE_DRAFTS);
$conversation->deleteToFolder($user, false);
} else {
// Delete forever
$conversation->deleteForever();
@@ -2137,20 +2221,37 @@ class ConversationsController extends Controller
$response['msg'] = __('Not enough permissions');
}
$merge_conversation = Conversation::find($request->merge_conversation_id);
if (!empty($request->merge_conversation_id) && is_array($request->merge_conversation_id)) {
$sigle_conv = count($request->merge_conversation_id) == 1;
if (!$merge_conversation) {
$response['msg'] = __('Conversation not found');
}
if (!$response['msg'] && !$user->can('view', $merge_conversation)) {
$response['msg'] = __('Not enough permissions');
}
foreach ($request->merge_conversation_id as $merge_conversation_id) {
$merge_conversation = Conversation::find($merge_conversation_id);
if (!$response['msg']) {
$conversation->mergeConversations($merge_conversation, $user);
$response['msg'] = '';
$response['status'] = 'success';
\Session::flash('flash_success_floating', __('Conversations merged'));
if (!$merge_conversation) {
$response['msg'] = __('Conversation not found');
if ($sigle_conv) {
break;
}
}
if (!$response['msg'] && !$user->can('view', $merge_conversation)) {
$response['msg'] = __('Not enough permissions').': #'.$merge_conversation->number;
if ($sigle_conv) {
break;
}
}
if (!$response['msg']) {
$conversation->mergeConversations($merge_conversation, $user);
if ($response['status'] != 'success') {
\Session::flash('flash_success_floating', __('Conversations merged'));
}
$response['status'] = 'success';
}
}
}
break;
@@ -2163,7 +2264,7 @@ class ConversationsController extends Controller
if (!$conversation) {
$response['msg'] = __('Conversation not found');
}
if (!$response['msg'] && !$conversation->mailbox->userHasAccess($user->id)) {
if (!$response['msg'] && !$user->can('view', $conversation)) {
$response['msg'] = __('Not enough permissions');
}
@@ -2195,7 +2296,7 @@ class ConversationsController extends Controller
if (!$conversation) {
$response['msg'] = __('Conversation not found');
}
if (!$response['msg'] && !$conversation->mailbox->userHasAccess($user->id)) {
if (!$response['msg'] && !$user->can('update', $conversation)) {
$response['msg'] = __('Not enough permissions');
}
@@ -2203,8 +2304,7 @@ class ConversationsController extends Controller
$subject = trim($subject);
if (!$response['msg'] && $subject) {
$conversation->subject = $subject;
$conversation->save();
$conversation->changeSubject($subject, $user);
$response['status'] = 'success';
}
@@ -2246,7 +2346,62 @@ class ConversationsController extends Controller
])->render();
$response['status'] = 'success';
}
break;
case 'retry_send':
$thread = Thread::find($request->thread_id);
if (!$thread) {
$response['msg'] = __('Thread not found');
} elseif (!$user->can('view', $thread->conversation)) {
$response['msg'] = __('Not enough permissions');
}
if (!$response['msg']) {
$job_id = $thread->getFailedJobId();
if ($job_id) {
\App\FailedJob::retry($job_id);
$thread->send_status = SendLog::STATUS_ACCEPTED;
$thread->updateSendStatusData(['msg' => '']);
$thread->save();
$response['status'] = 'success';
}
}
break;
case 'load_customer_info':
$customer = Customer::getByEmail($request->customer_email);
if ($customer) {
// Previous conversations
$prev_conversations = [];
$mailbox = Mailbox::find($request->mailbox_id);
if ($mailbox && $mailbox->userHasAccess($user->id)) {
$conversation_id = (int)$request->conversation_id ?? 0;
$prev_conversations = $mailbox->conversations()
->where('customer_id', $customer->id)
->where('id', '<>', $conversation_id)
->where('status', '!=', Conversation::STATUS_SPAM)
->where('state', Conversation::STATE_PUBLISHED)
//->limit(self::PREV_CONVERSATIONS_LIMIT)
->orderBy('created_at', 'desc')
->paginate(self::PREV_CONVERSATIONS_LIMIT);
}
$response['html'] = \View::make('conversations/partials/customer_sidebar')->with([
'customer' => $customer,
'prev_conversations' => $prev_conversations,
])->render();
$response['status'] = 'success';
} else {
$response['msg'] = 'Customer not found';
}
break;
default:
@@ -2277,6 +2432,8 @@ class ConversationsController extends Controller
return $this->ajaxHtmlMoveConv();
case 'merge_conv':
return $this->ajaxHtmlMergeConv();
case 'assignee_filter':
return $this->ajaxAssigneeFilter();
case 'default_redirect':
return $this->ajaxHtmlDefaultRedirect();
}
@@ -2415,9 +2572,11 @@ class ConversationsController extends Controller
abort(403);
}
$mailboxes = \Eventy::filter( 'conversations.move_conv.mailboxes', $user->mailboxesCanView() );
return view('conversations/ajax_html/move_conv', [
'conversation' => $conversation,
'mailboxes' => $user->mailboxesCanView(),
'mailboxes' => $mailboxes,
]);
}
@@ -2461,6 +2620,50 @@ class ConversationsController extends Controller
]);
}
/**
* Filter conversations by assignee.
*/
public function ajaxAssigneeFilter()
{
$users = collect([]);
$mailbox_id = Input::get('mailbox_id');
$user_id = Input::get('user_id');
$user = auth()->user();
if ($mailbox_id) {
$mailbox = Mailbox::find($mailbox_id);
if (!$mailbox) {
abort(404);
}
if (!$user->can('view', $mailbox)) {
\Helper::denyAccess();
}
// Show users having access to the mailbox.
$users = $mailbox->usersAssignable();
} else {
// Show users from all accessible mailboxes.
$mailboxes = $user->mailboxesCanView();
foreach ($mailboxes as $mailbox) {
$users = $users->merge($mailbox->usersAssignable())->unique('id');
}
}
if (!$users->contains('id', $user->id)) {
$users[] = $user;
}
// Sort by full name.
$users = User::sortUsers($users);
return view('conversations/ajax_html/assignee_filter', [
'users' => $users,
'user_id' => $user_id,
]);
}
/**
* Change default redirect for the mailbox.
*/
@@ -2522,7 +2725,8 @@ class ConversationsController extends Controller
$redirect_url = route('mailboxes.view.folder', ['id' => $conversation->mailbox_id, 'folder_id' => $folder_id]);
break;
case MailboxUser::AFTER_SEND_NEXT:
$redirect_url = $conversation->urlNext(Conversation::getFolderParam());
// We need to get not any next conversation, but ACTIVE next conversation.
$redirect_url = $conversation->urlNext(Conversation::getFolderParam(), Conversation::STATUS_ACTIVE, true);
break;
}
} else {
@@ -2574,7 +2778,7 @@ class ConversationsController extends Controller
if ($attachment) {
$response['status'] = 'success';
$response['url'] = $attachment->url();
$response['attachment_id'] = $attachment->id;
$response['attachment_id'] = encrypt($attachment->id);
} else {
$response['msg'] = __('Error occurred uploading file');
}
@@ -2609,6 +2813,11 @@ class ConversationsController extends Controller
if (!$response['msg']) {
$query_conversations = Conversation::getQueryByFolder($folder, $user->id);
if (!empty($request->params['user_id'])) {
$query_conversations->where('conversations.user_id', (int)$request->params['user_id']);
}
$conversations = $folder->queryAddOrderBy($query_conversations)->paginate(Conversation::DEFAULT_LIST_SIZE, ['*'], 'page', $request->page);
}
@@ -2617,6 +2826,7 @@ class ConversationsController extends Controller
$response['html'] = view('conversations/conversations_table', [
'folder' => $folder,
'conversations' => $conversations,
'params' => $request->params ?? [],
])->render();
return $response;
@@ -2792,6 +3002,8 @@ class ConversationsController extends Controller
*/
public function searchCustomers($request, $user)
{
$limited_visibility = config('app.limit_user_customer_visibility') && !$user->isAdmin();
// Get IDs of mailboxes to which user has access
$mailbox_ids = $user->mailboxesIdsCanView();
@@ -2818,9 +3030,9 @@ class ConversationsController extends Controller
$query->where('customers.first_name', $like_op, $like)
->orWhere('customers.last_name', $like_op, $like)
->orWhere(\Helper::isPgSql() ? \DB::raw('(customers.first_name || \' \' || customers.last_name)') : \DB::raw('CONCAT(customers.first_name, " ", customers.last_name)'), $like_op, $like)
->orWhere('customers.company', $like_op, $like)
->orWhere('customers.job_title', $like_op, $like)
->orWhere('customers.phones', $like_op, '%"'.\Helper::phoneToNumeric($q).'"%')
->orWhere('customers.websites', $like_op, $like)
->orWhere('customers.social_profiles', $like_op, $like)
->orWhere('customers.address', $like_op, $like)
@@ -2828,6 +3040,12 @@ class ConversationsController extends Controller
->orWhere('customers.state', $like_op, $like)
->orWhere('customers.zip', $like_op, $like)
->orWhere('emails.email', $like_op, $like);
$phone_numeric = \Helper::phoneToNumeric($q);
if ($phone_numeric) {
$query->orWhere('customers.phones', $like_op, '%"'.$phone_numeric.'"%');
}
});
if (!empty($filters['mailbox']) && in_array($filters['mailbox'], $mailbox_ids)) {
@@ -2836,6 +3054,12 @@ class ConversationsController extends Controller
//$join->on('conversations.mailbox_id', '=', $filters['mailbox']);
});
$query_customers->where('conversations.mailbox_id', '=', $filters['mailbox']);
} elseif ($limited_visibility) {
// Force only mailboxes the user has access to.
$query_customers->join('conversations', function ($join) use ($filters) {
$join->on('conversations.customer_id', '=', 'customers.id');
});
$query_customers->whereIn('conversations.mailbox_id', $mailbox_ids);
}
$query_customers = \Eventy::filter('search.customers.apply_filters', $query_customers, $filters, $q);
@@ -2874,6 +3098,7 @@ class ConversationsController extends Controller
$response['html'] = view('conversations/conversations_table', [
'conversations' => $conversations,
'params' => $request->params ?? [],
'conversations_filter' => $request->filter['f'] ?? $request->filter ?? [],
])->render();
return $response;
@@ -2898,6 +3123,10 @@ class ConversationsController extends Controller
}
}
if (!empty($request->params['user_id'])) {
$query_conversations->where('conversations.user_id', (int)$request->params['user_id']);
}
return $query_conversations->paginate(Conversation::DEFAULT_LIST_SIZE);
}
@@ -2910,17 +3139,20 @@ class ConversationsController extends Controller
$attachments = [];
if (!empty($request->attachments_all)) {
$embeds = [];
$attachments_all = $this->decodeAttachmentsIds($request->attachments_all);
if (!empty($request->attachments)) {
$attachments = $request->attachments;
$attachments = $this->decodeAttachmentsIds($request->attachments);
}
if (!empty($request->embeds)) {
$embeds = $request->embeds;
$embeds = $this->decodeAttachmentsIds($request->embeds);
}
if (count($attachments) != count($embeds)) {
$attachments_to_remove = array_diff($attachments_all, $attachments);
$attachments_to_remove = array_diff($attachments_to_remove, $embeds);
if (count($attachments)
&& count($attachments) != count($embeds)
) {
$has_attachments = true;
}
$attachments_to_remove = array_diff($request->attachments_all, $attachments);
$attachments_to_remove = array_diff($attachments_to_remove, $embeds);
Attachment::deleteByIds($attachments_to_remove);
}
@@ -2930,6 +3162,20 @@ class ConversationsController extends Controller
];
}
public function decodeAttachmentsIds($attachments_list)
{
foreach ($attachments_list as $i => $attachment_id) {
$attachment_id_decrypted = \Helper::decrypt($attachment_id);
if ($attachment_id_decrypted == $attachment_id) {
unset($attachments_list[$i]);
} else {
$attachments_list[$i] = $attachment_id_decrypted;
}
}
return $attachments_list;
}
/**
* Undo reply.
*/
@@ -2988,6 +3234,10 @@ class ConversationsController extends Controller
if ($thread->first) {
// This was a new conversation, move it to drafts
$conversation->state = Thread::STATE_DRAFT;
// Add a record to the conversation_folder table.
$conversation->addToFolder(Folder::TYPE_DRAFTS);
$conversation->updateFolder();
$conversation->mailbox->updateFoldersCounters();
$folder_id = null;
@@ -3031,7 +3281,9 @@ class ConversationsController extends Controller
if (!empty($name_parts[1])) {
$customer_data['last_name'] = $name_parts[1];
}
$customer_data['phones'] = [$request_phone];
if ($request_phone) {
$customer_data['phones'] = [$request_phone];
}
}
// Check if name field contains ID of the customer.
@@ -3125,4 +3377,13 @@ class ConversationsController extends Controller
'mailbox' => $mailbox,
]);
}
public function getUndoTimeout($can_undo)
{
if ($can_undo) {
return Conversation::UNDO_TIMOUT;
} else {
return 1;
}
}
}

View File

@@ -23,10 +23,12 @@ class CustomersController extends Controller
/**
* Edit customer.
*/
public function update($id)
public function update(Request $request, $id)
{
$customer = Customer::findOrFail($id);
$this->checkLimitVisibility($customer);
$customer_emails = $customer->emails;
if (count($customer_emails)) {
foreach ($customer_emails as $row) {
@@ -56,6 +58,8 @@ class CustomersController extends Controller
$customer = Customer::findOrFail($id);
$flash_message = '';
$this->checkLimitVisibility($customer);
// First name or email must be specified
$validator = Validator::make($request->all(), [
'first_name' => 'nullable|string|max:255|required_without:emails.0',
@@ -77,7 +81,7 @@ class CustomersController extends Controller
// Photo
$validator->after(function ($validator) use ($customer, $request) {
if ($request->hasFile('photo_url')) {
$path_url = $customer->savePhoto($request->file('photo_url')->getRealPath(), $request->file('photo_url')->getMimeType());
$path_url = $customer->savePhoto($request->file('photo_url')->getRealPath() ?: $request->file('photo_url')->getPathname(), $request->file('photo_url')->getMimeType());
if ($path_url) {
$customer->photo_url = $path_url;
@@ -126,7 +130,7 @@ class CustomersController extends Controller
'email' => $email->email,
'tag_email_begin' => '<strong>',
'tag_email_end' => '</strong>',
'customer' => $email->customer->getFullName(),
'customer' => htmlspecialchars($email->customer->getFullName()),
'a_begin' => '<strong><a href="'.$email->customer->url().'" target="_blank">',
'a_end' => '</a></strong>',
]).' ';
@@ -147,6 +151,15 @@ class CustomersController extends Controller
if (isset($request_data['photo_url'])) {
unset($request_data['photo_url']);
}
$nonfillable_fields = [
'channel',
'channel_id',
];
foreach ($nonfillable_fields as $field) {
if (isset($request_data[$field])) {
unset($request_data[$field]);
}
}
$customer->setData($request_data);
// Websites
@@ -193,6 +206,24 @@ class CustomersController extends Controller
return redirect()->route('customers.update', ['id' => $id]);
}
public function checkLimitVisibility($customer)
{
$user = auth()->user();
$limited_visibility = config('app.limit_user_customer_visibility') && !$user->isAdmin();
if ($limited_visibility) {
$mailbox_ids = $user->mailboxesIdsCanView();
$accesible = Conversation::where('customer_id', $customer->id)
->whereIn('conversations.mailbox_id', $mailbox_ids)
->exists();
if (!$accesible) {
\Helper::denyAccess();
}
}
}
/**
* User mailboxes.
*/
@@ -233,11 +264,18 @@ class CustomersController extends Controller
{
$customer = Customer::findOrFail($id);
$conversations = $customer->conversations()
$query = $customer->conversations()
->where('customer_id', $customer->id)
->whereIn('mailbox_id', auth()->user()->mailboxesIdsCanView())
->orderBy('created_at', 'desc')
->paginate(Conversation::DEFAULT_LIST_SIZE);
->orderBy('created_at', 'desc');
$user = auth()->user();
if ($user->canSeeOnlyAssignedConversations()) {
$query->where('user_id', '=', $user->id);
}
$conversations = $query->paginate(Conversation::DEFAULT_LIST_SIZE);
return view('customers/conversations', [
'customer' => $customer,
@@ -257,6 +295,9 @@ class CustomersController extends Controller
$q = $request->q;
$user = auth()->user();
$limited_visibility = config('app.limit_user_customer_visibility') && !$user->isAdmin();
$join_emails = false;
if ($request->search_by == 'all' || $request->search_by == 'email' || $request->exclude_email) {
$join_emails = true;
@@ -279,18 +320,36 @@ class CustomersController extends Controller
}
}
if ($request->search_by == 'all' || $request->search_by == 'email') {
$customers_query->where('emails.email', 'like', '%'.$q.'%');
}
if ($request->exclude_email) {
$customers_query->where('emails.email', '<>', $request->exclude_email);
}
if ($request->search_by == 'all' || $request->search_by == 'name') {
$customers_query->orWhere('first_name', 'like', '%'.$q.'%')
->orWhere('last_name', 'like', '%'.$q.'%');
}
if ($request->search_by == 'phone') {
$customers_query->where('customers.phones', 'like', '%'.$q.'%');
$customers_query->where(function ($query) use ($q, $request) {
if ($request->search_by == 'all' || $request->search_by == 'email') {
$query->where('emails.email', 'like', '%'.$q.'%');
}
if ($request->exclude_email) {
$query->where('emails.email', '<>', $request->exclude_email);
}
if ($request->exclude_id) {
$query->where('customers.id', '<>', $request->exclude_id);
}
if ($request->search_by == 'all' || $request->search_by == 'name') {
$query->orWhere('first_name', 'like', '%'.$q.'%')
->orWhere('last_name', 'like', '%'.$q.'%')
->orWhere(\Helper::isPgSql() ? \DB::raw('(first_name || \' \' || last_name)') : \DB::raw('CONCAT(first_name, " ", last_name)'), 'like', '%'.$q.'%');
}
if ($request->search_by == 'phone') {
$phone_numeric = \Helper::phoneToNumeric($q);
if (!$phone_numeric) {
$phone_numeric = $q;
}
$query->where('customers.phones', 'like', '%'.$phone_numeric.'%');
}
});
if ($limited_visibility) {
$mailbox_ids = $user->mailboxesIdsCanView();
$customers_query->join('conversations', 'conversations.customer_id', '=', 'customers.id');
$customers_query->whereIn('conversations.mailbox_id', $mailbox_ids);
$customers_query->groupby('customers.id');
}
$customers = $customers_query->paginate(20);
@@ -311,7 +370,8 @@ class CustomersController extends Controller
// Get phone which matches
$phones = $customer->getPhones();
foreach ($phones as $phone) {
if (strstr($phone['value'], $q)) {
$phone_numeric = \Helper::phoneToNumeric($q);
if (strstr($phone['value'], $q) || strstr($phone['n'] ?? '', $phone_numeric)) {
$text = $phone['value'];
if ($customer->getFullName()) {
$text .= ' — '.$customer->getFullName();
@@ -333,7 +393,8 @@ class CustomersController extends Controller
if (!empty($request->use_id)) {
$id = $customer->id;
} else {
$id = $customer->email;
// https://github.com/freescout-helpdesk/freescout/issues/4057
$id = $customer->email ?? $customer->getMainEmail();
}
}
$response['results'][] = [
@@ -361,14 +422,21 @@ class CustomersController extends Controller
switch ($request->action) {
// Change conversation user
// Change conversation customer.
case 'create':
// First name or email must be specified
$validator = Validator::make($request->all(), [
$validator_config = [
'first_name' => 'required|string|max:255',
'last_name' => 'nullable|string|max:255',
'email' => 'required|email|unique:emails,email',
]);
];
$limited_visibility = config('app.limit_user_customer_visibility') && !$user->isAdmin();
if ($limited_visibility) {
$validator_config['email'] = 'required|email';
}
// First name or email must be specified.
$validator = Validator::make($request->all(), $validator_config);
if ($validator->fails()) {
foreach ($validator->errors()->getMessages()as $errors) {
@@ -411,4 +479,41 @@ class CustomersController extends Controller
return \Response::json($response);
}
}
public function merge(Request $request, $id)
{
$customer = Customer::findOrFail($id);
// $customers = Customer::where('id', '!=', $id)
// ->orderBy('first_name')
// ->orderBy('last_name')
// ->get();
return view('customers/merge', ['customer' => $customer]);
}
/**
* Merge handling function.
*/
public function mergeSave(Request $request, $id)
{
$request->validate([
'customer2_id' => 'required|exists:customers,id',
//'keep_attributes' => 'array'
]);
$customer = Customer::findOrFail($id);
$customer2 = Customer::find($request->customer2_id);
// Ensure customers are different
if ($id === $customer2->id) {
return redirect()->back()->with('error', __('Cannot merge the same customer'));
}
$customer->mergeWith($customer2/*, $request->keep_attributes ?? []*/);
\Session::flash('flash_success_floating', __('Customers merged successfully'));
return redirect()->route('customers.update', ['id' => $id]);
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Conversation;
use App\Folder;
use App\Mailbox;
use App\MailboxUser;
use App\Thread;
use App\User;
use Illuminate\Http\Request;
@@ -85,8 +86,14 @@ class MailboxesController extends Controller
->withInput();
}
$request_data = [
'email' => $request->email,
'name' => trim(strip_tags($request->name)),
'ratings' => $request->ratings ?? 1,
];
$mailbox = new Mailbox();
$mailbox->fill($request->all());
$mailbox->fill($request_data);
$mailbox->save();
@@ -105,6 +112,8 @@ class MailboxesController extends Controller
{
$mailbox = Mailbox::findOrFail($id);
$user = auth()->user();
// Redirect to the accessible page.
if (!$user->can('updateSettings', $mailbox) && !$user->can('updateEmailSignature', $mailbox)) {
$accessible_route = '';
@@ -159,11 +168,16 @@ class MailboxesController extends Controller
$mailbox = Mailbox::findOrFail($id);
$user = auth()->user();
$can_update_settings = $user->can('updateSettings', $mailbox);
$can_update_signature = $user->can('updateEmailSignature', $mailbox);
if (!$user->can('updateSettings', $mailbox) && !$user->can('updateEmailSignature', $mailbox)) {
if (!$can_update_settings && !$can_update_signature) {
\Helper::denyAccess();
}
$allowed_fields = [];
if ($user->can('updateSettings', $mailbox)) {
// Checkboxes
@@ -182,7 +196,7 @@ class MailboxesController extends Controller
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:40',
'email' => 'required|string|email|max:128|unique:mailboxes,email,'.$id,
'aliases' => 'nullable|string|max:255',
'aliases' => 'nullable|string',
'from_name' => 'required|integer',
'from_name_custom' => 'nullable|string|max:128',
'ticket_status' => 'required|integer',
@@ -203,9 +217,22 @@ class MailboxesController extends Controller
->withErrors($validator)
->withInput();
}
$allowed_fields = [
'name',
'email',
'aliases',
'aliases_reply',
'auto_bcc',
'from_name',
'from_name_custom',
'ticket_status',
'ticket_assignee',
'before_reply',
];
}
if ($user->can('updateEmailSignature', $mailbox)) {
if ($can_update_signature) {
$validator = Validator::make($request->all(), [
'signature' => 'nullable|string',
]);
@@ -215,11 +242,29 @@ class MailboxesController extends Controller
->withErrors($validator)
->withInput();
}
$allowed_fields[] = 'signature';
}
\Eventy::action('mailbox.settings_before_save', $mailbox, $request);
$mailbox->fill($request->all());
// Leave only allowed fields.
$fields = $request->all();
foreach ($fields as $field_name => $value) {
if (!in_array($field_name, $allowed_fields)) {
unset($fields[$field_name]);
}
}
$mailbox->fill($fields);
// Chat: Start a new conversation when receiving a reply to the closed / deleted Chat conversation.
if (!empty($request->chat_start_new)) {
$mailbox->setMetaParam('chat_start_new', true);
} else {
$mailbox->removeMetaParam('chat_start_new');
}
$mailbox->signature = \Helper::stripDangerousTags($mailbox->signature);
$mailbox->save();
@@ -268,8 +313,24 @@ class MailboxesController extends Controller
$this->authorize('updatePermissions', $mailbox);
$user = auth()->user();
$user_ids = \Eventy::filter('mailbox.permission_users', $request->users ?? [], $id);
// Get pivot data from mailbox_user to pass to sync() not to loose values.
// https://github.com/freescout-help-desk/freescout/issues/4339
$users_with_settings = [];
$mailbox_users = MailboxUser::where('mailbox_id', $id)
//->whereIn('user_id', $user_ids)
->get()
->keyBy('user_id')
->toArray();
foreach ($user_ids as $user_id) {
$users_with_settings[$user_id] = $mailbox_users[$user_id] ?? [];
}
// Settings for admins are being reset here.
// So we restore them below.
$mailbox->users()->sync($users_with_settings);
$mailbox->users()->sync(\Eventy::filter('mailbox.permission_users', $request->users, $id) ?: []);
$mailbox->syncPersonalFolders($request->users);
// Save admins settings.
@@ -281,6 +342,15 @@ class MailboxesController extends Controller
$admin->mailboxes()->attach($id);
$mailbox_user = $admin->mailboxesWithSettings()->where('mailbox_id', $id)->first();
}
// Restore settings for admins.
if (!empty($mailbox_users[$admin->id])) {
$admin_settings = $mailbox_users[$admin->id];
foreach (MailboxUser::$pivot_settings as $pivot_parameter) {
if (isset($admin_settings[$pivot_parameter])) {
$mailbox_user->settings->$pivot_parameter = $admin_settings[$pivot_parameter];
}
}
}
$mailbox_user->settings->hide = (isset($request->managers[$admin->id]['hide']) ? (int)$request->managers[$admin->id]['hide'] : false);
$mailbox_user->settings->save();
}
@@ -560,7 +630,14 @@ class MailboxesController extends Controller
}
}
$mailbox->fill($request->all());
$data = [
'auto_reply_enabled' => $request->auto_reply_enabled,
'auto_reply_subject' => $request->auto_reply_subject,
'auto_reply_message' => $request->auto_reply_message,
];
$mailbox->fill($data);
$mailbox->auto_reply_message = \Helper::stripDangerousTags($mailbox->auto_reply_message);
$mailbox->save();
@@ -622,17 +699,22 @@ class MailboxesController extends Controller
}
if (!$response['msg']) {
$test_result = false;
$test_result = [
'status' => 'error'
];
try {
$test_result = \App\Misc\Mail::sendTestMail($request->to, $mailbox);
$test_result = \MailHelper::sendTestMail($request->to, $mailbox);
} catch (\Exception $e) {
$response['msg'] = $e->getMessage();
$test_result['msg'] = $e->getMessage();
}
if (!$test_result && !$response['msg']) {
$response['msg'] = __('Error occurred sending email. Please check your mail server logs for more details.');
if ($test_result['status'] == 'error') {
$response['msg'] = $test_result['msg']
?: __('Error occurred sending email. Please check your mail server logs for more details.');
}
$response['log'] = $test_result['log'] ?? '';
}
if (!$response['msg']) {
@@ -670,13 +752,14 @@ class MailboxesController extends Controller
if (!$response['msg'] && !$tested) {
$test_result = false;
try {
$test_result = \MailHelper::fetchTest($mailbox);
} catch (\Exception $e) {
$response['msg'] = $e->getMessage();
}
$test_result = \MailHelper::fetchTest($mailbox);
if (!$test_result && !$response['msg']) {
$response['log'] = $test_result['log'] ?? '';
if ($test_result['result'] != 'success' && $test_result['msg']) {
$response['msg'] = $test_result['msg'];
}
if ($test_result['result'] != 'success' && !$response['msg']) {
$response['msg'] = __('Error occurred connecting to the server');
}
}
@@ -706,28 +789,33 @@ class MailboxesController extends Controller
$imap_folders = $client->getFolders();
$response['folders'] = [];
if (count($imap_folders)) {
foreach ($imap_folders as $imap_folder) {
if (!empty($imap_folder->name)) {
$response['folders'][] = $imap_folder->name;
}
// Maybe we need a recursion here.
if (!empty($imap_folder->children)) {
foreach ($imap_folder->children as $child_imap_folder) {
// Old library.
if (!empty($child_imap_folder->fullName)) {
$response['folders'][] = $child_imap_folder->fullName;
}
// New library.
if (!empty($child_imap_folder->full_name)) {
$response['folders'][] = $child_imap_folder->full_name;
}
}
}
}
$response = $this->interateFolders($response, $imap_folders);
$response['folders'] = array_values(array_unique($response['folders']));
}
if (count($response['folders'])) {
// https://github.com/freescout-helpdesk/freescout/issues/3933
// Exclude duplicate INBOX.Name and Name folders.
// $folder_excluded = false;
// foreach ($response['folders'] as $i => $folder_name) {
// if (\Str::startsWith($folder_name, 'INBOX.')) {
// foreach ($response['folders'] as $folder_name2) {
// if ($folder_name != $folder_name2 && $folder_name == 'INBOX.'.$folder_name2) {
// unset($response['folders'][$i]);
// $folder_excluded = true;
// continue 2;
// }
// }
// }
// }
// if ($folder_excluded) {
// $response['folders'] = array_values($response['folders']);
// }
$response['msg_success'] = __('IMAP folders retrieved: '.implode(', ', $response['folders']));
} else {
$response['msg_success'] = __('Connected, but no IMAP folders found');
@@ -780,17 +868,23 @@ class MailboxesController extends Controller
// Mute notifications
case 'mute':
if (!$user) {
\Helper::denyAccess();
}
$mailbox = Mailbox::find($request->mailbox_id);
if (!$mailbox) {
$response['msg'] = __('Mailbox not found');
} elseif (!$user->can('view', $mailbox)) {
$response['msg'] = __('Not enough permissions');
}
if (!$response['msg']) {
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $mailbox->id)->first();
if (!$mailbox_user) {
// User may not be connected to the mailbox yet
if (!$mailbox_user && $user->isAdmin()) {
// Admin may not be connected to the mailbox yet
$user->mailboxes()->attach($mailbox->id);
$mailbox_user = $user->mailboxesWithSettings()->where('mailbox_id', $mailbox->id)->first();
}
@@ -813,10 +907,35 @@ class MailboxesController extends Controller
return \Response::json($response);
}
// Recursively interate over folders.
public function interateFolders($response, $imap_folders, $subfolder = false) {
foreach ($imap_folders as $imap_folder) {
if (!empty($imap_folder->name) && !$subfolder) {
$response['folders'][] = $imap_folder->name;
}
// Check for children and recurse.
if (!empty($imap_folder->children)) {
$response = $this->interateFolders($response, $imap_folder->children, true);
}
// Old library.
if (!empty($imap_folder->fullName)) {
$response['folders'][] = $imap_folder->fullName;
} elseif (!empty($imap_folder->full_name)) {
// New library.
$response['folders'][] = $imap_folder->full_name;
}
}
return $response;
}
public function oauth(Request $request)
{
$mailbox_id = $request->id ?? '';
$provider = $request->provider ?? '';
$in_out = $request->in_out ?? 'in';
$state_data = [];
if (!empty($request->state)) {
@@ -827,6 +946,9 @@ class MailboxesController extends Controller
if (!empty($state_data['provider'])) {
$provider = $state_data['provider'];
}
if (!empty($state_data['in_out'])) {
$in_out = $state_data['in_out'];
}
}
// MS Exchange.
@@ -844,10 +966,17 @@ class MailboxesController extends Controller
if (empty($mailbox)) {
return __('Mailbox not found').': '.$mailbox_id;
}
if (empty($mailbox->in_username)) {
if ($in_out == 'in') {
$username = $mailbox->in_username;
$password = $mailbox->in_password;
} else {
$username = $mailbox->out_username;
$password = $mailbox->out_password;
}
if (empty($username)) {
return 'Enter oAuth Client ID as Username and save mailbox settings';
}
if (empty($mailbox->in_password)) {
if (empty($password)) {
return 'Enter oAuth Client Secret as Password and save mailbox settings';
}
@@ -857,14 +986,16 @@ class MailboxesController extends Controller
}
if (empty($request->code)) {
// Start.
$state = [
'provider' => $provider,
'mailbox_id' => $mailbox_id,
'state' => crc32($mailbox->in_username.$mailbox->in_password),
'in_out' => $in_out,
'state' => crc32($username.$password),
];
$url = \MailHelper::oauthGetAuthorizationUrl(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
'state' => json_encode($state),
'client_id' => $mailbox->in_username,
'client_id' => $username,
]);
if ($url) {
\Session::put('mailbox_oauth_'.$provider.'_'.$mailbox_id, $state);
@@ -885,21 +1016,40 @@ class MailboxesController extends Controller
return 'Invalid oAuth state';
} else {
// state is set.
// Try to get an access token (using the authorization code grant)
$token_data = \MailHelper::oauthGetAccessToken(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
'client_id' => $mailbox->in_username,
'client_secret' => $mailbox->in_password,
'client_id' => $username,
'client_secret' => $password,
'code' => $request->code,
]);
if (!empty($token_data['a_token'])) {
// Set username and password for the oppozite in_out.
if ($in_out == 'in') {
if (empty($mailbox->out_server)
|| (trim($mailbox->out_server) == \MailHelper::OAUTH_MICROSOFT_SMTP
&& (!$mailbox->out_username || $mailbox->out_username == $username))
) {
$mailbox->out_username = $username;
$mailbox->out_password = $password;
}
//$mailbox->out_method = Mailbox::OUT_METHOD_SMTP;
} else {
$mailbox->in_username = $username;
$mailbox->in_password = $password;
}
$mailbox->setMetaParam('oauth', $token_data, true);
} elseif (!empty($token_data['error'])) {
return __('Error occurred').': '.htmlspecialchars($token_data['error']);
}
return redirect()->route('mailboxes.connection.incoming', ['id' => $mailbox_id]);
if ($in_out == 'in') {
$route = 'mailboxes.connection.incoming';
} else {
$route = 'mailboxes.connection';
}
return redirect()->route($route, ['id' => $mailbox_id]);
}
}
@@ -907,12 +1057,20 @@ class MailboxesController extends Controller
{
$mailbox_id = $request->id ?? '';
$provider = $request->provider ?? '';
$in_out = $request->in_out ?? 'in';
$mailbox = Mailbox::findOrFail($mailbox_id);
$this->authorize('admin', $mailbox);
// oAuth Disconnect.
$mailbox->removeMetaParam('oauth', true);
return \MailHelper::oauthDisconnect($provider, route('mailboxes.connection.incoming', ['id' => $mailbox_id]));
if ($in_out == 'in') {
$route = 'mailboxes.connection.incoming';
} else {
$route = 'mailboxes.connection';
}
return \MailHelper::oauthDisconnect($provider, route($route, ['id' => $mailbox_id]));
}
}

View File

@@ -76,6 +76,9 @@ class ModulesController extends Controller
'installed' => true,
'activated' => \App\Module::isLicenseActivated($module->getAlias(), $module->get('authorUrl')),
'license' => \App\Module::getLicense($module->getAlias()),
// Update configuration for third party modules
'latestVersionNumberUrl' => $module->get('latestVersionUrl'),
'latestVersionZipUrl' => $module->get('latestVersionZipUrl'),
// Determined later
'new_version' => '',
];
@@ -128,7 +131,7 @@ class ModulesController extends Controller
$modules_directory[$i_dir]['active'] = \App\Module::isActive($dir_module['alias']);
$modules_directory[$i_dir]['activated'] = false;
// Do not show third-party modules in Modules Derectory.
// Do not show third-party modules in Modules Directory.
if (\App\Module::isThirdParty($dir_module)) {
$third_party_modules[] = $modules_directory[$i_dir];
unset($modules_directory[$i_dir]);
@@ -138,12 +141,63 @@ class ModulesController extends Controller
$modules_directory = [];
}
// Loop through each installed module
foreach ($installed_modules as $i_installed => $module) {
// Check if the module is an official one
if (\App\Module::isOfficial($module['authorUrl'])) {
continue;
}
// Get the URL for the latest version of the module
$latest_version_number_url = $module['latestVersionNumberUrl'] ?? null;
if (! $latest_version_number_url) {
continue;
}
// Create a new Guzzle HTTP client
$client = new \GuzzleHttp\Client();
try {
// Send a GET request to the latest version URL
$response = $client->request('GET', $latest_version_number_url, \Helper::setGuzzleDefaultOptions());
// Get the latest version number from the response body
$latest_version = trim((string) $response->getBody());
if (empty($latest_version)) {
continue;
}
// If it is the module.json file - try to parse the body.
preg_match('#"version":[^"]*"([\d\.]+)"#', $latest_version, $m);
if (!empty($m[1])) {
$latest_version = $m[1];
}
// Get the current version of the module
$current_version = $module['version'];
} catch (\Exception $e) {
// If there's an exception, skip to the next iteration
continue;
}
// If the latest version is greater than the current version
if (version_compare($latest_version, $current_version, '>')) {
// Update the installed module's version
$installed_modules[ $i_installed ]['new_version'] = $latest_version;
// Set the flag to indicate that updates are available
$updates_available = true;
}
}
// Check modules symlinks. Somestimes instead of symlinks folders with files appear.
$invalid_symlinks = \App\Module::checkSymlinks(
collect($installed_modules)->where('active', true)->pluck('alias')->toArray()
);
// Sort all modules.
asort($all_modules);
return view('modules/modules', [
'installed_modules' => $installed_modules,
'modules_directory' => $modules_directory,
@@ -328,6 +382,8 @@ class ModulesController extends Controller
if (!$response['msg']) {
\App\Module::setActive($alias, true);
$user_locale = app()->getLocale();
$outputLog = new BufferedOutput();
\Artisan::call('freescout:module-install', ['module_alias' => $alias], $outputLog);
$output = $outputLog->fetch();
@@ -338,6 +394,9 @@ class ModulesController extends Controller
$name = $module->getName();
}
// After clearing cache the locale may be not set.
\Helper::setUserLocale($user_locale);
$type = 'danger';
$msg = __('Error occurred activating ":name" module', ['name' => $name]);
if (session('flashes_floating') && is_array(session('flashes_floating'))) {
@@ -387,6 +446,8 @@ class ModulesController extends Controller
$alias = $request->alias;
\App\Module::setActive($alias, false);
$user_locale = app()->getLocale();
$outputLog = new BufferedOutput();
\Artisan::call('freescout:clear-cache', [], $outputLog);
$output = $outputLog->fetch();
@@ -398,6 +459,9 @@ class ModulesController extends Controller
$name = $module->getName();
}
// After clearing cache the locale may be not set.
\Helper::setUserLocale($user_locale);
$type = 'danger';
$msg = __('Error occurred deactivating :name module', ['name' => $name]);
if (strstr($output, 'Configuration cached successfully')) {

View File

@@ -87,14 +87,15 @@ class OpenController extends Controller
->withInput();
}
$request_data = $request->all();
// Do not allow user to set his role
if (isset($request_data['role'])) {
unset($request_data['role']);
}
if (isset($request_data['photo_url'])) {
unset($request_data['photo_url']);
}
$request_data = [
'email' => $request->email,
'password' => $request->password,
'job_title' => $request->job_title,
'phone' => $request->phone,
'timezone' => $request->timezone,
'time_format' => $request->time_format,
];
$user->fill($request_data);
$user->password = bcrypt($request->password);
@@ -108,7 +109,7 @@ class OpenController extends Controller
// Login user
Auth::guard()->login($user);
\Session::flash('flash_success_floating', __('Welcome to :company_name!', ['company_name' => Option::getCompanyName()]));
\Session::flash('flash_success_floating', __('Welcome to :company_name!', ['company_name' => htmlspecialchars(Option::getCompanyName())]));
return redirect()->route('dashboard');
}

View File

@@ -394,17 +394,22 @@ class SettingsController extends Controller
}
if (!$response['msg']) {
$test_result = false;
$test_result = [
'status' => 'error'
];
try {
$test_result = \MailHelper::sendTestMail($request->to);
} catch (\Exception $e) {
$response['msg'] = $e->getMessage();
$test_result['msg'] = $e->getMessage();
}
if (!$test_result && !$response['msg']) {
$response['msg'] = __('Error occurred sending email. Please check your mail server logs for more details.');
if ($test_result['status'] == 'error') {
$response['msg'] = $test_result['msg']
?: __('Error occurred sending email. Please check your mail server logs for more details.');
}
$response['log'] = $test_result['log'] ?? '';
}
if (!$response['msg']) {

View File

@@ -53,7 +53,7 @@ class SystemController extends Controller
// Check if cache files are writable.
$non_writable_cache_file = '';
if (function_exists('shell_exec')) {
$non_writable_cache_file = shell_exec('find '.base_path('storage/framework/cache/data/').' -type f | xargs -I {} sh -c \'[ ! -w "{}" ] && echo {}\' 2>&1 | head -n 1');
$non_writable_cache_file = \Helper::shellExec('find '.base_path('storage/framework/cache/data/').' -type f | xargs -I {} sh -c \'[ ! -w "{}" ] && echo {}\' 2>&1 | head -n 1');
$non_writable_cache_file = trim($non_writable_cache_file ?? '');
// Leave only one line (in case head -n 1 does not work)
$non_writable_cache_file = preg_replace("#[\r\n].+#m", '', $non_writable_cache_file);
@@ -80,8 +80,8 @@ class SystemController extends Controller
$env_is_writable = is_writable(base_path('.env'));
// Jobs
$queued_jobs = \App\Job::orderBy('created_at', 'desc')->get();
$failed_jobs = \App\FailedJob::orderBy('failed_at', 'desc')->get();
$queued_jobs = \App\Job::orderBy('created_at', 'desc')->limit(100)->get();
$failed_jobs = \App\FailedJob::orderBy('failed_at', 'desc')->limit(100)->get();
$failed_queues = $failed_jobs->pluck('queue')->unique();
// Commands
@@ -97,7 +97,7 @@ class SystemController extends Controller
$running_commands = 0;
try {
$processes = preg_split("/[\r\n]/", shell_exec("ps aux | grep '{$command_identifier}'"));
$processes = preg_split("/[\r\n]/", \Helper::shellExec("ps auxww | grep '{$command_identifier}'"));
$pids = [];
foreach ($processes as $process) {
$process = trim($process);
@@ -290,6 +290,7 @@ class SystemController extends Controller
$params = [];
$params['--days'] = (int)$request->days;
$params['--unseen'] = (int)$request->unseen;
$params['--debug'] = (int)$request->debug;
\Artisan::call('freescout:fetch-emails', $params, $outputLog);
break;
@@ -403,10 +404,10 @@ class SystemController extends Controller
$payload = json_decode($job->payload, true);
if (!empty($payload['data']['command'])) {
$html .= '<pre>'.print_r(unserialize($payload['data']['command']), 1).'</pre>';
$html .= '<pre>'.\Helper::stripDangerousTags(print_r(unserialize($payload['data']['command']), 1)).'</pre>';
}
$html .= '<pre>'.$job->exception.'</pre>';
$html .= '<pre>'.\Helper::stripDangerousTags($job->exception).'</pre>';
return response($html);
}

View File

@@ -2,7 +2,6 @@
namespace App\Http\Controllers;
use App\Events\UserDeleted;
use App\Folder;
use App\Mailbox;
use App\Subscription;
@@ -86,8 +85,16 @@ class UsersController extends Controller
->withInput();
}
$data = [
'first_name' => $request->first_name,
'last_name' => $request->last_name,
'email' => $request->email,
];
$user = new User();
$user->fill($request->all());
$user->fill($data);
if (!$auth_user->can('changeRole', $user)) {
$user->role = User::ROLE_USER;
}
@@ -161,6 +168,8 @@ class UsersController extends Controller
$user = User::findOrFail($id);
$this->authorize('update', $user);
$auth_user = auth()->user();
// This is also present in PublicController::userSetup
$validator = Validator::make($request->all(), [
'first_name' => 'required|string|max:20',
@@ -213,7 +222,7 @@ class UsersController extends Controller
}
// Save language into session.
if (auth()->user()->id == $id && $request->locale) {
if ($auth_user->id == $id && $request->locale) {
session()->put('user_locale', $request->locale);
}
@@ -222,7 +231,7 @@ class UsersController extends Controller
if (isset($request_data['photo_url'])) {
unset($request_data['photo_url']);
}
if (!auth()->user()->can('changeRole', $user)) {
if (!$auth_user->can('changeRole', $user)) {
unset($request_data['role']);
}
if ($user->status != User::STATUS_DELETED) {
@@ -232,11 +241,43 @@ class UsersController extends Controller
$request_data['status'] = User::STATUS_ACTIVE;
}
}
$user->setData($request_data);
if (empty($request->input('enable_kb_shortcuts'))) {
$user->enable_kb_shortcuts = false;
// Sanitize $request_data array.
// $allowed_fields = [
// 'first_name',
// 'last_name',
// 'email',
// 'emails',
// 'job_title',
// 'phone',
// 'locale',
// 'timezone'
// 'time_format',
// 'photo_url',
// ];
$admin_fields = [
'role',
'status',
'email',
];
$nonfillable_fields = [
'type',
'password',
];
if (!$auth_user->isAdmin()) {
foreach ($admin_fields as $field) {
if (isset($request_data[$field])) {
unset($request_data[$field]);
}
}
}
foreach ($nonfillable_fields as $field) {
if (isset($request_data[$field])) {
unset($request_data[$field]);
}
}
$user->setData($request_data);
$user = \Eventy::filter('user.save_profile', $user, $request);
@@ -259,6 +300,10 @@ class UsersController extends Controller
$user = User::findOrFail($id);
if ($user->isDeleted()) {
abort(404);
}
$mailboxes = Mailbox::all();
$users = $this->getUsersForSidebar($id);
@@ -317,6 +362,10 @@ class UsersController extends Controller
$user = User::findOrFail($id);
$this->authorize('update', $user);
if ($user->isDeleted()) {
abort(404);
}
$subscriptions = $user->subscriptions()->select('medium', 'event')->get();
$person = '';
@@ -479,106 +528,24 @@ class UsersController extends Controller
$response['msg'] = __('User not found');
} elseif (!$auth_user->can('delete', $user)) {
$response['msg'] = __('Not enough permissions');
} elseif ($auth_user->id == $user->id) {
// Do not allow admin delete himself.
$response['msg'] = __('Not enough permissions');
}
// Check if the user is the only one admin
if (!$response['msg'] && $user->isAdmin()) {
$admins_count = User::where('role', User::ROLE_ADMIN)->count();
if ($admins_count < 2) {
$response['msg'] = __('Administrator can not be deleted');
}
}
// - not needed as only admin can delete users and
// current admin can not delete himself.
// if (!$response['msg'] && $user->isAdmin()) {
// $admins_count = User::where('role', User::ROLE_ADMIN)->count();
// if ($admins_count < 2) {
// $response['msg'] = __('Administrator can not be deleted');
// }
// }
if (!$response['msg']) {
// We have to process conversations one by one to move them to Unassigned folder,
// as conversations may be in different mailboxes
// $user->conversations()->update(['user_id' => null, 'folder_id' => ]);
$mailbox_unassigned_folders = [];
$user->conversations->each(function ($conversation) use ($auth_user, $request) {
// We don't fire ConversationUserChanged event to avoid sending notifications to users
if (!empty($request->assign_user)
&& !empty($request->assign_user[$conversation->mailbox_id])
&& (int) $request->assign_user[$conversation->mailbox_id] != -1
) {
// Set assignee.
// In this case conversation stays assigned, just assignee changes.
$conversation->user_id = $request->assign_user[$conversation->mailbox_id];
} else {
// Make convesation Unassigned.
// Unset assignee.
// Maybe use changeUser() here.
$conversation->user_id = null;
if ($conversation->isPublished()
&& ($conversation->isActive() || $conversation->isPending())
) {
// Change conversation folder to UNASSIGNED.
$folder_id = null;
if (!empty($mailbox_unassigned_folders[$conversation->mailbox_id])) {
$folder_id = $mailbox_unassigned_folders[$conversation->mailbox_id];
} else {
$folder = $conversation->mailbox->folders()
->where('type', Folder::TYPE_UNASSIGNED)
->first();
if ($folder) {
$folder_id = $folder->id;
$mailbox_unassigned_folders[$conversation->mailbox_id] = $folder_id;
}
}
if ($folder_id) {
$conversation->folder_id = $folder_id;
}
}
}
$conversation->save();
// Create lineitem thread
$thread = new Thread();
$thread->conversation_id = $conversation->id;
$thread->user_id = $conversation->user_id;
$thread->type = Thread::TYPE_LINEITEM;
$thread->state = Thread::STATE_PUBLISHED;
$thread->status = Thread::STATUS_NOCHANGE;
$thread->action_type = Thread::ACTION_TYPE_USER_CHANGED;
$thread->source_via = Thread::PERSON_USER;
$thread->source_type = Thread::SOURCE_TYPE_WEB;
$thread->customer_id = $conversation->customer_id;
$thread->created_by_user_id = $auth_user->id;
$thread->save();
});
// Recalculate counters for folders
//if ($user->isAdmin()) {
// Admin has access to all mailboxes
Mailbox::all()->each(function ($mailbox) {
$mailbox->updateFoldersCounters();
});
// } else {
// $user->mailboxes->each(function ($mailbox) {
// $mailbox->updateFoldersCounters();
// });
// }
// Disconnect user from mailboxes.
$user->mailboxes()->sync([]);
$user->folders()->delete();
$user->status = \App\User::STATUS_DELETED;
// Update email.
$email_suffix = User::EMAIL_DELETED_SUFFIX.date('YmdHis');
// We have to truncate email to avoid "Data too long" error.
$user->email = mb_substr($user->email, 0, User::EMAIL_MAX_LENGTH - mb_strlen($email_suffix)).$email_suffix;
$user->save();
event(new UserDeleted($user, $auth_user));
$user->deleteUser($auth_user, $request->assign_user);
\Session::flash('flash_success_floating', __('User deleted').': '.$user->getFullName());
@@ -588,6 +555,7 @@ class UsersController extends Controller
default:
$response['msg'] = 'Unknown action';
$response = \Eventy::filter('users.ajax.response_default', $response, $request);
break;
}

View File

@@ -30,13 +30,8 @@ class HttpsRedirect {
{
if (\Helper::isHttps()) {
//$request->setTrustedProxies( [ $request->getClientIp() ], array_keys($this->headers));
if (//!$request->secure()
!in_array(strtolower($_SERVER['X_FORWARDED_PROTO'] ?? ''), array('https', 'on', 'ssl', '1'), true)
&& strtolower($_SERVER['HTTPS'] ?? '') != 'on'
&& ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') != 'https'
&& ($_SERVER['HTTP_CF_VISITOR'] ?? '') != '{"scheme":"https"}'
) {
//!$request->secure()
if (!\Helper::isCurrentUrlHttps()) {
return redirect()->secure($request->getRequestUri());
}
}

View File

@@ -6,11 +6,26 @@ use Closure;
class ResponseHeaders
{
private $unwanted_headers = [
'X-Powered-By',
'Server',
];
public function handle($request, Closure $next)
{
$this->removeUnwantedHeaders($this->unwanted_headers);
$response = $next($request);
// Disable caching
// Secure headers.
//$response->headers->set('Referrer-Policy', 'no-referrer-when-downgrade');
$response->headers->set('X-Content-Type-Options', 'nosniff');
// $response->headers->set('X-XSS-Protection', '1; mode=block');
// $response->headers->set('X-Frame-Options', 'DENY');
//$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
//$response->headers->set('Content-Security-Policy', "style-src 'self'");
// Disable caching.
if (method_exists($response, 'header')) {
$response->header('Pragma', 'no-cache');
$response->header('Cache-Control', 'no-cache, max-age=0, must-revalidate, no-store');
@@ -18,4 +33,11 @@ class ResponseHeaders
return $response;
}
private function removeUnwantedHeaders($headerList)
{
foreach ($headerList as $header) {
header_remove($header);
}
}
}

View File

@@ -72,6 +72,13 @@ class SendNotificationToUsers implements ShouldQueue
return;
}
// Do not send email notifications to support agents
// if the email is a bounce and mail server send limit reached.
// https://github.com/freescout-help-desk/freescout/issues/4806
if ($last_thread->isBounce() && strstr($last_thread->body, 'message limit exceeded')) {
return;
}
// Limit conversation history
if (config('app.email_user_history') == 'last') {
$this->threads = $this->threads->slice(0, 2);

View File

@@ -3,6 +3,7 @@
namespace App\Jobs;
use App\Mail\ReplyToCustomer;
use App\Customer;
use App\SendLog;
use App\Thread;
use App\Misc\SwiftGetSmtpQueueId;
@@ -12,7 +13,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Mail;
use Webklex\IMAP\Client;
//use Webklex\IMAP\Client;
class SendReplyToCustomer implements ShouldQueue
{
@@ -116,6 +117,17 @@ class SendReplyToCustomer implements ShouldQueue
return;
}
// After sending an email we are saving it into "IMAP Folder To Save Outgoing Replies".
// This process may stuck or make SendReplyToCustomer job die with
// "Allowed memory size of NNN bytes exhausted" error.
// https://github.com/freescout-helpdesk/freescout/issues/3632
if ($this->attempts() >= 1 &&
($this->last_thread->send_status == SendLog::STATUS_ACCEPTED
|| $this->last_thread->isSendStatusSuccess())
) {
return;
}
if (count($this->threads) == 1) {
$new = true;
}
@@ -156,6 +168,19 @@ class SendReplyToCustomer implements ShouldQueue
}
}
// For Outlook threading.
// https://github.com/freescout-help-desk/freescout/issues/4922
if ($last_customer_thread) {
$outloook_thread_index = $last_customer_thread->getHeader('Thread-Index');
if (!empty($outloook_thread_index)) {
$headers['Thread-Index'] = $outloook_thread_index;
$outloook_thread_topic = $last_customer_thread->getHeader('Thread-Topic');
if ($outloook_thread_topic) {
$headers['Thread-Topic'] = $outloook_thread_topic;
}
}
}
// Conversation history.
$email_conv_history = config('app.email_conv_history');
@@ -219,6 +244,12 @@ class SendReplyToCustomer implements ShouldQueue
}
}
// Try to get customer by email
if (!$this->customer) {
$this->customer = Customer::getByEmail($this->customer_email);
return;
}
$to_array = $mailbox->removeMailboxEmailsFromList($this->last_thread->getToArray());
$cc_array = $mailbox->removeMailboxEmailsFromList($this->last_thread->getCcArray());
$bcc_array = $mailbox->removeMailboxEmailsFromList($this->last_thread->getBccArray());
@@ -266,12 +297,17 @@ class SendReplyToCustomer implements ShouldQueue
$reply_mail = new ReplyToCustomer($this->conversation, $this->threads, $headers, $mailbox, $subject, $threads_count);
$smtp_queue_id = null;
try {
Mail::to($to)
->cc($cc_array)
->bcc($bcc_array)
->send($reply_mail);
$this->last_thread->send_status = SendLog::STATUS_ACCEPTED;
$this->last_thread->save();
$smtp_queue_id = SwiftGetSmtpQueueId::$last_smtp_queue_id;
} catch (\Exception $e) {
// We come here in case SMTP server unavailable for example
@@ -317,7 +353,7 @@ class SendReplyToCustomer implements ShouldQueue
// If an email has not been sent after 1 hour - show an error message to support agent.
if ($this->attempts() >= 3 || $response_code >= 500) {
$this->last_thread->send_status = SendLog::STATUS_SEND_ERROR;
$this->last_thread->send_status = SendLog::STATUS_SEND_INTERMEDIATE_ERROR;
$this->last_thread->updateSendStatusData(['msg' => $error_message]);
$this->last_thread->save();
}
@@ -345,82 +381,98 @@ class SendReplyToCustomer implements ShouldQueue
}
$imap_sent_folder = $mailbox->imap_sent_folder;
if ($imap_sent_folder) {
if ($imap_sent_folder && \MailHelper::$smtp_mime_message) {
try {
$client = \MailHelper::getMailboxClient($mailbox);
$client->connect();
$envelope['from'] = $mailbox->getMailFrom(null, $this->conversation)['address'];
$envelope['to'] = $this->customer_email;
$envelope['subject'] = $subject;
$envelope['date'] = now()->toRfc2822String();
$envelope['message_id'] = $this->message_id;
// $mail_from = $mailbox->getMailFrom($this->last_thread->created_by_user ?? null, $this->conversation);
// CC.
if (count($cc_array)) {
$envelope['cc'] = implode(',', $cc_array);
}
// if (!empty($mail_from['name'])) {
// $envelope['from'] = '"'.$mail_from['name'].'" <'.$mail_from['address'].'>';
// } else {
// $envelope['from'] = $mail_from['address'];
// }
// if (is_array($to) && !empty($to[0]) && !empty($to[0]['name']) && !empty($to[0]['email'])) {
// $envelope['to'] = '"'.$to[0]['name'].'" <'.$to[0]['email'].'>';
// } else {
// $envelope['to'] = $this->customer_email;
// }
// $envelope['subject'] = $subject;
// $envelope['date'] = now()->toRfc2822String();
// $envelope['message_id'] = $this->message_id;
// Get penultimate email Message-Id if reply
if (!$new && !empty($last_customer_thread) && $last_customer_thread->message_id) {
$envelope['custom_headers'] = [
'In-Reply-To: <'.$last_customer_thread->message_id.'>',
'References: '.$references,
];
}
// Remove new lines to avoid "imap_mail_compose(): header injection attempt in subject".
foreach ($envelope as $i => $envelope_value) {
$envelope[$i] = preg_replace("/[\r\n]/", '', $envelope_value);
}
// // CC.
// if (count($cc_array)) {
// $envelope['cc'] = implode(',', $cc_array);
// }
$parts = [];
// // Get penultimate email Message-Id if reply
// if (!$new && !empty($last_customer_thread) && $last_customer_thread->message_id) {
// $envelope['custom_headers'] = [
// 'In-Reply-To: <'.$last_customer_thread->message_id.'>',
// 'References: '.$references,
// ];
// }
// // Remove new lines to avoid "imap_mail_compose(): header injection attempt in subject".
// foreach ($envelope as $i => $envelope_value) {
// $envelope[$i] = preg_replace("/[\r\n]/", '', $envelope_value);
// }
// Multipart flag.
if ($this->last_thread->has_attachments) {
$multipart = [];
$multipart["type"] = TYPEMULTIPART;
$multipart["subtype"] = "alternative";
$parts[] = $multipart;
}
// $parts = [];
// Body.
$part_body['type'] = TYPETEXT;
$part_body['subtype'] = 'html';
$part_body['contents.data'] = $reply_mail->render();
$part_body['charset'] = 'utf-8';
// // Multipart flag.
// if ($this->last_thread->has_attachments) {
// $multipart = [];
// $multipart["type"] = \Webklex\PHPIMAP\IMAP::TYPEMULTIPART;
// $multipart["subtype"] = "mixed";
// // https://github.com/freescout-helpdesk/freescout/issues/3934
// //$multipart["subtype"] = "alternative";
// $parts[] = $multipart;
// }
$parts[] = $part_body;
// // Body.
// $part_body['type'] = \Webklex\PHPIMAP\IMAP::TYPETEXT;
// $part_body['subtype'] = 'html';
// $part_body['contents.data'] = $reply_mail->render();
// $part_body['charset'] = 'utf-8';
// Add attachments.
if ($this->last_thread->has_attachments) {
// $parts[] = $part_body;
foreach ($this->last_thread->attachments as $attachment) {
// // Add attachments.
// if ($this->last_thread->has_attachments) {
if ($attachment->embedded) {
continue;
}
// foreach ($this->last_thread->attachments as $attachment) {
if ($attachment->fileExists()) {
$part = [];
$part["type"] = 'APPLICATION';
$part["encoding"] = ENCBASE64;
$part["subtype"] = "octet-stream";
$part["description"] = $attachment->file_name;
$part['disposition.type'] = 'attachment';
$part['disposition'] = array('filename' => $attachment->file_name);
$part['type.parameters'] = array('name' => $attachment->file_name);
$part["description"] = '';
$part["contents.data"] = base64_encode($attachment->getFileContents());
// if ($attachment->embedded) {
// continue;
// }
// if ($attachment->fileExists()) {
// $part = [];
// $part["type"] = 'APPLICATION';
// $part["encoding"] = \Webklex\PHPIMAP\IMAP::ENCBASE64;
// $part["subtype"] = "octet-stream";
// $part["description"] = $attachment->file_name;
// $part['disposition.type'] = 'attachment';
// $part['disposition'] = array('filename' => $attachment->file_name);
// $part['type.parameters'] = array('name' => $attachment->file_name);
// $part["description"] = '';
// $part["contents.data"] = base64_encode($attachment->getFileContents());
$parts[] = $part;
} else {
\Log::error('[IMAP Folder To Save Outgoing Replies] Thread: '.$this->last_thread->id.'. Attachment file not find on disk: '.$attachment->getLocalFilePath());
}
}
}
// $parts[] = $part;
// } else {
// \Log::error('[IMAP Folder To Save Outgoing Replies] Thread: '.$this->last_thread->id.'. Attachment file not find on disk: '.$attachment->getLocalFilePath());
// }
// }
// }
try {
// https://github.com/freescout-helpdesk/freescout/issues/3502
$imap_sent_folder = mb_convert_encoding($imap_sent_folder, "UTF7-IMAP","UTF-8");
// https://github.com/Webklex/php-imap/issues/380
if (method_exists($client, 'getFolderByPath')) {
$folder = $client->getFolderByPath($imap_sent_folder);
@@ -430,29 +482,35 @@ class SendReplyToCustomer implements ShouldQueue
// Get folder method does not work if sent folder has spaces.
if ($folder) {
try {
$save_result = $this->saveEmailToFolder($client, $folder, $envelope, $parts, $bcc_array);
//$save_result = $this->saveEmailToFolder($client, $folder, $envelope, $parts, $bcc_array);
$save_result = $this->saveEmailToFolder($folder, \MailHelper::$smtp_mime_message);
\MailHelper::$smtp_mime_message = '';
// Sometimes emails with attachments by some reason are not saved.
// https://github.com/freescout-helpdesk/freescout/issues/2749
// if (!$save_result) {
// // Save without attachments.
// $save_result = $this->saveEmailToFolder($client, $folder, $envelope, [$part_body], $bcc_array);
if (!$save_result) {
// Save without attachments.
$this->saveEmailToFolder($client, $folder, $envelope, [$part_body], $bcc_array);
\Log::error($this->getImapSaveErrorPrefix($mailbox).'Could not save outgoing reply to the IMAP folder (check folder name and make sure IMAP folder does not have spaces - folders with spaces do not work): '.$imap_sent_folder);
}
//}
} catch (\Exception $e) {
// Just log error and continue.
\Helper::logException($e, 'Could not save outgoing reply to the IMAP folder: ');
\Helper::logException($e, $this->getImapSaveErrorPrefix($mailbox).'Could not save outgoing reply to the IMAP folder: ');
}
} else {
\Log::error('Could not save outgoing reply to the IMAP folder (make sure IMAP folder does not have spaces - folders with spaces do not work): '.$imap_sent_folder);
\Log::error($this->getImapSaveErrorPrefix($mailbox).'Could not save outgoing reply to the IMAP folder (check folder name and make sure IMAP folder does not have spaces - folders with spaces do not work): '.$imap_sent_folder);
}
} catch (\Exception $e) {
// Just log error and continue.
\Helper::logException($e, 'Could not save outgoing reply to the IMAP folder, IMAP folder not found: '.$imap_sent_folder.' - ');
\Helper::logException($e, $this->getImapSaveErrorPrefix($mailbox).'Could not save outgoing reply to the IMAP folder, IMAP folder not found: '.$imap_sent_folder.' - ');
//$this->saveToSendLog('['.date('Y-m-d H:i:s').'] Could not save outgoing reply to the IMAP folder: '.$imap_sent_folder);
}
} catch (\Exception $e) {
// Just log error and continue.
//$this->saveToSendLog('['.date('Y-m-d H:i:s').'] Could not get mailbox IMAP folder: '.$imap_sent_folder);
\Helper::logException($e, 'Could not save outgoing reply to the IMAP folder: '.$imap_sent_folder.' - ');
\Helper::logException($e, $this->getImapSaveErrorPrefix($mailbox).'Could not save outgoing reply to the IMAP folder: '.$imap_sent_folder.' - ');
}
}
@@ -468,29 +526,39 @@ class SendReplyToCustomer implements ShouldQueue
$this->saveToSendLog('', $smtp_queue_id);
}
// Save an email to IMAP folder.
public function saveEmailToFolder($client, $folder, $envelope, $parts, $bcc = [])
public function getImapSaveErrorPrefix($mailbox)
{
$envelope_str = imap_mail_compose($envelope, $parts);
// Add BCC.
// https://stackoverflow.com/questions/47353938/php-imap-append-with-bcc
if (!empty($bcc)) {
// There will be a "To:" parameter for sure.
$to_pos = strpos($envelope_str , "To:");
if ($to_pos !== false) {
$bcc_str = "Bcc: " . implode(',', $bcc) . "\r\n";
$envelope_str = substr_replace($envelope_str , $bcc_str, $to_pos, 0);
}
}
if (get_class($client) == 'Webklex\PHPIMAP\Client') {
return $folder->appendMessage($envelope_str, ['Seen'], now()->format('d-M-Y H:i:s O'));
} else {
return $folder->appendMessage($envelope_str, '\Seen', now()->format('d-M-Y H:i:s O'));
}
return '['.$mailbox->name.' » Connection Settings » Fetching Emails » IMAP Folder To Save Outgoing Replies] ';
}
// Save an email to IMAP folder.
public function saveEmailToFolder($folder, $message_str)
{
return $folder->appendMessage($message_str, ['\Seen'], now()->format('d-M-Y H:i:s O'));
}
// public function saveEmailToFolder($client, $folder, $envelope, $parts, $bcc = [])
// {
// $envelope_str = imap_mail_compose($envelope, $parts);
// // Add BCC.
// // https://stackoverflow.com/questions/47353938/php-imap-append-with-bcc
// if (!empty($bcc)) {
// // There will be a "To:" parameter for sure.
// $to_pos = strpos($envelope_str , "To:");
// if ($to_pos !== false) {
// $bcc_str = "Bcc: " . implode(',', $bcc) . "\r\n";
// $envelope_str = substr_replace($envelope_str , $bcc_str, $to_pos, 0);
// }
// }
// if (get_class($client) == 'Webklex\PHPIMAP\Client') {
// return $folder->appendMessage($envelope_str, ['\Seen'], now()->format('d-M-Y H:i:s O'));
// } else {
// return $folder->appendMessage($envelope_str, '\Seen', now()->format('d-M-Y H:i:s O'));
// }
// }
/**
* The job failed to process.
* This method is called after attempts had finished.

View File

@@ -2,9 +2,8 @@
namespace App\Jobs;
use App\Conversation;
use App\ConversationFolder;
use App\Folder;
use Illuminate\Support\Facades\Cache;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
@@ -15,28 +14,33 @@ class UpdateFolderCounters implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* @var Folder
*/
private $folder;
/**
* Create a new job instance.
*
* @return void
*/
// Cache lock key
private $lockKey;
public function __construct(Folder $folder)
{
$this->folder = $folder;
$this->lockKey = "folder_update_lock_{$folder->id}";
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$this->folder->updateCountersNow();
// Check if another job is processing this folder
if (Cache::has($this->lockKey)) {
return; // Exit if already processing
}
// Set lock with a timeout of 5 minutes (adjust as needed)
Cache::put($this->lockKey, true, now()->addMinutes(5));
try {
// Perform the folder update logic
$this->folder->updateCountersNow();
} finally {
// Release the lock after processing
Cache::forget($this->lockKey);
}
}
}
}

View File

@@ -25,6 +25,8 @@ class LogFailedLogin
*/
public function handle(Failed $event)
{
\Eventy::action('listeners.failed_login', $event);
activity()
//->causedBy($event->user)
->withProperties(['ip' => app('request')->ip(), 'email' => request()->email])

View File

@@ -81,6 +81,10 @@ class ReplyToCustomer extends Mailable
if (!empty($new_headers) || $from_alias) {
$mailbox = $this->mailbox;
$this->withSwiftMessage(function ($swiftmessage) use ($new_headers, $from_alias, $mailbox, $thread) {
\MailHelper::$smtp_mime_message = '';
$headers = null;
if (!empty($new_headers)) {
if (!empty($new_headers['Message-ID'])) {
$swiftmessage->setId($new_headers['Message-ID']);
@@ -111,6 +115,10 @@ class ReplyToCustomer extends Mailable
$from_alias_name = $mailbox_mail_from['name'];
}
if (!$headers) {
$headers = $swiftmessage->getHeaders();
}
$swift_from = $headers->get('From');
if ($from_alias_name) {
@@ -125,15 +133,24 @@ class ReplyToCustomer extends Mailable
}
}
\Eventy::action('email.reply_to_customer.swiftmessage', $swiftmessage, $from_alias, $thread, $mailbox);
if ($mailbox->imap_sent_folder) {
\MailHelper::$smtp_mime_message = $swiftmessage->toString();
}
return $swiftmessage;
});
}
$template_html = \Eventy::filter('email.reply_to_customer.template_name_html','emails/customer/reply_fancy');
$template_text = \Eventy::filter('email.reply_to_customer.template_name_text','emails/customer/reply_fancy_text');
// from($this->from) Sets only email, name stays empty.
// So we set from in Mail::setMailDriver
$message = $this->subject($this->subject)
->view('emails/customer/reply_fancy')
->text('emails/customer/reply_fancy_text');
->view($template_html)
->text($template_text);
if ($thread->has_attachments) {
foreach ($thread->attachments as $attachment) {

View File

@@ -91,15 +91,20 @@ class UserNotification extends Mailable
});
}
$subject = '[#'.$this->conversation->number.'] '.$this->conversation->subject;
$subject = \Eventy::filter('email.user_notification.subject', '[#' . (isset($this->conversation->number) ? $this->conversation->number : '') . '] ' . $this->conversation->subject, $this->conversation);
$customer = $this->conversation->customer;
$thread = $this->threads->first();
$template_html = \Eventy::filter('email.user_notification.template_name_html', 'emails/user/notification');
$template_text = \Eventy::filter('email.user_notification.template_name_text', 'emails/user/notification_text');
$template_fields = \Eventy::filter('email.user_notification.template_fields', ['customer' => $customer, 'thread' => $thread, 'mailbox' => $this->mailbox]);
return $this->subject($subject)
->from($this->from['address'], $this->from['name'])
->view('emails/user/notification', ['customer' => $customer, 'thread' => $thread, 'mailbox' => $this->mailbox])
->text('emails/user/notification_text', ['customer' => $customer, 'thread' => $thread, 'mailbox' => $this->mailbox]);
->view($template_html, $template_fields)
->text($template_text, $template_fields);
}
}

View File

@@ -58,6 +58,9 @@ class Mailbox extends Model
const OUT_ENCRYPTION_NONE = 1;
const OUT_ENCRYPTION_SSL = 2;
const OUT_ENCRYPTION_TLS = 3;
// StartTLS is already included in TLS:
// allow_self_signed = true
// verify_peer = false
public static $out_encryptions = [
self::OUT_ENCRYPTION_NONE => '',
@@ -82,11 +85,16 @@ class Mailbox extends Model
const IN_ENCRYPTION_NONE = 1;
const IN_ENCRYPTION_SSL = 2;
const IN_ENCRYPTION_TLS = 3;
// For Webklex/laravel-imap this option is the same as 'none'.
// For Webklex/php-imap it works:
// https://github.com/Webklex/php-imap/pull/180
const IN_ENCRYPTION_STARTTLS = 4;
public static $in_encryptions = [
self::IN_ENCRYPTION_NONE => '',
self::IN_ENCRYPTION_SSL => 'ssl',
self::IN_ENCRYPTION_TLS => 'tls',
self::IN_ENCRYPTION_STARTTLS => 'starttls',
];
/**
@@ -140,7 +148,7 @@ class Mailbox extends Model
*
* @var [type]
*/
protected $fillable = ['name', 'email', 'aliases', 'aliases_reply', 'auto_bcc', 'from_name', 'from_name_custom', 'ticket_status', 'ticket_assignee', 'template', 'before_reply', 'signature', 'out_method', 'out_server', 'out_username', 'out_password', 'out_port', 'out_encryption', 'in_server', 'in_port', 'in_username', 'in_password', 'in_protocol', 'in_encryption', 'in_validate_cert', 'auto_reply_enabled', 'auto_reply_subject', 'auto_reply_message', 'office_hours_enabled', 'ratings', 'ratings_placement', 'ratings_text', 'imap_sent_folder'];
protected $fillable = ['name', 'email', 'aliases', 'aliases_reply', 'auto_bcc', 'from_name', 'from_name_custom', 'ticket_status', 'ticket_assignee', 'before_reply', 'signature', 'out_method', 'out_server', 'out_username', 'out_password', 'out_port', 'out_encryption', 'in_server', 'in_port', 'in_username', 'in_password', 'in_protocol', 'in_encryption', 'in_validate_cert', 'auto_reply_enabled', 'auto_reply_subject', 'auto_reply_message', 'office_hours_enabled', 'ratings', 'ratings_placement', 'ratings_text', 'imap_sent_folder'];
protected static function boot()
{
@@ -969,6 +977,19 @@ class Mailbox extends Model
return $this->meta['oauth'][$param] ?? '';
}
public function inOauthEnabled()
{
return $this->oauthEnabled()
&& $this->in_username !== null && !strstr($this->in_username, '@');
}
public function outOauthEnabled()
{
return $this->oauthEnabled()
&& $this->out_username !== null && !strstr($this->out_username, '@')
&& $this->out_server !== null && trim($this->out_server) == \MailHelper::OAUTH_MICROSOFT_SMTP;
}
public function setEmailAttribute($value)
{
if ($value) {

View File

@@ -20,6 +20,13 @@ class MailboxUser extends Model
public $timestamps = false;
public static $pivot_settings = [
'after_send',
'hide',
'mute',
'access',
];
// Does not work as we receive it via pivot
// protected $casts = [
// 'access' => 'array',

View File

@@ -0,0 +1,178 @@
<?php
namespace App\Misc;
class ConversationActionButtons
{
// Location constants
const LOCATION_TOOLBAR = 'toolbar';
const LOCATION_DROPDOWN = 'dropdown';
const LOCATION_BOTH = 'both';
/**
* Get all available conversation actions
*/
public static function getActions($conversation, $user, $mailbox)
{
$actions = [
// Default toolbar actions
'reply' => [
'icon' => 'glyphicon-share-alt',
'location' => self::LOCATION_TOOLBAR,
'label' => __('Reply'),
'permission' => function ($conversation) {
return ( ! $conversation->isPhone() || ( $conversation->customer && $conversation->customer->getMainEmail() ) )
&& \Eventy::filter('conversation.reply_button.enabled', true, $conversation);
},
'class' => 'conv-reply',
'fixed_location' => true,
],
'note' => [
'icon' => 'glyphicon-edit',
'location' => self::LOCATION_TOOLBAR,
'label' => __('Note'),
'permission' => function ($conversation) {
return \Eventy::filter('conversation.note_button.enabled', true, $conversation);
},
'class' => 'conv-add-note',
'fixed_location' => true,
],
'delete' => [
'icon' => 'glyphicon-trash',
'location' => self::LOCATION_TOOLBAR,
'label' => $conversation->state != \App\Conversation::STATE_DELETED ? __('Delete') : __('Delete Forever'),
'permission' => function ($conversation, $user) {
return $user->can('delete', $conversation);
},
'class' => $conversation->state != \App\Conversation::STATE_DELETED ? 'conv-delete' : 'conv-delete-forever',
'fixed_location' => true,
],
'delete_mobile' => [
'icon' => 'glyphicon-trash',
'location' => self::LOCATION_DROPDOWN,
'label' => $conversation->state != \App\Conversation::STATE_DELETED ? __('Delete') : __('Delete Forever'),
'permission' => function ($conversation, $user) {
return $user->can('delete', $conversation);
},
'class' => $conversation->state != \App\Conversation::STATE_DELETED ? 'conv-delete' : 'conv-delete-forever',
'fixed_location' => true,
'mobile_only' => true,
],
// Default dropdown actions
'follow' => [
'icon' => 'glyphicon-bell',
'location' => self::LOCATION_DROPDOWN,
'label' => __('Follow'),
'permission' => function () {
return true;
},
'class' => 'conv-follow',
'has_opposite' => true,
'opposite' => [
'label' => __('Unfollow'),
'class' => 'conv-follow',
],
'fixed_location' => true,
],
'forward' => [
'icon' => 'glyphicon-arrow-right',
'location' => self::LOCATION_DROPDOWN,
'label' => __('Forward'),
'permission' => function () {
return true;
},
'class' => 'conv-forward',
'fixed_location' => true,
],
'merge' => [
'icon' => 'glyphicon-indent-left',
'location' => self::LOCATION_DROPDOWN,
'label' => __('Merge'),
'permission' => function ($conversation) {
return ! $conversation->isChat();
},
'class' => '',
'url' => function ($conversation) {
return route('conversations.ajax_html', array_merge([ 'action' => 'merge_conv' ], \Request::all(), [ 'conversation_id' => $conversation->id ]));
},
'attrs' => [
'data-trigger' => 'modal',
'data-modal-title' => __('Merge Conversations'),
'data-modal-no-footer' => 'true',
'data-modal-on-show' => 'initMergeConv',
],
'fixed_location' => true,
],
'move' => [
'icon' => 'glyphicon-log-out',
'location' => self::LOCATION_DROPDOWN,
'label' => __('Move'),
'permission' => function ($conversation, $user) {
return $user->can('move', \App\Conversation::class);
},
'class' => '',
'url' => function ($conversation) {
return route('conversations.ajax_html', array_merge([ 'action' => 'move_conv' ], \Request::all(), [ 'conversation_id' => $conversation->id ]));
},
'attrs' => [
'data-trigger' => 'modal',
'data-modal-title' => __('Move Conversation'),
'data-modal-no-footer' => 'true',
'data-modal-on-show' => 'initMoveConv',
],
'fixed_location' => true,
],
'print' => [
'icon' => 'glyphicon-print',
'location' => self::LOCATION_DROPDOWN,
'label' => __('Print'),
'class' => '',
'permission' => function () {
return true;
},
'url' => function () {
return \Request::getRequestUri() . '&amp;print=1';
},
'attrs' => [
'target' => '_blank',
],
'fixed_location' => true,
],
];
// Allow overriding default actions while preserving backwards compatibility
$actions = \Eventy::filter('conversation.get_action_buttons', $actions, $conversation, $user, $mailbox);
// Filter actions based on permissions
foreach ($actions as $key => $action) {
if (! $action['permission']($conversation, $user)) {
unset($actions[ $key ]);
}
}
return $actions;
}
/**
* Get actions for a specific location
*/
public static function getActionsByLocation($actions, $location)
{
return array_filter($actions, function ($action) use ($location) {
if (! empty($action['fixed_location'])) {
// Modified logic for responsive display
if ($location === self::LOCATION_TOOLBAR) {
// Show in toolbar if it's a toolbar action
return $action['location'] === self::LOCATION_TOOLBAR;
} elseif ($location === self::LOCATION_DROPDOWN) {
// Show in dropdown if explicitly flagged or if it's a dropdown action
return $action['location'] === self::LOCATION_DROPDOWN ||
( ! empty($action['show_in_dropdown']) && $action['location'] === self::LOCATION_TOOLBAR );
}
}
return $action['location'] === $location;
});
}
}

27
app/Misc/Functions.php Normal file
View File

@@ -0,0 +1,27 @@
<?php
if (! function_exists('__j')) {
/**
* Translate the given message and prepare it for using in JS.
*/
function __j($key, $replace = [], $locale = null)
{
$text = __($key, $replace, $locale);
return strtr($text, [
'"' => '&quot;',
"'" => '&#039;',
]);
}
/**
* Translate the given message and prepare it for using in HTML.
*
*/
function __h($key, $replace = [], $locale = null)
{
$text = __($key, $replace, $locale);
return htmlspecialchars($text);
}
}

View File

@@ -32,14 +32,18 @@ class Helper
/**
* Limit for IN queries.
* MariaDB may not work with more than 999 elements in IN clause.
* https://github.com/freescout-help-desk/freescout/issues/4623
*/
const IN_LIMIT = 1000;
const IN_LIMIT = 999;
/**
* Permissions for directories.
*/
const DIR_PERMISSIONS = 0755;
const DB_INT_MAX = 2147483647;
public static $csp_nonce = null;
/**
@@ -59,6 +63,8 @@ class Helper
'php.*',
'sh',
'pl',
'phtml',
'phar',
];
/**
@@ -324,6 +330,9 @@ class Helper
'ja' => ['name' => '日本語',
'name_en' => 'Japanese',
],
'kz' => ['name' => 'қазақ тілі',
'name_en' => 'Kazakh',
],
'ko' => ['name' => '한국어 (韓國語)',
'name_en' => 'Korean (Johab)',
],
@@ -518,19 +527,21 @@ class Helper
{
// Remove all kinds of spaces after tags.
// https://stackoverflow.com/questions/3230623/filter-all-types-of-whitespace-in-php
//
// Keep in mind that preg_replace() may return NULL if "u" flag is used.
$text = preg_replace("/^(.*)>[\r\n]*\s+/mu", '$1>', $text ?? '');
// Remove <script> and <style> blocks.
$text = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $text);
$text = preg_replace('#<style(.*?)>(.*?)</style>#is', '', $text);
$text = preg_replace('#<script(.*?)>(.*?)</script>#is', '', $text ?? '');
$text = preg_replace('#<style(.*?)>(.*?)</style>#is', '', $text ?? '');
// Remove tags.
$text = strip_tags($text);
$text = preg_replace('/\s+/mu', ' ', $text);
$text = strip_tags($text ?? '');
$text = preg_replace('/\s+/mu', ' ', $text ?? '');
// Trim
$text = trim($text);
$text = preg_replace('/^\s+/mu', '', $text);
$text = trim($text ?? '');
$text = preg_replace('/^\s+/mu', '', $text ?? '');
// Causes "General error: 1366 Incorrect string value"
// Remove "undetectable" whitespaces
@@ -541,11 +552,60 @@ class Helper
// }
// $text = urldecode($text);
$text = trim(preg_replace('/[ ]+/', ' ', $text));
$text = trim(preg_replace('/[ ]+/', ' ', $text ?? ''));
return $text;
}
public static function stripDangerousTags($html, $allowed_tags = [])
{
// <script src="/storage/attachment/8/1/1/test.js?id=7&token=c4786c4497db3c6254a0c310623a43c3">
// <iframe src="/storage/attachment/8/1/1/1.html?id=95&token=3dced8dc80305031b358119f3d156204"></iframe>
// <object data="/storage/attachment/8/1/1/1.html?id=95&token=3dced8dc80305031b358119f3d156204" type="text/html"></object>
$tags = ['script', 'form', 'iframe', 'object'];
$attrs = 'src|data';
$tags = array_diff($tags, $allowed_tags);
foreach ($tags as $tag) {
$html = preg_replace('#<'.$tag.'(.*?)>(.*?)<\s*/\s*'.$tag.'\s*>#is', '', $html ?? '');
// Remove unclosed restricted tags.
$html = preg_replace('#<'.$tag.'(.*?)>#is', '', $html ?? '');
}
// If some tag is allowed make sure that it does not point to the file on the current server.
if (!empty($allowed_tags)) {
foreach ($allowed_tags as $tag) {
$html = preg_replace_callback('#<'.$tag.'(.*?)>#is',
function ($matches) use ($attrs) {
preg_match("/(src|data)\s*=\s*['\"]([^'\"]+)['\"]/i", $matches[1], $attr_match);
if (!empty($attr_match[2])) {
$url = trim($attr_match[2]);
$parts = parse_url($url);
// Remove tag.
if (!preg_match("#^(https?:)?//#i", $url)
|| empty($parts['host'])
|| (strtolower($parts['host']) == strtolower(self::getDomain()))
|| preg_match("#/storage/attachment/.*token#", $parts['host'])
|| preg_match("#/storage/uploads/.*\.#", $parts['host'])
) {
return '';
}
}
return $matches[0];
},
$html
);
}
}
return $html;
}
/**
* Get preview of the text in a plain form.
*/
@@ -627,7 +687,7 @@ class Helper
/**
* Resize image without using Intervention package.
*/
public static function resizeImage($file, $mime_type, $thumb_width, $thumb_height)
public static function resizeImage($file, $mime_type, $thumb_width, $thumb_height, $transparency = false)
{
list($width, $height) = getimagesize($file);
if (!$width) {
@@ -637,8 +697,10 @@ class Helper
if (preg_match('/png/i', $mime_type)) {
$src = imagecreatefrompng($file);
$kek = imagecolorallocate($src, 255, 255, 255);
imagefill($src, 0, 0, $kek);
if (!$transparency) {
$kek = imagecolorallocate($src, 255, 255, 255);
imagefill($src, 0, 0, $kek);
}
} elseif (preg_match('/gif/i', $mime_type)) {
$src = imagecreatefromgif($file);
@@ -666,6 +728,10 @@ class Helper
}
$thumb = imagecreatetruecolor($thumb_width, $thumb_height);
if ($transparency && preg_match('/png/i', $mime_type)) {
imagealphablending($thumb, false);
imagesavealpha($thumb, true);
}
// Resize and crop
imagecopyresampled($thumb,
$src,
@@ -749,9 +815,9 @@ class Helper
return 'Error: '.$e->getMessage().'; File: '.$e->getFile().' ('.$e->getLine().')';
}
public static function denyAccess()
public static function denyAccess($msg = '')
{
abort(403, 'This action is unauthorized.');
abort(403, $msg ?: 'This action is unauthorized.');
}
/**
@@ -801,6 +867,21 @@ class Helper
\Log::error($prefix.self::formatException($e));
}
public static function encrypt($value, $password = null)
{
try {
if (!$password) {
$value = encrypt($value);
} else {
$value = (new \Illuminate\Encryption\Encrypter(md5($password)))->encrypt($value);
}
} catch (\Exception $e) {
// Do nothing.
}
return $value;
}
/**
* Safely decrypt.
*
@@ -808,10 +889,20 @@ class Helper
*
* @return [type] [description]
*/
public static function decrypt($value)
public static function decrypt($value, $password = null, $force_unserialize = false)
{
try {
$value = decrypt($value);
if (!$password) {
$value = app('encrypter')->decrypt($value, false);
} else {
$value = (new \Illuminate\Encryption\Encrypter(md5($password)))->decrypt($value, false);
}
// If the value is scalar - unserialize it,
// Otherwise - do not, as objects may contain dangerous code.
if (!preg_match("^[idsa]:", $value) || $force_unserialize) {
$value = unserialize($value);
}
} catch (\Exception $e) {
// Do nothing.
}
@@ -876,6 +967,16 @@ class Helper
}
}
public static function setUserLocale($user_locale = '')
{
if (!$user_locale) {
$user_locale = \Eventy::filter('locale', session('user_locale'));
}
if ($user_locale) {
\Helper::setLocale($user_locale);
}
}
public static function setLocale($locale)
{
if (in_array($locale, config('app.locales'))) {
@@ -896,14 +997,14 @@ class Helper
if (is_string($locale) && isset(self::$locales[$locale])) {
$data = self::$locales[$locale];
} else {
return;
return null;
}
if ($param) {
if (isset(self::$locales[$locale])) {
return self::$locales[$locale][$param];
} else {
return;
return null;
}
} else {
return $data;
@@ -928,6 +1029,8 @@ class Helper
$env_path = app()->environmentFilePath();
$contents = file_get_contents($env_path);
$value = preg_replace("#[\r\n\t]#", '', $value);
if (strstr($value, '"')) {
// Escape quotes.
$value = '"'.str_replace('"', '\"', $value).'"';
@@ -1183,7 +1286,19 @@ class Helper
*/
public static function strSplitKeepWords($str, $max_length = 75)
{
$array_words = explode(' ', $str);
$space = html_entity_decode('&nbsp;');
$str = strtr($str, [
'、' => '、'.$space,
'。' => '。'.$space,
// '.' => '.'.$space,
// ',' => ','.$space,
//':' => ':'.$space,
// '—' => '—'.$space,
// '।' => '।'.$space,
]);
$array_words = explode($space, $str);
$currentLength = 0;
@@ -1240,46 +1355,63 @@ class Helper
}
/**
* It looks like this is not used anywhere.
* Json encode to avoid "Unable to JSON encode payload. Error code: 5"
*/
// public static function jsonEncodeSafe($value, $options = 0, $depth = 512, $utfErrorFlag = false)
// {
// $encoded = json_encode($value, $options, $depth);
// switch (json_last_error()) {
// case JSON_ERROR_NONE:
// return $encoded;
// // case JSON_ERROR_DEPTH:
// // return 'Maximum stack depth exceeded'; // or trigger_error() or throw new Exception()
// // case JSON_ERROR_STATE_MISMATCH:
// // return 'Underflow or the modes mismatch'; // or trigger_error() or throw new Exception()
// // case JSON_ERROR_CTRL_CHAR:
// // return 'Unexpected control character found';
// // case JSON_ERROR_SYNTAX:
// // return 'Syntax error, malformed JSON'; // or trigger_error() or throw new Exception()
// case JSON_ERROR_UTF8:
// $clean = self::utf8ize($value);
// if ($utfErrorFlag) {
// //return 'UTF8 encoding error'; // or trigger_error() or throw new Exception()
// }
// return self::jsonEncodeSafe($clean, $options, $depth, true);
// // default:
// // return 'Unknown error'; // or trigger_error() or throw new Exception()
public static function jsonEncodeSafe($value, $options = 0, $depth = 512, $attempt = 1)
{
$msg = '';
$encoded = json_encode($value, $options, $depth);
// }
// }
switch (json_last_error()) {
case JSON_ERROR_NONE:
return $encoded;
case JSON_ERROR_DEPTH:
$msg = 'Maximum stack depth exceeded';
break;
case JSON_ERROR_STATE_MISMATCH:
$msg = 'Underflow or the modes mismatch';
break;
case JSON_ERROR_CTRL_CHAR:
$msg = 'Unexpected control character found';
break;
case JSON_ERROR_SYNTAX:
$msg = 'Syntax error, malformed JSON';
break;
case JSON_ERROR_UTF8:
$clean = self::utf8ize($value);
if ($attempt > 1) {
$msg = 'UTF8 encoding error';
} else {
return self::jsonEncodeSafe($clean, $options, $depth, 2);
}
break;
// default:
// return '';
}
throw new \Exception("Could not encode JSON: ".$msg, 1);
//return '';
}
// public static function utf8ize($mixed)
// {
// if (is_array($mixed)) {
// foreach ($mixed as $key => $value) {
// $mixed[$key] = self::utf8ize($value);
// }
// } else if (is_string ($mixed)) {
// return utf8_encode($mixed);
// }
// return $mixed;
// }
public static function utf8ize($mixed)
{
if (is_array($mixed)) {
foreach ($mixed as $key => $value) {
$mixed[$key] = self::utf8ize($value);
}
} else if (is_string($mixed)) {
return self::utf8Encode($mixed);
}
return $mixed;
}
/**
* Replacement for utf8_encode().
*/
public static function utf8Encode($string)
{
return mb_convert_encoding($string, 'UTF-8', 'UTF-8');
}
/**
* Check if host is available on the port specified.
@@ -1351,13 +1483,17 @@ class Helper
case 'http':
case 'https':
//$value = preg_replace_callback('~(?:(https?)://([^\s<]+)|(www\.[^\s<]+?\.[^\s<]+))(?<![\.,:])~i', function ($match) use ($protocol, &$links, $attr) {
$value = preg_replace_callback('%(\b(([\w-]+)://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))%s', function ($match) use ($protocol, &$links, $attr) {
if ($match[3]) {
$protocol = $match[3];
//$value = preg_replace_callback('%(\b(([\w-]+)://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))%s', function ($match) use ($protocol, &$links, $attr) {
// https://github.com/freescout-helpdesk/freescout/issues/3402
$nbsp = html_entity_decode('&nbsp;');
$value = preg_replace_callback('%([>\r\n\s:;\( '.$nbsp.']|^)((([\w-]+)://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))%s', function ($match) use ($protocol, &$links, $attr) {
if ($match[4]) {
$protocol = $match[4];
}
$link = $match[1];
$link = substr($link, strlen($match[2]));
return '<' . array_push($links, "<a $attr href=\"$protocol://$link\">$protocol://$link</a>") . '>';
$link = $match[2];
$link = substr($link, strlen($match[3]));
//return '<' . array_push($links, "<a $attr href=\"$protocol://$link\">$protocol://$link</a>") . '>';
return $match[1].'<' . array_push($links, "<a $attr href=\"$protocol://$link\">".$match[2]."</a>") . '>';
}, $value) ?: $value;
break;
case 'mail': $value = preg_replace_callback('~([^\s<>]+?@[^\s<]+?\.[^\s<]+)(?<![\.,:\)])~', function ($match) use (&$links, $attr) { return '<' . array_push($links, "<a $attr href=\"mailto:{$match[1]}\">{$match[1]}</a>") . '>'; }, $value) ?: $value;
@@ -1367,7 +1503,7 @@ class Helper
}
}
// Insert all link
// Insert all links
return preg_replace_callback('/<(\d+)>/', function ($match) use (&$links) { return $links[$match[1] - 1]; }, $value ?? '') ?: $value;
}
@@ -1409,7 +1545,7 @@ class Helper
$pids = [];
try {
$processes = preg_split("/[\r\n]/", shell_exec("ps aux | grep '".$search."'"));
$processes = preg_split("/[\r\n]/", \Helper::shellExec("ps aux | grep '".$search."'"));
foreach ($processes as $process) {
$process = trim($process);
preg_match("/^[\S]+\s+([\d]+)\s+/", $process, $m);
@@ -1537,6 +1673,14 @@ class Helper
return self::isPgSql() ? 'ilike' : 'like';
}
// PostgreSQL truncates string if it contains \u0000 symbol starting from this symbol.
// https://stackoverflow.com/questions/31671634/handling-unicode-sequences-in-postgresql
// https://github.com/freescout-helpdesk/freescout/issues/3485
public static function sqlSanitizeString($string)
{
return str_replace(json_decode('"\u0000"'), "", $string);
}
public static function humanFileSize($size, $unit="")
{
if ((!$unit && $size >= 1<<30) || $unit == "GB") {
@@ -1615,24 +1759,31 @@ class Helper
// 307 - Temporary Redirect.
if (!preg_match("/(200|301|302|307)/", $headers[0])) {
return false;
throw new \Exception('HTTP Status Code: '.$headers[0], 1);
//return false;
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_URL, $url);
\Helper::setCurlDefaultOptions($ch);
curl_setopt($ch, CURLOPT_TIMEOUT, 180);
$contents = curl_exec($ch);
if (curl_errno($ch)) {
throw new \Exception(curl_errno($ch).' '.curl_error($ch), 1);
$curl_errno = curl_errno($ch);
if ($curl_errno) {
throw new \Exception('Curl Error Number: '.$curl_errno, 1);
}
curl_close($ch);
if (!$contents) {
return false;
if ($contents == '') {
$https_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
throw new \Exception('Empty Response. Curl Error Number: '.$curl_errno.'. Response Status Code: '.$https_status, 1);
//return false;
} else {
curl_close($ch);
}
return $contents;
@@ -1686,7 +1837,7 @@ class Helper
} elseif ($ext == 'pdf') {
// Rename PDF to avoid running embedded JavaScript.
if ($uploaded_file && !$contents) {
$contents = file_get_contents($uploaded_file->getRealPath());
$contents = file_get_contents($uploaded_file->getRealPath() ?: $uploaded_file->getPathname());
}
if ($contents && strstr($contents, '/JavaScript')) {
$file_name = $file_name.'_';
@@ -1722,6 +1873,11 @@ class Helper
$file_name = mb_convert_encoding($file_name, 'UTF-8', 'UTF-8');
$file_name = preg_replace('/[' . $escaped_regex . ']/', '_', $file_name);
$file_name = preg_replace("/[\t\r\n]/", '', $file_name);
// Remove unprintable characters and invalid unicode characters.
// https://github.com/freescout-help-desk/freescout/issues/4681
$file_name = preg_replace("#\p{C}+#u", '', $file_name);
// https://github.com/freescout-help-desk/freescout/issues/2123#issuecomment-2775392740
$file_name = preg_replace("#\p{Cf}+#u", '', $file_name);
return $file_name;
}
@@ -1765,7 +1921,33 @@ class Helper
public static function isHttps($url = '')
{
return self::getProtocol($url) == 'https';
if (\Helper::isInstaller()) {
// In the Installer we determine HTTPS from URL.
return self::isCurrentUrlHttps();
} else {
return self::getProtocol($url) == 'https';
}
}
public static function isInstaller()
{
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
$request_uri = preg_replace("#\?.*#", '', $request_uri);
return strstr($request_uri, '/install/') || preg_match("#/install$#", $request_uri);
}
public static function isCurrentUrlHttps()
{
if (in_array(strtolower($_SERVER['X_FORWARDED_PROTO'] ?? ''), array('https', 'on', 'ssl', '1'), true)
|| strtolower($_SERVER['HTTPS'] ?? '') == 'on'
|| ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') == 'https'
|| ($_SERVER['HTTP_CF_VISITOR'] ?? '') == '{"scheme":"https"}'
) {
return true;
} else {
return false;
}
}
public static function fixProtocol($url)
@@ -1883,7 +2065,7 @@ class Helper
if (self::isConsole() || !function_exists('shell_exec')) {
$pcntl_enabled = extension_loaded('pcntl');
} else {
$pcntl_enabled = preg_match("/enable/m", shell_exec("php -i | grep pcntl") ?? '');
$pcntl_enabled = preg_match("/enable/m", \Helper::shellExec("php -i | grep pcntl") ?? '');
}
$php_extensions['pcntl (console PHP)'] = $pcntl_enabled;
@@ -1897,7 +2079,13 @@ class Helper
'proc_open (PHP)' => function_exists('proc_open'),
'fpassthru (PHP)' => function_exists('fpassthru'),
'symlink (PHP)' => function_exists('symlink'),
'ps (shell)' => function_exists('shell_exec') ? shell_exec('ps') : false,
'iconv (PHP)' => function_exists('iconv'),
// If posix_isatty() function is not enabled on the server the question in the
// console command makes it wait infinitely and be aborted.
// Commands should avoid using interctive functions or use special flags.
//'posix_isatty (PHP)' => function_exists('posix_isatty'),
'pcntl_signal (console PHP)' => function_exists('shell_exec') ? (int)\Helper::shellExec('php -r "echo (int)function_exists(\'pcntl_signal\');"') : false,
'ps (shell)' => function_exists('shell_exec') ? \Helper::shellExec('ps') : false,
];
}
@@ -1923,7 +2111,7 @@ class Helper
if (\Option::get('send_emails_problem')) {
$flashes[] = [
'type' => 'warning',
'text' => __('There is a problem processing outgoing mail queue — an admin should check :%a_begin%System Status:%a_end% and :%a_begin_recommendations%Recommendations:%a_end%', ['%a_begin%' => '<a href="'.route('system').'#cron" target="_blank">', '%a_end%' => '</a>', /*'%a_begin_logs%' => '<a href="'.route('logs', ['name' => 'send_errors']).'#cron" target="_blank">',*/ '%a_begin_recommendations%' => '<a href="https://github.com/freescout-helpdesk/freescout/wiki/Background-Jobs" target="_blank">']),
'text' => __('There is a problem processing outgoing mail queue — an admin should check :%a_begin%System Status:%a_end% and :%a_begin_recommendations%Recommendations:%a_end%', ['%a_begin%' => '<a href="'.route('system').'#cron" target="_blank">', '%a_end%' => '</a>', /*'%a_begin_logs%' => '<a href="'.route('logs', ['name' => 'send_errors']).'#cron" target="_blank">',*/ '%a_begin_recommendations%' => '<a href="'.config('app.freescout_repo').'/wiki/Background-Jobs" target="_blank">']),
'unescaped' => true,
];
}
@@ -1997,7 +2185,7 @@ class Helper
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, config('app.curl_ssl_verifypeer'));
}
public static function setGuzzleDefaultOptions($params)
public static function setGuzzleDefaultOptions($params = [])
{
$default_params = [
'timeout' => config('app.curl_timeout'),
@@ -2027,9 +2215,28 @@ class Helper
$nonce = \Helper::cspNonce();
return "<meta http-equiv=\"Content-Security-Policy\" content=\"script-src 'self' 'nonce-".$nonce."' "
.config('app.csp_script_src').' '.\Eventy::filter('csp.script_src', '')."\">";
//<meta property=\"csp-nonce\" id=\"csp-nonce\" content=\"".$nonce."\">";
$script_src = config('app.csp_script_src').' '.\Eventy::filter('csp.script_src', '');
$script_domains = '';
$scripts = explode(' ', $script_src);
foreach ($scripts as $url) {
$url = trim($url);
if (!preg_match("#^(http|//)#", $url)) {
$url = '//'.$url;
}
$parts = parse_url($url);
if (!empty($parts['host'])) {
$domain = preg_replace("#['\"; \r\n]#", '', $parts['host']);
$script_domains .= ' '.$domain;
}
}
// frame-src https://recaptcha.net; connect-src https://recaptcha.net;
return "<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'self' ".$script_domains."; img-src * 'self' data:; font-src * 'self' data:; style-src * 'self' 'unsafe-inline'; form-action 'self'; frame-src * 'self'; script-src 'self' 'nonce-".$nonce."' "
.$script_src.";"
.config('app.csp_custom').\Eventy::filter('csp.custom', '')."\">";
}
public static function cspNonceAttr()
@@ -2059,4 +2266,44 @@ class Helper
\Session::forget('chat_mode');
}
}
public static function detectCloudFlare()
{
if (!empty($_SERVER['HTTP_CF_IPCOUNTRY'])
|| !empty($_SERVER['HTTP_CF_CONNECTING_IP'])
|| !empty($_SERVER['HTTP_CF_VISITOR'])
|| !empty($_SERVER['HTTP_CF_RAY'])
|| ($_SERVER['HTTP_CDN_LOOP'] ?? '') == 'cloudflare'
) {
return true;
} else {
return false;
}
}
// Correct format: 2023-12-14 19:21
// Datepicker with enableTime option enabled
// may return value in different format on iOS Safari: 2023-12-14T11:25
public static function sanitizeDatepickerDatetime($datetime)
{
return str_replace('T', ' ', $datetime);
}
// To catch possible exception:
// shell_exec(): Unable to execute
public static function shellExec($command)
{
try {
return shell_exec($command);
} catch (\Exception $e) {
self::logException($e, '\Helper::shellExec() - ');
}
return '';
}
public static function startsiWith($text, $string)
{
return (stripos($text, $string) === 0);
}
}

View File

@@ -5,7 +5,7 @@ namespace App\Misc;
use App\Mailbox;
use App\Option;
use App\SendLog;
use Webklex\IMAP\Client;
//use Webklex\IMAP\Client;
// todo: rename into MailHelper
class Mail
@@ -15,6 +15,7 @@ class Mail
*/
const REPLY_SEPARATOR_HTML = 'fsReplyAbove';
const REPLY_SEPARATOR_TEXT = '-- Please reply above this line --';
const REPLY_SEPARATOR_NOTIFICATION = 'fsNotifReplyAbove';
/**
* Message-ID prefixes for outgoing emails.
@@ -48,6 +49,7 @@ class Mail
const FETCH_SCHEDULE_HOURLY = 60;
const OAUTH_PROVIDER_MICROSOFT = 'ms';
const OAUTH_MICROSOFT_SMTP = 'smtp.office365.com';
/**
* If reply is not extracted properly from the incoming email, add here a new separator.
@@ -60,7 +62,11 @@ class Mail
self::REPLY_SEPARATOR_TEXT, // Our plain text separator
// Email service providers specific separators.
// <div class="gmail_quote" style="font-family:sans-serif;">
'<div class="gmail_quote">', // Gmail
'<div class="gmail_quote" ', // Gmail
'<div class="gmail_quote gmail_quote_container"', // Gmail
'<div class="protonmail_quote">', // https://github.com/freescout-help-desk/freescout/issues/4537
'<div id="appendonsend"></div>', // Outlook / Live / Hotmail / Microsoft
'<div name="quote" ',
'yahoo_quoted_', // Yahoo, full: <div id=3D"ydp6h4f5c59yahoo_quoted_2937493705"
@@ -68,15 +74,45 @@ class Mail
'------------------ Original ------------------', // QQ English
'<div id=3D"divRplyFwdMsg" dir=', // Outlook
'regex:/<div style="border:none;border\-top:solid \#[A-Z0-9]{6} 1\.0pt;padding:3\.0pt 0in 0in 0in">[^<]*<p class="MsoNormal"><b>/', // MS Outlook
// https://github.com/freescout-help-desk/freescout/issues/4629#issuecomment-2870297514
'regex:/<div style="border:none;border\-top:solid \#[A-Z0-9]{6} 1\.0pt;padding:3\.0pt 0cm 0cm 0cm">[^<]*<p class="MsoNormal"><b>/', // MS Outlook
// General separators.
'regex:/<blockquote((?!quote)[^>])*>/', // General sepator. Should skip Gmail's <blockquote class="gmail_quote">.
//'regex:/<blockquote((?!quote)[^>])*>/', // General sepator. Should skip Gmail's <blockquote class="gmail_quote">.
// https://github.com/freescout-help-desk/freescout/issues/4629#issuecomment-2870299221
'<blockquote type="cite"',
// This is used both for quotes and replies.
//'<blockquote class="gmail_quote"',
// https://github.com/freescout-help-desk/freescout/issues/4629#issuecomment-2874816496
'<div dir="auto" id="mail-editor-reference-message-container">',
// https://github.com/freescout-help-desk/freescout/issues/4764
'<!--html--><section>',
'regex:/<!\-\-html\-\->\s*<section>/',
'<!-- originalMessage -->',
'‐‐‐‐‐‐‐ Original Message ‐‐‐‐‐‐‐',
'--------------- Original Message ---------------',
'-------- Αρχικό μήνυμα --------', // Greek
];
/**
* Used to substitue encoding during mail body decoding
* via iconv() or mb_convert_encoding().
* https://github.com/freescout-help-desk/freescout/issues/4282
*/
public static $encoding_substitution = [
'iso-2022-jp' => 'iso-2022-jp-ms',
'gb2312' => 'gb18030',
];
/**
* Used when decoding mime strings.
*/
public static $mime_encoding_substitution = [
'iso-2022-jp' => 'iso-2022-jp-ms',
'ks_c_5601-1987' => 'cp949',
//'gb2312' => 'gb18030',
];
/**
* md5 of the last applied mail config.
*/
@@ -86,6 +122,11 @@ class Mail
* Used to get SMTP queue id when sending emails to customers.
*/
public static $smtp_queue_id_plugin_registered = false;
/**
* Used to store the last sent email message.
*/
public static $smtp_mime_message = '';
/**
* Configure mail sending parameters.
@@ -97,20 +138,52 @@ class Mail
public static function setMailDriver($mailbox = null, $user_from = null, $conversation = null)
{
if ($mailbox) {
// Configure mail driver according to Mailbox settings
// Configure mail driver according to Mailbox settings.
$oauth = $mailbox->outOauthEnabled();
// Refresh Access Token.
if ($oauth) {
if ((strtotime($mailbox->oauthGetParam('issued_on')) + (int)$mailbox->oauthGetParam('expires_in')) < time()) {
// Try to get an access token (using the authorization code grant)
$token_data = \MailHelper::oauthGetAccessToken(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
'client_id' => $mailbox->out_username,
'client_secret' => $mailbox->out_password,
'refresh_token' => $mailbox->oauthGetParam('r_token'),
]);
if (!empty($token_data['a_token'])) {
$mailbox->setMetaParam('oauth', $token_data, true);
} elseif (!empty($token_data['error'])) {
$error_message = 'Error occurred refreshing oAuth Access Token: '.$token_data['error'];
\Helper::log(\App\ActivityLog::NAME_EMAILS_SENDING,
\App\ActivityLog::DESCRIPTION_EMAILS_SENDING_ERROR_TO_CUSTOMER, [
'error' => $error_message,
'mailbox' => $mailbox->name,
]);
//throw new \Exception($error_message, 1);
}
}
}
\Config::set('mail.driver', $mailbox->getMailDriverName());
\Config::set('mail.from', $mailbox->getMailFrom($user_from, $conversation));
// SMTP
// SMTP.
if ($mailbox->out_method == Mailbox::OUT_METHOD_SMTP) {
\Config::set('mail.host', $mailbox->out_server);
\Config::set('mail.port', $mailbox->out_port);
if (!$mailbox->out_username) {
\Config::set('mail.username', null);
\Config::set('mail.password', null);
if ($oauth) {
\Config::set('mail.auth_mode', 'XOAUTH2');
\Config::set('mail.username', $mailbox->email);
\Config::set('mail.password', $mailbox->oauthGetParam('a_token'));
} else {
\Config::set('mail.username', $mailbox->out_username);
\Config::set('mail.password', $mailbox->out_password);
if (!$mailbox->out_username) {
\Config::set('mail.username', null);
\Config::set('mail.password', null);
} else {
\Config::set('mail.username', $mailbox->out_username);
\Config::set('mail.password', $mailbox->out_password);
}
}
\Config::set('mail.encryption', $mailbox->getOutEncryptionName());
}
@@ -222,6 +295,33 @@ class Mail
$vars = \Eventy::filter('mail_vars.replace', $vars, $data);
/**
* Retrieves all mail var codes from the text, including fallback values.
*
* @link https://regex101.com/r/icWukp/1
*/
preg_match_all(
'#\{%(?<var>[a-zA-Z.]+)(,fallback=(?<fallback>[^}]*))?%\}#',
$text,
$matches
);
// Add fallback values to the $vars array, if present.
foreach($matches['var'] as $i => $var) {
$merge_code = "{%{$var}%}";
$full_match = $matches[0][$i];
$has_fallback = false !== strpos($full_match, ',fallback=');
$fallback_val = $has_fallback ? $matches['fallback'][$i] ?? null : null;
$merge_val = isset($vars[$merge_code]) ? $vars[$merge_code] : $fallback_val;
if (null !== $merge_val || true === $remove_non_replaced) {
$vars[$full_match] = $merge_val;
$vars[$merge_code] = $merge_val;
}
}
$vars = \Eventy::filter('mail_vars.replace_after_fallback', $vars, $data);
if ($escape) {
foreach ($vars as $i => $var) {
$vars[$i] = htmlspecialchars($var ?? '');
@@ -281,14 +381,29 @@ class Mail
return Option::get('mail_driver', 'mail');
}
public static function registerSmtpLogger()
{
$logger = new \Swift_Plugins_Loggers_ArrayLogger();
\Mail::getSwiftMailer()->registerPlugin(new \Swift_Plugins_LoggerPlugin($logger));
return $logger;
}
/**
* Send test email from mailbox.
*/
public static function sendTestMail($to, $mailbox = null)
{
$result = [
'status' => 'success',
'msg' => '',
'log' => '',
];
if ($mailbox) {
// Configure mail driver according to Mailbox settings
\MailHelper::setMailDriver($mailbox);
$smtp_logger = self::registerSmtpLogger();
$status_message = '';
@@ -301,6 +416,7 @@ class Mail
} else {
// System email
\MailHelper::setSystemMailDriver();
$smtp_logger = self::registerSmtpLogger();
$status_message = '';
@@ -316,15 +432,17 @@ class Mail
if (\Mail::failures() || $status_message) {
SendLog::log(null, null, $to, SendLog::MAIL_TYPE_TEST, SendLog::STATUS_SEND_ERROR, null, null, $status_message);
if ($status_message) {
throw new \Exception($status_message, 1);
} else {
return false;
$result['msg'] = $status_message;
}
$result['status'] = 'error';
$result['log'] = $smtp_logger->dump();
} else {
SendLog::log(null, null, $to, SendLog::MAIL_TYPE_TEST, SendLog::STATUS_ACCEPTED);
return true;
$result['status'] = 'success';
}
return $result;
}
/**
@@ -332,41 +450,64 @@ class Mail
*/
public static function fetchTest($mailbox)
{
$client = \MailHelper::getMailboxClient($mailbox);
$result = [
'result' => 'success',
'msg' => '',
'log' => '',
];
// Connect to the Server
$client->connect();
$client = null;
// Get folder
$folder = $client->getFolder('INBOX');
try {
\Config::set('imap.options.debug', true);
\Webklex\PHPIMAP\Connection\Protocols\ImapProtocol::$output_debug_log = false;
\Webklex\PHPIMAP\Connection\Protocols\PopProtocol::$output_debug_log = false;
if (!$folder) {
throw new \Exception('Could not get mailbox folder: INBOX', 1);
}
// Get unseen messages for a period
$messages = $folder->query()->unseen()->since(now()->subDays(1))->leaveUnread()->get();
$client = \MailHelper::getMailboxClient($mailbox);
$last_error = '';
if (method_exists($client, 'getLastError')) {
$last_error = $client->getLastError();
}
if ($last_error && stristr($last_error, 'The specified charset is not supported')) {
// Solution for MS mailboxes.
// https://github.com/freescout-helpdesk/freescout/issues/176
$messages = $folder->query()->unseen()->since(now()->subDays(1))->leaveUnread()->setCharset(null)->get();
if (count($client->getErrors()) > 1) {
$last_error = $client->getLastError();
} else {
$last_error = null;
// Connect to the Server
$client->connect();
// Get folder
$folder = $client->getFolder('INBOX');
if (!$folder) {
throw new \Exception('Could not get mailbox folder: INBOX', 1);
}
// Get unseen messages for a period
$messages = $folder->query()->unseen()->since(now()->subDays(1))->leaveUnread()->get();
$last_error = '';
if (method_exists($client, 'getLastError')) {
$last_error = $client->getLastError();
}
if ($last_error && stristr($last_error, 'The specified charset is not supported')) {
// Solution for MS mailboxes.
// https://github.com/freescout-helpdesk/freescout/issues/176
$messages = $folder->query()->unseen()->since(now()->subDays(1))->leaveUnread()->setCharset(null)->get();
if (count($client->getErrors()) > 1) {
$last_error = $client->getLastError();
} else {
$last_error = null;
}
}
if ($last_error) {
//throw new \Exception($last_error, 1);
$result['result'] = 'error';
$result['msg'] = $last_error;
}
} catch (\Exception $e) {
$result['result'] = 'error';
$result['msg'] = $e->getMessage();
}
if ($last_error) {
throw new \Exception($last_error, 1);
} else {
return true;
if ($result['result'] == 'error') {
$result['log'] = \Webklex\PHPIMAP\Connection\Protocols\ImapProtocol::getDebugLog();
}
return $result;
}
/**
@@ -501,9 +642,11 @@ class Mail
$autoresponder_headers = [
'x-autoreply' => '',
'x-autorespond' => '',
'x-autoresponder' => '',
'auto-submitted' => '', // this can be auto-replied, auto-generated, etc.
'precedence' => ['auto_reply', 'bulk', 'junk'],
'x-precedence' => ['auto_reply', 'bulk', 'junk'],
'delivered-to' => ['autoresponder'],
'precedence' => ['auto_reply', 'bulk', 'junk', 'list'],
'x-precedence' => ['auto_reply', 'bulk', 'junk', 'list'],
];
$headers = explode("\n", $headers_str ?? '');
@@ -564,15 +707,35 @@ class Mail
*/
public static function parseHeaders($headers_str)
{
try {
return imap_rfc822_parse_headers($headers_str);
} catch (\Exception $e) {
return;
//try {
//return imap_rfc822_parse_headers($headers_str);
//return (new \Webklex\PHPIMAP\Header(''))->rfc822_parse_headers($headers_str);
return \Webklex\PHPIMAP\Header::rfc822_parse_headers($headers_str);
// } catch (\Exception $e) {
// return;
// }
}
// Replacement for https://www.php.net/manual/en/function.imap-utf8.php
public static function imapUtf8($mime_encoded_text)
{
if (function_exists('imap_utf8')) {
return imap_utf8($mime_encoded_text);
} else {
return iconv_mime_decode($mime_encoded_text, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, "UTF-8");
}
}
public static function getHeader($headers_str, $header)
{
// Quick check to same resources.
if (!stristr($headers_str, $header)) {
return '';
}
$header = strtolower($header);
$header = str_replace('-', '_', $header);
$headers = self::parseHeaders($headers_str);
if (!$headers) {
return;
@@ -581,7 +744,7 @@ class Mail
if (property_exists($headers, $header)) {
$value = $headers->$header;
} else {
return;
return '';
}
switch ($header) {
case 'message_id':
@@ -598,9 +761,10 @@ class Mail
public static function getMailboxClient($mailbox)
{
$oauth = $mailbox->oauthEnabled();
$new_library = config('app.new_fetching_library');
/*$new_library = config('app.new_fetching_library');
if (!$oauth && !$new_library) {
if (!$new_library) {
// Old.
return new \Webklex\IMAP\Client([
'host' => $mailbox->in_server,
'port' => $mailbox->in_port,
@@ -610,71 +774,71 @@ class Mail
'password' => $mailbox->in_password,
'protocol' => $mailbox->getInProtocolName(),
]);
} else {*/
// New
if ($oauth) {
\Config::set('imap.accounts.default', [
'host' => $mailbox->in_server,
'port' => $mailbox->in_port,
'encryption' => $mailbox->getInEncryptionName(),
'validate_cert' => $mailbox->in_validate_cert,
'username' => $mailbox->email,
'password' => $mailbox->oauthGetParam('a_token'),
'protocol' => $mailbox->getInProtocolName(),
'authentication' => 'oauth',
]);
} else {
\Config::set('imap.accounts.default', [
'host' => $mailbox->in_server,
'port' => $mailbox->in_port,
'encryption' => $mailbox->getInEncryptionName(),
'validate_cert' => $mailbox->in_validate_cert,
// 'username' => $mailbox->email,
// 'password' => $mailbox->oauthGetParam('a_token'),
// 'protocol' => $mailbox->getInProtocolName(),
// 'authentication' => 'oauth',
'username' => $mailbox->in_username,
'password' => $mailbox->in_password,
'protocol' => $mailbox->getInProtocolName(),
]);
}
// To enable debug: /vendor/webklex/php-imap/src/Connection/Protocols
// Debug in console
if (app()->runningInConsole()) {
\Config::set('imap.options.debug', config('app.debug'));
}
if ($oauth) {
\Config::set('imap.accounts.default', [
'host' => $mailbox->in_server,
'port' => $mailbox->in_port,
'encryption' => $mailbox->getInEncryptionName(),
'validate_cert' => $mailbox->in_validate_cert,
'username' => $mailbox->email,
'password' => $mailbox->oauthGetParam('a_token'),
'protocol' => $mailbox->getInProtocolName(),
'authentication' => 'oauth',
$cm = new \Webklex\PHPIMAP\ClientManager(config('imap'));
// Refresh Access Token.
if ($oauth) {
if ((strtotime($mailbox->oauthGetParam('issued_on')) + (int)$mailbox->oauthGetParam('expires_in')) < time()) {
// Try to get an access token (using the authorization code grant)
$token_data = \MailHelper::oauthGetAccessToken(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
'client_id' => $mailbox->in_username,
'client_secret' => $mailbox->in_password,
'refresh_token' => $mailbox->oauthGetParam('r_token'),
]);
} else {
\Config::set('imap.accounts.default', [
'host' => $mailbox->in_server,
'port' => $mailbox->in_port,
'encryption' => $mailbox->getInEncryptionName(),
'validate_cert' => $mailbox->in_validate_cert,
// 'username' => $mailbox->email,
// 'password' => $mailbox->oauthGetParam('a_token'),
// 'protocol' => $mailbox->getInProtocolName(),
// 'authentication' => 'oauth',
'username' => $mailbox->in_username,
'password' => $mailbox->in_password,
'protocol' => $mailbox->getInProtocolName(),
]);
}
// To enable debug: /vendor/webklex/php-imap/src/Connection/Protocols
// Debug in console
if (app()->runningInConsole()) {
\Config::set('imap.options.debug', config('app.debug'));
}
$cm = new \Webklex\PHPIMAP\ClientManager(config('imap'));
// Refresh Access Token.
if ($oauth) {
if ((strtotime($mailbox->oauthGetParam('issued_on')) + (int)$mailbox->oauthGetParam('expires_in')) < time()) {
// Try to get an access token (using the authorization code grant)
$token_data = \MailHelper::oauthGetAccessToken(\MailHelper::OAUTH_PROVIDER_MICROSOFT, [
'client_id' => $mailbox->in_username,
'client_secret' => $mailbox->in_password,
'refresh_token' => $mailbox->oauthGetParam('r_token'),
if (!empty($token_data['a_token'])) {
$mailbox->setMetaParam('oauth', $token_data, true);
} elseif (!empty($token_data['error'])) {
$error_message = 'Error occurred refreshing oAuth Access Token: '.$token_data['error'];
\Helper::log(\App\ActivityLog::NAME_EMAILS_FETCHING,
\App\ActivityLog::DESCRIPTION_EMAILS_FETCHING_ERROR, [
'error' => $error_message,
'mailbox' => $mailbox->name,
]);
if (!empty($token_data['a_token'])) {
$mailbox->setMetaParam('oauth', $token_data, true);
} elseif (!empty($token_data['error'])) {
$error_message = 'Error occurred refreshing oAuth Access Token: '.$token_data['error'];
\Helper::log(\App\ActivityLog::NAME_EMAILS_FETCHING,
\App\ActivityLog::DESCRIPTION_EMAILS_FETCHING_ERROR, [
'error' => $error_message,
'mailbox' => $mailbox->name,
]);
throw new \Exception($error_message, 1);
}
throw new \Exception($error_message, 1);
}
}
// This makes it authenticate two times.
//$cm->setTimeout(60);
return $cm->account('default');
}
// This makes it authenticate two times.
//$cm->setTimeout(60);
return $cm->account('default');
//}
}
/**
@@ -705,7 +869,7 @@ class Mail
$client = \MailHelper::getMailboxClient($mailbox);
$client->connect();
} catch (\Exception $e) {
\Helper::logException($e, '('.$mailbox->name.') Could not fetch specific message by Message-ID via IMAP:');
\Helper::logException($e, '('.$mailbox->name.') Could not fetch specific message by Message-ID:');
return null;
}
@@ -714,9 +878,16 @@ class Mail
foreach ($imap_folders as $folder_name) {
try {
$folder = self::getImapFolder($client, $folder_name);
if (!$folder) {
\Log::error('('.$mailbox->name.') Show Original - folder not found: '.$folder_name);
continue;
}
// Message-ID: <123@123.com>
$search_message_id = addcslashes($message_id, '\"');
$query = $folder->query()
->text('<'.$message_id.'>')
//->text('<'.$message_id.'>')
->whereMessageId('"<'.$search_message_id.'>"')
->leaveUnread()
->limit(1);
@@ -741,10 +912,11 @@ class Mail
if ($last_error && stristr($last_error, 'The specified charset is not supported')) {
// Solution for MS mailboxes.
// https://github.com/freescout-helpdesk/freescout/issues/176
$query = $folder->query()->text('<'.$message_id.'>')->leaveUnread()->limit(1)->setCharset(null);
//$query = $folder->query()->text('<'.$message_id.'>')->leaveUnread()->limit(1)->setCharset(null);
$query = $folder->query()->whereMessageId('"<'.$search_message_id.'>"')->leaveUnread()->limit(1)->setCharset(null);
if ($message_date) {
$query->since($message_date->subDays(7));
$query->before($message_date->addDays(7));
$query->before($message_date->addDays(14));
}
$messages = $query->get();
$no_charset = true;
@@ -896,6 +1068,9 @@ class Mail
public static function getImapFolder($client, $folder_name)
{
// https://github.com/freescout-helpdesk/freescout/issues/3502
$folder_name = mb_convert_encoding($folder_name, "UTF7-IMAP","UTF-8");
if (method_exists($client, 'getFolderByPath')) {
return $client->getFolderByPath($folder_name);
} else {
@@ -908,12 +1083,19 @@ class Mail
*/
public static function decodeSubject($subject)
{
// Sometimes trying to decode non-encoded strings leads
// to loosing accents.
// https://github.com/freescout-help-desk/freescout/issues/4506
if (!strstr($subject, '=?')) {
return $subject;
}
// Remove new lines as iconv_mime_decode() may loose a part separated by new line:
// =?utf-8?Q?Gesch=C3=A4ftskonto?= erstellen =?utf-8?Q?f=C3=BCr?=
// 249143
$subject = preg_replace("/[\r\n]/", '', $subject);
// https://github.com/freescout-helpdesk/freescout/issues/3185
$subject = str_ireplace('=?iso-2022-jp?', '=?iso-2022-jp-ms?', $subject);
//$subject = str_ireplace('=?iso-2022-jp?', '=?iso-2022-jp-ms?', $subject);
$subject = self::substituteMimeEncoding($subject);
// Sometimes imap_utf8() can't decode the subject, for example:
// =?iso-2022-jp?B?GyRCIXlCaBsoQjEzMhskQjlmISEhViUsITwlRyVzGyhCJhskQiUoJS8lOSVGJWolIiFXQGxMZ0U5JE4kPyRhJE4jURsoQiYbJEIjQSU1JW0lcyEhIVo3bjQpJSglLyU5JUYlaiUiISYlbyE8JS8hWxsoQg==?=
@@ -966,7 +1148,7 @@ class Mail
// Try imap_utf8().
// =?iso-2022-jp?B?IBskQiFaSEcyPDpuQ?= =?iso-2022-jp?B?C4wTU1qIVs3Mkp2JSIlLyU3JSItahsoQg==?=
$subject_decoded = \imap_utf8($joined_parts);
$subject_decoded = self::imapUtf8($joined_parts);
if ($subject_decoded
&& trim($subject_decoded) != trim($joined_parts)
@@ -989,7 +1171,7 @@ class Mail
// =?iso-2022-jp?B?IBskQiFaSEcyPDpuQC4wTU1qIVs3Mkp2JSIlLyU3JSItahsoQg==?=
// =?iso-2022-jp?B?GyRCQGlNVTtZRTkhIT4uTlMbKEI=?=
if (self::isNotYetFullyDecoded($subject_decoded)) {
$subject_decoded = \imap_utf8($subject);
$subject_decoded = self::imapUtf8($subject);
}
// All previous functions could not decode text.
@@ -1016,6 +1198,61 @@ class Mail
|| \Str::contains($subject_decoded, $invalid_utf_symbols);
}
public static function getHashedReplySeparator($message_id) {
$separator = \MailHelper::REPLY_SEPARATOR_HTML;
if ($message_id) {
$separator .= substr(md5($message_id.config('app.key')), 0, 8);
}
return $separator;
}
// Sanitize status message - remove SMTP username and password.
public static function sanitizeSmtpStatusMessage($status_message) {
$status_message = preg_replace('#(username ")[^"]+(")#', '$1***$2', $status_message ?? '');
$status_message = preg_replace("#(Swift_Transport_Esmtp_Auth_LoginAuthenticator\->authenticate\(Object\(Swift_SmtpTransport\), ')[^\']+(', ')[^\']+('\))#", '$1***$2***$3', $status_message ?? '');
return $status_message;
}
public static function parseEml($content, $mailbox) {
if (!str_contains($content, "\r\n")){
$content = str_replace("\n", "\r\n", $content);
}
$raw_header = substr($content, 0, strpos($content, "\r\n\r\n"));
$raw_body = substr($content, strlen($raw_header)+8);
//\Config::set('app.new_fetching_library', 'true');
$client = \MailHelper::getMailboxClient($mailbox);
$client->openFolder("INBOX");
return \Webklex\PHPIMAP\Message::make(null, null, $client, $raw_header, $raw_body, [], \Webklex\PHPIMAP\IMAP::ST_UID);
}
// Substitue encoding during mail body decoding.
// https://github.com/freescout-help-desk/freescout/issues/4282
public static function substituteEncoding($encoding)
{
$encoding = strtolower($encoding);
if (!empty(self::$encoding_substitution[$encoding])) {
return self::$encoding_substitution[$encoding];
} else {
return $encoding;
}
}
public static function substituteMimeEncoding($string)
{
foreach (self::$mime_encoding_substitution as $from => $into) {
$string = str_ireplace('=?'.$from.'?', '=?'.$into.'?', $string);
}
return $string;
}
// public static function oauthGetProvider($provider_code, $params)
// {
// $provider = null;

View File

@@ -401,76 +401,45 @@ class Module extends Model
// Download new version.
if (!$result['msg']) {
$params = [
'license' => self::getLicense($alias),
'module_alias' => $alias,
'url' => self::getAppUrl(),
];
$license_details = WpApi::getVersion($params);
// Check if the module's author is officially recognized
if (self::isOfficial($module->authorUrl)) {
$params = [
'license' => self::getLicense($alias),
'module_alias' => $alias,
'url' => self::getAppUrl(),
];
$license_details = WpApi::getVersion($params);
if (WpApi::$lastError) {
$result['msg'] = WpApi::$lastError['message'];
} elseif (!empty($license_details['code']) && !empty($license_details['message'])) {
$result['msg'] = $license_details['message'];
} elseif (!empty($license_details['required_app_version']) && !\Helper::checkAppVersion($license_details['required_app_version'])) {
$result['msg'] = 'Module requires app version:'.' '.$license_details['required_app_version'];
} elseif (!empty($license_details['download_link'])) {
// Download module.
$module_archive = \Module::getPath().DIRECTORY_SEPARATOR.$alias.'.zip';
try {
\Helper::downloadRemoteFile($license_details['download_link'], $module_archive);
} catch (\Exception $e) {
$result['msg'] = $e->getMessage();
}
if (!file_exists($module_archive)) {
$result['download_error'] = true;
if (WpApi::$lastError) {
$result['msg'] = WpApi::$lastError['message'];
} elseif (!empty($license_details['code']) && !empty($license_details['message'])) {
$result['msg'] = $license_details['message'];
} elseif (!empty($license_details['required_app_version']) && !\Helper::checkAppVersion($license_details['required_app_version'])) {
$result['msg'] = 'Module requires app version:'.' '.$license_details['required_app_version'];
} elseif (!empty($license_details['download_link'])) {
// If a download link is available, proceed to update the module from the URL
$result = self::updateFromUrl($module, $license_details['download_link'], $result);
} elseif ($license_details['status'] && $result['msg'] = self::getErrorMessage($license_details['status'])) {
//$result['msg'] = ;
} else {
// Extract.
try {
// Sometimes by some reason Public folder becomes a symlink leading to itself.
// It causes an error during updating process.
// https://github.com/freescout-helpdesk/freescout/issues/2709
$public_folder = $module->getPath().DIRECTORY_SEPARATOR.'Public';
try {
if (is_link($public_folder)) {
unlink($public_folder);
}
} catch (\Exception $e) {
// Do nothing.
}
\Helper::unzip($module_archive, \Module::getPath());
} catch (\Exception $e) {
$result['msg'] = $e->getMessage();
}
// Check if extracted module exists.
\Module::clearCache();
$module = \Module::findByAlias($alias);
if (!$module) {
$result['download_error'] = true;
}
$result['msg'] = __('Error occurred').': '.json_encode($license_details);
}
// Remove archive.
if (file_exists($module_archive)) {
\File::delete($module_archive);
}
if ($result['download_error']) {
$result['download_msg'] = __('Error occurred downloading the module. Please :%a_being%download:%a_end% module manually and extract into :folder', ['%a_being%' => '<a href="'.$license_details['download_link'].'" target="_blank">', '%a_end%' => '</a>', 'folder' => '<strong>'.\Module::getPath().'</strong>']);
}
} elseif ($license_details['status'] && $result['msg'] = self::getErrorMessage($license_details['status'])) {
//$result['msg'] = ;
} else {
$result['msg'] = __('Error occurred').': '.json_encode($license_details);
// If the module's author is not officially recognized, check for a direct download link
$latest_version_zip_url = $module->latestVersionZipUrl ?? null;
if (!empty($latest_version_zip_url)) {
// Update the module from the provided ZIP URL
$result = self::updateFromUrl($module, $module->latestVersionZipUrl, $result);
} else {
// If no download link is available, set an error message indicating the module cannot be downloaded
$result['msg'] = __('Error occurred') . ': module not available for download';
}
}
}
// Run post-update instructions.
if (!$result['msg'] && !$result['download_error']) {
$output_log = new BufferedOutput();
\Artisan::call('freescout:module-install', ['module_alias' => $alias], $output_log);
$result['output'] = $output_log->fetch() ?: ' ';
@@ -500,6 +469,83 @@ class Module extends Model
return $result;
}
/**
* Updates a module from a given URL.
*
* @param Module $module The module to be updated.
* @param string $url The URL where the new version of the module can be downloaded.
* @param array $result An associative array to store the result of the update operation.
*
* @return void
*/
private static function updateFromUrl($module, $url, $result)
{
$alias = $module->alias;
// Download module.
$module_archive = \Module::getPath() . DIRECTORY_SEPARATOR . $alias . '.zip';
try {
\Helper::downloadRemoteFile($url, $module_archive);
} catch (\Exception $e) {
$result['msg'] = $e->getMessage();
}
if (!file_exists($module_archive)) {
$result['download_error'] = true;
} else {
// Extract.
try {
$module_path = $module->getPath();
// Sometimes by some reason Public folder becomes a symlink leading to itself.
// It causes an error during updating process.
// https://github.com/freescout-helpdesk/freescout/issues/2709
$public_folder = $module_path.DIRECTORY_SEPARATOR.'Public';
try {
if (is_link($public_folder)) {
unlink($public_folder);
}
} catch (\Exception $e) {
// Do nothing.
}
\Helper::unzip($module_archive, \Module::getPath());
// Rename the folder if the archive has been downloaded from GitHub:
// SomeModule-master >> SomeModule.
// https://github.com/freescout-help-desk/freescout/issues/4611
$basename = basename($url, '.zip');
if (file_exists($module_path.'-'.$basename)) {
\File::moveDirectory($module_path.'-'.$basename, $module_path, true);
}
} catch (\Exception $e) {
$result['msg'] = $e->getMessage();
}
// Check if extracted module exists.
\Module::clearCache();
$module = \Module::findByAlias($alias);
if (!$module) {
$result['download_error'] = true;
}
}
// Remove archive.
if (file_exists($module_archive)) {
\File::delete($module_archive);
}
if ($result['download_error']) {
$result['download_msg'] = __('Error occurred downloading the module. Please :%a_being%download:%a_end% module manually and extract into :folder', [
'%a_being%' => '<a href="' . $url . '" target="_blank">',
'%a_end%' => '</a>',
'folder' => '<strong>' . \Module::getPath() . '</strong>',
]);
}
return $result;
}
public static function getErrorMessage($code, $result = null)
{
$msg = '';

View File

@@ -205,8 +205,16 @@ class Option extends Model
*/
public static function maybeSerialize($data)
{
if (is_array($data) || is_object($data)) {
return serialize($data);
// if (is_array($data) || is_object($data)) {
// return serialize($data);
// }
// We don't use serialize() function as it is not safe.
if (is_array($data)) {
try {
return \Helper::jsonEncodeSafe($data);
} catch (\Exception $e) {
// Don nothing.
}
}
return $data;
@@ -217,6 +225,8 @@ class Option extends Model
*/
public static function maybeUnserialize($original)
{
// This is kept jsut for backward compatibility.
// Options are not seralized using serialize() anymore.
if (self::isSerialized($original)) {
try {
$original = unserialize($original);
@@ -227,6 +237,19 @@ class Option extends Model
return $original;
}
if (is_string($original) && !empty($original[0])
&& ($original[0] == '{' || $original[0] == '[')
) {
try {
$result = json_decode($original, true);
if (is_array($result)) {
return $result;
}
} catch (\Exception $e) {
// Do nothing.
}
}
return $original;
}

View File

@@ -24,15 +24,9 @@ class ConversationPolicy
if ($user->isAdmin()) {
return true;
} else {
if ($conversation->mailbox->users->contains($user)) {
if ($conversation->userHasAccessToMailbox($user->id)) {
// Maybe user can see only assigned conversations.
if ($conversation->user_id != $user->id
&& $user->hasManageMailboxPermission($conversation->mailbox_id, Mailbox::ACCESS_PERM_ASSIGNED)
) {
return false;
} else {
return true;
}
return $this->checkIsOnlyAssigned($conversation, $user);
} else {
return false;
}
@@ -53,13 +47,7 @@ class ConversationPolicy
} else {
if ($conversation->mailbox->users_cached->contains($user)) {
// Maybe user can see only assigned conversations.
if ($conversation->user_id != $user->id
&& $user->hasManageMailboxPermission($conversation->mailbox_id, Mailbox::ACCESS_PERM_ASSIGNED)
) {
return false;
} else {
return true;
}
return $this->checkIsOnlyAssigned($conversation, $user);
} else {
return false;
}
@@ -79,8 +67,9 @@ class ConversationPolicy
if ($user->isAdmin()) {
return true;
} else {
if ($conversation->mailbox->users->contains($user)) {
return true;
if ($conversation->userHasAccessToMailbox($user->id)) {
// Maybe user can see only assigned conversations.
return $this->checkIsOnlyAssigned($conversation, $user);
} else {
return false;
}
@@ -95,7 +84,18 @@ class ConversationPolicy
if ($user->isAdmin()) {
return true;
} else {
return $user->hasPermission(User::PERM_DELETE_CONVERSATIONS);
if (!$user->hasPermission(User::PERM_DELETE_CONVERSATIONS)) {
return false;
}
if (!$conversation->id) {
return true;
}
if ($conversation->userHasAccessToMailbox($user->id)) {
// Maybe user can see only assigned conversations.
return $this->checkIsOnlyAssigned($conversation, $user);
} else {
return false;
}
}
}
@@ -115,4 +115,17 @@ class ConversationPolicy
}
return Mailbox::count() > 1;
}
public function checkIsOnlyAssigned($conversation, $user)
{
// Maybe user can see only assigned conversations.
if (!\Eventy::filter('conversation.is_user_assignee', $conversation->user_id == $user->id, $conversation, $user->id)
&& $conversation->created_by_user_id != $user->id
&& $user->canSeeOnlyAssignedConversations()
) {
return false;
} else {
return true;
}
}
}

View File

@@ -55,7 +55,6 @@ class UserPolicy
if ($user->isAdmin()
|| $user->id == $model->id
|| $user->hasPermission(User::PERM_EDIT_USERS)
|| $user->canManageMailbox($model->id)
) {
return true;
} else {

View File

@@ -41,7 +41,7 @@ class AppServiceProvider extends ServiceProvider
{
// Forse HTTPS if using CloudFlare "Flexible SSL"
// https://support.cloudflare.com/hc/en-us/articles/200170416-What-do-the-SSL-options-mean-
if (\Helper::getProtocol() == 'https') {
if (\Helper::isHttps()) {
// $_SERVER['HTTPS'] = 'on';
// $_SERVER['SERVER_PORT'] = '443';
$this->app['url']->forceScheme('https');

View File

@@ -14,6 +14,7 @@ class SendLog extends Model
* https://documentation.mailgun.com/en/latest/api-events.html#event-types.
*/
const STATUS_ACCEPTED = 1; // accepted (for delivery)
const STATUS_SEND_INTERMEDIATE_ERROR = 10;
const STATUS_SEND_ERROR = 2;
const STATUS_DELIVERY_SUCCESS = 4;
const STATUS_DELIVERY_ERROR = 5; // rejected, failed
@@ -42,6 +43,7 @@ class SendLog extends Model
* @var [type]
*/
public static $status_errors = [
self::STATUS_SEND_INTERMEDIATE_ERROR,
self::STATUS_SEND_ERROR,
self::STATUS_DELIVERY_ERROR,
];
@@ -94,6 +96,9 @@ class SendLog extends Model
*/
public static function log($thread_id, $message_id, $email, $mail_type, $status, $customer_id = null, $user_id = null, $status_message = null, $smtp_queue_id = null)
{
// Sanitize status message - remove SMTP username and password.
$status_message = \MailHelper::sanitizeSmtpStatusMessage($status_message);
$send_log = new self();
$send_log->thread_id = $thread_id;
$send_log->message_id = $message_id;
@@ -106,7 +111,12 @@ class SendLog extends Model
if ($smtp_queue_id) {
$send_log->smtp_queue_id = $smtp_queue_id;
}
$send_log->save();
try {
$send_log->save();
} catch (\Exception $e) {
\Helper::logException($e, 'Error occurred saving a record to `send_logs` table. ');
return false;
}
return true;
}

View File

@@ -5,6 +5,7 @@
namespace App;
use App\Mailbox;
use App\Notifications\BroadcastNotification;
use App\Notifications\WebsiteNotification;
use Illuminate\Database\Eloquent\Model;
@@ -282,6 +283,13 @@ class Subscription extends Model
}
}
//}
// https://github.com/freescout-helpdesk/freescout/issues/3843#issuecomment-1974811457
if ($conversation->user_id != $subscription->user_id
&& $subscription->user->canSeeOnlyAssignedConversations()
) {
continue;
}
if (\Eventy::filter('subscription.filter_out', false, $subscription, $thread)) {
continue;
@@ -400,11 +408,12 @@ class Subscription extends Model
$thread_id = self::chooseThread($notify_info['threads'])->id;
foreach ($notify_info['users'] as $user) {
$broadcast_id = $thread_id.'_'.$user->id;
$mediums = [$medium];
if (!empty($broadcasts[$thread_id]['mediums'])) {
$mediums = array_unique(array_merge($mediums, $broadcasts[$thread_id]['mediums']));
if (!empty($broadcasts[$broadcast_id]['mediums'])) {
$mediums = array_unique(array_merge($mediums, $broadcasts[$broadcast_id]['mediums']));
}
$broadcasts[$thread_id] = [
$broadcasts[$broadcast_id] = [
'user' => $user,
'conversation' => $notify_info['conversation'],
'threads' => $notify_info['threads'],
@@ -414,7 +423,7 @@ class Subscription extends Model
}
}
// \Notification::sendNow($notify_info['users'], new BroadcastNotification($notify_info['conversation'], $notify_info['threads'][0]));
foreach ($broadcasts as $thread_id => $to_broadcast) {
foreach ($broadcasts as $broadcast_id => $to_broadcast) {
$broadcast_notification = new BroadcastNotification($to_broadcast['conversation'], self::chooseThread($to_broadcast['threads']), $to_broadcast['mediums']);
$broadcast_notification->delay($delay);
$to_broadcast['user']->notify($broadcast_notification);

View File

@@ -2,6 +2,8 @@
namespace App;
use App\Mailbox;
use App\SendLog;
use App\Events\ConversationStatusChanged;
use App\Events\ConversationUserChanged;
use App\Events\UserAddedNote;
@@ -309,11 +311,19 @@ class Thread extends Model
// Change "background:" to "background-color:".
// https://github.com/freescout-helpdesk/freescout/issues/2560
// Keep in mind that with large texts preg_replace() may return null.
$body = preg_replace("/(<[^<>]+style=[\"'][^\"']*)background: *([^;() ]+;)/", '$1background-color:$2', $body) ?: $body;
$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;
// Remove only the <!--[if !mso]><!--> and <!--<![endif]--> around the elements.
// https://github.com/freescout-helpdesk/freescout/pull/3865#issuecomment-1990758149
$body = preg_replace('/<!\-\-\[if [^>]+\]><!\-\->(.*?)<![ ]+\-\-<!\[endif\]\-\->/s', '$1', $body);
// https://github.com/freescout-helpdesk/freescout/issues/3894
// Remove <!--[if !mso]><!--> and <!--<![endif]--> comments, preserving the data inside
//$body = preg_replace('/(<!\-\-\[if [^>]+\]>|<!\[endif\]\-\->)/', '', $body);
return \Helper::purifyHtml($body);
}
@@ -563,7 +573,12 @@ class Thread extends Model
return \App\User::getDeletedUser();
}
} else {
return $this->created_by_customer;
// In some cases the created_by_customer can be empty.
if ($this->created_by_customer) {
return $this->created_by_customer;
} else {
return \App\Customer::getDummyCustomer();
}
}
}
@@ -649,7 +664,7 @@ class Thread extends Model
}
} elseif ($this->action_type == self::ACTION_TYPE_CUSTOMER_CHANGED) {
if ($conversation_number) {
$did_this = __(':person changed the customer to :customer in conversation #:conversation_number', ['customer' => $this->customer->getFullName(true), 'conversation_number' => $conversation_number]);
$did_this = __(':person changed the customer to :customer in conversation #:conversation_number', ['customer' => ($this->customer_cached ? $this->customer_cached->getFullName(true) : ''), 'conversation_number' => $conversation_number]);
} else {
$customer_name = '';
if ($this->customer_cached) {
@@ -806,7 +821,14 @@ class Thread extends Model
$name = $mailbox->name;
if ($mailbox->from_name == Mailbox::FROM_NAME_CUSTOM && $mailbox->from_name_custom) {
$name = $mailbox->from_name_custom;
$data = [
'mailbox' => $mailbox,
'mailbox_from_name' => '', // To avoid recursion.
'conversation' => $this->conversation,
// If we reach here it means the thread has been created by user.
'user' => $this->getCreatedBy(),
];
$name = \MailHelper::replaceMailVars($mailbox->from_name_custom, $data, false, true);
} elseif ($mailbox->from_name == Mailbox::FROM_NAME_USER && $this->getCreatedBy()) {
$name = $this->getCreatedBy()->getFirstName(true);
}
@@ -872,6 +894,9 @@ class Thread extends Model
} else {
$send_status_data = $new_data;
}
if (!empty($send_status_data['msg'])) {
$send_status_data['msg'] = \MailHelper::sanitizeSmtpStatusMessage($send_status_data['msg']);
}
$this->send_status_data = \Helper::jsonEncodeUtf8($send_status_data);
} else {
$this->send_status_data = null;
@@ -1331,8 +1356,10 @@ class Thread extends Model
}
if ($by_user && $this->created_by_user_id == $by_user->id) {
$name = __('you');
} else {
} elseif ($this->created_by_user) {
$name = $this->created_by_user->getFullName();
} else {
$name = '';
}
return $name;
@@ -1390,7 +1417,23 @@ class Thread extends Model
*/
public function fetchBody()
{
$message = \MailHelper::fetchMessage($this->conversation->mailbox, $this->message_id, $this->getMailDate());
$mailbox = null;
// The conversation may has been moved from another mailbox.
if (!empty($this->conversation->meta['orig_mailbox_id'])) {
$mailbox = Mailbox::find($this->conversation->meta['orig_mailbox_id']);
}
if (!$mailbox) {
$mailbox = $this->conversation->mailbox;
}
$message = \MailHelper::fetchMessage($mailbox, $this->message_id, $this->getMailDate());
// Try without limiting by date.
// https://github.com/freescout-helpdesk/freescout/issues/3658
if (!$message) {
$message = \MailHelper::fetchMessage($mailbox, $this->message_id);
}
if (!$message) {
return '';
@@ -1410,6 +1453,20 @@ class Thread extends Model
return \MailHelper::parseHeaders($this->headers);
}
public function getHeader($header_name)
{
return \MailHelper::getHeader($this->headers, $header_name);
}
public function getFromHeader()
{
if (empty($this->headers)) {
return '';
}
preg_match("#From:\s*.*[^\s]*\s*<\s*(.*[^\s])\s*>\s*\n#", $this->headers ?? '', $m);
return $m[1] ?? '';
}
public function getMailDate()
{
$data = $this->parseHeaders();
@@ -1517,4 +1574,38 @@ class Thread extends Model
$threads = self::sortThreads($threads);
return $threads->first();
}
public function canRetrySend()
{
if ($this->isSendStatusSuccess()) {
return false;
}
// Check if failed_job still exists.
if (!$this->getFailedJobId()) {
return false;
}
return true;
}
public function isSendStatusSuccess()
{
// We have not tried to send the email yet.
if ((int)$this->send_status == 0) {
return false;
}
if (!in_array($this->send_status, [SendLog::STATUS_SEND_ERROR, SendLog::STATUS_DELIVERY_ERROR, SendLog::STATUS_SEND_INTERMEDIATE_ERROR])) {
return true;
}
return false;
}
public function getFailedJobId()
{
return \App\FailedJob::where('queue', 'emails')
->where('payload', 'like', '{"displayName":"App\\\\\\\\Jobs\\\\\\\\SendReplyToCustomer"%{i:0;i:'.$this->id.';%')
->value('id');
}
}

View File

@@ -7,6 +7,7 @@
namespace App;
use App\Email;
use App\Events\UserDeleted;
use App\Follower;
use App\Mail\PasswordChanged;
use App\Mail\UserInvite;
@@ -116,12 +117,20 @@ class User extends Authenticatable
*
* @var [type]
*/
protected $fillable = ['role', 'status', 'first_name', 'last_name', 'email', 'password', 'timezone', 'photo_url', 'type', 'emails', 'job_title', 'phone', 'time_format', 'enable_kb_shortcuts', 'locale'];
protected $fillable = ['role', 'status', 'first_name', 'last_name', 'email', 'password', 'timezone', 'photo_url', 'type', 'emails', 'job_title', 'phone', 'time_format', 'locale'];
protected $casts = [
'permissions' => 'array',
];
public function __construct(array $attributes = array())
{
$this->setRawAttributes(array_merge($this->attributes, array(
'timezone' => config('app.timezone') ?: User::DEFAULT_TIMEZONE
)), true);
parent::__construct($attributes);
}
/**
* For array_unique function.
*
@@ -298,6 +307,7 @@ class User extends Authenticatable
return $settings;
}
/**
* Get IDs of mailboxes to which user has access.
*/
@@ -542,6 +552,8 @@ class User extends Authenticatable
's' => 'ss',
'l' => 'cccc',
'O' => 'xx',
// https://stackoverflow.com/questions/59682843/php-intldateformatter-returns-incorrect-year
'Y' => 'y',
]);
// Remove dot from month name.
@@ -617,7 +629,7 @@ class User extends Authenticatable
}
if (stripos($dateForHuman, 'just') === false) {
return __(':date @ :time', ['date' => $dateForHuman, 'time' => $date->format('H:i')]);
return __(':date @ :time', ['date' => $dateForHuman, 'time' => self::dateFormat($date, 'H:i')]);
} else {
return $dateForHuman;
}
@@ -858,7 +870,9 @@ class User extends Authenticatable
{
$real_path = $uploaded_file;
if (!is_string($uploaded_file)) {
$real_path = $uploaded_file->getRealPath();
// Fallback to getPathname() for Windows.
// https://github.com/freescout-help-desk/freescout/issues/4105
$real_path = $uploaded_file->getRealPath() ?: $uploaded_file->getPathname();
$mime_type = $uploaded_file->getMimeType();
}
@@ -986,6 +1000,22 @@ class User extends Authenticatable
$data['password'] = \Hash::make($data['password']);
}
// Strip tags.
$fields_strip = [
'first_name',
'last_name',
'phone',
'timezone',
];
foreach ($fields_strip as $field) {
if (in_array($field, array_keys($data))) {
if ($data[$field] !== null) {
$data[$field] = strip_tags($data[$field]);
}
}
}
if ($replace_data) {
$this->fill($data);
} else {
@@ -1198,4 +1228,121 @@ class User extends Authenticatable
{
return User::where('type', User::TYPE_ROBOT);
}
// Truncate fields to their max lengths to avoid PostgreSQL error:
// SQLSTATE[22001]: String data, right truncated: 7 ERROR: value too long for type character varying(100).
// https://github.com/freescout-helpdesk/freescout/issues/3489
public function setFirstNameAttribute($first_name)
{
$this->attributes['first_name'] = mb_substr($first_name ?? '', 0, 20);
}
public function setLastNameAttribute($last_name)
{
$this->attributes['last_name'] = mb_substr($last_name ?? '', 0, 30);
}
public function setEmailAttribute($email)
{
$this->attributes['email'] = mb_substr($email ?? '', 0, 100);
}
public function setJobTitleAttribute($job_title)
{
$this->attributes['job_title'] = mb_substr($job_title ?? '', 0, 100);
}
public function canSeeOnlyAssignedConversations()
{
return $this->hasManageMailboxPermission(0, Mailbox::ACCESS_PERM_ASSIGNED);
}
public function deleteUser($auth_user, $assign_user)
{
// We have to process conversations one by one to move them to Unassigned folder,
// as conversations may be in different mailboxes
// $this->conversations()->update(['user_id' => null, 'folder_id' => ]);
$mailbox_unassigned_folders = [];
$this->conversations->each(function ($conversation) use ($auth_user, $assign_user) {
// We don't fire ConversationUserChanged event to avoid sending notifications to users
if (!empty($assign_user)
&& !empty($assign_user[$conversation->mailbox_id])
&& (int) $assign_user[$conversation->mailbox_id] != -1
) {
// Set assignee.
// In this case conversation stays assigned, just assignee changes.
$conversation->user_id = $assign_user[$conversation->mailbox_id];
} else {
// Make convesation Unassigned.
// Unset assignee.
// Maybe use changeUser() here.
$conversation->user_id = null;
if ($conversation->isPublished()
&& ($conversation->isActive() || $conversation->isPending())
) {
// Change conversation folder to UNASSIGNED.
$folder_id = null;
if (!empty($mailbox_unassigned_folders[$conversation->mailbox_id])) {
$folder_id = $mailbox_unassigned_folders[$conversation->mailbox_id];
} else {
$folder = $conversation->mailbox->folders()
->where('type', Folder::TYPE_UNASSIGNED)
->first();
if ($folder) {
$folder_id = $folder->id;
$mailbox_unassigned_folders[$conversation->mailbox_id] = $folder_id;
}
}
if ($folder_id) {
$conversation->folder_id = $folder_id;
}
}
}
$conversation->save();
// Create lineitem thread
$thread = new Thread();
$thread->conversation_id = $conversation->id;
$thread->user_id = $conversation->user_id;
$thread->type = Thread::TYPE_LINEITEM;
$thread->state = Thread::STATE_PUBLISHED;
$thread->status = Thread::STATUS_NOCHANGE;
$thread->action_type = Thread::ACTION_TYPE_USER_CHANGED;
$thread->source_via = Thread::PERSON_USER;
$thread->source_type = Thread::SOURCE_TYPE_WEB;
$thread->customer_id = $conversation->customer_id;
$thread->created_by_user_id = $auth_user->id;
$thread->save();
});
// Recalculate counters for folders
//if ($this->isAdmin()) {
// Admin has access to all mailboxes
Mailbox::all()->each(function ($mailbox) {
$mailbox->updateFoldersCounters();
});
// } else {
// $this->mailboxes->each(function ($mailbox) {
// $mailbox->updateFoldersCounters();
// });
// }
// Disconnect user from mailboxes.
$this->mailboxes()->sync([]);
$this->folders()->delete();
$this->status = \App\User::STATUS_DELETED;
// Update email.
$email_suffix = User::EMAIL_DELETED_SUFFIX.date('YmdHis');
// We have to truncate email to avoid "Data too long" error.
$this->email = mb_substr($this->email, 0, User::EMAIL_MAX_LENGTH - mb_strlen($email_suffix)).$email_suffix;
$this->save();
event(new UserDeleted($this, $auth_user));
}
}

View File

@@ -22,7 +22,6 @@
"lord/laroute": "2.4.7",
"axn/laravel-laroute": "1.3.0",
"html2text/html2text": "4.1.0",
"webklex/laravel-imap": "1.2.7",
"watson/rememberable": "2.0.4",
"nwidart/laravel-modules": "2.7.0",
"tormjens/eventy": "0.5.4",
@@ -89,12 +88,14 @@
"Swift_": "overrides/swiftmailer/swiftmailer/lib/classes",
"HTMLPurifier_": "overrides/ezyang/htmlpurifier/library"
},
"files": [
"app/Misc/Functions.php"
],
"fs-comment": "Above new lines are listed files containing custom updates. Below - only PHP compatibility updates.",
"psr-4": {
"App\\": "app/",
"Modules\\": "Modules/",
"Axn\\Laroute\\Routes\\": "overrides/axn/laravel-laroute/src/Routes/",
"Webklex\\IMAP\\": "overrides/webklex/laravel-imap/src/IMAP/",
"Webklex\\PHPIMAP\\": "overrides/webklex/php-imap/src/",
"RachidLaasri\\LaravelInstaller\\Helpers\\": "overrides/rachidlaasri/laravel-installer/src/Helpers/",
"RachidLaasri\\LaravelInstaller\\Middleware\\": "overrides/rachidlaasri/laravel-installer/src/Middleware/",
@@ -107,6 +108,7 @@
"Illuminate\\Foundation\\": "overrides/laravel/framework/src/Illuminate/Foundation/",
"Illuminate\\Foundation\\Testing\\": "overrides/laravel/framework/src/Illuminate/Foundation/Testing/",
"Illuminate\\Foundation\\Http\\Middleware\\": "overrides/laravel/framework/src/Illuminate/Foundation/Http/Middleware/",
"Illuminate\\Foundation\\Bootstrap\\": "overrides/laravel/framework/src/Illuminate/Foundation/Bootstrap/",
"Illuminate\\Routing\\": "overrides/laravel/framework/src/Illuminate/Routing/",
"Illuminate\\Broadcasting\\Broadcasters\\": "overrides/laravel/framework/src/Illuminate/Broadcasting/Broadcasters/",
"Illuminate\\Validation\\Concerns\\": "overrides/laravel/framework/src/Illuminate/Validation/Concerns/",
@@ -124,56 +126,97 @@
"Symfony\\Component\\HttpFoundation\\File\\MimeType\\": "overrides/symfony/http-foundation/File/MimeType/",
"Chumper\\Zipper\\Repositories\\": "overrides/chumper/zipper/src/Chumper/Zipper/Repositories/",
"Faker\\Provider\\": "overrides/fzaninotto/faker/src/Faker/Provider/",
"Illuminate\\Database\\": "overrides/laravel/framework/src/Illuminate/Database/",
"Chumper\\Zipper\\": "overrides/chumper/zipper/src/Chumper/Zipper/",
"Illuminate\\Support\\": "overrides/laravel/framework/src/Illuminate/Support/",
"Illuminate\\Http\\": "overrides/laravel/framework/src/Illuminate/Http/",
"Illuminate\\Database\\Eloquent\\": "overrides/laravel/framework/src/Illuminate/Database/Eloquent/",
"Illuminate\\Database\\Eloquent\\Concerns\\": "overrides/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/",
"Illuminate\\Database\\Eloquent\\Relations\\": "overrides/laravel/framework/src/Illuminate/Database/Eloquent/Relations/",
"Illuminate\\Database\\Schema\\": "overrides/laravel/framework/src/Illuminate/Database/Schema/",
"Illuminate\\Database\\Query\\": "overrides/laravel/framework/src/Illuminate/Database/Query/",
"Illuminate\\Pagination\\": "overrides/laravel/framework/src/Illuminate/Pagination/",
"Illuminate\\Session\\": "overrides/laravel/framework/src/Illuminate/Session/",
"Illuminate\\Session\\Middleware\\": "overrides/laravel/framework/src/Illuminate/Session/Middleware/",
"Illuminate\\Queue\\": "overrides/laravel/framework/src/Illuminate/Queue/",
"Carbon\\": "overrides/nesbot/carbon/src/Carbon/",
"Illuminate\\Cache\\": "overrides/laravel/framework/src/Illuminate/Cache/",
"Illuminate\\Config\\": "overrides/laravel/framework/src/Illuminate/Config/",
"Illuminate\\Notifications\\": "overrides/laravel/framework/src/Illuminate/Notifications/",
"Illuminate\\Auth\\Access\\": "overrides/laravel/framework/src/Illuminate/Auth/Access/",
"Illuminate\\Broadcasting\\": "overrides/laravel/framework/src/Illuminate/Broadcasting/",
"Illuminate\\Bus\\": "overrides/laravel/framework/src/Illuminate/Bus/",
"Illuminate\\Pipeline\\": "overrides/laravel/framework/src/Illuminate/Pipeline/",
"Doctrine\\DBAL\\Driver\\": "overrides/doctrine/dbal/lib/Doctrine/DBAL/Driver/",
"Doctrine\\DBAL\\Schema\\": "overrides/doctrine/dbal/lib/Doctrine/DBAL/Schema/",
"Doctrine\\DBAL\\Platforms\\": "overrides/doctrine/dbal/lib/Doctrine/DBAL/Platforms/",
"Symfony\\Component\\Finder\\Iterator\\": "overrides/symfony/finder/Iterator/",
"Symfony\\Component\\Finder\\": "overrides/symfony/finder/",
"Symfony\\Component\\Console\\Helper\\": "overrides/symfony/console/Helper/",
"Symfony\\Component\\Console\\": "overrides/symfony/console/",
"Symfony\\Component\\Console\\Command\\": "overrides/symfony/console/Command/",
"Symfony\\Component\\Console\\Output\\": "overrides/symfony/console/Output/",
"DebugBar\\": "overrides/maximebf/debugbar/src/DebugBar/",
"DebugBar\\DataFormatter\\": "overrides/maximebf/debugbar/src/DataFormatter/DataFormatter/",
"DebugBar\\DataCollector\\PDO\\": "overrides/maximebf/debugbar/src/DataFormatter/DataCollector/PDO/",
"Illuminate\\Cache\\Console\\": "overrides/laravel/framework/src/Illuminate/Cache/Console/",
"Dotenv\\": "overrides/vlucas/phpdotenv/src/",
"Illuminate\\View\\": "overrides/laravel/framework/src/Illuminate/View/",
"Illuminate\\View\\Compilers\\": "overrides/laravel/framework/src/Illuminate/View/Compilers/",
"Illuminate\\View\\Compilers\\Concerns\\": "overrides/laravel/framework/src/Illuminate/View/Compilers/Concerns/",
"Illuminate\\View\\Concerns\\": "overrides/laravel/framework/src/Illuminate/View/Concerns/",
"Illuminate\\Console\\": "overrides/laravel/framework/src/Illuminate/Console/",
"Illuminate\\Log\\": "overrides/laravel/framework/src/Illuminate/Log/",
"Illuminate\\Validation\\": "overrides/laravel/framework/src/Illuminate/Validation/",
"Symfony\\Component\\Routing\\": "overrides/symfony/routing/",
"Symfony\\Component\\VarDumper\\Cloner\\": "overrides/symfony/var-dumper/Cloner/",
"Symfony\\Component\\VarDumper\\Dumper\\": "overrides/symfony/var-dumper/Dumper/",
"Devfactory\\Minify\\Providers\\": "overrides/devfactory/minify/src/Providers/",
"Barryvdh\\Debugbar\\": "overrides/barryvdh/laravel-debugbar/src/",
"Barryvdh\\Debugbar\\DataFormatter\\": "overrides/barryvdh/laravel-debugbar/src/DataFormatter/",
"Barryvdh\\Debugbar\\DataCollector\\": "overrides/barryvdh/laravel-debugbar/src/DataCollector/",
"Symfony\\Component\\Process\\": "overrides/symfony/process/",
"Symfony\\Component\\HttpKernel\\": "overrides/symfony/http-kernel/",
"Symfony\\Component\\HttpKernel\\Exception\\": "overrides/symfony/http-kernel/Exception/",
"Symfony\\Component\\HttpKernel\\HttpCache\\": "overrides/symfony/http-kernel/HttpCache/",
"Symfony\\Component\\Debug\\Exception\\": "overrides/symfony/debug/Exception/",
"Spatie\\String\\": "overrides/spatie/string/src/",
"GuzzleHttp\\": "overrides/guzzlehttp/guzzle/src/",
"GuzzleHttp\\Cookie\\": "overrides/guzzlehttp/guzzle/src/Cookie/",
"GuzzleHttp\\Handler\\": "overrides/guzzlehttp/guzzle/src/Handler/",
"GuzzleHttp\\Psr7\\": "overrides/guzzlehttp/psr7/src/",
"GuzzleHttp\\Promise\\": "overrides/guzzlehttp/promises/src/",
"Ramsey\\Uuid\\": "overrides/ramsey/uuid/src/",
"Rap2hpoutre\\LaravelLogViewer\\": "overrides/rap2hpoutre/laravel-log-viewer/src/Rap2hpoutre/LaravelLogViewer/",
"Symfony\\Component\\Console\\Descriptor\\": "overrides/symfony/console/Descriptor",
"Symfony\\Component\\Console\\Descriptor\\": "overrides/symfony/console/Descriptor/",
"Symfony\\Component\\Console\\Input\\": "overrides/symfony/console/Input/",
"Symfony\\Component\\Console\\Formatter\\": "overrides/symfony/console/Formatter/",
"Symfony\\Component\\Console\\Exception\\": "overrides/symfony/console/Exception/",
"Symfony\\Component\\Console\\Question\\": "overrides/symfony/console/",
"Symfony\\Component\\CssSelector\\XPath\\Extension\\": "overrides/symfony/css-selector/XPath/Extension/",
"Javoscript\\MacroableModels\\": "overrides/javoscript/laravel-macroable-models/src/"
"Symfony\\Component\\Translation\\Loader\\": "overrides/symfony/translation/Loader/",
"Symfony\\Component\\Translation\\Formatter\\": "overrides/symfony/translation/Formatter/",
"Symfony\\Component\\Translation\\": "overrides/symfony/translation/",
"Javoscript\\MacroableModels\\": "overrides/javoscript/laravel-macroable-models/src/",
"League\\Flysystem\\": "overrides/league/flysystem/src/",
"Whoops\\": "overrides/filp/whoops/src/Whoops/",
"Whoops\\Handler\\": "overrides/filp/whoops/src/Whoops/Handler/",
"Whoops\\Util\\": "overrides/filp/whoops/src/Whoops/Util/",
"Mews\\Purifier\\": "overrides/mews/purifier/src/",
"Cron\\": "overrides/mtdowling/cron-expression/src/Cron/",
"Psy\\": "overrides/psy/psysh/src/",
"Psy\\Input\\": "overrides/psy/psysh/src/Input/",
"Psy\\Output\\": "overrides/psy/psysh/src/Output/",
"Psy\\Command\\": "overrides/psy/psysh/src/Command/",
"Psy\\TimeitCommand\\": "overrides/psy/psysh/src/TimeitCommand/",
"Psy\\Command\\ListCommand\\": "overrides/psy/psysh/src/Command/ListCommand/",
"Psy\\Exception\\": "overrides/psy/psysh/src/ErrorException/",
"Psy\\VarDumper\\": "overrides/psy/psysh/src/VarDumper/"
},
"exclude-from-classmap": [
"Modules/",
"vendor/axn/laravel-laroute/src/Routes/Collection.php",
"vendor/webklex/laravel-imap/src/IMAP/Message.php",
"vendor/webklex/laravel-imap/src/IMAP/Attachment.php",
"vendor/webklex/laravel-imap/src/IMAP/Client.php",
"vendor/rachidlaasri/laravel-installer/src/Helpers/RequirementsChecker.php",
"vendor/rachidlaasri/laravel-installer/src/Helpers/PermissionsChecker.php",
"vendor/rachidlaasri/laravel-installer/src/Helpers/EnvironmentManager.php",
@@ -219,24 +262,34 @@
"vendor/chumper/zipper/src/Chumper/Zipper/Repositories/ZipRepository.php",
"vendor/fzaninotto/faker/src/Faker/Provider/Base.php",
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrDef/URI/Host.php",
"vendor/laravel/framework/src/Illuminate/Database/DetectsLostConnections.php",
"vendor/laravel/framework/src/Illuminate/Support/Collection.php",
"vendor/laravel/framework/src/Illuminate/Http/Request.php",
"vendor/laravel/framework/src/Illuminate/Http/RedirectResponse.php",
"vendor/symfony/http-foundation/ParameterBag.php",
"vendor/symfony/http-foundation/HeaderBag.php",
"vendor/symfony/http-foundation/Response.php",
"vendor/symfony/http-foundation/AcceptHeader.php",
"vendor/symfony/http-foundation/FileBag.php",
"vendor/symfony/http-foundation/Request.php",
"vendor/symfony/http-foundation/StreamedResponse.php",
"vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php",
"vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php",
"vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php",
"vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php",
"vendor/laravel/framework/src/Illuminate/Database/Eloquent/Relations/Relation.php",
"vendor/laravel/framework/src/Illuminate/Database/Schema/Builder.php",
"vendor/laravel/framework/src/Illuminate/Database/Schema/Blueprint.php",
"vendor/laravel/framework/src/Illuminate/Pagination/AbstractPaginator.php",
"vendor/laravel/framework/src/Illuminate/Pagination/Paginator.php",
"vendor/laravel/framework/src/Illuminate/Pagination/LengthAwarePaginator.php",
"vendor/laravel/framework/src/Illuminate/Routing/RouteCollection.php",
"vendor/laravel/framework/src/Illuminate/Session/FileSessionHandler.php",
"vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php",
"vendor/laravel/framework/src/Illuminate/Queue/Listener.php",
"vendor/nesbot/carbon/src/Carbon/Carbon.php",
"vendor/nesbot/carbon/src/Carbon/Translator.php",
"vendor/laravel/framework/src/Illuminate/Cache/Repository.php",
"vendor/laravel/framework/src/Illuminate/Config/Repository.php",
"vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOConnection.php",
@@ -252,6 +305,8 @@
"vendor/laravel/framework/src/Illuminate/Support/Optional.php",
"vendor/laravel/framework/src/Illuminate/Support/Fluent.php",
"vendor/laravel/framework/src/Illuminate/Database/Eloquent/Factory.php",
"vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php",
"vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php",
"vendor/symfony/finder/Finder.php",
"vendor/symfony/finder/Iterator/FilterIterator.php",
"vendor/symfony/finder/Iterator/FileTypeFilterIterator.php",
@@ -262,12 +317,18 @@
"vendor/symfony/finder/Iterator/DepthRangeFilterIterator.php",
"vendor/symfony/finder/Iterator/DateRangeFilterIterator.php",
"vendor/symfony/console/Helper/HelperSet.php",
"vendor/symfony/console/Helper/HelperInterface.php",
"vendor/symfony/console/Helper/Table.php",
"vendor/symfony/console/Application.php",
"vendor/symfony/debug/Exception/FatalErrorException.php",
"vendor/maximebf/debugbar/src/DebugBar/DebugBar.php",
"vendor/maximebf/debugbar/src/DebugBar/JavascriptRenderer.php",
"vendor/maximebf/debugbar/src/DebugBar/DataFormatter/DataFormatter.php",
"vendor/maximebf/debugbar/src/DebugBar/DataCollector/PDO/PDOCollector.php",
"vendor/laravel/framework/src/Illuminate/Cache/Console/ClearCommand.php",
"vendor/vlucas/phpdotenv/src/Loader.php",
"vendor/laravel/framework/src/Illuminate/View/View.php",
"vendor/laravel/framework/src/Illuminate/View/FileViewFinder.php",
"vendor/laravel/framework/src/Illuminate/View/Compilers/Compiler.php",
"vendor/laravel/framework/src/Illuminate/View/Compilers/Concerns/CompilesLayouts.php",
"vendor/laravel/framework/src/Illuminate/View/Concerns/ManagesLayouts.php",
@@ -275,38 +336,151 @@
"vendor/symfony/routing/CompiledRoute.php",
"vendor/symfony/var-dumper/Cloner/Data.php",
"vendor/symfony/var-dumper/Cloner/Stub.php",
"vendor/symfony/var-dumper/Cloner/AbstractCloner.php",
"vendor/symfony/var-dumper/Dumper/HtmlDumper.php",
"vendor/devfactory/minify/src/Providers/BaseProvider.php",
"vendor/barryvdh/laravel-debugbar/src/DataFormatter/QueryFormatter.php",
"vendor/barryvdh/laravel-debugbar/src/DataCollector/QueryCollector.php",
"vendor/symfony/process/Process.php",
"vendor/symfony/http-kernel/UriSigner.php",
"vendor/symfony/http-kernel/Exception/HttpException.php",
"vendor/symfony/http-kernel/Exception/AccessDeniedHttpException.php",
"vendor/symfony/http-kernel/Exception/MethodNotAllowedHttpException.php",
"vendor/symfony/http-kernel/Exception/NotFoundHttpException.php",
"vendor/symfony/http-kernel/Exception/ServiceUnavailableHttpException.php",
"vendor/symfony/http-kernel/Exception/UnauthorizedHttpException.php",
"vendor/symfony/http-kernel/HttpCache/Store.php",
"vendor/barryvdh/laravel-debugbar/src/JavascriptRenderer.php",
"vendor/spatie/string/src/Str.php",
"vendor/guzzlehttp/guzzle/src/Client.php",
"vendor/guzzlehttp/guzzle/src/Cookie/CookieJar.php",
"vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php",
"vendor/guzzlehttp/guzzle/src/Handler/StreamHandler.php",
"vendor/guzzlehttp/guzzle/src/HandlerStack.php",
"vendor/guzzlehttp/guzzle/src/Middleware.php",
"vendor/guzzlehttp/psr7/src/LazyOpenStream.php",
"vendor/guzzlehttp/psr7/src/Uri.php",
"vendor/guzzlehttp/promises/src/RejectedPromise.php",
"vendor/guzzlehttp/promises/src/PromiseInterface.php",
"vendor/guzzlehttp/promises/src/FulfilledPromise.php",
"vendor/guzzlehttp/promises/src/Utils.php",
"vendor/guzzlehttp/promises/src/Promise.php",
"vendor/ramsey/uuid/src/Uuid.php",
"vendor/ramsey/uuid/src/UuidFactory.php",
"vendor/laravel/framework/src/Illuminate/Routing/Router.php",
"vendor/laravel/framework/src/Illuminate/Filesystem/Filesystem.php",
"vendor/laravel/framework/src/Illuminate/Console/Application.php",
"vendor/rap2hpoutre/laravel-log-viewer/src/Rap2hpoutre/LaravelLogViewer/LaravelLogViewer.php",
"vendor/rap2hpoutre/laravel-log-viewer/src/controllers/LogViewerController.php",
"vendor/symfony/console/Descriptor/TextDescriptor.php",
"vendor/symfony/console/Input/ArrayInput.php",
"vendor/symfony/console/Input/StringInput.php",
"vendor/symfony/console/Input/Input.php",
"vendor/symfony/console/Helper/Helper.php",
"vendor/symfony/console/Helper/ProcessHelper.php",
"vendor/symfony/console/Command/Command.php",
"vendor/symfony/console/Output/Output.php",
"vendor/symfony/console/Formatter/OutputFormatterStyleStack.php",
"vendor/symfony/console/Exception/CommandNotFoundException.php",
"vendor/symfony/console/Question/Question.php",
"vendor/symfony/finder/Iterator/SortableIterator.php",
"vendor/symfony/css-selector/XPath/Extension/NodeExtension.php",
"vendor/symfony/translation/Loader/ArrayLoader.php",
"vendor/symfony/translation/Formatter/MessageFormatter.php",
"vendor/symfony/translation/Translator.php",
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrTransform/NameSync.php",
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/AttrValidator.php",
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/ChildDef/List.php",
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/Length.php",
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/Encoder.php",
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer.php",
"vendor/ezyang/htmlpurifier/library/HTMLPurifier/Lexer/DOMLex.php",
"vendor/javoscript/laravel-macroable-models/src/MacroableModels.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Attachment.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/EmbeddedFile.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/MailTransport.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/MailTransport.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Message.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/MimePart.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/SendmailTransport.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/SmtpTransport.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/SwiftException.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/TransportException.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/IoException.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/MimePart.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/SimpleHeaderSet.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/SimpleHeaderFactory.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/ContentEncoder/Base64ContentEncoder.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/ContentEncoder/QpContentEncoder.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Headers/AbstractHeader.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Headers/ParameterizedHeader.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Headers/IdentificationHeader.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/Headers/MailboxHeader.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/AbstractSmtpTransport.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/EsmtpTransport.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/MailInvoker.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/MailTransport.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/SimpleMailInvoker.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/StreamBuffer.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/Esmtp/AuthHandler.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/Encoder/QpEncoder.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/DiskKeyCache.php",
"vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/SimpleKeyCacheInputStream.php",
"vendor/natxet/cssmin/src/CssMin.php",
"vendor/webklex/php-imap/src/Header.php",
"vendor/webklex/php-imap/src/Structure.php",
"vendor/webklex/php-imap/src/Message.php",
"vendor/webklex/php-imap/src/Attachment.php",
"vendor/webklex/php-imap/src/Connection/Protocols/ImapProtocol.php"
"vendor/webklex/php-imap/src/Part.php",
"vendor/webklex/php-imap/src/Client.php",
"vendor/webklex/php-imap/src/Connection/Protocols/ImapProtocol.php",
"vendor/webklex/php-imap/src/Connection/Protocols/LegacyProtocol.php",
"vendor/webklex/php-imap/src/Query/Query.php",
"vendor/webklex/php-imap/src/Query/WhereQuery.php",
"vendor/webklex/php-imap/src/ClientManager.php",
"vendor/webklex/php-imap/src/Folder.php",
"vendor/webklex/php-imap/src/EncodingAliases.php",
"vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php",
"vendor/laravel/framework/src/Illuminate/Log/Writer.php",
"vendor/laravel/framework/src/Illuminate/Notifications/RoutesNotifications.php",
"vendor/laravel/framework/src/Illuminate/Notifications/SendQueuedNotifications.php",
"vendor/laravel/framework/src/Illuminate/Notifications/ChannelManager.php",
"vendor/laravel/framework/src/Illuminate/Notifications/NotificationSender.php",
"vendor/laravel/framework/src/Illuminate/Auth/Access/Gate.php",
"vendor/laravel/framework/src/Illuminate/Broadcasting/BroadcastManager.php",
"vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php",
"vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php",
"vendor/league/flysystem/src/Filesystem.php",
"vendor/league/flysystem/src/FilesystemInterface.php",
"vendor/league/flysystem/src/FileNotFoundException.php",
"vendor/mews/purifier/src/Purifier.php",
"vendor/mtdowling/cron-expression/src/Cron/CronExpression.php",
"vendor/laravel/framework/src/Illuminate/Mail/Mailer.php",
"vendor/filp/whoops/src/Whoops/Run.php",
"vendor/filp/whoops/src/Whoops/Handler/PrettyPageHandler.php",
"vendor/filp/whoops/src/Whoops/Util/TemplateHelper.php",
"vendor/laravel/framework/src/Illuminate/Validation/Factory.php",
"vendor/laravel/framework/src/Illuminate/Validation/ValidationRuleParser.php",
"vendor/laravel/framework/src/Illuminate/Foundation/Validation/ValidatesRequests.php",
"vendor/chumper/zipper/src/Chumper/Zipper/Zipper.php",
"vendor/psy/psysh/src/Shell.php",
"vendor/psy/psysh/src/CodeCleaner.php",
"vendor/psy/psysh/src/Input/FilterOptions.php",
"vendor/psy/psysh/src/Input/ShellInput.php",
"vendor/psy/psysh/src/Output/ShellOutput.php",
"vendor/psy/psysh/src/Command/Command.php",
"vendor/psy/psysh/src/Command/ListCommand.php",
"vendor/psy/psysh/src/Command/TimeitCommand/TimeitVisitor.php",
"vendor/psy/psysh/src/Command/ListCommand/ClassConstantEnumerator.php",
"vendor/psy/psysh/src/Command/ListCommand/Enumerator.php",
"vendor/psy/psysh/src/Command/ListCommand/ClassEnumerator.php",
"vendor/psy/psysh/src/Command/ListCommand/ConstantEnumerator.php",
"vendor/psy/psysh/src/Command/ListCommand/FunctionEnumerator.php",
"vendor/psy/psysh/src/Exception/ErrorException.php",
"vendor/psy/psysh/src/Exception/BreakException.php",
"vendor/psy/psysh/src/VarDumper/Dumper.php",
"vendor/laravel/framework/src/Illuminate/Foundation/Application.php"
]
},
"autoload-dev": {

509
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@ return [
| or any other location as required by the application or its packages.
*/
'version' => '1.8.106',
'version' => '1.8.187',
/*
|--------------------------------------------------------------------------
@@ -98,8 +98,8 @@ return [
*/
'locale' => env('APP_LOCALE', 'en'),
'locales' => ['en', 'zh-CN', 'hr', 'cs', 'da', 'nl', 'fi', 'fr', 'de', 'it', 'ja', 'ko', 'no', 'fa', 'pl', 'pt-PT', 'pt-BR', 'ru', 'es', 'sk', 'sv'],
'locales_rtl' => ['fa'],
'locales' => ['en', 'ar', 'zh-CN', 'hr', 'cs', 'da', 'nl', 'fi', 'fr', 'de', 'hu', 'it', 'ja', 'kz', 'ko', 'no', 'fa', 'pl', 'pt-PT', 'pt-BR', 'ro', 'ru', 'es', 'sk', 'sv', 'tr'],
'locales_rtl' => ['ar', 'fa'],
'default_locale' => 'en',
/*
@@ -170,10 +170,10 @@ return [
/*
|--------------------------------------------------------------------------
| FreeScout eepository
| FreeScout repository
|-------------------------------------------------------------------------
*/
'freescout_repo' => 'https://github.com/freescout-helpdesk/freescout',
'freescout_repo' => 'https://freescout.net/github',
/*
|--------------------------------------------------------------------------
@@ -188,7 +188,7 @@ return [
| Checks for new jobs every --sleep seconds.
| If --tries is set and job fails it is being processed right away without any delay.
| --delay parameter does not work to set delays between retry attempts.
| --timeout parameter sets job timeout and is used to avoid queue:work stucking.
| --timeout parameter sets job timeout and is used to avoid queue:work freezing.
|
| Jobs sending emails are retried manually in handle().
| Number of retries is set in each job class.
@@ -219,6 +219,7 @@ return [
|-------------------------------------------------------------------------
*/
'fetch_schedule' => env('APP_FETCH_SCHEDULE', 1),
'fetch_unseen' => env('APP_FETCH_UNSEEN', 1),
/*
|--------------------------------------------------------------------------
@@ -288,7 +289,7 @@ return [
|
|-------------------------------------------------------------------------
*/
'no_retry_mail_errors' => env('APP_NO_RETRY_MAIL_ERRORS', '(no valid recipients|does not comply with RFC|message file too big)'),
'no_retry_mail_errors' => env('APP_NO_RETRY_MAIL_ERRORS', '(no valid recipients|does not comply with RFC|message file too big|malformed address)'),
/*
|--------------------------------------------------------------------------
@@ -338,6 +339,26 @@ return [
*/
'use_mail_date_on_fetching' => env('APP_USE_MAIL_DATE_ON_FETCHING', false),
/*
|--------------------------------------------------------------------------
| Don't add quotes around date in the SINCE IMAP instruction on fetching.
| https://github.com/freescout-help-desk/freescout/issues/4175
|
|-------------------------------------------------------------------------
*/
'since_without_quotes_on_fetching' => env('APP_SINCE_WITHOUT_QUOTES_ON_FETCHING', false),
/*
|--------------------------------------------------------------------------
| Emails are fetched in bunches. The larger the bunch's size the more chances
| to face "Allowed memory size exhausted" error. The smaller its size the more
| connections are made to the mail server and the more time fetching takes.
| https://github.com/freescout-help-desk/freescout/issues/4343
|
|-------------------------------------------------------------------------
*/
'fetching_bunch_size' => env('APP_FETCHING_BUNCH_SIZE', 100),
/*
|--------------------------------------------------------------------------
| Dashboard path.
@@ -392,13 +413,6 @@ return [
*/
'custom_mail_headers' => env('APP_CUSTOM_MAIL_HEADERS', ''),
/*
|--------------------------------------------------------------------------
| Library used to fetch emails: webklex/laravel-imap, webklex/php-imap
|-------------------------------------------------------------------------
*/
'new_fetching_library' => env('APP_NEW_FETCHING_LIBRARY', false),
/*
|--------------------------------------------------------------------------
| Timeout for curl and GuzzleHttp.
@@ -455,6 +469,14 @@ return [
*/
'show_only_assigned_conversations' => env('APP_SHOW_ONLY_ASSIGNED_CONVERSATIONS', ''),
/*
|--------------------------------------------------------------------------
| Limit non-admin users to only see customers with conversations
| in mailboxes they are assigned to. This option does not affect admin users.
|-------------------------------------------------------------------------
*/
'limit_user_customer_visibility' => env('APP_LIMIT_USER_CUSTOMER_VISIBILITY', false),
/*
|--------------------------------------------------------------------------
| By default X-Frame-Options header is enabled and set to SAMEORIGIN.
@@ -472,6 +494,7 @@ return [
*/
'csp_enabled' => env('APP_CSP_ENABLED', true),
'csp_script_src' => env('APP_CSP_SCRIPT_SRC', ''),
'csp_custom' => env('APP_CSP_CUSTOM', ''),
/*
|--------------------------------------------------------------------------
@@ -480,6 +503,16 @@ return [
*/
'cloudflare_is_used' => env('APP_CLOUDFLARE_IS_USED', false),
/*
|--------------------------------------------------------------------------
| When this option is enabled you may see an extra text below customer's replies, for example:
| On Thu, Jan 4, 2024 at 8:41 AM John Doe | Demo <test@example.org> wrote:
|
| But overall reply separation in this case is more reliable.
|-------------------------------------------------------------------------
*/
'alternative_reply_separation' => env('APP_ALTERNATIVE_REPLY_SEPARATION', false),
/*
|--------------------------------------------------------------------------
| Autoloaded Service Providers
@@ -607,4 +640,4 @@ return [
'Updater' => Codedge\Updater\UpdaterFacade::class,
],
];
];

View File

@@ -36,7 +36,7 @@ return [
'sqlite' => [
'driver' => 'sqlite',
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'prefix' => env('DB_TABLE_PREFIX', ''),
],
'mysql' => [
@@ -49,11 +49,13 @@ return [
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix' => env('DB_TABLE_PREFIX', ''),
'strict' => false,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('DB_MYSQL_ATTR_SSL_CA'),
PDO::MYSQL_ATTR_SSL_CERT => env('DB_MYSQL_ATTR_SSL_CERT'),
PDO::ATTR_PERSISTENT => env('DB_ATTR_PERSISTENT'),
]) : [],
],
@@ -67,7 +69,7 @@ return [
//'port' => env('DB_TEST_PORT', '3306'),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix' => env('DB_TABLE_PREFIX', ''),
//'prefix_indexes' => true,
'strict' => false,
'engine' => null,
@@ -81,7 +83,7 @@ return [
'username' => env('DB_TEST_USERNAME', 'freescout-test'),
'password' => env('DB_TEST_PASSWORD', 'freescout-test'),
'charset' => 'utf8',
'prefix' => '',
'prefix' => env('DB_TABLE_PREFIX', ''),
'schema' => 'public',
'sslmode' => 'prefer',
],
@@ -94,9 +96,13 @@ return [
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'prefix' => env('DB_TABLE_PREFIX', ''),
'schema' => 'public',
'sslmode' => env('DB_PGSQL_SSLMODE', 'prefer'),
'options' => extension_loaded('pdo_pgsql') ? array_filter([
PDO::PGSQL_ATTR_DISABLE_PREPARES => env('DB_PGSQL_ATTR_DISABLE_PREPARES'),
PDO::ATTR_PERSISTENT => env('DB_ATTR_PERSISTENT'),
]) : [],
],
'sqlsrv' => [
@@ -107,7 +113,7 @@ return [
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'prefix' => env('DB_TABLE_PREFIX', ''),
],
],

View File

@@ -102,7 +102,8 @@ return [
*/
'options' => [
'delimiter' => '/',
'fetch' => FT_UID,
//'fetch' => FT_UID,
'fetch' => 1,
'fetch_body' => true,
'fetch_attachment' => true,
'fetch_flags' => true,

View File

@@ -88,6 +88,18 @@ return [
'password' => env('MAIL_PASSWORD'),
/*
|--------------------------------------------------------------------------
| Auth mode
|--------------------------------------------------------------------------
|
| This option have an effect when set to 'XOAUTH2' only.
| In other cases Swiftmailer chooses auth mode automatically.
|
*/
'auth_mode' => env('MAIL_AUTH_MODE', ''),
/*
|--------------------------------------------------------------------------
| Sendmail System Path
@@ -120,4 +132,13 @@ return [
],
],
/*
|--------------------------------------------------------------------------
| Swiftmailer SMTP timeout.
|--------------------------------------------------------------------------
|
*/
'smtp_timeout' => env('MAIL_SMTP_TIMEOUT', 30),
];

View File

@@ -24,11 +24,13 @@ return [
'settings' => [
'default' => [
'HTML.Doctype' => 'HTML 4.01 Transitional',
'HTML.Allowed' => 'div[style],b,strong,i,em,u,a[href|title],ul,ol,li,p[style],br,span[style],img[width|height|alt|src],table[width|class],tr[bgcolor],td[style|colspan|rowspan|width],th[style|colspan|rowspan],thead,tfoot,tbody,blockquote,pre,s,strike,h1,h2,h3,h4,h5,h6',
//'CSS.AllowedProperties' => 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,color,background-color,text-align',
'CSS.AllowedProperties' => 'font-weight,font-style,text-decoration,color,background-color,text-align,border,border-top,border-left,border-bottom,border-right',
'HTML.Allowed' => 'div[style],b,strong,i,em,u,a[href|title|style],ul,ol,li,p[style],br,span[style],img[width|height|alt|src|style],table[style|border|bgcolor|cellspacing|cellpadding|border|width|class],tr[style|bgcolor],td[style|colspan|rowspan|width|bgcolor|border|valign|align],th[style|colspan|rowspan],thead,tfoot,tbody,blockquote,pre,s,strike,font[style|color],h1[style],h2[style],h3[style],h4[style],h5[style],h6,center',
'CSS.AllowedProperties' => 'display,overflow,border-radius,letter-spacing,white-space,font-size,margin,margin-top,margin-right,margin-bottom,margin-left,background,text-transform,max-width,max-height,width,height,font,padding,padding-top,padding-right,padding-bottom,padding-left,font-family,border-color,font-weight,font-style,text-decoration,color,background-color,text-align,border,border-top,border-left,border-bottom,border-right',
'AutoFormat.AutoParagraph' => true,
'AutoFormat.RemoveEmpty' => true,
'CSS.Proprietary' => true,
'CSS.AllowTricky' => true,
'CSS.MaxImgLength' => null,
'URI.AllowedSchemes' => [
'http' => true,
'https' => true,

View File

@@ -41,6 +41,7 @@ return [
'repository_vendor' => 'freescout-helpdesk', //env('SELF_UPDATER_REPO_VENDOR', ''),
'repository_name' => 'freescout', //env('SELF_UPDATER_REPO_NAME', ''),
'repository_url' => '',
'github_api_url' => 'https://freescout.net/github-api',
'download_path' => storage_path().DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'updater', //env('SELF_UPDATER_DOWNLOAD_PATH', sys_get_temp_dir()),
],
],

View File

@@ -188,10 +188,16 @@ return [
| take place, and can be used to mitigate CSRF attacks. By default, we
| do not enable this as other CSRF protection services are in place.
|
| Supported: "lax", "strict"
| Supported: "lax", "strict", "none"
|
| When set to "lax" for example the SSO auth does not work:
| https://github.com/freescout-help-desk/freescout/issues/4750
|
| If set to "none" the "secure" flag should be set:
| https://github.com/freescout-help-desk/freescout/issues/4769
|
*/
'same_site' => null,
'same_site' => env('SESSION_SAME_SITE', null),
];

View File

@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ChangeAliasesColumnInMailboxesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('mailboxes', function (Blueprint $table) {
// https://github.com/freescout-helpdesk/freescout/issues/3525
$table->text('aliases')->nullable()->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
}
}

View File

@@ -0,0 +1,39 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddIndexToThreadsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// https://github.com/freescout-help-desk/freescout/issues/4069#issuecomment-2175476858
Schema::table('threads', function (Blueprint $table) {
$table->index(['created_at']);
});
// Schema::table('conversations', function (Blueprint $table) {
// $table->index(['mailbox_id', 'state', 'status'], 'conversations_id_state_status');
// });
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('threads', function (Blueprint $table) {
$table->dropIndex(['created_at']);
});
// Schema::table('conversations', function (Blueprint $table) {
// $table->dropIndex('conversations_id_state_status');
// });
}
}

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class ChangeImapSentFolderColumnInMailboxesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('mailboxes', function (Blueprint $table) {
$table->string('imap_sent_folder', 50)->nullable()->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('mailboxes', function (Blueprint $table) {
$table->string('imap_sent_folder', 25)->nullable()->change();
});
}
}

View File

@@ -0,0 +1,477 @@
<?php
namespace Barryvdh\Debugbar\DataCollector;
use DebugBar\DataCollector\PDO\PDOCollector;
use DebugBar\DataCollector\TimeDataCollector;
/**
* Collects data about SQL statements executed with PDO
*/
class QueryCollector extends PDOCollector
{
protected $timeCollector;
protected $queries = [];
protected $renderSqlWithParams = false;
protected $findSource = false;
protected $middleware = [];
protected $explainQuery = false;
protected $explainTypes = ['SELECT']; // ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+
protected $showHints = false;
protected $reflection = [];
/**
* @param TimeDataCollector $timeCollector
*/
public function __construct(?TimeDataCollector $timeCollector = null)
{
$this->timeCollector = $timeCollector;
}
/**
* Renders the SQL of traced statements with params embedded
*
* @param boolean $enabled
* @param string $quotationChar NOT USED
*/
public function setRenderSqlWithParams($enabled = true, $quotationChar = "'")
{
$this->renderSqlWithParams = $enabled;
}
/**
* Show or hide the hints in the parameters
*
* @param boolean $enabled
*/
public function setShowHints($enabled = true)
{
$this->showHints = $enabled;
}
/**
* Enable/disable finding the source
*
* @param bool $value
* @param array $middleware
*/
public function setFindSource($value, array $middleware)
{
$this->findSource = (bool) $value;
$this->middleware = $middleware;
}
/**
* Enable/disable the EXPLAIN queries
*
* @param bool $enabled
* @param array|null $types Array of types to explain queries (select/insert/update/delete)
*/
public function setExplainSource($enabled, $types)
{
$this->explainQuery = $enabled;
if($types){
$this->explainTypes = $types;
}
}
/**
*
* @param string $query
* @param array $bindings
* @param float $time
* @param \Illuminate\Database\Connection $connection
*/
public function addQuery($query, $bindings, $time, $connection)
{
$explainResults = [];
$time = $time / 1000;
$endTime = microtime(true);
$startTime = $endTime - $time;
$hints = $this->performQueryAnalysis($query);
$pdo = $connection->getPdo();
$bindings = $connection->prepareBindings($bindings);
// Run EXPLAIN on this query (if needed)
if ($this->explainQuery && preg_match('/^('.implode($this->explainTypes).') /i', $query)) {
$statement = $pdo->prepare('EXPLAIN ' . $query);
$statement->execute($bindings);
$explainResults = $statement->fetchAll(\PDO::FETCH_CLASS);
}
$bindings = $this->getDataFormatter()->checkBindings($bindings);
if (!empty($bindings) && $this->renderSqlWithParams) {
foreach ($bindings as $key => $binding) {
// This regex matches placeholders only, not the question marks,
// nested in quotes, while we iterate through the bindings
// and substitute placeholders by suitable values.
$regex = is_numeric($key)
? "/\?(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/"
: "/:{$key}(?=(?:[^'\\\']*'[^'\\\']*')*[^'\\\']*$)/";
$query = preg_replace($regex, $pdo->quote($binding), $query, 1);
}
}
$source = [];
if ($this->findSource) {
try {
$source = $this->findSource();
} catch (\Exception $e) {
}
}
$this->queries[] = [
'query' => $query,
'type' => 'query',
'bindings' => $this->getDataFormatter()->escapeBindings($bindings),
'time' => $time,
'source' => $source,
'explain' => $explainResults,
'connection' => $connection->getDatabaseName(),
'hints' => $this->showHints ? $hints : null,
];
if ($this->timeCollector !== null) {
$this->timeCollector->addMeasure($query, $startTime, $endTime);
}
}
/**
* Explainer::performQueryAnalysis()
*
* Perform simple regex analysis on the code
*
* @package xplain (https://github.com/rap2hpoutre/mysql-xplain-xplain)
* @author e-doceo
* @copyright 2014
* @version $Id$
* @access public
* @param string $query
* @return string
*/
protected function performQueryAnalysis($query)
{
$hints = [];
if (preg_match('/^\\s*SELECT\\s*`?[a-zA-Z0-9]*`?\\.?\\*/i', $query)) {
$hints[] = 'Use <code>SELECT *</code> only if you need all columns from table';
}
if (preg_match('/ORDER BY RAND()/i', $query)) {
$hints[] = '<code>ORDER BY RAND()</code> is slow, try to avoid if you can.
You can <a href="http://stackoverflow.com/questions/2663710/how-does-mysqls-order-by-rand-work" target="_blank">read this</a>
or <a href="http://stackoverflow.com/questions/1244555/how-can-i-optimize-mysqls-order-by-rand-function" target="_blank">this</a>';
}
if (strpos($query, '!=') !== false) {
$hints[] = 'The <code>!=</code> operator is not standard. Use the <code>&lt;&gt;</code> operator to test for inequality instead.';
}
if (stripos($query, 'WHERE') === false && preg_match('/^(SELECT) /i', $query)) {
$hints[] = 'The <code>SELECT</code> statement has no <code>WHERE</code> clause and could examine many more rows than intended';
}
if (preg_match('/LIMIT\\s/i', $query) && stripos($query, 'ORDER BY') === false) {
$hints[] = '<code>LIMIT</code> without <code>ORDER BY</code> causes non-deterministic results, depending on the query execution plan';
}
if (preg_match('/LIKE\\s[\'"](%.*?)[\'"]/i', $query, $matches)) {
$hints[] = 'An argument has a leading wildcard character: <code>' . $matches[1]. '</code>.
The predicate with this argument is not sargable and cannot use an index if one exists.';
}
return $hints;
}
/**
* Use a backtrace to search for the origins of the query.
*
* @return array
*/
protected function findSource()
{
$stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT, 50);
$sources = [];
foreach ($stack as $index => $trace) {
$sources[] = $this->parseTrace($index, $trace);
}
return array_filter($sources);
}
/**
* Parse a trace element from the backtrace stack.
*
* @param int $index
* @param array $trace
* @return object|bool
*/
protected function parseTrace($index, array $trace)
{
$frame = (object) [
'index' => $index,
'namespace' => null,
'name' => null,
'line' => isset($trace['line']) ? $trace['line'] : '?',
];
if (isset($trace['function']) && $trace['function'] == 'substituteBindings') {
$frame->name = 'Route binding';
return $frame;
}
if (isset($trace['class']) &&
isset($trace['file']) &&
!$this->fileIsInExcludedPath($trace['file'])
) {
$file = $trace['file'];
if (isset($trace['object']) && is_a($trace['object'], 'Twig_Template')) {
list($file, $frame->line) = $this->getTwigInfo($trace);
} elseif (strpos($file, storage_path()) !== false) {
$hash = pathinfo($file, PATHINFO_FILENAME);
if (! $frame->name = $this->findViewFromHash($hash)) {
$frame->name = $hash;
}
$frame->namespace = 'view';
return $frame;
} elseif (strpos($file, 'Middleware') !== false) {
$frame->name = $this->findMiddlewareFromFile($file);
if ($frame->name) {
$frame->namespace = 'middleware';
} else {
$frame->name = $this->normalizeFilename($file);
}
return $frame;
}
$frame->name = $this->normalizeFilename($file);
return $frame;
}
return false;
}
/**
* Check if the given file is to be excluded from analysis
*
* @param string $file
* @return bool
*/
protected function fileIsInExcludedPath($file)
{
$excludedPaths = [
'/vendor/laravel/framework/src/Illuminate/Database',
'/vendor/laravel/framework/src/Illuminate/Events',
'/vendor/barryvdh/laravel-debugbar',
];
$normalizedPath = str_replace('\\', '/', $file);
foreach ($excludedPaths as $excludedPath) {
if (strpos($normalizedPath, $excludedPath) !== false) {
return true;
}
}
return false;
}
/**
* Find the middleware alias from the file.
*
* @param string $file
* @return string|null
*/
protected function findMiddlewareFromFile($file)
{
$filename = pathinfo($file, PATHINFO_FILENAME);
foreach ($this->middleware as $alias => $class) {
if (strpos($class, $filename) !== false) {
return $alias;
}
}
}
/**
* Find the template name from the hash.
*
* @param string $hash
* @return null|string
*/
protected function findViewFromHash($hash)
{
$finder = app('view')->getFinder();
if (isset($this->reflection['viewfinderViews'])) {
$property = $this->reflection['viewfinderViews'];
} else {
$reflection = new \ReflectionClass($finder);
$property = $reflection->getProperty('views');
$property->setAccessible(true);
$this->reflection['viewfinderViews'] = $property;
}
foreach ($property->getValue($finder) as $name => $path){
if (sha1($path) == $hash || md5($path) == $hash) {
return $name;
}
}
}
/**
* Get the filename/line from a Twig template trace
*
* @param array $trace
* @return array The file and line
*/
protected function getTwigInfo($trace)
{
$file = $trace['object']->getTemplateName();
if (isset($trace['line'])) {
foreach ($trace['object']->getDebugInfo() as $codeLine => $templateLine) {
if ($codeLine <= $trace['line']) {
return [$file, $templateLine];
}
}
}
return [$file, -1];
}
/**
* Shorten the path by removing the relative links and base dir
*
* @param string $path
* @return string
*/
protected function normalizeFilename($path)
{
if (file_exists($path)) {
$path = realpath($path);
}
return str_replace(base_path(), '', $path);
}
/**
* Collect a database transaction event.
* @param string $event
* @param \Illuminate\Database\Connection $connection
* @return array
*/
public function collectTransactionEvent($event, $connection)
{
$source = [];
if ($this->findSource) {
try {
$source = $this->findSource();
} catch (\Exception $e) {
}
}
$this->queries[] = [
'query' => $event,
'type' => 'transaction',
'bindings' => [],
'time' => 0,
'source' => $source,
'explain' => [],
'connection' => $connection->getDatabaseName(),
'hints' => null,
];
}
/**
* Reset the queries.
*/
public function reset()
{
$this->queries = [];
}
/**
* {@inheritDoc}
*/
public function collect()
{
$totalTime = 0;
$queries = $this->queries;
$statements = [];
foreach ($queries as $query) {
$totalTime += $query['time'];
$statements[] = [
'sql' => $this->getDataFormatter()->formatSql($query['query']),
'type' => $query['type'],
'params' => [],
'bindings' => $query['bindings'],
'hints' => $query['hints'],
'backtrace' => array_values($query['source']),
'duration' => $query['time'],
'duration_str' => ($query['type'] == 'transaction') ? '' : $this->formatDuration($query['time']),
'stmt_id' => $this->getDataFormatter()->formatSource(reset($query['source'])),
'connection' => $query['connection'],
];
//Add the results from the explain as new rows
foreach($query['explain'] as $explain){
$statements[] = [
'sql' => ' - EXPLAIN #' . $explain->id . ': `' . $explain->table . '` (' . $explain->select_type . ')',
'type' => 'explain',
'params' => $explain,
'row_count' => $explain->rows,
'stmt_id' => $explain->id,
];
}
}
$nb_statements = array_filter($queries, function ($query) {
return $query['type'] == 'query';
});
$data = [
'nb_statements' => count($nb_statements),
'nb_failed_statements' => 0,
'accumulated_duration' => $totalTime,
'accumulated_duration_str' => $this->formatDuration($totalTime),
'statements' => $statements
];
return $data;
}
/**
* {@inheritDoc}
*/
public function getName()
{
return 'queries';
}
/**
* {@inheritDoc}
*/
public function getWidgets()
{
return [
"queries" => [
"icon" => "database",
"widget" => "PhpDebugBar.Widgets.LaravelSQLQueriesWidget",
"map" => "queries",
"default" => "[]"
],
"queries:badge" => [
"map" => "queries.nb_statements",
"default" => 0
]
];
}
}

View File

@@ -31,15 +31,15 @@ class Controller extends BaseController
$groups = [''=>'Choose a group'] + $groups;
$selected_locale = request()->input('locale');
if ($selected_locale == 'en') {
$selected_locale = '';
}
// if ($selected_locale == 'en') {
// $selected_locale = '';
// }
if (!$selected_locale) {
foreach ($locales as $locale) {
if ($locale != 'en') {
//if ($locale != 'en') {
$selected_locale = $locale;
break;
}
//}
}
}
@@ -78,7 +78,7 @@ class Controller extends BaseController
->with('numTodo', $numTodo)
->with('numChanged', $numChanged)
->with('editUrl', action('\Barryvdh\TranslationManager\Controller@postEdit', [$group]))
->with('deleteEnabled', $this->manager->getConfig('delete_enabled'));
->with('deleteEnabled', (bool) $this->manager->getConfig('delete_enabled'));
}
public function getView($group = null)
@@ -198,7 +198,8 @@ class Controller extends BaseController
public function postAddGroup(Request $request)
{
$group = str_replace(".", '', $request->input('new-group'));
//$group = str_replace(".", '', $request->input('new-group'));
$group = $this->sanitizeFilename($request->input('new-group'));
if ($group)
{
return redirect()->action('\Barryvdh\TranslationManager\Controller@getView',$group);
@@ -212,8 +213,12 @@ class Controller extends BaseController
public function postAddLocale(Request $request)
{
$locales = $this->manager->getLocales();
$newLocale = str_replace([], '-', trim($request->input('new-locale')));
if (!$newLocale || in_array($newLocale, $locales)) {
//$newLocale = str_replace([], '-', trim($request->input('new-locale')));
$newLocale = $this->sanitizeFilename($request->input('new-locale'));
if (!$newLocale
|| in_array($newLocale, $locales)
|| !in_array($newLocale, array_keys(\Helper::$locales))
) {
return redirect()->back();
}
$this->manager->addLocale($newLocale);
@@ -227,4 +232,15 @@ class Controller extends BaseController
}
return redirect()->back();
}
protected function sanitizeFilename($input)
{
$input = \Str::ascii(trim($input));
return str_replace(
['<', '>', '%', '/', '\\', '.', ' ', '"', "'", '#', '?'],
'',
$input
);
}
}

View File

@@ -449,7 +449,12 @@ class Manager
// Modules translations are added here too.
foreach ($groupKeys as $key) {
// Split the group and item
list($group, $item) = explode('.', $key, 2);
$parts = explode('.', $key, 2);
if (count($parts) < 2) {
\Log::warning("Translation key without group: $key");
continue;
}
list($group, $item) = $parts;
$this->missingKey('', $group, $item);
}
@@ -578,6 +583,14 @@ class Manager
$translations = $groups[$group];
// Sort translations alphabetically.
ksort($translations);
// Strips some tags to avoid XSS when translations are inserted via {!! ... !!}.
foreach ($translations as $key => $value) {
$value = \Helper::stripDangerousTags($value);
$translations[$key] = $value;
}
$output = json_encode($translations, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_UNICODE);
$this->files->put($path, $output);
@@ -692,9 +705,10 @@ class Manager
public function removeLocale($locale)
{
if (!$locale) {
if (!$locale || !in_array($locale, array_keys(\Helper::$locales))) {
return false;
}
$this->ignoreLocales = array_merge($this->ignoreLocales, [$locale]);
// Only delete from DB.
//$this->saveIgnoredLocales();

View File

@@ -53,7 +53,7 @@ class Zipper
*
* @param Filesystem $fs
*/
public function __construct(Filesystem $fs = null)
public function __construct(?Filesystem $fs = null)
{
$this->file = $fs ? $fs : new Filesystem();
}

View File

@@ -300,9 +300,15 @@ class GithubRepositoryType extends AbstractRepositoryType implements SourceRepos
throw new \Exception('No repository specified. Please enter a valid Github repository owner and name in your config.');
}
if (!empty($this->config['github_api_url'])) {
$github_api_url = $this->config['github_api_url'];
} else {
$github_api_url = self::GITHUB_API_URL.'/repos/'.$this->config['repository_vendor'].'/'.$this->config['repository_name'];
}
return $this->client->request(
'GET',
self::GITHUB_API_URL.'/repos/'.$this->config['repository_vendor'].'/'.$this->config['repository_name'].'/tags', [
$github_api_url.'/tags', [
'timeout' => config('app.curl_timeout'),
'connect_timeout' => config('app.curl_connect_timeout'),
'proxy' => config('app.proxy'),

View File

@@ -58,7 +58,7 @@ abstract class BaseProvider implements Countable
/**
* @param null $publicPath
*/
public function __construct($publicPath = null, $config = null, Filesystem $file = null)
public function __construct($publicPath = null, $config = null, ?Filesystem $file = null)
{
$this->file = $file ?: new Filesystem;

View File

@@ -20,6 +20,7 @@ if (PHP_VERSION_ID >= 80000) {
*
* @return bool
*/
#[\ReturnTypeWillChange]
public function setFetchMode($mode, ...$args)
{
return $this->doSetFetchMode($mode, ...$args);

View File

@@ -138,7 +138,7 @@ class HTMLPurifier_Encoder
// need to do PHP stuff. 99% of the time, this will be the case.
if (preg_match(
'/^[\x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]*$/Du',
$str
$str ?? ''
)) {
return $str;
}

View File

@@ -78,7 +78,7 @@ class HTMLPurifier_Length
if ($this->n === '0' && $this->unit === false) {
return true;
}
if (!ctype_lower($this->unit)) {
if ($this->unit === false || !ctype_lower((string)$this->unit)) {
$this->unit = strtolower($this->unit);
}
if (!isset(HTMLPurifier_Length::$allowedUnits[$this->unit])) {

View File

@@ -272,7 +272,11 @@ class HTMLPurifier_Lexer
*/
protected static function removeIEConditional($string)
{
// https://github.com/freescout-helpdesk/freescout/issues/3894
return preg_replace(
// This change caused: https://github.com/freescout-helpdesk/freescout/issues/3907#issuecomment-2043126228
// '#<!--\[if [^>]+\]>(.*?)<!\[endif\]-->#si', // probably should generalize for all strings
// '$1',
'#<!--\[if [^>]+\]>.*?<!\[endif\]-->#si', // probably should generalize for all strings
'',
$string

View File

@@ -54,7 +54,7 @@ class HTMLPurifier_Lexer_DOMLex extends HTMLPurifier_Lexer
if ($config->get('Core.AggressivelyFixLt')) {
$char = '[^a-z!\/]';
$comment = "/<!--(.*?)(-->|\z)/is";
$html = preg_replace_callback($comment, array($this, 'callbackArmorCommentEntities'), $html);
$html = preg_replace_callback($comment, array($this, 'callbackArmorCommentEntities'), $html ?? '');
do {
$old = $html;
$html = preg_replace("/<($char)/i", '&lt;\\1', $html);

View File

@@ -0,0 +1,835 @@
<?php
/**
* Whoops - php errors for cool kids
* @author Filipe Dobreira <http://github.com/filp>
*/
namespace Whoops\Handler;
use InvalidArgumentException;
use RuntimeException;
use Symfony\Component\VarDumper\Cloner\AbstractCloner;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use UnexpectedValueException;
use Whoops\Exception\Formatter;
use Whoops\Util\Misc;
use Whoops\Util\TemplateHelper;
class PrettyPageHandler extends Handler
{
const EDITOR_SUBLIME = "sublime";
const EDITOR_TEXTMATE = "textmate";
const EDITOR_EMACS = "emacs";
const EDITOR_MACVIM = "macvim";
const EDITOR_PHPSTORM = "phpstorm";
const EDITOR_IDEA = "idea";
const EDITOR_VSCODE = "vscode";
const EDITOR_ATOM = "atom";
const EDITOR_ESPRESSO = "espresso";
const EDITOR_XDEBUG = "xdebug";
const EDITOR_NETBEANS = "netbeans";
/**
* Search paths to be scanned for resources.
*
* Stored in the reverse order they're declared.
*
* @var array
*/
private $searchPaths = [];
/**
* Fast lookup cache for known resource locations.
*
* @var array
*/
private $resourceCache = [];
/**
* The name of the custom css file.
*
* @var string|null
*/
private $customCss = null;
/**
* The name of the custom js file.
*
* @var string|null
*/
private $customJs = null;
/**
* @var array[]
*/
private $extraTables = [];
/**
* @var bool
*/
private $handleUnconditionally = false;
/**
* @var string
*/
private $pageTitle = "Whoops! There was an error.";
/**
* @var array[]
*/
private $applicationPaths;
/**
* @var array[]
*/
private $blacklist = [
'_GET' => [],
'_POST' => [],
'_FILES' => [],
'_COOKIE' => [],
'_SESSION' => [],
'_SERVER' => [],
'_ENV' => [],
];
/**
* An identifier for a known IDE/text editor.
*
* Either a string, or a calalble that resolves a string, that can be used
* to open a given file in an editor. If the string contains the special
* substrings %file or %line, they will be replaced with the correct data.
*
* @example
* "txmt://open?url=%file&line=%line"
*
* @var callable|string $editor
*/
protected $editor;
/**
* A list of known editor strings.
*
* @var array
*/
protected $editors = [
"sublime" => "subl://open?url=file://%file&line=%line",
"textmate" => "txmt://open?url=file://%file&line=%line",
"emacs" => "emacs://open?url=file://%file&line=%line",
"macvim" => "mvim://open/?url=file://%file&line=%line",
"phpstorm" => "phpstorm://open?file=%file&line=%line",
"idea" => "idea://open?file=%file&line=%line",
"vscode" => "vscode://file/%file:%line",
"atom" => "atom://core/open/file?filename=%file&line=%line",
"espresso" => "x-espresso://open?filepath=%file&lines=%line",
"netbeans" => "netbeans://open/?f=%file:%line",
];
/**
* @var TemplateHelper
*/
protected $templateHelper;
/**
* Constructor.
*
* @return void
*/
public function __construct()
{
if (ini_get('xdebug.file_link_format') || get_cfg_var('xdebug.file_link_format')) {
// Register editor using xdebug's file_link_format option.
$this->editors['xdebug'] = function ($file, $line) {
return str_replace(['%f', '%l'], [$file, $line], ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'));
};
// If xdebug is available, use it as default editor.
$this->setEditor('xdebug');
}
// Add the default, local resource search path:
//$this->searchPaths[] = __DIR__ . "/../Resources";
$this->searchPaths[] = __DIR__ . "/../../../../../../vendor/filp/whoops/src/Whoops/Resources";
// blacklist php provided auth based values
$this->blacklist('_SERVER', 'PHP_AUTH_PW');
$this->templateHelper = new TemplateHelper();
if (class_exists('Symfony\Component\VarDumper\Cloner\VarCloner')) {
$cloner = new VarCloner();
// Only dump object internals if a custom caster exists for performance reasons
// https://github.com/filp/whoops/pull/404
$cloner->addCasters(['*' => function ($obj, $a, $stub, $isNested, $filter = 0) {
$class = $stub->class;
$classes = [$class => $class] + class_parents($obj) + class_implements($obj);
foreach ($classes as $class) {
if (isset(AbstractCloner::$defaultCasters[$class])) {
return $a;
}
}
// Remove all internals
return [];
}]);
$this->templateHelper->setCloner($cloner);
}
}
/**
* @return int|null
*
* @throws \Exception
*/
public function handle()
{
if (!$this->handleUnconditionally()) {
// Check conditions for outputting HTML:
// @todo: Make this more robust
if (PHP_SAPI === 'cli') {
// Help users who have been relying on an internal test value
// fix their code to the proper method
if (isset($_ENV['whoops-test'])) {
throw new \Exception(
'Use handleUnconditionally instead of whoops-test'
.' environment variable'
);
}
return Handler::DONE;
}
}
$templateFile = $this->getResource("views/layout.html.php");
$cssFile = $this->getResource("css/whoops.base.css");
$zeptoFile = $this->getResource("js/zepto.min.js");
$prismJs = $this->getResource("js/prism.js");
$prismCss = $this->getResource("css/prism.css");
$clipboard = $this->getResource("js/clipboard.min.js");
$jsFile = $this->getResource("js/whoops.base.js");
if ($this->customCss) {
$customCssFile = $this->getResource($this->customCss);
}
if ($this->customJs) {
$customJsFile = $this->getResource($this->customJs);
}
$inspector = $this->getInspector();
$frames = $this->getExceptionFrames();
$code = $this->getExceptionCode();
// List of variables that will be passed to the layout template.
$vars = [
"page_title" => $this->getPageTitle(),
// @todo: Asset compiler
"stylesheet" => file_get_contents($cssFile),
"zepto" => file_get_contents($zeptoFile),
"prismJs" => file_get_contents($prismJs),
"prismCss" => file_get_contents($prismCss),
"clipboard" => file_get_contents($clipboard),
"javascript" => file_get_contents($jsFile),
// Template paths:
"header" => $this->getResource("views/header.html.php"),
"header_outer" => $this->getResource("views/header_outer.html.php"),
"frame_list" => $this->getResource("views/frame_list.html.php"),
"frames_description" => $this->getResource("views/frames_description.html.php"),
"frames_container" => $this->getResource("views/frames_container.html.php"),
"panel_details" => $this->getResource("views/panel_details.html.php"),
"panel_details_outer" => $this->getResource("views/panel_details_outer.html.php"),
"panel_left" => $this->getResource("views/panel_left.html.php"),
"panel_left_outer" => $this->getResource("views/panel_left_outer.html.php"),
"frame_code" => $this->getResource("views/frame_code.html.php"),
"env_details" => $this->getResource("views/env_details.html.php"),
"title" => $this->getPageTitle(),
"name" => explode("\\", $inspector->getExceptionName()),
"message" => $inspector->getExceptionMessage(),
"previousMessages" => $inspector->getPreviousExceptionMessages(),
"docref_url" => $inspector->getExceptionDocrefUrl(),
"code" => $code,
"previousCodes" => $inspector->getPreviousExceptionCodes(),
"plain_exception" => Formatter::formatExceptionPlain($inspector),
"frames" => $frames,
"has_frames" => !!count($frames),
"handler" => $this,
"handlers" => $this->getRun()->getHandlers(),
"active_frames_tab" => count($frames) && $frames->offsetGet(0)->isApplication() ? 'application' : 'all',
"has_frames_tabs" => $this->getApplicationPaths(),
"tables" => [
"GET Data" => $this->masked($_GET, '_GET'),
"POST Data" => $this->masked($_POST, '_POST'),
"Files" => isset($_FILES) ? $this->masked($_FILES, '_FILES') : [],
"Cookies" => $this->masked($_COOKIE, '_COOKIE'),
"Session" => isset($_SESSION) ? $this->masked($_SESSION, '_SESSION') : [],
"Server/Request Data" => $this->masked($_SERVER, '_SERVER'),
"Environment Variables" => $this->masked($_ENV, '_ENV'),
],
];
if (isset($customCssFile)) {
$vars["stylesheet"] .= file_get_contents($customCssFile);
}
if (isset($customJsFile)) {
$vars["javascript"] .= file_get_contents($customJsFile);
}
// Add extra entries list of data tables:
// @todo: Consolidate addDataTable and addDataTableCallback
$extraTables = array_map(function ($table) use ($inspector) {
return $table instanceof \Closure ? $table($inspector) : $table;
}, $this->getDataTables());
$vars["tables"] = array_merge($extraTables, $vars["tables"]);
$plainTextHandler = new PlainTextHandler();
$plainTextHandler->setException($this->getException());
$plainTextHandler->setInspector($this->getInspector());
$vars["preface"] = "<!--\n\n\n" . $this->templateHelper->escape($plainTextHandler->generateResponse()) . "\n\n\n\n\n\n\n\n\n\n\n-->";
$this->templateHelper->setVariables($vars);
$this->templateHelper->render($templateFile);
return Handler::QUIT;
}
/**
* Get the stack trace frames of the exception currently being handled.
*
* @return \Whoops\Exception\FrameCollection
*/
protected function getExceptionFrames()
{
$frames = $this->getInspector()->getFrames();
if ($this->getApplicationPaths()) {
foreach ($frames as $frame) {
foreach ($this->getApplicationPaths() as $path) {
if (strpos($frame->getFile(), $path) === 0) {
$frame->setApplication(true);
break;
}
}
}
}
return $frames;
}
/**
* Get the code of the exception currently being handled.
*
* @return string
*/
protected function getExceptionCode()
{
$exception = $this->getException();
$code = $exception->getCode();
if ($exception instanceof \ErrorException) {
// ErrorExceptions wrap the php-error types within the 'severity' property
$code = Misc::translateErrorCode($exception->getSeverity());
}
return (string) $code;
}
/**
* @return string
*/
public function contentType()
{
return 'text/html';
}
/**
* Adds an entry to the list of tables displayed in the template.
*
* The expected data is a simple associative array. Any nested arrays
* will be flattened with `print_r`.
*
* @param string $label
* @param array $data
*
* @return static
*/
public function addDataTable($label, array $data)
{
$this->extraTables[$label] = $data;
return $this;
}
/**
* Lazily adds an entry to the list of tables displayed in the table.
*
* The supplied callback argument will be called when the error is
* rendered, it should produce a simple associative array. Any nested
* arrays will be flattened with `print_r`.
*
* @param string $label
* @param callable $callback Callable returning an associative array
*
* @throws InvalidArgumentException If $callback is not callable
*
* @return static
*/
public function addDataTableCallback($label, /* callable */ $callback)
{
if (!is_callable($callback)) {
throw new InvalidArgumentException('Expecting callback argument to be callable');
}
$this->extraTables[$label] = function (?\Whoops\Exception\Inspector $inspector = null) use ($callback) {
try {
$result = call_user_func($callback, $inspector);
// Only return the result if it can be iterated over by foreach().
return is_array($result) || $result instanceof \Traversable ? $result : [];
} catch (\Exception $e) {
// Don't allow failure to break the rendering of the original exception.
return [];
}
};
return $this;
}
/**
* Returns all the extra data tables registered with this handler.
*
* Optionally accepts a 'label' parameter, to only return the data table
* under that label.
*
* @param string|null $label
*
* @return array[]|callable
*/
public function getDataTables($label = null)
{
if ($label !== null) {
return isset($this->extraTables[$label]) ?
$this->extraTables[$label] : [];
}
return $this->extraTables;
}
/**
* Set whether to handle unconditionally.
*
* Allows to disable all attempts to dynamically decide whether to handle
* or return prematurely. Set this to ensure that the handler will perform,
* no matter what.
*
* @param bool|null $value
*
* @return bool|static
*/
public function handleUnconditionally($value = null)
{
if (func_num_args() == 0) {
return $this->handleUnconditionally;
}
$this->handleUnconditionally = (bool) $value;
return $this;
}
/**
* Adds an editor resolver.
*
* Either a string, or a closure that resolves a string, that can be used
* to open a given file in an editor. If the string contains the special
* substrings %file or %line, they will be replaced with the correct data.
*
* @example
* $run->addEditor('macvim', "mvim://open?url=file://%file&line=%line")
* @example
* $run->addEditor('remove-it', function($file, $line) {
* unlink($file);
* return "http://stackoverflow.com";
* });
*
* @param string $identifier
* @param string|callable $resolver
*
* @return static
*/
public function addEditor($identifier, $resolver)
{
$this->editors[$identifier] = $resolver;
return $this;
}
/**
* Set the editor to use to open referenced files.
*
* Pass either the name of a configured editor, or a closure that directly
* resolves an editor string.
*
* @example
* $run->setEditor(function($file, $line) { return "file:///{$file}"; });
* @example
* $run->setEditor('sublime');
*
* @param string|callable $editor
*
* @throws InvalidArgumentException If invalid argument identifier provided
*
* @return static
*/
public function setEditor($editor)
{
if (!is_callable($editor) && !isset($this->editors[$editor])) {
throw new InvalidArgumentException(
"Unknown editor identifier: $editor. Known editors:" .
implode(",", array_keys($this->editors))
);
}
$this->editor = $editor;
return $this;
}
/**
* Get the editor href for a given file and line, if available.
*
* @param string $filePath
* @param int $line
*
* @throws InvalidArgumentException If editor resolver does not return a string
*
* @return string|bool
*/
public function getEditorHref($filePath, $line)
{
$editor = $this->getEditor($filePath, $line);
if (empty($editor)) {
return false;
}
// Check that the editor is a string, and replace the
// %line and %file placeholders:
if (!isset($editor['url']) || !is_string($editor['url'])) {
throw new UnexpectedValueException(
__METHOD__ . " should always resolve to a string or a valid editor array; got something else instead."
);
}
$editor['url'] = str_replace("%line", rawurlencode($line), $editor['url']);
$editor['url'] = str_replace("%file", rawurlencode($filePath), $editor['url']);
return $editor['url'];
}
/**
* Determine if the editor link should act as an Ajax request.
*
* @param string $filePath
* @param int $line
*
* @throws UnexpectedValueException If editor resolver does not return a boolean
*
* @return bool
*/
public function getEditorAjax($filePath, $line)
{
$editor = $this->getEditor($filePath, $line);
// Check that the ajax is a bool
if (!isset($editor['ajax']) || !is_bool($editor['ajax'])) {
throw new UnexpectedValueException(
__METHOD__ . " should always resolve to a bool; got something else instead."
);
}
return $editor['ajax'];
}
/**
* Determines both the editor and if ajax should be used.
*
* @param string $filePath
* @param int $line
*
* @return array
*/
protected function getEditor($filePath, $line)
{
if (!$this->editor || (!is_string($this->editor) && !is_callable($this->editor))) {
return [];
}
if (is_string($this->editor) && isset($this->editors[$this->editor]) && !is_callable($this->editors[$this->editor])) {
return [
'ajax' => false,
'url' => $this->editors[$this->editor],
];
}
if (is_callable($this->editor) || (isset($this->editors[$this->editor]) && is_callable($this->editors[$this->editor]))) {
if (is_callable($this->editor)) {
$callback = call_user_func($this->editor, $filePath, $line);
} else {
$callback = call_user_func($this->editors[$this->editor], $filePath, $line);
}
if (empty($callback)) {
return [];
}
if (is_string($callback)) {
return [
'ajax' => false,
'url' => $callback,
];
}
return [
'ajax' => isset($callback['ajax']) ? $callback['ajax'] : false,
'url' => isset($callback['url']) ? $callback['url'] : $callback,
];
}
return [];
}
/**
* Set the page title.
*
* @param string $title
*
* @return static
*/
public function setPageTitle($title)
{
$this->pageTitle = (string) $title;
return $this;
}
/**
* Get the page title.
*
* @return string
*/
public function getPageTitle()
{
return $this->pageTitle;
}
/**
* Adds a path to the list of paths to be searched for resources.
*
* @param string $path
*
* @throws InvalidArgumentException If $path is not a valid directory
*
* @return static
*/
public function addResourcePath($path)
{
if (!is_dir($path)) {
throw new InvalidArgumentException(
"'$path' is not a valid directory"
);
}
array_unshift($this->searchPaths, $path);
return $this;
}
/**
* Adds a custom css file to be loaded.
*
* @param string|null $name
*
* @return static
*/
public function addCustomCss($name)
{
$this->customCss = $name;
return $this;
}
/**
* Adds a custom js file to be loaded.
*
* @param string|null $name
*
* @return static
*/
public function addCustomJs($name)
{
$this->customJs = $name;
return $this;
}
/**
* @return array
*/
public function getResourcePaths()
{
return $this->searchPaths;
}
/**
* Finds a resource, by its relative path, in all available search paths.
*
* The search is performed starting at the last search path, and all the
* way back to the first, enabling a cascading-type system of overrides for
* all resources.
*
* @param string $resource
*
* @throws RuntimeException If resource cannot be found in any of the available paths
*
* @return string
*/
protected function getResource($resource)
{
// If the resource was found before, we can speed things up
// by caching its absolute, resolved path:
if (isset($this->resourceCache[$resource])) {
return $this->resourceCache[$resource];
}
// Search through available search paths, until we find the
// resource we're after:
foreach ($this->searchPaths as $path) {
$fullPath = $path . "/$resource";
if (is_file($fullPath)) {
// Cache the result:
$this->resourceCache[$resource] = $fullPath;
return $fullPath;
}
}
// If we got this far, nothing was found.
throw new RuntimeException(
"Could not find resource '$resource' in any resource paths."
. "(searched: " . join(", ", $this->searchPaths). ")"
);
}
/**
* @deprecated
*
* @return string
*/
public function getResourcesPath()
{
$allPaths = $this->getResourcePaths();
// Compat: return only the first path added
return end($allPaths) ?: null;
}
/**
* @deprecated
*
* @param string $resourcesPath
*
* @return static
*/
public function setResourcesPath($resourcesPath)
{
$this->addResourcePath($resourcesPath);
return $this;
}
/**
* Return the application paths.
*
* @return array
*/
public function getApplicationPaths()
{
return $this->applicationPaths;
}
/**
* Set the application paths.
*
* @param array $applicationPaths
*
* @return void
*/
public function setApplicationPaths($applicationPaths)
{
$this->applicationPaths = $applicationPaths;
}
/**
* Set the application root path.
*
* @param string $applicationRootPath
*
* @return void
*/
public function setApplicationRootPath($applicationRootPath)
{
$this->templateHelper->setApplicationRootPath($applicationRootPath);
}
/**
* blacklist a sensitive value within one of the superglobal arrays.
* Alias for the hideSuperglobalKey method.
*
* @param string $superGlobalName The name of the superglobal array, e.g. '_GET'
* @param string $key The key within the superglobal
* @see hideSuperglobalKey
*
* @return static
*/
public function blacklist($superGlobalName, $key)
{
$this->blacklist[$superGlobalName][] = $key;
return $this;
}
/**
* Hide a sensitive value within one of the superglobal arrays.
*
* @param string $superGlobalName The name of the superglobal array, e.g. '_GET'
* @param string $key The key within the superglobal
* @return static
*/
public function hideSuperglobalKey($superGlobalName, $key)
{
return $this->blacklist($superGlobalName, $key);
}
/**
* Checks all values within the given superGlobal array.
*
* Blacklisted values will be replaced by a equal length string containing
* only '*' characters for string values.
* Non-string values will be replaced with a fixed asterisk count.
* We intentionally dont rely on $GLOBALS as it depends on the 'auto_globals_jit' php.ini setting.
*
* @param array|\ArrayAccess $superGlobal One of the superglobal arrays
* @param string $superGlobalName The name of the superglobal array, e.g. '_GET'
*
* @return array $values without sensitive data
*/
private function masked($superGlobal, $superGlobalName)
{
$blacklisted = $this->blacklist[$superGlobalName];
$values = $superGlobal;
foreach ($blacklisted as $key) {
if (isset($superGlobal[$key])) {
$values[$key] = str_repeat('*', is_string($superGlobal[$key]) ? strlen($superGlobal[$key]) : 3);
}
}
return $values;
}
}

View File

@@ -0,0 +1,545 @@
<?php
/**
* Whoops - php errors for cool kids
* @author Filipe Dobreira <http://github.com/filp>
*/
namespace Whoops;
use InvalidArgumentException;
use Throwable;
use Whoops\Exception\ErrorException;
use Whoops\Exception\Inspector;
use Whoops\Handler\CallbackHandler;
use Whoops\Handler\Handler;
use Whoops\Handler\HandlerInterface;
use Whoops\Util\Misc;
use Whoops\Util\SystemFacade;
final class Run implements RunInterface
{
/**
* @var bool
*/
private $isRegistered;
/**
* @var bool
*/
private $allowQuit = true;
/**
* @var bool
*/
private $sendOutput = true;
/**
* @var integer|false
*/
private $sendHttpCode = 500;
/**
* @var integer|false
*/
private $sendExitCode = 1;
/**
* @var HandlerInterface[]
*/
private $handlerStack = [];
/**
* @var array
* @psalm-var list<array{patterns: string, levels: int}>
*/
private $silencedPatterns = [];
/**
* @var SystemFacade
*/
private $system;
/**
* In certain scenarios, like in shutdown handler, we can not throw exceptions.
*
* @var bool
*/
private $canThrowExceptions = true;
public function __construct(?SystemFacade $system = null)
{
$this->system = $system ?: new SystemFacade;
}
/**
* Explicitly request your handler runs as the last of all currently registered handlers.
*
* @param callable|HandlerInterface $handler
*
* @return Run
*/
public function appendHandler($handler)
{
array_unshift($this->handlerStack, $this->resolveHandler($handler));
return $this;
}
/**
* Explicitly request your handler runs as the first of all currently registered handlers.
*
* @param callable|HandlerInterface $handler
*
* @return Run
*/
public function prependHandler($handler)
{
return $this->pushHandler($handler);
}
/**
* Register your handler as the last of all currently registered handlers (to be executed first).
* Prefer using appendHandler and prependHandler for clarity.
*
* @param callable|HandlerInterface $handler
*
* @return Run
*
* @throws InvalidArgumentException If argument is not callable or instance of HandlerInterface.
*/
public function pushHandler($handler)
{
$this->handlerStack[] = $this->resolveHandler($handler);
return $this;
}
/**
* Removes and returns the last handler pushed to the handler stack.
*
* @see Run::removeFirstHandler(), Run::removeLastHandler()
*
* @return HandlerInterface|null
*/
public function popHandler()
{
return array_pop($this->handlerStack);
}
/**
* Removes the first handler.
*
* @return void
*/
public function removeFirstHandler()
{
array_pop($this->handlerStack);
}
/**
* Removes the last handler.
*
* @return void
*/
public function removeLastHandler()
{
array_shift($this->handlerStack);
}
/**
* Returns an array with all handlers, in the order they were added to the stack.
*
* @return array
*/
public function getHandlers()
{
return $this->handlerStack;
}
/**
* Clears all handlers in the handlerStack, including the default PrettyPage handler.
*
* @return Run
*/
public function clearHandlers()
{
$this->handlerStack = [];
return $this;
}
/**
* Registers this instance as an error handler.
*
* @return Run
*/
public function register()
{
if (!$this->isRegistered) {
// Workaround PHP bug 42098
// https://bugs.php.net/bug.php?id=42098
class_exists("\\Whoops\\Exception\\ErrorException");
class_exists("\\Whoops\\Exception\\FrameCollection");
class_exists("\\Whoops\\Exception\\Frame");
class_exists("\\Whoops\\Exception\\Inspector");
$this->system->setErrorHandler([$this, self::ERROR_HANDLER]);
$this->system->setExceptionHandler([$this, self::EXCEPTION_HANDLER]);
$this->system->registerShutdownFunction([$this, self::SHUTDOWN_HANDLER]);
$this->isRegistered = true;
}
return $this;
}
/**
* Unregisters all handlers registered by this Whoops\Run instance.
*
* @return Run
*/
public function unregister()
{
if ($this->isRegistered) {
$this->system->restoreExceptionHandler();
$this->system->restoreErrorHandler();
$this->isRegistered = false;
}
return $this;
}
/**
* Should Whoops allow Handlers to force the script to quit?
*
* @param bool|int $exit
*
* @return bool
*/
public function allowQuit($exit = null)
{
if (func_num_args() == 0) {
return $this->allowQuit;
}
return $this->allowQuit = (bool) $exit;
}
/**
* Silence particular errors in particular files.
*
* @param array|string $patterns List or a single regex pattern to match.
* @param int $levels Defaults to E_STRICT | E_DEPRECATED.
*
* @return Run
*/
public function silenceErrorsInPaths($patterns, $levels = 10240)
{
$this->silencedPatterns = array_merge(
$this->silencedPatterns,
array_map(
function ($pattern) use ($levels) {
return [
"pattern" => $pattern,
"levels" => $levels,
];
},
(array) $patterns
)
);
return $this;
}
/**
* Returns an array with silent errors in path configuration.
*
* @return array
*/
public function getSilenceErrorsInPaths()
{
return $this->silencedPatterns;
}
/**
* Should Whoops send HTTP error code to the browser if possible?
* Whoops will by default send HTTP code 500, but you may wish to
* use 502, 503, or another 5xx family code.
*
* @param bool|int $code
*
* @return int|false
*
* @throws InvalidArgumentException
*/
public function sendHttpCode($code = null)
{
if (func_num_args() == 0) {
return $this->sendHttpCode;
}
if (!$code) {
return $this->sendHttpCode = false;
}
if ($code === true) {
$code = 500;
}
if ($code < 400 || 600 <= $code) {
throw new InvalidArgumentException(
"Invalid status code '$code', must be 4xx or 5xx"
);
}
return $this->sendHttpCode = $code;
}
/**
* Should Whoops exit with a specific code on the CLI if possible?
* Whoops will exit with 1 by default, but you can specify something else.
*
* @param int $code
*
* @return int
*
* @throws InvalidArgumentException
*/
public function sendExitCode($code = null)
{
if (func_num_args() == 0) {
return $this->sendExitCode;
}
if ($code < 0 || 255 <= $code) {
throw new InvalidArgumentException(
"Invalid status code '$code', must be between 0 and 254"
);
}
return $this->sendExitCode = (int) $code;
}
/**
* Should Whoops push output directly to the client?
* If this is false, output will be returned by handleException.
*
* @param bool|int $send
*
* @return bool
*/
public function writeToOutput($send = null)
{
if (func_num_args() == 0) {
return $this->sendOutput;
}
return $this->sendOutput = (bool) $send;
}
/**
* Handles an exception, ultimately generating a Whoops error page.
*
* @param Throwable $exception
*
* @return string Output generated by handlers.
*/
public function handleException($exception)
{
// Walk the registered handlers in the reverse order
// they were registered, and pass off the exception
$inspector = $this->getInspector($exception);
// Capture output produced while handling the exception,
// we might want to send it straight away to the client,
// or return it silently.
$this->system->startOutputBuffering();
// Just in case there are no handlers:
$handlerResponse = null;
$handlerContentType = null;
try {
foreach (array_reverse($this->handlerStack) as $handler) {
$handler->setRun($this);
$handler->setInspector($inspector);
$handler->setException($exception);
// The HandlerInterface does not require an Exception passed to handle()
// and neither of our bundled handlers use it.
// However, 3rd party handlers may have already relied on this parameter,
// and removing it would be possibly breaking for users.
$handlerResponse = $handler->handle($exception);
// Collect the content type for possible sending in the headers.
$handlerContentType = method_exists($handler, 'contentType') ? $handler->contentType() : null;
if (in_array($handlerResponse, [Handler::LAST_HANDLER, Handler::QUIT])) {
// The Handler has handled the exception in some way, and
// wishes to quit execution (Handler::QUIT), or skip any
// other handlers (Handler::LAST_HANDLER). If $this->allowQuit
// is false, Handler::QUIT behaves like Handler::LAST_HANDLER
break;
}
}
$willQuit = $handlerResponse == Handler::QUIT && $this->allowQuit();
} finally {
$output = $this->system->cleanOutputBuffer();
}
// If we're allowed to, send output generated by handlers directly
// to the output, otherwise, and if the script doesn't quit, return
// it so that it may be used by the caller
if ($this->writeToOutput()) {
// @todo Might be able to clean this up a bit better
if ($willQuit) {
// Cleanup all other output buffers before sending our output:
while ($this->system->getOutputBufferLevel() > 0) {
$this->system->endOutputBuffering();
}
// Send any headers if needed:
if (Misc::canSendHeaders() && $handlerContentType) {
header("Content-Type: {$handlerContentType}");
}
}
$this->writeToOutputNow($output);
}
if ($willQuit) {
// HHVM fix for https://github.com/facebook/hhvm/issues/4055
$this->system->flushOutputBuffer();
$this->system->stopExecution(
$this->sendExitCode()
);
}
return $output;
}
/**
* Converts generic PHP errors to \ErrorException instances, before passing them off to be handled.
*
* This method MUST be compatible with set_error_handler.
*
* @param int $level
* @param string $message
* @param string|null $file
* @param int|null $line
*
* @return bool
*
* @throws ErrorException
*/
public function handleError($level, $message, $file = null, $line = null)
{
if ($level & $this->system->getErrorReportingLevel()) {
foreach ($this->silencedPatterns as $entry) {
$pathMatches = (bool) preg_match($entry["pattern"], $file);
$levelMatches = $level & $entry["levels"];
if ($pathMatches && $levelMatches) {
// Ignore the error, abort handling
// See https://github.com/filp/whoops/issues/418
return true;
}
}
// XXX we pass $level for the "code" param only for BC reasons.
// see https://github.com/filp/whoops/issues/267
$exception = new ErrorException($message, /*code*/ $level, /*severity*/ $level, $file, $line);
if ($this->canThrowExceptions) {
throw $exception;
} else {
$this->handleException($exception);
}
// Do not propagate errors which were already handled by Whoops.
return true;
}
// Propagate error to the next handler, allows error_get_last() to
// work on silenced errors.
return false;
}
/**
* Special case to deal with Fatal errors and the like.
*
* @return void
*/
public function handleShutdown()
{
// If we reached this step, we are in shutdown handler.
// An exception thrown in a shutdown handler will not be propagated
// to the exception handler. Pass that information along.
$this->canThrowExceptions = false;
$error = $this->system->getLastError();
if ($error && Misc::isLevelFatal($error['type'])) {
// If there was a fatal error,
// it was not handled in handleError yet.
$this->allowQuit = false;
$this->handleError(
$error['type'],
$error['message'],
$error['file'],
$error['line']
);
}
}
/**
* @param Throwable $exception
*
* @return Inspector
*/
private function getInspector($exception)
{
return new Inspector($exception);
}
/**
* Resolves the giving handler.
*
* @param callable|HandlerInterface $handler
*
* @return HandlerInterface
*
* @throws InvalidArgumentException
*/
private function resolveHandler($handler)
{
if (is_callable($handler)) {
$handler = new CallbackHandler($handler);
}
if (!$handler instanceof HandlerInterface) {
throw new InvalidArgumentException(
"Handler must be a callable, or instance of "
. "Whoops\\Handler\\HandlerInterface"
);
}
return $handler;
}
/**
* Echo something to the browser.
*
* @param string $output
*
* @return Run
*/
private function writeToOutputNow($output)
{
if ($this->sendHttpCode() && Misc::canSendHeaders()) {
$this->system->setHttpResponseCode(
$this->sendHttpCode()
);
}
echo $output;
return $this;
}
}

View File

@@ -0,0 +1,352 @@
<?php
/**
* Whoops - php errors for cool kids
* @author Filipe Dobreira <http://github.com/filp>
*/
namespace Whoops\Util;
use Symfony\Component\VarDumper\Caster\Caster;
use Symfony\Component\VarDumper\Cloner\AbstractCloner;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\HtmlDumper;
use Whoops\Exception\Frame;
/**
* Exposes useful tools for working with/in templates
*/
class TemplateHelper
{
/**
* An array of variables to be passed to all templates
* @var array
*/
private $variables = [];
/**
* @var HtmlDumper
*/
private $htmlDumper;
/**
* @var HtmlDumperOutput
*/
private $htmlDumperOutput;
/**
* @var AbstractCloner
*/
private $cloner;
/**
* @var string
*/
private $applicationRootPath;
public function __construct()
{
// root path for ordinary composer projects
$this->applicationRootPath = dirname(dirname(dirname(dirname(dirname(dirname(__DIR__))))));
}
/**
* Escapes a string for output in an HTML document
*
* @param string $raw
* @return string
*/
public function escape($raw)
{
$flags = ENT_QUOTES;
// HHVM has all constants defined, but only ENT_IGNORE
// works at the moment
if (defined("ENT_SUBSTITUTE") && !defined("HHVM_VERSION")) {
$flags |= ENT_SUBSTITUTE;
} else {
// This is for 5.3.
// The documentation warns of a potential security issue,
// but it seems it does not apply in our case, because
// we do not blacklist anything anywhere.
$flags |= ENT_IGNORE;
}
$raw = str_replace(chr(9), ' ', $raw);
return htmlspecialchars($raw, $flags, "UTF-8");
}
/**
* Escapes a string for output in an HTML document, but preserves
* URIs within it, and converts them to clickable anchor elements.
*
* @param string $raw
* @return string
*/
public function escapeButPreserveUris($raw)
{
$escaped = $this->escape($raw);
return preg_replace(
"@([A-z]+?://([-\w\.]+[-\w])+(:\d+)?(/([\w/_\.#-]*(\?\S+)?[^\.\s])?)?)@",
"<a href=\"$1\" target=\"_blank\" rel=\"noreferrer noopener\">$1</a>",
$escaped
);
}
/**
* Makes sure that the given string breaks on the delimiter.
*
* @param string $delimiter
* @param string $s
* @return string
*/
public function breakOnDelimiter($delimiter, $s)
{
$parts = explode($delimiter, $s);
foreach ($parts as &$part) {
$part = '<span class="delimiter">' . $part . '</span>';
}
return implode($delimiter, $parts);
}
/**
* Replace the part of the path that all files have in common.
*
* @param string $path
* @return string
*/
public function shorten($path)
{
if ($this->applicationRootPath != "/") {
$path = str_replace($this->applicationRootPath, '&hellip;', $path);
}
return $path;
}
private function getDumper()
{
if (!$this->htmlDumper && class_exists('Symfony\Component\VarDumper\Cloner\VarCloner')) {
$this->htmlDumperOutput = new HtmlDumperOutput();
// re-use the same var-dumper instance, so it won't re-render the global styles/scripts on each dump.
$this->htmlDumper = new HtmlDumper($this->htmlDumperOutput);
$styles = [
'default' => 'color:#FFFFFF; line-height:normal; font:12px "Inconsolata", "Fira Mono", "Source Code Pro", Monaco, Consolas, "Lucida Console", monospace !important; word-wrap: break-word; white-space: pre-wrap; position:relative; z-index:99999; word-break: normal',
'num' => 'color:#BCD42A',
'const' => 'color: #4bb1b1;',
'str' => 'color:#BCD42A',
'note' => 'color:#ef7c61',
'ref' => 'color:#A0A0A0',
'public' => 'color:#FFFFFF',
'protected' => 'color:#FFFFFF',
'private' => 'color:#FFFFFF',
'meta' => 'color:#FFFFFF',
'key' => 'color:#BCD42A',
'index' => 'color:#ef7c61',
];
$this->htmlDumper->setStyles($styles);
}
return $this->htmlDumper;
}
/**
* Format the given value into a human readable string.
*
* @param mixed $value
* @return string
*/
public function dump($value)
{
$dumper = $this->getDumper();
if ($dumper) {
// re-use the same DumpOutput instance, so it won't re-render the global styles/scripts on each dump.
// exclude verbose information (e.g. exception stack traces)
if (class_exists('Symfony\Component\VarDumper\Caster\Caster')) {
$cloneVar = $this->getCloner()->cloneVar($value, Caster::EXCLUDE_VERBOSE);
// Symfony VarDumper 2.6 Caster class dont exist.
} else {
$cloneVar = $this->getCloner()->cloneVar($value);
}
$dumper->dump(
$cloneVar,
$this->htmlDumperOutput
);
$output = $this->htmlDumperOutput->getOutput();
$this->htmlDumperOutput->clear();
return $output;
}
return htmlspecialchars(print_r($value, true));
}
/**
* Format the args of the given Frame as a human readable html string
*
* @param Frame $frame
* @return string the rendered html
*/
public function dumpArgs(Frame $frame)
{
// we support frame args only when the optional dumper is available
if (!$this->getDumper()) {
return '';
}
$html = '';
$numFrames = count($frame->getArgs());
if ($numFrames > 0) {
$html = '<ol class="linenums">';
foreach ($frame->getArgs() as $j => $frameArg) {
$html .= '<li>'. $this->dump($frameArg) .'</li>';
}
$html .= '</ol>';
}
return $html;
}
/**
* Convert a string to a slug version of itself
*
* @param string $original
* @return string
*/
public function slug($original)
{
$slug = str_replace(" ", "-", $original);
$slug = preg_replace('/[^\w\d\-\_]/i', '', $slug);
return strtolower($slug);
}
/**
* Given a template path, render it within its own scope. This
* method also accepts an array of additional variables to be
* passed to the template.
*
* @param string $template
* @param array $additionalVariables
*/
public function render($template, ?array $additionalVariables = null)
{
$variables = $this->getVariables();
// Pass the helper to the template:
$variables["tpl"] = $this;
if ($additionalVariables !== null) {
$variables = array_replace($variables, $additionalVariables);
}
call_user_func(function () {
extract(func_get_arg(1));
require func_get_arg(0);
}, $template, $variables);
}
/**
* Sets the variables to be passed to all templates rendered
* by this template helper.
*
* @param array $variables
*/
public function setVariables(array $variables)
{
$this->variables = $variables;
}
/**
* Sets a single template variable, by its name:
*
* @param string $variableName
* @param mixed $variableValue
*/
public function setVariable($variableName, $variableValue)
{
$this->variables[$variableName] = $variableValue;
}
/**
* Gets a single template variable, by its name, or
* $defaultValue if the variable does not exist
*
* @param string $variableName
* @param mixed $defaultValue
* @return mixed
*/
public function getVariable($variableName, $defaultValue = null)
{
return isset($this->variables[$variableName]) ?
$this->variables[$variableName] : $defaultValue;
}
/**
* Unsets a single template variable, by its name
*
* @param string $variableName
*/
public function delVariable($variableName)
{
unset($this->variables[$variableName]);
}
/**
* Returns all variables for this helper
*
* @return array
*/
public function getVariables()
{
return $this->variables;
}
/**
* Set the cloner used for dumping variables.
*
* @param AbstractCloner $cloner
*/
public function setCloner($cloner)
{
$this->cloner = $cloner;
}
/**
* Get the cloner used for dumping variables.
*
* @return AbstractCloner
*/
public function getCloner()
{
if (!$this->cloner) {
$this->cloner = new VarCloner();
}
return $this->cloner;
}
/**
* Set the application root path.
*
* @param string $applicationRootPath
*/
public function setApplicationRootPath($applicationRootPath)
{
$this->applicationRootPath = $applicationRootPath;
}
/**
* Return the application root path.
*
* @return string
*/
public function getApplicationRootPath()
{
return $this->applicationRootPath;
}
}

View File

@@ -140,7 +140,7 @@ class CurlMultiHandler
while ($this->handles || !$queue->isEmpty()) {
// If there are no transfers, then sleep for the next delay
if (!$this->active && $this->delays) {
usleep($this->timeToNext());
usleep((int)$this->timeToNext());
}
$this->tick();
}

View File

@@ -76,7 +76,7 @@ class StreamHandler
array $options,
RequestInterface $request,
$startTime,
ResponseInterface $response = null,
?ResponseInterface $response = null,
$error = null
) {
if (isset($options['on_stats'])) {

View File

@@ -37,7 +37,7 @@ class HandlerStack
*
* @return HandlerStack
*/
public static function create(callable $handler = null)
public static function create(?callable $handler = null)
{
$stack = new self($handler ?: choose_handler());
$stack->push(Middleware::httpErrors(), 'http_errors');
@@ -51,7 +51,7 @@ class HandlerStack
/**
* @param callable $handler Underlying HTTP handler.
*/
public function __construct(callable $handler = null)
public function __construct(?callable $handler = null)
{
$this->handler = $handler;
}

View File

@@ -122,7 +122,7 @@ final class Middleware
*
* @return callable Returns a function that accepts the next handler.
*/
public static function tap(callable $before = null, callable $after = null)
public static function tap(?callable $before = null, ?callable $after = null)
{
return function (callable $handler) use ($before, $after) {
return function ($request, array $options) use ($handler, $before, $after) {
@@ -165,7 +165,7 @@ final class Middleware
*
* @return callable Returns a function that accepts the next handler.
*/
public static function retry(callable $decider, callable $delay = null)
public static function retry(?callable $decider, ?callable $delay = null)
{
return function (callable $handler) use ($decider, $delay) {
return new RetryMiddleware($decider, $handler, $delay);

View File

@@ -24,8 +24,8 @@ class FulfilledPromise implements PromiseInterface
}
public function then(
callable $onFulfilled = null,
callable $onRejected = null
?callable $onFulfilled = null,
?callable $onRejected = null
) {
// Return itself if there is no onFulfilled function.
if (!$onFulfilled) {

View File

@@ -21,16 +21,16 @@ class Promise implements PromiseInterface
* @param callable $cancelFn Fn that when invoked cancels the promise.
*/
public function __construct(
callable $waitFn = null,
callable $cancelFn = null
?callable $waitFn = null,
?callable $cancelFn = null
) {
$this->waitFn = $waitFn;
$this->cancelFn = $cancelFn;
}
public function then(
callable $onFulfilled = null,
callable $onRejected = null
?callable $onFulfilled = null,
?callable $onRejected = null
) {
if ($this->state === self::PENDING) {
$p = new Promise(null, [$this, 'cancel']);

View File

@@ -27,8 +27,8 @@ interface PromiseInterface
* @return PromiseInterface
*/
public function then(
callable $onFulfilled = null,
callable $onRejected = null
?callable $onFulfilled = null,
?callable $onRejected = null
);
/**

View File

@@ -24,8 +24,8 @@ class RejectedPromise implements PromiseInterface
}
public function then(
callable $onFulfilled = null,
callable $onRejected = null
?callable $onFulfilled = null,
?callable $onRejected = null
) {
// If there's no onRejected callback then just return self.
if (!$onRejected) {

View File

@@ -21,7 +21,7 @@ final class Utils
*
* @return TaskQueueInterface
*/
public static function queue(TaskQueueInterface $assign = null)
public static function queue(?TaskQueueInterface $assign = null)
{
static $queue;

View File

@@ -288,7 +288,7 @@ class Uri implements UriInterface
*
* @link https://tools.ietf.org/html/rfc3986#section-4.4
*/
public static function isSameDocumentReference(UriInterface $uri, UriInterface $base = null)
public static function isSameDocumentReference(UriInterface $uri, ?UriInterface $base = null)
{
if ($base !== null) {
$uri = UriResolver::resolve($base, $uri);

View File

@@ -125,7 +125,7 @@ class Gate implements GateContract
* @param array $abilities
* @return $this
*/
public function resource($name, $class, array $abilities = null)
public function resource($name, $class, ?array $abilities = null)
{
$abilities = $abilities ?: [
'view' => 'view',

View File

@@ -97,7 +97,7 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
public function __construct($name,
UserProvider $provider,
Session $session,
Request $request = null)
?Request $request = null)
{
$this->name = $name;
$this->session = $session;

View File

@@ -56,7 +56,7 @@ class BroadcastManager implements FactoryContract
* @param array|null $attributes
* @return void
*/
public function routes(array $attributes = null)
public function routes(?array $attributes = null)
{
if ($this->app->routesAreCached()) {
return;

View File

@@ -54,7 +54,7 @@ class Dispatcher implements QueueingDispatcher
* @param \Closure|null $queueResolver
* @return void
*/
public function __construct(Container $container, Closure $queueResolver = null)
public function __construct(Container $container, ?Closure $queueResolver = null)
{
$this->container = $container;
$this->queueResolver = $queueResolver;

View File

@@ -73,7 +73,7 @@ class Application extends SymfonyApplication implements ApplicationContract
/**
* {@inheritdoc}
*/
public function run(InputInterface $input = null, OutputInterface $output = null)
public function run(?InputInterface $input = null, ?OutputInterface $output = null)
{
$commandName = $this->getCommandName(
$input = $input ?: new ArgvInput

View File

@@ -0,0 +1,76 @@
<?php
namespace Illuminate\Database;
use Exception;
use Illuminate\Support\Str;
trait DetectsLostConnections
{
/**
* Determine if the given exception was caused by a lost connection.
*
* @param \Exception $e
* @return bool
*/
protected function causedByLostConnection(Exception $e)
{
$message = $e->getMessage();
return Str::contains($message, [
'server has gone away',
'no connection to the server',
'Lost connection',
'is dead or not enabled',
'Error while sending',
'decryption failed or bad record mac',
'server closed the connection unexpectedly',
'SSL connection has been closed unexpectedly',
'Error writing data to the connection',
'Resource deadlock avoided',
'Transaction() on null',
'child connection forced to terminate due to client_idle_limit',
'query_wait_timeout',
'reset by peer',
'Physical connection is not usable',
'TCP Provider: Error code 0x68',
'ORA-03114',
'Packets out of order. Expected',
'Adaptive Server connection failed',
'Communication link failure',
'connection is no longer usable',
'Login timeout expired',
'SQLSTATE[HY000] [2002] Connection refused',
'running with the --read-only option so it cannot execute this statement',
'The connection is broken and recovery is not possible. The connection is marked by the client driver as unrecoverable. No attempt was made to restore the connection.',
'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Try again',
'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo failed: Name or service not known',
'SQLSTATE[HY000] [2002] php_network_getaddresses: getaddrinfo for',
'SQLSTATE[HY000]: General error: 7 SSL SYSCALL error: EOF detected',
'SQLSTATE[HY000] [2002] Connection timed out',
'SSL: Connection timed out',
'SQLSTATE[HY000]: General error: 1105 The last transaction was aborted due to Seamless Scaling. Please retry.',
'Temporary failure in name resolution',
'SSL: Broken pipe',
'SQLSTATE[08S01]: Communication link failure',
'SQLSTATE[08006] [7] could not connect to server: Connection refused Is the server running on host',
'SQLSTATE[HY000]: General error: 7 SSL SYSCALL error: No route to host',
'The client was disconnected by the server because of inactivity. See wait_timeout and interactive_timeout for configuring this behavior.',
'SQLSTATE[08006] [7] could not translate host name',
'TCP Provider: Error code 0x274C',
'SQLSTATE[HY000] [2002] No such file or directory',
'SSL: Operation timed out',
'Reason: Server is in script upgrade mode. Only administrator can connect at this time.',
'Unknown $curl_error_code: 77',
'SSL: Handshake timed out',
'SQLSTATE[08006] [7] SSL error: sslv3 alert unexpected message',
'SQLSTATE[08006] [7] unrecognized SSL error code:',
'SQLSTATE[HY000] [2002] No connection could be made because the target machine actively refused it',
'SQLSTATE[HY000] [2002] A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond',
'SQLSTATE[HY000] [2002] Network is unreachable',
'SQLSTATE[HY000] [2002] The requested address is not valid in its context',
'SQLSTATE[HY000] [2002] A socket operation was attempted to an unreachable network',
'SQLSTATE[HY000]: General error: 3989',
]);
}
}

View File

@@ -150,7 +150,7 @@ class Builder
* @param array|null $scopes
* @return $this
*/
public function withoutGlobalScopes(array $scopes = null)
public function withoutGlobalScopes(?array $scopes = null)
{
if (is_array($scopes)) {
foreach ($scopes as $scope) {
@@ -423,7 +423,7 @@ class Builder
* @param \Closure|null $callback
* @return \Illuminate\Database\Eloquent\Model|static|mixed
*/
public function firstOr($columns = ['*'], Closure $callback = null)
public function firstOr($columns = ['*'], ?Closure $callback = null)
{
if ($columns instanceof Closure) {
$callback = $columns;

View File

@@ -18,7 +18,7 @@ trait HasGlobalScopes
*
* @throws \InvalidArgumentException
*/
public static function addGlobalScope($scope, Closure $implementation = null)
public static function addGlobalScope($scope, ?Closure $implementation = null)
{
if (is_string($scope) && ! is_null($implementation)) {
return static::$globalScopes[static::class][$scope] = $implementation;

View File

@@ -21,7 +21,7 @@ trait QueriesRelationships
* @param \Closure|null $callback
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', Closure $callback = null)
public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', ?Closure $callback = null)
{
if (strpos($relation, '.') !== false) {
return $this->hasNested($relation, $operator, $count, $boolean, $callback);
@@ -101,7 +101,7 @@ trait QueriesRelationships
* @param \Closure|null $callback
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function doesntHave($relation, $boolean = 'and', Closure $callback = null)
public function doesntHave($relation, $boolean = 'and', ?Closure $callback = null)
{
return $this->has($relation, '<', 1, $boolean, $callback);
}
@@ -126,7 +126,7 @@ trait QueriesRelationships
* @param int $count
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function whereHas($relation, Closure $callback = null, $operator = '>=', $count = 1)
public function whereHas($relation, ?Closure $callback = null, $operator = '>=', $count = 1)
{
return $this->has($relation, $operator, $count, 'and', $callback);
}
@@ -140,7 +140,7 @@ trait QueriesRelationships
* @param int $count
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function orWhereHas($relation, Closure $callback = null, $operator = '>=', $count = 1)
public function orWhereHas($relation, ?Closure $callback = null, $operator = '>=', $count = 1)
{
return $this->has($relation, $operator, $count, 'or', $callback);
}
@@ -152,7 +152,7 @@ trait QueriesRelationships
* @param \Closure|null $callback
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function whereDoesntHave($relation, Closure $callback = null)
public function whereDoesntHave($relation, ?Closure $callback = null)
{
return $this->doesntHave($relation, 'and', $callback);
}
@@ -164,7 +164,7 @@ trait QueriesRelationships
* @param \Closure $callback
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function orWhereDoesntHave($relation, Closure $callback = null)
public function orWhereDoesntHave($relation, ?Closure $callback = null)
{
return $this->doesntHave($relation, 'or', $callback);
}

View File

@@ -1046,7 +1046,7 @@ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializab
* @param array|null $except
* @return \Illuminate\Database\Eloquent\Model
*/
public function replicate(array $except = null)
public function replicate(?array $except = null)
{
$defaults = [
$this->getKeyName(),

View File

@@ -309,7 +309,7 @@ abstract class Relation
* @param bool $merge
* @return array
*/
public static function morphMap(array $map = null, $merge = true)
public static function morphMap(?array $map = null, $merge = true)
{
$map = static::buildMorphMapFromModels($map);
@@ -327,7 +327,7 @@ abstract class Relation
* @param string[]|null $models
* @return array|null
*/
protected static function buildMorphMapFromModels(array $models = null)
protected static function buildMorphMapFromModels(?array $models = null)
{
if (is_null($models) || Arr::isAssoc($models)) {
return $models;

View File

@@ -201,8 +201,8 @@ class Builder
* @return void
*/
public function __construct(ConnectionInterface $connection,
Grammar $grammar = null,
Processor $processor = null)
?Grammar $grammar = null,
?Processor $processor = null)
{
$this->connection = $connection;
$this->grammar = $grammar ?: $connection->getQueryGrammar();

View File

@@ -64,7 +64,7 @@ class Blueprint
* @param \Closure|null $callback
* @return void
*/
public function __construct($table, Closure $callback = null)
public function __construct($table, ?Closure $callback = null)
{
$this->table = $table;

Some files were not shown because too many files have changed in this diff Show More