diff --git a/docker/zammad/Dockerfile b/docker/zammad/Dockerfile index 1eee0b4..924c2f1 100644 --- a/docker/zammad/Dockerfile +++ b/docker/zammad/Dockerfile @@ -1,30 +1,42 @@ FROM zammad/zammad-docker-compose:5.4.1 AS builder COPY auto_install ${ZAMMAD_TMP_DIR}/auto_install + USER root RUN set -ex; \ apt-get update; \ - apt-get install -y --no-install-recommends git libclang-dev clang llvm pkg-config nettle-dev rustc cargo libmariadb-dev; - + apt-get install -y --no-install-recommends nodejs git libclang-dev clang llvm pkg-config nettle-dev; +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 cargo update -RUN cargo build -p sequoia-openpgp-ffi; -RUN ls -la "${SEQUOIA_DIR}/target/debug"; -# RUN chown -R "${ZAMMAD_USER}":"${ZAMMAD_USER}" "${SEQUOIA_DIR}" -# WORKDIR ${ZAMMAD_TMP_DIR} -# RUN echo "gem 'ruby_openpgp', git: 'https://github.com/throneless-tech/ruby_openpgp', branch: 'signing-and-userids'" >> Gemfile.local ; \ -# echo "gem 'rails-observers'" >> Gemfile.local ; \ -# bundle update tcr; \ -# bundle install --without test development mysql ; +RUN export PATH=~/.cargo/bin:$PATH && cargo build -p sequoia-openpgp-ffi; + +WORKDIR ${ZAMMAD_TMP_DIR} +RUN echo "gem 'ruby_openpgp', git: 'https://github.com/throneless-tech/ruby_openpgp', branch: 'signing-and-userids'" >> Gemfile.local +RUN echo "gem 'rails-observers'" >> Gemfile.local +RUN bundle install --without test development mysql + +RUN sed -i '/^[[:space:]]*# create install ready file/ i\ + echo "about to reinstall..."\n\ + bundle exec rake zammad:package:reinstall_all\n\ + bundle exec rake zammad:package:migrate\n\ + bundle exec rake assets:precompile\n\ + ' /docker-entrypoint.sh + +FROM node:16.18.0-slim as node + +FROM zammad/zammad-docker-compose:5.4.1 +USER ${ZAMMAD_USER} +ENV SEQUOIA_DIR=/usr/lib/sequoia +ENV LD_LIBRARY_PATH=${SEQUOIA_DIR}/target/debug +COPY --from=node /opt /opt +COPY --from=node /usr/local/bin /usr/local/bin +COPY --from=builder ${ZAMMAD_TMP_DIR} ${ZAMMAD_TMP_DIR} +COPY --from=builder ${SEQUOIA_DIR} ${SEQUOIA_DIR} +COPY --from=builder /usr/local/bundle /usr/local/bundle +COPY --from=builder /docker-entrypoint.sh /docker-entrypoint.sh -# RUN sed -i "s/# create install ready file/bundle exec rake zammad:package:migrate/g" contrib/docker/docker-entrypoint.sh -# FROM zammad/zammad-docker-compose:5.4.1 -# COPY --from=builder ${ZAMMAD_TMP_DIR} ${ZAMMAD_TMP_DIR} -# COPY --from=builder ${SEQUOIA_DIR} ${SEQUOIA_DIR} -# COPY --from=builder /usr/local/bundle /usr/local/bundle diff --git a/docker/zammad/Dockerfile-old b/docker/zammad/Dockerfile-old deleted file mode 100644 index 92e9762..0000000 --- a/docker/zammad/Dockerfile-old +++ /dev/null @@ -1,124 +0,0 @@ -FROM ruby:2.6.8-slim-bullseye AS builder - -LABEL maintainer="Abel Luck " -ARG DEBIAN_FRONTEND=noninteractive -ENV GOSU_VERSION 1.11 -COPY keys.asc /tmp/keys.asc -RUN set -ex; \ - apt-get update; \ - apt-get install -y --no-install-recommends gnupg2 dirmngr build-essential curl git libimlib2-dev libpq-dev patch shared-mime-info nodejs libclang-dev clang llvm pkg-config nettle-dev rustc cargo libmariadb-dev; \ - gpg2 --import /tmp/keys.asc ; \ - rm /tmp/keys.asc ; \ - gpgconf --kill all ; \ - rm -rf /var/lib/apt/lists/* ; \ - curl -s -J -L -o /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/${GOSU_VERSION}/gosu-$(dpkg --print-architecture)" ; \ - curl -s -J -L -o /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/${GOSU_VERSION}/gosu-$(dpkg --print-architecture).asc" ; \ - gpg2 --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu ; \ - rm /usr/local/bin/gosu.asc ; \ - chmod +x /usr/local/bin/gosu ; \ - gosu nobody true - -COPY package-auto-reinstall.patch /tmp/package-auto-reinstall.patch -COPY fetch_locales.rb /tmp/fetch_locales.rb - -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 -ARG ZAMMAD_PROJECT_URL=https://github.com/zammad/zammad.git -ARG ZAMMAD_GIT_TAG=develop -ENV ZAMMAD_TMP_DIR /tmp/zammad-${ZAMMAD_GIT_TAG} - -ENV ZAMMAD_DIR /opt/zammad -ENV ZAMMAD_USER zammad -ENV RAILS_ENV production - -RUN set -ex; \ - groupadd -g 1000 "${ZAMMAD_USER}"; \ - useradd -M -d "${ZAMMAD_DIR}" -s /bin/bash -u 1000 -g 1000 "${ZAMMAD_USER}" ; \ - git clone -b "${SEQUOIA_GIT_TAG}" --single-branch --depth 1 "${SEQUOIA_PROJECT_URL}" "${SEQUOIA_DIR}" ; \ - cd "${SEQUOIA_DIR}" && cargo build -p sequoia-openpgp-ffi ; \ - git clone -b "${ZAMMAD_GIT_TAG}" --single-branch --depth 1 "${ZAMMAD_PROJECT_URL}" "${ZAMMAD_TMP_DIR}" ; \ - cd ${ZAMMAD_TMP_DIR}; \ - echo "gem 'ruby_openpgp', git: 'https://github.com/throneless-tech/ruby_openpgp', branch: 'signing-and-userids'" >> Gemfile.local ; \ - echo "gem 'rails-observers'" >> Gemfile.local ; \ - bundle update tcr; \ - bundle install --without test development mysql ; \ - /tmp/fetch_locales.rb ; \ - sed -e 's#.*adapter: postgresql# adapter: nulldb#g' -e 's#.*username:.*# username: postgres#g' -e 's#.*password:.*# password: \n host: zammad-postgresql\n#g' < contrib/packager.io/database.yml.pkgr > config/database.yml ; \ - sed -i "/require 'rails\/all'/a require\ 'nulldb'" config/application.rb ; \ - sed -i 's/.*scheduler_\(err\|out\).log.*//g' script/scheduler.rb ; \ - touch db/schema.rb ; \ - bundle exec rake assets:precompile ; \ - chown -R "${ZAMMAD_USER}":"${ZAMMAD_USER}" "${ZAMMAD_TMP_DIR}" - -COPY auto_install "${ZAMMAD_TMP_DIR}"/auto_install - -FROM ruby:2.6.8-slim-bullseye - -LABEL maintainer="Abel Luck " -ARG BUILD_DATE -ARG DEBIAN_FRONTEND=noninteractive - -LABEL org.label-schema.build-date="$BUILD_DATE" \ - org.label-schema.name="Zammad" \ - org.label-schema.license="AGPL-3.0" \ - org.label-schema.description="Docker container for Zammad - Data Container" \ - org.label-schema.url="https://zammad.org" \ - org.label-schema.vcs-url="https://github.com/zammad/zammad" \ - org.label-schema.vcs-type="Git" \ - org.label-schema.vendor="Zammad" \ - org.label-schema.schema-version="2.9.0" \ - org.label-schema.docker.cmd="sysctl -w vm.max_map_count=262144;docker-compose up" - - -ARG ZAMMAD_GIT_TAG=develop -ENV RAILS_ENV production -ENV SEQUOIA_DIR=/usr/lib/sequoia -ENV LD_LIBRARY_PATH=${SEQUOIA_DIR}/target/debug -ENV ZAMMAD_DIR /opt/zammad -ENV ZAMMAD_READY_FILE ${ZAMMAD_DIR}/tmp/zammad.ready -ENV ZAMMAD_TMP_DIR /tmp/zammad-${ZAMMAD_GIT_TAG} -ENV ZAMMAD_USER zammad - -RUN set -ex; \ - apt-get update; \ - apt-get install -y --no-install-recommends curl libimlib2 libimlib2-dev libpq5 nginx rsync clang llvm pkg-config; \ - rm -rf /var/lib/apt/lists/* - -RUN set -ex; \ - groupadd -g 1000 "${ZAMMAD_USER}" ; \ - useradd -M -d "${ZAMMAD_DIR}" -s /bin/bash -u 1000 -g 1000 "${ZAMMAD_USER}" - -COPY --from=builder ${ZAMMAD_TMP_DIR} ${ZAMMAD_TMP_DIR} -COPY --from=builder ${SEQUOIA_DIR} ${SEQUOIA_DIR} -COPY --from=builder /usr/local/bin/gosu /usr/local/bin/gosu -COPY --from=builder /usr/local/bundle /usr/local/bundle - -COPY docker-entrypoint.sh / -RUN chmod +x /docker-entrypoint.sh -ENTRYPOINT ["/docker-entrypoint.sh"] - -WORKDIR ${ZAMMAD_DIR} - -ENV AUTOWIZARD_JSON= -ENV ELASTICSEARCH_HOST=zammad-elasticsearch -ENV ELASTICSEARCH_PORT=9200 -ENV ELASTICSEARCH_SCHEMA=http -ENV ELASTICSEARCH_SSL_VERIFY=true -ENV ELASTICSEARCH_PURGE=false -ENV MEMCACHED_HOST=zammad-memcached -ENV MEMCACHED_PORT=11211 -ENV POSTGRESQL_HOST=zammad-postgresql -ENV POSTGRESQL_PORT=5432 -ENV POSTGRESQL_USER=postgres -ENV POSTGRESQL_PASS= -ENV POSTGRESQL_DB=zammad_production -ENV POSTGRESQL_DB_CREATE=true -ENV ZAMMAD_RAILSSERVER_HOST=zammad-railsserver -ENV ZAMMAD_RAILSSERVER_PORT=3000 -ENV ZAMMAD_WEBSOCKET_HOST=zammad-websocket -ENV ZAMMAD_WEBSOCKET_PORT=6042 -ENV NGINX_SERVER_NAME=_ -ENV RAILS_SERVER puma -ENV RAILS_LOG_TO_STDOUT true diff --git a/docker/zammad/LICENSE.md b/docker/zammad/LICENSE.md deleted file mode 100644 index cba6f6a..0000000 --- a/docker/zammad/LICENSE.md +++ /dev/null @@ -1,660 +0,0 @@ -### GNU AFFERO GENERAL PUBLIC LICENSE - -Version 3, 19 November 2007 - -Copyright (C) 2007 Free Software Foundation, Inc. - - -Everyone is permitted to copy and distribute verbatim copies of this -license document, but changing it is not allowed. - -### Preamble - -The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - -The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains -free software for all its users. - -When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - -Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - -A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - -The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - -An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing -under this license. - -The precise terms and conditions for copying, distribution and -modification follow. - -### TERMS AND CONDITIONS - -#### 0. Definitions. - -"This License" refers to version 3 of the GNU Affero General Public -License. - -"Copyright" also means copyright-like laws that apply to other kinds -of works, such as semiconductor masks. - -"The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - -To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of -an exact copy. The resulting work is called a "modified version" of -the earlier work or a work "based on" the earlier work. - -A "covered work" means either the unmodified Program or a work based -on the Program. - -To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - -To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user -through a computer network, with no transfer of a copy, is not -conveying. - -An interactive user interface displays "Appropriate Legal Notices" to -the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - -#### 1. Source Code. - -The "source code" for a work means the preferred form of the work for -making modifications to it. "Object code" means any non-source form of -a work. - -A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - -The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - -The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - -The Corresponding Source need not include anything that users can -regenerate automatically from other parts of the Corresponding Source. - -The Corresponding Source for a work in source code form is that same -work. - -#### 2. Basic Permissions. - -All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - -You may make, run and propagate covered works that you do not convey, -without conditions so long as your license otherwise remains in force. -You may convey covered works to others for the sole purpose of having -them make modifications exclusively for you, or provide you with -facilities for running those works, provided that you comply with the -terms of this License in conveying all material for which you do not -control copyright. Those thus making or running the covered works for -you must do so exclusively on your behalf, under your direction and -control, on terms that prohibit them from making any copies of your -copyrighted material outside their relationship with you. - -Conveying under any other circumstances is permitted solely under the -conditions stated below. Sublicensing is not allowed; section 10 makes -it unnecessary. - -#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - -No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - -When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such -circumvention is effected by exercising rights under this License with -respect to the covered work, and you disclaim any intention to limit -operation or modification of the work as a means of enforcing, against -the work's users, your or third parties' legal rights to forbid -circumvention of technological measures. - -#### 4. Conveying Verbatim Copies. - -You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - -You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - -#### 5. Conveying Modified Source Versions. - -You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these -conditions: - -- a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. -- b) The work must carry prominent notices stating that it is - released under this License and any conditions added under - section 7. This requirement modifies the requirement in section 4 - to "keep intact all notices". -- c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. -- d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - -A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - -#### 6. Conveying Non-Source Forms. - -You may convey a covered work in object code form under the terms of -sections 4 and 5, provided that you also convey the machine-readable -Corresponding Source under the terms of this License, in one of these -ways: - -- a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. -- b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the Corresponding - Source from a network server at no charge. -- c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. -- d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. -- e) Convey the object code using peer-to-peer transmission, - provided you inform other peers where the object code and - Corresponding Source of the work are being offered to the general - public at no charge under subsection 6d. - -A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - -A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, -family, or household purposes, or (2) anything designed or sold for -incorporation into a dwelling. In determining whether a product is a -consumer product, doubtful cases shall be resolved in favor of -coverage. For a particular product received by a particular user, -"normally used" refers to a typical or common use of that class of -product, regardless of the status of the particular user or of the way -in which the particular user actually uses, or expects or is expected -to use, the product. A product is a consumer product regardless of -whether the product has substantial commercial, industrial or -non-consumer uses, unless such uses represent the only significant -mode of use of the product. - -"Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to -install and execute modified versions of a covered work in that User -Product from a modified version of its Corresponding Source. The -information must suffice to ensure that the continued functioning of -the modified object code is in no case prevented or interfered with -solely because modification has been made. - -If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - -The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or -updates for a work that has been modified or installed by the -recipient, or for the User Product in which it has been modified or -installed. Access to a network may be denied when the modification -itself materially and adversely affects the operation of the network -or violates the rules and protocols for communication across the -network. - -Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - -#### 7. Additional Terms. - -"Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - -When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - -Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders -of that material) supplement the terms of this License with terms: - -- a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or -- b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or -- c) Prohibiting misrepresentation of the origin of that material, - or requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or -- d) Limiting the use for publicity purposes of names of licensors - or authors of the material; or -- e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or -- f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions - of it) with contractual assumptions of liability to the recipient, - for any liability that these contractual assumptions directly - impose on those licensors and authors. - -All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - -If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - -Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; the -above requirements apply either way. - -#### 8. Termination. - -You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - -However, if you cease all violation of this License, then your license -from a particular copyright holder is reinstated (a) provisionally, -unless and until the copyright holder explicitly and finally -terminates your license, and (b) permanently, if the copyright holder -fails to notify you of the violation by some reasonable means prior to -60 days after the cessation. - -Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - -Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - -#### 9. Acceptance Not Required for Having Copies. - -You are not required to accept this License in order to receive or run -a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - -#### 10. Automatic Licensing of Downstream Recipients. - -Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - -An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - -You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - -#### 11. Patents. - -A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - -A contributor's "essential patent claims" are all patent claims owned -or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - -Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - -In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - -If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - -If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - -A patent license is "discriminatory" if it does not include within the -scope of its coverage, prohibits the exercise of, or is conditioned on -the non-exercise of one or more of the rights that are specifically -granted under this License. You may not convey a covered work if you -are a party to an arrangement with a third party that is in the -business of distributing software, under which you make payment to the -third party based on the extent of your activity of conveying the -work, and under which the third party grants, to any of the parties -who would receive the covered work from you, a discriminatory patent -license (a) in connection with copies of the covered work conveyed by -you (or copies made from those copies), or (b) primarily for and in -connection with specific products or compilations that contain the -covered work, unless you entered into that arrangement, or that patent -license was granted, prior to 28 March 2007. - -Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - -#### 12. No Surrender of Others' Freedom. - -If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under -this License and any other pertinent obligations, then as a -consequence you may not convey it at all. For example, if you agree to -terms that obligate you to collect a royalty for further conveying -from those to whom you convey the Program, the only way you could -satisfy both those terms and this License would be to refrain entirely -from conveying the Program. - -#### 13. Remote Network Interaction; Use with the GNU General Public License. - -Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your -version supports such interaction) an opportunity to receive the -Corresponding Source of your version by providing access to the -Corresponding Source from a network server at no charge, through some -standard or customary means of facilitating copying of software. This -Corresponding Source shall include the Corresponding Source for any -work covered by version 3 of the GNU General Public License that is -incorporated pursuant to the following paragraph. - -Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - -#### 14. Revised Versions of this License. - -The Free Software Foundation may publish revised and/or new versions -of the GNU Affero General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever -published by the Free Software Foundation. - -If the Program specifies that a proxy can decide which future versions -of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - -Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - -#### 15. Disclaimer of Warranty. - -THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT -WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND -PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE -DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR -CORRECTION. - -#### 16. Limitation of Liability. - -IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR -CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES -ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT -NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR -LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM -TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER -PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -#### 17. Interpretation of Sections 15 and 16. - -If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - -END OF TERMS AND CONDITIONS - -### How to Apply These Terms to Your New Programs - -If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these -terms. - -To do so, attach the following notices to the program. It is safest to -attach them to the start of each source file to most effectively state -the exclusion of warranty; and each file should have at least the -"copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as - published by the Free Software Foundation, either version 3 of the - License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper -mail. - -If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for -the specific requirements. - -You should also get your employer (if you work as a programmer) or -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. For more information on this, and how to apply and follow -the GNU AGPL, see . diff --git a/docker/zammad/README.md b/docker/zammad/README.md deleted file mode 100644 index 1e0e07f..0000000 --- a/docker/zammad/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# docker-zammad - -[![pipeline status](https://gitlab.com/digiresilience/link/docker-zammad/badges/master/pipeline.svg)](https://gitlab.com/digiresilience/link/docker-zammad/-/commits/master) - -Builds Link's Zammad docker container - -[Zammad](https://github.com/zammad/zammad) is a web based open source helpdesk/customer support system. - -This project started as a fork of the official [zammad docker](https://github.com/zammad/zammad-docker-compose) project. - -It builds the [Center for Digital Resilience's version of Zammad](https://gitlab.com/digiresilience/link/zammad) - -## Developer Notes - -### Building - -### Simple - -```bash -make ZAMMAD_TAG=v3.3.0 -``` - -Creates an image `digiresilience/zammad:v3.3.0` - -### Your own tag - -Supply your own docker image tag: - -```bash -make ZAMMAD_TAG=v3.3.0 DOCKER_TAG=myspecialversion -``` - -Creates an image `digiresilience/zammad:myspecialversion` - -### You special snowflake you - -```bash -make ZAMMAD_TAG=develop DOCKER_TAG=myspecialversion PROJECT_URL=http://my/zammadrepo.git DOCKER_NS=batman -``` - -Creates the image `batman/zammad:myspecialversion` based off `develop` branch of the git repo at `http://my/zammadrepo.git` - -### 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). - -## Credits and License - -Zammad is licensed under the [GNU Affero General Public License (AGPL) -v3+](https://www.gnu.org/licenses/agpl-3.0.en.html). So is this project. - -## Maintainers - -- Abel Luck of [Guardian Project](https://guardianproject.info) diff --git a/docker/zammad/docker-entrypoint.sh b/docker/zammad/docker-entrypoint.sh deleted file mode 100644 index f6188f0..0000000 --- a/docker/zammad/docker-entrypoint.sh +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/env bash - -set -e -AUTOWIZARD_JSON="${AUTOWIZARD_JSON:-}" -ELASTICSEARCH_ENABLED="${ELASTICSEARCH_ENABLED:-true}" -ELASTICSEARCH_HOST="${ELASTICSEARCH_HOST:-zammad-elasticsearch}" -ELASTICSEARCH_PORT="${ELASTICSEARCH_PORT:-9200}" -ELASTICSEARCH_SCHEMA="${ELASTICSEARCH_SCHEMA:-http}" -ELASTICSEARCH_NAMESPACE="${ELASTICSEARCH_NAMESPACE:-zammad}" -ELASTICSEARCH_REINDEX="${ELASTICSEARCH_REINDEX:-true}" -ELASTICSEARCH_SSL_VERIFY="${ELASTICSEARCH_SSL_VERIFY:-true}" -MEMCACHED_ENABLED="${MEMCACHED_DISABLED:-true}" -MEMCACHED_HOST="${MEMCACHED_HOST:-zammad-memcached}" -MEMCACHED_PORT="${MEMCACHED_PORT:-11211}" -POSTGRESQL_HOST="${POSTGRESQL_HOST:-zammad-postgresql}" -POSTGRESQL_PORT="${POSTGRESQL_PORT:-5432}" -POSTGRESQL_USER="${POSTGRESQL_USER:-postgres}" -POSTGRESQL_PASS="${POSTGRESQL_PASS:-}" -POSTGRESQL_DB="${POSTGRESQL_DB:-zammad_production}" -POSTGRESQL_DB_CREATE="${POSTGRESQL_DB_CREATE:-true}" -: "${RAILS_TRUSTED_PROXIES:=['127.0.0.1', '::1']}" -: "${RSYNC_ADDITIONAL_PARAMS:=--no-perms --no-owner}" -ZAMMAD_RAILSSERVER_HOST="${ZAMMAD_RAILSSERVER_HOST:-zammad-railsserver}" -ZAMMAD_RAILSSERVER_PORT="${ZAMMAD_RAILSSERVER_PORT:-3000}" -ZAMMAD_WEBSOCKET_HOST="${ZAMMAD_WEBSOCKET_HOST:-zammad-websocket}" -ZAMMAD_WEBSOCKET_PORT="${ZAMMAD_WEBSOCKET_PORT:-6042}" -ZAMMAD_LOG_LEVEL="${ZAMMAD_LOG_LEVEL:-warn}" -NGINX_SERVER_NAME="${NGINX_SERVER_NAME:-_}" -: "${NGINX_SERVER_SCHEME:=\$scheme}" - -function check_zammad_ready { - sleep 15 - until [ -f "${ZAMMAD_READY_FILE}" ]; do - echo "waiting for init container to finish install or update..." - sleep 10 - done -} - -# zammad init -if [ "$1" = 'zammad-init' ]; then - # install / update zammad - test -f "${ZAMMAD_READY_FILE}" && rm "${ZAMMAD_READY_FILE}" - rsync -a ${RSYNC_ADDITIONAL_PARAMS} --delete --exclude 'public/assets/images/*' --exclude 'storage/fs/*' "${ZAMMAD_TMP_DIR}/" "${ZAMMAD_DIR}" - rsync -a ${RSYNC_ADDITIONAL_PARAMS} "${ZAMMAD_TMP_DIR}"/public/assets/images/ "${ZAMMAD_DIR}"/public/assets/images - - - until (echo > /dev/tcp/"${POSTGRESQL_HOST}"/"${POSTGRESQL_PORT}") &> /dev/null; do - echo "zammad-init waiting for postgresql server to be ready..." - sleep 5 - done - - cd "${ZAMMAD_DIR}" - - # configure database - sed -e "s#.*adapter:.*# adapter: postgresql#g" -e "s#.*database:.*# database: ${POSTGRESQL_DB}#g" -e "s#.*username:.*# username: ${POSTGRESQL_USER}#g" -e "s#.*password:.*# password: ${POSTGRESQL_PASS}\\n host: ${POSTGRESQL_HOST}\\n port: ${POSTGRESQL_PORT}#g" < contrib/packager.io/database.yml.pkgr > config/database.yml - - if [ "${MEMCACHED_ENABLED}" == "true" ]; then - # configure memcache - sed -i -e "s/.*config.cache_store.*file_store.*cache_file_store.*/ config.cache_store = :dalli_store, '${MEMCACHED_HOST}:${MEMCACHED_PORT}'\\n config.session_store = :dalli_store, '${MEMCACHED_HOST}:${MEMCACHED_PORT}'/" config/application.rb - fi - - # configure trusted proxies - sed -i -e "s#config.action_dispatch.trusted_proxies =.*#config.action_dispatch.trusted_proxies = ${RAILS_TRUSTED_PROXIES}#" config/environments/production.rb - sed -i -e "s#.*config.log_level =.*#config.log_level = :${ZAMMAD_LOG_LEVEL}#" config/environments/production.rb - - - # check if database exists / update to new version - echo "initialising / updating database..." - if ! (bundle exec rails r 'puts User.any?' 2> /dev/null | grep -q true); then - if [ "${POSTGRESQL_DB_CREATE}" == "true" ]; then - bundle exec rake db:create - fi - bundle exec rake db:migrate - bundle exec rake db:seed - - # create autowizard.json on first install - if [ -n "${AUTOWIZARD_JSON}" ]; then - echo "${AUTOWIZARD_JSON}" | base64 -d > auto_wizard.json - fi - else - bundle exec rake db:migrate - fi - - # echo "auto installing packages..." - bundle exec rails r "Package.auto_install" - # bundle exec rails zammad:package:migrate - - # es config - echo "changing elasticsearch settings..." - if [ "${ELASTICSEARCH_ENABLED}" == "false" ]; then - bundle exec rails r "Setting.set('es_url', '')" - else - bundle exec rails r "Setting.set('es_url', '${ELASTICSEARCH_SCHEMA}://${ELASTICSEARCH_HOST}:${ELASTICSEARCH_PORT}')" - - bundle exec rails r "Setting.set('es_index', '${ELASTICSEARCH_NAMESPACE}')" - - if [ -n "${ELASTICSEARCH_USER}" ] && [ -n "${ELASTICSEARCH_PASS}" ]; then - bundle exec rails r "Setting.set('es_user', \"${ELASTICSEARCH_USER}\")" - bundle exec rails r "Setting.set('es_password', \"${ELASTICSEARCH_PASS}\")" - fi - - until (echo > /dev/tcp/${ELASTICSEARCH_HOST}/${ELASTICSEARCH_PORT}) &> /dev/null; do - echo "zammad railsserver waiting for elasticsearch server to be ready..." - sleep 5 - done - - if [ "${ELASTICSEARCH_SSL_VERIFY}" == "false" ]; then - SSL_SKIP_VERIFY="-k" - else - SSL_SKIP_VERIFY="" - fi - - if [ "${ELASTICSEARCH_REINDEX}" == "true" ]; then - if ! curl -s "${SSL_SKIP_VERIFY}" "${ELASTICSEARCH_SCHEMA}://${ELASTICSEARCH_HOST}:${ELASTICSEARCH_PORT}/_cat/indices" | grep -q zammad; then - echo "rebuilding es searchindex..." - bundle exec rake searchindex:rebuild - fi - fi - fi - echo "rebuilding assets..." - bundle exec rake assets:precompile &> /dev/null - - # chown everything to zammad user - chown -R "${ZAMMAD_USER}":"${ZAMMAD_USER}" "${ZAMMAD_DIR}" - - echo "zammad-init ready" - # create install ready file - su -c "echo 'zammad-init' > ${ZAMMAD_READY_FILE}" "${ZAMMAD_USER}" -fi - - -# zammad nginx -if [ "$1" = 'zammad-nginx' ]; then - check_zammad_ready - - # configure nginx - sed -e "s#proxy_set_header X-Forwarded-Proto .*;#proxy_set_header X-Forwarded-Proto ${NGINX_SERVER_SCHEME};#g" \ - -e 's#client_max_body_size .*#client_max_body_size 0;#' \ - -e "s#server .*:3000#server ${ZAMMAD_RAILSSERVER_HOST}:${ZAMMAD_RAILSSERVER_PORT}#g" \ - -e "s#server .*:6042#server ${ZAMMAD_WEBSOCKET_HOST}:${ZAMMAD_WEBSOCKET_PORT}#g" \ - -e "s#server_name .*#server_name ${NGINX_SERVER_NAME};#g" \ - -e 's#/var/log/nginx/zammad.\(access\|error\).log#/dev/stdout#g' < contrib/nginx/zammad.conf > /etc/nginx/sites-enabled/default - - echo "starting nginx..." - - exec /usr/sbin/nginx -g 'daemon off;' -fi - - -# zammad-railsserver -if [ "$1" = 'zammad-railsserver' ]; then - test -f /opt/zammad/tmp/pids/server.pid && rm /opt/zammad/tmp/pids/server.pid - - check_zammad_ready - - cd "${ZAMMAD_DIR}" - - echo "starting railsserver..." - - #shellcheck disable=SC2101 - exec gosu "${ZAMMAD_USER}":"${ZAMMAD_USER}" bundle exec rails server puma -b [::] -p "${ZAMMAD_RAILSSERVER_PORT}" -e "${RAILS_ENV}" -fi - - -# zammad-scheduler -if [ "$1" = 'zammad-scheduler' ]; then - check_zammad_ready - - cd "${ZAMMAD_DIR}" - - echo "starting scheduler..." - - exec gosu "${ZAMMAD_USER}":"${ZAMMAD_USER}" bundle exec script/scheduler.rb run -fi - - -# zammad-websocket -if [ "$1" = 'zammad-websocket' ]; then - check_zammad_ready - - cd "${ZAMMAD_DIR}" - - echo "starting websocket server..." - - exec gosu "${ZAMMAD_USER}":"${ZAMMAD_USER}" bundle exec script/websocket-server.rb -b 0.0.0.0 -p "${ZAMMAD_WEBSOCKET_PORT}" start -fi diff --git a/docker/zammad/fetch_locales.rb b/docker/zammad/fetch_locales.rb deleted file mode 100755 index 8bc4d4e..0000000 --- a/docker/zammad/fetch_locales.rb +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env ruby -# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ - -require 'rubygems' -require 'uri' -require 'net/http' -require 'json' -require 'yaml' - -version = File.read('VERSION') -version.strip! - -url_locales = 'https://i18n.zammad.com/locales.json' - -file_locales = "config/locales-#{version}.yml" - -# download locales -uri = URI.parse(url_locales) -http = Net::HTTP.new(uri.host, uri.port) -http.use_ssl = true -request = Net::HTTP::Get.new(uri) -response = http.request(request) -data = JSON.parse(response.body) - -puts "Writing #{file_locales}..." -File.open(file_locales, 'w') do |out| - YAML.dump(data, out) -end - -puts 'done' diff --git a/docker/zammad/keys.asc b/docker/zammad/keys.asc deleted file mode 100644 index 6a3850f..0000000 --- a/docker/zammad/keys.asc +++ /dev/null @@ -1,559 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -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 -=viYu ------END PGP PUBLIC KEY BLOCK----- ------BEGIN PGP PUBLIC KEY BLOCK----- - -mQINBFhoRoABEADCr3xFypcY2okDw1onNeAVt3MMS0TbQWVV8W6NHh9WN9H1tkrt -EIXH8SNSPjfsix039PQ0wQ6akmttcBYNZb6QHf89F1jFq/HU4wJQJKwFO1B9gZ7J -PDzgpmn8mmGIRuhAsP7L1EP/OQ89A4YDqLRyTpXnEA3qqHeFqe4PXXxrk0BlY0SJ -i0bAxH+GBRM+cDOpFnNkFQJXA4+fj49RGNWq7Os3IudJ8G7FQycsLZHxlU1pwzjW -CnQ+bodnoVqrKSYBPj90Rt+quUsLH8AI0jDwyn8n7C3wT3ESLLrXRDMtSUuwzDCl -T5DTtYubKezOEWnV4+0yyi6jyIumhXGMLgynI3mEmOrUqKZd6zXRSmXZSyQts6jo -Pio4Hbht0DKwnBP5AYuPSOMwY00JfJu/8WzkaDpscMn6ujRamGHP1pv1tkc8s96B -AINe61elwHIveQDoNFxsvURFOtIFJZrX1/G0h3ZRPjKKiR8/tB7BeVkoa77BPWlj -r9NZsNHjj/z/W6CuOblEgZ5IyMw1gmsqkZZs+3YLUFIct7f/dm8OWIsIkpdBqaZv -ORehSuyBLnygEhugN/N6Qf7ih0k1uymCjZqcqsNgjH+kfm86tYI1BXvsSQ3AdOTP -hUKGNfYNlOZdHCjMRKLVjVPhBpXGxugre1V59oAHCX47QYL8jlL5yC+czQARAQAB -tCxBYmVsIEx1Y2sgKGFiZWwpIDxhYmVsQGd1YXJkaWFucHJvamVjdC5pbmZvPokC -TgQTAQoAOBYhBJGFgT3czXieXUulG4hLZJw0DIH0BQJYaEaAAhshBQsJCAcDBRUK -CQgLBRYCAwEAAh4BAheAAAoJEIhLZJw0DIH0VEsQAKphoSkrwXDFEbFv+sRxiaRg -IOZ3TKhRAu3LU3XGwqCf8aykmCQXztL3okUN7J/x84fXMVPQ5DWVs4Je8ykzm+gB -FRR0kYL8nCpB8GosxKtLyuT640uPc4nT26pieskrO+zJy4rBBodmf6yWFGOyuYPv -/CwSejky1NRMTmlAbhrdcq+i8+k9V75xIZPHo0BlRCHUnfM4Bp8LkGH5n9C5Wx1v -irQWWn2m82sUBoO9t0UU38f0k2V5+//19UJz5pJixfQ7l1f7F2OTFJhq4IIjIYB/ -5cy8PV6w8EVjJQRHdP2unMXeBZ7Dpbapi6BRpIlUelkG/+27CXVkhAuYm6Eb9r3i -rPMSKZnlAOkD+8IRrs576lEtCV0yNulDB1y8BfEK+Eq8dEnKWNwb5uzLKGyDdJFP -G3sMsr/Z9bX8Eqi5EiD+w8XRJTDwr9fhAqvNO/TvTyYRo+TpiWHPj4pegrFBjbX2 -q/etfFljrT80n6QYF1Zu/Uf4aFUyAylJPRH7v1pbg7VIIyo1EWWUmnD7YLsUTbgf -pK+iMn+5Oufdn9zdg5CeGeBeDvjCsbABgmckQ9r6/VPwYGh6onK9PpWE6mJBEpTy -2qBkT8sfL2cf0sqOYwv13fHxioBMUjiiphBQ9j16MrejPRlR9jGz2Q6DyJrh7L1S -vglLyFrCsa6JpJo+JlXxiQIzBBABCgAdFiEE7mYgxxNrDSxFbApN6eKN6gCqVVYF -Alm/i6QACgkQ6eKN6gCqVVY0yxAAkG5FwZYHm6sc+h2MNT5ZVb/0yGvpoPxqWQsb -ICd9y9qflF6VjIKnWSKYWOJTwMuq6fRtrS62dzdN2K+RMaudI19noRtBssaw2WCl -8l9RpE0ZtbhLOP8wB8jaSkUOLF84t8Td/ZUCNT4X/BZUE8ov/4ZUuUL47WRV07yz -pZUKydqEw6xWnPKM8/GvrvIj0vij/00F7UURnq5LUsY1iH9EdzeTI1Hmb6OhtW2S -sG2tbkyqlUqmD8EsRZe+ek+/qwfJpWLGn6JoXuxeLvWpP/eTDDP0mFF6xK1egJyU -o7YaWMgiEcYnFYr+/TWbV8WQXrBK/qk82Grev2s0itTuLw0n5C2j8caIOsmhORKz -TbLgrK6ytGGdRikzVS0Q1zqWAKc80EWbtmHpnz8Dxq2vYfMvud04+xQ7cbqwADeA -rye8fdTkhwGwX6JOIYHoBC/IUeLjikRo1hprtn0vVC5zmU8GIhPScL1C5x3+TDOr -+VV6xGRnLqE44hd4rgRATemf8QmNjP8PLx/+PFaew3VCpiMUNsLMcJX5Tx3EyntN -YGCBHkpfc/WFqP64vMfP4uitQS5qVCq+o+Jlg7DWJz7vq7+ivuQx7Ll90Lh5493o -kH58L4MYv9O4Nb4rJslurzLFqrLrXYvQJZCMMf56j1VnCRk5XTnZNWJaF5B4bWNq -MJRpaFa5AQ0EWGhGvQEIAOQI1tWXlUtzjEozWrZuoB/iqCVB085U/NCjJ11hz4/9 -VJi4ewZJTLou5t6FIHM4l4RZ3oXzrkLxFLy9RaExGG/WSiIyLu4jF+hnU9EZZnJR -RC12kcleM1ZGJDutenVkFNGbxWySL7Dtzc1qZK0v0XL8yhdrUEDFTsJWZJRlZSLo -wGL/Rq6CiKnyMVK+2ab1LOQJQPTvswjuVsGpcjqxzUmpNiYDuCBcI6cPfSt0KZzB -aivF1EM86pkE32Jckthw/u/x1Ye1C7osxH+oGqti1aHnQRnyVL4uMTlStNshFHwb -cns4falyrIiLZTCauy3grNgpK/TREjQh/piRj1G9Si0AEQEAAYkCPAQYAQoAJgIb -DBYhBJGFgT3czXieXUulG4hLZJw0DIH0BQJcQb6vBQkFuqtyAAoJEIhLZJw0DIH0 -p3oQAKxa0fV4zS7uDZasNTnEm03OPR7wwuvaRH0UG2QfQvZ554rxqStM8vc9aYy8 -8vYdA+mpm+5jq5SqdNqp/quh1e6kSEmauBzWYm1ngkqNSqvtt7MwoXoIAWQyAsMq -uWov1AJLQvz3iA5HNepmaCgV6YZNsmOPawi0Shvz6+d6H9qC/6jilfxI1HTQgcCl -9POALCx27MGktZDmLM4slu5YW89RVlJz2P58FAQuCkADVWOuWx/jHZIqNcOzAc9L -Nlx4+9202v4hNYH/xHteJ7cejJh5H6RKQM+RpJA3sWRH7B6xro3PxMt9yVPttLcn -Fqulk5kDUct1eDX+icunq/14rY4BLpQqrc/0DnMimdF1W4ivlMD1MRZXXDdUd84D -4wVjJV8CFWTSm++guj+BBg02+xfGGrbmA1rqciTCY23Y9UpHUB/oBPTzHUBkNHsL -8Tg/QJu2tJ0JQDKcBQCKLPlmgQrF2+clc0IPLgy0YgTm1Z3hHlOSNnflSFzoXdHx -df6WlscXDiJWMEu35bOWRaeORQX0CplGNXpUZnCytjDUFlI/dVTpqZ9PEB8cXXFt -YebLPPhj24DNHPTcrsbhVCDFlIjjrLb4X54GwftenXl8n2qXm7DcJ0mHTESlDirz -GrShh5KVyjhN6kBXgTYYROma9DM2svmPzXAKDZ4QhuwafYU9uQENBFhoRr0BCADP -0Df9A0P9fc3hlWKZgo6RN4OL+oKnCxxCm4N5H1Dg0pFsxlGS43rYCUrZP/YZOZbS -eWLS095gf1zwXYg7BhgHYldMt7dkWvXJkaWYtqoh7Nhj396U39Z34G4is5aQhC5p -ncU7rmcy/hBWeoocM3ep32nvgbaVNTg0dOEE6Nw6FfEyvaKBEAOhXXTFoS7rVwYZ -cKRyrBRKPY/Ln/TjKjTnqfq9h6QfJi/jgsU47qg1wZ3yuBiE1iEoSMZEz0lAQx51 -kDsKwPvCQ79EPmQTCGZeZqSpM6dtYmznGr8jqd1tF1SRDwzZozQ53DsPw6AUSaIm -GKQzgN97lg80c4Yx0TJtABEBAAGJA3IEGAEKACYCGwIWIQSRhYE93M14nl1LpRuI -S2ScNAyB9AUCXEG+tgUJBbqreQFAwHQgBBkBCgAdFiEEpjlV15DEkBwxvQOW14DH -58WSeMgFAlhoRr0ACgkQ14DH58WSeMg8aAf/aM/zAMBQ2UTKswjWTH7ER/tzIrAo -NNNr0sogyN+/vgsY1Vp+WhlLZa5bgKAjHTVo6vDl4KshIqzVCcRtLt071skdTD+p -T2292IjmyW/Tfo/F+6fZTwH8yLmVoOpdAPNH3zvFAf3vKxAwJRodqNguMluRBMQ0 -1da/1vOCTwgftfIaisaJEUvPg3mjFQhfTIQIeU6pC+wPez6uBP4lJ1G5lX9sgJGc -p2PiZHXksozzxQ24YUC7o1g5291fBxMP4b/8pZ3ukBs/4URW7C/znyi/U2J5RBFT -pidJocWPxLE0O7a/f4PatUALL2OmJbX3zO6gvnYbumEWUys/X5e4TYQH9gkQiEtk -nDQMgfRcnRAAkQaoVI+5Vo8f2uMjfF89I/bj2Puat9OcBGhF0+QDdjxHmSvBA9yk -5f3J0xZVjqXLm1s0yp0lw1to37y9pMBF/w4mvIG9Pfbfd/H65Mg1qLEPCrkqRiw0 -of9hQcU2gMb6nEkYdB72PXm3NtUTGCjZTGa6HNO+H2OQC6PCsguDwdXwtTpWHXog -a0WsEaUm4zJKxAyNHPRbetwrmMoawAHnRWoVSM8XFXDwXqKC4FtxgpcL0uin/ipN -3geGN519k1mG/UB2e2HLrpdNvZqRsl8WFhS0uuOeSWZci5ci6HSyqkDPeLDsdsKP -PCTcOxPM2O2gp1Lec0qhSPboSuwob7ZTZASUbXVvYMeBLgkFNv4eL0R0ZbxqsI8t -b1l7dYWNQ+H5YJyjz5RW2xmLDsHKr9eaP+4Vd8iS66LqMZHD3VV1AQ6X2HVrjw4+ -9AKkpo85jmcgRH3dw3f0x2Vt0AG/u6Zko0Dt3f7eQsqKRkVMNihGJ8bWh6KNlZXe -ysWFp3dnw8b8xMpxDw09lAonnE58XQT/e3c3viuH3q6PDm9+rj+z9VKAOnTbWEko -4apoE51a7agMkQMZbk5OODxuaFUPKH/E8xsEbtJ1HuyHwd+avxk1EFwO9kMAuK73 -37Qviw6J6i9raC4P2mOKBelcmyhFIy7Lsev49fZTpCMzCgRtr/YBh9G5AQ0EXEG+ -xwEIALgE7g2GEFW3k/3LmiAJpzdKJkUuxze1Rp9twD5QD+IWSuVFbQeyie2xoVVW -B3LL7lDOj7eDK+85gI5ed+/0QFyDIVQqZ5LNzHaIQt5SDCaECyrXWHxWI6GbNBix -PTeHCR+h43D0/nWxAAAtCrYDlS29G7JHil55Bmck8iSB4MTojOkc6l77t3sZD2J1 -oQuM6DWKyuuDld2Degd2Bs0XtnCYIzvuj5wkt6LZB5EYRDpPvNFmU5R4+ye9Q3I3 -3Hg1xe/8w/Di1oDGTs4TRwbmUEFf6OrQWmQhqf2xO5AjJlhBWdsnNkeJoeZBhvOc -W0Rw/oIJTAcsORCkGVXW85US3x0AEQEAAYkCPAQYAQgAJhYhBJGFgT3czXieXUul -G4hLZJw0DIH0BQJcQb7HAhsgBQkB4TOAAAoJEIhLZJw0DIH0F8kQAJc2fmE55hrM -RoNOaGPE2VqBM8Hrv4jDkSjcQqN89OXnBXWzS77PGiEoNPl0x/SSIlB3JZpC2yaw -FcKjZWAln3US3DaurF7wHQ9jXYm3DvD6/W+nZJl4zAtOzZBAMCy3Q/GbLr2T5iPh -Y9DAd/znSHQJkQfqS6WP1xS1D8FwcRMAAmvqiUTq06Dwe+wSddfvnMZ51mGQeHQ0 -O3SdT4xEHHY3FW49gCT9uOleugZwhwV7YEjmOPlYiDBAoC5V6SIpQIy1z9uUaQEG -9Y/NF2d72LfI17N/2FKhpOY9kQZgopLu7tACIaBH1KrT0wbVWbl/v+bz49RWOtC9 -YsE9og4fhJ9AscXqJybPryAQSblQkYjmInu3xmAQXRqL5aZZerLeCLoWNPChQj8t -ON+8tJEr4Z8KI/4zsSPqAnXx0yaFGalkwUbgWS89mKGPTER9kypHTtIIN4+4jn8P -/gn0/qT/BrGYSV/mIBWYKize+i1yys1LbnBwiTOMXc7jNF5VbJnCK764WUo/1fGW -AKK9mgsgnQS4ogudWRpCrLaYPFBZxSwjJEh/DxIm/cMLeQyr3SFc88X3gJkSICxU -Rcx68TeLJ9AYe/zK7FMmo5WQilUi0I6qiHyZxUpTfztsUSW/DccptT5rakf+0bGW -sAUZwzJ6wZuOXVyvP2c4PWZvz0xg/53TuQINBF4XYdQBEADHeKkR978ggqGvHjks -UC6msXV3AG0fQN3KqaES6mDPkRVLe46xjSruFSix+EXzwC8Ez/VJ5jRD2VX2B4eS -D2Q83ZXsuUloI07c14qXkiHpq40RmI7XixfChcgxouwhpxwgWJgcdWS9a6YjC6aD -QAZ0Q3SrqM/ngISFSf3jOvCgYdmjv1/2cwYK8SHYxOUz8fjx2a8Arx4LU0Lo50Rz -Dof02/QyuMFm2hbn4gWEFAKQ3trQ9CnDdvhFsIqJob49mu4TTbIvzLhMYgcY9IfM -hSn2U5AxqYUKO+NQV1ptgl8l/ozL8C1NYlb+wxVbstEqG9bGib97VuQuq1EF9yqX -IHXFVCouyqZnkipe5Sxs678n4dWimq4EiKe+bUyusAlm5OmGzChErfuzQKcJkZbg -AjKBGfeAMVbvdueFWe2zC9AlxFQBq/U8vWVfKIYhOlfHd8jvnXrv3Dzw7LHYqbHp -x4dG9jbPsZb5SbTjo9XioeDLXKBi+K+M/FW16airzLZamyXqrCl1N0fydg7DtnZK -ZYtw6WkemqlctlgrqmkHCjm8RDUfYZkRzErXcKgsJRL8xQPthppjqYZoP2SI2czJ -Aom8nWe1siG4D8jNshQ96AQkP9zM34oMueJ81ve3EAjGb7p0YP05y6W4p6M7twWu -81vi69+Lc8K9KLLgp8N+TMn70QARAQABiQRyBBgBCAAmFiEEkYWBPdzNeJ5dS6Ub -iEtknDQMgfQFAl4XYdQCGwIFCQHuYoACQAkQiEtknDQMgfTBdCAEGQEIAB0WIQQN -4e6eInfqwPwdC4ZI7NsnpIJ6rgUCXhdh1AAKCRBI7NsnpIJ6rsV4D/46f8zawRKG -h+7AyI0JWHj7bDHIa+S3h7xPD/btYVY4Zd+xD8rGmP8KpHRWwslFtJf2rOZkT+jt -ZgoaZHHKwArtvYmWIkviFYd9pMsQ3pgaUf7U9hV92M2aLZrGz1MYSGydzYUu6I1v -dYq3T4szdwfZvSvlgSym8LeWSkalV6apJlAAyn+j6K6TqPGz6y6Ay8Gjext2KnzD -4SM7UBc9ypn3Uaj9+eSYsUTZpStMgUy9lEGVAozAM/+nWGsPjqYuqtiWA2IVfLyT -tjIqHL5hvDX+XMEnV2awurzesYxwv2gKXYSXR1CoenUMwCgcKgb8+DDqo+ElSMmJ -2mNitQrwISv+r2q1ViP0fePhMjVG4AghVw+8trudInAoMUoLYZws99Q41jL1RVe3 -ZW8iut+kox4ukPMa87UP75TiNghcAhN9+3yThNjzHdvQo9R6d+GCZp9wgDCK4FUi -61Svvqme9Q0BIvU84I4qGNQUHK0HMyBzghdu9YMHIOSOklH36xXyZ0AgQ2wHBCfK -Uw+nj+KZ2oYHPOylwUlBA4sMQmwqPyzPevfdDzwl2GLOMeJWz2XqK+Wnjz56+WN2 -psnBQfpn67inZ6MotzaTGnf9jpK/+PCHR6WE3Wz2XQrYg+yvRIcBRaQqa2yeRG0S -7lRdEjMnjYbfVyu9B7cbapvI5F7tufgfQjtPD/4oH1Uua6DRHajMXThyUBxhvyI6 -cNVJNa/0iLV+dkxp2AdphCOVMoAWqmOKl328QFga/rbewdB9rc+XGxbgnxnhmx78 -wCwe4V5GLxhZ+hgyRyzGaaf5HLd2/jjF2EGgTbaPZlhUYRMVdMmLz4r3MtK1xTa5 -BAAxPAy9Hn9qilFqmU/4ZY9Hzp/ORwPn7Ao9EKESDCE6trN5F1y4tzCwDJ/NQvQu -4MqTK30E1CxXiVyG6qHuJEmWCdReLzRQ8mJVxyEhuxZiWzTXHdc6+0l9fmtrdd0r -H3IIWJhPCz/LO/9bMHJnXQB5BRKXw5vZxSuqCEkfWBnj5utaHj0NrhkwEitkgQya -XRoAtJ2aaHZf5KiUXwclBDpDSL4Zb3GgI+USo6v95QWggWoURTO6RwTOHv7pymf7 -WcR0n9Lnc5GohsPsEDal1uZ8EnD+llErd4Vm5xdlgrxgEYeCqCI9fm+Re+iyQsnE -EDB1XM1jVSFFOvZmmKc0FUTPWqdC9eeaihYuaGGZhJilAw6hJmtAcjrjgp9IBXqg -f5whE0MtK33bWQXKWrTmy0BWCXxfWb+lATx2y6q8ghvbS1AB7vAw4/nF3TzHdO3u -Z9LR3nI5EA++MHfAbK73k4XX8KWdNyzi6YAJnsGbB9yKfR3ttNd/uUBOV/Y5eh4O -uQJOsAHUSZTIEsXVi7kCDQReF2I+ARAA0508Y3JwOCizaJgHDb2G5D4b7ofb4NwM -AJu4rRKWzYj9s4+elFPnmv+lWdtzKy82KYZus3T0AAlGPf9rbjAkjpZ8z2ZhxRFx -FggK5I7L9kXMPQYZ1XAiQyf0y0EtbbnzsMWdq20+tZeDCv49eKDd5jnIlifotrNW -p939DwA1zC61l2rrT7gA+lN6NOrXxxLiitChRBgcC2I+vzKGcfgGSY1cwh5yT+2o -T2yrca/qsJeRR04Tz5bYVv2Gb2USYXt+/CITQg6wR94DlDXW9M+8cFBF2JWFJnni -rpjkcKGSugiWp9BEaPNq9wOt52hfFGmQmL5/AZNVYM0C6g+ODyMwJrW/a0MrvZbZ -iJcvgD3OU5AKkUEwmS5KfhLwOYJktWvy7gaOo3/vGo1P4vE/QhgsOjwUps7n+uL7 -JL/vGZLJB9W4K4g/ofvp00knSehhE3ZnXncA4G8F5M4T6wSvJCgRgYI1BU8bFRNM -nohhEly7eXIAjkY2zIxt29NMpfCTbFHlPhwYj2OZhEmKnI2lksSTi6r/DkU6gr1O -J5ENsd9gVT8uIyIoBDM6S2IzHVuSGTTKNSo3KZasHuwmEeki8a3hsGuuaZepYrGw -bpH3aTu+oZ2xdLzlwzSEWxCI3TNzSqHS59ICjvwdFKyJx8AJUOHn06XDCC7qRWcw -6UCPXwI0y4kAEQEAAYkCPAQYAQgAJhYhBJGFgT3czXieXUulG4hLZJw0DIH0BQJe -F2I+AhsMBQkB7mKAAAoJEIhLZJw0DIH0E6gP/1jCiZMtcccXTBHtgg4toWiTC/0u -757Ea+BmzY34kjFx63ANK/+/Kusv/+Y91eijR1UhYds9Y5tmGYIwhbmGJgDz2FD2 -CVd9tXisoWUtDRflVcSu2SJ0+DMzBZXxrxHaGsWPo1S5dYHOIWSTik/Y5eWvgWHo -tMzhquL8cfzAzXpPX1jqBrJrXJCY016yt4vb6Nko2AoDee+DTwS93ZQV1vMZlf3Y -7+rvt5C6C9D5PxVk3ETtJK/eukcp+cAjRn7/EPkUoAUDzccvMwa9BoaiqPTRI5k8 -QsmYhg12Kcwu9X37VOUY2eoec2m6+k3dC6iMPZdNXptpgqzcF0p+mUj4/Zuxdixm -d4rNDv+J+3rwXcfDAt6RtQVpDj2EhpxV3srt1H+1FehVYpi8aahXN7ch0RhtRwAT -QHcmQ6X6FKM7HVaYG48FPwbvLWb3MEKt6iAwlkvzYparStA57hXNdvECTaGigYM1 -k63enoQ8AxpDAmwB3Y9d8duXpGa7wMWa7axdUV64rfog054nwH4vki0jJjnrz50K -qB3yVgc4A5YNaBZt9T45yF+wMT/kR3j5E4r/3RfReb2xw4UJ4jRy8Mg08aHvFUER -gleE5F68pwbfe/xytaq+PIVbD7WAJea+Euja3LpYEwevOwwCrouqFnQMJ0zJmyRj -ImeRZ27WyrurJD0guQINBF4XZM0BEAD+E2zSaYypg3YEppd0z4EQZCIbASNVKAD0 -GYsCAxiq1bCYPJFN/5hmhyHujDyMvXRz6uArEcUgLzh3iyVtFHmehbEQy0Gmo7Uj -Hi97oebo7JNN5Yq5c8G4tZqo37yAg3MBneJY3vq1uxHOXqrpzHBe1RvHzO2/ydMY -HGUdNaQL3iZRyw2uuS07zdLuLxDx546RpVtC2lZCm/Xn42A4q17ibub08zoq/s1F -obuDlXG3j9JPUDaOt4g2bK0sKMHjWtbcq+XhnWJKJ2k5l+gY2ro6OgqvPyYkauNg -SDnL0vUui/T/PfgkDer9PhLIo5TYWc+rFeiYqEcwwDEKP3x301Ywh7F3UJsxf8lQ -wAhZqeRnb8n17O+Zv1vZ67QlFFibnjeUvCWvMZ0hemCgdsKGzR3T2cMu0k5iL5Xm -i/mTRC+iN/0Heo3N1sGytMpweKJJR32+GlL0qlai93aSbL73356ksibhP6R5e/c0 -aTmUhgtcU1LZqEqo6eRii8SCab9pQw3/RoMxUZiLUigJJCpRAlAv6MGg7xUOZx+g -jyOiOVvom+DkAAel/L7hm6f5i9XRE+5bS74wkirHwWOoZnZkisSUs/ieOFwwG7ph -G6QrdJQ8xfxjPMR8W9xnrYD1Vd0qJBjnmUtHtg9el3see38huUHDbgY83x3ut6lW -1tS8YTrE1QARAQABiQI8BBgBCAAmFiEEkYWBPdzNeJ5dS6UbiEtknDQMgfQFAl4X -ZM0CGyAFCQHuYoAACgkQiEtknDQMgfTt7BAAjn/zMf0+br7Sk0DmOLgMaAh8Kcye -6VIiJmycJSaoe8j+2c8zwvZkrBnFIT6t53VmFw9l7AAFCmrud/ZIwc5se/E+wKzG -wKt1sMcWGtQ4TocD4M1ycdEdQb7nMPA47dN7ySee+KeBZJqe/SWSfkwc2Vf7KIM4 -xU8SATbWSJchALE77nD3K+w8p4s6sl0FRxFEp1YRh0u7Y94ATPg/apArhfZHxmK6 -d+pJLD05ZIjfDzRrNy9JYEug5KmIf9u9Y7XjWOQmT4O8PjYHIyuf/QbumRwYNxNA -0E90tXcVNaO8f4uEe6Yqzvy22YaBIekDa0Pex21O+nlzfLi/vmxeXook7S+OC9X+ -YD5ctbqFYsRHgR88NB6KLlESaSTI70KUSmrIWqOf2OUhiZMV70UevfQY2PwD5lfi -mlLbdd9XUVWJKHwU0mFwMrR2yK9YzuQC2b7/4wqCOGnjGtYGJcvKT1atLUeEKpoL -S3MDhW6751ZyADPa8M+rygrLmN3sv5gy/eLC6QxSoqV1Fwj4V9OTdQf4qHRGRJ4O -xWGYyv4ElpbXtnhi6No1EYe5LNASNQJipuvEbtLrT+NMKDGeQLET2ppHHNsJKCdq -c5Y6zNsd8CaSTHnskuQMr3RbII/CrONNxK2UX7kwoVeAr8WpsvqKoO9Bufr5Xj85 -qt8sxLYxB1Tx//g= -=U5jx ------END PGP PUBLIC KEY BLOCK----- diff --git a/docker/zammad/package-auto-reinstall.patch b/docker/zammad/package-auto-reinstall.patch deleted file mode 100644 index 9180e9a..0000000 --- a/docker/zammad/package-auto-reinstall.patch +++ /dev/null @@ -1,34 +0,0 @@ -diff --git a/app/models/package.rb b/app/models/package.rb -index 76f21fa..2915fa5 100644 ---- a/app/models/package.rb -+++ b/app/models/package.rb -@@ -493,4 +493,29 @@ execute all pending package migrations at once - file.exclude?('..') && file.exclude?('%2e%2e') - end - private_class_method :allowed_file_path? -+ -+ def self.auto_reinstall -+ path = "#{@@root}/auto_install/" -+ return if !File.exist?(path) -+ data = [] -+ Dir.foreach(path) do |entry| -+ if entry =~ /\.zpm/ && entry !~ /^\./ -+ data.push entry -+ end -+ end -+ data.each do |file| -+ json = _read_file("#{path}/#{file}", true) -+ package = JSON.parse(json) -+ installed_pkg = Package.find_by(name: package['name']) -+ if installed_pkg.nil? or Gem::Version.new(installed_pkg.version) < Gem::Version.new(package["version"]) -+ # install new package or newer version -+ install(string: json) -+ elsif Gem::Version.new(installed_pkg.version) == Gem::Version.new(package["version"]) -+ # reinstall existing version -+ install(string: json, reinstall: true) -+ end -+ end -+ data -+ end -+ - end diff --git a/packages/zammad-addon-hardening/src/db/addon/hardening/20200211123028_hardening_harden_settings.rb b/packages/zammad-addon-hardening/src/db/addon/hardening/20200211123028_hardening_harden_settings.rb index 14bdc31..73ff52a 100644 --- a/packages/zammad-addon-hardening/src/db/addon/hardening/20200211123028_hardening_harden_settings.rb +++ b/packages/zammad-addon-hardening/src/db/addon/hardening/20200211123028_hardening_harden_settings.rb @@ -1,46 +1,50 @@ class HardeningHardenSettings < ActiveRecord::Migration[5.2] def self.restore_setting(name) s = Setting.find_by(name: name) - if !s.nil? - s.state_current = s.state_initial - s.save! - end + return if s.nil? + + s.state_current = s.state_initial + s.save! end + def self.set_setting(name, value) s = Setting.find_by(name: name) - if !s.nil? - s.state_current = { "value" => value } - s.save! - end + return if s.nil? + + s.state_current = { 'value' => value } + s.save! end + def self.up - ["ui_send_client_stats", "geo_ip_backend", "geo_location_backend", "image_backend", "geo_calendar_backend"].each { |n| - self.set_setting(n, "") - } + %w[ui_send_client_stats geo_ip_backend geo_location_backend image_backend + geo_calendar_backend].each do |n| + set_setting(n, '') + end # disable customer ticket creation - self.set_setting("customer_ticket_create", false) + set_setting('customer_ticket_create', false) # disable user account registration - self.set_setting("user_create_account", false) + set_setting('user_create_account', false) # bump up min password length - self.set_setting("password_min_size", 10) + set_setting('password_min_size', 10) # delete default zammad user - nicole = User.find_by(email: "nicole.braun@zammad.org") - if !nicole.nil? - Ticket.where(customer: nicole).destroy_all - nicole.destroy - end + nicole = User.find_by(email: 'nicole.braun@zammad.org') + return if nicole.nil? + + Ticket.where(customer: nicole).destroy_all + nicole.destroy end def self.down - ["ui_send_client_stats", "geo_ip_backend", "geo_location_backend", "image_backend", "geo_calendar_backend"].each { |n| - self.restore_setting(n) - } - ["customer_ticket_create", "user_create_account", "password_min_size"].each { |n| - self.restore_setting(n) - } + %w[ui_send_client_stats geo_ip_backend geo_location_backend image_backend + geo_calendar_backend].each do |n| + restore_setting(n) + end + %w[customer_ticket_create user_create_account password_min_size].each do |n| + restore_setting(n) + end end end diff --git a/packages/zammad-addon-metamigo/src/db/addon/cdr_signal/20210525091356_cdr_signal_channel.rb b/packages/zammad-addon-metamigo/src/db/addon/metamigo/20210525091356_cdr_signal_channel.rb similarity index 100% rename from packages/zammad-addon-metamigo/src/db/addon/cdr_signal/20210525091356_cdr_signal_channel.rb rename to packages/zammad-addon-metamigo/src/db/addon/metamigo/20210525091356_cdr_signal_channel.rb diff --git a/packages/zammad-addon-metamigo/src/db/addon/cdr_voice/20210525091357_cdr_voice_channel.rb b/packages/zammad-addon-metamigo/src/db/addon/metamigo/20210525091357_cdr_voice_channel.rb similarity index 100% rename from packages/zammad-addon-metamigo/src/db/addon/cdr_voice/20210525091357_cdr_voice_channel.rb rename to packages/zammad-addon-metamigo/src/db/addon/metamigo/20210525091357_cdr_voice_channel.rb diff --git a/packages/zammad-addon-metamigo/src/db/addon/cdr_whatsapp/20210525091358_cdr_whatsapp_channel.rb b/packages/zammad-addon-metamigo/src/db/addon/metamigo/20210525091358_cdr_whatsapp_channel.rb similarity index 100% rename from packages/zammad-addon-metamigo/src/db/addon/cdr_whatsapp/20210525091358_cdr_whatsapp_channel.rb rename to packages/zammad-addon-metamigo/src/db/addon/metamigo/20210525091358_cdr_whatsapp_channel.rb diff --git a/packages/zammad-addon-pgp/src/app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee b/packages/zammad-addon-pgp/src/app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee new file mode 100644 index 0000000..b083e50 --- /dev/null +++ b/packages/zammad-addon-pgp/src/app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee @@ -0,0 +1,658 @@ +# 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 = $('') + for groupKey, groupMeta of groups + displayName = App.i18n.translateInline(groupMeta.name) + selection.closest('select').append("") + 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("") + 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 = $("") + 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("") + 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 = $("") + 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("") + 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') diff --git a/packages/zammad-addon-pgp/src/app/models/ticket.rb b/packages/zammad-addon-pgp/src/app/models/ticket.rb index 77c09b9..7231152 100644 --- a/packages/zammad-addon-pgp/src/app/models/ticket.rb +++ b/packages/zammad-addon-pgp/src/app/models/ticket.rb @@ -1,19 +1,17 @@ -# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ +# Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/ class Ticket < ApplicationModel include CanBeImported include HasActivityStreamLog include ChecksClientNotification - include ChecksLatestChangeObserved include CanCsvImport include ChecksHtmlSanitized include HasHistory include HasTags include HasSearchIndexBackend include HasOnlineNotifications - include HasKarmaActivityLog include HasLinks - include HasObjectManagerAttributesValidation + include HasObjectManagerAttributes include HasTaskbars include Ticket::CallsStatsTicketReopenLog include Ticket::EnqueuesUserTicketCounterJob @@ -21,6 +19,8 @@ class Ticket < ApplicationModel include Ticket::SetsCloseTime include Ticket::SetsOnlineNotificationSeen include Ticket::TouchesAssociations + include Ticket::TriggersSubscriptions + include Ticket::ChecksReopenAfterCertainTime include ::Ticket::Escalation include ::Ticket::Subject @@ -38,10 +38,15 @@ class Ticket < ApplicationModel include HasTransactionDispatcher + # workflow checks should run after before_create and before_update callbacks + include ChecksCoreWorkflow + validates :group_id, presence: true activity_stream_permission 'ticket.agent' + core_workflow_screens 'create_middle', 'edit', 'overview_bulk' + activity_stream_attributes_ignored :organization_id, # organization_id will change automatically on user update :create_article_type_id, :create_article_sender_id, @@ -57,19 +62,26 @@ class Ticket < ApplicationModel :update_escalation_at, :update_in_min, :update_diff_in_min, + :last_close_at, :last_contact_at, :last_contact_agent_at, :last_contact_customer_at, :last_owner_update_at, :preferences + search_index_attributes_relevant :organization_id, + :group_id, + :state_id, + :priority_id + history_attributes_ignored :create_article_type_id, :create_article_sender_id, :article_count, :preferences - history_relation_object 'Ticket::Article', 'Mention' + history_relation_object 'Ticket::Article', 'Mention', 'Ticket::SharedDraftZoom' + validates :note, length: { maximum: 250 } sanitized_html :note belongs_to :group, optional: true @@ -78,6 +90,7 @@ class Ticket < ApplicationModel has_many :ticket_time_accounting, class_name: 'Ticket::TimeAccounting', dependent: :destroy, inverse_of: :ticket has_many :flags, class_name: 'Ticket::Flag', dependent: :destroy has_many :mentions, as: :mentionable, dependent: :destroy + has_one :shared_draft, class_name: 'Ticket::SharedDraftZoom', inverse_of: :ticket, dependent: :destroy belongs_to :state, class_name: 'Ticket::State', optional: true belongs_to :priority, class_name: 'Ticket::Priority', optional: true belongs_to :owner, class_name: 'User', optional: true @@ -93,43 +106,6 @@ class Ticket < ApplicationModel =begin -get user access conditions - - conditions = Ticket.access_condition( User.find(1) , 'full') - -returns - - result = [user1, user2, ...] - -=end - - def self.access_condition(user, access) - sql = [] - bind = [] - - if user.permissions?('ticket.agent') - sql.push('group_id IN (?)') - bind.push(user.group_ids_access(access)) - end - - if user.permissions?('ticket.customer') - if !user.organization || ( !user.organization.shared || user.organization.shared == false ) - sql.push('tickets.customer_id = ?') - bind.push(user.id) - else - sql.push('(tickets.customer_id = ? OR tickets.organization_id = ?)') - bind.push(user.id) - bind.push(user.organization.id) - end - end - - return if sql.blank? - - [ sql.join(' OR ') ].concat(bind) - end - -=begin - processes tickets which have reached their pending time and sets next state_id processed_tickets = Ticket.process_pending @@ -204,6 +180,31 @@ returns result end + def auto_assign(user) + return if !persisted? + return if Setting.get('ticket_auto_assignment').blank? + return if owner_id != 1 + return if !TicketPolicy.new(user, self).full? + + user_ids_ignore = Array(Setting.get('ticket_auto_assignment_user_ids_ignore')).map(&:to_i) + return if user_ids_ignore.include?(user.id) + + ticket_auto_assignment_selector = Setting.get('ticket_auto_assignment_selector') + return if ticket_auto_assignment_selector.blank? + + condition = ticket_auto_assignment_selector[:condition].merge( + 'ticket.id' => { + 'operator' => 'is', + 'value' => id, + } + ) + + ticket_count, = Ticket.selectors(condition, limit: 1, current_user: user, access: 'full') + return if ticket_count.to_i.zero? + + update!(owner: user) + end + =begin processes escalated tickets @@ -220,7 +221,7 @@ returns result = [] # fetch all escalated and soon to be escalating tickets - where('escalation_at <= ?', Time.zone.now + 15.minutes).find_each(batch_size: 500) do |ticket| + where('escalation_at <= ?', 15.minutes.from_now).find_each(batch_size: 500) do |ticket| article_id = nil article = Ticket::Article.last_customer_agent_article(ticket.id) @@ -241,7 +242,7 @@ returns next end - # check if warning need to be sent + # check if warning needs to be sent TransactionJob.perform_now( object: 'Ticket', type: 'escalation_warning', @@ -321,10 +322,10 @@ returns # prevent cross merging tickets target_ticket = Ticket.find_by(id: data[:ticket_id]) raise 'no target ticket given' if !target_ticket - raise Exceptions::UnprocessableEntity, 'ticket already merged, no merge into merged ticket possible' if target_ticket.state.state_type.name == 'merged' + raise Exceptions::UnprocessableEntity, __('It is not possible to merge into an already merged ticket.') if target_ticket.state.state_type.name == 'merged' # check different ticket ids - raise Exceptions::UnprocessableEntity, 'Can\'t merge ticket with it self!' if id == target_ticket.id + raise Exceptions::UnprocessableEntity, __('A ticket cannot be merged into itself.') if id == target_ticket.id # update articles Transaction.execute context: 'merge' do @@ -413,6 +414,26 @@ returns # touch new ticket (to broadcast change) target_ticket.touch # rubocop:disable Rails/SkipsModelValidations + + EventBuffer.add('transaction', { + object: target_ticket.class.name, + type: 'update.received_merge', + data: target_ticket, + changes: {}, + id: target_ticket.id, + user_id: UserInfo.current_user_id, + created_at: Time.zone.now, + }) + + EventBuffer.add('transaction', { + object: self.class.name, + type: 'update.merged_into', + data: self, + changes: {}, + id: id, + user_id: UserInfo.current_user_id, + created_at: Time.zone.now, + }) end true end @@ -489,7 +510,7 @@ get count of tickets and tickets which match on selector access = options[:access] || 'full' raise 'no selectors given' if !selectors - query, bind_params, tables = selector2sql(selectors, current_user: current_user, execution_time: options[:execution_time]) + query, bind_params, tables = selector2sql(selectors, options) return [] if !query ActiveRecord::Base.transaction(requires_new: true) do @@ -497,20 +518,20 @@ get count of tickets and tickets which match on selector if !current_user || access == 'ignore' ticket_count = Ticket.distinct.where(query, *bind_params).joins(tables).count tickets = Ticket.distinct.where(query, *bind_params).joins(tables).limit(limit) - return [ticket_count, tickets] + next [ticket_count, tickets] end - access_condition = Ticket.access_condition(current_user, access) - ticket_count = Ticket.distinct.where(access_condition).where(query, *bind_params).joins(tables).count - tickets = Ticket.distinct.where(access_condition).where(query, *bind_params).joins(tables).limit(limit) + tickets = "TicketPolicy::#{access.camelize}Scope".constantize + .new(current_user).resolve + .distinct + .where(query, *bind_params) + .joins(tables) - return [ticket_count, tickets] + next [tickets.count, tickets.limit(limit)] rescue ActiveRecord::StatementInvalid => e Rails.logger.error e raise ActiveRecord::Rollback - end - [] end =begin @@ -561,419 +582,7 @@ condition example =end def self.selector2sql(selectors, options = {}) - current_user = options[:current_user] - current_user_id = UserInfo.current_user_id - if current_user - current_user_id = current_user.id - end - return if !selectors - - # remember query and bind params - query = '' - bind_params = [] - like = Rails.application.config.db_like - - if selectors.respond_to?(:permit!) - selectors = selectors.permit!.to_h - end - - # get tables to join - tables = '' - selectors.each do |attribute, selector_raw| - attributes = attribute.split('.') - selector = selector_raw.stringify_keys - next if !attributes[1] - next if attributes[0] == 'execution_time' - next if tables.include?(attributes[0]) - next if attributes[0] == 'ticket' && attributes[1] != 'mention_user_ids' - next if attributes[0] == 'ticket' && attributes[1] == 'mention_user_ids' && selector['pre_condition'] == 'not_set' - - if query != '' - query += ' AND ' - end - case attributes[0] - when 'customer' - tables += ', users customers' - query += 'tickets.customer_id = customers.id' - when 'organization' - tables += ', organizations' - query += 'tickets.organization_id = organizations.id' - when 'owner' - tables += ', users owners' - query += 'tickets.owner_id = owners.id' - when 'article' - tables += ', ticket_articles articles' - query += 'tickets.id = articles.ticket_id' - when 'ticket_state' - tables += ', ticket_states' - query += 'tickets.state_id = ticket_states.id' - when 'ticket' - if attributes[1] == 'mention_user_ids' - tables += ', mentions' - query += "tickets.id = mentions.mentionable_id AND mentions.mentionable_type = 'Ticket'" - end - else - raise "invalid selector #{attribute.inspect}->#{attributes.inspect}" - end - end - - # add conditions - no_result = false - selectors.each do |attribute, selector_raw| - - # validation - raise "Invalid selector #{selector_raw.inspect}" if !selector_raw - raise "Invalid selector #{selector_raw.inspect}" if !selector_raw.respond_to?(:key?) - - selector = selector_raw.stringify_keys - raise "Invalid selector, operator missing #{selector.inspect}" if !selector['operator'] - raise "Invalid selector, operator #{selector['operator']} is invalid #{selector.inspect}" if !selector['operator'].match?(%r{^(is|is\snot|contains|contains\s(not|all|one|all\snot|one\snot)|(after|before)\s\(absolute\)|(within\snext|within\slast|after|before|till|from)\s\(relative\))|(is\sin\sworking\stime|is\snot\sin\sworking\stime)$}) - - # validate value / allow blank but only if pre_condition exists and is not specific - if !selector.key?('value') || - (selector['value'].instance_of?(Array) && selector['value'].respond_to?(:blank?) && selector['value'].blank?) || - (selector['operator'].start_with?('contains') && selector['value'].respond_to?(:blank?) && selector['value'].blank?) - return nil if selector['pre_condition'].nil? - return nil if selector['pre_condition'].respond_to?(:blank?) && selector['pre_condition'].blank? - return nil if selector['pre_condition'] == 'specific' - end - - # validate pre_condition values - return nil if selector['pre_condition'] && selector['pre_condition'] !~ %r{^(not_set|current_user\.|specific)} - - # get attributes - attributes = attribute.split('.') - attribute = "#{ActiveRecord::Base.connection.quote_table_name("#{attributes[0]}s")}.#{ActiveRecord::Base.connection.quote_column_name(attributes[1])}" - - # magic selectors - if attributes[0] == 'ticket' && attributes[1] == 'out_of_office_replacement_id' - attribute = "#{ActiveRecord::Base.connection.quote_table_name("#{attributes[0]}s")}.#{ActiveRecord::Base.connection.quote_column_name('owner_id')}" - end - - if attributes[0] == 'ticket' && attributes[1] == 'tags' - selector['value'] = selector['value'].split(',').collect(&:strip) - end - - if selector['operator'].include?('in working time') - next if attributes[1] != 'calendar_id' - raise 'Please enable execution_time feature to use it (currently only allowed for triggers and schedulers)' if !options[:execution_time] - - biz = Calendar.lookup(id: selector['value'])&.biz - next if biz.blank? - - if ( selector['operator'] == 'is in working time' && !biz.in_hours?(Time.zone.now) ) || ( selector['operator'] == 'is not in working time' && biz.in_hours?(Time.zone.now) ) - no_result = true - break - end - - # skip to next condition - next - end - - if query != '' - query += ' AND ' - end - - # because of no grouping support we select not_set by sub select for mentions - if attributes[0] == 'ticket' && attributes[1] == 'mention_user_ids' - if selector['pre_condition'] == 'not_set' - query += if selector['operator'] == 'is' - "(SELECT 1 FROM mentions mentions_sub WHERE mentions_sub.mentionable_type = 'Ticket' AND mentions_sub.mentionable_id = tickets.id) IS NULL" - else - "1 = (SELECT 1 FROM mentions mentions_sub WHERE mentions_sub.mentionable_type = 'Ticket' AND mentions_sub.mentionable_id = tickets.id)" - end - else - query += if selector['operator'] == 'is' - 'mentions.user_id IN (?)' - else - 'mentions.user_id NOT IN (?)' - end - if selector['pre_condition'] == 'current_user.id' - bind_params.push current_user_id - else - bind_params.push selector['value'] - end - end - next - end - - if selector['operator'] == 'is' - if selector['pre_condition'] == 'not_set' - if attributes[1].match?(%r{^(created_by|updated_by|owner|customer|user)_id}) - query += "(#{attribute} IS NULL OR #{attribute} IN (?))" - bind_params.push 1 - else - query += "#{attribute} IS NULL" - end - elsif selector['pre_condition'] == 'current_user.id' - raise "Use current_user.id in selector, but no current_user is set #{selector.inspect}" if !current_user_id - - query += "#{attribute} IN (?)" - if attributes[1] == 'out_of_office_replacement_id' - bind_params.push User.find(current_user_id).out_of_office_agent_of.pluck(:id) - else - bind_params.push current_user_id - end - elsif selector['pre_condition'] == 'current_user.organization_id' - raise "Use current_user.id in selector, but no current_user is set #{selector.inspect}" if !current_user_id - - query += "#{attribute} IN (?)" - user = User.find_by(id: current_user_id) - bind_params.push user.organization_id - else - # rubocop:disable Style/IfInsideElse - if selector['value'].nil? - query += "#{attribute} IS NULL" - else - if attributes[1] == 'out_of_office_replacement_id' - query += "#{attribute} IN (?)" - bind_params.push User.find(selector['value']).out_of_office_agent_of.pluck(:id) - else - if selector['value'].class != Array - selector['value'] = [selector['value']] - end - query += if selector['value'].include?('') - "(#{attribute} IN (?) OR #{attribute} IS NULL)" - else - "#{attribute} IN (?)" - end - bind_params.push selector['value'] - end - end - # rubocop:enable Style/IfInsideElse - end - elsif selector['operator'] == 'is not' - if selector['pre_condition'] == 'not_set' - if attributes[1].match?(%r{^(created_by|updated_by|owner|customer|user)_id}) - query += "(#{attribute} IS NOT NULL AND #{attribute} NOT IN (?))" - bind_params.push 1 - else - query += "#{attribute} IS NOT NULL" - end - elsif selector['pre_condition'] == 'current_user.id' - query += "(#{attribute} IS NULL OR #{attribute} NOT IN (?))" - if attributes[1] == 'out_of_office_replacement_id' - bind_params.push User.find(current_user_id).out_of_office_agent_of.pluck(:id) - else - bind_params.push current_user_id - end - elsif selector['pre_condition'] == 'current_user.organization_id' - query += "(#{attribute} IS NULL OR #{attribute} NOT IN (?))" - user = User.find_by(id: current_user_id) - bind_params.push user.organization_id - else - # rubocop:disable Style/IfInsideElse - if selector['value'].nil? - query += "#{attribute} IS NOT NULL" - else - if attributes[1] == 'out_of_office_replacement_id' - bind_params.push User.find(selector['value']).out_of_office_agent_of.pluck(:id) - query += "(#{attribute} IS NULL OR #{attribute} NOT IN (?))" - else - if selector['value'].class != Array - selector['value'] = [selector['value']] - end - query += if selector['value'].include?('') - "(#{attribute} IS NOT NULL AND #{attribute} NOT IN (?))" - else - "(#{attribute} IS NULL OR #{attribute} NOT IN (?))" - end - bind_params.push selector['value'] - end - end - # rubocop:enable Style/IfInsideElse - end - elsif selector['operator'] == 'contains' - query += "#{attribute} #{like} (?)" - value = "%#{selector['value']}%" - bind_params.push value - elsif selector['operator'] == 'contains not' - query += "#{attribute} NOT #{like} (?)" - value = "%#{selector['value']}%" - bind_params.push value - elsif selector['operator'] == 'contains all' && attributes[0] == 'ticket' && attributes[1] == 'tags' - query += "? = ( - SELECT - COUNT(*) - FROM - tag_objects, - tag_items, - tags - WHERE - tickets.id = tags.o_id AND - tag_objects.id = tags.tag_object_id AND - tag_objects.name = 'Ticket' AND - tag_items.id = tags.tag_item_id AND - tag_items.name IN (?) - )" - bind_params.push selector['value'].count - bind_params.push selector['value'] - elsif selector['operator'] == 'contains one' && attributes[0] == 'ticket' && attributes[1] == 'tags' - tables += ', tag_objects, tag_items, tags' - query += " - tickets.id = tags.o_id AND - tag_objects.id = tags.tag_object_id AND - tag_objects.name = 'Ticket' AND - tag_items.id = tags.tag_item_id AND - tag_items.name IN (?)" - - bind_params.push selector['value'] - elsif selector['operator'] == 'contains all not' && attributes[0] == 'ticket' && attributes[1] == 'tags' - query += "0 = ( - SELECT - COUNT(*) - FROM - tag_objects, - tag_items, - tags - WHERE - tickets.id = tags.o_id AND - tag_objects.id = tags.tag_object_id AND - tag_objects.name = 'Ticket' AND - tag_items.id = tags.tag_item_id AND - tag_items.name IN (?) - )" - bind_params.push selector['value'] - elsif selector['operator'] == 'contains one not' && attributes[0] == 'ticket' && attributes[1] == 'tags' - query += "( - SELECT - COUNT(*) - FROM - tag_objects, - tag_items, - tags - WHERE - tickets.id = tags.o_id AND - tag_objects.id = tags.tag_object_id AND - tag_objects.name = 'Ticket' AND - tag_items.id = tags.tag_item_id AND - tag_items.name IN (?) - ) BETWEEN 0 AND 0" - bind_params.push selector['value'] - elsif selector['operator'] == 'before (absolute)' - query += "#{attribute} <= ?" - bind_params.push selector['value'] - elsif selector['operator'] == 'after (absolute)' - query += "#{attribute} >= ?" - bind_params.push selector['value'] - elsif selector['operator'] == 'within last (relative)' - query += "#{attribute} BETWEEN ? AND ?" - time = nil - case selector['range'] - when 'minute' - time = selector['value'].to_i.minutes.ago - when 'hour' - time = selector['value'].to_i.hours.ago - when 'day' - time = selector['value'].to_i.days.ago - when 'month' - time = selector['value'].to_i.months.ago - when 'year' - time = selector['value'].to_i.years.ago - else - raise "Unknown selector attributes '#{selector.inspect}'" - end - bind_params.push time - bind_params.push Time.zone.now - elsif selector['operator'] == 'within next (relative)' - query += "#{attribute} BETWEEN ? AND ?" - time = nil - case selector['range'] - when 'minute' - time = selector['value'].to_i.minutes.from_now - when 'hour' - time = selector['value'].to_i.hours.from_now - when 'day' - time = selector['value'].to_i.days.from_now - when 'month' - time = selector['value'].to_i.months.from_now - when 'year' - time = selector['value'].to_i.years.from_now - else - raise "Unknown selector attributes '#{selector.inspect}'" - end - bind_params.push Time.zone.now - bind_params.push time - elsif selector['operator'] == 'before (relative)' - query += "#{attribute} <= ?" - time = nil - case selector['range'] - when 'minute' - time = selector['value'].to_i.minutes.ago - when 'hour' - time = selector['value'].to_i.hours.ago - when 'day' - time = selector['value'].to_i.days.ago - when 'month' - time = selector['value'].to_i.months.ago - when 'year' - time = selector['value'].to_i.years.ago - else - raise "Unknown selector attributes '#{selector.inspect}'" - end - bind_params.push time - elsif selector['operator'] == 'after (relative)' - query += "#{attribute} >= ?" - time = nil - case selector['range'] - when 'minute' - time = selector['value'].to_i.minutes.from_now - when 'hour' - time = selector['value'].to_i.hours.from_now - when 'day' - time = selector['value'].to_i.days.from_now - when 'month' - time = selector['value'].to_i.months.from_now - when 'year' - time = selector['value'].to_i.years.from_now - else - raise "Unknown selector attributes '#{selector.inspect}'" - end - bind_params.push time - elsif selector['operator'] == 'till (relative)' - query += "#{attribute} <= ?" - time = nil - case selector['range'] - when 'minute' - time = selector['value'].to_i.minutes.from_now - when 'hour' - time = selector['value'].to_i.hours.from_now - when 'day' - time = selector['value'].to_i.days.from_now - when 'month' - time = selector['value'].to_i.months.from_now - when 'year' - time = selector['value'].to_i.years.from_now - else - raise "Unknown selector attributes '#{selector.inspect}'" - end - bind_params.push time - elsif selector['operator'] == 'from (relative)' - query += "#{attribute} >= ?" - time = nil - case selector['range'] - when 'minute' - time = selector['value'].to_i.minutes.ago - when 'hour' - time = selector['value'].to_i.hours.ago - when 'day' - time = selector['value'].to_i.days.ago - when 'month' - time = selector['value'].to_i.months.ago - when 'year' - time = selector['value'].to_i.years.ago - else - raise "Unknown selector attributes '#{selector.inspect}'" - end - bind_params.push time - else - raise "Invalid operator '#{selector['operator']}' for '#{selector['value'].inspect}'" - end - end - - return if no_result - - [query, bind_params, tables] + Ticket::Selector::Sql.new(selector: selectors, options: options).get end =begin @@ -1011,9 +620,10 @@ perform changes on ticket end end + objects = build_notification_template_objects(article) perform_notification = {} - perform_article = {} - changed = false + perform_article = {} + changed = false perform.each do |key, value| (object_name, attribute) = key.split('.', 2) raise "Unable to update object #{object_name}.#{attribute}, only can update tickets, send notifications and create articles!" if object_name != 'ticket' && object_name != 'article' && object_name != 'notification' @@ -1034,23 +644,7 @@ perform changes on ticket when 'static' value['value'] when 'relative' - pendtil = Time.zone.now - val = value['value'].to_i - - case value['range'] - when 'day' - pendtil += val.days - when 'minute' - pendtil += val.minutes - when 'hour' - pendtil += val.hours - when 'month' - pendtil += val.months - when 'year' - pendtil += val.years - end - - pendtil + TimeRangeHelper.relative(range: value['range'], value: value['value']) end if new_value @@ -1095,7 +689,7 @@ perform changes on ticket if value['pre_condition'].start_with?('not_set') value['value'] = 1 elsif value['pre_condition'].start_with?('current_user.') - raise 'Unable to use current_user, got no current_user_id for ticket.perform_changes' if !current_user_id + raise __("The required parameter 'current_user_id' is missing.") if !current_user_id value['value'] = current_user_id end @@ -1106,6 +700,14 @@ perform changes on ticket changed = true + if value['value'].is_a?(String) + value['value'] = NotificationFactory::Mailer.template( + templateInline: value['value'], + objects: objects, + quote: true, + ) + end + self[attribute] = value['value'] logger.debug { "set #{object_name}.#{attribute} = #{value['value'].inspect} for ticket_id #{id}" } end @@ -1114,10 +716,8 @@ perform changes on ticket save! end - objects = build_notification_template_objects(article) - perform_article.each do |key, value| - raise 'Unable to create article, we only support article.note' if key != 'article.note' + raise __("Article could not be created. An unsupported key other than 'article.note' was provided.") if key != 'article.note' add_trigger_note(id, value, objects, perform_origin) end @@ -1209,7 +809,7 @@ perform active triggers on ticket else ::Trigger.where(active: true).order(:name) end - return [true, 'No triggers active'] if triggers.blank? + return [true, __('No triggers active')] if triggers.blank? # check if notification should be send because of customer emails send_notification = true @@ -1226,95 +826,6 @@ perform active triggers on ticket triggers.each do |trigger| logger.debug { "Probe trigger (#{trigger.name}/#{trigger.id}) for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" } - condition = trigger.condition - - # check if one article attribute is used - one_has_changed_done = false - article_selector = false - trigger.condition.each_key do |key| - (object_name, attribute) = key.split('.', 2) - next if object_name != 'article' - next if attribute == 'id' - - article_selector = true - end - if article && article_selector - one_has_changed_done = true - end - if article && type == 'update' - one_has_changed_done = true - end - - # check ticket "has changed" options - has_changed_done = true - condition.each do |key, value| - next if value.blank? - next if value['operator'].blank? - next if !value['operator']['has changed'] - - # remove condition item, because it has changed - (object_name, attribute) = key.split('.', 2) - next if object_name != 'ticket' - next if item[:changes].blank? - next if !item[:changes].key?(attribute) - - condition.delete(key) - one_has_changed_done = true - end - - # check if we have not matching "has changed" attributes - condition.each_value do |value| - next if value.blank? - next if value['operator'].blank? - next if !value['operator']['has changed'] - - has_changed_done = false - break - end - - # check ticket action - if condition['ticket.action'] - next if condition['ticket.action']['operator'] == 'is' && condition['ticket.action']['value'] != type - next if condition['ticket.action']['operator'] != 'is' && condition['ticket.action']['value'] == type - - condition.delete('ticket.action') - end - next if !has_changed_done - - # check in min one attribute of condition has changed on update - one_has_changed_condition = false - if type == 'update' - - # verify if ticket condition exists - condition.each_key do |key| - (object_name, attribute) = key.split('.', 2) - next if object_name != 'ticket' - - one_has_changed_condition = true - next if item[:changes].blank? - next if !item[:changes].key?(attribute) - - one_has_changed_done = true - break - end - next if one_has_changed_condition && !one_has_changed_done - end - - # check if ticket selector is matching - condition['ticket.id'] = { - operator: 'is', - value: ticket.id, - } - next if article_selector && !article - - # check if article selector is matching - if article_selector - condition['article.id'] = { - operator: 'is', - value: article.id, - } - end - user_id = ticket.updated_by_id if article user_id = article.updated_by_id @@ -1323,7 +834,7 @@ perform active triggers on ticket user = User.lookup(id: user_id) # verify is condition is matching - ticket_count, tickets = Ticket.selectors(condition, limit: 1, execution_time: true, current_user: user, access: 'ignore') + ticket_count, tickets = Ticket.selectors(trigger.condition, limit: 1, execution_time: true, current_user: user, access: 'ignore', ticket_action: type, ticket_id: ticket.id, article_id: article&.id, changes: item[:changes], changes_required: true) next if ticket_count.blank? next if ticket_count.zero? @@ -1455,7 +966,7 @@ result customer = User.find_by(id: customer_id) return true if !customer - return true if organization_id == customer.organization_id + return true if organization_id.present? && customer.organization_id?(organization_id) self.organization_id = customer.organization_id true @@ -1523,9 +1034,30 @@ result # if another email notification trigger preceded this one # (see https://github.com/zammad/zammad/issues/1543) def build_notification_template_objects(article) + last_article = nil + last_internal_article = nil + last_external_article = nil + all_articles = articles + + if article.nil? + last_article = all_articles.last + last_internal_article = all_articles.reverse.find(&:internal?) + last_external_article = all_articles.reverse.find { |a| !a.internal? } + else + last_article = article + last_internal_article = article.internal? ? article : all_articles.reverse.find(&:internal?) + last_external_article = article.internal? ? all_articles.reverse.find { |a| !a.internal? } : article + end + { - ticket: self, - article: article || articles.last + ticket: self, + article: last_article, + last_article: last_article, + last_internal_article: last_internal_article, + last_external_article: last_external_article, + created_article: article, + created_internal_article: article&.internal? ? article : nil, + created_external_article: article&.internal? ? nil : article, } end @@ -1587,7 +1119,7 @@ result Mail::AddressList.new(recipient_email).addresses.each do |address| recipient_email = address.address email_address_validation = EmailAddressValidation.new(recipient_email) - break if recipient_email.present? && email_address_validation.valid_format? + break if recipient_email.present? && email_address_validation.valid? end rescue if recipient_email.present? @@ -1600,7 +1132,7 @@ result end email_address_validation = EmailAddressValidation.new(recipient_email) - next if !email_address_validation.valid_format? + next if !email_address_validation.valid? # do not send notification if system address next if EmailAddress.exists?(email: recipient_email.downcase) @@ -1700,7 +1232,7 @@ result sign = value['sign'].present? && value['sign'] != 'no' encryption = value['encryption'].present? && value['encryption'] != 'no' security = { - type: security_type, + type: security_type, sign: { success: false, }, @@ -1717,10 +1249,10 @@ result else cert = SMIMECertificate.for_sender_email_address(from) end - begin list = Mail::AddressList.new(email_address.email) from = list.addresses.first.to_s + if cert && !cert.expired? sign_found = true security[:sign][:success] = true @@ -1795,7 +1327,7 @@ result ) attachments_inline.each do |attachment| - Store.add( + Store.create!( object: 'Ticket::Article', o_id: message.id, data: attachment[:data], @@ -1805,6 +1337,11 @@ result end original_article = objects[:article] + + if ActiveModel::Type::Boolean.new.cast(value['include_attachments']) == true && original_article&.attachments.present? + original_article.clone_attachments('Ticket::Article', message.id, only_attached_attachments: true) + end + if original_article&.should_clone_inline_attachments? # rubocop:disable Style/GuardClause original_article.clone_attachments('Ticket::Article', message.id, only_inline_attachments: true) original_article.should_clone_inline_attachments = false # cancel the temporary flag after cloning @@ -1903,7 +1440,15 @@ result return 0 if !user.preferences[:mail_delivery_failed] return 0 if user.preferences[:mail_delivery_failed_data].blank? - # blocked for 60 full days - (user.preferences[:mail_delivery_failed_data].to_date - Time.zone.now.to_date).to_i + 61 + # blocked for 60 full days; see #4459 + remaining_days = (user.preferences[:mail_delivery_failed_data].to_date - Time.zone.now.to_date).to_i + 61 + return remaining_days if remaining_days.positive? + + # cleanup user preferences + user.preferences[:mail_delivery_failed] = false + user.preferences[:mail_delivery_failed_data] = nil + user.save! + 0 end + end diff --git a/packages/zammad-addon-pgp/src/db/addon/pgp_support/20220403000001_pgp_support.rb b/packages/zammad-addon-pgp/src/db/addon/pgp_support/20220403000001_pgp_support.rb new file mode 100644 index 0000000..b586175 --- /dev/null +++ b/packages/zammad-addon-pgp/src/db/addon/pgp_support/20220403000001_pgp_support.rb @@ -0,0 +1,65 @@ +class PGPSupport < ActiveRecord::Migration[5.2] + def self.up + # return if it's a new setup + # return unless Setting.exists?(name: 'system_init_done') + + 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 + ) + Setting.create_if_not_exists( + title: 'Defines postmaster filter.', + name: '0016_postmaster_filter_smime', + area: 'Postmaster::PreFilter', + description: 'Defines postmaster filter to handle secure mailing.', + options: {}, + state: 'Channel::Filter::SecureMailing', + frontend: false + ) + + 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 + end +end diff --git a/packages/zammad-addon-pgp/src/db/seeds/settings.rb b/packages/zammad-addon-pgp/src/db/seeds/settings.rb index 4648889..d7ba498 100644 --- a/packages/zammad-addon-pgp/src/db/seeds/settings.rb +++ b/packages/zammad-addon-pgp/src/db/seeds/settings.rb @@ -1,8 +1,10 @@ +# Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/ + Setting.create_if_not_exists( - title: 'Application secret', + title: __('Application secret'), name: 'application_secret', area: 'Core', - description: 'Defines the random application secret.', + description: __('Defines the random application secret.'), options: {}, state: SecureRandom.hex(128), preferences: { @@ -12,30 +14,30 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'System Init Done', + title: __('System Init Done'), name: 'system_init_done', area: 'Core', - description: 'Defines if application is in init mode.', + description: __('Defines if application is in init mode.'), options: {}, state: false, preferences: { online_service_disable: true }, frontend: true ) Setting.create_if_not_exists( - title: 'App Version', + title: __('App Version'), name: 'app_version', area: 'Core::WebApp', - description: 'Only used internally to propagate current web app version to clients.', + description: __('Only used internally to propagate current web app version to clients.'), options: {}, state: '', preferences: { online_service_disable: true }, frontend: false ) Setting.create_if_not_exists( - title: 'Maintenance Mode', + title: __('Maintenance Mode'), name: 'maintenance_mode', area: 'Core::WebApp', - description: 'Enable or disable the maintenance mode of Zammad. If enabled, all non-administrators get logged out and only administrators can start a new session.', + description: __('Enable or disable the maintenance mode of Zammad. If enabled, all non-administrators get logged out and only administrators can start a new session.'), options: {}, state: false, preferences: { @@ -44,10 +46,10 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'Maintenance Login', + title: __('Maintenance Login'), name: 'maintenance_login', area: 'Core::WebApp', - description: 'Put a message on the login page. To change it, click on the text area below and change it in-line.', + description: __('Put a message on the login page. To change it, click on the text area below and change it in-line.'), options: {}, state: false, preferences: { @@ -56,42 +58,42 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'Maintenance Login', + title: __('Maintenance Login'), name: 'maintenance_login_message', area: 'Core::WebApp', - description: 'Message for login page.', + description: __('Message for login page.'), options: {}, - state: 'Something about to share. Click here to change.', + state: __('This is a default maintenance message. Click here to change.'), preferences: { permission: ['admin.maintenance'], }, frontend: true ) Setting.create_if_not_exists( - title: 'Developer System', + title: __('Developer System'), name: 'developer_mode', area: 'Core::Develop', - description: 'Defines if application is in developer mode (useful for developer, all users have the same password, password reset will work without email delivery).', + description: __('Defines if the application is in developer mode (all users have the same password and password reset will work without email delivery).'), options: {}, state: Rails.env.development?, preferences: { online_service_disable: true }, frontend: true ) Setting.create_if_not_exists( - title: 'Online Service', + title: __('Online Service'), name: 'system_online_service', area: 'Core', - description: 'Defines if application is used as online service.', + description: __('Defines if application is used as online service.'), options: {}, state: false, preferences: { online_service_disable: true }, frontend: true ) Setting.create_if_not_exists( - title: 'Product Name', + title: __('Product Name'), name: 'product_name', area: 'System::Branding', - description: 'Defines the name of the application, shown in the web interface, tabs and title bar of the web browser.', + description: __('Defines the name of the application, shown in the web interface, tabs, and title bar of the web browser.'), options: { form: [ { @@ -108,14 +110,14 @@ Setting.create_if_not_exists( placeholder: true, permission: ['admin.branding'], }, - state: 'Zammad Helpdesk', + state: __('Zammad Helpdesk'), frontend: true ) Setting.create_if_not_exists( - title: 'Logo', + title: __('Logo'), name: 'product_logo', area: 'System::Branding', - description: 'Defines the logo of the application, shown in the web interface.', + description: __('Defines the logo of the application, shown in the web interface.'), options: { form: [ { @@ -135,10 +137,10 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'Organization', + title: __('Organization'), name: 'organization', area: 'System::Branding', - description: 'Will be shown in the app and is included in email footers.', + description: __('Will be shown in the app and is included in email footers.'), options: { form: [ { @@ -158,10 +160,10 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'Locale', + title: __('Locale'), name: 'locale_default', area: 'System::Branding', - description: 'Defines the system default language.', + description: __('Defines the default system language.'), options: { form: [ { @@ -178,10 +180,10 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'Timezone', + title: __('Timezone'), name: 'timezone_default', area: 'System::Branding', - description: 'Defines the system default timezone.', + description: __('Defines the default system timezone.'), options: { form: [ { @@ -198,22 +200,23 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_or_update( - title: 'Pretty Date', + title: __('Pretty Date'), name: 'pretty_date_format', area: 'System::Branding', - description: 'Defines pretty date format.', + description: __('Defines pretty date format.'), options: { form: [ { - display: '', - null: false, - name: 'pretty_date_format', - tag: 'select', - options: { - relative: 'relative - e. g. "2 hours ago" or "2 days and 15 minutes ago"', - absolute: 'absolute - e. g. "Monday 09:30" or "Tuesday 23. Feb 14:20"', - timestamp: 'timestamp - e. g. "2018-08-30 14:30"', + display: '', + null: false, + name: 'pretty_date_format', + tag: 'select', + options: { + relative: __('relative - e. g. "2 hours ago" or "2 days and 15 minutes ago"'), + absolute: __('absolute - e. g. "Monday 09:30" or "Tuesday 23. Feb 14:20"'), + timestamp: __('timestamp - e. g. "2018-08-30 14:30"'), }, + translate: true, }, ], }, @@ -229,12 +232,12 @@ options = {} (10..99).each do |item| options[item] = item end -system_id = rand(10..99) +system_id = rand(10..99) # rubocop:disable Zammad/ForbidRand Setting.create_if_not_exists( - title: 'SystemID', + title: __('SystemID'), name: 'system_id', area: 'System::Base', - description: 'Defines the system identifier. Every ticket number contains this ID. This ensures that only tickets which belong to your system will be processed as follow-ups (useful when communicating between two instances of Zammad).', + description: __('Defines the system identifier. Every ticket number contains this ID. This ensures that only tickets which belong to your system will be processed as follow-ups (useful when communicating between two instances of Zammad).'), options: { form: [ { @@ -256,10 +259,10 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'Fully Qualified Domain Name', + title: __('Fully Qualified Domain Name'), name: 'fqdn', area: 'System::Base', - description: 'Defines the fully qualified domain name of the system. This setting is used as a variable, #{setting.fqdn} which is found in all forms of messaging used by the application, to build links to the tickets within your system.', # rubocop:disable Lint/InterpolationCheck + description: __('Defines the fully qualified domain name of the system. This setting is used as a variable, #{setting.fqdn} which is found in all forms of messaging used by the application, to build links to the tickets within your system.'), # rubocop:disable Lint/InterpolationCheck options: { form: [ { @@ -279,18 +282,18 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'Websocket backend', + title: __('Websocket backend'), name: 'websocket_backend', area: 'System::WebSocket', - description: 'Defines how to reach websocket server. "websocket" is default on production, "websocketPort" is for CI', + description: __('Defines how to reach websocket server. "websocket" is default on production, "websocketPort" is for CI'), state: Rails.env.production? ? 'websocket' : 'websocketPort', frontend: true ) Setting.create_if_not_exists( - title: 'Websocket port', + title: __('Websocket port'), name: 'websocket_port', area: 'System::WebSocket', - description: 'Defines the port of the websocket server.', + description: __('Defines the port of the websocket server.'), options: { form: [ { @@ -306,10 +309,10 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'HTTP type', + title: __('HTTP type'), name: 'http_type', area: 'System::Base', - description: 'Define the http protocol of your instance.', + description: __('Defines the HTTP protocol of your instance.'), options: { form: [ { @@ -334,22 +337,23 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Storage Mechanism', + title: __('Storage Mechanism'), name: 'storage_provider', area: 'System::Storage', - description: '"Database" stores all attachments in the database (not recommended for storing large amounts of data). "Filesystem" stores the data in the filesystem. You can switch between the modules even on a system that is already in production without any loss of data.', + description: __('"Database" stores all attachments in the database (not recommended for storing large amounts of data). "Filesystem" stores the data in the filesystem. You can switch between the modules even on a system that is already in production without any loss of data.'), options: { form: [ { - display: '', - null: true, - name: 'storage_provider', - tag: 'select', - tranlate: true, - options: { - 'DB' => 'Database', - 'File' => 'Filesystem', + display: '', + null: true, + name: 'storage_provider', + tag: 'select', + tranlate: true, + options: { + 'DB' => __('Database'), + 'File' => __('Filesystem'), }, + translate: true, }, ], }, @@ -363,10 +367,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Image Service', + title: __('Image Service'), name: 'image_backend', area: 'System::Services', - description: 'Defines the backend for user and organization image lookups.', + description: __('Defines the backend for user and organization image lookups.'), options: { form: [ { @@ -376,7 +380,7 @@ Setting.create_if_not_exists( tag: 'select', options: { '' => '-', - 'Service::Image::Zammad' => 'Zammad Image Service', + 'Service::Image::Zammad' => __('Zammad Image Service'), }, }, ], @@ -390,10 +394,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Geo IP Service', + title: __('Geo IP Service'), name: 'geo_ip_backend', area: 'System::Services', - description: 'Defines the backend for geo IP lookups. Shows also location of an IP address if an IP address is shown.', + description: __('Defines the backend for geo IP lookups. Also shows location of an IP address if it is traceable.'), options: { form: [ { @@ -403,7 +407,7 @@ Setting.create_if_not_exists( tag: 'select', options: { '' => '-', - 'Service::GeoIp::Zammad' => 'Zammad GeoIP Service', + 'Service::GeoIp::Zammad' => __('Zammad GeoIP Service'), }, }, ], @@ -417,10 +421,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Geo Location Service', + title: __('Geo Location Service'), name: 'geo_location_backend', area: 'System::Services', - description: 'Defines the backend for geo location lookups to store geo locations for addresses.', + description: __('Defines the backend for geo location lookups to store geo locations for addresses.'), options: { form: [ { @@ -430,7 +434,7 @@ Setting.create_if_not_exists( tag: 'select', options: { '' => '-', - 'Service::GeoLocation::Gmaps' => 'Google Maps', + 'Service::GeoLocation::Gmaps' => __('Google Maps'), }, }, ], @@ -444,10 +448,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Geo Calendar Service', + title: __('Geo Calendar Service'), name: 'geo_calendar_backend', area: 'System::Services', - description: 'Defines the backend for geo calendar lookups. Used for initial calendar succession.', + description: __('Defines the backend for geo calendar lookups. Used for initial calendar succession.'), options: { form: [ { @@ -457,7 +461,7 @@ Setting.create_if_not_exists( tag: 'select', options: { '' => '-', - 'Service::GeoCalendar::Zammad' => 'Zammad GeoCalendar Service', + 'Service::GeoCalendar::Zammad' => __('Zammad GeoCalendar Service'), }, }, ], @@ -471,10 +475,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Proxy Settings', + title: __('Proxy Settings'), name: 'proxy', area: 'System::Network', - description: 'Address of the proxy server for http and https resources.', + description: __('Address of the proxy server for http and https resources.'), options: { form: [ { @@ -496,10 +500,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Proxy User', + title: __('Proxy User'), name: 'proxy_username', area: 'System::Network', - description: 'Username for proxy connection.', + description: __('Username for proxy connection.'), options: { form: [ { @@ -520,10 +524,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Proxy Password', + title: __('Proxy Password'), name: 'proxy_password', area: 'System::Network', - description: 'Password for proxy connection.', + description: __('Password for proxy connection.'), options: { form: [ { @@ -544,10 +548,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'No Proxy', + title: __('No Proxy'), name: 'proxy_no', area: 'System::Network', - description: 'No proxy for the following hosts.', + description: __('No proxy for the following hosts.'), options: { form: [ { @@ -567,18 +571,17 @@ Setting.create_if_not_exists( }, frontend: false ) - Setting.create_if_not_exists( - title: 'Send client stats', - name: 'ui_send_client_stats', + title: __('Core Workflow Ajax Mode'), + name: 'core_workflow_ajax_mode', area: 'System::UI', - description: 'Send client stats/error message to central server to improve the usability.', + description: __('Defines if the core workflow communication should run over ajax instead of websockets.'), options: { form: [ { display: '', null: true, - name: 'ui_send_client_stats', + name: 'core_workflow_ajax_mode', tag: 'boolean', options: { true => 'yes', @@ -589,42 +592,16 @@ Setting.create_if_not_exists( }, state: false, preferences: { - prio: 1, + prio: 3, permission: ['admin.system'], }, frontend: true ) Setting.create_if_not_exists( - title: 'Client storage', - name: 'ui_client_storage', - area: 'System::UI', - description: 'Use client storage to cache data to enhance performance of application.', - options: { - form: [ - { - display: '', - null: true, - name: 'ui_client_storage', - tag: 'boolean', - options: { - true => 'yes', - false => 'no', - }, - }, - ], - }, - state: false, - preferences: { - prio: 2, - permission: ['admin.system'], - }, - frontend: true -) -Setting.create_if_not_exists( - title: 'User Organization Selector - email', + title: __('User Organization Selector - email'), name: 'ui_user_organization_selector_with_email', area: 'UI::UserOrganizatiomSelector', - description: 'Display of the e-mail in the result of the user/organization widget.', + description: __('Defines if the email should be displayed in the result of the user/organization widget.'), options: { form: [ { @@ -648,10 +625,10 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'Note - default visibility', + title: __('Note - default visibility'), name: 'ui_ticket_zoom_article_note_new_internal', area: 'UI::TicketZoom', - description: 'Default visibility for new note.', + description: __('Defines the default visibility for new notes.'), options: { form: [ { @@ -674,11 +651,39 @@ Setting.create_if_not_exists( }, frontend: true ) + Setting.create_if_not_exists( - title: 'Email - subject field', + title: __('Article - visibility confirmation dialog'), + name: 'ui_ticket_zoom_article_visibility_confirmation_dialog', + area: 'UI::TicketZoom', + description: __('Defines if the agent has to accept a confirmation dialog when changing the article visibility to "public".'), + options: { + form: [ + { + display: '', + null: true, + name: 'ui_ticket_zoom_article_visibility_confirmation_dialog', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + state: false, + preferences: { + prio: 100, + permission: ['admin.ui'], + }, + frontend: true +) + +Setting.create_if_not_exists( + title: __('Email - subject field'), name: 'ui_ticket_zoom_article_email_subject', area: 'UI::TicketZoom', - description: 'Use subject field for emails. If disabled, the ticket title will be used as subject.', + description: __('Use subject field for emails. If disabled, the ticket title will be used as subject.'), options: { form: [ { @@ -702,10 +707,10 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'Email - full quote', + title: __('Email - full quote'), name: 'ui_ticket_zoom_article_email_full_quote', area: 'UI::TicketZoom', - description: 'Enable if you want to quote the full email in your answer. The quoted email will be put at the end of your answer. If you just want to quote a certain phrase, just mark the text and press reply (this feature is always available).', + description: __('Enable if you want to quote the full email in your answer. The quoted email will be put at the end of your answer. If you just want to quote a certain phrase, just mark the text and press reply (this feature is always available).'), options: { form: [ { @@ -729,10 +734,10 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'Email - quote header', + title: __('Email - quote header'), name: 'ui_ticket_zoom_article_email_full_quote_header', area: 'UI::TicketZoom', - description: 'Enable if you want a timestamped reply header to be automatically inserted in front of quoted messages.', + description: __('Enable if you want a timestamped reply header to be automatically inserted in front of quoted messages.'), options: { form: [ { @@ -756,10 +761,10 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'Twitter - tweet initials', + title: __('Twitter - tweet initials'), name: 'ui_ticket_zoom_article_twitter_initials', area: 'UI::TicketZoom', - description: 'Add sender initials to end of a tweet.', + description: __('Add sender initials to end of a tweet.'), options: { form: [ { @@ -783,10 +788,10 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'Sidebar Attachments', + title: __('Sidebar Attachments'), name: 'ui_ticket_zoom_attachments_preview', area: 'UI::TicketZoom::Preview', - description: 'Enables preview of attachments.', + description: __('Enables preview of attachments.'), options: { form: [ { @@ -810,10 +815,10 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'Sidebar Attachments', + title: __('Sidebar Attachments'), name: 'ui_ticket_zoom_sidebar_article_attachments', area: 'UI::TicketZoom::Preview', - description: 'Enables a sidebar to show an overview of all attachments.', + description: __('Enables a sidebar to show an overview of all attachments.'), options: { form: [ { @@ -838,13 +843,13 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Set notes for ticket create types.', + title: __('Set notes for ticket create types.'), name: 'ui_ticket_create_notes', area: 'UI::TicketCreate', - description: 'Set notes for ticket create types by selecting type.', + description: __('Set notes for ticket create types by selecting type.'), options: {}, state: { - #'email-out' => 'Attention: When creating a ticket an e-mail is sent.', + # 'email-out' => __('Attention: When creating a ticket an email is sent.'), }, preferences: { permission: ['admin.ui'], @@ -853,10 +858,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Default type for a new ticket', + title: __('Default type for a new ticket'), name: 'ui_ticket_create_default_type', area: 'UI::TicketCreate', - description: 'Select default ticket type', + description: __('Select default ticket type'), options: { form: [ { @@ -881,10 +886,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Available types for a new ticket', + title: __('Available types for a new ticket'), name: 'ui_ticket_create_available_types', area: 'UI::TicketCreate', - description: 'Set available ticket types', + description: __('Set available ticket types'), options: { form: [ { @@ -909,10 +914,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Open ticket indicator', + title: __('Open ticket indicator'), name: 'ui_sidebar_open_ticket_indicator_colored', area: 'UI::Sidebar', - description: 'Color representation of the open ticket indicator in the sidebar.', + description: __('Color representation of the open ticket indicator in the sidebar.'), options: { form: [ { @@ -936,10 +941,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Open ticket indicator', + title: __('Open ticket indicator'), name: 'ui_table_group_by_show_count', area: 'UI::Base', - description: 'Total display of the number of objects in a grouping.', + description: __('Total display of the number of objects in a grouping.'), options: { form: [ { @@ -963,10 +968,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Priority Icons in Overviews', + title: __('Priority Icons in Overviews'), name: 'ui_ticket_overview_priority_icon', area: 'UI::TicketOverview::PriorityIcons', - description: 'Enables priority icons in ticket overviews.', + description: __('Enables priority icons in ticket overviews.'), options: { form: [ { @@ -991,10 +996,63 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'New User Accounts', + title: __('Maximum number of ticket shown in overviews'), + name: 'ui_ticket_overview_ticket_limit', + area: 'UI::TicketOverview::TicketLimit', + description: __('Define the maximum number of ticket shown in overviews.'), + options: {}, + state: 2000, + preferences: { + permission: ['admin.overview'], + }, + frontend: true +) + +Setting.create_if_not_exists( + title: __('Maximum number of open tabs.'), + name: 'ui_task_mananger_max_task_count', + area: 'UI::TaskManager::Task::MaxCount', + description: __('Defines the maximum number of allowed open tabs before auto cleanup removes surplus tabs when creating new tabs.'), + options: {}, + state: 30, + preferences: { + permission: ['admin.ui'], + }, + frontend: true +) + +Setting.create_if_not_exists( + title: __('Password Login'), + name: 'user_show_password_login', + area: 'Security::Base', + description: __('Show password login for users on login page. Disabling only takes effect if third-party authentication is enabled.'), + options: { + form: [ + { + display: '', + null: true, + name: 'user_show_password_login', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + state: true, + preferences: { + prio: 5, + permission: ['admin.security'], + }, + frontend: true +) + +Setting.create_if_not_exists( + title: __('New User Accounts'), name: 'user_create_account', area: 'Security::Base', - description: 'Enables users to create their own account via web interface.', + description: __('Enables users to create their own account via web interface. This setting is only effective if the password login is enabled.'), options: { form: [ { @@ -1017,10 +1075,10 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'Lost Password', + title: __('Lost Password'), name: 'user_lost_password', area: 'Security::Base', - description: 'Activates lost password feature for users.', + description: __('Activates lost password feature for users. This setting is only effective if the password login is enabled.'), options: { form: [ { @@ -1043,16 +1101,16 @@ Setting.create_if_not_exists( frontend: true ) -options = [ { value: '0', name: 'disabled' }, { value: 1.hour.seconds, name: '1 hour' }, { value: 2.hours.seconds, name: '2 hours' }, { value: 1.day.seconds, name: '1 day' }, { value: 7.days.seconds, name: '1 week' }, { value: 14.days.seconds, name: '2 weeks' }, { value: 21.days.seconds, name: '3 weeks' }, { value: 28.days.seconds, name: '4 weeks' } ] +options = [ { value: '0', name: 'disabled' }, { value: 1.hour.seconds.to_s, name: __('1 hour') }, { value: 2.hours.seconds.to_s, name: __('2 hours') }, { value: 1.day.seconds.to_s, name: __('1 day') }, { value: 7.days.seconds.to_s, name: __('1 week') }, { value: 14.days.seconds.to_s, name: __('2 weeks') }, { value: 21.days.seconds.to_s, name: __('3 weeks') }, { value: 28.days.seconds.to_s, name: __('4 weeks') } ] Setting.create_if_not_exists( - title: 'Session Timeout', + title: __('Session Timeout'), name: 'session_timeout', area: 'Security::Base', - description: 'Defines the session timeout for inactivity of users. Based on the assigned permissions the highest timeout value will be used, otherwise the default.', + description: __('Defines the session timeout for inactivity of users. Based on the assigned permissions the highest timeout value will be used, otherwise the default.'), options: { form: [ { - display: 'Default', + display: __('Default'), null: false, name: 'default', tag: 'select', @@ -1060,7 +1118,7 @@ Setting.create_if_not_exists( translate: true, }, { - display: 'admin', + display: __('admin'), null: false, name: 'admin', tag: 'select', @@ -1068,7 +1126,7 @@ Setting.create_if_not_exists( translate: true, }, { - display: 'ticket.agent', + display: __('ticket.agent'), null: false, name: 'ticket.agent', tag: 'select', @@ -1076,7 +1134,7 @@ Setting.create_if_not_exists( translate: true, }, { - display: 'ticket.customer', + display: __('ticket.customer'), null: false, name: 'ticket.customer', tag: 'select', @@ -1089,18 +1147,18 @@ Setting.create_if_not_exists( prio: 30, }, state: { - 'default' => 4.weeks.seconds, - 'admin' => 4.weeks.seconds, - 'ticket.agent' => 4.weeks.seconds, - 'ticket.customer' => 4.weeks.seconds, + 'default' => 4.weeks.seconds.to_s, + 'admin' => 4.weeks.seconds.to_s, + 'ticket.agent' => 4.weeks.seconds.to_s, + 'ticket.customer' => 4.weeks.seconds.to_s, }, frontend: true ) Setting.create_if_not_exists( - title: 'User email for muliple users', + title: __('User email for multiple users'), name: 'user_email_multiple_use', area: 'Model::User', - description: 'Allow to use email address for muliple users.', + description: __('Allow using one email address for multiple users.'), options: { form: [ { @@ -1122,17 +1180,50 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Authentication via %s', - name: 'auth_ldap', + title: __('Authentication via %s'), + name: 'auth_internal', area: 'Security::Authentication', - description: 'Enables user authentication via %s.', + description: __('Enables user authentication via %s.'), preferences: { - title_i18n: ['LDAP'], - description_i18n: ['LDAP'], + title_i18n: [__('internal database')], + description_i18n: [__('internal database')], permission: ['admin.security'], }, state: { - adapter: 'Auth::Ldap', + priority: 1, + adapter: 'Auth::Backend::Internal', + }, + frontend: false +) +Setting.create_if_not_exists( + title: __('Authentication via %s'), + name: 'auth_developer', + area: 'Security::Authentication', + description: __('Enables user authentication via %s.'), + preferences: { + title_i18n: [__('developer password')], + description_i18n: [__('developer password')], + permission: ['admin.security'], + }, + state: { + priority: 2, + adapter: 'Auth::Backend::Developer', + }, + frontend: false +) +Setting.create_if_not_exists( + title: __('Authentication via %s'), + name: 'auth_ldap', + area: 'Security::Authentication', + description: __('Enables user authentication via %s.'), + preferences: { + title_i18n: [__('LDAP')], + description_i18n: [__('LDAP')], + permission: ['admin.security'], + }, + state: { + priority: 3, + adapter: 'Auth::Backend::Ldap', host: 'localhost', port: 389, bind_dn: 'cn=Manager,dc=example,dc=org', @@ -1152,10 +1243,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Automatic account link on initial logon', + title: __('Automatic account link on initial logon'), name: 'auth_third_party_auto_link_at_inital_login', area: 'Security::ThirdPartyAuthentication', - description: 'Enables the automatic linking of an existing account on initial login via a third party application. If this is disabled, an existing user must first log into Zammad and then link his "Third Party" account to his Zammad account via Profile -> Linked Accounts.', + description: __('Enables the automatic linking of an existing account on initial login via a third party application. If this is disabled, an existing user must first log into Zammad and then link his "Third Party" account to his Zammad account via Profile -> Linked Accounts.'), options: { form: [ { @@ -1178,10 +1269,36 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Authentication via %s', + title: __('Automatic account linking notification'), + name: 'auth_third_party_linking_notification', + area: 'Security::ThirdPartyAuthentication', + description: __('Enables sending of an email notification to a user when they link their account with a third-party application.'), + options: { + form: [ + { + display: '', + null: true, + name: 'auth_third_party_linking_notification', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + preferences: { + permission: ['admin.security'], + prio: 20, + }, + state: false, + frontend: false +) +Setting.create_if_not_exists( + title: __('Authentication via %s'), name: 'auth_twitter', area: 'Security::ThirdPartyAuthentication', - description: 'Enables user authentication via %s. Register your app first at [%s](%s).', + description: __('Enables user authentication via %s. Register your app first at [%s](%s).'), options: { form: [ { @@ -1199,32 +1316,39 @@ Setting.create_if_not_exists( preferences: { controller: 'SettingsAreaSwitch', sub: ['auth_twitter_credentials'], - title_i18n: ['Twitter'], - description_i18n: ['Twitter', 'Twitter Developer Site', 'https://dev.twitter.com/apps'], + title_i18n: [__('Twitter')], + description_i18n: [__('Twitter'), __('Twitter Developer Site'), 'https://dev.twitter.com/apps'], permission: ['admin.security'], }, state: false, frontend: true ) Setting.create_if_not_exists( - title: 'Twitter App Credentials', + title: __('Twitter App Credentials'), name: 'auth_twitter_credentials', area: 'Security::ThirdPartyAuthentication::Twitter', - description: 'App credentials for Twitter.', + description: __('App credentials for Twitter.'), options: { form: [ { - display: 'Twitter Key', + display: __('Twitter Key'), null: true, name: 'key', tag: 'input', }, { - display: 'Twitter Secret', + display: __('Twitter Secret'), null: true, name: 'secret', tag: 'input', }, + { + display: __('Your callback URL'), + null: true, + name: 'callback_url', + tag: 'auth_provider', + provider: 'auth_twitter', + }, ], }, state: {}, @@ -1234,10 +1358,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Authentication via %s', + title: __('Authentication via %s'), name: 'auth_facebook', area: 'Security::ThirdPartyAuthentication', - description: 'Enables user authentication via %s. Register your app first at [%s](%s).', + description: __('Enables user authentication via %s. Register your app first at [%s](%s).'), options: { form: [ { @@ -1255,8 +1379,8 @@ Setting.create_if_not_exists( preferences: { controller: 'SettingsAreaSwitch', sub: ['auth_facebook_credentials'], - title_i18n: ['Facebook'], - description_i18n: ['Facebook', 'Facebook Developer Site', 'https://developers.facebook.com/apps/'], + title_i18n: [__('Facebook')], + description_i18n: [__('Facebook'), __('Facebook Developer Site'), 'https://developers.facebook.com/apps/'], permission: ['admin.security'], }, state: false, @@ -1264,24 +1388,31 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Facebook App Credentials', + title: __('Facebook App Credentials'), name: 'auth_facebook_credentials', area: 'Security::ThirdPartyAuthentication::Facebook', - description: 'App credentials for Facebook.', + description: __('App credentials for Facebook.'), options: { form: [ { - display: 'App ID', + display: __('App ID'), null: true, name: 'app_id', tag: 'input', }, { - display: 'App Secret', + display: __('App Secret'), null: true, name: 'app_secret', tag: 'input', }, + { + display: __('Your callback URL'), + null: true, + name: 'callback_url', + tag: 'auth_provider', + provider: 'auth_facebook', + }, ], }, state: {}, @@ -1292,10 +1423,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Authentication via %s', + title: __('Authentication via %s'), name: 'auth_google_oauth2', area: 'Security::ThirdPartyAuthentication', - description: 'Enables user authentication via %s. Register your app first at [%s](%s).', + description: __('Enables user authentication via %s. Register your app first at [%s](%s).'), options: { form: [ { @@ -1313,32 +1444,39 @@ Setting.create_if_not_exists( preferences: { controller: 'SettingsAreaSwitch', sub: ['auth_google_oauth2_credentials'], - title_i18n: ['Google'], - description_i18n: ['Google', 'Google API Console Site', 'https://console.cloud.google.com/apis/credentials'], + title_i18n: [__('Google')], + description_i18n: [__('Google'), __('Google API Console Site'), 'https://console.cloud.google.com/apis/credentials'], permission: ['admin.security'], }, state: false, frontend: true ) Setting.create_if_not_exists( - title: 'Google App Credentials', + title: __('Google App Credentials'), name: 'auth_google_oauth2_credentials', area: 'Security::ThirdPartyAuthentication::Google', - description: 'Enables user authentication via Google.', + description: __('Enables user authentication via Google.'), options: { form: [ { - display: 'Client ID', + display: __('Client ID'), null: true, name: 'client_id', tag: 'input', }, { - display: 'Client Secret', + display: __('Client Secret'), null: true, name: 'client_secret', tag: 'input', }, + { + display: __('Your callback URL'), + null: true, + name: 'callback_url', + tag: 'auth_provider', + provider: 'auth_google_oauth2', + }, ], }, state: {}, @@ -1349,10 +1487,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Authentication via %s', + title: __('Authentication via %s'), name: 'auth_linkedin', area: 'Security::ThirdPartyAuthentication', - description: 'Enables user authentication via %s. Register your app first at [%s](%s).', + description: __('Enables user authentication via %s. Register your app first at [%s](%s).'), options: { form: [ { @@ -1370,32 +1508,39 @@ Setting.create_if_not_exists( preferences: { controller: 'SettingsAreaSwitch', sub: ['auth_linkedin_credentials'], - title_i18n: ['LinkedIn'], - description_i18n: ['LinkedIn', 'Linkedin Developer Site', 'https://www.linkedin.com/developer/apps'], + title_i18n: [__('LinkedIn')], + description_i18n: [__('LinkedIn'), __('LinkedIn Developer Site'), 'https://www.linkedin.com/developer/apps'], permission: ['admin.security'], }, state: false, frontend: true ) Setting.create_if_not_exists( - title: 'LinkedIn App Credentials', + title: __('LinkedIn App Credentials'), name: 'auth_linkedin_credentials', area: 'Security::ThirdPartyAuthentication::Linkedin', - description: 'Enables user authentication via LinkedIn.', + description: __('Enables user authentication via LinkedIn.'), options: { form: [ { - display: 'App ID', + display: __('App ID'), null: true, name: 'app_id', tag: 'input', }, { - display: 'App Secret', + display: __('App Secret'), null: true, name: 'app_secret', tag: 'input', }, + { + display: __('Your callback URL'), + null: true, + name: 'callback_url', + tag: 'auth_provider', + provider: 'auth_linkedin', + }, ], }, state: {}, @@ -1406,10 +1551,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Authentication via %s', + title: __('Authentication via %s'), name: 'auth_github', area: 'Security::ThirdPartyAuthentication', - description: 'Enables user authentication via %s. Register your app first at [%s](%s).', + description: __('Enables user authentication via %s. Register your app first at [%s](%s).'), options: { form: [ { @@ -1427,32 +1572,39 @@ Setting.create_if_not_exists( preferences: { controller: 'SettingsAreaSwitch', sub: ['auth_github_credentials'], - title_i18n: ['Github'], - description_i18n: ['Github', 'Github OAuth Applications', 'https://github.com/settings/applications'], + title_i18n: [__('GitHub')], + description_i18n: [__('GitHub'), __('GitHub OAuth Applications'), 'https://github.com/settings/applications'], permission: ['admin.security'], }, state: false, frontend: true ) Setting.create_if_not_exists( - title: 'Github App Credentials', + title: __('GitHub App Credentials'), name: 'auth_github_credentials', area: 'Security::ThirdPartyAuthentication::Github', - description: 'Enables user authentication via Github.', + description: __('Enables user authentication via GitHub.'), options: { form: [ { - display: 'App ID', + display: __('App ID'), null: true, name: 'app_id', tag: 'input', }, { - display: 'App Secret', + display: __('App Secret'), null: true, name: 'app_secret', tag: 'input', }, + { + display: __('Your callback URL'), + null: true, + name: 'callback_url', + tag: 'auth_provider', + provider: 'auth_github', + }, ], }, state: {}, @@ -1463,10 +1615,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Authentication via %s', + title: __('Authentication via %s'), name: 'auth_gitlab', area: 'Security::ThirdPartyAuthentication', - description: 'Enables user authentication via %s. Register your app first at [%s](%s).', + description: __('Enables user authentication via %s. Register your app first at [%s](%s).'), options: { form: [ { @@ -1484,39 +1636,46 @@ Setting.create_if_not_exists( preferences: { controller: 'SettingsAreaSwitch', sub: ['auth_gitlab_credentials'], - title_i18n: ['GitLab'], - description_i18n: ['GitLab', 'GitLab Applications', 'https://your-gitlab-host/admin/applications'], + title_i18n: [__('GitLab')], + description_i18n: [__('GitLab'), __('GitLab Applications'), 'https://your-gitlab-host/admin/applications'], permission: ['admin.security'], }, state: false, frontend: true ) Setting.create_if_not_exists( - title: 'GitLab App Credentials', + title: __('GitLab App Credentials'), name: 'auth_gitlab_credentials', area: 'Security::ThirdPartyAuthentication::GitLab', - description: 'Enables user authentication via GitLab.', + description: __('Enables user authentication via GitLab.'), options: { form: [ { - display: 'App ID', + display: __('App ID'), null: true, name: 'app_id', tag: 'input', }, { - display: 'App Secret', + display: __('App Secret'), null: true, name: 'app_secret', tag: 'input', }, { - display: 'Site', + display: __('Site'), null: true, name: 'site', tag: 'input', placeholder: 'https://gitlab.YOURDOMAIN.com/api/v4/', }, + { + display: __('Your callback URL'), + null: true, + name: 'callback_url', + tag: 'auth_provider', + provider: 'auth_gitlab', + }, ], }, state: {}, @@ -1527,10 +1686,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Authentication via %s', + title: __('Authentication via %s'), name: 'auth_microsoft_office365', area: 'Security::ThirdPartyAuthentication', - description: 'Enables user authentication via %s. Register your app first at [%s](%s).', + description: __('Enables user authentication via %s. Register your app first at [%s](%s).'), options: { form: [ { @@ -1548,39 +1707,46 @@ Setting.create_if_not_exists( preferences: { controller: 'SettingsAreaSwitch', sub: ['auth_microsoft_office365_credentials'], - title_i18n: ['Office 365'], - description_i18n: ['Office 365', 'Microsoft Application Registration Portal', 'https://portal.azure.com'], + title_i18n: [__('Microsoft')], + description_i18n: [__('Microsoft'), __('Microsoft Application Registration Portal'), 'https://portal.azure.com'], permission: ['admin.security'], }, state: false, frontend: true ) Setting.create_if_not_exists( - title: 'Office 365 App Credentials', + title: __('Microsoft 365 App Credentials'), name: 'auth_microsoft_office365_credentials', area: 'Security::ThirdPartyAuthentication::Office365', - description: 'Enables user authentication via Office 365.', + description: __('Enables user authentication via Microsoft 365.'), options: { form: [ { - display: 'App ID', + display: __('App ID'), null: true, name: 'app_id', tag: 'input', }, { - display: 'App Secret', + display: __('App Secret'), null: true, name: 'app_secret', tag: 'input', }, { - display: 'App Tenant ID', + display: __('App Tenant ID'), null: true, name: 'app_tenant', tag: 'input', placeholder: 'common', }, + { + display: __('Your callback URL'), + null: true, + name: 'callback_url', + tag: 'auth_provider', + provider: 'auth_microsoft_office365', + }, ], }, state: {}, @@ -1590,10 +1756,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Authentication via %s', + title: __('Authentication via %s'), name: 'auth_weibo', area: 'Security::ThirdPartyAuthentication', - description: 'Enables user authentication via %s. Register your app first at [%s](%s).', + description: __('Enables user authentication via %s. Register your app first at [%s](%s).'), options: { form: [ { @@ -1611,32 +1777,39 @@ Setting.create_if_not_exists( preferences: { controller: 'SettingsAreaSwitch', sub: ['auth_weibo_credentials'], - title_i18n: ['Weibo'], - description_i18n: ['Sina Weibo', 'Sina Weibo Open Protal', 'http://open.weibo.com'], + title_i18n: [__('Weibo')], + description_i18n: [__('Sina Weibo'), __('Sina Weibo Open Portal'), 'http://open.weibo.com'], permission: ['admin.security'], }, state: false, frontend: true ) Setting.create_if_not_exists( - title: 'Weibo App Credentials', + title: __('Weibo App Credentials'), name: 'auth_weibo_credentials', area: 'Security::ThirdPartyAuthentication::Weibo', - description: 'Enables user authentication via Weibo.', + description: __('Enables user authentication via Weibo.'), options: { form: [ { - display: 'App ID', + display: __('App ID'), null: true, name: 'client_id', tag: 'input', }, { - display: 'App Secret', + display: __('App Secret'), null: true, name: 'client_secret', tag: 'input', }, + { + display: __('Your callback URL'), + null: true, + name: 'callback_url', + tag: 'auth_provider', + provider: 'auth_weibo', + }, ], }, state: {}, @@ -1646,10 +1819,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Authentication via %s', + title: __('Authentication via %s'), name: 'auth_saml', area: 'Security::ThirdPartyAuthentication', - description: 'Enables user authentication via %s.', + description: __('Enables user authentication via %s.'), options: { form: [ { @@ -1667,48 +1840,77 @@ Setting.create_if_not_exists( preferences: { controller: 'SettingsAreaSwitch', sub: ['auth_saml_credentials'], - title_i18n: ['SAML'], - description_i18n: ['SAML'], + title_i18n: [__('SAML')], + description_i18n: [__('SAML')], permission: ['admin.security'], }, state: false, frontend: true ) Setting.create_if_not_exists( - title: 'SAML App Credentials', + title: __('SAML App Credentials'), name: 'auth_saml_credentials', area: 'Security::ThirdPartyAuthentication::SAML', - description: 'Enables user authentication via SAML.', + description: __('Enables user authentication via SAML.'), options: { form: [ { - display: 'IDP SSO target URL', + display: __('Display name'), + null: true, + name: 'display_name', + tag: 'input', + placeholder: __('SAML'), + }, + { + display: __('IDP SSO target URL'), null: true, name: 'idp_sso_target_url', tag: 'input', placeholder: 'https://capriza.github.io/samling/samling.html', }, { - display: 'IDP certificate', + display: __('IDP Single Logout target URL'), + null: true, + name: 'idp_slo_service_url', + tag: 'input', + placeholder: 'https://capriza.github.io/samling/slo.html', + }, + { + display: __('IDP certificate'), null: true, name: 'idp_cert', - tag: 'input', + tag: 'textarea', placeholder: '-----BEGIN CERTIFICATE-----\n...-----END CERTIFICATE-----', }, { - display: 'IDP certificate fingerprint', + display: __('IDP certificate fingerprint'), null: true, name: 'idp_cert_fingerprint', tag: 'input', placeholder: 'E7:91:B2:E1:...', }, { - display: 'Name Identifier Format', + display: __('Name Identifier Format'), null: true, name: 'name_identifier_format', tag: 'input', placeholder: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', }, + { + display: __('UID Attribute Name'), + null: true, + name: 'uid_attribute', + tag: 'input', + placeholder: '', + help: __('Attribute that uniquely identifies the user. If unset, the name identifier returned by the IDP is used.') + }, + { + display: __('Your callback URL'), + null: true, + name: 'callback_url', + tag: 'auth_provider', + provider: 'auth_saml', + }, ], }, state: {}, @@ -1719,10 +1921,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Minimum length', + title: __('Minimum length'), name: 'password_min_size', area: 'Security::Password', - description: 'Password needs to have at least a minimal number of characters.', + description: __('Password needs to have at least a minimal number of characters.'), options: { form: [ { @@ -1759,21 +1961,22 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: '2 lower and 2 upper characters', + title: __('2 lower case and 2 upper case characters'), name: 'password_min_2_lower_2_upper_characters', area: 'Security::Password', - description: 'Password needs to contain 2 lower and 2 upper characters.', + description: __('Password needs to contain 2 lower case and 2 upper case characters.'), options: { form: [ { - display: '', - null: true, - name: 'password_min_2_lower_2_upper_characters', - tag: 'select', - options: { + display: '', + null: true, + name: 'password_min_2_lower_2_upper_characters', + tag: 'select', + options: { 1 => 'yes', 0 => 'no', }, + translate: true, }, ], }, @@ -1784,21 +1987,22 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Digit required', + title: __('Digit required'), name: 'password_need_digit', area: 'Security::Password', - description: 'Password needs to contain at least one digit.', + description: __('Password needs to contain at least one digit.'), options: { form: [ { - display: 'Needed', - null: true, - name: 'password_need_digit', - tag: 'select', - options: { + display: __('Needed'), + null: true, + name: 'password_need_digit', + tag: 'select', + options: { 1 => 'yes', 0 => 'no', }, + translate: true, }, ], }, @@ -1809,21 +2013,22 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Special character required', + title: __('Special character required'), name: 'password_need_special_character', area: 'Security::Password', - description: 'Password needs to contain at least one special character.', + description: __('Password needs to contain at least one special character.'), options: { form: [ { - display: 'Needed', - null: true, - name: 'password_need_special_character', - tag: 'select', - options: { + display: __('Needed'), + null: true, + name: 'password_need_special_character', + tag: 'select', + options: { 1 => 'yes', 0 => 'no', }, + translate: true, }, ], }, @@ -1834,10 +2039,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Maximum failed logins', + title: __('Maximum failed logins'), name: 'password_max_login_failed', area: 'Security::Password', - description: 'Number of failed logins after account will be deactivated.', + description: __('Defines after how many failed logins accounts will be deactivated.'), options: { form: [ { @@ -1868,16 +2073,17 @@ Setting.create_if_not_exists( }, state: 5, preferences: { - permission: ['admin.security'], + authentication: true, + permission: ['admin.security'], }, - frontend: false + frontend: true ) Setting.create_if_not_exists( - title: 'Ticket Hook', + title: __('Ticket Hook'), name: 'ticket_hook', area: 'Ticket::Base', - description: 'The identifier for a ticket, e.g. Ticket#, Call#, MyTicket#. The default is Ticket#.', + description: __('The identifier for a ticket, e.g. Ticket#, Call#, MyTicket#. The default is Ticket#.'), options: { form: [ { @@ -1889,6 +2095,7 @@ Setting.create_if_not_exists( ], }, preferences: { + prio: 1000, render: true, placeholder: true, authentication: true, @@ -1898,10 +2105,10 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'Ticket Hook Divider', + title: __('Ticket Hook Divider'), name: 'ticket_hook_divider', area: 'Ticket::Base::Shadow', - description: 'The divider between TicketHook and ticket number. E. g. \': \'.', + description: __('The divider between TicketHook and ticket number. E. g. \': \'.'), options: { form: [ { @@ -1919,13 +2126,13 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Ticket Hook Position', + title: __('Ticket Hook Position'), name: 'ticket_hook_position', area: 'Ticket::Base', - description: "The format of the subject. + description: __("The format of the subject. * **Right** means **Some Subject [Ticket#12345]** * **Left** means **[Ticket#12345] Some Subject** -* **None** means **Some Subject** (without ticket number). In the last case you should enable *postmaster_follow_up_search_in* to recognize follow-ups based on email headers and/or body.", +* **None** means **Some Subject** (without ticket number), in which case it recognizes follow-ups based on email headers."), options: { form: [ { @@ -1935,25 +2142,26 @@ Setting.create_if_not_exists( tag: 'select', translate: true, options: { - 'left' => 'left', - 'right' => 'right', - 'none' => 'none', + 'left' => __('left'), + 'right' => __('right'), + 'none' => __('none'), }, }, ], }, state: 'right', preferences: { + prio: 2000, controller: 'SettingsAreaTicketHookPosition', permission: ['admin.ticket'], }, frontend: false ) Setting.create_if_not_exists( - title: 'Ticket Last Contact Behaviour', + title: __('Ticket Last Contact Behaviour'), name: 'ticket_last_contact_behaviour', area: 'Ticket::Base', - description: 'Sets the last customer contact based on the last contact of a customer or on the last contact of a customer to whom an agent has not yet responded.', + description: __('Defines how the last customer contact time of tickets should be calculated.'), options: { form: [ { @@ -1963,25 +2171,52 @@ Setting.create_if_not_exists( tag: 'select', translate: true, options: { - 'based_on_customer_reaction' => 'Last customer contact (without consideration an agent has replied to it)', - 'check_if_agent_already_replied' => 'Last customer contact (with consideration an agent has replied to it)', + 'based_on_customer_reaction' => __('Use the time of the very last customer article.'), + 'check_if_agent_already_replied' => __('Use the start time of the last customer thread (which may consist of multiple articles).'), }, }, ], }, state: 'check_if_agent_already_replied', preferences: { + prio: 3000, permission: ['admin.ticket'], }, frontend: false ) Setting.create_if_not_exists( - title: 'Ticket Number Format', + title: __('Ticket Conditions Expert Mode'), + name: 'ticket_allow_expert_conditions', + area: 'Ticket::Core', + description: __('Defines if the ticket conditions editor supports complex logical expressions.'), + options: { + form: [ + { + display: '', + null: true, + name: 'ticket_allow_expert_conditions', + tag: 'boolean', + options: { + true => __('yes'), + false => __('no'), + }, + }, + ], + }, + state: true, + preferences: { + online_service_disable: true, + permission: ['admin.ticket'], + }, + frontend: true +) +Setting.create_if_not_exists( + title: __('Ticket Number Format'), name: 'ticket_number', area: 'Ticket::Number', - description: "Selects the ticket number generator module. + description: __("Selects the ticket number generator module. * **Increment** increments the ticket number, the SystemID and the counter are used with SystemID.Counter format (e.g. 1010138, 1010139). -* With **Date** the ticket numbers will be generated by the current date, the SystemID and the counter. The format looks like Year.Month.Day.SystemID.counter (e.g. 201206231010138, 201206231010139).", +* With **Date** the ticket numbers will be generated by the current date, the SystemID and the counter. The format looks like Year.Month.Day.SystemID.counter (e.g. 201206231010138, 201206231010139)."), options: { form: [ { @@ -1991,8 +2226,8 @@ Setting.create_if_not_exists( tag: 'select', translate: true, options: { - 'Ticket::Number::Increment' => 'Increment (SystemID.Counter)', - 'Ticket::Number::Date' => 'Date (Year.Month.Day.SystemID.Counter)', + 'Ticket::Number::Increment' => __('Increment (SystemID.Counter)'), + 'Ticket::Number::Date' => __('Date (Year.Month.Day.SystemID.Counter)'), }, }, ], @@ -2006,14 +2241,14 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Ticket Number Increment', + title: __('Ticket Number Increment'), name: 'ticket_number_increment', area: 'Ticket::Number', description: '-', options: { form: [ { - display: 'Checksum', + display: __('Checksum'), null: true, name: 'checksum', tag: 'boolean', @@ -2023,7 +2258,7 @@ Setting.create_if_not_exists( }, }, { - display: 'Min. size of number', + display: __('Min. size of number'), null: true, name: 'min_size', tag: 'select', @@ -2063,14 +2298,14 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Ticket Number Increment Date', + title: __('Ticket Number Increment Date'), name: 'ticket_number_date', area: 'Ticket::Number', description: '-', options: { form: [ { - display: 'Checksum', + display: __('Checksum'), null: true, name: 'checksum', tag: 'boolean', @@ -2092,10 +2327,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Auto Assigment', + title: __('Auto Assignment'), name: 'ticket_auto_assignment', area: 'Web::Base', - description: 'Enable ticket auto assignment.', + description: __('Enable ticket auto assignment.'), options: { form: [ { @@ -2118,10 +2353,10 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'Time Accounting Selector', + title: __('Time Accounting Selector'), name: 'ticket_auto_assignment_selector', area: 'Web::Base', - description: 'Enable auto assignment for following matching tickets.', + description: __('Enable auto assignment for following matching tickets.'), options: { form: [ {}, @@ -2135,10 +2370,10 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_or_update( - title: 'Time Accounting Selector', + title: __('Time Accounting Selector'), name: 'ticket_auto_assignment_user_ids_ignore', area: 'Web::Base', - description: 'Define an exception of "automatic assignment" for certain users (e.g. executives).', + description: __('Define an exception of "automatic assignment" for certain users (e.g. executives).'), options: { form: [ {}, @@ -2152,14 +2387,14 @@ Setting.create_or_update( frontend: true ) Setting.create_if_not_exists( - title: 'Ticket Number ignore system_id', + title: __('Ticket Number ignore system_id'), name: 'ticket_number_ignore_system_id', area: 'Ticket::Core', description: '-', options: { form: [ { - display: 'Ignore system_id', + display: __('Ignore system_id'), null: true, name: 'ticket_number_ignore_system_id', tag: 'boolean', @@ -2179,14 +2414,14 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Recursive Ticket Triggers', + title: __('Recursive Ticket Triggers'), name: 'ticket_trigger_recursive', area: 'Ticket::Core', - description: 'Activate the recursive processing of ticket triggers.', + description: __('Activate the recursive processing of ticket triggers.'), options: { form: [ { - display: 'Recursive Ticket Triggers', + display: __('Recursive Ticket Triggers'), null: true, name: 'ticket_trigger_recursive', tag: 'boolean', @@ -2205,14 +2440,14 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Recursive Ticket Triggers Loop Max.', + title: __('Recursive Ticket Triggers Loop Max.'), name: 'ticket_trigger_recursive_max_loop', area: 'Ticket::Core', - description: 'Maximum number of recursively executed triggers.', + description: __('Maximum number of recursively executed triggers.'), options: { form: [ { - display: 'Recursive Ticket Triggers', + display: __('Recursive Ticket Triggers'), null: true, name: 'ticket_trigger_recursive_max_loop', tag: 'select', @@ -2250,10 +2485,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Enable Ticket creation', + title: __('Enable Ticket creation'), name: 'customer_ticket_create', area: 'CustomerWeb::Base', - description: 'Defines if a customer can create tickets via the web interface.', + description: __('Defines if a customer can create tickets via the web interface.'), options: { form: [ { @@ -2277,24 +2512,23 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Group selection for Ticket creation', + title: __('Group selection for Ticket creation'), name: 'customer_ticket_create_group_ids', area: 'CustomerWeb::Base', - description: 'Defines groups for which a customer can create tickets via web interface. "-" means all groups are available.', + description: __('Defines groups for which a customer can create tickets via web interface. No selection means all groups are available.'), options: { form: [ { - display: '', - null: true, - name: 'group_ids', - tag: 'select', - multiple: true, - nulloption: true, - relation: 'Group', + display: '', + null: true, + name: 'group_ids', + tag: 'multiselect', + multiple: true, + relation: 'Group', }, ], }, - state: '', + state: nil, preferences: { authentication: true, permission: ['admin.channel_web'], @@ -2303,10 +2537,39 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Enable Ticket creation', + title: __('Tab behaviour after ticket action'), + name: 'ticket_secondary_action', + area: 'CustomerWeb::Base', + description: __('Defines the tab behaviour after a ticket action.'), + options: { + form: [ + { + display: '', + null: true, + name: 'ticket_secondary_action', + tag: 'boolean', + options: { + 'closeTab' => __('Close tab'), + 'closeTabOnTicketClose' => __('Close tab on ticket close'), + 'closeNextInOverview' => __('Next in overview'), + 'stayOnTab' => __('Stay on tab'), + }, + }, + ], + }, + state: 'stayOnTab', + preferences: { + authentication: true, + permission: ['admin.channel_web'], + }, + frontend: true +) + +Setting.create_if_not_exists( + title: __('Enable Ticket creation'), name: 'form_ticket_create', area: 'Form::Base', - description: 'Defines if tickets can be created via web form.', + description: __('Defines if tickets can be created via web form.'), options: { form: [ { @@ -2337,10 +2600,10 @@ if group group_id = group.id end Setting.create_if_not_exists( - title: 'Group selection for Ticket creation', + title: __('Group selection for ticket creation'), name: 'form_ticket_create_group_id', area: 'Form::Base', - description: 'Defines if group of created tickets via web form.', + description: __('Defines the group of tickets created via web form.'), options: { form: [ { @@ -2360,10 +2623,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Limit tickets by ip per hour', + title: __('Limit tickets by IP per hour'), name: 'form_ticket_create_by_ip_per_hour', area: 'Form::Base', - description: 'Defines limit of tickets by ip per hour via web form.', + description: __('Defines a limit for how many tickets can be created via web form from one IP address per hour.'), options: { form: [ { @@ -2381,10 +2644,10 @@ Setting.create_if_not_exists( frontend: false, ) Setting.create_if_not_exists( - title: 'Limit tickets by ip per day', + title: __('Limit tickets by IP per day'), name: 'form_ticket_create_by_ip_per_day', area: 'Form::Base', - description: 'Defines limit of tickets by ip per day via web form.', + description: __('Defines a limit for how many tickets can be created via web form from one IP address per day.'), options: { form: [ { @@ -2402,10 +2665,10 @@ Setting.create_if_not_exists( frontend: false, ) Setting.create_if_not_exists( - title: 'Limit tickets per day', + title: __('Limit tickets per day'), name: 'form_ticket_create_per_day', area: 'Form::Base', - description: 'Defines limit of tickets per day via web form.', + description: __('Defines a limit for how many tickets can be created via web form per day.'), options: { form: [ { @@ -2424,10 +2687,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Ticket Subject Size', + title: __('Ticket Subject Size'), name: 'ticket_subject_size', area: 'Email::Base', - description: 'Max. length of the subject in an email reply.', + description: __('Max. length of the subject in an email reply.'), options: { form: [ { @@ -2445,10 +2708,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Ticket Subject Reply', + title: __('Ticket Subject Reply'), name: 'ticket_subject_re', area: 'Email::Base', - description: 'The text at the beginning of the subject in an email reply, e.g. RE, AW, or AS.', + description: __('The text at the beginning of the subject in an email reply, e.g. RE, AW, or AS.'), options: { form: [ { @@ -2467,10 +2730,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Ticket Subject Forward', + title: __('Ticket Subject Forward'), name: 'ticket_subject_fwd', area: 'Email::Base', - description: 'The text at the beginning of the subject in an email forward, e. g. FWD.', + description: __('The text at the beginning of the subject in an email forward, e. g. FWD.'), options: { form: [ { @@ -2489,22 +2752,23 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Sender Format', + title: __('Sender Format'), name: 'ticket_define_email_from', area: 'Email::Base', - description: 'Defines how the From field of emails (sent from answers and email tickets) should look like.', + description: __('Defines how the From field of emails (sent from answers and email tickets) should look like.'), options: { form: [ { - display: '', - null: true, - name: 'ticket_define_email_from', - tag: 'select', - options: { - SystemAddressName: 'System Address Display Name', - AgentNameSystemAddressName: 'Agent Name + FromSeparator + System Address Display Name', - AgentName: 'Agent Name', + display: '', + null: true, + name: 'ticket_define_email_from', + tag: 'select', + options: { + SystemAddressName: __('System Address Display Name'), + AgentNameSystemAddressName: __('Agent Name + FromSeparator + System Address Display Name'), + AgentName: __('Agent Name'), }, + translate: true, }, ], }, @@ -2516,10 +2780,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Sender Format Separator', + title: __('Sender Format Separator'), name: 'ticket_define_email_from_separator', area: 'Email::Base', - description: 'Defines the separator between the agent\'s real name and the given group email address.', + description: __('Defines the separator between the agent\'s real name and the given group email address.'), options: { form: [ { @@ -2538,10 +2802,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Maximum Email Size', + title: __('Maximum Email Size'), name: 'postmaster_max_size', area: 'Email::Base', - description: 'Maximum size in MB of emails.', + description: __('Defines the maximum accepted email size in MB.'), options: { form: [ { @@ -2588,22 +2852,23 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Additional follow-up detection', + title: __('Additional follow-up detection'), name: 'postmaster_follow_up_search_in', area: 'Email::Base', - description: 'By default the follow-up check is done via the subject of an email. With this setting you can add more fields for which the follow-up check will be executed.', + description: __('By default, the follow-up check is done via the subject of an email. This setting lets you add more fields for which the follow-up check will be executed.'), options: { form: [ { - display: '', - null: true, - name: 'postmaster_follow_up_search_in', - tag: 'checkbox', - options: { - 'references' => 'References - Search for follow-up also in In-Reply-To or References headers.', - 'body' => 'Body - Search for follow-up also in mail body.', - 'attachment' => 'Attachment - Search for follow-up also in attachments.', + display: '', + null: true, + name: 'postmaster_follow_up_search_in', + tag: 'checkbox', + options: { + 'references' => __('References - Search for follow-up also in In-Reply-To or References headers.'), + 'body' => __('Body - Search for follow-up also in mail body.'), + 'attachment' => __('Attachment - Search for follow-up also in attachments.'), }, + translate: true, }, ], }, @@ -2615,22 +2880,23 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Sender based on Reply-To header', + title: __('Sender based on Reply-To header'), name: 'postmaster_sender_based_on_reply_to', area: 'Email::Base', - description: 'Set/overwrite sender/from of email based on reply-to header. Useful to set correct customer if email is received from a third party system on behalf of a customer.', + description: __('Set/overwrite sender/from of email based on "Reply-To" header. Useful to set correct customer if email is received from a third-party system on behalf of a customer.'), options: { form: [ { - display: '', - null: true, - name: 'postmaster_sender_based_on_reply_to', - tag: 'select', - options: { + display: '', + null: true, + name: 'postmaster_sender_based_on_reply_to', + tag: 'select', + options: { '' => '-', - 'as_sender_of_email' => 'Take reply-to header as sender/from of email.', - 'as_sender_of_email_use_from_realname' => 'Take reply-to header as sender/from of email and use realname of origin from.', + 'as_sender_of_email' => __('Take Reply-To header as sender/from of email.'), + 'as_sender_of_email_use_from_realname' => __('Take Reply-To header as sender/from of email and use the real name of origin from.'), }, + translate: true, }, ], }, @@ -2642,10 +2908,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Customer selection based on sender and receiver list', + title: __('Customer selection based on sender and receiver list'), name: 'postmaster_sender_is_agent_search_for_customer', area: 'Email::Base', - description: 'If the sender is an agent, set the first user in the recipient list as a customer.', + description: __('If the sender is an agent, set the first user in the recipient list as the customer.'), options: { form: [ { @@ -2668,10 +2934,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Send postmaster mail if mail too large', + title: __('Send postmaster mail if mail too large'), name: 'postmaster_send_reject_if_mail_too_large', area: 'Email::Base', - description: 'Send postmaster reject mail to sender of mail if mail is too large.', + description: __('Send postmaster reject mail to sender of mail if mail is too large.'), options: { form: [ { @@ -2695,10 +2961,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Notification Sender', + title: __('Notification Sender'), name: 'notification_sender', area: 'Email::Base', - description: 'Defines the sender of email notifications.', + description: __('Defines the sender of email notifications.'), options: { form: [ { @@ -2709,7 +2975,7 @@ Setting.create_if_not_exists( }, ], }, - state: 'Notification Master ', # rubocop:disable Lint/InterpolationCheck + state: '#{config.product_name} ', # rubocop:disable Lint/InterpolationCheck preferences: { online_service_disable: true, permission: ['admin.channel_email', 'admin.channel_google', 'admin.channel_microsoft365'], @@ -2718,10 +2984,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Block Notifications', + title: __('Block Notifications'), name: 'send_no_auto_response_reg_exp', area: 'Email::Base', - description: 'If this regex matches, no notification will be sent by the sender.', + description: __('If this regex matches, no notification will be sent by the sender.'), options: { form: [ { @@ -2741,10 +3007,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Bcc address for all outgoing emails', + title: __('BCC address for all outgoing emails'), name: 'system_bcc', area: 'Email::Enhanced', - description: 'To archive all outgoing emails from Zammad to external, you can store a bcc email address here.', + description: __('To archive all outgoing emails from Zammad to external, you can store a BCC email address here.'), options: {}, state: '', preferences: { online_service_disable: true }, @@ -2752,10 +3018,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'API Token Access', + title: __('API Token Access'), name: 'api_token_access', area: 'API::Base', - description: 'Enable REST API using tokens (not username/email address and password). Each user needs to create its own access tokens in user profile.', + description: __('Enable REST API using tokens (not username/email address and password). Each user needs to create its own access tokens in user profile.'), options: { form: [ { @@ -2774,13 +3040,13 @@ Setting.create_if_not_exists( preferences: { permission: ['admin.api'], }, - frontend: false + frontend: true ) Setting.create_if_not_exists( - title: 'API Password Access', + title: __('API Password Access'), name: 'api_password_access', area: 'API::Base', - description: 'Enable REST API access using the username/email address and password for the authentication user.', + description: __('Enable REST API access using the username/email address and password for the authentication user.'), options: { form: [ { @@ -2803,10 +3069,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Monitoring Token', + title: __('Monitoring Token'), name: 'monitoring_token', area: 'HealthCheck::Base', - description: 'Token for monitoring.', + description: __('Token for monitoring.'), options: { form: [ { @@ -2825,10 +3091,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Enable Chat', + title: __('Enable Chat'), name: 'chat', area: 'Chat::Base', - description: 'Enable/disable online chat.', + description: __('Enable/disable online chat.'), options: { form: [ { @@ -2852,10 +3118,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Agent idle timeout', + title: __('Agent idle timeout'), name: 'chat_agent_idle_timeout', area: 'Chat::Extended', - description: 'Idle timeout in seconds until agent is set offline automatically.', + description: __('Idle timeout in seconds until agent is set offline automatically.'), options: { form: [ { @@ -2874,10 +3140,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Defines searchable models.', + title: __('Defines searchable models.'), name: 'models_searchable', area: 'Models::Base', - description: 'Defines the searchable models.', + description: __('Defines the searchable models.'), options: {}, state: Models.searchable.map(&:to_s), preferences: { @@ -2887,93 +3153,93 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Default Screen', + title: __('Default Screen'), name: 'default_controller', area: 'Core', - description: 'Defines the default screen.', + description: __('Defines the default screen.'), options: {}, state: '#dashboard', frontend: true ) Setting.create_if_not_exists( - title: 'Elasticsearch Endpoint URL', + title: __('Elasticsearch Endpoint URL'), name: 'es_url', area: 'SearchIndex::Elasticsearch', - description: 'Defines endpoint of Elasticsearch.', + description: __('Defines endpoint of Elasticsearch.'), state: '', preferences: { online_service_disable: true }, frontend: false ) Setting.create_if_not_exists( - title: 'Elasticsearch Endpoint User', + title: __('Elasticsearch Endpoint User'), name: 'es_user', area: 'SearchIndex::Elasticsearch', - description: 'Defines HTTP basic auth user of Elasticsearch.', + description: __('Defines HTTP basic auth user of Elasticsearch.'), state: '', preferences: { online_service_disable: true }, frontend: false ) Setting.create_if_not_exists( - title: 'Elasticsearch Endpoint Password', + title: __('Elasticsearch Endpoint Password'), name: 'es_password', area: 'SearchIndex::Elasticsearch', - description: 'Defines HTTP basic auth password of Elasticsearch.', + description: __('Defines HTTP basic auth password of Elasticsearch.'), state: '', preferences: { online_service_disable: true }, frontend: false ) Setting.create_if_not_exists( - title: 'Elasticsearch Endpoint Index', + title: __('Elasticsearch Endpoint Index'), name: 'es_index', area: 'SearchIndex::Elasticsearch', - description: 'Defines Elasticsearch index name.', + description: __('Defines Elasticsearch index name.'), state: 'zammad', preferences: { online_service_disable: true }, frontend: false ) Setting.create_if_not_exists( - title: 'Elasticsearch Attachment Extensions', + title: __('Elasticsearch Attachment Extensions'), name: 'es_attachment_ignore', area: 'SearchIndex::Elasticsearch', - description: 'Defines attachment extensions which will be ignored by Elasticsearch.', + description: __('Defines attachment extensions which will be ignored by Elasticsearch.'), state: [ '.png', '.jpg', '.jpeg', '.mpeg', '.mpg', '.mov', '.bin', '.exe', '.box', '.mbox' ], preferences: { online_service_disable: true }, frontend: false ) Setting.create_if_not_exists( - title: 'Elasticsearch Attachment Size', + title: __('Elasticsearch Attachment Size'), name: 'es_attachment_max_size_in_mb', area: 'SearchIndex::Elasticsearch', - description: 'Define max. attachment size for Elasticsearch.', + description: __('Define max. attachment size for Elasticsearch.'), state: 10, preferences: { online_service_disable: true }, frontend: false ) Setting.create_if_not_exists( - title: 'Elasticsearch Total Payload Size', + title: __('Elasticsearch Total Payload Size'), name: 'es_total_max_size_in_mb', area: 'SearchIndex::Elasticsearch', - description: 'Define max. payload size for Elasticsearch.', + description: __('Define max. payload size for Elasticsearch.'), state: 300, preferences: { online_service_disable: true }, frontend: false ) Setting.create_if_not_exists( - title: 'Elasticsearch Pipeline Name', + title: __('Elasticsearch Pipeline Name'), name: 'es_pipeline', area: 'SearchIndex::Elasticsearch', - description: 'Define pipeline name for Elasticsearch.', + description: __('Define pipeline name for Elasticsearch.'), state: '', preferences: { online_service_disable: true }, frontend: false ) Setting.create_if_not_exists( - title: 'Import Mode', + title: __('Import Mode'), name: 'import_mode', area: 'Import::Base', - description: 'Puts Zammad into import mode (disables some triggers).', + description: __('Puts Zammad into import mode (disables some triggers).'), options: { form: [ { @@ -2992,19 +3258,19 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'Import Backend', + title: __('Import Backend'), name: 'import_backend', area: 'Import::Base::Internal', - description: 'Set backend which is being used for import.', + description: __('Set backend which is being used for import.'), options: {}, state: '', frontend: true ) Setting.create_if_not_exists( - title: 'Ignore Escalation/SLA Information', + title: __('Ignore Escalation/SLA Information'), name: 'import_ignore_sla', area: 'Import::Base', - description: 'Ignore escalation/SLA information for import.', + description: __('Ignore escalation/SLA information for import.'), options: { form: [ { @@ -3024,10 +3290,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Import Endpoint', + title: __('Import Endpoint'), name: 'import_otrs_endpoint', area: 'Import::OTRS', - description: 'Defines OTRS endpoint to import users, tickets, states and articles.', + description: __('Defines an OTRS endpoint to import users, tickets, states, and articles.'), options: { form: [ { @@ -3042,10 +3308,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Import Key', + title: __('Import Key'), name: 'import_otrs_endpoint_key', area: 'Import::OTRS', - description: 'Defines OTRS endpoint authentication key.', + description: __('Defines OTRS endpoint authentication key.'), options: { form: [ { @@ -3061,10 +3327,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Import User for HTTP basic authentication', + title: __('Import User for HTTP basic authentication'), name: 'import_otrs_user', area: 'Import::OTRS', - description: 'Defines HTTP basic authentication user (only if OTRS is protected via HTTP basic auth).', + description: __('Defines HTTP basic authentication user (only if OTRS is protected via HTTP basic auth).'), options: { form: [ { @@ -3080,10 +3346,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Import Password for http basic authentication', + title: __('Import Password for HTTP basic authentication'), name: 'import_otrs_password', area: 'Import::OTRS', - description: 'Defines http basic authentication password (only if OTRS is protected via http basic auth).', + description: __('Defines HTTP basic authentication password (only if OTRS is protected via HTTP basic auth).'), options: { form: [ { @@ -3099,10 +3365,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Import Endpoint', + title: __('Import Endpoint'), name: 'import_zendesk_endpoint', area: 'Import::Zendesk', - description: 'Defines Zendesk endpoint to import users, ticket, states and articles.', + description: __('Defines a Zendesk endpoint to import users, tickets, states, and articles.'), options: { form: [ { @@ -3117,10 +3383,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Import Key for requesting the Zendesk API', + title: __('Import API key for requesting the Zendesk API'), name: 'import_zendesk_endpoint_key', area: 'Import::Zendesk', - description: 'Defines Zendesk endpoint authentication key.', + description: __('Defines Zendesk endpoint authentication API key.'), options: { form: [ { @@ -3136,10 +3402,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Import User for requesting the Zendesk API', + title: __('Import User for requesting the Zendesk API'), name: 'import_zendesk_endpoint_username', area: 'Import::Zendesk', - description: 'Defines Zendesk endpoint authentication user.', + description: __('Defines Zendesk endpoint authentication user.'), options: { form: [ { @@ -3155,10 +3421,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Import Endpoint', + title: __('Import Endpoint'), name: 'import_freshdesk_endpoint', area: 'Import::Freshdesk', - description: 'Defines Freshdesk endpoint to import users, ticket, states and articles.', + description: __('Defines a Freshdesk endpoint to import users, tickets, states, and articles.'), options: { form: [ { @@ -3173,10 +3439,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Import Key for requesting the Freshdesk API', + title: __('Import API key for requesting the Freshdesk API'), name: 'import_freshdesk_endpoint_key', area: 'Import::Freshdesk', - description: 'Defines Freshdesk endpoint authentication key.', + description: __('Defines Freshdesk endpoint authentication API key.'), options: { form: [ { @@ -3192,10 +3458,65 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Import Backends', + title: __('Import Endpoint'), + name: 'import_kayako_endpoint', + area: 'Import::Kayako', + description: __('Defines a Kayako endpoint to import users, tickets, states, and articles.'), + options: { + form: [ + { + display: '', + null: false, + name: 'import_kayako_endpoint', + tag: 'input', + }, + ], + }, + state: 'https://yours.kayako.com/api/v1', + frontend: false +) +Setting.create_if_not_exists( + title: __('Import User for requesting the Kayako API'), + name: 'import_kayako_endpoint_username', + area: 'Import::Kayako', + description: __('Defines Kayako endpoint authentication user.'), + options: { + form: [ + { + display: '', + null: false, + name: 'import_kayako_endpoint_username', + tag: 'input', + }, + ], + }, + state: '', + frontend: false +) +Setting.create_if_not_exists( + title: __('Import Password for requesting the Kayako API'), + name: 'import_kayako_endpoint_password', + area: 'Import::Kayako', + description: __('Defines Kayako endpoint authentication password.'), + options: { + form: [ + { + display: '', + null: false, + name: 'import_kayako_endpoint_password', + tag: 'input', + }, + ], + }, + state: '', + frontend: false +) + +Setting.create_if_not_exists( + title: __('Import Backends'), name: 'import_backends', area: 'Import', - description: 'A list of active import backends that get scheduled automatically.', + description: __('A list of active import backends that gets scheduled automatically.'), options: {}, state: ['Import::Ldap', 'Import::Exchange'], preferences: { @@ -3205,10 +3526,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Sequencer log level', + title: __('Sequencer log level'), name: 'sequencer_log_level', area: 'Core', - description: 'Defines the log levels for various logging actions of the Sequencer.', + description: __('Defines the log levels for various logging actions of the Sequencer.'), options: {}, state: { sequence: { @@ -3240,10 +3561,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Time Accounting', + title: __('Time Accounting'), name: 'time_accounting', area: 'Web::Base', - description: 'Enable time accounting.', + description: __('Enable time accounting.'), options: { form: [ { @@ -3267,10 +3588,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Time Accounting Selector', + title: __('Time Accounting Selector'), name: 'time_accounting_selector', area: 'Web::Base', - description: 'Enable time accounting for these tickets.', + description: __('Enable time accounting for these tickets.'), options: { form: [ {}, @@ -3285,10 +3606,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'New Tags', + title: __('New Tags'), name: 'tag_new', area: 'Web::Base', - description: 'Allow users to create new tags.', + description: __('Allow users to create new tags.'), options: { form: [ { @@ -3312,10 +3633,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Default calendar tickets subscriptions', + title: __('Default calendar tickets subscriptions'), name: 'defaults_calendar_subscriptions_tickets', area: 'Defaults::CalendarSubscriptions', - description: 'Defines the default calendar tickets subscription settings.', + description: __('Defines the default calendar tickets subscription settings.'), options: {}, state: { escalation: { @@ -3338,218 +3659,235 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Defines translator identifier.', - name: 'translator_key', - area: 'i18n::translator_key', - description: 'Defines the translator identifier for contributions.', - options: {}, - state: '', - frontend: false -) - -Setting.create_if_not_exists( - title: 'Defines postmaster filter.', + title: __('Defines postmaster filter.'), name: '0005_postmaster_filter_trusted', area: 'Postmaster::PreFilter', - description: 'Defines postmaster filter to remove X-Zammad headers from not trusted sources.', + description: __('Defines postmaster filter to remove X-Zammad headers from untrustworthy sources.'), options: {}, state: 'Channel::Filter::Trusted', frontend: false ) Setting.create_if_not_exists( - title: 'Defines postmaster filter.', + title: __('Defines postmaster filter.'), name: '0006_postmaster_filter_auto_response_check', area: 'Postmaster::PreFilter', - description: 'Defines postmaster filter to identify auto responses to prevent auto replies from Zammad.', + description: __('Defines postmaster filter to identify auto responses to prevent auto replies from Zammad.'), options: {}, state: 'Channel::Filter::AutoResponseCheck', frontend: false ) Setting.create_if_not_exists( - title: 'Defines postmaster filter.', + title: __('Defines postmaster filter.'), name: '0007_postmaster_filter_follow_up_check', area: 'Postmaster::PreFilter', - description: 'Defines postmaster filter to identify follow-ups (based on admin settings).', + description: __('Defines postmaster filter to identify follow-ups (based on admin settings).'), options: {}, state: 'Channel::Filter::FollowUpCheck', frontend: false ) Setting.create_if_not_exists( - title: 'Defines postmaster filter.', + title: __('Defines postmaster filter.'), name: '0008_postmaster_filter_follow_up_merged', area: 'Postmaster::PreFilter', - description: 'Defines postmaster filter to identify follow-up ticket for merged tickets.', + description: __('Defines postmaster filter to identify follow-up ticket for merged tickets.'), options: {}, state: 'Channel::Filter::FollowUpMerged', frontend: false ) Setting.create_if_not_exists( - title: 'Defines postmaster filter.', + title: __('Defines postmaster filter.'), + name: '0009_postmaster_filter_follow_up_assignment', + area: 'Postmaster::PreFilter', + description: __('Defines postmaster filter to set the owner (based on group follow up assignment).'), + options: {}, + state: 'Channel::Filter::FollowUpAssignment', + frontend: false +) +Setting.create_if_not_exists( + title: __('Defines postmaster filter.'), name: '0011_postmaster_sender_based_on_reply_to', area: 'Postmaster::PreFilter', - description: 'Defines postmaster filter to set the sender/from of emails based on reply-to header.', + description: __('Defines postmaster filter to set the sender/from of emails based on reply-to header.'), options: {}, state: 'Channel::Filter::ReplyToBasedSender', frontend: false ) Setting.create_if_not_exists( - title: 'Define postmaster filter.', + title: __('Define postmaster filter.'), name: '0018_postmaster_import_archive', area: 'Postmaster::PreFilter', - description: 'Define postmaster filter to import archive mailboxes.', + description: __('Define postmaster filter to import archive mailboxes.'), options: {}, state: 'Channel::Filter::ImportArchive', frontend: false ) Setting.create_if_not_exists( - title: 'Defines postmaster filter.', + title: __('Defines postmaster filter.'), name: '0012_postmaster_filter_sender_is_system_address', area: 'Postmaster::PreFilter', - description: 'Defines postmaster filter to check if email has been created by Zammad itself and will set the article sender.', + description: __('Defines postmaster filter to check if email has been created by Zammad itself and will set the article sender.'), options: {}, state: 'Channel::Filter::SenderIsSystemAddress', frontend: false ) Setting.create_if_not_exists( - title: 'Defines postmaster filter.', + title: __('Defines postmaster filter.'), name: '0014_postmaster_filter_own_notification_loop_detection', area: 'Postmaster::PreFilter', - description: 'Define postmaster filter to check if email is a own created notification email, then ignore it to prevent email loops.', + description: __('Defines postmaster filter to check if the email is a self-created notification email, then ignore it to prevent email loops.'), options: {}, state: 'Channel::Filter::OwnNotificationLoopDetection', frontend: false ) Setting.create_if_not_exists( - title: 'Defines postmaster filter.', + title: __('Defines postmaster filter.'), name: '0015_postmaster_filter_identify_sender', area: 'Postmaster::PreFilter', - description: 'Defines postmaster filter to identify sender user.', + description: __('Defines postmaster filter to identify sender user.'), options: {}, state: 'Channel::Filter::IdentifySender', frontend: false ) Setting.create_if_not_exists( - title: 'Defines postmaster filter.', + title: __('Defines postmaster filter.'), name: '0016_postmaster_filter_smime', area: 'Postmaster::PreFilter', - description: 'Defines postmaster filter to handle secure mailing.', + description: __('Defines postmaster filter to handle secure mailing.'), options: {}, state: 'Channel::Filter::SecureMailing', frontend: false ) Setting.create_if_not_exists( - title: 'Defines postmaster filter.', + title: __('Defines postmaster filter.'), name: '0030_postmaster_filter_out_of_office_check', area: 'Postmaster::PreFilter', - description: 'Defines postmaster filter to identify out-of-office emails for follow-up detection and keeping current ticket state.', + description: __('Defines postmaster filter to identify out-of-office emails for follow-up detection and keeping current ticket state.'), options: {}, state: 'Channel::Filter::OutOfOfficeCheck', frontend: false ) Setting.create_if_not_exists( - title: 'Defines postmaster filter.', + title: __('Defines postmaster filter.'), name: '0200_postmaster_filter_follow_up_possible_check', area: 'Postmaster::PreFilter', - description: 'Define postmaster filter to check if follow-ups get created (based on admin settings).', + description: __('Define postmaster filter to check if follow-ups get created (based on admin settings).'), options: {}, state: 'Channel::Filter::FollowUpPossibleCheck', frontend: false ) Setting.create_if_not_exists( - title: 'Defines postmaster filter.', + title: __('Defines postmaster filter.'), name: '0900_postmaster_filter_bounce_follow_up_check', area: 'Postmaster::PreFilter', - description: 'Defines postmaster filter to identify postmaster bounced - to handle it as follow-up of the original ticket.', + description: __('Defines postmaster filter to identify postmaster bounces; and handles them as follow-up of the original tickets'), options: {}, state: 'Channel::Filter::BounceFollowUpCheck', frontend: false ) Setting.create_if_not_exists( - title: 'Defines postmaster filter.', + title: __('Defines postmaster filter.'), name: '0950_postmaster_filter_bounce_delivery_permanent_failed', area: 'Postmaster::PreFilter', - description: 'Defines postmaster filter to identify postmaster bounced - disable sending notification on permanent deleivery failed.', + description: __('Defines postmaster filter to identify postmaster bounces; and disables sending notification if delivery fails permanently.'), options: {}, state: 'Channel::Filter::BounceDeliveryPermanentFailed', frontend: false ) Setting.create_if_not_exists( - title: 'Defines postmaster filter.', + title: __('Defines postmaster filter.'), name: '0955_postmaster_filter_bounce_delivery_temporary_failed', area: 'Postmaster::PreFilter', - description: 'Defines postmaster filter to identify postmaster bounced - reopen ticket on permanent temporary failed.', + description: __('Defines postmaster filter to identify postmaster bounces; and reopens tickets if delivery fails permanently.'), options: {}, state: 'Channel::Filter::BounceDeliveryTemporaryFailed', frontend: false ) Setting.create_if_not_exists( - title: 'Defines postmaster filter.', + title: __('Defines postmaster filter.'), name: '1000_postmaster_filter_database_check', area: 'Postmaster::PreFilter', - description: 'Defines postmaster filter for filters managed via admin interface.', + description: __('Defines postmaster filter for filters managed via admin interface.'), options: {}, state: 'Channel::Filter::Database', frontend: false ) Setting.create_if_not_exists( - title: 'Defines postmaster filter.', + title: __('Defines postmaster filter.'), name: '5000_postmaster_filter_icinga', area: 'Postmaster::PreFilter', - description: 'Defines postmaster filter to manage Icinga (http://www.icinga.org) emails.', + description: __('Defines postmaster filter to manage Icinga (http://www.icinga.org) emails.'), options: {}, state: 'Channel::Filter::Icinga', frontend: false ) Setting.create_if_not_exists( - title: 'Defines postmaster filter.', + title: __('Defines postmaster filter.'), name: '5100_postmaster_filter_nagios', area: 'Postmaster::PreFilter', - description: 'Defines postmaster filter to manage Nagios (http://www.nagios.org) emails.', + description: __('Defines postmaster filter to manage Nagios (http://www.nagios.org) emails.'), options: {}, state: 'Channel::Filter::Nagios', frontend: false ) Setting.create_if_not_exists( - title: 'Defines postmaster filter.', + title: __('Defines postmaster filter.'), name: '5300_postmaster_filter_monit', area: 'Postmaster::PreFilter', - description: 'Defines postmaster filter to manage Monit (https://mmonit.com/monit/) emails.', + description: __('Defines postmaster filter to manage Monit (https://mmonit.com/monit/) emails.'), options: {}, state: 'Channel::Filter::Monit', frontend: false ) Setting.create_if_not_exists( - title: 'Defines postmaster filter.', + title: __('Defines postmaster filter.'), name: '5400_postmaster_filter_service_now_check', area: 'Postmaster::PreFilter', - description: 'Defines postmaster filter to identify service now mails for correct follow-ups.', + description: __('Defines postmaster filter to identify ServiceNow mails for correct follow-ups.'), options: {}, state: 'Channel::Filter::ServiceNowCheck', frontend: false ) Setting.create_if_not_exists( - title: 'Defines postmaster filter.', + title: __('Defines postmaster filter.'), name: '5401_postmaster_filter_service_now_check', area: 'Postmaster::PostFilter', - description: 'Defines postmaster filter to identify service now mails for correct follow-ups.', + description: __('Defines postmaster filter to identify ServiceNow mails for correct follow-ups.'), options: {}, state: 'Channel::Filter::ServiceNowCheck', frontend: false ) Setting.create_if_not_exists( - title: 'Define postmaster filter.', + title: __('Defines postmaster filter.'), + name: '5400_postmaster_filter_jira_check', + area: 'Postmaster::PreFilter', + description: __('Defines postmaster filter to identify Jira mails for correct follow-ups.'), + options: {}, + state: 'Channel::Filter::JiraCheck', + frontend: false +) +Setting.create_if_not_exists( + title: __('Defines postmaster filter.'), + name: '5401_postmaster_filter_jira_check', + area: 'Postmaster::PostFilter', + description: __('Defines postmaster filter to identify Jira mails for correct follow-ups.'), + options: {}, + state: 'Channel::Filter::JiraCheck', + frontend: false +) +Setting.create_if_not_exists( + title: __('Define postmaster filter.'), name: '5500_postmaster_internal_article_check', area: 'Postmaster::PreFilter', - description: 'Defines the postmaster filter which set the article internal if a forwarded, replied or sent email also exists with the article internal received.', + description: __('Defines postmaster filter which sets the articles visibility to internal if it is a rely to an internal article or the last outgoing email is internal.'), options: {}, state: 'Channel::Filter::InternalArticleCheck', frontend: false ) Setting.create_if_not_exists( - title: 'Icinga integration', + title: __('Icinga integration'), name: 'icinga_integration', area: 'Integration::Switch', - description: 'Defines if Icinga (http://www.icinga.org) is enabled or not.', + description: __('Defines if Icinga (http://www.icinga.org) is enabled or not.'), options: { form: [ { @@ -3572,10 +3910,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Sender', + title: __('Sender'), name: 'icinga_sender', area: 'Integration::Icinga', - description: 'Defines the sender email address of Icinga emails.', + description: __('Defines the sender email address of Icinga emails.'), options: { form: [ { @@ -3595,10 +3933,10 @@ Setting.create_if_not_exists( frontend: false, ) Setting.create_if_not_exists( - title: 'Auto close', + title: __('Auto close'), name: 'icinga_auto_close', area: 'Integration::Icinga', - description: 'Defines if tickets should be closed if service is recovered.', + description: __('Defines if tickets should be closed if service is recovered.'), options: { form: [ { @@ -3621,10 +3959,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Auto close state', + title: __('Auto-close state'), name: 'icinga_auto_close_state_id', area: 'Integration::Icinga', - description: 'Defines the state of auto closed tickets.', + description: __('Defines the state of auto-closed tickets.'), options: { form: [ { @@ -3644,10 +3982,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Nagios integration', + title: __('Nagios integration'), name: 'nagios_integration', area: 'Integration::Switch', - description: 'Defines if Nagios (http://www.nagios.org) is enabled or not.', + description: __('Defines if Nagios (http://www.nagios.org) is enabled or not.'), options: { form: [ { @@ -3670,10 +4008,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Sender', + title: __('Sender'), name: 'nagios_sender', area: 'Integration::Nagios', - description: 'Defines the sender email address of Nagios emails.', + description: __('Defines the sender email address of Nagios emails.'), options: { form: [ { @@ -3693,10 +4031,10 @@ Setting.create_if_not_exists( frontend: false, ) Setting.create_if_not_exists( - title: 'Auto close', + title: __('Auto close'), name: 'nagios_auto_close', area: 'Integration::Nagios', - description: 'Defines if tickets should be closed if service is recovered.', + description: __('Defines if tickets should be closed if service is recovered.'), options: { form: [ { @@ -3719,10 +4057,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Auto close state', + title: __('Auto-close state'), name: 'nagios_auto_close_state_id', area: 'Integration::Nagios', - description: 'Defines the state of auto closed tickets.', + description: __('Defines the state of auto-closed tickets.'), options: { form: [ { @@ -3742,10 +4080,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Checkmk integration', + title: __('Checkmk integration'), name: 'check_mk_integration', area: 'Integration::Switch', - description: 'Defines if Checkmk (https://checkmk.com/) is enabled or not.', + description: __('Defines if Checkmk (https://checkmk.com/) is enabled or not.'), options: { form: [ { @@ -3768,10 +4106,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Group', + title: __('Group'), name: 'check_mk_group_id', area: 'Integration::CheckMK', - description: 'Defines the group of created tickets.', + description: __('Defines the group of created tickets.'), options: { form: [ { @@ -3791,10 +4129,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Auto close', + title: __('Auto close'), name: 'check_mk_auto_close', area: 'Integration::CheckMK', - description: 'Defines if tickets should be closed if service is recovered.', + description: __('Defines if tickets should be closed if service is recovered.'), options: { form: [ { @@ -3817,10 +4155,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Auto close state', + title: __('Auto-close state'), name: 'check_mk_auto_close_state_id', area: 'Integration::CheckMK', - description: 'Defines the state of auto closed tickets.', + description: __('Defines the state of auto-closed tickets.'), options: { form: [ { @@ -3840,10 +4178,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Checkmk token', + title: __('Checkmk token'), name: 'check_mk_token', area: 'Core', - description: 'Defines the Checkmk token for allowing updates.', + description: __('Defines the Checkmk token for allowing updates.'), options: {}, state: ENV['CHECK_MK_TOKEN'] || SecureRandom.hex(16), preferences: { @@ -3852,10 +4190,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Monit integration', + title: __('Monit integration'), name: 'monit_integration', area: 'Integration::Switch', - description: 'Defines if Monit (https://mmonit.com/monit/) is enabled or not.', + description: __('Defines if Monit (https://mmonit.com/monit/) is enabled or not.'), options: { form: [ { @@ -3878,10 +4216,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Sender', + title: __('Sender'), name: 'monit_sender', area: 'Integration::Monit', - description: 'Defines the sender email address of the service emails.', + description: __('Defines the sender email address of the service emails.'), options: { form: [ { @@ -3901,10 +4239,10 @@ Setting.create_if_not_exists( frontend: false, ) Setting.create_if_not_exists( - title: 'Auto close', + title: __('Auto close'), name: 'monit_auto_close', area: 'Integration::Monit', - description: 'Defines if tickets should be closed if service is recovered.', + description: __('Defines if tickets should be closed if service is recovered.'), options: { form: [ { @@ -3928,10 +4266,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Auto close state', + title: __('Auto-close state'), name: 'monit_auto_close_state_id', area: 'Integration::Monit', - description: 'Defines the state of auto closed tickets.', + description: __('Defines the state of auto-closed tickets.'), options: { form: [ { @@ -3953,10 +4291,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'LDAP integration', + title: __('LDAP integration'), name: 'ldap_integration', area: 'Integration::Switch', - description: 'Defines if LDAP is enabled or not.', + description: __('Defines if LDAP is enabled or not.'), options: { form: [ { @@ -3980,10 +4318,10 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'Exchange config', + title: __('Exchange config'), name: 'exchange_config', area: 'Integration::Exchange', - description: 'Defines the Exchange config.', + description: __('Defines the Exchange config.'), options: {}, state: {}, preferences: { @@ -3993,10 +4331,23 @@ Setting.create_if_not_exists( frontend: false, ) Setting.create_if_not_exists( - title: 'Exchange integration', + title: __('Exchange OAuth'), + name: 'exchange_oauth', + area: 'Integration::Exchange', + description: __('Defines the Exchange OAuth config.'), + options: {}, + state: {}, + preferences: { + prio: 2, + permission: ['admin.integration'], + }, + frontend: false, +) +Setting.create_if_not_exists( + title: __('Exchange integration'), name: 'exchange_integration', area: 'Integration::Switch', - description: 'Defines if Exchange is enabled or not.', + description: __('Defines if Exchange is enabled or not.'), options: { form: [ { @@ -4020,23 +4371,10 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'LDAP config', - name: 'ldap_config', - area: 'Integration::LDAP', - description: 'Defines the LDAP config.', - options: {}, - state: {}, - preferences: { - prio: 2, - permission: ['admin.integration'], - }, - frontend: false, -) -Setting.create_if_not_exists( - title: 'i-doit integration', + title: __('i-doit integration'), name: 'idoit_integration', area: 'Integration::Switch', - description: 'Defines if i-doit (http://www.i-doit) is enabled or not.', + description: __('Defines if the i-doit (https://www.i-doit.org/) integration is enabled or not.'), options: { form: [ { @@ -4060,10 +4398,10 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'i-doit config', + title: __('i-doit config'), name: 'idoit_config', area: 'Integration::Idoit', - description: 'Defines the i-doit config.', + description: __('Defines the i-doit config.'), options: {}, state: {}, preferences: { @@ -4073,10 +4411,10 @@ Setting.create_if_not_exists( frontend: false, ) Setting.create_if_not_exists( - title: 'GitLab integration', + title: __('GitLab integration'), name: 'gitlab_integration', area: 'Integration::Switch', - description: 'Defines if the GitLab (http://www.gitlab.com) integration is enabled or not.', + description: __('Defines if the GitLab (http://www.gitlab.com) integration is enabled or not.'), options: { form: [ { @@ -4100,10 +4438,10 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'GitLab config', + title: __('GitLab config'), name: 'gitlab_config', area: 'Integration::GitLab', - description: 'Stores the GitLab configuration.', + description: __('Stores the GitLab configuration.'), options: {}, state: { endpoint: 'https://gitlab.com/api/graphql', @@ -4115,10 +4453,10 @@ Setting.create_if_not_exists( frontend: false, ) Setting.create_if_not_exists( - title: 'GitHub integration', + title: __('GitHub integration'), name: 'github_integration', area: 'Integration::Switch', - description: 'Defines if the GitHub (http://www.github.com) integration is enabled or not.', + description: __('Defines if the GitHub (http://www.github.com) integration is enabled or not.'), options: { form: [ { @@ -4142,10 +4480,10 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'GitHub config', + title: __('GitHub config'), name: 'github_config', area: 'Integration::GitHub', - description: 'Stores the GitHub configuration.', + description: __('Stores the GitHub configuration.'), options: {}, state: { endpoint: 'https://api.github.com/graphql', @@ -4157,46 +4495,46 @@ Setting.create_if_not_exists( frontend: false, ) Setting.create_if_not_exists( - title: 'Defines sync transaction backend.', + title: __('Defines sync transaction backend.'), name: '0100_trigger', area: 'Transaction::Backend::Sync', - description: 'Defines the transaction backend to execute triggers.', + description: __('Defines the transaction backend to execute triggers.'), options: {}, state: 'Transaction::Trigger', frontend: false ) Setting.create_if_not_exists( - title: 'Defines transaction backend.', + title: __('Defines transaction backend.'), name: '0100_notification', area: 'Transaction::Backend::Async', - description: 'Defines the transaction backend to send agent notifications.', + description: __('Defines the transaction backend to send agent notifications.'), options: {}, state: 'Transaction::Notification', frontend: false ) Setting.create_if_not_exists( - title: 'Defines transaction backend.', + title: __('Defines transaction backend.'), name: '1000_signature_detection', area: 'Transaction::Backend::Async', - description: 'Defines the transaction backend to detect customer signatures in emails.', + description: __('Defines the transaction backend to detect customer signatures in emails.'), options: {}, state: 'Transaction::SignatureDetection', frontend: false ) Setting.create_if_not_exists( - title: 'Defines transaction backend.', + title: __('Defines transaction backend.'), name: '6000_slack_webhook', area: 'Transaction::Backend::Async', - description: 'Defines the transaction backend which posts messages to Slack (http://www.slack.com).', + description: __('Defines the transaction backend which posts messages to Slack (http://www.slack.com).'), options: {}, state: 'Transaction::Slack', frontend: false ) Setting.create_if_not_exists( - title: 'Slack integration', + title: __('Slack integration'), name: 'slack_integration', area: 'Integration::Switch', - description: 'Defines if Slack (http://www.slack.org) is enabled or not.', + description: __('Defines if Slack (http://www.slack.org) is enabled or not.'), options: { form: [ { @@ -4219,10 +4557,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Slack config', + title: __('Slack config'), name: 'slack_config', area: 'Integration::Slack', - description: 'Defines the slack config.', + description: __('Defines the Slack config.'), options: {}, state: { items: [] @@ -4234,10 +4572,10 @@ Setting.create_if_not_exists( frontend: false, ) Setting.create_if_not_exists( - title: 'sipgate.io integration', + title: __('sipgate.io integration'), name: 'sipgate_integration', area: 'Integration::Switch', - description: 'Defines if sipgate.io (http://www.sipgate.io) is enabled or not.', + description: __('Defines if sipgate.io (http://www.sipgate.io) is enabled or not.'), options: { form: [ { @@ -4262,10 +4600,31 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'sipgate.io config', + title: __('sipgate.io Token'), + name: 'sipgate_token', + area: 'Integration::Sipgate', + description: __('Token for Sipgate.'), + options: { + form: [ + { + display: '', + null: false, + name: 'sipgate_token', + tag: 'input', + }, + ], + }, + state: ENV['SIPGATE_TOKEN'] || SecureRandom.urlsafe_base64(20), + preferences: { + permission: ['admin.integration'], + }, + frontend: false +) +Setting.create_if_not_exists( + title: __('sipgate.io config'), name: 'sipgate_config', area: 'Integration::Sipgate', - description: 'Defines the sipgate.io config.', + description: __('Defines the sipgate.io config.'), options: {}, state: { 'outbound' => { 'routing_table' => [], 'default_caller_id' => '' }, 'inbound' => { 'block_caller_ids' => [] } }, preferences: { @@ -4275,10 +4634,10 @@ Setting.create_if_not_exists( frontend: false, ) Setting.create_if_not_exists( - title: 'sipgate.io alternative fqdn', + title: __('sipgate.io alternative FQDN'), name: 'sipgate_alternative_fqdn', area: 'Integration::Sipgate::Expert', - description: 'Alternative FQDN for callbacks if you operate Zammad in internal network.', + description: __('Alternative FQDN for callbacks if you operate Zammad in an internal network.'), options: { form: [ { @@ -4296,10 +4655,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'cti integration', + title: __('CTI integration'), name: 'cti_integration', area: 'Integration::Switch', - description: 'Defines if generic CTI is enabled or not.', + description: __('Defines if generic CTI integration is enabled or not.'), options: { form: [ { @@ -4324,10 +4683,10 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'cti config', + title: __('CTI config'), name: 'cti_config', area: 'Integration::Cti', - description: 'Defines the cti config.', + description: __('Defines the CTI config.'), options: {}, state: { 'outbound' => { 'routing_table' => [], 'default_caller_id' => '' }, 'inbound' => { 'block_caller_ids' => [] } }, preferences: { @@ -4337,10 +4696,10 @@ Setting.create_if_not_exists( frontend: false, ) Setting.create_if_not_exists( - title: 'CTI Token', + title: __('CTI Token'), name: 'cti_token', area: 'Integration::Cti', - description: 'Token for cti.', + description: __('Token for CTI.'), options: { form: [ { @@ -4358,10 +4717,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'cti customer last activity', + title: __('CTI customer last activity'), name: 'cti_customer_last_activity', area: 'Integration::Cti', - description: 'Defines the range in seconds of customer activity to trigger the user profile dialog on call.', + description: __('Defines the duration of customer activity (in seconds) on a call until the user profile dialog is shown.'), options: {}, state: 30.days, preferences: { @@ -4371,10 +4730,10 @@ Setting.create_if_not_exists( frontend: false, ) Setting.create_if_not_exists( - title: 'Placetel integration', + title: __('Placetel integration'), name: 'placetel_integration', area: 'Integration::Switch', - description: 'Defines if Placetel (http://www.placetel.de) is enabled or not.', + description: __('Defines if Placetel (http://www.placetel.de) is enabled or not.'), options: { form: [ { @@ -4399,10 +4758,10 @@ Setting.create_if_not_exists( frontend: true ) Setting.create_if_not_exists( - title: 'Placetel config', + title: __('Placetel config'), name: 'placetel_config', area: 'Integration::Placetel', - description: 'Defines the Placetel config.', + description: __('Defines the Placetel config.'), options: {}, state: { 'outbound' => { 'routing_table' => [], 'default_caller_id' => '' }, 'inbound' => { 'block_caller_ids' => [] } }, preferences: { @@ -4413,10 +4772,10 @@ Setting.create_if_not_exists( frontend: false, ) Setting.create_if_not_exists( - title: 'PLACETEL Token', + title: __('Placetel Token'), name: 'placetel_token', area: 'Integration::Placetel', - description: 'Token for Placetel.', + description: __('Defines the token for Placetel.'), options: { form: [ { @@ -4434,10 +4793,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Clearbit integration', + title: __('Clearbit integration'), name: 'clearbit_integration', area: 'Integration::Switch', - description: 'Defines if Clearbit (http://www.clearbit.com) is enabled or not.', + description: __('Defines if Clearbit (http://www.clearbit.com) is enabled or not.'), options: { form: [ { @@ -4460,10 +4819,10 @@ Setting.create_if_not_exists( frontend: false ) Setting.create_if_not_exists( - title: 'Clearbit config', + title: __('Clearbit config'), name: 'clearbit_config', area: 'Integration::Clearbit', - description: 'Defines the Clearbit config.', + description: __('Defines the Clearbit config.'), options: {}, state: {}, frontend: false, @@ -4473,87 +4832,29 @@ Setting.create_if_not_exists( }, ) Setting.create_if_not_exists( - title: 'Defines transaction backend.', + title: __('Defines transaction backend.'), name: '9000_clearbit_enrichment', area: 'Transaction::Backend::Async', - description: 'Defines the transaction backend which will enrich customer and organization information from Clearbit (http://www.clearbit.com).', + description: __('Defines the transaction backend which will enrich customer and organization information from Clearbit (http://www.clearbit.com).'), options: {}, state: 'Transaction::ClearbitEnrichment', frontend: false ) Setting.create_if_not_exists( - title: 'Defines transaction backend.', + title: __('Defines transaction backend.'), name: '9100_cti_caller_id_detection', area: 'Transaction::Backend::Async', - description: 'Defines the transaction backend which detects caller IDs in objects and store them for CTI lookups.', + description: __('Defines the transaction backend which detects caller IDs in objects and stores them for CTI lookups.'), options: {}, state: 'Transaction::CtiCallerIdDetection', frontend: false ) + Setting.create_if_not_exists( - title: 'Defines transaction backend.', - name: '9200_karma', - area: 'Transaction::Backend::Async', - description: 'Defines the transaction backend which creates the karma score.', - options: {}, - state: 'Transaction::Karma', - frontend: false -) -Setting.create_if_not_exists( - title: 'Defines karma levels.', - name: 'karma_levels', - area: 'Core::Karma', - description: 'Defines the karma levels.', - options: {}, - state: [ - { - name: 'Beginner', - start: 0, - end: 499, - }, - { - name: 'Newbie', - start: 500, - end: 1999, - }, - { - name: 'Intermediate', - start: 2000, - end: 4999, - }, - { - name: 'Professional', - start: 5000, - end: 6999, - }, - { - name: 'Expert', - start: 7000, - end: 8999, - }, - { - name: 'Master', - start: 9000, - end: 18_999, - }, - { - name: 'Evangelist', - start: 19_000, - end: 49_999, - }, - { - name: 'Hero', - start: 50_000, - end: nil, - }, - ], - frontend: false -) -Setting.create_if_not_exists( - title: 'Set limit of agents', + title: __('Set agent limit'), name: 'system_agent_limit', area: 'Core::Online', - description: 'Defines the limit of the agents.', + description: __('Defines the agent limit.'), options: {}, state: false, preferences: { online_service_disable: true }, @@ -4561,10 +4862,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'HTML Email CSS Font', + title: __('HTML Email CSS Font'), name: 'html_email_css_font', area: 'Core', - description: 'Defines the CSS font information for HTML Emails.', + description: __('Defines the CSS font information for HTML emails.'), options: {}, state: "font-family:'Helvetica Neue', Helvetica, Arial, Geneva, sans-serif; font-size: 12px;", preferences: { @@ -4572,13 +4873,23 @@ Setting.create_if_not_exists( }, frontend: false ) +Setting.create_if_not_exists( + title: __('HTML Sanitizer Processing Timeout'), + name: 'html_sanitizer_processing_timeout', + area: 'Core', + description: __('Defines processing timeout for the html sanitizer.'), + options: {}, + state: 20, + preferences: {}, + frontend: false +) # add the dashboard stats backend for 'Stats::TicketWaitingTime' Setting.create_if_not_exists( - title: 'Stats Backend', + title: __('Stats Backend'), name: 'Stats::TicketWaitingTime', area: 'Dashboard::Stats', - description: 'Defines a dashboard stats backend that get scheduled automatically.', + description: __('Defines a dashboard stats backend that gets scheduled automatically.'), options: {}, state: 'Stats::TicketWaitingTime', preferences: { @@ -4590,10 +4901,10 @@ Setting.create_if_not_exists( # add the dashboard stats backend for 'Stats::TicketEscalation' Setting.create_if_not_exists( - title: 'Stats Backend', + title: __('Stats Backend'), name: 'Stats::TicketEscalation', area: 'Dashboard::Stats', - description: 'Defines a dashboard stats backend that get scheduled automatically.', + description: __('Defines a dashboard stats backend that gets scheduled automatically.'), options: {}, state: 'Stats::TicketEscalation', preferences: { @@ -4605,10 +4916,10 @@ Setting.create_if_not_exists( # add the dashboard stats backend for 'Stats::TicketChannelDistribution' Setting.create_if_not_exists( - title: 'Stats Backend', + title: __('Stats Backend'), name: 'Stats::TicketChannelDistribution', area: 'Dashboard::Stats', - description: 'Defines a dashboard stats backend that get scheduled automatically.', + description: __('Defines a dashboard stats backend that gets scheduled automatically.'), options: {}, state: 'Stats::TicketChannelDistribution', preferences: { @@ -4620,10 +4931,10 @@ Setting.create_if_not_exists( # add the dashboard stats backend for 'Stats::TicketLoadMeasure' Setting.create_if_not_exists( - title: 'Stats Backend', + title: __('Stats Backend'), name: 'Stats::TicketLoadMeasure', area: 'Dashboard::Stats', - description: 'Defines a dashboard stats backend that get scheduled automatically.', + description: __('Defines a dashboard stats backend that gets scheduled automatically.'), options: {}, state: 'Stats::TicketLoadMeasure', preferences: { @@ -4635,10 +4946,10 @@ Setting.create_if_not_exists( # add the dashboard stats backend for 'Stats::TicketInProcess' Setting.create_if_not_exists( - title: 'Stats Backend', + title: __('Stats Backend'), name: 'Stats::TicketInProcess', area: 'Dashboard::Stats', - description: 'Defines a dashboard stats backend that get scheduled automatically.', + description: __('Defines a dashboard stats backend that gets scheduled automatically.'), options: {}, state: 'Stats::TicketInProcess', preferences: { @@ -4650,10 +4961,10 @@ Setting.create_if_not_exists( # add the dashboard stats backend for 'Stats::TicketReopen' Setting.create_if_not_exists( - title: 'Stats Backend', + title: __('Stats Backend'), name: 'Stats::TicketReopen', area: 'Dashboard::Stats', - description: 'Defines a dashboard stats backend that get scheduled automatically.', + description: __('Defines a dashboard stats backend that gets scheduled automatically.'), options: {}, state: 'Stats::TicketReopen', preferences: { @@ -4664,10 +4975,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Kb multi-lingual support', + title: __('Knowledge Base multilingual support'), name: 'kb_multi_lingual_support', area: 'Kb::Core', - description: 'Support of multi-lingual Knowledge Base.', + description: __('Support of multilingual Knowledge Base.'), options: {}, state: true, preferences: { online_service_disable: true }, @@ -4675,10 +4986,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Kb active', + title: __('Knowledge Base active'), name: 'kb_active', area: 'Kb::Core', - description: 'Defines if KB navbar button is enabled', + description: __('Defines if Knowledge Base navbar button is enabled.'), state: true, preferences: { prio: 1, @@ -4690,10 +5001,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Kb active publicly', + title: __('Knowledge Base active publicly'), name: 'kb_active_publicly', area: 'Kb::Core', - description: 'Defines if KB navbar button is enabled for users without KB permission', + description: __('Defines if Knowledge Base navbar button is enabled for users without Knowledge Base permission.'), state: false, preferences: { prio: 1, @@ -4705,10 +5016,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'Define timeframe where a own created note can get deleted.', + title: __('Defines the timeframe during which a self-created note can be deleted.'), name: 'ui_ticket_zoom_article_delete_timeframe', area: 'UI::TicketZoomArticle', - description: "Set timeframe in seconds. If it's set to 0 you can delete notes without time limits", + description: __("Set timeframe in seconds. If it's set to 0 you can delete notes without time limits"), options: {}, state: 600, preferences: { @@ -4718,10 +5029,29 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'S/MIME integration', + title: __('Hint for adding an article to an existing ticket.'), + name: 'ui_ticket_add_article_hint', + area: 'UI::TicketZoomArticle', + description: __('Highlights if the note a user is writing is public or private'), + options: {}, + state: { + # 'note-internal' => 'You are writing an |internal note|, only people of your organization will see it.', + # 'note-public' => 'You are writing a |public note|.', + # 'phone-internal' => 'You are writing an |internal phone note|, only people of your organization will see it.', + # 'phone-public' => 'You are writing a |public phone note|.', + # .... + }, + preferences: { + permission: ['admin.ui'], + }, + frontend: true +) + +Setting.create_if_not_exists( + title: __('S/MIME integration'), name: 'smime_integration', area: 'Integration::Switch', - description: 'Defines if S/MIME encryption is enabled or not.', + description: __('Defines if S/MIME encryption is enabled or not.'), options: { form: [ { @@ -4746,10 +5076,10 @@ Setting.create_if_not_exists( ) Setting.create_if_not_exists( - title: 'S/MIME config', + title: __('S/MIME config'), name: 'smime_config', area: 'Integration::SMIME', - description: 'Defines the S/MIME config.', + description: __('Defines the S/MIME config.'), options: {}, state: {}, preferences: { @@ -4801,11 +5131,12 @@ Setting.create_if_not_exists( frontend: true, ) + Setting.create_if_not_exists( - title: 'Authentication via %s', + title: __('Authentication via %s'), name: 'auth_sso', area: 'Security::ThirdPartyAuthentication', - description: 'Enables button for user authentication via %s. The button will redirect to /auth/sso on user interaction.', + description: __('Enables button for user authentication via %s. The button will redirect to /auth/sso on user interaction.'), options: { form: [ { @@ -4823,10 +5154,38 @@ Setting.create_if_not_exists( preferences: { controller: 'SettingsAreaSwitch', sub: {}, - title_i18n: ['SSO'], - description_i18n: ['SSO', 'Button for Single Sign On.'], + title_i18n: [__('SSO')], + description_i18n: [__('SSO')], permission: ['admin.security'], }, state: false, frontend: true ) + +Setting.create_if_not_exists( + title: __('Show calendar weeks in the picker of date/datetime fields'), + name: 'datepicker_show_calendar_weeks', + area: 'System::UI', + description: __('Defines if calendar weeks are shown in the picker of date/datetime fields to easily select the correct date.'), + options: { + form: [ + { + display: '', + null: true, + name: 'datepicker_show_calendar_weeks', + tag: 'boolean', + options: { + true => 'yes', + false => 'no', + }, + }, + ], + }, + state: false, + preferences: { + render: true, + prio: 4, + permission: ['admin.system'], + }, + frontend: true +)