Zammad Docker and addon updates
This commit is contained in:
parent
dab5ce0521
commit
aa18d3904e
16 changed files with 1972 additions and 2976 deletions
|
|
@ -1,30 +1,42 @@
|
||||||
FROM zammad/zammad-docker-compose:5.4.1 AS builder
|
FROM zammad/zammad-docker-compose:5.4.1 AS builder
|
||||||
COPY auto_install ${ZAMMAD_TMP_DIR}/auto_install
|
COPY auto_install ${ZAMMAD_TMP_DIR}/auto_install
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
RUN set -ex; \
|
RUN set -ex; \
|
||||||
apt-get update; \
|
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_PROJECT_URL=https://gitlab.com/sequoia-pgp/sequoia-ffi.git
|
||||||
ARG SEQUOIA_GIT_TAG=main
|
ARG SEQUOIA_GIT_TAG=main
|
||||||
ENV SEQUOIA_DIR=/usr/lib/sequoia
|
ENV SEQUOIA_DIR=/usr/lib/sequoia
|
||||||
ENV LD_LIBRARY_PATH=${SEQUOIA_DIR}/target/debug
|
ENV LD_LIBRARY_PATH=${SEQUOIA_DIR}/target/debug
|
||||||
|
|
||||||
RUN git clone -b "${SEQUOIA_GIT_TAG}" --single-branch --depth 1 "${SEQUOIA_PROJECT_URL}" "${SEQUOIA_DIR}";
|
RUN git clone -b "${SEQUOIA_GIT_TAG}" --single-branch --depth 1 "${SEQUOIA_PROJECT_URL}" "${SEQUOIA_DIR}";
|
||||||
WORKDIR ${SEQUOIA_DIR}
|
WORKDIR ${SEQUOIA_DIR}
|
||||||
RUN cargo update
|
RUN export PATH=~/.cargo/bin:$PATH && cargo build -p sequoia-openpgp-ffi;
|
||||||
RUN cargo build -p sequoia-openpgp-ffi;
|
|
||||||
RUN ls -la "${SEQUOIA_DIR}/target/debug";
|
WORKDIR ${ZAMMAD_TMP_DIR}
|
||||||
# RUN chown -R "${ZAMMAD_USER}":"${ZAMMAD_USER}" "${SEQUOIA_DIR}"
|
RUN echo "gem 'ruby_openpgp', git: 'https://github.com/throneless-tech/ruby_openpgp', branch: 'signing-and-userids'" >> Gemfile.local
|
||||||
# WORKDIR ${ZAMMAD_TMP_DIR}
|
RUN echo "gem 'rails-observers'" >> Gemfile.local
|
||||||
# RUN echo "gem 'ruby_openpgp', git: 'https://github.com/throneless-tech/ruby_openpgp', branch: 'signing-and-userids'" >> Gemfile.local ; \
|
RUN bundle install --without test development mysql
|
||||||
# echo "gem 'rails-observers'" >> Gemfile.local ; \
|
|
||||||
# bundle update tcr; \
|
RUN sed -i '/^[[:space:]]*# create install ready file/ i\
|
||||||
# bundle install --without test development mysql ;
|
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
|
|
||||||
|
|
|
||||||
|
|
@ -1,124 +0,0 @@
|
||||||
FROM ruby:2.6.8-slim-bullseye AS builder
|
|
||||||
|
|
||||||
LABEL maintainer="Abel Luck <abel@guardianproject.info>"
|
|
||||||
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 <abel@guardianproject.info>"
|
|
||||||
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
|
|
||||||
|
|
@ -1,660 +0,0 @@
|
||||||
### GNU AFFERO GENERAL PUBLIC LICENSE
|
|
||||||
|
|
||||||
Version 3, 19 November 2007
|
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc.
|
|
||||||
<https://fsf.org/>
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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 <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
# docker-zammad
|
|
||||||
|
|
||||||
[](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 <abel@guardianproject.info> of [Guardian Project](https://guardianproject.info)
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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'
|
|
||||||
|
|
@ -1,559 +0,0 @@
|
||||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
||||||
|
|
||||||
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
|
|
||||||
=viYu
|
|
||||||
-----END PGP PUBLIC KEY BLOCK-----
|
|
||||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
||||||
|
|
||||||
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
|
|
||||||
qt8sxLYxB1Tx//g=
|
|
||||||
=U5jx
|
|
||||||
-----END PGP PUBLIC KEY BLOCK-----
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,46 +1,50 @@
|
||||||
class HardeningHardenSettings < ActiveRecord::Migration[5.2]
|
class HardeningHardenSettings < ActiveRecord::Migration[5.2]
|
||||||
def self.restore_setting(name)
|
def self.restore_setting(name)
|
||||||
s = Setting.find_by(name: name)
|
s = Setting.find_by(name: name)
|
||||||
if !s.nil?
|
return if s.nil?
|
||||||
s.state_current = s.state_initial
|
|
||||||
s.save!
|
s.state_current = s.state_initial
|
||||||
end
|
s.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.set_setting(name, value)
|
def self.set_setting(name, value)
|
||||||
s = Setting.find_by(name: name)
|
s = Setting.find_by(name: name)
|
||||||
if !s.nil?
|
return if s.nil?
|
||||||
s.state_current = { "value" => value }
|
|
||||||
s.save!
|
s.state_current = { 'value' => value }
|
||||||
end
|
s.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.up
|
def self.up
|
||||||
["ui_send_client_stats", "geo_ip_backend", "geo_location_backend", "image_backend", "geo_calendar_backend"].each { |n|
|
%w[ui_send_client_stats geo_ip_backend geo_location_backend image_backend
|
||||||
self.set_setting(n, "")
|
geo_calendar_backend].each do |n|
|
||||||
}
|
set_setting(n, '')
|
||||||
|
end
|
||||||
|
|
||||||
# disable customer ticket creation
|
# disable customer ticket creation
|
||||||
self.set_setting("customer_ticket_create", false)
|
set_setting('customer_ticket_create', false)
|
||||||
|
|
||||||
# disable user account registration
|
# disable user account registration
|
||||||
self.set_setting("user_create_account", false)
|
set_setting('user_create_account', false)
|
||||||
|
|
||||||
# bump up min password length
|
# bump up min password length
|
||||||
self.set_setting("password_min_size", 10)
|
set_setting('password_min_size', 10)
|
||||||
|
|
||||||
# delete default zammad user
|
# delete default zammad user
|
||||||
nicole = User.find_by(email: "nicole.braun@zammad.org")
|
nicole = User.find_by(email: 'nicole.braun@zammad.org')
|
||||||
if !nicole.nil?
|
return if nicole.nil?
|
||||||
Ticket.where(customer: nicole).destroy_all
|
|
||||||
nicole.destroy
|
Ticket.where(customer: nicole).destroy_all
|
||||||
end
|
nicole.destroy
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.down
|
def self.down
|
||||||
["ui_send_client_stats", "geo_ip_backend", "geo_location_backend", "image_backend", "geo_calendar_backend"].each { |n|
|
%w[ui_send_client_stats geo_ip_backend geo_location_backend image_backend
|
||||||
self.restore_setting(n)
|
geo_calendar_backend].each do |n|
|
||||||
}
|
restore_setting(n)
|
||||||
["customer_ticket_create", "user_create_account", "password_min_size"].each { |n|
|
end
|
||||||
self.restore_setting(n)
|
%w[customer_ticket_create user_create_account password_min_size].each do |n|
|
||||||
}
|
restore_setting(n)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -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 = $('<select class="form-control"></select>')
|
||||||
|
for groupKey, groupMeta of groups
|
||||||
|
displayName = App.i18n.translateInline(groupMeta.name)
|
||||||
|
selection.closest('select').append("<optgroup label=\"#{displayName}\" class=\"js-#{groupKey}\"></optgroup>")
|
||||||
|
optgroup = selection.find("optgroup.js-#{groupKey}")
|
||||||
|
for elementKey, elementGroup of elements
|
||||||
|
elementGroup = @elementKeyGroup(elementKey)
|
||||||
|
if elementGroup is groupKey
|
||||||
|
attributeConfig = elements[elementKey]
|
||||||
|
displayName = App.i18n.translateInline(attributeConfig.display)
|
||||||
|
|
||||||
|
selected = ''
|
||||||
|
if elementKey is selectedValue
|
||||||
|
selected = 'selected="selected"'
|
||||||
|
optgroup.append("<option value=\"#{elementKey}\" #{selected}>#{displayName}</option>")
|
||||||
|
selection
|
||||||
|
|
||||||
|
# disable - if we only have one attribute
|
||||||
|
@disableRemoveForOneAttribute: (elementFull) ->
|
||||||
|
if elementFull.find('.js-attributeSelector select').length > 1
|
||||||
|
elementFull.find('.js-remove').removeClass('is-disabled')
|
||||||
|
else
|
||||||
|
elementFull.find('.js-remove').addClass('is-disabled')
|
||||||
|
|
||||||
|
@updateAttributeSelectors: (elementFull) ->
|
||||||
|
|
||||||
|
# enable all
|
||||||
|
elementFull.find('.js-attributeSelector select option').prop('disabled', false)
|
||||||
|
|
||||||
|
# disable all used attributes
|
||||||
|
elementFull.find('.js-attributeSelector select').each(->
|
||||||
|
keyLocal = $(@).val()
|
||||||
|
elementFull.find('.js-attributeSelector select option[value="' + keyLocal + '"]').attr('disabled', true)
|
||||||
|
)
|
||||||
|
|
||||||
|
# disable - if we only have one attribute
|
||||||
|
@disableRemoveForOneAttribute(elementFull)
|
||||||
|
|
||||||
|
@rebuildAttributeSelectors: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||||
|
|
||||||
|
# set attribute
|
||||||
|
if groupAndAttribute
|
||||||
|
elementRow.find('.js-attributeSelector select').val(groupAndAttribute)
|
||||||
|
|
||||||
|
notificationTypeMatch = groupAndAttribute.match(/^notification.([\w]+)$/)
|
||||||
|
articleTypeMatch = groupAndAttribute.match(/^article.([\w]+)$/)
|
||||||
|
|
||||||
|
if _.isArray(notificationTypeMatch) && notificationType = notificationTypeMatch[1]
|
||||||
|
elementRow.find('.js-setAttribute').html('').addClass('hide')
|
||||||
|
elementRow.find('.js-setArticle').html('').addClass('hide')
|
||||||
|
@buildNotificationArea(notificationType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||||
|
else if !attribute.article_body_cc_only && _.isArray(articleTypeMatch) && articleType = articleTypeMatch[1]
|
||||||
|
elementRow.find('.js-setAttribute').html('').addClass('hide')
|
||||||
|
elementRow.find('.js-setNotification').html('').addClass('hide')
|
||||||
|
@buildArticleArea(articleType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||||
|
else
|
||||||
|
elementRow.find('.js-setNotification').html('').addClass('hide')
|
||||||
|
elementRow.find('.js-setArticle').html('').addClass('hide')
|
||||||
|
if !elementRow.find('.js-setAttribute div').get(0)
|
||||||
|
attributeSelectorElement = $( App.view('generic/ticket_perform_action/attribute_selector')(
|
||||||
|
attribute: attribute
|
||||||
|
name: name
|
||||||
|
meta: meta || {}
|
||||||
|
))
|
||||||
|
elementRow.find('.js-setAttribute').html(attributeSelectorElement).removeClass('hide')
|
||||||
|
|
||||||
|
if attribute.simple_attribute_selector
|
||||||
|
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||||
|
else
|
||||||
|
@buildOperator(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||||
|
|
||||||
|
@buildOperator: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||||
|
currentOperator = elementRow.find('.js-operator option:selected').attr('value')
|
||||||
|
|
||||||
|
if !meta.operator
|
||||||
|
meta.operator = currentOperator
|
||||||
|
|
||||||
|
name = "#{attribute.name}::#{groupAndAttribute}::operator"
|
||||||
|
|
||||||
|
selection = $("<select class=\"form-control\" name=\"#{name}\"></select>")
|
||||||
|
attributeConfig = elements[groupAndAttribute]
|
||||||
|
if !attributeConfig || !attributeConfig.operator
|
||||||
|
elementRow.find('.js-operator').parent().addClass('hide')
|
||||||
|
else
|
||||||
|
elementRow.find('.js-operator').parent().removeClass('hide')
|
||||||
|
if attributeConfig && attributeConfig.operator
|
||||||
|
for operator in attributeConfig.operator
|
||||||
|
operatorName = App.i18n.translateInline(operator)
|
||||||
|
selected = ''
|
||||||
|
if meta.operator is operator
|
||||||
|
selected = 'selected="selected"'
|
||||||
|
selection.append("<option value=\"#{operator}\" #{selected}>#{operatorName}</option>")
|
||||||
|
selection
|
||||||
|
|
||||||
|
elementRow.find('.js-operator select').replaceWith(selection)
|
||||||
|
|
||||||
|
@buildPreCondition(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||||
|
|
||||||
|
@buildPreCondition: (elementFull, elementRow, groupAndAttribute, elements, meta, attributeConfig) ->
|
||||||
|
currentOperator = elementRow.find('.js-operator option:selected').attr('value')
|
||||||
|
currentPreCondition = elementRow.find('.js-preCondition option:selected').attr('value')
|
||||||
|
|
||||||
|
if !meta.pre_condition
|
||||||
|
meta.pre_condition = currentPreCondition
|
||||||
|
|
||||||
|
toggleValue = =>
|
||||||
|
preCondition = elementRow.find('.js-preCondition option:selected').attr('value')
|
||||||
|
if preCondition isnt 'specific'
|
||||||
|
elementRow.find('.js-value select').html('')
|
||||||
|
elementRow.find('.js-value').addClass('hide')
|
||||||
|
else
|
||||||
|
elementRow.find('.js-value').removeClass('hide')
|
||||||
|
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||||
|
|
||||||
|
# force to use auto complition on user lookup
|
||||||
|
attribute = clone(attributeConfig, true)
|
||||||
|
|
||||||
|
name = "#{attribute.name}::#{groupAndAttribute}::value"
|
||||||
|
attributeSelected = elements[groupAndAttribute]
|
||||||
|
|
||||||
|
preCondition = false
|
||||||
|
if attributeSelected?.relation is 'User'
|
||||||
|
preCondition = 'user'
|
||||||
|
attribute.tag = 'user_autocompletion'
|
||||||
|
if attributeSelected?.relation is 'Organization'
|
||||||
|
preCondition = 'org'
|
||||||
|
attribute.tag = 'autocompletion_ajax'
|
||||||
|
if !preCondition || attribute.user_action is false
|
||||||
|
elementRow.find('.js-preCondition select').html('')
|
||||||
|
elementRow.find('.js-preCondition').closest('.controls').addClass('hide')
|
||||||
|
toggleValue()
|
||||||
|
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||||
|
return
|
||||||
|
|
||||||
|
elementRow.find('.js-preCondition').closest('.controls').removeClass('hide')
|
||||||
|
name = "#{attribute.name}::#{groupAndAttribute}::pre_condition"
|
||||||
|
|
||||||
|
selection = $("<select class=\"form-control\" name=\"#{name}\" ></select>")
|
||||||
|
options = {}
|
||||||
|
if preCondition is 'user'
|
||||||
|
options =
|
||||||
|
'current_user.id': App.i18n.translateInline('current user')
|
||||||
|
'specific': App.i18n.translateInline('specific user')
|
||||||
|
|
||||||
|
if attributeSelected.null is true
|
||||||
|
options['not_set'] = App.i18n.translateInline('unassign user')
|
||||||
|
|
||||||
|
else if preCondition is 'org'
|
||||||
|
options =
|
||||||
|
'current_user.organization_id': App.i18n.translateInline('current user organization')
|
||||||
|
'specific': App.i18n.translateInline('specific organization')
|
||||||
|
|
||||||
|
for key, value of options
|
||||||
|
selected = ''
|
||||||
|
if key is meta.pre_condition
|
||||||
|
selected = 'selected="selected"'
|
||||||
|
selection.append("<option value=\"#{key}\" #{selected}>#{App.i18n.translateInline(value)}</option>")
|
||||||
|
elementRow.find('.js-preCondition').closest('.controls').removeClass('hide')
|
||||||
|
elementRow.find('.js-preCondition select').replaceWith(selection)
|
||||||
|
|
||||||
|
elementRow.find('.js-preCondition select').on('change', (e) ->
|
||||||
|
toggleValue()
|
||||||
|
)
|
||||||
|
|
||||||
|
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||||
|
toggleValue()
|
||||||
|
|
||||||
|
@buildValue: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||||
|
name = "#{attribute.name}::#{groupAndAttribute}::value"
|
||||||
|
|
||||||
|
# build new item
|
||||||
|
attributeConfig = elements[groupAndAttribute]
|
||||||
|
config = clone(attributeConfig, true)
|
||||||
|
|
||||||
|
if config?.relation is 'User'
|
||||||
|
config.tag = 'user_autocompletion'
|
||||||
|
config.disableCreateObject = true
|
||||||
|
if config?.relation is 'Organization'
|
||||||
|
config.tag = 'autocompletion_ajax'
|
||||||
|
|
||||||
|
# render ui element
|
||||||
|
item = ''
|
||||||
|
if config && App.UiElement[config.tag]
|
||||||
|
config['name'] = name
|
||||||
|
if attribute.value && attribute.value[groupAndAttribute]
|
||||||
|
config['value'] = _.clone(attribute.value[groupAndAttribute]['value'])
|
||||||
|
config.multiple = false
|
||||||
|
config.default = undefined
|
||||||
|
config.nulloption = config.null
|
||||||
|
if config.tag is 'multiselect' || config.tag is 'multi_tree_select'
|
||||||
|
config.multiple = true
|
||||||
|
if config.tag is 'checkbox'
|
||||||
|
config.tag = 'select'
|
||||||
|
if config.tag is 'datetime'
|
||||||
|
config.validationContainer = 'self'
|
||||||
|
item = App.UiElement[config.tag].render(config, {})
|
||||||
|
|
||||||
|
relative_operators = [
|
||||||
|
__('before (relative)'),
|
||||||
|
__('within next (relative)'),
|
||||||
|
__('within last (relative)'),
|
||||||
|
__('after (relative)'),
|
||||||
|
__('till (relative)'),
|
||||||
|
__('from (relative)'),
|
||||||
|
__('relative'),
|
||||||
|
]
|
||||||
|
|
||||||
|
upcoming_operator = meta?.operator
|
||||||
|
|
||||||
|
if !_.include(config?.operator, upcoming_operator)
|
||||||
|
if Array.isArray(config?.operator)
|
||||||
|
upcoming_operator = config.operator[0]
|
||||||
|
else
|
||||||
|
upcoming_operator = null
|
||||||
|
|
||||||
|
if _.include(relative_operators, upcoming_operator)
|
||||||
|
config['name'] = "#{attribute.name}::#{groupAndAttribute}"
|
||||||
|
if attribute.value && attribute.value[groupAndAttribute]
|
||||||
|
config['value'] = _.clone(attribute.value[groupAndAttribute])
|
||||||
|
item = App.UiElement['time_range'].render(config, {})
|
||||||
|
|
||||||
|
elementRow.find('.js-setAttribute > .flex > .js-value').removeClass('hide').html(item)
|
||||||
|
|
||||||
|
@buildNotificationArea: (notificationType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||||
|
|
||||||
|
return if elementRow.find(".js-setNotification .js-body-#{notificationType}").get(0)
|
||||||
|
|
||||||
|
elementRow.find('.js-setNotification').empty()
|
||||||
|
|
||||||
|
options =
|
||||||
|
'article_last_sender': __('Sender of last article')
|
||||||
|
'ticket_owner': __('Owner')
|
||||||
|
'ticket_customer': __('Customer')
|
||||||
|
'ticket_agents': __('All agents')
|
||||||
|
|
||||||
|
name = "#{attribute.name}::notification.#{notificationType}"
|
||||||
|
|
||||||
|
messageLength = switch notificationType
|
||||||
|
when 'sms' then 160
|
||||||
|
else 200000
|
||||||
|
|
||||||
|
# meta.recipient was a string in the past (single-select) so we convert it to array if needed
|
||||||
|
if !_.isArray(meta.recipient)
|
||||||
|
meta.recipient = [meta.recipient]
|
||||||
|
|
||||||
|
columnSelectOptions = []
|
||||||
|
for key, value of options
|
||||||
|
selected = undefined
|
||||||
|
for recipient in meta.recipient
|
||||||
|
if key is recipient
|
||||||
|
selected = true
|
||||||
|
columnSelectOptions.push({ value: key, name: App.i18n.translatePlain(value), selected: selected })
|
||||||
|
|
||||||
|
columnSelectRecipientUserOptions = []
|
||||||
|
for user in App.User.all()
|
||||||
|
key = "userid_#{user.id}"
|
||||||
|
selected = undefined
|
||||||
|
for recipient in meta.recipient
|
||||||
|
if key is recipient
|
||||||
|
selected = true
|
||||||
|
columnSelectRecipientUserOptions.push({ value: key, name: "#{user.firstname} #{user.lastname}", selected: selected })
|
||||||
|
|
||||||
|
columnSelectRecipient = new App.ColumnSelect
|
||||||
|
attribute:
|
||||||
|
name: "#{name}::recipient"
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: __('Variables'),
|
||||||
|
group: columnSelectOptions
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('User'),
|
||||||
|
group: columnSelectRecipientUserOptions
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
selectionRecipient = columnSelectRecipient.element()
|
||||||
|
|
||||||
|
if notificationType is 'webhook'
|
||||||
|
notificationElement = $( App.view('generic/ticket_perform_action/webhook')(
|
||||||
|
attribute: attribute
|
||||||
|
name: name
|
||||||
|
notificationType: notificationType
|
||||||
|
meta: meta || {}
|
||||||
|
))
|
||||||
|
|
||||||
|
notificationElement.find('.js-recipient select').replaceWith(selectionRecipient)
|
||||||
|
|
||||||
|
|
||||||
|
if App.Webhook.search(filter: { active: true }).length isnt 0 || !_.isEmpty(meta.webhook_id)
|
||||||
|
webhookSelection = App.UiElement.select.render(
|
||||||
|
name: "#{name}::webhook_id"
|
||||||
|
multiple: false
|
||||||
|
null: false
|
||||||
|
relation: 'Webhook'
|
||||||
|
value: meta.webhook_id
|
||||||
|
translate: false
|
||||||
|
nulloption: true
|
||||||
|
)
|
||||||
|
else
|
||||||
|
webhookSelection = App.view('generic/ticket_perform_action/webhook_not_available')( attribute: attribute )
|
||||||
|
|
||||||
|
notificationElement.find('.js-webhooks').html(webhookSelection)
|
||||||
|
|
||||||
|
else
|
||||||
|
notificationElement = $( App.view('generic/ticket_perform_action/notification')(
|
||||||
|
attribute: attribute
|
||||||
|
name: name
|
||||||
|
notificationType: notificationType
|
||||||
|
meta: meta || {}
|
||||||
|
))
|
||||||
|
|
||||||
|
notificationElement.find('.js-recipient select').replaceWith(selectionRecipient)
|
||||||
|
|
||||||
|
visibilitySelection = App.UiElement.select.render(
|
||||||
|
name: "#{name}::internal"
|
||||||
|
multiple: false
|
||||||
|
null: false
|
||||||
|
options: { true: __('internal'), false: __('public') }
|
||||||
|
value: meta.internal || 'false'
|
||||||
|
translate: true
|
||||||
|
)
|
||||||
|
|
||||||
|
includeAttachmentsCheckbox = App.UiElement.select.render(
|
||||||
|
name: "#{name}::include_attachments"
|
||||||
|
multiple: false
|
||||||
|
null: false
|
||||||
|
options: { true: __('Yes'), false: __('No') }
|
||||||
|
value: meta.include_attachments || 'false'
|
||||||
|
translate: true
|
||||||
|
)
|
||||||
|
|
||||||
|
notificationElement.find('.js-internal').html(visibilitySelection)
|
||||||
|
notificationElement.find('.js-include_attachments').html(includeAttachmentsCheckbox)
|
||||||
|
|
||||||
|
notificationElement.find('.js-body div[contenteditable="true"]').ce(
|
||||||
|
mode: 'richtext'
|
||||||
|
placeholder: __('message')
|
||||||
|
maxlength: messageLength
|
||||||
|
)
|
||||||
|
new App.WidgetPlaceholder(
|
||||||
|
el: notificationElement.find('.js-body div[contenteditable="true"]').parent()
|
||||||
|
objects: [
|
||||||
|
{
|
||||||
|
prefix: 'ticket'
|
||||||
|
object: 'Ticket'
|
||||||
|
display: __('Ticket')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prefix: 'article'
|
||||||
|
object: 'TicketArticle'
|
||||||
|
display: __('Article')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prefix: 'user'
|
||||||
|
object: 'User'
|
||||||
|
display: __('Current User')
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
elementRow.find('.js-setNotification').html(notificationElement).removeClass('hide')
|
||||||
|
|
||||||
|
if App.Config.get('smime_integration') == true || App.Config.get('pgp_integration') == true
|
||||||
|
selection = App.UiElement.select.render(
|
||||||
|
name: "#{name}::sign"
|
||||||
|
multiple: false
|
||||||
|
options: {
|
||||||
|
'no': __('Do not sign email')
|
||||||
|
'discard': __('Sign email (if not possible, discard notification)')
|
||||||
|
'always': __('Sign email (if not possible, send notification anyway)')
|
||||||
|
}
|
||||||
|
value: meta.sign
|
||||||
|
translate: true
|
||||||
|
)
|
||||||
|
|
||||||
|
elementRow.find('.js-sign').html(selection)
|
||||||
|
|
||||||
|
selection = App.UiElement.select.render(
|
||||||
|
name: "#{name}::encryption"
|
||||||
|
multiple: false
|
||||||
|
options: {
|
||||||
|
'no': __('Do not encrypt email')
|
||||||
|
'discard': __('Encrypt email (if not possible, discard notification)')
|
||||||
|
'always': __('Encrypt email (if not possible, send notification anyway)')
|
||||||
|
}
|
||||||
|
value: meta.encryption
|
||||||
|
translate: true
|
||||||
|
)
|
||||||
|
|
||||||
|
elementRow.find('.js-encryption').html(selection)
|
||||||
|
|
||||||
|
@buildArticleArea: (articleType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||||
|
|
||||||
|
return if elementRow.find(".js-setArticle .js-body-#{articleType}").get(0)
|
||||||
|
|
||||||
|
elementRow.find('.js-setArticle').empty()
|
||||||
|
|
||||||
|
name = "#{attribute.name}::article.#{articleType}"
|
||||||
|
selection = App.UiElement.select.render(
|
||||||
|
name: "#{name}::internal"
|
||||||
|
multiple: false
|
||||||
|
null: false
|
||||||
|
label: __('Visibility')
|
||||||
|
options: { true: 'internal', false: 'public' }
|
||||||
|
value: meta.internal
|
||||||
|
translate: true
|
||||||
|
)
|
||||||
|
articleElement = $( App.view('generic/ticket_perform_action/article')(
|
||||||
|
attribute: attribute
|
||||||
|
name: name
|
||||||
|
articleType: articleType
|
||||||
|
meta: meta || {}
|
||||||
|
))
|
||||||
|
articleElement.find('.js-internal').html(selection)
|
||||||
|
articleElement.find('.js-body div[contenteditable="true"]').ce(
|
||||||
|
mode: 'richtext'
|
||||||
|
placeholder: __('message')
|
||||||
|
maxlength: 200000
|
||||||
|
)
|
||||||
|
new App.WidgetPlaceholder(
|
||||||
|
el: articleElement.find('.js-body div[contenteditable="true"]').parent()
|
||||||
|
objects: [
|
||||||
|
{
|
||||||
|
prefix: 'ticket'
|
||||||
|
object: 'Ticket'
|
||||||
|
display: __('Ticket')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prefix: 'article'
|
||||||
|
object: 'TicketArticle'
|
||||||
|
display: __('Article')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prefix: 'user'
|
||||||
|
object: 'User'
|
||||||
|
display: __('Current User')
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
elementRow.find('.js-setArticle').html(articleElement).removeClass('hide')
|
||||||
|
|
@ -1,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
|
class Ticket < ApplicationModel
|
||||||
include CanBeImported
|
include CanBeImported
|
||||||
include HasActivityStreamLog
|
include HasActivityStreamLog
|
||||||
include ChecksClientNotification
|
include ChecksClientNotification
|
||||||
include ChecksLatestChangeObserved
|
|
||||||
include CanCsvImport
|
include CanCsvImport
|
||||||
include ChecksHtmlSanitized
|
include ChecksHtmlSanitized
|
||||||
include HasHistory
|
include HasHistory
|
||||||
include HasTags
|
include HasTags
|
||||||
include HasSearchIndexBackend
|
include HasSearchIndexBackend
|
||||||
include HasOnlineNotifications
|
include HasOnlineNotifications
|
||||||
include HasKarmaActivityLog
|
|
||||||
include HasLinks
|
include HasLinks
|
||||||
include HasObjectManagerAttributesValidation
|
include HasObjectManagerAttributes
|
||||||
include HasTaskbars
|
include HasTaskbars
|
||||||
include Ticket::CallsStatsTicketReopenLog
|
include Ticket::CallsStatsTicketReopenLog
|
||||||
include Ticket::EnqueuesUserTicketCounterJob
|
include Ticket::EnqueuesUserTicketCounterJob
|
||||||
|
|
@ -21,6 +19,8 @@ class Ticket < ApplicationModel
|
||||||
include Ticket::SetsCloseTime
|
include Ticket::SetsCloseTime
|
||||||
include Ticket::SetsOnlineNotificationSeen
|
include Ticket::SetsOnlineNotificationSeen
|
||||||
include Ticket::TouchesAssociations
|
include Ticket::TouchesAssociations
|
||||||
|
include Ticket::TriggersSubscriptions
|
||||||
|
include Ticket::ChecksReopenAfterCertainTime
|
||||||
|
|
||||||
include ::Ticket::Escalation
|
include ::Ticket::Escalation
|
||||||
include ::Ticket::Subject
|
include ::Ticket::Subject
|
||||||
|
|
@ -38,10 +38,15 @@ class Ticket < ApplicationModel
|
||||||
|
|
||||||
include HasTransactionDispatcher
|
include HasTransactionDispatcher
|
||||||
|
|
||||||
|
# workflow checks should run after before_create and before_update callbacks
|
||||||
|
include ChecksCoreWorkflow
|
||||||
|
|
||||||
validates :group_id, presence: true
|
validates :group_id, presence: true
|
||||||
|
|
||||||
activity_stream_permission 'ticket.agent'
|
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
|
activity_stream_attributes_ignored :organization_id, # organization_id will change automatically on user update
|
||||||
:create_article_type_id,
|
:create_article_type_id,
|
||||||
:create_article_sender_id,
|
:create_article_sender_id,
|
||||||
|
|
@ -57,19 +62,26 @@ class Ticket < ApplicationModel
|
||||||
:update_escalation_at,
|
:update_escalation_at,
|
||||||
:update_in_min,
|
:update_in_min,
|
||||||
:update_diff_in_min,
|
:update_diff_in_min,
|
||||||
|
:last_close_at,
|
||||||
:last_contact_at,
|
:last_contact_at,
|
||||||
:last_contact_agent_at,
|
:last_contact_agent_at,
|
||||||
:last_contact_customer_at,
|
:last_contact_customer_at,
|
||||||
:last_owner_update_at,
|
:last_owner_update_at,
|
||||||
:preferences
|
:preferences
|
||||||
|
|
||||||
|
search_index_attributes_relevant :organization_id,
|
||||||
|
:group_id,
|
||||||
|
:state_id,
|
||||||
|
:priority_id
|
||||||
|
|
||||||
history_attributes_ignored :create_article_type_id,
|
history_attributes_ignored :create_article_type_id,
|
||||||
:create_article_sender_id,
|
:create_article_sender_id,
|
||||||
:article_count,
|
:article_count,
|
||||||
:preferences
|
:preferences
|
||||||
|
|
||||||
history_relation_object 'Ticket::Article', 'Mention'
|
history_relation_object 'Ticket::Article', 'Mention', 'Ticket::SharedDraftZoom'
|
||||||
|
|
||||||
|
validates :note, length: { maximum: 250 }
|
||||||
sanitized_html :note
|
sanitized_html :note
|
||||||
|
|
||||||
belongs_to :group, optional: true
|
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 :ticket_time_accounting, class_name: 'Ticket::TimeAccounting', dependent: :destroy, inverse_of: :ticket
|
||||||
has_many :flags, class_name: 'Ticket::Flag', dependent: :destroy
|
has_many :flags, class_name: 'Ticket::Flag', dependent: :destroy
|
||||||
has_many :mentions, as: :mentionable, 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 :state, class_name: 'Ticket::State', optional: true
|
||||||
belongs_to :priority, class_name: 'Ticket::Priority', optional: true
|
belongs_to :priority, class_name: 'Ticket::Priority', optional: true
|
||||||
belongs_to :owner, class_name: 'User', optional: true
|
belongs_to :owner, class_name: 'User', optional: true
|
||||||
|
|
@ -93,43 +106,6 @@ class Ticket < ApplicationModel
|
||||||
|
|
||||||
=begin
|
=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
|
processes tickets which have reached their pending time and sets next state_id
|
||||||
|
|
||||||
processed_tickets = Ticket.process_pending
|
processed_tickets = Ticket.process_pending
|
||||||
|
|
@ -204,6 +180,31 @@ returns
|
||||||
result
|
result
|
||||||
end
|
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
|
=begin
|
||||||
|
|
||||||
processes escalated tickets
|
processes escalated tickets
|
||||||
|
|
@ -220,7 +221,7 @@ returns
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
# fetch all escalated and soon to be escalating tickets
|
# 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_id = nil
|
||||||
article = Ticket::Article.last_customer_agent_article(ticket.id)
|
article = Ticket::Article.last_customer_agent_article(ticket.id)
|
||||||
|
|
@ -241,7 +242,7 @@ returns
|
||||||
next
|
next
|
||||||
end
|
end
|
||||||
|
|
||||||
# check if warning need to be sent
|
# check if warning needs to be sent
|
||||||
TransactionJob.perform_now(
|
TransactionJob.perform_now(
|
||||||
object: 'Ticket',
|
object: 'Ticket',
|
||||||
type: 'escalation_warning',
|
type: 'escalation_warning',
|
||||||
|
|
@ -321,10 +322,10 @@ returns
|
||||||
# prevent cross merging tickets
|
# prevent cross merging tickets
|
||||||
target_ticket = Ticket.find_by(id: data[:ticket_id])
|
target_ticket = Ticket.find_by(id: data[:ticket_id])
|
||||||
raise 'no target ticket given' if !target_ticket
|
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
|
# 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
|
# update articles
|
||||||
Transaction.execute context: 'merge' do
|
Transaction.execute context: 'merge' do
|
||||||
|
|
@ -413,6 +414,26 @@ returns
|
||||||
|
|
||||||
# touch new ticket (to broadcast change)
|
# touch new ticket (to broadcast change)
|
||||||
target_ticket.touch # rubocop:disable Rails/SkipsModelValidations
|
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
|
end
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
@ -489,7 +510,7 @@ get count of tickets and tickets which match on selector
|
||||||
access = options[:access] || 'full'
|
access = options[:access] || 'full'
|
||||||
raise 'no selectors given' if !selectors
|
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
|
return [] if !query
|
||||||
|
|
||||||
ActiveRecord::Base.transaction(requires_new: true) do
|
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'
|
if !current_user || access == 'ignore'
|
||||||
ticket_count = Ticket.distinct.where(query, *bind_params).joins(tables).count
|
ticket_count = Ticket.distinct.where(query, *bind_params).joins(tables).count
|
||||||
tickets = Ticket.distinct.where(query, *bind_params).joins(tables).limit(limit)
|
tickets = Ticket.distinct.where(query, *bind_params).joins(tables).limit(limit)
|
||||||
return [ticket_count, tickets]
|
next [ticket_count, tickets]
|
||||||
end
|
end
|
||||||
|
|
||||||
access_condition = Ticket.access_condition(current_user, access)
|
tickets = "TicketPolicy::#{access.camelize}Scope".constantize
|
||||||
ticket_count = Ticket.distinct.where(access_condition).where(query, *bind_params).joins(tables).count
|
.new(current_user).resolve
|
||||||
tickets = Ticket.distinct.where(access_condition).where(query, *bind_params).joins(tables).limit(limit)
|
.distinct
|
||||||
|
.where(query, *bind_params)
|
||||||
|
.joins(tables)
|
||||||
|
|
||||||
return [ticket_count, tickets]
|
next [tickets.count, tickets.limit(limit)]
|
||||||
rescue ActiveRecord::StatementInvalid => e
|
rescue ActiveRecord::StatementInvalid => e
|
||||||
Rails.logger.error e
|
Rails.logger.error e
|
||||||
raise ActiveRecord::Rollback
|
raise ActiveRecord::Rollback
|
||||||
|
|
||||||
end
|
end
|
||||||
[]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
=begin
|
=begin
|
||||||
|
|
@ -561,419 +582,7 @@ condition example
|
||||||
=end
|
=end
|
||||||
|
|
||||||
def self.selector2sql(selectors, options = {})
|
def self.selector2sql(selectors, options = {})
|
||||||
current_user = options[:current_user]
|
Ticket::Selector::Sql.new(selector: selectors, options: options).get
|
||||||
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]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
=begin
|
=begin
|
||||||
|
|
@ -1011,9 +620,10 @@ perform changes on ticket
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
objects = build_notification_template_objects(article)
|
||||||
perform_notification = {}
|
perform_notification = {}
|
||||||
perform_article = {}
|
perform_article = {}
|
||||||
changed = false
|
changed = false
|
||||||
perform.each do |key, value|
|
perform.each do |key, value|
|
||||||
(object_name, attribute) = key.split('.', 2)
|
(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'
|
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'
|
when 'static'
|
||||||
value['value']
|
value['value']
|
||||||
when 'relative'
|
when 'relative'
|
||||||
pendtil = Time.zone.now
|
TimeRangeHelper.relative(range: value['range'], value: value['value'])
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if new_value
|
if new_value
|
||||||
|
|
@ -1095,7 +689,7 @@ perform changes on ticket
|
||||||
if value['pre_condition'].start_with?('not_set')
|
if value['pre_condition'].start_with?('not_set')
|
||||||
value['value'] = 1
|
value['value'] = 1
|
||||||
elsif value['pre_condition'].start_with?('current_user.')
|
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
|
value['value'] = current_user_id
|
||||||
end
|
end
|
||||||
|
|
@ -1106,6 +700,14 @@ perform changes on ticket
|
||||||
|
|
||||||
changed = true
|
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']
|
self[attribute] = value['value']
|
||||||
logger.debug { "set #{object_name}.#{attribute} = #{value['value'].inspect} for ticket_id #{id}" }
|
logger.debug { "set #{object_name}.#{attribute} = #{value['value'].inspect} for ticket_id #{id}" }
|
||||||
end
|
end
|
||||||
|
|
@ -1114,10 +716,8 @@ perform changes on ticket
|
||||||
save!
|
save!
|
||||||
end
|
end
|
||||||
|
|
||||||
objects = build_notification_template_objects(article)
|
|
||||||
|
|
||||||
perform_article.each do |key, value|
|
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)
|
add_trigger_note(id, value, objects, perform_origin)
|
||||||
end
|
end
|
||||||
|
|
@ -1209,7 +809,7 @@ perform active triggers on ticket
|
||||||
else
|
else
|
||||||
::Trigger.where(active: true).order(:name)
|
::Trigger.where(active: true).order(:name)
|
||||||
end
|
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
|
# check if notification should be send because of customer emails
|
||||||
send_notification = true
|
send_notification = true
|
||||||
|
|
@ -1226,95 +826,6 @@ perform active triggers on ticket
|
||||||
triggers.each do |trigger|
|
triggers.each do |trigger|
|
||||||
logger.debug { "Probe trigger (#{trigger.name}/#{trigger.id}) for this object (Ticket:#{ticket.id}/Loop:#{local_options[:loop_count]})" }
|
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
|
user_id = ticket.updated_by_id
|
||||||
if article
|
if article
|
||||||
user_id = article.updated_by_id
|
user_id = article.updated_by_id
|
||||||
|
|
@ -1323,7 +834,7 @@ perform active triggers on ticket
|
||||||
user = User.lookup(id: user_id)
|
user = User.lookup(id: user_id)
|
||||||
|
|
||||||
# verify is condition is matching
|
# 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.blank?
|
||||||
next if ticket_count.zero?
|
next if ticket_count.zero?
|
||||||
|
|
@ -1455,7 +966,7 @@ result
|
||||||
|
|
||||||
customer = User.find_by(id: customer_id)
|
customer = User.find_by(id: customer_id)
|
||||||
return true if !customer
|
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
|
self.organization_id = customer.organization_id
|
||||||
true
|
true
|
||||||
|
|
@ -1523,9 +1034,30 @@ result
|
||||||
# if another email notification trigger preceded this one
|
# if another email notification trigger preceded this one
|
||||||
# (see https://github.com/zammad/zammad/issues/1543)
|
# (see https://github.com/zammad/zammad/issues/1543)
|
||||||
def build_notification_template_objects(article)
|
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,
|
ticket: self,
|
||||||
article: article || articles.last
|
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
|
end
|
||||||
|
|
||||||
|
|
@ -1587,7 +1119,7 @@ result
|
||||||
Mail::AddressList.new(recipient_email).addresses.each do |address|
|
Mail::AddressList.new(recipient_email).addresses.each do |address|
|
||||||
recipient_email = address.address
|
recipient_email = address.address
|
||||||
email_address_validation = EmailAddressValidation.new(recipient_email)
|
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
|
end
|
||||||
rescue
|
rescue
|
||||||
if recipient_email.present?
|
if recipient_email.present?
|
||||||
|
|
@ -1600,7 +1132,7 @@ result
|
||||||
end
|
end
|
||||||
|
|
||||||
email_address_validation = EmailAddressValidation.new(recipient_email)
|
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
|
# do not send notification if system address
|
||||||
next if EmailAddress.exists?(email: recipient_email.downcase)
|
next if EmailAddress.exists?(email: recipient_email.downcase)
|
||||||
|
|
@ -1700,7 +1232,7 @@ result
|
||||||
sign = value['sign'].present? && value['sign'] != 'no'
|
sign = value['sign'].present? && value['sign'] != 'no'
|
||||||
encryption = value['encryption'].present? && value['encryption'] != 'no'
|
encryption = value['encryption'].present? && value['encryption'] != 'no'
|
||||||
security = {
|
security = {
|
||||||
type: security_type,
|
type: security_type,
|
||||||
sign: {
|
sign: {
|
||||||
success: false,
|
success: false,
|
||||||
},
|
},
|
||||||
|
|
@ -1717,10 +1249,10 @@ result
|
||||||
else
|
else
|
||||||
cert = SMIMECertificate.for_sender_email_address(from)
|
cert = SMIMECertificate.for_sender_email_address(from)
|
||||||
end
|
end
|
||||||
|
|
||||||
begin
|
begin
|
||||||
list = Mail::AddressList.new(email_address.email)
|
list = Mail::AddressList.new(email_address.email)
|
||||||
from = list.addresses.first.to_s
|
from = list.addresses.first.to_s
|
||||||
|
|
||||||
if cert && !cert.expired?
|
if cert && !cert.expired?
|
||||||
sign_found = true
|
sign_found = true
|
||||||
security[:sign][:success] = true
|
security[:sign][:success] = true
|
||||||
|
|
@ -1795,7 +1327,7 @@ result
|
||||||
)
|
)
|
||||||
|
|
||||||
attachments_inline.each do |attachment|
|
attachments_inline.each do |attachment|
|
||||||
Store.add(
|
Store.create!(
|
||||||
object: 'Ticket::Article',
|
object: 'Ticket::Article',
|
||||||
o_id: message.id,
|
o_id: message.id,
|
||||||
data: attachment[:data],
|
data: attachment[:data],
|
||||||
|
|
@ -1805,6 +1337,11 @@ result
|
||||||
end
|
end
|
||||||
|
|
||||||
original_article = objects[:article]
|
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
|
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.clone_attachments('Ticket::Article', message.id, only_inline_attachments: true)
|
||||||
original_article.should_clone_inline_attachments = false # cancel the temporary flag after cloning
|
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]
|
||||||
return 0 if user.preferences[:mail_delivery_failed_data].blank?
|
return 0 if user.preferences[:mail_delivery_failed_data].blank?
|
||||||
|
|
||||||
# blocked for 60 full days
|
# blocked for 60 full days; see #4459
|
||||||
(user.preferences[:mail_delivery_failed_data].to_date - Time.zone.now.to_date).to_i + 61
|
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
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue