Compare commits

...

185 Commits

Author SHA1 Message Date
bdalsass
13239c2751 N°8201 - [CVE_Request]_Cross-Site-Script Reflected(XSS Reflected at the name="attr_installed" (Low or Medium) 2025-05-23 10:06:01 +02:00
bdalsass
81b20ee583 N°8168 - Stored XSS in portals lnk 2025-05-23 08:42:56 +02:00
bdalsass
38683c20b1 N°8313 - edit dashboard (fix broken test) 2025-05-16 14:05:55 +02:00
bdalsass
81791dd253 N°8313 - edit dashboard 2025-05-16 14:05:55 +02:00
bdalsass
e77e0eec9f N°8355 - render_dashboard 2025-05-16 14:05:55 +02:00
jf-cbd
960133c0df N°8379 - fix backup issue 2025-05-13 16:27:59 +02:00
jf-cbd
5232694c04 N°8281 - Unable to click search button in multiple request template 2025-03-21 11:20:37 +01:00
jf-cbd
874a5fd2ce Dump autoloader 2025-03-12 11:36:06 +01:00
jf-cbd
063bb9680e N°8231 - Better variable fallback 2025-03-06 16:09:51 +01:00
jf-cbd
8f8ac46f55 N°8215 - When PHP warning are enabled, Global Request doesn't work 2025-03-06 11:59:08 +01:00
denis.flaven@combodo.com
07b904ee1b N°8231 - making rest api logs more readable 2025-03-06 11:59:08 +01:00
denis.flaven@combodo.com
affed69999 Version number bump. 2025-02-07 10:09:48 +01:00
denis.flaven@combodo.com
d5754fc568 N°8135 - Bump datamodel version. 2025-01-31 17:04:56 +01:00
jf-cbd
44290db312 N°8134 - Portal user profile is broken, regression from 7776 2025-01-28 10:23:44 +01:00
jf-cbd
c49ceae75e Fix HandleForm call 2025-01-21 16:46:15 +01:00
jf-cbd
8980f627e9 Fix format 2025-01-21 12:09:06 +01:00
jf-cbd
160bfd714b N°7776 remove twig from ajax calls 2025-01-20 15:41:22 +01:00
jf-cbd
8d58372074 Update unattended installation script documentation 2025-01-16 17:13:26 +01:00
Eric Espie
37fc1a5723 N°7810 - security hardening 2024-12-27 09:04:28 +01:00
jf-cbd
95aa444ee6 Security hardening 2024-12-13 16:48:13 +01:00
jf-cbd
f5de808c7c Security hardening (#685)
* security hardening
2024-12-13 15:09:18 +01:00
Benjamin Dalsass
e03033ce52 N°7219 - Fatal error following dashboard modification when dashboard title contains an é 2024-11-27 09:40:22 +01:00
jf-cbd
374b35f78a 🚀 Fix GitHub action 2024-11-07 14:50:46 +01:00
jf-cbd
04bd8cc5ce 🚀 Update GitHub actions to improve PR classification 2024-10-22 16:07:47 +02:00
denis.flaven@combodo.com
8cb701bda3 🔖 Prepare 2.7.11 version 2024-09-26 16:53:24 +02:00
jf-cbd
1b29746806 Rename github token 2024-09-23 17:14:41 +02:00
jf-cbd
fb9c317256 Add an action in the workflow to automatically add pull requests to the Combodo PRs dashboard 2024-09-23 14:43:33 +02:00
Eric Espie
0904a21e3f Cleanup ItopTestCase 2024-06-24 11:50:37 +02:00
Molkobain
82d11eeb47 N°7127 - Upgrade handlebars.js to v4.7.8 2024-06-21 11:19:39 +02:00
Eric Espie
142d6c8993 N°7533 - Detect and warns on Galera clusters 2024-06-20 11:06:57 +02:00
Timothee
320922a13d N°7545 Correctly display error message 2024-06-17 16:49:33 +02:00
Eric Espie
f03d731b1d N°7533 - Prevent installation of iTop on Galera clusters 2024-06-12 16:14:23 +02:00
Eric Espie
8be7628668 N°7548 - Code hardening 2024-05-29 18:11:36 +02:00
Eric Espie
62caf16153 N°7364 - Code hardening 2024-05-21 14:20:30 +02:00
odain
163a3afc0f N°7426 - no session created - replace php_sapi_name() by PHP_SAPI in unattended 2024-05-16 15:31:08 +02:00
odain
f8b54be896 N°7426 - no session created - replace php_sapi_name() by PHP_SAPI 2024-05-16 14:10:54 +02:00
Romain Quetiez
53dc452d61 Avoid unnecessary custom test environment compilations (base compilation of file modification time) 2024-05-16 09:53:04 +02:00
Romain Quetiez
ccaf2dc5b7 Make the tests compatible with windows (and linux) 2024-05-16 09:53:04 +02:00
Molkobain
5d5df5ad1a N°7255 - Fix misc. stylesheets not working in portal since N°7047 2024-05-07 10:37:39 +02:00
Molkobain
32140b360f Cherry pick fixes from 59a955f4 2024-04-29 11:45:09 +02:00
jf-cbd
d85767a838 Update test to run only on commmunity builds 2024-04-24 11:14:40 +02:00
jf-cbd
eeec57536b Security hardening 2024-04-23 11:55:39 +02:00
Molkobain
16ff6341d0 N°7455 - Fix regression from 4c784886, wrong class tested 2024-04-19 09:14:53 +02:00
Molkobain
9dab8679d6 N°7448 - Update dictionary entry 2024-04-18 18:44:20 +02:00
Molkobain
4c78488644 N°7455 - Ensure form renderer class extends FormRenderer 2024-04-18 18:15:02 +02:00
odain
b65e931c4c N°7439 - setup wizard broken on essential targets after fresh install via unattended CLI 2024-04-15 09:57:54 +02:00
odain
6cb3519308 N°7407 - test readability 2024-04-12 16:59:56 +02:00
odain
cfb9fae648 N°7407 - fix previous commit 2024-04-12 16:49:11 +02:00
odain
f4e791734f N°7407 - remove phpunit annotation 2024-04-12 14:49:20 +02:00
odain
6653ab0668 N°7407 - fix missing extension installation via unattended from production-modules or extension folder 2024-04-12 13:25:40 +02:00
odain
7ab258ba03 N°7439 - setup wizard broken on essential targets 2024-04-12 11:32:04 +02:00
odain
b5af30a93f N°7407 - refactor unattended tests to make work anywhere 2024-04-12 11:32:04 +02:00
Molkobain
bbfa601ab1 N°7446 - Add temporary workaround to fix an issue due to DB views creation. 2024-04-12 09:59:14 +02:00
odain
172b1cb1ff N°7407 - add phpunit annotation to exclude tests on top of targets 2024-04-11 18:54:43 +02:00
Molkobain
ca356859a3 N°7446 - Add comment for better understanding 2024-04-11 16:55:48 +02:00
Molkobain
5efe294895 N°7446 - Fix custom datamodel test class not creating DB tables correctly 2024-04-11 16:50:27 +02:00
odain
e0170ccc7e N°7407 - InstallationFileServiceTest relies on local installation.xml to reduce maintenance 2024-04-10 14:05:39 +02:00
odain
3b78885f38 N°7407 - unattended test cleanup for PHP 8.x deprecations 2024-04-09 11:06:03 +02:00
odain
ed562c9f73 N°7407 - fix unattended test enhancement 2024-04-09 10:02:06 +02:00
odain-cbd
85c576a986 N°7407 - N°7306 - Ease iTop installation via unattended CLI by using installation.xml choices (#641)
* N°7306 - Use iTop configuration settings to run unattended installation (instead of XML file settings)

* 7306 - fix infinite loop with db_tls.ca null

* 7306 - complete fields to use from itop configuration instead of XML setup

* fix using default language from conf

* 6365 - temp work

* 6365 - add option to select modules from installation.xml

* 6365-select modules option in unattended install

* 6365 - pass env to service + debug failed test

* 6365 - debug ci again + separate process annotation

* 6365 - fix test + cleanup

* 6365 - ci using use_installation_xml mode

* 6365 - ci using use_installation_xml mode

* 6365 - pass selected_modules to unattended

* N°6365 - Compute selected modules based on selected extensions coming from XML setup

* switch constr parameters and fix call from unattended cli

* 6365 - use use_installation_xml for unattended install only when no selected modules already provided

* test ci XML setup including selected extensions but no modules

* test ci installing iTop without selected modules/extenesions: guess via installation.xml

* same but without even providing XML setup - comment it in ci_description.ini

* 7306 - cleanup requires

* use infra master

* N°6365 - make current unattended CLI work with any iTop version (CLIPage compatibility)

* N°6365 - log which modules will be installed during setup

* N°6365 - unattended documentation + bash helper

* 6365- fix warning due to copies index access

* 6365 - enhance traces feedback to understant which and how modules are computes

* 6365 - enhance bash CLI + doc

* 6365 - fix require clipage compatibility

* 6365 - add return for better cli ouput

* 6365 - enhance ouput messages

* Document the usage and harmonize argument names (still not perfect)

* 6365 - fix use of new param param-file

* 6365 - fix test + vardump cleanup

* N°6365 - use underscore for unattended install options as advices in the PR

* 6365 -enhance test by using PHP_BINARY

---------

Co-authored-by: Romain Quetiez <romain.quetiez@combodo.com>
2024-04-09 10:00:58 +02:00
odain-cbd
5a34c76cc4 N°5120 - Unattended install : use cty toolkit version (#624)
* N°5120 - Unattended install : use cty toolkit version

* N°5120 - CLI mode validation

* 5120 - bring CI enhancements

* 5120 - bring back saas use-itop-config option: to read db settings from conf directly

* 5120 - move unattended script in setup folder/unattended-install

* 5120 - use-itop-config option: take db_tls_enabled and db_tls_ca into account

* 5120 - move test

* 5120 - put ci enhancements back - logs and conf preservation with install mode

* 5120 - keep PR simple - remove saas use-itop-config option for now

* 5120 - remove ci enhancement to preserve configuration

* Update setup/unattended-install/README.md

Co-authored-by: Thomas Casteleyn <thomas.casteleyn@super-visions.com>

* 5120 - documentation

* 5120 - fix log level

* 5120 - fix test

* fix phpunit test comment

---------

Co-authored-by: Thomas Casteleyn <thomas.casteleyn@super-visions.com>
2024-03-29 11:09:27 +01:00
Molkobain
da99a250bf N°7075 - Add check for Content Security Policies (CSP) in the setup 2024-03-21 14:20:22 +01:00
Pierre Goiffon
dbd5ba0377 N°7302 Fix SetupUtilsTest::testHumanReadableSize 2024-03-13 09:56:31 +01:00
Pierre Goiffon
5d6f293956 N°7302 Report SetupUtilsTest::testHumanReadableSize in the 2.7 branch 2024-03-13 09:48:46 +01:00
Pierre Goiffon
986c24d777 N°7302 Fix unit name in \SetupUtils::HumanReadableSize (#626) 2024-03-12 18:09:29 +01:00
Pierre Goiffon
763112c179 N°7344 rest.php core/get : add try/catch around query execution (#622)
Co-authored-by: Thomas Casteleyn <thomas.casteleyn@super-visions.com>
Co-authored-by: Molkobain <lajarige.guillaume@free.fr>
2024-03-12 18:08:04 +01:00
Pierre Goiffon
a5efd981d8 N°7343 Catch ParseError when loading dict files in setup (#615) 2024-03-12 18:05:38 +01:00
Pierre Goiffon
2922b22478 \DBObject::GetSearchForUniquenessRule remove wrong @api 2024-03-12 17:34:28 +01:00
Pierre Goiffon
2af05a437e N°4314 - Fix Uniqueness rules not working with Silo
(cherry picked from commit e8c11f38d2)
2024-03-12 17:32:56 +01:00
Molkobain
a9f8dcc5e8 N°7122 N°4164 - Portal: Hide log off button when user can't actually log off (eg. SSO using SAML or other providers) (#599)
* N°7122 N°4164 - Portal: Hide log off button when user can't actually log off (eg. SSO using SAML or other providers)

* N°7122 - Fix typo

Co-authored-by: Thomas Casteleyn <thomas.casteleyn@super-visions.com>

---------

Co-authored-by: Thomas Casteleyn <thomas.casteleyn@super-visions.com>
2024-03-11 21:13:09 +01:00
Pierre Goiffon
c325294e17 N°6599 Update moment from 2.22.2 to 2.30.1 2024-03-11 16:39:05 +01:00
Pierre Goiffon
da490739be 💡 Fix \CMDBSource::Query phpdoc block 2024-03-07 11:10:53 +01:00
Pierre Goiffon
b867faa355 Fix DBObjectTest
Error was "Class 'Combodo\iTop\Test\UnitTest\Core\DBObjectSet' not found"
2024-03-04 08:09:35 +01:00
jf-cbd
7453cc184f ✏️ fix a typo 2024-03-01 17:25:08 +01:00
Anne-Catherine
473cf004b6 N°6968 - Audit duration : add of a rule multiplie by 4 the time of response (#575) 2024-02-29 16:33:04 +01:00
Pierre Goiffon
f6fec506b1 💡 Some PHPDoc hints on value types to pass to DBObject::Set 2024-02-20 12:10:00 +01:00
Molkobain
5c12151c26 📝 Update PR template 2024-02-19 09:46:12 +01:00
Pierre Goiffon
5d6c4939f6 N°7245 Bettor logs on RunTimeEnvironment::CallInstallerHandlers exceptions (#606) 2024-02-14 11:01:12 +01:00
Molkobain
1b3a2c8470 N°5775 - Show error message to user in case of issue during token generation 2024-01-24 14:49:51 +01:00
Dennis Lassiter
618d8e6468 N°5775 - Allow configuration of OAuth client on MS Azure with single tenant (#553)
* Add Tenant-Support for Azure OAuthClient

* Improvement: Make tenant required

* Improvment: Removed check for null-value

Since last commit, the "tenant"-field either set to a custom value or "common" by default. It is not allowed to be null

* Add field description

---------

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>
2024-01-24 14:38:54 +01:00
Molkobain
01a955a16f 📝 Add PR template 2024-01-16 19:42:39 +01:00
Anne-Catherine
87582a021b N°6993 - Error message on bulk transition on object containing a null blob (#596) 2024-01-15 15:49:19 +01:00
odain
9830178a47 N°7085 - ci restore runTestsInSeparateProcesses 2024-01-12 08:36:15 +01:00
odain-cbd
c140ebcb6b N°7085 - Fix infinite loop in login page until fatal error occurs (#592)
* N°7085 - login page infinite loop until fatal error- add Config->AddAllowedLoginTypes

* N°7085 - reproduce issue via a test

* N°7085-fix infinite loop

* N°7085 - ci: fix config file rights in tearDown

* N°7085 - ci: fix config file rights in tearDown (again)

* N°7085 - ci: fix config file content

* N°7085 - ci : add runTestsInSeparateProcesses

* Update core/config.class.inc.php

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>

* N°7085 - exit -1 + enhance log message

* PR feedbacks from Romain regarding LoginTest annotations

---------

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>
2024-01-12 08:13:40 +01:00
Molkobain
7a0a4e377b N°7137 - Fix regression from 7fffbb60, missing "default_value" parameter 2024-01-11 16:40:40 +01:00
Thomas Casteleyn
7fffbb60e9 N°7137 - DataSynchro: Remove "Organization" as default value for SynchroReplica->dest_class (#551) 2024-01-11 15:38:02 +01:00
Pierre Goiffon
2fd9523c16 🔖 Prepare 2.7.10 version 2024-01-05 15:50:41 +01:00
Pierre Goiffon
a4f6f6e877 N°4368 Fix CORB blocking regression (#598)
Don't send X-Content-Type-Options HTTP header for certain WebPage impl to workaround CORB blocking
To disable globally this new behavior introduced in 9865bf07, set the `security.enable_header_xcontent_type_options` config parameter to false

Thanks @Molkobain for the review !
2024-01-05 10:41:18 +01:00
Molkobain
94c604a6af N°3062 - Fix setup.css compilation test to ensure that it is versioned correctly. 2023-12-21 12:00:26 +01:00
Pierre Goiffon
6995a3c641 N°6889 backup mysqldump call : restore possibility to connect using socket protocol (#591)
With previous fix (N°6123) we forced to use the tcp protocol each time. This was blocking for users wanting to connect using the socket protocol on localhost.

Now for localhost we will : 
- send both port and protocol arguments if the `db_host` config parameter does contain a port
- don't send any of the port or protocol arguments if `db_host` doesn't contain a port
2023-12-20 15:19:50 +01:00
Pierre Goiffon
9865bf0779 N°4368 add sending X-Content-Type-Options HTTP header
Replace in consumers the \WebPage::add_xframe_options call by \WebPage::add_http_headers
2023-12-19 18:25:26 +01:00
Pierre Goiffon
d5449cca42 💡 iTopMutex: add link to mysql doc 2023-12-08 17:20:37 +01:00
Molkobain
5d38d22c50 N°7023 - Fix regression from the initial fix that throw exceptions even for ext. keys set programatically (eg. ComputeValues), which we still want to allow 2023-12-06 16:27:37 +01:00
Pierre Goiffon
99d69493d1 N°7023 - Update tests so that we are now checking negative ext. keys 2023-12-04 22:36:26 +01:00
Molkobain
c9bb628c30 N°7023 - Improve debug message on portal \DBObject::CheckChangedExtKeysValues() call 2023-12-04 22:36:09 +01:00
Molkobain
08e8d15d78 N°7023 - Fix check to write error when adding a contact on a new user request on the end-users portal 2023-12-04 22:35:07 +01:00
Molkobain
7b59df216b N°7005 - Fix portal stylesheets not being re-compiled when outdated
Stylesheets should remain as a relative path in the portal configuration, only when consumed by the TWIG should they become URLs

Note that if not absolute, URLs will be append to ITOP/pages/
2023-11-24 16:40:58 +01:00
Molkobain
cb5eab812e N°938 - Update compiled portal stylesheet 2023-11-24 16:22:36 +01:00
Anne-Catherine
c9b73a7fe2 N°938 - Improve print of portal object page and portal dashboard page (#537)
N°938 - Improve print of portal object page and portal dashboard page
2023-11-24 15:17:42 +01:00
Molkobain
3b2da39469 N°6989 - Security hardening 2023-11-22 18:02:50 +01:00
odain
fc22d91232 N°6949 - Run subset of itop core unit tests after the setup when validating a module github code 2023-11-22 10:14:58 +01:00
Stephen Abello
b10bcb976d N°6951 - Security hardening 2023-11-21 09:42:11 +01:00
Pierre Goiffon
5a43448644 N°6458 Security hardening 2023-11-15 11:14:07 +01:00
Pierre Goiffon
77409eed99 🎨 DBObject small phpdoc fixes 2023-11-13 16:25:37 +01:00
Anne-Catherine
83a70daf68 N°6887 - Fix excessive OQL requests to display user's grant matrix (#564)
* N°6887 - Fix excessive OQL requests to display user's grant matrix

* N°6887 - Rename variable and add PHPDoc

---------

Co-authored-by: Molkobain <lajarige.guillaume@free.fr>
2023-11-08 14:57:28 +01:00
Molkobain
a49a4e6c2b N°6886 - Add OAuth tests folder to removable directories list 2023-10-30 16:54:28 +01:00
Romain Quetiez
7419749ba6 Prerequisites for boosting tests 2023-10-26 20:51:28 +02:00
Romain Quetiez
037dfe1df6 Optimize tests execution time 2023-10-25 17:51:12 +02:00
Romain Quetiez
0b26d45014 Prerequisites for boosting tests 2023-10-25 17:50:41 +02:00
Molkobain
4fd8177165 N°3715 - Fix unit tests 2023-10-23 14:55:06 +02:00
Anne-Catherine
a2cdf214f0 N°3715 - Export above 1000 entries ignore obsolete data from user preference (#468)
* N°3715 - Export above 10000 entries ignore obsolete data from user preference
2023-10-20 17:02:16 +02:00
Anne-Catherine
013173019f N°2909 - Search on Enum, Date, TagSet,... with index fails (#496) 2023-10-20 16:45:35 +02:00
Stephen Abello
9469681a0c N°6777 - Security hardening 2023-10-17 09:12:40 +02:00
Pierre Goiffon
c72cb7e70e N°6606 security hardening 2023-10-13 17:15:37 +02:00
Pierre Goiffon
9df92665e0 N°6606 Backport of utils::ENUM_SANITIZATION_FILTER_* constants
Were introduced in 3.0.0, but not added to the support/2.7 branch
2023-10-13 17:10:35 +02:00
Molkobain
8ecebee511 PHP unit tests: Fix typo for "final private" methods as they can't be both 2023-09-20 16:11:39 +02:00
Pierre Goiffon
35cd965360 N°6629 Update ci_description php_version 2023-09-19 12:25:40 +02:00
Pierre Goiffon
e5dd51f637 N°6600 Portal download attachment : don't display anymore SQL query on attachment not found error (#525) 2023-09-19 09:54:43 +02:00
Molkobain
0a6c82dfe1 N°6752 - PHP unit tests: Fix typo in postbuild_integration.xml.dist 2023-09-19 08:37:46 +02:00
Molkobain
24c0f4950f Add missing .htaccess / web.config files in .gitignore for /extensions folder 2023-09-18 15:26:06 +02:00
Molkobain
d4dbbc59d4 N°6754 - PHP unit tests: Add local PHPUnit XML files to .gitignore 2023-09-18 15:23:52 +02:00
Molkobain
dc0cd44c79 N°6752 - PHP unit tests: Migrate usages of unitestautoload.php to composer autoloader 2023-09-18 15:14:44 +02:00
odain
6c6131ce03 N°5491 - test enhancement to reduce false positive 2023-09-15 10:07:42 +02:00
odain-cbd
e76728b2bf N°5491 - Inconsistent dictionnary entries regarding arguments to pass to Dict::Format-test first (#545) 2023-09-13 12:02:49 +02:00
Pierre Goiffon
e946fc65fc N°6709 New ItopTestCase::RequireOnceCurrentModuleFile 2023-09-07 14:38:19 +02:00
Pierre Goiffon
0d8ff7bbac N°6629 Set commit tests back to Mysql
For now we have perf issues on Jenkins with MariaDB (see N°6694)
2023-09-04 10:25:41 +02:00
Anne-Catherine
bf768311c2 N°5136 - "Select All objects" add obsolete objects even if the parameter show obsolete data is not activated (#467) 2023-08-31 15:13:20 +02:00
Molkobain
a8c689c6c0 N°6436 - Add unit test to ensure that we don't lose an API during merge between branches 2023-08-18 09:55:45 +02:00
Molkobain
1990ccb5d8 N°6436 - Move interfaces enumeration from 1 line to 1 line / interface (and re-ordered them) for easier merges in newer branches 2023-08-18 09:52:55 +02:00
Molkobain
e107be56e4 N°6097 - Tests: Fix missing hook entry in PHPUnit XML file that led to compiled environment being re-build for each test case 2023-08-18 09:51:15 +02:00
Molkobain
ed6df77cbb N°6097 - Tests: Optimize performances by creating custom env. only once and re-using it across test classes 2023-08-10 15:45:39 +02:00
Molkobain
1ad28312ec N°6097 - Tests: Introduce autoloader for "utility" classes and move them to a sub-folder for better organization as folder was still messy
Note that unittestautoload.php is now useless. We just keep for now until everything is migrated (projects / branches / modules)
2023-08-10 15:45:39 +02:00
Molkobain
f002aa04cd N°6097 - Tests: Enable PHP unit tests on a custom DataModel 2023-08-10 15:45:39 +02:00
Molkobain
b86d70623e N°6097 - Tests: Temporarily add test case for the new ItopCustomDatamodelTestCase class 2023-08-10 15:45:39 +02:00
Molkobain
fe3467309d N°6097 - Tests: Refactor base test classes for better extensibility 2023-08-10 15:45:39 +02:00
Molkobain
851ab9c356 N°6097 - Add \utils::GetDataPath() method to avoid duplicating manual path build 2023-08-10 15:45:39 +02:00
Molkobain
aef3c2e609 N°6097 - Fix \CMDBSource::DropDB() not resetting cache like \CMDBSource::DropTable() which can lead to errors when trying to re-create it afterwards 2023-08-10 15:45:39 +02:00
Pierre Goiffon
6d13397ba1 Add other integration tests in the beforeSetup group
All of those tests can be ran without a running iTop instance, and are blocking
2023-08-08 15:33:09 +02:00
Pierre Goiffon
c0c8a13864 💡 \MetaModel::GetObject : remove documented throw Exception 2023-08-04 14:55:38 +02:00
Pierre Goiffon
cd9beec313 Merge remote-tracking branch 'origin/support/2.6' into support/2.7 2023-07-26 12:07:09 +02:00
Pierre Goiffon
8295eaed90 Merge remote-tracking branch 'origin/support/2.5' into support/2.6 2023-07-26 12:06:32 +02:00
Eric Espie
5475b9fbbe N°3454 - MoveToProd in 2 steps - fix utils::GetCurrentModuleName() 2023-07-25 17:44:43 +02:00
Eric Espie
6f8e7c7002 N°3454 - MoveToProd in 2 steps - fix utils::GetCurrentModuleUrl() 2023-07-25 17:20:37 +02:00
Pierre Goiffon
bc7c1b4744 N°6590 Fix DictionariesConsistencyTest for PL dict files 2023-07-24 11:14:37 +02:00
Eric Espie
4d8246c4d8 N°6436 - Integrate Performance Audit pre requisite in iTop Pro 2.7.9 (changed config variable name) 2023-07-19 15:13:43 +02:00
Eric Espie
5c61d725e1 N°6436 - Integrate Performance Audit pre requisite in iTop Pro 2.7.9 (changed config variable name) 2023-07-19 15:06:00 +02:00
Eric Espie
2c4cad4dac N°6436 - Integrate Performance Audit pre requisite in iTop Pro 2.7.9 (avoid unnecessary calls) 2023-07-19 10:37:41 +02:00
Eric Espie
da45651121 Merge branch 'feature/6548_Hide_DBHost_and_DBUser_in_log' into support/2.7 2023-07-18 09:34:48 +02:00
Eric Espie
d388ce9a06 Merge branch 'feature/6548_Hide_DBHost_and_DBUser_in_log' into support/2.7 2023-07-18 09:17:40 +02:00
Eric Espie
47e71d8838 Merge branch 'feature/6436-Integrate_Performance_Audit_extensibility' into support/2.7 2023-07-18 09:17:05 +02:00
Stephen Abello
2b5973ec67 N°6436 - Integrate Performance Audit pre requisite in iTop Pro 2.7.9 2023-07-18 09:15:37 +02:00
Eric Espie
78396d8e4a 6548 - [ER] Hide DBHost and DBUser in log 2023-07-10 17:37:27 +02:00
Stephen Abello
9afc22bd8f N°6123 - Add tests and comments 2023-07-07 09:29:15 +02:00
Pierre Goiffon
264a8cd70a N°6494 - Some tests are run twice, some never
(cherry picked from commit a2a0b2cd0b)

(cherry picked from commit 4c9ea0c9d4)

# Conflicts:
#	tests/php-unit-tests/integration-tests/DictionariesConsistencyTest.php
2023-07-06 15:45:09 +02:00
Stephen Abello
aa1834170b N°6427 - Fix SwiftMailer not retrieving sendmail path 2023-07-06 14:31:54 +02:00
Stephen Abello
f94d67ab35 N°6340 - Fix permission refused when sending an email and renewing OAuth token in synchronous mode 2023-07-06 10:28:10 +02:00
Stephen Abello
3048c8c41f N°5560 - Display an error when trying to regenerate an expired OAuth token 2023-07-06 09:52:00 +02:00
Stephen Abello
246e4a9f50 N°6123 - Fix warnings when launching a backup on MariaDB > v10.6.1 with localhost dbhost 2023-07-06 09:28:01 +02:00
Molkobain
6d58adb6dd N°6359 - Fix JS crash due to new version trying to detect MSIE browser through a dependency that we don't have.
Cherry-picked from f889c53d71
2023-07-05 08:41:00 +02:00
Pierre Goiffon
5a0b5364d6 N°4698 setup/phpinfo.php : if no iTop installation then display a proper message instead of an exception (#265) 2023-06-14 10:18:38 +02:00
Pierre Goiffon
76eed2eba0 N°6098 updateLicenses script : check availability of the required JQ command (#458)
This packaging script requires both bash and the JQ command when running on Windows.
If the later isn't available, it will run without throwing an error...

With this change the script will now check directly at launch for the JQ command availability, and exit in error if it isn't.
2023-06-14 10:17:00 +02:00
Eric Espie
1ec671ef61 N°6351 - code hardening 2023-06-14 09:08:42 +02:00
Eric Espie
72716b7ec8 N°6396 - Protect URL display 2023-06-12 11:36:51 +02:00
Eric Espie
4f999de844 N°6359 - ⬆️ Update jQuery BBQ (from https://github.com/cee-chen/jquery-bbq) 2023-06-08 14:30:09 +02:00
odain
ea49c0a87c enable authent-cas in ci 2023-06-07 21:44:17 +02:00
odain
6cc971849b ci: enhance AddProfile 2023-06-07 21:44:00 +02:00
Pierre Goiffon
2405810864 N°6238 Security hardening 2023-06-07 16:45:35 +02:00
Eric Espie
fff46d99fc N°6358 - Login REST API - renamed test 2023-06-07 15:31:51 +02:00
odain
3a891f707c ci: enhance AddProfile test method to work with any User (not only UserLocal) 2023-06-07 15:06:28 +02:00
odain
8b6ea43ebe N°6358 - Login REST API - fix cas + add tests 2023-06-07 15:05:32 +02:00
Eric Espie
90cf7502e8 N°6358 - Login REST API 2023-06-07 10:09:30 +02:00
Eric Espie
c596fa2967 N°6358 - Login API REST 2023-06-07 09:17:24 +02:00
Timothee
a45177410e N°6350 - Fixing phpunit test 2023-06-06 16:47:06 +02:00
Eric Espie
9e96ea2873 N°6350 - code hardening 2023-06-01 15:35:56 +02:00
Eric Espie
1172159745 N°6351 - code hardening 2023-06-01 15:12:50 +02:00
Pierre Goiffon
fa038ded3d N°6254 ItopDataTestCase::CreateUserRequest : fix new argument default value
Was creating error Too few arguments passed
2023-04-26 16:42:27 +02:00
Pierre Goiffon
e7ea1b831c N°6254 ItopDataTestCase::CreateUserRequest : now pass fields values as array
More versatile way of doing things !
2023-04-26 16:22:26 +02:00
Molkobain
4aff65f98b N°6217 - Add accessiblity meta data for title on "Power menu" 2023-04-25 21:51:32 +02:00
acognet
3c94974d9d N°541 - Dashlets: Improve readability when to much labels (pie chart) or too long labels (bar chart) 2023-04-25 12:09:11 +02:00
Molkobain
fbd72b2783 N°6217 - Add accessiblity meta data for title on "Power menu" 2023-04-20 11:03:43 +02:00
Anne-Catherine
4e95ca3c7b N°541 - Dashlets: Improve readability when to much labels (pie chart) or too long labels (bar chart) (#452)
* N°541 - Dashlets: Improve readability when to much labels (pie chart) or too long labels (bar chart)
2023-04-13 11:23:20 +02:00
Pierre Goiffon
1114ed9562 N°6099 DeadLockLog : improve documentation and use existing constants (#441) 2023-04-12 10:21:34 +02:00
Pierre Goiffon
34368fe795 N°6173 \HTMLSanitizer::Sanitize : Fix handling only svg_sanitizer (#450) 2023-04-11 17:52:41 +02:00
Molkobain
0f016d7511 N°6112 - Dashboard: Improve robustness by trimming dashlet ID returned by server 2023-03-17 15:37:57 +01:00
odain
0001e8ffc4 💚 use new ci validation 2020-10-09 10:13:51 +02:00
249 changed files with 8928 additions and 2139 deletions

83
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,83 @@
<!--
IMPORTANT: Please follow the guidelines within this PR template before submitting it, it will greatly help us process your PR. 🙏
Any PRs not following the guidelines or with missing information will not be considered.
-->
## Base information
| Question | Answer
|---------------------------------------------------------------|--------
| Related to a SourceForge thead / Another PR / Combodo ticket? | <!-- Put the URL -->
| Type of change? | Bug fix / Enhancement / Translations
## Symptom (bug) / Objective (enhancement)
<!--
If it's a bug
- Explain the symptom in details
- If possible put error messages, logs or screenshots (you can paste image directly in this editor).
If it's an enhancement
- Describe what is blocking you, what is the objective with as much details as possible.
- Add screenshots if it's related to UI.
-->
## Reproduction procedure (bug)
<!--
Remove this section only if it's NOT a bug.
Otherwise, explain step by step how to reproduce the issue on a standard iTop Community.
If it requires a custom datamodel, provide the minimal XML delta to reproduce it on a standard iTop Community.
-->
1. On iTop x.y.z <!-- Put complete iTop version (eg. 3.1.0-2) -->
2. With PHP x.y.z <!-- Put complete PHP version (eg. 8.1.24) -->
2. First go there
2. Then do that
3. ...
4. Finally, see that...
## Cause (bug)
<!--
Remove this section only if it's NOT a bug.
Otherwise, explain what is the cause of the issue (where in the code and why)
-->
## Proposed solution (bug and enhancement)
<!--
Explain in details how you are proposing to solve this:
- What did you do in the code and why
- If you changed something in the UI, put before / after screenshots (you can paste image directly in this editor)
-->
## Checklist before requesting a review
<!--
Don't remove these lines, check them once done.
-->
- [ ] I have performed a self-review of my code
- [ ] I have tested all changes I made on an iTop instance
- [ ] I have added a unit test, otherwise I have explained why I couldn't
- [ ] Is the PR clear and detailed enough so anyone can understand digging in the code?
## Checklist of things to do before PR is ready to merge
<!--
Things that needs to be done in the PR before it can be considered as ready to be merged
Examples:
- Changes requested in the review
- Unit test to add
- Dictionary entries to translate
- ...
-->
- [ ] ...
- [ ] ...
- [ ] ...

43
.github/workflows/action.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
name: Add PRs to Combodo PRs Dashboard
on:
pull_request_target:
types:
- opened
jobs:
add-to-project:
name: Add PR to Combodo Project
runs-on: ubuntu-latest
steps:
- name: Check if author is a member of the organization
id: check-membership
run: |
ORG="Combodo"
AUTHOR=$(jq -r .pull_request.user.login "$GITHUB_EVENT_PATH")
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -H "Authorization: token ${{ secrets.PR_AUTOMATICALLY_ADD_TO_PROJECT }}" \
"https://api.github.com/orgs/$ORG/members/$AUTHOR")
if [ "$RESPONSE" == "404" ]; then
echo "project_url=https://github.com/orgs/Combodo/projects/5" >> $GITHUB_ENV
echo "is_member=false" >> $GITHUB_ENV
else
echo "project_url=https://github.com/orgs/Combodo/projects/4" >> $GITHUB_ENV
echo "is_member=true" >> $GITHUB_ENV
fi
- name: Add internal tag if member
if: env.is_member == 'true'
run: |
curl -X POST -H "Authorization: token ${{ secrets.PR_AUTOMATICALLY_ADD_TO_PROJECT }}" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/Combodo/iTop/issues/${{ github.event.pull_request.number }}/labels \
-d '{"labels":["internal"]}'
env:
is_member: ${{ env.is_member }}
- name: Add PR to the appropriate project
uses: actions/add-to-project@v1.0.2
with:
project-url: ${{ env.project_url }}
github-token: ${{ secrets.PR_AUTOMATICALLY_ADD_TO_PROJECT }}

6
.gitignore vendored
View File

@@ -24,7 +24,9 @@ tests/*/vendor/*
# iTop extensions
/extensions/**
!/extensions/.htaccess
!/extensions/readme.txt
!/extensions/web.config
# all logs but listing prevention
/log/**
@@ -32,8 +34,10 @@ tests/*/vendor/*
!/log/index.php
!/log/web.config
# PHPUnit cache file
# PHPUnit: Cache file, local XML working copies
/tests/php-unit-tests/.phpunit.result.cache
/tests/php-unit-tests/phpunit.xml
/tests/php-unit-tests/postbuild_integration.xml
# Jetbrains

View File

@@ -27,7 +27,7 @@ $iTopFolder = __DIR__."/../../../";
require_once("$iTopFolder/approot.inc.php");
require_once(APPROOT."/application/utils.inc.php");
if (php_sapi_name() !== 'cli')
if (PHP_SAPI !== 'cli')
{
throw new \Exception('This script can only run from CLI');
}
@@ -48,4 +48,4 @@ if (!file_exists($sCssFile))
{
fwrite(STDERR, "Failed to compile $sCssFile, exiting.");
exit(1);
}
}

View File

@@ -26,7 +26,7 @@ $iTopFolder = __DIR__ . "/../../" ;
require_once ("$iTopFolder/approot.inc.php");
require_once (APPROOT."/setup/setuputils.class.inc.php");
if (php_sapi_name() !== 'cli')
if (PHP_SAPI !== 'cli')
{
throw new \Exception('This script can only run from CLI');
}
@@ -70,4 +70,4 @@ if (false === empty($aMissing)) {
echo "Some new tests dirs exists !\n"
.' They must be declared either in the allowed or denied list in '.iTopComposer::class." (see N°2651).\n"
.' List of dirs:'."\n".var_export($aMissing, true);
}
}

View File

@@ -19,17 +19,24 @@
* The target license file path is in `$xmlFilePath`
*/
$iTopFolder = __DIR__ . "/../../" ;
$xmlFilePath = $iTopFolder . "setup/licenses/community-licenses.xml";
$iTopFolder = __DIR__."/../../";
$xmlFilePath = $iTopFolder."setup/licenses/community-licenses.xml";
function get_scope($product_node)
{
$jqExec = shell_exec("jq -V"); // a param is mandatory otherwise the script will freeze
if ((null === $jqExec) || (false === $jqExec)) {
echo "/!\ JQ is required but cannot be launched :( \n";
echo "Check this script PHPDoc block for instructions\n";
die(-1);
}
function get_scope($product_node) {
$scope = $product_node->getAttribute("scope");
if ($scope === "")
{ //put iTop first
if ($scope === "") { //put iTop first
return "aaaaaaaaa";
}
return $scope;
}

View File

@@ -425,6 +425,12 @@ class UserRightsProfile extends UserRightsAddOnAPI
UR_ACTION_BULK_DELETE => 'bd',
);
/**
* @var array $aUsersProfilesList Cache of users' profiles. Hash array of user ID => [profile ID => profile friendlyname, profile ID => profile friendlyname, ...]
* @since 2.7.10 3.0.4 3.1.1 3.2.0 N°6887
*/
private $aUsersProfilesList = [];
// Installation: create the very first user
public function CreateAdministrator($sAdminUser, $sAdminPwd, $sLanguage = 'EN US')
{
@@ -654,8 +660,12 @@ class UserRightsProfile extends UserRightsAddOnAPI
$sAction = self::$m_aActionCodes[$iActionCode];
$bStatus = null;
// Cache user's profiles
if(false === array_key_exists($iUser, $this->aUsersProfilesList)){
$this->aUsersProfilesList[$iUser] = UserRights::ListProfiles($oUser);
}
// Call the API of UserRights because it caches the list for us
foreach(UserRights::ListProfiles($oUser) as $iProfile => $oProfile)
foreach($this->aUsersProfilesList[$iUser] as $iProfile => $oProfile)
{
$bGrant = $this->GetProfileActionGrant($iProfile, $sClass, $sAction);
if (!is_null($bGrant))
@@ -781,11 +791,16 @@ class UserRightsProfile extends UserRightsAddOnAPI
// Note: this code is VERY close to the code of IsActionAllowed()
$iUser = $oUser->GetKey();
// Cache user's profiles
if(false === array_key_exists($iUser, $this->aUsersProfilesList)){
$this->aUsersProfilesList[$iUser] = UserRights::ListProfiles($oUser);
}
// Note: The object set is ignored because it was interesting to optimize for huge data sets
// and acceptable to consider only the root class of the object set
$bStatus = null;
// Call the API of UserRights because it caches the list for us
foreach(UserRights::ListProfiles($oUser) as $iProfile => $oProfile)
foreach($this->aUsersProfilesList[$iUser] as $iProfile => $oProfile)
{
$bGrant = $this->GetClassStimulusGrant($iProfile, $sClass, $sStimulusCode);
if (!is_null($bGrant))
@@ -814,8 +829,9 @@ class UserRightsProfile extends UserRightsAddOnAPI
}
/**
* Find out which attribute is corresponding the the dimension 'owner org'
* returns null if no such attribute has been found (no filtering should occur)
* @param string $sClass
* @return string|null Find out which attribute is corresponding the dimension 'owner org'
* returns null if no such attribute has been found (no filtering should occur)
*/
public static function GetOwnerOrganizationAttCode($sClass)
{

View File

@@ -604,10 +604,10 @@ class UserRightsProfile extends UserRightsAddOnAPI
/**
* Read and cache organizations allowed to the given user
*
* @param $oUser
* @param $sClass (not used here but can be used in overloads)
* @param User $oUser
* @param string $sClass (not used here but can be used in overloads)
*
* @return array
* @return array keys of the User allowed org
* @throws \CoreException
* @throws \Exception
*/

View File

@@ -42,7 +42,7 @@ class ajax_page extends WebPage implements iTabbedPage
$this->m_sReadyScript = "";
//$this->add_header("Content-type: text/html; charset=utf-8");
$this->no_cache();
$this->add_xframe_options();
$this->add_http_headers();
$this->m_oTabs = new TabManager();
$this->sContentType = 'text/html';
$this->sContentDisposition = 'inline';
@@ -51,6 +51,16 @@ class ajax_page extends WebPage implements iTabbedPage
utils::InitArchiveMode();
}
/**
* Disabling sending the header so that resource won't be blocked by CORB. See parent method documentation.
* @return void
* @since 2.7.10 3.0.4 3.1.2 3.2.0 N°4368 method creation
*/
public function add_xcontent_type_options()
{
// Nothing to do !
}
/**
* @inheritDoc
* @throws \Exception

View File

@@ -1316,6 +1316,11 @@ interface iRestServiceProvider
public function ExecOperation($sVersion, $sVerb, $aParams);
}
interface iRestInputSanitizer
{
public function SanitizeJsonInput(string $sJsonInput): string;
}
/**
* Minimal REST response structure. Derive this structure to add response data and error codes.
*
@@ -1405,6 +1410,14 @@ class RestResult
* @api
*/
public $message;
/**
* Sanitize the content of this result to hide sensitive information
*/
public function SanitizeContent()
{
// The default implementation does nothing
}
}
/**
@@ -1621,6 +1634,8 @@ class RestUtils
*
* @return DBObject The object found
* @throws Exception If the input structure is not valid or it could not find exactly one object
*
* @see DBObject::CheckChangedExtKeysValues() generic method to check that we can access the linked object isn't used in that use case because values can be literal, OQL, friendlyname
*/
public static function FindObjectFromKey($sClass, $key, $bAllowNullValue = false)
{
@@ -1707,8 +1722,16 @@ class RestUtils
elseif (is_string($key))
{
// OQL
$oSearch = DBObjectSearch::FromOQL($key);
}
try {
$oSearch = DBObjectSearch::FromOQL($key);
} catch (Exception $e) {
throw new CoreOqlException('Query failed to execute', [
'query' => $key,
'exception_class' => get_class($e),
'exception_message' => $e->getMessage(),
]);
}
}
else
{
throw new Exception("Wrong format for key");
@@ -1857,4 +1880,28 @@ class RestUtils
interface iModuleExtension
{
public function __construct();
}
/**
* KPI logging extensibility point
*
* KPI Logger extension
*/
interface iKPILoggerExtension
{
/**
* Init the statistics collected
*
* @return void
*/
public function InitStats();
/**
* Add a new KPI to the stats
*
* @param \Combodo\iTop\Core\Kpi\KpiLogData $oKPILogData
*
* @return mixed
*/
public function LogOperation($oKPILogData);
}

View File

@@ -3218,13 +3218,13 @@ EOF
if ($oAttDef->GetEditClass() == 'Document')
{
$oDocument = $this->Get($sAttCode);
if (!$oDocument->IsEmpty())
if (is_object($oDocument) && !$oDocument->IsEmpty())
{
$sDisplayValue = $this->GetAsHTML($sAttCode);
$sDisplayValue .= "<br/>".Dict::Format('UI:OpenDocumentInNewWindow_',
$oDocument->GetDisplayLink(get_class($this), $this->GetKey(), $sAttCode)).", \n";
$oDocument->GetDisplayLink(get_class($this), $this->GetKey(), $sAttCode)).", \n";
$sDisplayValue .= "<br/>".Dict::Format('UI:DownloadDocument_',
$oDocument->GetDownloadLink(get_class($this), $this->GetKey(), $sAttCode)).", \n";
$oDocument->GetDownloadLink(get_class($this), $this->GetKey(), $sAttCode)).", \n";
}
else
{
@@ -4003,7 +4003,9 @@ EOF
/** @var \iApplicationObjectExtension $oExtensionInstance */
foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
{
$oExtensionInstance->OnDBInsert($this, self::GetCurrentChange());
$oKPI = new ExecutionKPI();
$oExtensionInstance->OnDBInsert($this, self::GetCurrentChange());
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnDBInsert');
}
return $res;
@@ -4020,13 +4022,16 @@ EOF
protected function DBCloneTracked_Internal($newKey = null)
{
$oNewObj = parent::DBCloneTracked_Internal($newKey);
/** @var cmdbAbstractObject $oNewObj */
$oNewObj = MetaModel::GetObject(get_class($this), parent::DBCloneTracked_Internal($newKey));
// Invoke extensions after insertion (the object must exist, have an id, etc.)
/** @var \iApplicationObjectExtension $oExtensionInstance */
foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
{
$oKPI = new ExecutionKPI();
$oExtensionInstance->OnDBInsert($oNewObj, self::GetCurrentChange());
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnDBInsert');
}
return $oNewObj;
@@ -4054,7 +4059,9 @@ EOF
/** @var \iApplicationObjectExtension $oExtensionInstance */
foreach (MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
{
$oKPI = new ExecutionKPI();
$oExtensionInstance->OnDBUpdate($this, self::GetCurrentChange());
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnDBUpdate');
}
}
catch (Exception $e)
@@ -4100,7 +4107,9 @@ EOF
/** @var \iApplicationObjectExtension $oExtensionInstance */
foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
{
$oKPI = new ExecutionKPI();
$oExtensionInstance->OnDBDelete($this, self::GetCurrentChange());
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnDBDelete');
}
return parent::DBDeleteTracked_Internal($oDeletionPlan);
@@ -4118,7 +4127,10 @@ EOF
/** @var \iApplicationObjectExtension $oExtensionInstance */
foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
{
if ($oExtensionInstance->OnIsModified($this))
$oKPI = new ExecutionKPI();
$bIsModified = $oExtensionInstance->OnIsModified($this);
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnIsModified');
if ($bIsModified)
{
return true;
}
@@ -4162,7 +4174,9 @@ EOF
/** @var \iApplicationObjectExtension $oExtensionInstance */
foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
{
$oKPI = new ExecutionKPI();
$aNewIssues = $oExtensionInstance->OnCheckToWrite($this);
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnCheckToWrite');
if (is_array($aNewIssues) && (count($aNewIssues) > 0)) // Some extensions return null instead of an empty array
{
$this->m_aCheckIssues = array_merge($this->m_aCheckIssues, $aNewIssues);
@@ -4210,7 +4224,9 @@ EOF
/** @var \iApplicationObjectExtension $oExtensionInstance */
foreach(MetaModel::EnumPlugins('iApplicationObjectExtension') as $oExtensionInstance)
{
$oKPI = new ExecutionKPI();
$aNewIssues = $oExtensionInstance->OnCheckToDelete($this);
$oKPI->ComputeStatsForExtension($oExtensionInstance, 'OnCheckToDelete');
if (is_array($aNewIssues) && count($aNewIssues) > 0)
{
$this->m_aDeleteIssues = array_merge($this->m_aDeleteIssues, $aNewIssues);
@@ -4722,7 +4738,7 @@ EOF
$bResult = (count($aErrors) == 0);
if ($bResult)
{
list($bResult, $aErrors) = $oObj->CheckToWrite();
[$bResult, $aErrors] = $oObj->CheckToWrite();
}
if ($bPreview)
{
@@ -4748,6 +4764,11 @@ EOF
);
if ($bResult && (!$bPreview))
{
// doing the check will load multiple times same objects :/
// but it shouldn't cost too much on execution time
// user can mitigate by selecting less extkeys/lnk to set and/or less objects to update 🤷‍♂️
$oObj->CheckChangedExtKeysValues();
$oObj->DBUpdate();
}
}

View File

@@ -33,7 +33,7 @@ class CSVPage extends WebPage
parent::__construct($s_title);
$this->add_header("Content-type: text/plain; charset=".self::PAGES_CHARSET);
$this->no_cache();
$this->add_xframe_options();
$this->add_http_headers();
//$this->add_header("Content-Transfer-Encoding: binary");
}

View File

@@ -529,10 +529,7 @@ EOF
*/
public function Render($oPage, $bEditMode = false, $aExtraParams = array(), $bCanEdit = true)
{
if (!array_key_exists('dashboard_div_id', $aExtraParams))
{
$aExtraParams['dashboard_div_id'] = utils::Sanitize($this->GetId(), '', 'element_identifier');
}
$aExtraParams['dashboard_div_id'] = utils::Sanitize($aExtraParams['dashboard_div_id'] ?? null, $this->GetId(), utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER);
$oPage->add('<div class="dashboard-title-line"><div class="dashboard-title">'.htmlentities(Dict::S($this->sTitle), ENT_QUOTES, 'UTF-8', false).'</div></div>');
@@ -1002,7 +999,7 @@ EOF
$sSelectorHtml .= '</div>';
$sSelectorHtml = addslashes($sSelectorHtml);
$sFile = addslashes($this->GetDefinitionFile());
$sReloadURL = $this->GetReloadURL();
$sReloadURL = json_encode($this->GetReloadURL());
$oPage->add_ready_script(
<<<EOF
@@ -1059,7 +1056,7 @@ EOF
$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.iframe-transport.js');
$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.fileupload.js');
$sEditMenu = "<div id=\"DashboardMenu\"><ul><li><i class=\"top-right-icon icon-additional-arrow fas fa-pencil-alt\"></i><ul>";
$aActions = array();
$sFile = addslashes($this->sDefinitionFile);
$sJSExtraParams = json_encode($aExtraParams);
@@ -1091,6 +1088,7 @@ EOF
EOF
);
$sReloadURL = json_encode($this->GetReloadURL());
$oPage->add_script(
<<<EOF
function EditDashboard(sId, sDashboardFile, aExtraParams)
@@ -1188,19 +1186,19 @@ EOF
$oPage->add('</div>');
$oPage->add('<div id="event_bus"/>'); // For exchanging messages between the panes, same as in the designer
$oPage->add('</div>');
$sDialogTitle = Dict::S('UI:DashboardEdit:Title');
$sOkButtonLabel = Dict::S('UI:Button:Save');
$sCancelButtonLabel = Dict::S('UI:Button:Cancel');
$sId = addslashes($this->sId);
$sLayoutClass = addslashes($this->sLayoutClass);
$sId = json_encode($this->sId);
$sLayoutClass = json_encode($this->sLayoutClass);
$sAutoReload = $this->bAutoReload ? 'true' : 'false';
$sAutoReloadSec = (string) $this->iAutoReloadSec;
$sTitle = addslashes($this->sTitle);
$sFile = addslashes($this->GetDefinitionFile());
$sTitle = json_encode($this->sTitle);
$sFile = json_encode($this->GetDefinitionFile());
$sUrl = utils::GetAbsoluteUrlAppRoot().'pages/ajax.render.php';
$sReloadURL = $this->GetReloadURL();
$sReloadURL = json_encode($this->GetReloadURL());
$sExitConfirmationMessage = addslashes(Dict::S('UI:NavigateAwayConfirmationMessage'));
$sCancelConfirmationMessage = addslashes(Dict::S('UI:CancelConfirmationMessage'));
@@ -1250,15 +1248,15 @@ $('#dashboard_editor').dialog({
});
$('#dashboard_editor .ui-layout-center').runtimedashboard({
dashboard_id: '$sId',
layout_class: '$sLayoutClass',
title: '$sTitle',
dashboard_id: $sId,
layout_class: $sLayoutClass,
title: $sTitle,
auto_reload: $sAutoReload,
auto_reload_sec: $sAutoReloadSec,
submit_to: '$sUrl',
submit_parameters: {operation: 'save_dashboard', file: '$sFile', extra_params: $sJSExtraParams, reload_url: '$sReloadURL'},
submit_parameters: {operation: 'save_dashboard', file: $sFile, extra_params: $sJSExtraParams, reload_url: '$sReloadURL'},
render_to: '$sUrl',
render_parameters: {operation: 'render_dashboard', file: '$sFile', extra_params: $sJSExtraParams, reload_url: '$sReloadURL'},
render_parameters: {operation: 'render_dashboard', file: $sFile, extra_params: $sJSExtraParams, reload_url: '$sReloadURL'},
new_dashlet_parameters: {operation: 'new_dashlet'}
});

View File

@@ -372,7 +372,7 @@ EOF;
if (!$oPage->IsPrintableVersion())
{
$sMenuTitle = Dict::S('UI:ConfigureThisList');
$sHtml = '<div class="itop_popup toolkit_menu" id="tk_'.$this->iListId.'"><ul><li><i class="fas fa-tools"></i><i class="fas fa-caret-down"></i><ul>';
$sHtml = '<div class="itop_popup toolkit_menu" id="tk_'.$this->iListId.'"><ul><li aria-label="'.Dict::S('UI:Menu:Toolkit').'"><i class="fas fa-tools"></i><i class="fas fa-caret-down"></i><ul>';
$oMenuItem1 = new JSPopupMenuItem('iTop::ConfigureList', $sMenuTitle, "$('#datatable_dlg_".$this->iListId."').dialog('open');");
$aActions = array(

View File

@@ -1009,6 +1009,7 @@ EOF
$iTotalCount = 0;
$aValues = array();
$aURLs = array();
foreach ($aRes as $iRow => $aRow)
{
$sValue = $aRow['grouped_by_1'];
@@ -1016,7 +1017,8 @@ EOF
$aGroupBy[(int)$iRow] = (int) $aRow[$sFctVar];
$iTotalCount += $aRow['_itop_count_'];
$aValues[] = array('label' => html_entity_decode(strip_tags($sHtmlValue), ENT_QUOTES, 'UTF-8'), 'label_html' => $sHtmlValue, 'value' => (int) $aRow[$sFctVar]);
// Build the search for this subset
$oSubsetSearch = $this->m_oFilter->DeepClone();
$oCondition = new BinaryExpression($oGroupByExp, '=', new ScalarExpression($sValue));
@@ -1030,16 +1032,23 @@ EOF
{
case 'bars':
$aNames = array();
$iMaxNbCharsInLabel = 0;
foreach($aValues as $idx => $aValue)
{
$aNames[$idx] = $aValue['label'];
if ($iMaxNbCharsInLabel < mb_strlen($aValue['label'])) {
$iMaxNbCharsInLabel = mb_strlen($aValue['label']);
}
}
$sJSNames = json_encode($aNames);
$sJson = json_encode($aValues);
$oPage->add_ready_script(
<<<EOF
var iChartDefaultHeight = 200,
iChartLegendHeight = 6 * $iMaxNbCharsInLabel,
iChartTotalHeight = iChartDefaultHeight + iChartLegendHeight;
$('#my_chart_$sId').height(iChartTotalHeight+ 'px');
var chart = c3.generate({
bindto: d3.select('#my_chart_$sId'),
data: {
@@ -1107,8 +1116,19 @@ EOF
}
$sJSColumns = json_encode($aColumns);
$sJSNames = json_encode($aNames);
$iNbLinesToAddForName = 0;
if (count($aNames) > 50) {
// Calculation of the number of legends line add to the height of the graph to have a maximum of 5 legend columns
$iNbLinesIncludedInChartHeight = 10;
$iNbLinesToAddForName = ceil(count($aNames) / 5) - $iNbLinesIncludedInChartHeight;
}
$oPage->add_ready_script(
<<<EOF
// Calculate height of graph : 200px (minimum height for the chart) + 20*iNbLinesToAddForName for the legend
var iChartDefaultHeight = 200,
iChartLegendHeight = 20 * $iNbLinesToAddForName,
iChartTotalHeight = (iChartDefaultHeight + iChartLegendHeight);
$('#my_chart_$sId').height(iChartTotalHeight + 'px');
var chart = c3.generate({
bindto: d3.select('#my_chart_$sId'),
data: {
@@ -1915,11 +1935,13 @@ class MenuBlock extends DisplayBlock
{
if (count($aFavoriteActions) > 0)
{
$sHtml .= "<div class=\"itop_popup actions_menu\"><ul>\n<li>".Dict::S('UI:Menu:OtherActions')."<i class=\"fas fa-caret-down\"></i>"."\n<ul>\n";
$sActionsMenuLabel = Dict::S('UI:Menu:OtherActions');
$sHtml .= "<div class=\"itop_popup actions_menu\"><ul>\n<li aria-label=\"{$sActionsMenuLabel}\">{$sActionsMenuLabel}<i class=\"fas fa-caret-down\"></i>"."\n<ul>\n";
}
else
{
$sHtml .= "<div class=\"itop_popup actions_menu\"><ul>\n<li>".Dict::S('UI:Menu:Actions')."<i class=\"fas fa-caret-down\"></i>"."\n<ul>\n";
$sActionsMenuLabel = Dict::S('UI:Menu:Actions');
$sHtml .= "<div class=\"itop_popup actions_menu\"><ul>\n<li aria-label=\"{$sActionsMenuLabel}\">{$sActionsMenuLabel}<i class=\"fas fa-caret-down\"></i>"."\n<ul>\n";
}
$sHtml .= $oPage->RenderPopupMenuItems($aActions, $aFavoriteActions);

View File

@@ -71,7 +71,7 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
$this->SetRootUrl(utils::GetAbsoluteUrlAppRoot());
$this->add_header("Content-type: text/html; charset=".self::PAGES_CHARSET);
$this->no_cache();
$this->add_xframe_options();
$this->add_http_headers();
$this->add_linked_stylesheet("../css/jquery.treeview.css");
$this->add_linked_stylesheet("../css/jquery.autocomplete.css");
$this->add_linked_stylesheet("../css/jquery-ui-timepicker-addon.css");
@@ -1219,7 +1219,7 @@ EOF;
{
$sLogonMessage = Dict::Format('UI:LoggedAsMessage', $sUserName);
}
$sLogOffMenu = "<span id=\"logOffBtn\"><ul><li><i class=\"top-right-icon icon-additional-arrow fas fa-power-off\"></i><ul>";
$sLogOffMenu = "<span id=\"logOffBtn\"><ul><li aria-label=\"" . Dict::S("UI:PowerMenu") . "\"><i class=\"top-right-icon icon-additional-arrow fas fa-power-off\"></i><ul>";
$sLogOffMenu .= "<li><span>$sLogonMessage</span></li>\n";
$aActions = array();

View File

@@ -51,7 +51,7 @@ class LoginBasic extends AbstractLoginFSMExtension
protected function OnCheckCredentials(&$iErrorCode)
{
if ($_SESSION['login_mode'] == 'basic')
if (isset($_SESSION['login_mode']) && $_SESSION['login_mode'] == 'basic')
{
list($sAuthUser, $sAuthPwd) = $this->GetAuthUserAndPassword();
if (!UserRights::CheckCredentials($sAuthUser, $sAuthPwd, $_SESSION['login_mode'], 'internal'))
@@ -67,7 +67,7 @@ class LoginBasic extends AbstractLoginFSMExtension
protected function OnCredentialsOK(&$iErrorCode)
{
if ($_SESSION['login_mode'] == 'basic')
if (isset($_SESSION['login_mode']) && $_SESSION['login_mode'] == 'basic')
{
$sAuthUser = $_SESSION['auth_user'];
LoginWebPage::OnLoginSuccess($sAuthUser, 'internal', $_SESSION['login_mode']);
@@ -77,8 +77,13 @@ class LoginBasic extends AbstractLoginFSMExtension
protected function OnError(&$iErrorCode)
{
if ($_SESSION['login_mode'] == 'basic')
if (isset($_SESSION['login_mode']) && $_SESSION['login_mode'] == 'basic')
{
$iOnExit = LoginWebPage::getIOnExit();
if ($iOnExit === LoginWebPage::EXIT_RETURN)
{
return LoginWebPage::LOGIN_FSM_RETURN; // Error, exit FSM
}
LoginWebPage::HTTP401Error();
}
return LoginWebPage::LOGIN_FSM_CONTINUE;
@@ -86,7 +91,7 @@ class LoginBasic extends AbstractLoginFSMExtension
protected function OnConnected(&$iErrorCode)
{
if ($_SESSION['login_mode'] == 'basic')
if (isset($_SESSION['login_mode']) && $_SESSION['login_mode'] == 'basic')
{
$_SESSION['can_logoff'] = true;
return LoginWebPage::CheckLoggedUser($iErrorCode);

View File

@@ -77,7 +77,7 @@ class LoginDefaultAfter extends AbstractLoginFSMExtension implements iLogoutExte
{
self::ResetLoginSession();
$iOnExit = LoginWebPage::getIOnExit();
if ($iOnExit == LoginWebPage::EXIT_RETURN)
if ($iOnExit === LoginWebPage::EXIT_RETURN)
{
return LoginWebPage::LOGIN_FSM_RETURN; // Error, exit FSM
}
@@ -93,6 +93,12 @@ class LoginDefaultAfter extends AbstractLoginFSMExtension implements iLogoutExte
{
if (!isset($_SESSION['login_mode']))
{
// N°6358 - if EXIT_RETURN was asked, send an error
if (LoginWebPage::getIOnExit() === LoginWebPage::EXIT_RETURN) {
$iErrorCode = LoginWebPage::EXIT_CODE_WRONGCREDENTIALS;
return LoginWebPage::LOGIN_FSM_ERROR;
}
// If no plugin validated the user, exit
self::ResetLoginSession();
exit();
@@ -111,6 +117,11 @@ class LoginDefaultAfter extends AbstractLoginFSMExtension implements iLogoutExte
protected function OnConnected(&$iErrorCode)
{
unset($_SESSION['login_temp_auth_user']);
if (is_null(UserRights::GetUserObject())){
//N°7085 avoid infinite loop
IssueLog::Error("No user logged in. exit");
exit(-1);
}
return LoginWebPage::LOGIN_FSM_CONTINUE;
}
@@ -126,4 +137,4 @@ class LoginDefaultAfter extends AbstractLoginFSMExtension implements iLogoutExte
}
}
}
}
}

View File

@@ -35,7 +35,7 @@ class LoginExternal extends AbstractLoginFSMExtension
protected function OnCheckCredentials(&$iErrorCode)
{
if ($_SESSION['login_mode'] == 'external')
if (isset($_SESSION['login_mode']) && $_SESSION['login_mode'] == 'external')
{
$sAuthUser = $this->GetAuthUser();
if (!UserRights::CheckCredentials($sAuthUser, '', $_SESSION['login_mode'], 'external'))
@@ -51,7 +51,7 @@ class LoginExternal extends AbstractLoginFSMExtension
protected function OnCredentialsOK(&$iErrorCode)
{
if ($_SESSION['login_mode'] == 'external')
if (isset($_SESSION['login_mode']) && $_SESSION['login_mode'] == 'external')
{
$sAuthUser = $_SESSION['auth_user'];
LoginWebPage::OnLoginSuccess($sAuthUser, 'external', $_SESSION['login_mode']);
@@ -61,7 +61,7 @@ class LoginExternal extends AbstractLoginFSMExtension
protected function OnConnected(&$iErrorCode)
{
if ($_SESSION['login_mode'] == 'external')
if (isset($_SESSION['login_mode']) && $_SESSION['login_mode'] == 'external')
{
$_SESSION['can_logoff'] = false;
return LoginWebPage::CheckLoggedUser($iErrorCode);
@@ -71,8 +71,13 @@ class LoginExternal extends AbstractLoginFSMExtension
protected function OnError(&$iErrorCode)
{
if ($_SESSION['login_mode'] == 'external')
if (isset($_SESSION['login_mode']) && $_SESSION['login_mode'] == 'external')
{
$iOnExit = LoginWebPage::getIOnExit();
if ($iOnExit === LoginWebPage::EXIT_RETURN)
{
return LoginWebPage::LOGIN_FSM_RETURN; // Error, exit FSM
}
LoginWebPage::HTTP401Error();
}
return LoginWebPage::LOGIN_FSM_CONTINUE;

View File

@@ -43,6 +43,10 @@ class LoginForm extends AbstractLoginFSMExtension implements iLoginUIExtension
exit;
}
if (LoginWebPage::getIOnExit() === LoginWebPage::EXIT_RETURN) {
return LoginWebPage::LOGIN_FSM_CONTINUE;
}
// No credentials yet, display the form
$oPage = LoginWebPage::NewLoginWebPage();
$oPage->DisplayLoginForm($this->bForceFormOnError);
@@ -62,7 +66,7 @@ class LoginForm extends AbstractLoginFSMExtension implements iLoginUIExtension
*/
protected function OnCheckCredentials(&$iErrorCode)
{
if ($_SESSION['login_mode'] == 'form')
if (isset($_SESSION['login_mode']) && $_SESSION['login_mode'] == 'form')
{
$sAuthUser = utils::ReadPostedParam('auth_user', '', 'raw_data');
$sAuthPwd = utils::ReadPostedParam('auth_pwd', null, 'raw_data');
@@ -82,7 +86,7 @@ class LoginForm extends AbstractLoginFSMExtension implements iLoginUIExtension
*/
protected function OnCredentialsOK(&$iErrorCode)
{
if ($_SESSION['login_mode'] == 'form')
if (isset($_SESSION['login_mode']) && $_SESSION['login_mode'] == 'form')
{
$sAuthUser = $_SESSION['auth_user'];
// Store 'auth_user' in session for further use
@@ -96,7 +100,7 @@ class LoginForm extends AbstractLoginFSMExtension implements iLoginUIExtension
*/
protected function OnError(&$iErrorCode)
{
if ($_SESSION['login_mode'] == 'form')
if (isset($_SESSION['login_mode']) && $_SESSION['login_mode'] == 'form')
{
$this->bForceFormOnError = true;
}
@@ -108,7 +112,7 @@ class LoginForm extends AbstractLoginFSMExtension implements iLoginUIExtension
*/
protected function OnConnected(&$iErrorCode)
{
if ($_SESSION['login_mode'] == 'form')
if (isset($_SESSION['login_mode']) && $_SESSION['login_mode'] == 'form')
{
$_SESSION['can_logoff'] = true;
return LoginWebPage::CheckLoggedUser($iErrorCode);

View File

@@ -40,7 +40,7 @@ class LoginURL extends AbstractLoginFSMExtension
protected function OnReadCredentials(&$iErrorCode)
{
if ($_SESSION['login_mode'] == 'url')
if (isset($_SESSION['login_mode']) && $_SESSION['login_mode'] == 'url')
{
$_SESSION['login_temp_auth_user'] = utils::ReadParam('auth_user', '', false, 'raw_data');
}
@@ -49,7 +49,7 @@ class LoginURL extends AbstractLoginFSMExtension
protected function OnCheckCredentials(&$iErrorCode)
{
if ($_SESSION['login_mode'] == 'url')
if (isset($_SESSION['login_mode']) && $_SESSION['login_mode'] == 'url')
{
$sAuthUser = utils::ReadParam('auth_user', '', false, 'raw_data');
$sAuthPwd = utils::ReadParam('auth_pwd', null, false, 'raw_data');
@@ -66,7 +66,7 @@ class LoginURL extends AbstractLoginFSMExtension
protected function OnCredentialsOK(&$iErrorCode)
{
if ($_SESSION['login_mode'] == 'url')
if (isset($_SESSION['login_mode']) && $_SESSION['login_mode'] == 'url')
{
$sAuthUser = $_SESSION['auth_user'];
LoginWebPage::OnLoginSuccess($sAuthUser, 'internal', $_SESSION['login_mode']);
@@ -76,7 +76,7 @@ class LoginURL extends AbstractLoginFSMExtension
protected function OnError(&$iErrorCode)
{
if ($_SESSION['login_mode'] == 'url')
if (isset($_SESSION['login_mode']) && $_SESSION['login_mode'] == 'url')
{
$this->bErrorOccurred = true;
}

View File

@@ -32,7 +32,7 @@ class LoginWebPage extends NiceWebPage
{
const EXIT_PROMPT = 0;
const EXIT_HTTP_401 = 1;
const EXIT_RETURN = 2;
const EXIT_RETURN = 2; // Non interactive mode (ajax, rest, ...)
const EXIT_CODE_OK = 0;
const EXIT_CODE_MISSINGLOGIN = 1;
@@ -85,7 +85,7 @@ class LoginWebPage extends NiceWebPage
parent::__construct($sTitle);
$this->SetStyleSheet();
$this->no_cache();
$this->add_xframe_options();
$this->add_http_headers();
}
public function SetStyleSheet()
@@ -352,14 +352,20 @@ class LoginWebPage extends NiceWebPage
$this->output();
}
public static function ResetSession()
public static function ResetSession($bFullCleanup = false)
{
// Unset all of the session variables.
unset($_SESSION['auth_user']);
unset($_SESSION['login_state']);
unset($_SESSION['can_logoff']);
unset($_SESSION['archive_mode']);
unset($_SESSION['impersonate_user']);
if ($bFullCleanup) {
// Unset all of the session variables.
foreach (array_keys($_SESSION) as $sKey) {
unset($_SESSION[$sKey]);
}
} else {
unset($_SESSION['auth_user']);
unset($_SESSION['login_state']);
unset($_SESSION['can_logoff']);
unset($_SESSION['archive_mode']);
unset($_SESSION['impersonate_user']);
}
UserRights::_ResetSessionCache();
// If it's desired to kill the session, also delete the session cookie.
// Note: This will destroy the session, and not just the session data!
@@ -931,7 +937,7 @@ class LoginWebPage extends NiceWebPage
}
else
{
if ($iOnExit == self::EXIT_RETURN)
if ($iOnExit === self::EXIT_RETURN)
{
return self::EXIT_CODE_PORTALUSERNOTAUTHORIZED;
}
@@ -987,7 +993,7 @@ class LoginWebPage extends NiceWebPage
{
if ($bMustBeAdmin && !UserRights::IsAdministrator())
{
if ($iOnExit == self::EXIT_RETURN)
if ($iOnExit === self::EXIT_RETURN)
{
return self::EXIT_CODE_MUSTBEADMIN;
}
@@ -1003,7 +1009,7 @@ class LoginWebPage extends NiceWebPage
}
$iRet = call_user_func(array(self::$sHandlerClass, 'ChangeLocation'), $sRequestedPortalId, $iOnExit);
}
if ($iOnExit == self::EXIT_RETURN)
if ($iOnExit === self::EXIT_RETURN)
{
return $iRet;
}

View File

@@ -91,4 +91,10 @@ else
$_SESSION['itop_env'] = ITOP_DEFAULT_ENV;
}
$sConfigFile = APPCONF.$sEnv.'/'.ITOP_CONFIG_FILE;
MetaModel::Startup($sConfigFile, false /* $bModelOnly */, $bAllowCache, false /* $bTraceSourceFiles */, $sEnv);
try {
MetaModel::Startup($sConfigFile, false /* $bModelOnly */, $bAllowCache, false /* $bTraceSourceFiles */, $sEnv);
}
catch (MySQLException $e) {
IssueLog::Debug($e->getMessage());
throw new MySQLException('Could not connect to the DB server', []);
}

View File

@@ -17,6 +17,7 @@
* You should have received a copy of the GNU Affero General Public License
*/
use Combodo\iTop\Service\Module\ModuleService;
use ScssPhp\ScssPhp\Compiler;
@@ -44,6 +45,74 @@ class FileUploadException extends Exception
*/
class utils
{
/**
* @var string
* @since 2.7.10 3.0.0
*/
public const ENUM_SANITIZATION_FILTER_INTEGER = 'integer';
/**
* Datamodel class
* @var string
* @since 2.7.10 3.0.0
* @since 2.7.10 3.0.4 3.1.1 3.2.0 N°6606 update PHPDoc
* @uses MetaModel::IsValidClass()
*/
public const ENUM_SANITIZATION_FILTER_CLASS = 'class';
/**
* @var string
* @since 2.7.10 3.0.4 3.1.1 3.2.0 N°6606
* @uses class_exists()
*/
public const ENUM_SANITIZATION_FILTER_PHP_CLASS = 'php_class';
/**
* @var string
* @since 2.7.10 3.0.0
*/
public const ENUM_SANITIZATION_FILTER_STRING = 'string';
/**
* @var string
* @since 2.7.10 3.0.0
*/
public const ENUM_SANITIZATION_FILTER_CONTEXT_PARAM = 'context_param';
/**
* @var string
* @since 2.7.10 3.0.0
*/
public const ENUM_SANITIZATION_FILTER_PARAMETER = 'parameter';
/**
* @var string
* @since 2.7.10 3.0.0
*/
public const ENUM_SANITIZATION_FILTER_FIELD_NAME = 'field_name';
/**
* @var string
* @since 2.7.10 3.0.0
*/
public const ENUM_SANITIZATION_FILTER_TRANSACTION_ID = 'transaction_id';
/**
* @var string For XML / HTML node identifiers
* @since 2.7.10 3.0.0
*/
public const ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER = 'element_identifier';
/**
* @var string
* @since 2.7.10 3.0.0
*/
public const ENUM_SANITIZATION_FILTER_RAW_DATA = 'raw_data';
/**
* @var string
* @since 3.0.2 3.1.0 N°4899
* @since 2.7.10 N°6606
*/
public const ENUM_SANITIZATION_FILTER_URL = 'url';
/**
* @var string
* @since 3.0.0
* @since 2.7.10 N°6606
*/
public const DEFAULT_SANITIZATION_FILTER = self::ENUM_SANITIZATION_FILTER_RAW_DATA;
/**
* Cache when getting config from disk or set externally (using {@link SetConfig})
* @internal
@@ -130,16 +199,8 @@ class utils
public static function IsModeCLI()
{
$sSAPIName = php_sapi_name();
$sCleanName = strtolower(trim($sSAPIName));
if ($sCleanName == 'cli')
{
return true;
}
else
{
return false;
}
$sCleanName = strtolower(trim(PHP_SAPI));
return ($sCleanName === 'cli');
}
protected static $bPageMode = null;
@@ -247,13 +308,13 @@ class utils
}
return self::Sanitize($retValue, $defaultValue, $sSanitizationFilter);
}
public static function ReadPostedParam($sName, $defaultValue = '', $sSanitizationFilter = 'parameter')
{
$retValue = isset($_POST[$sName]) ? $_POST[$sName] : $defaultValue;
return self::Sanitize($retValue, $defaultValue, $sSanitizationFilter);
}
public static function Sanitize($value, $defaultValue, $sSanitizationFilter)
{
if ($value === $defaultValue)
@@ -269,13 +330,12 @@ class utils
$retValue = $defaultValue;
}
}
return $retValue;
return $retValue;
}
/**
* @param string|string[] $value
* @param string $sSanitizationFilter one of : integer, class, string, context_param, parameter, field_name,
* element_identifier, transaction_id, parameter, raw_data
* @param string $sSanitizationFilter one of utils::ENUM_SANITIZATION_* const
*
* @return string|string[]|bool boolean for :
* * the 'class' filter (true if valid, false otherwise)
@@ -284,16 +344,20 @@ class utils
* @since 2.5.2 2.6.0 new 'transaction_id' filter
* @since 2.7.0 new 'element_identifier' filter
* @since 2.7.7, 3.0.2, 3.1.0 N°4899 - new 'url' filter
* @since 2.7.10 N°6606 use the utils::ENUM_SANITIZATION_* const
* @since 2.7.10 N°6606 new case for ENUM_SANITIZATION_FILTER_PHP_CLASS
*
* @link https://www.php.net/manual/en/filter.filters.sanitize.php PHP sanitization filters
*/
protected static function Sanitize_Internal($value, $sSanitizationFilter)
{
switch ($sSanitizationFilter)
{
case 'integer':
case static::ENUM_SANITIZATION_FILTER_INTEGER:
$retValue = filter_var($value, FILTER_SANITIZE_NUMBER_INT);
break;
case 'class':
case static::ENUM_SANITIZATION_FILTER_CLASS:
$retValue = $value;
if (!MetaModel::IsValidClass($value))
{
@@ -301,14 +365,21 @@ class utils
}
break;
case 'string':
case static::ENUM_SANITIZATION_FILTER_STRING:
$retValue = filter_var($value, FILTER_SANITIZE_SPECIAL_CHARS);
break;
case 'context_param':
case 'parameter':
case 'field_name':
case 'transaction_id':
case static::ENUM_SANITIZATION_FILTER_PHP_CLASS:
$retValue = $value;
if (!class_exists($value)) {
$retValue = false;
}
break;
case static::ENUM_SANITIZATION_FILTER_CONTEXT_PARAM:
case static::ENUM_SANITIZATION_FILTER_PARAMETER:
case static::ENUM_SANITIZATION_FILTER_FIELD_NAME:
case static::ENUM_SANITIZATION_FILTER_TRANSACTION_ID:
if (is_array($value))
{
$retValue = array();
@@ -326,7 +397,7 @@ class utils
{
switch ($sSanitizationFilter)
{
case 'transaction_id':
case static::ENUM_SANITIZATION_FILTER_TRANSACTION_ID:
// same as parameter type but keep the dot character
// see N°1835 : when using file transaction_id on Windows you get *.tmp tokens
// it must be included at the regexp beginning otherwise you'll get an invalid character error
@@ -334,18 +405,18 @@ class utils
array("options" => array("regexp" => '/^[\. A-Za-z0-9_=-]*$/')));
break;
case 'parameter':
case static::ENUM_SANITIZATION_FILTER_PARAMETER:
$retValue = filter_var($value, FILTER_VALIDATE_REGEXP,
array("options" => array("regexp" => '/^[ A-Za-z0-9_=-]*$/'))); // the '=', '%3D, '%2B', '%2F'
// characters are used in serialized filters (starting 2.5, only the url encoded versions are presents, but the "=" is kept for BC)
break;
case 'field_name':
case static::ENUM_SANITIZATION_FILTER_FIELD_NAME:
$retValue = filter_var($value, FILTER_VALIDATE_REGEXP,
array("options" => array("regexp" => '/^[A-Za-z0-9_]+(->[A-Za-z0-9_]+)*$/'))); // att_code or att_code->name or AttCode->Name or AttCode->Key2->Name
break;
case 'context_param':
case static::ENUM_SANITIZATION_FILTER_CONTEXT_PARAM:
$retValue = filter_var($value, FILTER_VALIDATE_REGEXP,
array("options" => array("regexp" => '/^[ A-Za-z0-9_=%:+-]*$/')));
break;
@@ -355,17 +426,18 @@ class utils
break;
// For XML / HTML node identifiers
case 'element_identifier':
case static::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER:
$retValue = preg_replace('/[^a-zA-Z0-9_]/', '', $value);
break;
// For URL
case 'url':
case static::ENUM_SANITIZATION_FILTER_URL:
$retValue = filter_var($value, FILTER_SANITIZE_URL);
$retValue = filter_var($retValue, FILTER_VALIDATE_URL);
break;
default:
case 'raw_data':
case static::ENUM_SANITIZATION_FILTER_RAW_DATA:
$retValue = $value;
// Do nothing
}
@@ -401,11 +473,11 @@ class utils
$sMimeType = self::GetFileMimeType($sTmpName);
$oDocument = new ormDocument($doc_content, $sMimeType, $sName);
break;
case UPLOAD_ERR_NO_FILE:
// no file to load, it's a normal case, just return an empty document
break;
case UPLOAD_ERR_FORM_SIZE:
case UPLOAD_ERR_INI_SIZE:
throw new FileUploadException(Dict::Format('UI:Error:UploadedFileTooBig', ini_get('upload_max_filesize')));
@@ -414,7 +486,7 @@ class utils
case UPLOAD_ERR_PARTIAL:
throw new FileUploadException(Dict::S('UI:Error:UploadedFileTruncated.'));
break;
case UPLOAD_ERR_NO_TMP_DIR:
throw new FileUploadException(Dict::S('UI:Error:NoTmpDir'));
break;
@@ -427,7 +499,7 @@ class utils
$sName = is_null($sIndex) ? $aFileInfo['name'] : $aFileInfo['name'][$sIndex];
throw new FileUploadException(Dict::Format('UI:Error:UploadStoppedByExtension_FileName', $sName));
break;
default:
throw new FileUploadException(Dict::Format('UI:Error:UploadFailedUnknownCause_Code', $sError));
break;
@@ -535,17 +607,17 @@ class utils
return $aSelectedObj;
}
public static function GetNewTransactionId()
{
return privUITransaction::GetNewTransactionId();
}
public static function IsTransactionValid($sId, $bRemoveTransaction = true)
{
return privUITransaction::IsTransactionValid($sId, $bRemoveTransaction);
}
public static function RemoveTransaction($sId)
{
return privUITransaction::RemoveTransaction($sId);
@@ -730,9 +802,9 @@ class utils
$aDateTokens = array_keys($aSpec);
$aDateRegexps = array_values($aSpec);
}
$sDateRegexp = str_replace($aDateTokens, $aDateRegexps, $sFormat);
if (preg_match('!^(?<head>)'.$sDateRegexp.'(?<tail>)$!', $sDate, $aMatches))
{
$sYear = isset($aMatches['year']) ? $aMatches['year'] : 0;
@@ -749,7 +821,7 @@ class utils
}
// http://www.spaweditor.com/scripts/regex/index.php
}
/**
* Convert an old date/time format specification (using % placeholders)
* to a format compatible with DateTime::createFromFormat
@@ -1168,7 +1240,7 @@ class utils
{
$aArguments['param_file'] = $sParamFile;
}
$aArgs = array();
foreach($aArguments as $sName => $value)
{
@@ -1177,7 +1249,7 @@ class utils
$aArgs[] = "--$sName=".escapeshellarg($value);
}
$sArgs = implode(' ', $aArgs);
$sScript = realpath(APPROOT.$sScriptName);
if (!file_exists($sScript))
{
@@ -1226,13 +1298,23 @@ class utils
}
}
/**
* @return string A path to the folder into which data can be written
* @internal
* @since N°6097 2.7.10 3.0.4 3.1.1
*/
public static function GetDataPath(): string
{
return APPROOT.'data/';
}
/**
* @return string A path to a folder into which any module can store cache data
* The corresponding folder is created or cleaned upon code compilation
*/
public static function GetCachePath()
{
return APPROOT.'data/cache-'.MetaModel::GetEnvironment().'/';
return static::GetDataPath().'cache-'.MetaModel::GetEnvironment().'/';
}
/**
* @return string A path to a folder into which any module can store log
@@ -1258,7 +1340,7 @@ class utils
public static function GetPopupMenuItems($oPage, $iMenuId, $param, &$aActions, $sTableId = null, $sDataTableId = null)
{
// 1st - add standard built-in menu items
//
//
switch($iMenuId)
{
case iPopupMenuExtension::MENU_OBJLIST_TOOLKIT:
@@ -1283,7 +1365,7 @@ class utils
"mailto:?body=".urlencode($sUrl).' ' // Add an extra space to make it work in Outlook
);
}
if (UserRights::IsActionAllowed($param->GetFilter()->GetClass(), UR_ACTION_BULK_READ, $param) != UR_ALLOWED_NO)
{
// Bulk export actions
@@ -1297,7 +1379,7 @@ class utils
}
$aResult[] = new JSPopupMenuItem('UI:Menu:AddToDashboard', Dict::S('UI:Menu:AddToDashboard'), "DashletCreationDlg('$sOQL', '$sContext')");
$aResult[] = new JSPopupMenuItem('UI:Menu:ShortcutList', Dict::S('UI:Menu:ShortcutList'), "ShortcutListDlg('$sOQL', '$sDataTableId', '$sContext')");
break;
case iPopupMenuExtension::MENU_OBJDETAILS_ACTIONS:
@@ -1311,7 +1393,7 @@ class utils
$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/tabularfieldsselector.js');
$oPage->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/jquery.dragtable.js');
$oPage->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/dragtable.css');
$aResult = array(
new SeparatorPopupMenuItem(),
// Static menus: Email this page & CSV Export
@@ -1375,7 +1457,7 @@ class utils
if (is_object($oMenuItem))
{
$aActions[$oMenuItem->GetUID()] = $oMenuItem->GetMenuItem();
foreach($oMenuItem->GetLinkedScripts() as $sLinkedScript)
{
$oPage->add_linked_script($sLinkedScript);
@@ -1512,7 +1594,7 @@ class utils
return $sProposed;
}
}
/**
* Some characters cause troubles with jQuery when used inside DOM IDs, so let's replace them by the safe _ (underscore)
* @param string $sId The ID to sanitize
@@ -1522,13 +1604,13 @@ class utils
{
return str_replace(array(':', '[', ']', '+', '-'), '_', $sId);
}
/**
* Helper to execute an HTTP POST request
* Source: http://netevil.org/blog/2006/nov/http-post-from-php-without-curl
* originaly named after do_post_request
* Does not require cUrl but requires openssl for performing https POSTs.
*
*
* @param string $sUrl The URL to POST the data to
* @param array $aData The data to POST as an array('param_name' => value)
* @param string $sOptionnalHeaders Additional HTTP headers as a string with newlines between headers
@@ -1539,11 +1621,11 @@ class utils
*
* @return string The result of the POST request
* @throws Exception with a specific error message depending on the cause
*/
*/
public static function DoPostRequest($sUrl, $aData, $sOptionnalHeaders = null, &$aResponseHeaders = null, $aCurlOptions = array())
{
// $sOptionnalHeaders is a string containing additional HTTP headers that you would like to send in your request.
if (function_exists('curl_init'))
{
// If cURL is available, let's use it, since it provides a greater control over the various HTTP/SSL options
@@ -1577,7 +1659,7 @@ class utils
CURLOPT_POSTFIELDS => http_build_query($aData),
CURLOPT_HTTPHEADER => $aHTTPHeaders,
);
$aAllOptions = $aCurlOptions + $aOptions;
$ch = curl_init($sUrl);
curl_setopt_array($ch, $aAllOptions);
@@ -1603,7 +1685,7 @@ class utils
else
{
// cURL is not available let's try with streams and fopen...
$sData = http_build_query($aData);
$aParams = array('http' => array(
'method' => 'POST',
@@ -1615,7 +1697,7 @@ class utils
$aParams['http']['header'] .= $sOptionnalHeaders;
}
$ctx = stream_context_create($aParams);
$fp = @fopen($sUrl, 'rb', false, $ctx);
if (!$fp)
{
@@ -1656,7 +1738,7 @@ class utils
/**
* Get a standard list of character sets
*
*
* @param array $aAdditionalEncodings Additional values
* @return array of iconv code => english label, sorted by label
*/
@@ -1686,8 +1768,8 @@ class utils
public static function HtmlEntities($sValue)
{
return htmlentities($sValue, ENT_QUOTES, 'UTF-8');
}
}
/**
* Helper to encapsulation iTop's html_entity_decode
* @param string $sValue
@@ -1716,7 +1798,7 @@ class utils
return $e->getMessage();
}
}
/**
* Convert (?) plain text to some HTML markup by replacing newlines by <br/> tags
* and escaping HTML entities
@@ -1729,7 +1811,7 @@ class utils
$sText = str_replace("\r", "\n", $sText);
return str_replace("\n", '<br/>', htmlentities($sText, ENT_QUOTES, 'UTF-8'));
}
/**
* Eventually compiles the SASS (.scss) file into the CSS (.css) file
*
@@ -1792,7 +1874,7 @@ class utils
return $sCss;
}
public static function GetImageSize($sImageData)
{
if (function_exists('getimagesizefromstring')) // PHP 5.4.0 or higher
@@ -1849,7 +1931,7 @@ class utils
case 'image/png':
$img = @imagecreatefromstring($oImage->GetData());
break;
default:
// Unsupported image type, return the image as-is
//throw new Exception("Unsupported image type: '".$oImage->GetMimeType()."'. Cannot resize the image, original image will be used.");
@@ -1863,14 +1945,14 @@ class utils
else
{
// Let's scale the image, preserving the transparency for GIFs and PNGs
$fScale = min($iMaxImageWidth / $iWidth, $iMaxImageHeight / $iHeight);
$iNewWidth = $iWidth * $fScale;
$iNewHeight = $iHeight * $fScale;
$new = imagecreatetruecolor($iNewWidth, $iNewHeight);
// Preserve transparency
if(($oImage->GetMimeType() == "image/gif") || ($oImage->GetMimeType() == "image/png"))
{
@@ -1878,38 +1960,38 @@ class utils
imagealphablending($new, false);
imagesavealpha($new, true);
}
imagecopyresampled($new, $img, 0, 0, 0, 0, $iNewWidth, $iNewHeight, $iWidth, $iHeight);
ob_start();
switch ($oImage->GetMimeType())
{
case 'image/gif':
imagegif($new); // send image to output buffer
break;
case 'image/jpeg':
imagejpeg($new, null, 80); // null = send image to output buffer, 80 = good quality
break;
case 'image/png':
imagepng($new, null, 5); // null = send image to output buffer, 5 = medium compression
break;
}
$oResampledImage = new ormDocument(ob_get_contents(), $oImage->GetMimeType(), $oImage->GetFileName());
@ob_end_clean();
imagedestroy($img);
imagedestroy($new);
return $oResampledImage;
}
}
/**
* Create a 128 bit UUID in the format: {########-####-####-####-############}
*
*
* Note: this method can be run from the command line as well as from the web server.
* Note2: this method is not cryptographically secure! If you need a cryptographically secure value
* consider using open_ssl or PHP 7 methods.
@@ -1945,26 +2027,9 @@ class utils
*/
public static function GetCurrentModuleName($iCallDepth = 0)
{
$sCurrentModuleName = '';
$aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$sCallerFile = realpath($aCallStack[$iCallDepth]['file']);
foreach(GetModulesInfo() as $sModuleName => $aInfo)
{
if ($aInfo['root_dir'] !== '')
{
$sRootDir = realpath(APPROOT.$aInfo['root_dir']);
if(substr($sCallerFile, 0, strlen($sRootDir)) === $sRootDir)
{
$sCurrentModuleName = $sModuleName;
break;
}
}
}
return $sCurrentModuleName;
return ModuleService::GetInstance()->GetCurrentModuleName($iCallDepth + 1);
}
/**
* **Warning** : returned result can be invalid as we're using backtrace to find the module dir name
*
@@ -1978,24 +2043,7 @@ class utils
*/
public static function GetCurrentModuleDir($iCallDepth)
{
$sCurrentModuleDir = '';
$aCallStack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$sCallerFile = realpath($aCallStack[$iCallDepth]['file']);
foreach(GetModulesInfo() as $sModuleName => $aInfo)
{
if ($aInfo['root_dir'] !== '')
{
$sRootDir = realpath(APPROOT.$aInfo['root_dir']);
if(substr($sCallerFile, 0, strlen($sRootDir)) === $sRootDir)
{
$sCurrentModuleDir = basename($sRootDir);
break;
}
}
}
return $sCurrentModuleDir;
return ModuleService::GetInstance()->GetCurrentModuleDir($iCallDepth);
}
/**
@@ -2010,14 +2058,9 @@ class utils
*/
public static function GetCurrentModuleUrl()
{
$sDir = static::GetCurrentModuleDir(1);
if ( $sDir !== '')
{
return static::GetAbsoluteUrlModulesRoot().'/'.$sDir;
}
return '';
return ModuleService::GetInstance()->GetCurrentModuleUrl(1);
}
/**
* @param string $sProperty The name of the property to retrieve
* @param mixed $defaultvalue
@@ -2025,24 +2068,18 @@ class utils
*/
public static function GetCurrentModuleSetting($sProperty, $defaultvalue = null)
{
$sModuleName = static::GetCurrentModuleName(1);
return MetaModel::GetModuleSetting($sModuleName, $sProperty, $defaultvalue);
return ModuleService::GetInstance()->GetCurrentModuleSetting($sProperty, $defaultvalue);
}
/**
* @param string $sModuleName
* @return string|NULL compiled version of a given module, as it was seen by the compiler
*/
public static function GetCompiledModuleVersion($sModuleName)
{
$aModulesInfo = GetModulesInfo();
if (array_key_exists($sModuleName, $aModulesInfo))
{
return $aModulesInfo[$sModuleName]['version'];
}
return null;
return ModuleService::GetInstance()->GetCompiledModuleVersion($sModuleName);
}
/**
* Check if the given path/url is an http(s) URL
* @param string $sPath
@@ -2057,7 +2094,7 @@ class utils
}
return $bRet;
}
/**
* Check if the given URL is a link to download a document/image on the CURRENT iTop
* In such a case we can read the content of the file directly in the database (if the users rights allow) and return the ormDocument
@@ -2106,7 +2143,7 @@ class utils
}
return $result;
}
/**
* Read the content of a file (and retrieve its MIME type) from either:
* - an URL pointing to a blob (image/document) on the current iTop server
@@ -2150,7 +2187,7 @@ class utils
'html' => 'text/html',
'exe' => 'application/octet-stream',
);
$sData = null;
$sMimeType = 'text/plain'; // Default MIME Type: treat the file as a bunch a characters...
$sFileName = 'uploaded-file'; // Default name for downloaded-files
@@ -2206,7 +2243,7 @@ class utils
}
$sExtension = strtolower(pathinfo($sPath, PATHINFO_EXTENSION));
$sFileName = basename($sPath);
if (array_key_exists($sExtension, $aKnownExtensions))
{
$sMimeType = $aKnownExtensions[$sExtension];
@@ -2220,7 +2257,7 @@ class utils
}
return $oUploadedDoc;
}
protected static function ParseHeaders($aHeaders)
{
$aCleanHeaders = array();
@@ -2245,7 +2282,7 @@ class utils
}
return $aCleanHeaders;
}
/**
* @return string a string based on compilation time or (if not available because the datamodel has not been loaded)
* the version of iTop. This string is useful to prevent browser side caching of content that may vary at each
@@ -2295,6 +2332,38 @@ class utils
return in_array($sClass, $aHugeClasses);
}
/**
* Helper around the native strlen() PHP method to test a string for null or empty value
*
* @link https://www.php.net/releases/8.1/en.php#deprecations_and_bc_breaks "Passing null to non-nullable internal function parameters is deprecated"
*
* @param string|null $sString
*
* @return bool if string null or empty
* @since 3.0.2 N°5302
* @since 2.7.10 N°6458 add method in the 2.7 branch
*/
public static function IsNullOrEmptyString(?string $sString): bool
{
return $sString === null || strlen($sString) === 0;
}
/**
* Helper around the native strlen() PHP method to test a string not null or empty value
*
* @link https://www.php.net/releases/8.1/en.php#deprecations_and_bc_breaks "Passing null to non-nullable internal function parameters is deprecated"
*
* @param string|null $sString
*
* @return bool if string is not null and not empty
* @since 3.0.2 N°5302
* @since 2.7.10 N°6458 add method in the 2.7 branch
*/
public static function IsNotNullOrEmptyString(?string $sString): bool
{
return !static::IsNullOrEmptyString($sString);
}
/**
* Check if iTop is in a development environment (VCS vs build number)
*
@@ -2368,7 +2437,7 @@ class utils
{
return false;
}
return substr_compare($haystack, $needle, -strlen($needle)) === 0;
}
@@ -2504,4 +2573,5 @@ class utils
return (substr(PHP_OS,0,3) === 'WIN');
}
}

View File

@@ -483,12 +483,24 @@ class WebPage implements Page
}
/**
* @param string|null $sHeaderValue for example `SAMESITE`. If null will set the header using the config parameter value.
* @param string|null $sXFrameOptionsHeaderValue passed to {@see add_xframe_options}
*
* @return void
* @since 2.7.10 3.0.4 3.1.2 3.2.0 N°4368 method creation, replace {@see add_xframe_options} consumers call
*/
public function add_http_headers($sXFrameOptionsHeaderValue = null)
{
$this->add_xframe_options($sXFrameOptionsHeaderValue);
$this->add_xcontent_type_options();
}
/**
* @param string|null $sHeaderValue for example `SAMESITE`. If null will set the header using the `security_header_xframe` config parameter value.
*
* @since 2.7.3 3.0.0 N°3416
* @uses security_header_xframe config parameter
* @uses \utils::GetConfig()
* @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
*
* @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options HTTP header MDN documentation
*/
public function add_xframe_options($sHeaderValue = null)
{
@@ -499,6 +511,38 @@ class WebPage implements Page
$this->add_header('X-Frame-Options: '.$sHeaderValue);
}
/**
* Warning : this header will trigger the Cross-Origin Read Blocking (CORB) protection for some mime types (HTML, XML except SVG, JSON, text/plain)
* In consequence some children pages will override this method.
*
* Sending header can be disabled globally using the `security.enable_header_xcontent_type_options` optional config parameter.
*
* @return void
* @since 2.7.10 3.0.4 3.1.2 3.2.0 N°4368 method creation
*
* @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options HTTP header MDN documentation
* @link https://chromium.googlesource.com/chromium/src/+/master/services/network/cross_origin_read_blocking_explainer.md#determining-whether-a-response-is-corb_protected "Determining whether a response is CORB-protected"
*/
public function add_xcontent_type_options()
{
try {
$oConfig = utils::GetConfig();
} catch (ConfigException|CoreException $e) {
$oConfig = null;
}
if (is_null($oConfig)) {
$bSendXContentTypeOptionsHttpHeader = true;
} else {
$bSendXContentTypeOptionsHttpHeader = $oConfig->Get('security.enable_header_xcontent_type_options');
}
if ($bSendXContentTypeOptionsHttpHeader === false) {
return;
}
$this->add_header('X-Content-Type-Options: nosniff');
}
/**
* Add needed headers to the page so that it will no be cached
*/

View File

@@ -44,10 +44,20 @@ class XMLPage extends WebPage
$this->m_bHeaderSent = false;
$this->add_header("Content-type: text/xml; charset=".self::PAGES_CHARSET);
$this->no_cache();
$this->add_xframe_options();
$this->add_http_headers();
$this->add_header("Content-location: export.xml");
}
/**
* Disabling sending the header so that resource won't be blocked by CORB. See parent method documentation.
* @return void
* @since 2.7.10 3.0.4 3.1.2 3.2.0 N°4368 method creation
*/
public function add_xcontent_type_options()
{
// Nothing to do !
}
public function output()
{
if (!$this->m_bPassThrough)

View File

@@ -14,7 +14,7 @@ define('APPCONF', APPROOT.'conf/');
* @used-by utils::GetItopVersionWikiSyntax()
* @used-by iTopModulesPhpVersionIntegrationTest
*/
define('ITOP_CORE_VERSION', '2.7.9');
define('ITOP_CORE_VERSION', '2.7.12');
require_once APPROOT.'bootstrap.inc.php';

View File

@@ -22,14 +22,8 @@ define('ITOP_DEFAULT_ENV', 'production');
define('MAINTENANCE_MODE_FILE', APPROOT.'data/.maintenance');
define('READONLY_MODE_FILE', APPROOT.'data/.readonly');
if (function_exists('microtime'))
{
$fItopStarted = microtime(true);
}
else
{
$fItopStarted = 1000 * time();
}
$fItopStarted = microtime(true);
$iItopInitialMemory = memory_get_usage(true);
if (! isset($GLOBALS['bBypassAutoload']) || $GLOBALS['bBypassAutoload'] == false)
{
@@ -54,7 +48,7 @@ if (file_exists(MAINTENANCE_MODE_FILE) && !$bBypassMaintenance)
http_response_code(503);
// Display message depending on the request
include(APPROOT.'application/maintenancemsg.php');
$sSAPIName = strtoupper(trim(php_sapi_name()));
$sSAPIName = strtoupper(trim(PHP_SAPI));
switch (true)
{

View File

@@ -15,6 +15,7 @@
"combodo/tcpdf": "~6.4.4",
"firebase/php-jwt": "~6.4.0",
"guzzlehttp/guzzle": "^6.5.8",
"guzzlehttp/psr7": "~1.9.1",
"laminas/laminas-mail": "^2.11",
"laminas/laminas-servicemanager": "^3.5",
"league/oauth2-google": "^3.0",
@@ -61,6 +62,7 @@
"sources/application",
"sources/Composer",
"sources/Controller",
"sources/Service",
"sources/Core"
],
"exclude-from-classmap": [

21
composer.lock generated
View File

@@ -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": "e5c0746c3d1bb9df9151c910e8f50955",
"content-hash": "abee0f7bd244530a88b08e1e338b0ec5",
"packages": [
{
"name": "combodo/tcpdf",
@@ -516,16 +516,16 @@
},
{
"name": "guzzlehttp/psr7",
"version": "1.9.0",
"version": "1.9.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "e98e3e6d4f86621a9b75f623996e6bbdeb4b9318"
"reference": "e4490cabc77465aaee90b20cfc9a770f8c04be6b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/e98e3e6d4f86621a9b75f623996e6bbdeb4b9318",
"reference": "e98e3e6d4f86621a9b75f623996e6bbdeb4b9318",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/e4490cabc77465aaee90b20cfc9a770f8c04be6b",
"reference": "e4490cabc77465aaee90b20cfc9a770f8c04be6b",
"shasum": ""
},
"require": {
@@ -544,11 +544,6 @@
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.9-dev"
}
},
"autoload": {
"files": [
"src/functions_include.php"
@@ -606,7 +601,7 @@
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/1.9.0"
"source": "https://github.com/guzzle/psr7/tree/1.9.1"
},
"funding": [
{
@@ -622,7 +617,7 @@
"type": "tidelift"
}
],
"time": "2022-06-20T21:43:03+00:00"
"time": "2023-04-17T16:00:37+00:00"
},
{
"name": "laminas/laminas-loader",
@@ -4757,5 +4752,5 @@
"platform-overrides": {
"php": "7.1.3"
},
"plugin-api-version": "2.1.0"
"plugin-api-version": "2.3.0"
}

View File

@@ -67,8 +67,7 @@ class MyHelpers
// format sss.mmmuuupppnnn
public static function getmicrotime()
{
list($usec, $sec) = explode(" ",microtime());
return ((float)$usec + (float)$sec);
return microtime(true);
}
/*
@@ -420,6 +419,7 @@ class MyHelpers
//}
return $sOutput;
}
}
/**
@@ -524,5 +524,3 @@ class Str
return (strtolower($sString) == $sString);
}
}
?>

View File

@@ -138,7 +138,7 @@ abstract class AttributeDefinition
protected $aCSSClasses;
public function GetType()
public function GetType()
{
return Dict::S('Core:'.get_class($this));
}
@@ -2695,6 +2695,11 @@ class AttributeObjectKey extends AttributeDBFieldVoid
return ($proposedValue == 0);
}
/**
* @inheritDoc
*
* @param int|DBObject $proposedValue Object key or valid ({@see MetaModel::IsValidObject()}) datamodel object
*/
public function MakeRealValue($proposedValue, $oHostObj)
{
if (is_null($proposedValue))
@@ -2707,7 +2712,6 @@ class AttributeObjectKey extends AttributeDBFieldVoid
}
if (MetaModel::IsValidObject($proposedValue))
{
/** @var \DBObject $proposedValue */
return $proposedValue->GetKey();
}
@@ -3771,7 +3775,7 @@ class AttributeFinalClass extends AttributeString
*/
class AttributePassword extends AttributeString implements iAttributeNoGroupBy
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
/**
* Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329)
@@ -3847,7 +3851,7 @@ class AttributePassword extends AttributeString implements iAttributeNoGroupBy
*/
class AttributeEncryptedString extends AttributeString implements iAttributeNoGroupBy
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
static $sKey = null; // Encryption key used for all encrypted fields
static $sLibrary = null; // Encryption library used for all encrypted fields
@@ -5940,6 +5944,11 @@ class AttributeDateTime extends AttributeDBField
}
}
/**
* @inheritDoc
*
* @param int|string $proposedValue timestamp ({@see DateTime::getTimestamp()) or date as string, following the {@see GetInternalFormat} format.
*/
public function MakeRealValue($proposedValue, $oHostObj)
{
if (is_null($proposedValue))
@@ -7655,9 +7664,9 @@ class AttributeBlob extends AttributeDefinition
}
/**
* Users can provide the document from an URL (including an URL on iTop itself)
* for CSV import. Administrators can even provide the path to a local file
* {@inheritDoc}
* {@inheritDoc}
*
* @param string $proposedValue Can be an URL (including an URL to iTop itself), or a local path (CSV import)
*
* @see AttributeDefinition::MakeRealValue()
*/
@@ -9234,7 +9243,7 @@ class AttributeSubItem extends AttributeDefinition
*/
class AttributeOneWayPassword extends AttributeDefinition implements iAttributeNoGroupBy
{
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
const SEARCH_WIDGET_TYPE = self::SEARCH_WIDGET_TYPE_RAW;
/**
* Useless constructor, but if not present PHP 7.4.0/7.4.1 is crashing :( (N°2329)

View File

@@ -149,7 +149,9 @@ abstract class BulkExport
$this->oSearch = null;
$this->iChunkSize = 0;
$this->sFormatCode = null;
$this->aStatusInfo = array();
$this->aStatusInfo = [
'show_obsolete_data' => utils::ShowObsoleteData(),
];
$this->oBulkExportResult = null;
$this->sTmpFile = '';
$this->bLocalizeOutput = false;
@@ -203,15 +205,17 @@ abstract class BulkExport
if ($oInfo && ($oInfo->Get('user_id') == UserRights::GetUserId()))
{
$sFormatCode = $oInfo->Get('format');
$oSearch = DBObjectSearch::unserialize($oInfo->Get('search'));
$aStatusInfo = json_decode($oInfo->Get('status_info'),true);
$oSearch = DBObjectSearch::unserialize($oInfo->Get('search'));
$oSearch->SetShowObsoleteData($aStatusInfo['show_obsolete_data']);
$oBulkExporter = self::FindExporter($sFormatCode, $oSearch);
if ($oBulkExporter)
{
$oBulkExporter->SetFormat($sFormatCode);
$oBulkExporter->SetObjectList($oSearch);
$oBulkExporter->SetChunkSize($oInfo->Get('chunk_size'));
$oBulkExporter->SetStatusInfo(json_decode($oInfo->Get('status_info'), true));
$oBulkExporter->SetStatusInfo($aStatusInfo);
$oBulkExporter->SetLocalizeOutput($oInfo->Get('localize_output'));
@@ -289,6 +293,7 @@ abstract class BulkExport
*/
public function SetObjectList(DBSearch $oSearch)
{
$oSearch->SetShowObsoleteData($this->aStatusInfo['show_obsolete_data']);
$this->oSearch = $oSearch;
}

View File

@@ -95,7 +95,7 @@ class MySQLHasGoneAwayException extends MySQLException
{
return array(
2006,
2013
2013,
);
}
@@ -134,6 +134,12 @@ class CMDBSource
const ENUM_DB_VENDOR_MARIADB = 'MariaDB';
const ENUM_DB_VENDOR_PERCONA = 'Percona';
/**
* @since 2.7.10 3.0.4 3.1.2 3.0.2 N°6889 constant creation
* @internal will be removed in a future version
*/
const MYSQL_DEFAULT_PORT = 3306;
/**
* Error: 1205 SQLSTATE: HY000 (ER_LOCK_WAIT_TIMEOUT)
* Message: Lock wait timeout exceeded; try restarting transaction
@@ -319,16 +325,19 @@ class CMDBSource
/**
* @param string $sDbHost initial value ("p:domain:port" syntax)
* @param string $sServer server variable to update
* @param int $iPort port variable to update
* @param int|null $iPort port variable to update, will return null if nothing is specified in $sDbHost
*
* @since 2.7.10 3.0.4 3.1.2 3.2.0 N°6889 will return null in $iPort if port isn't present in $sDbHost. Use {@see MYSQL_DEFAULT_PORT} if needed
*
* @link http://php.net/manual/en/mysqli.persistconns.php documentation for the "p:" prefix (persistent connexion)
*/
public static function InitServerAndPort($sDbHost, &$sServer, &$iPort)
{
$aConnectInfo = explode(':', $sDbHost);
$bUsePersistentConnection = false;
if (strcasecmp($aConnectInfo[0], 'p') == 0)
if (strcasecmp($aConnectInfo[0], 'p') === 0)
{
// we might have "p:" prefix to use persistent connections (see http://php.net/manual/en/mysqli.persistconns.php)
$bUsePersistentConnection = true;
$sServer = $aConnectInfo[0].':'.$aConnectInfo[1];
}
@@ -346,10 +355,6 @@ class CMDBSource
{
$iPort = (int)($aConnectInfo[1]);
}
else
{
$iPort = 3306;
}
}
/**
@@ -535,6 +540,7 @@ class CMDBSource
{
self::$m_sDBName = '';
}
self::_TablesInfoCacheReset(); // reset the table info cache!
}
public static function CreateTable($sQuery)
@@ -668,10 +674,9 @@ class CMDBSource
/**
* @param string $sSQLQuery
*
* @return \mysqli_result|null
* @throws \MySQLException
* @throws \MySQLHasGoneAwayException
* @throws \CoreException
* @return mysqli_result|null
* @throws MySQLException
* @throws MySQLHasGoneAwayException
*
* @since 2.7.0 N°679 handles nested transactions
*/
@@ -731,8 +736,9 @@ class CMDBSource
{
self::LogDeadLock($e);
throw new MySQLException('Failed to issue SQL query', array('query' => $sSql, $e));
}
$oKPI->ComputeStats('Query exec (mySQL)', $sSql);
} finally {
$oKPI->ComputeStats('Query exec (mySQL)', $sSql);
}
if ($oResult === false)
{
$aContext = array('query' => $sSql);
@@ -1290,8 +1296,8 @@ class CMDBSource
*/
public static function IsSameFieldTypes($sItopGeneratedFieldType, $sDbFieldType)
{
list($sItopFieldDataType, $sItopFieldTypeOptions, $sItopFieldOtherOptions) = static::GetFieldDataTypeAndOptions($sItopGeneratedFieldType);
list($sDbFieldDataType, $sDbFieldTypeOptions, $sDbFieldOtherOptions) = static::GetFieldDataTypeAndOptions($sDbFieldType);
[$sItopFieldDataType, $sItopFieldTypeOptions, $sItopFieldOtherOptions] = static::GetFieldDataTypeAndOptions($sItopGeneratedFieldType);
[$sDbFieldDataType, $sDbFieldTypeOptions, $sDbFieldOtherOptions] = static::GetFieldDataTypeAndOptions($sDbFieldType);
if (strcasecmp($sItopFieldDataType, $sDbFieldDataType) !== 0)
{
@@ -1728,8 +1734,20 @@ class CMDBSource
return false;
}
/**
* @return string query to upgrade database charset and collation if needed, null if not
public static function GetClusterNb()
{
$result = 0;
$sSql = "SHOW STATUS LIKE 'wsrep_cluster_size';";
$aRows = self::QueryToArray($sSql);
if (count($aRows) > 0)
{
$result = $aRows[0]['Value'];
}
return intval($result);
}
/**
* @return string query to upgrade database charset and collation if needed, null if not
* @throws \MySQLException
*
* @since 2.5.0 N°1001 switch to utf8mb4

View File

@@ -963,6 +963,14 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'log_kpi_generate_legacy_report' => array(
'type' => 'bool',
'description' => 'Generate the legacy KPI report (kpi.html)',
'default' => true,
'value' => '',
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'max_linkset_output' => array(
'type' => 'integer',
'description' => 'Maximum number of items shown when getting a list of related items in an email, using the form $this->some_list$. 0 means no limit.',
@@ -1312,6 +1320,14 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'security.enable_header_xcontent_type_options' => [
'type' => 'bool',
'description' => 'If set to false, iTop will stop sending the X-Content-Type-Options HTTP header. This header could trigger CORB protection on certain resources (JSON, XML, HTML, text) therefore blocking them.',
'default' => true,
'value' => '',
'source_of_value' => '',
'show_in_conf_sample' => false,
],
'behind_reverse_proxy' => [
'type' => 'bool',
'description' => 'If true, then proxies custom header (X-Forwarded-*) are taken into account. Use only if the webserver is not publicly accessible (reachable only by the reverse proxy)',
@@ -1618,7 +1634,7 @@ class Config
}
if (strlen($sNoise) > 0)
{
// Note: sNoise is an html output, but so far it was ok for me (e.g. showing the entire call stack)
// Note: sNoise is an html output, but so far it was ok for me (e.g. showing the entire call stack)
throw new ConfigException('Syntax error in configuration file',
array('file' => $sConfigFile, 'error' => '<tt>'.htmlentities($sNoise, ENT_QUOTES, 'UTF-8').'</tt>'));
}
@@ -1893,6 +1909,24 @@ class Config
$this->m_sAllowedLoginTypes = implode('|', $aAllowedLoginTypes);
}
/**
* @since 2.7.11 N°7085
* Add login mode if not configured already
* @param string $sLoginMode
*
* @return void
*/
public function AddAllowedLoginTypes($sLoginMode)
{
$aAllowedLoginTypes = $this->GetAllowedLoginTypes();
if (in_array($sLoginMode, $aAllowedLoginTypes)){
return;
}
$aAllowedLoginTypes[] = $sLoginMode;
$this->SetAllowedLoginTypes($aAllowedLoginTypes);
}
public function SetExternalAuthenticationVariable($sExtAuthVariable)
{
$this->m_sExtAuthVariable = $sExtAuthVariable;
@@ -2412,7 +2446,7 @@ class ConfigPlaceholdersResolver
}
$sPattern = '/\%(env|server)\((\w+)\)(?:\?:(\w*))?\%/'; //3 capturing groups, ie `%env(HTTP_PORT)?:8080%` produce: `env` `HTTP_PORT` and `8080`.
if (! preg_match_all($sPattern, $rawValue, $aMatchesCollection, PREG_SET_ORDER))
{
return $rawValue;
@@ -2465,4 +2499,4 @@ class ConfigPlaceholdersResolver
IssueLog::Error($sErrorMessage, self::class, array($sSourceName, $sKey, $sDefault, $sWholeMask));
throw new ConfigException($sErrorMessage);
}
}
}

View File

@@ -229,12 +229,47 @@ class CoreUnexpectedValue extends CoreException
{
}
/**
* @since 2.7.10 3.0.4 3.1.1 3.2.0 N°6458 object creation
*/
class InvalidExternalKeyValueException extends CoreUnexpectedValue
{
private const ENUM_PARAMS_OBJECT = 'current_object';
private const ENUM_PARAMS_ATTCODE = 'attcode';
private const ENUM_PARAMS_ATTVALUE = 'attvalue';
private const ENUM_PARAMS_USER = 'current_user';
public function __construct($oObject, $sAttCode, $aContextData = null, $oPrevious = null)
{
$aContextData[self::ENUM_PARAMS_OBJECT] = get_class($oObject) . '::' . $oObject->GetKey();
$aContextData[self::ENUM_PARAMS_ATTCODE] = $sAttCode;
$aContextData[self::ENUM_PARAMS_ATTVALUE] = $oObject->Get($sAttCode);
$oCurrentUser = UserRights::GetUserObject();
if (false === is_null($oCurrentUser)) {
$aContextData[self::ENUM_PARAMS_USER] = get_class($oCurrentUser) . '::' . $oCurrentUser->GetKey();
}
parent::__construct('Attribute pointing to an object that is either non existing or not readable by the current user', $aContextData, '', $oPrevious);
}
public function GetAttCode(): string
{
return $this->getContextData()[self::ENUM_PARAMS_ATTCODE];
}
public function GetAttValue(): string
{
return $this->getContextData()[self::ENUM_PARAMS_ATTVALUE];
}
}
class SecurityException extends CoreException
{
}
/**
* Throwned when querying on an object that exists in the database but is archived
* Thrown when querying on an object that exists in the database but is archived
*
* @see N.1108
* @since 2.5.1

View File

@@ -188,8 +188,8 @@ final class ItopCounter
if (!$hDBLink)
{
throw new Exception("Could not connect to the DB server (host=$sDBHost, user=$sDBUser): ".mysqli_connect_error().' (mysql errno: '.mysqli_connect_errno().')');
}
throw new MySQLException('Could not connect to the DB server '.mysqli_connect_error().' (mysql errno: '.mysqli_connect_errno(), array('host' => $sDBHost, 'user' => $sDBUser));
}
return $hDBLink;
}

View File

@@ -15,6 +15,7 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
use Combodo\iTop\Application\Helper\ExportHelper;
/**
* Bulk export: CSV export
@@ -113,6 +114,7 @@ class CSVBulkExport extends TabularBulkExport
case 'csv_options':
$oP->add('<fieldset><legend>'.Dict::S('Core:BulkExport:CSVOptions').'</legend>');
$oP->add(ExportHelper::GetAlertForExcelMaliciousInjection());
$oP->add('<table class="export_parameters"><tr><td style="vertical-align:top">');
$oP->add('<h3>'.Dict::S('UI:CSVImport:SeparatorCharacter').'</h3>');
$sRawSeparator = utils::ReadParam('separator', ',', true, 'raw_data');

View File

@@ -532,11 +532,10 @@ abstract class DBObject implements iDisplay
* Attributes setter
*
* Set $sAttCode to $value.
* The value must be valid according to the type of attribute.
* The value must be valid according to the type of attribute : see the different {@see AttributeDefinition::MakeRealValue()} implementations
* The value will not be recorded into the DB until DBObject::DBWrite() is called.
*
* @api
* @see DBWrite()
*
* @param string $sAttCode
* @param mixed $value
@@ -544,6 +543,8 @@ abstract class DBObject implements iDisplay
* @return bool
* @throws CoreException
* @throws CoreUnexpectedValue
*
* @see DBWrite()
*/
public function Set($sAttCode, $value)
{
@@ -2084,16 +2085,17 @@ abstract class DBObject implements iDisplay
}
/**
* @param array $aUniquenessRuleProperties uniqueness rule properties
*
* @param string $sUniquenessRuleId uniqueness rule ID
* @return \DBSearch
* @throws \OQLException
* @throws \CoreException
*
* @internal
*
* @param string $sUniquenessRuleId uniqueness rule ID
* @param array $aUniquenessRuleProperties uniqueness rule properties
*
* @return \DBSearch
* @throws \CoreException
* @throws \OQLException
* @since 2.6.0 N°659 uniqueness constraint
* @api
* @since 2.6.0 N°659 uniqueness constraint
* @since 2.7.11 3.1.2 3.2.0 N°4314 Fix Uniqueness rules not working with Silo
*/
protected function GetSearchForUniquenessRule($sUniquenessRuleId, $aUniquenessRuleProperties)
{
@@ -2123,8 +2125,10 @@ abstract class DBObject implements iDisplay
$oUniquenessQuery->AddConditionForInOperatorUsingParam('finalclass', $aChildClassesWithRuleDisabled, false);
}
return $oUniquenessQuery;
}
$oUniquenessQuery->AllowAllData();
return $oUniquenessQuery;
}
/**
* Check integrity rules (before inserting or updating the object)
@@ -2140,7 +2144,6 @@ abstract class DBObject implements iDisplay
* @throws \ArchivedObjectException
* @throws \CoreException
* @throws \OQLException
*
*/
public function DoCheckToWrite()
{
@@ -2195,7 +2198,6 @@ abstract class DBObject implements iDisplay
}
/**
*
* @api
* @api-advanced
*
@@ -2211,7 +2213,6 @@ abstract class DBObject implements iDisplay
* @throws \ArchivedObjectException
* @throws \CoreException
* @throws \OQLException
*
*/
final public function CheckToWrite()
{
@@ -2225,7 +2226,7 @@ abstract class DBObject implements iDisplay
$oKPI = new ExecutionKPI();
$this->DoCheckToWrite();
$oKPI->ComputeStats('CheckToWrite', get_class($this));
$oKPI->ComputeStatsForExtension($this, 'DoCheckToWrite');
if (count($this->m_aCheckIssues) == 0)
{
$this->m_bCheckStatus = true;
@@ -2238,6 +2239,87 @@ abstract class DBObject implements iDisplay
return array($this->m_bCheckStatus, $this->m_aCheckIssues, $this->m_bSecurityIssue);
}
/**
* Checks for extkey attributes values. This will throw exception on non-existing as well as non-accessible objects (silo, scopes).
* That's why the test is done for all users including Administrators
*
* Note that due to perf issues, this isn't called directly by the ORM, but has to be called by consumers when possible.
*
* @param callable(string, string):bool|null $oIsObjectLoadableCallback Override to check if object is accessible.
* Parameters are object class and key
* Return value should be false if cannot access object, true otherwise
* @return void
*
* @throws ArchivedObjectException
* @throws CoreException if cannot get object attdef list
* @throws CoreUnexpectedValue
* @throws InvalidExternalKeyValueException
* @throws MySQLException
* @throws SecurityException if one extkey is pointing to an invalid value
*
* @link https://github.com/Combodo/iTop/security/advisories/GHSA-245j-66p9-pwmh
* @since 2.7.10 3.0.4 3.1.1 3.2.0 N°6458
*
* @see \RestUtils::FindObjectFromKey for the same check in the REST endpoint
*/
final public function CheckChangedExtKeysValues(callable $oIsObjectLoadableCallback = null)
{
if (is_null($oIsObjectLoadableCallback)) {
$oIsObjectLoadableCallback = function ($sClass, $sId) {
$oRemoteObject = MetaModel::GetObject($sClass, $sId, false);
if (is_null($oRemoteObject)) {
return false;
}
return true;
};
}
$aChanges = $this->ListChanges();
$aAttCodesChanged = array_keys($aChanges);
foreach ($aAttCodesChanged as $sAttDefCode) {
$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttDefCode);
if ($oAttDef instanceof AttributeLinkedSetIndirect) {
/** @var ormLinkSet $oOrmSet */
$oOrmSet = $this->Get($sAttDefCode);
while ($oLnk = $oOrmSet->Fetch()) {
$oLnk->CheckChangedExtKeysValues($oIsObjectLoadableCallback);
}
continue;
}
/** @noinspection PhpConditionCheckedByNextConditionInspection */
/** @noinspection NotOptimalIfConditionsInspection */
if (($oAttDef instanceof AttributeHierarchicalKey) || ($oAttDef instanceof AttributeExternalKey)) {
$sRemoteObjectClass = $oAttDef->GetTargetClass();
$sRemoteObjectKey = $this->Get($sAttDefCode);
} else if ($oAttDef instanceof AttributeObjectKey) {
$sRemoteObjectClassAttCode = $oAttDef->Get('class_attcode');
$sRemoteObjectClass = $this->Get($sRemoteObjectClassAttCode);
$sRemoteObjectKey = $this->Get($sAttDefCode);
} else {
continue;
}
if (utils::IsNullOrEmptyString($sRemoteObjectClass)
|| utils::IsNullOrEmptyString($sRemoteObjectKey)
) {
continue;
}
// 0 : Undefined ext. key (EG. non-mandatory and no value provided)
// < 0 : Non yet persisted object
/** @noinspection TypeUnsafeComparisonInspection Non-strict comparison as object ID can be string */
if ($sRemoteObjectKey <= 0) {
continue;
}
if (false === $oIsObjectLoadableCallback($sRemoteObjectClass, $sRemoteObjectKey)) {
throw new InvalidExternalKeyValueException($this, $sAttDefCode);
}
}
}
/**
* Check if it is allowed to delete the existing object from the database
*
@@ -2383,13 +2465,13 @@ abstract class DBObject implements iDisplay
* @api
* @api-advanced
*
* @see \DBObject::ListPreviousValuesForUpdatedAttributes() to get previous values anywhere in the CRUD stack
* @see https://www.itophub.io/wiki/page?id=latest%3Acustomization%3Asequence_crud iTop CRUD stack documentation
* @return array attname => currentvalue List the attributes that have been changed using {@see DBObject::Set()}.
* @return array attcode => currentvalue List the attributes that have been changed using {@see DBObject::Set()}.
* Reset during {@see DBObject::DBUpdate()}
* @throws Exception
* @see \DBObject::ListPreviousValuesForUpdatedAttributes() to get previous values anywhere in the CRUD stack
* @see https://www.itophub.io/wiki/page?id=latest%3Acustomization%3Asequence_crud iTop CRUD stack documentation
* @uses m_aCurrValues
*/
*/
public function ListChanges()
{
if ($this->m_bIsInDB)
@@ -2680,7 +2762,6 @@ abstract class DBObject implements iDisplay
* @throws \Exception
*
* @internal
*
*/
public function DBInsertNoReload()
{
@@ -2693,8 +2774,12 @@ abstract class DBObject implements iDisplay
$sRootClass = MetaModel::GetRootClass($sClass);
// Ensure the update of the values (we are accessing the data directly)
$oKPI = new ExecutionKPI();
$this->DoComputeValues();
$oKPI->ComputeStatsForExtension($this, 'DoComputeValues');
$oKPI = new ExecutionKPI();
$this->OnInsert();
$oKPI->ComputeStatsForExtension($this, 'OnInsert');
if ($this->m_iKey < 0)
{
@@ -2712,7 +2797,7 @@ abstract class DBObject implements iDisplay
}
// Ultimate check - ensure DB integrity
list($bRes, $aIssues) = $this->CheckToWrite();
[$bRes, $aIssues] = $this->CheckToWrite();
if (!$bRes)
{
throw new CoreCannotSaveObjectException(array('issues' => $aIssues, 'class' => get_class($this), 'id' => $this->GetKey()));
@@ -2818,7 +2903,9 @@ abstract class DBObject implements iDisplay
$this->m_aOrigValues[$sAttCode] = $value;
}
$oKPI = new ExecutionKPI();
$this->AfterInsert();
$oKPI->ComputeStatsForExtension($this, 'AfterInsert');
// Activate any existing trigger
$sClass = get_class($this);
@@ -2954,8 +3041,6 @@ abstract class DBObject implements iDisplay
* Persist an object to the DB, for the first time
*
* @api
* @see DBWrite
*
* @return int|null inserted object key
*
* @throws \ArchivedObjectException
@@ -2965,10 +3050,12 @@ abstract class DBObject implements iDisplay
* @throws \CoreWarning
* @throws \MySQLException
* @throws \OQLException
*
* @see DBWrite
*/
public function DBInsert()
{
$this->DBInsertNoReload();
$this->DBInsertNoReload();
if (MetaModel::DBIsReadOnly())
{
@@ -3067,13 +3154,13 @@ abstract class DBObject implements iDisplay
* Update an object in DB
*
* @api
* @see DBObject::DBWrite()
*
* @return int object key
*
* @throws \CoreException
* @throws \CoreCannotSaveObjectException if CheckToWrite() returns issues
* @throws \Exception
*
* @see DBObject::DBWrite()
*/
public function DBUpdate()
{
@@ -3081,6 +3168,7 @@ abstract class DBObject implements iDisplay
{
throw new CoreException("DBUpdate: could not update a newly created object, please call DBInsert instead");
}
// Protect against reentrance (e.g. cascading the update of ticket logs)
static $aUpdateReentrance = array();
$sKey = get_class($this).'::'.$this->GetKey();
@@ -3094,8 +3182,11 @@ abstract class DBObject implements iDisplay
try
{
$oKPI = new ExecutionKPI();
$this->DoComputeValues();
// Stop watches
$oKPI->ComputeStatsForExtension($this, 'DoComputeValues');
// Stop watches
$sState = $this->GetState();
if ($sState != '')
{
@@ -3114,7 +3205,9 @@ abstract class DBObject implements iDisplay
}
}
}
$this->OnUpdate();
$oKPI = new ExecutionKPI();
$this->OnUpdate();
$oKPI->ComputeStatsForExtension($this, 'OnUpdate');
$aChanges = $this->ListChanges();
if (count($aChanges) == 0)
@@ -3126,7 +3219,7 @@ abstract class DBObject implements iDisplay
}
// Ultimate check - ensure DB integrity
list($bRes, $aIssues) = $this->CheckToWrite();
[$bRes, $aIssues] = $this->CheckToWrite();
if (!$bRes)
{
throw new CoreCannotSaveObjectException(array(
@@ -3326,7 +3419,9 @@ abstract class DBObject implements iDisplay
try
{
$this->AfterUpdate();
$oKPI = new ExecutionKPI();
$this->AfterUpdate();
$oKPI->ComputeStatsForExtension($this, 'AfterUpdate');
// Reload to get the external attributes
if ($bHasANewExternalKeyValue)
@@ -3405,13 +3500,18 @@ abstract class DBObject implements iDisplay
/**
* Make the current changes persistent - clever wrapper for Insert or Update
*
* @api
*
* @api
*
* @return int
*
* @throws \CoreCannotSaveObjectException
* @throws \CoreException
*
* @throws ArchivedObjectException
* @throws CoreCannotSaveObjectException
* @throws CoreException
* @throws CoreUnexpectedValue
* @throws CoreWarning
* @throws MySQLException
* @throws OQLException
*/
public function DBWrite()
{

View File

@@ -763,7 +763,10 @@ class DBObjectSet implements iDBObjectSetIterator
try
{
$oKPI = new ExecutionKPI();
$this->m_oSQLResult = CMDBSource::Query($sSQL);
$sOQL = $this->GetPseudoOQL($this->m_oFilter, $this->GetRealSortOrder(), $this->m_iLimitCount, $this->m_iLimitStart, false);
$oKPI->ComputeStats('OQL Query Exec', $sOQL);
} catch (MySQLException $e)
{
// 1116 = ER_TOO_MANY_TABLES
@@ -843,8 +846,11 @@ class DBObjectSet implements iDBObjectSetIterator
{
if (is_null($this->m_iNumTotalDBRows))
{
$oKPI = new ExecutionKPI();
$sSQL = $this->m_oFilter->MakeSelectQuery(array(), $this->m_aArgs, null, null, 0, 0, true);
$resQuery = CMDBSource::Query($sSQL);
$sOQL = $this->GetPseudoOQL($this->m_oFilter, array(), 0, 0, true);
$oKPI->ComputeStats('OQL Query Exec', $sOQL);
if (!$resQuery) return 0;
$aRow = CMDBSource::FetchArray($resQuery);
@@ -855,6 +861,42 @@ class DBObjectSet implements iDBObjectSetIterator
return $this->m_iNumTotalDBRows + count($this->m_aAddedObjects); // Does it fix Trac #887 ??
}
/**
* @param \DBSearch $oFilter
* @param array $aOrder
* @param int $iLimitCount
* @param int $iLimitStart
* @param bool $bCount
*
* @return string
*/
private function GetPseudoOQL($oFilter, $aOrder, $iLimitCount, $iLimitStart, $bCount)
{
$sOQL = '';
if ($bCount) {
$sOQL .= 'COUNT ';
}
$sOQL .= $oFilter->ToOQL();
if ($iLimitCount > 0) {
$sOQL .= ' LIMIT ';
if ($iLimitStart > 0) {
$sOQL .= "$iLimitStart, ";
}
$sOQL .= "$iLimitCount";
}
if (count($aOrder) > 0) {
$sOQL .= ' ORDER BY ';
$aOrderBy = [];
foreach ($aOrder as $sAttCode => $bAsc) {
$aOrderBy[] = $sAttCode.' '.($bAsc ? 'ASC' : 'DESC');
}
$sOQL .= implode(', ', $aOrderBy);
}
return $sOQL;
}
/**
* Check if the count exceeds a given limit
*
@@ -871,8 +913,11 @@ class DBObjectSet implements iDBObjectSetIterator
{
if (is_null($this->m_iNumTotalDBRows))
{
$oKPI = new ExecutionKPI();
$sSQL = $this->m_oFilter->MakeSelectQuery(array(), $this->m_aArgs, null, null, $iLimit + 2, 0, true);
$resQuery = CMDBSource::Query($sSQL);
$sOQL = $this->GetPseudoOQL($this->m_oFilter, array(), $iLimit + 2, 0, true);
$oKPI->ComputeStats('OQL Query Exec', $sOQL);
if ($resQuery)
{
$aRow = CMDBSource::FetchArray($resQuery);
@@ -883,7 +928,7 @@ class DBObjectSet implements iDBObjectSetIterator
{
$iCount = 0;
}
}
}
else
{
$iCount = $this->m_iNumTotalDBRows;
@@ -908,8 +953,11 @@ class DBObjectSet implements iDBObjectSetIterator
{
if (is_null($this->m_iNumTotalDBRows))
{
$oKPI = new ExecutionKPI();
$sSQL = $this->m_oFilter->MakeSelectQuery(array(), $this->m_aArgs, null, null, $iLimit + 2, 0, true);
$resQuery = CMDBSource::Query($sSQL);
$sOQL = $this->GetPseudoOQL($this->m_oFilter, array(), $iLimit + 2, 0, true);
$oKPI->ComputeStats('OQL Query Exec', $sOQL);
if ($resQuery)
{
$aRow = CMDBSource::FetchArray($resQuery);
@@ -920,7 +968,7 @@ class DBObjectSet implements iDBObjectSetIterator
{
$iCount = 0;
}
}
}
else
{
$iCount = $this->m_iNumTotalDBRows;

View File

@@ -866,11 +866,11 @@ abstract class DBSearch
return;
}
if (count($aColumns) == 0)
{
$aColumns = array_keys(MetaModel::ListAttributeDefs($this->GetClass()));
// Add the standard id (as first column)
array_unshift($aColumns, 'id');
if (count($aColumns) == 0)
{
$aColumns = array_keys(MetaModel::ListAttributeDefs($this->GetClass()));
// Add the standard id (as first column)
array_unshift($aColumns, 'id');
}
$aQueryCols = CMDBSource::GetColumns($resQuery, $sSQL);
@@ -900,6 +900,55 @@ abstract class DBSearch
return $aRes;
}
/**
* Selects a column ($sAttCode) from the specified class ($sClassAlias - default main class) of the DBsearch object and gives the result as an array
* @param string $sAttCode
* @param string|null $sClassAlias
*
* @return array
* @throws ConfigException
* @throws CoreException
* @throws MissingQueryArgument
* @throws MySQLException
* @throws MySQLHasGoneAwayException
*/
public function SelectAttributeToArray(string $sAttCode, ?string $sClassAlias = null):array
{
if(is_null($sClassAlias)) {
$sClassAlias = $this->GetClassAlias();
}
$sClass = $this->GetClass();
if($sAttCode === 'id'){
$aAttToLoad[$sClassAlias]=[];
} else {
$aAttToLoad[$sClassAlias][$sAttCode] = MetaModel::GetAttributeDef($sClass, $sAttCode);
}
$sSQL = $this->MakeSelectQuery([], [], $aAttToLoad);
$resQuery = CMDBSource::Query($sSQL);
if (!$resQuery)
{
return [];
}
$sColName = $sClassAlias.$sAttCode;
$aRes = [];
while ($aRow = CMDBSource::FetchArray($resQuery))
{
$aMappedRow = array();
if($sAttCode === 'id') {
$aMappedRow[$sAttCode] = $aRow[$sColName];
} else {
$aMappedRow[$sAttCode] = $aAttToLoad[$sClassAlias][$sAttCode]->FromSQLToValue($aRow, $sColName);
}
$aRes[] = $aMappedRow;
}
CMDBSource::FreeResult($resQuery);
return $aRes;
}
////////////////////////////////////////////////////////////////////////////
//
// Construction of the SQL queries

View File

@@ -3,7 +3,7 @@
//
// This file is part of iTop.
//
// iTop is free software; you can redistribute it and/or modify
// iTop is free software; you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
@@ -18,7 +18,7 @@
/**
* Class Dict
* Management of localizable strings
* Management of localizable strings
*
* @copyright Copyright (C) 2010-2018 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
@@ -144,33 +144,50 @@ class Dict
* @return string
*/
public static function S($sStringCode, $sDefault = null, $bUserLanguageOnly = false)
{
$aInfo = self::GetLabelAndLangCode($sStringCode, $sDefault, $bUserLanguageOnly);
return $aInfo['label'];
}
/**
* Returns a localised string from the dictonary with its associated lang code
*
* @param string $sStringCode The code identifying the dictionary entry
* @param string $sDefault Default value if there is no match in the dictionary
* @param bool $bUserLanguageOnly True to allow the use of the default language as a fallback, false otherwise
*
* @return array{
* lang: string, label: string
* } with localized label string and used lang code
*/
private static function GetLabelAndLangCode($sStringCode, $sDefault = null, $bUserLanguageOnly = false)
{
// Attempt to find the string in the user language
//
$sLangCode = self::GetUserLanguage();
self::InitLangIfNeeded($sLangCode);
if (!array_key_exists($sLangCode, self::$m_aData))
if (! array_key_exists($sLangCode, self::$m_aData))
{
IssueLog::Warning("Cannot find $sLangCode in dictionnaries. default labels displayed");
IssueLog::Warning("Cannot find $sLangCode in all registered dictionaries.");
// It may happen, when something happens before the dictionaries get loaded
return $sStringCode;
return [ 'label' => $sStringCode, 'lang' => $sLangCode ];
}
$aCurrentDictionary = self::$m_aData[$sLangCode];
if (is_array($aCurrentDictionary) && array_key_exists($sStringCode, $aCurrentDictionary))
{
return $aCurrentDictionary[$sStringCode];
return [ 'label' => $aCurrentDictionary[$sStringCode], 'lang' => $sLangCode ];
}
if (!$bUserLanguageOnly)
{
// Attempt to find the string in the default language
//
self::InitLangIfNeeded(self::$m_sDefaultLanguage);
$aDefaultDictionary = self::$m_aData[self::$m_sDefaultLanguage];
if (is_array($aDefaultDictionary) && array_key_exists($sStringCode, $aDefaultDictionary))
{
return $aDefaultDictionary[$sStringCode];
return [ 'label' => $aDefaultDictionary[$sStringCode], 'lang' => self::$m_sDefaultLanguage ];
}
// Attempt to find the string in english
//
@@ -179,17 +196,17 @@ class Dict
$aDefaultDictionary = self::$m_aData['EN US'];
if (is_array($aDefaultDictionary) && array_key_exists($sStringCode, $aDefaultDictionary))
{
return $aDefaultDictionary[$sStringCode];
return [ 'label' => $aDefaultDictionary[$sStringCode], 'lang' => 'EN US' ];
}
}
// Could not find the string...
//
if (is_null($sDefault))
{
return $sStringCode;
return [ 'label' => $sStringCode, 'lang' => null ];
}
return $sDefault;
return [ 'label' => $sDefault, 'lang' => null ];
}
@@ -205,19 +222,25 @@ class Dict
*/
public static function Format($sFormatCode /*, ... arguments ....*/)
{
$sLocalizedFormat = self::S($sFormatCode);
['label' => $sLocalizedFormat, 'lang' => $sLangCode] = self::GetLabelAndLangCode($sFormatCode);
$aArguments = func_get_args();
array_shift($aArguments);
if ($sLocalizedFormat == $sFormatCode)
{
// Make sure the information will be displayed (ex: an error occuring before the dictionary gets loaded)
return $sFormatCode.' - '.implode(', ', $aArguments);
}
return vsprintf($sLocalizedFormat, $aArguments);
try{
return vsprintf($sLocalizedFormat, $aArguments);
} catch(\Throwable $e){
\IssueLog::Error("Cannot format dict key", null, ["sFormatCode" => $sFormatCode, "sLangCode" => $sLangCode, 'exception_msg' => $e->getMessage() ]);
return $sFormatCode.' - '.implode(', ', $aArguments);
}
}
/**
* Initialize a the entries for a given language (replaces the former Add() method)
* @param string $sLanguageCode Code identifying the language i.e. 'FR-FR', 'EN-US'
@@ -227,7 +250,7 @@ class Dict
{
self::$m_aData[$sLanguageCode] = $aEntries;
}
/**
* Set the list of available languages
* @param hash $aLanguagesList
@@ -288,7 +311,7 @@ class Dict
{
$sDictFile = APPROOT.'env-'.utils::GetCurrentEnvironment().'/dictionaries/'.str_replace(' ', '-', strtolower($sLangCode)).'.dict.php';
require_once($sDictFile);
if (self::GetApcService()->function_exists('apc_store')
&& (self::$m_sApplicationPrefix !== null))
{
@@ -298,7 +321,7 @@ class Dict
}
return $bResult;
}
/**
* Enable caching (cached using APC)
* @param string $sApplicationPrefix The prefix for uniquely identiying this iTop instance
@@ -342,14 +365,14 @@ class Dict
}
}
}
public static function MakeStats($sLanguageCode, $sLanguageRef = 'EN US')
{
$aMissing = array(); // Strings missing for the target language
$aUnexpected = array(); // Strings defined for the target language, but not found in the reference dictionary
$aNotTranslated = array(); // Strings having the same value in both dictionaries
$aOK = array(); // Strings having different values in both dictionaries
foreach (self::$m_aData[$sLanguageRef] as $sStringCode => $sValue)
{
if (!array_key_exists($sStringCode, self::$m_aData[$sLanguageCode]))
@@ -357,7 +380,7 @@ class Dict
$aMissing[$sStringCode] = $sValue;
}
}
foreach (self::$m_aData[$sLanguageCode] as $sStringCode => $sValue)
{
if (!array_key_exists($sStringCode, self::$m_aData[$sLanguageRef]))
@@ -380,7 +403,7 @@ class Dict
}
return array($aMissing, $aUnexpected, $aNotTranslated, $aOK);
}
public static function Dump()
{
MyHelpers::var_dump_html(self::$m_aData);
@@ -403,7 +426,7 @@ class Dict
// No need to actually load the strings since it's only used to know the list of languages
// at setup time !!
}
/**
* Export all the dictionary entries - of the given language - whose code matches the given prefix
* missing entries in the current language will be replaced by entries in the default language
@@ -416,7 +439,7 @@ class Dict
self::InitLangIfNeeded(self::$m_sDefaultLanguage);
$aEntries = array();
$iLength = strlen($sStartingWith);
// First prefill the array with entries from the default language
foreach(self::$m_aData[self::$m_sDefaultLanguage] as $sCode => $sEntry)
{
@@ -425,7 +448,7 @@ class Dict
$aEntries[$sCode] = $sEntry;
}
}
// Now put (overwrite) the entries for the user language
foreach(self::$m_aData[self::GetUserLanguage()] as $sCode => $sEntry)
{

View File

@@ -23,6 +23,8 @@
* @license http://opensource.org/licenses/AGPL-3.0
*/
use Combodo\iTop\Application\Helper\ExportHelper;
require_once(APPROOT.'application/xlsxwriter.class.php');
class ExcelBulkExport extends TabularBulkExport
@@ -89,6 +91,7 @@ class ExcelBulkExport extends TabularBulkExport
case 'xlsx_options':
$oP->add('<fieldset><legend>'.Dict::S('Core:BulkExport:XLSXOptions').'</legend>');
$oP->add(ExportHelper::GetAlertForExcelMaliciousInjection());
$oP->add('<table class="export_parameters"><tr><td style="vertical-align:top">');
$sChecked = (utils::ReadParam('formatted_text', 0) == 1) ? ' checked ' : '';

View File

@@ -49,10 +49,9 @@ abstract class HTMLSanitizer
$sSanitizerClass = 'HTMLDOMSanitizer';
} else if (false === is_subclass_of($sSanitizerClass, HTMLSanitizer::class)) {
if ($sConfigKey === 'html_sanitizer') {
IssueLog::Warning('The configured "'.$sConfigKey.'" class "'.$sSanitizerClass.'" is not a subclass of HTMLSanitizer. Will use HTMLDOMSanitizer as the default sanitizer.');
IssueLog::Warning('The configured "'.$sConfigKey.'" class "'.$sSanitizerClass.'" is not a subclass of '.HTMLSanitizer::class.'. Will use HTMLDOMSanitizer as the default sanitizer.');
$sSanitizerClass = 'HTMLDOMSanitizer';
}
if ($sConfigKey === 'svg_sanitizer') {
} else {
IssueLog::Error('The configured "'.$sConfigKey.'" class "'.$sSanitizerClass.'" is not a subclass of '.HTMLSanitizer::class.' ! Won\'t sanitize string.');
return $sHTML;

View File

@@ -15,6 +15,8 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
use Combodo\iTop\Core\Kpi\KpiLogData;
use Combodo\iTop\Service\Module\ModuleService;
/**
@@ -30,6 +32,8 @@ class ExecutionKPI
static protected $m_bEnabled_Memory = false;
static protected $m_bBlameCaller = false;
static protected $m_sAllowedUser = '*';
static protected $m_bGenerateLegacyReport = true;
static protected $m_fSlowQueries = 0;
static protected $m_aStats = array(); // Recurrent operations
static protected $m_aExecData = array(); // One shot operations
@@ -77,14 +81,39 @@ class ExecutionKPI
return false;
}
static public function SetGenerateLegacyReport($bReportExtensionsOnly)
{
self::$m_bGenerateLegacyReport = $bReportExtensionsOnly;
}
static public function SetSlowQueries($fSlowQueries)
{
self::$m_fSlowQueries = $fSlowQueries;
}
static public function GetDescription()
{
$aFeatures = array();
if (self::$m_bEnabled_Duration) $aFeatures[] = 'Duration';
if (self::$m_bEnabled_Memory) $aFeatures[] = 'Memory usage';
$sFeatures = implode(', ', $aFeatures);
$sFeatures = 'Measures: '.implode(', ', $aFeatures);
$sFor = self::$m_sAllowedUser == '*' ? 'EVERYBODY' : "'".trim(self::$m_sAllowedUser)."'";
return "KPI logging is active for $sFor. Measures: $sFeatures";
$sSlowQueries = '';
if (self::$m_fSlowQueries > 0) {
$sSlowQueries = ". Slow Queries: ".self::$m_fSlowQueries."s";
}
$aExtensions = [];
/** @var \iKPILoggerExtension $oExtensionInstance */
foreach (MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance) {
$aExtensions[] = ModuleService::GetInstance()->GetModuleNameFromObject($oExtensionInstance);
}
$sExtensions = '';
if (count($aExtensions) > 0) {
$sExtensions = '. KPI Extensions: ['.implode(', ', $aExtensions).']';
}
return "KPI logging is active for $sFor. $sFeatures$sSlowQueries$sExtensions";
}
static public function ReportStats()
@@ -92,7 +121,28 @@ class ExecutionKPI
if (!self::IsEnabled()) return;
global $fItopStarted;
global $iItopInitialMemory;
$sExecId = microtime(); // id to differentiate the hrefs!
$sRequest = $_SERVER['REQUEST_URI'].' ('.$_SERVER['REQUEST_METHOD'].')';
if (isset($_POST['operation'])) {
$sRequest .= ' operation: '.$_POST['operation'];
}
$fStop = MyHelpers::getmicrotime();
if (($fStop - $fItopStarted) > self::$m_fSlowQueries) {
// Invoke extensions to log the KPI operation
/** @var \iKPILoggerExtension $oExtensionInstance */
$iCurrentMemory = self::memory_get_usage();
$iPeakMemory = self::memory_get_peak_usage();
foreach (MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance) {
$oKPILogData = new KpiLogData(KpiLogData::TYPE_REQUEST, 'Page', $sRequest, $fItopStarted, $fStop, '', $iItopInitialMemory, $iCurrentMemory, $iPeakMemory);
$oExtensionInstance->LogOperation($oKPILogData);
}
}
if (!self::$m_bGenerateLegacyReport) {
return;
}
$aBeginTimes = array();
foreach (self::$m_aExecData as $aOpStats)
@@ -105,7 +155,7 @@ class ExecutionKPI
self::Report("<hr/>");
self::Report("<div style=\"background-color: grey; padding: 10px;\">");
self::Report("<h3><a name=\"".md5($sExecId)."\">KPIs</a> - ".$_SERVER['REQUEST_URI']." (".$_SERVER['REQUEST_METHOD'].")</h3>");
self::Report("<h3><a name=\"".md5($sExecId)."\">KPIs</a> - $sRequest</h3>");
self::Report("<p>".date('Y-m-d H:i:s', $fItopStarted)."</p>");
self::Report("<p>log_kpi_user_id: ".UserRights::GetUserId()."</p>");
self::Report("<div>");
@@ -200,8 +250,6 @@ class ExecutionKPI
self::Report("<p><a href=\"#end-".md5($sExecId)."\">Next page stats</a></p>");
$fSlowQueries = MetaModel::GetConfig()->Get('log_kpi_slow_queries');
// Report operation details
foreach (self::$m_aStats as $sOperation => $aOpStats)
{
@@ -245,7 +293,7 @@ class ExecutionKPI
$sTotalInter = round($fTotalInter, 3);
$sMinInter = round($fMinInter, 3);
$sMaxInter = round($fMaxInter, 3);
if (($fTotalInter >= $fSlowQueries))
if (($fTotalInter >= self::$m_fSlowQueries))
{
if ($bDisplayHeader)
{
@@ -271,11 +319,19 @@ class ExecutionKPI
self::Report('<a name="end-'.md5($sExecId).'">&nbsp;</a>');
}
public static function InitStats()
{
// Invoke extensions to initialize the KPI statistics
/** @var \iKPILoggerExtension $oExtensionInstance */
foreach (MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance) {
$oExtensionInstance->InitStats();
}
}
public function __construct()
{
$this->ResetCounters();
}
}
// Get the duration since startup, and reset the counter for the next measure
//
@@ -283,8 +339,14 @@ class ExecutionKPI
{
global $fItopStarted;
if (!self::IsEnabled()) {
return;
}
$aNewEntry = null;
$fStarted = $this->m_fStarted;
$fStopped = $this->m_fStarted;
if (self::$m_bEnabled_Duration)
{
$fStopped = MyHelpers::getmicrotime();
@@ -297,6 +359,9 @@ class ExecutionKPI
$this->m_fStarted = $fStopped;
}
$iInitialMemory = is_null($this->m_iInitialMemory) ? 0 : $this->m_iInitialMemory;
$iCurrentMemory = 0;
$iPeakMemory = 0;
if (self::$m_bEnabled_Memory)
{
$iCurrentMemory = self::memory_get_usage();
@@ -306,41 +371,102 @@ class ExecutionKPI
}
$aNewEntry['mem_begin'] = $this->m_iInitialMemory;
$aNewEntry['mem_end'] = $iCurrentMemory;
if (function_exists('memory_get_peak_usage'))
{
$aNewEntry['mem_peak'] = memory_get_peak_usage();
}
$iPeakMemory = self::memory_get_peak_usage();
$aNewEntry['mem_peak'] = $iPeakMemory;
// Reset for the next operation (if the object is recycled)
$this->m_iInitialMemory = $iCurrentMemory;
}
if (!is_null($aNewEntry))
if (self::$m_bEnabled_Duration || self::$m_bEnabled_Memory) {
// Invoke extensions to log the KPI operation
/** @var \iKPILoggerExtension $oExtensionInstance */
foreach(MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance)
{
$sExtension = ModuleService::GetInstance()->GetModuleNameFromCallStack(1);
$oKPILogData = new KpiLogData(
KpiLogData::TYPE_REPORT,
'Step',
$sOperationDesc,
$fStarted,
$fStopped,
$sExtension,
$iInitialMemory,
$iCurrentMemory,
$iPeakMemory);
$oExtensionInstance->LogOperation($oKPILogData);
}
}
if (!is_null($aNewEntry) && self::$m_bGenerateLegacyReport)
{
self::$m_aExecData[] = $aNewEntry;
}
$this->ResetCounters();
}
public function ComputeStatsForExtension($object, $sMethod)
{
if (!self::IsEnabled()) {
return;
}
$sSignature = ModuleService::GetInstance()->GetModuleMethodSignature($object, $sMethod);
if (utils::StartsWith($sSignature, '[')) {
$this->ComputeStats('Extension', $sSignature);
}
}
public function ComputeStats($sOperation, $sArguments)
{
if (!self::IsEnabled()) {
return;
}
if (self::$m_bEnabled_Duration)
{
$fStopped = MyHelpers::getmicrotime();
$fDuration = $fStopped - $this->m_fStarted;
if (self::$m_bBlameCaller)
{
self::$m_aStats[$sOperation][$sArguments][] = array(
'time' => $fDuration,
'callers' => MyHelpers::get_callstack(1),
);
}
else
{
self::$m_aStats[$sOperation][$sArguments][] = array(
'time' => $fDuration
);
}
}
$aCallstack = [];
if (self::$m_bGenerateLegacyReport) {
if (self::$m_bBlameCaller) {
$aCallstack = MyHelpers::get_callstack(1);
self::$m_aStats[$sOperation][$sArguments][] = [
'time' => $fDuration,
'callers' => $aCallstack,
];
} else {
self::$m_aStats[$sOperation][$sArguments][] = [
'time' => $fDuration
];
}
}
$iInitialMemory = is_null($this->m_iInitialMemory) ? 0 : $this->m_iInitialMemory;
$iCurrentMemory = 0;
$iPeakMemory = 0;
if (self::$m_bEnabled_Memory)
{
$iCurrentMemory = self::memory_get_usage();
$iPeakMemory = self::memory_get_peak_usage();
}
// Invoke extensions to log the KPI operation
/** @var \iKPILoggerExtension $oExtensionInstance */
foreach (MetaModel::EnumPlugins('iKPILoggerExtension') as $oExtensionInstance) {
$sExtension = ModuleService::GetInstance()->GetModuleNameFromCallStack(1);
$oKPILogData = new KpiLogData(
KpiLogData::TYPE_STATS,
$sOperation,
$sArguments,
$this->m_fStarted,
$fStopped,
$sExtension,
$iInitialMemory,
$iCurrentMemory,
$iPeakMemory,
$aCallstack);
$oExtensionInstance->LogOperation($oKPILogData);
}
}
}
protected function ResetCounters()
@@ -370,35 +496,7 @@ class ExecutionKPI
static protected function memory_get_usage()
{
if (function_exists('memory_get_usage'))
{
return memory_get_usage(true);
}
// Copied from the PHP manual
//
//If its Windows
//Tested on Win XP Pro SP2. Should work on Win 2003 Server too
//Doesn't work for 2000
//If you need it to work for 2000 look at http://us2.php.net/manual/en/function.memory-get-usage.php#54642
if (substr(PHP_OS,0,3) == 'WIN')
{
$output = array();
exec('tasklist /FI "PID eq ' . getmypid() . '" /FO LIST', $output);
return preg_replace( '/[\D]/', '', $output[5] ) * 1024;
}
else
{
//We now assume the OS is UNIX
//Tested on Mac OS X 10.4.6 and Linux Red Hat Enterprise 4
//This should work on most UNIX systems
$pid = getmypid();
exec("ps -eo%mem,rss,pid | grep $pid", $output);
$output = explode(" ", $output[0]);
//rss is given in 1024 byte units
return $output[1] * 1024;
}
return memory_get_usage(true);
}
static public function memory_get_peak_usage($bRealUsage = false)

View File

@@ -741,7 +741,9 @@ class ToolsLog extends LogAPI
/**
* @see \CMDBSource::LogDeadLock()
* @since 2.7.1
* @since 2.7.1 PR #139
*
* @link https://dev.mysql.com/doc/refman/5.7/en/innodb-deadlocks.html
*/
class DeadLockLog extends LogAPI
{
@@ -761,14 +763,15 @@ class DeadLockLog extends LogAPI
parent::Enable($sTargetFile);
}
/** @noinspection PhpUnreachableStatementInspection */
private static function GetChannelFromMysqlErrorNo($iMysqlErrorNo)
{
switch ($iMysqlErrorNo)
{
case 1205:
case CMDBSource::MYSQL_ERRNO_WAIT_TIMEOUT:
return self::CHANNEL_WAIT_TIMEOUT;
break;
case 1213:
case CMDBSource::MYSQL_ERRNO_DEADLOCK:
return self::CHANNEL_DEADLOCK_FOUND;
break;
default:

View File

@@ -1213,8 +1213,10 @@ abstract class MetaModel
*
* @return AttributeDefinition[]
* @throws \CoreException
*
* @see GetAttributesList for attcode list
*/
final static public function ListAttributeDefs($sClass)
final public static function ListAttributeDefs($sClass)
{
self::_check_subclass($sClass);
return self::$m_aAttribDefs[$sClass];
@@ -1223,8 +1225,10 @@ abstract class MetaModel
/**
* @param string $sClass
*
* @return array
* @return string[] list of attcodes
* @throws \CoreException
*
* @see ListAttributeDefs to get AttributeDefinition array instead
*/
final public static function GetAttributesList($sClass)
{
@@ -2778,7 +2782,23 @@ abstract class MetaModel
// Build the list of available extensions
//
$aInterfaces = array('iApplicationUIExtension', 'iPreferencesExtension', 'iApplicationObjectExtension', 'iLoginFSMExtension', 'iLoginUIExtension', 'iLogoutExtension', 'iQueryModifier', 'iOnClassInitialization', 'iPopupMenuExtension', 'iPageUIExtension', 'iPortalUIExtension', 'ModuleHandlerApiInterface', 'iNewsroomProvider', 'iModuleExtension');
$aInterfaces = [
'iLoginFSMExtension',
'iLogoutExtension',
'iLoginUIExtension',
'iPreferencesExtension',
'iApplicationUIExtension',
'iApplicationObjectExtension',
'iPopupMenuExtension',
'iPageUIExtension',
'iPortalUIExtension',
'iQueryModifier',
'iOnClassInitialization',
'iModuleExtension',
'iKPILoggerExtension',
'ModuleHandlerApiInterface',
'iNewsroomProvider',
];
foreach($aInterfaces as $sInterface)
{
self::$m_aExtensionClasses[$sInterface] = array();
@@ -6348,7 +6368,9 @@ abstract class MetaModel
ExecutionKPI::EnableDuration(self::$m_oConfig->Get('log_kpi_duration'));
ExecutionKPI::EnableMemory(self::$m_oConfig->Get('log_kpi_memory'));
ExecutionKPI::SetAllowedUser(self::$m_oConfig->Get('log_kpi_user_id'));
ExecutionKPI::SetAllowedUser(self::$m_oConfig->Get('log_kpi_user_id'));
ExecutionKPI::SetGenerateLegacyReport(self::$m_oConfig->Get('log_kpi_generate_legacy_report'));
ExecutionKPI::SetSlowQueries(self::$m_oConfig->Get('log_kpi_slow_queries'));
self::$m_bSkipCheckToWrite = self::$m_oConfig->Get('skip_check_to_write');
self::$m_bSkipCheckExtKeys = self::$m_oConfig->Get('skip_check_ext_keys');
@@ -6485,6 +6507,7 @@ abstract class MetaModel
CMDBSource::InitFromConfig(self::$m_oConfig);
// Later when timezone implementation is correctly done: CMDBSource::SetTimezone($sDBTimezone);
ExecutionKPI::InitStats();
}
/**
@@ -6704,7 +6727,13 @@ abstract class MetaModel
if ($bMustBeFound && empty($aRow))
{
throw new CoreException("No result for the single row query: '$sSQL'");
$sNotFoundErrorMessage = "No result for the single row query";
IssueLog::Info($sNotFoundErrorMessage, LogChannels::CMDB_SOURCE, [
'class' => $sClass,
'key' => $iKey,
'sql_query' => $sSQL,
]);
throw new CoreException($sNotFoundErrorMessage);
}
return $aRow;
@@ -6784,28 +6813,21 @@ abstract class MetaModel
* $bMustBeFound=false)
* @throws CoreException if no result found and $bMustBeFound=true
* @throws ArchivedObjectException if archive mode disabled and result is archived and $bMustBeFound=true
* @throws \Exception
*
*/
public static function GetObject($sClass, $iKey, $bMustBeFound = true, $bAllowAllData = false, $aModifierProperties = null)
{
$oObject = self::GetObjectWithArchive($sClass, $iKey, $bMustBeFound, $bAllowAllData, $aModifierProperties);
if (empty($oObject))
{
if (empty($oObject)) {
return null;
}
if (!utils::IsArchiveMode() && $oObject->IsArchived())
{
if ($bMustBeFound)
{
if (!utils::IsArchiveMode() && $oObject->IsArchived()) {
if ($bMustBeFound) {
throw new ArchivedObjectException("The object $sClass::$iKey is archived");
}
else
{
return null;
}
return null;
}
return $oObject;

View File

@@ -22,7 +22,9 @@
* A class to serialize the execution of some code sections
* Emulates the API of PECL Mutex class
* Relies on MySQL locks because the API sem_get is not always present in the
* installed PHP.
* installed PHP.
*
* @link https://dev.mysql.com/doc/refman/5.7/en/locking-functions.html MySQL locking functions documentation
*
* @copyright Copyright (C) 2013-2018 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
@@ -257,7 +259,7 @@ class iTopMutex
$this->hDBLink = CMDBSource::GetMysqliInstance($sServer, $sUser, $sPwd, $sSource, $bTlsEnabled, $sTlsCA, false);
if (!$this->hDBLink) {
throw new Exception("Could not connect to the DB server (host=$sServer, user=$sUser): ".mysqli_connect_error().' (mysql errno: '.mysqli_connect_errno().')');
throw new MySQLException('Could not connect to the DB server '.mysqli_connect_error().' (mysql errno: '.mysqli_connect_errno(), array('host' => $sDBHost, 'user' => $sDBUser));
}
// Make sure that the server variable `wait_timeout` is at least 86400 seconds for this connection,

View File

@@ -34,7 +34,9 @@
*/
class ObjectResult
{
public $code;
use SanitizeTrait;
public $code;
public $message;
public $class;
public $key;
@@ -122,6 +124,19 @@ class ObjectResult
{
$this->fields[$sAttCode] = $this->MakeResultValue($oObject, $sAttCode, $bExtendedOutput);
}
public function SanitizeContent()
{
foreach($this->fields as $sFieldAttCode => $fieldValue)
{
try {
$oAttDef = MetaModel::GetAttributeDef($this->class, $sFieldAttCode);
} catch (Exception $e) { // for special cases like ID
continue;
}
$this->SanitizeFieldIfSensitive($this->fields, $sFieldAttCode, $fieldValue, $oAttDef);
}
}
}
@@ -181,6 +196,16 @@ class RestResultWithObjects extends RestResult
$sObjKey = get_class($oObject).'::'.$oObject->GetKey();
$this->objects[$sObjKey] = $oObjRes;
}
public function SanitizeContent()
{
parent::SanitizeContent();
foreach($this->objects as $sObjKey => $oObjRes)
{
$oObjRes->SanitizeContent();
}
}
}
class RestResultWithRelations extends RestResultWithObjects
@@ -247,9 +272,10 @@ class RestDelete
*
* @package Core
*/
class CoreServices implements iRestServiceProvider
class CoreServices implements iRestServiceProvider, iRestInputSanitizer
{
/**
use SanitizeTrait;
/**
* Enumerate services delivered by this class
*
* @param string $sVersion The version (e.g. 1.0) supported by the services
@@ -663,6 +689,33 @@ class CoreServices implements iRestServiceProvider
}
return $oResult;
}
public function SanitizeJsonInput(string $sJsonInput): string
{
$sSanitizedJsonInput = $sJsonInput;
$aJsonData = json_decode($sSanitizedJsonInput, true);
$sOperation = $aJsonData['operation'];
switch ($sOperation) {
case 'core/check_credentials':
if (isset($aJsonData['password'])) {
$aJsonData['password'] = '*****';
}
break;
case 'core/update':
case 'core/create':
default :
$sClass = $aJsonData['class'];
if (isset($aJsonData['fields'])) {
foreach ($aJsonData['fields'] as $sFieldAttCode => $fieldValue) {
$oAttDef = MetaModel::GetAttributeDef($sClass, $sFieldAttCode);
$this->SanitizeFieldIfSensitive($aJsonData['fields'], $sFieldAttCode, $fieldValue, $oAttDef);
}
}
break;
}
return json_encode($aJsonData, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
/**
* Helper for object deletion
@@ -802,3 +855,50 @@ class CoreServices implements iRestServiceProvider
return $iLimit * max(0, $iPage - 1);
}
}
trait SanitizeTrait
{
/**
* Sanitize a field if it is sensitive.
*
* @param array $fields The fields array
* @param string $sFieldAttCode The attribute code
* @param mixed $oAttDef The attribute definition
* @throws Exception
*/
private function SanitizeFieldIfSensitive(array &$fields, string $sFieldAttCode, $fieldValue, $oAttDef): void
{
// for simple attribute
if ($oAttDef instanceof iAttributeNoGroupBy) // iAttributeNoGroupBy is equivalent to sensitive attribute
{
$fields[$sFieldAttCode] = '*****';
return;
}
// for 1-n / n-n relation
if ($oAttDef instanceof AttributeLinkedSet) {
foreach ($fieldValue as $i => $aLnkValues) {
foreach ($aLnkValues as $sLnkAttCode => $sLnkValue) {
$oLnkAttDef = MetaModel::GetAttributeDef($oAttDef->GetLinkedClass(), $sLnkAttCode);
if ($oLnkAttDef instanceof iAttributeNoGroupBy) { // 1-n relation
$fields[$sFieldAttCode][$i][$sLnkAttCode] = '*****';
}
elseif ($oAttDef instanceof AttributeLinkedSetIndirect && $oLnkAttDef instanceof AttributeExternalField) { // for n-n relation
$oExtKeyAttDef = MetaModel::GetAttributeDef($oLnkAttDef->GetTargetClass(), $oLnkAttDef->GetExtAttCode());
if ($oExtKeyAttDef instanceof iAttributeNoGroupBy) {
$fields[$sFieldAttCode][$i][$sLnkAttCode] = '*****';
}
}
}
}
return;
}
// for external attribute
if ($oAttDef instanceof AttributeExternalField) {
$oExtKeyAttDef = MetaModel::GetAttributeDef($oAttDef->GetTargetClass(), $oAttDef->GetExtAttCode());
if ($oExtKeyAttDef instanceof iAttributeNoGroupBy) {
$fields[$sFieldAttCode] = '*****';
}
}
}
}

View File

@@ -537,7 +537,7 @@ EOF
}
else
{
throw new Exception('graphviz not found (executable path: '.$sDotExecutable.')');
throw new Exception('graphviz not found');
}
return $sHtml;
}
@@ -592,7 +592,7 @@ EOF
}
else
{
throw new Exception('graphviz not found (executable path: '.$sDotExecutable.')');
throw new Exception('graphviz not found');
}
return $sHtml;
}

View File

@@ -126,7 +126,9 @@ abstract class Trigger extends cmdbAbstractObject
$oAction = MetaModel::GetObject('Action', $iActionId);
if ($oAction->IsActive())
{
$oKPI = new ExecutionKPI();
$oAction->DoExecute($this, $aContextArgs);
$oKPI->ComputeStatsForExtension($oAction, 'DoExecute');
}
}
}

View File

@@ -110,7 +110,7 @@ abstract class UserRightsAddOnAPI
$oSearchSharers->AllowAllData();
$oSearchSharers->AddCondition_ReferencedBy($oShareSearch, 'sharing_org_id');
$aSharers = array();
foreach($oSearchSharers->ToDataArray(array('id')) as $aRow)
foreach($oSearchSharers->SelectAttributeToArray('id') as $aRow)
{
$aSharers[] = $aRow['id'];
}
@@ -135,7 +135,7 @@ abstract class UserRightsAddOnAPI
$oOrgField = new FieldExpression('org_id', $sShareClass);
$oSearchShares->AddConditionExpression(new BinaryExpression($oOrgField, 'IN', $oListExpr));
$aShared = array();
foreach($oSearchShares->ToDataArray(array($sShareAttCode)) as $aRow)
foreach($oSearchShares->SelectAttributeToArray($sShareAttCode) as $aRow)
{
$aShared[] = $aRow[$sShareAttCode];
}
@@ -817,6 +817,9 @@ class UserRights
}
}
/**
* @return string connected {@see User} login field value, otherwise empty string
*/
public static function GetUser()
{
if (is_null(self::$m_oUser))
@@ -829,7 +832,9 @@ class UserRights
}
}
/** User */
/**
* @return User|null
*/
public static function GetUserObject()
{
if (is_null(self::$m_oUser))
@@ -1029,7 +1034,7 @@ class UserRights
/**
* @param string $sClass
* @param int $iActionCode
* @param int $iActionCode see UR_ACTION_* constants
* @param DBObjectSet $oInstanceSet
* @param User $oUser
* @return int (UR_ALLOWED_YES|UR_ALLOWED_NO|UR_ALLOWED_DEPENDS)

View File

@@ -17,7 +17,7 @@
*/
// Beware the version number MUST be enclosed with quotes otherwise v2.3.0 becomes v2 0.3 .0
$version: "v2.7.9";
$version: "v2.7.12";
$approot-relative: "../../../../../" !default; // relative to env-***/branding/themes/***/main.css
// Base colors

View File

@@ -15,10 +15,14 @@
*
* You should have received a copy of the GNU Affero General Public License
*/
/* integrityCheck: begin (do not remove/edit) */
/* Helpers classes */
.center {
text-align: center;
}
.hidden {
display: none;
}
/* Animations */
@keyframes progress_bar_color_ongoing {
from {
@@ -265,4 +269,4 @@ fieldset > legend {
font-weight: bold;
color: #e60000b8;
}
/* integrityCheck: end (do not remove/edit) */

View File

@@ -56,6 +56,9 @@ $progress-bar-error-bg-color: #F56565 !default;
.center {
text-align: center;
}
.hidden {
display: none;
}
/* Animations */
@keyframes progress_bar_color_ongoing {

View File

@@ -5,7 +5,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'authent-cas/2.7.9',
'authent-cas/2.7.12',
array(
// Identification
//

View File

@@ -47,6 +47,11 @@ class CASLoginExtension extends AbstractLoginFSMExtension implements iLogoutExte
protected function OnReadCredentials(&$iErrorCode)
{
if (LoginWebPage::getIOnExit() === LoginWebPage::EXIT_RETURN) {
// Not allowed if not already connected
return LoginWebPage::LOGIN_FSM_CONTINUE;
}
if (!isset($_SESSION['login_mode']) || ($_SESSION['login_mode'] == 'cas'))
{
static::InitCASClient();
@@ -71,7 +76,8 @@ class CASLoginExtension extends AbstractLoginFSMExtension implements iLogoutExte
return LoginWebPage::LOGIN_FSM_ERROR;
}
}
$_SESSION['login_mode'] = 'cas';
$_SESSION['login_mode'] = 'cas';
phpCAS::forceAuthentication(); // Redirect to CAS and exit
}
}
@@ -80,7 +86,7 @@ class CASLoginExtension extends AbstractLoginFSMExtension implements iLogoutExte
protected function OnCheckCredentials(&$iErrorCode)
{
if ($_SESSION['login_mode'] == 'cas')
if (isset($_SESSION['login_mode']) && $_SESSION['login_mode'] == 'cas')
{
if (!isset($_SESSION['auth_user']))
{
@@ -97,7 +103,7 @@ class CASLoginExtension extends AbstractLoginFSMExtension implements iLogoutExte
protected function OnCredentialsOK(&$iErrorCode)
{
if ($_SESSION['login_mode'] == 'cas')
if (isset($_SESSION['login_mode']) && $_SESSION['login_mode'] == 'cas')
{
$sAuthUser = $_SESSION['auth_user'];
if (!LoginWebPage::CheckUser($sAuthUser))
@@ -112,9 +118,15 @@ class CASLoginExtension extends AbstractLoginFSMExtension implements iLogoutExte
protected function OnError(&$iErrorCode)
{
if ($_SESSION['login_mode'] == 'cas')
if (isset($_SESSION['login_mode']) && $_SESSION['login_mode'] == 'cas')
{
unset($_SESSION['phpCAS']);
unset($_SESSION['phpCAS']);
if (LoginWebPage::getIOnExit() === LoginWebPage::EXIT_RETURN) {
// don't display the login page
return LoginWebPage::LOGIN_FSM_CONTINUE;
}
if ($iErrorCode != LoginWebPage::EXIT_CODE_MISSINGLOGIN)
{
$oLoginWebPage = new LoginWebPage();
@@ -127,7 +139,7 @@ class CASLoginExtension extends AbstractLoginFSMExtension implements iLogoutExte
protected function OnConnected(&$iErrorCode)
{
if ($_SESSION['login_mode'] == 'cas')
if (isset($_SESSION['login_mode']) && $_SESSION['login_mode'] == 'cas')
{
$_SESSION['can_logoff'] = true;
return LoginWebPage::CheckLoggedUser($iErrorCode);
@@ -156,7 +168,7 @@ class CASLoginExtension extends AbstractLoginFSMExtension implements iLogoutExte
{
phpCAS::setLogger(new CASLogger(APPROOT.'log/cas.log'));
}
// Initialize phpCAS
$sCASVersion = Config::Get('cas_version');
$sCASHost = Config::Get('cas_host');

View File

@@ -27,7 +27,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'authent-external/2.7.9',
'authent-external/2.7.12',
array(
// Identification
//

View File

@@ -9,7 +9,7 @@ if (function_exists('ldap_connect'))
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'authent-ldap/2.7.9',
'authent-ldap/2.7.12',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'authent-local/2.7.9',
'authent-local/2.7.12',
array(
// Identification
//

View File

@@ -24,7 +24,7 @@
/** @noinspection PhpUnhandledExceptionInspection */
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'combodo-db-tools/2.7.9',
'combodo-db-tools/2.7.12',
array(
// Identification
//

View File

@@ -19,7 +19,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-attachments/2.7.9',
'itop-attachments/2.7.12',
array(
// Identification
//

View File

@@ -53,14 +53,7 @@ class DBRestore extends DBBackup
$sUser = self::EscapeShellArg($this->sDBUser);
$sPwd = self::EscapeShellArg($this->sDBPwd);
$sDBName = self::EscapeShellArg($this->sDBName);
if (empty($this->sMySQLBinDir))
{
$sMySQLExe = 'mysql';
}
else
{
$sMySQLExe = '"'.$this->sMySQLBinDir.'/mysql"';
}
$sMySQLExe = DBBackup::MakeSafeMySQLCommand($this->sMySQLBinDir, 'mysql');
if (is_null($this->iDBPort))
{
$sPortOption = '';

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-backup/2.7.9',
'itop-backup/2.7.12',
array(
// Identification
//

View File

@@ -56,15 +56,7 @@ try
//
$sMySQLBinDir = MetaModel::GetConfig()->GetModuleSetting('itop-backup', 'mysql_bindir', '');
$sMySQLBinDir = utils::ReadParam('mysql_bindir', $sMySQLBinDir, true);
if (empty($sMySQLBinDir))
{
$sMySQLDump = 'mysqldump';
}
else
{
//echo 'Info - Found mysql_bindir: '.$sMySQLBinDir;
$sMySQLDump = '"'.$sMySQLBinDir.'/mysqldump"';
}
$sMySQLDump = DBBackup::MakeSafeMySQLCommand($sMySQLBinDir, 'mysqldump');
$sCommand = "$sMySQLDump -V 2>&1";
$aOutput = array();

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-bridge-virtualization-storage/2.7.9',
'itop-bridge-virtualization-storage/2.7.12',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-change-mgmt-itil/2.7.9',
'itop-change-mgmt-itil/2.7.12',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-change-mgmt/2.7.9',
'itop-change-mgmt/2.7.12',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-config-mgmt/2.7.9',
'itop-config-mgmt/2.7.12',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-config/2.7.9',
'itop-config/2.7.12',
array(
// Identification
//

View File

@@ -24,7 +24,7 @@
/** @noinspection PhpUnhandledExceptionInspection */
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-core-update/2.7.9',
'itop-core-update/2.7.12',
array(
// Identification
//

View File

@@ -18,7 +18,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-datacenter-mgmt/2.7.9',
'itop-datacenter-mgmt/2.7.12',
array(
// Identification
//

View File

@@ -25,7 +25,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-endusers-devices/2.7.9',
'itop-endusers-devices/2.7.12',
array(
// Identification
//

View File

@@ -24,7 +24,7 @@
/** @noinspection PhpUnhandledExceptionInspection */
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-files-information/2.7.9',
'itop-files-information/2.7.12',
array(
// Identification
//

View File

@@ -6,7 +6,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-full-itil/2.7.9',
'itop-full-itil/2.7.12',
array(
// Identification
//

View File

@@ -0,0 +1,19 @@
<?php
class TokenValidation
{
// construct function
public function __construct()
{
}
public function isSetupTokenValid($sParamToken) : bool
{
if (!file_exists(APPROOT.'data/.setup')) {
return false;
}
$sSetupToken = trim(file_get_contents(APPROOT.'data/.setup'));
unlink(APPROOT.'data/.setup');
return $sParamToken === $sSetupToken;
}
}

View File

@@ -7,7 +7,7 @@ class HubConnectorPage extends NiceWebPage
parent::__construct($sTitle);
$this->no_cache();
$this->add_xframe_options();
$this->add_http_headers();
$sImagesDir = utils::GetAbsoluteUrlAppRoot().'images';
$sModuleImagesDir = utils::GetAbsoluteUrlModulesRoot().'itop-hub-connector/images';

View File

@@ -280,6 +280,7 @@ try
require_once ('hubconnectorpage.class.inc.php');
require_once (APPROOT.'/application/startup.inc.php');
require_once('TokenValidation.php');
$sTargetRoute = utils::ReadParam('target', ''); // ||browse_extensions|deploy_extensions|
@@ -299,11 +300,20 @@ try
case 'inform_after_setup':
// Hidden IFRAME at the end of the setup
require_once (APPROOT.'/application/ajaxwebpage.class.inc.php');
$oPage = new NiceWebPage('');
$aDataToPost = MakeDataToPost($sTargetRoute);
$oPage->add('<form id="hub_launch_form" action="'.$sHubUrlStateless.'" method="post">');
$oPage->add('<input type="hidden" name="json" value="'.htmlentities(json_encode($aDataToPost), ENT_QUOTES, 'UTF-8').'">');
$oPage->add_ready_script('$("#hub_launch_form").submit();');
$sParamToken = utils::ReadParam('setup_token');
$oTokenValidation = new TokenValidation();
$bIsTokenValid = $oTokenValidation->isSetupTokenValid($sParamToken);
if (UserRights::IsAdministrator() || $bIsTokenValid) {
$oPage = new NiceWebPage('');
$aDataToPost = MakeDataToPost($sTargetRoute);
$oPage->add('<form id="hub_launch_form" action="' . $sHubUrlStateless . '" method="post">');
$oPage->add('<input type="hidden" name="json" value="' . htmlentities(json_encode($aDataToPost), ENT_QUOTES, 'UTF-8') . '">');
$oPage->add_ready_script('$("#hub_launch_form").submit();');
} else {
IssueLog::Error('TokenValidation failed on inform_after_setup page');
throw new Exception("Not allowed");
}
break;
default:

View File

@@ -5,7 +5,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-hub-connector/2.7.9',
'itop-hub-connector/2.7.12',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-incident-mgmt-itil/2.7.9',
'itop-incident-mgmt-itil/2.7.12',
array(
// Identification
//

View File

@@ -3,7 +3,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-knownerror-mgmt/2.7.9',
'itop-knownerror-mgmt/2.7.12',
array(
// Identification
//

View File

@@ -92,6 +92,8 @@ const OAuthConnect = function(sClass, sId, sAjaxUri) {
function (oData) {
if (oData.status === 'success') {
oOpenSignInWindow(oData.data.authorization_url, 'OAuth authorization')
} else {
alert(oData.error_description);
}
}
);

View File

@@ -202,6 +202,7 @@
$this->Set('refresh_token', $oAccessToken->getRefreshToken());
}
$this->Set('status', 'active');
$this->AllowWrite();
$this->DBUpdate();
}
]]></code>
@@ -338,6 +339,11 @@
<default_value>no</default_value>
<is_null_allowed>true</is_null_allowed>
</field>
<field id="tenant" xsi:type="AttributeString">
<sql>tenant</sql>
<default_value>common</default_value>
<is_null_allowed>false</is_null_allowed>
</field>
</fields>
<presentation>
<details>
@@ -363,15 +369,18 @@
<item id="redirect_url">
<rank>50</rank>
</item>
<item id="client_id">
<item id="tenant">
<rank>60</rank>
</item>
<item id="client_secret">
<item id="client_id">
<rank>70</rank>
</item>
<item id="mailbox_list">
<item id="client_secret">
<rank>80</rank>
</item>
<item id="mailbox_list">
<rank>90</rank>
</item>
</items>
</item>
</items>

View File

@@ -93,6 +93,8 @@ Dict::Add('EN US', 'English', 'English', array(
'Class:OAuthClientAzure/Attribute:used_for_smtp+' => 'At least one OAuth client must have this flag to “Yes”, if you want iTop to use it for sending mails',
'Class:OAuthClientAzure/Attribute:used_for_smtp/Value:yes' => 'Yes',
'Class:OAuthClientAzure/Attribute:used_for_smtp/Value:no' => 'No',
'Class:OAuthClientAzure/Attribute:tenant' => 'Tenant',
'Class:OAuthClientAzure/Attribute:tenant+' => 'Tenant ID of the configured application. For multi-tenant application, use common.',
));
//

View File

@@ -5,7 +5,7 @@
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-oauth-client/2.7.9',
'itop-oauth-client/2.7.12',
array(
// Identification
//

View File

@@ -10,7 +10,9 @@ use cmdbAbstractObject;
use Combodo\iTop\Application\TwigBase\Controller\Controller;
use Combodo\iTop\Core\Authentication\Client\OAuth\OAuthClientProviderFactory;
use Dict;
use Exception;
use IssueLog;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use MetaModel;
use utils;
@@ -30,8 +32,13 @@ class AjaxOauthClientController extends Controller
$aResult = ['status' => 'success', 'data' => []];
$sAuthorizationUrl = OAuthClientProviderFactory::GetAuthorizationUrl($oOAuthClient);
$aResult['data']['authorization_url'] = $sAuthorizationUrl;
try {
$sAuthorizationUrl = OAuthClientProviderFactory::GetAuthorizationUrl($oOAuthClient);
$aResult['data']['authorization_url'] = $sAuthorizationUrl;
} catch (Exception $oException) {
$aResult['status'] = 'error';
$aResult['error_description'] = $oException->getMessage();
}
$this->DisplayJSONPage($aResult);
}
@@ -64,13 +71,15 @@ class AjaxOauthClientController extends Controller
}
if (isset($aQuery['code'])) {
$sCode = $aQuery['code'];
$oAccessToken = OAuthClientProviderFactory::GetAccessTokenFromCode($oOAuthClient, $sCode);
$oOAuthClient->SetAccessToken($oAccessToken);
$aResult['status'] = 'success';
try {
$oAccessToken = OAuthClientProviderFactory::GetAccessTokenFromCode($oOAuthClient, $sCode);
$oOAuthClient->SetAccessToken($oAccessToken);
$aResult['status'] = 'success';
}
catch (IdentityProviderException $e) {
$aResult['status'] = 'error';
$aResult['error_description'] = $e->getMessage();
}
}
} else {
$aResult['status'] = 'error';

View File

@@ -20,7 +20,7 @@
/** @noinspection PhpUnhandledExceptionInspection */
SetupWebPage::AddModule(
__FILE__, // Path to the current file, all other file names are relative to the directory containing this file
'itop-portal-base/2.7.9', array(
'itop-portal-base/2.7.12', array(
// Identification
'label' => 'Portal Development Library',
'category' => 'Portal',

View File

@@ -64,7 +64,7 @@ foreach ($aPortalConf['properties']['themes'] as $sKey => $value)
{
if (!is_array($value))
{
$aPortalConf['properties']['themes'][$sKey] = $_ENV['COMBODO_ABSOLUTE_URL'].utils::GetCSSFromSASS('env-'.utils::GetCurrentEnvironment().'/'.$value,
$aPortalConf['properties']['themes'][$sKey] = utils::GetCSSFromSASS('env-'.utils::GetCurrentEnvironment().'/'.$value,
$aImportPaths);
}
else
@@ -72,7 +72,7 @@ foreach ($aPortalConf['properties']['themes'] as $sKey => $value)
$aValues = array();
foreach ($value as $sSubValue)
{
$aValues[] = $_ENV['COMBODO_ABSOLUTE_URL'].utils::GetCSSFromSASS('env-'.utils::GetCurrentEnvironment().'/'.$sSubValue,
$aValues[] = utils::GetCSSFromSASS('env-'.utils::GetCurrentEnvironment().'/'.$sSubValue,
$aImportPaths);
}
$aPortalConf['properties']['themes'][$sKey] = $aValues;

View File

@@ -15,6 +15,11 @@
# You should have received a copy of the GNU Affero General Public License
# along with iTop. If not, see <http://www.gnu.org/licenses/>
p_user_profile_brick_edit_person:
path: '/user/edit_person'
defaults:
_controller: 'Combodo\iTop\Portal\Controller\UserProfileBrickController::EditPerson'
p_user_profile_brick:
path: '/user/{sBrickId}'
defaults:

View File

@@ -1379,3 +1379,19 @@ table .group-actions {
.wiki_broken_link {
text-decoration: line-through;
}
@media print {
/* Prevent URLs from being displayed */
a[href]::after, img[src]::after {
content: none !important;
/* Force modals to be displayed one after another instead of stacked */
}
.modal.in {
position: relative;
top: unset;
z-index: unset;
overflow-y: unset;
}
#drag_overlay {
display: none;
}
}

View File

@@ -1463,3 +1463,27 @@ table .group-actions .item-action-wrapper .panel-body > p:last-child{
.wiki_broken_link {
text-decoration: line-through;
}
@media print {
/* Prevent URLs from being displayed */
a[href], img[src] {
&::after {
content: none !important;
}
}
/* Force modals to be displayed one after another instead of stacked */
.modal {
&.in {
position: relative;
top: unset;
z-index: unset;
overflow-y: unset;
}
}
#drag_overlay {
display: none;
}
}

File diff suppressed because one or more lines are too long

View File

@@ -49,6 +49,7 @@ use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use UnaryExpression;
use URLButtonItem;
use utils;
/**
* Class ManageBrickController
@@ -259,6 +260,7 @@ class ManageBrickController extends BrickController
'oBrick' => $oBrick,
'sBrickId' => $sBrickId,
'sToken' => $oExporter->SaveState(),
'sWikiUrl' => 'https://www.itophub.io/wiki/page?id='.utils::GetItopVersionWikiSyntax().'%3Auser%3Alists#excel_export',
);
return $this->render(static::EXCEL_EXPORT_TEMPLATE_PATH, $aData);

View File

@@ -1035,7 +1035,11 @@ class ObjectController extends BrickController
// When reaching to an Attachment, we have to check security on its host object instead of the Attachment itself
if ($sObjectClass === 'Attachment')
{
$oAttachment = MetaModel::GetObject($sObjectClass, $sObjectId, true, true);
$oAttachment = MetaModel::GetObject($sObjectClass, $sObjectId, false, true);
if ($oAttachment === null) {
throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
}
$sHostClass = $oAttachment->Get('item_class');
$sHostId = $oAttachment->Get('item_id');
}
@@ -1242,7 +1246,12 @@ class ObjectController extends BrickController
$bIgnoreSilos = $oScopeValidator->IsAllDataAllowedForScope(UserRights::ListProfiles(), $sObjectClass);
$aParams = array('objects_id' => $aObjectIds);
$oSearch = DBObjectSearch::FromOQL("SELECT $sObjectClass WHERE id IN (:objects_id)");
if ($bIgnoreSilos === true)
if (!$oScopeValidator->AddScopeToQuery($oSearch, $sObjectClass)
) {
IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' not allowed to read ' . $sObjectClass . ' object.');
throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
}
if ($bIgnoreSilos === true)
{
$oSearch->AllowAllData();
}

View File

@@ -35,7 +35,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpException;
use UserRights;
use utils;
use Dict;
/**
* Class UserProfileBrickController
*
@@ -66,34 +66,9 @@ class UserProfileBrickController extends BrickController
$oRequestManipulator = $this->get('request_manipulator');
/** @var \Combodo\iTop\Portal\Helper\ObjectFormHandlerHelper $ObjectFormHandler */
$ObjectFormHandler = $this->get('object_form_handler');
/** @var \Combodo\iTop\Portal\Brick\BrickCollection $oBrickCollection */
$oBrickCollection = $this->get('brick_collection');
$oBrick = $this->GetBrick($sBrickId);
// If the brick id was not specified, we get the first one registered that is an instance of UserProfileBrick as default
if ($sBrickId === null)
{
/** @var \Combodo\iTop\Portal\Brick\PortalBrick $oTmpBrick */
foreach ($oBrickCollection->GetBricks() as $oTmpBrick)
{
if ($oTmpBrick instanceof UserProfileBrick)
{
$oBrick = $oTmpBrick;
}
}
// We make sure a UserProfileBrick was found
if (!isset($oBrick) || $oBrick === null)
{
$oBrick = new UserProfileBrick();
//throw new HttpException(Response::HTTP_INTERNAL_SERVER_ERROR, 'UserProfileBrick : Brick could not be loaded as there was no UserProfileBrick loaded in the application.');
}
}
else
{
$oBrick = $oBrickCollection->GetBrickById($sBrickId);
}
$aData = array();
$aData = array();
// Setting form mode regarding the demo mode parameter
$bDemoMode = MetaModel::GetConfig()->Get('demo_mode');
@@ -130,11 +105,12 @@ class UserProfileBrickController extends BrickController
$oCurContact = UserRights::GetContactObject();
$sCurContactClass = get_class($oCurContact);
$sCurContactId = $oCurContact->GetKey();
$aForm = $oBrick->GetForm();
$aForm['submit_endpoint'] = $this->generateUrl('p_user_profile_brick_edit_person', ['sBrickId' => $sBrickId]);
// Preparing forms
$aData['forms']['contact'] = $ObjectFormHandler->HandleForm($oRequest, $sFormMode, $sCurContactClass, $sCurContactId,
$oBrick->GetForm());
$aData['forms']['preferences'] = $this->HandlePreferencesForm($oRequest, $sFormMode);
$aData['forms']['contact'] = $ObjectFormHandler->HandleForm($oRequest, $sFormMode, $sCurContactClass, $sCurContactId,
$aForm);
$aData['forms']['preferences'] = $this->HandlePreferencesForm($oRequest, $sFormMode);
// - If user can change password, we display the form
$aData['forms']['password'] = (UserRights::CanChangePassword()) ? $this->HandlePasswordForm($oRequest, $sFormMode) : null;
@@ -150,6 +126,35 @@ class UserProfileBrickController extends BrickController
return $oResponse;
}
public function EditPerson(Request $oRequest)
{
/** @var \Combodo\iTop\Portal\Helper\ObjectFormHandlerHelper $oObjectFormHandler */
$oObjectFormHandler = $this->get('object_form_handler');
/** @var \Combodo\iTop\Portal\Helper\SecurityHelper $oSecurityHelper */
$oSecurityHelper = $this->get('security_helper');
$oCurContact = UserRights::GetContactObject();
$sObjectClass = get_class($oCurContact);
$sObjectId = $oCurContact->GetKey();
// Checking security layers
// Warning : This is a dirty quick fix to allow editing its own contact information
$bAllowWrite = ($sObjectClass === 'Person' && $sObjectId == UserRights::GetContactId());
if (!$oSecurityHelper->IsActionAllowed(UR_ACTION_MODIFY, $sObjectClass, $sObjectId) && !$bAllowWrite) {
IssueLog::Warning(__METHOD__ . ' at line ' . __LINE__ . ' : User #' . UserRights::GetUserId() . ' not allowed to modify ' . $sObjectClass . '::' . $sObjectId . ' object.');
throw new HttpException(Response::HTTP_NOT_FOUND, Dict::S('UI:ObjectDoesNotExist'));
}
$aForm = $this->GetBrick()->GetForm();
$aForm['submit_endpoint'] = $this->generateUrl('p_user_profile_brick_edit_person');
$aData = ['sMode' => 'edit'];
$aData['form'] = $oObjectFormHandler->HandleForm($oRequest, $aData['sMode'], $sObjectClass, $sObjectId, $aForm);
return new JsonResponse($aData);
}
/**
* @param \Symfony\Component\HttpFoundation\Request $oRequest
* @param string $sFormMode
@@ -388,4 +393,34 @@ class UserProfileBrickController extends BrickController
return $aFormData;
}
/**
* @param $sBrickId
* @return \Combodo\iTop\Portal\Brick\PortalBrick|UserProfileBrick
* @throws \Combodo\iTop\Portal\Brick\BrickNotFoundException
*/
public function GetBrick($sBrickId = null)
{
/** @var \Combodo\iTop\Portal\Brick\BrickCollection $oBrickCollection */
$oBrickCollection = $this->get('brick_collection');
// If the brick id was not specified, we get the first one registered that is an instance of UserProfileBrick as default
if ($sBrickId === null) {
/** @var \Combodo\iTop\Portal\Brick\PortalBrick $oTmpBrick */
foreach ($oBrickCollection->GetBricks() as $oTmpBrick) {
if ($oTmpBrick instanceof UserProfileBrick) {
$oBrick = $oTmpBrick;
}
}
// We make sure a UserProfileBrick was found
if (!isset($oBrick) || $oBrick === null) {
$oBrick = new UserProfileBrick();
//throw new HttpException(Response::HTTP_INTERNAL_SERVER_ERROR, 'UserProfileBrick : Brick could not be loaded as there was no UserProfileBrick loaded in the application.');
}
} else {
$oBrick = $oBrickCollection->GetBrickById($sBrickId);
}
return $oBrick;
}
}

View File

@@ -29,6 +29,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Exception\HttpException;
use UserRights;
use utils;
/**
* Class UserProvider
@@ -91,6 +92,9 @@ class UserProvider implements ContainerAwareInterface
}
$this->oContainer->set('combodo.current_user', $oUser);
// User allowed to log off or not
$this->oContainer->set('combodo.current_user.can_logoff', utils::CanLogOff());
// Allowed portals
$aAllowedPortals = UserRights::GetAllowedPortals();

View File

@@ -132,10 +132,8 @@ class ObjectFormHandlerHelper
$bModal = ($oRequest->isXmlHttpRequest() && empty($sOperation));
// - Retrieve form properties
if ($aFormProperties === null)
{
$aFormProperties = ApplicationHelper::GetLoadedFormFromClass($this->aCombodoPortalInstanceConf['forms'], $sObjectClass, $sMode);
}
$aFormProperties = $aFormProperties ?? ApplicationHelper::GetLoadedFormFromClass($this->aCombodoPortalInstanceConf['forms'], $sObjectClass, $sMode);
// - Create and
if (empty($sOperation))
{
@@ -243,13 +241,17 @@ class ObjectFormHandlerHelper
case static::ENUM_MODE_CREATE:
case static::ENUM_MODE_EDIT:
case static::ENUM_MODE_VIEW:
$sFormEndpoint = $this->oUrlGenerator->generate(
'p_object_'.$sMode,
array(
'sObjectClass' => $sObjectClass,
'sObjectId' => $sObjectId,
)
);
if(array_key_exists('submit_endpoint', $aFormProperties)) {
$sFormEndpoint = $aFormProperties['submit_endpoint'];
} else {
$sFormEndpoint = $this->oUrlGenerator->generate(
'p_object_' . $sMode,
array(
'sObjectClass' => $sObjectClass,
'sObjectId' => $sObjectId,
)
);
}
break;
case static::ENUM_MODE_APPLY_STIMULUS:
@@ -282,7 +284,8 @@ class ObjectFormHandlerHelper
->SetActionRulesToken($sActionRulesToken)
->SetRenderer($oFormRenderer)
->SetFormProperties($aFormProperties);
$oFormManager->PrepareFormAndHTMLDocument();
$oFormManager->PrepareFields();
$oFormManager->Build();
$aFormData['hidden_fields'] = $oFormManager->GetHiddenFieldsId();
// Check the number of editable fields

View File

@@ -11,6 +11,7 @@
</div>
<div id="export-feedback">
<p id="export-excel-warning" class="alert alert-warning" role="alert">{{ 'UI:Bulk:Export:MaliciousInjection:Alert:Message'|dict_format(sWikiUrl)|raw }}</p>
<p class="export-message" style="text-align:center;">{{ 'ExcelExport:PreparingExport'|dict_s }}</p>
<div class="progress">
<div class="progress-bar" role="progressbar" style="width: 0%"

View File

@@ -3,11 +3,13 @@
{% if app['combodo.current_user'] is defined and app['combodo.current_user'] is not null %}
{% set bUserConnected = true %}
{% set bUserCanLogOff = app['combodo.current_user.can_logoff'] %}
{% set sUserFullname = app['combodo.current_user'].Get('first_name') ~ ' ' ~ app['combodo.current_user'].Get('last_name') %}
{% set sUserEmail = app['combodo.current_user'].Get('email') %}
{% set sUserPhotoUrl = app['combodo.current_contact.photo_url'] %}
{% else %}
{% set bUserConnected = false %}
{% set bUserCanLogOff = false %}
{% set sUserFullname = '' %}
{% set sUserEmail = '' %}
{% set sUserPhotoUrl = app['combodo.portal.base.absolute_url'] ~ 'img/user-profile-default-256px.png' %}
@@ -51,9 +53,9 @@
<link href="{{ app['combodo.absolute_url'] ~ 'css/c3.min.css'|add_itop_version }}" rel="stylesheet">
<link href="{{ app['combodo.absolute_url'] ~ 'js/ckeditor/plugins/codesnippet/lib/highlight/styles/obsidian.css'|add_itop_version }}" rel="stylesheet">
{# - Bootstrap theme #}
<link href="{{ app['combodo.portal.instance.conf'].properties.themes.bootstrap|add_itop_version }}" rel="stylesheet" id="css_bootstrap_theme">
<link href="{{ app['combodo.absolute_url'] ~ app['combodo.portal.instance.conf'].properties.themes.bootstrap|add_itop_version }}" rel="stylesheet" id="css_bootstrap_theme">
{# - Portal adjustments for BS theme #}
<link href="{{ app['combodo.portal.instance.conf'].properties.themes.portal|add_itop_version }}" rel="stylesheet" id="css_portal">
<link href="{{ app['combodo.absolute_url'] ~ app['combodo.portal.instance.conf'].properties.themes.portal|add_itop_version }}" rel="stylesheet" id="css_portal">
{# UI Extensions CSS, in an undefined order #}
{% if app['ui_extensions_helper'].css_files is defined %}
{% for css_file in app['ui_extensions_helper'].css_files %}
@@ -62,12 +64,12 @@
{% endif %}
{# Custom CSS that is supposed to do adjustments to the portal #}
{% if app['combodo.portal.instance.conf'].properties.themes.custom is defined %}
<link href="{{ app['combodo.portal.instance.conf'].properties.themes.custom|add_itop_version }}" rel="stylesheet">
<link href="{{ app['combodo.absolute_url'] ~ app['combodo.portal.instance.conf'].properties.themes.custom|add_itop_version }}" rel="stylesheet">
{% endif %}
{# Others CSS that will come after the theme/portal/custom, in an undefined order #}
{% if app['combodo.portal.instance.conf'].properties.themes.others is defined %}
{% for theme in app['combodo.portal.instance.conf'].properties.themes.others %}
<link href="{{ theme|add_itop_version }}" rel="stylesheet">
<link href="{{ app['combodo.absolute_url'] ~ theme|add_itop_version }}" rel="stylesheet">
{% endfor %}
{% endif %}
{% endblock %}
@@ -230,11 +232,13 @@
<li><a href="{{ aPortal.url }}" target="_blank"><span class="brick_icon {{ sIconClass }} fa-2x fa-fw"></span>{{ aPortal.label|dict_s }}</a></li>
{% endif %}
{% endfor %}
{# We display the separator only if the user has more then 1 portal. Meaning default portal + console or several portal at least #}
{% if app['combodo.current_user.allowed_portals']|length > 1 %}
<li role="separator" class="divider"></li>
{% endif %}
<li><a href="{{ app['combodo.absolute_url'] }}pages/logoff.php"><span class="brick_icon fas fa-sign-out-alt fa-2x fa-fw"></span>{{ 'Brick:Portal:UserProfile:Navigation:Dropdown:Logout'|dict_s }}</a></li>
{% if bUserCanLogOff %}
{# We display the separator only if the user has more then 1 portal. Meaning default portal + console or several portal at least #}
{% if app['combodo.current_user.allowed_portals']|length > 1 %}
<li role="separator" class="divider"></li>
{% endif %}
<li><a href="{{ app['combodo.absolute_url'] }}pages/logoff.php"><span class="brick_icon fas fa-sign-out-alt fa-2x fa-fw"></span>{{ 'Brick:Portal:UserProfile:Navigation:Dropdown:Logout'|dict_s }}</a></li>
{% endif %}
{% endif %}
</ul>
</div>
@@ -269,11 +273,13 @@
<li><a href="{{ aPortal.url }}" {% if app['combodo.portal.instance.conf'].properties.allowed_portals.opening_mode == 'tab' %}target="_blank"{% endif %} title="{{ aPortal.label|dict_s }}"><span class="brick_icon {{ sGlyphiconClass }} fa-lg fa-fw"></span>{{ aPortal.label|dict_s }}</a></li>
{% endif %}
{% endfor %}
{# We display the separator only if the user has more then 1 portal. Meaning default portal + console or several portal at least #}
{% if app['combodo.current_user.allowed_portals']|length > 1 %}
<li role="separator" class="divider"></li>
{% endif %}
<li><a href="{{ app['combodo.absolute_url'] }}pages/logoff.php"><span class="brick_icon fas fa-sign-out-alt fa-lg fa-fw"></span>{{ 'Brick:Portal:UserProfile:Navigation:Dropdown:Logout'|dict_s }}</a></li>
{% if bUserCanLogOff %}
{# We display the separator only if the user has more then 1 portal. Meaning default portal + console or several portal at least #}
{% if app['combodo.current_user.allowed_portals']|length > 1 %}
<li role="separator" class="divider"></li>
{% endif %}
<li><a href="{{ app['combodo.absolute_url'] }}pages/logoff.php"><span class="brick_icon fas fa-sign-out-alt fa-lg fa-fw"></span>{{ 'Brick:Portal:UserProfile:Navigation:Dropdown:Logout'|dict_s }}</a></li>
{% endif %}
</ul>
</div>
</div>
@@ -458,8 +464,8 @@
sBody = '{{ 'Error:XHR:Fail'|dict_format(constant('ITOP_APPLICATION_SHORT'))|escape('js') }}';
}
var oModalElem = $('#modal-for-alert');
oModalElem.find('.modal-content .modal-header .modal-title').html(sTitle);
oModalElem.find('.modal-content .modal-body .alert').addClass('alert-danger').html(sBody);
oModalElem.find('.modal-content .modal-header .modal-title').text(sTitle);
oModalElem.find('.modal-content .modal-body .alert').addClass('alert-danger').text(sBody);
oModalElem.modal('show');
};
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More