diff --git a/.circleci/config.yml b/.circleci/config.yml index c751dc98a9c..bc0985a8e5c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -261,6 +261,8 @@ workflows: filters: tags: only: /.*/ # run also on tag creation + branches: + ignore: /.*/ # do not run on branch update - php_7_1_test_suite: requires: - checkout diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000000..f968a2b15c7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,140 @@ +name: "GLPI CI" + +on: ["push"] + +jobs: + lint: + name: "Lint" + runs-on: "ubuntu-latest" + services: + app: + image: "glpi/githubactions-php:7.0" + options: >- + --volume /glpi:/var/glpi + steps: + - name: "Checkout" + uses: "actions/checkout@v1" + with: + fetch-depth: 1 + - name: "Deploy source into app container" + run: | + sudo cp --no-target-directory --preserve --recursive `pwd` /glpi + sudo chown -R 1000:1000 /glpi + - name: "Install dependencies" + run: | + docker exec ${{ job.services.app.id }} composer config --unset platform + docker exec ${{ job.services.app.id }} rm composer.lock + docker exec ${{ job.services.app.id }} composer --version + docker exec ${{ job.services.app.id }} echo "node version: $(node --version)" + docker exec ${{ job.services.app.id }} echo "npm version: $(npm --version)" + docker exec ${{ job.services.app.id }} bin/console dependencies install --ci + - name: "PHP Parallel Lint" + run: | + docker exec ${{ job.services.app.id }} vendor/bin/parallel-lint --exclude ./files/ --exclude ./plugins/ --exclude ./tools/vendor/ --exclude ./vendor/ . + - name: "PHP Security checker" + run: | + docker exec ${{ job.services.app.id }} vendor/bin/security-checker security:check + - name: "PHP CS" + run: | + docker exec ${{ job.services.app.id }} vendor/bin/phpcs -d memory_limit=512M -p -n --extensions=php --standard=vendor/glpi-project/coding-standard/GlpiStandard/ --ignore=/.git/,/config/,/files/,/lib/,/node_modules/,/plugins/,/tests/config/,/vendor/ ./ + - name: "ESLint" + run: | + docker exec ${{ job.services.app.id }} node_modules/.bin/eslint ./js && echo "ESLint found no errors" + docker exec ${{ job.services.app.id }} node_modules/.bin/eslint --env=node --parser-options=ecmaVersion:6 --rule 'indent: ["error", 4]' ./webpack.config.js && echo "ESLint found no errors" + - name: "Check CSS compilation" + run: | + docker exec ${{ job.services.app.id }} bin/console build:compile_scss + + tests: + name: "Test on PHP ${{ matrix.php-version }} using ${{ matrix.db-image }}" + runs-on: "ubuntu-latest" + strategy: + fail-fast: false + matrix: + db-image: + #- "mariadb:10.1" + #- "mariadb:10.2" + #- "mariadb:10.3" + - "mariadb:10.4" + #- "mysql:5.6" + #- "mysql:5.7" + - "mysql:8.0" + php-version: + #- "7.0" + #- "7.1" + #- "7.2" + - "7.3" + - "7.4-rc" + exclude: + - {db-image: "mysql:8.0", php-version: "7.4-rc"} + services: + app: + image: "glpi/githubactions-php:${{ matrix.php-version }}" + options: >- + --volume /glpi:/var/glpi + db: + image: "glpi/githubactions-${{ matrix.db-image }}" + env: + MYSQL_ALLOW_EMPTY_PASSWORD: "yes" + options: >- + --shm-size=1g + dovecot: + image: "glpi/githubactions-dovecot" + openldap: + image: "glpi/githubactions-openldap" + steps: + - name: "Checkout" + uses: "actions/checkout@v1" + - name: "Deploy source into app container" + run: | + sudo cp --no-target-directory --preserve --recursive `pwd` /glpi + sudo chown -R 1000:1000 /glpi + - name: "Initialize databases" + run: | + docker exec ${{ job.services.db.id }} mysql --user=root --execute="CREATE DATABASE \`glpi\`;" + docker exec ${{ job.services.db.id }} mysql --user=root --execute="CREATE DATABASE \`glpitest0723\`;" + cat tests/glpi-0.72.3-empty.sql | docker exec --interactive ${{ job.services.db.id }} mysql --user=root glpitest0723 + - name: "Install dependencies" + run: | + docker exec ${{ job.services.app.id }} composer config --unset platform + docker exec ${{ job.services.app.id }} rm composer.lock + docker exec ${{ job.services.app.id }} composer --version + docker exec ${{ job.services.app.id }} echo "node version: $(node --version)" + docker exec ${{ job.services.app.id }} echo "npm version: $(npm --version)" + docker exec ${{ job.services.app.id }} bin/console dependencies install --ci + - name: "Update DB tests" + run: | + docker exec ${{ job.services.app.id }} bin/console glpi:database:configure --config-dir=./tests --no-interaction --reconfigure --db-name=glpitest0723 --db-host=db --db-user=root + docker exec ${{ job.services.app.id }} bin/console glpi:migration:myisam_to_innodb --config-dir=./tests --no-interaction + docker exec ${{ job.services.app.id }} bin/console glpi:database:update --config-dir=./tests --allow-unstable --no-interaction + docker exec ${{ job.services.app.id }} bin/console glpi:database:update --config-dir=./tests --allow-unstable --no-interaction | grep -q "No migration needed." || (echo "glpi:database:update command FAILED" && exit 1) + docker exec ${{ job.services.app.id }} bin/console glpi:migration:myisam_to_innodb --config-dir=./tests --no-interaction + docker exec ${{ job.services.app.id }} bin/console glpi:migration:timestamps --config-dir=./tests --no-interaction + - name: "Install DB tests" + run: | + docker exec ${{ job.services.app.id }} bin/console glpi:database:install --config-dir=./tests --no-interaction --reconfigure --db-name=glpi --db-host=db --db-user=root + docker exec ${{ job.services.app.id }} bin/console glpi:database:update --config-dir=./tests --no-interaction | grep -q "No migration needed." || (echo "glpi:database:update command FAILED" && exit 1) + - name: "Database tests" + run: | + docker exec ${{ job.services.app.id }} bin/console glpi:database:configure --config-dir=./tests --no-interaction --reconfigure --db-name=glpitest0723 --db-host=db --db-user=root + docker exec ${{ job.services.app.id }} vendor/bin/atoum -p 'php -d memory_limit=512M' --debug --force-terminal --use-dot-report --configurations tests/telemetry.php --bootstrap-file tests/bootstrap.php --no-code-coverage --max-children-number 1 -d tests/database + docker exec ${{ job.services.app.id }} bin/console glpi:database:configure --config-dir=./tests --no-interaction --reconfigure --db-name=glpi --db-host=db --db-user=root + - name: "Unit tests" + run: | + docker exec ${{ job.services.app.id }} vendor/bin/atoum -p 'php -d memory_limit=512M' --debug --force-terminal --use-dot-report --configurations tests/telemetry.php --bootstrap-file tests/bootstrap.php --no-code-coverage -d tests/units + - name: "Functionnal tests" + run: | + docker exec ${{ job.services.app.id }} vendor/bin/atoum -p 'php -d memory_limit=512M' --debug --force-terminal --use-dot-report --configurations tests/telemetry.php --bootstrap-file tests/bootstrap.php --no-code-coverage --max-children-number 1 -d tests/functionnal + - name: "LDAP tests" + run: | + for f in `ls tests/LDAP/ldif/*.ldif`; do cat $f | docker exec --interactive ${{ job.services.openldap.id }} ldapadd -x -H ldap://127.0.0.1:3890/ -D "cn=Manager,dc=glpi,dc=org" -w insecure ; done + docker exec ${{ job.services.app.id }} vendor/bin/atoum -p 'php -d memory_limit=512M' --debug --force-terminal --use-dot-report --configurations tests/telemetry.php --bootstrap-file tests/bootstrap.php --no-code-coverage --max-children-number 1 -d tests/LDAP + - name: "IMAP tests" + run: | + for f in `ls tests/emails-tests/*.eml`; do cat $f | docker exec --user glpi --interactive ${{ job.services.dovecot.id }} getmail_maildir /home/glpi/Maildir/ ; done + docker exec ${{ job.services.app.id }} sed -e 's/127.0.0.1/dovecot/g' -i tests/imap/MailCollector.php + docker exec ${{ job.services.app.id }} vendor/bin/atoum -p 'php -d memory_limit=512M' --debug --force-terminal --use-dot-report --configurations tests/telemetry.php --bootstrap-file tests/bootstrap.php --no-code-coverage --max-children-number 1 -d tests/imap + - name: "WEB tests" + run: | + docker exec ${{ job.services.app.id }} php -S localhost:8088 tests/router.php &>/dev/null & + docker exec ${{ job.services.app.id }} vendor/bin/atoum -p 'php -d memory_limit=512M' --debug --force-terminal --use-dot-report --configurations tests/telemetry.php --bootstrap-file tests/bootstrap.php --no-code-coverage --max-children-number 1 -d tests/web diff --git a/.travis.yml b/.travis.yml index 80626dfe077..94cddda52a8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,6 +25,8 @@ script: #note: default maria version is 5.5 for all main php versions list exept nightly matrix: include: + hosts: + - openldap - php: 7.0 addons: mariadb: 10.2 @@ -32,6 +34,8 @@ matrix: packages: - ldap-utils - slapd + hosts: + - openldap - php: 7.1 addons: mariadb: 10.1 @@ -39,30 +43,40 @@ matrix: packages: - ldap-utils - slapd + hosts: + - openldap - php: 7.2 addons: apt: packages: - ldap-utils - slapd + hosts: + - openldap - php: 7.3 addons: apt: packages: - ldap-utils - slapd + hosts: + - openldap - php: 7.4snapshot addons: apt: packages: - ldap-utils - slapd + hosts: + - openldap - php: nightly addons: apt: packages: - ldap-utils - slapd + hosts: + - openldap allow_failures: - php: nightly - php: 7.4snapshot diff --git a/CHANGELOG.md b/CHANGELOG.md index d02aba13e1e..4d7bcd4ad93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ The present file will list all changes made to the project; according to the - Add and answer approvals from timeline - Add lightbox with PhotoSwipe to timeline images - Ability to copy tasks while merging tickets +- the API gives the ID of the user who logs in with initSession ### Changed diff --git a/ajax/dropdownTrackingDeviceType.php b/ajax/dropdownTrackingDeviceType.php index 60d9617d2be..e86bb9a3e18 100644 --- a/ajax/dropdownTrackingDeviceType.php +++ b/ajax/dropdownTrackingDeviceType.php @@ -36,10 +36,27 @@ Session::checkLoginUser(); +// Read parameters +$context = $_POST['context'] ?? ''; +$itemtype = $_POST["itemtype"] ?? ''; + +// Check for required params +if (empty($itemtype)) { + http_response_code(400); + Toolbox::logWarning("Bad request: itemtype cannot be empty"); + die; +} + +// Check if itemtype is valid in the given context +if ($context == "impact") { + $isValidItemtype = isset($CFG_GLPI['impact_asset_types'][$itemtype]); +} else { + $isValidItemtype = CommonITILObject::isPossibleToAssignType($itemtype); +} + // Make a select box -if (isset($_POST["itemtype"]) - && CommonITILObject::isPossibleToAssignType($_POST["itemtype"])) { - $table = getTableForItemType($_POST["itemtype"]); +if ($isValidItemtype) { + $table = getTableForItemType($itemtype); $rand = mt_rand(); if (isset($_POST["rand"])) { @@ -52,7 +69,7 @@ } echo "
"; $field_id = Html::cleanId("dropdown_".$_POST['myname'].$rand); - $p = ['itemtype' => $_POST["itemtype"], + $p = ['itemtype' => $itemtype, 'entity_restrict' => $_POST['entity_restrict'], 'table' => $table, 'multiple' => $_POST["multiple"], @@ -60,11 +77,16 @@ 'rand' => $_POST["rand"]]; if (isset($_POST["used"]) && !empty($_POST["used"])) { - if (isset($_POST["used"][$_POST["itemtype"]])) { - $p["used"] = $_POST["used"][$_POST["itemtype"]]; + if (isset($_POST["used"][$itemtype])) { + $p["used"] = $_POST["used"][$itemtype]; } } + // Add context if defined + if (!empty($context)) { + $p["context"] = $context; + } + echo Html::jsAjaxDropdown($_POST['myname'], $field_id, $CFG_GLPI['root_doc']."/ajax/getDropdownFindNum.php", $p); diff --git a/ajax/impact.php b/ajax/impact.php new file mode 100644 index 00000000000..6ed094a3251 --- /dev/null +++ b/ajax/impact.php @@ -0,0 +1,166 @@ +. + * --------------------------------------------------------------------- + */ + +const DELTA_ACTION_ADD = 1; +const DELTA_ACTION_UPDATE = 2; +const DELTA_ACTION_DELETE = 3; + +$AJAX_INCLUDE = 1; +include ('../inc/includes.php'); + +// Send UTF8 Headers +header("Content-Type: application/json; charset=UTF-8"); +Html::header_nocache(); + +Session::checkLoginUser(); + +switch ($_SERVER['REQUEST_METHOD']) { + // GET request: build the impact graph for a given asset + case 'GET': + $itemtype = $_GET["itemtype"] ?? ""; + $items_id = $_GET["items_id"] ?? ""; + + // Check required params + if (empty($itemtype) || empty($items_id)) { + Toolbox::throwBadRequest("Missing itemtype or items_id"); + } + + // Check that the the target asset exist + if (!Impact::assetExist($itemtype, $items_id)) { + Toolbox::throwBadRequest("Object[class=$itemtype, id=$items_id] doesn't exist"); + } + + // Prepare graph + $item = new $itemtype; + $item->getFromDB($items_id); + $graph = Impact::makeDataForCytoscape(Impact::buildGraph($item)); + $params = Impact::prepareParams($item); + + // Output graph + header('Content-Type: application/json'); + echo json_encode([ + 'graph' => $graph, + 'params' => $params + ]); + + break; + + // Post request: update the store impact dependencies, compounds or items + case 'POST': + // Check required params + if (!isset($_POST['impacts'])) { + Toolbox::throwBadRequest("Missing 'impacts' payload"); + } + + // Decode data (should be json) + $data = Toolbox::jsonDecode($_POST['impacts'], true); + if (!is_array($data)) { + Toolbox::throwBadRequest("Payload should be an array"); + } + + // Save impact relation delta + $em = new ImpactRelation(); + foreach ($data['edges'] as $impact) { + // Extract action + $action = $impact['action']; + unset($impact['action']); + + switch ($action) { + case DELTA_ACTION_ADD: + $em->add($impact); + break; + + case DELTA_ACTION_DELETE: + $impact['id'] = ImpactRelation::getIDFromInput($impact); + $em->delete($impact); + break; + + default: + break; + } + } + + // Save impact compound delta + $em = new ImpactCompound(); + foreach ($data['compounds'] as $id => $compound) { + // Extract action + $action = $compound['action']; + unset($compound['action']); + + switch ($action) { + case DELTA_ACTION_ADD: + $newCompoundID = $em->add($compound); + + // Update id reference in impactitem + // This is needed because some nodes might have this compound + // temporary id as their parent id + foreach ($data['items'] as $nodeID => $node) { + if ($node['parent_id'] === $id) { + $data['items'][$nodeID]['parent_id'] = $newCompoundID; + } + } + break; + + case DELTA_ACTION_UPDATE: + $compound['id'] = $id; + $em->update($compound); + break; + + case DELTA_ACTION_DELETE: + $em->delete(['id' => $id]); + break; + + default: + break; + } + } + + // Save impact item delta + $em = new ImpactItem(); + foreach ($data['items'] as $id => $impactItem) { + // Extract action + $action = $impactItem['action']; + unset($impactItem['action']); + + switch ($action) { + case DELTA_ACTION_UPDATE: + $impactItem['id'] = $id; + $em->update($impactItem); + break; + } + } + + header('Content-Type: application/javascript'); + http_response_code(200); + break; +} \ No newline at end of file diff --git a/ajax/planning.php b/ajax/planning.php index f2d873ef675..5c54b1983a7 100644 --- a/ajax/planning.php +++ b/ajax/planning.php @@ -49,6 +49,26 @@ exit; } +if ($_REQUEST["action"] == "view_changed") { + echo Planning::viewChanged($_REQUEST['view']); + exit; +} + +if ($_REQUEST["action"] == "get_externalevent_template") { + $key = 'planningexternaleventtemplates_id'; + if (isset($_POST[$key]) + && $_POST[$key] > 0) { + $template = new PlanningExternalEventTemplate(); + $template->getFromDB($_POST[$key]); + + $template->fields = array_map('html_entity_decode', $template->fields); + $template->fields['rrule'] = json_decode($template->fields['rrule'], true); + header("Content-Type: application/json; charset=UTF-8"); + echo json_encode($template->fields, JSON_NUMERIC_CHECK); + exit; + } +} + Html::header_nocache(); header("Content-Type: text/html; charset=UTF-8"); diff --git a/apirest.md b/apirest.md index 72bcd231e77..b992ab93ab8 100644 --- a/apirest.md +++ b/apirest.md @@ -116,7 +116,7 @@ App(lication) token * "Authorization: user_token q56hqkniwot8wntb3z1qarka5atf365taaa2uyjrn" * **Returns**: - * 200 (OK) with the *session_token* string. + * 200 (OK) with the *session_token* string and the *ID of the logged in user*. * 400 (Bad Request) with a message indicating an error in input parameter. * 401 (UNAUTHORIZED) @@ -131,7 +131,8 @@ $ curl -X GET \ < 200 OK < { - "session_token": "83af7e620c83a50a18d3eac2f6ed05a3ca0bea62" + "session_token": "83af7e620c83a50a18d3eac2f6ed05a3ca0bea62", + "users_id": "42" } $ curl -X GET \ @@ -142,7 +143,8 @@ $ curl -X GET \ < 200 OK < { - "session_token": "83af7e620c83a50a18d3eac2f6ed05a3ca0bea62" + "session_token": "83af7e620c83a50a18d3eac2f6ed05a3ca0bea62", + "users_id": "42" } ``` diff --git a/bin/console b/bin/console index 3c4a498fb43..e50f18b965e 100755 --- a/bin/console +++ b/bin/console @@ -48,7 +48,6 @@ if (isset($_SERVER['argv']) && ['dependencies', 'install'] === array_slice($_SER if (array_key_exists('ci', $options) && true === $options['ci']) { $composer_command .= ' --prefer-dist --no-progress'; - $npm_command = 'npm ci'; } chdir(dirname(__FILE__, 2)); diff --git a/composer.json b/composer.json index f7ad0c5e54c..f4d8ddbc7c7 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "michelf/php-markdown": "^1.6", "monolog/monolog": "^1.23", "phpmailer/phpmailer": "^6.0", + "rlanvin/php-rrule": "^2.1", "sabre/vobject": "^4.1", "scssphp/scssphp": "^1.0", "sebastian/diff": "^1.4 || ^2.0 || ^3.0", diff --git a/composer.lock b/composer.lock index e0fc33de461..cb530ab419f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "72e957afc009386f24c46813fd1997e5", + "content-hash": "75ba5b63c8a245e4cab4626ac084c62a", "packages": [ { "name": "container-interop/container-interop", @@ -487,6 +487,50 @@ ], "time": "2017-10-23T01:57:42+00:00" }, + { + "name": "rlanvin/php-rrule", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/rlanvin/php-rrule.git", + "reference": "c71d0f9251ba967b211ddab820c7012df6962b19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rlanvin/php-rrule/zipball/c71d0f9251ba967b211ddab820c7012df6962b19", + "reference": "c71d0f9251ba967b211ddab820c7012df6962b19", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8|^5.5|^6.5" + }, + "suggest": { + "ext-intl": "Intl extension is needed for humanReadable()" + }, + "type": "library", + "autoload": { + "psr-4": { + "RRule\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Lightweight and fast recurrence rules for PHP (RFC 5545)", + "homepage": "https://github.com/rlanvin/php-rrule", + "keywords": [ + "date", + "ical", + "recurrence", + "recurring", + "rrule" + ], + "time": "2019-01-15T05:31:37+00:00" + }, { "name": "sabre/uri", "version": "2.1.2", @@ -523,9 +567,9 @@ "authors": [ { "name": "Evert Pot", + "role": "Developer", "email": "me@evertpot.com", - "homepage": "http://evertpot.com/", - "role": "Developer" + "homepage": "http://evertpot.com/" } ], "description": "Functions for making sense out of URIs.", @@ -675,14 +719,14 @@ "authors": [ { "name": "Evert Pot", + "role": "Developer", "email": "me@evertpot.com", - "homepage": "http://evertpot.com/", - "role": "Developer" + "homepage": "http://evertpot.com/" }, { "name": "Markus Staab", - "email": "markus.staab@redaxo.de", - "role": "Developer" + "role": "Developer", + "email": "markus.staab@redaxo.de" } ], "description": "sabre/xml is an XML library that you may not hate.", @@ -810,16 +854,16 @@ }, { "name": "simplepie/simplepie", - "version": "1.5.2", + "version": "1.5.3", "source": { "type": "git", "url": "https://github.com/simplepie/simplepie.git", - "reference": "0e8fe72132dad765d25db4cabc69a91139af1263" + "reference": "173663382a9346acd53df60c7ffb20689c9cf1f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplepie/simplepie/zipball/0e8fe72132dad765d25db4cabc69a91139af1263", - "reference": "0e8fe72132dad765d25db4cabc69a91139af1263", + "url": "https://api.github.com/repos/simplepie/simplepie/zipball/173663382a9346acd53df60c7ffb20689c9cf1f6", + "reference": "173663382a9346acd53df60c7ffb20689c9cf1f6", "shasum": "" }, "require": { @@ -873,7 +917,7 @@ "feeds", "rss" ], - "time": "2018-08-02T05:43:58+00:00" + "time": "2019-09-22T23:21:30+00:00" }, { "name": "symfony/console", @@ -1309,16 +1353,16 @@ }, { "name": "zendframework/zend-i18n", - "version": "2.9.1", + "version": "2.9.2", "source": { "type": "git", "url": "https://github.com/zendframework/zend-i18n.git", - "reference": "9233ee8553564a6e45e8311a7173734ba4e5db9b" + "reference": "e17a54b3aee333ab156958f570cde630acee8b07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-i18n/zipball/9233ee8553564a6e45e8311a7173734ba4e5db9b", - "reference": "9233ee8553564a6e45e8311a7173734ba4e5db9b", + "url": "https://api.github.com/repos/zendframework/zend-i18n/zipball/e17a54b3aee333ab156958f570cde630acee8b07", + "reference": "e17a54b3aee333ab156958f570cde630acee8b07", "shasum": "" }, "require": { @@ -1373,7 +1417,7 @@ "i18n", "zf" ], - "time": "2019-09-26T11:54:57+00:00" + "time": "2019-09-30T12:04:37+00:00" }, { "name": "zendframework/zend-json", @@ -3134,8 +3178,8 @@ "authors": [ { "name": "Frank Kleine", - "homepage": "http://frankkleine.de/", - "role": "Developer" + "role": "Developer", + "homepage": "http://frankkleine.de/" } ], "description": "Virtual file system to mock the real file system in unit tests.", @@ -3143,7 +3187,7 @@ "time": "2019-08-01T01:38:37+00:00" }, { - "name": "natxet/cssmin", + "name": "natxet/CssMin", "version": "v3.0.6", "source": { "type": "git", diff --git a/css/impact.scss b/css/impact.scss new file mode 100644 index 00000000000..4fd8c7b66a2 --- /dev/null +++ b/css/impact.scss @@ -0,0 +1,359 @@ + +#network_container { + height: 70vh; + + div { + z-index: 1 !important; + position: absolute !important; + } +} + +#help_text { + font-weight: bold; + display: none; +} + +#impact_tools { + margin-left : auto; + margin-right : 5px; + background-color: white; + padding : 5px; + border : 1px solid lightgray; + border-radius : 2px; + + span:hover { + background-color: lightgray; + border-radius: 2px; + } +} + +#save_impact { + font-weight : bold; + color : gray; + margin-right : 20px; +} + +.impact-mb-2 { + margin-bottom: 2em; +} + +.network-parent { + border: 1px solid #f1f1f1; + position:relative +} + +.network-table { + max-width: none !important; +} + +.impact-dialog { + display: none; +} + +i.fa-impact-manipulation { + display: inline; + font-size: 14px; +} + +.impact_toolbar { + position : absolute; + display : inline-flex; + justify-content: flex-start; + left : 0; + right : 0; + z-index : 20; + flex-wrap : wrap; + + span { + float : left; + color : gray; + font-size : 1.3em; + padding : 4px 8px; + transition: all 0.3s ease; + cursor : pointer; + border : 2px inset transparent; + } + + .active { + border: 2px inset #f4f4f4; + background-color: #fafafa; + } +} + +.impact_toolbar_right { + float: right !important; +} + +.clean { + color: #1ca448 !important; +} + +.dirty { + color: #eea818 !important; + + &:hover { + background-color: #fec95c !important; + color: #8f5a0a !important; + } +} + +/* + * "More" dropdown menu at the end of the toolbar + */ + +.more-btn, +.more-menu-btn { + background : none; + border : 0 none; + line-height : normal; + overflow : visible; + -webkit-user-select: none; + -moz-user-select : none; + -ms-user-select : none; + width : 100%; + text-align : left; + outline : none; + cursor : pointer; +} + +.more-disabled { + cursor: default !important; + + &:hover { + background-color: white !important; + } +} + +.more-dot { + background-color: #aab8c2; + margin : 0 auto; + display : inline-block; + width : 7px; + height : 7px; + margin-right : 1px; + border-radius : 50%; + transition : background-color 0.3s; +} + +.more-menu { + position : absolute; + top : 42px; + z-index : 900; + padding : 10px 0; + background-color: #fff; + border : 1px solid #ccd8e0; + border-radius : 4px; + box-shadow : 1px 1px 3px rgba(0,0,0,0.25); + opacity : 0; + transform : translate(0, 15px) scale(.95); + transition : transform 0.1s ease-out, opacity 0.1s ease-out; + pointer-events : none; + right : 1px; +} + +.more-menu-caret { + position: absolute; + top : -10px; + right : 10px; + width : 18px; + height : 10px; + float : left; + overflow: hidden; +} + +.more-menu-caret-outer, +.more-menu-caret-inner { + position : absolute; + display : inline-block; + margin-left: -1px; + font-size : 0; + line-height: 1; +} + +.more-menu-caret-outer { + border-bottom: 10px solid #c1d0da; + border-left : 10px solid transparent; + border-right : 10px solid transparent; + height : auto; + left : 0; + top : 0; + width : auto; +} + +.more-menu-caret-inner { + top : 1px; + left : 1px; + border-left : 9px solid transparent; + border-right : 9px solid transparent; + border-bottom: 9px solid #fff; +} + +.more-menu-items { + margin : 0; + list-style: none; + padding : 0; +} + +.more-menu-item { + display: block; + + &:hover { + background-color: lightgray; + } +} + +.more-menu-btn { + min-width : 100%; + color : #66757f; + cursor : pointer; + display : block; + font-size : 13px; + line-height: 18px; + padding : 5px 10px; + position : relative; + white-space: nowrap; + font-size : 1.2em !important; +} + +.more-btn:hover .more-dot, +.show-more-menu .more-dot { + background-color: #516471; +} + +.show-more-menu .more-menu { + opacity: 1; + transform: translate(0, 0) scale(1); + pointer-events: auto; +} + +/* + * Cytoscape context menu + */ + +.cy-context-menus-cxt-menuitem { + padding-left: 5px !important; + font-size: 1.15em; + background-color:white; + + i { + padding-right: 10px !important; + color: gray; + max-width: 10px; + } +} + +.cy-context-menus-cxt-menu { + border-radius : 2px; + border : 1px solid lightgray; + -webkit-box-shadow: 4px 4px 6px 3px rgba(0,0,0,0.17); + -moz-box-shadow : 4px 4px 6px 3px rgba(0,0,0,0.17); + box-shadow : 4px 4px 6px 3px rgba(0,0,0,0.17); +} + +/* + * Custom range input + */ + +.impact-range { + height : 18px; + -webkit-appearance: none; + margin : 10px 0; + border-width : 0 !important; + margin-top : 0 !important; + margin-bottom : 0 !important; +} + +.impact-range:focus { + outline: none; +} + +.impact-range::-webkit-slider-runnable-track { + width : 100%; + height : 4px; + cursor : pointer; + animate : 0.2s; + box-shadow : 0px 0px 0px #000000; + background : #AEC8D8; + border-radius: 25px; + border : 1px solid #8A8A8A; +} + +.impact-range::-webkit-slider-thumb { + box-shadow : 1px 1px 1px #828282; + border : 1px solid #8A8A8A; + height : 10px; + width : 14px; + border-radius : 2px; + background : #66757F; + cursor : pointer; + -webkit-appearance: none; + margin-top : -4px; +} + +.impact-range:focus::-webkit-slider-runnable-track { + background: #AEC8D8; +} + +.impact-range::-moz-range-track { + width : 100%; + height : 4px; + cursor : pointer; + animate : 0.2s; + box-shadow : 0px 0px 0px #000000; + background : #AEC8D8; + border-radius: 25px; + border : 1px solid #8A8A8A; +} + +.impact-range::-moz-range-thumb { + box-shadow : 1px 1px 1px #828282; + border : 1px solid #8A8A8A; + height : 10px; + width : 14px; + border-radius: 2px; + background : #66757F; + cursor : pointer; +} + +.impact-range::-ms-track { + width : 100%; + height : 4px; + cursor : pointer; + animate : 0.2s; + background : transparent; + border-color: transparent; + color : transparent; +} + +.impact-range::-ms-fill-lower { + background : #AEC8D8; + border : 1px solid #8A8A8A; + border-radius: 50px; + box-shadow : 0px 0px 0px #000000; +} + +.impact-range::-ms-fill-upper { + background : #AEC8D8; + border : 1px solid #8A8A8A; + border-radius: 50px; + box-shadow : 0px 0px 0px #000000; +} + +.impact-range::-ms-thumb { + margin-top : 1px; + box-shadow : 1px 1px 1px #828282; + border : 1px solid #8A8A8A; + height : 10px; + width : 14px; + border-radius: 2px; + background : #66757F; + cursor : pointer; +} + +.impact-range:focus::-ms-fill-lower { + background: #AEC8D8; +} + +.impact-range:focus::-ms-fill-upper { + background: #AEC8D8; +} diff --git a/css/styles.scss b/css/styles.scss index cfe03206187..9a8242b0f72 100644 --- a/css/styles.scss +++ b/css/styles.scss @@ -1107,6 +1107,24 @@ span.vsubmit, a.vsubmit { /* ################--------------- Table ---------------#################### */ +.card { + box-shadow: 0px 1px 2px 1px #999; + + .field { + display: table-row; + padding: 5px; + + label { + display: table-cell; + padding: 10px 5px; + + & ~ div { + display: table-cell; + } + } + } +} + table { font-size: 11px; @@ -2280,7 +2298,8 @@ a.icon_nav_move { .actor_icon { padding-bottom: 2px; - vertical-align: middle; + vertical-align: top; + color: rgb(68, 68, 68); font-size: 14px; } @@ -2341,7 +2360,7 @@ a.icon_nav_move { display: block; > li label { - width: 175px; + width: 173px; } } } @@ -2423,6 +2442,16 @@ a.icon_nav_move { width: auto; overflow: hidden; + .fc-time-grid-event { + // TODO check new version, + // Fullcalendar 2.4.0 seems to have removed this property + overflow: hidden; + } + + .end-of-day { + border-right: 1px solid #bdbdbd; + } + .fc-toolbar h2 { font-size: 1.2em; @@ -2449,6 +2478,10 @@ a.icon_nav_move { } } + .event_today { + background: #fcf8e3; + } + .event_today.event_todo .ui-widget-content { color: #FFA100; @@ -2459,6 +2492,7 @@ a.icon_nav_move { .fc-event { font-weight: normal; + display: block; .fc-content { margin-right: 8px; @@ -2501,6 +2535,14 @@ a.icon_nav_move { } } + .fc-timeline { + .fc-event { + .content { + max-height: 25px; + } + } + } + .fc-list-item-title .event_type { height: 12px; width: 6px; @@ -6432,3 +6474,5 @@ div.banner-impersonate button { margin-left: 5px; text-decoration: underline; } + +@import 'impact'; \ No newline at end of file diff --git a/front/impact.php b/front/impact.php new file mode 100644 index 00000000000..f8d431e29ab --- /dev/null +++ b/front/impact.php @@ -0,0 +1,49 @@ +. + * --------------------------------------------------------------------- + */ + +include ('../inc/includes.php'); +Html::header(__('Impact'), $_SERVER['PHP_SELF'], "tools", "impact"); + + +$itemtype = $_GET["type"] ?? null; +$items_id = $_GET["id"] ?? null; + +if (!empty($itemtype) && !empty($items_id) && Impact::assetExist($itemtype, $items_id)) { + $item = new $itemtype; + $item->getFromDB($items_id); + Impact::loadLibs(); + Impact::prepareImpactNetwork($item); + Impact::buildNetwork($item); +} + +Impact::printImpactForm(); +Html::footer(); diff --git a/front/planningeventcategory.form.php b/front/planningeventcategory.form.php new file mode 100644 index 00000000000..d033b3986cc --- /dev/null +++ b/front/planningeventcategory.form.php @@ -0,0 +1,36 @@ +. + * --------------------------------------------------------------------- + */ + +include ('../inc/includes.php'); + +$dropdown = new PlanningEventCategory(); +include (GLPI_ROOT . "/front/dropdown.common.form.php"); diff --git a/front/planningeventcategory.php b/front/planningeventcategory.php new file mode 100644 index 00000000000..bf929771e50 --- /dev/null +++ b/front/planningeventcategory.php @@ -0,0 +1,36 @@ +. + * --------------------------------------------------------------------- + */ + +include ('../inc/includes.php'); + +$dropdown = new PlanningEventCategory(); +include (GLPI_ROOT . "/front/dropdown.common.php"); diff --git a/front/planningexternalevent.form.php b/front/planningexternalevent.form.php new file mode 100644 index 00000000000..7337a27f86b --- /dev/null +++ b/front/planningexternalevent.form.php @@ -0,0 +1,85 @@ +. + * --------------------------------------------------------------------- + */ + +use Glpi\Event; + +include ('../inc/includes.php'); + +Session::checkRight("planning", READ); + +if (empty($_GET["id"])) { + $_GET["id"] = ""; +} + +$extevent = new PlanningExternalEvent(); + +if (isset($_POST["add"])) { + $extevent->check(-1, CREATE, $_POST); + + if ($newID = $extevent->add($_POST)) { + if ($_SESSION['glpibackcreated']) { + Html::redirect($extevent->getLinkURL()); + } + } + Html::back(); + +} else if (isset($_POST["delete"])) { + $extevent->check($_POST["id"], DELETE); + $extevent->delete($_POST); + $extevent->redirectToList(); + +} else if (isset($_POST["restore"])) { + $extevent->check($_POST["id"], DELETE); + $extevent->restore($_POST); + $extevent->redirectToList(); + +} else if (isset($_POST["purge"])) { + $extevent->check($_POST["id"], PURGE); + $extevent->delete($_POST, 1); + $extevent->redirectToList(); + +} else if (isset($_POST["update"])) { + $extevent->check($_POST["id"], UPDATE); + $extevent->update($_POST); + Html::back(); + +} else { + Html::header( + PlanningExternalEvent::getTypeName(Session::getPluralNumber()), + $_SERVER['PHP_SELF'], + "helpdesk", + "planning", + "external" + ); + $extevent->display(['id' => $_GET["id"]]); + Html::footer(); +} diff --git a/front/planningexternalevent.php b/front/planningexternalevent.php new file mode 100644 index 00000000000..f697b25b66c --- /dev/null +++ b/front/planningexternalevent.php @@ -0,0 +1,47 @@ +. + * --------------------------------------------------------------------- + */ + +include ('../inc/includes.php'); + +Session::checkRight("planning", READ); + +Html::header( + PlanningExternalEvent::getTypeName(Session::getPluralNumber()), + $_SERVER['PHP_SELF'], + "helpdesk", + "planning", + "external" +); + +Search::show('PlanningExternalEvent'); + +Html::footer(); diff --git a/front/planningexternaleventtemplate.form.php b/front/planningexternaleventtemplate.form.php new file mode 100644 index 00000000000..7c980688433 --- /dev/null +++ b/front/planningexternaleventtemplate.form.php @@ -0,0 +1,40 @@ +. + * --------------------------------------------------------------------- + */ + +/** + * @since 9.5 + */ + +include ('../inc/includes.php'); + +$dropdown = new PlanningExternalEventTemplate(); +include (GLPI_ROOT . "/front/dropdown.common.form.php"); diff --git a/front/planningexternaleventtemplate.php b/front/planningexternaleventtemplate.php new file mode 100644 index 00000000000..db2e72a05e6 --- /dev/null +++ b/front/planningexternaleventtemplate.php @@ -0,0 +1,40 @@ +. + * --------------------------------------------------------------------- + */ + +/** + * @since 9.5 + */ + +include ('../inc/includes.php'); + +$dropdown = new PlanningExternalEventTemplate(); +include (GLPI_ROOT . "/front/dropdown.common.php"); diff --git a/inc/api.class.php b/inc/api.class.php index 308a1454881..52f4c16d11c 100644 --- a/inc/api.class.php +++ b/inc/api.class.php @@ -249,7 +249,10 @@ protected function initSession($params = []) { // stop session and return session key session_write_close(); - return ['session_token' => $_SESSION['valid_id']]; + return [ + 'session_token' => $_SESSION['valid_id'], + 'users_id' => Session::getLoginUserID(), + ]; } diff --git a/inc/autoload.function.php b/inc/autoload.function.php index 80efd2e7801..1eb489c02fe 100644 --- a/inc/autoload.function.php +++ b/inc/autoload.function.php @@ -65,7 +65,7 @@ function isAPI() { /** * Determine if an object name is a plugin one * - * @param string $classname class name to analyze + * @param string $classname class name to analyse * * @return boolean[object false or an object containing plugin name and class name */ diff --git a/inc/change.class.php b/inc/change.class.php index bce62441bb0..3914db69647 100644 --- a/inc/change.class.php +++ b/inc/change.class.php @@ -1555,4 +1555,78 @@ static function getDefaultValues($entity = 0) { 'checklistcontent' => '' ]; } + + /** + * Get active changes for an item + * + * @since 9.5 + * + * @param string $itemtype Item type + * @param integer $items_id ID of the Item + * + * @return DBmysqlIterator + */ + public function getActiveChangesForItem($itemtype, $items_id) { + global $DB; + + return $DB->request([ + 'SELECT' => [ + $this->getTable() . '.id', + $this->getTable() . '.name' + ], + 'FROM' => $this->getTable(), + 'LEFT JOIN' => [ + 'glpi_changes_items' => [ + 'ON' => [ + 'glpi_changes_items' => 'changes_id', + $this->getTable() => 'id' + ] + ] + ], + 'WHERE' => [ + 'glpi_changes_items.itemtype' => $itemtype, + 'glpi_changes_items.items_id' => $items_id, + 'NOT' => [ + $this->getTable() . '.status' => array_merge( + $this->getSolvedStatusArray(), + $this->getClosedStatusArray() + ) + ] + ] + ]); + } + + /** + * Get assets linked to this object + * + * @since 9.5 + * + * @param bool $addNames Insert asset names + * + * @return array + */ + public function getLinkedItems(bool $addNames = true) { + global $DB; + + $assets = $DB->request([ + 'SELECT' => ["id", "itemtype", "items_id"], + 'FROM' => "glpi_changes_items", + 'WHERE' => ["changes_id" => $this->getID()] + ]); + + $assets = iterator_to_array($assets); + + if ($addNames) { + foreach ($assets as $key => $asset) { + /** @var CommonDBTM $item */ + $item = new $asset['itemtype']; + $item->getFromDB($asset['id']); + + // Add name + $assets[$key]['name'] = $item->fields['name']; + } + } + + return $assets; + } } diff --git a/inc/commondbtm.class.php b/inc/commondbtm.class.php index 6343ebe2844..7ddf0a14609 100644 --- a/inc/commondbtm.class.php +++ b/inc/commondbtm.class.php @@ -5225,4 +5225,57 @@ static function checkCircularRelation($items_id, $parents_id) { // No circular relations return false; } + + /** + * Get incidents, request, changes and problem linked to this object + * + * @return array + */ + public function getITILTickets($count = false) { + $ticket = new Ticket(); + $problem = new Problem(); + $change = new Change(); + + $data = [ + 'incidents' => iterator_to_array( + $ticket->getActiveTicketsForItem( + get_class($this), + $this->getID(), + Ticket::INCIDENT_TYPE + ), + false + ), + 'requests' => iterator_to_array( + $ticket->getActiveTicketsForItem( + get_class($this), + $this->getID(), + Ticket::DEMAND_TYPE + ), + false + ), + 'changes' => iterator_to_array( + $change->getActiveChangesForItem( + get_class($this), + $this->getID() + ), + false + ), + 'problems' => iterator_to_array( + $problem->getActiveProblemsForItem( + get_class($this), + $this->getID() + ), + false + ) + ]; + + if ($count) { + $data['count'] = count($data['incidents']) + + count($data['requests']) + + count($data['changes']) + + count($data['problems']); + } + + return $data; + } } diff --git a/inc/commondropdown.class.php b/inc/commondropdown.class.php index 37054a7349d..355a56e2e44 100644 --- a/inc/commondropdown.class.php +++ b/inc/commondropdown.class.php @@ -259,7 +259,9 @@ function showForm($ID, $options = []) { echo "". __('Comments').""; echo " \n"; + echo ""; + + echo "\n"; foreach ($fields as $field) { if (($field['name'] == 'entities_id') @@ -429,6 +431,29 @@ function showForm($ID, $options = []) { echo ""; break; + case 'tinymce': + Html::textarea([ + 'name' => $field['name'], + 'value' => $this->fields[$field['name']], + 'enable_richtext' => true, + ]); + break; + + case 'duration' : + $toadd = []; + for ($i=9; $i<=100; $i++) { + $toadd[] = $i*HOUR_TIMESTAMP; + } + Dropdown::showTimeStamp($field['name'], [ + 'min' => 0, + 'max' => 8*HOUR_TIMESTAMP, + 'value' => $this->fields[$field['name']], + 'addfirstminutes' => true, + 'inhours' => true, + 'toadd' => $toadd + ]); + break; + default: $this->displaySpecificTypeField($ID, $field); break; diff --git a/inc/commonglpi.class.php b/inc/commonglpi.class.php index 04e2462870c..e896052a490 100644 --- a/inc/commonglpi.class.php +++ b/inc/commonglpi.class.php @@ -306,6 +306,24 @@ function addStandardTab($itemtype, array &$ong, array $options) { return $this; } + /** + * Add the impact tab if enabled for this item type + * + * @param array $ong defined tabs + * @param array $options options (for withtemplate) + * + * @return CommonGLPI + **/ + function addImpactTab(array &$ong, array $options) { + global $CFG_GLPI; + + // Check if impact analysis is enabled for this item type + if (isset($CFG_GLPI['impact_asset_types'][static::class])) { + $this->addStandardTab('Impact', $ong, $options); + } + + return $this; + } /** * Add default tab for form diff --git a/inc/commonitiltask.class.php b/inc/commonitiltask.class.php index ae3978b72b3..1ef216e94bb 100644 --- a/inc/commonitiltask.class.php +++ b/inc/commonitiltask.class.php @@ -1045,7 +1045,10 @@ static function genericPopulatePlanning($itemtype, $options = []) { if ($item->getFromDB($data["id"]) && $item->canViewItem()) { if ($parentitem->getFromDBwithData($item->fields[$parentitem->getForeignKeyField()], 0)) { - $key = $data["begin"]."$$$".$itemtype."$$$".$data["id"]; + $key = $data["begin"]. + "$$$".$itemtype. + "$$$".$data["id"]. + "$$$".$who."$$$".$who_group; $interv[$key]['color'] = $options['color']; $interv[$key]['event_type_color'] = $options['event_type_color']; $interv[$key]['itemtype'] = $itemtype; diff --git a/inc/computer.class.php b/inc/computer.class.php index 6409ffc7f8b..4978220e16a 100644 --- a/inc/computer.class.php +++ b/inc/computer.class.php @@ -74,6 +74,7 @@ function defineTabs($options = []) { $ong = []; $this->addDefaultFormTab($ong) + ->addImpactTab($ong, $options) ->addStandardTab('Item_OperatingSystem', $ong, $options) ->addStandardTab('Item_Devices', $ong, $options) ->addStandardTab('Item_Disk', $ong, $options) diff --git a/inc/config.class.php b/inc/config.class.php index e91f27baede..6c976f5d1b8 100644 --- a/inc/config.class.php +++ b/inc/config.class.php @@ -2023,6 +2023,8 @@ static function getLibraries($all = false) { 'check' => 'ScssPhp\ScssPhp\Compiler' ], [ 'name' => 'zendframework/zend-mail', 'check' => 'Zend\\Mail\\Protocol\\Imap' ], + [ 'name' => 'rlanvin/php-rrule', + 'check' => 'RRule\\RRule' ], ]; if (Toolbox::canUseCAS()) { $deps[] = [ diff --git a/inc/consumable.class.php b/inc/consumable.class.php index b8465f876fc..996568ca8b7 100644 --- a/inc/consumable.class.php +++ b/inc/consumable.class.php @@ -226,7 +226,7 @@ static function processMassiveActionsForOneItemtype(MassiveAction $ma, CommonDBT $ma->addMessage($item->getErrorMessage(ERROR_RIGHT)); } } - Event::log($item->fields['consumableitems_id'], "consumables", 5, "inventory", + Event::log($item->fields['consumableitems_id'], "consumableitems", 5, "inventory", //TRANS: %s is the user login sprintf(__('%s gives a consumable'), $_SESSION["glpiname"])); } else { diff --git a/inc/dbmysql.class.php b/inc/dbmysql.class.php index 218a193de72..4e9ba7d840c 100644 --- a/inc/dbmysql.class.php +++ b/inc/dbmysql.class.php @@ -639,7 +639,7 @@ function listTables($table = 'glpi_%', array $where = []) { * * @return DBmysqlIterator */ - public function getMyIsamTables(): DBmysqlIterator { + public function getMyIsamTables() { $iterator = $this->listTables('glpi_%', ['engine' => 'MyIsam']); return $iterator; } @@ -1195,7 +1195,7 @@ public function buildUpdate($table, $params, $clauses, array $joins = []) { //JOINS $it = new DBmysqlIterator($this); - $query .= $it->analyzeJoins($joins); + $query .= $it->analyseJoins($joins); $query .= " SET "; foreach ($params as $field => $value) { @@ -1320,7 +1320,7 @@ public function buildDelete($table, $where, array $joins = []) { $query = "DELETE " . self::quoteName($table) . " FROM ". self::quoteName($table); $it = new DBmysqlIterator($this); - $query .= $it->analyzeJoins($joins); + $query .= $it->analyseJoins($joins); $query .= " WHERE " . $it->analyseCrit($where); return $query; @@ -1532,7 +1532,7 @@ public function inTransaction() { * * @since 9.5.0 */ - public function areTimezonesAvailable(string &$msg = '') { + public function areTimezonesAvailable(&$msg = '') { $mysql_db_res = $this->request('SHOW DATABASES LIKE ' . $this->quoteValue('mysql')); if ($mysql_db_res->count() === 0) { $msg = __('Access to timezone database (mysql) is not allowed.'); @@ -1638,4 +1638,22 @@ public function clearSchemaCache() { $this->table_cache = []; $this->field_cache = []; } + + /** + * Quote a value for a specified type + * Should be used for PDO, but this will prevent heavy + * replacements in the source code in the future. + * + * @param mixed $value Value to quote + * @param integer $type Value type, defaults to PDO::PARAM_STR + * + * @return mixed + * + * @since 9.5.0 + */ + public function quote($value, $type = 2/*\PDO::PARAM_STR*/) { + return "'" . $this->escape($value) . "'"; + //return $this->dbh->quote($value, $type); + } + } diff --git a/inc/dbmysqliterator.class.php b/inc/dbmysqliterator.class.php index 6c7f4a401a7..3ba36281ca6 100644 --- a/inc/dbmysqliterator.class.php +++ b/inc/dbmysqliterator.class.php @@ -270,7 +270,7 @@ function buildQuery ($table, $crit = "", $log = false) { // JOIN if (!empty($join)) { - $this->sql .= $this->analyzeJoins($join); + $this->sql .= $this->analyseJoins($join); } // WHERE criteria list @@ -511,9 +511,9 @@ public function analyseCrit ($crit, $bool = "AND") { } else if ($name === 'RAW') { $key = key($value); $value = current($value); - $ret .= '((' . $key . ') ' . $this->analyzeCriterion($value) . ')'; + $ret .= '((' . $key . ') ' . $this->analyseCriterion($value) . ')'; } else { - $ret .= DBmysql::quoteName($name) . ' ' . $this->analyzeCriterion($value); + $ret .= DBmysql::quoteName($name) . ' ' . $this->analyseCriterion($value); } } return $ret; @@ -524,11 +524,11 @@ public function analyseCrit ($crit, $bool = "AND") { * * @since 9.3.1 * - * @param mixed $value Value to analyze + * @param mixed $value Value to analyse * * @return string */ - private function analyzeCriterion($value) { + private function analyseCriterion($value) { $criterion = null; $crit_value; @@ -540,20 +540,20 @@ private function analyzeCriterion($value) { if (is_array($value)) { if (count($value) == 2 && isset($value[0]) && $this->isOperator($value[0])) { $criterion = "{$value[0]} %crit_value"; - $crit_value = $this->analyzeCriterionValue($value[1]); + $crit_value = $this->analyseCriterionValue($value[1]); } else { if (!count($value)) { throw new \RuntimeException('Empty IN are not allowed'); } // Array of Values $criterion = "IN (%crit_value)"; - $crit_value = $this->analyzeCriterionValue($value); + $crit_value = $this->analyseCriterionValue($value); } } else { if ($value instanceof \QuerySubquery) { $criterion = "IN %crit_value"; } - $crit_value = $this->analyzeCriterionValue($value); + $crit_value = $this->analyseCriterionValue($value); } $criterion = str_replace('%crit_value', $crit_value, $criterion); } @@ -561,7 +561,7 @@ private function analyzeCriterion($value) { return $criterion; } - private function analyzeCriterionValue($value) { + private function analyseCriterionValue($value) { $crit_value = null; if (is_array($value)) { foreach ($value as $k => $v) { @@ -581,12 +581,12 @@ private function analyzeCriterionValue($value) { * * @since 9.4.0 * - * @param array $joinarray Array of joins to analyze + * @param array $joinarray Array of joins to analyse * [jointype => [table => criteria]] * * @return string */ - public function analyzeJoins(array $joinarray) { + public function analyseJoins(array $joinarray) { $query = ''; foreach ($joinarray as $jointype => $jointables) { if (!in_array($jointype, ['JOIN', 'LEFT JOIN', 'INNER JOIN', 'RIGHT JOIN'])) { diff --git a/inc/dcroom.class.php b/inc/dcroom.class.php index 6267f346c1d..e92da46d920 100644 --- a/inc/dcroom.class.php +++ b/inc/dcroom.class.php @@ -55,6 +55,7 @@ function defineTabs($options = []) { $this ->addStandardTab('Rack', $ong, $options) ->addDefaultFormTab($ong) + ->addImpactTab($ong, $options) ->addStandardTab('Infocom', $ong, $options) ->addStandardTab('Contract_Item', $ong, $options) ->addStandardTab('Document_Item', $ong, $options) @@ -63,7 +64,6 @@ function defineTabs($options = []) { ->addStandardTab('Item_Problem', $ong, $options) ->addStandardTab('Change_Item', $ong, $options) ->addStandardTab('Log', $ong, $options); - ; return $ong; } diff --git a/inc/define.php b/inc/define.php index 7558948adcc..069fab9c3b1 100644 --- a/inc/define.php +++ b/inc/define.php @@ -277,7 +277,7 @@ $CFG_GLPI["ticket_types"] = ['Computer', 'Monitor', 'NetworkEquipment', 'Peripheral', 'Phone', 'Printer', 'Software', 'SoftwareLicense', 'Certificate', - 'Line', 'DCRoom', 'Rack', 'Enclosure', 'Cluster']; + 'Line', 'DCRoom', 'Rack', 'Enclosure', 'Cluster', 'PDU']; $CFG_GLPI["link_types"] = ['Budget', 'CartridgeItem', 'Computer', 'ConsumableItem', 'Contact', 'Contract', 'Monitor', @@ -363,7 +363,7 @@ $CFG_GLPI["contract_types"] = array_merge(['Computer', 'Monitor', 'NetworkEquipment', 'Peripheral', 'Phone', 'Printer', 'Project', 'Line', 'Software', 'SoftwareLicense', 'Certificate', - 'DCRoom', 'Rack', 'Enclosure', 'Cluster'], + 'DCRoom', 'Rack', 'Enclosure', 'Cluster', 'PDU'], $CFG_GLPI['itemdevices']); @@ -383,8 +383,8 @@ // Items which can planned something $CFG_GLPI['planning_types'] = ['ChangeTask', 'ProblemTask', 'Reminder', - 'TicketTask', 'ProjectTask']; -$CFG_GLPI['planning_add_types'] = ['Reminder']; + 'TicketTask', 'ProjectTask', 'PlanningExternalEvent']; +$CFG_GLPI['planning_add_types'] = ['PlanningExternalEvent']; $CFG_GLPI["globalsearch_types"] = ['Computer', 'Contact', 'Contract', 'Document', 'Monitor', @@ -484,7 +484,8 @@ 'tools' => [ 'project' => ['gantt'], 'knowbaseitem' => ['tinymce', 'jstree'], - 'reminder' => ['tinymce'] + 'reminder' => ['tinymce'], + 'impact' => ['colorpicker'] ], 'management' => [ 'datacenter' => [ @@ -494,9 +495,10 @@ 'config' => [ 'config' => ['colorpicker'], 'commondropdown' => [ - 'ProjectState' => ['colorpicker'], - 'SolutionTemplate' => ['tinymce'], - 'ITILFollowupTemplate' => ['tinymce'] + 'ITILFollowupTemplate' => ['tinymce'], + 'PlanningEventCategory' => ['colorpicker'], + 'ProjectState' => ['colorpicker'], + 'SolutionTemplate' => ['tinymce'], ], 'notification' => [ 'notificationtemplate' => ['tinymce'] @@ -510,3 +512,16 @@ //Maximum time, in miliseconds a saved search should not exeed //so we count it on display (using automatic mode). $CFG_GLPI['max_time_for_count'] = 200; + +$CFG_GLPI["impact_asset_types"] = [ + 'Computer' => "pics/impact/computer.png", + 'Monitor' => "pics/impact/monitor.png", + 'NetworkEquipment' => "pics/impact/networkequipment.png", + 'Peripheral' => "pics/impact/peripheral.png", + 'Phone' => "pics/impact/phone.png", + 'Printer' => "pics/impact/printer.png", + 'Software' => "pics/impact/software.png", + 'DCRoom' => "pics/impact/dcroom.png", + 'Rack' => "pics/impact/rack.png", + 'Enclosure' => "pics/impact/enclosure.png" +]; diff --git a/inc/displaypreference.class.php b/inc/displaypreference.class.php index 04c8cc2bdf2..a47d0dfd3ea 100644 --- a/inc/displaypreference.class.php +++ b/inc/displaypreference.class.php @@ -268,7 +268,7 @@ function orderItem(array $input, $action) { * @param $target form target * @param $itemtype item type * - * @return nothing + * @return void **/ function showFormPerso($target, $itemtype) { global $CFG_GLPI, $DB; @@ -440,7 +440,7 @@ class='submit'>"; * @param $target form target * @param $itemtype item type * - * @return nothing + * @return void **/ function showFormGlobal($target, $itemtype) { global $CFG_GLPI, $DB; diff --git a/inc/document.class.php b/inc/document.class.php index 4038977a357..9cd55e26055 100644 --- a/inc/document.class.php +++ b/inc/document.class.php @@ -354,7 +354,7 @@ function prepareInputForUpdate($input) { * - target filename : where to go when done. * - withtemplate boolean : template or basic item * - * @return Nothing (display) + * @return void **/ function showForm($ID, $options = []) { $this->initForm($ID, $options); @@ -1312,7 +1312,7 @@ static function uploadDocument(array &$input, $FILEDESC) { * @param $dir dir to search a free path for the file * @param $sha1sum SHA1 of the file * - * @return nothing + * @return string **/ static function getUploadFileValidLocationName($dir, $sha1sum) { if (empty($dir)) { @@ -1437,7 +1437,9 @@ static function isValidDoc($filename) { * * @param $options array of possible options * - * @return nothing (print out an HTML select box) + * @return integer|string + * integer if option display=true (random part of elements id) + * string if option display=false (HTML code) **/ static function dropdown($options = []) { global $DB, $CFG_GLPI; diff --git a/inc/document_item.class.php b/inc/document_item.class.php index f434f1e6387..1e6a3b5553d 100644 --- a/inc/document_item.class.php +++ b/inc/document_item.class.php @@ -328,7 +328,7 @@ static function cloneItem($itemtype, $oldid, $newid, $newitemtype = '') { * * @param $doc Document object * - * @return nothing (HTML display) + * @return void **/ static function showForDocument(Document $doc) { $instID = $doc->fields['id']; diff --git a/inc/dropdown.class.php b/inc/dropdown.class.php index 37d6dee807b..43b81447a97 100755 --- a/inc/dropdown.class.php +++ b/inc/dropdown.class.php @@ -543,7 +543,9 @@ static function getDropdownArrayNames($table, $ids) { * - emptylabel : empty label if empty displayed (default self::EMPTY_VALUE) * - display_emptychoice : display empty choice (default false) * - * @return nothing (print out an HTML select box) + * @return integer|string + * integer if option display=true (random part of elements id) + * string if option display=false (HTML code) **/ static function showItemTypes($name, $types = [], $options = []) { global $CFG_GLPI; @@ -584,7 +586,9 @@ static function showItemTypes($name, $types = [], $options = []) { * @param $options array of possible options: * - may be value (default value) / field (used field to search itemtype) * - * @return nothing (print out an HTML select box) + * @return integer|string + * integer if option display=true (random part of elements id) + * string if option display=false (HTML code) **/ static function dropdownUsedItemTypes($name, $itemtype_ref, $options = []) { global $DB; @@ -620,7 +624,10 @@ static function dropdownUsedItemTypes($name, $itemtype_ref, $options = []) { * @param $store_path path where icons are stored * @param $display boolean display of get string ? (true by default) * - * @return nothing (print out an HTML select box) + * + * @return void|string + * void if param display=true + * string if param display=false (HTML code) **/ static function dropdownIcons($myname, $value, $store_path, $display = true) { @@ -832,13 +839,13 @@ static function getStandardDropdownItemTypes() { Session::getPluralNumber()), 'SolutionType' => _n('Solution type', 'Solution types', Session::getPluralNumber()), + 'SolutionTemplate' => _n('Solution template', + 'Solution templates', + Session::getPluralNumber()), 'RequestType' => _n('Request source', 'Request sources', Session::getPluralNumber()), 'ITILFollowupTemplate' => _n('Followup template', 'Followup templates', Session::getPluralNumber()), - 'SolutionTemplate' => _n('Solution template', - 'Solution templates', - Session::getPluralNumber()), 'ProjectState' => _n('Project state', 'Project states', Session::getPluralNumber()), 'ProjectType' => _n('Project type', 'Project types', @@ -848,6 +855,9 @@ static function getStandardDropdownItemTypes() { Session::getPluralNumber()), 'ProjectTaskTemplate' => _n('Project task template', 'Project task templates', Session::getPluralNumber()), + 'PlanningExternalEventTemplate' + => PlanningExternalEventTemplate::getTypeName( + Session::getPluralNumber()), ], _n('Type', 'Types', Session::getPluralNumber()) => [ @@ -1250,7 +1260,10 @@ static function getLanguageName($value) { * - step step time (defaut config GLPI) * * @since 0.85 update prototype - *@return Nothing (display) + * + * @return integer|string + * integer if option display=true (random part of elements id) + * string if option display=false (HTML code) **/ static function showHours($name, $options = []) { global $CFG_GLPI; @@ -1798,6 +1811,10 @@ static function showAdvanceDateRestrictionSwitch($enabled = 0) { * 'key2' => 'val2'), * 'optgroupname2' => array('key3' => 'val3', * 'key4' => 'val4')) + * + * @return integer|string + * integer if option display=true (random part of elements id) + * string if option display=false (HTML code) **/ static function showFromArray($name, array $elements, $options = []) { @@ -3206,8 +3223,15 @@ public static function getDropdownFindNum($post, $json = true) { $where[] = ['OR' => $orwhere]; } - //If software or plugins : filter to display only the objects that are allowed to be visible in Helpdesk - if (in_array($post['itemtype'], $CFG_GLPI["helpdesk_visible_types"])) { + // If software or plugins : filter to display only the objects that are allowed to be visible in Helpdesk + $filterHelpdesk = in_array($post['itemtype'], $CFG_GLPI["helpdesk_visible_types"]); + + if (isset($post['context']) && $post['context'] == "impact" + && isset($CFG_GLPI['impact_asset_types'][$post['itemtype']])) { + $filterHelpdesk = false; + } + + if ($filterHelpdesk) { $where['is_helpdesk_visible'] = 1; } diff --git a/inc/dropdowntranslation.class.php b/inc/dropdowntranslation.class.php index 5c3f9e3c119..c58e44895ca 100644 --- a/inc/dropdowntranslation.class.php +++ b/inc/dropdowntranslation.class.php @@ -239,7 +239,7 @@ function checkBeforeAddorUpdate($input, $add = true) { * @param $input array of user values * @param $add boolean true if translation is added, false if update (tgrue by default) * - * @return nothing + * @return void **/ function generateCompletename($input, $add = true) { global $DB; diff --git a/inc/enclosure.class.php b/inc/enclosure.class.php index 05d825a5460..823b8eda0c8 100644 --- a/inc/enclosure.class.php +++ b/inc/enclosure.class.php @@ -51,6 +51,7 @@ static function getTypeName($nb = 0) { function defineTabs($options = []) { $ong = []; $this->addDefaultFormTab($ong) + ->addImpactTab($ong, $options) ->addStandardTab('Item_Enclosure', $ong, $options) ->addStandardTab('Item_Devices', $ong, $options) ->addStandardTab('NetworkPort', $ong, $options) @@ -61,7 +62,6 @@ function defineTabs($options = []) { ->addStandardTab('Item_Problem', $ong, $options) ->addStandardTab('Change_Item', $ong, $options) ->addStandardTab('Log', $ong, $options); - ; return $ong; } diff --git a/inc/entity.class.php b/inc/entity.class.php index 37621255a4f..42991eb5e2a 100644 --- a/inc/entity.class.php +++ b/inc/entity.class.php @@ -394,7 +394,7 @@ static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtem /** * Print a good title for entity pages * - *@return nothing (display) + *@return void **/ function title() { // Empty title for entities diff --git a/inc/event.class.php b/inc/event.class.php index 79a028df0ab..d474f6d7c1f 100644 --- a/inc/event.class.php +++ b/inc/event.class.php @@ -207,10 +207,10 @@ static function displayItemLogID($type, $items_id) { $type = getSingular($type); $url = ''; if ($item = getItemForItemtype($type)) { - $url = $item->getFormURL(); + $url = $item->getFormURLWithID($items_id); } if (!empty($url)) { - echo "".$items_id.""; + echo "".$items_id.""; } else { echo $items_id; } diff --git a/inc/fieldblacklist.class.php b/inc/fieldblacklist.class.php index 744ae32ee36..0d250647eb3 100644 --- a/inc/fieldblacklist.class.php +++ b/inc/fieldblacklist.class.php @@ -238,7 +238,7 @@ function displaySpecificTypeField($ID, $field = []) { /** * Display a dropdown which contains all the available itemtypes * - * @return nothing + * @return void **/ function showItemtype() { global $CFG_GLPI; diff --git a/inc/fieldunicity.class.php b/inc/fieldunicity.class.php index c5b1e182fd4..9a6cf9dc8a3 100644 --- a/inc/fieldunicity.class.php +++ b/inc/fieldunicity.class.php @@ -152,7 +152,7 @@ function displaySpecificTypeField($ID, $field = []) { * @param ID the field unicity item id * @param value the selected value (default 0) * - * @return nothing + * @return void **/ function showItemtype($ID, $value = 0) { global $CFG_GLPI; @@ -234,7 +234,7 @@ public static function getUnicityFieldsConfig($itemtype, $entities_id = 0, $chec * * @param $unicity an instance of CommonDBTM class * - * @return nothing + * @return void **/ static function selectCriterias(CommonDBTM $unicity) { global $DB; @@ -509,7 +509,7 @@ function prepareInputForUpdate($input) { * * @param itemtype * - * @return nothing + * @return void **/ static function deleteForItemtype($itemtype) { global $DB; diff --git a/inc/group.class.php b/inc/group.class.php index 56d2a0dc8e4..9056d79e8c2 100644 --- a/inc/group.class.php +++ b/inc/group.class.php @@ -206,7 +206,7 @@ function defineTabs($options = []) { * - target filename : where to go when done. * - withtemplate boolean : template or basic item * - * @return Nothing (display) + * @return void **/ function showForm($ID, $options = []) { @@ -299,7 +299,7 @@ function showForm($ID, $options = []) { /** * Print a good title for group pages * - *@return nothing (display) + *@return void **/ function title() { global $CFG_GLPI; diff --git a/inc/group_user.class.php b/inc/group_user.class.php index 6813f4b7376..4a10385e0cf 100644 --- a/inc/group_user.class.php +++ b/inc/group_user.class.php @@ -790,4 +790,111 @@ protected static function getListForItemParams(CommonDBTM $item, $noent = false) $params['SELECT'][] = self::getTable() . '.is_userdelegate'; return $params; } + + + function post_addItem() { + global $DB; + + // add new user to plannings + $groups_id = $this->fields['groups_id']; + $planning_k = 'group_'.$groups_id.'_users'; + + // find users with the current group in their plannings + $user_inst = new User; + $users = $user_inst->find([ + 'plannings' => ['LIKE', "%$planning_k%"] + ]); + + // add the new user to found plannings + $query = $DB->buildUpdate( + User::getTable(), [ + 'plannings' => new QueryParam(), + ], [ + 'id' => new QueryParam() + ] + ); + $stmt = $DB->prepare($query); + $in_transaction = $DB->inTransaction(); + if (!$in_transaction) { + $DB->beginTransaction(); + } + foreach ($users as $user) { + $users_id = $user['id']; + $plannings = importArrayFromDB($user['plannings']); + $nb_users = count($plannings['plannings'][$planning_k]['users']); + + // add the planning for the user + $plannings['plannings'][$planning_k]['users']['user_'.$this->fields['users_id']]= [ + 'color' => Planning::getPaletteColor('bg', $nb_users), + 'display' => true, + 'type' => 'user' + ]; + + // if current user logged, append also to its session + if ($users_id == Session::getLoginUserID()) { + $_SESSION['glpi_plannings'] = $plannings; + } + + // save the planning completed to db + $json_plannings = exportArrayToDB($plannings); + $stmt->bind_param('si', $json_plannings, $users_id); + $stmt->execute(); + } + + if (!$in_transaction) { + $DB->commit(); + } + $stmt->close(); + } + + + function post_purgeItem() { + global $DB; + + // remove user from plannings + $groups_id = $this->fields['groups_id']; + $planning_k = 'group_'.$groups_id.'_users'; + + // find users with the current group in their plannings + $user_inst = new User; + $users = $user_inst->find([ + 'plannings' => ['LIKE', "%$planning_k%"] + ]); + + // remove the deleted user to found plannings + $query = $DB->buildUpdate( + User::getTable(), [ + 'plannings' => new QueryParam(), + ], [ + 'id' => new QueryParam() + ] + ); + $stmt = $DB->prepare($query); + $in_transaction = $DB->inTransaction(); + if (!$in_transaction) { + $DB->beginTransaction(); + } + foreach ($users as $user) { + $users_id = $user['id']; + $plannings = importArrayFromDB($user['plannings']); + + // delete planning for the user + unset($plannings['plannings'][$planning_k]['users']['user_'.$this->fields['users_id']]); + + // if current user logged, append also to its session + if ($users_id == Session::getLoginUserID()) { + $_SESSION['glpi_plannings'] = $plannings; + } + + // save the planning completed to db + $json_plannings = exportArrayToDB($plannings); + $stmt->bind_param('si', $json_plannings, $users_id); + $stmt->execute(); + } + + if (!$in_transaction) { + $DB->commit(); + } + $stmt->close(); + } } diff --git a/inc/html.class.php b/inc/html.class.php index 5d3adec7f5f..e051718b060 100644 --- a/inc/html.class.php +++ b/inc/html.class.php @@ -471,7 +471,7 @@ static function weblink_extract($value) { /** * Redirection to $_SERVER['HTTP_REFERER'] page * - * @return nothing + * @return void **/ static function back() { self::redirect(self::getBackUrl()); @@ -484,7 +484,7 @@ static function back() { * @param $dest string: Redirection destination * @param $http_response_code string: Forces the HTTP response code to the specified value * - * @return nothing + * @return void **/ static function redirect($dest, $http_response_code = 302) { @@ -519,7 +519,7 @@ static function redirect($dest, $http_response_code = 302) { * @param $params param to add to URL (default '') * @since 0.85 * - * @return nothing + * @return void **/ static function redirectToLogin($params = '') { global $CFG_GLPI; @@ -543,7 +543,7 @@ static function redirectToLogin($params = '') { /** * Display common message for item not found * - * @return Nothing + * @return void **/ static function displayNotFoundError() { global $CFG_GLPI, $HEADER_LOADED; @@ -570,7 +570,7 @@ static function displayNotFoundError() { /** * Display common message for privileges errors * - * @return Nothing (die) + * @return void **/ static function displayRightError() { self::displayErrorAndDie(__("You don't have permission to perform this action.")); @@ -655,7 +655,7 @@ static function displayMessageAfterRedirect() { if ($('#message_after_redirect_$msgtype').dialog('isOpen') && !$(e.target).is('.ui-dialog, a') && !$(e.target).closest('.ui-dialog').length) { - $('#message_after_redirect_$msgtype').dialog('close'); + $('#message_after_redirect_$msgtype').remove(); // redo focus on initial element e.target.focus(); } @@ -701,7 +701,7 @@ static function displayAjaxMessageAfterRedirect() { * @param $ref_title Title to display (default '') * @param $ref_btts Extra items to display array(link=>text...) (default '') * - * @return nothing + * @return void **/ static function displayTitle($ref_pic_link = "", $ref_pic_text = "", $ref_title = "", $ref_btts = "") { @@ -923,7 +923,7 @@ static function getBackUrl($url_in = "") { * @param $message string displayed before dying * @param $minimal set to true do not display app menu (false by default) * - * @return nothing as function kill script + * @return void **/ static function displayErrorAndDie ($message, $minimal = false) { global $CFG_GLPI, $HEADER_LOADED; @@ -954,7 +954,7 @@ static function displayErrorAndDie ($message, $minimal = false) { * @param $additionalactions string additional actions to do on success confirmation * (default '') * - * @return nothing + * @return string **/ static function addConfirmationOnAction($string, $additionalactions = '') { @@ -971,7 +971,7 @@ static function addConfirmationOnAction($string, $additionalactions = '') { * @param $additionalactions string additional actions to do on success confirmation * (default '') * - * @return confirmation script + * @return string confirmation script **/ static function getConfirmationOnActionScript($string, $additionalactions = '') { @@ -1017,7 +1017,7 @@ static function getConfirmationOnActionScript($string, $additionalactions = '') * - percent current level * * - * @return nothing (display) + * @return void **/ static function progressBar($id, array $options = []) { @@ -1063,7 +1063,7 @@ static function progressBar($id, array $options = []) { * * @param $msg initial message (under the bar) (default ' ') * - * @return nothing + * @return void **/ static function createProgressBar($msg = " ") { @@ -1080,7 +1080,7 @@ static function createProgressBar($msg = " ") { * * @param $msg message under the bar (default ' ') * - * @return nothing + * @return void **/ static function changeProgressBarMessage($msg = " ") { @@ -1096,7 +1096,7 @@ static function changeProgressBarMessage($msg = " ") { * @param $tot Maximum Value * @param $msg message inside the bar (default is %) (default '') * - * @return nothing + * @return void **/ static function changeProgressBarPosition($crt, $tot, $msg = "") { @@ -1129,7 +1129,7 @@ static function changeProgressBarPosition($crt, $tot, $msg = "") { * - simple : display a simple progress bar (no title / only percent) * - forcepadding : boolean force str_pad to force refresh (default true) * - * @return nothing + * @return void **/ static function displayProgressBar($width, $percent, $options = []) { global $CFG_GLPI; @@ -1182,7 +1182,7 @@ static function displayProgressBar($width, $percent, $options = []) { * @param $item item corresponding to the page displayed (default 'none') * @param $option option corresponding to the page displayed (default '') * - * @return nothing + * @return void **/ static function includeHeader($title = '', $sector = 'none', $item = 'none', $option = '') { global $CFG_GLPI, $PLUGIN_HOOKS; @@ -1410,7 +1410,7 @@ static function getMenuInfos() { $menu['tools']['title'] = __('Tools'); $menu['tools']['types'] = ['Project', 'Reminder', 'RSSFeed', 'KnowbaseItem', 'ReservationItem', 'Report', 'MigrationCleaner', - 'SavedSearch']; + 'SavedSearch', 'Impact']; $menu['plugins']['title'] = _n('Plugin', 'Plugins', Session::getPluralNumber()); $menu['plugins']['types'] = []; @@ -2351,7 +2351,7 @@ static function getCheckbox(array $options) { * * @param $options array * - * @return nothing (display only) + * @return void **/ static function showCheckbox(array $options = []) { echo self::getCheckbox($options); @@ -2405,7 +2405,7 @@ static function showMassiveActionCheckBox($itemtype, $id, array $options = []) { * * @param $name given name/id to the form (default '') * - * @return nothing / display item + * @return void **/ static function openMassiveActionsForm($name = '') { echo Html::getOpenMassiveActionsForm($name); @@ -3316,7 +3316,7 @@ static function showDatesTimelineGraph($options = []) { * * @param $target target of the form * - * @return nothing + * @return void **/ static function showProfileSelecter($target) { global $CFG_GLPI; @@ -3368,7 +3368,9 @@ static function showProfileSelecter($target) { * - display : boolean / display the item : false return the datas * - autoclose : boolean / autoclose the item : default true (false permit to scroll) * - * @return nothing (print out an HTML div) + * @return void|string + * void if option display=true + * string if option display=false (HTML code) **/ static function showToolTip($content, $options = []) { global $CFG_GLPI; @@ -3580,7 +3582,10 @@ class='autocompletion-text-field'"; * @param $display boolean display or get js script (true by default) * @param $readonly boolean editor will be readonly or not * - * @return nothing + * + * @return void|string + * integer if param display=true + * string if param display=false (HTML code) **/ static function initEditorSystem($name, $rand = '', $display = true, $readonly = false) { global $CFG_GLPI; @@ -3842,7 +3847,7 @@ static function printAjaxPager($title, $start, $numrows, $additional_info = '', * @param $pad Pad used (default 0) * @param $jsexpand Expand using JS ? (default false) * - * @return nothing + * @return void **/ static function printCleanArray($tab, $pad = 0, $jsexpand = false) { @@ -3907,7 +3912,7 @@ static function printCleanArray($tab, $pad = 0, $jsexpand = false) { * @param $item_type_output_param item type parameter for export (default 0) * @param $additional_info Additional information to display (default '') * - * @return nothing (print a pager) + * @return void * **/ static function printPager($start, $numrows, $target, $parameters, $item_type_output = 0, @@ -6245,7 +6250,7 @@ static public function manageRefreshPage($timer = false, $callback = null) { * * @param string $action action to switch (should be actually 'getHtml' or 'getList') * - * @return nothing (display) + * @return string */ static function fuzzySearch($action = '') { global $CFG_GLPI; diff --git a/inc/htmltablegroup.class.php b/inc/htmltablegroup.class.php index 9fa77b128eb..76e56ec5454 100644 --- a/inc/htmltablegroup.class.php +++ b/inc/htmltablegroup.class.php @@ -182,7 +182,7 @@ function prepareDisplay() { * 'display_header_for_each_group' display the header of each group * 'display_header_on_foot_for_each_group' repeat group header on foot of group * - * @return nothing (display only) + * @return void **/ function displayGroup($totalNumberOfColumn, array $params) { diff --git a/inc/htmltableheader.class.php b/inc/htmltableheader.class.php index bfec8c3f13e..fdbc58c044a 100644 --- a/inc/htmltableheader.class.php +++ b/inc/htmltableheader.class.php @@ -61,7 +61,7 @@ abstract protected function getTable(); * @param $header_name [out] string header name * @param $subheader_name [out] string sub header name ( = '' in case of super header) * - * @return nothing + * @return void **/ abstract function getHeaderAndSubHeaderName(&$header_name, &$subheader_name); diff --git a/inc/htmltablemain.class.php b/inc/htmltablemain.class.php index 96fa06ec34f..43776e836fa 100644 --- a/inc/htmltablemain.class.php +++ b/inc/htmltablemain.class.php @@ -78,7 +78,7 @@ function __construct() { * * @param $name the name to print inside the header * - * @return nothing + * @return void **/ function setTitle($name) { $this->title = $name; @@ -101,7 +101,7 @@ function tryAddHeader() { * * TODO : study to be sure that the order is the one we have defined ... * - * @return nothing + * @return boolean|HTMLTableGroup **/ function createGroup($name, $content) { @@ -128,7 +128,7 @@ function addItemType($itemtype, $title) { * * @param $group_name (string) the group name * - * @return nothing + * @return boolean|HTMLTableGroup **/ function getGroup($group_name) { @@ -184,7 +184,7 @@ function getNumberOfRows() { * 'display_super_for_each_group' display the super header befor each group * 'display_title_for_each_group' display the title of each group * - * @return nothing (display only) + * @return void **/ function display(array $params) { diff --git a/inc/impact.class.php b/inc/impact.class.php new file mode 100644 index 00000000000..c51ed65f85e --- /dev/null +++ b/inc/impact.class.php @@ -0,0 +1,1097 @@ + 0 + $total = 0; + } else if ($isEnabledAsset) { + // If on an asset, get the number of its direct dependencies + $total = count($DB->request([ + 'FROM' => ImpactRelation::getTable(), + 'WHERE' => [ + 'OR' => [ + [ + 'itemtype_source' => get_class($item), + 'items_id_source' => $item->fields['id'], + ], + [ + 'itemtype_impacted' => get_class($item), + 'items_id_impacted' => $item->fields['id'], + ] + ] + ] + ])); + } else if ($isITILObject) { + // Tab name for an ITIL object : always 0 + $total = 0; + } + + return self::createTabEntry(__("Impact analysis"), $total); + } + + public static function displayTabContentForItem( + CommonGLPI $item, + $tabnum = 1, + $withtemplate = 0 + ) { + global $CFG_GLPI; + + $class = get_class($item); + + // Only enabled for CommonDBTM + if (!is_a($item, "CommonDBTM")) { + throw new InvalidArgumentException( + "Argument \$item ($class) must be a CommonDBTM)." + ); + } + + $ID = $item->fields['id']; + + // Don't show the impact analysis on new object + if ($item->isNewID($ID)) { + return false; + } + + // Check READ rights + $itemtype = $item->getType(); + if (!$itemtype::canView()) { + return false; + } + + // For an ITIL object, load the first linked element by default + if (is_a($item, "CommonITILObject")) { + $linked_items = $item->getLinkedItems(); + + // Search for a valid linked item of this ITILObject + $found = false; + foreach ($linked_items as $linked_item) { + $class = $linked_item['itemtype']; + if (isset($CFG_GLPI['impact_asset_types'][$class])) { + $item = new $class; + $found = $item->getFromDB($linked_item['items_id']); + break; + } + } + + // No valid linked item were found, tab shouldn't be visible + if (!$found) { + return false; + } + + self::printAssetSelectionForm($linked_items); + } + + // Show graph if the impact analysis is enable for $class + if (isset($CFG_GLPI['impact_asset_types'][$class])) { + self::loadLibs(); + self::prepareImpactNetwork($item); + self::buildNetwork($item); + } + + return true; + } + + /** + * Load the cytoscape and spectrum-colorpicker librairies + * + * @since 9.5 + */ + public static function loadLibs() { + echo Html::css('public/lib/spectrum-colorpicker.css'); + echo Html::script("public/lib/spectrum-colorpicker.js"); + echo Html::css('public/lib/cytoscape.css'); + echo Html::script("public/lib/cytoscape.js"); + } + + /** + * Print the asset selection form used in the impact tab of ITIL objects + * + * @param array $items + * + * @since 9.5 + */ + public static function printAssetSelectionForm(array $items) { + global $CFG_GLPI; + + // Dropdown values + $values = []; + + // Add a value in the dropdown for each items, grouped by type + foreach ($items as $item) { + if (isset($CFG_GLPI['impact_asset_types'][$item['itemtype']])) { + // Add itemtype group if it doesn't exist in the dropdown yet + $itemtype_label = $item['itemtype']::getTypeName(); + if (!isset($values[$itemtype_label])) { + $values[$itemtype_label] = []; + } + + $key = $item['itemtype'] . "::" . $item['items_id']; + $values[$itemtype_label][$key] = $item['name']; + } + } + + Dropdown::showFromArray("impact_assets_selection_dropdown", $values); + echo '
'; + + // Form interaction: load a new graph on value change + echo Html::scriptBlock(' + $(function() { + var selector = "select[name=impact_assets_selection_dropdown]"; + + $(selector).change(function(){ + var values = $(selector + " option:selected").val().split("::"); + + $.ajax({ + type: "GET", + url: "'. $CFG_GLPI['root_doc'] . '/ajax/impact.php", + data: { + itemtype: values[0], + items_id: values[1], + }, + success: function(data, textStatus, jqXHR) { + GLPIImpact.buildNetwork( + JSON.parse(data.graph), + JSON.parse(data.params) + ); + } + }); + }); + }); + '); + } + + /** + * Load the impact network container + * + * @since 9.5 + */ + public static function printImpactNetworkContainer() { + global $CFG_GLPI; + + $action = $CFG_GLPI['root_doc'] . '/ajax/impact.php'; + $formName = "form_impact_network"; + + echo "
"; + echo ""; + + // First row: header + echo ""; + echo ""; + echo ""; + + // Second row: network graph + echo '"; + + echo "
" . __('Impact graph') . "
'; + echo '
'; + echo ''; + echo '
'; + echo '' . __("Save") . ' '; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo '
'; + self::printDropdownMenu(); + echo '
'; + echo '
'; + echo "
"; + Html::closeForm(); + } + + /** + * Print the dropdown menu at the end of the toolbar + */ + public static function printDropdownMenu() { + echo + '
' . + '' . + "
"; + + // JS to show/hide the dropdown + echo Html::scriptBlock(" + var el = document.querySelector('.more'); + var btn = $('.more')[0]; + var menu = el.querySelector('.more-menu'); + var visible = false; + + function showMenu(e) { + e.preventDefault(); + if (!visible) { + visible = true; + el.classList.add('show-more-menu'); + $(menu).show(); + document.addEventListener('mousedown', hideMenu, false); + } else { + visible = false; + el.classList.remove('show-more-menu'); + $(menu).hide(); + document.removeEventListener('mousedown', hideMenu); + } + } + + function hideMenu(e) { + if (e.target.id == 'expand_toolbar') { + return; + } + if (btn.contains(e.target)) { + return; + } + if (visible) { + visible = false; + el.classList.remove('show-more-menu'); + $(menu).hide(); + document.removeEventListener('mousedown', hideMenu); + } + } + "); + } + + /** + * Build the impact graph starting from a node + * + * @since 9.5 + * + * @param CommonDBTM $item Current item + * + * @return array Array containing edges and nodes + */ + public static function buildGraph(CommonDBTM $item) { + $nodes = []; + $edges = []; + + // Explore the graph forward + self::buildGraphFromNode($nodes, $edges, $item, self::DIRECTION_FORWARD); + + // Explore the graph backward + self::buildGraphFromNode($nodes, $edges, $item, self::DIRECTION_BACKWARD); + + // Add current node to the graph if no impact relations were found + if (count($nodes) == 0) { + self::addNode($nodes, $item); + } + + return [ + 'nodes' => $nodes, + 'edges' => $edges + ]; + } + + /** + * Explore dependencies of the current item, subfunction of buildGraph() + * + * @since 9.5 + * + * @param array $edges Edges of the graph + * @param array $nodes Nodes of the graph + * @param CommonDBTM $node Current node + * @param int $direction The direction in which the graph + * is being explored : DIRECTION_FORWARD + * or DIRECTION_BACKWARD + * @param array $explored_nodes List of nodes that have already been + * explored + * + * @throws InvalidArgumentException + */ + private static function buildGraphFromNode( + array &$nodes, + array &$edges, + CommonDBTM $node, + int $direction, + array $explored_nodes = [] + ) { + global $DB; + + // Source and target are determined by the direction in which we are + // exploring the graph + switch ($direction) { + case self::DIRECTION_BACKWARD: + $source = "source"; + $target = "impacted"; + break; + case self::DIRECTION_FORWARD: + $source = "impacted"; + $target = "source"; + break; + default: + throw new InvalidArgumentException( + "Invalid value for argument \$direction ($direction)." + ); + } + + // Get relations of the current node + $relations = $DB->request([ + 'FROM' => ImpactRelation::getTable(), + 'WHERE' => [ + 'itemtype_' . $target => get_class($node), + 'items_id_' . $target => $node->fields['id'] + ] + ]); + + // Add current code to the graph if we found at least one impact relation + if (count($relations)) { + self::addNode($nodes, $node); + } + + // Iterate on each relations found + foreach ($relations as $related_item) { + // Add the related node + $related_node = new $related_item['itemtype_' . $source]; + $related_node->getFromDB($related_item['items_id_' . $source]); + self::addNode($nodes, $related_node); + + // Add or update the relation on the graph + $edgeID = self::getEdgeID($node, $related_node, $direction); + self::addEdge($edges, $edgeID, $node, $related_node, $direction); + + // Keep exploring from this node unless we already went through it + $related_node_id = self::getNodeID($related_node); + if (!isset($explored_nodes[$related_node_id])) { + $explored_nodes[$related_node_id] = true; + self::buildGraphFromNode( + $nodes, + $edges, + $related_node, + $direction, + $explored_nodes + ); + } + } + } + + /** + * Add a node to the node list if missing + * + * @param array $nodes Nodes of the graph + * @param CommonDBTM $item Node to add + * + * @since 9.5 + * + * @return bool true if the node was missing, else false + */ + private static function addNode(array &$nodes, CommonDBTM $item) { + global $CFG_GLPI; + + // Check if the node already exist + $key = self::getNodeID($item); + if (isset($nodes[$key])) { + return false; + } + + // Get web path to the image matching the itemtype from config + $image_name = $CFG_GLPI["impact_asset_types"][get_class($item)]; + + // Add default image if the real path doesn't lead to an existing file + if (!file_exists(__DIR__ . "/../$image_name")) { + $image_name = "pics/impact/default.png"; + } + + // Define basic data of the new node + $new_node = [ + 'id' => $key, + 'label' => $item->fields['name'], + 'image' => $CFG_GLPI['root_doc'] . "/$image_name", + 'ITILObjects' => $item->getITILTickets(true), + 'link' => $item->getLinkURL() + ]; + + // Alter the label if we found some linked ITILObjects + $itil_tickets_count = $new_node['ITILObjects']['count']; + if ($itil_tickets_count > 0) { + $new_node['label'] .= " ($itil_tickets_count)"; + $new_node['hasITILObjects'] = 1; + } + + // Load or create a new ImpactItem object + $impact_item = ImpactItem::findForItem($item); + if (!$impact_item) { + $impact_item = new ImpactItem(); + $newID = $impact_item->add([ + 'itemtype' => get_class($item), + 'items_id' => $item->fields['id'] + ]); + $impact_item->getFromDB($newID); + } + + // Load node position and parent + $new_node['impactitem_id'] = $impact_item->fields['id']; + $new_node['parent'] = $impact_item->fields['parent_id']; + $new_node['position_x'] = $impact_item->fields['position_x']; + $new_node['position_y'] = $impact_item->fields['position_y']; + + // If the node has a parent, add it to the node list aswell + if (!empty($new_node['parent'])) { + $compound = new ImpactCompound(); + $compound->getFromDB($new_node['parent']); + + if (!isset($nodes[$new_node['parent']])) { + $nodes[$new_node['parent']] = [ + 'id' => $compound->fields['id'], + 'label' => $compound->fields['name'], + 'color' => $compound->fields['color'], + ]; + } + } + + // Insert the node + $nodes[$key] = $new_node; + return true; + } + + /** + * Add an edge to the edge list if missing, else update it's direction + * + * @param array $edges Edges of the graph + * @param string $key ID of the new edge + * @param CommonDBTM $itemA One of the node connected to this edge + * @param CommonDBTM $itemB The other node connected to this edge + * @param int $direction Direction of the edge : A to B or B to A ? + * + * @since 9.5 + * + * @return bool true if the node was missing, else false + * + * @throws InvalidArgumentException + */ + private static function addEdge( + array &$edges, + string $key, + CommonDBTM $itemA, + CommonDBTM $itemB, + int $direction + ) { + // Just update the flag if the edge already exist + if (isset($edges[$key])) { + $edges[$key]['flag'] = $edges[$key]['flag'] | $direction; + return; + } + + // Assign 'from' and 'to' according to the direction + switch ($direction) { + case self::DIRECTION_FORWARD: + $from = self::getNodeID($itemA); + $to = self::getNodeID($itemB); + break; + case self::DIRECTION_BACKWARD: + $from = self::getNodeID($itemB); + $to = self::getNodeID($itemA); + break; + default: + throw new InvalidArgumentException( + "Invalid value for argument \$direction ($direction)." + ); + } + + // Add the new edge + $edges[$key] = [ + 'id' => $key, + 'source' => $from, + 'target' => $to, + 'flag' => $direction + ]; + } + + /** + * Build the graph and the cytoscape object + * + * @since 9.5 + * + * @param array $nodes Nodes of the graph + * @param array $edges Edges of the graph + */ + public static function buildNetwork(CommonDBTM $item) { + // Build the graph + $graph = self::makeDataForCytoscape(Impact::buildGraph($item)); + $params = self::prepareParams($item); + + echo Html::scriptBlock(" + $(function() { + GLPIImpact.buildNetwork($graph, $params); + }); + "); + } + + /** + * Get saved graph params for the current item + * + * @param CommonDBTM $item + * + * @return string $item + */ + public static function prepareParams(CommonDBTM $item) { + $impact_item = ImpactItem::findForItem($item); + + return json_encode([ + 'zoom' => $impact_item->fields['zoom'], + 'pan_x' => $impact_item->fields['pan_x'], + 'pan_y' => $impact_item->fields['pan_y'], + 'impact_color' => $impact_item->fields['impact_color'], + 'depends_color' => $impact_item->fields['depends_color'], + 'impact_and_depends_color' => $impact_item->fields['impact_and_depends_color'], + 'show_depends' => $impact_item->fields['show_depends'], + 'show_impact' => $impact_item->fields['show_impact'], + 'max_depth' => $impact_item->fields['max_depth'], + ]); + } + + /** + * Convert the php array reprensenting the graph into the format required by + * the Cytoscape library + * + * @param array $graph + * + * @return string json data + */ + public static function makeDataForCytoscape(array $graph) { + $data = []; + + foreach ($graph['nodes'] as $node) { + $data[] = [ + 'group' => 'nodes', + 'data' => $node, + ]; + } + + foreach ($graph['edges'] as $edge) { + $data[] = [ + 'group' => 'edges', + 'data' => $edge, + ]; + } + + return json_encode($data); + } + + /** + * Load the add node dialog + * + * @since 9.5 + */ + public static function printAddNodeDialog() { + global $CFG_GLPI; + $rand = mt_rand(); + + echo '
'; + echo ''; + + // First row: itemtype field + echo ""; + echo ""; + echo ""; + echo ""; + + // Second row: items_id field + echo ""; + echo ""; + echo ""; + echo ""; + + echo "
"; + Dropdown::showItemTypes( + 'item_type', + array_keys($CFG_GLPI['impact_asset_types']), + [ + 'value' => null, + 'width' => '100%', + 'emptylabel' => Dropdown::EMPTY_VALUE, + 'rand' => $rand + ] + ); + echo "
"; + Ajax::updateItemOnSelectEvent("dropdown_item_type$rand", "results", + $CFG_GLPI["root_doc"]. + "/ajax/dropdownTrackingDeviceType.php", + [ + 'itemtype' => '__VALUE__', + 'entity_restrict' => 0, + 'multiple' => 1, + 'admin' => 1, + 'rand' => $rand, + 'myname' => "item_id", + 'context' => "impact" + ] + ); + echo "\n"; + echo "\n"; + echo "
"; + echo "
"; + } + + /** + * Load the "show ongoing tickets" dialog + * + * @since 9.5 + */ + public static function printShowOngoingDialog() { + // This dialog will be built dynamically on the front end + echo '
'; + } + + /** + * Load the color configuration dialog + * + * @since 9.5 + */ + public static function printColorConfigDialog() { + echo '
'; + echo ""; + + // First row: depends color field + echo ""; + echo ""; + echo ""; + + // Second row: impact color field + echo ""; + echo ""; + echo ""; + + // Third row: impact and depends color field + echo ""; + echo ""; + echo ""; + + echo "
"; + Html::showColorField("depends_color", []); + echo ""; + echo "
"; + Html::showColorField("impact_color", []); + echo ""; + echo "
"; + Html::showColorField("impact_and_depends_color", []); + echo ""; + echo "
"; + echo "
"; + } + + /** + * Load the "edit compound" dialog + * + * @since 9.5 + */ + public static function printEditCompoundDialog() { + echo '
'; + echo ""; + + // First row: name field + echo ""; + echo ""; + echo ""; + echo ""; + + // Second row: color field + echo ""; + echo ""; + echo ""; + echo ""; + + echo "
"; + echo ""; + echo ""; + echo Html::input("compound_name", []); + echo "
"; + echo ""; + echo ""; + Html::showColorField("compound_color", [ + 'value' => '#d2d2d2' + ]); + echo "
"; + echo "
"; + } + + /** + * Export the dialogs defined in the backend + * + * @return string + */ + public static function exportDialogs() { + return json_encode([ + [ + 'key' => 'addNode', + 'id' => "#add_node_dialog", + 'inputs' => [ + 'itemType' => "select[name=item_type]", + 'itemID' => "select[name=item_id]" + ] + ], + [ + 'key' => 'configColor', + 'id' => '#color_config_dialog', + 'inputs' => [ + 'dependsColor' => "input[name=depends_color]", + 'impactColor' => "input[name=impact_color]", + 'impactAndDependsColor' => "input[name=impact_and_depends_color]", + ] + ], + [ + 'key' => "ongoingDialog", + 'id' => "#ongoing_dialog" + ], + [ + 'key' => "editCompoundDialog", + 'id' => "#edit_compound_dialog", + 'inputs' => [ + 'name' => "input[name=compound_name]", + 'color' => "input[name=compound_color]", + ] + ] + ]); + } + + /** + * Export the toolbar defined in the backend + * + * @return string + */ + public static function exportToolbar() { + return json_encode([ + ['key' => 'helpText', 'id' => "#help_text"], + ['key' => 'tools', 'id' => "#impact_tools"], + ['key' => 'save', 'id' => "#save_impact"], + ['key' => 'addNode', 'id' => "#add_node"], + ['key' => 'addEdge', 'id' => "#add_edge"], + ['key' => 'addCompound', 'id' => "#add_compound"], + ['key' => 'deleteElement', 'id' => "#delete_element"], + ['key' => 'export', 'id' => "#export_graph"], + ['key' => 'expandToolbar', 'id' => "#expand_toolbar"], + ['key' => 'toggleImpact', 'id' => "#toggle_impact"], + ['key' => 'toggleDepends', 'id' => "#toggle_depends"], + ['key' => 'colorPicker', 'id' => "#color_picker"], + ['key' => 'maxDepth', 'id' => "#max_depth"], + ['key' => 'maxDepthView', 'id' => "#max_depth_view"], + ]); + } + + /** + * Prepare the impact network + * + * @since 9.5 + * + * @param CommonDBTM $item The specified item + */ + public static function prepareImpactNetwork(CommonDBTM $item) { + // Load requirements + self::printImpactNetworkContainer(); + self::printAddNodeDialog(); + self::printColorConfigDialog(); + self::printEditCompoundDialog(); + echo Html::script("js/impact.js"); + + // Load backend values + $locales = self::getLocales(); + $default = self::DEFAULT_COLOR; + $forward = self::IMPACT_COLOR; + $backward = self::DEPENDS_COLOR; + $both = self::IMPACT_AND_DEPENDS_COLOR; + $start_node = self::getNodeID($item); + $form = "form[name=form_impact_network]"; + $dialogs = self::exportDialogs(); + $toolbar = self::exportToolbar(); + + // Bind the backend values to the client and start the network + echo Html::scriptBlock(" + $(function() { + GLPIImpact.prepareNetwork( + $(\"#network_container\"), + '$locales', + { + default : '$default', + forward : '$forward', + backward: '$backward', + both : '$both', + }, + '$start_node', + '$form', + '$dialogs', + '$toolbar' + ) + }); + "); + } + + /** + * Check that a given asset exist in the DB + * + * @param string $itemtype Class of the asset + * @param string $items_id id of the asset + */ + public static function assetExist(string $itemtype, string $items_id) { + global $CFG_GLPI; + + try { + // Check this asset type is enabled + if (!isset($CFG_GLPI['impact_asset_types'][$itemtype])) { + return false; + } + + // Try to create an object matching the given item type + $reflection_class = new ReflectionClass($itemtype); + if (!$reflection_class->isInstantiable()) { + return false; + } + + // Look for a matching asset in the DB + $asset = new $itemtype(); + return $asset->getFromDB($items_id); + } catch (ReflectionException $e) { + // Class does not exist + return false; + } + } + + /** + * Create an ID for a node (itemtype::items_id) + * + * @param CommonDBTM $item Name of the node + * + * @return string + */ + public static function getNodeID(CommonDBTM $item) { + return get_class($item) . "::" . $item->fields['id']; + } + + /** + * Create an ID for an edge (NodeID->NodeID) + * + * @param CommonDBTM $itemA First node of the edge + * @param CommonDBTM $itemB Second node of the edge + * @param int $direction Direction of the edge : A to B or B to A ? + * + * @return string|null + * + * @throws InvalidArgumentException + */ + public static function getEdgeID( + CommonDBTM $itemA, + CommonDBTM $itemB, + int $direction + ) { + switch ($direction) { + case self::DIRECTION_FORWARD: + return self::getNodeID($itemA) . "->" . self::getNodeID($itemB); + + case self::DIRECTION_BACKWARD: + return self::getNodeID($itemB) . "->" . self::getNodeID($itemA); + + default: + throw new InvalidArgumentException( + "Invalid value for argument \$direction ($direction)." + ); + } + } + + /** + * Print the form for the global impact page + */ + public static function printImpactForm() { + global $CFG_GLPI; + $rand = mt_rand(); + + echo ""; + + echo ''; + + // First row: header + echo ""; + echo ""; + echo ""; + + // Second row: itemtype field + echo ""; + echo ""; + echo ""; + echo ""; + + // Third row: items_id field + echo ""; + echo ""; + echo ""; + echo ""; + + // Fourth row: submit + echo ""; + + echo "
" . __('Impact analysis') . "
"; + Dropdown::showItemTypes( + 'type', + array_keys($CFG_GLPI['impact_asset_types']), + [ + 'value' => null, + 'width' => '100%', + 'emptylabel' => Dropdown::EMPTY_VALUE, + 'rand' => $rand + ] + ); + echo "
"; + Ajax::updateItemOnSelectEvent("dropdown_type$rand", "form_results", + $CFG_GLPI["root_doc"] . "/ajax/dropdownTrackingDeviceType.php", + [ + 'itemtype' => '__VALUE__', + 'entity_restrict' => 0, + 'multiple' => 1, + 'admin' => 1, + 'rand' => $rand, + 'myname' => "id", + ] + ); + echo "\n"; + echo "\n"; + echo "
"; + echo Html::submit(__("Show impact analysis")); + echo "
"; + echo "

"; + Html::closeForm(); + } + + /** + * Build the locales that will be used in the client side + * + * @return string json encoded locales array + */ + public static function getLocales() { + $locales = [ + 'add' => __('Add'), + 'addDescription' => __('Click in an empty space to place a new asset.'), + 'addEdge' => __('Add Impact relation'), + 'addEdgeHelpText' => __("Draw a line between two assets to add an impact relation"), + 'addNode' => __('Add Asset'), + 'addNodeHelpText' => __("Click anywhere to add a new asset"), + 'addCompoundHelpText' => __("Draw a square containing the assets you wish to group"), + 'addCompoundTooltip' => __("Create a new group"), + 'addEdgeTooltip' => __("Add a new impact relation"), + 'addNodeTooltip' => __("Add a new asset to the impact network"), + 'back' => __('Back'), + 'cancel' => __('Cancel'), + 'changes' => __("Changes"), + 'colorConfiguration' => __("Colors"), + 'compoundProperties' => __("Group properties..."), + 'compoundProperties+' => __("Set name and/or color for this group"), + 'createEdgeError' => __('Cannot link edges to a cluster.'), + 'del' => __('Delete selected'), + 'delete' => __("Delete"), + 'delete+' => __("Delete element"), + 'deleteClusterError' => __('Clusters cannot be deleted.'), + 'deleteHelpText' => __("Click on an element to remove it from the network"), + 'deleteTooltip' => __("Delete an element from the impact network"), + 'downloadTooltip' => __("Export the impact network"), + 'duplicateAsset' => __('This asset already exists.'), + 'duplicateEdge' => __("An identical link already exist between theses two asset."), + 'edgeDescription' => __('Click on an asset and drag the link to another asset to connect them.'), + 'edit' => __('Edit'), + 'editEdge' => __('Edit Impact relation'), + 'editEdgeDescription' => __('Click on the control points and drag them to a asset to connect to it.'), + 'editGroup' => __("Edit group"), + 'editNode' => __('Edit Asset'), + 'editClusterError' => __('Clusters cannot be edited.'), + 'expandToolbarTooltip' => __("Show more options ..."), + 'export' => __("Export"), + 'goTo' => __("Go to"), + 'goTo+' => __("Open this element in a new tab"), + 'incidents' => __("Incidents"), + 'linkToSelf' => __("Can't link an asset to itself."), + 'new' => __("Add asset"), + 'new+' => __("Add a new asset to the graph"), + 'newAsset' => __("New asset"), + 'notEnoughItems' => __("You need to select at least 2 assets to make a group"), + 'ongoingTickets' => __("Ongoing tickets"), + 'problems' => __("Problems"), + 'removeFromCompound' => __("Remove from group"), + 'removeFromCompound+' => __("Remove this asset from the group"), + 'requests' => __("Requests"), + 'save' => __("Save"), + 'showDepends' => __("Depends"), + 'showImpact' => __("Impact"), + 'showColorsTooltip' => __("Edit relation's color"), + 'showDependsTooltip' => __("Toggle \"depends\" visibility"), + 'showImpactTooltip' => __("Toggle \"impacted\" visibility"), + 'showOngoing' => __("Show ongoing tickets"), + 'showOngoing+' => __("Show ongoing tickets for this item"), + 'unexpectedError' => __("Unexpected error."), + 'unsavedChanges' => __("You have unsaved changes"), + 'workspaceSaved' => __("No unsaved changes"), + ]; + + return addslashes(json_encode($locales)); + } +} diff --git a/inc/impactcompound.class.php b/inc/impactcompound.class.php new file mode 100644 index 00000000000..ebbd9ae1136 --- /dev/null +++ b/inc/impactcompound.class.php @@ -0,0 +1,12 @@ +request([ + 'SELECT' => [ + 'glpi_impactitems.id', + ], + 'FROM' => self::getTable(), + 'WHERE' => [ + 'glpi_impactitems.itemtype' => get_class($item), + 'glpi_impactitems.items_id' => $item->getID(), + ] + ]); + + $res = $it->next(); + + if (!$res) { + return false; + } + + $impactItem = new self(); + $impactItem->getFromDB($res['id']); + + return $impactItem; + } +} diff --git a/inc/impactrelation.class.php b/inc/impactrelation.class.php new file mode 100644 index 00000000000..271841ec126 --- /dev/null +++ b/inc/impactrelation.class.php @@ -0,0 +1,97 @@ +request([ + 'FROM' => self::getTable(), + 'WHERE' => [ + 'itemtype_source' => $input['itemtype_source'], + 'items_id_source' => $input['items_id_source'], + 'itemtype_impacted' => $input['itemtype_impacted'], + 'items_id_impacted' => $input['items_id_impacted'] + ] + ]); + if (count($it)) { + return false; + } + + // Check if source and impacted are valid objets + $source_exist = Impact::assetExist( + $input['itemtype_source'], + $input['items_id_source'] + ); + $impacted_exist = Impact::assetExist( + $input['itemtype_impacted'], + $input['items_id_impacted'] + ); + if (!$source_exist || !$impacted_exist) { + return false; + } + + return $input; + } + + /** + * Get an impact id from an input form + * + * @param array $input Array containing the impact to be deleted + * @param array $options + * @param bool $history + * + * @return bool false on failure + */ + public static function getIDFromInput(array $input) { + global $DB; + + // Check that the link exist + $it = $DB->request([ + 'FROM' => self::getTable(), + 'WHERE' => [ + 'itemtype_source' => $input['itemtype_source'], + 'items_id_source' => $input['items_id_source'], + 'itemtype_impacted' => $input['itemtype_impacted'], + 'items_id_impacted' => $input['items_id_impacted'] + ] + ]); + + if (count($it)) { + return $it->next()['id']; + } + + return false; + } +} diff --git a/inc/infocom.class.php b/inc/infocom.class.php index d424f0e0ffa..8fc2c2f3b74 100644 --- a/inc/infocom.class.php +++ b/inc/infocom.class.php @@ -270,7 +270,7 @@ function prepareInputForAdd($input) { * @param item CommonDBTM object: the item whose status have changed * @param action_add true if object is added, false if updated (true by default) * - * @return nothing + * @return void **/ static function manageDateOnStatusChange(CommonDBTM $item, $action_add = true) { global $CFG_GLPI; @@ -319,7 +319,7 @@ static function manageDateOnStatusChange(CommonDBTM $item, $action_add = true) { * @param action the action to peform (copy from another date) (default 0) * @param params array of additional parameters needed to perform the task * - * @return nothing + * @return void **/ static function autofillDates(&$infocoms = [], $field = '', $action = 0, $params = []) { diff --git a/inc/ipaddress.class.php b/inc/ipaddress.class.php index f366e2c9919..898e0581c0d 100644 --- a/inc/ipaddress.class.php +++ b/inc/ipaddress.class.php @@ -535,7 +535,7 @@ static function isIPv4MappedToIPv6Address($address) { /** * Replace textual representation by its canonical form. * - * @return nothing (internal class update) + * @return void **/ function canonicalizeTextual() { $this->setAddressFromBinary($this->getBinary()); diff --git a/inc/ipnetwork.class.php b/inc/ipnetwork.class.php index 4a7a06b228f..4edcaf636e9 100644 --- a/inc/ipnetwork.class.php +++ b/inc/ipnetwork.class.php @@ -878,7 +878,7 @@ static function computeNetworkRangeFromAdressAndNetmask($address, $netmask, &$fi * First, reset the tree, then, update each network by its own field, letting * CommonImplicitTreeDropdown working such as it would in case of standard update * - * @return nothing + * @return void **/ static function recreateTree() { global $DB; diff --git a/inc/item_disk.class.php b/inc/item_disk.class.php index 733e32454e8..02ea4e3ac94 100644 --- a/inc/item_disk.class.php +++ b/inc/item_disk.class.php @@ -262,7 +262,7 @@ function showForm($ID, $options = []) { * @param $item Item object * @param $withtemplate boolean Template or basic item (default 0) * - * @return Nothing (call to classes members) + * @return void **/ static function showForItem(CommonDBTM $item, $withtemplate = 0) { global $DB; diff --git a/inc/item_operatingsystem.class.php b/inc/item_operatingsystem.class.php index ff312b7f67c..4f2d6c69fbb 100644 --- a/inc/item_operatingsystem.class.php +++ b/inc/item_operatingsystem.class.php @@ -72,7 +72,7 @@ static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtem * * @since 9.2 * - * @return Nothing (call to classes members) + * @return void **/ static function showForItem(CommonDBTM $item, $withtemplate = 0) { global $DB, $CFG_GLPI; diff --git a/inc/item_problem.class.php b/inc/item_problem.class.php index 61971a48e57..6136364b4b2 100644 --- a/inc/item_problem.class.php +++ b/inc/item_problem.class.php @@ -83,7 +83,7 @@ function prepareInputForAdd($input) { * * @param $problem Problem object * - * @return Nothing (display) + * @return void **/ static function showForProblem(Problem $problem) { global $DB, $CFG_GLPI; diff --git a/inc/item_project.class.php b/inc/item_project.class.php index 9698753b0d7..4c05ead8632 100644 --- a/inc/item_project.class.php +++ b/inc/item_project.class.php @@ -83,7 +83,7 @@ function prepareInputForAdd($input) { * * @param $project Project object * - * @return Nothing (display) + * @return void **/ static function showForProject(Project $project) { global $DB, $CFG_GLPI; diff --git a/inc/item_ticket.class.php b/inc/item_ticket.class.php index 8674fa1e98f..55c51ef975b 100644 --- a/inc/item_ticket.class.php +++ b/inc/item_ticket.class.php @@ -170,7 +170,7 @@ function prepareInputForAdd($input) { * - _users_id_requester : ID of the requester user * - items_id : array of elements (itemtype => array(id1, id2, id3, ...)) * - * @return Nothing (display) + * @return void **/ static function itemAddForm(Ticket $ticket, $options = []) { global $CFG_GLPI; @@ -370,7 +370,7 @@ static function showItemToAdd($tickets_id, $itemtype, $items_id, $options) { * * @param $ticket Ticket object * - * @return Nothing (display) + * @return void **/ static function showForTicket(Ticket $ticket) { global $DB, $CFG_GLPI; @@ -561,7 +561,7 @@ static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtem * - multiple : allow multiple choice * - rand : random number * - * @return nothing (print out an HTML select box) + * @return integer random part of elements id **/ static function dropdownAllDevices($myname, $itemtype, $items_id = 0, $admin = 0, $users_id = 0, $entity_restrict = -1, $options = []) { @@ -656,7 +656,7 @@ static function dropdownAllDevices($myname, $itemtype, $items_id = 0, $admin = 0 * - used : ID of the requester user * - multiple : allow multiple choice * - * @return nothing (print out an HTML select box) + * @return void **/ static function dropdownMyDevices($userID = 0, $entity_restrict = -1, $itemtype = 0, $items_id = 0, $options = []) { global $DB, $CFG_GLPI; diff --git a/inc/itilfollowuptemplate.class.php b/inc/itilfollowuptemplate.class.php index 48e79ea7e7c..687c7a34e4f 100644 --- a/inc/itilfollowuptemplate.class.php +++ b/inc/itilfollowuptemplate.class.php @@ -100,16 +100,4 @@ function rawSearchOptions() { return $tab; } - - function displaySpecificTypeField($ID, $field = []) { - switch ($field['type']) { - case 'tinymce' : - Html::textarea([ - 'name' => 'content', - 'value' => $this->fields["content"], - 'enable_richtext' => true, - ]); - break; - } - } } diff --git a/inc/itiltemplate.class.php b/inc/itiltemplate.class.php index ff00d1bb5a0..c9cb1d56e99 100644 --- a/inc/itiltemplate.class.php +++ b/inc/itiltemplate.class.php @@ -551,7 +551,7 @@ function isMandatoryField($field) { * * @param $tt ITILTemplate object * - * @return Nothing (call to classes members) + * @return void **/ static function showCentralPreview(ITILTemplate $tt) { diff --git a/inc/itiltemplatehiddenfield.class.php b/inc/itiltemplatehiddenfield.class.php index 94533501f2f..31e4668e287 100644 --- a/inc/itiltemplatehiddenfield.class.php +++ b/inc/itiltemplatehiddenfield.class.php @@ -149,7 +149,7 @@ static function getExcludedFields() { * @param $tt ITIL Template * @param $withtemplate boolean Template or basic item (default 0) * - * @return Nothing (call to classes members) + * @return void **/ static function showForITILTemplate(ITILTemplate $tt, $withtemplate = 0) { global $DB; diff --git a/inc/itiltemplatemandatoryfield.class.php b/inc/itiltemplatemandatoryfield.class.php index 69b789331cf..2061145245a 100644 --- a/inc/itiltemplatemandatoryfield.class.php +++ b/inc/itiltemplatemandatoryfield.class.php @@ -148,7 +148,7 @@ static function getExcludedFields() { * @param ITILTemplate $tt ITIL Template * @param boolean $withtemplate Template or basic item (default 0) * - * @return Nothing (call to classes members) + * @return void **/ static function showForITILTemplate(ITILTemplate $tt, $withtemplate = 0) { global $DB; diff --git a/inc/itiltemplatepredefinedfield.class.php b/inc/itiltemplatepredefinedfield.class.php index 80f161a8cdd..7ef00ee59e3 100644 --- a/inc/itiltemplatepredefinedfield.class.php +++ b/inc/itiltemplatepredefinedfield.class.php @@ -223,7 +223,7 @@ static function getExcludedFields() { * @param $tt ITIL Template * @param $withtemplate boolean Template or basic item (default 0) * - * @return Nothing (call to classes members) + * @return void **/ static function showForITILTemplate(ITILTemplate $tt, $withtemplate = 0) { global $DB, $CFG_GLPI; diff --git a/inc/knowbaseitem.class.php b/inc/knowbaseitem.class.php index 4a8eee49948..76f5a9f7156 100644 --- a/inc/knowbaseitem.class.php +++ b/inc/knowbaseitem.class.php @@ -230,7 +230,7 @@ static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtem /** * Actions done at the end of the getEmpty function * - *@return nothing + *@return void **/ function post_getEmpty() { @@ -647,7 +647,7 @@ function prepareInputForUpdate($input) { * @param $options array * - target for the Form * - * @return nothing (display the form) + * @return void **/ function showForm($ID, $options = []) { global $CFG_GLPI; @@ -832,7 +832,7 @@ function showForm($ID, $options = []) { /** * Add kb item to the public FAQ * - * @return nothing + * @return void **/ function addToFaq() { global $DB; @@ -870,7 +870,9 @@ function updateCounter() { * * @param $options array of options * - * @return nothing (display item : question and answer) + * @return void|string + * void if option display=true + * string if option display=false (HTML code) **/ function showFull($options = []) { global $DB, $CFG_GLPI; @@ -977,7 +979,7 @@ function showFull($options = []) { * * @param $options $_GET * - * @return nothing (display the form) + * @return void **/ function searchForm($options) { global $CFG_GLPI; @@ -1024,7 +1026,7 @@ function searchForm($options) { * * @param $options $_GET * - * @return nothing (display the form) + * @return void **/ function showBrowseForm($options) { global $CFG_GLPI; @@ -1073,7 +1075,7 @@ function showBrowseForm($options) { * * @param $options $_GET * - * @return nothing (display the form) + * @return void **/ function showManageForm($options) { global $CFG_GLPI; @@ -1606,7 +1608,7 @@ static function showList($options, $type = 'search') { * * @param $type type : recent / popular / not published * - * @return nothing (display table) + * @return void **/ static function showRecentPopular($type) { global $DB, $CFG_GLPI; diff --git a/inc/knowbaseitemtranslation.class.php b/inc/knowbaseitemtranslation.class.php index 48d161fa208..bef4468dfa4 100644 --- a/inc/knowbaseitemtranslation.class.php +++ b/inc/knowbaseitemtranslation.class.php @@ -138,7 +138,7 @@ static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtem * * @param $options array of options * - * @return nothing (display item : question and answer) + * @return void **/ function showFull($options = []) { global $DB, $CFG_GLPI; diff --git a/inc/levelagreementlevel.class.php b/inc/levelagreementlevel.class.php index 5ce7deef757..6246782491d 100644 --- a/inc/levelagreementlevel.class.php +++ b/inc/levelagreementlevel.class.php @@ -298,7 +298,9 @@ static function getExecutionTimes($options = []) { * - max_time : max time to use * - used : already used values * - * @return nothing + * @return integer|string + * integer if option display=true (random part of elements id) + * string if option display=false (HTML code) **/ static function dropdownExecutionTime($name, $options = []) { $p['value'] = ''; diff --git a/inc/line.class.php b/inc/line.class.php index 3417db85797..234cc1cc0a0 100644 --- a/inc/line.class.php +++ b/inc/line.class.php @@ -85,7 +85,7 @@ function defineTabs($options = []) { * - target for the Form * - withtemplate : template or basic item * - * @return Nothing (display) + * @return void **/ function showForm($ID, $options = []) { diff --git a/inc/link.class.php b/inc/link.class.php index a5ae84bbb5f..32dbd6480c8 100644 --- a/inc/link.class.php +++ b/inc/link.class.php @@ -131,7 +131,7 @@ function getEmpty() { * @param $options array * - target filename : where to go when done. * - * @return Nothing (display) + * @return void **/ function showForm($ID, $options = []) { diff --git a/inc/link_itemtype.class.php b/inc/link_itemtype.class.php index 9c9e47ef948..dc730b2412e 100644 --- a/inc/link_itemtype.class.php +++ b/inc/link_itemtype.class.php @@ -56,7 +56,7 @@ function getForbiddenStandardMassiveAction() { * * @param $link : Link * - * @return Nothing (display) + * @return void **/ static function showForLink($link) { global $DB,$CFG_GLPI; diff --git a/inc/location.class.php b/inc/location.class.php index 16179c06813..39554a4c340 100644 --- a/inc/location.class.php +++ b/inc/location.class.php @@ -411,7 +411,7 @@ static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtem * * @since 0.85 * - * @return Nothing (display) + * @return void **/ function showItems() { global $DB, $CFG_GLPI; diff --git a/inc/massiveaction.class.php b/inc/massiveaction.class.php index 78b42790119..64c1328b44d 100644 --- a/inc/massiveaction.class.php +++ b/inc/massiveaction.class.php @@ -64,8 +64,6 @@ class MassiveAction { * @param $GET something like $_GET * @param $stage the current stage * @param boolean $single Get actions for a single item - * - * @return nothing (it is a constructor). **/ function __construct (array $POST, array $GET, $stage, $single = false) { global $CFG_GLPI; @@ -405,7 +403,7 @@ function getCheckItem($POST) { /** * Add hidden fields containing all the checked items to the current form * - * @return nothing (display) + * @return void **/ function addHiddenFields() { @@ -643,7 +641,7 @@ static function getAllMassiveActions($item, $is_deleted = 0, CommonDBTM $checkit /** * Main entry of the modal window for massive actions * - * @return nothing: display + * @return void **/ function showSubForm() { $processor = $this->processor; @@ -659,7 +657,7 @@ function showSubForm() { /** * Class-specific method used to show the fields to specify the massive action * - * @return nothing (display only) + * @return void **/ function showDefaultSubForm() { echo Html::submit(_x('button', 'Post'), ['name' => 'massiveaction']); @@ -947,7 +945,7 @@ static function showMassiveActionsSubForm(MassiveAction $ma) { * * Display and update the progress bar. If the delay is more than 1 second, then activate it * - * @return nothing (display only) + * @return void **/ function updateProgressBars() { @@ -1268,7 +1266,7 @@ static function processMassiveActionsForOneItemtype(MassiveAction $ma, CommonDBT * * @param $redirect link to the page * - * @return nothing + * @return void **/ function setRedirect($redirect) { $this->redirect = $redirect; @@ -1280,7 +1278,7 @@ function setRedirect($redirect) { * * @param $message the message to add * - * @return nothing + * @return void **/ function addMessage($message) { $this->results['messages'][] = $message; diff --git a/inc/monitor.class.php b/inc/monitor.class.php index 40fc9efacd4..69c078bbea8 100644 --- a/inc/monitor.class.php +++ b/inc/monitor.class.php @@ -75,6 +75,7 @@ function defineTabs($options = []) { $ong = []; $this->addDefaultFormTab($ong); + $this->addImpactTab($ong, $options); $this->addStandardTab('Item_OperatingSystem', $ong, $options); $this->addStandardTab('Item_Devices', $ong, $options); $this->addStandardTab('Computer_Item', $ong, $options); diff --git a/inc/netpoint.class.php b/inc/netpoint.class.php index b1ed3d68db9..632352771b3 100644 --- a/inc/netpoint.class.php +++ b/inc/netpoint.class.php @@ -106,7 +106,7 @@ function executeAddMulti(array $input) { * @param $entity_restrict Restrict to a defined entity(default -1) * @param $devtype (default '') * - * @return nothing (display the select box) + * @return integer random part of elements id **/ static function dropdownNetpoint($myname, $value = 0, $locations_id = -1, $display_comment = 1, $entity_restrict = -1, $devtype = '') { @@ -245,7 +245,7 @@ static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtem * * @param $item Location * - * @return Nothing (display) + * @return void **/ static function showForLocation($item) { global $DB, $CFG_GLPI; diff --git a/inc/networkalias.class.php b/inc/networkalias.class.php index 16fcd754447..1f9e4b8278c 100644 --- a/inc/networkalias.class.php +++ b/inc/networkalias.class.php @@ -91,7 +91,7 @@ static function getInternetNameFromID($ID) { * - target for the Form * - withtemplate template or basic computer * - * @return Nothing (display) + * @return void **/ function showForm ($ID, $options = []) { diff --git a/inc/networkequipment.class.php b/inc/networkequipment.class.php index 881de5bf62d..c28c8ba6a3e 100644 --- a/inc/networkequipment.class.php +++ b/inc/networkequipment.class.php @@ -122,6 +122,7 @@ function defineTabs($options = []) { $ong = []; $this->addDefaultFormTab($ong) + ->addImpactTab($ong, $options) ->addStandardTab('Item_OperatingSystem', $ong, $options) ->addStandardTab('Item_Devices', $ong, $options) ->addStandardTab('Item_Disk', $ong, $options) diff --git a/inc/networkname.class.php b/inc/networkname.class.php index c481cd24bc4..1e9fc53f57a 100644 --- a/inc/networkname.class.php +++ b/inc/networkname.class.php @@ -87,7 +87,7 @@ function defineTabs($options = []) { * - target for the Form * - withtemplate template or basic computer * - *@return Nothing (display) + *@return void **/ function showForm($ID, $options = []) { global $CFG_GLPI; diff --git a/inc/networkportinstantiation.class.php b/inc/networkportinstantiation.class.php index 26061ede9c3..e5127728ca9 100644 --- a/inc/networkportinstantiation.class.php +++ b/inc/networkportinstantiation.class.php @@ -840,7 +840,7 @@ static function showConnection($netport, $edit = false) { * - entity_sons : boolean / if entity restrict specified auto select its sons * only available if entity is a single value not an array (default false) * - * @return nothing (print out an HTML select box) + * @return integer random part of elements id **/ static function dropdownConnect($ID, $options = []) { global $CFG_GLPI; diff --git a/inc/notification_notificationtemplate.class.php b/inc/notification_notificationtemplate.class.php index 62afc100d4c..8b577ff8822 100644 --- a/inc/notification_notificationtemplate.class.php +++ b/inc/notification_notificationtemplate.class.php @@ -109,7 +109,7 @@ static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtem * @param Notification $notif Notification object * @param boolean $withtemplate Template or basic item (default '') * - * @return Nothing (call to classes members) + * @return void **/ static function showForNotification(Notification $notif, $withtemplate = 0) { global $DB; diff --git a/inc/notificationtarget.class.php b/inc/notificationtarget.class.php index 8618eebddb8..84090ab4a06 100644 --- a/inc/notificationtarget.class.php +++ b/inc/notificationtarget.class.php @@ -1339,7 +1339,7 @@ static function countForGroup(Group $group) { * * @param $group Group object * - * @return nothing + * @return void **/ static function showForGroup(Group $group) { global $DB; diff --git a/inc/notificationtargetprojecttask.class.php b/inc/notificationtargetprojecttask.class.php index c360b7ed3ff..d5eac8a12a6 100644 --- a/inc/notificationtargetprojecttask.class.php +++ b/inc/notificationtargetprojecttask.class.php @@ -443,9 +443,6 @@ function addDataForTemplate($event, $options = []) { $this->data["##projecttask.numberofdocuments##"] = count($this->data['documents']); - // Items infos - $items = getAllDataFromTable('glpi_items_projects', $restrict); - $this->getTags(); foreach ($this->tag_descriptions[NotificationTarget::TAG_LANGUAGE] as $tag => $values) { if (!isset($this->data[$tag])) { diff --git a/inc/notificationtargetticket.class.php b/inc/notificationtargetticket.class.php index 186577ac934..3550ed2e448 100644 --- a/inc/notificationtargetticket.class.php +++ b/inc/notificationtargetticket.class.php @@ -457,9 +457,9 @@ function getDataForObject(CommonDBTM $item, array $options, $simple = false) { ] ); $current = current($replysolved); - $data['##ticket.solution.approval.description##'] = $current['content']; - $data['##ticket.solution.approval.date##'] = Html::convDateTime($current['date']); - $data['##ticket.solution.approval.author##'] = Html::clean(getUserName($current['users_id'])); + $data['##ticket.solution.approval.description##'] = $current ? $current['content'] : ''; + $data['##ticket.solution.approval.date##'] = $current ? Html::convDateTime($current['date']) : ''; + $data['##ticket.solution.approval.author##'] = $current ? Html::clean(getUserName($current['users_id'])) : ''; //Validation infos $restrict = ['tickets_id' => $item->getField('id')]; diff --git a/inc/olalevel.class.php b/inc/olalevel.class.php index b4ac85601af..93668fc9c88 100644 --- a/inc/olalevel.class.php +++ b/inc/olalevel.class.php @@ -215,7 +215,7 @@ function getActions() { * @param $ID ID of the rule * @param $options array of possible options * - * @return nothing + * @return void **/ function showForm($ID, $options = []) { diff --git a/inc/olalevel_ticket.class.php b/inc/olalevel_ticket.class.php index f1c3220e9e9..bc992f95a28 100644 --- a/inc/olalevel_ticket.class.php +++ b/inc/olalevel_ticket.class.php @@ -98,7 +98,7 @@ function getFromDBForTicket($ID, $olaType) { * * @since 9.1 2 parameters mandatory * - * @return nothing + * @return void **/ function deleteForTicket($tickets_id, $olaType) { global $DB; @@ -204,7 +204,7 @@ static function cronOlaTicket(CronTask $task) { * * @since 9.1 2 parameters mandatory * - * @return nothing + * @return void **/ static function doLevelForTicket(array $data, $olaType) { diff --git a/inc/peripheral.class.php b/inc/peripheral.class.php index 8a266c4d83c..54c3177af75 100644 --- a/inc/peripheral.class.php +++ b/inc/peripheral.class.php @@ -75,6 +75,7 @@ function defineTabs($options = []) { $ong = []; $this->addDefaultFormTab($ong); + $this->addImpactTab($ong, $options); $this->addStandardTab('Item_OperatingSystem', $ong, $options); $this->addStandardTab('Item_Devices', $ong, $options); $this->addStandardTab('Computer_Item', $ong, $options); diff --git a/inc/phone.class.php b/inc/phone.class.php index 4db224adaab..613e0d7b320 100644 --- a/inc/phone.class.php +++ b/inc/phone.class.php @@ -71,6 +71,7 @@ function defineTabs($options = []) { $ong = []; $this->addDefaultFormTab($ong); + $this->addImpactTab($ong, $options); $this->addStandardTab('Item_OperatingSystem', $ong, $options); $this->addStandardTab('Item_SoftwareVersion', $ong, $options); $this->addStandardTab('Item_Devices', $ong, $options); diff --git a/inc/planning.class.php b/inc/planning.class.php index 9561e47cf41..99958a2a3f3 100755 --- a/inc/planning.class.php +++ b/inc/planning.class.php @@ -35,6 +35,7 @@ } use Sabre\VObject; +use RRule\RRule; /** * Planning Class @@ -74,23 +75,68 @@ static function getTypeName($nb = 0) { return __('Planning'); } - /** - * @see CommonGLPI::getMenuContent() - * - * @since 9.1 - **/ - static function getMenuContent() { - global $CFG_GLPI; + static function getMenuContent() { $menu = []; - if (static::canView()) { - $menu['title'] = self::getTypeName(); - $menu['page'] = '/front/planning.php'; + + if (Planning::canView()) { + $menu = [ + 'title' => static::getMenuName(), + 'shortcut' => static::getMenuShorcut(), + 'page' => static::getSearchURL(false), + ]; + + if ($data = static::getAdditionalMenuLinks()) { + $menu['links'] = $data; + } + + if ($options = static::getAdditionalMenuOptions()) { + $menu['options'] = $options; + } + } + + return $menu; + } + + + static function getAdditionalMenuLinks() { + $links = []; + + if (Planning::canView()) { + $title = Planning::getTypeName(Session::getPluralNumber()); + $planning = " + $title + "; + + $links[$planning] = Planning::getSearchURL(false); } - if (count($menu)) { - return $menu; + + if (PlanningExternalEvent::canView()) { + $ext_title = PlanningExternalEvent::getTypeName(Session::getPluralNumber()); + $external = " + $ext_title + "; + + $links[$external] = PlanningExternalEvent::getSearchURL(false); + } + + return $links; + } + + + static function getAdditionalMenuOptions() { + if (PlanningExternalEvent::canView()) { + return [ + 'external' => [ + 'title' => PlanningExternalEvent::getTypeName(Session::getPluralNumber()), + 'page' => PlanningExternalEvent::getSearchURL(false), + 'links' => [ + 'add' => '/front/planningexternalevent.form.php', + 'search' => '/front/planningexternalevent.php', + ] + static::getAdditionalMenuLinks() + ] + ]; } - return false; } @@ -263,7 +309,7 @@ static function checkAlreadyPlanned($users_id, $begin, $end, $except = []) { * optional : * - limitto : limit display to a specific user * - * @return Nothing (display function) + * @return void **/ static function checkAvailability($params = []) { global $CFG_GLPI, $DB; @@ -524,7 +570,7 @@ static function checkAvailability($params = []) { * Function name change since version 0.84 show() => showPlanning * Function prototype changes in 9.1 (no more parameters) * - * @return Nothing (display function) + * @return void **/ static function showPlanning($fullview = true) { if (!static::canView()) { @@ -533,15 +579,27 @@ static function showPlanning($fullview = true) { self::initSessionForCurrentUser(); + // scheduler feature key + // schedular part of fullcalendar is distributed with opensource licence (GLPv3) + // but this licence is incompatible with GLPI (GPLv2) + // see https://fullcalendar.io/license + $scheduler_key = Plugin::doHookFunction('planning_scheduler_key'); + echo ""; + + // define options for current page $rand = ''; if ($fullview) { + // full planning view (Assistance > Planning) Planning::showPlanningFilter(); $options = [ 'full_view' => true, - 'default_view' => 'timeGridWeek', + 'default_view' => $_SESSION['glpi_plannings']['lastview'] ?? 'timeGridWeek', + 'license_key' => $scheduler_key, + 'resources' => self::getTimelineResources() ]; } else { + // short view (on Central page) $rand = rand(); $options = [ 'full_view' => false, @@ -552,16 +610,69 @@ static function showPlanning($fullview = true) { ]; } + // display planning (and call js from js/planning.js) echo "
"; echo ""; echo Html::scriptBlock("$(function() { GLPIPlanning.display(".json_encode($options)."); + GLPIPlanning.planningFilters(); });"); return; } + static function getTimelineResources() { + $resources = []; + foreach ($_SESSION['glpi_plannings']['plannings'] as $planning_id => $planning) { + $exploded = explode('_', $planning_id); + if ($planning['type'] == 'group_users') { + $group_exploded = explode('_', $planning_id); + $group_id = (int) $group_exploded[1]; + $group = new Group; + $group->getFromDB($group_id); + $resources[] = [ + 'id' => $planning_id, + 'title' => $group->getName(), + 'eventAllow' => false, + 'is_visible' => $planning['display'], + 'itemtype' => 'Group_User', + 'items_id' => $group_id + ]; + foreach (array_keys($planning['users']) as $planning_id_user) { + $child_exploded = explode('_', $planning_id_user); + $user = new User; + $users_id = (int) $child_exploded[1]; + $user->getFromDB($users_id); + $resources[] = [ + 'id' => $planning_id_user, + 'title' => $user->getName(), + 'is_visible' => $planning['display'], + 'itemtype' => 'User', + 'items_id' => $users_id, + 'parentId' => $planning_id, + ]; + } + } else { + $itemtype = $exploded[0]; + $object = new $itemtype; + $users_id = (int) $exploded[1]; + $object->getFromDB($users_id); + + $resources[] = [ + 'id' => $planning_id, + 'title' => $object->getName(), + 'group_id' => false, + 'is_visible' => $planning['display'], + 'itemtype' => $itemtype, + 'items_id' => $users_id + ]; + } + } + + return $resources; + } + /** * Return a palette array (for example self::$palette_bg) * @param string $palette_name the short name for palette (bg, fg, ev) @@ -608,7 +719,7 @@ static function getPaletteColor($palette_name = 'bg', $color_index = 0) { * * Also manage color index in $_SESSION['glpi_plannings_color_index'] * - * @return Nothing (display function) + * @return void */ static function initSessionForCurrentUser() { global $CFG_GLPI; @@ -637,7 +748,7 @@ static function initSessionForCurrentUser() { } } - // computer color index for plannings + // compute color index for plannings $_SESSION['glpi_plannings_color_index'] = 0; foreach ($_SESSION['glpi_plannings']['plannings'] as $planning) { if ($planning['type'] == 'user') { @@ -655,7 +766,7 @@ static function initSessionForCurrentUser() { * and color choosing. * Call self::showSingleLinePlanningFilter for each filters and plannings * - * @return Nothing (display function) + * @return void */ static function showPlanningFilter() { global $CFG_GLPI; @@ -671,6 +782,10 @@ static function showPlanningFilter() { echo "
"; foreach ($_SESSION['glpi_plannings'] as $filter_heading => $filters) { + if (!in_array($filter_heading, array_keys($headings))) { + continue; + } + echo "
"; echo "

"; echo $headings[$filter_heading]; @@ -692,140 +807,6 @@ static function showPlanningFilter() { } echo "

"; echo "
"; - - $ajax_url = $CFG_GLPI['root_doc']."/ajax/planning.php"; - $JS = <<').dialog({ - modal: true, - open: function () { - $(this).load(url); - }, - position: { - my: 'top', - at: 'center', - of: $('#planning_filter') - } - }); - }); - - $('#planning_filter .filter_option').on( 'click', function( e ) { - $(this).children('ul').toggle(); - }); - - $(document).click(function(e){ - if ($(e.target).closest('#planning_filter .filter_option').length === 0) { - $('#planning_filter .filter_option ul').hide(); - } - }); - - $('#planning_filter .delete_planning').on( 'click', function( e ) { - var deleted = $(this); - var li = deleted.closest('ul.filters > li'); - $.ajax({ - url: '{$ajax_url}', - type: 'POST', - data: { - action: 'delete_filter', - filter: deleted.attr('value'), - type: li.attr('event_type') - }, - success: function(html) { - li.remove(); - calendar.refetchEvents(); - } - }); - }); - - var sendDisplayEvent = function(current_checkbox, refresh_planning) { - var current_li = current_checkbox.parents('li'); - var parent_name = null; - if (current_li.parent('ul.group_listofusers').length == 1) { - parent_name = current_li - .parent('ul.group_listofusers') - .parent('li') - .attr('event_name'); - } - $.ajax({ - url: '{$ajax_url}', - type: 'POST', - data: { - action: 'toggle_filter', - name: current_li.attr('event_name'), - type: current_li.attr('event_type'), - parent: parent_name, - display: current_checkbox.is(':checked') - }, - success: function(html) { - if (refresh_planning) { - // don't refresh planning if event triggered from parent checkbox - calendar.refetchEvents(); - } - } - }); - } - - $('#planning_filter li:not(li.group_users) input[type="checkbox"]') - .on( 'click', function( e ) { - sendDisplayEvent($(this), true); - } - ); - - $('#planning_filter li.group_users > span > input[type="checkbox"]') - .on('change', function( e ) { - var parent_checkbox = $(this); - var chidren_checkboxes = parent_checkbox - .parents('li.group_users') - .find('ul.group_listofusers input[type="checkbox"]'); - chidren_checkboxes.prop('checked', parent_checkbox.prop('checked')); - chidren_checkboxes.each(function(index) { - sendDisplayEvent($(this), false); - }); - - // refresh planning once for all checkboxes (and not for each) - calendar.refetchEvents(); - } - ); - - $('#planning_filter .color_input input').on('change', function(e, color) { - var current_li = $(this).parents('li'); - var parent_name = null; - if (current_li.length >= 1) { - parent_name = current_li.eq(1).attr('event_name'); - current_li = current_li.eq(0) - } - $.ajax({ - url: '{$ajax_url}', - type: 'POST', - data: { - action: 'color_filter', - name: current_li.attr('event_name'), - type: current_li.attr('event_type'), - parent: parent_name, - color: color.toHexString() - }, - success: function(html) { - calendar.refetchEvents(); - } - }); - }); - - $('#planning_filter li.group_users .toggle').on('click', function(e) { - $(this).parent().toggleClass('expanded'); - }); - - $('#planning_filter_toggle > a.toggle').on('click', function(e) { - $('#planning_filter_content').animate({ width:'toggle' }, 300, 'swing', function() { - $('#planning_filter').toggleClass('folded'); - $('#planning_container').toggleClass('folded'); - }); - }); - }); -JAVASCRIPT; - echo Html::scriptBlock($JS); } @@ -839,7 +820,7 @@ static function showPlanningFilter() { * * 'filter_color_index' (integer): index of the color to use in self::$palette_bg * @param $options * - * @return Nothing (display function) + * @return void */ static function showSingleLinePlanningFilter($filter_key, $filter_data, $options = []) { global $CFG_GLPI; @@ -887,8 +868,12 @@ class='".$filter_data['type']."'>"; 'checked' => $filter_data['display']]); if ($filter_data['type'] != 'event_filter') { - $icon_type = explode('_', $filter_data['type']); - echo ""; + $exploded = explode('_', $filter_data['type']); + $icon = "user"; + if ($exploded[0] === 'group') { + $icon = "users"; + } + echo ""; } echo ""; @@ -965,7 +950,7 @@ class='".$filter_data['type']."'>"; /** * Display ajax form to add actor on planning * - * @return Nothing (display function) + * @return void */ static function showAddPlanningForm() { global $CFG_GLPI; @@ -1003,7 +988,7 @@ static function showAddPlanningForm() { * Display 'User' part of self::showAddPlanningForm spcified by planning type dropdown. * Actually called by ajax/planning.php * - * @return Nothing (display function) + * @return void */ static function showAddUserForm() { global $CFG_GLPI; @@ -1059,7 +1044,7 @@ static function sendAddUserForm($params = []) { * Display 'All users of a group' part of self::showAddPlanningForm spcified by planning type dropdown. * Actually called by ajax/planning.php * - * @return Nothing (display function) + * @return void */ static function showAddGroupUsersForm() { echo __("Group")." :
"; @@ -1145,7 +1130,7 @@ static function editEventForm($params = []) { $item = getItemForItemtype($params['itemtype']); $item->showForm(intval($params['id']), $options); $callback = "$('.ui-dialog-content').dialog('close'); - window.calendar.refetchEvents(); + GLPIPlanning.refresh(); displayAjaxMessageAfterRedirect();"; Html::ajaxForm("#edit_event_form$rand", $callback); } @@ -1158,7 +1143,7 @@ static function editEventForm($params = []) { * * @since 9.1 * - * @return Nothing (display function) + * @return void */ static function showAddGroupForm($params = []) { @@ -1244,7 +1229,7 @@ static function showAddEventForm($params = []) { * - end : end of selection range. * (should be an ISO_8601 date, but could be anything wo can be parsed by strtotime) * - * @return Nothing (display function) + * @return void */ static function showAddEventSubForm($params = []) { @@ -1257,7 +1242,7 @@ static function showAddEventSubForm($params = []) { 'end' => $params['end'], 'formoptions' => "id='ajax_reminder$rand'"]); $callback = "$('.ui-dialog-content').dialog('close'); - window.calendar.refetchEvents(); + GLPIPlanning.refresh(); displayAjaxMessageAfterRedirect();"; Html::ajaxForm("#ajax_reminder$rand", $callback); } @@ -1274,8 +1259,10 @@ static function showAddEventSubForm($params = []) { * - id (integer): id of item who receive the planification * - itemtype (string): itemtype of item who receive the planification * - begin (string) : start date of event + * - _display_dates (bool) : display dates fields (default true) * - end (optionnal) (string) : end date of event. Ifg missing, it will computerd from begin+1hour * - rand_user (integer) : users_id to check planning avaibility + * - rand : specific rand if needed (default is generated one) */ static function showAddEventClassicForm($params = []) { global $CFG_GLPI; @@ -1284,6 +1271,13 @@ static function showAddEventClassicForm($params = []) { echo ""; } + $rand = mt_rand(); + if (isset($params['rand'])) { + $rand = $params['rand']; + } + + $display_dates = $params['_display_dates'] ?? true; + $mintime = $CFG_GLPI["planning_begin"]; if (isset($params["begin"]) && !empty($params["begin"])) { $begin = $params["begin"]; @@ -1306,19 +1300,23 @@ static function showAddEventClassicForm($params = []) { $end = date("Y-m-d H:i:s", strtotime($begin)+HOUR_TIMESTAMP); } - echo ""; - - echo "\n"; + echo "
".__('Start date').""; - $rand_begin = Html::showDateTimeField("plan[begin]", - ['value' => $begin, - 'timestep' => -1, - 'maybeempty' => false, - 'canedit' => true, - 'mindate' => '', - 'maxdate' => '', - 'mintime' => $mintime, - 'maxtime' => $CFG_GLPI["planning_end"]]); - echo "
"; + + if ($display_dates) { + echo ""; + } echo "\n"; @@ -1358,8 +1368,11 @@ static function showAddEventClassicForm($params = []) { && isset($params['itemtype']) && PlanningRecall::isAvailable()) { echo ""; } echo "
".__('Start date').""; + Html::showDateTimeField("plan[begin]", [ + 'value' => $begin, + 'timestep' => -1, + 'maybeempty' => false, + 'canedit' => true, + 'mindate' => '', + 'maxdate' => '', + 'mintime' => $mintime, + 'maxtime' => $CFG_GLPI["planning_end"], + 'rand' => $rand, + ]); + echo "
".__('Period')." "; @@ -1330,26 +1328,38 @@ static function showAddEventClassicForm($params = []) { echo ""; - $default_delay = floor((strtotime($end)-strtotime($begin))/$CFG_GLPI['time_step']/MINUTE_TIMESTAMP)*$CFG_GLPI['time_step']*MINUTE_TIMESTAMP; + $empty_label = Dropdown::EMPTY_VALUE; + $default_delay = $params['duration'] ?? 0; + if ($display_dates) { + $empty_label = __('Specify an end date'); + $default_delay = floor((strtotime($end)-strtotime($begin))/$CFG_GLPI['time_step']/MINUTE_TIMESTAMP)*$CFG_GLPI['time_step']*MINUTE_TIMESTAMP; + } - $rand = Dropdown::showTimeStamp("plan[_duration]", ['min' => 0, - 'max' => 50*HOUR_TIMESTAMP, - 'value' => $default_delay, - 'emptylabel' => __('Specify an end date')]); + Dropdown::showTimeStamp("plan[_duration]", [ + 'min' => 0, + 'max' => 50*HOUR_TIMESTAMP, + 'value' => $default_delay, + 'emptylabel' => $empty_label, + 'rand' => $rand, + ]); echo "
"; - $event_options = ['duration' => '__VALUE__', - 'end' => $end, - 'name' => "plan[end]", - 'global_begin' => $CFG_GLPI["planning_begin"], - 'global_end' => $CFG_GLPI["planning_end"]]; - - Ajax::updateItemOnSelectEvent("dropdown_plan[_duration]$rand", "date_end$rand", - $CFG_GLPI["root_doc"]."/ajax/planningend.php", $event_options); - - if ($default_delay == 0) { - $params['duration'] = 0; - Ajax::updateItem("date_end$rand", $CFG_GLPI["root_doc"]."/ajax/planningend.php", $params); + $event_options = [ + 'duration' => '__VALUE__', + 'end' => $end, + 'name' => "plan[end]", + 'global_begin' => $CFG_GLPI["planning_begin"], + 'global_end' => $CFG_GLPI["planning_end"] + ]; + + if ($display_dates) { + Ajax::updateItemOnSelectEvent("dropdown_plan[_duration]$rand", "date_end$rand", + $CFG_GLPI["root_doc"]."/ajax/planningend.php", $event_options); + + if ($default_delay == 0) { + $params['duration'] = 0; + Ajax::updateItem("date_end$rand", $CFG_GLPI["root_doc"]."/ajax/planningend.php", $params); + } } echo "
"._x('Planning', 'Reminder').""; - PlanningRecall::dropdown(['itemtype' => $params['itemtype'], - 'items_id' => $params['items_id']]); + PlanningRecall::dropdown([ + 'itemtype' => $params['itemtype'], + 'items_id' => $params['items_id'], + 'rand' => $rand, + ]); echo "
\n"; @@ -1376,7 +1389,7 @@ static function showAddEventClassicForm($params = []) { * - parent : in case of type=users_group, must contains the id of the group * - name : contains a string with type and id concatened with a '_' char (ex user_41). * - display : boolean value to set to his line - * @return nothing + * @return void */ static function toggleFilter($options = []) { @@ -1407,7 +1420,7 @@ static function toggleFilter($options = []) { * - parent : in case of type=users_group, must contains the id of the group * - name : contains a string with type and id concatened with a '_' char (ex user_41). * - color : rgb color (preceded by '#'' char) - * @return nothing + * @return void */ static function colorFilter($options = []) { $key = 'filters'; @@ -1433,7 +1446,7 @@ static function colorFilter($options = []) { * @param array $options: should contains : * - type : event type, can be event_filter, user, group or group_users * - filter : contains a string with type and id concatened with a '_' char (ex user_41). - * @return nothing + * @return void */ static function deleteFilter($options = []) { @@ -1477,6 +1490,7 @@ static function constructEventsArray($options = []) { $param['start'] = ''; $param['end'] = ''; + $param['view_name'] = ''; $param['display_done_events'] = true; if (is_array($options) && count($options)) { @@ -1524,30 +1538,117 @@ static function constructEventsArray($options = []) { } $index_color = array_search("user_$users_id", array_keys($_SESSION['glpi_plannings'])); - $events[] = ['title' => $event['name'], - 'content' => $content, - 'tooltip' => $tooltip, - 'start' => $begin, - 'end' => $end, - 'editable' => isset($event['editable'])?$event['editable']:false, - 'color' => (empty($event['color'])? - Planning::$palette_bg[$index_color]: - $event['color']), - 'borderColor' => (empty($event['event_type_color'])? - self::getPaletteColor('ev', $event['itemtype']): - $event['event_type_color']), - 'textColor' => Planning::$palette_fg[$index_color], - 'typeColor' => (empty($event['event_type_color'])? - self::getPaletteColor('ev', $event['itemtype']): - $event['event_type_color']), - 'url' => isset($event['url'])?$event['url']:"", - 'ajaxurl' => isset($event['ajaxurl'])?$event['ajaxurl']:"", - 'itemtype' => $event['itemtype'], - 'parentitemtype' => isset($event['parentitemtype'])? - $event['parentitemtype']:"", - 'items_id' => $event['id'], - 'priority' => isset($event['priority'])?$event['priority']:"", - 'state' => isset($event['state'])?$event['state']:""]; + $new_event = [ + 'title' => $event['name'], + 'content' => $content, + 'tooltip' => $tooltip, + 'start' => $begin, + 'end' => $end, + 'editable' => $event['editable'] ?? false, + 'rendering' => isset($event['background']) && $event['background'] + ? 'background' + : '', + 'color' => (empty($event['color'])? + Planning::$palette_bg[$index_color]: + $event['color']), + 'borderColor' => (empty($event['event_type_color'])? + self::getPaletteColor('ev', $event['itemtype']): + $event['event_type_color']), + 'textColor' => Planning::$palette_fg[$index_color], + 'typeColor' => (empty($event['event_type_color'])? + self::getPaletteColor('ev', $event['itemtype']): + $event['event_type_color']), + 'url' => $event['url'] ?? "", + 'ajaxurl' => $event['ajaxurl'] ?? "", + 'itemtype' => $event['itemtype'], + 'parentitemtype' => $event['parentitemtype'] ?? "", + 'items_id' => $event['id'], + 'resourceId' => $event['resourceId'], + 'priority' => $event['priority'] ?? "", + 'state' => $event['state'] ?? "", + ]; + + // override color if view is ressource and category color exists + // maybe we need a better way for displaying categories color + if ($param['view_name'] == "resourceWeek" + && !empty($event['event_cat_color'])) { + $new_event['color'] = $event['event_cat_color']; + } + + // manage reccurent events + if (isset($event['rrule']) && count($event['rrule'])) { + $rrule = $event['rrule']; + $rrule['dtstart'] = $new_event['start']; + + // RRule PHP lib fails on some timezone format + // i.e. datefmt_create: no such time zone: 'GMT+00:00': U_ILLEGAL_ARGUMENT_ERROR + // convert dtstart to UTC and use Z as timezone indication + $dtstart_datetime = new \DateTime($rrule['dtstart']); + $dtstart_datetime->setTimezone(new DateTimeZone('UTC')); + $rrule_o = new RRule( + array_merge( + $rrule, + [ + 'dtstart' => $dtstart_datetime->format('Ymd\THis\Z') + ] + ) + ); + + // append icon to distinguish reccurent event in views + $new_event = array_merge($new_event, [ + 'icon' => 'fas fa-history', + 'icon_alt' => $rrule_o->humanReadable(), + ]); + + // for fullcalendar, we need to pass start in the rrule key + // and also set a duration in milliseconds + // (we'll compute it between [end-start] dates) + $ms_duration = (strtotime($new_event['end']) - strtotime($new_event['start'])) * 1000; + unset($new_event['start'], $new_event['end']); + + // For list view, only display only the next occurence + // to avoid issues performances (range in list view is 10 years long) + if ($param['view_name'] == "listFull") { + $next_date = $rrule_o->getNthOccurrenceAfter(new DateTime(), 1); + $new_event = array_merge($new_event, [ + 'start' => $next_date->format('c'), + 'end' => $next_date->add(new DateInterval("PT".($ms_duration / 1000)."S")) + ->format('c'), + ]); + } else { + // merge rrule properties for others view + + // the fullcalencard plugin waits for integer types for number (not strings) + if (isset($rrule['interval'])) { + $rrule['interval'] = (int) $rrule['interval']; + } + if (isset($rrule['count'])) { + $rrule['count'] = (int) $rrule['count']; + } + + // clean empty values in rrule + foreach ($rrule as $key => $value) { + if (is_null($value) || $value == '') { + unset($rrule[$key]); + } + } + + // rrule.js lib decides to change one key for day of the week. + // see https://github.com/jakubroztocil/rrule#differences-from-icalendar-rfc + if (isset($rrule['byday'])) { + $rrule['byweekday'] = $rrule['byday']; + unset($rrule['byday']); + } + + $new_event = array_merge($new_event, [ + 'rrule' => $rrule, + 'duration' => $ms_duration + ]); + + } + } + + $events[] = $new_event; } return $events; @@ -1569,7 +1670,7 @@ static function constructEventsArray($options = []) { * - color: string with #rgb color for event's foreground color. * - event_type_color : string with #rgb color for event's foreground color. * @param array $raw_events: (passed by reference) the events array in construction - * @return nothing + * @return void */ static function constructEventsArraySingleLine($actor, $params = [], &$raw_events = []) { @@ -1593,16 +1694,17 @@ static function constructEventsArraySingleLine($actor, $params = [], &$raw_event $params['whogroup'] = $actor_array[1]; } - if (isset($params['color'])) { - $params['color'] = $params['color']; - } - $params['event_type_color'] = $params['event_type_color']; $current_events = $params['planning_type']::populatePlanning($params); if (count($current_events) > 0) { $raw_events = array_merge($raw_events, $current_events); } } } + + // fill type of planning + $raw_events = array_map(function($arr) use($actor) { + return $arr + ['resourceId' => $actor]; + }, $raw_events); } @@ -1633,6 +1735,7 @@ static function updateEventTimes($params = []) { $abort = false; + // we should not edit events from closed parent if (!empty($item->fields['tickets_id'])) { // todo: to same checks for changes, problems, projects and maybe reminders and others depending on incoming itemtypes $ticket = new Ticket(); @@ -1645,6 +1748,14 @@ static function updateEventTimes($params = []) { } } + // if event has rrule property, we shouldn't change it's data + // by dragging or resizing it + if (isset($item->fields['rrule']) + && strlen($item->fields['rrule'])) { + $abort = true; + Session::addMessageAfterRedirect(__("You cannot directly move or resize reccurent events"), false, ERROR); + } + if (!$abort) { $update = [ 'id' => $params['items_id'], @@ -1658,6 +1769,69 @@ static function updateEventTimes($params = []) { $update['users_id_tech'] = $item->fields['users_id_tech']; } + // manage moving event between resource (actors) + if (isset($params['new_actor_itemtype']) + && isset($params['new_actor_items_id']) + && !empty($params['new_actor_itemtype']) + && !empty($params['new_actor_items_id'])) { + + $new_actor_itemtype = strtolower($params['new_actor_itemtype']); + + // reminders don't have group assignement for planning + if (!($new_actor_itemtype === 'group' + && $item instanceof Reminder)) { + switch ($new_actor_itemtype) { + case "group": + $update['groups_id_tech'] = $params['new_actor_items_id']; + if (strtolower($params['old_actor_itemtype']) === "user") { + $update['users_id_tech'] = 0; + } + break; + + case "user": + if (isset($item->fields['users_id_tech'])) { + $update['users_id_tech'] = $params['new_actor_items_id']; + if (strtolower($params['old_actor_itemtype']) === "group") { + $update['groups_id_tech'] = 0; + } + } else { + $update['users_id'] = $params['new_actor_items_id']; + } + break; + } + } + + // special case for project tasks + // which have a link tables for their relation with groups/users + if ($item instanceof ProjectTask) { + // get actor for finding relation with item + $actor = new $params['old_actor_itemtype']; + $actor->getFromDB((int) $params['old_actor_items_id']); + + // get current relation + $team_old = new ProjectTaskTeam; + $team_old->getFromDBForItems($item, $actor); + + // if new relation already exists, delete old relation + $actor_new = new $params['new_actor_itemtype']; + $actor_new->getFromDB((int) $params['new_actor_items_id']); + $team_new = new ProjectTaskTeam; + if ($team_new->getFromDBForItems($item, $actor_new)) { + $team_old->delete([ + 'id' => $team_old->fields['id'] + ]); + + } else { + // else update relation + $team_old->update([ + 'id' => $team_old->fields['id'], + 'itemtype' => $params['new_actor_itemtype'], + 'items_id' => $params['new_actor_items_id'], + ]); + } + } + } + if (is_subclass_of($item, "CommonITILTask")) { $parentitemtype = $item->getItilObjectItemType(); if (!$update["_job"] = getItemForItemtype($parentitemtype)) { @@ -1685,7 +1859,7 @@ static function updateEventTimes($params = []) { * (default '') * @param $complete complete display (more details) (default 0) * - * @return Nothing (display function) + * @return string **/ static function displayPlanningItem(array $val, $who, $type = "", $complete = 0) { global $CFG_GLPI; @@ -1740,7 +1914,7 @@ static private function displayUsingTwoDigits($time) { * * @param $who ID of the user * - * @return Nothing (display function) + * @return void **/ static function showCentral($who) { global $CFG_GLPI; @@ -1895,4 +2069,16 @@ function getRights($interface = 'central') { return $values; } + + /** + * Save the last view used in fullcalendar + * + * @since 9.5 + * + * @param string $view_name + * @return void + */ + static function viewChanged($view_name = "ListView") { + $_SESSION['glpi_plannings']['lastview'] = $view_name; + } } diff --git a/inc/planningevent.class.php b/inc/planningevent.class.php index decf067d5da..3e5ce6b2539 100644 --- a/inc/planningevent.class.php +++ b/inc/planningevent.class.php @@ -1,4 +1,5 @@ fields["users_id"])) { + $this->fields["users_id"] = Session::getLoginUserID(); + } + + if (isset($this->field['rrule'])) { + $this->field['rrule'] = json_decode($this->field['rrule'], true); + } + } + + function post_addItem() { + // Add document if needed + $this->input = $this->addFiles($this->input, [ + 'force_update' => true, + 'content_field' => 'text'] + ); + + if (isset($this->fields["users_id"]) + && isset($this->fields["begin"]) + && !empty($this->fields["begin"])) { + Planning::checkAlreadyPlanned( + $this->fields["users_id"], + $this->fields["begin"], + $this->fields["end"], + [ + $this->getType() => [$this->fields['id']] + ] + ); + } + + if (isset($this->input['_planningrecall'])) { + $this->input['_planningrecall']['items_id'] = $this->fields['id']; + PlanningRecall::manageDatas($this->input['_planningrecall']); + } + } + + + function prepareInputForAdd($input) { + + Toolbox::manageBeginAndEndPlanDates($input['plan']); + + $input["name"] = trim($input["name"]); + + if (empty($input["name"])) { + $input["name"] = __('Without title'); + } + + $input["begin"] = $input["end"] = "NULL"; + + if (isset($input['plan'])) { + if (!empty($input['plan']["begin"]) + && !empty($input['plan']["end"]) + && ($input['plan']["begin"] < $input['plan']["end"])) { + + $input['_plan'] = $input['plan']; + unset($input['plan']); + $input['is_planned'] = 1; + $input["begin"] = $input['_plan']["begin"]; + $input["end"] = $input['_plan']["end"]; + + } else if (isset($this->fields['begin']) + && isset($this->fields['end'])) { + Session::addMessageAfterRedirect( + __('Error in entering dates. The starting date is later than the ending date'), + false, ERROR); + } + } + + // set new date. + $input["date"] = $_SESSION["glpi_currenttime"]; + + // encode rrule + if (isset($input['rrule'])) { + $input['rrule'] = $this->encodeRrule($input['rrule']); + } + + return $input; + } + + + function prepareInputForUpdate($input) { + + Toolbox::manageBeginAndEndPlanDates($input['plan']); + + if (isset($input['_planningrecall'])) { + PlanningRecall::manageDatas($input['_planningrecall']); + } + + if (isset($input["name"])) { + $input["name"] = trim($input["name"]); + + if (empty($input["name"])) { + $input["name"] = __('Without title'); + } + } + + if (isset($input['plan'])) { + + if (!empty($input['plan']["begin"]) + && !empty($input['plan']["end"]) + && ($input['plan']["begin"] < $input['plan']["end"])) { + + $input['_plan'] = $input['plan']; + unset($input['plan']); + $input['is_planned'] = 1; + $input["begin"] = $input['_plan']["begin"]; + $input["end"] = $input['_plan']["end"]; + + } else if (isset($this->fields['begin']) + && isset($this->fields['end'])) { + Session::addMessageAfterRedirect( + __('Error in entering dates. The starting date is later than the ending date'), + false, ERROR); + } + } + + $input = $this->addFiles($input, ['content_field' => 'text']); + + // encode rrule + if (isset($input['rrule'])) { + $input['rrule'] = $this->encodeRrule($input['rrule']); + } + + return $input; + } + + function encodeRrule(Array $rrule = []) { + if ($rrule['freq'] == null) { + return ""; + } + + if (count($rrule) > 0) { + $rrule = json_encode($rrule); + } + + return $rrule; + } + + + function post_updateItem($history = 1) { + if (isset($this->fields["users_id"]) + && isset($this->fields["begin"]) + && !empty($this->fields["begin"])) { + Planning::checkAlreadyPlanned( + $this->fields["users_id"], + $this->fields["begin"], + $this->fields["end"], + [ + $this->getType() => [$this->fields['id']] + ] + ); + } + if (in_array("begin", $this->updates)) { + PlanningRecall::managePlanningUpdates( + $this->getType(), + $this->getID(), + $this->fields["begin"] + ); + } + + } + + + function pre_updateInDB() { + // Set new user if initial user have been deleted + if (isset($this->fields['users_id']) + && $this->fields['users_id'] == 0 + && $uid = Session::getLoginUserID()) { + $this->fields['users_id'] = $uid; + $this->updates[] ="users_id"; + } + } + + + /** + * Populate the planning with planned event + * + * @param $options array of possible options: + * - who ID of the user (0 = undefined) + * - who_group ID of the group of users (0 = undefined) + * - begin Date + * - end Date + * - color + * - event_type_color + * - check_planned (boolean) + * - display_done_events (boolean) + * + * @return array of planning item + **/ + static function populatePlanning($options = []) { + global $DB, $CFG_GLPI; + + $default_options = [ + 'genical' => false, + 'color' => '', + 'event_type_color' => '', + 'check_planned' => false, + 'display_done_events' => true, + ]; + $options = array_merge($default_options, $options); + + $events = []; + $event_obj = new static; + $itemtype = $event_obj->getType(); + $item_fk = getForeignKeyFieldForItemType($itemtype); + $table = self::getTable(); + $has_bg = $DB->fieldExists($table, 'background'); + + if (!isset($options['begin']) || $options['begin'] == 'NULL' + || !isset($options['end']) || $options['end'] == 'NULL') { + return $events; + } + + $who = $options['who']; + $who_group = $options['who_group']; + $begin = $options['begin']; + $end = $options['end']; + + if ($options['genical']) { + $_SESSION["glpiactiveprofile"][static::$rightname] = READ; + } + $visibility_criteria = []; + if ($event_obj instanceof CommonDBVisible) { + $visibility_criteria = self::getVisibilityCriteria(true); + } + $nreadpub = []; + $nreadpriv = []; + + // See public event ? + if (!$options['genical'] + && $who === Session::getLoginUserID() + && self::canView() + && isset($visibility_criteria['WHERE'])) { + $nreadpub = $visibility_criteria['WHERE']; + } + unset($visibility_criteria['WHERE']); + + // See my private event ? + if (($who_group === "mine") || ($who === Session::getLoginUserID())) { + $nreadpriv = ["$table.users_id" => Session::getLoginUserID()]; + } else { + if ($who > 0) { + $nreadpriv = ["$table.users_id" => $who]; + } + if ($who_group > 0 && $itemtype == 'Reminder') { + $ngrouppriv = ["glpi_groups_reminders.groups_id" => $who_group]; + if (!empty($nreadpriv)) { + $nreadpriv['OR'] = [$nreadpriv, $ngrouppriv]; + } else { + $nreadpriv = $ngrouppriv; + } + } + } + + $NASSIGN = []; + + if (count($nreadpub) + && count($nreadpriv)) { + $NASSIGN = ['OR' => [$nreadpub, $nreadpriv]]; + } else if (count($nreadpub)) { + $NASSIGN = $nreadpub; + } else { + $NASSIGN = $nreadpriv; + } + + if (!count($NASSIGN)) { + return $events; + } + + $WHERE = [ + 'begin' => ['<', $end], + 'end' => ['>', $begin] + ] + $NASSIGN; + + if ($DB->fieldExists($table, 'is_planned')) { + $WHERE["$table.is_planned"] = 1; + } + + if ($options['check_planned']) { + $WHERE['state'] = ['!=', Planning::INFO]; + } + + if (!$options['display_done_events']) { + $WHERE['OR'] = [ + 'state' => Planning::TODO, + 'AND' => [ + 'state' => Planning::INFO, + 'end' => ['>', new \QueryExpression('NOW()')] + ] + ]; + } + + $event_obj->getEmpty(); + if (isset($event_obj->fields['rrule'])) { + unset($WHERE['end']); + $WHERE[] = [ + 'OR' => [ + 'end' => ['>', $begin], + 'rrule' => ['!=', ""], + ] + ]; + } + + $criteria = [ + 'SELECT' => ["$table.*"], + 'DISTINCT' => true, + 'FROM' => $table, + 'WHERE' => $WHERE, + 'ORDER' => 'begin' + ] + $visibility_criteria; + + if (isset($event_obj->fields['planningeventcategories_id'])) { + $c_table = PlanningEventCategory::getTable(); + $criteria['SELECT'][] = "$c_table.color AS cat_color"; + $criteria['JOIN'] = [ + $c_table => [ + 'FKEY' => [ + $c_table => 'id', + $table => 'planningeventcategories_id', + ] + ] + ]; + } + + $iterator = $DB->request($criteria); + + $events_toadd = []; + + if (count($iterator)) { + while ($data = $iterator->next()) { + if ($event_obj->getFromDB($data["id"]) && $event_obj->canViewItem()) { + $key = $data["begin"]."$$".$itemtype."$$".$data["id"]; + + $url = (!$options['genical']) + ? $event_obj->getFormURLWithID($data['id']) + : $CFG_GLPI["url_base"]. + self::getFormURLWithID($data['id'], false); + + $is_rrule = isset($data['rrule']) && strlen($data['rrule']) > 0; + + $events[$key] = [ + 'color' => $options['color'], + 'event_type_color' => $options['event_type_color'], + 'event_cat_color' => $data['cat_color'] ?? "", + 'itemtype' => $itemtype, + $item_fk => $data['id'], + 'id' => $data['id'], + 'users_id' => $data["users_id"], + 'state' => $data["state"], + 'background' => $has_bg ? $data['background'] : false, + 'name' => Html::clean(Html::resume_text($data["name"], $CFG_GLPI["cut"])), + 'text' => Html::resume_text(Html::clean(Toolbox::unclean_cross_side_scripting_deep($data["text"])), + $CFG_GLPI["cut"]), + 'ajaxurl' => $CFG_GLPI["root_doc"]."/ajax/planning.php". + "?action=edit_event_form". + "&itemtype=$itemtype". + "&id=".$data['id']. + "&url=$url", + 'editable' => $event_obj->canUpdateItem(), + 'url' => $url, + 'begin' => !$is_rrule && (strcmp($begin, $data["begin"]) > 0) + ? $begin + : $data["begin"], + 'end' => !$is_rrule && (strcmp($end, $data["end"]) < 0) + ? $end + : $data["end"], + 'rrule' => isset($data['rrule']) && !empty($data['rrule']) + ? json_decode($data['rrule'], true) + : [] + ]; + + // when checking avaibility, we need to explode rrules events + // to check if future occurences of the primary event + // doesn't match current range + if ($options['check_planned'] && count($events[$key]['rrule'])) { + $event = $events[$key]; + $duration = strtotime($event['end']) - strtotime($event['begin']); + $rrule_data = array_merge($event['rrule'], ['dtstart' => $event['begin']]); + $rrule = new RRule($rrule_data); + + // rrule object doesn't any duration property, + // so we remove the duration from the begin part of the range + // (minus 1second to avoid mathing precise end date) + // to check if event started before begin and could be still valid + $begin_datetime = new DateTime($options['begin']); + $begin_datetime->sub(New DateInterval("PT".($duration - 1)."S")); + + $occurences = $rrule->getOccurrencesBetween($begin_datetime, $options['end']); + + // add the found occurences to the final tab after replacing their dates + foreach ($occurences as $currentDate) { + $events_toadd[] = array_merge($event, [ + 'begin' => $currentDate->format('Y-m-d H:i:s'), + 'end' => $currentDate->add(new DateInterval("PT".$duration."S")) + ->format('Y-m-d H:i:s'), + ]); + } + + // remove primary event (with rrule) + // as the final array now have all the occurences + unset($events[$key]); + } + } + } + } + + if (count($events_toadd)) { + $events = $events + $events_toadd; + } + + return $events; + } + + /** + * Display a Planning Item + * + * @param $val array of the item to display + * @param $who ID of the user (0 if all) + * @param $type position of the item in the time block (in, through, begin or end) + * default '') + * @param $complete complete display (more details) (default 0) + * + * @return Nothing (display function) + **/ + static function displayPlanningItem(array $val, $who, $type = "", $complete = 0) { + global $CFG_GLPI; + + $html = ""; + $rand = mt_rand(); + $users_id = ""; // show users_id reminder + $img = "rdv_private.png"; // default icon for reminder + $item_fk = getForeignKeyFieldForItemType(static::getType()); + + if ($val["users_id"] != Session::getLoginUserID()) { + $users_id = "
".sprintf(__('%1$s: %2$s'), __('By'), getUserName($val["users_id"])); + $img = "rdv_public.png"; + } + + $html.= " "; + $html.= ""; + + $html.= $users_id; + $html.= ""; + $recall = ''; + if (isset($val[$item_fk])) { + $pr = new PlanningRecall(); + if ($pr->getFromDBForItemAndUser($val['itemtype'], $val[$item_fk], + Session::getLoginUserID())) { + $recall = "
".sprintf(__('Recall on %s'), + Html::convDateTime($pr->fields['when'])). + ""; + } + } + + if ($complete) { + $html.= "".Planning::getState($val["state"])."
"; + $html.= "
".$val["text"].$recall."
"; + } else { + $html.= Html::showToolTip("".Planning::getState($val["state"])."
+ ".$val["text"].$recall, + ['applyto' => "reminder_".$val[$item_fk].$rand, + 'display' => false]); + } + return $html; + } + + + /** + * Display a mini form html for setup a reccuring event + * to construct an rrule array + * + * @param string $rrule existing rrule entry with ical format (https://www.kanzaki.com/docs/ical/rrule.html) + * @param array $options can contains theses keys: + * - 'rand' => random string for generated inputs + * @return string the generated html + */ + static function showRepetitionForm(string $rrule = "", array $options = []): string { + $rrule = json_decode($rrule, true) ?? []; + $defaults = [ + 'freq' => null, + 'interval' => 1, + 'until' => null, + 'byday' => [], + 'bymonth' => [], + ]; + $rrule = array_merge($defaults, $rrule); + + $default_options = [ + 'rand' => mt_rand(), + ]; + $options = array_merge($default_options, $options); + $rand = $options['rand']; + + $out = "
"; + $out.= Dropdown::showFromArray('rrule[freq]', [ + null => __("Never"), + 'daily' => __("Each day"), + 'weekly' => __("Each week"), + 'monthly' => __("Each month"), + 'yearly' => __("Each year"), + ], [ + 'value' => $rrule['freq'], + 'rand' => $rand, + 'display' => false, + 'on_change' => "$(\"#toggle_ar\").toggle($(\"#dropdown_rrule_freq_$rand\").val().length > 0)" + ]); + + $display_tar = $rrule['freq'] == null ? "none" : "inline"; + $display_ar = $rrule['freq'] == null + || !($rrule['interval'] > 1 + || $rrule['until'] != null + || count($rrule['byday']) > 0 + || count($rrule['bymonth']) > 0) + ? "none" : "table"; + + $out.= ""; + $out.= " + + "; + $out.= "
"; + + $out.= "
"; + $out.= ""; + $out.= "
".Dropdown::showNumber('rrule[interval]', [ + 'value' => $rrule['interval'], + 'rand' => $rand, + 'display' => false, + ])."
"; + $out.= "
"; + + $out.= "
"; + $out.= ""; + $out.= "
".Html::showDateTimeField('rrule[until]', [ + 'value' => $rrule['until'], + 'rand' => $rand, + 'display' => false, + ])."
"; + $out.= "
"; + + $out.= "
"; + $out.= ""; + $out.= "
".Dropdown::showFromArray('rrule[byday]', [ + 'MO' => __('Monday'), + 'TU' => __('Tuesday'), + 'WE' => __('Wednesday'), + 'TH' => __('Thursday'), + 'FR' => __('Friday'), + 'SA' => __('Saturday'), + 'SU' => __('Sunday'), + ], [ + 'values' => $rrule['byday'], + 'rand' => $rand, + 'display' => false, + 'display_emptychoice' => true, + 'multiple' => true, + ])."
"; + $out.= "
"; + + $out.= "
"; + $out.= ""; + $out.= "
".Dropdown::showFromArray('rrule[bymonth]', [ + 1 => __('January'), + 2 => __('February'), + 3 => __('March'), + 4 => __('April'), + 5 => __('May'), + 6 => __('June'), + 7 => __('July'), + 8 => __('August'), + 9 => __('September'), + 10 => __('October'), + 11 => __('November'), + 12 => __('December'), + ], [ + 'values' => $rrule['bymonth'], + 'rand' => $rand, + 'display' => false, + 'display_emptychoice' => true, + 'multiple' => true, + ])."
"; + $out.= "
"; + + $out.= "
"; // #advanced_repetition + $out.= "
"; // #toggle_ar + $out.= "
"; // .card + return $out; + } + + /** * Display a Planning Item * diff --git a/inc/planningeventcategory.class.php b/inc/planningeventcategory.class.php new file mode 100644 index 00000000000..40a528b5ecb --- /dev/null +++ b/inc/planningeventcategory.class.php @@ -0,0 +1,53 @@ +. + * --------------------------------------------------------------------- + */ + +if (!defined('GLPI_ROOT')) { + die("Sorry. You can't access this file directly"); +} + +class PlanningEventCategory extends CommonDropdown { + + + static function getTypeName($nb = 0) { + return _n('Event category', 'Event categories', $nb); + } + + function getAdditionalFields() { + return [ + [ + 'name' => 'color', + 'label' => __('Color'), + 'type' => 'color', + ] + ]; + } +} diff --git a/inc/planningexternalevent.class.php b/inc/planningexternalevent.class.php new file mode 100644 index 00000000000..33d0876cb23 --- /dev/null +++ b/inc/planningexternalevent.class.php @@ -0,0 +1,272 @@ +. + * --------------------------------------------------------------------- + */ + +class PlanningExternalEvent extends CommonDBTM { + use PlanningEvent; + + public $dohistory = true; + static $rightname = 'externalevent'; + + const MANAGE_BG_EVENTS = 1024; + + static function getTypeName($nb = 0) { + return _n('External event', 'External events', $nb); + } + + function defineTabs($options = []) { + $ong = []; + $this->addDefaultFormTab($ong); + $this->addStandardTab('Document_Item', $ong, $options); + $this->addStandardTab('Log', $ong, $options); + + return $ong; + } + + static function canUpdate() { + // we permits globally to update this object, + // as users can update their onw items + return Session::haveRightsOr(self::$rightname, [ + CREATE, + UPDATE, + self::MANAGE_BG_EVENTS + ]); + } + + function canUpdateItem() { + // if we don't have the right to manage background events, + // we don't have the right to edit the item + if ($this->fields["background"] + && !Session::haveRight(self::$rightname, self::MANAGE_BG_EVENTS)) { + return false; + } + + // the current user can update only this own events without UPDATE right + // but not bg one, see above + if ($this->fields['users_id'] != Session::getLoginUserID() + && !Session::haveRight(self::$rightname, UPDATE)) { + return false; + } + + return parent::canUpdateItem(); + } + + function showForm($ID, $options = []) { + global $CFG_GLPI; + + $canedit = $this->can($ID, UPDATE); + $rand = mt_rand(); + $rand_plan = mt_rand(); + $rand_rrule = mt_rand(); + + $this->initForm($ID, $options); + $this->showFormHeader($options); + + if ($canedit) { + $tpl_class = 'PlanningExternalEventTemplate'; + echo ""; + echo "".$tpl_class::getTypeName().""; + echo ""; + $tpl_class::dropdown([ + 'value' => $this->fields['planningexternaleventtemplates_id'], + 'entity' => $this->getEntityID(), + 'rand' => $rand, + 'on_change' => "template_update$rand(this.value)" + ]); + + $ajax_url = $CFG_GLPI["root_doc"]."/ajax/planning.php"; + $JS = << 0) { + $("#textfield_name{$rand}").val(data.name); + } + $("#dropdown_state{$rand}").trigger("setValue", data.state); + if (data.planningeventcategories_id > 0) { + $("#dropdown_planningeventcategories_id{$rand}") + .trigger("setValue", data.planningeventcategories_id); + } + $("#dropdown_background{$rand}").trigger("setValue", data.background); + if (data.text.length > 0) { + $("#text{$rand}").html(data.text); + if (contenttinymce = tinymce.get("text{$rand}")) { + contenttinymce.setContent(data.text); + } + } + + // set planification fields + if (data.duration > 0) { + $("#dropdown_plan__duration_{$rand_plan}").trigger("setValue", data.duration); + } + $("#dropdown__planningrecall_before_time_{$rand_plan}") + .trigger("setValue", data.before_time); + + // set rrule fields + if (data.rrule != null + && data.rrule.freq != null ) { + $("#dropdown_rrule_freq_{$rand_rrule}").trigger("setValue", data.rrule.freq); + $("#dropdown_rrule_interval_{$rand_rrule}").trigger("setValue", data.rrule.interval); + $("#showdate{$rand_rrule}").val(data.rrule.until); + $("#dropdown_rrule_byweekday_{$rand_rrule}").trigger("setValue", data.rrule.byweekday); + $("#dropdown_rrule_bymonth_{$rand_rrule}").trigger("setValue", data.rrule.bymonth); + } + }); + } +JAVASCRIPT; + echo Html::scriptBlock($JS); + echo ""; + } + + echo "".__('Title').""; + echo ""; + if (!$ID) { + echo Html::hidden('users_id', ['value' => $this->fields['users_id']]); + } + if ($canedit) { + Html::autocompletionTextField($this, "name", [ + 'size' => '80', + 'entity' => -1, + 'user' => $this->fields["users_id"], + 'rand' => $rand + ]); + } else { + echo $this->fields['name']; + } + if (isset($options['from_planning_edit_ajax']) && $options['from_planning_edit_ajax']) { + echo Html::hidden('from_planning_edit_ajax'); + } + echo ""; + echo ""; + + echo ""; + echo "".__('Status').""; + echo ""; + if ($canedit) { + Planning::dropdownState("state", $this->fields["state"], true, [ + 'rand' => $rand, + ]); + } else { + echo Planning::getState($this->fields["state"]); + } + echo ""; + echo ""; + + echo ""; + echo ""; + echo "".__('Category').""; + echo ""; + if ($canedit) { + PlanningEventCategory::dropdown([ + 'value' => $this->fields['planningeventcategories_id'], + 'rand' => $rand + ]); + } else { + echo Dropdown::getDropdownName( + PlanningEventCategory::getTable(), + $this->fields['planningeventcategories_id'] + ); + } + echo ""; + echo ""; + + echo ""; + echo "".__('Background event').""; + echo ""; + if ($canedit) { + Dropdown::showYesNo('background', $this->fields['background'], -1, [ + 'rand' => $rand, + ]); + } else { + echo Dropdown::getYesNo($this->fields['background']); + } + echo ""; + echo ""; + + echo "".__('Calendar').""; + echo ""; + Planning::showAddEventClassicForm([ + 'items_id' => $this->fields['id'], + 'itemtype' => $this->getType(), + 'begin' => $this->fields['begin'], + 'end' => $this->fields['end'], + 'rand_user' => $this->fields['users_id'], + 'rand' => $rand_plan, + ]); + echo ""; + + echo "".__('Repeat').""; + echo ""; + echo self::showRepetitionForm($this->fields['rrule'], [ + 'rand' => $rand_rrule + ]); + echo ""; + + echo "".__('Description')."". + ""; + + if ($canedit) { + Html::textarea([ + 'name' => 'text', + 'value' => $this->fields["text"], + 'enable_richtext' => true, + 'enable_fileupload' => true, + 'rand' => $rand, + 'editor_id' => 'text'.$rand, + ]); + } else { + echo "
"; + echo Toolbox::unclean_html_cross_side_scripting_deep($this->fields["text"]); + echo "
"; + } + + echo ""; + + $this->showFormButtons($options); + + return true; + } + + function getRights($interface = 'central') { + $values = parent::getRights(); + + $values[self::MANAGE_BG_EVENTS] = __('manage background events'); + + return $values; + } +} diff --git a/inc/planningexternaleventtemplate.class.php b/inc/planningexternaleventtemplate.class.php new file mode 100644 index 00000000000..b1a97340ab6 --- /dev/null +++ b/inc/planningexternaleventtemplate.class.php @@ -0,0 +1,207 @@ +. + * --------------------------------------------------------------------- + */ + +if (!defined('GLPI_ROOT')) { + die("Sorry. You can't access this file directly"); +} + +/** + * Template for PlanningExternalEvent + * @since 9.5 +**/ +class PlanningExternalEventTemplate extends CommonDropdown { + use PlanningEvent { + prepareInputForAdd as prepareInputForAddTrait; + prepareInputForUpdate as prepareInputForUpdateTrait; + } + + // From CommonDBTM + public $dohistory = true; + public $can_be_translated = true; + + + static function getTypeName($nb = 0) { + return _n('External events template', 'External events templates', $nb); + } + + + function getAdditionalFields() { + return [ + [ + 'name' => 'state', + 'label' => __('Status'), + 'type' => 'planningstate', + ], [ + 'name' => 'planningeventcategories_id', + 'label' => __('Category'), + 'type' => 'dropdownValue', + 'list' => true + ], [ + 'name' => 'background', + 'label' => __('Background event'), + 'type' => 'bool' + ], [ + 'name' => 'plan', + 'label' => __('Calendar'), + 'type' => 'plan', + ], [ + 'name' => 'rrule', + 'label' => __('Repeat'), + 'type' => 'rrule', + ], [ + 'name' => 'text', + 'label' => __('Description'), + 'type' => 'tinymce', + ] + ]; + } + + + function displaySpecificTypeField($ID, $field = []) { + + switch ($field['type']) { + case 'planningstate' : + Planning::dropdownState("state", $this->fields["state"]); + break; + + case 'plan' : + Planning::showAddEventClassicForm([ + 'duration' => $this->fields['duration'], + 'itemtype' => self::getType(), + 'items_id' => $this->fields['id'], + '_display_dates' => false, + ]); + break; + + case 'rrule' : + echo self::showRepetitionForm($this->fields['rrule']); + break; + } + } + + + function rawSearchOptions() { + return array_merge(parent::rawSearchOptions(), [ + [ + 'id' => '4', + 'name' => __('Description'), + 'field' => 'text', + 'table' => self::getTable(), + 'datatype' => 'text', + 'htmltext' => true + ], [ + 'id' => '5', + 'name' => __('Status'), + 'field' => 'state', + 'table' => self::getTable(), + 'datatype' => 'specific' + ], [ + 'id' => '6', + 'name' => __('Category'), + 'field' => 'name', + 'table' => getTableForItemType('PlanningEventCategory'), + 'datatype' => 'dropdown' + ], [ + 'id' => '7', + 'name' => __('Background event'), + 'field' => 'background', + 'table' => self::getTable(), + 'datatype' => 'bool' + ], [ + 'id' => '8', + 'name' => __('Repeat'), + 'field' => 'rrule', + 'table' => self::getTable(), + 'datatype' => 'text' + ] + ]); + } + + + static function getSpecificValueToDisplay($field, $values, array $options = []) { + if (!is_array($values)) { + $values = [$field => $values]; + } + + switch ($field) { + case 'state': + return Planning::getState($values[$field]); + } + + return parent::getSpecificValueToDisplay($field, $values, $options); + } + + + static function getSpecificValueToSelect($field, $name = '', $values = '', array $options = []) { + if (!is_array($values)) { + $values = [$field => $values]; + } + $options['display'] = false; + + switch ($field) { + case 'state': + return Planning::dropdownState($name, $values[$field], $options); + } + + return parent::getSpecificValueToSelect($field, $name, $values, $options); + } + + + function prepareInputForAdd($input) { + $saved_input = $input; + $input = $this->prepareInputForAddTrait($input); + + return $this->parseExtraInput($saved_input, $input); + } + + + function prepareInputForupdate($input) { + $saved_input = $input; + $input = $this->prepareInputForupdateTrait($input); + + return $this->parseExtraInput($saved_input, $input); + } + + function parseExtraInput(array $orig_input = [], array $input = []) { + if (isset($orig_input['plan']) + && array_key_exists('_duration', $orig_input['plan'])) { + $input['duration'] = $orig_input['plan']['_duration']; + } + + if (isset($orig_input['_planningrecall']) + && array_key_exists('before_time', $orig_input['_planningrecall'])) { + $input['before_time'] = $orig_input['_planningrecall']['before_time']; + } + + return $input; + } +} diff --git a/inc/planningrecall.class.php b/inc/planningrecall.class.php index ce6362f1f03..c32834ca76d 100644 --- a/inc/planningrecall.class.php +++ b/inc/planningrecall.class.php @@ -229,7 +229,7 @@ static function managePlanningUpdates($itemtype, $items_id, $begin) { * - value : integer preselected value for before_time * - field : string field used as time mark (default begin) * - * @return nothing (print out an HTML select box) / return false if mandatory fields are not ok + * @return void|boolean print out an HTML select box or return false if mandatory fields are not ok **/ static function dropdown($options = []) { global $DB, $CFG_GLPI; @@ -240,6 +240,7 @@ static function dropdown($options = []) { $p['users_id'] = Session::getLoginUserID(); $p['value'] = Entity::CONFIG_NEVER; $p['field'] = 'begin'; + $p['rand'] = mt_rand(); if (is_array($options) && count($options)) { foreach ($options as $key => $val) { @@ -280,8 +281,10 @@ static function dropdown($options = []) { ksort($possible_values); - Dropdown::showFromArray('_planningrecall[before_time]', $possible_values, - ['value' => $p['value']]); + Dropdown::showFromArray('_planningrecall[before_time]', $possible_values, [ + 'value' => $p['value'], + 'rand' => $p['rand'], + ]); echo ""; echo ""; echo ""; @@ -302,7 +305,7 @@ static function dropdown($options = []) { * - value : integer preselected value for before_time * - field : string field used as time mark (default begin) * - * @return nothing (print out an HTML select box) / return false if mandatory fields are not ok + * @return void|boolean print out an HTML select box or return false if mandatory fields are not ok **/ static function specificForm($options = []) { global $CFG_GLPI; diff --git a/inc/plugin.class.php b/inc/plugin.class.php index 0f0feebecc3..6425769b37d 100644 --- a/inc/plugin.class.php +++ b/inc/plugin.class.php @@ -140,7 +140,7 @@ function init() { * @param $name Name of hook to use * @param $withhook boolean to load hook functions (false by default) * - * @return nothing + * @return void **/ static function load($name, $withhook = false) { global $LOADED_PLUGINS; @@ -170,7 +170,7 @@ static function load($name, $withhook = false) { * @param $forcelang force a specific lang (default '') * @param $coretrytoload lang trying to be load from core (default '') * - * @return nothing + * @return void **/ static function loadLang($name, $forcelang = '', $coretrytoload = '') { // $LANG needed : used when include lang file @@ -817,7 +817,7 @@ function isInstalled($plugin) { * @param $glpitables array of GLPI table name used by the plugin * @param $plugtables array of Plugin table name which have an itemtype * - * @return nothing + * @return void **/ static function migrateItemType($types = [], $glpitables = [], $plugtables = []) { global $DB; diff --git a/inc/printer.class.php b/inc/printer.class.php index e36ca8dcf60..085462f7587 100644 --- a/inc/printer.class.php +++ b/inc/printer.class.php @@ -75,6 +75,7 @@ function defineTabs($options = []) { $ong = []; $this->addDefaultFormTab($ong); + $this->addImpactTab($ong, $options); $this->addStandardTab('Item_OperatingSystem', $ong, $options); $this->addStandardTab('Cartridge', $ong, $options); $this->addStandardTab('Item_Devices', $ong, $options); diff --git a/inc/problem.class.php b/inc/problem.class.php index b3781c9082d..fd09115f1c8 100644 --- a/inc/problem.class.php +++ b/inc/problem.class.php @@ -1567,7 +1567,7 @@ static function getCommonLeftJoin() { * * @param $item CommonDBTM object * - * @return nothing (display a table) + * @return void **/ static function showListForItem(CommonDBTM $item) { global $DB, $CFG_GLPI; @@ -1825,6 +1825,79 @@ static function getDefaultValues($entity = 0) { 'users_id_validate' => [], '_tasktemplates_id' => [] ]; + } + + /** + * get active problems for an item + * + * @since 9.5 + * + * @param string $itemtype Item type + * @param integer $items_id ID of the Item + * + * @return DBmysqlIterator + */ + public function getActiveProblemsForItem($itemtype, $items_id) { + global $DB; + + return $DB->request([ + 'SELECT' => [ + $this->getTable() . '.id', + $this->getTable() . '.name' + ], + 'FROM' => $this->getTable(), + 'LEFT JOIN' => [ + 'glpi_items_problems' => [ + 'ON' => [ + 'glpi_items_problems' => 'problems_id', + $this->getTable() => 'id' + ] + ] + ], + 'WHERE' => [ + 'glpi_items_problems.itemtype' => $itemtype, + 'glpi_items_problems.items_id' => $items_id, + 'NOT' => [ + $this->getTable() . '.status' => array_merge( + $this->getSolvedStatusArray(), + $this->getClosedStatusArray() + ) + ] + ] + ]); + } + + /** + * Get assets linked to this object + * + * @since 9.5 + * + * @param bool $addNames Insert asset names + * + * @return array + */ + public function getLinkedItems(bool $addNames = true) { + global $DB; + + $assets = $DB->request([ + 'SELECT' => ["id", "itemtype", "items_id"], + 'FROM' => "glpi_items_problems", + 'WHERE' => ["problems_id" => $this->getID()] + ]); + + $assets = iterator_to_array($assets); + + if ($addNames) { + foreach ($assets as $key => $asset) { + /** @var CommonDBTM $item */ + $item = new $asset['itemtype']; + $item->getFromDB($asset['id']); + + // Add name + $assets[$key]['name'] = $item->fields['name']; + } + } + return $assets; } } diff --git a/inc/profile.class.php b/inc/profile.class.php index d518129c4f5..e2f4f35946f 100644 --- a/inc/profile.class.php +++ b/inc/profile.class.php @@ -1074,15 +1074,31 @@ function showFormTracking($openform = true, $closeform = true) { echo "\n"; echo ""; - $rights = [['itemtype' => 'Stat', - 'label' => __('Statistics'), - 'field' => 'statistic'], - ['itemtype' => 'Planning', - 'label' => __('Planning'), - 'field' => 'planning']]; + $rights = [ + [ + 'itemtype' => 'Stat', + 'label' => __('Statistics'), + 'field' => 'statistic' + ],[ + 'itemtype' => 'Planning', + 'label' => __('Planning'), + 'field' => 'planning' + ] + ]; $matrix_options['title'] = __('Visibility'); $this->displayRightsChoiceMatrix($rights, $matrix_options); + $rights = [ + [ + 'itemtype' => 'PlanningExternalEvent', + 'label' => PlanningExternalEvent::getTypeName(Session::getPluralNumber()), + 'field' => 'externalevent' + ] + ]; + + $matrix_options['title'] = __('Planning'); + $this->displayRightsChoiceMatrix($rights, $matrix_options); + $rights = [['itemtype' => 'Problem', 'label' => _n('Problem', 'Problems', Session::getPluralNumber()), 'field' => 'problem']]; @@ -1121,7 +1137,7 @@ function showFormTracking($openform = true, $closeform = true) { * @param $statuses all available statuses for the given cycle (obj::getAllStatusArray()) * @param $canedit can we edit the elements ? * - * @return nothing + * @return void **/ function displayLifeCycleMatrix($title, $html_field, $db_field, $statuses, $canedit) { @@ -1203,7 +1219,7 @@ function showFormLifeCycle($openform = true, $closeform = true) { * @param $db_field field inside the DB (to get current state) * @param $canedit can we edit the elements ? * - * @return nothing + * @return void **/ function displayLifeCycleMatrixTicketHelpdesk($title, $html_field, $db_field, $canedit) { @@ -2589,7 +2605,9 @@ static function dropdownRights(array $values, $name, $current, $options = []) { * - display : display or get string (default true) * - rand : specific rand (default is generated one) * - * @return nothing (print out an HTML select box) + * @return integer|string + * integer if option display=true (random part of elements id) + * string if option display=false (HTML code) **/ static function dropdownRight($name, $options = []) { diff --git a/inc/project.class.php b/inc/project.class.php index 191c5317e09..e49439d6cee 100644 --- a/inc/project.class.php +++ b/inc/project.class.php @@ -1354,7 +1354,7 @@ static function checkPlanAndRealDates($input) { /** * Print the HTML array children of a TreeDropdown * - * @return Nothing (display) + * @return void **/ function showChildren() { global $DB, $CFG_GLPI; @@ -1416,7 +1416,7 @@ function showChildren() { * - target for the Form * - withtemplate template or basic computer * - *@return Nothing (display) + *@return void **/ function showForm($ID, $options = []) { global $CFG_GLPI, $DB; diff --git a/inc/projectcost.class.php b/inc/projectcost.class.php index deff4e5c6e7..2756c4dfe6c 100644 --- a/inc/projectcost.class.php +++ b/inc/projectcost.class.php @@ -327,7 +327,7 @@ function showForm($ID, $options = []) { * @param $project Project object * @param $withtemplate boolean Template or basic item (default 0) * - * @return Nothing (call to classes members) + * @return void **/ static function showForProject(Project $project, $withtemplate = 0) { global $DB, $CFG_GLPI; diff --git a/inc/projecttask.class.php b/inc/projecttask.class.php index 6d3bb0c58f5..3c19446b36c 100644 --- a/inc/projecttask.class.php +++ b/inc/projecttask.class.php @@ -1071,7 +1071,7 @@ function rawSearchOptions() { * * @param $item Project or ProjectTask object * - * @return nothing + * @return void **/ static function showFor($item) { global $DB, $CFG_GLPI; @@ -1531,7 +1531,7 @@ function showDebug() { /** * Populate the planning with planned project tasks * - * @since 0.85 + * @since 9.1 * * @param $options array of possible options: * - who ID of the user (0 = undefined) @@ -1642,7 +1642,10 @@ static function populatePlanning($options = []) { if ($DB->numrows($result) > 0) { for ($i=0; $data=$DB->fetchAssoc($result); $i++) { if ($task->getFromDB($data["id"])) { - $key = $data["plan_start_date"]."$$$"."ProjectTask"."$$$".$data["id"]; + $key = $data["plan_start_date"]. + "$$$"."ProjectTask". + "$$$".$data["id"]. + "$$$".$who."$$$".$who_group; $interv[$key]['color'] = $options['color']; $interv[$key]['event_type_color'] = $options['event_type_color']; $interv[$key]['itemtype'] = 'ProjectTask'; @@ -1699,7 +1702,7 @@ static function populatePlanning($options = []) { * (default '') * @param $complete complete display (more details) (default 0) * - * @return Nothing (display function) + * @return string **/ static function displayPlanningItem(array $val, $who, $type = "", $complete = 0) { global $CFG_GLPI; diff --git a/inc/rack.class.php b/inc/rack.class.php index ba4555b7de0..85e4e074662 100644 --- a/inc/rack.class.php +++ b/inc/rack.class.php @@ -67,6 +67,7 @@ function defineTabs($options = []) { $this ->addStandardTab('Item_Rack', $ong, $options) ->addDefaultFormTab($ong) + ->addImpactTab($ong, $options) ->addStandardTab('Infocom', $ong, $options) ->addStandardTab('Contract_Item', $ong, $options) ->addStandardTab('Document_Item', $ong, $options) @@ -74,7 +75,6 @@ function defineTabs($options = []) { ->addStandardTab('Item_Problem', $ong, $options) ->addStandardTab('Change_Item', $ong, $options) ->addStandardTab('Log', $ong, $options); - ; return $ong; } diff --git a/inc/reminder.class.php b/inc/reminder.class.php index 86307d32762..e2e47d67617 100644 --- a/inc/reminder.class.php +++ b/inc/reminder.class.php @@ -331,41 +331,6 @@ static public function getVisibilityCriteria($forceall = false) { return $criteria; } - function post_addItem() { - // Add document if needed - $this->input = $this->addFiles($this->input, ['force_update' => true, - 'content_field' => 'text']); - - if (isset($this->fields["begin"]) && !empty($this->fields["begin"])) { - Planning::checkAlreadyPlanned($this->fields["users_id"], $this->fields["begin"], - $this->fields["end"], - ['Reminder' => [$this->fields['id']]]); - } - if (isset($this->input['_planningrecall'])) { - $this->input['_planningrecall']['items_id'] = $this->fields['id']; - PlanningRecall::manageDatas($this->input['_planningrecall']); - } - - } - - - /** - * @see CommonDBTM::post_updateItem() - **/ - function post_updateItem($history = 1) { - - if (isset($this->fields["begin"]) && !empty($this->fields["begin"])) { - Planning::checkAlreadyPlanned($this->fields["users_id"], $this->fields["begin"], - $this->fields["end"], - ['Reminder' => [$this->fields['id']]]); - } - if (in_array("begin", $this->updates)) { - PlanningRecall::managePlanningUpdates($this->getType(), $this->getID(), - $this->fields["begin"]); - } - - } - function rawSearchOptions() { $tab = []; @@ -578,105 +543,10 @@ static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtem } - /** - * @see CommonDBTM::prepareInputForAdd() - **/ - function prepareInputForAdd($input) { - - Toolbox::manageBeginAndEndPlanDates($input['plan']); - - $input["name"] = trim($input["name"]); - - if (empty($input["name"])) { - $input["name"] = __('Without title'); - } - - $input["begin"] = $input["end"] = "NULL"; - - if (isset($input['plan'])) { - if (!empty($input['plan']["begin"]) - && !empty($input['plan']["end"]) - && ($input['plan']["begin"] < $input['plan']["end"])) { - - $input['_plan'] = $input['plan']; - unset($input['plan']); - $input['is_planned'] = 1; - $input["begin"] = $input['_plan']["begin"]; - $input["end"] = $input['_plan']["end"]; - - } else { - Session::addMessageAfterRedirect( - __('Error in entering dates. The starting date is later than the ending date'), - false, ERROR); - } - } - - // set new date. - $input["date"] = $_SESSION["glpi_currenttime"]; - - return $input; - } - - - /** - * @see CommonDBTM::prepareInputForUpdate() - **/ - function prepareInputForUpdate($input) { - - Toolbox::manageBeginAndEndPlanDates($input['plan']); - - if (isset($input['_planningrecall'])) { - PlanningRecall::manageDatas($input['_planningrecall']); - } - - if (isset($input["name"])) { - $input["name"] = trim($input["name"]); - - if (empty($input["name"])) { - $input["name"] = __('Without title'); - } - } - - if (isset($input['plan'])) { - - if (!empty($input['plan']["begin"]) - && !empty($input['plan']["end"]) - && ($input['plan']["begin"] < $input['plan']["end"])) { - - $input['_plan'] = $input['plan']; - unset($input['plan']); - $input['is_planned'] = 1; - $input["begin"] = $input['_plan']["begin"]; - $input["end"] = $input['_plan']["end"]; - - } else { - Session::addMessageAfterRedirect( - __('Error in entering dates. The starting date is later than the ending date'), - false, ERROR); - } - } - - $input = $this->addFiles($input, ['content_field' => 'text']); - - return $input; - } - - - function pre_updateInDB() { - - // Set new user if initial user have been deleted - if (($this->fields['users_id'] == 0) - && ($uid = Session::getLoginUserID())) { - $this->fields['users_id'] = $uid; - $this->updates[] ="users_id"; - } - } - - function post_getEmpty() { - $this->fields["name"] = __('New note'); - $this->fields["users_id"] = Session::getLoginUserID(); + + parent::post_getEmpty(); } @@ -862,172 +732,6 @@ function showForm($ID, $options = []) { } - /** - * Populate the planning with planned reminder - * - * @param $options array of possible options: - * - who ID of the user (0 = undefined) - * - who_group ID of the group of users (0 = undefined) - * - begin Date - * - end Date - * - color - * - event_type_color - * - check_planned (boolean) - * - display_done_events (boolean) - * - * @return array of planning item - **/ - static function populatePlanning($options = []) { - global $DB, $CFG_GLPI; - - $default_options = [ - 'genical' => false, - 'color' => '', - 'event_type_color' => '', - 'check_planned' => false, - 'display_done_events' => true, - ]; - $options = array_merge($default_options, $options); - - $interv = []; - $reminder = new self; - - if (!isset($options['begin']) || ($options['begin'] == 'NULL') - || !isset($options['end']) || ($options['end'] == 'NULL')) { - return $interv; - } - - $who = $options['who']; - $who_group = $options['who_group']; - $begin = $options['begin']; - $end = $options['end']; - - if ($options['genical']) { - $_SESSION["glpiactiveprofile"][static::$rightname] = READ; - } - $visibility_criteria = self::getVisibilityCriteria(true); - $nreadpub = []; - $nreadpriv = []; - - // See public reminder ? - if (!$options['genical'] - && $who === Session::getLoginUserID() - && self::canView()) { - $nreadpub = $visibility_criteria['WHERE']; - } - unset($visibility_criteria['WHERE']); - - // See my private reminder ? - if (($who_group === "mine") || ($who === Session::getLoginUserID())) { - $nreadpriv = ['glpi_reminders.users_id' => Session::getLoginUserID()]; - } else { - if ($who > 0) { - $nreadpriv = ['glpi_reminders.users_id' => $who]; - } - if ($who_group > 0) { - $ngrouppriv = ['glpi_groups_reminders.groups_id' => $who_group]; - if (!empty($nreadpriv)) { - $nreadpriv['OR'] = [$nreadpriv, $ngrouppriv]; - } else { - $nreadpriv = $ngrouppriv; - } - } - } - - $NASSIGN = []; - - if (count($nreadpub) - && count($nreadpriv)) { - $NASSIGN = ['OR' => [$nreadpub, $nreadpriv]]; - } else if (count($nreadpub)) { - $NASSIGN = $nreadpub; - } else { - $NASSIGN = $nreadpriv; - } - - if (!count($NASSIGN)) { - return $interv; - } - - $WHERE = [ - 'glpi_reminders.is_planned' => 1, - 'begin' => ['<', $end], - 'end' => ['>', $begin] - ] + $NASSIGN; - - if ($options['check_planned']) { - $WHERE['state'] = ['!=', Planning::INFO]; - } - - if (!$options['display_done_events']) { - $WHERE['OR'] = [ - 'state' => Planning::TODO, - 'AND' => [ - 'state' => Planning::INFO, - 'end' => ['>', new \QueryExpression('NOW()')] - ] - ]; - } - - $table = self::getTable(); - $criteria = [ - 'SELECT' => "$table.*", - 'DISTINCT' => true, - 'FROM' => $table, - 'WHERE' => $WHERE, - 'ORDER' => 'begin' - ] + $visibility_criteria; - - $iterator = $DB->request($criteria); - - if (count($iterator)) { - while ($data = $iterator->next()) { - if ($reminder->getFromDB($data["id"]) - && $reminder->canViewItem()) { - $key = $data["begin"]."$$"."Reminder"."$$".$data["id"]; - $interv[$key]['color'] = $options['color']; - $interv[$key]['event_type_color'] = $options['event_type_color']; - $interv[$key]["itemtype"] = 'Reminder'; - $interv[$key]["reminders_id"] = $data["id"]; - $interv[$key]["id"] = $data["id"]; - - if (strcmp($begin, $data["begin"]) > 0) { - $interv[$key]["begin"] = $begin; - } else { - $interv[$key]["begin"] = $data["begin"]; - } - - if (strcmp($end, $data["end"]) < 0) { - $interv[$key]["end"] = $end; - } else { - $interv[$key]["end"] = $data["end"]; - } - $interv[$key]["name"] = Html::clean(Html::resume_text($data["name"], $CFG_GLPI["cut"])); - $interv[$key]["text"] - = Html::resume_text(Html::clean(Toolbox::unclean_cross_side_scripting_deep($data["text"])), - $CFG_GLPI["cut"]); - - $interv[$key]["users_id"] = $data["users_id"]; - $interv[$key]["state"] = $data["state"]; - $interv[$key]["state"] = $data["state"]; - if (!$options['genical']) { - $interv[$key]["url"] = Reminder::getFormURLWithID($data['id']); - } else { - $interv[$key]["url"] = $CFG_GLPI["url_base"]. - Reminder::getFormURLWithID($data['id'], false); - } - $interv[$key]["ajaxurl"] = $CFG_GLPI["root_doc"]."/ajax/planning.php". - "?action=edit_event_form". - "&itemtype=Reminder". - "&id=".$data['id']. - "&url=".$interv[$key]["url"]; - $interv[$key]["editable"] = $reminder->canUpdateItem(); - } - } - } - return $interv; - } - /** * Display a Planning Item @@ -1038,7 +742,7 @@ static function populatePlanning($options = []) { * (default '') * @param $complete complete display (more details) (default 0) * - * @return Nothing (display function) + * @return string **/ static function displayPlanningItem(array $val, $who, $type = "", $complete = 0) { global $CFG_GLPI; @@ -1089,7 +793,7 @@ static function displayPlanningItem(array $val, $who, $type = "", $complete = 0) * * @param $personal boolean : display reminders created by me ? (true by default) * - * @return Nothing (display function) + * @return void **/ static function showListForCentral($personal = true) { global $DB, $CFG_GLPI; diff --git a/inc/reservation.class.php b/inc/reservation.class.php index 2dd76469394..db306f6c145 100644 --- a/inc/reservation.class.php +++ b/inc/reservation.class.php @@ -275,7 +275,7 @@ function test_valid_date() { * @param $type error type : date / is_res / other * @param $ID ID of the item * - * @return nothing + * @return void **/ function displayError($type, $ID) { diff --git a/inc/rssfeed.class.php b/inc/rssfeed.class.php index 18c8d6302d0..6e9b054a42a 100644 --- a/inc/rssfeed.class.php +++ b/inc/rssfeed.class.php @@ -905,7 +905,7 @@ static function getRSSFeed($url, $cache_duration = DAY_TIMESTAMP) { * * @param $personal boolean display rssfeeds created by me ? (true by default) * - * @return Nothing (display function) + * @return void **/ static function showListForCentral($personal = true) { global $DB, $CFG_GLPI; diff --git a/inc/rule.class.php b/inc/rule.class.php index 3f9efa8f175..a34f6148159 100644 --- a/inc/rule.class.php +++ b/inc/rule.class.php @@ -847,7 +847,7 @@ static function getSpecificValueToSelect($field, $name = '', $values = '', array * - target filename : where to go when done. * - withtemplate boolean : template or basic item * - * @return nothing + * @return void **/ function showForm($ID, $options = []) { global $CFG_GLPI; diff --git a/inc/rulecollection.class.php b/inc/rulecollection.class.php index be4ffd9d140..aef1e229b01 100644 --- a/inc/rulecollection.class.php +++ b/inc/rulecollection.class.php @@ -404,7 +404,7 @@ function showEngineSummary() { * @param $target * @param $options array * - * @return nothing + * @return void **/ function showListRules($target, $options = []) { global $CFG_GLPI; @@ -584,7 +584,7 @@ function showListRules($target, $options = []) { * * @param $target * - * @return nothing + * @return void **/ function showAdditionalInformationsInForm($target) { } @@ -810,7 +810,7 @@ function moveRule($ID, $ref_ID, $type = 'after') { * * @since 0.85 * - * @return nothing (display) + * @return void **/ static function titleBackup() { global $CFG_GLPI; @@ -900,7 +900,7 @@ function duplicateRule($ID) { * * @since 0.85 * - * @return nothing, send attachment to browser + * @return void send attachment to browser **/ static function exportRulesToXML($items = []) { @@ -1003,7 +1003,7 @@ static function exportRulesToXML($items = []) { * * @since 0.85 * - * @return nothing (display) + * @return void **/ static function displayImportRulesForm() { @@ -1837,7 +1837,7 @@ function preProcessPreviewResults($output) { /** * Print a title if needed which will be displayed above list of rules * - * @return nothing (display) + * @return void **/ function title() { } diff --git a/inc/ruledictionnaryprintercollection.class.php b/inc/ruledictionnaryprintercollection.class.php index de2b1238c0e..56e0f86af56 100644 --- a/inc/ruledictionnaryprintercollection.class.php +++ b/inc/ruledictionnaryprintercollection.class.php @@ -324,7 +324,7 @@ function replayDictionnaryOnOnePrinter(array &$new_printers, array $res_rule, * @param $ID the old printer's id * @param $new_printers_id the new printer's id * - * @return nothing + * @return void **/ function moveDirectConnections($ID, $new_printers_id) { global $DB; diff --git a/inc/savedsearch.class.php b/inc/savedsearch.class.php index f836d12b6b3..2d17283a894 100644 --- a/inc/savedsearch.class.php +++ b/inc/savedsearch.class.php @@ -552,7 +552,7 @@ function prepareQueryToUse($type, $query_tab) { * * @param integer $ID ID of the saved search * - * @return nothing + * @return void **/ function load($ID) { global $CFG_GLPI; diff --git a/inc/search.class.php b/inc/search.class.php index 7ef4e20aa43..1428a4b203d 100755 --- a/inc/search.class.php +++ b/inc/search.class.php @@ -89,7 +89,7 @@ static function show($itemtype) { * @param $itemtype item type to manage * @param $params search params passed to prepareDatasForSearch function * - * @return nothing + * @return void **/ static function showList($itemtype, $params) { @@ -555,7 +555,7 @@ private static function hasHaving($criterias, $searchopt) { * * @param $data array of search datas prepared to generate SQL * - * @return nothing + * @return void **/ static function constructSQL(array &$data) { global $CFG_GLPI, $DB; @@ -1219,7 +1219,7 @@ static function constructAdditionalSqlForMetacriteria($criteria = [], * @param array $data array of search data prepared to get data * @param boolean $onlycount If we just want to count results * - * @return nothing + * @return void **/ static function constructData(array &$data, $onlycount = false) { if (!isset($data['sql']) || !isset($data['sql']['search'])) { @@ -1264,15 +1264,15 @@ static function constructData(array &$data, $onlycount = false) { } } - $data['data']['execution_time'] = $DBread->execution_time; - if (isset($data['search']['savedsearches_id'])) { - SavedSearch::updateExecutionTime( - (int)$data['search']['savedsearches_id'], - $DBread->execution_time - ); - } - if ($result) { + $data['data']['execution_time'] = $DBread->execution_time; + if (isset($data['search']['savedsearches_id'])) { + SavedSearch::updateExecutionTime( + (int)$data['search']['savedsearches_id'], + $DBread->execution_time + ); + } + $data['data']['totalcount'] = 0; // if real search or complete export : get numrows from request if (!$data['search']['no_search'] @@ -1512,7 +1512,7 @@ static function constructData(array &$data, $onlycount = false) { * * @param $data array of search datas prepared to get datas * - * @return nothing + * @return void **/ static function displayData(array &$data) { global $CFG_GLPI; @@ -2273,7 +2273,7 @@ static function getLogicalOperators($only_not = false) { * @param $itemtype type to display the form * @param $params array of parameters may include sort, is_deleted, criteria, metacriteria * - * @return nothing (displays) + * @return void **/ static function showGenericSearch($itemtype, array $params) { global $CFG_GLPI; @@ -6452,7 +6452,7 @@ static function giveItem($itemtype, $ID, array $data, $meta = 0, /** * Reset save searches * - * @return nothing + * @return void **/ static function resetSaveSearch() { diff --git a/inc/session.class.php b/inc/session.class.php index fcd2c08f461..9058681ebb0 100644 --- a/inc/session.class.php +++ b/inc/session.class.php @@ -304,7 +304,7 @@ static function initNavigateListItems($itemtype, $title = "") { * (default 'all') * @param boolean $is_recursive Also display sub entities of the active entity? (false by default) * - * @return Nothing + * @return boolean true on success, false on failure **/ static function changeActiveEntities($ID = "all", $is_recursive = false) { @@ -606,6 +606,8 @@ static function loadLanguage($forcelang = '', $with_plugins = true) { $TRANSLATE = new Zend\I18n\Translator\Translator; $TRANSLATE->setLocale($trytoload); + \Locale::setDefault($trytoload); + $cache = Config::getCache('cache_trans', 'core', false); if ($cache !== false && !defined('TU_USER')) { $TRANSLATE->setCache($cache); diff --git a/inc/slalevel.class.php b/inc/slalevel.class.php index 45fb307b9d4..58584ed1480 100644 --- a/inc/slalevel.class.php +++ b/inc/slalevel.class.php @@ -215,7 +215,7 @@ function getActions() { * @param $ID ID of the rule * @param $options array of possible options * - * @return nothing + * @return void **/ function showForm($ID, $options = []) { diff --git a/inc/slalevel_ticket.class.php b/inc/slalevel_ticket.class.php index 1791dab6d81..2f91c0376da 100644 --- a/inc/slalevel_ticket.class.php +++ b/inc/slalevel_ticket.class.php @@ -94,7 +94,7 @@ function getFromDBForTicket($ID, $slaType) { * * @since 9.1 2 parameters mandatory * - * @return nothing + * @return void **/ function deleteForTicket($tickets_id, $slaType) { global $DB; @@ -200,7 +200,7 @@ static function cronSlaTicket(CronTask $task) { * * @since 9.1 2 parameters mandatory * - * @return nothing + * @return void **/ static function doLevelForTicket(array $data, $slaType) { diff --git a/inc/software.class.php b/inc/software.class.php index 49c80540466..e321c2cde93 100644 --- a/inc/software.class.php +++ b/inc/software.class.php @@ -93,6 +93,7 @@ function defineTabs($options = []) { $ong = []; $this->addDefaultFormTab($ong); + $this->addImpactTab($ong, $options); $this->addStandardTab('SoftwareVersion', $ong, $options); $this->addStandardTab('SoftwareLicense', $ong, $options); $this->addStandardTab('Item_SoftwareVersion', $ong, $options); @@ -199,7 +200,7 @@ function cleanDBonPurge() { * * @since 0.85 * - * @return nothing + * @return void **/ static function updateValidityIndicator($ID) { @@ -701,7 +702,7 @@ function rawSearchOptions() { * @param $myname select name * @param $entity_restrict restrict to a defined entity * - * @return nothing (print out an HTML select box) + * @return integer random part of elements id **/ static function dropdownSoftwareToInstall($myname, $entity_restrict) { global $CFG_GLPI; @@ -731,7 +732,7 @@ static function dropdownSoftwareToInstall($myname, $entity_restrict) { * @param $myname select name * @param $entity_restrict restrict to a defined entity * - * @return nothing (print out an HTML select box) + * @return integer random part of elements id **/ static function dropdownLicenseToInstall($myname, $entity_restrict) { global $CFG_GLPI, $DB; @@ -931,7 +932,7 @@ function removeFromTrash($ID) { /** * Show softwares candidates to be merged with the current * - * @return nothing + * @return void **/ function showMergeCandidates() { global $DB, $CFG_GLPI; diff --git a/inc/softwarelicense.class.php b/inc/softwarelicense.class.php index d15f396cd68..8647f7b6a44 100644 --- a/inc/softwarelicense.class.php +++ b/inc/softwarelicense.class.php @@ -138,7 +138,7 @@ static function computeValidityIndicator($ID, $number = -1) { * * @since 0.85 * - * @return nothing + * @return void **/ static function updateValidityIndicator($ID) { @@ -1003,7 +1003,7 @@ static function countForSoftware($softwares_id) { * * @param $software Software object * - * @return nothing + * @return void **/ static function showForSoftware(Software $software) { global $DB, $CFG_GLPI; diff --git a/inc/softwareversion.class.php b/inc/softwareversion.class.php index 23019dd3213..1331b3b4464 100644 --- a/inc/softwareversion.class.php +++ b/inc/softwareversion.class.php @@ -216,7 +216,9 @@ function rawSearchOptions() { * - value : integer / value of the selected version * - used : array / already used items * - * @return nothing (print out an HTML select box) + * @return integer|string + * integer if option display=true (random part of elements id) + * string if option display=false (HTML code) **/ static function dropdownForOneSoftware($options = []) { global $CFG_GLPI, $DB; @@ -273,7 +275,7 @@ static function dropdownForOneSoftware($options = []) { * * @param $soft Software object * - * @return nothing + * @return void **/ static function showForSoftware(Software $soft) { global $DB, $CFG_GLPI; diff --git a/inc/solutiontemplate.class.php b/inc/solutiontemplate.class.php index 9b34fbb0661..5545bed860b 100644 --- a/inc/solutiontemplate.class.php +++ b/inc/solutiontemplate.class.php @@ -86,25 +86,4 @@ function rawSearchOptions() { return $tab; } - - - /** - * @see CommonDropdown::displaySpecificTypeField() - **/ - function displaySpecificTypeField($ID, $field = []) { - - switch ($field['type']) { - case 'tinymce' : - // Display empty field - echo " "; - // And a new line to have a complete display - echo ""; - $rand = mt_rand(); - Html::initEditorSystem($field['name'].$rand); - echo ""; - break; - } - } - } diff --git a/inc/supplier.class.php b/inc/supplier.class.php index 8a1a58337d1..0c139ea8e42 100644 --- a/inc/supplier.class.php +++ b/inc/supplier.class.php @@ -104,7 +104,7 @@ function defineTabs($options = []) { * - target form target * - withtemplate boolean : template or basic item * - *@return Nothing (display) + *@return void **/ function showForm($ID, $options = []) { @@ -453,7 +453,7 @@ function getLinks($withname = false) { /** * Print the HTML array for infocoms linked * - *@return Nothing (display) + *@return void * **/ function showInfocoms() { diff --git a/inc/ticket.class.php b/inc/ticket.class.php index a4ba89eb228..b18c00b9d24 100644 --- a/inc/ticket.class.php +++ b/inc/ticket.class.php @@ -832,11 +832,24 @@ static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtem function defineTabs($options = []) { + global $CFG_GLPI; + $ong = []; + $this->defineDefaultObjectTabs($ong, $options); $this->addStandardTab('TicketValidation', $ong, $options); $this->addStandardTab('KnowbaseItem_Item', $ong, $options); $this->addStandardTab('Item_Ticket', $ong, $options); + + // Enable impact tab if there is a valid linked item + foreach ($this->getLinkedItems() as $linkedItem) { + $class = $linkedItem['itemtype']; + if (isset($CFG_GLPI['impact_asset_types'][$class])) { + $this->addStandardTab('Impact', $ong, $options); + break; + } + } + $this->addStandardTab('TicketCost', $ong, $options); $this->addStandardTab('Itil_Project', $ong, $options); $this->addStandardTab('ProjectTask_Ticket', $ong, $options); @@ -851,7 +864,7 @@ function defineTabs($options = []) { /** * Retrieve data of the hardware linked to the ticket if exists * - * @return nothing : set computerfound to 1 if founded + * @return void **/ function getAdditionalDatas() { @@ -1943,7 +1956,7 @@ function post_addItem() { * * @param $input array : input array * - * @return nothing + * @return boolean **/ function manageValidationAdd($input) { @@ -2214,6 +2227,47 @@ function countActiveTicketsForItem($itemtype, $items_id) { 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' + ], + '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() . '.type' => $type, + 'NOT' => [ + $this->getTable() . '.status' => array_merge( + $this->getSolvedStatusArray(), + $this->getClosedStatusArray() + ) + ] + ] + ]); + } /** * Count solved tickets for an hardware last X days @@ -3418,7 +3472,7 @@ static function computeTco(CommonDBTM $item) { * @param $ticket_template boolean ticket template for preview : false if not used for preview * (false by default) * - * @return nothing (print the helpdesk) + * @return void **/ function showFormHelpdesk($ID, $ticket_template = false) { global $CFG_GLPI; @@ -7074,4 +7128,38 @@ public static function merge(int $merge_target_id, array $ticket_ids, array &$st } return true; } + + /** + * Get assets linked to this object + * + * @since 9.5 + * + * @param bool $addNames Insert asset names + * + * @return array + */ + public function getLinkedItems(bool $addNames = true) { + global $DB; + + $assets = $DB->request([ + 'SELECT' => ["id", "itemtype", "items_id"], + 'FROM' => "glpi_items_tickets", + 'WHERE' => ["tickets_id" => $this->getID()] + ]); + + $assets = iterator_to_array($assets); + + if ($addNames) { + foreach ($assets as $key => $asset) { + /** @var CommonDBTM $item */ + $item = new $asset['itemtype']; + $item->getFromDB($asset['items_id']); + + // Add name + $assets[$key]['name'] = $item->fields['name']; + } + } + + return $assets; + } } diff --git a/inc/ticket_ticket.class.php b/inc/ticket_ticket.class.php index c5c0317b5e6..18ff35efa3c 100644 --- a/inc/ticket_ticket.class.php +++ b/inc/ticket_ticket.class.php @@ -166,7 +166,7 @@ static function getLinkedTicketsTo ($ID) { * * @param $ID ID of the ticket id * - * @return nothing display + * @return void **/ static function displayLinkedTicketsTo ($ID) { global $DB, $CFG_GLPI; diff --git a/inc/ticketrecurrent.class.php b/inc/ticketrecurrent.class.php index dc58fc736dd..ba1ee185618 100644 --- a/inc/ticketrecurrent.class.php +++ b/inc/ticketrecurrent.class.php @@ -292,7 +292,7 @@ function rawSearchOptions() { /** * Show next creation date * - * @return nothing only display + * @return void **/ function showInfos() { diff --git a/inc/tickettemplate.class.php b/inc/tickettemplate.class.php index 61b5f586699..df03a108ea6 100644 --- a/inc/tickettemplate.class.php +++ b/inc/tickettemplate.class.php @@ -125,7 +125,7 @@ static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtem * * @param $tt ITILTemplate object * - * @return Nothing (call to classes members) + * @return void **/ static function showHelpdeskPreview(ITILTemplate $tt) { diff --git a/inc/toolbox.class.php b/inc/toolbox.class.php index 789d1983792..7f001b088eb 100644 --- a/inc/toolbox.class.php +++ b/inc/toolbox.class.php @@ -723,26 +723,16 @@ static function setDebugMode($mode = null, $debug_sql = null, $debug_vars = null // If debug mode activated : display some information if ($_SESSION['glpi_use_mode'] == Session::DEBUG_MODE) { - // display_errors only need for for E_ERROR, E_PARSE, ... which cannot be catched // Recommended development settings ini_set('display_errors', 'On'); - error_reporting(E_ALL | E_STRICT); + error_reporting(E_ALL); set_error_handler(['Toolbox','userErrorHandlerDebug']); - - } else { + } else if (!defined('TU_USER')) { // Recommended production settings ini_set('display_errors', 'Off'); - if (defined('TU_USER')) { - //do not set error_reporting to a low level for unit tests - error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT); - } + error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT); set_error_handler(['Toolbox', 'userErrorHandlerNormal']); } - - if (defined('TU_USER')) { - //user default error handler from tests - set_error_handler(null); - } } @@ -3215,4 +3205,10 @@ static function getPictureUrl($path) { return $CFG_GLPI["root_doc"] . '/front/document.send.php?file=_pictures/' . $path; } + + public static function throwBadRequest($message) { + http_response_code(400); + Toolbox::logWarning($message); + die(json_encode(["message" => $message])); + } } diff --git a/inc/transfer.class.php b/inc/transfer.class.php index c685c52faa2..b51f2382198 100644 --- a/inc/transfer.class.php +++ b/inc/transfer.class.php @@ -1043,7 +1043,7 @@ function simulateTransfer($items) { * * Transfer item to a new Item if $ID==$newID : only update entities_id field : * $ID!=$new ID -> copy datas (like template system) - * @return nothing (diplays) + * @return void **/ function transferItem($itemtype, $ID, $newID) { global $CFG_GLPI, $DB; diff --git a/inc/useremail.class.php b/inc/useremail.class.php index 3ec267051b4..fc5d455fbb8 100644 --- a/inc/useremail.class.php +++ b/inc/useremail.class.php @@ -192,7 +192,7 @@ function showChildForItemForm($canedit, $field_name, $id) { * * @param $user User object * - * @return nothing + * @return void **/ static function showForUser(User $user) { diff --git a/install/empty_data.php b/install/empty_data.php index 3d6549c0be5..2cece04d488 100644 --- a/install/empty_data.php +++ b/install/empty_data.php @@ -7083,10 +7083,42 @@ 'profiles_id' => '8', 'name' => 'cluster', 'rights' => 1, - + ], [ + 'profiles_id' => '1', + 'name' => 'externalevent', + 'rights' => 0, + ], [ + 'profiles_id' => '2', + 'name' => 'externalevent', + 'rights' => 1, + ], [ + 'profiles_id' => '3', + 'name' => 'externalevent', + 'rights' => 1055, + ], [ + 'profiles_id' => '4', + 'name' => 'externalevent', + 'rights' => 1055, + ], [ + 'profiles_id' => '5', + 'name' => 'externalevent', + 'rights' => 0, + ], [ + 'profiles_id' => '6', + 'name' => 'externalevent', + 'rights' => 1, + ], [ + 'profiles_id' => '7', + 'name' => 'externalevent', + 'rights' => 31, + ], [ + 'profiles_id' => '8', + 'name' => 'externalevent', + 'rights' => 1, ], ]; + $tables['glpi_profiles'] = [ [ 'id' => '1', diff --git a/install/mysql/glpi-empty.sql b/install/mysql/glpi-empty.sql index 24ac00b003b..598bc753787 100644 --- a/install/mysql/glpi-empty.sql +++ b/install/mysql/glpi-empty.sql @@ -1178,6 +1178,66 @@ CREATE TABLE `glpi_configs` ( UNIQUE KEY `unicity` (`context`,`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +### Dump table glpi_impacts + +DROP TABLE IF EXISTS `glpi_impactrelations`; +CREATE TABLE `glpi_impactrelations` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `itemtype_source` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci', + `items_id_source` INT(11) NOT NULL DEFAULT '0', + `itemtype_impacted` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci', + `items_id_impacted` INT(11) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `unicity` ( + `itemtype_source`, + `items_id_source`, + `itemtype_impacted`, + `items_id_impacted` + ), + KEY `source_asset` (`itemtype_source`, `items_id_source`), + KEY `impacted_asset` (`itemtype_impacted`, `items_id_impacted`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + + +### Dump table glpi_impacts_compounds + +DROP TABLE IF EXISTS `glpi_impactcompounds`; +CREATE TABLE `glpi_impactcompounds` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `name` VARCHAR(255) NULL DEFAULT '' COLLATE 'utf8_unicode_ci', + `color` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + + +### Dump table glpi_impacts_parent + +DROP TABLE IF EXISTS `glpi_impactitems`; +CREATE TABLE `glpi_impactitems` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `itemtype` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci', + `items_id` INT(11) NOT NULL DEFAULT '0', + `parent_id` INT(11) NOT NULL DEFAULT '0', + `zoom` FLOAT NOT NULL DEFAULT '0', + `pan_x` FLOAT NOT NULL DEFAULT '0', + `pan_y` FLOAT NOT NULL DEFAULT '0', + `impact_color` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci', + `depends_color` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci', + `impact_and_depends_color` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci', + `position_x` FLOAT NOT NULL DEFAULT '0', + `position_y` FLOAT NOT NULL DEFAULT '0', + `show_depends` TINYINT NOT NULL DEFAULT '1', + `show_impact` TINYINT NOT NULL DEFAULT '1', + `max_depth` INT(11) NOT NULL DEFAULT '5', + PRIMARY KEY (`id`), + UNIQUE KEY `unicity` ( + `itemtype`, + `items_id` + ), + KEY `source` (`itemtype`, `items_id`), + KEY `parent_id` (`parent_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + ### Dump table glpi_consumableitems DROP TABLE IF EXISTS `glpi_consumableitems`; @@ -2273,7 +2333,7 @@ CREATE TABLE `glpi_documents_items` ( `date_mod` timestamp NULL DEFAULT NULL, `users_id` int(11) DEFAULT '0', `timeline_position` tinyint(1) NOT NULL DEFAULT '0', - `date_creation` datetime DEFAULT NULL, + `date_creation` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `unicity` (`documents_id`,`itemtype`,`items_id`), KEY `item` (`itemtype`,`items_id`,`entities_id`,`is_recursive`), @@ -7583,3 +7643,78 @@ CREATE TABLE `glpi_items_clusters` ( UNIQUE KEY `unicity` (`clusters_id`,`itemtype`,`items_id`), KEY `item` (`itemtype`,`items_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +### Dump table glpi_planningexternalevents + +DROP TABLE IF EXISTS `glpi_planningexternalevents`; +CREATE TABLE `glpi_planningexternalevents` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `planningexternaleventtemplates_id` int(11) NOT NULL DEFAULT '0', + `entities_id` int(11) NOT NULL DEFAULT '0', + `date` timestamp NULL DEFAULT NULL, + `users_id` int(11) NOT NULL DEFAULT '0', + `groups_id` int(11) NOT NULL DEFAULT '0', + `name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, + `text` text COLLATE utf8_unicode_ci, + `begin` timestamp NULL DEFAULT NULL, + `end` timestamp NULL DEFAULT NULL, + `rrule` text COLLATE utf8_unicode_ci, + `state` int(11) NOT NULL DEFAULT '0', + `planningeventcategories_id` int(11) NOT NULL DEFAULT '0', + `background` tinyint(1) NOT NULL DEFAULT '0', + `date_mod` timestamp NULL DEFAULT NULL, + `date_creation` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `planningexternaleventtemplates_id` (`planningexternaleventtemplates_id`), + KEY `entities_id` (`entities_id`), + KEY `date` (`date`), + KEY `begin` (`begin`), + KEY `end` (`end`), + KEY `users_id` (`users_id`), + KEY `groups_id` (`groups_id`), + KEY `state` (`state`), + KEY `planningeventcategories_id` (`planningeventcategories_id`), + KEY `date_mod` (`date_mod`), + KEY `date_creation` (`date_creation`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +### Dump table glpi_planningexternaleventtemplates + +DROP TABLE IF EXISTS `glpi_planningexternaleventtemplates`; +CREATE TABLE `glpi_planningexternaleventtemplates` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `entities_id` int(11) NOT NULL DEFAULT '0', + `name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, + `text` text COLLATE utf8_unicode_ci, + `comment` text COLLATE utf8_unicode_ci, + `duration` int(11) NOT NULL DEFAULT '0', + `before_time` int(11) NOT NULL DEFAULT '0', + `rrule` text COLLATE utf8_unicode_ci, + `state` int(11) NOT NULL DEFAULT '0', + `planningeventcategories_id` int(11) NOT NULL DEFAULT '0', + `background` tinyint(1) NOT NULL DEFAULT '0', + `date_mod` timestamp NULL DEFAULT NULL, + `date_creation` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `entities_id` (`entities_id`), + KEY `state` (`state`), + KEY `planningeventcategories_id` (`planningeventcategories_id`), + KEY `date_mod` (`date_mod`), + KEY `date_creation` (`date_creation`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +### Dump table glpi_planningeventcategories + +DROP TABLE IF EXISTS `glpi_planningeventcategories`; +CREATE TABLE `glpi_planningeventcategories` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, + `color` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, + `comment` text COLLATE utf8_unicode_ci, + `date_mod` timestamp NULL DEFAULT NULL, + `date_creation` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `name` (`name`), + KEY `date_mod` (`date_mod`), + KEY `date_creation` (`date_creation`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; diff --git a/install/update.php b/install/update.php index 9a854917e90..68ff5b957e8 100644 --- a/install/update.php +++ b/install/update.php @@ -128,7 +128,7 @@ function update_importDropdown ($table, $name) { /** * Display the form of content update (addslashes compatibility (V0.4)) * - * @return nothing (displays) + * @return void */ function showContentUpdateForm() { $_SESSION['do_content_update'] = true; diff --git a/install/update_042_05.php b/install/update_042_05.php index bb49f38b51c..9405966f3e9 100644 --- a/install/update_042_05.php +++ b/install/update_042_05.php @@ -1379,7 +1379,7 @@ function dropMaintenanceField() { * @param $compDpdName string the name of the dropdown foreign key on glpi_computers (eg : hdtype, processor) * @param $specif string the name of the dropdown value entry on glpi_computer (eg : hdspace, processor_speed) optionnal argument. * - * @return nothing if everything is good, else display mysql query and error. + * @return void */ function compDpd2Device($devtype, $devname, $dpdname, $compDpdName, $specif = '') { global $DB; diff --git a/install/update_94_95.php b/install/update_94_95.php index 9062317aef3..2dd0c6d7d09 100644 --- a/install/update_94_95.php +++ b/install/update_94_95.php @@ -365,7 +365,7 @@ function update94to95() { /** Add "date_creation" field on document_items */ if (!$DB->fieldExists('glpi_documents_items', 'date_creation')) { - $migration->addField('glpi_documents_items', 'date_creation', 'datetime'); + $migration->addField('glpi_documents_items', 'date_creation', 'timestamp', ['null' => true]); $migration->addPostQuery( $DB->buildUpdate( 'glpi_documents_items', @@ -455,6 +455,104 @@ function update94to95() { $migration->addKey('glpi_tickettemplatepredefinedfields', 'tickettemplates_id'); /** /ITiL templates */ + /** /Add Externals events for planning */ + if (!$DB->tableExists('glpi_planningexternalevents')) { + $query = "CREATE TABLE `glpi_planningexternalevents` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `planningexternaleventtemplates_id` int(11) NOT NULL DEFAULT '0', + `entities_id` int(11) NOT NULL DEFAULT '0', + `date` timestamp NULL DEFAULT NULL, + `users_id` int(11) NOT NULL DEFAULT '0', + `groups_id` int(11) NOT NULL DEFAULT '0', + `name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, + `text` text COLLATE utf8_unicode_ci, + `begin` timestamp NULL DEFAULT NULL, + `end` timestamp NULL DEFAULT NULL, + `rrule` text COLLATE utf8_unicode_ci, + `state` int(11) NOT NULL DEFAULT '0', + `planningeventcategories_id` int(11) NOT NULL DEFAULT '0', + `background` tinyint(1) NOT NULL DEFAULT '0', + `date_mod` timestamp NULL DEFAULT NULL, + `date_creation` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `planningexternaleventtemplates_id` (`planningexternaleventtemplates_id`), + KEY `entities_id` (`entities_id`), + KEY `date` (`date`), + KEY `begin` (`begin`), + KEY `end` (`end`), + KEY `users_id` (`users_id`), + KEY `groups_id` (`groups_id`), + KEY `state` (`state`), + KEY `planningeventcategories_id` (`planningeventcategories_id`), + KEY `date_mod` (`date_mod`), + KEY `date_creation` (`date_creation`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci"; + $DB->queryOrDie($query, "add table glpi_planningexternalevents"); + + $new_rights = ALLSTANDARDRIGHT + PlanningExternalEvent::MANAGE_BG_EVENTS; + $migration->addRight('externalevent', $new_rights, [ + 'planning' => Planning::READMY + ]); + } + + // partial update (for developers) + if (!$DB->fieldExists('glpi_planningexternalevents', 'planningexternaleventtemplates_id')) { + $migration->addField('glpi_planningexternalevents', 'planningexternaleventtemplates_id', 'int', [ + 'after' => 'id' + ]); + $migration->addKey('glpi_planningexternalevents', 'planningexternaleventtemplates_id'); + } + + if (!$DB->tableExists('glpi_planningeventcategories')) { + $query = "CREATE TABLE `glpi_planningeventcategories` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, + `color` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, + `comment` text COLLATE utf8_unicode_ci, + `date_mod` timestamp NULL DEFAULT NULL, + `date_creation` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `name` (`name`), + KEY `date_mod` (`date_mod`), + KEY `date_creation` (`date_creation`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;"; + $DB->queryOrDie($query, "add table glpi_planningeventcategories"); + } + + // partial update (for developers) + if (!$DB->fieldExists('glpi_planningeventcategories', 'color')) { + $migration->addField("glpi_planningeventcategories", "color", "string", [ + 'after' => "name" + ] + ); + } + + if (!$DB->tableExists('glpi_planningexternaleventtemplates')) { + $query = "CREATE TABLE `glpi_planningexternaleventtemplates` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `entities_id` int(11) NOT NULL DEFAULT '0', + `name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, + `text` text COLLATE utf8_unicode_ci, + `comment` text COLLATE utf8_unicode_ci, + `duration` int(11) NOT NULL DEFAULT '0', + `before_time` int(11) NOT NULL DEFAULT '0', + `rrule` text COLLATE utf8_unicode_ci, + `state` int(11) NOT NULL DEFAULT '0', + `planningeventcategories_id` int(11) NOT NULL DEFAULT '0', + `background` tinyint(1) NOT NULL DEFAULT '0', + `date_mod` timestamp NULL DEFAULT NULL, + `date_creation` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `entities_id` (`entities_id`), + KEY `state` (`state`), + KEY `planningeventcategories_id` (`planningeventcategories_id`), + KEY `date_mod` (`date_mod`), + KEY `date_creation` (`date_creation`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci"; + $DB->queryOrDie($query, "add table glpi_planningexternaleventtemplates"); + } + /** /Add Externals events for planning */ + if (!$DB->fieldExists('glpi_entities', 'autopurge_delay')) { $migration->addField("glpi_entities", "autopurge_delay", "integer", [ 'after' => "autoclose_delay", @@ -604,6 +702,72 @@ function update94to95() { } /** /Add source item id to TicketTask. Used by tasks created by merging tickets */ + /** Impact analysis */ + // Impact config + $migration->addConfig(['impact_assets_list' => '[]']); + + // Impact dependencies + if (!$DB->tableExists('glpi_impactrelations')) { + $query = "CREATE TABLE `glpi_impactrelations` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `itemtype_source` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci', + `items_id_source` INT(11) NOT NULL DEFAULT '0', + `itemtype_impacted` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci', + `items_id_impacted` INT(11) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + UNIQUE KEY `unicity` ( + `itemtype_source`, + `items_id_source`, + `itemtype_impacted`, + `items_id_impacted` + ), + KEY `source_asset` (`itemtype_source`, `items_id_source`), + KEY `impacted_asset` (`itemtype_impacted`, `items_id_impacted`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci"; + $DB->queryOrDie($query, "add table glpi_impacts"); + } + + // Impact compounds + if (!$DB->tableExists('glpi_impactcompounds')) { + $query = "CREATE TABLE `glpi_impactcompounds` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `name` VARCHAR(255) NULL DEFAULT '' COLLATE 'utf8_unicode_ci', + `color` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci', + PRIMARY KEY (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci"; + $DB->queryOrDie($query, "add table glpi_impacts_compounds"); + } + + // Impact parents + if (!$DB->tableExists('glpi_impactitems')) { + $query = "CREATE TABLE `glpi_impactitems` ( + `id` INT(11) NOT NULL AUTO_INCREMENT, + `itemtype` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci', + `items_id` INT(11) NOT NULL DEFAULT '0', + `parent_id` INT(11) NOT NULL DEFAULT '0', + `zoom` FLOAT NOT NULL DEFAULT '0', + `pan_x` FLOAT NOT NULL DEFAULT '0', + `pan_y` FLOAT NOT NULL DEFAULT '0', + `impact_color` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci', + `depends_color` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci', + `impact_and_depends_color` VARCHAR(255) NOT NULL DEFAULT '' COLLATE 'utf8_unicode_ci', + `position_x` FLOAT NOT NULL DEFAULT '0', + `position_y` FLOAT NOT NULL DEFAULT '0', + `show_depends` TINYINT NOT NULL DEFAULT '1', + `show_impact` TINYINT NOT NULL DEFAULT '1', + `max_depth` INT(11) NOT NULL DEFAULT '5', + PRIMARY KEY (`id`), + UNIQUE KEY `unicity` ( + `itemtype`, + `items_id` + ), + KEY `source` (`itemtype`, `items_id`), + KEY `parent_id` (`parent_id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci"; + $DB->queryOrDie($query, "add table glpi_impacts_parent"); + } + /** /Impact analysis */ + // ************ Keep it at the end ************** foreach ($ADDTODISPLAYPREF as $type => $tab) { $rank = 1; diff --git a/js/impact.js b/js/impact.js new file mode 100644 index 00000000000..cf0f7e9f48d --- /dev/null +++ b/js/impact.js @@ -0,0 +1,2330 @@ +/** + * --------------------------------------------------------------------- + * GLPI - Gestionnaire Libre de Parc Informatique + * Copyright (C) 2015-2019 Teclib' and contributors. + * + * http://glpi-project.org + * + * based on GLPI - Gestionnaire Libre de Parc Informatique + * Copyright (C) 2003-2014 by the INDEPNET Development Team. + * + * --------------------------------------------------------------------- + * + * LICENSE + * + * This file is part of GLPI. + * + * GLPI is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * GLPI is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GLPI. If not, see . + * --------------------------------------------------------------------- + */ + + +// Load cytoscape +var cytoscape = window.cytoscape; + +// Needed for JS lint validation +/* global _ */ +/* global showMenu */ + +var GLPIImpact = { + + // Constants to represent nodes and edges + NODE: 1, + EDGE: 2, + + // Constants for graph direction (bitmask) + DEFAULT : 0, // 0b00 + FORWARD : 1, // 0b01 + BACKWARD: 2, // 0b10 + BOTH : 3, // 0b11 + + // Constants for graph edition mode + EDITION_DEFAULT : 1, + EDITION_ADD_NODE : 2, + EDITION_ADD_EDGE : 3, + EDITION_DELETE : 4, + EDITION_ADD_COMPOUND: 5, + + // Constants for ID separator + NODE_ID_SEPERATOR: "::", + EDGE_ID_SEPERATOR: "->", + + // Constants for delta action + DELTA_ACTION_ADD : 1, + DELTA_ACTION_UPDATE: 2, + DELTA_ACTION_DELETE: 3, + + // Store the initial state of the graph + initialState: null, + + // Store translated labels + locales: {}, + + // Store the visibility settings of the different direction of the graph + directionVisibility: {}, + + // Store color for egdes + edgeColors: {}, + + // Cytoscape instance + cy: null, + + // The impact network container + impactContainer: null, + + // The graph edition mode + editionMode: null, + + // Start node of the graph + startNode: null, + + // Form + form: null, + + // Maximum depth of the graph + maxDepth: 5, + + // Store registered dialogs and their inputs + dialogs: { + addNode: { + id: null, + inputs: { + itemType: null, + itemID : null + } + }, + configColor: { + id: null, + inputs: { + dependsColor : null, + impactColor : null, + impactAndDependsColor: null + } + }, + ongoingDialog: { + id: null + }, + editCompoundDialog: { + id: null, + inputs: { + name : null, + color: null + } + } + }, + + // Store registered toolbar items + toolbar: { + helpText : null, + tools : null, + save : null, + addNode : null, + addEdge : null, + addCompound : null, + deleteElement: null, + export : null, + expandToolbar: null, + toggleImpact : null, + toggleDepends: null, + colorPicker : null, + maxDepth : null, + maxDepthView : null, + }, + + // Data that needs to be stored/shared between events + eventData: { + addEdgeStart : null, // Store starting node of a new edge + tmpEles : null, // Temporary collection used when adding an edge + lastClick : null, // Store last click timestamp + boxSelected : [], + grabNodeStart: null, + boundingBox : null + }, + + /** + * Get network style + * + * @returns {Array} + */ + getNetworkStyle: function() { + return [ + { + selector: 'core', + style: { + 'selection-box-opacity' : '0.2', + 'selection-box-border-width': '0', + 'selection-box-color' : '#24acdf' + } + }, + { + selector: 'node:parent', + style: { + 'padding' : '30px', + 'shape' : 'roundrectangle', + 'border-width' : '1px', + 'background-opacity': '0.5', + 'font-size' : '1.1em', + 'background-color' : '#d2d2d2', + 'text-margin-y' : '20px', + 'text-opacity' : 0.7, + } + }, + { + selector: 'node:parent[label]', + style: { + 'label': 'data(label)', + } + }, + { + selector: 'node:parent[color]', + style: { + 'border-color' : 'data(color)', + 'background-color' : 'data(color)', + } + }, + { + selector: ':selected', + style: { + 'overlay-opacity': 0.2, + } + }, + { + selector: '[todelete=1]:selected', + style: { + 'overlay-opacity': 0.2, + 'overlay-color': 'red', + } + }, + { + selector: 'node[image]', + style: { + 'label' : 'data(label)', + 'shape' : 'rectangle', + 'background-color' : '#666', + 'background-image' : 'data(image)', + 'background-fit' : 'contain', + 'background-opacity': '0', + 'font-size' : '1em', + 'text-opacity' : 0.7, + } + }, + { + selector: '[hidden=1], [depth > ' + GLPIImpact.maxDepth + ']', + style: { + 'opacity': '0', + } + }, + { + selector: '[id="tmp_node"]', + style: { + 'opacity': '0', + } + }, + { + selector: 'edge', + style: { + 'width' : 1, + 'line-color' : this.edgeColors[0], + 'target-arrow-color': this.edgeColors[0], + 'target-arrow-shape': 'triangle', + 'arrow-scale' : 0.7, + 'curve-style' : 'bezier' + } + }, + { + selector: '[flag=' + GLPIImpact.FORWARD + ']', + style: { + 'line-color' : this.edgeColors[GLPIImpact.FORWARD], + 'target-arrow-color': this.edgeColors[GLPIImpact.FORWARD], + } + }, + { + selector: '[flag=' + GLPIImpact.BACKWARD + ']', + style: { + 'line-color' : this.edgeColors[GLPIImpact.BACKWARD], + 'target-arrow-color': this.edgeColors[GLPIImpact.BACKWARD], + } + }, + { + selector: '[flag=' + GLPIImpact.BOTH + ']', + style: { + 'line-color' : this.edgeColors[GLPIImpact.BOTH], + 'target-arrow-color': this.edgeColors[GLPIImpact.BOTH], + } + } + ]; + }, + + /** + * Get network layout + * + * @returns {Object} + */ + getPresetLayout: function () { + return { + name: 'preset', + positions: function(node) { + return { + x: parseFloat(node.data('position_x')), + y: parseFloat(node.data('position_y')), + }; + } + }; + }, + + /** + * Get network layout + * + * @returns {Object} + */ + getDagreLayout: function () { + return { + name: 'dagre', + rankDir: 'LR', + fit: false + }; + }, + + /** + * Get the current state of the graph + * + * @returns {Object} + */ + getCurrentState: function() { + var data = {edges: {}, compounds: {}, items: {}}; + + // Load edges + GLPIImpact.cy.edges().forEach(function(edge) { + data.edges[edge.data('id')] = { + source: edge.data('source'), + target: edge.data('target'), + }; + }); + + // Load compounds + GLPIImpact.cy.filter("node:parent").forEach(function(compound) { + data.compounds[compound.data('id')] = { + name: compound.data('label'), + color: compound.data('color'), + }; + }); + + // Load items + GLPIImpact.cy.filter("node:childless").forEach(function(node) { + data.items[node.data('id')] = { + impactitem_id: node.data('impactitem_id'), + parent : node.data('parent'), + position : node.position() + }; + }); + + return data; + }, + + /** + * Delta computation for edges + * + * @returns {Object} + */ + computeEdgeDelta: function(currentEdges) { + var edgesDelta = {}; + + // First iterate on the edges we had in the initial state + Object.keys(GLPIImpact.initialState.edges).forEach(function(edgeID) { + var edge = GLPIImpact.initialState.edges[edgeID]; + if (Object.prototype.hasOwnProperty.call(currentEdges, edgeID)) { + // If the edge is still here in the current state, nothing happened + // Remove it from the currentEdges data so we can skip it later + delete currentEdges[edgeID]; + } else { + // If the edge is missing in the current state, it has been deleted + var source = edge.source.split(GLPIImpact.NODE_ID_SEPERATOR); + var target = edge.target.split(GLPIImpact.NODE_ID_SEPERATOR); + edgesDelta[edgeID] = { + action : GLPIImpact.DELTA_ACTION_DELETE, + itemtype_source : source[0], + items_id_source : source[1], + itemtype_impacted: target[0], + items_id_impacted: target[1] + }; + } + }); + + // Now iterate on the edges we have in the current state + // Since we removed the edges that were not modified in the previous step, + // the remaining edges can only be new ones + Object.keys(currentEdges).forEach(function (edgeID) { + var edge = currentEdges[edgeID]; + var source = edge.source.split(GLPIImpact.NODE_ID_SEPERATOR); + var target = edge.target.split(GLPIImpact.NODE_ID_SEPERATOR); + edgesDelta[edgeID] = { + action : GLPIImpact.DELTA_ACTION_ADD, + itemtype_source : source[0], + items_id_source : source[1], + itemtype_impacted: target[0], + items_id_impacted: target[1] + }; + }); + + return edgesDelta; + }, + + /** + * Delta computation for compounds + * + * @returns {Object} + */ + computeCompoundsDelta: function(currentCompounds) { + var compoundsDelta = {}; + + // First iterate on the compounds we had in the initial state + Object.keys(GLPIImpact.initialState.compounds).forEach(function(compoundID) { + var compound = GLPIImpact.initialState.compounds[compoundID]; + if (Object.prototype.hasOwnProperty.call(currentCompounds, compoundID)) { + // If the compound is still here in the current state + var currentCompound = currentCompounds[compoundID]; + + // Check for updates ... + if (compound.name != currentCompound.name + || compound.color != currentCompound.color) { + compoundsDelta[compoundID] = { + action: GLPIImpact.DELTA_ACTION_UPDATE, + name : currentCompound.name, + color : currentCompound.color + }; + } + + // Remove it from the currentCompounds data + delete currentCompounds[compoundID]; + } else { + // If the compound is missing in the current state, it's been deleted + compoundsDelta[compoundID] = { + action : GLPIImpact.DELTA_ACTION_DELETE, + }; + } + }); + + // Now iterate on the compounds we have in the current state + Object.keys(currentCompounds).forEach(function (compoundID) { + compoundsDelta[compoundID] = { + action: GLPIImpact.DELTA_ACTION_ADD, + name : currentCompounds[compoundID].name, + color : currentCompounds[compoundID].color + }; + }); + + return compoundsDelta; + }, + + /** + * Delta computation for parents + * + * @returns {Object} + */ + computeItemsDelta: function(currentNodes) { + var itemsDelta = {}; + + // Now iterate on the parents we have in the current state + Object.keys(currentNodes).forEach(function (nodeID) { + var node = currentNodes[nodeID]; + itemsDelta[node.impactitem_id] = { + action : GLPIImpact.DELTA_ACTION_UPDATE, + parent_id: node.parent, + }; + + // Set parent to 0 if null + if (node.parent == undefined) { + node.parent = 0; + } + + if (nodeID == GLPIImpact.startNode) { + // Starting node of the graph, save viewport and edge colors + itemsDelta[node.impactitem_id] = { + action : GLPIImpact.DELTA_ACTION_UPDATE, + parent_id : node.parent, + position_x : node.position.x, + position_y : node.position.y, + zoom : GLPIImpact.cy.zoom(), + pan_x : GLPIImpact.cy.pan().x, + pan_y : GLPIImpact.cy.pan().y, + impact_color : GLPIImpact.edgeColors[GLPIImpact.FORWARD], + depends_color : GLPIImpact.edgeColors[GLPIImpact.BACKWARD], + impact_and_depends_color: GLPIImpact.edgeColors[GLPIImpact.BOTH], + show_depends : GLPIImpact.directionVisibility[GLPIImpact.BACKWARD], + show_impact : GLPIImpact.directionVisibility[GLPIImpact.FORWARD], + max_depth : GLPIImpact.maxDepth, + }; + } else { + // Others nodes of the graph, store only their parents and position + itemsDelta[node.impactitem_id] = { + action : GLPIImpact.DELTA_ACTION_UPDATE, + parent_id : node.parent, + position_x: node.position.x, + position_y: node.position.y, + }; + } + + }); + + return itemsDelta; + }, + + /** + * Compute the delta betwteen the initial state and the current state + * + * @returns {Object} + */ + computeDelta: function () { + // Store the delta for edges, compounds and parent + var result = {}; + + // Get the current state of the graph + var currentState = this.getCurrentState(); + + // Compute each deltas + result.edges = this.computeEdgeDelta(currentState.edges); + result.compounds = this.computeCompoundsDelta(currentState.compounds); + result.items = this.computeItemsDelta(currentState.items); + + return result; + }, + + /** + * Get the context menu items + * + * @returns {Array} + */ + getContextMenuItems: function(){ + return [ + { + id : 'goTo', + content : '' + this.getLocale("goTo"), + tooltipText : this.getLocale("goTo+"), + selector : 'node', + onClickFunction: this.menuOnGoTo + }, + { + id : 'showOngoing', + content : '' + this.getLocale("showOngoing"), + tooltipText : this.getLocale("showOngoing+"), + selector : 'node[hasITILObjects=1]', + onClickFunction: this.menuOnShowOngoing + }, + { + id : 'editCompound', + content : '' + this.getLocale("compoundProperties"), + tooltipText : this.getLocale("compoundProperties+"), + selector : 'node:parent', + onClickFunction: this.menuOnEditCompound + }, + { + id : 'removeFromCompound', + content : '' + this.getLocale("removeFromCompound"), + tooltipText : this.getLocale("removeFromCompound+"), + selector : 'node:child', + onClickFunction: this.menuOnRemoveFromCompound + }, + { + id : 'delete', + content : '' + this.getLocale("delete"), + tooltipText : this.getLocale("delete+"), + selector : 'node, edge', + onClickFunction: this.menuOnDelete + }, + { + id : 'new', + content : '' + this.getLocale("new"), + tooltipText : this.getLocale("new+"), + coreAsWell : true, + onClickFunction: this.menuOnNew + } + ]; + }, + + /** + * Build the add node dialog + * + * @param {string} itemID + * @param {string} itemType + * @param {Object} position x, y + * + * @returns {Object} + */ + getAddNodeDialog: function(itemID, itemType, position) { + // Build a new graph from the selected node and insert it + var buttonAdd = { + text: GLPIImpact.getLocale("add"), + click: function() { + var node = { + itemtype: $(itemID).val(), + items_id: $(itemType).val(), + }; + var nodeID = GLPIImpact.makeID(GLPIImpact.NODE, node.itemtype, node.items_id); + + // Check if the node is already on the graph + if (GLPIImpact.cy.filter('node[id="' + nodeID + '"]').length > 0) { + alert(GLPIImpact.getLocale("duplicateAsset")); + return; + } + + // Build the new subgraph + $.when(GLPIImpact.buildGraphFromNode(node)).then( + function (graph) { + // Insert the new graph data into the current graph + GLPIImpact.insertGraph(graph, { + id: nodeID, + x: position.x, + y: position.y + }); + GLPIImpact.updateFlags(); + $(GLPIImpact.dialogs.addNode.id).dialog("close"); + GLPIImpact.setEditionMode(GLPIImpact.EDITION_DEFAULT); + }, + function () { + // Ajax failed + alert(GLPIImpact.getLocale("unexpectedError")); + } + ); + } + }; + + // Exit edit mode + var buttonCancel = { + text: GLPIImpact.getLocale("cancel"), + click: function() { + $(this).dialog("close"); + GLPIImpact.setEditionMode(GLPIImpact.EDITION_DEFAULT); + } + }; + + return { + title: this.getLocale("newAsset"), + modal: true, + position: { + my: 'center', + at: 'center', + of: GLPIImpact.impactContainer + }, + buttons: [buttonAdd, buttonCancel] + }; + }, + + /** + * Build the color picker dialog + * + * @param {JQuery} backward + * @param {JQuery} forward + * @param {JQuery} both + * + * @returns {Object} + */ + getColorPickerDialog: function(backward, forward, both) { + // Update color fields to match saved values + $(GLPIImpact.dialogs.configColor.inputs.dependsColor).spectrum( + "set", + GLPIImpact.edgeColors[GLPIImpact.BACKWARD] + ); + $(GLPIImpact.dialogs.configColor.inputs.impactColor).spectrum( + "set", + GLPIImpact.edgeColors[GLPIImpact.FORWARD] + ); + $(GLPIImpact.dialogs.configColor.inputs.impactAndDependsColor).spectrum( + "set", + GLPIImpact.edgeColors[GLPIImpact.BOTH] + ); + + var buttonUpdate = { + text: "Update", + click: function() { + GLPIImpact.setEdgeColors({ + backward: backward.val(), + forward : forward.val(), + both : both.val(), + }); + GLPIImpact.updateStyle(); + $(this).dialog( "close" ); + GLPIImpact.cy.trigger("change"); + } + }; + + return { + modal: true, + width: 'auto', + position: { + my: 'center', + at: 'center', + of: GLPIImpact.impactContainer + }, + draggable: false, + title: this.getLocale("colorConfiguration"), + buttons: [buttonUpdate] + }; + }, + + /** + * Build the add node dialog + * + * @returns {Object} + */ + getOngoingDialog: function() { + return { + title: GLPIImpact.getLocale("ongoingTickets"), + modal: true, + position: { + my: 'center', + at: 'center', + of: GLPIImpact.impactContainer + }, + buttons: [] + }; + }, + + /** + * Build the add node dialog + * + * @param {string} itemID + * @param {string} itemType + * @param {Object} position x, y + * + * @returns {Object} + */ + getEditCompoundDialog: function(compound) { + // Reset inputs: + $(GLPIImpact.dialogs.editCompoundDialog.inputs.name).val( + compound.data('label') + ); + $(GLPIImpact.dialogs.editCompoundDialog.inputs.color).spectrum( + "set", + compound.data('color') + ); + + // Save group details + var buttonSave = { + text: GLPIImpact.getLocale("save"), + click: function() { + // Save compound name + compound.data( + 'label', + $(GLPIImpact.dialogs.editCompoundDialog.inputs.name).val() + ); + + // Save compound color + compound.data( + 'color', + $(GLPIImpact.dialogs.editCompoundDialog.inputs.color).val() + ); + + // Close dialog + $(this).dialog("close"); + GLPIImpact.cy.trigger("change"); + } + }; + + return { + title: GLPIImpact.getLocale("editGroup"), + modal: true, + position: { + my: 'center', + at: 'center', + of: GLPIImpact.impactContainer + }, + buttons: [buttonSave] + }; + }, + + /** + * Register the dialogs generated by the backend server + * + * @param {string} key + * @param {string} id + * @param {Object} inputs + */ + registerDialog: function(key, id, inputs) { + GLPIImpact.dialogs[key]['id'] = id; + if (inputs) { + Object.keys(inputs).forEach(function (inputKey){ + GLPIImpact.dialogs[key]['inputs'][inputKey] = inputs[inputKey]; + }); + } + }, + + /** + * Register the toolbar elements generated by the backend server + * + * @param {string} key + * @param {string} id + */ + registerToobar: function(key, id) { + GLPIImpact.toolbar[key] = id; + }, + + /** + * Create a tooltip for a toolbar's item + * + * @param {string} content + * + * @returns {Object} + */ + getTooltip: function(content) { + return { + position: { + my: 'bottom center', + at: 'top center' + }, + content: this.getLocale(content), + style: { + classes: 'qtip-shadow qtip-bootstrap' + }, + show: { + solo: true, + delay: 100 + }, + hide: { + fixed: true, + delay: 100 + } + }; + }, + + /** + * Initialise variables + * + * @param {JQuery} impactContainer + * @param {string} locales json + * @param {Object} colors properties: default, forward, backward, both + * @param {string} startNode + * @param {string} dialogs json + * @param {string} toolbar json + */ + prepareNetwork: function( + impactContainer, + locales, + colors, + startNode, + form, + dialogs, + toolbar + ) { + + // Set container + this.impactContainer = impactContainer; + + // Set locales from json + this.locales = JSON.parse(locales); + + // Init directionVisibility + this.directionVisibility[GLPIImpact.FORWARD] = true; + this.directionVisibility[GLPIImpact.BACKWARD] = true; + + // Set colors for edges + this.setEdgeColors(colors); + + // Set start node + this.startNode = startNode; + + // Register form + this.form = form; + + // Register dialogs + JSON.parse(dialogs).forEach(function(dialog) { + GLPIImpact.registerDialog(dialog.key, dialog.id, dialog.inputs); + }); + + // Register toolbars + JSON.parse(toolbar).forEach(function(element) { + GLPIImpact.registerToobar(element.key, element.id); + }); + this.initToolbar(); + }, + + /** + * Build the network graph + * + * @param {string} data (json) + */ + buildNetwork: function(data, params) { + // Init workspace status + GLPIImpact.showDefaultWorkspaceStatus(); + + // Apply custom colors if defined + if (params.impact_color != '') { + this.setEdgeColors({ + forward : params.impact_color, + backward: params.depends_color, + both : params.impact_and_depends_color, + }); + } + + // Preset layout + var layout = this.getPresetLayout(); + + // Init cytoscape + this.cy = cytoscape({ + container: this.impactContainer, + elements : data, + style : this.getNetworkStyle(), + layout : layout, + wheelSensitivity: 0.25, + }); + + // Store initial data + this.initialState = this.getCurrentState(); + + // Enable context menu + this.cy.contextMenus({ + menuItems: this.getContextMenuItems(), + menuItemClasses: [], + contextMenuClasses: [] + }); + + // Enable grid + this.cy.gridGuide({ + gridStackOrder: 0, + snapToGridOnRelease: true, + snapToGridDuringDrag: true, + gridSpacing: 12, + drawGrid: true, + panGrid: true, + }); + + // Apply saved visibility + if (!parseInt(params.show_depends)) { + GLPIImpact.toggleVisibility(GLPIImpact.BACKWARD); + } + if (!parseInt(params.show_impact)) { + GLPIImpact.toggleVisibility(GLPIImpact.FORWARD); + } + + // Apply max depth + this.maxDepth = params.max_depth; + this.updateFlags(); + + // Set viewport + if (params.zoom != '0') { + // If viewport params are set, apply them + this.cy.viewport({ + zoom: parseFloat(params.zoom), + pan: { + x: parseFloat(params.pan_x), + y: parseFloat(params.pan_y), + } + }); + + // Check viewport is not empty or contains only one item + var viewport = GLPIImpact.cy.extent(); + var empty = true; + GLPIImpact.cy.nodes().forEach(function(node) { + if (node.position().x > viewport.x1 + && node.position().x < viewport.x2 + && node.position().y > viewport.x1 + && node.position().y < viewport.x2 + ){ + empty = false; + } + }); + + if (empty || GLPIImpact.cy.filter("node:childless").length == 1) { + this.cy.fit(); + + if (this.cy.zoom() > 2.3) { + this.cy.zoom(2.3); + this.cy.center(); + } + } + } else { + // Else fit the graph and reduce zoom if needed + this.cy.fit(); + + if (this.cy.zoom() > 2.3) { + this.cy.zoom(2.3); + this.cy.center(); + } + } + + // Register events handlers for cytoscape object + this.cy.on('mousedown', 'node', this.nodeOnMousedown); + this.cy.on('mouseup', 'node', this.nodeOnMouseup); + this.cy.on('mousemove', this.onMousemove); + this.cy.on('mouseover', this.onMouseover); + this.cy.on('mouseout', this.onMouseout); + this.cy.on('click', this.onClick); + this.cy.on('click', 'edge', this.edgeOnClick); + this.cy.on('click', 'node', this.nodeOnClick); + this.cy.on('box', this.onBox); + this.cy.on('drag add remove pan zoom change', this.onChange); + this.cy.on('doubleClick', this.onDoubleClick); + + // Global events + $(document).keydown(this.onKeyDown); + + // Enter EDITION_DEFAULT mode + this.setEditionMode(GLPIImpact.EDITION_DEFAULT); + + // Init depth value + var text = GLPIImpact.maxDepth; + if (GLPIImpact.maxDepth >= 10) { + text = "infinity"; + } + $(GLPIImpact.toolbar.maxDepthView).html("Max depth: " + text); + $(GLPIImpact.toolbar.maxDepth).val(GLPIImpact.maxDepth); + }, + + /** + * Create ID for nodes and egdes + * + * @param {number} type (NODE or EDGE) + * @param {string} a + * @param {string} b + * + * @returns {string|null} + */ + makeID: function(type, a, b) { + switch (type) { + case GLPIImpact.NODE: + return a + "::" + b; + case GLPIImpact.EDGE: + return a + "->" + b; + } + + return null; + }, + + /** + * Helper to make an ID selector + * We can't use the short syntax "#id" because our ids contains + * non-alpha-numeric characters + * + * @param {string} id + * + * @returns {string} + */ + makeIDSelector: function(id) { + return "[id='" + id + "']"; + }, + + /** + * Reload the graph style + */ + updateStyle: function() { + this.cy.style(this.getNetworkStyle()); + // If either the source of the target node of an edge is hidden, hide the + // edge too by setting it's dept to the maximum value + this.cy.edges().forEach(function(edge) { + var source = GLPIImpact.cy.filter(GLPIImpact.makeIDSelector(edge.data('source'))); + var target = GLPIImpact.cy.filter(GLPIImpact.makeIDSelector(edge.data('target'))); + if (source.visible() && target.visible()) { + edge.data('depth', 0); + } else { + edge.data('depth', Number.MAX_VALUE); + } + }); + }, + + /** + * Update the flags of the edges of the graph + * Explore the graph forward then backward + */ + updateFlags: function() { + // Keep track of visited nodes + var exploredNodes; + + // Set all flag to the default value (0) + this.cy.edges().forEach(function(edge) { + edge.data("flag", 0); + }); + this.cy.nodes().data("depth", 0); + + // Run through the graph forward + exploredNodes = {}; + exploredNodes[this.startNode] = true; + this.exploreGraph(exploredNodes, GLPIImpact.FORWARD, this.startNode, 0); + + // Run through the graph backward + exploredNodes = {}; + exploredNodes[this.startNode] = true; + this.exploreGraph(exploredNodes, GLPIImpact.BACKWARD, this.startNode, 0); + + this.updateStyle(); + }, + + /** + * Toggle impact/depends visibility + * + * @param {*} toToggle + */ + toggleVisibility: function(toToggle) { + // Update toolbar icons + if (toToggle == GLPIImpact.FORWARD) { + $(GLPIImpact.toolbar.toggleImpact).find('i').toggleClass("fa-eye fa-eye-slash"); + } else { + $(GLPIImpact.toolbar.toggleDepends).find('i').toggleClass("fa-eye fa-eye-slash"); + } + + // Update visibility setting + GLPIImpact.directionVisibility[toToggle] = !GLPIImpact.directionVisibility[toToggle]; + + // Compute direction + var direction; + var forward = GLPIImpact.directionVisibility[GLPIImpact.FORWARD]; + var backward = GLPIImpact.directionVisibility[GLPIImpact.BACKWARD]; + + if (forward && backward) { + direction = GLPIImpact.BOTH; + } else if (!forward && backward) { + direction = GLPIImpact.BACKWARD; + } else if (forward && !backward) { + direction = GLPIImpact.FORWARD; + } else { + direction = 0; + } + + // Hide all nodes + GLPIImpact.cy.filter("node").data('hidden', 1); + + // Show/Hide edges according to the direction + GLPIImpact.cy.filter("edge").forEach(function(edge) { + if (edge.data('flag') & direction) { + edge.data('hidden', 0); + + // If the edge is visible, show the nodes they are connected to it + var sourceFilter = "node[id='" + edge.data('source') + "']"; + var targetFilter = "node[id='" + edge.data('target') + "']"; + GLPIImpact.cy.filter(sourceFilter + ", " + targetFilter) + .data("hidden", 0); + + // Make the parents of theses node visibles too + GLPIImpact.cy.filter(sourceFilter + ", " + targetFilter) + .parent() + .data("hidden", 0); + } else { + edge.data('hidden', 1); + } + }); + + // Start node should always be visible + GLPIImpact.cy.filter(GLPIImpact.makeIDSelector(GLPIImpact.startNode)) + .data("hidden", 0); + + GLPIImpact.updateStyle(); + }, + + /** + * Explore a graph in a given direction using recursion + * + * @param {Array} exploredNodes + * @param {number} direction + * @param {string} currentNodeID + * @param {number} depth + */ + exploreGraph: function(exploredNodes, direction, currentNodeID, depth) { + // Set node depth + var node = this.cy.filter(this.makeIDSelector(currentNodeID)); + if (node.data('depth') == 0 || node.data('depth') > depth) { + node.data('depth', depth); + } + + // If node has a parent, set it's depth too + if (node.isChild() && ( + node.parent().data('depth') == 0 || + node.parent().data('depth') > depth + )) { + node.parent().data('depth', depth); + } + + depth++; + + // Depending on the direction, we are looking for edge that either begin + // from the current node (source) or end on the current node (target) + var sourceOrTarget; + + // The next node is the opposite of sourceOrTarget : if our node is at + // the start (source) then the next is at the end (target) + var nextNode; + + switch (direction) { + case GLPIImpact.FORWARD: + sourceOrTarget = "source"; + nextNode = "target"; + break; + case GLPIImpact.BACKWARD: + sourceOrTarget = "target"; + nextNode = "source"; + break; + } + + // Find the edges connected to the current node + this.cy.elements('edge[' + sourceOrTarget + '="' + currentNodeID + '"]') + .forEach(function(edge) { + // Get target node from computer nextNode att name + var targetNode = edge.data(nextNode); + + // Set flag + edge.data("flag", direction | edge.data("flag")); + + // Check we haven't go through this node yet + if(exploredNodes[targetNode] == undefined) { + exploredNodes[targetNode] = true; + // Go to next node + GLPIImpact.exploreGraph(exploredNodes, direction, targetNode, depth); + } + }); + }, + + /** + * Get translated value for a given key + * + * @param {string} key + */ + getLocale: function(key) { + return this.locales[key]; + }, + + /** + * Ask the backend to build a graph from a specific node + * + * @param {Object} node + * @returns {Array|null} + */ + buildGraphFromNode: function(node) { + var dfd = jQuery.Deferred(); + + // Request to backend + $.ajax({ + type: "GET", + url: CFG_GLPI.root_doc + "/ajax/impact.php", + dataType: "json", + data: node, + success: function(data) { + dfd.resolve(JSON.parse(data.graph)); + }, + error: function () { + dfd.reject(); + } + }); + + return dfd.promise(); + }, + + + getDistance: function(a, b) { + return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2)); + }, + + /** + * Insert another new graph into the current one + * + * @param {Array} graph + * @param {Object} startNode data, x, y + */ + insertGraph: function(graph, startNode) { + var toAdd = []; + + // Find closest available space near the graph + var boundingBox = this.cy.filter().boundingBox(); + var distances = { + right: this.getDistance( + { + x: boundingBox.x2, + y: (boundingBox.y1 + boundingBox.y2) / 2 + }, + startNode + ), + left: this.getDistance( + { + x: boundingBox.x1, + y: (boundingBox.y1 + boundingBox.y2) / 2 + }, + startNode + ), + top: this.getDistance( + { + x: (boundingBox.x1 + boundingBox.x2) / 2, + y: boundingBox.y2 + }, + startNode + ), + bottom: this.getDistance( + { + x: (boundingBox.x1 + boundingBox.x2) / 2, + y: boundingBox.y1 + }, + startNode + ), + }; + var lowest = Math.min.apply(null, Object.values(distances)); + var direction = Object.keys(distances).filter(function (x) { + return distances[x] === lowest; + })[0]; + + // Try to add the new graph nodes + for (var i=0; i 0) { + continue; + } + // Store node to add them at once with a layout + toAdd.push(graph[i]); + } + + // Just place the node if only one result is found + if (toAdd.length == 1) { + toAdd[0].position = { + x: startNode.x, + y: startNode.y, + }; + + this.cy.add(toAdd); + return; + } + + // Add nodes and apply layout + var eles = this.cy.add(toAdd); + var options = GLPIImpact.getDagreLayout(); + + // Place the layout anywhere to compute it's bounding box + var layout = eles.layout(options); + layout.run(); + + var newGraphBoundingBox = eles.boundingBox(); + var startingPoint; + + // Now compute the real location where we want it + switch (direction) { + case 'right': + startingPoint = { + x: newGraphBoundingBox.x1, + y: (newGraphBoundingBox.y1 + newGraphBoundingBox.y2) / 2, + }; + break; + case 'left': + startingPoint = { + x: newGraphBoundingBox.x2, + y: (newGraphBoundingBox.y1 + newGraphBoundingBox.y2) / 2, + }; + break; + case 'top': + startingPoint = { + x: (newGraphBoundingBox.x1 + newGraphBoundingBox.x2) / 2, + y: newGraphBoundingBox.y1, + }; + break; + case 'bottom': + startingPoint = { + x: (newGraphBoundingBox.x1 + newGraphBoundingBox.x2) / 2, + y: newGraphBoundingBox.y2, + }; + break; + } + + newGraphBoundingBox.x1 += startNode.x - startingPoint.x; + newGraphBoundingBox.x2 += startNode.x - startingPoint.x; + newGraphBoundingBox.y1 += startNode.y - startingPoint.y; + newGraphBoundingBox.y2 += startNode.y - startingPoint.y; + + options.boundingBox = newGraphBoundingBox; + + // Apply layout again with correct bounding box + layout = eles.layout(options); + layout.run(); + + this.cy.animate({ + center: { + eles : GLPIImpact.cy.filter(""), + }, + }); + }, + + /** + * Set the colors + * + * @param {object} colors default, backward, forward, both + */ + setEdgeColors: function (colors) { + this.setColorIfExist(GLPIImpact.DEFAULT, colors.default); + this.setColorIfExist(GLPIImpact.BACKWARD, colors.backward); + this.setColorIfExist(GLPIImpact.FORWARD, colors.forward); + this.setColorIfExist(GLPIImpact.BOTH, colors.both); + }, + + /** + * Set color if exist + * + * @param {object} colors default, backward, forward, both + */ + setColorIfExist: function (index, color) { + if (color !== undefined) { + this.edgeColors[index] = color; + } + }, + + /** + * Exit current edition mode and enter a new one + * + * @param {number} mode + */ + setEditionMode: function (mode) { + // Switching to a mode we are already in -> go to default + if (this.editionMode == mode) { + mode = GLPIImpact.EDITION_DEFAULT; + } + + this.exitEditionMode(); + this.enterEditionMode(mode); + this.editionMode = mode; + }, + + /** + * Exit current edition mode + */ + exitEditionMode: function() { + switch (this.editionMode) { + case GLPIImpact.EDITION_DEFAULT: + GLPIImpact.cy.nodes().ungrabify(); + break; + + case GLPIImpact.EDITION_ADD_NODE: + $(this.toolbar.addNode).removeClass("active"); + break; + + case GLPIImpact.EDITION_ADD_EDGE: + $(GLPIImpact.toolbar.addEdge).removeClass("active"); + // Empty event data and remove tmp node + GLPIImpact.eventData.addEdgeStart = null; + GLPIImpact.cy.filter("#tmp_node").remove(); + break; + + case GLPIImpact.EDITION_DELETE: + this.cy.filter().unselect(); + this.cy.data('todelete', 0); + $(GLPIImpact.toolbar.deleteElement).removeClass("active"); + break; + + case GLPIImpact.EDITION_ADD_COMPOUND: + GLPIImpact.cy.panningEnabled(true); + GLPIImpact.cy.boxSelectionEnabled(false); + $(GLPIImpact.toolbar.addCompound).removeClass("active"); + break; + } + }, + + /** + * Enter a new edition mode + * + * @param {number} mode + */ + enterEditionMode: function(mode) { + switch (mode) { + case GLPIImpact.EDITION_DEFAULT: + this.clearHelpText(); + GLPIImpact.cy.nodes().grabify(); + $(this.impactContainer).css('cursor', "move"); + break; + + case GLPIImpact.EDITION_ADD_NODE: + this.showHelpText("addNodeHelpText"); + $(this.toolbar.addNode).addClass("active"); + $(this.impactContainer).css('cursor', "copy"); + break; + + case GLPIImpact.EDITION_ADD_EDGE: + this.showHelpText("addEdgeHelpText"); + $(this.toolbar.addEdge).addClass("active"); + $(this.impactContainer).css('cursor', "crosshair"); + break; + + case GLPIImpact.EDITION_DELETE: + this.cy.filter().unselect(); + this.showHelpText("deleteHelpText"); + $(this.toolbar.deleteElement).addClass("active"); + break; + + case GLPIImpact.EDITION_ADD_COMPOUND: + GLPIImpact.cy.panningEnabled(false); + GLPIImpact.cy.boxSelectionEnabled(true); + this.showHelpText("addCompoundHelpText"); + $(this.toolbar.addCompound).addClass("active"); + $(this.impactContainer).css('cursor', "crosshair"); + break; + } + }, + + /** + * Hide the toolbar and show an help text + * + * @param {string} text + */ + showHelpText: function(text) { + $(GLPIImpact.toolbar.helpText).html(this.getLocale(text)).show(); + }, + + /** + * Hide the help text and show the toolbar + */ + clearHelpText: function() { + $(GLPIImpact.toolbar.helpText).hide(); + }, + + /** + * Export the graph in the given format + * + * @param {string} format + * @param {boolean} transparentBackground (png only) + * + * @returns {Object} filename, filecontent + */ + download: function(format, transparentBackground) { + var filename; + var filecontent; + + // Create fake link + GLPIImpact.impactContainer.append(""); + var link = $('#impact_download'); + + switch (format) { + case 'png': + filename = "impact.png"; + filecontent = this.cy.png({ + bg: transparentBackground ? "transparent" : "white" + }); + break; + + case 'jpeg': + filename = "impact.jpeg"; + filecontent = this.cy.jpg(); + break; + } + + // Trigger download and remore the link + link.prop('download', filename); + link.prop("href", filecontent); + link[0].click(); + link.remove(); + }, + + /** + * Get node at target position + * + * @param {Object} position x, y + * @param {function} filter if false return null + */ + getNodeAt: function(position, filter) { + var nodes = this.cy.nodes(); + + for (var i=0; i position.x + && nodes[i].boundingBox().y1 < position.y + && nodes[i].boundingBox().y2 > position.y) { + // Check if the node is excluded + if (filter(nodes[i])) { + return nodes[i]; + } + } + } + + return null; + }, + + /** + * Enable the save button + */ + showCleanWorkspaceStatus: function() { + $(GLPIImpact.toolbar.save).removeClass('dirty'); + $(GLPIImpact.toolbar.save).addClass('clean'); + $(GLPIImpact.toolbar.save).find('i').removeClass("fas fa-exclamation-triangle"); + $(GLPIImpact.toolbar.save).find('i').addClass("fas fa-check"); + $(GLPIImpact.toolbar.save).find('i').qtip(GLPIImpact.getTooltip("workspaceSaved")); + }, + + /** + * Enable the save button + */ + showDirtyWorkspaceStatus: function() { + $(GLPIImpact.toolbar.save).removeClass('clean'); + $(GLPIImpact.toolbar.save).addClass('dirty'); + $(GLPIImpact.toolbar.save).find('i').removeClass("fas fa-check"); + $(GLPIImpact.toolbar.save).find('i').addClass("fas fa-exclamation-triangle"); + $(GLPIImpact.toolbar.save).find('i').qtip(this.getTooltip("unsavedChanges")); + }, + + /** + * Enable the save button + */ + showDefaultWorkspaceStatus: function() { + $(GLPIImpact.toolbar.save).removeClass('clean'); + $(GLPIImpact.toolbar.save).removeClass('dirty'); + $(GLPIImpact.toolbar.save).find('i').removeClass("fas fa-check"); + $(GLPIImpact.toolbar.save).find('i').removeClass("fas fa-exclamation-triangle"); + }, + + /** + * Build the ongoing dialog content according to the list of ITILObjects + * + * @param {Object} ITILObjects requests, incidents, changes, problems + * + * @returns {string} + */ + buildOngoingDialogContent: function(ITILObjects) { + return this.listElements("requests", ITILObjects.requests, "ticket") + + this.listElements("incidents", ITILObjects.incidents, "ticket") + + this.listElements("changes", ITILObjects.changes , "change") + + this.listElements("problems", ITILObjects.problems, "problem"); + }, + + /** + * Build an html list + * + * @param {string} title requests, incidents, changes, problems + * @param {string} elements requests, incidents, changes, problems + * @param {string} url key used to generate the URL + * + * @returns {string} + */ + listElements: function(title, elements, url) { + var html = ""; + + if (elements.length > 0) { + html += "

" + this.getLocale(title) + "

"; + html += "
    "; + + elements.forEach(function(element) { + var link = "./" + url + ".form.php?id=" + element.id; + html += '
  • ' + element.name + + '
  • '; + }); + html += "
"; + } + + return html; + }, + + /** + * Add a new compound from the selected nodes + */ + addCompoundFromSelection: _.debounce(function(){ + // Check that there is enough selected nodes + if (GLPIImpact.eventData.boxSelected.length < 2) { + alert(GLPIImpact.getLocale("notEnoughItems")); + } else { + // Create the compound + var newCompound = GLPIImpact.cy.add({group: 'nodes'}); + + // Set parent for coumpound member + GLPIImpact.eventData.boxSelected.forEach(function(ele) { + ele.move({'parent': newCompound.data('id')}); + }); + + // Show edit dialog + $(GLPIImpact.dialogs.editCompoundDialog.id).dialog( + GLPIImpact.getEditCompoundDialog(newCompound) + ); + + // Back to default mode + GLPIImpact.setEditionMode(GLPIImpact.EDITION_DEFAULT); + } + + // Clear the selection + GLPIImpact.eventData.boxSelected = []; + GLPIImpact.cy.filter(":selected").unselect(); + }, 100, false), + + /** + * Remove an element from the graph + * + * @param {object} ele + */ + deleteFromGraph: function(ele) { + if (ele.data('id') == GLPIImpact.startNode) { + alert("Can't remove starting node"); + return; + } + + if (ele.isEdge()) { + // Case 1: removing an edge + ele.remove(); + // this.cy.remove(impact.makeIDSelector(ele.data('id'))); + } else if (ele.isParent()) { + // Case 2: removing a compound + // Remove only the parent + ele.children().move({parent: null}); + ele.remove(); + + } else { + // Case 3: removing a node + // Remove parent if last child of a compound + if (!ele.isOrphan() && ele.parent().children().length <= 2) { + this.deleteFromGraph(ele.parent()); + } + + // Remove all edges connected to this node from graph and delta + ele.remove(); + } + + // Update flags + GLPIImpact.updateFlags(); + }, + + /** + * Handle global click events + * + * @param {JQuery.Event} event + */ + onClick: function (event) { + switch (GLPIImpact.editionMode) { + case GLPIImpact.EDITION_DEFAULT: + break; + + case GLPIImpact.EDITION_ADD_NODE: + // Click in EDITION_ADD_NODE : add a new node + $(GLPIImpact.dialogs.addNode.id).dialog(GLPIImpact.getAddNodeDialog( + GLPIImpact.dialogs.addNode.inputs.itemType, + GLPIImpact.dialogs.addNode.inputs.itemID, + event.position + )); + break; + + case GLPIImpact.EDITION_ADD_EDGE: + break; + + case GLPIImpact.EDITION_DELETE: + break; + } + }, + + /** + * Handle click on edge + * + * @param {JQuery.Event} event + */ + edgeOnClick: function (event) { + switch (GLPIImpact.editionMode) { + case GLPIImpact.EDITION_DEFAULT: + break; + + case GLPIImpact.EDITION_ADD_NODE: + break; + + case GLPIImpact.EDITION_ADD_EDGE: + break; + + case GLPIImpact.EDITION_DELETE: + // Remove the edge from the graph + GLPIImpact.deleteFromGraph(event.target); + break; + } + }, + + /** + * Handle click on node + * + * @param {JQuery.Event} event + */ + nodeOnClick: function (event) { + switch (GLPIImpact.editionMode) { + case GLPIImpact.EDITION_DEFAULT: + if (GLPIImpact.eventData.lastClick != null) { + // Trigger homemade double click event + if (event.timeStamp - GLPIImpact.eventData.lastClick < 500) { + event.target.trigger('doubleClick', event); + } + } + + GLPIImpact.eventData.lastClick = event.timeStamp; + break; + + case GLPIImpact.EDITION_ADD_NODE: + break; + + case GLPIImpact.EDITION_ADD_EDGE: + break; + + case GLPIImpact.EDITION_DELETE: + GLPIImpact.deleteFromGraph(event.target); + break; + } + }, + + /** + * Handle end of box selection event + * + * @param {JQuery.Event} event + */ + onBox: function (event) { + switch (GLPIImpact.editionMode) { + case GLPIImpact.EDITION_DEFAULT: + break; + + case GLPIImpact.EDITION_ADD_NODE: + break; + + case GLPIImpact.EDITION_ADD_EDGE: + break; + + case GLPIImpact.EDITION_DELETE: + break; + + case GLPIImpact.EDITION_ADD_COMPOUND: + var ele = event.target; + // Add node to selected list if he is not part of a compound already + if (ele.isNode() && ele.isOrphan() && !ele.isParent()) { + GLPIImpact.eventData.boxSelected.push(ele); + } + GLPIImpact.addCompoundFromSelection(); + break; + } + }, + + /** + * Handle any graph modification + * + * @param {*} event + */ + onChange: function() { + GLPIImpact.showDirtyWorkspaceStatus(); + }, + + /** + * Double click handler + * @param {JQuery.Event} event + */ + onDoubleClick: function(event) { + // Open edit dialog on compound nodes + if (event.target.isParent()) { + $(GLPIImpact.dialogs.editCompoundDialog.id).dialog( + GLPIImpact.getEditCompoundDialog(event.target) + ); + } + }, + + /** + * Handler for key down events + * + * @param {JQuery.Event} event + */ + onKeyDown: function(event) { + switch (event.which) { + // ESC + case 27: + // Exit specific edition mode + if (GLPIImpact.editionMode != GLPIImpact.EDITION_DEFAULT) { + GLPIImpact.setEditionMode(GLPIImpact.EDITION_DEFAULT); + } + break; + + // Delete + case 46: + // Delete selected elements + GLPIImpact.cy.filter(":selected").forEach(function(ele) { + GLPIImpact.deleteFromGraph(ele); + }); + break; + } + }, + + /** + * Handle mousedown events on nodes + * + * @param {JQuery.Event} event + */ + nodeOnMousedown: function (event) { + switch (GLPIImpact.editionMode) { + case GLPIImpact.EDITION_DEFAULT: + $(GLPIImpact.impactContainer).css('cursor', "grabbing"); + + // If we are not on a compound node or a node already inside one + if (event.target.isOrphan() && !event.target.isParent()) { + GLPIImpact.eventData.grabNodeStart = event.target; + } + break; + + case GLPIImpact.EDITION_ADD_NODE: + break; + + case GLPIImpact.EDITION_ADD_EDGE: + if (!event.target.isParent()) { + GLPIImpact.eventData.addEdgeStart = this.data('id'); + } + break; + + case GLPIImpact.EDITION_DELETE: + break; + + case GLPIImpact.EDITION_ADD_COMPOUND: + break; + } + }, + + /** + * Handle mouseup events on nodes + * + * @param {JQuery.Event} event + */ + nodeOnMouseup: function (event) { + switch (GLPIImpact.editionMode) { + case GLPIImpact.EDITION_DEFAULT: + $(GLPIImpact.impactContainer).css('cursor', "grab"); + + // Check if we were grabbing a node + if (GLPIImpact.eventData.grabNodeStart != null) { + // Reset eventData for node grabbing + GLPIImpact.eventData.grabNodeStart = null; + GLPIImpact.eventData.boundingBox = null; + } + + break; + + case GLPIImpact.EDITION_ADD_NODE: + break; + + case GLPIImpact.EDITION_ADD_EDGE: + // Exit if no start node + if (GLPIImpact.eventData.addEdgeStart == null) { + return; + } + + // Reset addEdgeStart + var startEdge = GLPIImpact.eventData.addEdgeStart; // Keep a copy to use later + GLPIImpact.eventData.addEdgeStart = null; + + // Remove current tmp collection + event.cy.remove(GLPIImpact.eventData.tmpEles); + GLPIImpact.eventData.tmpEles = null; + + // Option 1: Edge between a node and the fake tmp_node -> ignore + if (this.data('id') == 'tmp_node') { + return; + } + + // Option 2: Edge between two nodes that already exist -> ignore + var edgeID = GLPIImpact.makeID(GLPIImpact.EDGE, startEdge, this.data('id')); + if (event.cy.filter('edge[id="' + edgeID + '"]').length > 0) { + return; + } + + // Option 3: Both end of the edge are actually the same node -> ignore + if (startEdge == this.data('id')) { + return; + } + + // Option 4: Edge between two nodes that does not exist yet -> create it! + event.cy.add({ + group: 'edges', + data: { + id: edgeID, + source: startEdge, + target: this.data('id') + } + }); + + // Update dependencies flags according to the new link + GLPIImpact.updateFlags(); + break; + + case GLPIImpact.EDITION_DELETE: + break; + } + }, + + /** + * Handle mousemove events on nodes + * + * @param {JQuery.Event} event + */ + onMousemove: _.throttle(function(event) { + var node; + + switch (GLPIImpact.editionMode) { + case GLPIImpact.EDITION_DEFAULT: + // No action if we are not grabbing a node + if (GLPIImpact.eventData.grabNodeStart == null) { + return; + } + + // Look for a compound at the cursor position + node = GLPIImpact.getNodeAt(event.position, function(node) { + return node.isParent(); + }); + + if (node) { + // If we have a bounding box defined, the grabbed node is already + // being placed into a compound, we need to check if it was moved + // outside this original bouding box to know if the user is trying + // to move if away from the compound + if (GLPIImpact.eventData.boundingBox != null) { + // If the user tried to move out of the compound + if (GLPIImpact.eventData.boundingBox.x1 > event.position.x + || GLPIImpact.eventData.boundingBox.x2 < event.position.x + || GLPIImpact.eventData.boundingBox.y1 > event.position.y + || GLPIImpact.eventData.boundingBox.y2 < event.position.y) { + // Remove it from the compound + GLPIImpact.eventData.grabNodeStart.move({parent: null}); + GLPIImpact.eventData.boundingBox = null; + } + } else { + // If we found a compound, add the grabbed node inside + GLPIImpact.eventData.grabNodeStart.move({parent: node.data('id')}); + + // Store the original bouding box of the compound + GLPIImpact.eventData.boundingBox = node.boundingBox(); + } + } else { + // Else; reset it's parent so it can be removed from any temporary + // compound while the user is stil grabbing + GLPIImpact.eventData.grabNodeStart.move({parent: null}); + } + + break; + + case GLPIImpact.EDITION_ADD_NODE: + break; + + case GLPIImpact.EDITION_ADD_EDGE: + // No action if we are not placing an edge + if (GLPIImpact.eventData.addEdgeStart == null) { + return; + } + + // Remove current tmp collection + if (GLPIImpact.eventData.tmpEles != null) { + event.cy.remove(GLPIImpact.eventData.tmpEles); + } + + node = GLPIImpact.getNodeAt(event.position, function(node) { + var nodeID = node.data('id'); + + // Can't link to itself + if (nodeID == GLPIImpact.eventData.addEdgeStart) { + return false; + } + + // Can't link to parent + if (node.isParent()) { + return false; + } + + // The created edge shouldn't already exist + var edgeID = GLPIImpact.makeID(GLPIImpact.EDGE, GLPIImpact.eventData.addEdgeStart, nodeID); + if (GLPIImpact.cy.filter('edge[id="' + edgeID + '"]').length > 0) { + return false; + } + + // The node must be visible + if (GLPIImpact.cy.getElementById(nodeID).data('hidden')) { + return false; + } + + return true; + }); + + if (node != null) { + node = node.data('id'); + + // Add temporary edge to node hovered by the user + GLPIImpact.eventData.tmpEles = event.cy.add([ + { + group: 'edges', + data: { + id: GLPIImpact.makeID(GLPIImpact.EDGE, GLPIImpact.eventData.addEdgeStart, node), + source: GLPIImpact.eventData.addEdgeStart, + target: node + } + } + ]); + } else { + // Add temporary edge to a new invisible node at mouse position + GLPIImpact.eventData.tmpEles = event.cy.add([ + { + group: 'nodes', + data: { + id: 'tmp_node', + }, + position: { + x: event.position.x, + y: event.position.y + } + }, + { + group: 'edges', + data: { + id: GLPIImpact.makeID( + GLPIImpact.EDGE, + GLPIImpact.eventData.addEdgeStart, + "tmp_node" + ), + source: GLPIImpact.eventData.addEdgeStart, + target: 'tmp_node', + } + } + ]); + } + break; + + case GLPIImpact.EDITION_DELETE: + break; + } + }, 25), + + /** + * Handle global mouseover events + * + * @param {JQuery.Event} event + */ + onMouseover: function(event) { + switch (GLPIImpact.editionMode) { + case GLPIImpact.EDITION_DEFAULT: + if (event.target.data('id') == undefined || !event.target.isNode()) { + break; + } + $(GLPIImpact.impactContainer).css('cursor', "grab"); + break; + + case GLPIImpact.EDITION_ADD_NODE: + break; + + case GLPIImpact.EDITION_ADD_EDGE: + break; + + case GLPIImpact.EDITION_DELETE: + if (event.target.data('id') == undefined) { + break; + } + + $(GLPIImpact.impactContainer).css('cursor', "default"); + var id = event.target.data('id'); + + // Remove red overlay + event.cy.filter().data('todelete', 0); + event.cy.filter().unselect(); + + // Store here if one default node + if (event.target.data('id') == GLPIImpact.startNode) { + $(GLPIImpact.impactContainer).css('cursor', "not-allowed"); + break; + } + + // Add red overlay + event.target.data('todelete', 1); + event.target.select(); + + if (event.target.isNode()){ + var sourceFilter = "edge[source='" + id + "']"; + var targetFilter = "edge[target='" + id + "']"; + event.cy.filter(sourceFilter + ", " + targetFilter) + .data('todelete', 1) + .select(); + } + break; + } + }, + + /** + * Handle global mouseout events + * + * @param {JQuery.Event} event + */ + onMouseout: function(event) { + switch (GLPIImpact.editionMode) { + case GLPIImpact.EDITION_DEFAULT: + $(GLPIImpact.impactContainer).css('cursor', "move"); + break; + + case GLPIImpact.EDITION_ADD_NODE: + break; + + case GLPIImpact.EDITION_ADD_EDGE: + break; + + case GLPIImpact.EDITION_DELETE: + // Remove red overlay + $(GLPIImpact.impactContainer).css('cursor', "move"); + event.cy.filter().data('todelete', 0); + event.cy.filter().unselect(); + break; + } + }, + + /** + * Handle "goTo" menu event + * + * @param {JQuery.Event} event + */ + menuOnGoTo: function(event) { + window.open(event.target.data('link'), 'blank'); + }, + + /** + * Handle "showOngoing" menu event + * + * @param {JQuery.Event} event + */ + menuOnShowOngoing: function(event) { + $(GLPIImpact.dialogs.ongoingDialog.id).html( + GLPIImpact.buildOngoingDialogContent(event.target.data('ITILObjects')) + ); + $(GLPIImpact.dialogs.ongoingDialog.id).dialog(GLPIImpact.getOngoingDialog()); + }, + + /** + * Handle "EditCompound" menu event + * + * @param {JQuery.Event} event + */ + menuOnEditCompound: function (event) { + $(GLPIImpact.dialogs.editCompoundDialog.id).dialog( + GLPIImpact.getEditCompoundDialog(event.target) + ); + }, + + /** + * Handler for "removeFromCompound" action + * + * @param {JQuery.Event} event + */ + menuOnRemoveFromCompound: function(event) { + var parent = GLPIImpact.cy.getElementById( + event.target.data('parent') + ); + + // Remove node from compound + event.target.move({parent: null}); + + // Destroy compound if only one or zero member left + if (parent.children().length < 2) { + parent.children().move({parent: null}); + GLPIImpact.cy.remove(parent); + } + }, + + /** + * Handler for "delete" menu action + * + * @param {JQuery.Event} event + */ + menuOnDelete: function(event){ + GLPIImpact.deleteFromGraph(event.target); + }, + + /** + * Handler for "new" menu action + * + * @param {JQuery.Event} event + */ + menuOnNew: function(event) { + $(GLPIImpact.dialogs.addNode.id).dialog(GLPIImpact.getAddNodeDialog( + GLPIImpact.dialogs.addNode.inputs.itemType, + GLPIImpact.dialogs.addNode.inputs.itemID, + event.position + )); + }, + + /** + * Set event handler for toolbar events + */ + initToolbar: function() { + // Save the graph + $(GLPIImpact.toolbar.save).click(function() { + GLPIImpact.showCleanWorkspaceStatus(); + // Send data as JSON on submit + $.ajax({ + type: "POST", + url: $(GLPIImpact.form).prop('action'), + data: { + 'impacts': JSON.stringify(GLPIImpact.computeDelta()) + }, + success: function(){ + GLPIImpact.initialState = GLPIImpact.getCurrentState(); + }, + error: function(){ + GLPIImpact.showDirtyWorkspaceStatus(); + alert("error"); + }, + }); + }); + + // Add a new node on the graph + $(GLPIImpact.toolbar.addNode).click(function() { + GLPIImpact.setEditionMode(GLPIImpact.EDITION_ADD_NODE); + }); + $(GLPIImpact.toolbar.addNode).qtip(this.getTooltip("addNodeTooltip")); + + // Add a new edge on the graph + $(GLPIImpact.toolbar.addEdge).click(function() { + GLPIImpact.setEditionMode(GLPIImpact.EDITION_ADD_EDGE); + }); + $(GLPIImpact.toolbar.addEdge).qtip(this.getTooltip("addEdgeTooltip")); + + // Add a new compound on the graph + $(GLPIImpact.toolbar.addCompound).click(function() { + GLPIImpact.setEditionMode(GLPIImpact.EDITION_ADD_COMPOUND); + }); + $(GLPIImpact.toolbar.addCompound).qtip(this.getTooltip("addCompoundTooltip")); + + // Enter delete mode + $(GLPIImpact.toolbar.deleteElement).click(function() { + GLPIImpact.setEditionMode(GLPIImpact.EDITION_DELETE); + }); + $(GLPIImpact.toolbar.deleteElement).qtip(this.getTooltip("deleteTooltip")); + + // Export graph + $(GLPIImpact.toolbar.export).click(function() { + GLPIImpact.download( + 'png', + false + ); + }); + $(GLPIImpact.toolbar.export).qtip(this.getTooltip("downloadTooltip")); + + // "More" dropdown menu + $(GLPIImpact.toolbar.expandToolbar).click(showMenu); + + // Toggle impact visibility + $(GLPIImpact.toolbar.toggleImpact).click(function() { + GLPIImpact.toggleVisibility(GLPIImpact.FORWARD); + GLPIImpact.cy.trigger("change"); + }); + + // Toggle depends visibility + $(GLPIImpact.toolbar.toggleDepends).click(function() { + GLPIImpact.toggleVisibility(GLPIImpact.BACKWARD); + GLPIImpact.cy.trigger("change"); + }); + + // Color picker + $(GLPIImpact.toolbar.colorPicker).click(function() { + $(GLPIImpact.dialogs.configColor.id).dialog(GLPIImpact.getColorPickerDialog( + $(GLPIImpact.dialogs.configColor.inputs.dependsColor), + $(GLPIImpact.dialogs.configColor.inputs.impactColor), + $(GLPIImpact.dialogs.configColor.inputs.impactAndDependsColor) + )); + }); + + // Depth selector + $(GLPIImpact.toolbar.maxDepth).on('input', function() { + var max = $(GLPIImpact.toolbar.maxDepth).val(); + GLPIImpact.maxDepth = max; + + if (max == 10) { + max = "infinity"; + GLPIImpact.maxDepth = Number.MAX_VALUE; + } + + $(GLPIImpact.toolbar.maxDepthView).html("Max depth: " + max); + GLPIImpact.updateStyle(); + GLPIImpact.cy.trigger("change"); + }); + } +}; + + diff --git a/js/planning.js b/js/planning.js index 72d3d24ecb2..440134eb349 100644 --- a/js/planning.js +++ b/js/planning.js @@ -1,8 +1,8 @@ - /* global FullCalendar, FullCalendarLocales */ - -var calendar = null; -var GLPIPlanning = { +var GLPIPlanning = { + calendar: null, + all_resources: [], + visible_res: [], display: function(params) { // get passed options and merge it with default ones @@ -12,11 +12,14 @@ var GLPIPlanning = { full_view: true, default_view: 'timeGridWeek', height: GLPIPlanning.getHeight, + plugins: ['dayGrid', 'interaction', 'list', 'timeGrid', 'resourceTimeline', 'rrule'], + license_key: "", + resources: [], rand: '', header: { left: 'prev,next,today', center: 'title', - right: 'dayGridMonth, timeGridWeek, timeGridDay, listFull' + right: 'dayGridMonth, timeGridWeek, timeGridDay, listFull, resourceWeek' }, }; options = Object.assign({}, default_options, options); @@ -27,8 +30,14 @@ var GLPIPlanning = { var disable_qtip = false; var disable_edit = false; - calendar = new FullCalendar.Calendar(document.getElementById(dom_id), { - plugins: ['dayGrid', 'interaction', 'list', 'timeGrid'], + // manage visible resources + this.all_resources = options.resources; + this.visible_res = Object.keys(this.all_resources).filter(function(index) { + return GLPIPlanning.all_resources[index].is_visible; + }); + + this.calendar = new FullCalendar.Calendar(document.getElementById(dom_id), { + plugins: options.plugins, height: options.height, theme: true, weekNumbers: options.full_view ? true : false, @@ -37,9 +46,22 @@ var GLPIPlanning = { eventLimit: true, // show 'more' button when too mmany events minTime: CFG_GLPI.planning_begin, maxTime: CFG_GLPI.planning_end, + schedulerLicenseKey: options.license_key, + resourceAreaWidth: '15%', + nowIndicator: true, listDayAltFormat: false, agendaEventMinHeight: 13, header: options.header, + //resources: options.resources, + resources: function(fetchInfo, successCallback) { + // Filter resources by whether their id is in visible_res. + var filteredResources = []; + filteredResources = options.resources.filter(function(elem, index) { + return GLPIPlanning.visible_res.indexOf(index.toString()) !== -1; + }); + + successCallback(filteredResources); + }, views: { listFull: { type: 'list', @@ -53,6 +75,31 @@ var GLPIPlanning = { end: (new Date(currentDate.getTime())).setFullYear(current_year + 5) }; } + }, + resourceWeek: { + type: 'resourceTimeline', + buttonText: 'Timeline Week', + duration: { weeks: 1 }, + //hiddenDays: [6, 0], + groupByDateAndResource: true, + }, + }, + resourceRender: function(info) { + var icon = ""; + switch (info.resource._resource.extendedProps.itemtype.toLowerCase()) { + case "group": + case "group_user": + icon = "users"; + break; + case "user": + icon = "user"; + } + $(info.el) + .find('.fc-cell-text') + .prepend(' '); + + if (info.resource._resource.extendedProps.itemtype == 'Group_User') { + info.el.style.backgroundColor = 'lightgray'; } }, eventRender: function(info) { @@ -62,17 +109,28 @@ var GLPIPlanning = { var view = info.view; var eventtype_marker = ''; - element.find('.fc-content').after(eventtype_marker); - element.find('.fc-list-item-title > a').prepend(eventtype_marker); + element.append(eventtype_marker); var content = extProps.content; var tooltip = extProps.tooltip; if (view.type !== 'dayGridMonth' && view.type.indexOf('list') < 0 + && event.rendering != "background" && !event.allDay){ element.append('
'+content+'
'); } + // add icon if exists + if ("icon" in extProps) { + var icon_alt = ""; + if ("icon_alt" in extProps) { + icon_alt = extProps.icon_alt; + } + + element.find(".fc-title, .fc-list-item-title") + .append(" "); + } + // add classes to current event var added_classes = ''; if (typeof event.end !== 'undefined' @@ -139,9 +197,11 @@ var GLPIPlanning = { datesRender: function(info) { var view = info.view; - // force refetch events from ajax on view change (don't refetch on firt load) + // force refetch events from ajax on view change (don't refetch on first load) if (loaded) { - calendar.refetchEvents(); + GLPIPlanning.refresh(); + } else { + loaded = true; } // specific process for full list @@ -161,32 +221,30 @@ var GLPIPlanning = { // show controls buttons $('#planning .fc-left .fc-button-group').show(); } + + // set end of day markers for timeline + GLPIPlanning.setEndofDays(info.view); }, - eventAfterAllRender: function() { - // set a var to force refetch events (see viewRender callback) - loaded = true; - - // scroll div to first element needed to be viewed - var scrolltoevent = $('#'+dom_id+' .event_past.event_todo').first(); - if (scrolltoevent.length == 0) { - scrolltoevent = $('#'+dom_id+' .event_today').first(); - } - if (scrolltoevent.length == 0) { - scrolltoevent = $('#'+dom_id+' .event_future').first(); - } - if (scrolltoevent.length == 0) { - scrolltoevent = $('#'+dom_id+' .event_past').last(); - } - if (scrolltoevent.length) { - $('#'+dom_id+' .fc-scroller').scrollTop(scrolltoevent.prop('offsetTop')-25); - } + viewSkeletonRender : function(info) { + // inform backend we changed view (to store it in session) + $.ajax({ + url: CFG_GLPI.root_doc+"/ajax/planning.php", + type: 'POST', + data: { + action: 'view_changed', + view: info.view.type + } + }); + + // set end of day markers for timeline + GLPIPlanning.setEndofDays(info.view); }, events: { url: CFG_GLPI.root_doc+"/ajax/planning.php", type: 'POST', extraParams: function() { - var view_name = calendar - ? calendar.state.viewType + var view_name = GLPIPlanning.calendar + ? GLPIPlanning.calendar.state.viewType : options.default_view; var display_done_events = 1; if (view_name.indexOf('list') >= 0) { @@ -194,16 +252,17 @@ var GLPIPlanning = { } return { 'action': 'get_events', - 'display_done_events': display_done_events + 'display_done_events': display_done_events, + 'view_name': view_name }; }, success: function(data) { if (!options.full_view && data.length == 0) { - calendar.setOption('height', 0); + GLPIPlanning.calendar.setOption('height', 0); } }, - failure: function() { - console.error('there was an error while fetching events!'); + failure: function(error) { + console.error('there was an error while fetching events!', error); } }, @@ -211,9 +270,7 @@ var GLPIPlanning = { // EDIT EVENTS editable: true, // we can drag and resize events eventResize: function(info) { - var event = info.event; - var revertFunc = info.revert; - GLPIPlanning.editEventTimes(event, revertFunc); + GLPIPlanning.editEventTimes(info); }, eventResizeStart: function() { disable_edit = true; @@ -230,9 +287,7 @@ var GLPIPlanning = { }, eventDrop: function(info) { disable_qtip = false; - var event = info.event; - var revertFunc = info.revert; - GLPIPlanning.editEventTimes(event, revertFunc); + GLPIPlanning.editEventTimes(info); }, eventClick: function(info) { var event = info.event; @@ -246,7 +301,7 @@ var GLPIPlanning = { width: 'auto', height: 'auto', close: function() { - calendar.refetchEvents(); + GLPIPlanning.refresh(); } }) .load(ajaxurl, function() { @@ -258,10 +313,19 @@ var GLPIPlanning = { // ADD EVENTS selectable: true, - /*selectHelper: function(start, end) { - return $('
').text(start+' '+end); - },*/ // doesn't work anymore: see https://github.com/fullcalendar/fullcalendar/issues/2832 select: function(info) { + var itemtype = (((((info || {}) + .resource || {}) + ._resource || {}) + .extendedProps || {}) + .itemtype || {}); + + // prevent adding events on group users + if (itemtype === 'Group_User') { + GLPIPlanning.calendar.unselect(); + return false; + } + var start = info.start; var end = info.end; $('
').dialog({ @@ -281,6 +345,10 @@ var GLPIPlanning = { } ); }, + close: function() { + $(this).dialog("close"); + $(this).remove(); + }, position: { my: 'center', at: 'center', @@ -288,13 +356,13 @@ var GLPIPlanning = { } }); - calendar.unselect(); + GLPIPlanning.calendar.unselect(); } }); var loadedLocales = Object.keys(FullCalendarLocales); if (loadedLocales.length === 1) { - calendar.setOption('locale', loadedLocales[0]); + GLPIPlanning.calendar.setOption('locale', loadedLocales[0]); } $('.planning_on_central a') @@ -313,8 +381,8 @@ var GLPIPlanning = { window_focused = true; }; - window.calendar = calendar; // Required as object is not accessible by forms callback - calendar.render(); + //window.calendar = calendar; // Required as object is not accessible by forms callback + GLPIPlanning.calendar.render(); // attach button (planning and refresh) in planning header $('#'+dom_id+' .fc-toolbar .fc-center h2') @@ -325,16 +393,230 @@ var GLPIPlanning = { ); $('#refresh_planning').click(function() { - calendar.refetchEvents(); + GLPIPlanning.refresh(); }); // attach the date picker to planning GLPIPlanning.initFCDatePicker(); }, + refresh: function() { + if (typeof(GLPIPlanning.calendar.refetchResources) == 'function') { + GLPIPlanning.calendar.refetchResources(); + } + GLPIPlanning.calendar.refetchEvents(); + window.displayAjaxMessageAfterRedirect(); + }, + + // add/remove resource (like when toggling it in side bar) + toggleResource: function(res_name, active) { + // find the index of current resource to find it in our array of visible resources + var index = GLPIPlanning.all_resources.findIndex(function(current) { + return current.id == res_name; + }); + + if (index !== -1) { + // add only if not already present + if (active && GLPIPlanning.visible_res.indexOf(index.toString()) === -1) { + GLPIPlanning.visible_res.push(index.toString()); + } else if (!active) { + GLPIPlanning.visible_res.splice(GLPIPlanning.visible_res.indexOf(index.toString()), 1); + } + } + }, + + setEndofDays: function(view) { + // add a class to last col of day in timeline view + // to visualy separate days + if (view.constructor.name === "ResourceTimelineView") { + // compute the number of hour slots displayed + var time_beg = CFG_GLPI.planning_begin.split(':'); + var time_end = CFG_GLPI.planning_end.split(':'); + var int_beg = parseInt(time_beg[0]) * 60 + parseInt(time_beg[1]); + var int_end = parseInt(time_end[0]) * 60 + parseInt(time_end[1]); + var sec_inter = int_end - int_beg; + var nb_slots = Math.ceil(sec_inter / 60); + + // add class to day list header + $('#planning .fc-time-area.fc-widget-header table tr:nth-child(2) th') + .addClass('end-of-day'); + + // add class to hours list header + $('#planning .fc-time-area.fc-widget-header table tr:nth-child(3) th:nth-child('+nb_slots+'n)') + .addClass('end-of-day'); + + // add class to content bg (content slots) + $('#planning .fc-time-area.fc-widget-content table td:nth-child('+nb_slots+'n)') + .addClass('end-of-day'); + } + }, + planningFilters: function() { + $('#planning_filter a.planning_add_filter' ).on( 'click', function( e ) { + e.preventDefault(); // to prevent change of url on anchor + var url = $(this).attr('href'); + $('
').dialog({ + modal: true, + open: function () { + $(this).load(url); + }, + position: { + my: 'top', + at: 'center', + of: $('#planning_filter') + } + }); + }); + + $('#planning_filter .filter_option').on( 'click', function() { + $(this).children('ul').toggle(); + }); + + $(document).click(function(e){ + if ($(e.target).closest('#planning_filter .filter_option').length === 0) { + $('#planning_filter .filter_option ul').hide(); + } + }); + + $('#planning_filter .delete_planning').on( 'click', function() { + var deleted = $(this); + var li = deleted.closest('ul.filters > li'); + $.ajax({ + url: CFG_GLPI.root_doc+"/ajax/planning.php", + type: 'POST', + data: { + action: 'delete_filter', + filter: deleted.attr('value'), + type: li.attr('event_type') + }, + success: function() { + li.remove(); + GLPIPlanning.refresh(); + } + }); + }); + + var sendDisplayEvent = function(current_checkbox, refresh_planning) { + var current_li = current_checkbox.parents('li'); + var parent_name = null; + if (current_li.parent('ul.group_listofusers').length == 1) { + parent_name = current_li + .parent('ul.group_listofusers') + .parent('li') + .attr('event_name'); + } + var event_name = current_li.attr('event_name'); + var event_type = current_li.attr('event_type'); + var checked = current_checkbox.is(':checked'); + + return $.ajax({ + url: CFG_GLPI.root_doc+"/ajax/planning.php", + type: 'POST', + data: { + action: 'toggle_filter', + name: event_name, + type: event_type, + parent: parent_name, + display: checked + }, + success: function() { + GLPIPlanning.toggleResource(event_name, checked); + + if (refresh_planning) { + // don't refresh planning if event triggered from parent checkbox + GLPIPlanning.refresh(); + } + } + }); + }; + + $('#planning_filter li:not(li.group_users) input[type="checkbox"]') + .on( 'click', function() { + sendDisplayEvent($(this), true); + }); + + $('#planning_filter li.group_users > span > input[type="checkbox"]') + .on('change', function() { + var parent_checkbox = $(this); + var parent_li = parent_checkbox.parents('li'); + var checked = parent_checkbox.prop('checked'); + var event_name = parent_li.attr('event_name'); + var chidren_checkboxes = parent_checkbox + .parents('li.group_users') + .find('ul.group_listofusers input[type="checkbox"]'); + chidren_checkboxes.prop('checked', checked); + var promises = []; + chidren_checkboxes.each(function() { + promises.push(sendDisplayEvent($(this), false)); + }); + + GLPIPlanning.toggleResource(event_name, checked); + + // refresh planning once for all checkboxes (and not for each) + // after theirs promises done + $.when.apply($, promises).then(function() { + GLPIPlanning.refresh(); + }); + }); + + $('#planning_filter .color_input input').on('change', function(e, color) { + var current_li = $(this).parents('li'); + var parent_name = null; + if (current_li.length >= 1) { + parent_name = current_li.eq(1).attr('event_name'); + current_li = current_li.eq(0); + } + $.ajax({ + url: CFG_GLPI.root_doc+"/ajax/planning.php", + type: 'POST', + data: { + action: 'color_filter', + name: current_li.attr('event_name'), + type: current_li.attr('event_type'), + parent: parent_name, + color: color.toHexString() + }, + success: function() { + GLPIPlanning.refresh(); + } + }); + }); + + $('#planning_filter li.group_users .toggle').on('click', function() { + $(this).parent().toggleClass('expanded'); + }); + + $('#planning_filter_toggle > a.toggle').on('click', function() { + $('#planning_filter_content').animate({ width:'toggle' }, 300, 'swing', function() { + $('#planning_filter').toggleClass('folded'); + $('#planning_container').toggleClass('folded'); + }); + }); + }, + // send ajax for event storage (on event drag/resize) - editEventTimes: function(event, revertFunc) { - var extProps = event.extendedProps; + editEventTimes: function(info) { + var event = info.event; + var revertFunc = info.revert; + var extProps = event.extendedProps; + + var old_itemtype = null; + var old_items_id = null; + var new_itemtype = null; + var new_items_id = null; + + // manage moving the events between resources (users, groups) + if ("newResource" in info + && info.newResource !== null) { + var new_extProps = info.newResource._resource.extendedProps; + new_itemtype = new_extProps.itemtype; + new_items_id = new_extProps.items_id; + } + if ("oldResource" in info + && info.oldResource !== null) { + var old_extProps = info.oldResource._resource.extendedProps; + old_itemtype = old_extProps.itemtype; + old_items_id = old_extProps.items_id; + } var start = event.start; var end = event.end; @@ -351,18 +633,21 @@ var GLPIPlanning = { url: CFG_GLPI.root_doc+"/ajax/planning.php", type: 'POST', data: { - action: 'update_event_times', - start: start.toISOString(), - end: end.toISOString(), - itemtype: extProps.itemtype, - items_id: extProps.items_id + action: 'update_event_times', + start: start.toISOString(), + end: end.toISOString(), + itemtype: extProps.itemtype, + items_id: extProps.items_id, + new_actor_itemtype: new_itemtype, + new_actor_items_id: new_items_id, + old_actor_itemtype: old_itemtype, + old_actor_items_id: old_items_id, }, success: function(html) { if (!html) { revertFunc(); } - calendar.refetchEvents(); - window.displayAjaxMessageAfterRedirect(); + GLPIPlanning.refresh(); }, error: function() { revertFunc(); @@ -381,7 +666,7 @@ var GLPIPlanning = { dateFormat: 'DD, d MM, yy', onSelect: function() { var selected_date = $(this).datepicker('getDate'); - calendar.gotoDate(selected_date); + GLPIPlanning.calendar.gotoDate(selected_date); } }).next('.ui-datepicker-trigger').addClass('pointer'); diff --git a/lib/bundles/cytoscape.js b/lib/bundles/cytoscape.js new file mode 100644 index 00000000000..7bffa8fe28c --- /dev/null +++ b/lib/bundles/cytoscape.js @@ -0,0 +1,38 @@ +/** + * --------------------------------------------------------------------- + * GLPI - Gestionnaire Libre de Parc Informatique + * Copyright (C) 2015-2018 Teclib' and contributors. + * + * http://glpi-project.org + * + * based on GLPI - Gestionnaire Libre de Parc Informatique + * Copyright (C) 2003-2014 by the INDEPNET Development Team. + * + * --------------------------------------------------------------------- + * + * LICENSE + * + * This file is part of GLPI. + * + * GLPI is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * GLPI is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GLPI. If not, see . + * --------------------------------------------------------------------- + */ + +// Cytoscape.js lib +window.cytoscape = require('cytoscape'); +window._ = require('lodash'); +require('cytoscape-context-menus'); +require('cytoscape-context-menus/cytoscape-context-menus.css'); +require('cytoscape-grid-guide'); +require('cytoscape-dagre'); diff --git a/lib/bundles/fullcalendar.js b/lib/bundles/fullcalendar.js index 7d8ba8a4e51..8c33fb0eb3e 100644 --- a/lib/bundles/fullcalendar.js +++ b/lib/bundles/fullcalendar.js @@ -28,6 +28,8 @@ * along with GLPI. If not, see . * --------------------------------------------------------------------- */ +// RRule dependency +window.rrule = require('rrule'); // Fullcalendar library require('@fullcalendar/core'); @@ -35,6 +37,7 @@ require('@fullcalendar/daygrid'); require('@fullcalendar/interaction'); require('@fullcalendar/list'); require('@fullcalendar/timegrid'); +require('@fullcalendar/rrule'); require('@fullcalendar/core/main.css'); require('@fullcalendar/daygrid/main.css'); diff --git a/package-lock.json b/package-lock.json index 77a8d8dca6b..74bb95c3464 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,11 @@ "resolved": "https://registry.npmjs.org/@fullcalendar/list/-/list-4.3.0.tgz", "integrity": "sha512-Al+XaKynC4RsJpXyWOWJgrVQXFOSC+fg8DI4HGAt/mrrwazZ1L0pTQwCLeU8auZp53VFazCiNxV+jkAbuDdbDQ==" }, + "@fullcalendar/rrule": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@fullcalendar/rrule/-/rrule-4.3.0.tgz", + "integrity": "sha512-80nPrIW0hA/vtT2VcDzSzk2yQ/bqdJB4zCiVhrGAg3+DtVxnormElTQQg84Iw1q1WeouT9lo2FYzG9B4SIDQEw==" + }, "@fullcalendar/timegrid": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-4.3.0.tgz", @@ -1251,6 +1256,45 @@ "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", "dev": true }, + "cytoscape": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.7.3.tgz", + "integrity": "sha512-70WU7ZITYnxPFjorGe3Oa1SPY9Qa4ZZyi1k2F67VeSxx8X4ReoWBfIMt5ogZkVUC5dMxOV7hbFyrcJJ86wR7LA==", + "requires": { + "heap": "^0.2.6", + "lodash.debounce": "^4.0.8" + } + }, + "cytoscape-context-menus": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/cytoscape-context-menus/-/cytoscape-context-menus-3.0.6.tgz", + "integrity": "sha512-eRsAIg/HYOZOUfTvBK4L0t9ikdRuQULoB8ilB8AZ9D7zGfRjQip/quDH/H20n3IFMe6pb9XYAwVMHWZtlMjR1w==" + }, + "cytoscape-dagre": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/cytoscape-dagre/-/cytoscape-dagre-2.2.2.tgz", + "integrity": "sha512-zsg36qNwua/L2stJSWkcbSDcvW3E6VZf6KRe6aLnQJxuXuz89tMqI5EVYVKEcNBgzTEzFMFv0PE3T0nD4m6VDw==", + "requires": { + "dagre": "^0.8.2" + } + }, + "cytoscape-grid-guide": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cytoscape-grid-guide/-/cytoscape-grid-guide-2.2.1.tgz", + "integrity": "sha512-zJFImEcvd6ubHBlKdAQwmP5qOvYg2u2brwbvadSc9fM2N7WDcYol6d/wKvsLm+g+ixgIhglKKC2X1yEGvQiPeA==", + "requires": { + "functional-red-black-tree": "^1.0.1" + } + }, + "dagre": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.4.tgz", + "integrity": "sha512-Dj0csFDrWYKdavwROb9FccHfTC4fJbyF/oJdL9LNZJ8WUvl968P6PAKEriGqfbdArVJEmmfA+UyumgWEwcHU6A==", + "requires": { + "graphlib": "^2.1.7", + "lodash": "^4.17.4" + } + }, "date-now": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", @@ -2605,8 +2649,7 @@ "functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" }, "fuzzy": { "version": "0.1.3", @@ -2747,6 +2790,14 @@ "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", "dev": true }, + "graphlib": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.7.tgz", + "integrity": "sha512-TyI9jIy2J4j0qgPmOOrHTCtpPqJGN/aurBwc6ZT+bRii+di1I+Wv3obRhVrmBEXet+qkMaEX67dXrwsd3QQM6w==", + "requires": { + "lodash": "^4.17.5" + } + }, "gridstack": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/gridstack/-/gridstack-0.4.0.tgz", @@ -2822,6 +2873,11 @@ "minimalistic-assert": "^1.0.1" } }, + "heap": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.6.tgz", + "integrity": "sha1-CH4fELBGky/IWU3Z5tN4r8nR5aw=" + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -3369,6 +3425,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" + }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3378,6 +3439,12 @@ "yallist": "^3.0.2" } }, + "luxon": { + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.17.2.tgz", + "integrity": "sha512-qELKtIj3HD41N+MvgoxArk8DZGUb4Gpiijs91oi+ZmKJzRlxY6CoyTwNoUwnogCVs4p8HuxVJDik9JbnYgrCng==", + "optional": true + }, "make-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", @@ -4447,6 +4514,15 @@ "inherits": "^2.0.1" } }, + "rrule": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rrule/-/rrule-2.6.2.tgz", + "integrity": "sha512-xL38CM1zOYOIp4OO8hdD6zHH5UdR9siHMvPiv+CCSh7o0LYJ0owg87QcFW7GXJ0PfpLBHjanEMvvBjJxbRhAcQ==", + "requires": { + "luxon": "^1.3.3", + "tslib": "^1.9.0" + } + }, "run-async": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", @@ -5196,8 +5272,7 @@ "tslib": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", - "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", - "dev": true + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" }, "tty-browserify": { "version": "0.0.0", diff --git a/package.json b/package.json index 0b5387975d0..a97bc72edd6 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,15 @@ "@fullcalendar/interaction": "^4.3.0", "@fullcalendar/list": "^4.3.0", "@fullcalendar/timegrid": "^4.3.0", + "@fullcalendar/rrule": "^4.3.0", "chartist": "^0.10.1", "chartist-plugin-legend": "^0.6.2", "chartist-plugin-tooltips": "^0.0.17", "codemirror": "^5.48.4", + "cytoscape": "^3.7.3", + "cytoscape-dagre": "^2.2.1", + "cytoscape-context-menus": "^3.0.6", + "cytoscape-grid-guide": "^2.2.1", "diff-match-patch": "^1.0.4", "fuzzy": "^0.1.3", "gridstack": "^0.4.0", @@ -36,6 +41,7 @@ "photoswipe": "^4.1.3", "prismjs": "^1.17.1", "qtip2": "^3.0.3", + "rrule": "^2.6.0", "select2": "^4.0.10", "spectrum-colorpicker": "^1.8.0", "spin.js": "^2.3.2", diff --git a/pics/impact/README.txt b/pics/impact/README.txt new file mode 100644 index 00000000000..56eab151ef1 --- /dev/null +++ b/pics/impact/README.txt @@ -0,0 +1,2 @@ +Icons made by Smashicons : https://www.flaticon.com/authors/smashicons +www.flaticon.com is licensed by "CC 3.0 BY" : http://creativecommons.org/licenses/by/3.0/ diff --git a/pics/impact/computer.png b/pics/impact/computer.png new file mode 100644 index 00000000000..570303090cf Binary files /dev/null and b/pics/impact/computer.png differ diff --git a/pics/impact/dcroom.png b/pics/impact/dcroom.png new file mode 100644 index 00000000000..50aad2f5ba4 Binary files /dev/null and b/pics/impact/dcroom.png differ diff --git a/pics/impact/default.png b/pics/impact/default.png new file mode 100644 index 00000000000..18a7cfa1ed8 Binary files /dev/null and b/pics/impact/default.png differ diff --git a/pics/impact/enclosure.png b/pics/impact/enclosure.png new file mode 100644 index 00000000000..23991ca798a Binary files /dev/null and b/pics/impact/enclosure.png differ diff --git a/pics/impact/monitor.png b/pics/impact/monitor.png new file mode 100644 index 00000000000..90e9bd729f2 Binary files /dev/null and b/pics/impact/monitor.png differ diff --git a/pics/impact/networkequipment.png b/pics/impact/networkequipment.png new file mode 100644 index 00000000000..33658037ba0 Binary files /dev/null and b/pics/impact/networkequipment.png differ diff --git a/pics/impact/peripheral.png b/pics/impact/peripheral.png new file mode 100644 index 00000000000..ccd108c9682 Binary files /dev/null and b/pics/impact/peripheral.png differ diff --git a/pics/impact/phone.png b/pics/impact/phone.png new file mode 100644 index 00000000000..5a8a2fdc084 Binary files /dev/null and b/pics/impact/phone.png differ diff --git a/pics/impact/printer.png b/pics/impact/printer.png new file mode 100644 index 00000000000..6e218993225 Binary files /dev/null and b/pics/impact/printer.png differ diff --git a/pics/impact/rack.png b/pics/impact/rack.png new file mode 100644 index 00000000000..350107b816e Binary files /dev/null and b/pics/impact/rack.png differ diff --git a/pics/impact/software.png b/pics/impact/software.png new file mode 100644 index 00000000000..3e6371ae0af Binary files /dev/null and b/pics/impact/software.png differ diff --git a/tests/DbTestCase.php b/tests/DbTestCase.php index 029c0c79ec7..f662e0d2dd5 100644 --- a/tests/DbTestCase.php +++ b/tests/DbTestCase.php @@ -81,7 +81,7 @@ protected function setEntity($entityname, $subtree) { * @param int $id The id of added object * @param array $input the input used for add object (optionnal) * - * @return nothing (do tests) + * @return void */ protected function checkInput(CommonDBTM $object, $id = 0, $input = []) { $this->integer((int)$id)->isGreaterThan(0); diff --git a/tests/abstracts/AbstractPlanningEvent.php b/tests/abstracts/AbstractPlanningEvent.php new file mode 100644 index 00000000000..43836867055 --- /dev/null +++ b/tests/abstracts/AbstractPlanningEvent.php @@ -0,0 +1,139 @@ +. + * --------------------------------------------------------------------- +*/ + +abstract class AbstractPlanningEvent extends \DbTestCase { + protected $myclass = ""; + protected $input = []; + + private $begin = ""; + private $end = ""; + private $duration = ""; + + public function beforeTestMethod($method) { + parent::beforeTestMethod($method); + + $now = time(); + $this->duration = 2 * \HOUR_TIMESTAMP; + $this->begin = date('Y-m-d H:i:s', $now); + $this->end = date('Y-m-d H:i:s', $now + $this->duration); + + $this->input = [ + 'name' => 'test add external event', + 'test' => 'comment for external event', + 'plan' => [ + 'begin' => $this->begin, + '_duration' => $this->duration, + ], + 'rrule' => [ + 'freq' => 'daily', + 'interval' => 1, + 'byweekday' => 'MO', + 'bymonth' => 1, + ], + 'state' => \Planning::TODO, + 'background' => 1, + ]; + } + + public function testAdd() { + $event = new $this->myclass; + $id = $event->add($this->input); + + $this->integer((int) $id)->isGreaterThan(0); + $this->boolean($event->getFromDB($id))->isTrue(); + + // check end date + if (isset($event->fields['end'])) { + $this->string($event->fields['end'])->isEqualTo($this->end); + } + + // check rrule encoding + $this->string($event->fields['rrule']) + ->isEqualTo('{"freq":"daily","interval":1,"byweekday":"MO","bymonth":1}'); + + return $event; + } + + + public function testUpdate() { + $this->login(); + + $event = new $this->myclass; + $id = $event->add($this->input); + + $new_begin = date("Y-m-d H:i:s", strtotime($this->begin) + $this->duration); + $new_end = date("Y-m-d H:i:s", strtotime($this->end) + $this->duration); + + $update = array_merge($this->input, [ + 'id' => $id, + 'name' => 'updated external event', + 'test' => 'updated comment for external event', + 'plan' => [ + 'begin' => $new_begin, + '_duration' => $this->duration, + ], + 'rrule' => [ + 'freq' => 'monthly', + 'interval' => 2, + 'byweekday' => 'TU', + 'bymonth' => 2, + ], + 'state' => \Planning::INFO, + 'background' => 0, + ]); + $this->boolean($event->update($update))->isTrue(); + + // check dates (we added duration to both dates on update) + if (isset($event->fields['begin'])) { + $this->string($event->fields['begin']) + ->isEqualTo($new_begin); + } + if (isset($event->fields['end'])) { + $this->string($event->fields['end']) + ->isEqualTo($new_end); + } + + // check rrule encoding + $this->string($event->fields['rrule']) + ->isEqualTo('{"freq":"monthly","interval":2,"byweekday":"TU","bymonth":2}'); + } + + + public function testDelete() { + $event = new $this->myclass; + $id = $event->add($this->input); + + $this->boolean($event->delete([ + 'id' => $id, + ]))->isTrue(); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 69df7362899..9e09791ae1f 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -30,6 +30,7 @@ * --------------------------------------------------------------------- */ +ini_set('display_errors', 'On'); error_reporting(E_ALL); define('GLPI_CACHE_DIR', __DIR__ . '/files/_cache'); @@ -475,7 +476,7 @@ function loadDataset() { ], 'AuthLdap' => [ [ 'name' => '_local_ldap', - 'host' => '127.0.0.1', + 'host' => 'openldap', 'basedn' => 'dc=glpi,dc=org', 'rootdn' => 'cn=Manager,dc=glpi,dc=org', 'port' => '3890', @@ -497,8 +498,7 @@ function loadDataset() { 'group_condition' => '(objectclass=groupOfNames)', 'group_member_field' => 'member' ] - ] - + ], ]; // To bypass various right checks @@ -576,7 +576,7 @@ function loadDataset() { * @param string $type * @param string $name * @param boolean $onlyid - * @return the item, or its id + * @return CommonGLPI|false the item, or its id */ function getItemByTypeName($type, $name, $onlyid = false) { diff --git a/tests/functionnal/Impact.class.php b/tests/functionnal/Impact.class.php new file mode 100644 index 00000000000..e5fdd4ae0c6 --- /dev/null +++ b/tests/functionnal/Impact.class.php @@ -0,0 +1,283 @@ +. + * --------------------------------------------------------------------- +*/ + +namespace tests\units; + +class Impact extends \DbTestCase { + + public function testGetTabNameForItem_notCommonDBTM() { + $impact = new \Impact(); + + $this->exception(function() use ($impact) { + $notCommonDBTM = new \Impact(); + $impact->getTabNameForItem($notCommonDBTM); + })->isInstanceOf(\InvalidArgumentException::class); + } + + public function testGetTabNameForItem_notEnabledOrITIL() { + $impact = new \Impact(); + + $this->exception(function() use ($impact) { + $notEnabledOrITIL = new \ImpactCompound(); + $impact->getTabNameForItem($notEnabledOrITIL); + })->isInstanceOf(\InvalidArgumentException::class); + } + + public function testGetTabNameForItem_tabCountDisabled() { + $oldSession = $_SESSION['glpishow_count_on_tabs']; + $_SESSION['glpishow_count_on_tabs'] = false; + + $impact = new \Impact(); + $computer = new \Computer(); + $tab_name = $impact->getTabNameForItem($computer); + $_SESSION['glpishow_count_on_tabs'] = $oldSession; + + $this->string($tab_name)->isEqualTo("Impact analysis"); + } + + public function testGetTabNameForItem_enabledAsset() { + $oldSession = $_SESSION['glpishow_count_on_tabs']; + $_SESSION['glpishow_count_on_tabs'] = true; + + $impact = new \Impact(); + $impactRelationManager = new \ImpactRelation(); + + // Get computers + $computer1 = getItemByTypeName('Computer', '_test_pc01'); + $computer2 = getItemByTypeName('Computer', '_test_pc02'); + $computer3 = getItemByTypeName('Computer', '_test_pc03'); + + // Create an impact graph + $impactRelationManager->add([ + 'itemtype_source' => "Computer", + 'items_id_source' => $computer1->fields['id'], + 'itemtype_impacted' => "Computer", + 'items_id_impacted' => $computer2->fields['id'], + ]); + $impactRelationManager->add([ + 'itemtype_source' => "Computer", + 'items_id_source' => $computer2->fields['id'], + 'itemtype_impacted' => "Computer", + 'items_id_impacted' => $computer3->fields['id'], + ]); + $tab_name = $impact->getTabNameForItem($computer2); + $_SESSION['glpishow_count_on_tabs'] = $oldSession; + + $this->string($tab_name)->isEqualTo("Impact analysis 2"); + } + + public function testGetTabNameForItem_ITILObject() { + $oldSession = $_SESSION['glpishow_count_on_tabs']; + $_SESSION['glpishow_count_on_tabs'] = true; + + $impact = new \Impact(); + $impactRelationManager = new \ImpactRelation(); + $ticketManager = new \Ticket(); + $itemTicketManger = new \Item_Ticket(); + + // Get computers + $computer1 = getItemByTypeName('Computer', '_test_pc01'); + $computer2 = getItemByTypeName('Computer', '_test_pc02'); + $computer3 = getItemByTypeName('Computer', '_test_pc03'); + + // Create an impact graph + $impactRelationManager->add([ + 'itemtype_source' => "Computer", + 'items_id_source' => $computer1->fields['id'], + 'itemtype_impacted' => "Computer", + 'items_id_impacted' => $computer2->fields['id'], + ]); + $impactRelationManager->add([ + 'itemtype_source' => "Computer", + 'items_id_source' => $computer2->fields['id'], + 'itemtype_impacted' => "Computer", + 'items_id_impacted' => $computer3->fields['id'], + ]); + + // Create a ticket and link it to the computer + $ticketId = $ticketManager->add(['name' => "test", 'content' => "test"]); + $itemTicketManger->add([ + 'itemtype' => "Computer", + 'items_id' => $computer2->fields['id'], + 'tickets_id' => $ticketId, + ]); + + // Get the actual ticket + $ticket = new \Ticket; + $ticket->getFromDB($ticketId); + + $tab_name = $impact->getTabNameForItem($ticket); + $_SESSION['glpishow_count_on_tabs'] = $oldSession; + + $this->string($tab_name)->isEqualTo("Impact analysis"); + } + + public function testBuildGraph_empty() { + $computer = getItemByTypeName('Computer', '_test_pc01'); + $graph = \Impact::buildGraph($computer); + + $this->array($graph)->hasKeys(["nodes", "edges"]); + + // Nodes should contain only _test_pc01 + $id = $computer->fields['id']; + $this->array($graph["nodes"])->hasSize(1); + $this->string($graph["nodes"]["Computer::$id"]['label'])->isEqualTo("_test_pc01"); + + // Edges should be empty + $this->array($graph["edges"])->hasSize(0); + } + + public function testBuildGraph_complex() { + $impactRelationManager = new \ImpactRelation(); + $impactItemManager = new \ImpactItem(); + $impactCompoundManager = new \ImpactCompound(); + + $computer1 = getItemByTypeName('Computer', '_test_pc01'); + $computer2 = getItemByTypeName('Computer', '_test_pc02'); + $computer3 = getItemByTypeName('Computer', '_test_pc03'); + $computer4 = getItemByTypeName('Computer', '_test_pc11'); + $computer5 = getItemByTypeName('Computer', '_test_pc12'); + $computer6 = getItemByTypeName('Computer', '_test_pc13'); + + // Set compounds + $compound01Id = $impactCompoundManager->add([ + 'name' => "_test_compound01", + 'color' => "#000011", + ]); + $compound02Id = $impactCompoundManager->add([ + 'name' => "_test_compound02", + 'color' => "#110000", + ]); + + // Set impact items + $impactItemManager->add([ + 'itemtype' => "Computer", + 'items_id' => $computer1->fields['id'], + 'parent_id' => 0, + ]); + $impactItemManager->add([ + 'itemtype' => "Computer", + 'items_id' => $computer2->fields['id'], + 'parent_id' => $compound01Id, + ]); + $impactItemManager->add([ + 'itemtype' => "Computer", + 'items_id' => $computer3->fields['id'], + 'parent_id' => $compound01Id, + ]); + $impactItemManager->add([ + 'itemtype' => "Computer", + 'items_id' => $computer4->fields['id'], + 'parent_id' => $compound02Id, + ]); + $impactItemManager->add([ + 'itemtype' => "Computer", + 'items_id' => $computer5->fields['id'], + 'parent_id' => $compound02Id, + ]); + $impactItemManager->add([ + 'itemtype' => "Computer", + 'items_id' => $computer6->fields['id'], + 'parent_id' => $compound02Id, + ]); + + // Set relations + $impactRelationManager->add([ + 'itemtype_source' => "Computer", + 'items_id_source' => $computer1->fields['id'], + 'itemtype_impacted' => "Computer", + 'items_id_impacted' => $computer2->fields['id'], + ]); + $impactRelationManager->add([ + 'itemtype_source' => "Computer", + 'items_id_source' => $computer2->fields['id'], + 'itemtype_impacted' => "Computer", + 'items_id_impacted' => $computer3->fields['id'], + ]); + $impactRelationManager->add([ + 'itemtype_source' => "Computer", + 'items_id_source' => $computer3->fields['id'], + 'itemtype_impacted' => "Computer", + 'items_id_impacted' => $computer4->fields['id'], + ]); + $impactRelationManager->add([ + 'itemtype_source' => "Computer", + 'items_id_source' => $computer4->fields['id'], + 'itemtype_impacted' => "Computer", + 'items_id_impacted' => $computer5->fields['id'], + ]); + $impactRelationManager->add([ + 'itemtype_source' => "Computer", + 'items_id_source' => $computer2->fields['id'], + 'itemtype_impacted' => "Computer", + 'items_id_impacted' => $computer6->fields['id'], + ]); + $impactRelationManager->add([ + 'itemtype_source' => "Computer", + 'items_id_source' => $computer6->fields['id'], + 'itemtype_impacted' => "Computer", + 'items_id_impacted' => $computer2->fields['id'], + ]); + + // Build graph from pc02 + $computer = getItemByTypeName('Computer', '_test_pc02'); + $graph = \Impact::buildGraph($computer); + // var_dump($graph); + $this->array($graph)->hasKeys(["nodes", "edges"]); + + // Nodes should contain 8 elements (6 nodes + 2 compounds) + $this->array($graph["nodes"])->hasSize(8); + $nodes = array_filter($graph["nodes"], function($elem) { + return !isset($elem['color']); + }); + $this->array($nodes)->hasSize(6); + $compounds = array_filter($graph["nodes"], function($elem) { + return isset($elem['color']); + }); + $this->array($compounds)->hasSize(2); + + // Edges should contain 6 elements (3 forward, 1 backward, 2 both) + $this->array($graph["edges"])->hasSize(6); + $backward = array_filter($graph["edges"], function($elem) { + return $elem["flag"] == \Impact::DIRECTION_BACKWARD; + }); + $this->array($backward)->hasSize(1); + $forward = array_filter($graph["edges"], function($elem) { + return $elem["flag"] == \Impact::DIRECTION_FORWARD; + }); + $this->array($forward)->hasSize(3); + $both = array_filter($graph["edges"], function($elem) { + return $elem["flag"] == (\Impact::DIRECTION_FORWARD | \Impact::DIRECTION_BACKWARD); + }); + $this->array($both)->hasSize(2); + } +} \ No newline at end of file diff --git a/tests/functionnal/ImpactItem.class.php b/tests/functionnal/ImpactItem.class.php new file mode 100644 index 00000000000..a58030ccdbd --- /dev/null +++ b/tests/functionnal/ImpactItem.class.php @@ -0,0 +1,56 @@ +. + * --------------------------------------------------------------------- +*/ + +namespace tests\units; + +class ImpactItem extends \DbTestCase { + + public function testFindForItem_inexistent() { + $computer = getItemByTypeName('Computer', '_test_pc02'); + + $this->boolean(\ImpactItem::findForItem($computer))->isFalse(); + } + + public function testFindForItem_exist() { + $impactItemManager = new \ImpactItem(); + $computer = getItemByTypeName('Computer', '_test_pc02'); + + $id = $impactItemManager->add([ + 'itemtype' => "Computer", + 'items_id' => $computer->fields['id'], + 'parent_id' => 0, + ]); + + $impactItem = \ImpactItem::findForItem($computer); + $this->integer((int) $impactItem->fields['id'])->isEqualTo($id); + } +} diff --git a/tests/functionnal/ImpactRelation.class.php b/tests/functionnal/ImpactRelation.class.php new file mode 100644 index 00000000000..4d9a65a24fb --- /dev/null +++ b/tests/functionnal/ImpactRelation.class.php @@ -0,0 +1,144 @@ +. + * --------------------------------------------------------------------- +*/ + +namespace tests\units; + +class ImpactRelation extends \DbTestCase { + + public function testPrepareInputForAdd_requiredFields() { + $impactRelationManager = new \ImpactRelation(); + $res = $impactRelationManager->add([]); + + $this->boolean($res)->isFalse(); + } + + public function testPrepareInputForAdd_differentItems() { + $computer = getItemByTypeName('Computer', '_test_pc02'); + $impactRelationManager = new \ImpactRelation(); + $res = $impactRelationManager->add([ + 'itemtype_source' => "Computer", + 'items_id_source' => $computer->fields['id'], + 'itemtype_impacted' => "Computer", + 'items_id_impacted' => $computer->fields['id'], + ]); + + $this->boolean($res)->isFalse(); + } + + public function testPrepareInputForAdd_duplicate() { + $computer1 = getItemByTypeName('Computer', '_test_pc01'); + $computer2 = getItemByTypeName('Computer', '_test_pc02'); + $impactRelationManager = new \ImpactRelation(); + + $impactRelationManager->add([ + 'itemtype_source' => "Computer", + 'items_id_source' => $computer1->fields['id'], + 'itemtype_impacted' => "Computer", + 'items_id_impacted' => $computer2->fields['id'], + ]); + + $res = $impactRelationManager->add([ + 'itemtype_source' => "Computer", + 'items_id_source' => $computer1->fields['id'], + 'itemtype_impacted' => "Computer", + 'items_id_impacted' => $computer2->fields['id'], + ]); + + $this->boolean($res)->isFalse(); + } + + public function testPrepareInputForAdd_assetExist() { + $impactRelationManager = new \ImpactRelation(); + + $res = $impactRelationManager->add([ + 'itemtype_source' => "Computer", + 'items_id_source' => -40, + 'itemtype_impacted' => "Computer", + 'items_id_impacted' => -78, + ]); + + $this->boolean($res)->isFalse(); + } + + public function testPrepareInputForAdd_valid() { + $computer1 = getItemByTypeName('Computer', '_test_pc01'); + $computer2 = getItemByTypeName('Computer', '_test_pc02'); + $impactRelationManager = new \ImpactRelation(); + + $res = $impactRelationManager->add([ + 'itemtype_source' => "Computer", + 'items_id_source' => $computer1->fields['id'], + 'itemtype_impacted' => "Computer", + 'items_id_impacted' => $computer2->fields['id'], + ]); + + $this->integer($res); + } + + public function testGetIDFromInput_invalid() { + $computer1 = getItemByTypeName('Computer', '_test_pc01'); + $computer2 = getItemByTypeName('Computer', '_test_pc02'); + + $input = [ + 'itemtype_source' => "Computer", + 'items_id_source' => $computer1->fields['id'], + 'itemtype_impacted' => "Computer", + 'items_id_impacted' => $computer2->fields['id'], + ]; + + $id = \ImpactRelation::getIDFromInput($input); + $this->boolean($id)->isFalse(); + } + + public function testGetIDFromInput_valid() { + $computer1 = getItemByTypeName('Computer', '_test_pc01'); + $computer2 = getItemByTypeName('Computer', '_test_pc02'); + $impactRelationManager = new \ImpactRelation(); + + $id1 = $impactRelationManager->add([ + 'itemtype_source' => "Computer", + 'items_id_source' => $computer1->fields['id'], + 'itemtype_impacted' => "Computer", + 'items_id_impacted' => $computer2->fields['id'], + ]); + + $input = [ + 'itemtype_source' => "Computer", + 'items_id_source' => $computer1->fields['id'], + 'itemtype_impacted' => "Computer", + 'items_id_impacted' => $computer2->fields['id'], + ]; + + $id2 = \ImpactRelation::getIDFromInput($input); + $this->integer($id1)->isEqualTo($id2); + } +} diff --git a/tests/functionnal/NotificationMailing.php b/tests/functionnal/NotificationMailing.php index 966edcca09f..a4ab1345028 100644 --- a/tests/functionnal/NotificationMailing.php +++ b/tests/functionnal/NotificationMailing.php @@ -38,6 +38,10 @@ class NotificationMailing extends DbTestCase { + /** + * @ignore + * @see https://gitlab.alpinelinux.org/alpine/aports/issues/7392 + */ public function testCheck() { $instance = new \NotificationMailing(); diff --git a/tests/functionnal/PlanningExternalEvent.php b/tests/functionnal/PlanningExternalEvent.php new file mode 100644 index 00000000000..891a39d8255 --- /dev/null +++ b/tests/functionnal/PlanningExternalEvent.php @@ -0,0 +1,40 @@ +. + * --------------------------------------------------------------------- +*/ + +namespace tests\units; + +include_once __DIR__ . '/../abstracts/AbstractPlanningEvent.php'; + +class PlanningExternalEvent extends \AbstractPlanningEvent { + public $myclass = "\PlanningExternalEvent"; + +} \ No newline at end of file diff --git a/tests/functionnal/PlanningExternalEventTemplate.php b/tests/functionnal/PlanningExternalEventTemplate.php new file mode 100644 index 00000000000..6d057bdfde3 --- /dev/null +++ b/tests/functionnal/PlanningExternalEventTemplate.php @@ -0,0 +1,58 @@ +. + * --------------------------------------------------------------------- +*/ + +namespace tests\units; + +include_once __DIR__ . '/../abstracts/AbstractPlanningEvent.php'; + +class PlanningExternalEventTemplate extends \AbstractPlanningEvent { + protected $myclass = "\PlanningExternalEventTemplate"; + + public function beforeTestMethod($method) { + parent::beforeTestMethod($method); + + $this->input = array_merge($this->input, [ + '_planningrecall' => [ + 'before_time' => 2 * \HOUR_TIMESTAMP, + ], + ]); + } + + public function testAdd() { + $event = parent::testAdd(); + + $this->integer((int) $event->fields['before_time']) + ->isEqualTo(2 * \HOUR_TIMESTAMP); + $this->integer((int) $event->fields['duration']) + ->isEqualTo(2 * \HOUR_TIMESTAMP); + } +} diff --git a/tests/functionnal/Search.php b/tests/functionnal/Search.php index 19171ef5fea..79871b18600 100644 --- a/tests/functionnal/Search.php +++ b/tests/functionnal/Search.php @@ -88,7 +88,7 @@ private function doSearch($itemtype, $params, array $forcedisplay = []) { private function getClasses($function = false, array $excludes = []) { $classes = []; foreach (new \DirectoryIterator('inc/') as $fileInfo) { - if ($fileInfo->isDot()) { + if (!$fileInfo->isFile()) { continue; } diff --git a/tests/units/Html.php b/tests/units/Html.php index 08136890414..c785e8bb80a 100644 --- a/tests/units/Html.php +++ b/tests/units/Html.php @@ -365,7 +365,8 @@ public function testGetMenuInfos() { 'ReservationItem', 'Report', 'MigrationCleaner', - 'SavedSearch' + 'SavedSearch', + 'Impact' ]; $this->string($menu['tools']['title'])->isIdenticalTo('Tools'); $this->array($menu['tools']['types'])->isIdenticalTo($expected); diff --git a/tests/web/APIRest.php b/tests/web/APIRest.php index 7e69dc889e7..3b0e1901cd7 100644 --- a/tests/web/APIRest.php +++ b/tests/web/APIRest.php @@ -215,6 +215,8 @@ public function testInitSessionUserToken() { $data = json_decode($body, true); $this->variable($data)->isNotFalse(); $this->array($data)->hasKey('session_token'); + $this->array($data)->hasKey('users_id'); + $this->integer((int) $data['users_id'])->isEqualTo($uid); } /** diff --git a/tests/web/APIXmlrpc.php b/tests/web/APIXmlrpc.php index 4b9c8f63499..12b649f7b13 100644 --- a/tests/web/APIXmlrpc.php +++ b/tests/web/APIXmlrpc.php @@ -106,6 +106,7 @@ protected function query($resource = "", $params = [], $expected_code = 200, $ex * @covers API::initSession */ public function initSessionCredentials() { + $uid = getItemByTypeName('User', TU_USER, true); $data = $this->query('initSession', ['query' => [ 'login' => TU_USER, @@ -114,6 +115,8 @@ public function initSessionCredentials() { $this->variable($data)->isNotFalse(); $this->array($data)->hasKey('session_token'); $this->session_token = $data['session_token']; + $this->array($data)->hasKey('users_id'); + $this->integer((int) $data['users_id'])->isEqualTo($uid); } /** diff --git a/webpack.config.js b/webpack.config.js index 75e0f58d0b7..d2754bf95dd 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -75,12 +75,14 @@ var libsConfig = { rules: [ { // Load scripts with no compilation for packages that are directly providing "dist" files. - // This prevents useless compilation pass and can also + // This prevents useless compilation pass and can also // prevents incompatibility issues with the webpack require feature. test: /\.js$/, include: [ path.resolve(__dirname, 'node_modules/@fullcalendar'), path.resolve(__dirname, 'node_modules/codemirror'), + path.resolve(__dirname, 'node_modules/cystoscape'), + path.resolve(__dirname, 'node_modules/cytoscape-context-menus'), path.resolve(__dirname, 'node_modules/gridstack'), path.resolve(__dirname, 'node_modules/jstree'), path.resolve(__dirname, 'node_modules/photoswipe'),