Compare commits

...

29 Commits

Author SHA1 Message Date
Martin Edenhofer
8777ff5c85 Release 3.2.1 2020-03-03 10:28:08 +01:00
Martin Edenhofer
bf1e58804f Maintenance: Migrate email tests to new host. 2020-03-01 16:59:23 +01:00
Rolf Schmidt
4e75f8ff35 Enhancement: Reduce shown messages after password reset to one single version. 2020-03-01 16:59:23 +01:00
Rolf Schmidt
dad0af4b86 Enhancement: Provide unique error code for technical errors to make it easy to find them in the logs. 2020-03-01 16:59:23 +01:00
Thorsten Eckel
692addcf2a Enhancement: Reflect current_user in ticket_customer lookup. 2020-03-01 16:59:23 +01:00
Rolf Schmidt
2bfa144ce0 Enhancement: Made CORS Preflight Check specification conform. 2020-03-01 16:59:23 +01:00
Thorsten Eckel
67889e5970 Maintenance: Add Content Security Policy. 2020-03-01 16:59:23 +01:00
Rolf Schmidt
215bbe7884 Feature: Chat origin whitelisting functionality. 2020-03-01 16:59:23 +01:00
Thorsten Eckel
bcdf306a75 Enhancement: Improve handling of Ticket number in frontend. 2020-03-01 16:59:23 +01:00
Thorsten Eckel
f82ce99ca6 Enhancement: Limit data send back to the browser for valid session. 2020-03-01 16:59:23 +01:00
Rolf Schmidt
a5f314949e Enhancement: Harden default reverse proxy configuration to not send it's name and version number to the client. 2020-03-01 16:59:23 +01:00
Rolf Schmidt
7ce3166cef Enhancement: Requests should never be cached by the browser. 2020-03-01 16:59:23 +01:00
Rolf Schmidt
a4ded74601 Enhancement: Ensure added HTML is converted back to its original format when identifying signature. 2020-03-01 16:59:23 +01:00
Thorsten Eckel
8212295b27 Refactoring: Error request specs are not in RSpec format. 2020-03-01 16:59:23 +01:00
Thorsten Eckel
fd84f3a102 Bumped puma to 3.12.4 (CVE-2020-5247). 2020-03-01 15:17:13 +01:00
Thorsten Eckel
63c23f22de Bumped nokogiri to 1.10.8 (CVE-2020-7595). 2020-02-25 08:16:21 +01:00
Mantas Masalskis
992e734c9e Fixes #2895 - moving from Manage Knowledge base view to another Manage view requires multiple clicks 2020-01-26 21:40:39 +02:00
Mantas Masalskis
375c492931 Fixes #2406 - Forwarding: Attachment of original content gets removed starting from the second forward without reloading per ticket 2020-01-10 14:52:22 +01:00
Martin Edenhofer
19bb0240db Fixed issue #2873 - Tweet which is containing * will not send out, Twitter::Error::Unauthorized: Could not authenticate you. 2019-12-23 09:34:16 +01:00
Thorsten Eckel
528479cee6 Updated rake to 2.0.8 (CVE-2019-16782) and rails to 5.2.4.1. 2019-12-20 17:56:33 +01:00
Thorsten Eckel
40972b3fde Closes #2869 - Added Swedish chat translation - thanks to @pitah81 ❤️ 2019-12-18 17:13:53 +01:00
Thorsten Eckel
d131f6c5d3 Follow up - 045902351 - fixes #2754
045902351 adds a `rescue` clause
to fix a bug in which IMAP parsing errors
cause the entire email import process to fail.[0]

This bugfix contains an error:
we're still not sure why,
but the `next` inside the `rescue` clause
does not act like the `next`s outside of it
(execution of the containing `.each` block continues,
even after passing `next`).

This commit puts `next` in the right place,
so that the method will skip processing of the current message
and move onto the next one.

[0]: https://github.com/zammad/zammad/issues/2754
2019-12-17 11:03:43 +01:00
Thorsten Eckel
d79c85cfb6 Bumped puma to 13.12.2 (CVE-2019-16770). 2019-12-06 10:28:31 +01:00
Thorsten Eckel
02066c1a24 Follow up - ff6b4256e1 - Fixes #2826: Call of private method #blank? on Twitter client instance blocks migration. 2019-12-05 12:05:05 +01:00
Thorsten Eckel
19b597132b Added CentOS 8 support for packager.io 2019-12-05 09:21:05 +01:00
Thorsten Eckel
566eab44f7 Fixes #2826 - Invalid stored Twitter credentials block migration. 2019-12-04 13:07:59 +01:00
Thorsten Eckel
de2e8f4b8e Fixes #2824 - Elasticsearch 7.5.x support. 2019-12-04 12:04:51 +01:00
Martin Edenhofer
16fc27bdd6 Fixed issue #2823 - Unable to open notifications (if chat is enabled). 2019-12-03 16:37:30 +01:00
Martin Edenhofer
e0ff35cb14 Updated changes to 3.2.0. 2019-12-03 07:43:08 +01:00
96 changed files with 1031 additions and 506 deletions

View File

@@ -423,7 +423,7 @@ test:browser:integration:api_client_php:
ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_PASSWORD: "test"
script:
- RAILS_ENV=test bundle exec rake db:create
- bundle exec rake zammad:ci:test:start zammad:setup:auto_wizard
- RAILS_ENV=test bundle exec rake zammad:ci:test:start zammad:setup:auto_wizard
- git clone https://github.com/zammad/zammad-api-client-php.git
- php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
- php composer-setup.php --install-dir=/usr/local/bin

View File

@@ -16,6 +16,19 @@ targets:
build_dependencies:
- http://download.fedoraproject.org/pub/epel/7/x86_64/Packages/i/imlib2-1.4.5-9.el7.x86_64.rpm
- http://download.fedoraproject.org/pub/epel/7/x86_64/Packages/i/imlib2-devel-1.4.5-9.el7.x86_64.rpm
centos-8:
dependencies:
- curl
- elasticsearch
- nginx
- postgresql-server
- which
- epel-release
- imlib2
- imlib2-devel
build_dependencies:
- http://download.fedoraproject.org/pub/epel/8/Everything/x86_64/Packages/i/imlib2-1.4.9-8.el8.x86_64.rpm
- http://download.fedoraproject.org/pub/epel/8/Everything/x86_64/Packages/i/imlib2-devel-1.4.9-8.el8.x86_64.rpm
debian-8:
dependencies:
- curl

View File

@@ -14,7 +14,9 @@
# Configuration parameters: CountComments, ExcludedMethods.
# ExcludedMethods: refine
Metrics/BlockLength:
Max: 1987
Max: 1653
Exclude:
- 'spec/requests/ticket_spec.rb'
# Offense count: 16
RSpec/AnyInstance:

View File

@@ -1,15 +1,93 @@
# Change Log
## [3.2.0](https://github.com/zammad/zammad/tree/3.2.0) (2019-xx-xx)
[Full Changelog](https://github.com/zammad/zammad/compare/3.1.0...3.2.0)
## [3.2.1](https://github.com/zammad/zammad/tree/3.2.1) (2020-03-03)
[Full Changelog](https://github.com/zammad/zammad/compare/3.2.0...3.2.1)
**Implemented enhancements:**
**Fixed bugs:**
- Moving from Manage Knowledge base view to another Manage view requires multiple clicks [2895](https://github.com/zammad/zammad/issues/2895) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Forwarding: Attachment of original content gets removed starting from the second forward without reloading per ticket [2406](https://github.com/zammad/zammad/issues/2406) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend / JS app](https://github.com/zammad/zammad/labels/frontend / JS app)] [[ticket](https://github.com/zammad/zammad/labels/ticket)]
- Tweet which is containing * will not send out, Twitter::Error::Unauthorized: Could not authenticate you. [2873](https://github.com/zammad/zammad/issues/2873) [[third party issue](https://github.com/zammad/zammad/labels/third party issue)]
- Added Swedish chat translation [2869](https://github.com/zammad/zammad/pull/2869)
- Error while fetching email from inbox will block processing of other mails in inbox [2754](https://github.com/zammad/zammad/issues/2754) [[bug](https://github.com/zammad/zammad/labels/bug)] [[mail processing](https://github.com/zammad/zammad/labels/mail processing)]
- Invalid stored Twitter credentials block migration [2826](https://github.com/zammad/zammad/issues/2826) [[bug](https://github.com/zammad/zammad/labels/bug)] [[channel](https://github.com/zammad/zammad/labels/channel)] [[migration](https://github.com/zammad/zammad/labels/migration)]
- Zammad doesn't work with Elasticsearch 7.5.x [2824](https://github.com/zammad/zammad/issues/2824) [[bug](https://github.com/zammad/zammad/labels/bug)] [[search](https://github.com/zammad/zammad/labels/search)]
- Unable to open notifications (if chat is enabled) [2823](https://github.com/zammad/zammad/issues/2823) [[bug](https://github.com/zammad/zammad/labels/bug)] [[regression](https://github.com/zammad/zammad/labels/regression)]
## [3.2.0](https://github.com/zammad/zammad/tree/3.2.0) (2019-12-02)
[Full Changelog](https://github.com/zammad/zammad/compare/3.1.0...3.2.0)
**Implemented enhancements:**
- multiselect for field type select in trigger filters [1559](https://github.com/zammad/zammad/issues/1559) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[trigger](https://github.com/zammad/zammad/labels/trigger)]
- Chat enhancement: Option to set Agent display name [2291](https://github.com/zammad/zammad/issues/2291) [[chat](https://github.com/zammad/zammad/labels/chat)] [[enhancement](https://github.com/zammad/zammad/labels/enhancement)]
- Article deletion for agents within 10 minutes of creation [2687](https://github.com/zammad/zammad/issues/2687) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[ticket](https://github.com/zammad/zammad/labels/ticket)]
- Allow direct or extention numbers in answeringNumber of CTI API [2797](https://github.com/zammad/zammad/issues/2797) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)]
- Backup permission issue: Could not change directory [2508](https://github.com/zammad/zammad/issues/2508) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)]
- Triggers not present in the permissions list of a role [2608](https://github.com/zammad/zammad/issues/2608) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)]
- Telegram: Can't receive Videos and "too large" files [2289](https://github.com/zammad/zammad/issues/2289) [[bug](https://github.com/zammad/zammad/labels/bug)] [[channel](https://github.com/zammad/zammad/labels/channel)] [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[telegram](https://github.com/zammad/zammad/labels/telegram)]
- Send postmaster email to sender if email is too big and not processed by Zammad [2092](https://github.com/zammad/zammad/issues/2092) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)]
- Enhance postmaster filters to be able ignoring vacation mails (out of office) [2734](https://github.com/zammad/zammad/issues/2734) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)]
- Display only used Channels in Dashboard [1010](https://github.com/zammad/zammad/issues/1010) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)]
- Sometimes elasticsearch takes longer as expected and will raise timeout in Zammad [2720](https://github.com/zammad/zammad/issues/2720) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)]
- Enhance CTI-Caller-Log to "mark as seen" all unread entries at once [2647](https://github.com/zammad/zammad/issues/2647) [[CTI](https://github.com/zammad/zammad/labels/CTI)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[enhancement](https://github.com/zammad/zammad/labels/enhancement)]
- Added new placetel outbound support for call logging. [2703](https://github.com/zammad/zammad/issues/2703) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)]
- No description for Macros [813](https://github.com/zammad/zammad/issues/813) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)]
- Trigger word "attachment" leads to attachment warning pop-up even if it is just in quoted customer text. [2325](https://github.com/zammad/zammad/issues/2325) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[frontend / JS app](https://github.com/zammad/zammad/labels/frontend / JS app)]
**Fixed bugs:**
- Insertion of inline pictures does not work, if you copy an image in your Browser [2645](https://github.com/zammad/zammad/issues/2645) [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[bug](https://github.com/zammad/zammad/labels/bug)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge base)] [[ticket](https://github.com/zammad/zammad/labels/ticket)]
- It seems you're running an external Elasticsearch server on http://localhost:9200 [2400](https://github.com/zammad/zammad/issues/2400) [[bug](https://github.com/zammad/zammad/labels/bug)] [[update](https://github.com/zammad/zammad/labels/update)]
- ticket auto assignment is not working if only one excluded user is defined [2692](https://github.com/zammad/zammad/issues/2692) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Can't add new articles via Macro [542](https://github.com/zammad/zammad/issues/542) [[bug](https://github.com/zammad/zammad/labels/bug)] [[macros](https://github.com/zammad/zammad/labels/macros)]
- Error while fetching email from inbox will block processing of other mails in inbox [2754](https://github.com/zammad/zammad/issues/2754) [[bug](https://github.com/zammad/zammad/labels/bug)] [[mail processing](https://github.com/zammad/zammad/labels/mail processing)]
- Useless display of delete icon below note [2813](https://github.com/zammad/zammad/issues/2813) [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[bug](https://github.com/zammad/zammad/labels/bug)] [[ticket](https://github.com/zammad/zammad/labels/ticket)]
- TimeAccounting modal dialog input not focused [2818](https://github.com/zammad/zammad/issues/2818) [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[bug](https://github.com/zammad/zammad/labels/bug)] [[regression](https://github.com/zammad/zammad/labels/regression)] [[ticket](https://github.com/zammad/zammad/labels/ticket)]
- Datepicker in bulk action not usable in bulk action if ticket list is longer the one page. [2274](https://github.com/zammad/zammad/issues/2274) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend / JS app](https://github.com/zammad/zammad/labels/frontend / JS app)] [[overviews](https://github.com/zammad/zammad/labels/overviews)]
- Array values (e.g. several states) nuke reporting profiles [2809](https://github.com/zammad/zammad/issues/2809) [[blocker](https://github.com/zammad/zammad/labels/blocker)] [[bug](https://github.com/zammad/zammad/labels/bug)] [[regression](https://github.com/zammad/zammad/labels/regression)] [[reporting](https://github.com/zammad/zammad/labels/reporting)]
- Unprocessable mail if follow up search is enabled for attachments with contains invalid charsets (postmaster_follow_up_search_in with attachment) [2808](https://github.com/zammad/zammad/issues/2808) [[bug](https://github.com/zammad/zammad/labels/bug)] [[mail processing](https://github.com/zammad/zammad/labels/mail processing)]
- Admin Settings-> Knowledge base messes up display of other Admin settings [2784](https://github.com/zammad/zammad/issues/2784) [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[bug](https://github.com/zammad/zammad/labels/bug)]
- Zammad ignores disabled Chat-Topics for agents [2441](https://github.com/zammad/zammad/issues/2441) [[bug](https://github.com/zammad/zammad/labels/bug)] [[chat](https://github.com/zammad/zammad/labels/chat)]
- Knowledge Base - Attachments not working for internal readers [2777](https://github.com/zammad/zammad/issues/2777) [[bug](https://github.com/zammad/zammad/labels/bug)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge base)]
- Twitter DMs and Mentions do not arrive [2460](https://github.com/zammad/zammad/issues/2460) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend / JS app](https://github.com/zammad/zammad/labels/frontend / JS app)]
- Image/attachment preview is to small, need to download image and open it locally [2796](https://github.com/zammad/zammad/issues/2796) [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)]
- Reporting does not return all tickets when using values with that "devide" values (e. g. some_value) [2785](https://github.com/zammad/zammad/issues/2785) [[bug](https://github.com/zammad/zammad/labels/bug)] [[reporting](https://github.com/zammad/zammad/labels/reporting)]
- not possible to search or link KB-answer within ticket [2789](https://github.com/zammad/zammad/issues/2789) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend / JS app](https://github.com/zammad/zammad/labels/frontend / JS app)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge base)]
- Wrong dates on synced calendars [2229](https://github.com/zammad/zammad/issues/2229) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend / JS app](https://github.com/zammad/zammad/labels/frontend / JS app)]
- CSV-Import doesn't lookup records by "name" field [2658](https://github.com/zammad/zammad/issues/2658) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)]
- Signature detection fails if no signature range could be found [2740](https://github.com/zammad/zammad/issues/2740) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Can't forward articles containing a <img> tag without src attribute [2701](https://github.com/zammad/zammad/issues/2701) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend / JS app](https://github.com/zammad/zammad/labels/frontend / JS app)]
- Unable to process email where group has option `follow_up_possible: 'new_ticket'` and arraiving email has no subject header [2768](https://github.com/zammad/zammad/issues/2768) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Removing a record drops whole index on Elasticsearch 6 and later [2742](https://github.com/zammad/zammad/issues/2742) [[bug](https://github.com/zammad/zammad/labels/bug)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge base)]
- Can't change Ticket attributes of "Shared Organization" Ticket [2750](https://github.com/zammad/zammad/issues/2750) [[bug](https://github.com/zammad/zammad/labels/bug)]
- SAML authentication [2741](https://github.com/zammad/zammad/issues/2741) [[authentication](https://github.com/zammad/zammad/labels/authentication)]
- Fixes #2699 - Excel export of report or time accounting drops additional fields in row if integer field contain `null/nil/undefined`. [2699](https://github.com/zammad/zammad/issues/2699) [[bug](https://github.com/zammad/zammad/labels/bug)]
- System mail notification subjects ending with =0A [2726](https://github.com/zammad/zammad/issues/2726) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Default value not set for attributes of type input, select, tree_select, richtext, textarea, checkbox [1653](https://github.com/zammad/zammad/issues/1653) [[bug](https://github.com/zammad/zammad/labels/bug)] [[object manager attribute](https://github.com/zammad/zammad/labels/object manager attribute)]
- Gitlab Login not working after upgrade to Zammad 3.x [2595](https://github.com/zammad/zammad/issues/2595) [[authentication](https://github.com/zammad/zammad/labels/authentication)] [[bug](https://github.com/zammad/zammad/labels/bug)]
- Registering of Twitter App fails for Twitter App with existing webhooks [2730](https://github.com/zammad/zammad/issues/2730) [[admin area](https://github.com/zammad/zammad/labels/admin area)] [[bug](https://github.com/zammad/zammad/labels/bug)] [[channel](https://github.com/zammad/zammad/labels/channel)]
- No owner set for Tickets imported from Zendesk [2702](https://github.com/zammad/zammad/issues/2702) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)]
- Zendesk import doesn't consider all tickets [2694](https://github.com/zammad/zammad/issues/2694) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)]
- Zendesk import fails for deleted tickets [1161](https://github.com/zammad/zammad/issues/1161) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)]
- Zendesk import fails due import of (too long) ticket description as (unused) note [2538](https://github.com/zammad/zammad/issues/2538) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)]
- Put _type, _doc and _id to reserved words for object managaer becasue of elasticssearch internal use [2728](https://github.com/zammad/zammad/issues/2728) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Quick text launched in text editor even if new line is between :: [2710](https://github.com/zammad/zammad/issues/2710) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend / JS app](https://github.com/zammad/zammad/labels/frontend / JS app)]
- Unable to process mail with enabled follow up detection in content of attachments [2723](https://github.com/zammad/zammad/issues/2723) [[bug](https://github.com/zammad/zammad/labels/bug)]
- mail processing impossible: invalid byte sequence in UTF-8 [2722](https://github.com/zammad/zammad/issues/2722) [[bug](https://github.com/zammad/zammad/labels/bug)] [[mail processing](https://github.com/zammad/zammad/labels/mail processing)]
- Unable to set owner via `X-Zammad-Ticket-owner` header [2719](https://github.com/zammad/zammad/issues/2719) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Chat header's height is too small [2596](https://github.com/zammad/zammad/issues/2596) [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[bug](https://github.com/zammad/zammad/labels/bug)] [[chat](https://github.com/zammad/zammad/labels/chat)]
- Inconsistency in "follow up" strings [2661](https://github.com/zammad/zammad/issues/2661) [[bug](https://github.com/zammad/zammad/labels/bug)] [[translation](https://github.com/zammad/zammad/labels/translation)]
- Limit on copy and paste in note box for new articles [2696](https://github.com/zammad/zammad/issues/2696) [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend / JS app](https://github.com/zammad/zammad/labels/frontend / JS app)] [[ticket](https://github.com/zammad/zammad/labels/ticket)]
- Telegram not possible to close ticket via `/end` [2695](https://github.com/zammad/zammad/issues/2695) [[bug](https://github.com/zammad/zammad/labels/bug)] [[channel](https://github.com/zammad/zammad/labels/channel)] [[telegram](https://github.com/zammad/zammad/labels/telegram)]
- clickable URL (template) for ObjectManager Attribute [2103](https://github.com/zammad/zammad/issues/2103) [[feature backlog](https://github.com/zammad/zammad/labels/feature backlog)] [[object manager attribute](https://github.com/zammad/zammad/labels/object manager attribute)]
- Time zone incorrectly calculated in ticket list [2697](https://github.com/zammad/zammad/issues/2697) [[bug](https://github.com/zammad/zammad/labels/bug)]
- ObjectManager::Attribute names collide with reserved application words ( e.g. `url`) and break functionalities [2664](https://github.com/zammad/zammad/issues/2664) [[bug](https://github.com/zammad/zammad/labels/bug)] [[object manager attribute](https://github.com/zammad/zammad/labels/object manager attribute)]
- Zammad ignores group mapping on Telegram if same sending user [2379](https://github.com/zammad/zammad/issues/2379) [[bug](https://github.com/zammad/zammad/labels/bug)] [[channel](https://github.com/zammad/zammad/labels/channel)]
- Linking of external account in profile is broken [2675](https://github.com/zammad/zammad/issues/2675) [[authentication](https://github.com/zammad/zammad/labels/authentication)] [[bug](https://github.com/zammad/zammad/labels/bug)] [[personal settings/menu](https://github.com/zammad/zammad/labels/personal settings/menu)]
- Scheduler/Job with current_user in condition is not working [2685](https://github.com/zammad/zammad/issues/2685) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Issue2541FixNotificationEmailWithoutBody database migration blocks update when records with `current_user.id` in selector exist [2663](https://github.com/zammad/zammad/issues/2663) [[bug](https://github.com/zammad/zammad/labels/bug)] [[update](https://github.com/zammad/zammad/labels/update)]
- Zammad logs permanently to STDOUT on 3.1 [2660](https://github.com/zammad/zammad/issues/2660) [[bug](https://github.com/zammad/zammad/labels/bug)]
- Last line in note is not selectable [2654](https://github.com/zammad/zammad/issues/2654) [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[bug](https://github.com/zammad/zammad/labels/bug)] [[ticket](https://github.com/zammad/zammad/labels/ticket)]
- Services die with: ThreadError: can't create Thread: Resource temporarily unavailable [2656](https://github.com/zammad/zammad/issues/2656) [[bug](https://github.com/zammad/zammad/labels/bug)] [[third party issue](https://github.com/zammad/zammad/labels/third party issue)] [[waiting for feedback](https://github.com/zammad/zammad/labels/waiting for feedback)]

View File

@@ -2,7 +2,7 @@ source 'https://rubygems.org'
# core - base
ruby '2.5.5'
gem 'rails', '5.2.3'
gem 'rails', '5.2.4.1'
# core - rails additions
gem 'activerecord-import'
@@ -13,7 +13,7 @@ gem 'json'
gem 'rails-observers'
# core - application servers
gem 'puma', group: :puma
gem 'puma', '~>3.12', group: :puma
gem 'unicorn', group: :unicorn
# core - supported ORMs
@@ -49,7 +49,7 @@ gem 'eco'
gem 'sassc-rails'
# asset handling - pipeline
gem 'sprockets'
gem 'sprockets', '~> 3.7.2'
gem 'uglifier'
gem 'autoprefixer-rails'

View File

@@ -49,37 +49,37 @@ GEM
specs:
aasm (5.0.0)
concurrent-ruby (~> 1.0)
actioncable (5.2.3)
actionpack (= 5.2.3)
actioncable (5.2.4.1)
actionpack (= 5.2.4.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailer (5.2.3)
actionpack (= 5.2.3)
actionview (= 5.2.3)
activejob (= 5.2.3)
actionmailer (5.2.4.1)
actionpack (= 5.2.4.1)
actionview (= 5.2.4.1)
activejob (= 5.2.4.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.2.3)
actionview (= 5.2.3)
activesupport (= 5.2.3)
rack (~> 2.0)
actionpack (5.2.4.1)
actionview (= 5.2.4.1)
activesupport (= 5.2.4.1)
rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.2.3)
activesupport (= 5.2.3)
actionview (5.2.4.1)
activesupport (= 5.2.4.1)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
activejob (5.2.3)
activesupport (= 5.2.3)
activejob (5.2.4.1)
activesupport (= 5.2.4.1)
globalid (>= 0.3.6)
activemodel (5.2.3)
activesupport (= 5.2.3)
activerecord (5.2.3)
activemodel (= 5.2.3)
activesupport (= 5.2.3)
activemodel (5.2.4.1)
activesupport (= 5.2.4.1)
activerecord (5.2.4.1)
activemodel (= 5.2.4.1)
activesupport (= 5.2.4.1)
arel (>= 9.0)
activerecord-import (1.0.2)
activerecord (>= 3.2)
@@ -91,11 +91,11 @@ GEM
multi_json (~> 1.11, >= 1.11.2)
rack (>= 1.5.2, < 3)
railties (>= 4.0)
activestorage (5.2.3)
actionpack (= 5.2.3)
activerecord (= 5.2.3)
activestorage (5.2.4.1)
actionpack (= 5.2.4.1)
activerecord (= 5.2.4.1)
marcel (~> 0.3.1)
activesupport (5.2.3)
activesupport (5.2.4.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
@@ -120,7 +120,7 @@ GEM
msgpack (~> 1.0)
browser (2.5.3)
buftok (0.2.0)
builder (3.2.3)
builder (3.2.4)
byebug (11.0.1)
capybara (3.21.0)
addressable
@@ -185,7 +185,7 @@ GEM
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
equalizer (0.0.11)
erubi (1.8.0)
erubi (1.9.0)
eventmachine (1.2.7)
execjs (2.7.0)
factory_bot (5.0.2)
@@ -245,7 +245,7 @@ GEM
http-form_data (2.1.1)
http_parser.rb (0.6.0)
httpclient (2.8.3)
i18n (1.6.0)
i18n (1.7.0)
concurrent-ruby (~> 1.0)
icalendar (2.5.3)
ice_cube (~> 0.16)
@@ -272,7 +272,7 @@ GEM
logging (2.2.2)
little-plugger (~> 1.1)
multi_json (~> 1.10)
loofah (2.3.1)
loofah (2.4.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
lumberjack (1.0.13)
@@ -285,11 +285,11 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2019.0331)
mimemagic (0.3.3)
mini_mime (1.0.1)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
mini_racer (0.2.4)
libv8 (>= 6.3)
minitest (5.11.3)
minitest (5.13.0)
msgpack (1.2.4)
multi_json (1.13.1)
multi_xml (0.6.0)
@@ -300,8 +300,8 @@ GEM
nestful (1.1.3)
net-ldap (0.16.1)
netrc (0.11.0)
nio4r (2.3.1)
nokogiri (1.10.5)
nio4r (2.5.2)
nokogiri (1.10.8)
mini_portile2 (~> 2.4.0)
nori (2.6.0)
notiffany (0.1.1)
@@ -377,24 +377,24 @@ GEM
binding_of_caller (>= 0.7)
pry (>= 0.9.11)
public_suffix (3.0.3)
puma (3.12.1)
rack (2.0.7)
puma (3.12.4)
rack (2.0.8)
rack-livereload (0.3.17)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (5.2.3)
actioncable (= 5.2.3)
actionmailer (= 5.2.3)
actionpack (= 5.2.3)
actionview (= 5.2.3)
activejob (= 5.2.3)
activemodel (= 5.2.3)
activerecord (= 5.2.3)
activestorage (= 5.2.3)
activesupport (= 5.2.3)
rails (5.2.4.1)
actioncable (= 5.2.4.1)
actionmailer (= 5.2.4.1)
actionpack (= 5.2.4.1)
actionview (= 5.2.4.1)
activejob (= 5.2.4.1)
activemodel (= 5.2.4.1)
activerecord (= 5.2.4.1)
activestorage (= 5.2.4.1)
activesupport (= 5.2.4.1)
bundler (>= 1.3.0)
railties (= 5.2.3)
railties (= 5.2.4.1)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.4)
actionpack (>= 5.0.1.x)
@@ -403,19 +403,19 @@ GEM
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.0.4)
loofah (~> 2.2, >= 2.2.2)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
rails-observers (0.1.5)
activemodel (>= 4.0)
railties (5.2.3)
actionpack (= 5.2.3)
activesupport (= 5.2.3)
railties (5.2.4.1)
actionpack (= 5.2.4.1)
activesupport (= 5.2.4.1)
method_source
rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0)
rainbow (3.0.0)
raindrops (0.19.0)
rake (12.3.2)
rake (12.3.3)
rb-fsevent (0.10.3)
rb-inotify (0.10.0)
ffi (~> 1.0)
@@ -511,7 +511,7 @@ GEM
tins (~> 1.0)
test-unit (3.3.3)
power_assert
thor (0.20.3)
thor (1.0.1)
thread_safe (0.3.6)
tilt (2.0.9)
tins (1.20.2)
@@ -624,9 +624,9 @@ DEPENDENCIES
pry-remote
pry-rescue
pry-stack_explorer
puma
puma (~> 3.12)
rack-livereload
rails (= 5.2.3)
rails (= 5.2.4.1)
rails-controller-testing
rails-observers
rb-fsevent
@@ -647,7 +647,7 @@ DEPENDENCIES
spring
spring-commands-rspec
spring-commands-testunit
sprockets
sprockets (~> 3.7.2)
tcr!
telegramAPI
telephone_number

View File

@@ -51,7 +51,7 @@ class App.ManageKnowledgeBase extends App.ControllerTabs
release: ->
super
@modal.el.remove()
@modal?.el.remove()
processLoaded: ->
if @knowledge_base_id

View File

@@ -17,6 +17,7 @@ class App.CustomerChat extends App.Controller
constructor: ->
super
@popovers = []
@chatWindows = {}
@maxChatWindows = 4
preferences = @Session.get('preferences')
@@ -221,7 +222,12 @@ class App.CustomerChat extends App.Controller
chats
updateMeta: =>
$('.popover').remove()
# clear old popovers
for popover in @popovers
popover.popover('destroy')
@popovers = []
activeChatTopcis = @activeChatTopcis()
@$('.js-header').html(App.view('customer_chat/chat_header')(chats: activeChatTopcis))
@refreshElements()
@@ -244,7 +250,7 @@ class App.CustomerChat extends App.Controller
for chat in App.Chat.all()
do (chat) =>
@$(".js-header .js-waitingCustomers[data-chat-id=#{chat.id}] .js-badgeWaitingCustomers").text(@meta.waiting_chat_count_by_chat[chat.id])
@el.find(".js-waitingCustomers[data-chat-id=#{chat.id}] .js-info").popover(
@popovers.push @el.find(".js-waitingCustomers[data-chat-id=#{chat.id}] .js-info").popover(
trigger: 'hover'
html: true
animation: false
@@ -258,7 +264,7 @@ class App.CustomerChat extends App.Controller
)
else
@badgeWaitingCustomers.text(@meta.waiting_chat_count)
@el.find('.js-waitingCustomers .js-totalInfo').popover(
@popovers.push @el.find('.js-waitingCustomers .js-totalInfo').popover(
trigger: 'hover'
html: true
animation: false
@@ -280,7 +286,7 @@ class App.CustomerChat extends App.Controller
@addChat(session)
@meta.active_sessions = false
@el.find('.js-chattingCustomers .js-info').popover(
@popovers.push @el.find('.js-chattingCustomers .js-info').popover(
trigger: 'hover'
html: true
animation: false
@@ -293,7 +299,7 @@ class App.CustomerChat extends App.Controller
@chatSessionList(@meta.running_chat_session_list)
)
@el.find('.js-activeAgents .js-info').popover(
@popovers.push @el.find('.js-activeAgents .js-info').popover(
trigger: 'hover'
html: true
animation: false

View File

@@ -58,22 +58,13 @@ class Index extends App.ControllerContent
)
success: (data) =>
if data.message is 'ok'
# if in developer mode, redirect to set new password
if data.token && @Config.get('developer_mode') is true
redirect = =>
@navigate "#password_reset_verify/#{data.token}"
@delay(redirect, 2000)
@render(sent: true)
else
@$('[name=username]').val('')
@notify(
type: 'error'
msg: App.i18n.translateContent('Username or email address invalid, please try again.')
)
@formEnable( @el.find('.form-password') )
# if in developer mode, redirect to set new password
if data.token && @Config.get('developer_mode') is true
redirect = =>
@navigate "#password_reset_verify/#{data.token}"
@delay(redirect, 2000)
@render(sent: true)
App.Config.set('password_reset', Index, 'Routes')

View File

@@ -1005,9 +1005,12 @@ class App.TicketZoom extends App.Controller
# reset task state
taskReset: =>
@form_id = App.ControllerForm.formId()
@articleNew.form_id = @form_id
@articleNew.render()
@articleView.updateFormId(@form_id)
@localTaskData =
ticket: {}
article: {}

View File

@@ -132,7 +132,10 @@ class App.TicketZoomArticleNew extends App.Controller
textRange.select()
isIE10: ->
Function('/*@cc_on return document.documentMode===10@*/')()
detected = App.Browser.detection()
return false if !detected.browser
return false if detected.browser.name != 'Explorer'
return detected.browser.major == 10
release: =>
if @subscribeIdTextModule

View File

@@ -60,6 +60,9 @@ class App.TicketZoomArticleView extends App.Controller
false
updateFormId: (newFormId) ->
for id, viewItem of @articleController
viewItem.updateFormId(newFormId)
class ArticleViewItem extends App.ObserverController
model: 'TicketArticle'
@@ -131,7 +134,12 @@ class ArticleViewItem extends App.ObserverController
attachments = App.TicketArticle.contentAttachments(article)
if article.attachments
for attachment in article.attachments
attachment.url = "#{App.Config.get('api_path')}/ticket_attachment/#{article.ticket_id}/#{article.id}/#{attachment.id}?disposition=attachment"
dispositionParams = ''
if attachment?.preferences['Content-Type'] isnt 'application/pdf' && attachment?.preferences['Content-Type'] isnt 'text/html'
dispositionParams = '?disposition=attachment'
attachment.url = "#{App.Config.get('api_path')}/ticket_attachment/#{article.ticket_id}/#{article.id}/#{attachment.id}#{dispositionParams}"
attachment.preview_url = "#{App.Config.get('api_path')}/ticket_attachment/#{article.ticket_id}/#{article.id}/#{attachment.id}?view=preview"
if attachment && attachment.preferences && attachment.preferences['original-format'] is true
@@ -205,7 +213,7 @@ class ArticleViewItem extends App.ObserverController
size: 40
)
new App.TicketZoomArticleActions(
@articleActions = new App.TicketZoomArticleActions(
el: @$('.js-article-actions')
ticket: @ticket
article: article
@@ -425,3 +433,6 @@ class ArticleViewItem extends App.ObserverController
e.preventDefault()
e.stopPropagation()
new App.TicketZoomArticleImageView(image: $(e.target).get(0).outerHTML, parentElement: $(e.currentTarget))
updateFormId: (newFormId) ->
@articleActions?.form_id = newFormId

View File

@@ -47,6 +47,33 @@ class Widget extends App.Controller
# enable translation inline
App.Config.set('translation_inline', true)
@observer = new MutationObserver((mutations) ->
mutations.forEach((mutation) ->
mutation.addedNodes.forEach((addedNode) ->
$(addedNode).find('span.translation').on('click.translation', (e) ->
e.stopPropagation()
return false
)
$(addedNode).find('span.translation').on('keydown.translation', (e) ->
e.stopPropagation()
return true
)
)
mutation.removedNodes.forEach((removedNode) ->
$(removedNode).find('span.translation').off('.translation')
)
)
)
@observer.observe(document.body, {
subtree: true,
childList: true,
})
# rerender controllers
App.Event.trigger('ui:rerender')
@@ -93,7 +120,9 @@ class Widget extends App.Controller
element
disable: ->
$('body').off('focus.translation blur.translation')
@observer.disconnect()
$('body').off('.translation')
# disable translation inline
App.Config.set('translation_inline', false)

View File

@@ -236,7 +236,7 @@ class _i18nSingleton extends Spine.Module
return string if !string
if App.Config.get('translation_inline')
return '<span class="translation" onkeydown="arguments[0].stopPropagation(); return true" onclick="arguments[0].stopPropagation(); return false" contenteditable="true" title="' + App.Utils.htmlEscape(string) + '">' + App.Utils.htmlEscape(@translate(string)) + '</span>'
return '<span class="translation" contenteditable="true" title="' + App.Utils.htmlEscape(string) + '">' + App.Utils.htmlEscape(@translate(string)) + '</span>'
translated = @translate(string, args, true, true)

View File

@@ -679,7 +679,8 @@ class App.Utils
$('<div/>').html(message).contents().each (index, node) ->
text = $(node).text()
if node.nodeType == Node.TEXT_NODE
res.push text
# convert text back to HTML as it was before
res.push $('<div>').text(text).html()
if text.trim().length
contentNodes.push index
else if node.nodeType == Node.ELEMENT_NODE

View File

@@ -193,6 +193,7 @@ App.ViewHelpers =
canDownload: (contentType) ->
contentType = App.Utils.contentTypeCleanup(contentType)
return false if contentType is 'application/pdf'
contentType != 'text/html'
canPreview: (contentType) ->

View File

@@ -1,5 +1,5 @@
class App.Chat extends App.Model
@configure 'Chat', 'name', 'active', 'public', 'max_queue', 'block_ip', 'block_country', 'note'
@configure 'Chat', 'name', 'active', 'public', 'max_queue', 'block_ip', 'whitelisted_websites', 'block_country', 'note'
@extend Spine.Model.Ajax
@url: @apiPath + '/chats'
@countries:
@@ -250,6 +250,7 @@ class App.Chat extends App.Model
{ name: 'note', display: 'Note', tag: 'textarea', limit: 250, null: true },
{ name: 'max_queue', display: 'Max. clients in waitlist', tag: 'input', default: 2 },
{ name: 'block_ip', display: 'Blocked IPs (separated by ;)', tag: 'input', default: '', null: true },
{ name: 'whitelisted_websites', display: 'Allow websites (separated by ;)', tag: 'input', default: '', null: true },
{ name: 'block_country', display: 'Blocked countries', tag: 'column_select', multiple: true, null: true, default: '', options: @countries, seperator: ';' },
{ name: 'active', display: 'Active', tag: 'active', default: true },
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },

View File

@@ -4,7 +4,10 @@
<% if @sent: %>
<h2><%- @T('We\'ve sent password reset instructions to your email address.') %><small></small></h2>
<p><%- @T('If you don\'t receive instructions within a minute or two, check your email\'s spam and junk filters, or try resending your request.') %></p>
<a href="#" class="subtle retry">&raquo; <%- @T('again') %> &laquo;</a>
<div class="form-controls">
<a class="btn btn--text btn--subtle js-cancel" href="#login"><%- @T( 'Cancel & Go Back' ) %></a>
<button class="btn btn--primary retry align-right"><%- @T( 'again' ) %></button>
</div>
<% else: %>
<h2><%- @T( 'Forgot your password?' ) %><small></small></h2>
<form class="form-password">

View File

@@ -1,3 +1,3 @@
<small class="task-subline">
<%- @C('ticket_hook') %> <span class="ticket-number js-objectNumber" data-number="<%- @C('ticket_hook') %><%- @ticket.number %>"><%- @ticket.number %></span> - <%- @T('created') %> <%- @humanTime(@ticket.created_at) %> <% if !@isCustomer && @ticket.escalation_at: %> - <%- @T('escalation') %> <%- @humanTime(@ticket.escalation_at, true) %><% end %>
</small>
<%- @C('ticket_hook') %> <span class="ticket-number js-objectNumber" data-number="<%- @C('ticket_hook') %><%= @ticket.number %>"><%= @ticket.number %></span> - <%- @T('created') %> <%- @humanTime(@ticket.created_at) %> <% if !@isCustomer && @ticket.escalation_at: %> - <%- @T('escalation') %> <%- @humanTime(@ticket.escalation_at, true) %><% end %>
</small>

View File

@@ -10,6 +10,7 @@ class ApplicationController < ActionController::Base
include ApplicationController::HasUser
include ApplicationController::HasResponseExtentions
include ApplicationController::PreventsCsrf
include ApplicationController::HasSecureContentSecurityPolicyForDownloads
include ApplicationController::LogsHttpAccess
include ApplicationController::ChecksAccess
end

View File

@@ -75,8 +75,18 @@ module ApplicationController::HandlesErrors
data[:error_human] = data[:error]
end
if Rails.env.production? && data[:error_human].present?
data[:error] = data.delete(:error_human)
if Rails.env.production?
if data[:error_human].present?
data[:error] = data.delete(:error_human)
else
# We want to avoid leaking of internal information but also want the user
# to give the administrator a reference to find the cause of the error.
# Therefore we generate a one time unique error ID that can be used to
# search the logs and find the actual error message.
error_code_prefix = "Error ID #{SecureRandom.urlsafe_base64(6)}:"
Rails.logger.error "#{error_code_prefix} #{data[:error]}"
data[:error] = "#{error_code_prefix} Please contact your administrator."
end
end
data
end

View File

@@ -0,0 +1,23 @@
module ApplicationController::HasSecureContentSecurityPolicyForDownloads
extend ActiveSupport::Concern
included do
around_action do |_controller, block|
subscriber = proc do
policy = ActionDispatch::ContentSecurityPolicy.new
policy.default_src :none
policy.plugin_types 'application/pdf'
request.content_security_policy = policy
end
ActiveSupport::Notifications.subscribed(subscriber, 'send_file.action_controller') do
ActiveSupport::Notifications.subscribed(subscriber, 'send_data.action_controller') do
block.call
end
end
end
end
end

View File

@@ -3,7 +3,7 @@ module ApplicationController::SetsHeaders
included do
before_action :cors_preflight_check
after_action :set_access_control_headers
after_action :set_access_control_headers, :set_cache_control_headers
end
private
@@ -22,23 +22,25 @@ module ApplicationController::SetsHeaders
headers['Access-Control-Allow-Headers'] = 'Content-Type, Depth, User-Agent, X-File-Size, X-Requested-With, If-Modified-Since, X-File-Name, Cache-Control, Accept-Language'
end
def set_cache_control_headers
# by default http cache is disabled
# expires_now function only sets no-cache so we handle the headers by our own.
headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
headers['Pragma'] = 'no-cache'
headers['Expires'] = '-1'
end
# If this is a preflight OPTIONS request, then short-circuit the
# request, return only the necessary headers and return an empty
# text/plain.
def cors_preflight_check
return true if @_auth_type != 'token_auth' && @_auth_type != 'basic_auth'
cors_preflight_check_execute
end
def cors_preflight_check_execute
return true if request.method != 'OPTIONS'
return if request.method != 'OPTIONS'
headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, DELETE, PATCH, OPTIONS'
headers['Access-Control-Allow-Headers'] = 'Content-Type, Depth, User-Agent, X-File-Size, X-Requested-With, If-Modified-Since, X-File-Name, Cache-Control, Accept-Language'
headers['Access-Control-Max-Age'] = '1728000'
render text: '', content_type: 'text/plain'
false
render plain: ''
end
end

View File

@@ -2,7 +2,7 @@
class FormController < ApplicationController
skip_before_action :verify_csrf_token
before_action :cors_preflight_check_execute
before_action :cors_preflight_check
after_action :set_access_control_headers_execute
skip_before_action :user_device_check

View File

@@ -9,29 +9,12 @@ class TestsController < ApplicationController
render json: result
end
# GET /test/unprocessable_entity
def error_unprocessable_entity
raise Exceptions::UnprocessableEntity, 'some error message'
end
# GET /test/raised_exception
def error_raised_exception
exception = params.fetch(:exception, 'StandardError')
message = params.fetch(:message, 'no message provided')
# GET /test/not_authorized
def error_not_authorized
raise Exceptions::NotAuthorized, 'some error message'
end
# GET /test/ar_not_found
def error_ar_not_found
raise ActiveRecord::RecordNotFound, 'some error message'
end
# GET /test/standard_error
def error_standard_error
raise StandardError, 'some error message'
end
# GET /test/argument_error
def error_argument_error
raise ArgumentError, 'some error message'
raise exception.safe_constantize, message
end
end

View File

@@ -281,8 +281,9 @@ class TicketsController < ApplicationController
# return result
result = Ticket::ScreenOptions.list_by_customer(
customer_id: params[:customer_id],
limit: 15,
current_user: current_user,
customer_id: params[:customer_id],
limit: 15,
)
render json: result
end

View File

@@ -534,9 +534,12 @@ curl http://localhost/api/v1/users/email_verify_send -v -u #{login}:#{password}
raise Exceptions::UnprocessableEntity, 'No email!' if !params[:email]
# check is verify is possible to send
user = User.find_by(email: params[:email].downcase)
raise Exceptions::UnprocessableEntity, 'No such user!' if !user
if !user
# result is always positive to avoid leaking of existing user accounts
render json: { message: 'ok' }, status: :ok
return
end
#if user.verified == true
# render json: { error: 'Already verified!' }, status: :unprocessable_entity
@@ -610,14 +613,10 @@ curl http://localhost/api/v1/users/password_reset -v -u #{login}:#{password} -H
render json: { message: 'ok', token: result[:token].name }, status: :ok
return
end
# token sent to user, send ok to browser
render json: { message: 'ok' }, status: :ok
return
end
# unable to generate token
render json: { message: 'failed' }, status: :ok
# result is always positive to avoid leaking of existing user accounts
render json: { message: 'ok' }, status: :ok
end
=begin

View File

@@ -236,9 +236,7 @@ returns
def filter_attributes(attributes)
# remove forbidden attributes
%w[password token tokens token_ids].each do |item|
attributes.delete(item)
end
attributes.except!('password', 'token', 'tokens', 'token_ids')
end
=begin

View File

@@ -234,9 +234,10 @@ example
NOTICE
Rails.logger.error "Net::IMAP failed to parse message #{message_id}: #{e.message} (#{e.class})"
Rails.logger.error '(See https://github.com/zammad/zammad/issues/2754 for more details)'
next
end
next if message_meta.nil?
# ignore verify messages
next if !messages_is_too_old_verify?(message_meta, count, count_all)

View File

@@ -617,6 +617,23 @@ check if ip address is blocked for chat
=begin
check if website is allowed for chat
chat = Chat.find(123)
chat.website_whitelisted?('zammad.org')
=end
def website_whitelisted?(website)
return true if whitelisted_websites.blank?
whitelisted_websites.split(';').any? do |whitelisted_website|
website.downcase.include?(whitelisted_website.downcase.strip)
end
end
=begin
check if country is blocked for chat
chat = Chat.find(123)
@@ -633,10 +650,9 @@ check if country is blocked for chat
return false if geo_ip['country_code'].blank?
countries = block_country.split(';')
countries.each do |local_country|
return true if geo_ip['country_code'] == local_country
countries.any? do |local_country|
geo_ip['country_code'] == local_country
end
false
end
end

View File

@@ -897,34 +897,49 @@ is certain attribute used by triggers, overviews or schedulers
def check_name
return if !name
raise 'Name can\'t get used, *_id and *_ids are not allowed' if name.match?(/.+?_(id|ids)$/i)
raise 'Spaces in name are not allowed' if name.match?(/\s/)
raise 'Only letters from a-z, numbers from 0-9, and _ are allowed' if !name.match?(/^[a-z0-9_]+$/)
raise 'At least one letters is needed' if !name.match?(/[a-z]/)
if name.match?(/.+?_(id|ids)$/i)
errors.add(:name, "can't get used because *_id and *_ids are not allowed")
end
if name.match?(/\s/)
errors.add(:name, 'spaces are not allowed')
end
if !name.match?(/^[a-z0-9_]+$/)
errors.add(:name, 'Only letters from a-z because numbers from 0-9 and _ are allowed')
end
if !name.match?(/[a-z]/)
errors.add(:name, 'At least one letters is needed')
end
# do not allow model method names as attributes
reserved_words = %w[destroy true false integer select drop create alter index table varchar blob date datetime timestamp url icon initials avatar permission validate subscribe unsubscribe translate search _type _doc _id id]
raise "#{name} is a reserved word, please choose a different one" if name.match?(/^(#{reserved_words.join('|')})$/)
if name.match?(/^(#{reserved_words.join('|')})$/)
errors.add(:name, "#{name} is a reserved word! (1)")
end
# fixes issue #2236 - Naming an attribute "attribute" causes ActiveRecord failure
begin
ObjectLookup.by_id(object_lookup_id).constantize.instance_method_already_implemented? name
rescue ActiveRecord::DangerousAttributeError
raise "#{name} is a reserved word, please choose a different one"
errors.add(:name, "#{name} is a reserved word! (2)")
end
record = object_lookup.name.constantize.new
return true if !record.respond_to?(name.to_sym)
raise "#{name} already exists!" if record.attributes.key?(name) && new_record?
return true if record.attributes.key?(name)
if record.respond_to?(name.to_sym) && record.attributes.key?(name) && new_record?
errors.add(:name, "#{name} already exists!")
end
raise "#{name} is a reserved word, please choose a different one"
if errors.present?
raise ActiveRecord::RecordInvalid, self
end
true
end
def check_editable
return if editable
raise 'Attribute not editable!'
errors.add(:name, 'Attribute not editable!')
raise ActiveRecord::RecordInvalid, self
end
private

View File

@@ -180,11 +180,16 @@ returns
state_id_list_open = Ticket::State.by_category(:open).pluck(:id)
state_id_list_closed = Ticket::State.by_category(:closed).pluck(:id)
# open tickets by customer
access_condition = Ticket.access_condition(data[:current_user], 'read')
# get tickets
tickets_open = Ticket.where(
customer_id: data[:customer_id],
state_id: state_id_list_open
).limit(data[:limit] || 15).order(created_at: :desc)
)
.where(access_condition)
.limit(data[:limit] || 15).order(created_at: :desc)
assets = {}
ticket_ids_open = []
tickets_open.each do |ticket|
@@ -195,7 +200,9 @@ returns
tickets_closed = Ticket.where(
customer_id: data[:customer_id],
state_id: state_id_list_closed
).limit(data[:limit] || 15).order(created_at: :desc)
)
.where(access_condition)
.limit(data[:limit] || 15).order(created_at: :desc)
ticket_ids_closed = []
tickets_closed.each do |ticket|
ticket_ids_closed.push ticket.id

View File

@@ -1,5 +1,5 @@
<!-- svgstore fallback -->
<script>
<%= javascript_tag nonce: true do -%>
/*
detect if browser is
- Chrome 14-20
@@ -27,9 +27,9 @@
xhr.open('get', 'assets/images/icons.svg', true)
xhr.send()
})(document)
</script>
<% end -%>
<div id="app"></div>
<div class="splash">
<svg class="icon icon-logo"><use xlink:href="assets/images/icons.svg#icon-logo" /></svg>
<div class="splash-title">Loading...</div>
</div>
</div>

View File

@@ -8,9 +8,9 @@
<%= stylesheet_link_tag "application", :media => 'all' %>
<%= stylesheet_link_tag "application-print", :media => 'print' %>
<% if Rails.configuration.assets.debug %>
<%= javascript_include_tag "application" %>
<%= javascript_include_tag "application", nonce: true %>
<% else %>
<%= javascript_include_tag "application", :defer => 'defer' %>
<%= javascript_include_tag "application", nonce: true, :defer => 'defer' %>
<% end %>
<%= csrf_meta_tags %>
</head>

View File

@@ -70,10 +70,13 @@
</footer>
</div>
<script type='text/javascript'>
<%= javascript_tag nonce: true do -%>
if(window.fetch === undefined || window.Promise === undefined || Element.prototype.prepend === undefined){
document.write('<script src="<%= path_to_javascript('knowledge_base_public_polyfills') %>"><\/script>')
var polyfillScriptTag = document.createElement('script');
polyfillScriptTag.setAttribute('src', '<%= path_to_javascript('knowledge_base_public_polyfills') %>');
polyfillScriptTag.setAttribute('nonce', '<%= content_security_policy_nonce %>');
document.body.appendChild(polyfillScriptTag);
}
</script>
<% end -%>
<%= javascript_include_tag 'knowledge_base_public' %>
<%= javascript_include_tag 'knowledge_base_public', nonce: true %>

View File

@@ -1,7 +1,5 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/core.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/core.js", nonce: true %>
<style type="text/css">
body {
@@ -9,8 +7,8 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>

View File

@@ -1,7 +1,6 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/form.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/form.js", nonce: true %>
<style type="text/css">
body {
@@ -9,8 +8,8 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>

View File

@@ -1,7 +1,6 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/form_column_select.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/form_column_select.js", nonce: true %>
<style type="text/css">
body {
@@ -9,8 +8,8 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>

View File

@@ -1,7 +1,6 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/form_extended.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/form_extended.js", nonce: true %>
<style type="text/css">
body {
@@ -9,8 +8,8 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>

View File

@@ -1,7 +1,6 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/form_find.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/form_find.js", nonce: true %>
<style type="text/css">
body {
@@ -9,8 +8,8 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>

View File

@@ -1,7 +1,6 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/form_searchable_select.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/form_searchable_select.js", nonce: true %>
<style type="text/css">
body {
@@ -9,8 +8,8 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>

View File

@@ -1,7 +1,6 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/form_ticket_perform_action.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/form_ticket_perform_action.js", nonce: true %>
<style type="text/css">
body {
@@ -9,8 +8,8 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>

View File

@@ -1,7 +1,6 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/form_timer.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/form_timer.js", nonce: true %>
<style type="text/css">
body {
@@ -9,8 +8,8 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>

View File

@@ -1,7 +1,6 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/form_tree_select.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/form_tree_select.js", nonce: true %>
<style type="text/css">
body {
@@ -9,8 +8,8 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>

View File

@@ -1,7 +1,6 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/form_trim.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/form_trim.js", nonce: true %>
<style type="text/css">
body {
@@ -9,8 +8,8 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>

View File

@@ -1,7 +1,6 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/form_validation.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/form_validation.js", nonce: true %>
<style type="text/css">
body {
@@ -9,8 +8,8 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>
@@ -19,4 +18,4 @@ body {
<div id="forms"></div>
<button type="submit" class="btn btn-primary submit">Submit</button>
</form>
</div>
</div>

View File

@@ -1,7 +1,6 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/html_utils.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/html_utils.js", nonce: true %>
<style type="text/css">
body {
@@ -9,8 +8,8 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>

View File

@@ -1,7 +1,6 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/local_storage.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/local_storage.js", nonce: true %>
<style type="text/css">
body {
@@ -9,8 +8,8 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>

View File

@@ -1,7 +1,6 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/model.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/model.js", nonce: true %>
<style type="text/css">
body {
@@ -9,8 +8,8 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>

View File

@@ -1,7 +1,6 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/model_binding.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/model_binding.js", nonce: true %>
<style type="text/css">
body {
@@ -9,8 +8,8 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>

View File

@@ -1,7 +1,6 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/model_ticket.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/model_ticket.js", nonce: true %>
<style type="text/css">
body {
@@ -9,8 +8,8 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>

View File

@@ -1,7 +1,6 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/model_ui.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/model_ui.js", nonce: true %>
<style type="text/css">
body {
@@ -9,7 +8,7 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>

View File

@@ -1,7 +1,6 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/session.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/session.js", nonce: true %>
<style type="text/css">
body {
@@ -9,8 +8,8 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>

View File

@@ -1,7 +1,6 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/table.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/table.js", nonce: true %>
<style type="text/css">
body {
@@ -9,9 +8,9 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>
<div id="table"></div>
<div id="table"></div>

View File

@@ -1,7 +1,6 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/table_extended.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/table_extended.js", nonce: true %>
<style type="text/css">
body {
@@ -9,9 +8,9 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>
<div id="table"></div>
<div id="table"></div>

View File

@@ -1,7 +1,6 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/taskbar.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/taskbar.js", nonce: true %>
<style type="text/css">
body {
@@ -9,8 +8,8 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>

View File

@@ -1,7 +1,6 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/text_module.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/text_module.js", nonce: true %>
<style type="text/css">
body {
@@ -9,8 +8,8 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>

View File

@@ -1,7 +1,6 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/ticket_selector.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/ticket_selector.js", nonce: true %>
<style type="text/css">
body {
@@ -9,7 +8,7 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>

View File

@@ -1,7 +1,6 @@
<link rel="stylesheet" href="/assets/tests/qunit-1.21.0.css">
<script src="/assets/tests/qunit-1.21.0.js"></script>
<script src="/assets/tests/ui.js"></script>
<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/ui.js", nonce: true %>
<style type="text/css">
body {
@@ -9,7 +8,7 @@ body {
}
</style>
<script type="text/javascript">
</script>
<%= javascript_tag nonce: true do -%>
<% end -%>
<div id="qunit" class="u-dontfold"></div>

View File

@@ -16,8 +16,29 @@
# # policy.report_uri "/csp-violation-report-endpoint"
# end
Rails.application.config.content_security_policy do |policy|
base_uri = proc do
next if !Rails.env.production?
next if !Setting.get('system_init_done')
http_type = Setting.get('http_type')
fqdn = Setting.get('fqdn')
"#{http_type}://#{fqdn}"
end
policy.base_uri :self, base_uri
policy.default_src :self, :ws, :wss, 'https://log.zammad.com', 'https://images.zammad.com'
policy.font_src :self, :data
policy.img_src '*', :data
policy.object_src :none
policy.script_src :self, :unsafe_eval, :unsafe_inline, :strict_dynamic
policy.style_src :self, :unsafe_inline
end
# If you are using UJS then enable automatic nonce generation
# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
Rails.application.config.content_security_policy_nonce_generator = ->(_request) { SecureRandom.base64(16) }
# Report CSP violations to a specified URI
# For further information see the following documentation:

View File

@@ -6,6 +6,7 @@ Rails.application.routes.draw do
# just remember to delete public/index.html.
root to: 'init#index', via: :get
root to: 'errors#routing', via: %i[post put delete options]
# load routes from external files
dir = File.expand_path(__dir__)
@@ -18,6 +19,5 @@ Rails.application.routes.draw do
end
end
match '*a', to: 'errors#routing', via: %i[get post put delete]
match '*a', to: 'errors#routing', via: %i[get post put delete options]
end

View File

@@ -25,10 +25,6 @@ Zammad::Application.routes.draw do
match '/tests_taskbar', to: 'tests#taskbar', via: :get
match '/tests_text_module', to: 'tests#text_module', via: :get
match '/tests/wait/:sec', to: 'tests#wait', via: :get
match '/tests/unprocessable_entity', to: 'tests#error_unprocessable_entity', via: :get
match '/tests/not_authorized', to: 'tests#error_not_authorized', via: :get
match '/tests/ar_not_found', to: 'tests#error_ar_not_found', via: :get
match '/tests/standard_error', to: 'tests#error_standard_error', via: :get
match '/tests/argument_error', to: 'tests#error_argument_error', via: :get
match '/tests/raised_exception', to: 'tests#error_raised_exception', via: :get
end

View File

@@ -6,6 +6,9 @@
# replace 'localhost' with your fqdn if you want to use zammad from remote
ServerName localhost
# security - prevent information disclosure about server version
ServerTokens Prod
## don't loose time with IP address lookups
HostnameLookups Off

View File

@@ -9,6 +9,10 @@
<VirtualHost *:80>
ServerName example.com
# security - prevent information disclosure about server version
ServerTokens Prod
Redirect permanent / https://example.com
</VirtualHost>
@@ -25,6 +29,9 @@
# replace 'localhost' with your fqdn if you want to use zammad from remote
ServerName localhost
# security - prevent information disclosure about server version
ServerTokens Prod
## don't loose time with IP address lookups
HostnameLookups Off
@@ -46,7 +53,7 @@
ProxyPass /robots.txt !
ProxyPass /ws ws://127.0.0.1:6042/
ProxyPass / http://127.0.0.1:3000/
# Use settings below if proxying does not work and you receive HTTP-Errror 404
# if you use the settings below, make sure to comment out the above two options
# This may not apply to all systems, applies to openSuse

View File

@@ -16,6 +16,9 @@ server {
# replace 'localhost' with your fqdn if you want to use zammad from remote
server_name localhost;
# security - prevent information disclosure about server version
server_tokens off;
root /opt/zammad/public;
access_log /var/log/nginx/zammad.access.log;

View File

@@ -21,6 +21,9 @@ server {
server_name example.com;
# security - prevent information disclosure about server version
server_tokens off;
access_log /var/log/nginx/zammad.access.log;
error_log /var/log/nginx/zammad.error.log;
@@ -38,6 +41,9 @@ server {
server_name example.com;
# security - prevent information disclosure about server version
server_tokens off;
ssl_certificate /etc/nginx/ssl/example.com-fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/example.com-privkey.pem;

View File

@@ -483,6 +483,7 @@ class CreateTicket < ActiveRecord::Migration[4.2]
t.boolean :public, null: false, default: false
t.string :block_ip, limit: 5000, null: true
t.string :block_country, limit: 5000, null: true
t.string :whitelisted_websites, limit: 5000, null: true
t.string :preferences, limit: 5000, null: true
t.integer :updated_by_id, null: false
t.integer :created_by_id, null: false

View File

@@ -4,13 +4,20 @@ class Issue2460FixCorruptedTwitterIds < ActiveRecord::Migration[5.2]
Channel.where(area: 'Twitter::Account').each do |channel|
client = Twitter::REST::Client.new do |config|
config.consumer_key = channel.options['auth']['consumer_key']
config.consumer_secret = channel.options['auth']['consumer_secret']
config.access_token = channel.options['auth']['oauth_token']
config.access_token_secret = channel.options['auth']['oauth_token_secret']
client = nil
begin
client = Twitter::REST::Client.new do |config|
config.consumer_key = channel.options['auth']['consumer_key']
config.consumer_secret = channel.options['auth']['consumer_secret']
config.access_token = channel.options['auth']['oauth_token']
config.access_token_secret = channel.options['auth']['oauth_token_secret']
end
rescue => e
Rails.logger.error "Error while trying to update corrupted Twitter User ID: #{e.message}"
end
next if client.nil?
channel.options['user']['id'] = client.user.id.to_s
channel.save!

View File

@@ -0,0 +1,9 @@
class ChatAddAllowWebsite < ActiveRecord::Migration[5.1]
def up
# return if it's a new setup
return if !Setting.find_by(name: 'system_init_done')
add_column :chats, :whitelisted_websites, :string, limit: 5000, null: true
end
end

View File

@@ -1,4 +1,4 @@
module EmailHelper
class EmailHelper
=begin
@@ -454,6 +454,24 @@ returns
password: password,
},
},
{
adapter: 'smtp',
options: {
host: domain,
port: 587,
user: user,
password: password,
},
},
{
adapter: 'smtp',
options: {
host: domain,
port: 587,
user: email,
password: password,
},
},
]
outbounds = outbounds.concat(outbound)
end

View File

@@ -1,4 +1,4 @@
module EmailHelper
class EmailHelper
class Probe
=begin
@@ -400,6 +400,7 @@ returns on fail
'Incorrect username' => 'Authentication failed, username incorrect!',
'Lookup failed' => 'Authentication failed, username incorrect!',
'Invalid credentials' => 'Authentication failed, invalid credentials!',
'authentication not enabled' => 'Authentication not possible (not offered by the service)',
'getaddrinfo: nodename nor servname provided, or not known' => 'Hostname not found!',
'getaddrinfo: Name or service not known' => 'Hostname not found!',
'No route to host' => 'No route to host!',

View File

@@ -1,4 +1,4 @@
module EmailHelper
class EmailHelper
class Verify
=begin

View File

@@ -3,7 +3,7 @@ module SessionHelper
collections, assets = default_collections(user)
{
session: user,
session: user.filter_attributes(user.attributes),
models: models(user),
collections: collections,
assets: assets,

View File

@@ -105,6 +105,14 @@ class Sessions::Event::Base
user
end
def remote_ip
@headers&.fetch('X-Forwarded-For', nil).presence
end
def origin
@headers&.fetch('Origin', nil).presence
end
def permission_check(key, event)
user = current_user
return if !user

View File

@@ -24,17 +24,17 @@ return is sent as message back to peer
# geo ip lookup
geo_ip = nil
if @remote_ip
geo_ip = Service::GeoIp.location(@remote_ip)
if remote_ip
geo_ip = Service::GeoIp.location(remote_ip)
end
# dns lookup
dns_name = nil
if @remote_ip
if remote_ip
begin
dns = Resolv::DNS.new
dns.timeouts = 3
result = dns.getname @remote_ip
result = dns.getname remote_ip
if result
dns_name = result.to_s
end
@@ -51,7 +51,7 @@ return is sent as message back to peer
preferences: {
url: @payload['data']['url'],
participants: [@client_id],
remote_ip: @remote_ip,
remote_ip: remote_ip,
geo_ip: geo_ip,
dns_name: dns_name,
},

View File

@@ -21,8 +21,9 @@ return is sent as message back to peer
def run
return super if super
return if !check_chat_exists
return if !check_chat_block_by_ip
return if !check_chat_block_by_country
return if blocked_ip?
return if blocked_country?
return if blocked_origin?
# check if it's a chat sessin reconnect
session_id = nil
@@ -54,10 +55,28 @@ return is sent as message back to peer
}
end
def check_chat_block_by_ip
chat = current_chat
return true if !chat.blocked_ip?(@remote_ip)
def blocked_ip?
return false if !current_chat.blocked_ip?(remote_ip)
send_unavailable
true
end
def blocked_country?
return false if !current_chat.blocked_country?(remote_ip)
send_unavailable
true
end
def blocked_origin?
return false if current_chat.website_whitelisted?(origin)
send_unavailable
true
end
def send_unavailable
error = {
event: 'chat_error',
data: {
@@ -65,21 +84,5 @@ return is sent as message back to peer
},
}
Sessions.send(@client_id, error)
false
end
def check_chat_block_by_country
chat = current_chat
return true if !chat.blocked_country?(@remote_ip)
error = {
event: 'chat_error',
data: {
state: 'chat_unavailable',
},
}
Sessions.send(@client_id, error)
false
end
end

View File

@@ -79,6 +79,18 @@ namespace :searchindex do
pipeline = "zammad#{rand(999_999_999_999)}"
Setting.set('es_pipeline', pipeline)
end
# define pipeline_field_attributes
# ES 5.6 and nower has no ignore_missing support
pipeline_field_attributes = {
ignore_failure: true,
}
if es_multi_index?
pipeline_field_attributes = {
ignore_failure: true,
ignore_missing: true,
}
end
print 'create pipeline (pipeline)... '
SearchIndexBackend.processors(
"_ingest/pipeline/#{pipeline}": [
@@ -91,22 +103,19 @@ namespace :searchindex do
processors: [
{
foreach: {
field: 'article',
ignore_failure: true,
processor: {
field: 'article',
processor: {
foreach: {
field: '_ingest._value.attachment',
ignore_failure: true,
processor: {
field: '_ingest._value.attachment',
processor: {
attachment: {
target_field: '_ingest._value',
field: '_ingest._value._content',
ignore_failure: true,
}
target_field: '_ingest._value',
field: '_ingest._value._content',
}.merge(pipeline_field_attributes),
}
}
}.merge(pipeline_field_attributes),
}
}
}.merge(pipeline_field_attributes),
}
]
}
@@ -301,7 +310,7 @@ def es_pipeline?
true
end
# no mulit index for elasticsearch 5.6 and lower
# no multi index for elasticsearch 5.6 and lower
def es_multi_index?
number = es_version
return false if number.blank?

View File

@@ -444,7 +444,7 @@ class TwitterSync
=begin
create a tweet ot direct message from an article
create a tweet or direct message from an article
=end
@@ -484,6 +484,12 @@ create a tweet ot direct message from an article
Rails.logger.debug { 'Create tweet from article...' }
# rubocop:disable Style/AsciiComments
# workaround for https://github.com/sferik/twitter/issues/677
# https://github.com/zammad/zammad/issues/2873 - unable to post
# tweets with * - replace `*` with the wide-asterisk `*`.
# rubocop:enable Style/AsciiComments
article[:body].tr!('*', '*') if article[:body].present?
tweet = @client.update(
article[:body],
{

View File

@@ -48,7 +48,6 @@ class WebsocketServer
def self.onopen(websocket, handshake)
headers = handshake.headers
remote_ip = get_remote_ip(headers)
client_id = websocket.object_id.to_s
log 'notice', 'Client connected.', client_id
Sessions.create( client_id, {}, { type: 'websocket' } )
@@ -60,7 +59,6 @@ class WebsocketServer
last_ping: Time.now.utc.to_i,
error_count: 0,
headers: headers,
remote_ip: remote_ip,
}
end
@@ -103,7 +101,7 @@ class WebsocketServer
event: data['event'],
payload: data,
session: @clients[client_id][:session],
remote_ip: @clients[client_id][:remote_ip],
headers: @clients[client_id][:headers],
client_id: client_id,
clients: @clients,
options: @options,
@@ -116,12 +114,6 @@ class WebsocketServer
end
end
def self.get_remote_ip(headers)
return headers['X-Forwarded-For'] if headers && headers['X-Forwarded-For']
nil
end
def self.websocket_send(client_id, data)
msg = if data.class != Array
"[#{data.to_json}]"

View File

@@ -371,6 +371,23 @@ do($ = window.jQuery, window) ->
'Since you didn\'t respond in the last %s minutes your conversation with %s got closed.': 'Поскольку вы не отвечали в течение последних %s минут, ваш разговор с %s был закрыт.'
'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Поскольку вы не отвечали в течение последних %s минут, ваш разговор был закрыт.'
'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'К сожалению, ожидание свободного места требует больше времени. Повторите попытку позже или отправьте нам электронное письмо. Спасибо!'
'sv':
'<strong>Chat</strong> with us!': '<strong>Chatta</strong> med oss!'
'Scroll down to see new messages': 'Rulla ner för att se nya meddelanden'
'Online': 'Online'
'Offline': 'Offline'
'Connecting': 'Ansluter'
'Connection re-established': 'Anslutningen återupprättas'
'Today': 'I dag'
'Send': 'Skicka'
'Chat closed by %s': 'Chatt stängd av %s'
'Compose your message...': 'Skriv ditt meddelande...'
'All colleagues are busy.': 'Alla kollegor är upptagna.'
'You are on waiting list position <strong>%s</strong>.': 'Du är på väntelistan som position <strong>%s</strong>.'
'Start new conversation': 'Starta ny konversation'
'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Eftersom du inte svarat inom %s minuterna i din konversation med <strong>%s</strong> så stängdes chatten.'
'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Då du inte svarat inom de senaste %s minuterna så avslutades din chatt.'
'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Vi är ledsna, det tar längre tid som förväntat att få en ledig plats. Försök igen senare eller skicka ett e-postmeddelande till oss. Tack!'
sessionId: undefined
scrolledToBottom: true
scrollSnapTolerance: 10

View File

@@ -3306,3 +3306,9 @@ test('App.Utils.joinUrlComponents()', function() {
// expect @joinUrlComponents() to filter them out of the results before joining the rest with slashes
equal(App.Utils.joinUrlComponents('foo', undefined, 'bar', null, 'baz'), 'foo/bar/baz', 'with a list including null or undefined')
});
test('App.Utils.signatureIdentifyByHtmlHelper()', function() {
result = App.Utils.signatureIdentifyByHtmlHelper("&lt;script&gt;alert('fish2');&lt;/script&gt;<blockquote></blockquote>")
equal(result, "&lt;script&gt;alert('fish2');&lt;/script&gt;<span class=\"js-signatureMarker\"></span><blockquote></blockquote>", 'signatureIdentifyByHtmlHelper does not reactivate alert')
});

View File

@@ -15,5 +15,15 @@ RSpec.describe Issue2460FixCorruptedTwitterIds, type: :db_migration do
.to change { twitter_channel.reload.options[:user][:id] }
.to(twitter_api_user_id.to_s)
end
context 'with invalid credentials stored' do
before { allow(Twitter::REST::Client).to receive(:new).and_raise(Twitter::Error::Unauthorized.new('Could not authenticate you.')) }
it 'skips the Channel' do
expect { migrate }
.not_to change { twitter_channel.reload.options[:user][:id] }
end
end
end
end

9
spec/factories/chat.rb Normal file
View File

@@ -0,0 +1,9 @@
FactoryBot.define do
factory :chat do
sequence(:name) { |n| "Chat #{n}" }
max_queue { 5 }
active { true }
created_by_id { 1 }
updated_by_id { 1 }
end
end

18
spec/models/chat_spec.rb Normal file
View File

@@ -0,0 +1,18 @@
require 'rails_helper'
RSpec.describe Chat, type: :model do
describe 'website whitelisting' do
let(:chat) { create(:chat, whitelisted_websites: 'zammad.org') }
it 'detects whitelisted website' do
result = chat.website_whitelisted?('https://www.zammad.org')
expect(result).to be true
end
it 'detects non-whitelisted website' do
result = chat.website_whitelisted?('https://www.zammad.com')
expect(result).to be false
end
end
end

View File

@@ -54,14 +54,14 @@ RSpec.describe ObjectManager::Attribute, type: :model do
it 'rejects ActiveRecord reserved word "attribute"' do
expect do
described_class.add attributes_for :object_manager_attribute_text, name: 'attribute'
end.to raise_error 'attribute is a reserved word, please choose a different one'
end.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Name attribute is a reserved word! (2)')
end
%w[destroy true false integer select drop create alter index table varchar blob date datetime timestamp url icon initials avatar permission validate subscribe unsubscribe translate search _type _doc _id id].each do |reserved_word|
it "rejects Zammad reserved word '#{reserved_word}'" do
expect do
described_class.add attributes_for :object_manager_attribute_text, name: reserved_word
end.to raise_error "#{reserved_word} is a reserved word, please choose a different one"
end.to raise_error(ActiveRecord::RecordInvalid, /is a reserved word! \(1\)/)
end
end
@@ -69,7 +69,7 @@ RSpec.describe ObjectManager::Attribute, type: :model do
it "rejects word '#{reserved_word}' which is used for database references" do
expect do
described_class.add attributes_for :object_manager_attribute_text, name: reserved_word
end.to raise_error "Name can't get used, *_id and *_ids are not allowed"
end.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Name can't get used because *_id and *_ids are not allowed")
end
end

View File

@@ -39,6 +39,9 @@ RSpec.describe 'Api Auth', type: :request do
get '/api/v1/sessions', params: {}, as: :json
expect(response).to have_http_status(:ok)
expect(response.header['Access-Control-Allow-Origin']).to eq('*')
expect(response.header['Cache-Control']).to match(/no-cache, no-store/)
expect(response.header['Pragma']).to eq('no-cache')
expect(response.header['Expires']).to eq('-1')
expect(json_response).to be_a_kind_of(Hash)
expect(json_response).to be_truthy
end
@@ -57,6 +60,9 @@ RSpec.describe 'Api Auth', type: :request do
get '/api/v1/tickets', params: {}, as: :json
expect(response).to have_http_status(:ok)
expect(response.header['Access-Control-Allow-Origin']).to eq('*')
expect(response.header['Cache-Control']).to match(/no-cache, no-store/)
expect(response.header['Pragma']).to eq('no-cache')
expect(response.header['Expires']).to eq('-1')
expect(json_response).to be_a_kind_of(Array)
expect(json_response).to be_truthy
end
@@ -75,6 +81,9 @@ RSpec.describe 'Api Auth', type: :request do
get '/api/v1/tickets', params: {}, as: :json
expect(response).to have_http_status(:ok)
expect(response.header['Access-Control-Allow-Origin']).to eq('*')
expect(response.header['Cache-Control']).to match(/no-cache, no-store/)
expect(response.header['Pragma']).to eq('no-cache')
expect(response.header['Expires']).to eq('-1')
expect(json_response).to be_a_kind_of(Array)
expect(json_response).to be_truthy
end
@@ -104,6 +113,9 @@ RSpec.describe 'Api Auth', type: :request do
get '/api/v1/sessions', params: {}, as: :json
expect(response).to have_http_status(:ok)
expect(response.header['Access-Control-Allow-Origin']).to eq('*')
expect(response.header['Cache-Control']).to match(/no-cache, no-store/)
expect(response.header['Pragma']).to eq('no-cache')
expect(response.header['Expires']).to eq('-1')
expect(json_response).to be_a_kind_of(Hash)
expect(json_response).to be_truthy
@@ -251,6 +263,9 @@ RSpec.describe 'Api Auth', type: :request do
get '/api/v1/tickets', params: {}, as: :json
expect(response).to have_http_status(:ok)
expect(response.header['Access-Control-Allow-Origin']).to eq('*')
expect(response.header['Cache-Control']).to match(/no-cache, no-store/)
expect(response.header['Pragma']).to eq('no-cache')
expect(response.header['Expires']).to eq('-1')
expect(json_response).to be_a_kind_of(Array)
expect(json_response).to be_truthy
@@ -286,6 +301,9 @@ RSpec.describe 'Api Auth', type: :request do
Setting.set('api_token_access', true)
get '/api/v1/tickets', params: {}, as: :json
expect(response.header['Access-Control-Allow-Origin']).to eq('*')
expect(response.header['Cache-Control']).to match(/no-cache, no-store/)
expect(response.header['Pragma']).to eq('no-cache')
expect(response.header['Expires']).to eq('-1')
expect(response).to have_http_status(:ok)
expect(json_response).to be_a_kind_of(Array)
expect(json_response).to be_truthy
@@ -370,6 +388,9 @@ RSpec.describe 'Api Auth', type: :request do
get '/api/v1/tickets', params: {}, as: :json
expect(response).to have_http_status(:ok)
expect(response.header['Access-Control-Allow-Origin']).to eq('*')
expect(response.header['Cache-Control']).to match(/no-cache, no-store/)
expect(response.header['Pragma']).to eq('no-cache')
expect(response.header['Expires']).to eq('-1')
expect(json_response).to be_a_kind_of(Array)
expect(json_response).to be_truthy

View File

@@ -1,112 +0,0 @@
require 'rails_helper'
RSpec.describe 'Basics', type: :request do
describe 'request handling' do
it 'does json requests' do
# 404
get '/not_existing_url', as: :json
expect(response).to have_http_status(:not_found)
expect(json_response).to be_a_kind_of(Hash)
expect(json_response['error']).to eq('No route matches [GET] /not_existing_url')
# 401
get '/api/v1/organizations', as: :json
expect(response).to have_http_status(:unauthorized)
expect(json_response).to be_a_kind_of(Hash)
expect(json_response['error']).to eq('authentication failed')
# 422
get '/tests/unprocessable_entity', as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(json_response).to be_a_kind_of(Hash)
expect(json_response['error']).to eq('some error message')
# 401
get '/tests/not_authorized', as: :json
expect(response).to have_http_status(:unauthorized)
expect(json_response).to be_a_kind_of(Hash)
expect(json_response['error']).to eq('some error message')
# 401
get '/tests/ar_not_found', as: :json
expect(response).to have_http_status(:not_found)
expect(json_response).to be_a_kind_of(Hash)
expect(json_response['error']).to eq('some error message')
# 500
get '/tests/standard_error', as: :json
expect(response).to have_http_status(:internal_server_error)
expect(json_response).to be_a_kind_of(Hash)
expect(json_response['error']).to eq('some error message')
# 422
get '/tests/argument_error', as: :json
expect(response).to have_http_status(:unprocessable_entity)
expect(json_response).to be_a_kind_of(Hash)
expect(json_response['error']).to eq('some error message')
end
it 'does html requests' do
# 404
get '/not_existing_url'
expect(response).to have_http_status(:not_found)
expect(response.body).to match(/<html/)
expect(response.body).to match(%r{<title>404: Not Found</title>})
expect(response.body).to match(%r{<h1>404: Requested resource was not found</h1>})
expect(response.body).to match(%r{No route matches \[GET\] /not_existing_url})
# 401
get '/api/v1/organizations'
expect(response).to have_http_status(:unauthorized)
expect(response.body).to match(/<html/)
expect(response.body).to match(%r{<title>401: Unauthorized</title>})
expect(response.body).to match(%r{<h1>401: Unauthorized</h1>})
expect(response.body).to match(/authentication failed/)
# 422
get '/tests/unprocessable_entity'
expect(response).to have_http_status(:unprocessable_entity)
expect(response.body).to match(/<html/)
expect(response.body).to match(%r{<title>422: Unprocessable Entity</title>})
expect(response.body).to match(%r{<h1>422: The change you wanted was rejected.</h1>})
expect(response.body).to match(/some error message/)
# 401
get '/tests/not_authorized'
expect(response).to have_http_status(:unauthorized)
expect(response.body).to match(/<html/)
expect(response.body).to match(%r{<title>401: Unauthorized</title>})
expect(response.body).to match(%r{<h1>401: Unauthorized</h1>})
expect(response.body).to match(/some error message/)
# 401
get '/tests/ar_not_found'
expect(response).to have_http_status(:not_found)
expect(response.body).to match(/<html/)
expect(response.body).to match(%r{<title>404: Not Found</title>})
expect(response.body).to match(%r{<h1>404: Requested resource was not found</h1>})
expect(response.body).to match(/some error message/)
# 500
get '/tests/standard_error'
expect(response).to have_http_status(:internal_server_error)
expect(response.body).to match(/<html/)
expect(response.body).to match(%r{<title>500: Something went wrong</title>})
expect(response.body).to match(%r{<h1>500: We're sorry, but something went wrong.</h1>})
expect(response.body).to match(/some error message/)
# 422
get '/tests/argument_error'
expect(response).to have_http_status(:unprocessable_entity)
expect(response.body).to match(/<html/)
expect(response.body).to match(%r{<title>422: Unprocessable Entity</title>})
expect(response.body).to match(%r{<h1>422: The change you wanted was rejected.</h1>})
expect(response.body).to match(/some error message/)
end
end
end

View File

@@ -0,0 +1,25 @@
require 'rails_helper'
RSpec.describe 'CORS Preflight Check', type: :request do
shared_examples 'empty response' do
it { expect(response).to have_http_status(:ok) }
it { expect(response.body).to be_empty }
end
context 'valid route' do
before do
process :options, '/'
end
include_examples 'empty response'
end
context 'invalid route' do
before do
process :options, '/this_is_an_invalid_route'
end
include_examples 'empty response'
end
end

140
spec/requests/error_spec.rb Normal file
View File

@@ -0,0 +1,140 @@
require 'rails_helper'
RSpec.describe 'Error handling', type: :request do
shared_examples 'JSON response format' do
let(:as) { :json }
it { expect(response).to have_http_status(http_status) }
it { expect(json_response).to be_a_kind_of(Hash) }
it do
# There is a special case where we mask technical errors and return
# a random error code that can be easily found in the logs by an
# administrator. However, this makes it hard to check for the exact error
# message. Therefore we only check for the substring in this particular case
if message == 'Please contact your administrator'
expect(json_response['error']).to include(message)
else
expect(json_response['error']).to eq(message)
end
end
end
shared_examples 'HTML response format' do
let(:as) { :html }
it { expect(response).to have_http_status(http_status) }
it { expect(response.body).to include('<html') }
it { expect(response.body).to include("<title>#{title}</title>") }
it { expect(response.body).to include("<h1>#{headline}</h1>") }
it { expect(response.body).to include(message) }
end
context 'error with confidential message is raised' do
let(:admin_user) { create(:admin_user, groups: Group.all) }
let!(:ticket) { create(:ticket) }
let(:invalid_group_id) { 99_999 }
let(:message) { 'Please contact your administrator' }
let(:http_status) { :unprocessable_entity }
before do
# fake production ENV to enable error hiding
env = double(
production?: true,
test?: false,
development?: false
)
allow(::Rails).to receive(:env).and_return(env)
authenticated_as(admin_user)
put "/api/v1/tickets/#{ticket.id}?all=true", params: { group_id: invalid_group_id }, as: as
end
context 'requesting JSON' do
include_examples 'JSON response format'
end
context 'requesting HTML' do
let(:title) { '422: Unprocessable Entity' }
let(:headline) { '422: The change you wanted was rejected.' }
include_examples 'HTML response format'
end
end
context 'URL route does not exist' do
before do
get '/not_existing_url', as: as
end
let(:message) { 'No route matches [GET] /not_existing_url' }
let(:http_status) { :not_found }
context 'requesting JSON' do
include_examples 'JSON response format'
end
context 'requesting HTML' do
let(:title) { '404: Not Found' }
let(:headline) { '404: Requested resource was not found' }
include_examples 'HTML response format'
end
end
context 'request is not authenticated' do
before do
get '/api/v1/organizations', as: as
end
let(:message) { 'authentication failed' }
let(:http_status) { :unauthorized }
context 'requesting JSON' do
include_examples 'JSON response format'
end
context 'requesting HTML' do
let(:title) { '401: Unauthorized' }
let(:headline) { '401: Unauthorized' }
include_examples 'HTML response format'
end
end
context 'exception is raised' do
before do
get '/tests/raised_exception', params: { exception: exception.name, message: message }, as: as
end
shared_examples 'handles exception' do |exception, http_status, title, headline|
context "#{exception} is raised" do
let(:exception) { exception }
let(:http_status) { http_status }
let(:message) { 'some error message' }
context 'requesting JSON' do
include_examples 'JSON response format'
end
context 'requesting HTML' do
let(:title) { title }
let(:headline) { headline }
include_examples 'HTML response format'
end
end
end
include_examples 'handles exception', ActiveRecord::RecordNotFound, :not_found, '404: Not Found', '404: Requested resource was not found'
include_examples 'handles exception', Exceptions::NotAuthorized, :unauthorized, '401: Unauthorized', '401: Unauthorized'
include_examples 'handles exception', Exceptions::UnprocessableEntity, :unprocessable_entity, '422: Unprocessable Entity', '422: The change you wanted was rejected.'
include_examples 'handles exception', ArgumentError, :unprocessable_entity, '422: Unprocessable Entity', '422: The change you wanted was rejected.'
include_examples 'handles exception', StandardError, :internal_server_error, '500: Something went wrong', "500: We're sorry, but something went wrong."
end
end

View File

@@ -522,6 +522,10 @@ RSpec.describe 'Monitoring', type: :request do
prev_es_config = Setting.get('es_url')
Setting.set('es_url', 'http://127.0.0.1:92001')
# delete all background jobs created while seeding
# to have a clean state for checking for failed ones
Delayed::Job.destroy_all
# add a new object
object = create(:object_manager_attribute_text)

View File

@@ -2,6 +2,33 @@ require 'rails_helper'
RSpec.describe 'Sessions endpoints', type: :request do
describe 'GET /signshow' do
context 'user logged in' do
subject(:user) { create(:agent_user, password: password) }
let(:password) { SecureRandom.urlsafe_base64(20) }
let(:fingerprint) { SecureRandom.urlsafe_base64(40) }
before do
params = {
fingerprint: fingerprint,
username: user.login,
password: password
}
post '/api/v1/signin', params: params, as: :json
end
it 'leaks no sensitive data' do
params = { fingerprint: fingerprint }
get '/api/v1/signshow', params: params, as: :json
expect(json_response['session']).not_to include('password')
end
end
end
describe 'GET /auth/sso (single sign-on)' do
context 'with invalid user login' do
let(:login) { User.pluck(:login).max.next }

View File

@@ -2141,4 +2141,59 @@ RSpec.describe 'Ticket', type: :request do
end
end
end
describe 'GET /api/v1/ticket_customer' do
subject(:ticket) { create(:ticket, customer: customer_authorized) }
let(:organization_authorized) { create(:organization) }
let(:customer_authorized) { create(:customer_user, organization: organization_authorized) }
let(:organization_unauthorized) { create(:organization) }
let(:customer_unauthorized) { create(:customer_user, organization: organization_unauthorized) }
let(:agent) { create(:agent_user, groups: [ticket.group]) }
describe 'listing information' do
before do
ticket
end
shared_examples 'has access' do
it 'succeeds' do
get '/api/v1/ticket_customer',
params: { customer_id: customer_authorized.id },
as: :json
expect(json_response['ticket_ids_open']).to include(ticket.id)
expect(json_response['ticket_ids_closed']).to be_blank
end
end
shared_examples 'has no access' do
it 'fails' do
get '/api/v1/ticket_customer',
params: { customer_id: customer_authorized.id },
as: :json
expect(json_response['ticket_ids_open']).to be_blank
expect(json_response['ticket_ids_closed']).to be_blank
expect(json_response['assets']).to be_blank
end
end
context 'as agent', authenticated_as: -> { agent } do
include_examples 'has access'
end
context 'as authorized customer', authenticated_as: -> { customer_authorized } do
include_examples 'has access'
end
context 'as unauthorized customer', authenticated_as: -> { customer_unauthorized } do
include_examples 'has no access'
end
end
end
end

View File

@@ -126,7 +126,50 @@ RSpec.describe 'Ticket zoom', type: :system do
end
end
end
end
context 'when ticket has an attachment' do
let(:group) { Group.find_by(name: 'Users') }
let(:ticket) { create(:ticket, group: group) }
let(:article) { create(:ticket_article, ticket: ticket) }
let(:attachment_name) { 'some_file.txt' }
before do
Store.add(
object: 'Ticket::Article',
o_id: article.id,
data: 'some content',
filename: attachment_name,
preferences: {
'Content-Type' => 'text/plain',
},
created_by_id: 1,
)
end
context 'article was already forwarded once' do
before do
visit "#ticket/zoom/#{ticket.id}"
within(:active_content) do
find('a[data-type=emailForward]').click
click('.js-reset')
have_no_css('.js-reset')
end
end
it 'adds attachments when forwarding multiple times' do
within(:active_content) do
find('a[data-type=emailForward]').click
end
within('.js-writeArea') do
expect(page).to have_text attachment_name
end
end
end
end
end

View File

@@ -208,9 +208,11 @@ class SignupPasswordChangeAndResetTest < TestCase
click(css: '.content .btn--primary')
watch_for(
css: 'body',
value: 'address invalid',
value: 'sent password reset instructions',
)
click(css: '.content .btn--primary')
set(
css: 'input[name="username"]',
value: signup_user_email,

View File

@@ -4,9 +4,9 @@ class EmailHelperTest < ActiveSupport::TestCase
test 'a mx_records' do
domain = 'znuny.com'
domain = 'zammad.com'
mx_domains = EmailHelper.mx_records(domain)
assert_equal('arber.znuny.com', mx_domains[0])
assert_equal('mx2.zammad.com', mx_domains[0])
end
@@ -48,20 +48,20 @@ class EmailHelperTest < ActiveSupport::TestCase
test 'provider_inbound_mx' do
email = 'linus@znuny.com'
email = 'linus@zammad.com'
password = 'some_pw'
user, domain = EmailHelper.parse_email(email)
mx_domains = EmailHelper.mx_records(domain)
map = EmailHelper.provider_inbound_mx(user, email, password, mx_domains)
assert_equal('imap', map[0][:adapter])
assert_equal('arber.znuny.com', map[0][:options][:host])
assert_equal('mx2.zammad.com', map[0][:options][:host])
assert_equal(993, map[0][:options][:port])
assert_equal(user, map[0][:options][:user])
assert_equal(password, map[0][:options][:password])
assert_equal('imap', map[1][:adapter])
assert_equal('arber.znuny.com', map[1][:options][:host])
assert_equal('mx2.zammad.com', map[1][:options][:host])
assert_equal(993, map[1][:options][:port])
assert_equal(email, map[1][:options][:user])
assert_equal(password, map[1][:options][:password])
@@ -70,19 +70,19 @@ class EmailHelperTest < ActiveSupport::TestCase
test 'provider_inbound_guess' do
email = 'linus@znuny.com'
email = 'linus@zammad.com'
password = 'some_pw'
user, domain = EmailHelper.parse_email(email)
map = EmailHelper.provider_inbound_guess(user, email, password, domain)
assert_equal('imap', map[0][:adapter])
assert_equal('mail.znuny.com', map[0][:options][:host])
assert_equal('mail.zammad.com', map[0][:options][:host])
assert_equal(993, map[0][:options][:port])
assert_equal(user, map[0][:options][:user])
assert_equal(password, map[0][:options][:password])
assert_equal('imap', map[1][:adapter])
assert_equal('mail.znuny.com', map[1][:options][:host])
assert_equal('mail.zammad.com', map[1][:options][:host])
assert_equal(993, map[1][:options][:port])
assert_equal(email, map[1][:options][:user])
assert_equal(password, map[1][:options][:password])
@@ -91,21 +91,21 @@ class EmailHelperTest < ActiveSupport::TestCase
test 'provider_outbound_mx' do
email = 'linus@znuny.com'
email = 'linus@zammad.com'
password = 'some_pw'
user, domain = EmailHelper.parse_email(email)
mx_domains = EmailHelper.mx_records(domain)
map = EmailHelper.provider_outbound_mx(user, email, password, mx_domains)
assert_equal('smtp', map[0][:adapter])
assert_equal('arber.znuny.com', map[0][:options][:host])
assert_equal('mx2.zammad.com', map[0][:options][:host])
assert_equal(25, map[0][:options][:port])
assert_equal(true, map[0][:options][:start_tls])
assert_equal(user, map[0][:options][:user])
assert_equal(password, map[0][:options][:password])
assert_equal('smtp', map[1][:adapter])
assert_equal('arber.znuny.com', map[1][:options][:host])
assert_equal('mx2.zammad.com', map[1][:options][:host])
assert_equal(25, map[1][:options][:port])
assert_equal(true, map[1][:options][:start_tls])
assert_equal(email, map[1][:options][:user])
@@ -115,20 +115,20 @@ class EmailHelperTest < ActiveSupport::TestCase
test 'provider_outbound_guess' do
email = 'linus@znuny.com'
email = 'linus@zammad.com'
password = 'some_pw'
user, domain = EmailHelper.parse_email(email)
map = EmailHelper.provider_outbound_guess(user, email, password, domain)
assert_equal('smtp', map[0][:adapter])
assert_equal('mail.znuny.com', map[0][:options][:host])
assert_equal('mail.zammad.com', map[0][:options][:host])
assert_equal(25, map[0][:options][:port])
assert_equal(true, map[0][:options][:start_tls])
assert_equal(user, map[0][:options][:user])
assert_equal(password, map[0][:options][:password])
assert_equal('smtp', map[1][:adapter])
assert_equal('mail.znuny.com', map[1][:options][:host])
assert_equal('mail.zammad.com', map[1][:options][:host])
assert_equal(25, map[1][:options][:port])
assert_equal(true, map[1][:options][:start_tls])
assert_equal(email, map[1][:options][:user])
@@ -236,7 +236,7 @@ class EmailHelperTest < ActiveSupport::TestCase
result = EmailHelper::Probe.inbound(
adapter: 'imap',
options: {
host: 'arber.znuny.com',
host: 'mx2.zammad.com',
port: 993,
ssl: true,
user: 'some@example.com',
@@ -245,23 +245,22 @@ class EmailHelperTest < ActiveSupport::TestCase
)
assert_equal('invalid', result[:result])
assert_equal('Authentication failed!', result[:message_human])
assert_equal('arber.znuny.com', result[:settings][:options][:host])
assert_equal('mx2.zammad.com', result[:settings][:options][:host])
# realtest - test I
if !ENV['EMAILHELPER_MAILBOX_1']
raise "Need EMAILHELPER_MAILBOX_1 as ENV variable like export EMAILHELPER_MAILBOX_1='unittestemailhelper01@znuny.com:somepass'"
raise "Need EMAILHELPER_MAILBOX_1 as ENV variable like export EMAILHELPER_MAILBOX_1='unittestemailhelper01@zammad.com:somepass'"
end
mailbox_user = ENV['EMAILHELPER_MAILBOX_1'].split(':')[0]
mailbox_password = ENV['EMAILHELPER_MAILBOX_1'].split(':')[1]
user, _domain = EmailHelper.parse_email(mailbox_user)
result = EmailHelper::Probe.inbound(
adapter: 'imap',
options: {
host: 'arber.znuny.com',
host: 'mx2.zammad.com',
port: 993,
ssl: true,
user: user,
user: mailbox_user,
password: mailbox_password,
}
)
@@ -388,36 +387,33 @@ class EmailHelperTest < ActiveSupport::TestCase
{
adapter: 'smtp',
options: {
host: 'arber.znuny.com',
port: 25,
start_tls: true,
user: 'some@example.com',
password: 'password',
host: 'mx2.zammad.com',
port: 587,
user: 'some@example.com',
password: 'password',
}
},
'some@example.com',
)
assert_equal('invalid', result[:result])
assert_equal('Authentication failed!', result[:message_human])
assert_equal('arber.znuny.com', result[:settings][:options][:host])
assert_equal('mx2.zammad.com', result[:settings][:options][:host])
# realtest - test I
if !ENV['EMAILHELPER_MAILBOX_1']
raise "Need EMAILHELPER_MAILBOX_1 as ENV variable like export EMAILHELPER_MAILBOX_1='unittestemailhelper01@znuny.com:somepass'"
raise "Need EMAILHELPER_MAILBOX_1 as ENV variable like export EMAILHELPER_MAILBOX_1='unittestemailhelper01@zammad.com:somepass'"
end
mailbox_user = ENV['EMAILHELPER_MAILBOX_1'].split(':')[0]
mailbox_password = ENV['EMAILHELPER_MAILBOX_1'].split(':')[1]
user, _domain = EmailHelper.parse_email(mailbox_user)
result = EmailHelper::Probe.outbound(
{
adapter: 'smtp',
options: {
host: 'arber.znuny.com',
port: 25,
start_tls: true,
user: user,
password: mailbox_password,
host: 'mx2.zammad.com',
port: 587,
user: mailbox_user,
password: mailbox_password,
}
},
mailbox_user,
@@ -436,7 +432,7 @@ class EmailHelperTest < ActiveSupport::TestCase
# realtest - test I, with imap
if !ENV['EMAILHELPER_MAILBOX_1']
raise "Need EMAILHELPER_MAILBOX_1 as ENV variable like export EMAILHELPER_MAILBOX_1='unittestemailhelper01@znuny.com:somepass'"
raise "Need EMAILHELPER_MAILBOX_1 as ENV variable like export EMAILHELPER_MAILBOX_1='unittestemailhelper01@zammad.com:somepass'"
end
mailbox_user = ENV['EMAILHELPER_MAILBOX_1'].split(':')[0]
@@ -447,8 +443,8 @@ class EmailHelperTest < ActiveSupport::TestCase
password: mailbox_password,
)
assert_equal('ok', result[:result])
assert_equal('arber.znuny.com', result[:setting][:inbound][:options][:host])
assert_equal('arber.znuny.com', result[:setting][:outbound][:options][:host])
assert_equal('mx2.zammad.com', result[:setting][:inbound][:options][:host])
assert_equal('mx2.zammad.com', result[:setting][:outbound][:options][:host])
# realtest - test II, gmail with only pop3
if !ENV['EMAILHELPER_MAILBOX_2']
@@ -473,31 +469,29 @@ class EmailHelperTest < ActiveSupport::TestCase
# realtest - test I, with imap
if !ENV['EMAILHELPER_MAILBOX_1']
raise "Need EMAILHELPER_MAILBOX_1 as ENV variable like export EMAILHELPER_MAILBOX_1='unittestemailhelper01@znuny.com:somepass'"
raise "Need EMAILHELPER_MAILBOX_1 as ENV variable like export EMAILHELPER_MAILBOX_1='unittestemailhelper01@zammad.com:somepass'"
end
mailbox_user = ENV['EMAILHELPER_MAILBOX_1'].split(':')[0]
mailbox_password = ENV['EMAILHELPER_MAILBOX_1'].split(':')[1]
user, _domain = EmailHelper.parse_email(mailbox_user)
result = EmailHelper::Verify.email(
inbound: {
adapter: 'imap',
options: {
host: 'arber.znuny.com',
host: 'mx2.zammad.com',
port: 993,
ssl: true,
user: user,
user: mailbox_user,
password: mailbox_password,
},
},
outbound: {
adapter: 'smtp',
options: {
host: 'arber.znuny.com',
port: 25,
start_tls: true,
user: user,
password: mailbox_password,
host: 'mx2.zammad.com',
port: 587,
user: mailbox_user,
password: mailbox_password,
},
},
sender: mailbox_user,

View File

@@ -73,7 +73,7 @@ class ObjectManagerTest < ActiveSupport::TestCase
assert_not(attribute1)
# create invalid attributes
assert_raises(RuntimeError) do
assert_raises(ActiveRecord::RecordInvalid) do
ObjectManager::Attribute.add(
object: 'Ticket',
name: 'test2_id',
@@ -91,7 +91,7 @@ class ObjectManagerTest < ActiveSupport::TestCase
updated_by_id: 1,
)
end
assert_raises(RuntimeError) do
assert_raises(ActiveRecord::RecordInvalid) do
ObjectManager::Attribute.add(
object: 'Ticket',
name: 'test3_ids',
@@ -286,7 +286,7 @@ class ObjectManagerTest < ActiveSupport::TestCase
)
assert_equal(false, ObjectManager::Attribute.pending_migration?)
assert_raises(RuntimeError) do
assert_raises(ActiveRecord::RecordInvalid) do
ObjectManager::Attribute.add(
object: 'Ticket',
name: 'test13|',
@@ -307,7 +307,7 @@ class ObjectManagerTest < ActiveSupport::TestCase
end
assert_equal(false, ObjectManager::Attribute.pending_migration?)
assert_raises(RuntimeError) do
assert_raises(ActiveRecord::RecordInvalid) do
ObjectManager::Attribute.add(
object: 'Ticket',
name: 'test14!',
@@ -328,7 +328,7 @@ class ObjectManagerTest < ActiveSupport::TestCase
end
assert_equal(false, ObjectManager::Attribute.pending_migration?)
assert_raises(RuntimeError) do
assert_raises(ActiveRecord::RecordInvalid) do
ObjectManager::Attribute.add(
object: 'Ticket',
name: 'test15ä',
@@ -370,7 +370,7 @@ class ObjectManagerTest < ActiveSupport::TestCase
end
assert_equal(false, ObjectManager::Attribute.pending_migration?)
assert_raises(RuntimeError) do
assert_raises(ActiveRecord::RecordInvalid) do
ObjectManager::Attribute.add(
object: 'Ticket',
name: 'delete',
@@ -391,7 +391,7 @@ class ObjectManagerTest < ActiveSupport::TestCase
assert_equal(false, ObjectManager::Attribute.pending_migration?)
attribute_count = ObjectManager::Attribute.count
assert_raises(RuntimeError) do
assert_raises(ActiveRecord::RecordInvalid) do
ObjectManager::Attribute.add(
object: 'Ticket',
name: 'updated_at',
@@ -413,7 +413,7 @@ class ObjectManagerTest < ActiveSupport::TestCase
end
assert_equal(attribute_count, ObjectManager::Attribute.count)
assert_raises(RuntimeError) do
assert_raises(ActiveRecord::RecordInvalid) do
ObjectManager::Attribute.add(
object: 'Ticket',
name: 'updated_AT',