Update deps, remove PGP

This commit is contained in:
Darren Clarke 2023-09-27 11:13:19 +02:00
parent 79653705fe
commit 58ce48b031
56 changed files with 1057 additions and 5100 deletions

View file

@ -208,10 +208,32 @@ zammad-docker-build:
- npm ci - npm ci
- turbo build --force --filter zammad-addon-* - turbo build --force --filter zammad-addon-*
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- DOCKER_BUILDKIT=1 docker build --pull --no-cache -t ${DOCKER_NS}:${DOCKER_TAG} -f ${DOCKERFILE_PATH} ${DOCKER_CONTEXT} - DOCKER_BUILDKIT=1 docker build --build-arg EMBEDDED=true --pull --no-cache -t ${DOCKER_NS}:${DOCKER_TAG} -f ${DOCKERFILE_PATH} ${DOCKER_CONTEXT}
- docker push ${DOCKER_NS}:${DOCKER_TAG} - docker push ${DOCKER_NS}:${DOCKER_TAG}
zammad-docker-release: zammad-docker-release:
extends: .docker-release extends: .docker-release
variables: variables:
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/zammad DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/zammad
zammad-standalone-docker-build:
extends: .docker-build
variables:
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/zammad-standalone
DOCKERFILE_PATH: ./docker/zammad/Dockerfile
DOCKER_CONTEXT: ./docker/zammad
before_script:
- apk --update add nodejs npm
script:
- npm install npm@latest -g
- npm install -g turbo
- npm ci
- turbo build --force --filter zammad-addon-*
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- DOCKER_BUILDKIT=1 docker build --pull --no-cache -t ${DOCKER_NS}:${DOCKER_TAG} -f ${DOCKERFILE_PATH} ${DOCKER_CONTEXT}
- docker push ${DOCKER_NS}:${DOCKER_TAG}
zammad-standalone-docker-release:
extends: .docker-release
variables:
DOCKER_NS: ${CI_REGISTRY}/digiresilience/link/link-stack/zammad-standalone

View file

@ -21,17 +21,17 @@
"@fontsource/poppins": "^5.0.8", "@fontsource/poppins": "^5.0.8",
"@fontsource/roboto": "^5.0.8", "@fontsource/roboto": "^5.0.8",
"@mui/icons-material": "^5", "@mui/icons-material": "^5",
"@mui/lab": "^5.0.0-alpha.143", "@mui/lab": "^5.0.0-alpha.146",
"@mui/material": "^5", "@mui/material": "^5",
"@mui/x-data-grid-pro": "^6.13.0", "@mui/x-data-grid-pro": "^6.15.0",
"@mui/x-date-pickers-pro": "^6.13.0", "@mui/x-date-pickers-pro": "^6.15.0",
"@opensearch-project/opensearch": "^2.3.1", "@opensearch-project/opensearch": "^2.3.1",
"cryptr": "^6.3.0", "cryptr": "^6.3.0",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"http-proxy-middleware": "^2.0.6", "http-proxy-middleware": "^2.0.6",
"leafcutter-common": "*", "leafcutter-common": "*",
"material-ui-popup-state": "^5.0.9", "material-ui-popup-state": "^5.0.9",
"next": "13.4.19", "next": "13.5.3",
"next-auth": "^4.23.1", "next-auth": "^4.23.1",
"next-http-proxy-middleware": "^1.2.5", "next-http-proxy-middleware": "^1.2.5",
"nodemailer": "^6.9.5", "nodemailer": "^6.9.5",
@ -42,20 +42,20 @@
"react-iframe": "^1.8.5", "react-iframe": "^1.8.5",
"react-markdown": "^8.0.7", "react-markdown": "^8.0.7",
"react-polyglot": "^0.7.2", "react-polyglot": "^0.7.2",
"sharp": "^0.32.5", "sharp": "^0.32.6",
"swr": "^2.2.2", "swr": "^2.2.4",
"tss-react": "^4.9.0", "tss-react": "^4.9.2",
"uuid": "^9.0.0" "uuid": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.22.17", "@babel/core": "^7.23.0",
"@types/node": "^20.6.0", "@types/node": "^20.7.0",
"@types/react": "18.2.21", "@types/react": "18.2.23",
"@types/uuid": "^9.0.3", "@types/uuid": "^9.0.4",
"babel-loader": "^9.1.3", "babel-loader": "^9.1.3",
"eslint": "^8.49.0", "eslint": "^8.50.0",
"eslint-config-airbnb": "^19.0.4", "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-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.28.1", "eslint-plugin-import": "^2.28.1",
"eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-jsx-a11y": "^6.7.1",

