Update deps, remove PGP
This commit is contained in:
parent
79653705fe
commit
58ce48b031
56 changed files with 1057 additions and 5100 deletions
|
|
@ -9,12 +9,12 @@
|
|||
"lint": "eslint index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "7.22.17",
|
||||
"@babel/preset-env": "7.22.15",
|
||||
"@babel/preset-typescript": "7.22.15"
|
||||
"@babel/core": "7.23.0",
|
||||
"@babel/preset-env": "7.22.20",
|
||||
"@babel/preset-typescript": "7.23.0"
|
||||
},
|
||||
"peerDependencies": {},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.49.0"
|
||||
"eslint": "^8.50.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,15 +9,15 @@
|
|||
"fmt": "prettier \"profile/**/*.js\" --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.0",
|
||||
"@typescript-eslint/parser": "^6.7.0",
|
||||
"@rushstack/eslint-patch": "^1.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.7.3",
|
||||
"@typescript-eslint/parser": "^6.7.3",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-config-xo-space": "^0.34.0",
|
||||
"eslint-plugin-cypress": "^2.14.0",
|
||||
"eslint-plugin-cypress": "^2.15.1",
|
||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"eslint-plugin-jest": "^27.2.3",
|
||||
"eslint-plugin-jest": "^27.4.0",
|
||||
"eslint-plugin-no-use-extend-native": "^0.5.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-unicorn": "48.0.1",
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
"typescript": "^4.9.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.49.0",
|
||||
"eslint": "^8.50.0",
|
||||
"jest": "^29.7.0",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
"private": false,
|
||||
"devDependencies": {
|
||||
"@hapi/basic": "^7.0.2",
|
||||
"@types/jest": "^29.5.4",
|
||||
"@types/jest": "^29.5.5",
|
||||
"babel-preset-link": "*",
|
||||
"eslint-config-link": "*",
|
||||
"jest-config-link": "*",
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
"dependencies": {
|
||||
"@hapi/hapi": "^21.3.2",
|
||||
"@hapi/hoek": "^11.0.2",
|
||||
"joi": "^17.10.1",
|
||||
"joi": "^17.10.2",
|
||||
"next-auth": "4.23.1"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
"license": "AGPL-3.0-or-later",
|
||||
"private": false,
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.4",
|
||||
"@types/jest": "^29.5.5",
|
||||
"tsc-watch": "^6.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
"node": ">=14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/jest": "^29.5.4",
|
||||
"@types/jest": "^29.5.5",
|
||||
"jest": "^29.7.0",
|
||||
"jest-junit": "^16.0.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,15 +13,15 @@
|
|||
"@fontsource/poppins": "^5.0.8",
|
||||
"@fontsource/roboto": "^5.0.8",
|
||||
"@mui/icons-material": "^5",
|
||||
"@mui/lab": "^5.0.0-alpha.143",
|
||||
"@mui/lab": "^5.0.0-alpha.146",
|
||||
"@mui/material": "^5",
|
||||
"@mui/x-data-grid-pro": "^6.13.0",
|
||||
"@mui/x-date-pickers-pro": "^6.13.0",
|
||||
"@mui/x-data-grid-pro": "^6.15.0",
|
||||
"@mui/x-date-pickers-pro": "^6.15.0",
|
||||
"@opensearch-project/opensearch": "^2.3.1",
|
||||
"date-fns": "^2.30.0",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"material-ui-popup-state": "^5.0.9",
|
||||
"next": "13.4.19",
|
||||
"next": "13.5.3",
|
||||
"next-auth": "^4.23.1",
|
||||
"next-http-proxy-middleware": "^1.2.5",
|
||||
"nodemailer": "^6.9.5",
|
||||
|
|
@ -32,20 +32,20 @@
|
|||
"react-iframe": "^1.8.5",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-polyglot": "^0.7.2",
|
||||
"sharp": "^0.32.5",
|
||||
"swr": "^2.2.2",
|
||||
"tss-react": "^4.9.0",
|
||||
"uuid": "^9.0.0"
|
||||
"sharp": "^0.32.6",
|
||||
"swr": "^2.2.4",
|
||||
"tss-react": "^4.9.2",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.17",
|
||||
"@types/node": "^20.6.0",
|
||||
"@types/react": "18.2.21",
|
||||
"@types/uuid": "^9.0.3",
|
||||
"@babel/core": "^7.23.0",
|
||||
"@types/node": "^20.7.0",
|
||||
"@types/react": "18.2.23",
|
||||
"@types/uuid": "^9.0.4",
|
||||
"babel-loader": "^9.1.3",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint": "^8.50.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-next": "^13.4.19",
|
||||
"eslint-config-next": "^13.5.3",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -18,9 +18,9 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/figlet": "^1.5.6",
|
||||
"@types/lodash": "^4.14.198",
|
||||
"@types/lodash": "^4.14.199",
|
||||
"@types/node": "*",
|
||||
"@types/uuid": "^9.0.3",
|
||||
"@types/uuid": "^9.0.4",
|
||||
"camelcase-keys": "^9.0.0",
|
||||
"pg-monitor": "^2.0.0",
|
||||
"tsc-watch": "^6.0.4",
|
||||
|
|
@ -40,11 +40,11 @@
|
|||
"@promster/server": "^9.0.0",
|
||||
"@promster/types": "^5.0.0",
|
||||
"@types/convict": "^6.1.4",
|
||||
"@types/hapi__glue": "^6.1.6",
|
||||
"@types/hapi__glue": "^6.1.7",
|
||||
"@types/hapi__hapi": "^20.0.13",
|
||||
"@types/hapi__inert": "^5.2.6",
|
||||
"@types/hapi__vision": "^5.5.4",
|
||||
"@types/hapipal__schmervice": "^2.0.3",
|
||||
"@types/hapi__inert": "^5.2.7",
|
||||
"@types/hapi__vision": "^5.5.5",
|
||||
"@types/hapipal__schmervice": "^2.0.4",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^11.0.0",
|
||||
"convict": "^6.2.4",
|
||||
|
|
@ -52,13 +52,13 @@
|
|||
"figlet": "^1.6.0",
|
||||
"hapi-pino": "^12.1.0",
|
||||
"http-terminator": "^3.2.0",
|
||||
"joi": "^17.10.1",
|
||||
"joi": "^17.10.2",
|
||||
"lodash": "^4.17.21",
|
||||
"next-auth": "^4.23.1",
|
||||
"pg-promise": "^11.5.4",
|
||||
"pino": "^8.15.1",
|
||||
"pino-pretty": "^10.2.0",
|
||||
"prom-client": "^14.x.x",
|
||||
"uuid": "^9.0.0"
|
||||
"uuid": "^9.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@
|
|||
"@digiresilience/montar": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.22.17",
|
||||
"@babel/preset-env": "7.22.15",
|
||||
"@babel/preset-typescript": "7.22.15",
|
||||
"eslint": "^8.49.0",
|
||||
"@babel/core": "7.23.0",
|
||||
"@babel/preset-env": "7.22.20",
|
||||
"@babel/preset-typescript": "7.23.0",
|
||||
"eslint": "^8.50.0",
|
||||
"pino-pretty": "^10.2.0",
|
||||
"prettier": "^3.0.3",
|
||||
"ts-node": "^10.9.1",
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@
|
|||
"pg-promise": "^11.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.22.17",
|
||||
"@babel/preset-env": "7.22.15",
|
||||
"@babel/preset-typescript": "7.22.15",
|
||||
"@types/jest": "^29.5.4",
|
||||
"eslint": "^8.49.0",
|
||||
"@babel/core": "7.23.0",
|
||||
"@babel/preset-env": "7.22.20",
|
||||
"@babel/preset-typescript": "7.23.0",
|
||||
"@types/jest": "^29.5.5",
|
||||
"eslint": "^8.50.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-junit": "^16.0.0",
|
||||
"pino-pretty": "^10.2.0",
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
"node": ">=14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.4",
|
||||
"@types/jest": "^29.5.5",
|
||||
"babel-preset-link": "*",
|
||||
"eslint-config-link": "*",
|
||||
"jest-config-link": "*",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
"test": "echo n/a"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/backoff": "^2.5.2",
|
||||
"@types/backoff": "^2.5.3",
|
||||
"babel-preset-link": "*",
|
||||
"camelcase": "^8.0.0",
|
||||
"eslint-config-link": "*",
|
||||
|
|
@ -39,8 +39,8 @@
|
|||
"backoff": "^2.5.0",
|
||||
"camelcase-keys": "^9.0.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"snakecase-keys": "^5.4.6",
|
||||
"snakecase-keys": "^5.4.7",
|
||||
"ts-custom-error": "^3.3.1",
|
||||
"uuid": "^9.0.0"
|
||||
"uuid": "^9.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
# zammad-addon-pgp
|
||||
|
||||
Adds PGP integration into [Zammad](https://zammad.org) via [Sequoia](https://sequoia-pgp.org).
|
||||
|
||||
## Configuration
|
||||
|
||||
Once PGP addon has been successfully installed, there are a few steps required to set it up for use. This is also assuming that Zammad has already been correctly configured for sending and receiving email, and that you have command-line access to a system with the [gnupg](https://gnupg.org) client installed for generating and manipulating keys.
|
||||
|
||||
### For Thunderbird users
|
||||
|
||||
If you generated and manage your key through a current version of Thunderbird (see [here](https://support.mozilla.org/en-US/kb/openpgp-thunderbird-howto-and-faq) for general information on how to generate keys and use PGP in versions of Thunderbird 78 and above), you will need to use the following steps to prepare for the instructions below assuming you already have a key:
|
||||
|
||||
1. Go to `Account Settings -> End-To-End Encryption` for the relevant account and click the 'OpenPGP Key Manager' button to access the key manager.
|
||||
2. Click the relevant key and select `File -> Backup Secret Key(s) to File` and select a place to save the key file.
|
||||
3. It will ask you to set a passphrase for the key, make sure to note this down for use below.
|
||||
4. After the key has been saved to a file, go to that file in a terminal and run `gpg --import <key file>`. It will ask you for the passphrase you just set.
|
||||
5. Go to #2 below and continue with the instructions.
|
||||
|
||||
### Generate helpdesk key
|
||||
|
||||
In order to receive encrypted email, the Zammad helpdesk must have a PGP key associated with its email address. You can follow the instructions [here](https://docs.github.com/en/authentication/managing-commit-signature-verification/generating-a-new-gpg-key) in order to generate such a key if you do not already have one; when you get to the step where you enter your email address, use the email address associated with your Zammad installation.
|
||||
|
||||
1. Generate the key, using the instructions above. Make a note of the corresponding keyid.
|
||||
2. You'll need to remove the passphrase from the private key before adding it to Zammad. To do this, edit the key: `gpg --edit-key <keyid>`
|
||||
3. In the resulting prompt, type `passwd`. Enter the passphrasse you set during key generation, and when you are prompted for a new passphrase just leave it blank and hit 'enter' twice and confirm.
|
||||
4. As an admin user, go to the Zammad settings panel and under `Channels -> PGP Support` and click the `Add Key` button at the top.
|
||||
5. In your terminal, export the private key with `gpg --export-secret-key --armor <keyid>` and paste the entire resulting text block including the header and footer into the box for the private key. Do the same for the public key box by exporting with `gpg --export --armor <keyid>`. Select the group the key is associated with ('Users' by default).
|
||||
6. Submit the changes
|
||||
|
||||
Now your helpdesk is configured to accept encrypted email!
|
||||
|
||||
### Set user keys
|
||||
|
||||
In order to send an encrypted reply to a user who has submitted a ticket, they must have a public key configured. Either that user or an admin can go to settings under `Manage -> Users`, select the user account, and paste their public key in the `PGP Public Key` box. The key can be exported by the user by running `gpg --export --armor <keyid>`, where <keyid> is the keyid of the key associated with the email address with which they are sending an email to the helpdesk. Then submit the changes.
|
||||
|
||||
The helpdesk can now send encrypted email to that user!
|
||||
|
||||
## Help and Support
|
||||
|
||||
Join us in our public matrix channel [#cdr-link-dev-support:matrix.org](https://matrix.to/#/#cdr-link-dev-support:matrix.org?via=matrix.org&via=neo.keanu.im).
|
||||
|
||||
## License
|
||||
|
||||
[](https://gitlab.com/digiresilience/link/zamamd-addon-sigarillo/blob/master/LICENSE.md)
|
||||
|
||||
This is a free software project licensed under the GNU Affero General
|
||||
Public License v3.0 (GNU AGPLv3) by [The Center for Digital
|
||||
Resilience](https://digiresilience.org) and [Guardian
|
||||
Project](https://guardianproject.info).
|
||||
|
||||
🤸
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"name": "zammad-addon-pgp",
|
||||
"displayName": "PGP",
|
||||
"version": "2.0.0",
|
||||
"description": "Adds PGP integration into [Zammad](https://zammad.org) via [Sequoia](https://sequoia-pgp.org).",
|
||||
"scripts": {
|
||||
"migrate": "node ../../node_modules/zammad-addon-common/dist/migrate.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"zammad-addon-common": "*"
|
||||
},
|
||||
"author": "",
|
||||
"license": "AGPL-3.0-or-later"
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
3.1.3
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
ruby '3.1.3'
|
||||
|
||||
gem 'ruby_openpgp', git: 'https://github.com/throneless-tech/ruby_openpgp'
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
PATH
|
||||
remote: /Users/rae/Sites/Throneless/ruby_openpgp
|
||||
specs:
|
||||
ruby_openpgp (0.1.0)
|
||||
ffi (~> 1)
|
||||
rake (~> 13)
|
||||
rspec (~> 3)
|
||||
rubocop (~> 1.7)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
ast (2.4.2)
|
||||
diff-lcs (1.4.4)
|
||||
ffi (1.15.0)
|
||||
parallel (1.20.1)
|
||||
parser (3.0.1.1)
|
||||
ast (~> 2.4.1)
|
||||
rainbow (3.0.0)
|
||||
rake (13.0.3)
|
||||
regexp_parser (2.1.1)
|
||||
rexml (3.2.5)
|
||||
rspec (3.10.0)
|
||||
rspec-core (~> 3.10.0)
|
||||
rspec-expectations (~> 3.10.0)
|
||||
rspec-mocks (~> 3.10.0)
|
||||
rspec-core (3.10.1)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-expectations (3.10.1)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-mocks (3.10.2)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-support (3.10.2)
|
||||
rubocop (1.14.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.0.0.0)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml
|
||||
rubocop-ast (>= 1.5.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 3.0)
|
||||
rubocop-ast (1.5.0)
|
||||
parser (>= 3.0.1.1)
|
||||
ruby-progressbar (1.11.0)
|
||||
unicode-display_width (2.0.0)
|
||||
|
||||
PLATFORMS
|
||||
x86_64-darwin-18
|
||||
|
||||
DEPENDENCIES
|
||||
ruby_openpgp!
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.6.5p114
|
||||
|
||||
BUNDLED WITH
|
||||
2.2.9
|
||||
|
|
@ -1,256 +0,0 @@
|
|||
class Index extends App.ControllerIntegrationBase
|
||||
featureIntegration: 'pgp_integration'
|
||||
featureName: 'PGP'
|
||||
featureConfig: 'pgp_config'
|
||||
description: [
|
||||
['PGP (Pretty Good Privacy) is a widely accepted method (or more precisely, a protocol) for sending digitally signed and encrypted messages.']
|
||||
]
|
||||
events:
|
||||
'change .js-switch input': 'switch'
|
||||
|
||||
render: =>
|
||||
super
|
||||
new Form(
|
||||
el: @$('.js-form')
|
||||
)
|
||||
|
||||
new App.HttpLog(
|
||||
el: @$('.js-log')
|
||||
facility: 'PGP'
|
||||
)
|
||||
|
||||
class Form extends App.Controller
|
||||
events:
|
||||
'click .js-addPublicKey': 'addPublicKey'
|
||||
'click .js-addPrivateKey': 'addPrivateKey'
|
||||
'click .js-updateGroup': 'updateGroup'
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
@render()
|
||||
|
||||
currentConfig: ->
|
||||
App.Setting.get('pgp_config')
|
||||
|
||||
setConfig: (value) ->
|
||||
App.Setting.set('pgp_config', value, {notify: true})
|
||||
|
||||
render: =>
|
||||
@config = @currentConfig()
|
||||
|
||||
@html App.view('integration/pgp')(
|
||||
config: @config
|
||||
)
|
||||
@keyList()
|
||||
@groupList()
|
||||
|
||||
keyList: =>
|
||||
new List(el: @$('.js-keyList'))
|
||||
|
||||
groupList: =>
|
||||
new Group(
|
||||
el: @$('.js-groupList')
|
||||
config: @config
|
||||
)
|
||||
|
||||
addPublicKey: =>
|
||||
new PublicKey(
|
||||
callback: @keyList
|
||||
)
|
||||
|
||||
addPrivateKey: =>
|
||||
new PrivateKey(
|
||||
callback: @keyList
|
||||
)
|
||||
|
||||
updateGroup: (e) =>
|
||||
params = App.ControllerForm.params(e)
|
||||
@setConfig(params)
|
||||
|
||||
class PublicKey extends App.ControllerModal
|
||||
buttonClose: true
|
||||
buttonCancel: true
|
||||
buttonSubmit: 'Add'
|
||||
autoFocusOnFirstInput: false
|
||||
head: 'Add Public Key'
|
||||
large: true
|
||||
|
||||
content: ->
|
||||
|
||||
# show start dialog
|
||||
content = $(App.view('integration/pgp_public_key_add')(
|
||||
head: 'Add Public Key'
|
||||
))
|
||||
content
|
||||
|
||||
onSubmit: (e) =>
|
||||
params = new FormData($(e.currentTarget).closest('form').get(0))
|
||||
params.set('try', true)
|
||||
if _.isEmpty(params.get('data'))
|
||||
params.delete('data')
|
||||
@formDisable(e)
|
||||
|
||||
@ajax(
|
||||
id: 'pgp-public_key-add'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/integration/pgp/public_key"
|
||||
processData: false
|
||||
contentType: false
|
||||
cache: false
|
||||
data: params
|
||||
success: (data, status, xhr) =>
|
||||
@close()
|
||||
@callback()
|
||||
error: (data) =>
|
||||
@close()
|
||||
details = data.responseJSON || {}
|
||||
@notify
|
||||
type: 'error'
|
||||
msg: App.i18n.translateContent(details.error_human || details.error || 'The import failed.')
|
||||
timeout: 6000
|
||||
)
|
||||
|
||||
class PrivateKey extends App.ControllerModal
|
||||
buttonClose: true
|
||||
buttonCancel: true
|
||||
buttonSubmit: 'Add'
|
||||
autoFocusOnFirstInput: false
|
||||
head: 'Add Private Key'
|
||||
large: true
|
||||
|
||||
content: ->
|
||||
|
||||
# show start dialog
|
||||
content = $(App.view('integration/pgp_private_key_add')(
|
||||
head: 'Add Private Key'
|
||||
))
|
||||
content
|
||||
|
||||
onSubmit: (e) =>
|
||||
params = new FormData($(e.currentTarget).closest('form').get(0))
|
||||
params.set('try', true)
|
||||
if _.isEmpty(params.get('data'))
|
||||
params.delete('data')
|
||||
@formDisable(e)
|
||||
|
||||
@ajax(
|
||||
id: 'pgp-private_key-add'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/integration/pgp/private_key"
|
||||
processData: false
|
||||
contentType: false
|
||||
cache: false
|
||||
data: params
|
||||
success: (data, status, xhr) =>
|
||||
@close()
|
||||
@callback()
|
||||
error: (data) =>
|
||||
@close()
|
||||
details = data.responseJSON || {}
|
||||
@notify
|
||||
type: 'error'
|
||||
msg: App.i18n.translateContent(details.error_human || details.error || 'The import failed.')
|
||||
timeout: 6000
|
||||
)
|
||||
|
||||
|
||||
class List extends App.Controller
|
||||
events:
|
||||
'click .js-remove': 'remove'
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
@load()
|
||||
|
||||
load: =>
|
||||
@ajax(
|
||||
id: 'pgp-list'
|
||||
type: 'GET'
|
||||
url: "#{@apiPath}/integration/pgp/public_key"
|
||||
success: (data, status, xhr) =>
|
||||
@render(data)
|
||||
|
||||
error: (data, status) =>
|
||||
|
||||
# do not close window if request is aborted
|
||||
return if status is 'abort'
|
||||
|
||||
details = data.responseJSON || {}
|
||||
@notify(
|
||||
type: 'error'
|
||||
msg: App.i18n.translateContent(details.error_human || details.error || 'Loading failed.')
|
||||
)
|
||||
|
||||
# do something
|
||||
)
|
||||
|
||||
render: (data) =>
|
||||
@html App.view('integration/pgp_list')(
|
||||
keyPairs: data
|
||||
)
|
||||
|
||||
remove: (e) =>
|
||||
e.preventDefault()
|
||||
id = $(e.currentTarget).parents('tr').data('id')
|
||||
return if !id
|
||||
|
||||
@ajax(
|
||||
id: 'pgp-list'
|
||||
type: 'DELETE'
|
||||
url: "#{@apiPath}/integration/pgp/public_key"
|
||||
data: JSON.stringify(id: id)
|
||||
success: (data, status, xhr) =>
|
||||
@load()
|
||||
|
||||
error: (data, status) =>
|
||||
|
||||
# do not close window if request is aborted
|
||||
return if status is 'abort'
|
||||
|
||||
details = data.responseJSON || {}
|
||||
@notify(
|
||||
type: 'error'
|
||||
msg: App.i18n.translateContent(details.error_human || details.error || 'Server operation failed.')
|
||||
)
|
||||
)
|
||||
|
||||
class Group extends App.Controller
|
||||
constructor: ->
|
||||
super
|
||||
@render()
|
||||
|
||||
render: (data) =>
|
||||
groups = App.Group.search(sortBy: 'name', filter: active: true)
|
||||
@html App.view('integration/pgp_group')(
|
||||
groups: groups
|
||||
)
|
||||
for group in groups
|
||||
for type, selector of { default_sign: 'js-signDefault', default_encryption: 'js-encryptionDefault' }
|
||||
selected = true
|
||||
if @config?.group_id && @config.group_id[type]
|
||||
selected = @config.group_id[type][group.id.toString()]
|
||||
selection = App.UiElement.boolean.render(
|
||||
name: "group_id::#{type}::#{group.id}"
|
||||
multiple: false
|
||||
null: false
|
||||
nulloption: false
|
||||
value: selected
|
||||
class: 'form-control--small'
|
||||
)
|
||||
@$("[data-id=#{group.id}] .#{selector}").html(selection)
|
||||
|
||||
class State
|
||||
@current: ->
|
||||
App.Setting.get('pgp_integration')
|
||||
|
||||
App.Config.set(
|
||||
'Integrationpgp'
|
||||
{
|
||||
name: 'PGP'
|
||||
target: '#system/integration/pgp'
|
||||
description: 'PGP enables you to send digitally signed and encrypted messages.'
|
||||
controller: Index
|
||||
state: State
|
||||
}
|
||||
'NavBarIntegrations'
|
||||
)
|
||||
|
|
@ -1,658 +0,0 @@
|
|||
# coffeelint: disable=camel_case_classes
|
||||
|
||||
###
|
||||
|
||||
UI Element options:
|
||||
|
||||
**attribute.notification**
|
||||
|
||||
- Allows to send notifications (default: false)
|
||||
|
||||
**attribute.ticket_delete**
|
||||
|
||||
- Allows to delete the ticket (default: false)
|
||||
|
||||
**attribute.user_action**
|
||||
|
||||
- Allows pre conditions like current_user.id or user session specific values (default: true)
|
||||
|
||||
**attribute.article_body_cc_only**
|
||||
|
||||
- Renders only article body and cc attributes (default: false)
|
||||
|
||||
**attribute.no_dates**
|
||||
|
||||
- Does not include `date` and `datetime` attributes (default: false)
|
||||
|
||||
**attribute.no_richtext_uploads**
|
||||
|
||||
- Removes support for uploads in richtext attributes (default: false)
|
||||
|
||||
**attribute.sender_type**
|
||||
|
||||
- Includes sender type as a ticket attribute (default: false)
|
||||
|
||||
**attribute.simple_attribute_selector**
|
||||
|
||||
- Renders a simpler attribute without operator support (default: false)
|
||||
|
||||
**attribute.skip_unknown_attributes**
|
||||
|
||||
- Skips rendering of unknown attributes (default: false)
|
||||
|
||||
###
|
||||
|
||||
class App.UiElement.ApplicationAction
|
||||
@defaults: (attribute) ->
|
||||
defaults = ['ticket.state_id']
|
||||
|
||||
groups =
|
||||
ticket:
|
||||
name: __('Ticket')
|
||||
model: 'Ticket'
|
||||
article:
|
||||
name: __('Article')
|
||||
model: if attribute.article_body_cc_only then 'TicketArticle' else 'Article'
|
||||
|
||||
if attribute.notification
|
||||
groups.notification =
|
||||
name: __('Notification')
|
||||
model: 'Notification'
|
||||
|
||||
# merge config
|
||||
elements = {}
|
||||
for groupKey, groupMeta of groups
|
||||
if !groupMeta.model || !App[groupMeta.model]
|
||||
if groupKey is 'notification'
|
||||
elements["#{groupKey}.email"] = { name: 'email', display: __('Email') }
|
||||
elements["#{groupKey}.sms"] = { name: 'sms', display: __('SMS') }
|
||||
elements["#{groupKey}.webhook"] = { name: 'webhook', display: __('Webhook') }
|
||||
else if groupKey is 'article'
|
||||
elements["#{groupKey}.note"] = { name: 'note', display: __('Note') }
|
||||
else
|
||||
|
||||
for row in App[groupMeta.model].configure_attributes
|
||||
|
||||
# ignore all article attributes except body and cc
|
||||
if attribute.article_body_cc_only
|
||||
if groupMeta.model is 'TicketArticle'
|
||||
if row.name isnt 'body' and row.name isnt 'cc'
|
||||
continue
|
||||
|
||||
# ignore all date and datetime attributes
|
||||
if attribute.no_dates
|
||||
if row.tag is 'date' || row.tag is 'datetime'
|
||||
continue
|
||||
|
||||
# ignore passwords and relations
|
||||
if row.type isnt 'password' && row.name.substr(row.name.length-4,4) isnt '_ids'
|
||||
|
||||
# ignore readonly attributes
|
||||
if !row.readonly
|
||||
config = _.clone(row)
|
||||
|
||||
# disable uploads in richtext attributes
|
||||
if attribute.no_richtext_uploads
|
||||
if config.tag is 'richtext'
|
||||
config.upload = false
|
||||
|
||||
switch config.tag
|
||||
when 'datetime'
|
||||
config.operator = ['static', 'relative']
|
||||
when 'tag'
|
||||
config.operator = ['add', 'remove']
|
||||
|
||||
elements["#{groupKey}.#{config.name}"] = config
|
||||
|
||||
# add ticket deletion action
|
||||
if attribute.ticket_delete
|
||||
elements['ticket.action'] =
|
||||
name: 'action'
|
||||
display: __('Action')
|
||||
tag: 'select'
|
||||
null: false
|
||||
translate: true
|
||||
options:
|
||||
delete: 'Delete'
|
||||
|
||||
# add sender type selection as a ticket attribute
|
||||
if attribute.sender_type
|
||||
elements['ticket.formSenderType'] =
|
||||
name: 'formSenderType'
|
||||
display: __('Sender Type')
|
||||
tag: 'select'
|
||||
null: false
|
||||
translate: true
|
||||
options: [
|
||||
{ value: 'phone-in', name: __('Inbound Call') },
|
||||
{ value: 'phone-out', name: __('Outbound Call') },
|
||||
{ value: 'email-out', name: __('Email') },
|
||||
]
|
||||
|
||||
[defaults, groups, elements]
|
||||
|
||||
@placeholder: (elementFull, attribute, params, groups, elements) ->
|
||||
item = $( App.view('generic/ticket_perform_action/row')( attribute: attribute ) )
|
||||
selector = @buildAttributeSelector(elementFull, groups, elements)
|
||||
item.find('.js-attributeSelector').prepend(selector)
|
||||
item
|
||||
|
||||
@render: (attribute, params = {}) ->
|
||||
|
||||
[defaults, groups, elements] = @defaults(attribute)
|
||||
|
||||
# return item
|
||||
item = $( App.view('generic/ticket_perform_action/index')( attribute: attribute ) )
|
||||
|
||||
# add filter
|
||||
item.on('click', '.js-rowActions .js-add', (e) =>
|
||||
element = $(e.target).closest('.js-filterElement')
|
||||
placeholder = @placeholder(item, attribute, params, groups, elements)
|
||||
if element.get(0)
|
||||
element.after(placeholder)
|
||||
else
|
||||
item.append(placeholder)
|
||||
placeholder.find('.js-attributeSelector select').trigger('change')
|
||||
@updateAttributeSelectors(item)
|
||||
)
|
||||
|
||||
# remove filter
|
||||
item.on('click', '.js-rowActions .js-remove', (e) =>
|
||||
return if $(e.currentTarget).hasClass('is-disabled')
|
||||
$(e.target).closest('.js-filterElement').remove()
|
||||
@updateAttributeSelectors(item)
|
||||
)
|
||||
|
||||
# change attribute selector
|
||||
item.on('change', '.js-attributeSelector select', (e) =>
|
||||
elementRow = $(e.target).closest('.js-filterElement')
|
||||
groupAndAttribute = elementRow.find('.js-attributeSelector option:selected').attr('value')
|
||||
@rebuildAttributeSelectors(item, elementRow, groupAndAttribute, elements, {}, attribute)
|
||||
@updateAttributeSelectors(item)
|
||||
)
|
||||
|
||||
# change operator selector
|
||||
item.on('change', '.js-operator select', (e) =>
|
||||
elementRow = $(e.target).closest('.js-filterElement')
|
||||
groupAndAttribute = elementRow.find('.js-attributeSelector option:selected').attr('value')
|
||||
@buildOperator(item, elementRow, groupAndAttribute, elements, {}, attribute)
|
||||
)
|
||||
|
||||
# build initial params
|
||||
if _.isEmpty(params[attribute.name])
|
||||
|
||||
for groupAndAttribute in defaults
|
||||
|
||||
# build and append
|
||||
element = @placeholder(item, attribute, params, groups, elements)
|
||||
item.append(element)
|
||||
@rebuildAttributeSelectors(item, element, groupAndAttribute, elements, {}, attribute)
|
||||
|
||||
else
|
||||
|
||||
for groupAndAttribute, meta of params[attribute.name]
|
||||
# Skip unknown attributes.
|
||||
continue if attribute.skip_unknown_attributes and !_.includes(_.keys(elements), groupAndAttribute)
|
||||
|
||||
# build and append
|
||||
element = @placeholder(item, attribute, params, groups, elements)
|
||||
@rebuildAttributeSelectors(item, element, groupAndAttribute, elements, meta, attribute)
|
||||
item.append(element)
|
||||
|
||||
@disableRemoveForOneAttribute(item)
|
||||
item
|
||||
|
||||
@elementKeyGroup: (elementKey) ->
|
||||
elementKey.split(/\./)[0]
|
||||
|
||||
@buildAttributeSelector: (elementFull, groups, elements) ->
|
||||
|
||||
# find first possible attribute
|
||||
selectedValue = ''
|
||||
elementFull.find('.js-attributeSelector select option').each(->
|
||||
if !selectedValue && !$(@).prop('disabled')
|
||||
selectedValue = $(@).val()
|
||||
)
|
||||
|
||||
selection = $('<select class="form-control"></select>')
|
||||
for groupKey, groupMeta of groups
|
||||
displayName = App.i18n.translateInline(groupMeta.name)
|
||||
selection.closest('select').append("<optgroup label=\"#{displayName}\" class=\"js-#{groupKey}\"></optgroup>")
|
||||
optgroup = selection.find("optgroup.js-#{groupKey}")
|
||||
for elementKey, elementGroup of elements
|
||||
elementGroup = @elementKeyGroup(elementKey)
|
||||
if elementGroup is groupKey
|
||||
attributeConfig = elements[elementKey]
|
||||
displayName = App.i18n.translateInline(attributeConfig.display)
|
||||
|
||||
selected = ''
|
||||
if elementKey is selectedValue
|
||||
selected = 'selected="selected"'
|
||||
optgroup.append("<option value=\"#{elementKey}\" #{selected}>#{displayName}</option>")
|
||||
selection
|
||||
|
||||
# disable - if we only have one attribute
|
||||
@disableRemoveForOneAttribute: (elementFull) ->
|
||||
if elementFull.find('.js-attributeSelector select').length > 1
|
||||
elementFull.find('.js-remove').removeClass('is-disabled')
|
||||
else
|
||||
elementFull.find('.js-remove').addClass('is-disabled')
|
||||
|
||||
@updateAttributeSelectors: (elementFull) ->
|
||||
|
||||
# enable all
|
||||
elementFull.find('.js-attributeSelector select option').prop('disabled', false)
|
||||
|
||||
# disable all used attributes
|
||||
elementFull.find('.js-attributeSelector select').each(->
|
||||
keyLocal = $(@).val()
|
||||
elementFull.find('.js-attributeSelector select option[value="' + keyLocal + '"]').attr('disabled', true)
|
||||
)
|
||||
|
||||
# disable - if we only have one attribute
|
||||
@disableRemoveForOneAttribute(elementFull)
|
||||
|
||||
@rebuildAttributeSelectors: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||
|
||||
# set attribute
|
||||
if groupAndAttribute
|
||||
elementRow.find('.js-attributeSelector select').val(groupAndAttribute)
|
||||
|
||||
notificationTypeMatch = groupAndAttribute.match(/^notification.([\w]+)$/)
|
||||
articleTypeMatch = groupAndAttribute.match(/^article.([\w]+)$/)
|
||||
|
||||
if _.isArray(notificationTypeMatch) && notificationType = notificationTypeMatch[1]
|
||||
elementRow.find('.js-setAttribute').html('').addClass('hide')
|
||||
elementRow.find('.js-setArticle').html('').addClass('hide')
|
||||
@buildNotificationArea(notificationType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
else if !attribute.article_body_cc_only && _.isArray(articleTypeMatch) && articleType = articleTypeMatch[1]
|
||||
elementRow.find('.js-setAttribute').html('').addClass('hide')
|
||||
elementRow.find('.js-setNotification').html('').addClass('hide')
|
||||
@buildArticleArea(articleType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
else
|
||||
elementRow.find('.js-setNotification').html('').addClass('hide')
|
||||
elementRow.find('.js-setArticle').html('').addClass('hide')
|
||||
if !elementRow.find('.js-setAttribute div').get(0)
|
||||
attributeSelectorElement = $( App.view('generic/ticket_perform_action/attribute_selector')(
|
||||
attribute: attribute
|
||||
name: name
|
||||
meta: meta || {}
|
||||
))
|
||||
elementRow.find('.js-setAttribute').html(attributeSelectorElement).removeClass('hide')
|
||||
|
||||
if attribute.simple_attribute_selector
|
||||
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
else
|
||||
@buildOperator(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
|
||||
@buildOperator: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||
currentOperator = elementRow.find('.js-operator option:selected').attr('value')
|
||||
|
||||
if !meta.operator
|
||||
meta.operator = currentOperator
|
||||
|
||||
name = "#{attribute.name}::#{groupAndAttribute}::operator"
|
||||
|
||||
selection = $("<select class=\"form-control\" name=\"#{name}\"></select>")
|
||||
attributeConfig = elements[groupAndAttribute]
|
||||
if !attributeConfig || !attributeConfig.operator
|
||||
elementRow.find('.js-operator').parent().addClass('hide')
|
||||
else
|
||||
elementRow.find('.js-operator').parent().removeClass('hide')
|
||||
if attributeConfig && attributeConfig.operator
|
||||
for operator in attributeConfig.operator
|
||||
operatorName = App.i18n.translateInline(operator)
|
||||
selected = ''
|
||||
if meta.operator is operator
|
||||
selected = 'selected="selected"'
|
||||
selection.append("<option value=\"#{operator}\" #{selected}>#{operatorName}</option>")
|
||||
selection
|
||||
|
||||
elementRow.find('.js-operator select').replaceWith(selection)
|
||||
|
||||
@buildPreCondition(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
|
||||
@buildPreCondition: (elementFull, elementRow, groupAndAttribute, elements, meta, attributeConfig) ->
|
||||
currentOperator = elementRow.find('.js-operator option:selected').attr('value')
|
||||
currentPreCondition = elementRow.find('.js-preCondition option:selected').attr('value')
|
||||
|
||||
if !meta.pre_condition
|
||||
meta.pre_condition = currentPreCondition
|
||||
|
||||
toggleValue = =>
|
||||
preCondition = elementRow.find('.js-preCondition option:selected').attr('value')
|
||||
if preCondition isnt 'specific'
|
||||
elementRow.find('.js-value select').html('')
|
||||
elementRow.find('.js-value').addClass('hide')
|
||||
else
|
||||
elementRow.find('.js-value').removeClass('hide')
|
||||
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
|
||||
# force to use auto complition on user lookup
|
||||
attribute = clone(attributeConfig, true)
|
||||
|
||||
name = "#{attribute.name}::#{groupAndAttribute}::value"
|
||||
attributeSelected = elements[groupAndAttribute]
|
||||
|
||||
preCondition = false
|
||||
if attributeSelected?.relation is 'User'
|
||||
preCondition = 'user'
|
||||
attribute.tag = 'user_autocompletion'
|
||||
if attributeSelected?.relation is 'Organization'
|
||||
preCondition = 'org'
|
||||
attribute.tag = 'autocompletion_ajax'
|
||||
if !preCondition || attribute.user_action is false
|
||||
elementRow.find('.js-preCondition select').html('')
|
||||
elementRow.find('.js-preCondition').closest('.controls').addClass('hide')
|
||||
toggleValue()
|
||||
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
return
|
||||
|
||||
elementRow.find('.js-preCondition').closest('.controls').removeClass('hide')
|
||||
name = "#{attribute.name}::#{groupAndAttribute}::pre_condition"
|
||||
|
||||
selection = $("<select class=\"form-control\" name=\"#{name}\" ></select>")
|
||||
options = {}
|
||||
if preCondition is 'user'
|
||||
options =
|
||||
'current_user.id': App.i18n.translateInline('current user')
|
||||
'specific': App.i18n.translateInline('specific user')
|
||||
|
||||
if attributeSelected.null is true
|
||||
options['not_set'] = App.i18n.translateInline('unassign user')
|
||||
|
||||
else if preCondition is 'org'
|
||||
options =
|
||||
'current_user.organization_id': App.i18n.translateInline('current user organization')
|
||||
'specific': App.i18n.translateInline('specific organization')
|
||||
|
||||
for key, value of options
|
||||
selected = ''
|
||||
if key is meta.pre_condition
|
||||
selected = 'selected="selected"'
|
||||
selection.append("<option value=\"#{key}\" #{selected}>#{App.i18n.translateInline(value)}</option>")
|
||||
elementRow.find('.js-preCondition').closest('.controls').removeClass('hide')
|
||||
elementRow.find('.js-preCondition select').replaceWith(selection)
|
||||
|
||||
elementRow.find('.js-preCondition select').on('change', (e) ->
|
||||
toggleValue()
|
||||
)
|
||||
|
||||
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
toggleValue()
|
||||
|
||||
@buildValue: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||
name = "#{attribute.name}::#{groupAndAttribute}::value"
|
||||
|
||||
# build new item
|
||||
attributeConfig = elements[groupAndAttribute]
|
||||
config = clone(attributeConfig, true)
|
||||
|
||||
if config?.relation is 'User'
|
||||
config.tag = 'user_autocompletion'
|
||||
config.disableCreateObject = true
|
||||
if config?.relation is 'Organization'
|
||||
config.tag = 'autocompletion_ajax'
|
||||
|
||||
# render ui element
|
||||
item = ''
|
||||
if config && App.UiElement[config.tag]
|
||||
config['name'] = name
|
||||
if attribute.value && attribute.value[groupAndAttribute]
|
||||
config['value'] = _.clone(attribute.value[groupAndAttribute]['value'])
|
||||
config.multiple = false
|
||||
config.default = undefined
|
||||
config.nulloption = config.null
|
||||
if config.tag is 'multiselect' || config.tag is 'multi_tree_select'
|
||||
config.multiple = true
|
||||
if config.tag is 'checkbox'
|
||||
config.tag = 'select'
|
||||
if config.tag is 'datetime'
|
||||
config.validationContainer = 'self'
|
||||
item = App.UiElement[config.tag].render(config, {})
|
||||
|
||||
relative_operators = [
|
||||
__('before (relative)'),
|
||||
__('within next (relative)'),
|
||||
__('within last (relative)'),
|
||||
__('after (relative)'),
|
||||
__('till (relative)'),
|
||||
__('from (relative)'),
|
||||
__('relative'),
|
||||
]
|
||||
|
||||
upcoming_operator = meta?.operator
|
||||
|
||||
if !_.include(config?.operator, upcoming_operator)
|
||||
if Array.isArray(config?.operator)
|
||||
upcoming_operator = config.operator[0]
|
||||
else
|
||||
upcoming_operator = null
|
||||
|
||||
if _.include(relative_operators, upcoming_operator)
|
||||
config['name'] = "#{attribute.name}::#{groupAndAttribute}"
|
||||
if attribute.value && attribute.value[groupAndAttribute]
|
||||
config['value'] = _.clone(attribute.value[groupAndAttribute])
|
||||
item = App.UiElement['time_range'].render(config, {})
|
||||
|
||||
elementRow.find('.js-setAttribute > .flex > .js-value').removeClass('hide').html(item)
|
||||
|
||||
@buildNotificationArea: (notificationType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||
|
||||
return if elementRow.find(".js-setNotification .js-body-#{notificationType}").get(0)
|
||||
|
||||
elementRow.find('.js-setNotification').empty()
|
||||
|
||||
options =
|
||||
'article_last_sender': __('Sender of last article')
|
||||
'ticket_owner': __('Owner')
|
||||
'ticket_customer': __('Customer')
|
||||
'ticket_agents': __('All agents')
|
||||
|
||||
name = "#{attribute.name}::notification.#{notificationType}"
|
||||
|
||||
messageLength = switch notificationType
|
||||
when 'sms' then 160
|
||||
else 200000
|
||||
|
||||
# meta.recipient was a string in the past (single-select) so we convert it to array if needed
|
||||
if !_.isArray(meta.recipient)
|
||||
meta.recipient = [meta.recipient]
|
||||
|
||||
columnSelectOptions = []
|
||||
for key, value of options
|
||||
selected = undefined
|
||||
for recipient in meta.recipient
|
||||
if key is recipient
|
||||
selected = true
|
||||
columnSelectOptions.push({ value: key, name: App.i18n.translatePlain(value), selected: selected })
|
||||
|
||||
columnSelectRecipientUserOptions = []
|
||||
for user in App.User.all()
|
||||
key = "userid_#{user.id}"
|
||||
selected = undefined
|
||||
for recipient in meta.recipient
|
||||
if key is recipient
|
||||
selected = true
|
||||
columnSelectRecipientUserOptions.push({ value: key, name: "#{user.firstname} #{user.lastname}", selected: selected })
|
||||
|
||||
columnSelectRecipient = new App.ColumnSelect
|
||||
attribute:
|
||||
name: "#{name}::recipient"
|
||||
options: [
|
||||
{
|
||||
label: __('Variables'),
|
||||
group: columnSelectOptions
|
||||
},
|
||||
{
|
||||
label: __('User'),
|
||||
group: columnSelectRecipientUserOptions
|
||||
},
|
||||
]
|
||||
|
||||
selectionRecipient = columnSelectRecipient.element()
|
||||
|
||||
if notificationType is 'webhook'
|
||||
notificationElement = $( App.view('generic/ticket_perform_action/webhook')(
|
||||
attribute: attribute
|
||||
name: name
|
||||
notificationType: notificationType
|
||||
meta: meta || {}
|
||||
))
|
||||
|
||||
notificationElement.find('.js-recipient select').replaceWith(selectionRecipient)
|
||||
|
||||
|
||||
if App.Webhook.search(filter: { active: true }).length isnt 0 || !_.isEmpty(meta.webhook_id)
|
||||
webhookSelection = App.UiElement.select.render(
|
||||
name: "#{name}::webhook_id"
|
||||
multiple: false
|
||||
null: false
|
||||
relation: 'Webhook'
|
||||
value: meta.webhook_id
|
||||
translate: false
|
||||
nulloption: true
|
||||
)
|
||||
else
|
||||
webhookSelection = App.view('generic/ticket_perform_action/webhook_not_available')( attribute: attribute )
|
||||
|
||||
notificationElement.find('.js-webhooks').html(webhookSelection)
|
||||
|
||||
else
|
||||
notificationElement = $( App.view('generic/ticket_perform_action/notification')(
|
||||
attribute: attribute
|
||||
name: name
|
||||
notificationType: notificationType
|
||||
meta: meta || {}
|
||||
))
|
||||
|
||||
notificationElement.find('.js-recipient select').replaceWith(selectionRecipient)
|
||||
|
||||
visibilitySelection = App.UiElement.select.render(
|
||||
name: "#{name}::internal"
|
||||
multiple: false
|
||||
null: false
|
||||
options: { true: __('internal'), false: __('public') }
|
||||
value: meta.internal || 'false'
|
||||
translate: true
|
||||
)
|
||||
|
||||
includeAttachmentsCheckbox = App.UiElement.select.render(
|
||||
name: "#{name}::include_attachments"
|
||||
multiple: false
|
||||
null: false
|
||||
options: { true: __('Yes'), false: __('No') }
|
||||
value: meta.include_attachments || 'false'
|
||||
translate: true
|
||||
)
|
||||
|
||||
notificationElement.find('.js-internal').html(visibilitySelection)
|
||||
notificationElement.find('.js-include_attachments').html(includeAttachmentsCheckbox)
|
||||
|
||||
notificationElement.find('.js-body div[contenteditable="true"]').ce(
|
||||
mode: 'richtext'
|
||||
placeholder: __('message')
|
||||
maxlength: messageLength
|
||||
)
|
||||
new App.WidgetPlaceholder(
|
||||
el: notificationElement.find('.js-body div[contenteditable="true"]').parent()
|
||||
objects: [
|
||||
{
|
||||
prefix: 'ticket'
|
||||
object: 'Ticket'
|
||||
display: __('Ticket')
|
||||
},
|
||||
{
|
||||
prefix: 'article'
|
||||
object: 'TicketArticle'
|
||||
display: __('Article')
|
||||
},
|
||||
{
|
||||
prefix: 'user'
|
||||
object: 'User'
|
||||
display: __('Current User')
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
elementRow.find('.js-setNotification').html(notificationElement).removeClass('hide')
|
||||
|
||||
if App.Config.get('smime_integration') == true || App.Config.get('pgp_integration') == true
|
||||
selection = App.UiElement.select.render(
|
||||
name: "#{name}::sign"
|
||||
multiple: false
|
||||
options: {
|
||||
'no': __('Do not sign email')
|
||||
'discard': __('Sign email (if not possible, discard notification)')
|
||||
'always': __('Sign email (if not possible, send notification anyway)')
|
||||
}
|
||||
value: meta.sign
|
||||
translate: true
|
||||
)
|
||||
|
||||
elementRow.find('.js-sign').html(selection)
|
||||
|
||||
selection = App.UiElement.select.render(
|
||||
name: "#{name}::encryption"
|
||||
multiple: false
|
||||
options: {
|
||||
'no': __('Do not encrypt email')
|
||||
'discard': __('Encrypt email (if not possible, discard notification)')
|
||||
'always': __('Encrypt email (if not possible, send notification anyway)')
|
||||
}
|
||||
value: meta.encryption
|
||||
translate: true
|
||||
)
|
||||
|
||||
elementRow.find('.js-encryption').html(selection)
|
||||
|
||||
@buildArticleArea: (articleType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||
|
||||
return if elementRow.find(".js-setArticle .js-body-#{articleType}").get(0)
|
||||
|
||||
elementRow.find('.js-setArticle').empty()
|
||||
|
||||
name = "#{attribute.name}::article.#{articleType}"
|
||||
selection = App.UiElement.select.render(
|
||||
name: "#{name}::internal"
|
||||
multiple: false
|
||||
null: false
|
||||
label: __('Visibility')
|
||||
options: { true: 'internal', false: 'public' }
|
||||
value: meta.internal
|
||||
translate: true
|
||||
)
|
||||
articleElement = $( App.view('generic/ticket_perform_action/article')(
|
||||
attribute: attribute
|
||||
name: name
|
||||
articleType: articleType
|
||||
meta: meta || {}
|
||||
))
|
||||
articleElement.find('.js-internal').html(selection)
|
||||
articleElement.find('.js-body div[contenteditable="true"]').ce(
|
||||
mode: 'richtext'
|
||||
placeholder: __('message')
|
||||
maxlength: 200000
|
||||
)
|
||||
new App.WidgetPlaceholder(
|
||||
el: articleElement.find('.js-body div[contenteditable="true"]').parent()
|
||||
objects: [
|
||||
{
|
||||
prefix: 'ticket'
|
||||
object: 'Ticket'
|
||||
display: __('Ticket')
|
||||
},
|
||||
{
|
||||
prefix: 'article'
|
||||
object: 'TicketArticle'
|
||||
display: __('Article')
|
||||
},
|
||||
{
|
||||
prefix: 'user'
|
||||
object: 'User'
|
||||
display: __('Current User')
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
elementRow.find('.js-setArticle').html(articleElement).removeClass('hide')
|
||||
|
|
@ -1,614 +0,0 @@
|
|||
# coffeelint: disable=camel_case_classes
|
||||
class App.UiElement.ticket_perform_action
|
||||
@defaults: (attribute) ->
|
||||
defaults = ['ticket.state_id']
|
||||
|
||||
groups =
|
||||
ticket:
|
||||
name: 'Ticket'
|
||||
model: 'Ticket'
|
||||
article:
|
||||
name: 'Article'
|
||||
model: 'Article'
|
||||
|
||||
if attribute.notification
|
||||
groups.notification =
|
||||
name: 'Notification'
|
||||
model: 'Notification'
|
||||
|
||||
# merge config
|
||||
elements = {}
|
||||
for groupKey, groupMeta of groups
|
||||
if !groupMeta.model || !App[groupMeta.model]
|
||||
if groupKey is 'notification'
|
||||
elements["#{groupKey}.email"] = { name: 'email', display: 'Email' }
|
||||
elements["#{groupKey}.sms"] = { name: 'sms', display: 'SMS' }
|
||||
elements["#{groupKey}.webhook"] = { name: 'webhook', display: 'Webhook' }
|
||||
else if groupKey is 'article'
|
||||
elements["#{groupKey}.note"] = { name: 'note', display: 'Note' }
|
||||
else
|
||||
|
||||
for row in App[groupMeta.model].configure_attributes
|
||||
|
||||
# ignore passwords and relations
|
||||
if row.type isnt 'password' && row.name.substr(row.name.length-4,4) isnt '_ids'
|
||||
|
||||
# ignore readonly attributes
|
||||
if !row.readonly
|
||||
config = _.clone(row)
|
||||
|
||||
switch config.tag
|
||||
when 'datetime'
|
||||
config.operator = ['static', 'relative']
|
||||
when 'tag'
|
||||
config.operator = ['add', 'remove']
|
||||
|
||||
elements["#{groupKey}.#{config.name}"] = config
|
||||
|
||||
# add ticket deletion action
|
||||
if attribute.ticket_delete
|
||||
elements['ticket.action'] =
|
||||
name: 'action'
|
||||
display: 'Action'
|
||||
tag: 'select'
|
||||
null: false
|
||||
translate: true
|
||||
options:
|
||||
delete: 'Delete'
|
||||
|
||||
[defaults, groups, elements]
|
||||
|
||||
@placeholder: (elementFull, attribute, params, groups, elements) ->
|
||||
item = $( App.view('generic/ticket_perform_action/row')( attribute: attribute ) )
|
||||
selector = @buildAttributeSelector(elementFull, groups, elements)
|
||||
item.find('.js-attributeSelector').prepend(selector)
|
||||
item
|
||||
|
||||
@render: (attribute, params = {}) ->
|
||||
|
||||
[defaults, groups, elements] = @defaults(attribute)
|
||||
|
||||
# return item
|
||||
item = $( App.view('generic/ticket_perform_action/index')( attribute: attribute ) )
|
||||
|
||||
# add filter
|
||||
item.on('click', '.js-rowActions .js-add', (e) =>
|
||||
element = $(e.target).closest('.js-filterElement')
|
||||
placeholder = @placeholder(item, attribute, params, groups, elements)
|
||||
if element.get(0)
|
||||
element.after(placeholder)
|
||||
else
|
||||
item.append(placeholder)
|
||||
placeholder.find('.js-attributeSelector select').trigger('change')
|
||||
@updateAttributeSelectors(item)
|
||||
)
|
||||
|
||||
# remove filter
|
||||
item.on('click', '.js-rowActions .js-remove', (e) =>
|
||||
return if $(e.currentTarget).hasClass('is-disabled')
|
||||
$(e.target).closest('.js-filterElement').remove()
|
||||
@updateAttributeSelectors(item)
|
||||
)
|
||||
|
||||
# change attribute selector
|
||||
item.on('change', '.js-attributeSelector select', (e) =>
|
||||
elementRow = $(e.target).closest('.js-filterElement')
|
||||
groupAndAttribute = elementRow.find('.js-attributeSelector option:selected').attr('value')
|
||||
@rebuildAttributeSelectors(item, elementRow, groupAndAttribute, elements, {}, attribute)
|
||||
@updateAttributeSelectors(item)
|
||||
)
|
||||
|
||||
# change operator selector
|
||||
item.on('change', '.js-operator select', (e) =>
|
||||
elementRow = $(e.target).closest('.js-filterElement')
|
||||
groupAndAttribute = elementRow.find('.js-attributeSelector option:selected').attr('value')
|
||||
@buildOperator(item, elementRow, groupAndAttribute, elements, {}, attribute)
|
||||
)
|
||||
|
||||
# build initial params
|
||||
if _.isEmpty(params[attribute.name])
|
||||
|
||||
for groupAndAttribute in defaults
|
||||
|
||||
# build and append
|
||||
element = @placeholder(item, attribute, params, groups, elements)
|
||||
item.append(element)
|
||||
@rebuildAttributeSelectors(item, element, groupAndAttribute, elements, {}, attribute)
|
||||
|
||||
else
|
||||
|
||||
for groupAndAttribute, meta of params[attribute.name]
|
||||
|
||||
# build and append
|
||||
element = @placeholder(item, attribute, params, groups, elements)
|
||||
@rebuildAttributeSelectors(item, element, groupAndAttribute, elements, meta, attribute)
|
||||
item.append(element)
|
||||
|
||||
@disableRemoveForOneAttribute(item)
|
||||
item
|
||||
|
||||
@buildAttributeSelector: (elementFull, groups, elements) ->
|
||||
|
||||
# find first possible attribute
|
||||
selectedValue = ''
|
||||
elementFull.find('.js-attributeSelector select option').each(->
|
||||
if !selectedValue && !$(@).prop('disabled')
|
||||
selectedValue = $(@).val()
|
||||
)
|
||||
|
||||
selection = $('<select class="form-control"></select>')
|
||||
for groupKey, groupMeta of groups
|
||||
displayName = App.i18n.translateInline(groupMeta.name)
|
||||
selection.closest('select').append("<optgroup label=\"#{displayName}\" class=\"js-#{groupKey}\"></optgroup>")
|
||||
optgroup = selection.find("optgroup.js-#{groupKey}")
|
||||
for elementKey, elementGroup of elements
|
||||
spacer = elementKey.split(/\./)
|
||||
if spacer[0] is groupKey
|
||||
attributeConfig = elements[elementKey]
|
||||
displayName = App.i18n.translateInline(attributeConfig.display)
|
||||
|
||||
selected = ''
|
||||
if elementKey is selectedValue
|
||||
selected = 'selected="selected"'
|
||||
optgroup.append("<option value=\"#{elementKey}\" #{selected}>#{displayName}</option>")
|
||||
selection
|
||||
|
||||
# disable - if we only have one attribute
|
||||
@disableRemoveForOneAttribute: (elementFull) ->
|
||||
if elementFull.find('.js-attributeSelector select').length > 1
|
||||
elementFull.find('.js-remove').removeClass('is-disabled')
|
||||
else
|
||||
elementFull.find('.js-remove').addClass('is-disabled')
|
||||
|
||||
@updateAttributeSelectors: (elementFull) ->
|
||||
|
||||
# enable all
|
||||
elementFull.find('.js-attributeSelector select option').prop('disabled', false)
|
||||
|
||||
# disable all used attributes
|
||||
elementFull.find('.js-attributeSelector select').each(->
|
||||
keyLocal = $(@).val()
|
||||
elementFull.find('.js-attributeSelector select option[value="' + keyLocal + '"]').attr('disabled', true)
|
||||
)
|
||||
|
||||
# disable - if we only have one attribute
|
||||
@disableRemoveForOneAttribute(elementFull)
|
||||
|
||||
@rebuildAttributeSelectors: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||
|
||||
# set attribute
|
||||
if groupAndAttribute
|
||||
elementRow.find('.js-attributeSelector select').val(groupAndAttribute)
|
||||
|
||||
notificationTypeMatch = groupAndAttribute.match(/^notification.([\w]+)$/)
|
||||
articleTypeMatch = groupAndAttribute.match(/^article.([\w]+)$/)
|
||||
|
||||
if _.isArray(notificationTypeMatch) && notificationType = notificationTypeMatch[1]
|
||||
elementRow.find('.js-setAttribute').html('').addClass('hide')
|
||||
elementRow.find('.js-setArticle').html('').addClass('hide')
|
||||
@buildNotificationArea(notificationType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
else if _.isArray(articleTypeMatch) && articleType = articleTypeMatch[1]
|
||||
elementRow.find('.js-setAttribute').html('').addClass('hide')
|
||||
elementRow.find('.js-setNotification').html('').addClass('hide')
|
||||
@buildArticleArea(articleType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
else
|
||||
elementRow.find('.js-setNotification').html('').addClass('hide')
|
||||
elementRow.find('.js-setArticle').html('').addClass('hide')
|
||||
if !elementRow.find('.js-setAttribute div').get(0)
|
||||
attributeSelectorElement = $( App.view('generic/ticket_perform_action/attribute_selector')(
|
||||
attribute: attribute
|
||||
name: name
|
||||
meta: meta || {}
|
||||
))
|
||||
elementRow.find('.js-setAttribute').html(attributeSelectorElement).removeClass('hide')
|
||||
@buildOperator(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
|
||||
@buildOperator: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||
currentOperator = elementRow.find('.js-operator option:selected').attr('value')
|
||||
|
||||
if !meta.operator
|
||||
meta.operator = currentOperator
|
||||
|
||||
name = "#{attribute.name}::#{groupAndAttribute}::operator"
|
||||
|
||||
selection = $("<select class=\"form-control\" name=\"#{name}\"></select>")
|
||||
attributeConfig = elements[groupAndAttribute]
|
||||
if !attributeConfig || !attributeConfig.operator
|
||||
elementRow.find('.js-operator').parent().addClass('hide')
|
||||
else
|
||||
elementRow.find('.js-operator').parent().removeClass('hide')
|
||||
if attributeConfig && attributeConfig.operator
|
||||
for operator in attributeConfig.operator
|
||||
operatorName = App.i18n.translateInline(operator)
|
||||
selected = ''
|
||||
if meta.operator is operator
|
||||
selected = 'selected="selected"'
|
||||
selection.append("<option value=\"#{operator}\" #{selected}>#{operatorName}</option>")
|
||||
selection
|
||||
|
||||
elementRow.find('.js-operator select').replaceWith(selection)
|
||||
|
||||
@buildPreCondition(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
|
||||
@buildPreCondition: (elementFull, elementRow, groupAndAttribute, elements, meta, attributeConfig) ->
|
||||
currentOperator = elementRow.find('.js-operator option:selected').attr('value')
|
||||
currentPreCondition = elementRow.find('.js-preCondition option:selected').attr('value')
|
||||
|
||||
if !meta.pre_condition
|
||||
meta.pre_condition = currentPreCondition
|
||||
|
||||
toggleValue = =>
|
||||
preCondition = elementRow.find('.js-preCondition option:selected').attr('value')
|
||||
if preCondition isnt 'specific'
|
||||
elementRow.find('.js-value select').html('')
|
||||
elementRow.find('.js-value').addClass('hide')
|
||||
else
|
||||
elementRow.find('.js-value').removeClass('hide')
|
||||
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
|
||||
# force to use auto complition on user lookup
|
||||
attribute = _.clone(attributeConfig)
|
||||
|
||||
name = "#{attribute.name}::#{groupAndAttribute}::value"
|
||||
attributeSelected = elements[groupAndAttribute]
|
||||
|
||||
preCondition = false
|
||||
if attributeSelected.relation is 'User'
|
||||
preCondition = 'user'
|
||||
attribute.tag = 'user_autocompletion'
|
||||
if attributeSelected.relation is 'Organization'
|
||||
preCondition = 'org'
|
||||
attribute.tag = 'autocompletion_ajax'
|
||||
if !preCondition
|
||||
elementRow.find('.js-preCondition select').html('')
|
||||
elementRow.find('.js-preCondition').closest('.controls').addClass('hide')
|
||||
toggleValue()
|
||||
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
return
|
||||
|
||||
elementRow.find('.js-preCondition').closest('.controls').removeClass('hide')
|
||||
name = "#{attribute.name}::#{groupAndAttribute}::pre_condition"
|
||||
|
||||
selection = $("<select class=\"form-control\" name=\"#{name}\" ></select>")
|
||||
options = {}
|
||||
if preCondition is 'user'
|
||||
options =
|
||||
'current_user.id': App.i18n.translateInline('current user')
|
||||
'specific': App.i18n.translateInline('specific user')
|
||||
|
||||
if attributeSelected.null is true
|
||||
options['not_set'] = App.i18n.translateInline('unassign user')
|
||||
|
||||
else if preCondition is 'org'
|
||||
options =
|
||||
'current_user.organization_id': App.i18n.translateInline('current user organization')
|
||||
'specific': App.i18n.translateInline('specific organization')
|
||||
|
||||
for key, value of options
|
||||
selected = ''
|
||||
if key is meta.pre_condition
|
||||
selected = 'selected="selected"'
|
||||
selection.append("<option value=\"#{key}\" #{selected}>#{App.i18n.translateInline(value)}</option>")
|
||||
elementRow.find('.js-preCondition').closest('.controls').removeClass('hide')
|
||||
elementRow.find('.js-preCondition select').replaceWith(selection)
|
||||
|
||||
elementRow.find('.js-preCondition select').on('change', (e) ->
|
||||
toggleValue()
|
||||
)
|
||||
|
||||
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
toggleValue()
|
||||
|
||||
@buildValue: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||
name = "#{attribute.name}::#{groupAndAttribute}::value"
|
||||
|
||||
# build new item
|
||||
attributeConfig = elements[groupAndAttribute]
|
||||
config = _.clone(attributeConfig)
|
||||
|
||||
if config.relation is 'User'
|
||||
config.tag = 'user_autocompletion'
|
||||
if config.relation is 'Organization'
|
||||
config.tag = 'autocompletion_ajax'
|
||||
|
||||
# render ui element
|
||||
item = ''
|
||||
if config && App.UiElement[config.tag]
|
||||
config['name'] = name
|
||||
if attribute.value && attribute.value[groupAndAttribute]
|
||||
config['value'] = _.clone(attribute.value[groupAndAttribute]['value'])
|
||||
config.multiple = false
|
||||
config.nulloption = config.null
|
||||
if config.tag is 'checkbox'
|
||||
config.tag = 'select'
|
||||
tagSearch = "#{config.tag}_search"
|
||||
if config.tag is 'datetime'
|
||||
config.validationContainer = 'self'
|
||||
if App.UiElement[tagSearch]
|
||||
item = App.UiElement[tagSearch].render(config, {})
|
||||
else
|
||||
item = App.UiElement[config.tag].render(config, {})
|
||||
|
||||
relative_operators = [
|
||||
'before (relative)',
|
||||
'within next (relative)',
|
||||
'within last (relative)',
|
||||
'after (relative)',
|
||||
'till (relative)',
|
||||
'from (relative)',
|
||||
'relative'
|
||||
]
|
||||
|
||||
upcoming_operator = meta.operator
|
||||
|
||||
if !_.include(config.operator, upcoming_operator)
|
||||
if Array.isArray(config.operator)
|
||||
upcoming_operator = config.operator[0]
|
||||
else
|
||||
upcoming_operator = null
|
||||
|
||||
if _.include(relative_operators, upcoming_operator)
|
||||
config['name'] = "#{attribute.name}::#{groupAndAttribute}"
|
||||
if attribute.value && attribute.value[groupAndAttribute]
|
||||
config['value'] = _.clone(attribute.value[groupAndAttribute])
|
||||
item = App.UiElement['time_range'].render(config, {})
|
||||
|
||||
elementRow.find('.js-setAttribute > .flex > .js-value').removeClass('hide').html(item)
|
||||
|
||||
@buildNotificationArea: (notificationType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||
|
||||
return if elementRow.find(".js-setNotification .js-body-#{notificationType}").get(0)
|
||||
|
||||
elementRow.find('.js-setNotification').empty()
|
||||
|
||||
options =
|
||||
'article_last_sender': 'Sender of last article'
|
||||
'ticket_owner': 'Owner'
|
||||
'ticket_customer': 'Customer'
|
||||
'ticket_agents': 'All agents'
|
||||
|
||||
name = "#{attribute.name}::notification.#{notificationType}"
|
||||
|
||||
messageLength = switch notificationType
|
||||
when 'sms' then 160
|
||||
else 200000
|
||||
|
||||
# meta.recipient was a string in the past (single-select) so we convert it to array if needed
|
||||
if !_.isArray(meta.recipient)
|
||||
meta.recipient = [meta.recipient]
|
||||
|
||||
columnSelectOptions = []
|
||||
for key, value of options
|
||||
selected = undefined
|
||||
for recipient in meta.recipient
|
||||
if key is recipient
|
||||
selected = true
|
||||
columnSelectOptions.push({ value: key, name: App.i18n.translatePlain(value), selected: selected })
|
||||
|
||||
columnSelectRecipientUserOptions = []
|
||||
for user in App.User.all()
|
||||
key = "userid_#{user.id}"
|
||||
selected = undefined
|
||||
for recipient in meta.recipient
|
||||
if key is recipient
|
||||
selected = true
|
||||
columnSelectRecipientUserOptions.push({ value: key, name: "#{user.firstname} #{user.lastname}", selected: selected })
|
||||
|
||||
columnSelectRecipient = new App.ColumnSelect
|
||||
attribute:
|
||||
name: "#{name}::recipient"
|
||||
options: [
|
||||
{
|
||||
label: 'Variables',
|
||||
group: columnSelectOptions
|
||||
},
|
||||
{
|
||||
label: 'User',
|
||||
group: columnSelectRecipientUserOptions
|
||||
},
|
||||
]
|
||||
|
||||
selectionRecipient = columnSelectRecipient.element()
|
||||
|
||||
if notificationType is 'webhook'
|
||||
notificationElement = $( App.view('generic/ticket_perform_action/webhook')(
|
||||
attribute: attribute
|
||||
name: name
|
||||
notificationType: notificationType
|
||||
meta: meta || {}
|
||||
))
|
||||
|
||||
notificationElement.find('.js-recipient select').replaceWith(selectionRecipient)
|
||||
|
||||
|
||||
if App.Webhook.search(filter: { active: true }).length isnt 0 || !_.isEmpty(meta.webhook_id)
|
||||
webhookSelection = App.UiElement.select.render(
|
||||
name: "#{name}::webhook_id"
|
||||
multiple: false
|
||||
null: false
|
||||
relation: 'Webhook'
|
||||
value: meta.webhook_id
|
||||
translate: false
|
||||
nulloption: true
|
||||
)
|
||||
else
|
||||
webhookSelection = App.view('generic/ticket_perform_action/webhook_not_available')( attribute: attribute )
|
||||
|
||||
notificationElement.find('.js-webhooks').html(webhookSelection)
|
||||
|
||||
else
|
||||
notificationElement = $( App.view('generic/ticket_perform_action/notification')(
|
||||
attribute: attribute
|
||||
name: name
|
||||
notificationType: notificationType
|
||||
meta: meta || {}
|
||||
))
|
||||
|
||||
notificationElement.find('.js-recipient select').replaceWith(selectionRecipient)
|
||||
|
||||
visibilitySelection = App.UiElement.select.render(
|
||||
name: "#{name}::internal"
|
||||
multiple: false
|
||||
null: false
|
||||
options: { true: 'internal', false: 'public' }
|
||||
value: meta.internal || 'false'
|
||||
translate: true
|
||||
)
|
||||
|
||||
includeAttachmentsCheckbox = App.UiElement.select.render(
|
||||
name: "#{name}::include_attachments"
|
||||
multiple: false
|
||||
null: false
|
||||
options: { true: 'Yes', false: 'No' }
|
||||
value: meta.include_attachments || 'false'
|
||||
translate: true
|
||||
)
|
||||
|
||||
notificationElement.find('.js-internal').html(visibilitySelection)
|
||||
notificationElement.find('.js-include_attachments').html(includeAttachmentsCheckbox)
|
||||
|
||||
notificationElement.find('.js-body div[contenteditable="true"]').ce(
|
||||
mode: 'richtext'
|
||||
placeholder: 'message'
|
||||
maxlength: messageLength
|
||||
)
|
||||
new App.WidgetPlaceholder(
|
||||
el: notificationElement.find('.js-body div[contenteditable="true"]').parent()
|
||||
objects: [
|
||||
{
|
||||
prefix: 'ticket'
|
||||
object: 'Ticket'
|
||||
display: 'Ticket'
|
||||
},
|
||||
{
|
||||
prefix: 'article'
|
||||
object: 'TicketArticle'
|
||||
display: 'Article'
|
||||
},
|
||||
{
|
||||
prefix: 'user'
|
||||
object: 'User'
|
||||
display: 'Current User'
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
elementRow.find('.js-setNotification').html(notificationElement).removeClass('hide')
|
||||
|
||||
if App.Config.get('smime_integration') == true || App.Config.get('pgp_integration') == true
|
||||
selection = App.UiElement.select.render(
|
||||
name: "#{name}::sign"
|
||||
multiple: false
|
||||
options: {
|
||||
'no': 'Do not sign email'
|
||||
'discard': 'Sign email (if not possible, discard notification)'
|
||||
'always': 'Sign email (if not possible, send notification anyway)'
|
||||
}
|
||||
value: meta.sign
|
||||
translate: true
|
||||
)
|
||||
|
||||
elementRow.find('.js-sign').html(selection)
|
||||
|
||||
selection = App.UiElement.select.render(
|
||||
name: "#{name}::encryption"
|
||||
multiple: false
|
||||
options: {
|
||||
'no': 'Do not encrypt email'
|
||||
'discard': 'Encrypt email (if not possible, discard notification)'
|
||||
'always': 'Encrypt email (if not possible, send notification anyway)'
|
||||
}
|
||||
value: meta.encryption
|
||||
translate: true
|
||||
)
|
||||
|
||||
elementRow.find('.js-encryption').html(selection)
|
||||
|
||||
@buildArticleArea: (articleType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||
|
||||
return if elementRow.find(".js-setArticle .js-body-#{articleType}").get(0)
|
||||
|
||||
elementRow.find('.js-setArticle').empty()
|
||||
|
||||
name = "#{attribute.name}::article.#{articleType}"
|
||||
selection = App.UiElement.select.render(
|
||||
name: "#{name}::internal"
|
||||
multiple: false
|
||||
null: false
|
||||
label: 'Visibility'
|
||||
options: { true: 'internal', false: 'public' }
|
||||
value: meta.internal
|
||||
translate: true
|
||||
)
|
||||
articleElement = $( App.view('generic/ticket_perform_action/article')(
|
||||
attribute: attribute
|
||||
name: name
|
||||
articleType: articleType
|
||||
meta: meta || {}
|
||||
))
|
||||
articleElement.find('.js-internal').html(selection)
|
||||
articleElement.find('.js-body div[contenteditable="true"]').ce(
|
||||
mode: 'richtext'
|
||||
placeholder: 'message'
|
||||
maxlength: 200000
|
||||
)
|
||||
new App.WidgetPlaceholder(
|
||||
el: articleElement.find('.js-body div[contenteditable="true"]').parent()
|
||||
objects: [
|
||||
{
|
||||
prefix: 'ticket'
|
||||
object: 'Ticket'
|
||||
display: 'Ticket'
|
||||
},
|
||||
{
|
||||
prefix: 'article'
|
||||
object: 'TicketArticle'
|
||||
display: 'Article'
|
||||
},
|
||||
{
|
||||
prefix: 'user'
|
||||
object: 'User'
|
||||
display: 'Current User'
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
elementRow.find('.js-setArticle').html(articleElement).removeClass('hide')
|
||||
|
||||
@humanText: (condition) ->
|
||||
none = App.i18n.translateContent('No filter.')
|
||||
return [none] if _.isEmpty(condition)
|
||||
[defaults, groups, operators, elements] = @defaults()
|
||||
rules = []
|
||||
for attribute, value of condition
|
||||
|
||||
objectAttribute = attribute.split(/\./)
|
||||
|
||||
# get stored params
|
||||
if meta && objectAttribute[1]
|
||||
model = toCamelCase(objectAttribute[0])
|
||||
config = elements[attribute]
|
||||
|
||||
valueHuman = []
|
||||
if _.isArray(value)
|
||||
for data in value
|
||||
r = @humanTextLookup(config, data)
|
||||
valueHuman.push r
|
||||
else
|
||||
valueHuman.push @humanTextLookup(config, value)
|
||||
|
||||
if valueHuman.join
|
||||
valueHuman = valueHuman.join(', ')
|
||||
rules.push "#{App.i18n.translateContent('Set')} <b>#{App.i18n.translateContent(model)} -> #{App.i18n.translateContent(config.display)}</b> #{App.i18n.translateContent('to')} <b>#{valueHuman}</b>."
|
||||
|
||||
return [none] if _.isEmpty(rules)
|
||||
rules
|
||||
|
||||
@humanTextLookup: (config, value) ->
|
||||
return value if !App[config.relation]
|
||||
return value if !App[config.relation].exists(value)
|
||||
data = App[config.relation].fullLocal(value)
|
||||
return value if !data
|
||||
if data.displayName
|
||||
return App.i18n.translateContent( data.displayName() )
|
||||
valueHuman.push App.i18n.translateContent( data.name )
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
# Methods for displaying security ui elements and to get security params
|
||||
|
||||
App.SecurityOptions =
|
||||
|
||||
securityOptionsShow: ->
|
||||
@$('.js-securityOptions').removeClass('hide')
|
||||
|
||||
securityOptionsHide: ->
|
||||
@$('.js-securityOptions').addClass('hide')
|
||||
|
||||
securityOptionsShown: ->
|
||||
!@$('.js-securityOptions').hasClass('hide')
|
||||
|
||||
securityEnabled: ->
|
||||
App.Config.get('smime_integration') || App.Config.get('pgp_integration')
|
||||
|
||||
paramsSecurity: =>
|
||||
if @$('.js-securityOptions').hasClass('hide')
|
||||
return {}
|
||||
|
||||
security = {}
|
||||
security.encryption ||= {}
|
||||
security.sign ||= {}
|
||||
if App.Config.get('pgp_integration')
|
||||
security.type = 'PGP'
|
||||
else
|
||||
security.type = 'S/MIME'
|
||||
if @$('.js-securityEncrypt').hasClass('btn--active')
|
||||
security.encryption.success = true
|
||||
if @$('.js-securitySign').hasClass('btn--active')
|
||||
security.sign.success = true
|
||||
security
|
||||
|
||||
updateSecurityOptionsRemote: (key, ticket, article, securityOptions) ->
|
||||
if securityOptions.type == 'PGP'
|
||||
id = "pgp-check-#{key}"
|
||||
url = "#{@apiPath}/integration/pgp"
|
||||
securityConfig = App.Config.get('pgp_config')
|
||||
else
|
||||
id = "smime-check-#{key}"
|
||||
url = "#{@apiPath}/integration/smime"
|
||||
securityConfig = App.Config.get('smime_config')
|
||||
callback = =>
|
||||
@ajax(
|
||||
id: id
|
||||
type: 'POST'
|
||||
url: url
|
||||
data: JSON.stringify(ticket: ticket, article: article)
|
||||
processData: true
|
||||
success: (data, status, xhr) =>
|
||||
|
||||
# get default selected security options
|
||||
selected =
|
||||
encryption: true
|
||||
sign: true
|
||||
for type, selector of { default_sign: 'sign', default_encryption: 'encryption' }
|
||||
if securityConfig?.group_id?[type] && ticket.group_id
|
||||
if securityConfig.group_id[type][ticket.group_id.toString()] == false
|
||||
selected[selector] = false
|
||||
|
||||
@$('.js-securityEncryptComment').attr('title', data.encryption.comment)
|
||||
|
||||
# if encryption is possible
|
||||
if data.encryption.success is true
|
||||
@$('.js-securityEncrypt').attr('disabled', false)
|
||||
|
||||
# overrule current selection with Group configuration
|
||||
if selected.encryption
|
||||
@$('.js-securityEncrypt').addClass('btn--active')
|
||||
else
|
||||
@$('.js-securityEncrypt').removeClass('btn--active')
|
||||
|
||||
# if encryption is not possible
|
||||
else
|
||||
@$('.js-securityEncrypt').attr('disabled', true)
|
||||
@$('.js-securityEncrypt').removeClass('btn--active')
|
||||
|
||||
@$('.js-securitySignComment').attr('title', data.sign.comment)
|
||||
|
||||
# if sign is possible
|
||||
if data.sign.success is true
|
||||
@$('.js-securitySign').attr('disabled', false)
|
||||
|
||||
# overrule current selection with Group configuration
|
||||
if selected.sign
|
||||
@$('.js-securitySign').addClass('btn--active')
|
||||
else
|
||||
@$('.js-securitySign').removeClass('btn--active')
|
||||
|
||||
# if sign is possible
|
||||
else
|
||||
@$('.js-securitySign').attr('disabled', true)
|
||||
@$('.js-securitySign').removeClass('btn--active')
|
||||
|
||||
error: (data) ->
|
||||
details = data.responseJSON || {}
|
||||
console.log(details)
|
||||
)
|
||||
@delay(callback, 200, 'security-check')
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
<form>
|
||||
<h2><%- @T('Public & Private Keys') %></h2>
|
||||
<div class="settings-entry settings-entry--stretched js-keyList"></div>
|
||||
|
||||
<div class="btn btn--primary js-addPublicKey"><%- @T('Add Public Key') %></div>
|
||||
<div class="btn js-addPrivateKey"><%- @T('Add Private Key') %></div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2><%- @T('Default Behavior') %></h2>
|
||||
<p>Choose the default behavior of the PGP integration on per group basis. If signing or encrypting is not possible, the setting has no effect. Agents can always manually alter the behavior for each article.</p>
|
||||
<div class="settings-entry settings-entry--stretched js-groupList"></div>
|
||||
<div class="btn btn--primary js-updateGroup"><%- @T('Update') %></div>
|
||||
</form>
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
<table class="settings-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="55%"><%- @T('Group') %>
|
||||
<th><%- @T('Sign') %>
|
||||
<th><%- @T('Encryption') %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if _.isEmpty(@groups): %>
|
||||
<tr>
|
||||
<td colspan="6">
|
||||
<%- @T('No Entries') %>
|
||||
</td>
|
||||
</tr>
|
||||
<% else: %>
|
||||
<% for group in @groups: %>
|
||||
<tr data-id="<%= group.id %>">
|
||||
<td><%= group.name %>
|
||||
<td class="js-signDefault">
|
||||
<td class="js-encryptionDefault">
|
||||
<% end %>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
<table class="settings-list settings-list--stretch">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="35%"><%- @T('Email') %>
|
||||
<th width="60%"><%- @T('Fingerprint') %>
|
||||
<th width="5%"><%- @T('Actions') %>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if _.isEmpty(@keyPairs): %>
|
||||
<tr>
|
||||
<td colspan="6">
|
||||
<%- @T('No Entries') %>
|
||||
</td>
|
||||
</tr>
|
||||
<% else: %>
|
||||
<% for keyPair in @keyPairs: %>
|
||||
<tr data-id="<%= keyPair.id %>">
|
||||
<td><% if !_.isEmpty(keyPair.email_addresses): %><%= keyPair.email_addresses.toString() %><% end %>
|
||||
<% if keyPair.private_key: %><br><i><%- @T('Including private key.') %></i><% end %>
|
||||
<td title="<%= keyPair.fingerprint %>"><%= keyPair.fingerprint %>
|
||||
<td>
|
||||
<div class="dropdown dropdown--actions">
|
||||
<div class="btn btn--table btn--text btn--secondary js-action" data-toggle="dropdown">
|
||||
<%- @Icon('overflow-button') %>
|
||||
</div>
|
||||
<ul class="dropdown-menu dropdown-menu-right js-table-action-menu" role="menu">
|
||||
<% if keyPair.private_key: %>
|
||||
<li role="presentation" data-table-action="download-private">
|
||||
<a href="<%= @C('http_type') %>://<%= @C('fqdn')%>/api/v1/integration/pgp/private_key_download/<%= keyPair.id %>" download><%- @Icon('download') %> <%- @T('Download Private Key') %></a>
|
||||
</li>
|
||||
<% end %>
|
||||
<li role="presentation" data-table-action="download-public">
|
||||
<a href="<%= @C('http_type') %>://<%= @C('fqdn')%>/api/v1/integration/pgp/public_key_download/<%= keyPair.id %>"%download><%- @Icon('download') %> <%- @T('Download Public Key') %></a>
|
||||
</li>
|
||||
<li role="presentation" class="danger js-remove" data-table-action="remove">
|
||||
<%- @Icon('trash') %> <%- @T('Delete') %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
<div>
|
||||
<p class="alert alert--danger js-error hide"></p>
|
||||
|
||||
<div class="form-field-group">
|
||||
<div class="form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for="private_key-upload"><%- @T('Upload Private Key') %></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input name="file" type="file" id="private_key-upload">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="or-divider">
|
||||
<span><%- @T('or') %></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for="private_key-paste"><%- @T('Paste Private Key') %></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<textarea cols="25" rows="20" name="data" style="height: 200px;"
|
||||
id="private_key-paste"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for="private_key-secret"><%- @T('Enter Private Key Secret') %></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input class="form-control" name="secret" type="password" id="private_key-secret">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<div>
|
||||
<p class="alert alert--danger js-error hide"></p>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for="public_key-upload"><%- @T('Upload Public Key') %></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<input name="file" type="file" id="public_key-upload">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="or-divider">
|
||||
<span><%- @T('or') %></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="formGroup-label">
|
||||
<label for="public_key-paste"><%- @T('Paste Public Key') %></label>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<textarea cols="25" rows="20" name="data" style="height: 200px;"
|
||||
id="public_key-paste"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -1 +0,0 @@
|
|||
.icon-pgp { width:17px; height: 17px; }
|
||||
|
|
@ -1,154 +0,0 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class Integration::PGPController < ApplicationController
|
||||
prepend_before_action { authentication_check && authorize! }
|
||||
|
||||
def public_key_download
|
||||
cert = PGPKeypair.find(params[:id])
|
||||
|
||||
send_data(
|
||||
cert.public_key,
|
||||
filename: "#{cert.fingerprint}.asc",
|
||||
type: 'text/plain',
|
||||
disposition: 'attachment'
|
||||
)
|
||||
end
|
||||
|
||||
def private_key_download
|
||||
cert = PGPKeypair.find(params[:id])
|
||||
|
||||
send_data(
|
||||
cert.private_key,
|
||||
filename: "#{cert.fingerprint}-private.asc",
|
||||
type: 'text/plain',
|
||||
disposition: 'attachment'
|
||||
)
|
||||
end
|
||||
|
||||
def public_key_list
|
||||
render json: PGPKeypair.all, methods: :email_addresses
|
||||
end
|
||||
|
||||
def public_key_delete
|
||||
PGPKeypair.find(params[:id]).destroy!
|
||||
render json: {
|
||||
result: 'ok'
|
||||
}
|
||||
end
|
||||
|
||||
def public_key_add
|
||||
string = params[:data]
|
||||
string = params[:file].read.force_encoding('utf-8') if string.blank? && params[:file].present?
|
||||
|
||||
items = PGPKeypair.create_public_keys(string)
|
||||
|
||||
render json: {
|
||||
result: 'ok',
|
||||
response: items
|
||||
}
|
||||
rescue StandardError => e
|
||||
unprocessable_entity(e)
|
||||
end
|
||||
|
||||
def private_key_delete
|
||||
PGPKeypair.find(params[:id]).update!(
|
||||
private_key: nil,
|
||||
private_key_secret: nil
|
||||
)
|
||||
|
||||
render json: {
|
||||
result: 'ok'
|
||||
}
|
||||
end
|
||||
|
||||
def private_key_add
|
||||
string = params[:data]
|
||||
string = params[:file].read.force_encoding('utf-8') if string.blank? && params[:file].present?
|
||||
|
||||
raise "Parameter 'data' or 'file' required." if string.blank?
|
||||
|
||||
PGPKeypair.create_private_keys(string, params[:secret])
|
||||
|
||||
render json: {
|
||||
result: 'ok'
|
||||
}
|
||||
rescue StandardError => e
|
||||
unprocessable_entity(e)
|
||||
end
|
||||
|
||||
def search
|
||||
result = {
|
||||
type: 'PGP'
|
||||
}
|
||||
|
||||
result[:encryption] = article_encryption(params[:article])
|
||||
result[:sign] = article_sign(params[:ticket])
|
||||
|
||||
render json: result
|
||||
end
|
||||
|
||||
def article_encryption(article)
|
||||
result = {
|
||||
success: false,
|
||||
comment: 'no recipient found'
|
||||
}
|
||||
|
||||
return result if article.blank?
|
||||
return result if article[:to].blank? && article[:cc].blank?
|
||||
|
||||
recipient = [article[:to], article[:cc]].compact.join(',').to_s
|
||||
recipients = []
|
||||
begin
|
||||
list = Mail::AddressList.new(recipient)
|
||||
list.addresses.each do |address|
|
||||
recipients.push address.address
|
||||
end
|
||||
rescue StandardError # rubocop:disable Lint/SuppressedException
|
||||
end
|
||||
|
||||
return result if recipients.blank?
|
||||
|
||||
begin
|
||||
keys = PGPKeypair.for_recipient_email_addresses!(recipients)
|
||||
|
||||
if keys
|
||||
result[:success] = true
|
||||
result[:comment] = "keys found for #{recipients.join(',')}"
|
||||
end
|
||||
rescue StandardError => e
|
||||
result[:comment] = e.message
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def article_sign(ticket)
|
||||
result = {
|
||||
success: false,
|
||||
comment: 'key not found'
|
||||
}
|
||||
|
||||
return result if ticket.blank? || !ticket[:group_id]
|
||||
|
||||
group = Group.find_by(id: ticket[:group_id])
|
||||
return result unless group
|
||||
|
||||
email_address = group.email_address
|
||||
begin
|
||||
list = Mail::AddressList.new(email_address.email)
|
||||
from = list.addresses.first.to_s
|
||||
key = PGPKeypair.for_sender_email_address(from)
|
||||
if key
|
||||
result[:success] = true
|
||||
result[:comment] = "key for #{email_address.email} found"
|
||||
else
|
||||
result[:success] = false
|
||||
result[:comment] = "no key for #{email_address.email} found"
|
||||
end
|
||||
rescue StandardError => e
|
||||
result[:comment] = e.message
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class PGPKeypair < ApplicationModel
|
||||
validates :fingerprint, uniqueness: { case_sensitive: true }
|
||||
|
||||
def self.create_private_keys(raw, secret)
|
||||
Sequoia.emails_of(keys: raw).each do |address|
|
||||
downcased_address = address.downcase
|
||||
public_key = find_each.detect do |certificate|
|
||||
certificate.email_addresses.include?(downcased_address)
|
||||
end
|
||||
|
||||
unless public_key
|
||||
raise Exceptions::UnprocessableEntity,
|
||||
'The public key for this private key could not be found.'
|
||||
end
|
||||
|
||||
public_key.update!(private_key: raw, private_key_secret: secret)
|
||||
end
|
||||
end
|
||||
|
||||
def self.create_public_keys(raw)
|
||||
create!(public_key: raw)
|
||||
end
|
||||
|
||||
def self.for_sender_email_address(address)
|
||||
downcased_address = address.downcase
|
||||
where.not(private_key: nil).find_each.detect do |certificate|
|
||||
certificate.email_addresses.include?(downcased_address)
|
||||
end
|
||||
end
|
||||
|
||||
def self.for_recipient_email_addresses!(addresses)
|
||||
certificates = []
|
||||
remaining_addresses = addresses.map(&:downcase)
|
||||
find_each do |certificate|
|
||||
# intersection of both lists
|
||||
certificate_for = certificate.email_addresses & remaining_addresses
|
||||
next if certificate_for.blank?
|
||||
|
||||
certificates.push(certificate)
|
||||
|
||||
# subtract found recipient(s)
|
||||
remaining_addresses -= certificate_for
|
||||
|
||||
# end loop if no addresses are remaining
|
||||
break if remaining_addresses.blank?
|
||||
end
|
||||
|
||||
return certificates if remaining_addresses.blank?
|
||||
|
||||
raise ActiveRecord::RecordNotFound,
|
||||
"Can't find PGP encryption certificates for: #{remaining_addresses.join(', ')}"
|
||||
end
|
||||
|
||||
def public_key=(string)
|
||||
self.fingerprint = Sequoia.fingerprints_of(keys: string).first
|
||||
self[:public_key] = string
|
||||
end
|
||||
|
||||
def email_addresses
|
||||
@email_addresses ||= Sequoia.emails_of(keys: public_key)
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +0,0 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class Controllers::Integration::PGPControllerPolicy < Controllers::ApplicationControllerPolicy
|
||||
permit! :search, to: 'ticket.agent'
|
||||
default_permit!('admin.integration.pgp')
|
||||
end
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
# Be sure to restart your server when you modify this file.
|
||||
|
||||
# Add new inflection rules using the following format. Inflections
|
||||
# are locale specific, and you may define rules for as many different
|
||||
# locales as you wish. All of these examples are active by default:
|
||||
# ActiveSupport::Inflector.inflections(:en) do |inflect|
|
||||
# inflect.plural /^(ox)$/i, '\1en'
|
||||
# inflect.singular /^(ox)en/i, '\1'
|
||||
# inflect.irregular 'person', 'people'
|
||||
# inflect.uncountable %w( fish sheep )
|
||||
# end
|
||||
|
||||
# These inflection rules are supported but not enabled by default:
|
||||
# ActiveSupport::Inflector.inflections(:en) do |inflect|
|
||||
# inflect.acronym 'RESTful'
|
||||
# end
|
||||
|
||||
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
||||
|
||||
# Rails thinks the singularized version of knowledge_bases is knowledge_basis?!
|
||||
# see: KnowledgeBase.table_name.singularize
|
||||
inflect.irregular 'base', 'bases'
|
||||
inflect.acronym 'SMIME'
|
||||
inflect.acronym 'PGP'
|
||||
inflect.acronym 'GitLab'
|
||||
inflect.acronym 'GitHub'
|
||||
end
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
require 'ruby_openpgp'
|
||||
|
||||
Rails.application.config.before_configuration do
|
||||
#FIXME need icon
|
||||
icon = File.read("public/assets/images/icons/pgp.svg")
|
||||
doc = File.open("public/assets/images/icons.svg") { |f| Nokogiri::XML(f) }
|
||||
if !doc.at_css('#icon-pgp')
|
||||
doc.at('svg').add_child(icon)
|
||||
Rails.logger.debug "PGP support icon added to icon set"
|
||||
else
|
||||
Rails.logger.debug "PGP support icon already in icon set"
|
||||
end
|
||||
File.write("public/assets/images/icons.svg", doc.to_xml)
|
||||
end
|
||||
|
||||
# Rails.application.config.after_initialize do
|
||||
# Ticket::Article.add_observer Observer::Ticket::Article::CommunicatePgpSupport.instance
|
||||
# end
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
Zammad::Application.routes.draw do
|
||||
api_path = Rails.configuration.api_path
|
||||
|
||||
match api_path + '/integration/pgp', to: 'integration/pgp#search', via: :post
|
||||
match api_path + '/integration/pgp/public_key', to: 'integration/pgp#public_key_add', via: :post
|
||||
match api_path + '/integration/pgp/public_key', to: 'integration/pgp#public_key_delete', via: :delete
|
||||
match api_path + '/integration/pgp/public_key', to: 'integration/pgp#public_key_list', via: :get
|
||||
match api_path + '/integration/pgp/private_key', to: 'integration/pgp#private_key_add', via: :post
|
||||
match api_path + '/integration/pgp/private_key', to: 'integration/pgp#private_key_delete', via: :delete
|
||||
match api_path + '/integration/pgp/public_key_download/:id', to: 'integration/pgp#public_key_download', via: :get
|
||||
match api_path + '/integration/pgp/private_key_download/:id', to: 'integration/pgp#private_key_download', via: :get
|
||||
end
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Set up PGP addon
|
||||
class PGP < ActiveRecord::Migration[5.2]
|
||||
def self.up
|
||||
Setting.create_if_not_exists(
|
||||
title: 'PGP integration',
|
||||
name: 'pgp_integration',
|
||||
area: 'Integration::Switch',
|
||||
description: 'Defines if PGP encryption is enabled or not.',
|
||||
options: {
|
||||
form: [
|
||||
{
|
||||
display: '',
|
||||
null: true,
|
||||
name: 'pgp_integration',
|
||||
tag: 'boolean',
|
||||
options: {
|
||||
true => 'yes',
|
||||
false => 'no'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
state: false,
|
||||
preferences: {
|
||||
prio: 1,
|
||||
authentication: true,
|
||||
permission: ['admin.integration']
|
||||
},
|
||||
frontend: true
|
||||
)
|
||||
Setting.create_if_not_exists(
|
||||
title: 'PGP config',
|
||||
name: 'pgp_config',
|
||||
area: 'Integration::PGP',
|
||||
description: 'Defines the PGP config.',
|
||||
options: {},
|
||||
state: {},
|
||||
preferences: {
|
||||
prio: 2,
|
||||
permission: ['admin.integration']
|
||||
},
|
||||
frontend: true
|
||||
)
|
||||
|
||||
begin
|
||||
create_table :pgp_keypairs do |t|
|
||||
t.string :fingerprint, limit: 250, null: false
|
||||
t.binary :public_key, limit: 10.megabytes, null: false
|
||||
t.binary :private_key, limit: 10.megabytes, null: true
|
||||
t.string :private_key_secret, limit: 500, null: true
|
||||
t.timestamps limit: 3, null: false
|
||||
end
|
||||
add_index :pgp_keypairs, [:fingerprint], unique: true
|
||||
rescue StandardError => e
|
||||
puts "NOTICE: #{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class SecureMailing::PGP < SecureMailing::Backend
|
||||
def self.active?
|
||||
Setting.get('pgp_integration')
|
||||
end
|
||||
end
|
||||
|
|
@ -1,170 +0,0 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class SecureMailing::PGP::Incoming < SecureMailing::Backend::Handler
|
||||
attr_accessor :mail, :content_type
|
||||
|
||||
EXPRESSION_ENCRYPTED = %r{application/pgp-encrypted}i.freeze
|
||||
EXPRESSION_SIGNATURE = %r{application/pgp-signature}i.freeze
|
||||
|
||||
def initialize(mail)
|
||||
super()
|
||||
|
||||
@mail = mail
|
||||
@content_type = mail[:mail_instance].content_type
|
||||
end
|
||||
|
||||
def process
|
||||
return unless process?
|
||||
|
||||
initialize_article_preferences
|
||||
decrypt
|
||||
verify_signature
|
||||
log
|
||||
end
|
||||
|
||||
def initialize_article_preferences
|
||||
article_preferences[:security] = {
|
||||
type: 'PGP',
|
||||
sign: {
|
||||
success: false,
|
||||
comment: nil
|
||||
},
|
||||
encryption: {
|
||||
success: false,
|
||||
comment: nil
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def article_preferences
|
||||
@article_preferences ||= begin
|
||||
key = :'x-zammad-article-preferences'
|
||||
mail[key] ||= {}
|
||||
mail[key]
|
||||
end
|
||||
end
|
||||
|
||||
def process?
|
||||
signed? || encrypted?
|
||||
end
|
||||
|
||||
def signed?(check_content_type = content_type)
|
||||
EXPRESSION_SIGNATURE.match?(check_content_type)
|
||||
end
|
||||
|
||||
def encrypted?(check_content_type = content_type)
|
||||
EXPRESSION_ENCRYPTED.match?(check_content_type)
|
||||
end
|
||||
|
||||
def decrypt
|
||||
return unless encrypted?
|
||||
|
||||
success = false
|
||||
comment = 'Private key for decryption could not be found.'
|
||||
::PGPKeypair.where.not(private_key: [nil, '']).find_each do |cert|
|
||||
begin
|
||||
index = mail[:attachments].index { |file| file[:preferences]['Content-Type'] == 'application/pgp-encrypted' }
|
||||
data = mail[:attachments][index + 1][:data]
|
||||
decrypted_data = Sequoia.decrypt_for(ciphertext: data.chop, recipient: cert.private_key,
|
||||
password: cert.private_key_secret)
|
||||
rescue StandardError
|
||||
next
|
||||
end
|
||||
|
||||
parse_new_mail(decrypted_data)
|
||||
|
||||
success = true
|
||||
comment = cert.email_addresses.join(', ')
|
||||
|
||||
# overwrite content_type for signature checking
|
||||
@content_type = mail[:mail_instance].content_type
|
||||
break
|
||||
end
|
||||
|
||||
article_preferences[:security][:encryption] = {
|
||||
success: success,
|
||||
comment: comment
|
||||
}
|
||||
end
|
||||
|
||||
def verify_signature
|
||||
return unless signed?
|
||||
|
||||
success = false
|
||||
comment = 'Certificate for verification could not be found.'
|
||||
|
||||
::PGPKeypair.where.not(public_key: [nil, '']).find_each do |cert|
|
||||
next unless cert.email_addresses.include? mail[:from_email]
|
||||
|
||||
begin
|
||||
index = mail[:attachments].index { |file| file[:preferences]['Mime-Type'] == 'application/pgp-signature' }
|
||||
data = mail[:attachments][index][:data]
|
||||
verified_data = Sequoia.verify_detached_from(plaintext: mail[:mail_instance].body.encoded, signature: data.chop,
|
||||
sender: cert.public_key)
|
||||
rescue StandardError
|
||||
next
|
||||
end
|
||||
|
||||
parse_new_mail(verified_data)
|
||||
|
||||
success = true
|
||||
comment = cert.email_addresses.join(', ')
|
||||
|
||||
# overwrite content_type for signature checking
|
||||
@content_type = mail[:mail_instance].content_type
|
||||
break
|
||||
end
|
||||
|
||||
article_preferences[:security][:sign] = {
|
||||
success: success,
|
||||
comment: comment
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def log
|
||||
%i[sign encryption].each do |action|
|
||||
result = article_preferences[:security][action]
|
||||
next if result.blank?
|
||||
|
||||
if result[:success]
|
||||
status = 'success'
|
||||
elsif result[:comment].blank?
|
||||
# means not performed
|
||||
next
|
||||
else
|
||||
status = 'failed'
|
||||
end
|
||||
|
||||
HttpLog.create(
|
||||
direction: 'in',
|
||||
facility: 'PGP',
|
||||
url: "#{mail[:from_email]} -> #{mail[:to]}",
|
||||
status: status,
|
||||
ip: nil,
|
||||
request: {
|
||||
message_id: mail[:message_id]
|
||||
},
|
||||
response: article_preferences[:security],
|
||||
method: action,
|
||||
created_by_id: 1,
|
||||
updated_by_id: 1
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def parse_new_mail(new_mail)
|
||||
mail[:mail_instance].header['Content-Type'] = nil
|
||||
mail[:mail_instance].header['Content-Disposition'] = nil
|
||||
mail[:mail_instance].header['Content-Transfer-Encoding'] = nil
|
||||
mail[:mail_instance].header['Content-Description'] = nil
|
||||
|
||||
new_raw_mail = "#{mail[:mail_instance].header}#{new_mail}"
|
||||
|
||||
mail_new = Channel::EmailParser.new.parse(new_raw_mail)
|
||||
mail_new.each do |local_key, local_value|
|
||||
mail[local_key] = local_value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class SecureMailing::PGP::Outgoing < SecureMailing::Backend::Handler
|
||||
def initialize(mail, security)
|
||||
super()
|
||||
|
||||
@mail = mail
|
||||
@security = security
|
||||
end
|
||||
|
||||
def process
|
||||
return unless process?
|
||||
|
||||
if @security[:sign][:success]
|
||||
sign
|
||||
log('sign', 'success')
|
||||
end
|
||||
if @security[:encryption][:success]
|
||||
encrypt
|
||||
log('encryption', 'success')
|
||||
end
|
||||
end
|
||||
|
||||
def process?
|
||||
return false if @security.blank?
|
||||
return false if @security[:type] != 'PGP'
|
||||
|
||||
@security[:sign][:success] || @security[:encryption][:success]
|
||||
end
|
||||
|
||||
def cleanup(mail)
|
||||
part = Mail::Part.new
|
||||
if mail.multipart?
|
||||
if mail.content_type =~ /^(multipart[^;]+)/
|
||||
part.content_type Regexp.last_match(1)
|
||||
else
|
||||
part.content_type 'multipart/mixed'
|
||||
end
|
||||
mail.body.parts.each do |p|
|
||||
part.add_part cleanup(p)
|
||||
end
|
||||
else
|
||||
# retain important headers if present
|
||||
part.content_type mail.content_type
|
||||
part.content_id mail.header['Content-ID'] if mail.header['Content-ID']
|
||||
part.content_disposition mail.content_disposition if mail.content_disposition
|
||||
|
||||
# force base64 encoding
|
||||
part.body Mail::Encodings::Base64.encode(mail.body.to_s)
|
||||
part.body.encoding = 'base64'
|
||||
end
|
||||
part
|
||||
end
|
||||
|
||||
def sign
|
||||
from = @mail.from.first
|
||||
cert = PGPKeypair.for_sender_email_address(from)
|
||||
raise "Unable to find PGP private key for '#{from}'" unless cert
|
||||
|
||||
signature = Sequoia.sign_detached_with(plaintext: @mail.body.encoded, sender: cert.private_key,
|
||||
password: cert.private_key_secret)
|
||||
|
||||
signature_part = Mail::Part.new do
|
||||
content_type 'application/pgp-signature; name="signature.asc"'
|
||||
content_disposition 'attachment; filename="signature.asc"'
|
||||
content_description 'OpenPGP signature'
|
||||
body signature
|
||||
end
|
||||
@mail.add_part signature_part
|
||||
@mail.content_type "multipart/signed; protocol=\"application/pgp-signature\"; micalg=\"pgp-sha512\"; boundary=\"#{@mail.boundary}\""
|
||||
rescue StandardError => e
|
||||
log('sign', 'failed', e.message)
|
||||
raise
|
||||
end
|
||||
|
||||
def encrypt
|
||||
recipients = []
|
||||
recipients += @mail.to if @mail.to
|
||||
recipients += @mail.cc if @mail.cc
|
||||
recipients += @mail.bcc if @mail.bcc
|
||||
|
||||
certificates = PGPKeypair.for_recipient_email_addresses!(recipients)
|
||||
|
||||
encrypted_control = Mail::Part.new do
|
||||
content_type 'application/pgp-encrypted'
|
||||
content_description 'OpenPGP version'
|
||||
body 'Version: 1'
|
||||
end
|
||||
|
||||
plaintext = @mail.encoded
|
||||
encrypted_part = Mail::Part.new do
|
||||
content_type 'application/octet-stream; name="encrypted.asc"'
|
||||
content_disposition 'inline; filename="encrypted.asc"'
|
||||
content_description 'OpenPGP encrypted message'
|
||||
body Sequoia.encrypt_for(plaintext: plaintext, recipients: certificates.map(&:public_key))
|
||||
end
|
||||
@mail.body = nil
|
||||
@mail.add_part encrypted_control
|
||||
@mail.add_part encrypted_part
|
||||
@mail.content_type "multipart/encrypted; protocol=\"application/pgp-encrypted\"; boundary=\"#{@mail.boundary}\""
|
||||
rescue StandardError => e
|
||||
log('encryption', 'failed', e.message)
|
||||
raise
|
||||
end
|
||||
|
||||
def log(action, status, error = nil)
|
||||
HttpLog.create(
|
||||
direction: 'out',
|
||||
facility: 'PGP',
|
||||
url: "#{@mail[:from_email]} -> #{@mail[:to]}",
|
||||
status: status,
|
||||
ip: nil,
|
||||
request: @security,
|
||||
response: { error: error },
|
||||
method: action,
|
||||
created_by_id: 1,
|
||||
updated_by_id: 1
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class SecureMailing::PGP::Retry < SecureMailing::Backend::Handler
|
||||
def initialize(article)
|
||||
super()
|
||||
@article = article
|
||||
end
|
||||
|
||||
def process
|
||||
return existing_result if already_processed?
|
||||
|
||||
save_result if retry_succeeded?
|
||||
retry_result
|
||||
end
|
||||
|
||||
def signature_checked?
|
||||
@signature_checked ||= existing_result&.dig('sign', 'success') || false
|
||||
end
|
||||
|
||||
def decrypted?
|
||||
@decrypted ||= existing_result&.dig('encryption', 'success') || false
|
||||
end
|
||||
|
||||
def already_processed?
|
||||
signature_checked? && decrypted?
|
||||
end
|
||||
|
||||
def existing_result
|
||||
@article.preferences['security']
|
||||
end
|
||||
|
||||
def mail
|
||||
@mail ||= begin
|
||||
raw_mail = @article.as_raw.store_file.content
|
||||
Channel::EmailParser.new.parse(raw_mail).tap do |parsed|
|
||||
SecureMailing.incoming(parsed)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def retry_result
|
||||
@retry_result ||= mail['x-zammad-article-preferences']['security']
|
||||
end
|
||||
|
||||
def signature_found?
|
||||
return false if signature_checked?
|
||||
|
||||
retry_result['sign']['success']
|
||||
end
|
||||
|
||||
def decryption_succeeded?
|
||||
return false if decrypted?
|
||||
|
||||
retry_result['encryption']['success']
|
||||
end
|
||||
|
||||
def retry_succeeded?
|
||||
return true if signature_found?
|
||||
|
||||
decryption_succeeded?
|
||||
end
|
||||
|
||||
def save_result
|
||||
save_decrypted if decryption_succeeded?
|
||||
@article.preferences['security'] = retry_result
|
||||
@article.save!
|
||||
end
|
||||
|
||||
def save_decrypted
|
||||
@article.content_type = mail['content_type']
|
||||
@article.body = mail['body']
|
||||
|
||||
Store.remove(
|
||||
object: 'Ticket::Article',
|
||||
o_id: @article.id
|
||||
)
|
||||
|
||||
mail[:attachments]&.each do |attachment|
|
||||
filename = attachment[:filename].force_encoding('utf-8')
|
||||
unless filename.force_encoding('UTF-8').valid_encoding?
|
||||
filename = filename.utf8_encode(fallback: :read_as_sanitized_binary)
|
||||
end
|
||||
Store.add(
|
||||
object: 'Ticket::Article',
|
||||
o_id: @article.id,
|
||||
data: attachment[:data],
|
||||
filename: filename,
|
||||
preferences: attachment[:preferences],
|
||||
created_by_id: @article.created_by_id
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M512 176a176 176 0 01-208.8 173l-24 27a24 24 0 01-18 8H224v40a24 24 0 01-24 24h-40v40a24 24 0 01-24 24H24a24 24 0 01-24-24v-78a24 24 0 017-17l161.8-161.8A176 176 0 11512 176zm-176-48a48 48 0 1096 0 48 48 0 00-96 0z"/><script/></svg>
|
||||
|
Before Width: | Height: | Size: 304 B |
Loading…
Add table
Add a link
Reference in a new issue