From 7185af5c2d2448437090e551565dd7ad8ec9acd3 Mon Sep 17 00:00:00 2001 From: Benoit LORAND Date: Thu, 28 Mar 2024 18:20:48 +0100 Subject: [PATCH] Maj for glpi 10.0.14 and php82 --- alpine/.gitignore => .gitignore | 1 + alpine/README.md => README.md | 1 + alpine/docker-compose.yml | 29 - alpine/web-builder/Dockerfile | 83 - alpine/web-builder/glpi.cron | 6 - .../web-builder/glpi_ticket.class.php.patch | 19 - alpine/web-builder/initrc | 6 - alpine/web-builder/service/30-apache2/run | 5 - alpine/web-builder/service/90-glpi_init/run | 6 - alpine/web-builder/ticket.class.php | 7331 ----------------- debian/CAS-1.3.8.tgz | Bin 98163 -> 0 bytes debian/Dockerfile | 50 - debian/README.md | 0 debian/docker-compose.yml | 26 - debian/glpi.cron | 5 - debian/glpi_init.sh | 79 - debian/mysql_settings.ini | 4 - debian/service/20-cron/run | 7 - debian/service/30-apache2/run | 5 - debian/service/90-glpi_init/run | 6 - debian/service/template | 19 - docker-compose.yml | 64 + .../web-builder => web-builder}/CAS-1.3.8.tgz | Bin web-builder/Dockerfile | 81 + web-builder/glpi.cron | 6 + web-builder/glpi_ticket.class.php.patch | 27 + .../web-builder => web-builder}/httpd.conf | 15 +- web-builder/remoteip.conf | 10 + .../20-cron/run => web-builder/service/cron | 3 + .../glpi_init.sh => web-builder/service/glpi | 58 +- web-builder/service/php | 43 + .../service/template | 0 32 files changed, 270 insertions(+), 7725 deletions(-) rename alpine/.gitignore => .gitignore (87%) rename alpine/README.md => README.md (84%) delete mode 100644 alpine/docker-compose.yml delete mode 100644 alpine/web-builder/Dockerfile delete mode 100644 alpine/web-builder/glpi.cron delete mode 100644 alpine/web-builder/glpi_ticket.class.php.patch delete mode 100644 alpine/web-builder/initrc delete mode 100644 alpine/web-builder/service/30-apache2/run delete mode 100644 alpine/web-builder/service/90-glpi_init/run delete mode 100644 alpine/web-builder/ticket.class.php delete mode 100644 debian/CAS-1.3.8.tgz delete mode 100644 debian/Dockerfile delete mode 100644 debian/README.md delete mode 100644 debian/docker-compose.yml delete mode 100644 debian/glpi.cron delete mode 100644 debian/glpi_init.sh delete mode 100644 debian/mysql_settings.ini delete mode 100755 debian/service/20-cron/run delete mode 100755 debian/service/30-apache2/run delete mode 100755 debian/service/90-glpi_init/run delete mode 100644 debian/service/template create mode 100644 docker-compose.yml rename {alpine/web-builder => web-builder}/CAS-1.3.8.tgz (100%) create mode 100644 web-builder/Dockerfile create mode 100644 web-builder/glpi.cron create mode 100644 web-builder/glpi_ticket.class.php.patch rename {alpine/web-builder => web-builder}/httpd.conf (98%) create mode 100644 web-builder/remoteip.conf rename alpine/web-builder/service/20-cron/run => web-builder/service/cron (75%) rename alpine/web-builder/glpi_init.sh => web-builder/service/glpi (50%) create mode 100644 web-builder/service/php rename {alpine/web-builder => web-builder}/service/template (100%) diff --git a/alpine/.gitignore b/.gitignore similarity index 87% rename from alpine/.gitignore rename to .gitignore index 77d7892..994c871 100644 --- a/alpine/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +cache/ db/ etc/ files/ diff --git a/alpine/README.md b/README.md similarity index 84% rename from alpine/README.md rename to README.md index 1d1c23f..4d31b10 100644 --- a/alpine/README.md +++ b/README.md @@ -1,6 +1,7 @@ Ajouter un fichier mysql_settings.ini qui contient ``` +MARIADB_AUTO_UPGRADE=1 MYSQL_DATABASE= MYSQL_USER= MYSQL_PASSWORD='' diff --git a/alpine/docker-compose.yml b/alpine/docker-compose.yml deleted file mode 100644 index 239be86..0000000 --- a/alpine/docker-compose.yml +++ /dev/null @@ -1,29 +0,0 @@ -version: '3.5' -services: - web: - container_name: glpi-web - build: web-builder - restart: always - volumes: - - ./etc/:/etc/glpi/ - - ./files/:/var/lib/glpi/ - - ./log/:/var/log/glpi/ - env_file: - - ./mysql_settings.ini - environment: - - PHP_MEMORY_LIMIT=256M - - PHP_UPLOAD_MAX_FILESIZE=10M - - PHP_POST_MAX_SIZE=20M - - PHP_DATE_TIMEZONE=Europe/Paris - depends_on: - - db - - db: - image: mariadb - container_name: glpi-db - command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci - restart: always - env_file: - - ./mysql_settings.ini - volumes: - - ./db/:/var/lib/mysql/ diff --git a/alpine/web-builder/Dockerfile b/alpine/web-builder/Dockerfile deleted file mode 100644 index fa9dc9f..0000000 --- a/alpine/web-builder/Dockerfile +++ /dev/null @@ -1,83 +0,0 @@ -FROM alpine -MAINTAINER Benoit LORAND - -WORKDIR /root -ENV GLPI_CONFIG_DIR=/etc/glpi -ENV GLPI_VAR_DIR=/var/lib/glpi -ENV GLPI_LOG_DIR=/var/log/glpi -ENV GLPI_VERSION=9.5.5 -ENV FUSIONINVENTORY_VERSION=9.5+3.0 -ENV FIELDS_VERSION=1.12.4 -ENV DATAINJECTION_VERSION=2.9.0 - -RUN \ -apk add --no-cache \ - runit \ - php7-apache2 \ - php7 \ - mariadb-client \ - php7-pecl-apcu \ - php7-mysqli \ - php7-gd \ - php7-intl \ - php7-ldap \ - php7-xmlrpc \ - php7-xml \ - php7-exif \ - php7-zip \ - php7-bz2 \ - php7-opcache \ - php7-pear \ - php7-curl \ - php7-dom \ - php7-pdo \ - php7-json \ - php7-session \ - php7-ctype \ - php7-fileinfo \ - php7-mbstring \ - php7-simplexml \ - php7-iconv \ - php7-sodium \ - php7-imap \ - php7-pdo \ - php7-pdo_mysql \ - php7-pspell \ - php7-phar \ - patch - -COPY CAS-1.3.8.tgz /root/ -RUN pear install /root/CAS-1.3.8.tgz && \ -pear install Archive_Tar -COPY httpd.conf /etc/apache2 -COPY service/ /etc/service/ -COPY glpi_init.sh /root/glpi_init.sh -COPY glpi.cron /var/spool/cron/crontabs/apache -COPY initrc /etc/ -COPY glpi_ticket.class.php.patch /root/glpi_ticket.class.php.patch -ADD https://github.com/glpi-project/glpi/releases/download/${GLPI_VERSION}/glpi-${GLPI_VERSION}.tgz /root/glpi-${GLPI_VERSION}.tgz -ADD https://github.com/fusioninventory/fusioninventory-for-glpi/releases/download/glpi${FUSIONINVENTORY_VERSION}/fusioninventory-${FUSIONINVENTORY_VERSION}.tar.bz2 /root/fusioninventory-${FUSIONINVENTORY_VERSION}.tar.bz2 -ADD https://github.com/pluginsGLPI/fields/releases/download/${FIELDS_VERSION}/glpi-fields-${FIELDS_VERSION}.tar.bz2 /root/glpi-fields-${FIELDS_VERSION}.tar.bz2 -ADD https://github.com/pluginsGLPI/datainjection/releases/download/${DATAINJECTION_VERSION}/glpi-datainjection-${DATAINJECTION_VERSION}.tar.bz2 /root/glpi-datainjection-${DATAINJECTION_VERSION}.tar.bz2 - -RUN \ -mkdir -p /root/glpi_template/etc /root/glpi_template/files && \ -tar x -f /root/glpi-${GLPI_VERSION}.tgz && \ -cp -r /root/glpi/config/. /root/glpi_template/etc/. && \ -cp -r /root/glpi/files/. /root/glpi_template/files/. && \ -rm -r /root/glpi/config /root/glpi/files && \ -mv /root/glpi /var/www/glpi && \ -cd /var/www/glpi && \ -patch -Np0 -i /root/glpi_ticket.class.php.patch && \ -cd /var/www/glpi/plugins && \ -tar x -f /root/fusioninventory-${FUSIONINVENTORY_VERSION}.tar.bz2 && \ -cd /var/www/glpi/marketplace && \ -tar x -f /root/glpi-fields-${FIELDS_VERSION}.tar.bz2 && \ -tar x -f /root/glpi-datainjection-${DATAINJECTION_VERSION}.tar.bz2 && \ -chmod a+x /root/glpi_init.sh /etc/initrc && \ -chmod 600 /etc/crontabs/apache && \ -rm -f /var/www/html/* /root/CAS-1.3.8.tgz /root/glpi-${GLPI_VERSION}.tgz /root/fusioninventory-${FUSIONINVENTORY_VERSION}.tar.bz2 /root/glpi-fields-${FIELDS_VERSION}.tar.bz2 /root/glpi-datainjection-${DATAINJECTION_VERSION}.tar.bz2 && \ -rm -f /root/glpi_ticket.class.php.patch && \ -rm -rf /tmp/* /var/tmp/* - -ENTRYPOINT ["/etc/initrc"] diff --git a/alpine/web-builder/glpi.cron b/alpine/web-builder/glpi.cron deleted file mode 100644 index c92ce0d..0000000 --- a/alpine/web-builder/glpi.cron +++ /dev/null @@ -1,6 +0,0 @@ -GLPI_CONFIG_DIR=/etc/glpi -GLPI_VAR_DIR=/var/lib/glpi -GLPI_LOG_DIR=/var/log/glpi - -*/1 * * * * /usr/bin/php7 /var/www/glpi/front/cron.php -0 * * * * cd /var/www/glpi && php bin/console glpi:ldap:synchronize_users -n diff --git a/alpine/web-builder/glpi_ticket.class.php.patch b/alpine/web-builder/glpi_ticket.class.php.patch deleted file mode 100644 index 632af8c..0000000 --- a/alpine/web-builder/glpi_ticket.class.php.patch +++ /dev/null @@ -1,19 +0,0 @@ ---- inc/ticket.class.php.old 2020-07-16 14:26:59.000000000 +0200 -+++ inc/ticket.class.php 2020-09-11 18:09:43.200657894 +0200 -@@ -3806,7 +3806,7 @@ - } - } - -- if (empty($delegating) -+/** if (empty($delegating) - && NotificationTargetTicket::isAuthorMailingActivatedForHelpdesk()) { - echo ""; - echo "".__('Inform me about the actions taken').""; -@@ -3821,6 +3821,7 @@ - - echo ""; - } -+*/ - if (($_SESSION["glpiactiveprofile"]["helpdesk_hardware"] != 0) - && (count($_SESSION["glpiactiveprofile"]["helpdesk_item_type"]))) { - if (!$tt->isHiddenField('items_id')) { diff --git a/alpine/web-builder/initrc b/alpine/web-builder/initrc deleted file mode 100644 index 2908475..0000000 --- a/alpine/web-builder/initrc +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -set -e - -rm -r /run/* -mkdir -p /run/apache2 -exec /sbin/runsvdir -P /etc/service diff --git a/alpine/web-builder/service/30-apache2/run b/alpine/web-builder/service/30-apache2/run deleted file mode 100644 index 1a1a0bb..0000000 --- a/alpine/web-builder/service/30-apache2/run +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -. /etc/service/template - -msglog green "Starting Apache..." -exec /usr/sbin/httpd -D FOREGROUND diff --git a/alpine/web-builder/service/90-glpi_init/run b/alpine/web-builder/service/90-glpi_init/run deleted file mode 100644 index 767ea4b..0000000 --- a/alpine/web-builder/service/90-glpi_init/run +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -. /etc/service/template - -msglog green "Starting glpi_init.sh..." -/root/glpi_init.sh -sleep infinity diff --git a/alpine/web-builder/ticket.class.php b/alpine/web-builder/ticket.class.php deleted file mode 100644 index 279aeb1..0000000 --- a/alpine/web-builder/ticket.class.php +++ /dev/null @@ -1,7331 +0,0 @@ -. - * --------------------------------------------------------------------- - */ - -use Glpi\Event; - -if (!defined('GLPI_ROOT')) { - die("Sorry. You can't access this file directly"); -} - -/** - * Ticket Class -**/ -class Ticket extends CommonITILObject { - - // From CommonDBTM - public $dohistory = true; - static protected $forward_entity_to = ['TicketValidation', 'TicketCost']; - - // From CommonITIL - public $userlinkclass = 'Ticket_User'; - public $grouplinkclass = 'Group_Ticket'; - public $supplierlinkclass = 'Supplier_Ticket'; - - static $rightname = 'ticket'; - - protected $userentity_oncreate = true; - - const MATRIX_FIELD = 'priority_matrix'; - const URGENCY_MASK_FIELD = 'urgency_mask'; - const IMPACT_MASK_FIELD = 'impact_mask'; - const STATUS_MATRIX_FIELD = 'ticket_status'; - - // HELPDESK LINK HARDWARE DEFINITION : CHECKSUM SYSTEM : BOTH=1*2^0+1*2^1=3 - const HELPDESK_MY_HARDWARE = 0; - const HELPDESK_ALL_HARDWARE = 1; - - // Specific ones - /// Hardware datas used by getFromDBwithData - public $hardwaredatas = []; - /// Is a hardware found in getHardwareData / getFromDBwithData : hardware link to the job - public $computerfound = 0; - - // Request type - const INCIDENT_TYPE = 1; - // Demand type - const DEMAND_TYPE = 2; - - const READMY = 1; - const READALL = 1024; - const READGROUP = 2048; - const READASSIGN = 4096; - const ASSIGN = 8192; - const STEAL = 16384; - const OWN = 32768; - const CHANGEPRIORITY = 65536; - const SURVEY = 131072; - - - function getForbiddenStandardMassiveAction() { - - $forbidden = parent::getForbiddenStandardMassiveAction(); - - if (!Session::haveRightsOr(self::$rightname, [DELETE, PURGE])) { - $forbidden[] = 'delete'; - $forbidden[] = 'purge'; - $forbidden[] = 'restore'; - } - - return $forbidden; - } - - - /** - * Name of the type - * - * @param $nb : number of item in the type (default 0) - **/ - static function getTypeName($nb = 0) { - return _n('Ticket', 'Tickets', $nb); - } - - - /** - * @see CommonGLPI::getMenuShorcut() - * - * @since 0.85 - **/ - static function getMenuShorcut() { - return 't'; - } - - - /** - * @see CommonGLPI::getAdditionalMenuContent() - * - * @since 0.85 - **/ - static function getAdditionalMenuContent() { - - if (static::canCreate()) { - $menu = [ - 'create_ticket' => [ - 'title' => __('Create ticket'), - 'page' => static::getFormURL(false), - 'icon' => 'fas fa-plus', - ], - ]; - return $menu; - } else { - return self::getAdditionalMenuOptions(); - } - } - - - /** - * @see CommonGLPI::getAdditionalMenuLinks() - * - * @since 0.85 - **/ - static function getAdditionalMenuLinks() { - global $CFG_GLPI; - - $links = parent::getAdditionalMenuLinks(); - if (Session::haveRightsOr('ticketvalidation', TicketValidation::getValidateRights())) { - $opt = []; - $opt['reset'] = 'reset'; - $opt['criteria'][0]['field'] = 55; // validation status - $opt['criteria'][0]['searchtype'] = 'equals'; - $opt['criteria'][0]['value'] = CommonITILValidation::WAITING; - $opt['criteria'][0]['link'] = 'AND'; - - $opt['criteria'][1]['field'] = 59; // validation aprobator - $opt['criteria'][1]['searchtype'] = 'equals'; - $opt['criteria'][1]['value'] = Session::getLoginUserID(); - $opt['criteria'][1]['link'] = 'AND'; - - $opt['criteria'][2]['field'] = 52; // global validation status - $opt['criteria'][2]['searchtype'] = 'equals'; - $opt['criteria'][2]['value'] = CommonITILValidation::WAITING; - $opt['criteria'][2]['link'] = 'AND'; - - $opt['criteria'][3]['field'] = 12; // ticket status - $opt['criteria'][3]['searchtype'] = 'equals'; - $opt['criteria'][3]['value'] = Ticket::CLOSED; - $opt['criteria'][3]['link'] = 'AND NOT'; - - $opt['criteria'][4]['field'] = 12; // ticket status - $opt['criteria'][4]['searchtype'] = 'equals'; - $opt['criteria'][4]['value'] = Ticket::SOLVED; - $opt['criteria'][4]['link'] = 'AND NOT'; - - $pic_validate = "\""."; - - $links[$pic_validate] = Ticket::getSearchURL(false) . '?'.Toolbox::append_params($opt, '&'); - } - - return $links; - } - - - function canAssign() { - if (isset($this->fields['is_deleted']) && ($this->fields['is_deleted'] == 1) - || isset($this->fields['status']) && in_array($this->fields['status'], $this->getClosedStatusArray()) - ) { - return false; - } - return Session::haveRight(static::$rightname, self::ASSIGN); - } - - - function canAssignToMe() { - - if (isset($this->fields['is_deleted']) && $this->fields['is_deleted'] == 1 - || isset($this->fields['status']) && in_array($this->fields['status'], $this->getClosedStatusArray()) - ) { - return false; - } - return (Session::haveRight(self::$rightname, self::STEAL) - || (Session::haveRight(self::$rightname, self::OWN) - && ($this->countUsers(CommonITILActor::ASSIGN) == 0))); - } - - - static function canUpdate() { - - // To allow update of urgency and category for post-only - if (Session::getCurrentInterface() == "helpdesk") { - return Session::haveRight(self::$rightname, CREATE); - } - - return Session::haveRightsOr(self::$rightname, - [UPDATE, - self::ASSIGN, - self::STEAL, - self::OWN, - self::CHANGEPRIORITY]); - } - - - static function canView() { - return (Session::haveRightsOr(self::$rightname, - [self::READALL, self::READMY, UPDATE, self::READASSIGN, - self::READGROUP, self::OWN]) - || Session::haveRightsOr('ticketvalidation', TicketValidation::getValidateRights())); - } - - - /** - * Is the current user have right to show the current ticket ? - * - * @return boolean - **/ - function canViewItem() { - - if (!Session::haveAccessToEntity($this->getEntityID())) { - return false; - } - return (Session::haveRight(self::$rightname, self::READALL) - || (Session::haveRight(self::$rightname, self::READMY) - && (($this->fields["users_id_recipient"] === Session::getLoginUserID()) - || $this->isUser(CommonITILActor::REQUESTER, Session::getLoginUserID()) - || $this->isUser(CommonITILActor::OBSERVER, Session::getLoginUserID()))) - || (Session::haveRight(self::$rightname, self::READGROUP) - && isset($_SESSION["glpigroups"]) - && ($this->haveAGroup(CommonITILActor::REQUESTER, $_SESSION["glpigroups"]) - || $this->haveAGroup(CommonITILActor::OBSERVER, $_SESSION["glpigroups"]))) - || (Session::haveRight(self::$rightname, self::READASSIGN) - && ($this->isUser(CommonITILActor::ASSIGN, Session::getLoginUserID()) - || (isset($_SESSION["glpigroups"]) - && $this->haveAGroup(CommonITILActor::ASSIGN, $_SESSION["glpigroups"])) - || (Session::haveRight(self::$rightname, self::ASSIGN) - && ($this->fields["status"] == self::INCOMING)))) - || (Session::haveRightsOr('ticketvalidation', TicketValidation::getValidateRights()) - && TicketValidation::canValidate($this->fields["id"]))); - } - - - /** - * Is the current user have right to approve solution of the current ticket ? - * - * @return boolean - **/ - function canApprove() { - - return ((($this->fields["users_id_recipient"] === Session::getLoginUserID()) - && Session::haveRight('ticket', Ticket::SURVEY)) - || $this->isUser(CommonITILActor::REQUESTER, Session::getLoginUserID()) - || (isset($_SESSION["glpigroups"]) - && $this->haveAGroup(CommonITILActor::REQUESTER, $_SESSION["glpigroups"]))); - } - - - /** - * @see CommonDBTM::canMassiveAction() - **/ - function canMassiveAction($action, $field, $value) { - - switch ($action) { - case 'update' : - switch ($field) { - case 'status' : - if (!self::isAllowedStatus($this->fields['status'], $value)) { - return false; - } - break; - } - break; - } - return true; - } - - /** - * Check if current user can take into account the ticket. - * - * @return boolean - */ - public function canTakeIntoAccount() { - - // Can take into account if user is assigned user - if ($this->isUser(CommonITILActor::ASSIGN, Session::getLoginUserID()) - || (isset($_SESSION["glpigroups"]) - && $this->haveAGroup(CommonITILActor::ASSIGN, $_SESSION['glpigroups']))) { - return true; - } - - // Cannot take into account if user is a requester (and not assigned) - if ($this->isUser(CommonITILActor::REQUESTER, Session::getLoginUserID()) - || (isset($_SESSION["glpigroups"]) - && $this->haveAGroup(CommonITILActor::REQUESTER, $_SESSION['glpigroups']))) { - return false; - } - - $canAddTask = Session::haveRight("task", CommonITILTask::ADDALLITEM); - $canAddFollowup = Session::haveRightsOr( - 'followup', - [ - ITILFollowup::ADDALLTICKET, - ITILFollowup::ADDMYTICKET, - ITILFollowup::ADDGROUPTICKET, - ] - ); - - // Can take into account if user has rights to add tasks or followups, - // assuming that users that does not have those rights cannot treat the ticket. - return $canAddTask || $canAddFollowup; - } - - /** - * Check if ticket has already been taken into account. - * - * @return boolean - */ - public function isAlreadyTakenIntoAccount() { - - return array_key_exists('takeintoaccount_delay_stat', $this->fields) - && $this->fields['takeintoaccount_delay_stat'] != 0; - } - - /** - * Get Datas to be added for SLA add - * - * @param $slas_id SLA id - * @param $entities_id entity ID of the ticket - * @param $date begin date of the ticket - * @param $type type of SLA - * - * @since 9.1 (before getDatasToAddSla without type parameter) - * - * @return array of datas to add in ticket - **/ - function getDatasToAddSLA($slas_id, $entities_id, $date, $type) { - - list($dateField, $slaField) = SLA::getFieldNames($type); - - $calendars_id = Entity::getUsedConfig('calendars_id', $entities_id); - $data = []; - - $sla = new SLA(); - if ($sla->getFromDB($slas_id)) { - $sla->setTicketCalendar($calendars_id); - if ($sla->fields['type'] == SLM::TTR) { - $data["slalevels_id_ttr"] = SlaLevel::getFirstSlaLevel($slas_id); - } - // Compute time_to_resolve - $data[$dateField] = $sla->computeDate($date); - $data['sla_waiting_duration'] = 0; - - } else { - $data["slalevels_id_ttr"] = 0; - $data[$slaField] = 0; - $data['sla_waiting_duration'] = 0; - } - return $data; - - } - - /** - * Get Datas to be added for OLA add - * - * @param $olas_id OLA id - * @param $entities_id entity ID of the ticket - * @param $date begin date of the ticket - * @param $type type of OLA - * - * @since 9.2 (before getDatasToAddOla without type parameter) - * - * @return array of datas to add in ticket - **/ - function getDatasToAddOLA($olas_id, $entities_id, $date, $type) { - - list($dateField, $olaField) = OLA::getFieldNames($type); - - $calendars_id = Entity::getUsedConfig('calendars_id', $entities_id); - $data = []; - - $ola = new OLA(); - if ($ola->getFromDB($olas_id)) { - $ola->setTicketCalendar($calendars_id); - if ($ola->fields['type'] == SLM::TTR) { - $data["olalevels_id_ttr"] = OlaLevel::getFirstOlaLevel($olas_id); - $data['ola_ttr_begin_date'] = $date; - } - // Compute time_to_resolve - $data[$dateField] = $ola->computeDate($date); - $data['ola_waiting_duration'] = 0; - - } else { - $data["olalevels_id_ttr"] = 0; - $data[$olaField] = 0; - $data['ola_waiting_duration'] = 0; - } - return $data; - - } - - - /** - * Delete Level Agreement for the ticket - * - * @since 9.2 - * - * @param string $laType (SLA | OLA) - * @param integer $id the sla/ola id - * @param integer $subtype (SLM::TTR | SLM::TTO) - * @param bool $delete_date (default false) - * - * @return bool - **/ - function deleteLevelAgreement($laType, $la_id, $subtype, $delete_date = false) { - switch ($laType) { - case "SLA": - $prefix = "sla"; - $prefix_ticket = ""; - $level_ticket = new SlaLevel_Ticket(); - break; - case "OLA": - $prefix = "ola"; - $prefix_ticket = "internal_"; - $level_ticket = new OlaLevel_Ticket(); - break; - } - - $input = []; - switch ($subtype) { - case SLM::TTR : - $input[$prefix.'s_id_ttr'] = 0; - if ($delete_date) { - $input[$prefix_ticket.'time_to_resolve'] = ''; - } - break; - - case SLM::TTO : - $input[$prefix.'s_id_tto'] = 0; - if ($delete_date) { - $input[$prefix_ticket.'time_to_own'] = ''; - } - break; - } - - $input[$prefix.'_waiting_duration'] = 0; - $input['id'] = $la_id; - $level_ticket->deleteForTicket($la_id, $subtype); - - return $this->update($input); - } - - - /** - * Is the current user have right to create the current ticket ? - * - * @return boolean - **/ - function canCreateItem() { - - if (!Session::haveAccessToEntity($this->getEntityID())) { - return false; - } - return self::canCreate(); - } - - - /** - * Is the current user have right to update the current ticket ? - * - * @return boolean - **/ - function canUpdateItem() { - if (!$this->checkEntity()) { - return false; - } - - // for all, if no modification in ticket return true - if ($can_requester = $this->canRequesterUpdateItem()) { - return true; - } - - // for self-service only, if modification in ticket, we can't update the ticket - if (Session::getCurrentInterface() == "helpdesk" - && !$can_requester) { - return false; - } - - // if we don't have global UPDATE right, maybe we can own the current ticket - if (!Session::haveRight(self::$rightname, UPDATE) - && !$this->ownItem()) { - //we always return false, as ownItem() = true is managed by below self::canUpdate - return false; - } - - return self::canupdate(); - } - - - /** - * Is the current user is a requester of the current ticket and have the right to update it ? - * - * @return boolean - */ - function canRequesterUpdateItem() { - return ($this->isUser(CommonITILActor::REQUESTER, Session::getLoginUserID()) - || $this->fields["users_id_recipient"] === Session::getLoginUserID()) - && $this->fields['status'] != self::SOLVED - && $this->fields['status'] != self::CLOSED - && $this->numberOfFollowups() == 0 - && $this->numberOfTasks() == 0; - } - - /** - * Is the current user have OWN right and is the assigned to the ticket - * - * @return boolean - */ - function ownItem() { - return Session::haveRight(self::$rightname, self::OWN) - && $this->isUser(CommonITILActor::ASSIGN, Session::getLoginUserID()); - } - - - /** - * @since 0.85 - **/ - static function canDelete() { - - // to allow delete for self-service only if no action on the ticket - if (Session::getCurrentInterface() == "helpdesk") { - return Session::haveRight(self::$rightname, CREATE); - } - return Session::haveRight(self::$rightname, DELETE); - } - - /** - * is the current user could reopen the current ticket - * @since 9.2 - * @return boolean - */ - function canReopen() { - return Session::haveRight('followup', CREATE) - && in_array($this->fields["status"], $this->getClosedStatusArray()) - && ($this->isAllowedStatus($this->fields['status'], self::INCOMING) - || $this->isAllowedStatus($this->fields['status'], self::ASSIGNED)); - } - - - /** - * Is the current user have right to delete the current ticket ? - * - * @return boolean - **/ - function canDeleteItem() { - - if (!Session::haveAccessToEntity($this->getEntityID())) { - return false; - } - - // user can delete his ticket if no action on it - if (Session::getCurrentInterface() == "helpdesk" - && (!($this->isUser(CommonITILActor::REQUESTER, Session::getLoginUserID()) - || $this->fields["users_id_recipient"] === Session::getLoginUserID()) - || $this->numberOfFollowups() > 0 - || $this->numberOfTasks() > 0 - || $this->fields["date"] != $this->fields["date_mod"])) { - return false; - } - - return static::canDelete(); - } - - /** - * @see CommonITILObject::getDefaultActor() - **/ - function getDefaultActor($type) { - - if ($type == CommonITILActor::ASSIGN) { - if (Session::haveRight(self::$rightname, self::OWN) - && $_SESSION['glpiset_default_tech']) { - return Session::getLoginUserID(); - } - } - if ($type == CommonITILActor::REQUESTER) { - if (Session::haveRight(self::$rightname, CREATE) - && $_SESSION['glpiset_default_requester']) { - return Session::getLoginUserID(); - } - } - return 0; - } - - - /** - * @see CommonITILObject::getDefaultActorRightSearch() - **/ - function getDefaultActorRightSearch($type) { - - $right = "all"; - if ($type == CommonITILActor::ASSIGN) { - $right = "own_ticket"; - if (!Session::haveRight(self::$rightname, self::ASSIGN)) { - $right = 'id'; - } - } - return $right; - } - - - function pre_deleteItem() { - global $CFG_GLPI; - - if (!isset($this->input['_disablenotif']) && $CFG_GLPI['use_notifications']) { - NotificationEvent::raiseEvent('delete', $this); - } - return true; - } - - - function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) { - - if (static::canView()) { - $nb = 0; - $title = self::getTypeName(Session::getPluralNumber()); - if ($_SESSION['glpishow_count_on_tabs']) { - switch ($item->getType()) { - case 'User' : - $nb = countElementsInTable( - ['glpi_tickets', 'glpi_tickets_users'], [ - 'glpi_tickets_users.tickets_id' => new \QueryExpression(DB::quoteName('glpi_tickets.id')), - 'glpi_tickets_users.users_id' => $item->getID(), - 'glpi_tickets_users.type' => CommonITILActor::REQUESTER - ] + getEntitiesRestrictCriteria(self::getTable()) - ); - $title = __('Created tickets'); - break; - - case 'Supplier' : - $nb = countElementsInTable( - ['glpi_tickets', 'glpi_suppliers_tickets'], [ - 'glpi_suppliers_tickets.tickets_id' => new \QueryExpression(DB::quoteName('glpi_tickets.id')), - 'glpi_suppliers_tickets.suppliers_id' => $item->getID() - ] + getEntitiesRestrictCriteria(self::getTable()) - ); - break; - - case 'SLA' : - $nb = countElementsInTable( - 'glpi_tickets', [ - 'OR' => [ - 'slas_id_tto' => $item->getID(), - 'slas_id_ttr' => $item->getID() - ] - ] - ); - break; - case 'OLA' : - $nb = countElementsInTable( - 'glpi_tickets', [ - 'OR' => [ - 'olas_id_tto' => $item->getID(), - 'olas_id_ttr' => $item->getID() - ] - ] - ); - break; - - case 'Group' : - $nb = countElementsInTable( - ['glpi_tickets', 'glpi_groups_tickets'], [ - 'glpi_groups_tickets.tickets_id' => new \QueryExpression(DB::quoteName('glpi_tickets.id')), - 'glpi_groups_tickets.groups_id' => $item->getID(), - 'glpi_groups_tickets.type' => CommonITILActor::REQUESTER - ] + getEntitiesRestrictCriteria(self::getTable()) - ); - $title = __('Created tickets'); - break; - - default : - // Direct one - $nb = countElementsInTable( - 'glpi_items_tickets', - [ - 'INNER JOIN' => [ - 'glpi_tickets' => [ - 'FKEY' => [ - 'glpi_items_tickets' => 'tickets_id', - 'glpi_tickets' => 'id' - ] - ] - ], - 'WHERE' => [ - 'itemtype' => $item->getType(), - 'items_id' => $item->getID(), - 'is_deleted' => 0 - ] - ] - ); - - // Linked items - $linkeditems = $item->getLinkedItems(); - - if (count($linkeditems)) { - foreach ($linkeditems as $type => $tab) { - foreach ($tab as $ID) { - $nb += countElementsInTable( - 'glpi_items_tickets', - [ - 'INNER JOIN' => [ - 'glpi_tickets' => [ - 'FKEY' => [ - 'glpi_items_tickets' => 'tickets_id', - 'glpi_tickets' => 'id' - ] - ] - ], - 'WHERE' => [ - 'itemtype' => $type, - 'items_id' => $ID, - 'is_deleted' => 0 - ] - ] - ); - } - } - } - break; - } - - } // glpishow_count_on_tabs - // Not for Ticket class - if ($item->getType() != __CLASS__) { - return self::createTabEntry($title, $nb); - } - } // self::READALL right check - - // Not check self::READALL for Ticket itself - switch ($item->getType()) { - case __CLASS__ : - $ong = []; - - $timeline = $item->getTimelineItems(); - $nb_elements = count($timeline); - $ong[1] = __("Processing ticket")." $nb_elements"; - - // enquete si statut clos - $satisfaction = new TicketSatisfaction(); - if ($satisfaction->getFromDB($item->getID()) - && $item->fields['status'] == self::CLOSED) { - $ong[3] = __('Satisfaction'); - } - if ($item->canView()) { - $ong[4] = __('Statistics'); - } - return $ong; - - // default : - // return _n('Ticket','Tickets', Session::getPluralNumber()); - } - - return ''; - } - - - static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0) { - - switch ($item->getType()) { - case __CLASS__ : - switch ($tabnum) { - - case 1 : - echo "
"; - $rand = mt_rand(); - $item->showTimelineForm($rand); - $item->showTimeline($rand); - echo "
"; - break; - - case 3 : - $satisfaction = new TicketSatisfaction(); - if (($item->fields['status'] == self::CLOSED) - && $satisfaction->getFromDB($_GET["id"])) { - - $duration = Entity::getUsedConfig('inquest_duration', $item->fields['entities_id']); - $date2 = strtotime($satisfaction->fields['date_begin']); - if (($duration == 0) - || (strtotime("now") - $date2) <= $duration*DAY_TIMESTAMP) { - $satisfaction->showForm($item); - } else { - echo "

".__('Satisfaction survey expired')."

"; - } - - } else { - echo "

".__('No generated survey')."

"; - } - break; - - case 4 : - $item->showStats(); - break; - } - break; - - case 'Group' : - case 'SLA' : - case 'OLA' : - default : - self::showListForItem($item, $withtemplate); - } - return true; - } - - - function defineTabs($options = []) { - $ong = []; - - $this->defineDefaultObjectTabs($ong, $options); - $this->addStandardTab('TicketValidation', $ong, $options); - $this->addStandardTab('KnowbaseItem_Item', $ong, $options); - $this->addStandardTab('Item_Ticket', $ong, $options); - - if ($this->hasImpactTab()) { - $this->addStandardTab('Impact', $ong, $options); - } - - $this->addStandardTab('TicketCost', $ong, $options); - $this->addStandardTab('Itil_Project', $ong, $options); - $this->addStandardTab('ProjectTask_Ticket', $ong, $options); - $this->addStandardTab('Problem_Ticket', $ong, $options); - $this->addStandardTab('Change_Ticket', $ong, $options); - - $entity = $this->getEntityID(); - if (!(Entity::getUsedConfig('anonymize_support_agents', $entity) - && Session::getCurrentInterface() == 'helpdesk') - ) { - $this->addStandardTab('Log', $ong, $options); - } - - return $ong; - } - - - /** - * Retrieve data of the hardware linked to the ticket if exists - * - * @return void - **/ - function getAdditionalDatas() { - - $this->hardwaredatas = []; - - if (!empty($this->fields["id"])) { - $item_ticket = new Item_Ticket(); - $data = $item_ticket->find(['tickets_id' => $this->fields["id"]]); - - foreach ($data as $val) { - if (!empty($val["itemtype"]) && ($item = getItemForItemtype($val["itemtype"]))) { - if ($item->getFromDB($val["items_id"])) { - $this->hardwaredatas[] = $item; - } - } - } - } - - } - - - function cleanDBonPurge() { - - // OlaLevel_Ticket does not extends CommonDBConnexity - $olaLevel_ticket = new OlaLevel_Ticket(); - $olaLevel_ticket->deleteForTicket($this->fields['id'], SLM::TTO); - $olaLevel_ticket->deleteForTicket($this->fields['id'], SLM::TTR); - - // SlaLevel_Ticket does not extends CommonDBConnexity - $slaLevel_ticket = new SlaLevel_Ticket(); - $slaLevel_ticket->deleteForTicket($this->fields['id'], SLM::TTO); - $slaLevel_ticket->deleteForTicket($this->fields['id'], SLM::TTR); - - // TicketSatisfaction does not extends CommonDBConnexity - $tf = new TicketSatisfaction(); - $tf->deleteByCriteria(['tickets_id' => $this->fields['id']]); - - // CommonITILTask does not extends CommonDBConnexity - $tt = new TicketTask(); - $tt->deleteByCriteria(['tickets_id' => $this->fields['id']]); - - $this->deleteChildrenAndRelationsFromDb( - [ - Change_Ticket::class, - Item_Ticket::class, - Problem_Ticket::class, - ProjectTask_Ticket::class, - TicketCost::class, - Ticket_Ticket::class, - TicketValidation::class, - ] - ); - - parent::cleanDBonPurge(); - - } - - - function prepareInputForUpdate($input) { - global $DB; - - // Get ticket : need for comparison - $this->getFromDB($input['id']); - - // Clean new lines before passing to rules - if (isset($input["content"])) { - $input["content"] = preg_replace('/\\\\r\\\\n/', "\n", $input['content']); - $input["content"] = preg_replace('/\\\\n/', "\n", $input['content']); - } - - // automatic recalculate if user changes urgence or technician change impact - $canpriority = Session::haveRight(self::$rightname, self::CHANGEPRIORITY); - if ((isset($input['urgency']) && $input['urgency'] != $this->fields['urgency']) - || (isset($input['impact']) && $input['impact'] != $this->fields['impact']) - && ($canpriority && !isset($input['priority']) || !$canpriority) - ) { - if (!isset($input['urgency'])) { - $input['urgency'] = $this->fields['urgency']; - } - if (!isset($input['impact'])) { - $input['impact'] = $this->fields['impact']; - } - $input['priority'] = self::computePriority($input['urgency'], $input['impact']); - } - - // Security checks - if (!Session::isCron() - && !Session::haveRight(self::$rightname, self::ASSIGN)) { - if (isset($input["_itil_assign"]) - && isset($input['_itil_assign']['_type']) - && ($input['_itil_assign']['_type'] == 'user')) { - - // must own_ticket to grab a non assign ticket - if ($this->countUsers(CommonITILActor::ASSIGN) == 0) { - if ((!Session::haveRightsOr(self::$rightname, [self::STEAL, self::OWN])) - || !isset($input["_itil_assign"]['users_id']) - || ($input["_itil_assign"]['users_id'] != Session::getLoginUserID())) { - unset($input["_itil_assign"]); - } - - } else { - // Can not steal or can steal and not assign to me - if (!Session::haveRight(self::$rightname, self::STEAL) - || !isset($input["_itil_assign"]['users_id']) - || ($input["_itil_assign"]['users_id'] != Session::getLoginUserID())) { - unset($input["_itil_assign"]); - } - } - } - - // No supplier assign - if (isset($input["_itil_assign"]) - && isset($input['_itil_assign']['_type']) - && ($input['_itil_assign']['_type'] == 'supplier')) { - unset($input["_itil_assign"]); - } - - // No group - if (isset($input["_itil_assign"]) - && isset($input['_itil_assign']['_type']) - && ($input['_itil_assign']['_type'] == 'group')) { - unset($input["_itil_assign"]); - } - } - - //must be handled here for tickets. @see CommonITILObject::prepareInputForUpdate() - $input = $this->handleTemplateFields($input); - if ($input === false) { - return false; - } - - if (isset($input['entities_id'])) { - $entid = $input['entities_id']; - } else { - $entid = $this->fields['entities_id']; - } - - // Process Business Rules - $this->fillInputForBusinessRules($input); - - // Add actors on standard input - $rules = new RuleTicketCollection($entid); - $rule = $rules->getRuleClass(); - $changes = []; - $post_added = []; - $tocleanafterrules = []; - $usertypes = [ - CommonITILActor::ASSIGN => 'assign', - CommonITILActor::REQUESTER => 'requester', - CommonITILActor::OBSERVER => 'observer' - ]; - foreach ($usertypes as $k => $t) { - //handle new input - if (isset($input['_itil_'.$t]) && isset($input['_itil_'.$t]['_type'])) { - $field = $input['_itil_'.$t]['_type'].'s_id'; - if (isset($input['_itil_'.$t][$field]) - && !isset($input[$field.'_'.$t])) { - $input['_'.$field.'_'.$t][] = $input['_itil_'.$t][$field]; - $tocleanafterrules['_'.$field.'_'.$t][] = $input['_itil_'.$t][$field]; - } - } - - //handle existing actors: load all existing actors from ticket - //to make sure business rules will receive all informations, and not just - //what have been entered in the html form. - // - //ref also this actor into $post_added to avoid the filling of $changes - //and triggering businness rules when not needed - $users = $this->getUsers($k); - if (count($users)) { - $field = 'users_id'; - foreach ($users as $user) { - if (!isset($input['_'.$field.'_'.$t]) || !in_array($user[$field], $input['_'.$field.'_'.$t])) { - if (!isset($input['_'.$field.'_'.$t])) { - $post_added['_'.$field.'_'.$t] = '_'.$field.'_'.$t; - } - $input['_'.$field.'_'.$t][] = $user[$field]; - $tocleanafterrules['_'.$field.'_'.$t][] = $user[$field]; - } - } - } - - $groups = $this->getGroups($k); - if (count($groups)) { - $field = 'groups_id'; - foreach ($groups as $group) { - if (!isset($input['_'.$field.'_'.$t]) || !in_array($group[$field], $input['_'.$field.'_'.$t])) { - if (!isset($input['_'.$field.'_'.$t])) { - $post_added['_'.$field.'_'.$t] = '_'.$field.'_'.$t; - } - $input['_'.$field.'_'.$t][] = $group[$field]; - $tocleanafterrules['_'.$field.'_'.$t][] = $group[$field]; - } - } - } - - $suppliers = $this->getSuppliers($k); - if (count($suppliers)) { - $field = 'suppliers_id'; - foreach ($suppliers as $supplier) { - if (!isset($input['_'.$field.'_'.$t]) || !in_array($supplier[$field], $input['_'.$field.'_'.$t])) { - if (!isset($input['_'.$field.'_'.$t])) { - $post_added['_'.$field.'_'.$t] = '_'.$field.'_'.$t; - } - $input['_'.$field.'_'.$t][] = $supplier[$field]; - $tocleanafterrules['_'.$field.'_'.$t][] = $supplier[$field]; - } - } - } - } - - foreach ($rule->getCriterias() as $key => $val) { - if (array_key_exists($key, $input) - && !array_key_exists($key, $post_added)) { - if (!isset($this->fields[$key]) - || ($DB->escape($this->fields[$key]) != $input[$key])) { - $changes[] = $key; - } - } - } - - // Business Rules do not override manual SLA and OLA - $manual_slas_id = []; - $manual_olas_id = []; - foreach ([SLM::TTR, SLM::TTO] as $slmType) { - list($dateField, $slaField) = SLA::getFieldNames($slmType); - if (isset($input[$slaField]) && ($input[$slaField] > 0)) { - $manual_slas_id[$slmType] = $input[$slaField]; - } - - list($dateField, $olaField) = OLA::getFieldNames($slmType); - if (isset($input[$olaField]) && ($input[$olaField] > 0)) { - $manual_olas_id[$slmType] = $input[$olaField]; - } - } - - // Only process rules on changes - if (count($changes)) { - if (in_array('_users_id_requester', $changes)) { - // If _users_id_requester changed : set users_locations - $user = new User(); - if (isset($input["_itil_requester"]["users_id"]) - && $user->getFromDB($input["_itil_requester"]["users_id"])) { - $input['users_locations'] = $user->fields['locations_id']; - $changes[] = 'users_locations'; - } - // If _users_id_requester changed : add _groups_id_of_requester to changes - $changes[] = '_groups_id_of_requester'; - } - - $input = $rules->processAllRules($input, - $input, - ['recursive' => true, - 'entities_id' => $entid], - ['condition' => RuleTicket::ONUPDATE, - 'only_criteria' => $changes]); - $input = Toolbox::stripslashes_deep($input); - } - - // Clean actors fields added for rules - foreach ($tocleanafterrules as $key => $val) { - if ($input[$key] == $val) { - unset($input[$key]); - } - } - - // Manage fields from auto update or rules : map rule actions to standard additional ones - $usertypes = ['assign', 'requester', 'observer']; - $actortypes = ['user','group','supplier']; - foreach ($usertypes as $t) { - foreach ($actortypes as $a) { - if (isset($input['_'.$a.'s_id_'.$t])) { - switch ($a) { - case 'user' : - $additionalfield = '_additional_'.$t.'s'; - $input[$additionalfield][] = ['users_id' => $input['_'.$a.'s_id_'.$t]]; - break; - - default : - $additionalfield = '_additional_'.$a.'s_'.$t.'s'; - $input[$additionalfield][] = $input['_'.$a.'s_id_'.$t]; - break; - } - } - } - } - - if (isset($input['_link'])) { - $ticket_ticket = new Ticket_Ticket(); - if (!empty($input['_link']['tickets_id_2'])) { - if ($ticket_ticket->can(-1, CREATE, $input['_link'])) { - if ($ticket_ticket->add($input['_link'])) { - $input['_forcenotif'] = true; - } - } else { - Session::addMessageAfterRedirect(__('Unknown ticket'), false, ERROR); - } - } - } - - // SLA / OLA affect by rules : reset time_to_resolve / internal_time_to_resolve - // Manual SLA / OLA defined : reset time_to_resolve / internal_time_to_resolve - // No manual SLA / OLA and due date defined : reset auto SLA / OLA - foreach ([SLM::TTR, SLM::TTO] as $slmType) { - $this->slaAffect($slmType, $input, $manual_slas_id); - $this->olaAffect($slmType, $input, $manual_olas_id); - } - - if (isset($input['content'])) { - if (isset($input['_filename']) || isset($input['_content'])) { - $input['_disablenotif'] = true; - } else { - $input['_donotadddocs'] = true; - } - } - - $input = parent::prepareInputForUpdate($input); - return $input; - } - - - /** - * SLA affect by rules : reset time_to_resolve and time_to_own - * Manual SLA defined : reset time_to_resolve and time_to_own - * No manual SLA and due date defined : reset auto SLA - * - * @since 9.1 - * - * @param $type - * @param $input - * @param $manual_slas_id - */ - function slaAffect($type, &$input, $manual_slas_id) { - - list($dateField, $slaField) = SLA::getFieldNames($type); - - // Restore slas - if (isset($manual_slas_id[$type]) - && !isset($input['_'.$slaField])) { - $input[$slaField] = $manual_slas_id[$type]; - } - - // Ticket update - if (isset($this->fields['id']) && $this->fields['id'] > 0) { - if (!isset($manual_slas_id[$type]) - && isset($input[$slaField]) && ($input[$slaField] > 0) - && ($input[$slaField] != $this->fields[$slaField])) { - - if (isset($input[$dateField])) { - // Unset due date - unset($input[$dateField]); - } - } - - if (isset($input[$slaField]) && ($input[$slaField] > 0) - && ($input[$slaField] != $this->fields[$slaField])) { - - $date = $this->fields['date']; - /// Use updated date if also done - if (isset($input["date"])) { - $date = $input["date"]; - } - // Get datas to initialize SLA and set it - $sla_data = $this->getDatasToAddSLA($input[$slaField], $this->fields['entities_id'], - $date, $type); - if (count($sla_data)) { - foreach ($sla_data as $key => $val) { - $input[$key] = $val; - } - } - } - } else { // Ticket add - if (!isset($manual_slas_id[$type]) - && isset($input[$dateField]) && ($input[$dateField] != 'NULL')) { - // Valid due date - if ($input[$dateField] >= $input['date']) { - if (isset($input[$slaField])) { - unset($input[$slaField]); - } - } else { - // Unset due date - unset($input[$dateField]); - } - } - - if (isset($input[$slaField]) && ($input[$slaField] > 0)) { - // Get datas to initialize SLA and set it - $sla_data = $this->getDatasToAddSLA($input[$slaField], $input['entities_id'], - $input['date'], $type); - if (count($sla_data)) { - foreach ($sla_data as $key => $val) { - $input[$key] = $val; - } - } - } - } - } - - /** - * OLA affect by rules : reset internal_time_to_resolve and internal_time_to_own - * Manual OLA defined : reset internal_time_to_resolve and internal_time_to_own - * No manual OLA and due date defined : reset auto OLA - * - * @since 9.1 - * - * @param $type - * @param $input - * @param $manual_olas_id - */ - function olaAffect($type, &$input, $manual_olas_id) { - - list($dateField, $olaField) = OLA::getFieldNames($type); - - // Restore olas - if (isset($manual_olas_id[$type]) - && !isset($input['_'.$olaField])) { - $input[$olaField] = $manual_olas_id[$type]; - } - - // Ticket update - if (isset($this->fields['id']) && $this->fields['id'] > 0) { - if (!isset($manual_olas_id[$type]) - && isset($input[$olaField]) && ($input[$olaField] > 0) - && ($input[$olaField] != $this->fields[$olaField])) { - - if (isset($input[$dateField])) { - // Unset due date - unset($input[$dateField]); - } - } - - if (isset($input[$olaField]) && ($input[$olaField] > 0) - && ($input[$olaField] != $this->fields[$olaField] - || isset($input['_'.$olaField]))) { - - $date = date('Y-m-d H:i:s'); - - // Get datas to initialize OLA and set it - $ola_data = $this->getDatasToAddOLA($input[$olaField], $this->fields['entities_id'], - $date, $type); - if (count($ola_data)) { - foreach ($ola_data as $key => $val) { - $input[$key] = $val; - } - } - } - } else { // Ticket add - if (!isset($manual_olas_id[$type]) - && isset($input[$dateField]) && ($input[$dateField] != 'NULL')) { - // Valid due date - if ($input[$dateField] >= $input['date']) { - if (isset($input[$olaField])) { - unset($input[$olaField]); - } - } else { - // Unset due date - unset($input[$dateField]); - } - } - - if (isset($input[$olaField]) && ($input[$olaField] > 0)) { - // Get datas to initialize OLA and set it - $ola_data = $this->getDatasToAddOLA($input[$olaField], $input['entities_id'], - $input['date'], $type); - if (count($ola_data)) { - foreach ($ola_data as $key => $val) { - $input[$key] = $val; - } - } - } - } - } - - - /** - * Manage SLA level escalation - * - * @since 9.1 - * - * @param $slas_id - **/ - function manageSlaLevel($slas_id) { - - $calendars_id = Entity::getUsedConfig('calendars_id', $this->fields['entities_id']); - // Add first level in working table - $slalevels_id = SlaLevel::getFirstSlaLevel($slas_id); - - $sla = new SLA(); - if ($sla->getFromDB($slas_id)) { - $sla->setTicketCalendar($calendars_id); - $sla->addLevelToDo($this, $slalevels_id); - } - SlaLevel_Ticket::replayForTicket($this->getID(), $sla->getField('type')); - } - - /** - * Manage OLA level escalation - * - * @since 9.1 - * - * @param $slas_id - **/ - function manageOlaLevel($slas_id) { - - $calendars_id = Entity::getUsedConfig('calendars_id', $this->fields['entities_id']); - // Add first level in working table - $olalevels_id = OlaLevel::getFirstOlaLevel($slas_id); - - $ola = new OLA(); - if ($ola->getFromDB($slas_id)) { - $ola->setTicketCalendar($calendars_id); - $ola->addLevelToDo($this, $olalevels_id); - } - OlaLevel_Ticket::replayForTicket($this->getID(), $ola->getField('type')); - } - - - function pre_updateInDB() { - - if (!$this->isTakeIntoAccountComputationBlocked($this->input) - && !$this->isAlreadyTakenIntoAccount() - && $this->canTakeIntoAccount() - && !$this->isNew() - ) { - $this->updates[] = "takeintoaccount_delay_stat"; - $this->fields['takeintoaccount_delay_stat'] = $this->computeTakeIntoAccountDelayStat(); - } - - parent::pre_updateInDB(); - - } - - - /** - * Compute take into account stat of the current ticket - **/ - function computeTakeIntoAccountDelayStat() { - - if (isset($this->fields['id']) - && !empty($this->fields['date'])) { - $calendars_id = $this->getCalendar(); - $calendar = new Calendar(); - - // Using calendar - if (($calendars_id > 0) && $calendar->getFromDB($calendars_id)) { - return max(1, $calendar->getActiveTimeBetween($this->fields['date'], - $_SESSION["glpi_currenttime"])); - } - // Not calendar defined - return max(1, strtotime($_SESSION["glpi_currenttime"])-strtotime($this->fields['date'])); - } - return 0; - } - - - function post_updateItem($history = 1) { - global $CFG_GLPI; - - parent::post_updateItem($history); - - //Action for send_validation rule : do validation before clean - $this->manageValidationAdd($this->input); - - // Put same status on duplicated tickets when solving or closing (autoclose on solve) - if (isset($this->input['status']) - && in_array('status', $this->updates) - && (in_array($this->input['status'], $this->getSolvedStatusArray()) - || in_array($this->input['status'], $this->getClosedStatusArray()))) { - Ticket_Ticket::manageLinkedTicketsOnSolved($this->getID()); - } - - $donotif = count($this->updates); - - if (isset($this->input['_forcenotif'])) { - $donotif = true; - } - - // Manage SLA / OLA Level : add actions - foreach ([SLM::TTR, SLM::TTO] as $slmType) { - list($dateField, $slaField) = SLA::getFieldNames($slmType); - if (in_array($slaField, $this->updates) - && ($this->fields[$slaField] > 0)) { - $this->manageSlaLevel($this->fields[$slaField]); - } - - list($dateField, $olaField) = OLA::getFieldNames($slmType); - if (in_array($olaField, $this->updates) - && ($this->fields[$olaField] > 0)) { - $this->manageOlaLevel($this->fields[$olaField]); - } - } - - if (count($this->updates)) { - // Update Ticket Tco - if (in_array("actiontime", $this->updates) - || in_array("cost_time", $this->updates) - || in_array("cost_fixed", $this->updates) - || in_array("cost_material", $this->updates)) { - - if (!empty($this->input["items_id"])) { - foreach ($this->input["items_id"] as $itemtype => $items) { - foreach ($items as $items_id) { - if ($itemtype && ($item = getItemForItemtype($itemtype))) { - if ($item->getFromDB($items_id)) { - $newinput = []; - $newinput['id'] = $items_id; - $newinput['ticket_tco'] = self::computeTco($item); - $item->update($newinput); - } - } - } - } - } - } - - $donotif = true; - } - - if (isset($this->input['_disablenotif'])) { - $donotif = false; - } - - if ($donotif && $CFG_GLPI["use_notifications"]) { - $mailtype = "update"; - - if (isset($this->input["status"]) - && $this->input["status"] - && in_array("status", $this->updates) - && in_array($this->input["status"], $this->getSolvedStatusArray())) { - - $mailtype = "solved"; - } - - if (isset($this->input["status"]) - && $this->input["status"] - && in_array("status", $this->updates) - && in_array($this->input["status"], $this->getClosedStatusArray())) { - - $mailtype = "closed"; - } - // to know if a solution is approved or not - if ((isset($this->input['solvedate']) && ($this->input['solvedate'] == 'NULL') - && isset($this->oldvalues['solvedate']) && $this->oldvalues['solvedate']) - && (isset($this->input['status']) - && ($this->input['status'] != $this->oldvalues['status']) - && ($this->oldvalues['status'] == self::SOLVED))) { - - $mailtype = "rejectsolution"; - } - - // Read again ticket to be sure that all data are up to date - $this->getFromDB($this->fields['id']); - NotificationEvent::raiseEvent($mailtype, $this); - } - - // inquest created immediatly if delay = O - $inquest = new TicketSatisfaction(); - $rate = Entity::getUsedConfig('inquest_config', $this->fields['entities_id'], - 'inquest_rate'); - $delay = Entity::getUsedConfig('inquest_config', $this->fields['entities_id'], - 'inquest_delay'); - $type = Entity::getUsedConfig('inquest_config', $this->fields['entities_id']); - $max_closedate = $this->fields['closedate']; - - if (in_array("status", $this->updates) - && in_array($this->input["status"], $this->getClosedStatusArray()) - && ($delay == 0) - && ($rate > 0) - && (mt_rand(1, 100) <= $rate)) { - - // For reopened ticket - if ($inquest->getFromDB($this->fields['id'])) { - $resp = $inquest->fields; - $inquest->delete($resp); - } - - $inquest->add( - [ - 'tickets_id' => $this->fields['id'], - 'date_begin' => $_SESSION["glpi_currenttime"], - 'entities_id' => $this->fields['entities_id'], - 'type' => $type, - 'max_closedate' => $max_closedate, - ] - ); - } - } - - - function prepareInputForAdd($input) { - // Standard clean datas - $input = parent::prepareInputForAdd($input); - if ($input === false) { - return false; - } - - if (!isset($input["requesttypes_id"])) { - $input["requesttypes_id"] = RequestType::getDefault('helpdesk'); - } - - if (!isset($input['global_validation'])) { - $input['global_validation'] = CommonITILValidation::NONE; - } - - // Set additional default dropdown - $dropdown_fields = ['users_locations', 'items_locations']; - foreach ($dropdown_fields as $field) { - if (!isset($input[$field])) { - $input[$field] = 0; - } - } - if (!isset($input['itemtype']) || !isset($input['items_id']) || !($input['items_id'] > 0)) { - $input['itemtype'] = ''; - } - - // Get first item location - $item = null; - if (isset($input["items_id"]) - && is_array($input["items_id"]) - && (count($input["items_id"]) > 0)) { - $infocom = new Infocom(); - foreach ($input["items_id"] as $itemtype => $items) { - foreach ($items as $items_id) { - if ($item = getItemForItemtype($itemtype)) { - $item->getFromDB($items_id); - $input['items_states'] = $item->fields['states_id']; - $input['items_locations'] = $item->fields['locations_id']; - if ($infocom->getFromDBforDevice($itemtype, $items_id)) { - $input['items_businesscriticities'] - = Dropdown::getDropdownName('glpi_businesscriticities', - $infocom->fields['businesscriticities_id']); - } - if (isset($item->fields['groups_id'])) { - $input['items_groups'] = $item->fields['groups_id']; - - } - break(2); - } - } - } - } - - // Business Rules do not override manual SLA and OLA - $manual_slas_id = []; - $manual_olas_id = []; - foreach ([SLM::TTR, SLM::TTO] as $slmType) { - list($dateField, $slaField) = SLA::getFieldNames($slmType); - if (isset($input[$slaField]) && ($input[$slaField] > 0)) { - $manual_slas_id[$slmType] = $input[$slaField]; - } - list($dateField, $olaField) = OLA::getFieldNames($slmType); - if (isset($input[$olaField]) && ($input[$olaField] > 0)) { - $manual_olas_id[$slmType] = $input[$olaField]; - } - } - - // fill auto-assign when no tech defined (only for tech) - if (!isset($input['_auto_import']) - && isset($_SESSION['glpiset_default_tech']) && $_SESSION['glpiset_default_tech'] - && Session::getCurrentInterface() == 'central' - && (!isset($input['_users_id_assign']) || $input['_users_id_assign'] == 0) - && Session::haveRight("ticket", Ticket::OWN) - ) { - $input['_users_id_assign'] = Session::getLoginUserID(); - } - - // Process Business Rules - $this->fillInputForBusinessRules($input); - - $rules = new RuleTicketCollection($input['entities_id']); - - // Set unset variables with are needed - $tmprequester = 0; - $user = new User(); - if (isset($input["_users_id_requester"])) { - if (!is_array($input["_users_id_requester"]) - && $user->getFromDB($input["_users_id_requester"])) { - $input['users_locations'] = $user->fields['locations_id']; - $input['users_default_groups'] = $user->fields['groups_id']; - $tmprequester = $input["_users_id_requester"]; - } else if (is_array($input["_users_id_requester"]) && ($user_id = reset($input["_users_id_requester"])) !== false) { - if ($user->getFromDB($user_id)) { - $input['users_locations'] = $user->fields['locations_id']; - $input['users_default_groups'] = $user->fields['groups_id']; - } - } - } - - // Clean new lines before passing to rules - if (isset($input["content"])) { - $input["content"] = preg_replace('/\\\\r\\\\n/', "\\n", $input['content']); - $input["content"] = preg_replace('/\\\\n/', "\\n", $input['content']); - } - - $input = $rules->processAllRules($input, - $input, - ['recursive' => true], - ['condition' => RuleTicket::ONADD]); - $input = Toolbox::stripslashes_deep($input); - - // Recompute default values based on values computed by rules - $input = $this->computeDefaultValuesForAdd($input); - - if (isset($input['_users_id_requester']) - && !is_array($input['_users_id_requester']) - && ($input['_users_id_requester'] != $tmprequester)) { - // if requester set by rule, clear address from mailcollector - unset($input['_users_id_requester_notif']); - } - if (isset($input['_users_id_requester_notif']) - && isset($input['_users_id_requester_notif']['alternative_email']) - && is_array($input['_users_id_requester_notif']['alternative_email'])) { - foreach ($input['_users_id_requester_notif']['alternative_email'] as $email) { - if ($email && !NotificationMailing::isUserAddressValid($email)) { - Session::addMessageAfterRedirect( - sprintf(__('Invalid email address %s'), $email), - false, - ERROR - ); - return false; - } - } - } - - // Manage auto assign - $auto_assign_mode = Entity::getUsedConfig('auto_assign_mode', $input['entities_id']); - - switch ($auto_assign_mode) { - case Entity::CONFIG_NEVER : - break; - - case Entity::AUTO_ASSIGN_HARDWARE_CATEGORY : - if ($item != null) { - // Auto assign tech from item - if ((!isset($input['_users_id_assign']) || ($input['_users_id_assign'] == 0)) - && $item->isField('users_id_tech')) { - $input['_users_id_assign'] = $item->getField('users_id_tech'); - } - // Auto assign group from item - if ((!isset($input['_groups_id_assign']) || ($input['_groups_id_assign'] == 0)) - && $item->isField('groups_id_tech')) { - $input['_groups_id_assign'] = $item->getField('groups_id_tech'); - } - } - // Auto assign tech/group from Category - if (($input['itilcategories_id'] > 0) - && ((!isset($input['_users_id_assign']) || !$input['_users_id_assign']) - || (!isset($input['_groups_id_assign']) || !$input['_groups_id_assign']))) { - - $cat = new ITILCategory(); - $cat->getFromDB($input['itilcategories_id']); - if ((!isset($input['_users_id_assign']) || !$input['_users_id_assign']) - && $cat->isField('users_id')) { - $input['_users_id_assign'] = $cat->getField('users_id'); - } - if ((!isset($input['_groups_id_assign']) || !$input['_groups_id_assign']) - && $cat->isField('groups_id')) { - $input['_groups_id_assign'] = $cat->getField('groups_id'); - } - } - break; - - case Entity::AUTO_ASSIGN_CATEGORY_HARDWARE : - // Auto assign tech/group from Category - if (($input['itilcategories_id'] > 0) - && ((!isset($input['_users_id_assign']) || !$input['_users_id_assign']) - || (!isset($input['_groups_id_assign']) || !$input['_groups_id_assign']))) { - - $cat = new ITILCategory(); - $cat->getFromDB($input['itilcategories_id']); - if ((!isset($input['_users_id_assign']) || !$input['_users_id_assign']) - && $cat->isField('users_id')) { - $input['_users_id_assign'] = $cat->getField('users_id'); - } - if ((!isset($input['_groups_id_assign']) || !$input['_groups_id_assign']) - && $cat->isField('groups_id')) { - $input['_groups_id_assign'] = $cat->getField('groups_id'); - } - } - if ($item != null) { - // Auto assign tech from item - if ((!isset($input['_users_id_assign']) || ($input['_users_id_assign'] == 0)) - && $item->isField('users_id_tech')) { - $input['_users_id_assign'] = $item->getField('users_id_tech'); - } - // Auto assign group from item - if ((!isset($input['_groups_id_assign']) || ($input['_groups_id_assign'] == 0)) - && $item->isField('groups_id_tech')) { - $input['_groups_id_assign'] = $item->getField('groups_id_tech'); - } - } - break; - } - - // Replay setting auto assign if set in rules engine or by auto_assign_mode - // Do not force status if status has been set by rules - if (((isset($input["_users_id_assign"]) - && ((!is_array($input['_users_id_assign']) && $input["_users_id_assign"] > 0) - || is_array($input['_users_id_assign']) && count($input['_users_id_assign']) > 0)) - || (isset($input["_groups_id_assign"]) - && ((!is_array($input['_groups_id_assign']) && $input["_groups_id_assign"] > 0) - || is_array($input['_groups_id_assign']) && count($input['_groups_id_assign']) > 0)) - || (isset($input["_suppliers_id_assign"]) - && ((!is_array($input['_suppliers_id_assign']) && $input["_suppliers_id_assign"] > 0) - || is_array($input['_suppliers_id_assign']) && count($input['_suppliers_id_assign']) > 0))) - && (in_array($input['status'], $this->getNewStatusArray())) - && !$this->isStatusComputationBlocked($input)) { - $input["status"] = self::ASSIGNED; - } - - // Manage SLA / OLA asignment - // Manual SLA / OLA defined : reset due date - // No manual SLA / OLA and due date defined : reset auto SLA / OLA - foreach ([SLM::TTR, SLM::TTO] as $slmType) { - $this->slaAffect($slmType, $input, $manual_slas_id); - $this->olaAffect($slmType, $input, $manual_olas_id); - } - - // auto set type if not set - if (!isset($input["type"])) { - $input['type'] = Entity::getUsedConfig('tickettype', $input['entities_id'], '', - Ticket::INCIDENT_TYPE); - } - - return $input; - } - - - function post_addItem() { - global $CFG_GLPI; - - $this->manageValidationAdd($this->input); - - // Log this event - $username = 'anonymous'; - if (isset($_SESSION["glpiname"])) { - $username = $_SESSION["glpiname"]; - } - Event::log($this->fields['id'], "ticket", 4, "tracking", - sprintf(__('%1$s adds the item %2$s'), $username, - $this->fields['id'])); - - if (isset($this->input["_followup"]) - && is_array($this->input["_followup"]) - && (strlen($this->input["_followup"]['content']) > 0)) { - - $fup = new ITILFollowup(); - $type = "new"; - if (isset($this->fields["status"]) && ($this->fields["status"] == self::SOLVED)) { - $type = "solved"; - } - $toadd = ['type' => $type, - 'items_id' => $this->fields['id'], - 'itemtype' => 'Ticket']; - - if (isset($this->input["_followup"]['content']) - && (strlen($this->input["_followup"]['content']) > 0)) { - $toadd["content"] = $this->input["_followup"]['content']; - } - - if (isset($this->input["_followup"]['is_private'])) { - $toadd["is_private"] = $this->input["_followup"]['is_private']; - } - // $toadd['_no_notif'] = true; - - $fup->add($toadd); - } - - if ((isset($this->input["plan"]) && count($this->input["plan"])) - || (isset($this->input["actiontime"]) && ($this->input["actiontime"] > 0))) { - - $task = new TicketTask(); - $type = "new"; - if (isset($this->fields["status"]) && ($this->fields["status"] == self::SOLVED)) { - $type = "solved"; - } - $toadd = ["type" => $type, - "tickets_id" => $this->fields['id'], - "actiontime" => $this->input["actiontime"]]; - - if (isset($this->input["plan"]) && count($this->input["plan"])) { - $toadd["plan"] = $this->input["plan"]; - } - - if (isset($_SESSION['glpitask_private'])) { - $toadd['is_private'] = $_SESSION['glpitask_private']; - } - - // $toadd['_no_notif'] = true; - - $task->add($toadd); - } - - $ticket_ticket = new Ticket_Ticket(); - - // From interface - if (isset($this->input['_link'])) { - $this->input['_link']['tickets_id_1'] = $this->fields['id']; - // message if ticket's ID doesn't exist - if (!empty($this->input['_link']['tickets_id_2'])) { - if ($ticket_ticket->can(-1, CREATE, $this->input['_link'])) { - $ticket_ticket->add($this->input['_link']); - } else { - Session::addMessageAfterRedirect(__('Unknown ticket'), false, ERROR); - } - } - } - - // From mailcollector : do not check rights - if (isset($this->input["_linkedto"])) { - $input2 = [ - 'tickets_id_1' => $this->fields['id'], - 'tickets_id_2' => $this->input["_linkedto"], - 'link' => Ticket_Ticket::LINK_TO, - ]; - $ticket_ticket->add($input2); - } - - // Manage SLA / OLA Level : add actions - foreach ([SLM::TTR, SLM::TTO] as $slmType) { - list($dateField, $slaField) = SLA::getFieldNames($slmType); - if (isset($this->input[$slaField]) && ($this->input[$slaField] > 0)) { - $this->manageSlaLevel($this->input[$slaField]); - } - list($dateField, $olaField) = OLA::getFieldNames($slmType); - if (isset($this->input[$olaField]) && ($this->input[$olaField] > 0)) { - $this->manageOlaLevel($this->input[$olaField]); - } - } - - // Add project task link if needed - if (isset($this->input['_projecttasks_id'])) { - $projecttask = new ProjectTask(); - if ($projecttask->getFromDB($this->input['_projecttasks_id'])) { - $pt = new ProjectTask_Ticket(); - $pt->add(['projecttasks_id' => $this->input['_projecttasks_id'], - 'tickets_id' => $this->fields['id'], - /*'_no_notif' => true*/]); - } - } - - if (isset($this->input['_promoted_fup_id']) && $this->input['_promoted_fup_id'] > 0) { - $fup = new ITILFollowup(); - $fup->getFromDB($this->input['_promoted_fup_id']); - $fup->update([ - 'id' => $this->input['_promoted_fup_id'], - 'sourceof_items_id' => $this->getID() - ]); - Event::log($this->getID(), "ticket", 4, "tracking", - sprintf(__('%s promotes a followup from ticket %s'), $_SESSION["glpiname"], $fup->fields['items_id'])); - } - - if (!empty($this->input['items_id'])) { - $item_ticket = new Item_Ticket(); - foreach ($this->input['items_id'] as $itemtype => $items) { - foreach ($items as $items_id) { - $item_ticket->add(['items_id' => $items_id, - 'itemtype' => $itemtype, - 'tickets_id' => $this->fields['id'], - '_disablenotif' => true]); - } - } - } - - parent::post_addItem(); - - // Processing Email - if (!isset($this->input['_disablenotif']) && $CFG_GLPI["use_notifications"]) { - // Clean reload of the ticket - $this->getFromDB($this->fields['id']); - - $type = "new"; - if (isset($this->fields["status"]) && ($this->fields["status"] == self::SOLVED)) { - $type = "solved"; - } - NotificationEvent::raiseEvent($type, $this); - } - - if (isset($_SESSION['glpiis_ids_visible']) && !$_SESSION['glpiis_ids_visible']) { - Session::addMessageAfterRedirect(sprintf(__('%1$s (%2$s)'), - __('Your ticket has been registered, its treatment is in progress.'), - sprintf(__('%1$s: %2$s'), __('Ticket'), - "". - $this->fields['id'].""))); - } - - } - - - /** - * Manage Validation add from input - * - * @since 0.85 - * - * @param $input array : input array - * - * @return boolean - **/ - function manageValidationAdd($input) { - - //Action for send_validation rule - if (isset($input["_add_validation"])) { - if (isset($input['entities_id'])) { - $entid = $input['entities_id']; - } else if (isset($this->fields['entities_id'])) { - $entid = $this->fields['entities_id']; - } else { - return false; - } - - $validations_to_send = []; - if (!is_array($input["_add_validation"])) { - $input["_add_validation"] = [$input["_add_validation"]]; - } - - foreach ($input["_add_validation"] as $key => $validation) { - switch ($validation) { - case 'requester_supervisor' : - if (isset($input['_groups_id_requester']) - && $input['_groups_id_requester']) { - $users = Group_User::getGroupUsers( - $input['_groups_id_requester'], - ['is_manager' => 1] - ); - foreach ($users as $data) { - $validations_to_send[] = $data['id']; - } - } - // Add to already set groups - foreach ($this->getGroups(CommonITILActor::REQUESTER) as $d) { - $users = Group_User::getGroupUsers( - $d['groups_id'], - ['is_manager' => 1] - ); - foreach ($users as $data) { - $validations_to_send[] = $data['id']; - } - } - break; - - case 'assign_supervisor' : - if (isset($input['_groups_id_assign']) - && $input['_groups_id_assign']) { - $users = Group_User::getGroupUsers( - $input['_groups_id_assign'], - ['is_manager' => 1] - ); - foreach ($users as $data) { - $validations_to_send[] = $data['id']; - } - } - foreach ($this->getGroups(CommonITILActor::ASSIGN) as $d) { - $users = Group_User::getGroupUsers( - $d['groups_id'], - ['is_manager' => 1] - ); - foreach ($users as $data) { - $validations_to_send[] = $data['id']; - } - } - break; - - case 'requester_responsible': - if (isset($input['_users_id_requester'])) { - if (is_array($input['_users_id_requester'])) { - foreach ($input['_users_id_requester'] as $users_id) { - $user = new User(); - if ($user->getFromDB($users_id)) { - $validations_to_send[] = $user->getField('users_id_supervisor'); - } - } - } else { - $user = new User(); - if ($user->getFromDB($input['_users_id_requester'])) { - $validations_to_send[] = $user->getField('users_id_supervisor'); - } - } - } - break; - - default : - // Group case from rules - if ($key === 'group') { - foreach ($validation as $groups_id) { - $validation_right = 'validate_incident'; - if (isset($input['type']) - && ($input['type'] == Ticket::DEMAND_TYPE)) { - $validation_right = 'validate_request'; - } - $opt = ['groups_id' => $groups_id, - 'right' => $validation_right, - 'entity' => $entid]; - - $data_users = TicketValidation::getGroupUserHaveRights($opt); - - foreach ($data_users as $user) { - $validations_to_send[] = $user['id']; - } - } - } else { - $validations_to_send[] = $validation; - } - } - - } - - // Validation user added on ticket form - if (isset($input['users_id_validate'])) { - if (array_key_exists('groups_id', $input['users_id_validate'])) { - foreach ($input['users_id_validate'] as $key => $validation_to_add) { - if (is_numeric($key)) { - $validations_to_send[] = $validation_to_add; - } - } - } else { - foreach ($input['users_id_validate'] as $key => $validation_to_add) { - if (is_numeric($key)) { - $validations_to_send[] = $validation_to_add; - } - } - } - } - - // Keep only one - $validations_to_send = array_unique($validations_to_send); - - $validation = new TicketValidation(); - - if (count($validations_to_send)) { - $values = []; - $values['tickets_id'] = $this->fields['id']; - if (isset($input['id']) && $input['id'] != $this->fields['id']) { - $values['_ticket_add'] = true; - } - - // to know update by rules - if (isset($input["_rule_process"])) { - $values['_rule_process'] = $input["_rule_process"]; - } - // if auto_import, tranfert it for validation - if (isset($input['_auto_import'])) { - $values['_auto_import'] = $input['_auto_import']; - } - - // Cron or rule process of hability to do - if (Session::isCron() - || isset($input["_auto_import"]) - || isset($input["_rule_process"]) - || $validation->can(-1, CREATE, $values)) { // cron or allowed user - - $add_done = false; - foreach ($validations_to_send as $user) { - // Do not auto add twice same validation - if (!TicketValidation::alreadyExists($values['tickets_id'], $user)) { - $values["users_id_validate"] = $user; - if ($validation->add($values)) { - $add_done = true; - } - } - } - if ($add_done) { - Event::log($this->fields['id'], "ticket", 4, "tracking", - sprintf(__('%1$s updates the item %2$s'), $_SESSION["glpiname"], - $this->fields['id'])); - } - } - } - } - return true; - } - - - /** - * Get active or solved tickets for an hardware last X days - * - * @since 0.83 - * - * @param $itemtype string Item type - * @param $items_id integer ID of the Item - * @param $days integer day number - * - * @return array - **/ - function getActiveOrSolvedLastDaysTicketsForItem($itemtype, $items_id, $days) { - global $DB; - - $result = []; - - $iterator = $DB->request([ - 'FROM' => $this->getTable(), - 'LEFT JOIN' => [ - 'glpi_items_tickets' => [ - 'ON' => [ - 'glpi_items_tickets' => 'tickets_id', - $this->getTable() => 'id' - ] - ] - ], - 'WHERE' => [ - 'glpi_items_tickets.items_id' => $items_id, - 'glpi_items_tickets.itemtype' => $itemtype, - 'OR' => [ - [ - 'NOT' => [ - $this->getTable() . '.status' => array_merge( - $this->getClosedStatusArray(), - $this->getSolvedStatusArray() - ) - ] - ], - [ - 'NOT' => [$this->getTable() . '.solvedate' => null], - new \QueryExpression( - "ADDDATE(" . $DB->quoteName($this->getTable()) . - ".".$DB->quoteName('solvedate').", INTERVAL $days DAY) > NOW()" - ) - ] - ] - ] - ]); - - while ($tick = $iterator->next()) { - $result[$tick['id']] = $tick['name']; - } - - return $result; - } - - - /** - * Count active tickets for an hardware - * - * @since 0.83 - * - * @param $itemtype string Item type - * @param $items_id integer ID of the Item - * - * @return integer - **/ - function countActiveTicketsForItem($itemtype, $items_id) { - global $DB; - - $result = $DB->request([ - 'COUNT' => 'cpt', - 'FROM' => $this->getTable(), - 'LEFT JOIN' => [ - 'glpi_items_tickets' => [ - 'ON' => [ - 'glpi_items_tickets' => 'tickets_id', - $this->getTable() => 'id' - ] - ] - ], - 'WHERE' => [ - 'glpi_items_tickets.itemtype' => $itemtype, - 'glpi_items_tickets.items_id' => $items_id, - 'NOT' => [ - $this->getTable() . '.status' => array_merge( - $this->getSolvedStatusArray(), - $this->getClosedStatusArray() - ) - ] - ] - ])->next(); - return $result['cpt']; - } - - /** - * Get active tickets for an item - * - * @since 9.5 - * - * @param string $itemtype Item type - * @param integer $items_id ID of the Item - * @param string $type Type of the tickets (incident or request) - * - * @return DBmysqlIterator - */ - public function getActiveTicketsForItem($itemtype, $items_id, $type) { - global $DB; - - return $DB->request([ - 'SELECT' => [ - $this->getTable() . '.id', - $this->getTable() . '.name', - $this->getTable() . '.priority', - ], - 'FROM' => $this->getTable(), - 'LEFT JOIN' => [ - 'glpi_items_tickets' => [ - 'ON' => [ - 'glpi_items_tickets' => 'tickets_id', - $this->getTable() => 'id' - ] - ] - ], - 'WHERE' => [ - 'glpi_items_tickets.itemtype' => $itemtype, - 'glpi_items_tickets.items_id' => $items_id, - $this->getTable() . '.is_deleted' => 0, - $this->getTable() . '.type' => $type, - 'NOT' => [ - $this->getTable() . '.status' => array_merge( - $this->getSolvedStatusArray(), - $this->getClosedStatusArray() - ) - ] - ] - ]); - } - - /** - * Count solved tickets for an hardware last X days - * - * @since 0.83 - * - * @param $itemtype string Item type - * @param $items_id integer ID of the Item - * @param $days integer day number - * - * @return integer - **/ - function countSolvedTicketsForItemLastDays($itemtype, $items_id, $days) { - global $DB; - - $result = $DB->request([ - 'COUNT' => 'cpt', - 'FROM' => $this->getTable(), - 'LEFT JOIN' => [ - 'glpi_items_tickets' => [ - 'ON' => [ - 'glpi_items_tickets' => 'tickets_id', - $this->getTable() => 'id' - ] - ] - ], - 'WHERE' => [ - 'glpi_items_tickets.itemtype' => $itemtype, - 'glpi_items_tickets.items_id' => $items_id, - $this->getTable() . '.status' => array_merge( - $this->getSolvedStatusArray(), - $this->getClosedStatusArray() - ), - new \QueryExpression( - "ADDDATE(".$DB->quoteName($this->getTable().".solvedate").", INTERVAL $days DAY) > NOW()" - ), - 'NOT' => [ - $this->getTable() . '.solvedate' => null - ] - ] - ])->next(); - return $result['cpt']; - } - - - /** - * Update date mod of the ticket - * - * @since 0.83.3 new proto - * - * @param $ID ID of the ticket - * @param $no_stat_computation boolean do not cumpute take into account stat (false by default) - * @param $users_id_lastupdater integer to force last_update id (default 0 = not used) - **/ - function updateDateMod($ID, $no_stat_computation = false, $users_id_lastupdater = 0) { - - if ($this->getFromDB($ID)) { - if (!$no_stat_computation - && !$this->isAlreadyTakenIntoAccount() - && ($this->canTakeIntoAccount() || isCommandLine())) { - return $this->update( - [ - 'id' => $ID, - 'takeintoaccount_delay_stat' => $this->computeTakeIntoAccountDelayStat(), - '_disablenotif' => true - ] - ); - } - - parent::updateDateMod($ID, $no_stat_computation, $users_id_lastupdater); - } - } - - - /** - * Overloaded from commonDBTM - * - * @since 0.83 - * - * @param $type itemtype of object to add - * - * @return rights - **/ - function canAddItem($type) { - - if ($type == 'Document') { - if ($this->getField('status') == self::CLOSED) { - return false; - } - - if ($this->canAddFollowups()) { - return true; - } - } - - // as self::canUpdate & $this->canUpdateItem checks more general rights - // (like STEAL or OWN), - // we specify only the rights needed for this action - return $this->checkEntity() - && (Session::haveRight(self::$rightname, UPDATE) - || $this->canRequesterUpdateItem()); - } - - - /** - * Check if user can add followups to the ticket. - * - * @param integer $user_id - * - * @return boolean - */ - public function canUserAddFollowups($user_id) { - - $entity_id = $this->fields['entities_id']; - - $group_user = new Group_User(); - $user_groups = $group_user->getUserGroups($user_id, ['entities_id' => $entity_id]); - $user_groups_ids = []; - foreach ($user_groups as $user_group) { - $user_groups_ids[] = $user_group['id']; - } - - $rightname = ITILFollowup::$rightname; - - return ( - Profile::haveUserRight($user_id, $rightname, ITILFollowup::ADDMYTICKET, $entity_id) - && ($this->isUser(CommonITILActor::REQUESTER, $user_id) - || ( - isset($this->fields['users_id_recipient']) - && ($this->fields['users_id_recipient'] === $user_id) - ) - ) - ) - || Profile::haveUserRight($user_id, $rightname, ITILFollowup::ADDALLTICKET, $entity_id) - || ( - Profile::haveUserRight($user_id, $rightname, ITILFollowup::ADDGROUPTICKET, $entity_id) - && $this->haveAGroup(CommonITILActor::REQUESTER, $user_groups_ids) - ) - || $this->isUser(CommonITILActor::ASSIGN, Session::getLoginUserID()) - || $this->haveAGroup(CommonITILActor::ASSIGN, $user_groups_ids); - } - - - /** - * Get default values to search engine to override - **/ - static function getDefaultSearchRequest() { - - $search = ['criteria' => [0 => ['field' => 12, - 'searchtype' => 'equals', - 'value' => 'notclosed']], - 'sort' => 19, - 'order' => 'DESC']; - - if (Session::haveRight(self::$rightname, self::READALL)) { - $search['criteria'][0]['value'] = 'notold'; - } - return $search; - } - - - /** - * @see CommonDBTM::getSpecificMassiveActions() - **/ - function getSpecificMassiveActions($checkitem = null) { - - $actions = []; - - if (Session::getCurrentInterface() == 'central') { - if (Ticket::canUpdate() && Ticket::canDelete()) { - $actions[__CLASS__.MassiveAction::CLASS_ACTION_SEPARATOR.'merge_as_followup'] - = "". - __('Merge as Followup'); - } - - if (Item_Ticket::canCreate()) { - $actions['Item_Ticket'.MassiveAction::CLASS_ACTION_SEPARATOR.'add_item'] - = "". - _x('button', 'Add an item'); - } - - if (ITILFollowup::canCreate()) { - $actions['ITILFollowup'.MassiveAction::CLASS_ACTION_SEPARATOR.'add_followup'] - = "". - __('Add a new followup'); - } - - if (TicketTask::canCreate()) { - $actions[__CLASS__.MassiveAction::CLASS_ACTION_SEPARATOR.'add_task'] - = "". - __('Add a new task'); - } - - if (TicketValidation::canCreate()) { - $actions['TicketValidation'.MassiveAction::CLASS_ACTION_SEPARATOR.'submit_validation'] - = "". - __('Approval request'); - } - - if (Item_Ticket::canDelete()) { - $actions['Item_Ticket'.MassiveAction::CLASS_ACTION_SEPARATOR.'delete_item'] - = _x('button', 'Remove an item'); - } - - if (Session::haveRight(self::$rightname, UPDATE)) { - $actions[__CLASS__.MassiveAction::CLASS_ACTION_SEPARATOR.'add_actor'] - = "". - __('Add an actor'); - $actions[__CLASS__.MassiveAction::CLASS_ACTION_SEPARATOR.'update_notif'] - = __('Set notifications for all actors'); - $actions['Ticket_Ticket'.MassiveAction::CLASS_ACTION_SEPARATOR.'add'] - = "". - _x('button', 'Link tickets'); - - KnowbaseItem_Item::getMassiveActionsForItemtype($actions, __CLASS__, 0, $checkitem); - } - } - - $actions += parent::getSpecificMassiveActions($checkitem); - - return $actions; - } - - - static function showMassiveActionsSubForm(MassiveAction $ma) { - switch ($ma->getAction()) { - case 'merge_as_followup' : - $rand = mt_rand(); - $mergeparam = [ - 'name' => "_mergeticket", - 'used' => $ma->items['Ticket'], - 'displaywith' => ['id'], - 'rand' => $rand - ]; - echo ""; - echo "
"; - Ticket::dropdown($mergeparam); - echo "
"; - Html::showCheckbox([ - 'name' => 'with_followups', - 'id' => 'with_followups', - 'checked' => true - ]); - echo ""; - Html::showCheckbox([ - 'name' => 'with_documents', - 'id' => 'with_documents', - 'checked' => true - ]); - echo "
"; - Html::showCheckbox([ - 'name' => 'with_tasks', - 'id' => 'with_tasks', - 'checked' => true - ]); - echo ""; - Html::showCheckbox([ - 'name' => 'with_actors', - 'id' => 'with_actors', - 'checked' => true - ]); - echo "
"; - Dropdown::showFromArray('link_type', [ - 0 => __('None'), - Ticket_Ticket::LINK_TO => __('Linked to'), - Ticket_Ticket::DUPLICATE_WITH => __('Duplicates'), - Ticket_Ticket::SON_OF => __('Son of'), - Ticket_Ticket::PARENT_OF => __('Parent of') - ], ['value' => Ticket_Ticket::SON_OF, 'rand' => $rand]); - echo "
"; - echo Html::submit(_x('button', 'Merge'), [ - 'name' => 'merge', - 'confirm' => __('Confirm the merge? This ticket will be deleted!') - ]); - echo "
"; - return true; - } - return parent::showMassiveActionsSubForm($ma); - } - - - static function processMassiveActionsForOneItemtype(MassiveAction $ma, CommonDBTM $item, - array $ids) { - switch ($ma->getAction()) { - case 'merge_as_followup' : - $input = $ma->getInput(); - $status = []; - $mergeparams = [ - 'linktypes' => [], - 'link_type' => $input['link_type'] - ]; - - if ($input['with_followups']) { - $mergeparams['linktypes'][] = 'ITILFollowup'; - } - if ($input['with_tasks']) { - $mergeparams['linktypes'][] = 'TicketTask'; - } - if ($input['with_documents']) { - $mergeparams['linktypes'][] = 'Document'; - } - if ($input['with_actors']) { - $mergeparams['append_actors'] = [ - CommonITILActor::REQUESTER, - CommonITILActor::OBSERVER, - CommonITILActor::ASSIGN]; - } else { - $mergeparams['append_actors'] = []; - } - - Ticket::merge($input['_mergeticket'], $ids, $status, $mergeparams); - foreach ($status as $id => $status_code) { - if ($status_code == 0) { - $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_OK); - } else if ($status_code == 2) { - $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_NORIGHT); - $ma->addMessage($item->getErrorMessage(ERROR_ON_ACTION)); - } else { - $ma->itemDone($item->getType(), $id, MassiveAction::ACTION_KO); - $ma->addMessage($item->getErrorMessage(ERROR_ON_ACTION)); - } - } - return; - } - parent::processMassiveActionsForOneItemtype($ma, $item, $ids); - } - - - function rawSearchOptions() { - global $DB; - - $tab = []; - - $tab = array_merge($tab, $this->getSearchOptionsMain()); - - $tab[] = [ - 'id' => '155', - 'table' => $this->getTable(), - 'field' => 'time_to_own', - 'name' => __('Time to own'), - 'datatype' => 'datetime', - 'maybefuture' => true, - 'massiveaction' => false, - 'additionalfields' => ['status'] - ]; - - $tab[] = [ - 'id' => '158', - 'table' => $this->getTable(), - 'field' => 'time_to_own', - 'name' => __('Time to own + Progress'), - 'massiveaction' => false, - 'nosearch' => true, - 'additionalfields' => ['status'] - ]; - - $tab[] = [ - 'id' => '159', - 'table' => 'glpi_tickets', - 'field' => 'is_late', - 'name' => __('Time to own exceedeed'), - 'datatype' => 'bool', - 'massiveaction' => false, - 'computation' => 'IF('.$DB->quoteName('TABLE.time_to_own').' IS NOT NULL - AND '.$DB->quoteName('TABLE.status').' <> '.self::WAITING.' - AND ('.$DB->quoteName('TABLE.takeintoaccount_delay_stat').' - > TIME_TO_SEC(TIMEDIFF('.$DB->quoteName('TABLE.time_to_own').', - '.$DB->quoteName('TABLE.date').')) - OR ('.$DB->quoteName('TABLE.takeintoaccount_delay_stat').' = 0 - AND '.$DB->quoteName('TABLE.time_to_own').' < NOW())), - 1, 0)' - ]; - - $tab[] = [ - 'id' => '180', - 'table' => $this->getTable(), - 'field' => 'internal_time_to_resolve', - 'name' => __('Internal time to resolve'), - 'datatype' => 'datetime', - 'maybefuture' => true, - 'massiveaction' => false, - 'additionalfields' => ['status'] - ]; - - $tab[] = [ - 'id' => '181', - 'table' => $this->getTable(), - 'field' => 'internal_time_to_resolve', - 'name' => __('Internal time to resolve + Progress'), - 'massiveaction' => false, - 'nosearch' => true, - 'additionalfields' => ['status'] - ]; - - $tab[] = [ - 'id' => '182', - 'table' => $this->getTable(), - 'field' => 'is_late', - 'name' => __('Internal time to resolve exceedeed'), - 'datatype' => 'bool', - 'massiveaction' => false, - 'computation' => 'IF('.$DB->quoteName('TABLE.internal_time_to_resolve').' IS NOT NULL - AND '.$DB->quoteName('TABLE.status').' <> 4 - AND ('.$DB->quoteName('TABLE.solvedate').' > '.$DB->quoteName('TABLE.internal_time_to_resolve').' - OR ('.$DB->quoteName('TABLE.solvedate').' IS NULL - AND '.$DB->quoteName('TABLE.internal_time_to_resolve').' < NOW())), - 1, 0)' - ]; - - $tab[] = [ - 'id' => '185', - 'table' => $this->getTable(), - 'field' => 'internal_time_to_own', - 'name' => __('Internal time to own'), - 'datatype' => 'datetime', - 'maybefuture' => true, - 'massiveaction' => false, - 'additionalfields' => ['status'] - ]; - - $tab[] = [ - 'id' => '186', - 'table' => $this->getTable(), - 'field' => 'internal_time_to_own', - 'name' => __('Internal time to own + Progress'), - 'massiveaction' => false, - 'nosearch' => true, - 'additionalfields' => ['status'] - ]; - - $tab[] = [ - 'id' => '187', - 'table' => 'glpi_tickets', - 'field' => 'is_late', - 'name' => __('Internal time to own exceedeed'), - 'datatype' => 'bool', - 'massiveaction' => false, - 'computation' => 'IF('.$DB->quoteName('TABLE.internal_time_to_own').' IS NOT NULL - AND '.$DB->quoteName('TABLE.status').' <> '.self::WAITING.' - AND ('.$DB->quoteName('TABLE.takeintoaccount_delay_stat').' - > TIME_TO_SEC(TIMEDIFF('.$DB->quoteName('TABLE.internal_time_to_own').', - '.$DB->quoteName('TABLE.date').')) - OR ('.$DB->quoteName('TABLE.takeintoaccount_delay_stat').' = 0 - AND '.$DB->quoteName('TABLE.internal_time_to_own').' < NOW())), - 1, 0)' - ]; - - $max_date = '99999999'; - $tab[] = [ - 'id' => '188', - 'table' => $this->getTable(), - 'field' => 'next_escalation_level', - 'name' => __('Next escalation level'), - 'datatype' => 'datetime', - 'usehaving' => true, - 'maybefuture' => true, - 'massiveaction' => false, - // Get least value from TTO/TTR fields: - // - use TTO fields only if ticket not already taken into account, - // - use TTR fields only if ticket not already solved, - // - replace NULL or not kept values with 99999999 to be sure that they will not be returned by the LEAST function, - // - replace 99999999 by empty string to keep only valid values. - 'computation' => "REPLACE( - LEAST( - IF(".$DB->quoteName('TABLE.takeintoaccount_delay_stat')." <= 0, - COALESCE(".$DB->quoteName('TABLE.time_to_own').", $max_date), - $max_date), - IF(".$DB->quoteName('TABLE.takeintoaccount_delay_stat')." <= 0, - COALESCE(".$DB->quoteName('TABLE.internal_time_to_own').", $max_date), - $max_date), - IF(".$DB->quoteName('TABLE.solvedate')." IS NULL, - COALESCE(".$DB->quoteName('TABLE.time_to_resolve').", $max_date), - $max_date), - IF(".$DB->quoteName('TABLE.solvedate')." IS NULL, - COALESCE(".$DB->quoteName('TABLE.internal_time_to_resolve').", $max_date), - $max_date) - ), $max_date, '')" - ]; - - $tab[] = [ - 'id' => '14', - 'table' => $this->getTable(), - 'field' => 'type', - 'name' => __('Type'), - 'searchtype' => 'equals', - 'datatype' => 'specific' - ]; - - $tab[] = [ - 'id' => '13', - 'table' => 'glpi_items_tickets', - 'field' => 'items_id', - 'name' => _n('Associated element', 'Associated elements', Session::getPluralNumber()), - 'datatype' => 'specific', - 'comments' => true, - 'nosort' => true, - 'nosearch' => true, - 'additionalfields' => ['itemtype'], - 'joinparams' => [ - 'jointype' => 'child' - ], - 'forcegroupby' => true, - 'massiveaction' => false - ]; - - $tab[] = [ - 'id' => '131', - 'table' => 'glpi_items_tickets', - 'field' => 'itemtype', - 'name' => _n('Associated item type', 'Associated item types', Session::getPluralNumber()), - 'datatype' => 'itemtypename', - 'itemtype_list' => 'ticket_types', - 'nosort' => true, - 'additionalfields' => ['itemtype'], - 'joinparams' => [ - 'jointype' => 'child' - ], - 'forcegroupby' => true, - 'massiveaction' => false - ]; - - $tab[] = [ - 'id' => '9', - 'table' => 'glpi_requesttypes', - 'field' => 'name', - 'name' => __('Request source'), - 'datatype' => 'dropdown' - ]; - - $location_so = Location::rawSearchOptionsToAdd(); - foreach ($location_so as &$so) { - //duplicated search options :( - switch ($so['id']) { - case 3: - $so['id'] = 83; - break; - case 91: - $so['id'] = 84; - break; - case 92: - $so['id'] = 85; - break; - case 93: - $so['id'] = 86; - break; - } - } - $tab = array_merge($tab, $location_so); - - $tab = array_merge($tab, $this->getSearchOptionsActors()); - - $tab[] = [ - 'id' => 'sla', - 'name' => __('SLA') - ]; - - $tab[] = [ - 'id' => '37', - 'table' => 'glpi_slas', - 'field' => 'name', - 'linkfield' => 'slas_id_tto', - 'name' => __('SLA')." ".__('Time to own'), - 'massiveaction' => false, - 'datatype' => 'dropdown', - 'joinparams' => [ - 'condition' => "AND NEWTABLE.`type` = '".SLM::TTO."'" - ], - 'condition' => "`glpi_slas`.`type` = '".SLM::TTO."'" - ]; - - $tab[] = [ - 'id' => '30', - 'table' => 'glpi_slas', - 'field' => 'name', - 'linkfield' => 'slas_id_ttr', - 'name' => __('SLA')." ".__('Time to resolve'), - 'massiveaction' => false, - 'datatype' => 'dropdown', - 'joinparams' => [ - 'condition' => "AND NEWTABLE.`type` = '".SLM::TTR."'" - ], - 'condition' => "`glpi_slas`.`type` = '".SLM::TTR."'" - ]; - - $tab[] = [ - 'id' => '32', - 'table' => 'glpi_slalevels', - 'field' => 'name', - 'name' => __('SLA')." ".__('Escalation level'), - 'massiveaction' => false, - 'datatype' => 'dropdown', - 'joinparams' => [ - 'beforejoin' => [ - 'table' => 'glpi_slalevels_tickets', - 'joinparams' => [ - 'jointype' => 'child' - ] - ] - ], - 'forcegroupby' => true - ]; - - $tab[] = [ - 'id' => 'ola', - 'name' => __('OLA') - ]; - - $tab[] = [ - 'id' => '190', - 'table' => 'glpi_olas', - 'field' => 'name', - 'linkfield' => 'olas_id_tto', - 'name' => __('OLA')." ".__('Internal time to own'), - 'massiveaction' => false, - 'datatype' => 'dropdown', - 'joinparams' => [ - 'condition' => "AND NEWTABLE.`type` = '".SLM::TTO."'" - ], - 'condition' => "`glpi_olas`.`type` = '".SLM::TTO."'" - ]; - - $tab[] = [ - 'id' => '191', - 'table' => 'glpi_olas', - 'field' => 'name', - 'linkfield' => 'olas_id_ttr', - 'name' => __('OLA')." ".__('Internal time to resolve'), - 'massiveaction' => false, - 'datatype' => 'dropdown', - 'joinparams' => [ - 'condition' => "AND NEWTABLE.`type` = '".SLM::TTR."'" - ], - 'condition' => "`glpi_olas`.`type` = '".SLM::TTR."'" - ]; - - $tab[] = [ - 'id' => '192', - 'table' => 'glpi_olalevels', - 'field' => 'name', - 'name' => __('OLA')." ".__('Escalation level'), - 'massiveaction' => false, - 'datatype' => 'dropdown', - 'joinparams' => [ - 'beforejoin' => [ - 'table' => 'glpi_olalevels_tickets', - 'joinparams' => [ - 'jointype' => 'child' - ] - ] - ], - 'forcegroupby' => true - ]; - - $validation_options = TicketValidation::rawSearchOptionsToAdd(); - if (!Session::haveRightsOr( - 'ticketvalidation', - [ - TicketValidation::CREATEINCIDENT, - TicketValidation::CREATEREQUEST - ] - )) { - foreach ($validation_options as &$validation_option) { - if (isset($validation_option['table'])) { - $validation_option['massiveaction'] = false; - } - } - } - $tab = array_merge($tab, $validation_options); - - $tab[] = [ - 'id' => 'satisfaction', - 'name' => __('Satisfaction survey') - ]; - - $tab[] = [ - 'id' => '31', - 'table' => 'glpi_ticketsatisfactions', - 'field' => 'type', - 'name' => __('Type'), - 'massiveaction' => false, - 'searchtype' => ['equals', 'notequals'], - 'searchequalsonfield' => true, - 'joinparams' => [ - 'jointype' => 'child' - ], - 'datatype' => 'specific' - ]; - - $tab[] = [ - 'id' => '60', - 'table' => 'glpi_ticketsatisfactions', - 'field' => 'date_begin', - 'name' => __('Creation date'), - 'datatype' => 'datetime', - 'massiveaction' => false, - 'joinparams' => [ - 'jointype' => 'child' - ] - ]; - - $tab[] = [ - 'id' => '61', - 'table' => 'glpi_ticketsatisfactions', - 'field' => 'date_answered', - 'name' => __('Response date'), - 'datatype' => 'datetime', - 'massiveaction' => false, - 'joinparams' => [ - 'jointype' => 'child' - ] - ]; - - $tab[] = [ - 'id' => '62', - 'table' => 'glpi_ticketsatisfactions', - 'field' => 'satisfaction', - 'name' => __('Satisfaction'), - 'datatype' => 'number', - 'massiveaction' => false, - 'joinparams' => [ - 'jointype' => 'child' - ] - ]; - - $tab[] = [ - 'id' => '63', - 'table' => 'glpi_ticketsatisfactions', - 'field' => 'comment', - 'name' => __('Comments'), - 'datatype' => 'text', - 'massiveaction' => false, - 'joinparams' => [ - 'jointype' => 'child' - ] - ]; - - $tab = array_merge($tab, ITILFollowup::rawSearchOptionsToAdd()); - - $tab = array_merge($tab, TicketTask::rawSearchOptionsToAdd()); - - $tab = array_merge($tab, $this->getSearchOptionsStats()); - - $tab[] = [ - 'id' => '150', - 'table' => $this->getTable(), - 'field' => 'takeintoaccount_delay_stat', - 'name' => __('Take into account time'), - 'datatype' => 'timestamp', - 'forcegroupby' => true, - 'massiveaction' => false - ]; - - if (Session::haveRightsOr(self::$rightname, - [self::READALL, self::READASSIGN, self::OWN])) { - $tab[] = [ - 'id' => 'linktickets', - 'name' => _n('Linked ticket', 'Linked tickets', Session::getPluralNumber()) - ]; - - $tab[] = [ - 'id' => '40', - 'table' => 'glpi_tickets_tickets', - 'field' => 'tickets_id_1', - 'name' => __('All linked tickets'), - 'massiveaction' => false, - 'forcegroupby' => true, - 'searchtype' => 'equals', - 'joinparams' => [ - 'jointype' => 'item_item' - ], - 'additionalfields' => ['tickets_id_2'] - ]; - - $tab[] = [ - 'id' => '47', - 'table' => 'glpi_tickets_tickets', - 'field' => 'tickets_id_1', - 'name' => __('Duplicated tickets'), - 'massiveaction' => false, - 'searchtype' => 'equals', - 'joinparams' => [ - 'jointype' => 'item_item', - 'condition' => 'AND NEWTABLE.`link` = '.Ticket_Ticket::DUPLICATE_WITH - ], - 'additionalfields' => ['tickets_id_2'], - 'forcegroupby' => true - ]; - - $tab[] = [ - 'id' => '41', - 'table' => 'glpi_tickets_tickets', - 'field' => 'id', - 'name' => __('Number of all linked tickets'), - 'massiveaction' => false, - 'datatype' => 'count', - 'usehaving' => true, - 'joinparams' => [ - 'jointype' => 'item_item' - ] - ]; - - $tab[] = [ - 'id' => '46', - 'table' => 'glpi_tickets_tickets', - 'field' => 'id', - 'name' => __('Number of duplicated tickets'), - 'massiveaction' => false, - 'datatype' => 'count', - 'usehaving' => true, - 'joinparams' => [ - 'jointype' => 'item_item', - 'condition' => 'AND NEWTABLE.`link` = '.Ticket_Ticket::DUPLICATE_WITH - ] - ]; - - $tab[] = [ - 'id' => '50', - 'table' => 'glpi_tickets', - 'field' => 'id', - 'linkfield' => 'tickets_id_2', - 'name' => __('Parent tickets'), - 'massiveaction' => false, - 'searchtype' => 'equals', - 'datatype' => 'itemlink', - 'usehaving' => true, - 'joinparams' => [ - 'beforejoin' => [ - 'table' => 'glpi_tickets_tickets', - 'joinparams' => [ - 'jointype' => 'child', - 'linkfield' => 'tickets_id_1', - 'condition' => 'AND NEWTABLE.`link` = '.Ticket_Ticket::SON_OF, - ] - ] - ], - 'forcegroupby' => true - ]; - - $tab[] = [ - 'id' => '67', - 'table' => 'glpi_tickets', - 'field' => 'id', - 'linkfield' => 'tickets_id_1', - 'name' => __('Child tickets'), - 'massiveaction' => false, - 'searchtype' => 'equals', - 'datatype' => 'itemlink', - 'usehaving' => true, - 'joinparams' => [ - 'beforejoin' => [ - 'table' => 'glpi_tickets_tickets', - 'joinparams' => [ - 'jointype' => 'child', - 'linkfield' => 'tickets_id_2', - 'condition' => 'AND NEWTABLE.`link` = '.Ticket_Ticket::SON_OF, - ] - ] - ], - 'forcegroupby' => true - ]; - - $tab[] = [ - 'id' => '68', - 'table' => 'glpi_tickets_tickets', - 'field' => 'id', - 'name' => __('Number of sons tickets'), - 'massiveaction' => false, - 'datatype' => 'count', - 'usehaving' => true, - 'joinparams' => [ - 'linkfield' => 'tickets_id_2', - 'jointype' => 'child', - 'condition' => 'AND NEWTABLE.`link` = '.Ticket_Ticket::SON_OF - ], - 'forcegroupby' => true - ]; - - $tab[] = [ - 'id' => '69', - 'table' => 'glpi_tickets_tickets', - 'field' => 'id', - 'name' => __('Number of parent tickets'), - 'massiveaction' => false, - 'datatype' => 'count', - 'usehaving' => true, - 'joinparams' => [ - 'linkfield' => 'tickets_id_1', - 'jointype' => 'child', - 'condition' => 'AND NEWTABLE.`link` = '.Ticket_Ticket::SON_OF - ], - 'additionalfields' => ['tickets_id_2'] - ]; - - $tab = array_merge($tab, $this->getSearchOptionsSolution()); - - if (Session::haveRight('ticketcost', READ)) { - $tab = array_merge($tab, TicketCost::rawSearchOptionsToAdd()); - } - } - - if (Session::haveRight('problem', READ)) { - $tab[] = [ - 'id' => 'problem', - 'name' => __('Problems') - ]; - - $tab[] = [ - 'id' => '141', - 'table' => 'glpi_problems_tickets', - 'field' => 'id', - 'name' => _x('quantity', 'Number of problems'), - 'forcegroupby' => true, - 'usehaving' => true, - 'datatype' => 'count', - 'massiveaction' => false, - 'joinparams' => [ - 'jointype' => 'child' - ] - ]; - } - - // Filter search fields for helpdesk - if (!Session::isCron() // no filter for cron - && (Session::getCurrentInterface() != 'central')) { - $tokeep = ['common', 'requester','satisfaction']; - if (Session::haveRightsOr('ticketvalidation', - array_merge(TicketValidation::getValidateRights(), - TicketValidation::getCreateRights()))) { - $tokeep[] = 'validation'; - } - $keep = false; - foreach ($tab as $key => &$val) { - if (!isset($val['table'])) { - $keep = in_array($val['id'], $tokeep); - } - if (!$keep) { - if (isset($val['table'])) { - $val['nosearch'] = true; - } - } - } - } - return $tab; - } - - - /** - * @since 0.84 - * - * @param $field - * @param $values - * @param $options array - **/ - static function getSpecificValueToDisplay($field, $values, array $options = []) { - - if (!is_array($values)) { - $values = [$field => $values]; - } - switch ($field) { - case 'content' : - $content = Toolbox::unclean_cross_side_scripting_deep(Html::entity_decode_deep($values[$field])); - $content = Html::clean($content); - if (empty($content)) { - $content = ' '; - } - return nl2br($content); - - case 'type': - return self::getTicketTypeName($values[$field]); - } - return parent::getSpecificValueToDisplay($field, $values, $options); - } - - - /** - * @since 0.84 - * - * @param $field - * @param $name (default '') - * @param $values (default '') - * @param $options array - * - * @return string - **/ - static function getSpecificValueToSelect($field, $name = '', $values = '', array $options = []) { - - if (!is_array($values)) { - $values = [$field => $values]; - } - $options['display'] = false; - switch ($field) { - case 'content' : - return ""; - - case 'type': - $options['value'] = $values[$field]; - return self::dropdownType($name, $options); - } - return parent::getSpecificValueToSelect($field, $name, $values, $options); - } - - - /** - * Dropdown of ticket type - * - * @param $name select name - * @param $options array of options: - * - value : integer / preselected value (default 0) - * - toadd : array / array of specific values to add at the begining - * - on_change : string / value to transmit to "onChange" - * - display : boolean / display or get string (default true) - * - * @return string id of the select - **/ - static function dropdownType($name, $options = []) { - - $params = [ - 'value' => 0, - 'toadd' => [], - 'on_change' => '', - 'display' => true, - ]; - - if (is_array($options) && count($options)) { - foreach ($options as $key => $val) { - $params[$key] = $val; - } - } - - $items = []; - if (count($params['toadd']) > 0) { - $items = $params['toadd']; - } - - $items += self::getTypes(); - - return Dropdown::showFromArray($name, $items, $params); - } - - - /** - * Get ticket types - * - * @return array of types - **/ - static function getTypes() { - - $options = [ - self::INCIDENT_TYPE => __('Incident'), - self::DEMAND_TYPE => __('Request'), - ]; - - return $options; - } - - - /** - * Get ticket type Name - * - * @param $value type ID - **/ - static function getTicketTypeName($value) { - - switch ($value) { - case self::INCIDENT_TYPE : - return __('Incident'); - - case self::DEMAND_TYPE : - return __('Request'); - - default : - // Return $value if not defined - return $value; - } - } - - - /** - * get the Ticket status list - * - * @param $withmetaforsearch boolean (false by default) - * - * @return array - **/ - static function getAllStatusArray($withmetaforsearch = false) { - - // To be overridden by class - $tab = [self::INCOMING => _x('status', 'New'), - self::ASSIGNED => _x('status', 'Processing (assigned)'), - self::PLANNED => _x('status', 'Processing (planned)'), - self::WAITING => __('Pending'), - self::SOLVED => _x('status', 'Solved'), - self::CLOSED => _x('status', 'Closed')]; - - if ($withmetaforsearch) { - $tab['notold'] = _x('status', 'Not solved'); - $tab['notclosed'] = _x('status', 'Not closed'); - $tab['process'] = __('Processing'); - $tab['old'] = _x('status', 'Solved + Closed'); - $tab['all'] = __('All'); - } - return $tab; - } - - - /** - * Get the ITIL object closed status list - * - * @since 0.83 - * - * @return array - **/ - static function getClosedStatusArray() { - return [self::CLOSED]; - } - - - /** - * Get the ITIL object solved status list - * - * @since 0.83 - * - * @return array - **/ - static function getSolvedStatusArray() { - return [self::SOLVED]; - } - - /** - * Get the ITIL object new status list - * - * @since 0.83.8 - * - * @return array - **/ - static function getNewStatusArray() { - return [self::INCOMING]; - } - - /** - * Get the ITIL object assign or plan status list - * - * @since 0.83 - * - * @return array - **/ - static function getProcessStatusArray() { - return [self::ASSIGNED, self::PLANNED]; - } - - - /** - * Calculate Ticket TCO for an item - * - *@param $item CommonDBTM object of the item - * - *@return float - **/ - static function computeTco(CommonDBTM $item) { - global $DB; - - $totalcost = 0; - - $iterator = $DB->request([ - 'SELECT' => 'glpi_ticketcosts.*', - 'FROM' => 'glpi_ticketcosts', - 'LEFT JOIN' => [ - 'glpi_items_tickets' => [ - 'ON' => [ - 'glpi_items_tickets' => 'tickets_id', - 'glpi_ticketcosts' => 'tickets_id' - ] - ] - ], - 'WHERE' => [ - 'glpi_items_tickets.itemtype' => get_class($item), - 'glpi_items_tickets.items_id' => $item->getField('id'), - 'OR' => [ - 'glpi_ticketcosts.cost_time' => ['>', 0], - 'glpi_ticketcosts.cost_fixed' => ['>', 0], - 'glpi_ticketcosts.cost_material' => ['>', 0] - ] - ] - ]); - - while ($data = $iterator->next()) { - $totalcost += TicketCost::computeTotalCost( - $data["actiontime"], - $data["cost_time"], - $data["cost_fixed"], - $data["cost_material"] - ); - } - return $totalcost; - } - - - /** - * Print the helpdesk form - * - * @param $ID integer ID of the user who want to display the Helpdesk - * @param $ticket_template boolean ticket template for preview : false if not used for preview - * (false by default) - * - * @return void - **/ - function showFormHelpdesk($ID, $ticket_template = false) { - global $CFG_GLPI; - - if (!self::canCreate()) { - return false; - } - - if (!$ticket_template - && Session::haveRightsOr('ticketvalidation', TicketValidation::getValidateRights())) { - - $opt = []; - $opt['reset'] = 'reset'; - $opt['criteria'][0]['field'] = 55; // validation status - $opt['criteria'][0]['searchtype'] = 'equals'; - $opt['criteria'][0]['value'] = CommonITILValidation::WAITING; - $opt['criteria'][0]['link'] = 'AND'; - - $opt['criteria'][1]['field'] = 59; // validation aprobator - $opt['criteria'][1]['searchtype'] = 'equals'; - $opt['criteria'][1]['value'] = Session::getLoginUserID(); - $opt['criteria'][1]['link'] = 'AND'; - - $url_validate = Ticket::getSearchURL()."?".Toolbox::append_params($opt, - '&'); - - if (TicketValidation::getNumberToValidate(Session::getLoginUserID()) > 0) { - echo "". - __('Tickets awaiting approval')."

"; - } - } - - $email = UserEmail::getDefaultForUser($ID); - $default_use_notif = Entity::getUsedConfig('is_notif_enable_default', $_SESSION['glpiactive_entity'], '', 1); - - // Set default values... - $default_values = ['_users_id_requester_notif' - => ['use_notification' - => (($email == "")?0:$default_use_notif)], - 'nodelegate' => 1, - '_users_id_requester' => 0, - '_users_id_observer' => [0], - '_users_id_observer_notif' - => ['use_notification' => $default_use_notif], - 'name' => '', - 'content' => '', - 'itilcategories_id' => 0, - 'locations_id' => 0, - 'urgency' => 3, - 'items_id' => 0, - 'entities_id' => $_SESSION['glpiactive_entity'], - 'plan' => [], - 'global_validation' => CommonITILValidation::NONE, - '_add_validation' => 0, - 'type' => Entity::getUsedConfig('tickettype', - $_SESSION['glpiactive_entity'], - '', Ticket::INCIDENT_TYPE), - '_right' => "id", - '_content' => [], - '_tag_content' => [], - '_filename' => [], - '_tag_filename' => [], - '_tasktemplates_id' => []]; - - // Get default values from posted values on reload form - if (!$ticket_template) { - if (isset($_POST)) { - $options = $_POST; - } - } - - if (isset($options['name'])) { - $order = ["\\'", '\\"', "\\\\"]; - $replace = ["'", '"', "\\"]; - $options['name'] = str_replace($order, $replace, $options['name']); - } - - // Restore saved value or override with page parameter - $saved = $this->restoreInput(); - foreach ($default_values as $name => $value) { - if (!isset($options[$name])) { - if (isset($saved[$name])) { - $options[$name] = $saved[$name]; - } else { - $options[$name] = $value; - } - } - } - - // Check category / type validity - if ($options['itilcategories_id']) { - $cat = new ITILCategory(); - if ($cat->getFromDB($options['itilcategories_id'])) { - switch ($options['type']) { - case self::INCIDENT_TYPE : - if (!$cat->getField('is_incident')) { - $options['itilcategories_id'] = 0; - } - break; - - case self::DEMAND_TYPE : - if (!$cat->getField('is_request')) { - $options['itilcategories_id'] = 0; - } - break; - - default : - break; - } - } - } - - // Load ticket template if available : - $tt = $this->getITILTemplateToUse($ticket_template, $options['type'], - $options['itilcategories_id'], - $_SESSION["glpiactive_entity"]); - - // Put ticket template on $options for actors - $options['_tickettemplate'] = $tt; - - if (!$ticket_template) { - echo "
"; - } - - $delegating = User::getDelegateGroupsForUser($options['entities_id']); - - if (count($delegating) || $CFG_GLPI['use_check_pref']) { - echo "
"; - } - - if (count($delegating)) { - echo ""; - echo ""; - - echo "
".__('This ticket concerns me')." "; - - $rand = Dropdown::showYesNo("nodelegate", $options['nodelegate']); - - $params = ['nodelegate' => '__VALUE__', - 'rand' => $rand, - 'right' => "delegate", - '_users_id_requester' - => $options['_users_id_requester'], - '_users_id_requester_notif' - => $options['_users_id_requester_notif'], - 'use_notification' - => $options['_users_id_requester_notif']['use_notification'], - 'entity_restrict' - => $_SESSION["glpiactive_entity"]]; - - Ajax::UpdateItemOnSelectEvent("dropdown_nodelegate".$rand, "show_result".$rand, - $CFG_GLPI["root_doc"]."/ajax/dropdownDelegationUsers.php", - $params); - - $class = 'right'; - if ($CFG_GLPI['use_check_pref'] && $options['nodelegate']) { - echo "".__('Check your personnal information'); - $class = 'center'; - } - - echo "
"; - echo "
"; - - $self = new self(); - if ($options["_users_id_requester"] == 0) { - $options['_users_id_requester'] = Session::getLoginUserID(); - } else { - $options['_right'] = "delegate"; - } - $self->showActorAddFormOnCreate(CommonITILActor::REQUESTER, $options); - echo "
"; - if ($CFG_GLPI['use_check_pref'] && $options['nodelegate']) { - echo "
"; - User::showPersonalInformation(Session::getLoginUserID()); - } - echo "
"; - echo ""; - - } else { - // User as requester - $options['_users_id_requester'] = Session::getLoginUserID(); - - if ($CFG_GLPI['use_check_pref']) { - echo "".__('Check your personnal information').""; - echo ""; - User::showPersonalInformation(Session::getLoginUserID()); - echo ""; - echo ""; - } - } - - echo ""; - echo ""; - - // Predefined fields from template : reset them - if (isset($options['_predefined_fields'])) { - $options['_predefined_fields'] - = Toolbox::decodeArrayFromInput($options['_predefined_fields']); - } else { - $options['_predefined_fields'] = []; - } - - // Store predefined fields to be able not to take into account on change template - $predefined_fields = []; - $key = $this->getTemplateFormFieldName(); - - if (isset($tt->predefined) && count($tt->predefined)) { - foreach ($tt->predefined as $predeffield => $predefvalue) { - if (isset($options[$predeffield]) && isset($default_values[$predeffield])) { - // Is always default value : not set - // Set if already predefined field - // Set if ticket template change - if (((count($options['_predefined_fields']) == 0) - && ($options[$predeffield] == $default_values[$predeffield])) - || (isset($options['_predefined_fields'][$predeffield]) - && ($options[$predeffield] == $options['_predefined_fields'][$predeffield])) - || (isset($options[$key]) - && ($options[$key] != $tt->getID()))) { - $options[$predeffield] = $predefvalue; - $predefined_fields[$predeffield] = $predefvalue; - } - } else { // Not defined options set as hidden field - echo ""; - } - } - // All predefined override : add option to say predifined exists - if (count($predefined_fields) == 0) { - $predefined_fields['_all_predefined_override'] = 1; - } - } else { // No template load : reset predefined values - if (count($options['_predefined_fields'])) { - foreach ($options['_predefined_fields'] as $predeffield => $predefvalue) { - if ($options[$predeffield] == $predefvalue) { - $options[$predeffield] = $default_values[$predeffield]; - } - } - } - } - - if (isset($options['_tasktemplates_id'])) { - foreach ($options['_tasktemplates_id'] as $tasktemplates_id) { - echo ""; - } - } - - if (($CFG_GLPI['urgency_mask'] == (1<<3)) - || $tt->isHiddenField('urgency')) { - // Dont show dropdown if only 1 value enabled or field is hidden - echo ""; - } - - // Display predefined fields if hidden - if ($tt->isHiddenField('items_id')) { - if (!empty($options['items_id'])) { - foreach ($options['items_id'] as $itemtype => $items) { - foreach ($items as $items_id) { - echo ""; - } - } - } - } - if ($tt->isHiddenField('locations_id')) { - echo ""; - } - echo ""; - echo "
"; - - Plugin::doHook("pre_item_form", ['item' => $this, 'options' => &$options]); - - echo ""; - - echo ""; - echo ""; - echo ""; - - echo ""; - echo ""; - echo ""; - - if ($CFG_GLPI['urgency_mask'] != (1<<3)) { - if (!$tt->isHiddenField('urgency')) { - echo ""; - echo ""; - echo ""; - } - } - -/** if (empty($delegating) - && NotificationTargetTicket::isAuthorMailingActivatedForHelpdesk()) { - echo ""; - echo ""; - echo ""; - } -*/ - if (($_SESSION["glpiactiveprofile"]["helpdesk_hardware"] != 0) - && (count($_SESSION["glpiactiveprofile"]["helpdesk_item_type"]))) { - if (!$tt->isHiddenField('items_id')) { - echo ""; - echo ""; - echo ""; - } - } - - if (!$tt->isHiddenField('locations_id')) { - echo ""; - } - - if (!$tt->isHiddenField('_users_id_observer') - || $tt->isPredefinedField('_users_id_observer')) { - echo ""; - echo ""; - echo ""; - } - - if (!$tt->isHiddenField('name') - || $tt->isPredefinedField('name')) { - echo ""; - echo ""; - } - - if (!$tt->isHiddenField('content') - || $tt->isPredefinedField('content')) { - echo ""; - echo ""; - } - Plugin::doHook("post_item_form", ['item' => $this, 'options' => &$options]); - - if (!$ticket_template) { - echo ""; - echo ""; - } - - echo "
".__('Describe the incident or request').""; - if (Session::isMultiEntitiesMode()) { - echo "(".Dropdown::getDropdownName("glpi_entities", $_SESSION["glpiactive_entity"]).")"; - } - echo "
".sprintf(__('%1$s%2$s'), __('Type'), $tt->getMandatoryMark('type')).""; - self::dropdownType('type', ['value' => $options['type'], - 'on_change' => 'this.form.submit()']); - echo "
".sprintf(__('%1$s%2$s'), __('Category'), - $tt->getMandatoryMark('itilcategories_id')).""; - - $condition = ['is_helpdeskvisible' => 1]; - switch ($options['type']) { - case self::DEMAND_TYPE : - $condition['is_request'] = 1; - break; - default: // self::INCIDENT_TYPE : - $condition['is_incident'] = 1; - } - $opt = ['value' => $options['itilcategories_id'], - 'condition' => $condition, - 'entity' => $_SESSION["glpiactive_entity"], - 'on_change' => 'this.form.submit()']; - - if ($options['itilcategories_id'] && $tt->isMandatoryField("itilcategories_id")) { - $opt['display_emptychoice'] = false; - } - - ITILCategory::dropdown($opt); - echo "
".sprintf(__('%1$s%2$s'), __('Urgency'), $tt->getMandatoryMark('urgency')). - ""; - self::dropdownUrgency(['value' => $options["urgency"]]); - echo "
".__('Inform me about the actions taken').""; - if ($options["_users_id_requester"] == 0) { - $options['_users_id_requester'] = Session::getLoginUserID(); - } - $_POST['value'] = $options['_users_id_requester']; - $_POST['field'] = '_users_id_requester_notif'; - $_POST['use_notification'] = $options['_users_id_requester_notif']['use_notification']; - include (GLPI_ROOT."/ajax/uemailUpdate.php"); - - echo "
".sprintf(__('%1$s%2$s'), _n('Associated element', 'Associated elements', Session::getPluralNumber()), - $tt->getMandatoryMark('items_id')).""; - $options['_canupdate'] = Session::haveRight('ticket', CREATE); - Item_Ticket::itemAddForm($this, $options); - echo "
"; - printf(__('%1$s%2$s'), __('Location'), $tt->getMandatoryMark('locations_id')); - echo ""; - Location::dropdown(['value' => $options["locations_id"]]); - echo "
".sprintf(__('%1$s%2$s'), _n('Watcher', 'Watchers', 2), - $tt->getMandatoryMark('_users_id_observer')).""; - $options['_right'] = "all"; - - if (!$tt->isHiddenField('_users_id_observer')) { - // Observer - - if ($tt->isPredefinedField('_users_id_observer') - && !is_array($options['_users_id_observer'])) { - - //convert predefined value to array - $options['_users_id_observer'] = [$options['_users_id_observer']]; - $options['_users_id_observer_notif']['use_notification'] = - [$options['_users_id_observer_notif']['use_notification']]; - - // add new line to permit adding more observers - $options['_users_id_observer'][1] = 0; - $options['_users_id_observer_notif']['use_notification'][1] = 1; - } - - echo "
"; - if (isset($options['_users_id_observer'])) { - $observers = $options['_users_id_observer']; - foreach ($observers as $index_observer => $observer) { - $options = array_merge($options, ['_user_index' => $index_observer]); - self::showFormHelpdeskObserver($options); - } - } - echo "
"; - - } else { // predefined value - if (isset($options["_users_id_observer"]) && $options["_users_id_observer"]) { - echo self::getActorIcon('user', CommonITILActor::OBSERVER)." "; - echo Dropdown::getDropdownName("glpi_users", $options["_users_id_observer"]); - echo ""; - } - } - echo "
".sprintf(__('%1$s%2$s'), __('Title'), $tt->getMandatoryMark('name')).""; - if (!$tt->isHiddenField('name')) { - $opt = [ - 'value' => $options['name'], - 'maxlength' => 250, - 'size' => 80, - ]; - - if ($tt->isMandatoryField('name')) { - $opt['required'] = 'required'; - } - echo Html::input('name', $opt); - } else { - echo $options['name']; - echo ""; - } - echo "
".sprintf(__('%1$s%2$s'), __('Description'), $tt->getMandatoryMark('content')); - - $rand = mt_rand(); - $rand_text = mt_rand(); - - $cols = 100; - $rows = 10; - $content_id = "content$rand"; - echo ""; - - $content = $options['content']; - if (!$ticket_template) { - $content = Html::cleanPostForTextArea($options['content']); - } - $content = Html::setRichTextContent($content_id, $content, $rand); - - echo "
"; - $uploads = []; - if (isset($options['_content'])) { - $uploads['_content'] = $options['_content']; - $uploads['_tag_content'] = $options['_tag_content']; - } - Html::textarea([ - 'name' => 'content', - 'filecontainer' => 'content_info', - 'editor_id' => $content_id, - 'required' => $tt->isMandatoryField('content'), - 'cols' => $cols, - 'rows' => $rows, - 'enable_richtext' => true, - 'value' => $content, - 'uploads' => $uploads, - ]); - echo "
"; - - if (!$tt->isHiddenField('_documents_id')) { - if (isset($options['_filename'])) { - $uploads['_filename'] = $options['_filename']; - $uploads['_tag_filename'] = $options['_tag_filename']; - } - Html::file([ - // 'editor_id' => $content_id, - 'showtitle' => false, - 'multiple' => true, - 'uploads' => $uploads, - ]); - } - - echo "
"; - - if ($tt->isField('id') && ($tt->fields['id'] > 0)) { - echo ""; - echo ""; - } - echo ""; - echo "
"; - if (!$ticket_template) { - Html::closeForm(); - } - } - - /** - * Display a single oberver selector - * - * * @param $options array options for default values ($options of showActorAddFormOnCreate) - **/ - static function showFormHelpdeskObserver($options = []) { - global $CFG_GLPI; - - //default values - $ticket = new Ticket(); - $params = [ - '_users_id_observer_notif' => [ - 'use_notification' => true - ], - '_users_id_observer' => 0, - 'entities_id' => $_SESSION["glpiactive_entity"], - '_right' => "all", - ]; - - // overide default value by function parameters - if (is_array($options) && count($options)) { - foreach ($options as $key => $val) { - $params[$key] = $val; - } - } - - // add a user selector - $rand_observer = $ticket->showActorAddFormOnCreate(CommonITILActor::OBSERVER, $params); - - if (isset($params['_tickettemplate'])) { - // Replace template object by ID for ajax - $params['_tickettemplate'] = $params['_tickettemplate']->getID(); - } - - // add an additionnal observer on user selection - Ajax::updateItemOnSelectEvent("dropdown__users_id_observer[]$rand_observer", - "observer_$rand_observer", - $CFG_GLPI["root_doc"]."/ajax/helpdesk_observer.php", - $params); - - //remove 'new observer' anchor on user selection - echo Html::scriptBlock(" - $('#dropdown__users_id_observer__$rand_observer').on('change', function(event) { - $('#addObserver$rand_observer').remove(); - });"); - - // add "new observer" anchor - echo ""; - echo Html::image($CFG_GLPI['root_doc']."/pics/meta_plus.png", ['alt' => __('Add')]); - echo ""; - - // add an additionnal observer on anchor click - Ajax::updateItemOnEvent("addObserver$rand_observer", - "observer_$rand_observer", - $CFG_GLPI["root_doc"]."/ajax/helpdesk_observer.php", - $params, ['click']); - - // div for an additionnal observer - echo "
"; - - } - - static function getDefaultValues($entity = 0) { - global $CFG_GLPI; - - if (is_numeric(Session::getLoginUserID(false))) { - $users_id_requester = Session::getLoginUserID(); - $users_id_assign = Session::getLoginUserID(); - // No default requester if own ticket right = tech and update_ticket right to update requester - if (Session::haveRightsOr(self::$rightname, [UPDATE, self::OWN]) && !$_SESSION['glpiset_default_requester']) { - $users_id_requester = 0; - } - if (!Session::haveRight(self::$rightname, self::OWN) || !$_SESSION['glpiset_default_tech']) { - $users_id_assign = 0; - } - $entity = $_SESSION['glpiactive_entity']; - $requesttype = $_SESSION['glpidefault_requesttypes_id']; - } else { - $users_id_requester = 0; - $users_id_assign = 0; - $requesttype = $CFG_GLPI['default_requesttypes_id']; - } - - $type = Entity::getUsedConfig('tickettype', $entity, '', Ticket::INCIDENT_TYPE); - - $default_use_notif = Entity::getUsedConfig('is_notif_enable_default', $entity, '', 1); - - // Set default values... - return ['_users_id_requester' => $users_id_requester, - '_users_id_requester_notif' => ['use_notification' => [$default_use_notif], - 'alternative_email' => ['']], - '_groups_id_requester' => 0, - '_users_id_assign' => $users_id_assign, - '_users_id_assign_notif' => ['use_notification' => [$default_use_notif], - 'alternative_email' => ['']], - '_groups_id_assign' => 0, - '_users_id_observer' => 0, - '_users_id_observer_notif' => ['use_notification' => [$default_use_notif], - 'alternative_email' => ['']], - '_groups_id_observer' => 0, - '_link' => ['tickets_id_2' => '', - 'link' => ''], - '_suppliers_id_assign' => 0, - '_suppliers_id_assign_notif' => ['use_notification' => [$default_use_notif], - 'alternative_email' => ['']], - 'name' => '', - 'content' => '', - 'itilcategories_id' => 0, - 'urgency' => 3, - 'impact' => 3, - 'priority' => self::computePriority(3, 3), - 'requesttypes_id' => $requesttype, - 'actiontime' => 0, - 'date' => null, - 'entities_id' => $entity, - 'status' => self::INCOMING, - 'followup' => [], - 'itemtype' => '', - 'items_id' => 0, - 'locations_id' => 0, - 'plan' => [], - 'global_validation' => CommonITILValidation::NONE, - 'time_to_resolve' => 'NULL', - 'time_to_own' => 'NULL', - 'slas_id_tto' => 0, - 'slas_id_ttr' => 0, - 'internal_time_to_resolve' => 'NULL', - 'internal_time_to_own' => 'NULL', - 'olas_id_tto' => 0, - 'olas_id_ttr' => 0, - '_add_validation' => 0, - 'users_id_validate' => [], - 'type' => $type, - '_documents_id' => [], - '_tasktemplates_id' => [], - '_content' => [], - '_tag_content' => [], - '_filename' => [], - '_tag_filename' => []]; - } - - /** - * Get ticket template to use - * Use force_template first, then try on template define for type and category - * then use default template of active profile of connected user and then use default entity one - * - * @param $force_template integer itiltemplate_id to used (case of preview for example) - * (default 0) - * @param $type integer type of the ticket (default 0) - * @param $itilcategories_id integer ticket category (default 0) - * @param $entities_id integer (default -1) - * - * @since 0.84 - * @deprecated 9.5.0 - * - * @return ticket template object - **/ - function getTicketTemplateToUse($force_template = 0, $type = 0, $itilcategories_id = 0, - $entities_id = -1) { - Toolbox::deprecated('Use getITILTemplateToUse()'); - return $this->getITILTemplateToUse( - $force_template, - $type, - $itilcategories_id, - $entities_id - ); - } - - - function showForm($ID, $options = []) { - global $CFG_GLPI; - - if (isset($options['_add_fromitem']) && isset($options['itemtype'])) { - $item = new $options['itemtype']; - $item->getFromDB($options['items_id'][$options['itemtype']][0]); - $options['entities_id'] = $item->fields['entities_id']; - } - - $default_values = self::getDefaultValues(); - - // Restore saved value or override with page parameter - $saved = $this->restoreInput(); - - foreach ($default_values as $name => $value) { - if (!isset($options[$name])) { - if (isset($saved[$name])) { - $options[$name] = $saved[$name]; - } else { - $options[$name] = $value; - } - } - } - - if (isset($options['content'])) { - // Clean new lines to be fix encoding - $order = ['\\r', '\\n', "\\'", '\\"', "\\\\"]; - $replace = ["", "", "'", '"', "\\"]; - $options['content'] = str_replace($order, $replace, $options['content']); - } - if (isset($options['name'])) { - $order = ["\\'", '\\"', "\\\\"]; - $replace = ["'", '"', "\\"]; - $options['name'] = str_replace($order, $replace, $options['name']); - } - - if (!isset($options['_skip_promoted_fields'])) { - $options['_skip_promoted_fields'] = false; - } - - if (!$ID) { - // Override defaut values from projecttask if needed - if (isset($options['_projecttasks_id'])) { - $pt = new ProjectTask(); - if ($pt->getFromDB($options['_projecttasks_id'])) { - $options['name'] = $pt->getField('name'); - $options['content'] = $pt->getField('name'); - } - } - // Override defaut values from followup if needed - if (isset($options['_promoted_fup_id']) && !$options['_skip_promoted_fields']) { - $fup = new ITILFollowup(); - if ($fup->getFromDB($options['_promoted_fup_id'])) { - $options['content'] = $fup->getField('content'); - $options['_users_id_requester'] = $fup->fields['users_id']; - $options['_link'] = [ - 'link' => Ticket_Ticket::SON_OF, - 'tickets_id_2' => $fup->fields['items_id'] - ]; - } - //Allow overriding the default values - $options['_skip_promoted_fields'] = true; - } - } - - // Check category / type validity - if ($options['itilcategories_id']) { - $cat = new ITILCategory(); - if ($cat->getFromDB($options['itilcategories_id'])) { - switch ($options['type']) { - case self::INCIDENT_TYPE : - if (!$cat->getField('is_incident')) { - $options['itilcategories_id'] = 0; - } - break; - - case self::DEMAND_TYPE : - if (!$cat->getField('is_request')) { - $options['itilcategories_id'] = 0; - } - break; - - default : - break; - } - } - } - - // Default check - if ($ID > 0) { - $this->check($ID, READ); - } else { - // Create item - $this->check(-1, CREATE, $options); - } - - if (!$ID) { - $this->userentities = []; - if ($options["_users_id_requester"]) { - //Get all the user's entities - $requester_entities = Profile_User::getUserEntities($options["_users_id_requester"], true, - true); - $user_entities = $_SESSION['glpiactiveentities']; - $this->userentities = array_intersect($requester_entities, $user_entities); - } - $this->countentitiesforuser = count($this->userentities); - - if (($this->countentitiesforuser > 0) - && !in_array($this->fields["entities_id"], $this->userentities)) { - // If entity is not in the list of user's entities, - // then use as default value the first value of the user's entites list - $this->fields["entities_id"] = $this->userentities[0]; - // Pass to values - $options['entities_id'] = $this->userentities[0]; - } - } - - if ($options['type'] <= 0) { - $options['type'] = Entity::getUsedConfig('tickettype', $options['entities_id'], '', - Ticket::INCIDENT_TYPE); - } - - if (!isset($options['template_preview'])) { - $options['template_preview'] = 0; - } - - if (!isset($options['_promoted_fup_id'])) { - $options['_promoted_fup_id'] = 0; - } - - // Load template if available : - $tt = $this->getITILTemplateToUse( - $options['template_preview'], - $this->fields['type'], - ($ID ? $this->fields['itilcategories_id'] : $options['itilcategories_id']), - ($ID ? $this->fields['entities_id'] : $options['entities_id']) - ); - - // Predefined fields from template : reset them - if (isset($options['_predefined_fields'])) { - $options['_predefined_fields'] - = Toolbox::decodeArrayFromInput($options['_predefined_fields']); - } else { - $options['_predefined_fields'] = []; - } - - // Store predefined fields to be able not to take into account on change template - // Only manage predefined values on ticket creation - $predefined_fields = []; - $tpl_key = $this->getTemplateFormFieldName(); - if (!$ID) { - - if (isset($tt->predefined) && count($tt->predefined)) { - foreach ($tt->predefined as $predeffield => $predefvalue) { - if (isset($default_values[$predeffield])) { - // Is always default value : not set - // Set if already predefined field - // Set if ticket template change - if (((count($options['_predefined_fields']) == 0) - && ($options[$predeffield] == $default_values[$predeffield])) - || (isset($options['_predefined_fields'][$predeffield]) - && ($options[$predeffield] == $options['_predefined_fields'][$predeffield])) - || (isset($options[$tpl_key]) - && ($options[$tpl_key] != $tt->getID())) - // user pref for requestype can't overwrite requestype from template - // when change category - || (($predeffield == 'requesttypes_id') - && empty($saved))) { - - // Load template data - $options[$predeffield] = $predefvalue; - $this->fields[$predeffield] = $predefvalue; - $predefined_fields[$predeffield] = $predefvalue; - } - } - } - // All predefined override : add option to say predifined exists - if (count($predefined_fields) == 0) { - $predefined_fields['_all_predefined_override'] = 1; - } - - } else { // No template load : reset predefined values - if (count($options['_predefined_fields'])) { - foreach ($options['_predefined_fields'] as $predeffield => $predefvalue) { - if ($options[$predeffield] == $predefvalue) { - $options[$predeffield] = $default_values[$predeffield]; - } - } - } - } - } - // Put ticket template on $options for actors - $options[str_replace('s_id', '', $tpl_key)] = $tt; - - // check right used for this ticket - $canupdate = !$ID - || (Session::getCurrentInterface() == "central" - && $this->canUpdateItem()); - $can_requester = $this->canRequesterUpdateItem(); - $canpriority = Session::haveRight(self::$rightname, self::CHANGEPRIORITY); - $canassign = $this->canAssign(); - $canassigntome = $this->canAssignTome(); - - if ($ID && in_array($this->fields['status'], $this->getClosedStatusArray())) { - $canupdate = false; - // No update for actors - $options['_noupdate'] = true; - } - - $showuserlink = 0; - if (Session::haveRight('user', READ)) { - $showuserlink = 1; - } - - if ($options['template_preview']) { - // Add all values to fields of tickets for template preview - foreach ($options as $key => $val) { - if (!isset($this->fields[$key])) { - $this->fields[$key] = $val; - } - } - } - - // In percent - $colsize1 = '13'; - $colsize2 = '29'; - $colsize3 = '13'; - $colsize4 = '45'; - - $this->showFormHeader($options); - - echo ""; - echo ""; - echo $tt->getBeginHiddenFieldText('date'); - if (!$ID) { - printf(__('%1$s%2$s'), __('Opening date'), $tt->getMandatoryMark('date')); - } else { - echo __('Opening date'); - } - echo $tt->getEndHiddenFieldText('date'); - echo ""; - echo ""; - echo $tt->getBeginHiddenFieldValue('date'); - $date = $this->fields["date"]; - - if ($canupdate) { - Html::showDateTimeField("date", ['value' => $date, - 'maybeempty' => false, - 'required' => ($tt->isMandatoryField('date') && !$ID)]); - } else { - echo Html::convDateTime($date); - } - echo $tt->getEndHiddenFieldValue('date', $this); - echo ""; - - if ($ID) { - echo "".__('By').""; - echo ""; - if ($canupdate) { - User::dropdown(['name' => 'users_id_recipient', - 'value' => $this->fields["users_id_recipient"], - 'entity' => $this->fields["entities_id"], - 'right' => 'all']); - } else { - echo getUserName($this->fields["users_id_recipient"], $showuserlink); - } - - echo ""; - } else { - echo ""; - echo ""; - } - echo ""; - - echo ""; - if ($ID) { - echo "".__('Last update').""; - echo ""; - if ($this->fields['users_id_lastupdater'] > 0) { - //TRANS: %1$s is the update date, %2$s is the last updater name - printf(__('%1$s by %2$s'), Html::convDateTime($this->fields["date_mod"]), - getUserName($this->fields["users_id_lastupdater"], $showuserlink)); - } - echo ""; - } - echo ""; - - // SLAs - echo ""; - echo "".$tt->getBeginHiddenFieldText('time_to_own'); - if (!$ID) { - printf(__('%1$s%2$s'), __('Time to own'), $tt->getMandatoryMark('time_to_own')); - } else { - echo __('Time to own'); - } - echo $tt->getEndHiddenFieldText('time_to_own'); - echo ""; - echo ""; - $sla = new SLA(); - $sla->showForTicket($this, SLM::TTO, $tt, $canupdate); - echo ""; - echo "".$tt->getBeginHiddenFieldText('time_to_resolve'); - if (!$ID) { - printf(__('%1$s%2$s'), __('Time to resolve'), $tt->getMandatoryMark('time_to_resolve')); - } else { - echo __('Time to resolve'); - } - echo $tt->getEndHiddenFieldText('time_to_resolve'); - echo ""; - echo ""; - $sla->showForTicket($this, SLM::TTR, $tt, $canupdate); - echo ""; - echo ""; - - // OLAs - echo ""; - echo "".$tt->getBeginHiddenFieldText('internal_time_to_own'); - if (!$ID) { - printf(__('%1$s%2$s'), __('Internal time to own'), $tt->getMandatoryMark('internal_time_to_own')); - } else { - echo __('Internal time to own'); - } - echo $tt->getEndHiddenFieldText('internal_time_to_own'); - echo ""; - echo ""; - $ola = new OLA(); - $ola->showForTicket($this, SLM::TTO, $tt, $canupdate); - echo ""; - echo "".$tt->getBeginHiddenFieldText('internal_time_to_resolve'); - if (!$ID) { - printf(__('%1$s%2$s'), __('Internal time to resolve'), $tt->getMandatoryMark('internal_time_to_resolve')); - } else { - echo __('Internal time to resolve'); - } - echo $tt->getEndHiddenFieldText('internal_time_to_resolve'); - echo ""; - echo ""; - $ola->showForTicket($this, SLM::TTR, $tt, $canupdate); - echo ""; - echo ""; - - if ($ID - && (in_array($this->fields["status"], $this->getSolvedStatusArray()) - || in_array($this->fields["status"], $this->getClosedStatusArray()))) { - - echo ""; - echo "".__('Resolution date').""; - echo ""; - Html::showDateTimeField("solvedate", ['value' => $this->fields["solvedate"], - 'maybeempty' => false, - 'canedit' => $canupdate]); - echo ""; - if (in_array($this->fields["status"], $this->getClosedStatusArray())) { - echo "".__('Close date').""; - echo ""; - Html::showDateTimeField("closedate", ['value' => $this->fields["closedate"], - 'maybeempty' => false, - 'canedit' => $canupdate]); - echo ""; - } else { - echo " "; - } - echo ""; - } - - if ($ID) { - echo ""; - echo ""; - } - - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - - if (!$ID) { - echo "
".sprintf(__('%1$s%2$s'), __('Type'), - $tt->getMandatoryMark('type')).""; - // Permit to set type when creating ticket without update right - if ($canupdate) { - $opt = ['value' => $this->fields["type"]]; - /// Auto submit to load template - if (!$ID) { - $opt['on_change'] = 'this.form.submit()'; - } - $rand = self::dropdownType('type', $opt); - if ($ID) { - $params = ['type' => '__VALUE__', - 'entity_restrict' => $this->fields['entities_id'], - 'value' => $this->fields['itilcategories_id'], - 'currenttype' => $this->fields['type']]; - - Ajax::updateItemOnSelectEvent("dropdown_type$rand", "show_category_by_type", - $CFG_GLPI["root_doc"]."/ajax/dropdownTicketCategories.php", - $params); - } - } else { - echo self::getTicketTypeName($this->fields["type"]); - } - echo "".sprintf(__('%1$s%2$s'), __('Category'), - $tt->getMandatoryMark('itilcategories_id')).""; - // Permit to set category when creating ticket without update right - if ($canupdate || $can_requester) { - $conditions = []; - - $opt = ['value' => $this->fields["itilcategories_id"], - 'entity' => $this->fields["entities_id"]]; - if (Session::getCurrentInterface() == "helpdesk") { - $conditions['is_helpdeskvisible'] = 1; - } - /// Auto submit to load template - if (!$ID) { - $opt['on_change'] = 'this.form.submit()'; - } - /// if category mandatory, no empty choice - /// no empty choice is default value set on ticket creation, else yes - if (($ID || $options['itilcategories_id']) - && $tt->isMandatoryField("itilcategories_id") - && ($this->fields["itilcategories_id"] > 0)) { - $opt['display_emptychoice'] = false; - } - - switch ($this->fields["type"]) { - case self::INCIDENT_TYPE : - $conditions['is_incident'] = 1; - break; - - case self::DEMAND_TYPE : - $conditions['is_request'] = 1; - break; - - default : - break; - } - echo ""; - $opt['condition'] = $conditions; - ITILCategory::dropdown($opt); - echo ""; - } else { - echo Dropdown::getDropdownName("glpi_itilcategories", $this->fields["itilcategories_id"]); - } - echo "
"; - $this->showActorsPartForm($ID, $options); - echo ""; - } - - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - - echo ""; - echo ""; - echo ""; - // Display validation state - echo ""; - echo ""; - - echo ""; - echo ""; - echo ""; - - echo ""; - echo ""; - echo ""; - - echo ""; - echo ""; - echo ""; - - if (!$ID) { - echo ""; - echo ""; - } else { - echo ""; - echo ""; - } - echo ""; - - if (!$ID - && Session::haveRight('followup', ITILFollowup::ADDALLTICKET)) { - - echo ""; - // Need comment right to add a followup with the actiontime - echo ""; - echo ""; - echo ""; - } - - echo "
".$tt->getBeginHiddenFieldText('status'); - printf(__('%1$s%2$s'), __('Status'), $tt->getMandatoryMark('status')); - echo $tt->getEndHiddenFieldText('status').""; - echo $tt->getBeginHiddenFieldValue('status'); - if ($canupdate) { - self::dropdownStatus(['value' => $this->fields["status"], - 'showtype' => 'allowed']); - TicketValidation::alertValidation($this, 'status'); - } else { - echo self::getStatus($this->fields["status"]); - if ($this->canReopen()) { - $link = $this->getLinkURL(). "&_openfollowup=1&forcetab="; - $link .= "Ticket$1"; - echo " ". __('Reopen').""; - } - } - echo $tt->getEndHiddenFieldValue('status', $this); - - echo "".$tt->getBeginHiddenFieldText('requesttypes_id'); - printf(__('%1$s%2$s'), __('Request source'), $tt->getMandatoryMark('requesttypes_id')); - echo $tt->getEndHiddenFieldText('requesttypes_id').""; - echo $tt->getBeginHiddenFieldValue('requesttypes_id'); - if ($canupdate) { - RequestType::dropdown(['value' => $this->fields["requesttypes_id"], 'condition' => ['is_active' => 1, 'is_ticketheader' => 1]]); - } else { - echo Dropdown::getDropdownName('glpi_requesttypes', $this->fields["requesttypes_id"]); - echo Html::hidden('requesttypes_id', ['value' => $this->fields["requesttypes_id"]]); - } - echo $tt->getEndHiddenFieldValue('requesttypes_id', $this); - echo "
".$tt->getBeginHiddenFieldText('urgency'); - printf(__('%1$s%2$s'), __('Urgency'), $tt->getMandatoryMark('urgency')); - echo $tt->getEndHiddenFieldText('urgency').""; - - if ($canupdate || $can_requester) { - echo $tt->getBeginHiddenFieldValue('urgency'); - $idurgency = self::dropdownUrgency(['value' => $this->fields["urgency"]]); - echo $tt->getEndHiddenFieldValue('urgency', $this); - - } else { - $idurgency = "value_urgency".mt_rand(); - echo ""; - echo $tt->getBeginHiddenFieldValue('urgency'); - echo parent::getUrgencyName($this->fields["urgency"]); - echo $tt->getEndHiddenFieldValue('urgency', $this); - } - echo ""; - if (!$ID) { - echo $tt->getBeginHiddenFieldText('_add_validation'); - printf(__('%1$s%2$s'), __('Approval request'), $tt->getMandatoryMark('_add_validation')); - echo $tt->getEndHiddenFieldText('_add_validation'); - } else { - echo $tt->getBeginHiddenFieldText('global_validation'); - echo __('Approval'); - echo $tt->getEndHiddenFieldText('global_validation'); - } - echo ""; - if (!$ID) { - echo $tt->getBeginHiddenFieldValue('_add_validation'); - $validation_right = ''; - if (($options['type'] == self::INCIDENT_TYPE) - && Session::haveRight('ticketvalidation', TicketValidation::CREATEINCIDENT)) { - $validation_right = 'validate_incident'; - } - if (($options['type'] == self::DEMAND_TYPE) - && Session::haveRight('ticketvalidation', TicketValidation::CREATEREQUEST)) { - $validation_right = 'validate_request'; - } - - if (!empty($validation_right)) { - echo ""; - - $params = ['name' => "users_id_validate", - 'entity' => $this->fields['entities_id'], - 'right' => $validation_right, - 'users_id_validate' => $options['users_id_validate']]; - TicketValidation::dropdownValidator($params); - } - echo $tt->getEndHiddenFieldValue('_add_validation', $this); - if ($tt->isPredefinedField('global_validation')) { - echo ""; - } - } else { - echo $tt->getBeginHiddenFieldValue('global_validation'); - - if (Session::haveRightsOr('ticketvalidation', TicketValidation::getCreateRights()) - && $canupdate) { - TicketValidation::dropdownStatus('global_validation', - ['global' => true, - 'value' => $this->fields['global_validation']]); - } else { - echo TicketValidation::getStatus($this->fields['global_validation']); - } - echo $tt->getEndHiddenFieldValue('global_validation', $this); - - } - echo "
".$tt->getBeginHiddenFieldText('impact'); - printf(__('%1$s%2$s'), __('Impact'), $tt->getMandatoryMark('impact')); - echo $tt->getEndHiddenFieldText('impact').""; - echo $tt->getBeginHiddenFieldValue('impact'); - - if ($canupdate) { - $idimpact = self::dropdownImpact(['value' => $this->fields["impact"]]); - } else { - $idimpact = "value_impact".mt_rand(); - echo ""; - echo parent::getImpactName($this->fields["impact"]); - } - echo $tt->getEndHiddenFieldValue('impact', $this); - echo "".$tt->getBeginHiddenFieldText('locations_id'); - printf(__('%1$s%2$s'), __('Location'), $tt->getMandatoryMark('locations_id')); - echo $tt->getEndHiddenFieldText('locations_id').""; - echo $tt->getBeginHiddenFieldValue('locations_id'); - if ($canupdate) { - Location::dropdown(['value' => $this->fields['locations_id'], - 'entity' => $this->fields['entities_id']]); - } else { - echo Dropdown::getDropdownName('glpi_locations', $this->fields["locations_id"]); - } - echo $tt->getEndHiddenFieldValue('locations_id', $this); - echo "
".$tt->getBeginHiddenFieldText('priority'); - printf(__('%1$s%2$s'), __('Priority'), $tt->getMandatoryMark('priority')); - echo $tt->getEndHiddenFieldText('priority').""; - $idajax = 'change_priority_' . mt_rand(); - - if ($canpriority - && !$tt->isHiddenField('priority')) { - $idpriority = parent::dropdownPriority(['value' => $this->fields["priority"], - 'withmajor' => true]); - $idpriority = 'dropdown_priority'.$idpriority; - echo " "; - - } else { - $idpriority = 0; - echo $tt->getBeginHiddenFieldValue('priority'); - echo "".parent::getPriorityName($this->fields["priority"]).""; - echo ""; - echo $tt->getEndHiddenFieldValue('priority', $this); - } - - if ($canupdate || $can_requester) { - $params = ['urgency' => '__VALUE0__', - 'impact' => '__VALUE1__', - 'priority' => $idpriority]; - Ajax::updateItemOnSelectEvent(['dropdown_urgency'.$idurgency, - 'dropdown_impact'.$idimpact], - $idajax, - $CFG_GLPI["root_doc"]."/ajax/priority.php", $params); - } - echo "".$tt->getBeginHiddenFieldText('items_id'); - printf(__('%1$s%2$s'), _n('Associated element', 'Associated elements', Session::getPluralNumber()), $tt->getMandatoryMark('items_id')); - echo $tt->getEndHiddenFieldText('items_id'); - echo ""; - echo $tt->getBeginHiddenFieldValue('items_id'); - $options['_canupdate'] = Session::haveRight('ticket', CREATE); - if ($options['_canupdate']) { - Item_Ticket::itemAddForm($this, $options); - } - echo $tt->getEndHiddenFieldValue('items_id', $this); - echo "
".$tt->getBeginHiddenFieldText('actiontime'); - printf(__('%1$s%2$s'), __('Total duration'), $tt->getMandatoryMark('actiontime')); - echo $tt->getEndHiddenFieldText('actiontime').""; - echo $tt->getBeginHiddenFieldValue('actiontime'); - Dropdown::showTimeStamp('actiontime', ['value' => $options['actiontime'], - 'addfirstminutes' => true]); - echo $tt->getEndHiddenFieldValue('actiontime', $this); - echo "
"; - if ($ID) { - $this->showActorsPartForm($ID, $options); - } - - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - - echo ""; - echo ""; - echo ""; - echo ""; - - echo ""; - echo "'; - echo ""; - echo ""; - - if (!in_array($this->fields['status'], $this->getClosedStatusArray())) { - // View files added - echo ""; - // Permit to add doc when creating a ticket - echo ""; - echo ""; - echo ""; - } - - Plugin::doHook("post_item_form", ['item' => $this, 'options' => &$options]); - - echo "
".$tt->getBeginHiddenFieldText('name'); - printf(__('%1$s%2$s'), __('Title'), $tt->getMandatoryMark('name')); - echo $tt->getEndHiddenFieldText('name').""; - if ($canupdate || $can_requester) { - echo $tt->getBeginHiddenFieldValue('name'); - echo "isMandatoryField('name') ? " required='required'" : '') . - " value=\"".Html::cleanInputText($this->fields["name"])."\">"; - echo $tt->getEndHiddenFieldValue('name', $this); - } else { - if (empty($this->fields["name"])) { - echo __('Without title'); - } else { - echo $this->fields["name"]; - } - } - echo "
".$tt->getBeginHiddenFieldText('content'); - printf(__('%1$s%2$s'), __('Description'), $tt->getMandatoryMark('content')); - if ($canupdate || $can_requester) { - $content = Toolbox::unclean_cross_side_scripting_deep(Html::entity_decode_deep($this->fields['content'])); - Html::showTooltip(nl2br(Html::Clean($content))); - } - echo $tt->getEndHiddenFieldText('content').""; - - echo $tt->getBeginHiddenFieldValue('content'); - $rand = mt_rand(); - $rand_text = mt_rand(); - $rows = 10; - $content_id = "content$rand"; - - $content = $this->fields['content']; - if (!isset($options['template_preview'])) { - $content = Html::cleanPostForTextArea($content); - } - - $content = Html::setRichTextContent( - $content_id, - $content, - $rand, - !$canupdate - ); - - echo "
"; - if ($canupdate || $can_requester) { - $uploads = []; - if (isset($this->input['_content'])) { - $uploads['_content'] = $this->input['_content']; - $uploads['_tag_content'] = $this->input['_tag_content']; - } - Html::textarea([ - 'name' => 'content', - 'filecontainer' => 'content_info', - 'editor_id' => $content_id, - 'required' => $tt->isMandatoryField('content'), - 'rows' => $rows, - 'enable_richtext' => true, - 'value' => $content, - 'uploads' => $uploads, - ]); - echo "
"; - } else { - echo Toolbox::getHtmlToDisplay($content); - } - echo $tt->getEndHiddenFieldValue('content', $this); - - echo "
". _n('Linked ticket', 'Linked tickets', - Session::getPluralNumber()); - $rand_linked_ticket = mt_rand(); - if ($canupdate) { - echo "" . __s('Add') . ""; - } - echo '"; - if ($canupdate) { - echo ""; - - if (isset($options["_link"]) - && !empty($options["_link"]['tickets_id_2'])) { - echo ""; - } - } - - Ticket_Ticket::displayLinkedTicketsTo($ID); - echo "
"; - echo $tt->getBeginHiddenFieldText('_documents_id'); - $doctitle = sprintf(__('File (%s)'), Document::getMaxUploadSize()); - printf(__('%1$s%2$s'), $doctitle, $tt->getMandatoryMark('_documents_id')); - // Do not show if hidden. - if (!$tt->isHiddenField('_documents_id')) { - DocumentType::showAvailableTypesLink(); - } - echo $tt->getEndHiddenFieldText('_documents_id'); - echo ""; - // Do not set values - echo $tt->getEndHiddenFieldValue('_documents_id'); - if ($tt->isPredefinedField('_documents_id')) { - if (isset($options['_documents_id']) - && is_array($options['_documents_id']) - && count($options['_documents_id'])) { - - echo "".__('Default documents:').''; - echo "
"; - $doc = new Document(); - foreach ($options['_documents_id'] as $key => $val) { - if ($doc->getFromDB($val)) { - echo ""; - echo "- ".$doc->getNameID()."
"; - } - } - } - } - if (!$tt->isHiddenField('_documents_id')) { - $uploads = []; - if (isset($this->input['_filename'])) { - $uploads['_filename'] = $this->input['_filename']; - $uploads['_tag_filename'] = $this->input['_tag_filename']; - } - Html::file([ - 'filecontainer' => 'fileupload_info_ticket', - // 'editor_id' => $content_id, - 'showtitle' => false, - 'multiple' => true, - 'uploads' => $uploads, - ]); - } - echo "
"; - - $display_save_btn = (!array_key_exists('locked', $options) || !$options['locked']) - && ($canupdate || $can_requester || $canpriority || $canassign || $canassigntome); - - if ($display_save_btn - && !$options['template_preview']) { - if ($ID) { - echo "
"; - if ($this->fields["is_deleted"] == 1) { - if (self::canDelete()) { - echo "      "; - } - } else { - if ($display_save_btn) { - echo "      "; - } - } - if ($this->fields["is_deleted"] == 1) { - if (self::canPurge()) { - echo ""; - } - } else { - if ($this->canDeleteItem()) { - echo ""; - } - } - echo ""; - echo "
"; - } else { - echo "
"; - $add_params = ['name' => 'add']; - if ($options['_promoted_fup_id']) { - $add_params['confirm'] = __('Confirm the promotion?'); - } - echo Html::submit(_x('button', 'Add'), $add_params); - if ($tt->isField('id') && ($tt->fields['id'] > 0)) { - echo ""; - echo ""; - } - echo Html::hidden('_promoted_fup_id', ['value' => $options['_promoted_fup_id']]); - echo Html::hidden('_skip_promoted_fields', ['value' => $options['_skip_promoted_fields']]); - echo '
'; - } - } - - echo ""; - echo ""; - - echo ""; - - if (!$options['template_preview']) { - Html::closeForm(); - } - - return true; - } - - - /** - * @param $size (default 25) - **/ - static function showDocumentAddButton($size = 25) { - echo ""; - echo "'); - nbfiles++; - if (nbfiles==maxfiles) { - ".Html::jsHide('addfilebutton')." - } - }\" - " . __s('Add') . ""; - } - - - /** - * @param $start - * @param $status (default ''process) - * @param $showgrouptickets (true by default) - */ - static function showCentralList($start, $status = "process", $showgrouptickets = true) { - global $DB; - - if (!Session::haveRightsOr(self::$rightname, [CREATE, self::READALL, self::READASSIGN]) - && !Session::haveRightsOr('ticketvalidation', TicketValidation::getValidateRights())) { - - return false; - } - - $JOINS = []; - $WHERE = [ - 'is_deleted' => 0 - ]; - $search_users_id = [ - 'glpi_tickets_users.users_id' => Session::getLoginUserID(), - 'glpi_tickets_users.type' => CommonITILActor::REQUESTER - ]; - $search_assign = [ - 'glpi_tickets_users.users_id' => Session::getLoginUserID(), - 'glpi_tickets_users.type' => CommonITILActor::ASSIGN - ]; - $search_observer = [ - 'glpi_tickets_users.users_id' => Session::getLoginUserID(), - 'glpi_tickets_users.type' => CommonITILActor::OBSERVER - ]; - - if ($showgrouptickets) { - $search_users_id = [0]; - $search_assign = [0]; - - if (count($_SESSION['glpigroups'])) { - $search_assign = [ - 'glpi_groups_tickets.groups_id' => $_SESSION['glpigroups'], - 'glpi_groups_tickets.type' => CommonITILActor::ASSIGN - ]; - - if (Session::haveRight(self::$rightname, self::READGROUP)) { - $search_users_id = [ - 'glpi_groups_tickets.groups_id' => $_SESSION['glpigroups'], - 'glpi_groups_tickets.type' => CommonITILActor::REQUESTER - ]; - $search_observer = [ - 'glpi_groups_tickets.groups_id' => $_SESSION['glpigroups'], - 'glpi_groups_tickets.type' => CommonITILActor::OBSERVER - ]; - } - } - } - - switch ($status) { - case "waiting" : // waiting tickets - $WHERE = array_merge( - $WHERE, - $search_assign, - ['glpi_tickets.status' => self::WAITING] - ); - break; - - case "process" : // planned or assigned tickets - $WHERE = array_merge( - $WHERE, - $search_assign, - ['glpi_tickets.status' => self::getProcessStatusArray()] - ); - break; - - case "toapprove" : //tickets waiting for approval - $ORWHERE = ['AND' => $search_users_id]; - if (!$showgrouptickets && Session::haveRight('ticket', Ticket::SURVEY)) { - $ORWHERE[] = ['glpi_tickets.users_id_recipient' => Session::getLoginUserID()]; - } - $WHERE[] = ['OR' => $ORWHERE]; - $WHERE['glpi_tickets.status'] = self::SOLVED; - break; - - case "tovalidate" : // tickets waiting for validation - $JOINS['LEFT JOIN'] = [ - 'glpi_ticketvalidations' => [ - 'ON' => [ - 'glpi_ticketvalidations' => 'tickets_id', - 'glpi_tickets' => 'id' - ] - ] - ]; - $WHERE = array_merge( - $WHERE, - [ - 'users_id_validate' => Session::getLoginUserID(), - 'glpi_ticketvalidations.status' => CommonITILValidation::WAITING, - 'glpi_tickets.global_validation' => CommonITILValidation::WAITING, - 'NOT' => [ - 'glpi_tickets.status' => [self::SOLVED, self::CLOSED] - ] - ] - ); - break; - - case "validation.rejected" : // tickets with rejected validation (approval) - case "rejected": //old ambiguous key - $WHERE = array_merge( - $WHERE, - $search_assign, - [ - 'glpi_tickets.status' => ['<>', self::CLOSED], - 'glpi_tickets.global_validation' => CommonITILValidation::REFUSED - ] - ); - break; - - case "solution.rejected" : // tickets with rejected solution - $subq = new QuerySubQuery([ - 'SELECT' => 'last_solution.id', - 'FROM' => 'glpi_itilsolutions AS last_solution', - 'WHERE' => [ - 'last_solution.items_id' => new QueryExpression($DB->quoteName('glpi_tickets.id')), - 'last_solution.itemtype' => 'Ticket' - ], - 'ORDER' => 'last_solution.id DESC', - 'LIMIT' => 1 - ]); - - $JOINS['LEFT JOIN'] = [ - 'glpi_itilsolutions' => [ - 'ON' => [ - 'glpi_itilsolutions' => 'id', - $subq - ] - ] - ]; - - $WHERE = array_merge( - $WHERE, - $search_assign, - [ - 'glpi_tickets.status' => ['<>', self::CLOSED], - 'glpi_itilsolutions.status' => CommonITILValidation::REFUSED - ] - ); - break; - case "observed" : - $WHERE = array_merge( - $WHERE, - $search_observer, - [ - 'glpi_tickets.status' => [ - self::INCOMING, - self::PLANNED, - self::ASSIGNED, - self::WAITING - ], - 'NOT' => [ - $search_assign, - $search_users_id - ] - ] - ); - break; - - case "survey" : // tickets dont l'enqu??te de satisfaction n'est pas remplie et encore valide - $JOINS['INNER JOIN'] = [ - 'glpi_ticketsatisfactions' => [ - 'ON' => [ - 'glpi_ticketsatisfactions' => 'tickets_id', - 'glpi_tickets' => 'id' - ] - ], - 'glpi_entities' => [ - 'ON' => [ - 'glpi_tickets' => 'entities_id', - 'glpi_entities' => 'id' - ] - ] - ]; - $ORWHERE = ['AND' => $search_users_id]; - if (!$showgrouptickets && Session::haveRight('ticket', Ticket::SURVEY)) { - $ORWHERE[] = ['glpi_tickets.users_id_recipient' => Session::getLoginUserID()]; - } - $WHERE[] = ['OR' => $ORWHERE]; - - $WHERE = array_merge( - $WHERE, - [ - 'glpi_tickets.status' => self::CLOSED, - ['OR' => [ - 'glpi_entities.inquest_duration' => 0, - new \QueryExpression( - 'DATEDIFF(ADDDATE(' . $DB->quoteName('glpi_ticketsatisfactions.date_begin') . - ', INTERVAL ' . $DB->quoteName('glpi_entities.inquest_duration') . ' DAY), CURDATE()) > 0' - ) - ]], - 'glpi_ticketsatisfactions.date_answered' => null - ] - ); - break; - - case "requestbyself" : // on affiche les tickets demand??s le user qui sont planifi??s ou assign??s - // ?? quelqu'un d'autre (exclut les self-tickets) - - default : - $WHERE = array_merge( - $WHERE, - $search_users_id, - [ - 'glpi_tickets.status' => [ - self::INCOMING, - self::PLANNED, - self::ASSIGNED, - self::WAITING - ], - 'NOT' => $search_assign - ] - ); - } - - $criteria = [ - 'SELECT' => ['glpi_tickets.id', 'glpi_tickets.date_mod'], - 'DISTINCT' => true, - 'FROM' => 'glpi_tickets', - 'LEFT JOIN' => [ - 'glpi_tickets_users' => [ - 'ON' => [ - 'glpi_tickets_users' => 'tickets_id', - 'glpi_tickets' => 'id' - ] - ], - 'glpi_groups_tickets' => [ - 'ON' => [ - 'glpi_groups_tickets' => 'tickets_id', - 'glpi_tickets' => 'id' - ] - ] - ], - 'WHERE' => $WHERE + getEntitiesRestrictCriteria('glpi_tickets'), - 'ORDERBY' => 'glpi_tickets.date_mod DESC' - ]; - if (count($JOINS)) { - $criteria = array_merge_recursive($criteria, $JOINS); - } - $iterator = $DB->request($criteria); - $numrows = count($iterator); - $number = 0; - - if ($_SESSION['glpidisplay_count_on_home'] > 0) { - $iterator = $DB->request( - $criteria + [ - 'START' => (int)$start, - 'LIMIT' => (int)$_SESSION['glpidisplay_count_on_home'] - ] - ); - $number = count($iterator); - } - - if ($numrows > 0) { - echo ""; - echo ""; - if ($number) { - echo ""; - echo ""; - echo ""; - echo ""; - while ($data = $iterator->next()) { - self::showVeryShort($data['id'], $forcetab); - } - } - echo "
"; - - $options = [ - 'criteria' => [], - 'reset' => 'reset', - ]; - $forcetab = ''; - if ($showgrouptickets) { - switch ($status) { - case "toapprove" : - $options['criteria'][0]['field'] = 12; // status - $options['criteria'][0]['searchtype'] = 'equals'; - $options['criteria'][0]['value'] = self::SOLVED; - $options['criteria'][0]['link'] = 'AND'; - - $options['criteria'][1]['field'] = 71; // groups_id - $options['criteria'][1]['searchtype'] = 'equals'; - $options['criteria'][1]['value'] = 'mygroups'; - $options['criteria'][1]['link'] = 'AND'; - $forcetab = 'Ticket$2'; - - echo "". - Html::makeTitle(__('Your tickets to close'), $number, $numrows).""; - break; - - case "waiting" : - $options['criteria'][0]['field'] = 12; // status - $options['criteria'][0]['searchtype'] = 'equals'; - $options['criteria'][0]['value'] = self::WAITING; - $options['criteria'][0]['link'] = 'AND'; - - $options['criteria'][1]['field'] = 8; // groups_id_assign - $options['criteria'][1]['searchtype'] = 'equals'; - $options['criteria'][1]['value'] = 'mygroups'; - $options['criteria'][1]['link'] = 'AND'; - - echo "". - Html::makeTitle(__('Tickets on pending status'), $number, $numrows).""; - break; - - case "process" : - $options['criteria'][0]['field'] = 12; // status - $options['criteria'][0]['searchtype'] = 'equals'; - $options['criteria'][0]['value'] = 'process'; - $options['criteria'][0]['link'] = 'AND'; - - $options['criteria'][1]['field'] = 8; // groups_id_assign - $options['criteria'][1]['searchtype'] = 'equals'; - $options['criteria'][1]['value'] = 'mygroups'; - $options['criteria'][1]['link'] = 'AND'; - - echo "". - Html::makeTitle(__('Tickets to be processed'), $number, $numrows).""; - break; - - case "observed": - $options['criteria'][0]['field'] = 12; // status - $options['criteria'][0]['searchtype'] = 'equals'; - $options['criteria'][0]['value'] = 'notold'; - $options['criteria'][0]['link'] = 'AND'; - - $options['criteria'][1]['field'] = 65; // groups_id - $options['criteria'][1]['searchtype'] = 'equals'; - $options['criteria'][1]['value'] = 'mygroups'; - $options['criteria'][1]['link'] = 'AND'; - - echo "". - Html::makeTitle(__('Your observed tickets'), $number, $numrows).""; - break; - - case "requestbyself" : - default : - $options['criteria'][0]['field'] = 12; // status - $options['criteria'][0]['searchtype'] = 'equals'; - $options['criteria'][0]['value'] = 'notold'; - $options['criteria'][0]['link'] = 'AND'; - - $options['criteria'][1]['field'] = 71; // groups_id - $options['criteria'][1]['searchtype'] = 'equals'; - $options['criteria'][1]['value'] = 'mygroups'; - $options['criteria'][1]['link'] = 'AND'; - - echo "". - Html::makeTitle(__('Your tickets in progress'), $number, $numrows).""; - } - - } else { - switch ($status) { - case "waiting" : - $options['criteria'][0]['field'] = 12; // status - $options['criteria'][0]['searchtype'] = 'equals'; - $options['criteria'][0]['value'] = self::WAITING; - $options['criteria'][0]['link'] = 'AND'; - - $options['criteria'][1]['field'] = 5; // users_id_assign - $options['criteria'][1]['searchtype'] = 'equals'; - $options['criteria'][1]['value'] = Session::getLoginUserID(); - $options['criteria'][1]['link'] = 'AND'; - - echo "". - Html::makeTitle(__('Tickets on pending status'), $number, $numrows).""; - break; - - case "process" : - $options['criteria'][0]['field'] = 5; // users_id_assign - $options['criteria'][0]['searchtype'] = 'equals'; - $options['criteria'][0]['value'] = Session::getLoginUserID(); - $options['criteria'][0]['link'] = 'AND'; - - $options['criteria'][1]['field'] = 12; // status - $options['criteria'][1]['searchtype'] = 'equals'; - $options['criteria'][1]['value'] = 'process'; - $options['criteria'][1]['link'] = 'AND'; - - echo "". - Html::makeTitle(__('Tickets to be processed'), $number, $numrows).""; - break; - - case "tovalidate" : - $options['criteria'][0]['field'] = 55; // validation status - $options['criteria'][0]['searchtype'] = 'equals'; - $options['criteria'][0]['value'] = CommonITILValidation::WAITING; - $options['criteria'][0]['link'] = 'AND'; - - $options['criteria'][1]['field'] = 59; // validation aprobator - $options['criteria'][1]['searchtype'] = 'equals'; - $options['criteria'][1]['value'] = Session::getLoginUserID(); - $options['criteria'][1]['link'] = 'AND'; - - $options['criteria'][2]['field'] = 12; // validation aprobator - $options['criteria'][2]['searchtype'] = 'equals'; - $options['criteria'][2]['value'] = 'old'; - $options['criteria'][2]['link'] = 'AND NOT'; - - $options['criteria'][3]['field'] = 52; // global validation status - $options['criteria'][3]['searchtype'] = 'equals'; - $options['criteria'][3]['value'] = CommonITILValidation::WAITING; - $options['criteria'][3]['link'] = 'AND'; - $forcetab = 'TicketValidation$1'; - - echo "". - Html::makeTitle(__('Your tickets to validate'), $number, $numrows).""; - - break; - - case "validation.rejected" : - case "rejected" : // old ambiguous key - $options['criteria'][0]['field'] = 52; // validation status - $options['criteria'][0]['searchtype'] = 'equals'; - $options['criteria'][0]['value'] = CommonITILValidation::REFUSED; - $options['criteria'][0]['link'] = 'AND'; - - $options['criteria'][1]['field'] = 5; // assign user - $options['criteria'][1]['searchtype'] = 'equals'; - $options['criteria'][1]['value'] = Session::getLoginUserID(); - $options['criteria'][1]['link'] = 'AND'; - - echo "". - Html::makeTitle(__('Your tickets having rejected approval status'), $number, $numrows).""; - - break; - - case "solution.rejected" : - $options['criteria'][0]['field'] = 39; // last solution status - $options['criteria'][0]['searchtype'] = 'equals'; - $options['criteria'][0]['value'] = CommonITILValidation::REFUSED; - $options['criteria'][0]['link'] = 'AND'; - - $options['criteria'][1]['field'] = 5; // assign user - $options['criteria'][1]['searchtype'] = 'equals'; - $options['criteria'][1]['value'] = Session::getLoginUserID(); - $options['criteria'][1]['link'] = 'AND'; - - echo "". - Html::makeTitle(__('Your tickets having rejected solution'), $number, $numrows).""; - - break; - - case "toapprove" : - $options['criteria'][0]['field'] = 12; // status - $options['criteria'][0]['searchtype'] = 'equals'; - $options['criteria'][0]['value'] = self::SOLVED; - $options['criteria'][0]['link'] = 'AND'; - - $options['criteria'][1]['field'] = 4; // users_id_assign - $options['criteria'][1]['searchtype'] = 'equals'; - $options['criteria'][1]['value'] = Session::getLoginUserID(); - $options['criteria'][1]['link'] = 'AND'; - - $options['criteria'][2]['field'] = 22; // users_id_recipient - $options['criteria'][2]['searchtype'] = 'equals'; - $options['criteria'][2]['value'] = Session::getLoginUserID(); - $options['criteria'][2]['link'] = 'OR'; - - $options['criteria'][3]['field'] = 12; // status - $options['criteria'][3]['searchtype'] = 'equals'; - $options['criteria'][3]['value'] = self::SOLVED; - $options['criteria'][3]['link'] = 'AND'; - - $forcetab = 'Ticket$2'; - - echo "". - Html::makeTitle(__('Your tickets to close'), $number, $numrows).""; - break; - - case "observed" : - $options['criteria'][0]['field'] = 66; // users_id - $options['criteria'][0]['searchtype'] = 'equals'; - $options['criteria'][0]['value'] = Session::getLoginUserID(); - $options['criteria'][0]['link'] = 'AND'; - - $options['criteria'][1]['field'] = 12; // status - $options['criteria'][1]['searchtype'] = 'equals'; - $options['criteria'][1]['value'] = 'notold'; - $options['criteria'][1]['link'] = 'AND'; - - echo "". - Html::makeTitle(__('Your observed tickets'), $number, $numrows).""; - break; - - case "survey" : - $options['criteria'][0]['field'] = 12; // status - $options['criteria'][0]['searchtype'] = 'equals'; - $options['criteria'][0]['value'] = self::CLOSED; - $options['criteria'][0]['link'] = 'AND'; - - $options['criteria'][1]['field'] = 60; // enquete generee - $options['criteria'][1]['searchtype'] = 'contains'; - $options['criteria'][1]['value'] = '^'; - $options['criteria'][1]['link'] = 'AND'; - - $options['criteria'][2]['field'] = 61; // date_answered - $options['criteria'][2]['searchtype'] = 'contains'; - $options['criteria'][2]['value'] = 'NULL'; - $options['criteria'][2]['link'] = 'AND'; - - if (Session::haveRight('ticket', Ticket::SURVEY)) { - $options['criteria'][3]['field'] = 22; // author - $options['criteria'][3]['searchtype'] = 'equals'; - $options['criteria'][3]['value'] = Session::getLoginUserID(); - $options['criteria'][3]['link'] = 'AND'; - } else { - $options['criteria'][3]['field'] = 4; // requester - $options['criteria'][3]['searchtype'] = 'equals'; - $options['criteria'][3]['value'] = Session::getLoginUserID(); - $options['criteria'][3]['link'] = 'AND'; - } - $forcetab = 'Ticket$3'; - - echo "". - Html::makeTitle(__('Satisfaction survey'), $number, $numrows).""; - break; - - case "requestbyself" : - default : - $options['criteria'][0]['field'] = 4; // users_id - $options['criteria'][0]['searchtype'] = 'equals'; - $options['criteria'][0]['value'] = Session::getLoginUserID(); - $options['criteria'][0]['link'] = 'AND'; - - $options['criteria'][1]['field'] = 12; // status - $options['criteria'][1]['searchtype'] = 'equals'; - $options['criteria'][1]['value'] = 'notold'; - $options['criteria'][1]['link'] = 'AND'; - - echo "". - Html::makeTitle(__('Your tickets in progress'), $number, $numrows).""; - } - } - - echo "
".__('ID')."".__('Requester').""._n('Associated element', 'Associated elements', Session::getPluralNumber())."".__('Description')."
"; - - } - } - - /** - * Get tickets count - * - * @param $foruser boolean : only for current login user as requester (false by default) - **/ - static function showCentralCount($foruser = false) { - global $DB, $CFG_GLPI; - - // show a tab with count of jobs in the central and give link - if (!Session::haveRight(self::$rightname, self::READALL) && !self::canCreate()) { - return false; - } - if (!Session::haveRight(self::$rightname, self::READALL)) { - $foruser = true; - } - - $table = self::getTable(); - $criteria = [ - 'SELECT' => [ - 'glpi_tickets.status', - 'COUNT DISTINCT' => ["$table.id AS COUNT"], - ], - 'FROM' => $table, - 'WHERE' => getEntitiesRestrictCriteria($table), - 'GROUPBY' => 'status' - ]; - - if ($foruser) { - $criteria['LEFT JOIN'] = [ - 'glpi_tickets_users' => [ - 'ON' => [ - 'glpi_tickets_users' => 'tickets_id', - $table => 'id', [ - 'AND' => [ - 'glpi_tickets_users.type' => CommonITILActor::REQUESTER - ] - ] - ] - ], - 'glpi_ticketvalidations' => [ - 'ON' => [ - 'glpi_ticketvalidations' => 'tickets_id', - $table => 'id' - ] - ] - ]; - - if (Session::haveRight(self::$rightname, self::READGROUP) - && isset($_SESSION["glpigroups"]) - && count($_SESSION["glpigroups"])) { - $criteria['LEFT JOIN']['glpi_groups_tickets'] = [ - 'ON' => [ - 'glpi_groups_tickets' => 'tickets_id', - $table => 'id', [ - 'AND' => ['glpi_groups_tickets.type' => CommonITILActor::REQUESTER] - ] - ] - ]; - } - } - - if ($foruser) { - $ORWHERE = ['OR' => [ - 'glpi_tickets_users.users_id' => Session::getLoginUserID(), - 'glpi_tickets.users_id_recipient' => Session::getLoginUserID(), - 'glpi_ticketvalidations.users_id_validate' => Session::getLoginUserID() - ]]; - - if (Session::haveRight(self::$rightname, self::READGROUP) - && isset($_SESSION["glpigroups"]) - && count($_SESSION["glpigroups"])) { - $ORWHERE['OR']['glpi_groups_tickets.groups_id'] = $_SESSION['glpigroups']; - } - $criteria['WHERE'][] = $ORWHERE; - } - - $deleted_criteria = $criteria; - $criteria['WHERE']['glpi_tickets.is_deleted'] = 0; - $deleted_criteria['WHERE']['glpi_tickets.is_deleted'] = 1; - $iterator = $DB->request($criteria); - $deleted_iterator = $DB->request($deleted_criteria); - - $status = []; - foreach (self::getAllStatusArray() as $key => $val) { - $status[$key] = 0; - } - - while ($data = $iterator->next()) { - $status[$data["status"]] = $data["COUNT"]; - } - - $number_deleted = 0; - while ($data = $deleted_iterator->next()) { - $number_deleted += $data["COUNT"]; - } - - $options = [ - 'criteria' => [], - 'reset' => 'reset', - ]; - $options['criteria'][0]['field'] = 12; - $options['criteria'][0]['searchtype'] = 'equals'; - $options['criteria'][0]['value'] = 'process'; - $options['criteria'][0]['link'] = 'AND'; - - echo ""; - echo ""; - echo " - "; - - if (Session::haveRightsOr('ticketvalidation', TicketValidation::getValidateRights())) { - $number_waitapproval = TicketValidation::getNumberToValidate(Session::getLoginUserID()); - - $opt = [ - 'criteria' => [], - 'reset' => 'reset', - ]; - $opt['criteria'][0]['field'] = 55; // validation status - $opt['criteria'][0]['searchtype'] = 'equals'; - $opt['criteria'][0]['value'] = CommonITILValidation::WAITING; - $opt['criteria'][0]['link'] = 'AND'; - - $opt['criteria'][1]['field'] = 59; // validation aprobator - $opt['criteria'][1]['searchtype'] = 'equals'; - $opt['criteria'][1]['value'] = Session::getLoginUserID(); - $opt['criteria'][1]['link'] = 'AND'; - - echo ""; - echo ""; - echo ""; - } - - foreach ($status as $key => $val) { - $options['criteria'][0]['value'] = $key; - echo ""; - echo ""; - echo ""; - } - - $options['criteria'][0]['value'] = 'all'; - $options['is_deleted'] = 1; - echo ""; - echo ""; - echo ""; - - echo "
"; - - if (Session::getCurrentInterface() != "central") { - echo "". - __('Create a ticket')." ". __s('Add').""; - } else { - echo "".__('Ticket followup').""; - } - echo "
"._n('Ticket', 'Tickets', Session::getPluralNumber()).""._x('quantity', 'Number')."
".__('Ticket waiting for your approval')."".$number_waitapproval."
".self::getStatus($key)."$val
".__('Deleted')."".$number_deleted."

"; - } - - - static function showCentralNewList() { - global $DB; - - if (!Session::haveRight(self::$rightname, self::READALL)) { - return false; - } - - $criteria = self::getCommonCriteria(); - $criteria['WHERE'] = [ - 'status' => self::INCOMING, - 'is_deleted' => 0 - ] + getEntitiesRestrictCriteria(self::getTable()); - $criteria['LIMIT'] = (int)$_SESSION['glpilist_limit']; - $iterator = $DB->request($criteria); - $number = count($iterator); - - if ($number > 0) { - Session::initNavigateListItems('Ticket'); - - $options = [ - 'criteria' => [], - 'reset' => 'reset', - ]; - $options['criteria'][0]['field'] = 12; - $options['criteria'][0]['searchtype'] = 'equals'; - $options['criteria'][0]['value'] = self::INCOMING; - $options['criteria'][0]['link'] = 'AND'; - - echo "
"; - //TRANS: %d is the number of new tickets - echo ""; - - self::commonListHeader(Search::HTML_OUTPUT); - - while ($data = $iterator->next()) { - Session::addToNavigateListItems('Ticket', $data["id"]); - self::showShort($data["id"]); - } - echo "
".sprintf(_n('%d new ticket', '%d new tickets', $number), $number); - echo "".__('Show all').""; - echo "
"; - - } else { - echo "
"; - echo ""; - echo ""; - echo "
".__('No ticket found.')."
"; - echo "

"; - } - } - - /** - * Display tickets for an item - * - * Will also display tickets of linked items - * - * @param CommonDBTM $item CommonDBTM object - * @param integer $withtemplate (default 0) - * - * @return void (display a table) - **/ - static function showListForItem(CommonDBTM $item, $withtemplate = 0) { - global $DB; - - if (!Session::haveRightsOr(self::$rightname, - [self::READALL, self::READMY, self::READASSIGN, CREATE])) { - return false; - } - - if ($item->isNewID($item->getID())) { - return false; - } - - $criteria = self::getCommonCriteria(); - $restrict = []; - $options = [ - 'criteria' => [], - 'reset' => 'reset', - ]; - - switch ($item->getType()) { - case 'User' : - $restrict['glpi_tickets_users.users_id'] = $item->getID(); - $restrict['glpi_tickets_users.type'] = CommonITILActor::REQUESTER; - - $options['criteria'][0]['field'] = 4; // status - $options['criteria'][0]['searchtype'] = 'equals'; - $options['criteria'][0]['value'] = $item->getID(); - $options['criteria'][0]['link'] = 'AND'; - break; - - case 'SLA' : - $restrict[] = [ - 'OR' => [ - 'slas_id_tto' => $item->getID(), - 'slas_id_ttr' => $item->getID() - ] - ]; - $criteria['ORDERBY'] = 'glpi_tickets.time_to_resolve DESC'; - - $options['criteria'][0]['field'] = 30; - $options['criteria'][0]['searchtype'] = 'equals'; - $options['criteria'][0]['value'] = $item->getID(); - $options['criteria'][0]['link'] = 'AND'; - break; - - case 'OLA' : - $restrict[] = [ - 'OR' => [ - 'olas_id_tto' => $item->getID(), - 'olas_id_ttr' => $item->getID() - ] - ]; - $criteria['ORDERBY'] = 'glpi_tickets.internal_time_to_resolve DESC'; - - $options['criteria'][0]['field'] = 30; - $options['criteria'][0]['searchtype'] = 'equals'; - $options['criteria'][0]['value'] = $item->getID(); - $options['criteria'][0]['link'] = 'AND'; - break; - - case 'Supplier' : - $restrict['glpi_suppliers_tickets.suppliers_id'] = $item->getID(); - $restrict['glpi_suppliers_tickets.type'] = CommonITILActor::ASSIGN; - - $options['criteria'][0]['field'] = 6; - $options['criteria'][0]['searchtype'] = 'equals'; - $options['criteria'][0]['value'] = $item->getID(); - $options['criteria'][0]['link'] = 'AND'; - break; - - case 'Group' : - // Mini search engine - if ($item->haveChildren()) { - $tree = Session::getSavedOption(__CLASS__, 'tree', 0); - echo ""; - echo ""; - echo "
".__('Last tickets')."
"; - echo __('Child groups')." "; - Dropdown::showYesNo('tree', $tree, -1, - ['on_change' => 'reloadTab("start=0&tree="+this.value)']); - } else { - $tree = 0; - } - echo "
"; - - $restrict['glpi_groups_tickets.groups_id'] = ($tree ? getSonsOf('glpi_groups', $item->getID()) : $item->getID()); - $restrict['glpi_groups_tickets.type'] = CommonITILActor::REQUESTER; - - $options['criteria'][0]['field'] = 71; - $options['criteria'][0]['searchtype'] = ($tree ? 'under' : 'equals'); - $options['criteria'][0]['value'] = $item->getID(); - $options['criteria'][0]['link'] = 'AND'; - break; - - default : - $restrict['glpi_items_tickets.items_id'] = $item->getID(); - $restrict['glpi_items_tickets.itemtype'] = $item->getType(); - - // you can only see your tickets - if (!Session::haveRight(self::$rightname, self::READALL)) { - $or = [ - 'glpi_tickets.users_id_recipient' => Session::getLoginUserID(), - [ - 'AND' => [ - 'glpi_tickets_users.tickets_id' => new \QueryExpression('glpi_tickets.id'), - 'glpi_tickets_users.users_id' => Session::getLoginUserID() - ] - ] - ]; - if (count($_SESSION['glpigroups'])) { - $or['glpi_groups_tickets.groups_id'] = $_SESSION['glpigroups']; - } - $restrict[] = ['OR' => $or]; - } - - $options['criteria'][0]['field'] = 12; - $options['criteria'][0]['searchtype'] = 'equals'; - $options['criteria'][0]['value'] = 'all'; - $options['criteria'][0]['link'] = 'AND'; - - $options['metacriteria'][0]['itemtype'] = $item->getType(); - $options['metacriteria'][0]['field'] = Search::getOptionNumber($item->getType(), - 'id'); - $options['metacriteria'][0]['searchtype'] = 'equals'; - $options['metacriteria'][0]['value'] = $item->getID(); - $options['metacriteria'][0]['link'] = 'AND'; - break; - } - - $criteria['WHERE'] = $restrict + getEntitiesRestrictCriteria(self::getTable()); - $criteria['WHERE']['glpi_tickets.is_deleted'] = 0; - $criteria['LIMIT'] = (int)$_SESSION['glpilist_limit']; - $iterator = $DB->request($criteria); - $number = count($iterator); - - $colspan = 11; - if (count($_SESSION["glpiactiveentities"]) > 1) { - $colspan++; - } - - // Ticket for the item - // Link to open a new ticket - if ($item->getID() - && !$item->isDeleted() - && Ticket::isPossibleToAssignType($item->getType()) - && self::canCreate() - && !(!empty($withtemplate) && ($withtemplate == 2)) - && (!isset($item->fields['is_template']) || ($item->fields['is_template'] == 0))) { - echo "
"; - Html::showSimpleForm(Ticket::getFormURL(), - '_add_fromitem', __('New ticket for this item...'), - ['itemtype' => $item->getType(), - 'items_id' => $item->getID()]); - echo "
"; - } - - if ($item->getID() - && ($item->getType() == 'User') - && self::canCreate() - && !(!empty($withtemplate) && ($withtemplate == 2))) { - echo "
"; - Html::showSimpleForm(Ticket::getFormURL(), - '_add_fromitem', __('New ticket for this item...'), - ['_users_id_requester' => $item->getID()]); - echo "
"; - } - - echo "
"; - - if ($number > 0) { - echo ""; - if (Session::haveRight(self::$rightname, self::READALL)) { - Session::initNavigateListItems('Ticket', - //TRANS : %1$s is the itemtype name, %2$s is the name of the item (used for headings of a list) - sprintf(__('%1$s = %2$s'), $item->getTypeName(1), - $item->getName())); - - echo ""; - } else { - echo ""; - } - - } else { - echo "
"; - $title = sprintf(_n('Last %d ticket', 'Last %d tickets', $number), $number); - $link = "".__('Show all').""; - $title = printf(__('%1$s (%2$s)'), $title, $link); - echo "
".__("You don't have right to see all tickets")."
"; - echo ""; - } - - // Ticket list - if ($number > 0) { - self::commonListHeader(Search::HTML_OUTPUT); - - while ($data = $iterator->next()) { - Session::addToNavigateListItems('Ticket', $data["id"]); - self::showShort($data["id"]); - } - self::commonListHeader(Search::HTML_OUTPUT); - } - - echo "
".__('No ticket found.')."
"; - - // Tickets for linked items - $linkeditems = $item->getLinkedItems(); - $restrict = []; - if (count($linkeditems)) { - foreach ($linkeditems as $ltype => $tab) { - foreach ($tab as $lID) { - $restrict[] = ['AND' => ['itemtype' => $ltype, 'items_id' => $lID]]; - } - } - } - - if (count($restrict) - && Session::haveRight(self::$rightname, self::READALL)) { - $criteria = self::getCommonCriteria(); - $criteria['WHERE'] = ['OR' => $restrict] - + getEntitiesRestrictCriteria(self::getTable()); - $iterator = $DB->request($criteria); - $number = count($iterator); - - echo "
"; - echo ""; - if ($number > 0) { - self::commonListHeader(Search::HTML_OUTPUT); - while ($data = $iterator->next()) { - // Session::addToNavigateListItems(TRACKING_TYPE,$data["id"]); - self::showShort($data["id"]); - } - self::commonListHeader(Search::HTML_OUTPUT); - } else { - echo ""; - } - echo "
"; - echo _n('Ticket on linked items', 'Tickets on linked items', $number); - echo "
".__('No ticket found.')."
"; - - } // Subquery for linked item - - } - - /** - * @param $ID - * @param $forcetab string name of the tab to force at the display (default '') - **/ - static function showVeryShort($ID, $forcetab = '') { - // Prints a job in short form - // Should be called in a -segment - // Print links or not in case of user view - // Make new job object and fill it from database, if success, print it - $showprivate = false; - if (Session::haveRight('followup', ITILFollowup::SEEPRIVATE)) { - $showprivate = true; - } - - $job = new self(); - $rand = mt_rand(); - if ($job->getFromDBwithData($ID, 0)) { - $bgcolor = $_SESSION["glpipriority_".$job->fields["priority"]]; - $name = sprintf(__('%1$s: %2$s'), __('ID'), $job->fields["id"]); - // $rand = mt_rand(); - echo ""; - echo ""; - echo ""; - - echo ""; - - // Finish Line - echo ""; - } else { - echo ""; - echo ""; - } - } - - - public static function getCommonCriteria() { - $criteria = parent::getCommonCriteria(); - - $criteria['LEFT JOIN']['glpi_tickettasks'] = [ - 'ON' => [ - self::getTable() => 'id', - 'glpi_tickettasks' => 'tickets_id' - ] - ]; - - return $criteria; - } - - - /** - * @deprecated 9.5.0 - */ - static function getCommonSelect() { - Toolbox::deprecated('Use getCommonCriteria with db iterator'); - $SELECT = ""; - if (count($_SESSION["glpiactiveentities"])>1) { - $SELECT .= ", `glpi_entities`.`completename` AS entityname, - `glpi_tickets`.`entities_id` AS entityID "; - } - - return " DISTINCT `glpi_tickets`.*, - `glpi_itilcategories`.`completename` AS catname - $SELECT"; - } - - - /** - * @deprecated 9.5.0 - */ - static function getCommonLeftJoin() { - Toolbox::deprecated('Use getCommonCriteria with db iterator'); - - $FROM = ""; - if (count($_SESSION["glpiactiveentities"])>1) { - $FROM .= " LEFT JOIN `glpi_entities` - ON (`glpi_entities`.`id` = `glpi_tickets`.`entities_id`) "; - } - - return " LEFT JOIN `glpi_groups_tickets` - ON (`glpi_tickets`.`id` = `glpi_groups_tickets`.`tickets_id`) - LEFT JOIN `glpi_tickets_users` - ON (`glpi_tickets`.`id` = `glpi_tickets_users`.`tickets_id`) - LEFT JOIN `glpi_suppliers_tickets` - ON (`glpi_tickets`.`id` = `glpi_suppliers_tickets`.`tickets_id`) - LEFT JOIN `glpi_itilcategories` - ON (`glpi_tickets`.`itilcategories_id` = `glpi_itilcategories`.`id`) - LEFT JOIN `glpi_tickettasks` - ON (`glpi_tickets`.`id` = `glpi_tickettasks`.`tickets_id`) - LEFT JOIN `glpi_items_tickets` - ON (`glpi_tickets`.`id` = `glpi_items_tickets`.`tickets_id`) - $FROM"; - - } - - - /** - * @param $output - **/ - static function showPreviewAssignAction($output) { - - //If ticket is assign to an object, display this information first - if (isset($output["entities_id"]) - && isset($output["items_id"]) - && isset($output["itemtype"])) { - - if ($item = getItemForItemtype($output["itemtype"])) { - if ($item->getFromDB($output["items_id"])) { - echo ""; - echo ""; - - echo ""; - echo ""; - } - } - - unset($output["items_id"]); - unset($output["itemtype"]); - } - unset($output["entities_id"]); - return $output; - } - - - /** - * Give cron information - * - * @param $name : task's name - * - * @return array of information - **/ - static function cronInfo($name) { - - switch ($name) { - case 'closeticket' : - return ['description' => __('Automatic tickets closing')]; - - case 'alertnotclosed' : - return ['description' => __('Not solved tickets')]; - - case 'createinquest' : - return ['description' => __('Generation of satisfaction surveys')]; - - case 'purgeticket': - return ['description' => __('Automatic closed tickets purge')]; - } - return []; - } - - - /** - * Cron for ticket's automatic close - * - * @param $task : crontask object - * - * @return integer (0 : nothing done - 1 : done) - **/ - static function cronCloseTicket($task) { - global $DB; - - $ticket = new self(); - - // Recherche des entit??s - $tot = 0; - - $entities = $DB->request( - [ - 'SELECT' => 'id', - 'FROM' => Entity::getTable(), - ] - ); - foreach ($entities as $entity) { - $delay = Entity::getUsedConfig('autoclose_delay', $entity['id'], '', Entity::CONFIG_NEVER); - if ($delay >= 0) { - $criteria = [ - 'FROM' => self::getTable(), - 'WHERE' => [ - 'entities_id' => $entity['id'], - 'status' => self::SOLVED, - 'is_deleted' => 0 - ] - ]; - - if ($delay > 0) { - $calendars_id = Entity::getUsedConfig('calendars_id', $entity['id']); - $calendar = new Calendar(); - if ($calendars_id && $calendar->getFromDB($calendars_id) && $calendar->hasAWorkingDay()) { - $end_date = $calendar->computeEndDate( - date('Y-m-d H:i:s'), - - $delay * DAY_TIMESTAMP, - 0, - true - ); - $criteria['WHERE']['solvedate'] = ['<=', $end_date]; - } else { - // no calendar, remove all days - $criteria['WHERE'][] = new \QueryExpression( - "ADDDATE(" . $DB->quoteName('solvedate') . ", INTERVAL $delay DAY) < NOW()" - ); - } - } - - $nb = 0; - $iterator = $DB->request($criteria); - while ($tick = $iterator->next()) { - $ticket->update([ - 'id' => $tick['id'], - 'status' => self::CLOSED, - '_auto_update' => true - ]); - $nb++; - } - - if ($nb) { - $tot += $nb; - $task->addVolume($nb); - $task->log(Dropdown::getDropdownName('glpi_entities', $entity['id'])." : $nb"); - } - } - } - - return ($tot > 0 ? 1 : 0); - } - - - /** - * Cron for alert old tickets which are not solved - * - * @param $task : crontask object - * - * @return integer (0 : nothing done - 1 : done) - **/ - static function cronAlertNotClosed($task) { - global $DB, $CFG_GLPI; - - if (!$CFG_GLPI["use_notifications"]) { - return 0; - } - // Recherche des entit??s - $tot = 0; - foreach (Entity::getEntitiesToNotify('notclosed_delay') as $entity => $value) { - $iterator = $DB->request([ - 'FROM' => self::getTable(), - 'WHERE' => [ - 'entities_id' => $entity, - 'is_deleted' => 0, - 'status' => [ - self::INCOMING, - self::ASSIGNED, - self::PLANNED, - self::WAITING - ], - 'closedate' => null, - new QueryExpression("ADDDATE(" . $DB->quoteName('date') . ", INTERVAL $value DAY) < NOW()") - ] - ]); - $tickets = []; - while ($tick = $iterator->next()) { - $tickets[] = $tick; - } - - if (!empty($tickets)) { - if (NotificationEvent::raiseEvent('alertnotclosed', new self(), - ['items' => $tickets, - 'entities_id' => $entity])) { - - $tot += count($tickets); - $task->addVolume(count($tickets)); - $task->log(sprintf(__('%1$s: %2$s'), - Dropdown::getDropdownName('glpi_entities', $entity), - count($tickets))); - } - } - } - - return ($tot > 0 ? 1 : 0); - } - - - /** - * Cron for ticketsatisfaction's automatic generated - * - * @param $task : crontask object - * - * @return integer (0 : nothing done - 1 : done) - **/ - static function cronCreateInquest($task) { - global $DB; - - $conf = new Entity(); - $inquest = new TicketSatisfaction(); - $tot = 0; - $maxentity = []; - $tabentities = []; - - $rate = Entity::getUsedConfig('inquest_config', 0, 'inquest_rate'); - if ($rate > 0) { - $tabentities[0] = $rate; - } - - foreach ($DB->request('glpi_entities') as $entity) { - $rate = Entity::getUsedConfig('inquest_config', $entity['id'], 'inquest_rate'); - $parent = Entity::getUsedConfig('inquest_config', $entity['id'], 'entities_id'); - - if ($rate > 0) { - $tabentities[$entity['id']] = $rate; - } - } - - foreach ($tabentities as $entity => $rate) { - $parent = Entity::getUsedConfig('inquest_config', $entity, 'entities_id'); - $delay = Entity::getUsedConfig('inquest_config', $entity, 'inquest_delay'); - $duration = Entity::getUsedConfig('inquest_config', $entity, 'inquest_duration'); - $type = Entity::getUsedConfig('inquest_config', $entity); - $max_closedate = Entity::getUsedConfig('inquest_config', $entity, 'max_closedate'); - - $table = self::getTable(); - $iterator = $DB->request([ - 'SELECT' => [ - "$table.id", - "$table.closedate", - "$table.entities_id" - ], - 'FROM' => $table, - 'LEFT JOIN' => [ - 'glpi_ticketsatisfactions' => [ - 'ON' => [ - 'glpi_ticketsatisfactions' => 'tickets_id', - 'glpi_tickets' => 'id' - ] - ], - 'glpi_entities' => [ - 'ON' => [ - 'glpi_tickets' => 'entities_id', - 'glpi_entities' => 'id' - ] - ] - ], - 'WHERE' => [ - "$table.entities_id" => $entity, - "$table.is_deleted" => 0, - "$table.status" => self::CLOSED, - "$table.closedate" => ['>', $max_closedate], - new QueryExpression("ADDDATE(" . $DB->quoteName("$table.closedate") . ", INTERVAL $delay DAY) <= NOW()"), - new QueryExpression("ADDDATE(" . $DB->quoteName("glpi_entities.max_closedate") . ", INTERVAL $duration DAY) <= NOW()"), - "glpi_ticketsatisfactions.id" => null - ], - 'ORDERBY' => 'closedate ASC' - ]); - - $nb = 0; - $max_closedate = ''; - - while ($tick = $iterator->next()) { - $max_closedate = $tick['closedate']; - if (mt_rand(1, 100) <= $rate) { - if ($inquest->add(['tickets_id' => $tick['id'], - 'date_begin' => $_SESSION["glpi_currenttime"], - 'entities_id' => $tick['entities_id'], - 'type' => $type])) { - $nb++; - } - } - } - - // conservation de toutes les max_closedate des entites filles - if (!empty($max_closedate) - && (!isset($maxentity[$parent]) - || ($max_closedate > $maxentity[$parent]))) { - $maxentity[$parent] = $max_closedate; - } - - if ($nb) { - $tot += $nb; - $task->addVolume($nb); - $task->log(sprintf(__('%1$s: %2$s'), - Dropdown::getDropdownName('glpi_entities', $entity), $nb)); - } - } - - // Sauvegarde du max_closedate pour ne pas tester les m??me tickets 2 fois - foreach ($maxentity as $parent => $maxdate) { - $conf->getFromDB($parent); - $conf->update(['id' => $conf->fields['id'], - //'entities_id' => $parent, - 'max_closedate' => $maxdate]); - } - - return ($tot > 0 ? 1 : 0); - } - - - /** - * Cron for ticket's automatic purge - * - * @param CronTask $task CronTask object - * - * @return integer (0 : nothing done - 1 : done) - **/ - static function cronPurgeTicket(CronTask $task) { - global $DB; - - $ticket = new self(); - - //search entities - $tot = 0; - - $entities = $DB->request( - [ - 'SELECT' => 'id', - 'FROM' => Entity::getTable(), - ] - ); - - foreach ($entities as $entity) { - $delay = Entity::getUsedConfig('autopurge_delay', $entity['id'], '', Entity::CONFIG_NEVER); - if ($delay >= 0) { - $criteria = [ - 'FROM' => $ticket->getTable(), - 'WHERE' => [ - 'entities_id' => $entity['id'], - 'status' => $ticket->getClosedStatusArray(), - ] - ]; - - if ($delay > 0) { - // remove all days - $criteria['WHERE'][] = new \QueryExpression("ADDDATE(`closedate`, INTERVAL ".$delay." DAY) < NOW()"); - } - - $iterator = $DB->request($criteria); - $nb = 0; - - foreach ($iterator as $tick) { - $ticket->delete( - [ - 'id' => $tick['id'], - '_auto_update' => true - ], - true - ); - $nb++; - } - - if ($nb) { - $tot += $nb; - $task->addVolume($nb); - $task->log(Dropdown::getDropdownName('glpi_entities', $entity['id'])." : $nb"); - } - } - } - - return ($tot > 0 ? 1 : 0); - } - - /** - * Display debug information for current object - **/ - function showDebug() { - NotificationEvent::debugEvent($this); - } - - - /** - * @since 0.85 - * - * @see commonDBTM::getRights() - **/ - function getRights($interface = 'central') { - - $values = parent::getRights(); - unset($values[READ]); - $values[self::READMY] = __('See my ticket'); - //TRANS: short for : See tickets created by my groups - $values[self::READGROUP] = ['short' => __('See group ticket'), - 'long' => __('See tickets created by my groups')]; - if ($interface == 'central') { - $values[self::READALL] = __('See all tickets'); - //TRANS: short for : See assigned tickets (group associated) - $values[self::READASSIGN] = ['short' => __('See assigned'), - 'long' => __('See assigned tickets')]; - //TRANS: short for : Assign a ticket - $values[self::ASSIGN] = ['short' => __('Assign'), - 'long' => __('Assign a ticket')]; - //TRANS: short for : Steal a ticket - $values[self::STEAL] = ['short' => __('Steal'), - 'long' => __('Steal a ticket')]; - //TRANS: short for : To be in charge of a ticket - $values[self::OWN] = ['short' => __('Beeing in charge'), - 'long' => __('To be in charge of a ticket')]; - $values[self::CHANGEPRIORITY] = __('Change the priority'); - $values[self::SURVEY] = ['short' => __('Approve solution/Reply survey (my ticket)'), - 'long' => __('Approve solution and reply to survey for ticket created by me')]; - } - if ($interface == 'helpdesk') { - unset($values[UPDATE], $values[DELETE], $values[PURGE]); - } - return $values; - } - - /** - * Convert img of the collector for ticket - * - * @since 0.85 - * - * @param string $html html content of input - * @param array $files filenames - * @param array $tags image tags - * - * @return string html content - **/ - static function convertContentForTicket($html, $files, $tags) { - - preg_match_all("/src\s*=\s*['|\"](.+?)['|\"]/", $html, $matches, PREG_PATTERN_ORDER); - if (isset($matches[1]) && count($matches[1])) { - // Get all image src - - foreach ($matches[1] as $src) { - // Set tag if image matches - foreach ($files as $data => $filename) { - if (preg_match("/".$data."/i", $src)) { - $html = preg_replace("`]*\>`", "

".Document::getImageTag($tags[$filename])."

", $html); - } - } - } - } - - return $html; - - } - - - /** - * @since 0.90 - * - * @param $tickets_id - * @param $action (default 'add') - **/ - static function getSplittedSubmitButtonHtml($tickets_id, $action = "add") { - - $locale = _sx('button', 'Add'); - if ($action == 'update') { - $locale = _x('button', 'Save'); - } - $ticket = new self(); - $ticket->getFromDB($tickets_id); - $all_status = Ticket::getAllowedStatusArray($ticket->fields['status']); - $rand = mt_rand(); - - $html = "
- -   -
    "; - foreach ($all_status as $status_key => $status_label) { - $checked = ""; - if ($status_key == $ticket->fields['status']) { - $checked = "checked='checked'"; - } - $html .= "
  • "; - $html .= ""; - $html .= ""; - $html .= "
  • "; - } - $html .= "
"; - - $html.= ""; - return $html; - } - - - /** - * Get correct Calendar: Entity or Sla - * - * @since 0.90.4 - * - **/ - function getCalendar() { - - if (isset($this->fields['slas_id_ttr']) && $this->fields['slas_id_ttr'] > 0) { - $slm = new SLM(); - if ($slm->getFromDB($this->fields['slas_id_ttr'])) { - // not -1: calendar of the entity - if ($slm->getField('calendars_id') >= 0) { - return $slm->getField('calendars_id'); - } - } - } - return parent::getCalendar(); - } - - - /** - * Select a field using standard system - * - * @since 9.1 - */ - function getValueToSelect($field_id_or_search_options, $name = '', $values = '', $options = []) { - if (isset($field_id_or_search_options['linkfield'])) { - switch ($field_id_or_search_options['linkfield']) { - case 'requesttypes_id': - if (isset($field_id_or_search_options['joinparams']) && Toolbox::in_array_recursive('glpi_itilfollowups', $field_id_or_search_options['joinparams'])) { - $opt = ['is_itilfollowup' => 1]; - } else { - $opt = [ - 'OR' => [ - 'is_mail_default' => 1, - 'is_ticketheader' => 1 - ] - ]; - } - if ($field_id_or_search_options['linkfield'] == $name) { - $opt['is_active'] = 1; - } - if (isset( $options['condition'] )) { - if (!is_array($options['condition'])) { - $options['condition'] = [$options['condition']]; - } - $opt = array_merge($opt, $options['condition']); - } - $options['condition'] = $opt; - break; - } - } - return parent::getValueToSelect($field_id_or_search_options, $name, $values, $options); - } - - function showStatsDates() { - $now = time(); - $date_creation = strtotime($this->fields['date']); - $date_takeintoaccount = $date_creation + $this->fields['takeintoaccount_delay_stat']; - if ($date_takeintoaccount == $date_creation) { - $date_takeintoaccount = 0; - } - $internal_time_to_own = strtotime($this->fields['internal_time_to_own']); - $time_to_own = strtotime($this->fields['time_to_own']); - $internal_time_to_resolve = strtotime($this->fields['internal_time_to_resolve']); - $time_to_resolve = strtotime($this->fields['time_to_resolve']); - $solvedate = strtotime($this->fields['solvedate']); - $closedate = strtotime($this->fields['closedate']); - $goal_takeintoaccount = ($date_takeintoaccount > 0 ? $date_takeintoaccount : $now); - $goal_solvedate = ($solvedate > 0 ? $solvedate : $now); - - $sla = new SLA; - $ola = new OLA; - $sla_tto_link = - $sla_ttr_link = - $ola_tto_link = - $ola_ttr_link = ""; - - if ($sla->getFromDB($this->fields['slas_id_tto'])) { - $sla_tto_link = " - "; - } - if ($sla->getFromDB($this->fields['slas_id_ttr'])) { - $sla_ttr_link = " - "; - } - if ($ola->getFromDB($this->fields['olas_id_tto'])) { - $ola_tto_link = " - "; - } - if ($ola->getFromDB($this->fields['olas_id_ttr'])) { - $ola_ttr_link = " - "; - } - - $dates = [ - $date_creation.'_date_creation' => [ - 'timestamp' => $date_creation, - 'label' => __('Opening date'), - 'class' => 'creation' - ], - $date_takeintoaccount.'_date_takeintoaccount' => [ - 'timestamp' => $date_takeintoaccount, - 'label' => __('Take into account'), - 'class' => 'checked' - ], - $internal_time_to_own.'_internal_time_to_own' => [ - 'timestamp' => $internal_time_to_own, - 'label' => __('Internal time to own')." ".$ola_tto_link, - 'class' => ($internal_time_to_own < $goal_takeintoaccount - ? 'passed' : '')." ". - ($date_takeintoaccount != '' - ? 'checked' : ''), - ], - $time_to_own.'_time_to_own' => [ - 'timestamp' => $time_to_own, - 'label' => __('Time to own')." ".$sla_tto_link, - 'class' => ($time_to_own < $goal_takeintoaccount - ? 'passed' : '')." ". - ($date_takeintoaccount != '' - ? 'checked' : ''), - ], - $internal_time_to_resolve.'_internal_time_to_resolve' => [ - 'timestamp' => $internal_time_to_resolve, - 'label' => __('Internal time to resolve')." ".$ola_ttr_link, - 'class' => ($internal_time_to_resolve < $goal_solvedate - ? 'passed' : '')." ". - ($solvedate != '' - ? 'checked' : '') - ], - $time_to_resolve.'_time_to_resolve' => [ - 'timestamp' => $time_to_resolve, - 'label' => __('Time to resolve')." ".$sla_ttr_link, - 'class' => ($time_to_resolve < $goal_solvedate - ? 'passed' : '')." ". - ($solvedate != '' - ? 'checked' : '') - ], - $solvedate.'_solvedate' => [ - 'timestamp' => $solvedate, - 'label' => __('Resolution date'), - 'class' => 'checked' - ], - $closedate.'_closedate' => [ - 'timestamp' => $closedate, - 'label' => __('Closing date'), - 'class' => 'end' - ] - ]; - - Html::showDatesTimelineGraph([ - 'title' => _n('Date', 'Dates', Session::getPluralNumber()), - 'dates' => $dates, - 'add_now' => $this->getField('closedate') == "" - ]); - } - - /** - * Fill input with values related to business rules. - * - * @param array $input - * - * @return void - */ - private function fillInputForBusinessRules(array &$input) { - global $DB; - - $entities_id = isset($input['entities_id']) - ? $input['entities_id'] - : $this->fields['entities_id']; - - // If creation date is not set, then we're called during ticket creation - $creation_date = !empty($this->fields['date_creation']) - ? strtotime($this->fields['date_creation']) - : time(); - - // add calendars matching date creation (for business rules) - $calendars = []; - $ite_calendar = $DB->request([ - 'SELECT' => ['id'], - 'FROM' => Calendar::getTable(), - 'WHERE' => getEntitiesRestrictCriteria('', '', $entities_id, true) - ]); - foreach ($ite_calendar as $calendar_data) { - $calendar = new Calendar(); - $calendar->getFromDB($calendar_data['id']); - if ($calendar->isAWorkingHour($creation_date)) { - $calendars[] = $calendar_data['id']; - } - } - if (count($calendars)) { - $input['_date_creation_calendars_id'] = $calendars; - } - } - - /** - * Build parent condition for search - * - * @param string $fieldID field used in the condition: tickets_id, items_id - * - * @return string - */ - public static function buildCanViewCondition($fieldID) { - - $condition = ""; - $user = Session::getLoginUserID(); - $groups = "'" . implode("','", $_SESSION['glpigroups']) . "'"; - - $requester = CommonITILActor::REQUESTER; - $assign = CommonITILActor::ASSIGN; - $obs = CommonITILActor::OBSERVER; - - // Avoid empty IN () - if ($groups == "''") { - $groups = '-1'; - } - - if (Session::haveRight("ticket", Ticket::READMY)) { - // Add tickets where the users is requester, observer or recipient - // Subquery for requester/observer user - $user_query = "SELECT `tickets_id` - FROM `glpi_tickets_users` - WHERE `users_id` = '$user' AND type IN ($requester, $obs)"; - $condition .= "OR `$fieldID` IN ($user_query) "; - - // Subquery for recipient - $recipient_query = "SELECT `id` - FROM `glpi_tickets` - WHERE `users_id_recipient` = '$user'"; - $condition .= "OR `$fieldID` IN ($recipient_query) "; - } - - if (Session::haveRight("ticket", Ticket::READGROUP)) { - // Add tickets where the users is in a requester or observer group - // Subquery for requester/observer group - $group_query = "SELECT `tickets_id` - FROM `glpi_groups_tickets` - WHERE `groups_id` IN ($groups) AND type IN ($requester, $obs)"; - $condition .= "OR `$fieldID` IN ($group_query) "; - } - - if (Session::haveRightsOr("ticket", [ - Ticket::OWN, - Ticket::READASSIGN - ])) { - // Add tickets where the users is assigned - // Subquery for assigned user - $user_query = "SELECT `tickets_id` - FROM `glpi_tickets_users` - WHERE `users_id` = '$user' AND type = $assign"; - $condition .= "OR `$fieldID` IN ($user_query) "; - } - - if (Session::haveRight("ticket", Ticket::READASSIGN)) { - // Add tickets where the users is part of an assigned group - // Subquery for assigned group - $group_query = "SELECT `tickets_id` - FROM `glpi_groups_tickets` - WHERE `groups_id` IN ($groups) AND type = $assign"; - $condition .= "OR `$fieldID` IN ($group_query) "; - - if (Session::haveRight('ticket', Ticket::ASSIGN)) { - // Add new tickets - $tickets_query = "SELECT `id` - FROM `glpi_tickets` - WHERE `status` = '" . CommonITILObject::INCOMING . "'"; - $condition .= "OR `$fieldID` IN ($tickets_query) "; - } - } - - if (Session::haveRightsOr('ticketvalidation', [ - TicketValidation::VALIDATEINCIDENT, - TicketValidation::VALIDATEREQUEST - ])) { - // Add tickets where the users is the validator - // Subquery for validator - $validation_query = "SELECT `tickets_id` - FROM `glpi_ticketvalidations` - WHERE `users_id_validate` = '$user'"; - $condition .= "OR `$fieldID` IN ($validation_query) "; - } - - return $condition; - } - - public function getForbiddenSingleMassiveActions() { - $excluded = parent::getForbiddenSingleMassiveActions(); - if (in_array($this->fields['status'], $this->getClosedStatusArray())) { - //for closed Tickets, only keep transfer and unlock - $excluded[] = 'TicketValidation:submit_validation'; - $excluded[] = 'Ticket:*'; - } - return $excluded; - } - - public function getWhitelistedSingleMassiveActions() { - $whitelist = parent::getWhitelistedSingleMassiveActions(); - - if (!in_array($this->fields['status'], $this->getClosedStatusArray())) { - $whitelist[] = 'Item_Ticket:add_item'; - } - - return $whitelist; - } - - /** - * Merge one or more tickets into another existing ticket. - * Optionally sub-items like followups, documents, and tasks can be copied into the merged ticket. - * If a ticket cannot be merged, the process continues on to the next ticket. - * @param int $merge_target_id The ID of the ticket that the other tickets will be merged into - * @param array $ticket_ids Array of IDs of tickets to merge into the ticket with ID $merge_target_id - * @param array $params Array of parameters for the ticket merge. - * linktypes - Array of itemtypes that will be duplicated into the ticket $merge_target_id. - * By default, no sub-items are copied. Currently supported link types are ITILFollowup, Document, and TicketTask. - * full_transaction - Boolean value indicating if the entire merge must complete successfully, or if partial merges are allowed. - * By default, the full merge must complete. On failure, all database operations performed are rolled back. - * link_type - Integer indicating the link type of the merged tickets (See types in Ticket_Ticket). - * By default, this is Ticket_Ticket::SON_OF. To disable linking, use 0 or a negative value. - * append_actors - Array of actor types to migrate into the ticket $merge_ticket. See types in CommonITILActor. - * By default, all actors are added to the ticket. - * @param array $status Reference array that this function uses to store the status of each ticket attempted to be merged. - * id => status (0 = Success, 1 = Error, 2 = Insufficient Rights). - * @return boolean True if the merge was successful if "full_transaction" is true. - * Otherwise, true if any ticket was successfully merged. - * @since 9.5.0 - */ - public static function merge(int $merge_target_id, array $ticket_ids, array &$status, array $params = []) { - global $DB; - $p = [ - 'linktypes' => [], - 'full_transaction' => true, - 'link_type' => Ticket_Ticket::SON_OF, - 'append_actors' => [CommonITILActor::REQUESTER, CommonITILActor::OBSERVER, CommonITILActor::ASSIGN] - ]; - $p = array_replace($p, $params); - $ticket = new Ticket(); - $merge_target = new Ticket(); - $merge_target->getFromDB($merge_target_id); - $fup = new ITILFollowup(); - $document_item = new Document_Item(); - $task = new TicketTask(); - - if (!$merge_target->canAddFollowups()) { - foreach ($ticket_ids as $id) { - Toolbox::logError(sprintf(__('Not enough rights to merge tickets %d and %d'), $merge_target_id, $id)); - // Set status = 2 : Rights issue - $status[$id] = 2; - } - return false; - } - $in_transaction = $DB->inTransaction(); - - if ($p['full_transaction'] && !$in_transaction) { - $DB->beginTransaction(); - } - foreach ($ticket_ids as $id) { - try { - if (!$p['full_transaction'] && !$in_transaction) { - $DB->beginTransaction(); - } - if ($merge_target->canUpdateItem() && $ticket->can($id, DELETE)) { - if (!$ticket->getFromDB($id)) { - //Cannot retrieve ticket. Abort/fail the merge - throw new \RuntimeException(sprintf(__('Failed to load ticket %d'), $id), 1); - } - //Build followup from the original ticket - $input = [ - 'itemtype' => 'Ticket', - 'items_id' => $merge_target_id, - 'content' => $DB->escape($ticket->fields['name']."\n\n".$ticket->fields['content']), - 'users_id' => $ticket->fields['users_id_recipient'], - 'date_creation' => $ticket->fields['date_creation'], - 'date_mod' => $ticket->fields['date_mod'], - 'date' => $ticket->fields['date_creation'], - 'sourceitems_id' => $ticket->getID() - ]; - if (!$fup->add($input)) { - //Cannot add followup. Abort/fail the merge - throw new \RuntimeException(sprintf(__('Failed to add followup to ticket %d'), $merge_target_id), 1); - } - if (in_array('ITILFollowup', $p['linktypes'])) { - // Copy any followups to the ticket - $tomerge = $fup->find([ - 'items_id' => $id, - 'itemtype' => 'Ticket' - ]); - foreach ($tomerge as $fup2) { - $fup2['items_id'] = $merge_target_id; - $fup2['sourceitems_id'] = $id; - $fup2['content'] = $DB->escape($fup2['content']); - unset($fup2['id']); - if (!$fup->add($fup2)) { - // Cannot add followup. Abort/fail the merge - throw new \RuntimeException(sprintf(__('Failed to add followup to ticket %d'), $merge_target_id), 1); - } - } - } - if (in_array('TicketTask', $p['linktypes'])) { - $merge_tmp = ['tickets_id' => $merge_target_id]; - if (!$task->can(-1, CREATE, $merge_tmp)) { - throw new \RuntimeException(sprintf(__('Not enough rights to merge tickets %d and %d'), $merge_target_id, $id), 2); - } - // Copy any tasks to the ticket - $tomerge = $task->find([ - 'tickets_id' => $id - ]); - foreach ($tomerge as $task2) { - $task2['tickets_id'] = $merge_target_id; - $task2['sourceitems_id'] = $id; - $task2['content'] = $DB->escape($task2['content']); - unset($task2['id']); - unset($task2['uuid']); - if (!$task->add($task2)) { - //Cannot add followup. Abort/fail the merge - throw new \RuntimeException(sprintf(__('Failed to add task to ticket %d'), $merge_target_id), 1); - } - } - } - if (in_array('Document', $p['linktypes'])) { - if (!$merge_target->canAddItem('Document')) { - throw new \RuntimeException(sprintf(__('Not enough rights to merge tickets %d and %d'), $merge_target_id, $id), 2); - } - $tomerge = $document_item->find([ - 'itemtype' => 'Ticket', - 'items_id' => $id, - 'NOT' => [ - 'documents_id' => new \QuerySubQuery([ - 'SELECT' => 'documents_id', - 'FROM' => $document_item->getTable(), - 'WHERE' => [ - 'itemtype' => 'Ticket', - 'items_id' => $merge_target_id - ] - ]) - ] - ]); - - foreach ($tomerge as $document_item2) { - $document_item2['items_id'] = $merge_target_id; - unset($document_item2['id']); - if (!$document_item->add($document_item2)) { - //Cannot add document. Abort/fail the merge - throw new \RuntimeException(sprintf(__('Failed to add document to ticket %d'), $merge_target_id), 1); - } - } - } - if ($p['link_type'] > 0 && $p['link_type'] < 5) { - //Add relation (this is parent of merge target) - $tt = new Ticket_Ticket(); - $linkparams = [ - 'link' => $p['link_type'], - 'tickets_id_1' => $id, - 'tickets_id_2' => $merge_target_id - ]; - $tt->deleteByCriteria([ - 'OR' => [ - [ - 'AND' => [ - 'tickets_id_1' => $merge_target_id, - 'tickets_id_2' => $id - ] - ], - [ - 'AND' => [ - 'tickets_id_2' => $merge_target_id, - 'tickets_id_1' => $id - ] - ] - ] - ]); - if (!$tt->add($linkparams)) { - //Cannot link tickets. Abort/fail the merge - throw new \RuntimeException(sprintf(__('Failed to link tickets %d and %d'), $merge_target_id, $id), 1); - } - } - if (isset($p['append_actors'])) { - $tu = new Ticket_User(); - $existing_users = $tu->find(['tickets_id' => $merge_target_id]); - $gt = new Group_Ticket(); - $existing_groups = $gt->find(['tickets_id' => $merge_target_id]); - $st = new Supplier_Ticket(); - $existing_suppliers = $st->find(['tickets_id' => $merge_target_id]); - - foreach ($p['append_actors'] as $actor_type) { - $users = $tu->find([ - 'tickets_id' => $id, - 'type' => $actor_type - ]); - $groups = $gt->find([ - 'tickets_id' => $id, - 'type' => $actor_type - ]); - $suppliers = $st->find([ - 'tickets_id' => $id, - 'type' => $actor_type - ]); - $users = array_filter($users, function($user) use ($existing_users) { - foreach ($existing_users as $existing_user) { - if ($existing_user['users_id'] > 0 && $user['users_id'] > 0 && - $existing_user['users_id'] === $user['users_id'] && - $existing_user['type'] === $user['type']) { - // Internal users - return false; - } else if ($existing_user['users_id'] == 0 && $user['users_id'] == 0 && - $existing_user['alternative_email'] === $user['alternative_email'] && - $existing_user['type'] === $user['type']) { - // External users - return false; - } - } - return true; - }); - $groups = array_filter($groups, function($group) use ($existing_groups) { - foreach ($existing_groups as $existing_group) { - if ($existing_group['groups_id'] === $group['groups_id'] && - $existing_group['type'] === $group['type']) { - return false; - } - } - return true; - }); - $suppliers = array_filter($suppliers, function($supplier) use ($existing_suppliers) { - foreach ($existing_suppliers as $existing_supplier) { - if ($existing_supplier['suppliers_id'] > 0 && $supplier['suppliers_id'] > 0 && - $existing_supplier['suppliers_id'] === $supplier['suppliers_id'] && - $existing_supplier['type'] === $supplier['type']) { - // Internal suppliers - return false; - } else if ($existing_supplier['suppliers_id'] == 0 && $supplier['suppliers_id'] == 0 && - $existing_supplier['alternative_email'] === $supplier['alternative_email'] && - $existing_supplier['type'] === $supplier['type']) { - // External suppliers - return false; - } - } - return true; - }); - foreach ($users as $user) { - $user['tickets_id'] = $merge_target_id; - unset($user['id']); - $tu->add($user); - } - foreach ($groups as $group) { - $group['tickets_id'] = $merge_target_id; - unset($group['id']); - $gt->add($group); - } - foreach ($suppliers as $supplier) { - $supplier['tickets_id'] = $merge_target_id; - unset($supplier['id']); - $st->add($supplier); - } - } - } - //Delete this ticket - if (!$ticket->delete(['id' => $id, '_disablenotif' => true])) { - throw new \RuntimeException(sprintf(__('Failed to delete ticket %d'), $id), 1); - } - if (!$p['full_transaction'] && !$in_transaction) { - $DB->commit(); - } - $status[$id] = 0; - Event::log($merge_target_id, 'ticket', 4, 'tracking', - sprintf(__('%s merges ticket %s into %s'), $_SESSION['glpiname'], - $id, $merge_target_id)); - } else { - throw new \RuntimeException(sprintf(__('Not enough rights to merge tickets %d and %d'), $merge_target_id, $id), 2); - } - } catch (\RuntimeException $e) { - if ($e->getCode() < 1 || $e->getCode() > 2) { - $status[$id] = 1; - } else { - $status[$id] = $e->getCode(); - } - Toolbox::logError($e->getMessage()); - if (!$in_transaction) { - $DB->rollBack(); - } - if ($p['full_transaction']) { - return false; - } - } - } - if ($p['full_transaction'] && !$in_transaction) { - $DB->commit(); - } - return true; - } - - - static function getIcon() { - return "fas fa-exclamation-circle"; - } -} diff --git a/debian/CAS-1.3.8.tgz b/debian/CAS-1.3.8.tgz deleted file mode 100644 index eb5c0ee347a6c24de079ded469e8f3b60b774453..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 98163 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 A#Q*>R diff --git a/debian/Dockerfile b/debian/Dockerfile deleted file mode 100644 index 4856f2d..0000000 --- a/debian/Dockerfile +++ /dev/null @@ -1,50 +0,0 @@ -FROM php:7.4-apache-buster -MAINTAINER Benoit LORAND - -WORKDIR /root -ENV GLPI_CONFIG_DIR=/etc/glpi -ENV GLPI_VAR_DIR=/var/lib/glpi -ENV GLPI_LOG_DIR=/var/log/glpi -ENV GLPI_VERSION=9.5.1 -ENV FUSIONINVENTORY_VERSION=9.5.0+1.0 - -RUN \ -apt-get update && \ -apt-get install --no-install-recommends -y \ - runit \ - cron \ - libbz2-dev \ - libzip-dev \ - libxml2-dev \ - libldap2-dev \ - libicu-dev \ - libpng-dev \ - zlib1g-dev \ - default-mysql-client \ - && \ -pecl install apcu && docker-php-ext-enable apcu && \ -docker-php-ext-configure mysqli && docker-php-ext-install mysqli && \ -docker-php-ext-configure gd && docker-php-ext-install gd && \ -docker-php-ext-configure intl && docker-php-ext-install intl && \ -docker-php-ext-configure ldap && docker-php-ext-install ldap && \ -docker-php-ext-configure xmlrpc && docker-php-ext-install xmlrpc && \ -docker-php-ext-configure exif && docker-php-ext-install exif && \ -docker-php-ext-configure zip && docker-php-ext-install zip && \ -docker-php-ext-configure bz2 && docker-php-ext-install bz2 && \ -docker-php-ext-configure opcache && docker-php-ext-install opcache - -COPY CAS-1.3.8.tgz /root/ -RUN pear install /root/CAS-1.3.8.tgz -COPY service/ /etc/service/ -COPY glpi_init.sh /root/glpi_init.sh -COPY glpi.cron /etc/cron.d/glpi -ADD https://github.com/glpi-project/glpi/releases/download/${GLPI_VERSION}/glpi-${GLPI_VERSION}.tgz /root/glpi-${GLPI_VERSION}.tgz -ADD https://github.com/fusioninventory/fusioninventory-for-glpi/releases/download/glpi${FUSIONINVENTORY_VERSION}/fusioninventory-${FUSIONINVENTORY_VERSION}.tar.bz2 /root/fusioninventory-${FUSIONINVENTORY_VERSION}.tar.bz2 - -RUN \ -chmod a+x /root/glpi_init.sh && \ -rm -f /var/www/html/* /root/CAS-1.3.8.tgz && \ -apt-get clean && \ -rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* - -ENTRYPOINT ["/usr/bin/runsvdir", "-P", "/etc/service"] diff --git a/debian/README.md b/debian/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/debian/docker-compose.yml b/debian/docker-compose.yml deleted file mode 100644 index 917a90c..0000000 --- a/debian/docker-compose.yml +++ /dev/null @@ -1,26 +0,0 @@ -version: '3.5' -services: - web: - container_name: glpi-web - build: . - restart: always - ports: - - 8089:80 - volumes: - - ./etc/:/etc/glpi/ - - ./files/:/var/lib/glpi/ - - ./log/:/var/log/glpi/ - env_file: - - ./mysql_settings.ini - depends_on: - - db - - db: - image: mariadb - container_name: glpi-db - command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci - restart: always - env_file: - - ./mysql_settings.ini - volumes: - - ./db/:/var/lib/mysql/ diff --git a/debian/glpi.cron b/debian/glpi.cron deleted file mode 100644 index 1b31779..0000000 --- a/debian/glpi.cron +++ /dev/null @@ -1,5 +0,0 @@ -GLPI_CONFIG_DIR=/etc/glpi -GLPI_VAR_DIR=/var/lib/glpi -GLPI_LOG_DIR=/var/log/glpi - -*/1 * * * * www-data /usr/local/bin/php /var/www/html/front/cron.php diff --git a/debian/glpi_init.sh b/debian/glpi_init.sh deleted file mode 100644 index 8843288..0000000 --- a/debian/glpi_init.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/sh -GLPI_TARBALL="/root/glpi-9.5.1.tgz" -FUSION_TARBALL="/root/fusioninventory-9.5.0+1.0.tar.bz2" -NORMAL='\e[39m' -RED='\e[31m' -GREEN='\e[32m' - -msglog() { - case "${1}" in - green) - TEXT_COLOR="${GREEN}" - ;; - red) - TEXT_COLOR="${RED}" - ;; - normal) - TEXT_COLOR="${NORMAL}" - ;; - esac - DATE=$(date '+%Y %b %d %H:%M:%S') - echo ${DATE} ${TEXT_COLOR}${2}${NORMAL} -} - -waiting_for_db() { -while ! mysqlshow -h db -uroot -p${MYSQL_ROOT_PASSWORD} 2>&1 | grep "^| ${MYSQL_DATABASE}" > /dev/null 2>&1 ; do - msglog red "Waiting for mysql database initilization..." - sleep 5 -done -} - - -if [ -z "$(ls -A /var/www/html)" ] ; then - waiting_for_db - msglog red "Initialazing ${GLPI_TARBALL}..." - cd /root - tar xf ${GLPI_TARBALL} - cp -r /root/glpi/config/. /etc/glpi/. - cp -r /root/glpi/files/. /var/lib/glpi/. - rm -r /root/glpi/config /root/glpi/files - cp -r /root/glpi/. /var/www/html/. - cd /var/www/html/plugins - tar xf ${FUSION_TARBALL} - rm -r /root/glpi - mysql --host=db --user=root --password=${MYSQL_ROOT_PASSWORD} << EOF -use mysql; -GRANT SELECT ON time_zone_name TO '${MYSQL_USER}'@'%'; -EOF - cd /var/www/html - php bin/console db:install --config-dir=${GLPI_CONFIG_DIR} -L fr_FR -H db -d ${MYSQL_DATABASE} -u ${MYSQL_USER} -p ${MYSQL_PASSWORD} -n - php bin/console glpi:plugin:install -u glpi fusioninventory -n - php bin/console glpi:plugin:activate fusioninventory -n - rm install/install.php - chown -R www-data:www-data /var/www/html /etc/glpi /var/lib/glpi /var/log/glpi - msglog green "Initialazing complete..." -else - msglog green "GLPI is already initialized" - cd /var/www/html - GLPI_ACTUAL_VERSION=$(awk -F", '" '/^define\(.GLPI_VERSION/ { print $2 }' inc/define.php | sed 's/\([0-9\.]*\).*/\1/') - FUSIONINVENTORY_ACTUAL_VERSION=$(awk -F', "' '/^define \(.PLUGIN_FUSIONINVENTORY_VERSION/ { print $2 }' plugins/fusioninventory/setup.php | sed 's/\([0-9\.+]*\).*/\1/') - if [ "${GLPI_ACTUAL_VERSION}" = "${GLPI_VERSION}" -a "${FUSIONINVENTORY_ACTUAL_VERSION}" = "${FUSIONINVENTORY_VERSION}" ] ; then - msglog green "GLPI already up2date" - exit - fi - msglog red "Updating GLPI from ${GLPI_ACTUAL_VERSION} to ${GLPI_VERSION}" - waiting_for_db - php bin/console glpi:maintenance:enable -n - php bin/console glpi:plugin:deactivate fusioninventory -n - cd /root - tar xf ${GLPI_TARBALL} - rm -r glpi/config glpi/files /var/www/html - mv glpi /var/www/html - cd /var/www/html/plugins - tar xf ${FUSION_TARBALL} - rm /var/www/html/install/install.php - cd /var/www/html - php bin/console db:update --config-dir=${GLPI_CONFIG_DIR} -n - php bin/console glpi:maintenance:disable -n - chown -R www-data:www-data /var/www/html /etc/glpi /var/lib/glpi /var/log/glpi -fi diff --git a/debian/mysql_settings.ini b/debian/mysql_settings.ini deleted file mode 100644 index b66e7ca..0000000 --- a/debian/mysql_settings.ini +++ /dev/null @@ -1,4 +0,0 @@ -MYSQL_DATABASE= -MYSQL_USER= -MYSQL_PASSWORD='' -MYSQL_ROOT_PASSWORD='' diff --git a/debian/service/20-cron/run b/debian/service/20-cron/run deleted file mode 100755 index 2c89471..0000000 --- a/debian/service/20-cron/run +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh -. /etc/service/template - -msglog green "Starting Cron..." -# Touch cron files to fix 'NUMBER OF HARD LINKS > 1' issue. See https://github.com/phusion/baseimage-docker/issues/198 -touch -c /etc/crontab /etc/cron.*/* /var/spool/cron/crontabs/* -exec /usr/sbin/cron -f diff --git a/debian/service/30-apache2/run b/debian/service/30-apache2/run deleted file mode 100755 index bb78cc5..0000000 --- a/debian/service/30-apache2/run +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -. /etc/service/template - -msglog green "Starting Apache..." -exec apache2-foreground diff --git a/debian/service/90-glpi_init/run b/debian/service/90-glpi_init/run deleted file mode 100755 index 767ea4b..0000000 --- a/debian/service/90-glpi_init/run +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -. /etc/service/template - -msglog green "Starting glpi_init.sh..." -/root/glpi_init.sh -sleep infinity diff --git a/debian/service/template b/debian/service/template deleted file mode 100644 index 70543c1..0000000 --- a/debian/service/template +++ /dev/null @@ -1,19 +0,0 @@ -NORMAL='\e[39m' -RED='\e[31m' -GREEN='\e[32m' - -msglog() { - case "${1}" in - green) - TEXT_COLOR="${GREEN}" - ;; - red) - TEXT_COLOR="${RED}" - ;; - normal) - TEXT_COLOR="${NORMAL}" - ;; - esac - DATE=$(date '+%Y %b %d %H:%M:%S') - echo ${DATE} ${TEXT_COLOR}${2}${NORMAL} -} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4d01be7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,64 @@ +version: '3.5' +services: + web: + container_name: glpi-web + build: web-builder + restart: always + volumes: + - ./etc/:/etc/glpi/ + - ./files/:/var/lib/glpi/ + - ./log/:/var/log/glpi/ + - /etc/localtime:/etc/localtime:ro + env_file: + - ./mysql_settings.ini + environment: + - PHP_MEMORY_LIMIT=256M + - PHP_UPLOAD_MAX_FILESIZE=20M + - PHP_POST_MAX_SIZE=40M + - PHP_SESSION_GC_MAXLIFETIME=14400 + - PHP_DATE_TIMEZONE=Europe/Paris + - PHP_MAX_INPUT_VARS=100000 + depends_on: + - db + - redis + + cron: + container_name: glpi-cron + image : glpi-web:latest + restart: always + volumes: + - ./etc/:/etc/glpi/ + - ./files/:/var/lib/glpi/ + - ./log/:/var/log/glpi/ + - /etc/localtime:/etc/localtime:ro + env_file: + - ./mysql_settings.ini + environment: + - PHP_MEMORY_LIMIT=256M + - PHP_UPLOAD_MAX_FILESIZE=20M + - PHP_POST_MAX_SIZE=40M + - PHP_SESSION_GC_MAXLIFETIME=14400 + - PHP_DATE_TIMEZONE=Europe/Paris + entrypoint: /etc/service/cron + depends_on: + - db + - web + + db: + image: mariadb + container_name: glpi-db + command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci + restart: always + env_file: + - ./mysql_settings.ini + volumes: + - ./db/:/var/lib/mysql/ + - /etc/localtime:/etc/localtime:ro + + redis: + container_name: glpi-redis + image: redis:latest + volumes: + - ./cache:/data + restart: "always" + diff --git a/alpine/web-builder/CAS-1.3.8.tgz b/web-builder/CAS-1.3.8.tgz similarity index 100% rename from alpine/web-builder/CAS-1.3.8.tgz rename to web-builder/CAS-1.3.8.tgz diff --git a/web-builder/Dockerfile b/web-builder/Dockerfile new file mode 100644 index 0000000..aff2ad9 --- /dev/null +++ b/web-builder/Dockerfile @@ -0,0 +1,81 @@ +FROM alpine +MAINTAINER Benoit LORAND + +WORKDIR /root +ENV GLPI_CONFIG_DIR=/etc/glpi +ENV GLPI_VAR_DIR=/var/lib/glpi +ENV GLPI_LOG_DIR=/var/log/glpi +ENV GLPI_VERSION=10.0.14 +ENV FIELDS_VERSION=1.21.8 +ENV DATAINJECTION_VERSION=2.13.5 +ENV GLPIINVENTORY_VERSION=1.3.5 + + +RUN \ +apk add --no-cache \ + php82-apache2 \ + php82 \ + mariadb-client \ + php82-pecl-apcu \ + php82-pecl-redis \ + php82-mysqli \ + php82-gd \ + php82-intl \ + php82-ldap \ + php82-xml \ + php82-xmlreader \ + php82-xmlwriter \ + php82-exif \ + php82-zip \ + php82-bz2 \ + php82-opcache \ + php82-pear \ + php82-curl \ + php82-dom \ + php82-pdo \ + php82-json \ + php82-session \ + php82-ctype \ + php82-fileinfo \ + php82-mbstring \ + php82-simplexml \ + php82-iconv \ + php82-sodium \ + php82-imap \ + php82-pdo \ + php82-pdo_mysql \ + php82-pspell \ + php82-phar \ + patch + +COPY CAS-1.3.8.tgz /root/ +RUN pear82 install /root/CAS-1.3.8.tgz && \ +pear82 install Archive_Tar +COPY httpd.conf /etc/apache2 +COPY remoteip.conf /etc/apache2/conf.d +COPY service/ /etc/service/ +COPY glpi.cron /var/spool/cron/crontabs/apache +ADD https://github.com/glpi-project/glpi/releases/download/${GLPI_VERSION}/glpi-${GLPI_VERSION}.tgz /root/glpi-${GLPI_VERSION}.tgz +ADD https://github.com/pluginsGLPI/fields/releases/download/${FIELDS_VERSION}/glpi-fields-${FIELDS_VERSION}.tar.bz2 /root/glpi-fields-${FIELDS_VERSION}.tar.bz2 +ADD https://github.com/pluginsGLPI/datainjection/releases/download/${DATAINJECTION_VERSION}/glpi-datainjection-${DATAINJECTION_VERSION}.tar.bz2 /root/glpi-datainjection-${DATAINJECTION_VERSION}.tar.bz2 +ADD https://github.com/glpi-project/glpi-inventory-plugin/releases/download/${GLPIINVENTORY_VERSION}/glpi-glpiinventory-${GLPIINVENTORY_VERSION}.tar.bz2 /root/glpi-glpiinventory-${GLPIINVENTORY_VERSION}.tar.bz2 + +RUN \ +mkdir -p /root/glpi_template/etc /root/glpi_template/files && \ +tar -x -f /root/glpi-${GLPI_VERSION}.tgz && \ +cp -r /root/glpi/config/. /root/glpi_template/etc/. && \ +cp -r /root/glpi/files/. /root/glpi_template/files/. && \ +rm -r /root/glpi/config /root/glpi/files && \ +mv /root/glpi /var/www/glpi && \ +cd /var/www/glpi/marketplace && \ +tar x -f /root/glpi-fields-${FIELDS_VERSION}.tar.bz2 && \ +tar x -f /root/glpi-datainjection-${DATAINJECTION_VERSION}.tar.bz2 && \ +tar x -f /root/glpi-glpiinventory-${GLPIINVENTORY_VERSION}.tar.bz2 && \ +chmod 600 /etc/crontabs/apache && \ +rm -f /var/www/html/* /root/CAS-1.3.8.tgz /root/glpi-${GLPI_VERSION}.tgz /root/glpi-fields-${FIELDS_VERSION}.tar.bz2 /root/glpi-datainjection-${DATAINJECTION_VERSION}.tar.bz2 /root/glpi-glpiinventory-${GLPIINVENTORY_VERSION}.tar.bz2 && \ +rm -rf /tmp/* /var/tmp/* + +COPY logo.png /var/www/glpi/pics/logo.png + +WORKDIR /var/www/glpi +ENTRYPOINT ["/etc/service/glpi"] diff --git a/web-builder/glpi.cron b/web-builder/glpi.cron new file mode 100644 index 0000000..2bf32a4 --- /dev/null +++ b/web-builder/glpi.cron @@ -0,0 +1,6 @@ +GLPI_CONFIG_DIR=/etc/glpi +GLPI_VAR_DIR=/var/lib/glpi +GLPI_LOG_DIR=/var/log/glpi + +*/1 * * * * /usr/bin/php82 /var/www/glpi/front/cron.php +0 * * * * cd /var/www/glpi && /usr/bin/php82 bin/console glpi:ldap:synchronize_users -n diff --git a/web-builder/glpi_ticket.class.php.patch b/web-builder/glpi_ticket.class.php.patch new file mode 100644 index 0000000..49298db --- /dev/null +++ b/web-builder/glpi_ticket.class.php.patch @@ -0,0 +1,27 @@ +--- ticket.class.php.orig 2022-04-12 12:24:25.634142162 +0200 ++++ ticket.class.php 2022-04-12 12:21:31.000000000 +0200 +@@ -3740,6 +3740,7 @@ + + Plugin::doHook("pre_item_form", ['item' => $this, 'options' => &$options]); + ++ echo "
"; + echo ""; + echo ""; +@@ -3804,6 +3805,7 @@ + + echo ""; + } ++*/ + if (($_SESSION["glpiactiveprofile"]["helpdesk_hardware"] != 0) + && (count($_SESSION["glpiactiveprofile"]["helpdesk_item_type"]))) { + if (!$tt->isHiddenField('items_id')) { diff --git a/alpine/web-builder/httpd.conf b/web-builder/httpd.conf similarity index 98% rename from alpine/web-builder/httpd.conf rename to web-builder/httpd.conf index eb9d6df..06886a5 100644 --- a/alpine/web-builder/httpd.conf +++ b/web-builder/httpd.conf @@ -26,7 +26,7 @@ # Set to one of: Full | OS | Minor | Minimal | Major | Prod # where Full conveys the most information, and Prod the least. # -ServerTokens OS +ServerTokens Prod # # ServerRoot: The top of the directory tree under which the server's @@ -133,7 +133,7 @@ LoadModule headers_module modules/mod_headers.so #LoadModule unique_id_module modules/mod_unique_id.so LoadModule setenvif_module modules/mod_setenvif.so LoadModule version_module modules/mod_version.so -#LoadModule remoteip_module modules/mod_remoteip.so +LoadModule remoteip_module modules/mod_remoteip.so #LoadModule session_module modules/mod_session.so #LoadModule session_cookie_module modules/mod_session_cookie.so #LoadModule session_crypto_module modules/mod_session_crypto.so @@ -163,7 +163,7 @@ LoadModule dir_module modules/mod_dir.so #LoadModule speling_module modules/mod_speling.so #LoadModule userdir_module modules/mod_userdir.so LoadModule alias_module modules/mod_alias.so -#LoadModule rewrite_module modules/mod_rewrite.so +LoadModule rewrite_module modules/mod_rewrite.so LoadModule negotiation_module modules/mod_negotiation.so @@ -241,8 +241,8 @@ ServerSignature On # documents. By default, all requests are taken from this directory, but # symbolic links and aliases may be used to point to other locations. # -DocumentRoot "/var/www/glpi" - +DocumentRoot "/var/www/glpi/public" + # # Possible values for the Options directive are "None", "All", # or any combination of: @@ -268,6 +268,11 @@ DocumentRoot "/var/www/glpi" # Controls who can get stuff from this server. # Require all granted + RewriteEngine On + + # Redirect all requests to GLPI router, unless file exists. + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^(.*)$ index.php [QSA,L] # diff --git a/web-builder/remoteip.conf b/web-builder/remoteip.conf new file mode 100644 index 0000000..c38373e --- /dev/null +++ b/web-builder/remoteip.conf @@ -0,0 +1,10 @@ + + RemoteIPHeader X-Forwarded-For + RemoteIPProxiesHeader X-Forwarded-By + RemoteIPInternalProxy 172.23.0.0/24 + + + + LogFormat "%v %h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" \"X-Forwarded-For: %{X-Forwarded-For}i\" \"X-Forwarded-By: %{X-Forwarded-By}i\"" combined_fwd + CustomLog /proc/self/fd/1 combined_fwd + diff --git a/alpine/web-builder/service/20-cron/run b/web-builder/service/cron similarity index 75% rename from alpine/web-builder/service/20-cron/run rename to web-builder/service/cron index 2fa1207..069b36e 100644 --- a/alpine/web-builder/service/20-cron/run +++ b/web-builder/service/cron @@ -2,6 +2,9 @@ . /etc/service/template msglog green "Starting Cron..." +/etc/service/php + # Touch cron files to fix 'NUMBER OF HARD LINKS > 1' issue. See https://github.com/phusion/baseimage-docker/issues/198 +chown -R apache:apache /var/www/glpi /etc/glpi /var/lib/glpi /var/log/glpi touch -c /etc/crontab /etc/cron.*/* /var/spool/cron/crontabs/* exec /usr/sbin/crond -f -L /dev/stdout diff --git a/alpine/web-builder/glpi_init.sh b/web-builder/service/glpi similarity index 50% rename from alpine/web-builder/glpi_init.sh rename to web-builder/service/glpi index 1a7fe5c..310d8bb 100644 --- a/alpine/web-builder/glpi_init.sh +++ b/web-builder/service/glpi @@ -26,25 +26,15 @@ while ! mysqlshow -h db -uroot -p${MYSQL_ROOT_PASSWORD} 2>&1 | grep "^| ${MYSQL_ done } -updatephpini() { - variable=$1 - value=$2 - file="/etc/php7/php.ini" +start_apache() { + rm -r /run/* + mkdir -p /run/apache2 - if [ ! -z "${value}" ] ; then - msglog green "Updating $variable to $value in $file" - if grep "^${variable}" "${file}" > /dev/null 2<&1 ; then - sed -i "s@^\(${variable}\s*=\).*@\1 ${value}@g" "${file}" - else - echo ${variable} = ${value} >> ${file} - fi - fi + msglog green "Starting Apache..." + exec /usr/sbin/httpd -D FOREGROUND } -updatephpini memory_limit ${PHP_MEMORY_LIMIT} -updatephpini upload_max_filesize ${PHP_UPLOAD_MAX_FILESIZE} -updatephpini post_max_size ${PHP_POST_MAX_SIZE} -updatephpini date.timezone ${PHP_DATE_TIMEZONE} +/etc/service/php mkdir -p /var/www/glpi if [ ! -e "/etc/glpi/config_db.php" ] ; then @@ -58,38 +48,38 @@ use mysql; GRANT SELECT ON time_zone_name TO '${MYSQL_USER}'@'%'; EOF cd /var/www/glpi - php bin/console db:install --config-dir=${GLPI_CONFIG_DIR} -L fr_FR -H db -d ${MYSQL_DATABASE} -u ${MYSQL_USER} -p ${MYSQL_PASSWORD} -n - php bin/console glpi:plugin:install -u glpi fusioninventory -n - php bin/console glpi:plugin:activate fusioninventory -n + php82 bin/console db:install --config-dir=${GLPI_CONFIG_DIR} -L fr_FR -H db -d ${MYSQL_DATABASE} -u ${MYSQL_USER} -p ${MYSQL_PASSWORD} -n rm install/install.php chown -R apache:apache /var/www/glpi /etc/glpi /var/lib/glpi /var/log/glpi - patch -Np0 -i /root/glpi_ticket.class.php.patch echo "${GLPI_VERSION}" > /etc/glpi/glpi_actual_version - echo "${FUSIONINVENTORY_VERSION}" > /etc/glpi/fusioninventory_actual_version echo "${FIELDS_VERSION}" > /etc/glpi/fields_actual_version - echo "${DATAINJECTION_VERSION}" > /etc/glpi/datainjection_version + echo "${DATAINJECTION_VERSION}" > /etc/glpi/datainjection_actual_version + echo "${GLPIINVENTORY_VERSION}" > /etc/glpi/glpiinventory_actual_version msglog green "Initialazing complete..." else msglog green "GLPI is already initialized" - GLPI_ACTUAL_VERSION=$(cat /etc/glpi/glpi_actual_version) - FUSIONINVENTORY_ACTUAL_VERSION=$(cat /etc/glpi/fusioninventory_actual_version) - FIELDS_ACTUAL_VERSION=$(cat /etc/glpi/fields_actual_version) - DATAINJECTION_ACTUAL_VERSION=$(cat /etc/glpi/datainjection_version) - if [ "${GLPI_ACTUAL_VERSION}" = "${GLPI_VERSION}" -a "${FUSIONINVENTORY_ACTUAL_VERSION}" = "${FUSIONINVENTORY_VERSION}" -a "${FIELDS_ACTUAL_VERSION}" = "${FIELDS_VERSION}" -a "${DATAINJECTION_ACTUAL_VERSION}" = "${DATAINJECTION_VERSION}" ] ; then + GLPI_ACTUAL_VERSION=$(test -e /etc/glpi/glpi_actual_version && cat /etc/glpi/glpi_actual_version) + FIELDS_ACTUAL_VERSION=$(test -e /etc/glpi/fields_actual_version && cat /etc/glpi/fields_actual_version) + DATAINJECTION_ACTUAL_VERSION=$(test -e /etc/glpi/datainjection_actual_version && cat /etc/glpi/datainjection_actual_version) + GLPIINVENTORY_ACTUAL_VERSION=$(test -e /etc/glpi/glpiinventory_actual_version && cat /etc/glpi/glpiinventory_actual_version) + if [ "${GLPI_ACTUAL_VERSION}" = "${GLPI_VERSION}" -a "${FIELDS_ACTUAL_VERSION}" = "${FIELDS_VERSION}" -a "${DATAINJECTION_ACTUAL_VERSION}" = "${DATAINJECTION_VERSION}" -a "${GLPIINVENTORY_ACTUAL_VERSION}" = "${GLPIINVENTORY_VERSION}" ] ; then + chown -R apache:apache /var/www/glpi /etc/glpi /var/lib/glpi /var/log/glpi + [ -e "/var/www/glpi/install/install.php" ] && rm /var/www/glpi/install/install.php msglog green "GLPI already up2date" - exit + start_apache fi msglog red "Updating GLPI from ${GLPI_ACTUAL_VERSION} to ${GLPI_VERSION}" waiting_for_db cd /var/www/glpi - php bin/console glpi:maintenance:enable -n - php bin/console glpi:plugin:deactivate --all -n + php82 bin/console glpi:maintenance:enable -n + php82 bin/console glpi:plugin:deactivate --all # -n rm /var/www/glpi/install/install.php - php bin/console db:update --config-dir=${GLPI_CONFIG_DIR} -n && \ + php82 bin/console db:update --config-dir=${GLPI_CONFIG_DIR} -n && \ ( echo "${GLPI_VERSION}" > /etc/glpi/glpi_actual_version ; \ - echo "${FUSIONINVENTORY_VERSION}" > /etc/glpi/fusioninventory_actual_version ; \ echo "${FIELDS_VERSION}" > /etc/glpi/fields_actual_version ; \ - echo "${DATAINJECTION_VERSION}" > /etc/glpi/datainjection_actual_version ) - php bin/console glpi:maintenance:disable -n + echo "${DATAINJECTION_VERSION}" > /etc/glpi/datainjection_actual_version ; \ + echo "${GLPIINVENTORY_VERSION}" > /etc/glpi/glpiinventory_actual_version ) + php82 bin/console glpi:maintenance:disable -n chown -R apache:apache /var/www/glpi /etc/glpi /var/lib/glpi /var/log/glpi fi +start_apache diff --git a/web-builder/service/php b/web-builder/service/php new file mode 100644 index 0000000..1e069e0 --- /dev/null +++ b/web-builder/service/php @@ -0,0 +1,43 @@ +#!/bin/sh +NORMAL='' +RED='' +GREEN='' + +msglog() { + case "${1}" in + green) + TEXT_COLOR="${GREEN}" + ;; + red) + TEXT_COLOR="${RED}" + ;; + normal) + TEXT_COLOR="${NORMAL}" + ;; + esac + DATE=$(date '+%Y %b %d %H:%M:%S') + echo ${DATE} ${TEXT_COLOR}${2}${NORMAL} +} + +updatephpini() { + variable=$1 + value=$2 + file="/etc/php82/php.ini" + + if [ ! -z "${value}" ] ; then + msglog green "Updating $variable to $value in $file" + if grep "^${variable}" "${file}" > /dev/null 2<&1 ; then + sed -i "s@^\(${variable}\s*=\).*@\1 ${value}@g" "${file}" + else + echo ${variable} = ${value} >> ${file} + fi + fi +} + +updatephpini memory_limit ${PHP_MEMORY_LIMIT} +updatephpini upload_max_filesize ${PHP_UPLOAD_MAX_FILESIZE} +updatephpini post_max_size ${PHP_POST_MAX_SIZE} +updatephpini date.timezone ${PHP_DATE_TIMEZONE} +updatephpini max_input_vars ${PHP_MAX_INPUT_VARS} +updatephpini session.cookie_httponly 1 +updatephpini session.gc_maxlifetime ${PHP_SESSION_GC_MAXLIFETIME} diff --git a/alpine/web-builder/service/template b/web-builder/service/template similarity index 100% rename from alpine/web-builder/service/template rename to web-builder/service/template
-
-  $name -
-
"; - - if (isset($job->users[CommonITILActor::REQUESTER]) - && count($job->users[CommonITILActor::REQUESTER])) { - foreach ($job->users[CommonITILActor::REQUESTER] as $d) { - if ($d["users_id"] > 0) { - $userdata = getUserName($d["users_id"], 2); - $name = "".$userdata['name'].""; - $name = sprintf(__('%1$s %2$s'), $name, - Html::showToolTip($userdata["comment"], - ['link' => $userdata["link"], - 'display' => false])); - echo $name; - } else { - echo $d['alternative_email']." "; - } - echo "
"; - } - } - - if (isset($job->groups[CommonITILActor::REQUESTER]) - && count($job->groups[CommonITILActor::REQUESTER])) { - foreach ($job->groups[CommonITILActor::REQUESTER] as $d) { - echo Dropdown::getDropdownName("glpi_groups", $d["groups_id"]); - echo "
"; - } - } - - echo "
"; - if (!empty($job->hardwaredatas)) { - foreach ($job->hardwaredatas as $hardwaredatas) { - if ($hardwaredatas->canView()) { - echo $hardwaredatas->getTypeName()." - "; - echo "".$hardwaredatas->getLink()."
"; - } else if ($hardwaredatas) { - echo $hardwaredatas->getTypeName()." - "; - echo "".$hardwaredatas->getNameID()."
"; - } - } - } else { - echo __('General'); - } - echo "
"; - - $link = "getNameID().""; - $link = sprintf(__('%1$s (%2$s)'), $link, - sprintf(__('%1$s - %2$s'), $job->numberOfFollowups($showprivate), - $job->numberOfTasks($showprivate))); - $content = Toolbox::unclean_cross_side_scripting_deep(html_entity_decode($job->fields['content'], - ENT_QUOTES, - "UTF-8")); - $link = printf(__('%1$s %2$s'), $link, - Html::showToolTip(nl2br(Html::Clean($content)), - ['applyto' => 'ticket'.$job->fields["id"].$rand, - 'display' => false])); - echo "
".__('No ticket in progress.')."
".__('Assign equipment')."".$item->getLink(['comments' => true])."

".__('Support Informatique')."

".__('Describe the incident or request').""; + if (Session::isMultiEntitiesMode()) { + echo "(".Dropdown::getDropdownName("glpi_entities", $_SESSION["glpiactive_entity"]).")"; +@@ -3789,7 +3790,7 @@ + } + } + +- if (empty($delegating) ++/** if (empty($delegating) + && NotificationTargetTicket::isAuthorMailingActivatedForHelpdesk()) { + echo "
".__('Inform me about the actions taken')."