View file

@ -19,25 +19,25 @@
"@fontsource/poppins": "^5.0.8", "@fontsource/poppins": "^5.0.8",
"@fontsource/roboto": "^5.0.8", "@fontsource/roboto": "^5.0.8",
"@mui/icons-material": "^5", "@mui/icons-material": "^5",
"@mui/lab": "^5.0.0-alpha.143", "@mui/lab": "^5.0.0-alpha.146",
"@mui/material": "^5", "@mui/material": "^5",
"@mui/x-data-grid-pro": "^6.13.0", "@mui/x-data-grid-pro": "^6.15.0",
"@mui/x-date-pickers-pro": "^6.13.0", "@mui/x-date-pickers-pro": "^6.15.0",
"cryptr": "^6.3.0", "cryptr": "^6.3.0",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"graphql-request": "^6.1.0", "graphql-request": "^6.1.0",
"leafcutter-common": "*", "leafcutter-common": "*",
"material-ui-popup-state": "^5.0.9", "material-ui-popup-state": "^5.0.9",
"mui-chips-input": "^2.1.3", "mui-chips-input": "^2.1.3",
"next": "13.4.19", "next": "13.5.3",
"next-auth": "^4.23.1", "next-auth": "^4.23.1",
"ra-data-graphql": "^4.14.0", "ra-data-graphql": "^4.14.3",
"ra-i18n-polyglot": "^4.14.0", "ra-i18n-polyglot": "^4.14.3",
"ra-input-rich-text": "^4.14.0", "ra-input-rich-text": "^4.14.3",
"ra-language-english": "^4.14.0", "ra-language-english": "^4.14.3",
"ra-postgraphile": "^6.1.1", "ra-postgraphile": "^6.1.1",
"react": "18.2.0", "react": "18.2.0",
"react-admin": "^4.14.0", "react-admin": "^4.14.3",
"react-cookie": "^6.1.1", "react-cookie": "^6.1.1",
"react-digit-input": "^2.1.0", "react-digit-input": "^2.1.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
@ -45,20 +45,20 @@
"react-polyglot": "^0.7.2", "react-polyglot": "^0.7.2",
"react-qr-code": "^2.0.12", "react-qr-code": "^2.0.12",
"react-timer-hook": "^3.0.7", "react-timer-hook": "^3.0.7",
"sharp": "^0.32.5", "sharp": "^0.32.6",
"swr": "^2.2.2", "swr": "^2.2.4",
"tss-react": "^4.9.0", "tss-react": "^4.9.2",
"twilio-client": "^1.15.0" "twilio-client": "^1.15.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.22.17", "@babel/core": "^7.23.0",
"@types/node": "^20.6.0", "@types/node": "^20.7.0",
"@types/react": "18.2.21", "@types/react": "18.2.23",
"@types/uuid": "^9.0.3", "@types/uuid": "^9.0.4",
"babel-loader": "^9.1.3", "babel-loader": "^9.1.3",
"eslint": "^8.49.0", "eslint": "^8.50.0",
"eslint-config-airbnb": "^19.0.4", "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-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.28.1", "eslint-plugin-import": "^2.28.1",
"eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-jsx-a11y": "^6.7.1",

View file

@ -30,7 +30,7 @@
"hapi-auth-bearer-token": "^8.0.0", "hapi-auth-bearer-token": "^8.0.0",
"hapi-auth-jwt2": "^10.4.0", "hapi-auth-jwt2": "^10.4.0",
"hapi-swagger": "^17.1.0", "hapi-swagger": "^17.1.0",
"joi": "^17.10.1", "joi": "^17.10.2",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"jwks-rsa": "^3.0.1", "jwks-rsa": "^3.0.1",
"long": "^5.2.3", "long": "^5.2.3",
@ -40,8 +40,8 @@
"pg-promise": "^11.5.4", "pg-promise": "^11.5.4",
"postgraphile": "4.12.3", "postgraphile": "4.12.3",
"postgraphile-plugin-connection-filter": "^2.3.0", "postgraphile-plugin-connection-filter": "^2.3.0",
"remeda": "^1.26.0", "remeda": "^1.27.0",
"twilio": "^4.17.0", "twilio": "^4.18.0",
"typeorm": "^0.3.17", "typeorm": "^0.3.17",
"@whiskeysockets/baileys": "^6.4.1" "@whiskeysockets/baileys": "^6.4.1"
}, },

View file

@ -23,7 +23,7 @@
"graphql": "15.8.0" "graphql": "15.8.0"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.4", "@types/jest": "^29.5.5",
"pino-pretty": "^10.2.0", "pino-pretty": "^10.2.0",
"nodemon": "^3.0.1", "nodemon": "^3.0.1",
"tsconfig-link": "*", "tsconfig-link": "*",

View file

@ -14,16 +14,16 @@
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"node-fetch": "^3", "node-fetch": "^3",
"pg-promise": "^11.5.4", "pg-promise": "^11.5.4",
"remeda": "^1.26.0", "remeda": "^1.27.0",
"twilio": "^4.17.0" "twilio": "^4.18.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.22.17", "@babel/core": "7.23.0",
"@babel/preset-env": "7.22.15", "@babel/preset-env": "7.22.20",
"@babel/preset-typescript": "7.22.15", "@babel/preset-typescript": "7.23.0",
"@types/fluent-ffmpeg": "^2.1.21", "@types/fluent-ffmpeg": "^2.1.22",
"@types/jest": "^29.5.4", "@types/jest": "^29.5.5",
"eslint": "^8.49.0", "eslint": "^8.50.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-circus": "^29.7.0", "jest-circus": "^29.7.0",
"jest-junit": "^16.0.0", "jest-junit": "^16.0.0",

View file

@ -38,7 +38,10 @@ services:
<<: [ *common-zammad-variables, *common-global-variables ] <<: [ *common-zammad-variables, *common-global-variables ]
POSTGRESQL_USER: zammad POSTGRESQL_USER: zammad
POSTGRESQL_PASS: ${ZAMMAD_DATABASE_PASSWORD} POSTGRESQL_PASS: ${ZAMMAD_DATABASE_PASSWORD}
build: ./docker/zammad build:
context: ./docker/zammad
args:
EMBEDDED: "true"
image: registry.gitlab.com/digiresilience/link/link-stack/zammad:${LINK_STACK_VERSION} image: registry.gitlab.com/digiresilience/link/link-stack/zammad:${LINK_STACK_VERSION}
restart: on-failure restart: on-failure
user: 0:0 user: 0:0
@ -65,7 +68,10 @@ services:
- 8001:8080 - 8001:8080
depends_on: depends_on:
- zammad-railsserver - zammad-railsserver
build: ./docker/zammad build:
context: ./docker/zammad
args:
EMBEDDED: "true"
image: registry.gitlab.com/digiresilience/link/link-stack/zammad:${LINK_STACK_VERSION} image: registry.gitlab.com/digiresilience/link/link-stack/zammad:${LINK_STACK_VERSION}
restart: ${RESTART} restart: ${RESTART}
environment: environment:
@ -102,7 +108,10 @@ services:
environment: environment:
<<: [ *common-global-variables, *common-zammad-variables ] <<: [ *common-global-variables, *common-zammad-variables ]
RAILS_RELATIVE_URL_ROOT: /zammad RAILS_RELATIVE_URL_ROOT: /zammad
build: ./docker/zammad build:
context: ./docker/zammad
args:
EMBEDDED: "true"
image: registry.gitlab.com/digiresilience/link/link-stack/zammad:${LINK_STACK_VERSION} image: registry.gitlab.com/digiresilience/link/link-stack/zammad:${LINK_STACK_VERSION}
restart: ${RESTART} restart: ${RESTART}
volumes: volumes:
@ -129,7 +138,10 @@ services:
- zammad-redis - zammad-redis
environment: environment:
<<: [ *common-global-variables, *common-zammad-variables ] <<: [ *common-global-variables, *common-zammad-variables ]
build: ./docker/zammad build:
context: ./docker/zammad
args:
EMBEDDED: "true"
image: registry.gitlab.com/digiresilience/link/link-stack/zammad:${LINK_STACK_VERSION} image: registry.gitlab.com/digiresilience/link/link-stack/zammad:${LINK_STACK_VERSION}
restart: ${RESTART} restart: ${RESTART}
volumes: volumes:
@ -146,7 +158,10 @@ services:
- zammad-redis - zammad-redis
environment: environment:
<<: [ *common-global-variables, *common-zammad-variables ] <<: [ *common-global-variables, *common-zammad-variables ]
build: ./docker/zammad build:
context: ./docker/zammad
args:
EMBEDDED: "true"
image: registry.gitlab.com/digiresilience/link/link-stack/zammad:${LINK_STACK_VERSION} image: registry.gitlab.com/digiresilience/link/link-stack/zammad:${LINK_STACK_VERSION}
restart: ${RESTART} restart: ${RESTART}
volumes: volumes:

View file

@ -1 +1 @@
FROM docker.elastic.co/elasticsearch/elasticsearch:8.8.1 FROM docker.elastic.co/elasticsearch/elasticsearch:8.10.2

View file

@ -1 +1 @@
FROM heartexlabs/label-studio:1.8.0 FROM heartexlabs/label-studio:1.8.2

View file

@ -1 +1 @@
FROM nginxproxy/nginx-proxy:1.3.0 FROM nginxproxy/nginx-proxy:1.3.1

View file

@ -1 +1 @@
FROM opensearchproject/opensearch-dashboards:2.8.0 FROM opensearchproject/opensearch-dashboards:2.9.0

View file

@ -1 +1 @@
FROM opensearchproject/opensearch:2.8.0 FROM opensearchproject/opensearch:2.9.0

View file

@ -1 +1 @@
FROM postgres:15.3-bookworm FROM postgres:16.0-bookworm

View file

@ -1 +1 @@
FROM redis:7.0.11-bookworm FROM redis:7.2.1-bookworm

View file

@ -1,26 +1,22 @@
ARG ZAMMAD_VERSION=6.0.0-32 ARG ZAMMAD_VERSION=6.1.0
ARG EMBEDDED
FROM node:16.18.0-slim as node
FROM zammad/zammad-docker-compose:${ZAMMAD_VERSION} AS builder FROM zammad/zammad-docker-compose:${ZAMMAD_VERSION} AS builder
COPY --from=node /opt /opt
COPY --from=node /usr/local/bin /usr/local/bin
RUN mkdir -p /opt/zammad/contrib/link/addons RUN mkdir -p /opt/zammad/contrib/link/addons
COPY addons ${ZAMMAD_DIR}/contrib/link/addons COPY addons ${ZAMMAD_DIR}/contrib/link/addons
COPY setup.rb ${ZAMMAD_DIR}/contrib/link/setup.rb COPY setup.rb ${ZAMMAD_DIR}/contrib/link/setup.rb
COPY install.rb ${ZAMMAD_DIR}/contrib/link/install.rb COPY install.rb ${ZAMMAD_DIR}/contrib/link/install.rb
RUN sed -i '/proxy_set_header X-Forwarded-User "";/d' ${ZAMMAD_DIR}/contrib/nginx/zammad.conf; ENV EMBEDDED=${embedded}
RUN if [ "$EMBEDDED" = "true" ] ; then sed -i '/proxy_set_header X-Forwarded-User "";/d' ${ZAMMAD_DIR}/contrib/nginx/zammad.conf; fi
USER root USER root
RUN set -ex; \ RUN set -ex; \
apt-get update; \ apt-get update; \
apt-get install -y --no-install-recommends nodejs git libclang-dev clang llvm pkg-config nettle-dev; apt-get install -y --no-install-recommends;
# RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
# ARG SEQUOIA_PROJECT_URL=https://gitlab.com/sequoia-pgp/sequoia-ffi.git
# ARG SEQUOIA_GIT_TAG=main
# ENV SEQUOIA_DIR=/usr/lib/sequoia
# ENV LD_LIBRARY_PATH=${SEQUOIA_DIR}/target/debug
# RUN git clone -b "${SEQUOIA_GIT_TAG}" --single-branch --depth 1 "${SEQUOIA_PROJECT_URL}" "${SEQUOIA_DIR}";
# WORKDIR ${SEQUOIA_DIR}
# RUN export PATH=~/.cargo/bin:$PATH && cargo build -p sequoia-openpgp-ffi;
WORKDIR ${ZAMMAD_DIR} WORKDIR ${ZAMMAD_DIR}
# RUN echo "gem 'ruby_openpgp', git: 'https://github.com/throneless-tech/ruby_openpgp', branch: 'signing-and-userids'" >> Gemfile.local
RUN bundle install --without test development mysql RUN bundle install --without test development mysql
RUN sed -i '/^[[:space:]]*# create install ready file/ i\ RUN sed -i '/^[[:space:]]*# create install ready file/ i\
echo "about to reinstall..."\n\ echo "about to reinstall..."\n\
@ -30,12 +26,8 @@ RUN sed -i '/^[[:space:]]*# create install ready file/ i\
USER zammad USER zammad
RUN ZAMMAD_SAFE_MODE=1 bundle exec rails runner /opt/zammad/contrib/link/install.rb RUN ZAMMAD_SAFE_MODE=1 bundle exec rails runner /opt/zammad/contrib/link/install.rb
FROM node:16.18.0-slim as node
FROM zammad/zammad-docker-compose:${ZAMMAD_VERSION} FROM zammad/zammad-docker-compose:${ZAMMAD_VERSION}
USER ${ZAMMAD_USER} USER ${ZAMMAD_USER}
# ENV SEQUOIA_DIR=/usr/lib/sequoia
# ENV LD_LIBRARY_PATH=${SEQUOIA_DIR}/target/debug
ENV ZAMMAD_READY_FILE=${ZAMMAD_DIR}/var/zammad.ready ENV ZAMMAD_READY_FILE=${ZAMMAD_DIR}/var/zammad.ready
COPY --from=node /opt /opt COPY --from=node /opt /opt
COPY --from=node /usr/local/bin /usr/local/bin COPY --from=node /usr/local/bin /usr/local/bin

1781
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,12 +9,12 @@
"lint": "eslint index.js" "lint": "eslint index.js"
}, },
"dependencies": { "dependencies": {
"@babel/core": "7.22.17", "@babel/core": "7.23.0",
"@babel/preset-env": "7.22.15", "@babel/preset-env": "7.22.20",
"@babel/preset-typescript": "7.22.15" "@babel/preset-typescript": "7.23.0"
}, },
"peerDependencies": {}, "peerDependencies": {},
"devDependencies": { "devDependencies": {
"eslint": "^8.49.0" "eslint": "^8.50.0"
} }
} }

View file

@ -9,15 +9,15 @@
"fmt": "prettier \"profile/**/*.js\" --write" "fmt": "prettier \"profile/**/*.js\" --write"
}, },
"dependencies": { "dependencies": {
"@rushstack/eslint-patch": "^1.3.3", "@rushstack/eslint-patch": "^1.5.0",
"@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/eslint-plugin": "^6.7.3",
"@typescript-eslint/parser": "^6.7.0", "@typescript-eslint/parser": "^6.7.3",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-config-xo-space": "^0.34.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-eslint-comments": "^3.2.0",
"eslint-plugin-import": "^2.28.1", "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-no-use-extend-native": "^0.5.0",
"eslint-plugin-promise": "^6.1.1", "eslint-plugin-promise": "^6.1.1",
"eslint-plugin-unicorn": "48.0.1", "eslint-plugin-unicorn": "48.0.1",
@ -28,7 +28,7 @@
"typescript": "^4.9.5" "typescript": "^4.9.5"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.49.0", "eslint": "^8.50.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"typescript": "^5.2.2" "typescript": "^5.2.2"
} }

View file

@ -9,7 +9,7 @@
"private": false, "private": false,
"devDependencies": { "devDependencies": {
"@hapi/basic": "^7.0.2", "@hapi/basic": "^7.0.2",
"@types/jest": "^29.5.4", "@types/jest": "^29.5.5",
"babel-preset-link": "*", "babel-preset-link": "*",
"eslint-config-link": "*", "eslint-config-link": "*",
"jest-config-link": "*", "jest-config-link": "*",
@ -19,7 +19,7 @@
"dependencies": { "dependencies": {
"@hapi/hapi": "^21.3.2", "@hapi/hapi": "^21.3.2",
"@hapi/hoek": "^11.0.2", "@hapi/hoek": "^11.0.2",
"joi": "^17.10.1", "joi": "^17.10.2",
"next-auth": "4.23.1" "next-auth": "4.23.1"
}, },
"scripts": { "scripts": {

View file

@ -8,7 +8,7 @@
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"private": false, "private": false,
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.4", "@types/jest": "^29.5.5",
"tsc-watch": "^6.0.4" "tsc-watch": "^6.0.4"
}, },
"dependencies": { "dependencies": {

View file

@ -9,7 +9,7 @@
"node": ">=14" "node": ">=14"
}, },
"dependencies": { "dependencies": {
"@types/jest": "^29.5.4", "@types/jest": "^29.5.5",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-junit": "^16.0.0" "jest-junit": "^16.0.0"
}, },

View file

@ -13,15 +13,15 @@
"@fontsource/poppins": "^5.0.8", "@fontsource/poppins": "^5.0.8",
"@fontsource/roboto": "^5.0.8", "@fontsource/roboto": "^5.0.8",
"@mui/icons-material": "^5", "@mui/icons-material": "^5",
"@mui/lab": "^5.0.0-alpha.143", "@mui/lab": "^5.0.0-alpha.146",
"@mui/material": "^5", "@mui/material": "^5",
"@mui/x-data-grid-pro": "^6.13.0", "@mui/x-data-grid-pro": "^6.15.0",
"@mui/x-date-pickers-pro": "^6.13.0", "@mui/x-date-pickers-pro": "^6.15.0",
"@opensearch-project/opensearch": "^2.3.1", "@opensearch-project/opensearch": "^2.3.1",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"http-proxy-middleware": "^2.0.6", "http-proxy-middleware": "^2.0.6",
"material-ui-popup-state": "^5.0.9", "material-ui-popup-state": "^5.0.9",
"next": "13.4.19", "next": "13.5.3",
"next-auth": "^4.23.1", "next-auth": "^4.23.1",
"next-http-proxy-middleware": "^1.2.5", "next-http-proxy-middleware": "^1.2.5",
"nodemailer": "^6.9.5", "nodemailer": "^6.9.5",
@ -32,20 +32,20 @@
"react-iframe": "^1.8.5", "react-iframe": "^1.8.5",
"react-markdown": "^8.0.7", "react-markdown": "^8.0.7",
"react-polyglot": "^0.7.2", "react-polyglot": "^0.7.2",
"sharp": "^0.32.5", "sharp": "^0.32.6",
"swr": "^2.2.2", "swr": "^2.2.4",
"tss-react": "^4.9.0", "tss-react": "^4.9.2",
"uuid": "^9.0.0" "uuid": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.22.17", "@babel/core": "^7.23.0",
"@types/node": "^20.6.0", "@types/node": "^20.7.0",
"@types/react": "18.2.21", "@types/react": "18.2.23",
"@types/uuid": "^9.0.3", "@types/uuid": "^9.0.4",
"babel-loader": "^9.1.3", "babel-loader": "^9.1.3",
"eslint": "^8.49.0", "eslint": "^8.50.0",
"eslint-config-airbnb": "^19.0.4", "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-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.28.1", "eslint-plugin-import": "^2.28.1",
"eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-jsx-a11y": "^6.7.1",

File diff suppressed because one or more lines are too long

View file

@ -18,9 +18,9 @@
}, },
"devDependencies": { "devDependencies": {
"@types/figlet": "^1.5.6", "@types/figlet": "^1.5.6",
"@types/lodash": "^4.14.198", "@types/lodash": "^4.14.199",
"@types/node": "*", "@types/node": "*",
"@types/uuid": "^9.0.3", "@types/uuid": "^9.0.4",
"camelcase-keys": "^9.0.0", "camelcase-keys": "^9.0.0",
"pg-monitor": "^2.0.0", "pg-monitor": "^2.0.0",
"tsc-watch": "^6.0.4", "tsc-watch": "^6.0.4",
@ -40,11 +40,11 @@
"@promster/server": "^9.0.0", "@promster/server": "^9.0.0",
"@promster/types": "^5.0.0", "@promster/types": "^5.0.0",
"@types/convict": "^6.1.4", "@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__hapi": "^20.0.13",
"@types/hapi__inert": "^5.2.6", "@types/hapi__inert": "^5.2.7",
"@types/hapi__vision": "^5.5.4", "@types/hapi__vision": "^5.5.5",
"@types/hapipal__schmervice": "^2.0.3", "@types/hapipal__schmervice": "^2.0.4",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"commander": "^11.0.0", "commander": "^11.0.0",
"convict": "^6.2.4", "convict": "^6.2.4",
@ -52,13 +52,13 @@
"figlet": "^1.6.0", "figlet": "^1.6.0",
"hapi-pino": "^12.1.0", "hapi-pino": "^12.1.0",
"http-terminator": "^3.2.0", "http-terminator": "^3.2.0",
"joi": "^17.10.1", "joi": "^17.10.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"next-auth": "^4.23.1", "next-auth": "^4.23.1",
"pg-promise": "^11.5.4", "pg-promise": "^11.5.4",
"pino": "^8.15.1", "pino": "^8.15.1",
"pino-pretty": "^10.2.0", "pino-pretty": "^10.2.0",
"prom-client": "^14.x.x", "prom-client": "^14.x.x",
"uuid": "^9.0.0" "uuid": "^9.0.1"
} }
} }

View file

@ -10,10 +10,10 @@
"@digiresilience/montar": "*" "@digiresilience/montar": "*"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.22.17", "@babel/core": "7.23.0",
"@babel/preset-env": "7.22.15", "@babel/preset-env": "7.22.20",
"@babel/preset-typescript": "7.22.15", "@babel/preset-typescript": "7.23.0",
"eslint": "^8.49.0", "eslint": "^8.50.0",
"pino-pretty": "^10.2.0", "pino-pretty": "^10.2.0",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",

View file

@ -16,11 +16,11 @@
"pg-promise": "^11.5.4" "pg-promise": "^11.5.4"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.22.17", "@babel/core": "7.23.0",
"@babel/preset-env": "7.22.15", "@babel/preset-env": "7.22.20",
"@babel/preset-typescript": "7.22.15", "@babel/preset-typescript": "7.23.0",
"@types/jest": "^29.5.4", "@types/jest": "^29.5.5",
"eslint": "^8.49.0", "eslint": "^8.50.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-junit": "^16.0.0", "jest-junit": "^16.0.0",
"pino-pretty": "^10.2.0", "pino-pretty": "^10.2.0",

View file

@ -23,7 +23,7 @@
"node": ">=14" "node": ">=14"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.4", "@types/jest": "^29.5.5",
"babel-preset-link": "*", "babel-preset-link": "*",
"eslint-config-link": "*", "eslint-config-link": "*",
"jest-config-link": "*", "jest-config-link": "*",

View file

@ -26,7 +26,7 @@
"test": "echo n/a" "test": "echo n/a"
}, },
"devDependencies": { "devDependencies": {
"@types/backoff": "^2.5.2", "@types/backoff": "^2.5.3",
"babel-preset-link": "*", "babel-preset-link": "*",
"camelcase": "^8.0.0", "camelcase": "^8.0.0",
"eslint-config-link": "*", "eslint-config-link": "*",
@ -39,8 +39,8 @@
"backoff": "^2.5.0", "backoff": "^2.5.0",
"camelcase-keys": "^9.0.0", "camelcase-keys": "^9.0.0",
"eventemitter3": "^5.0.1", "eventemitter3": "^5.0.1",
"snakecase-keys": "^5.4.6", "snakecase-keys": "^5.4.7",
"ts-custom-error": "^3.3.1", "ts-custom-error": "^3.3.1",
"uuid": "^9.0.0" "uuid": "^9.0.1"
} }
} }

View file

@ -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
[![License GNU AGPL v3.0](https://img.shields.io/badge/License-AGPL%203.0-lightgrey.svg)](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).
🤸

View file

@ -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"
}

View file

@ -1 +0,0 @@
3.1.3

View file

@ -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'

View file

@ -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

View file

@ -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'
)

View file

@ -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')

View file

@ -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 )

View file

@ -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')

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -1 +0,0 @@
.icon-pgp { width:17px; height: 17px; }

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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