From c74d9bbafbafd6570b9f77b830513469bed825a2 Mon Sep 17 00:00:00 2001 From: Molkobain Date: Mon, 3 Sep 2018 15:51:43 +0200 Subject: [PATCH 01/19] Add .gitignore --- .gitignore | 121 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..43cf000c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,121 @@ + +conf/* +env-*/* + +# composer reserver directory, from sources, populate/update using "composer install" +vendor/* + +# all datas but listing prevention +data/* +!data/.htaccess +!data/index.php +!data/web.config + +# iTop extensions +extensions/* +!extensions/readme.txt + +# all logs but listing prevention +log/* +!log/.htaccess +!log/index.php +!log/web.config + + +# Jetbrains +.idea/** +!.idea/encodings.xml +!.idea/codeStyles +!.idea/codeStyles/* +!.idea/inspectionProfiles +!.idea/inspectionProfiles/* + + + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests +### Eclipse template + +.metadata +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders +.project + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + From 9ad8c6a96df92037c6070d0acae30de186f1c507 Mon Sep 17 00:00:00 2001 From: Molkobain Date: Mon, 3 Sep 2018 15:53:00 +0200 Subject: [PATCH 02/19] Add .idea project files --- .idea/codeStyles/Project.xml | 37 +++++ .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/encodings.xml | 6 + .idea/inspectionProfiles/Project_Default.xml | 154 +++++++++++++++++++ 4 files changed, 202 insertions(+) create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 000000000..cbc60d1a0 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,37 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 000000000..79ee123c2 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 000000000..97626ba45 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 000000000..ffbbf4b97 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,154 @@ + + + + \ No newline at end of file From a7000b2582a2f0851cf15aaddb2c5a4e7813d33b Mon Sep 17 00:00:00 2001 From: bruno DA SILVA Date: Fri, 7 Sep 2018 14:52:22 +0200 Subject: [PATCH 03/19] [WIP] jenkinsfile integration cherry picked from commits : 79157c465af1e12aba1e1d926b0c0b865859a78c e473c46dc3a341d7035c3e85be09ce7d3de2ac80 148309245b7e2b0831f0678eeebb6eeee20cdc03 5dc39fe0686a1761501c318e6d25e99e13845372 47c7a3c5e3e275a29137ef81111ff602e7e4c220 85b7e86e58ca39e03fa7bc79296a0e06c615078f 3fca465f1d4785139d519f7f2b4de8ec96feb299 7955dd86f408cf73cd3919013d032c2282f1c1b3 bef4ac83a4038b8a289cfba4d90b0232f6da2a03 bde83fc70530454366482008b03e72453d7fecbd --- .jenkins/bin/archive/gather_external_files.sh | 0 .jenkins/bin/init/append_files.sh | 14 + .jenkins/bin/init/composer_install.sh | 11 + .jenkins/bin/init/debug.sh | 13 + .jenkins/bin/tests/phpunit.sh | 8 + .../bin/unattended_install/default_env.sh | 6 + .../default-config-itop.php | 285 ++++++++++++++++++ .../unattended_install/default-params.xml | 69 +++++ .../unattended_install/unattended_install.php | 190 ++++++++++++ Jenkinsfile | 62 ++++ 10 files changed, 658 insertions(+) create mode 100755 .jenkins/bin/archive/gather_external_files.sh create mode 100755 .jenkins/bin/init/append_files.sh create mode 100755 .jenkins/bin/init/composer_install.sh create mode 100755 .jenkins/bin/init/debug.sh create mode 100755 .jenkins/bin/tests/phpunit.sh create mode 100755 .jenkins/bin/unattended_install/default_env.sh create mode 100644 .jenkins/configuration/default-environment/unattended_install/default-config-itop.php create mode 100644 .jenkins/configuration/default-environment/unattended_install/default-params.xml create mode 100644 .jenkins/configuration/default-environment/unattended_install/unattended_install.php create mode 100644 Jenkinsfile diff --git a/.jenkins/bin/archive/gather_external_files.sh b/.jenkins/bin/archive/gather_external_files.sh new file mode 100755 index 000000000..e69de29bb diff --git a/.jenkins/bin/init/append_files.sh b/.jenkins/bin/init/append_files.sh new file mode 100755 index 000000000..2e383001e --- /dev/null +++ b/.jenkins/bin/init/append_files.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -x + +# create target dirs +mkdir -p var +mkdir -p toolkit + +# cleanup target dirs +rm -rf toolkit/* + +# fill target dirs +curl http://www.combodo.com/documentation/iTopDataModelToolkit-2.3.zip | tar xvz --directory toolkit +cp -r .jenkins/configuration/default-environment/unattended_install/* toolkit diff --git a/.jenkins/bin/init/composer_install.sh b/.jenkins/bin/init/composer_install.sh new file mode 100755 index 000000000..cfea9704f --- /dev/null +++ b/.jenkins/bin/init/composer_install.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -x + +# on the root dir +composer install + + +# under the test dir +cd test +composer install diff --git a/.jenkins/bin/init/debug.sh b/.jenkins/bin/init/debug.sh new file mode 100755 index 000000000..8f9ea404a --- /dev/null +++ b/.jenkins/bin/init/debug.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -x + +whoami +pwd +ls + +echo "$BRANCH_NAME:${BRANCH_NAME}" + +echo "printenv :" +printenv + diff --git a/.jenkins/bin/tests/phpunit.sh b/.jenkins/bin/tests/phpunit.sh new file mode 100755 index 000000000..3c327f6c9 --- /dev/null +++ b/.jenkins/bin/tests/phpunit.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -x + +cd test + +export DEBUG_UNIT_TEST="0" + +php vendor/bin/phpunit --log-junit ../var/test/phpunit-log.junit.xml --teamcity \ No newline at end of file diff --git a/.jenkins/bin/unattended_install/default_env.sh b/.jenkins/bin/unattended_install/default_env.sh new file mode 100755 index 000000000..e4a9e8c98 --- /dev/null +++ b/.jenkins/bin/unattended_install/default_env.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -x + +cd toolkit +php unattended_install.php default-params.xml \ No newline at end of file diff --git a/.jenkins/configuration/default-environment/unattended_install/default-config-itop.php b/.jenkins/configuration/default-environment/unattended_install/default-config-itop.php new file mode 100644 index 000000000..71a82612b --- /dev/null +++ b/.jenkins/configuration/default-environment/unattended_install/default-config-itop.php @@ -0,0 +1,285 @@ + 'iTop is temporarily frozen, please wait... (the admin team)', + + // access_mode: Access mode: ACCESS_READONLY = 0, ACCESS_ADMIN_WRITE = 2, ACCESS_FULL = 3 + // default: 3 + 'access_mode' => 3, + + 'allowed_login_types' => 'form|basic|external', + + // apc_cache.enabled: If set, the APC cache is allowed (the PHP extension must also be active) + // default: true + 'apc_cache.enabled' => true, + + // apc_cache.query_ttl: Time to live set in APC for the prepared queries (seconds - 0 means no timeout) + // default: 3600 + 'apc_cache.query_ttl' => 3600, + + // app_root_url: Root URL used for navigating within the application, or from an email to the application (you can put $SERVER_NAME$ as a placeholder for the server's name) + // default: '' + 'app_root_url' => 'http://127.0.0.1/itop/svn/trunk/', + + // buttons_position: Position of the forms buttons: bottom | top | both + // default: 'both' + 'buttons_position' => 'both', + + // cas_include_path: The path where to find the phpCAS library + // default: '/usr/share/php' + 'cas_include_path' => '/usr/share/php', + + // cron_max_execution_time: Duration (seconds) of the page cron.php, must be shorter than php setting max_execution_time and shorter than the web server response timeout + // default: 600 + 'cron_max_execution_time' => 600, + + // csv_file_default_charset: Character set used by default for downloading and uploading data as a CSV file. Warning: it is case sensitive (uppercase is preferable). + // default: 'ISO-8859-1' + 'csv_file_default_charset' => 'ISO-8859-1', + + 'csv_import_charsets' => array ( + ), + + // csv_import_history_display: Display the history tab in the import wizard + // default: false + 'csv_import_history_display' => false, + + // date_and_time_format: Format for date and time display (per language) + // default: array ( + // 'default' => + // array ( + // 'date' => 'Y-m-d', + // 'time' => 'H:i:s', + // 'date_time' => '$date $time', + // ), + // ) + 'date_and_time_format' => array ( + 'default' => + array ( + 'date' => 'Y-m-d', + 'time' => 'H:i:s', + 'date_time' => '$date $time', + ), + 'FR FR' => + array ( + 'date' => 'd/m/Y', + 'time' => 'H:i:s', + 'date_time' => '$date $time', + ), + ), + + 'db_host' => '', + + 'db_name' => 'itop_ci_main', + + 'db_pwd' => 'c8mb0do', + + 'db_subname' => '', + + 'db_user' => 'root', + + // deadline_format: The format used for displaying "deadline" attributes: any string with the following placeholders: $date$, $difference$ + // default: '$difference$' + 'deadline_format' => '$difference$', + + 'default_language' => 'EN US', + + // disable_attachments_download_legacy_portal: Disable attachments download from legacy portal + // default: true + 'disable_attachments_download_legacy_portal' => true, + + // draft_attachments_lifetime: Lifetime (in seconds) of drafts' attachments and inline images: after this duration, the garbage collector will delete them. + // default: 3600 + 'draft_attachments_lifetime' => 3600, + + // email_asynchronous: If set, the emails are sent off line, which requires cron.php to be activated. Exception: some features like the email test utility will force the serialized mode + // default: false + 'email_asynchronous' => false, + + // email_default_sender_address: Default address provided in the email from header field. + // default: '' + 'email_default_sender_address' => '', + + // email_default_sender_label: Default label provided in the email from header field. + // default: '' + 'email_default_sender_label' => '', + + // email_transport: Mean to send emails: PHPMail (uses the function mail()) or SMTP (implements the client protocole) + // default: 'PHPMail' + 'email_transport' => 'SMTP', + + // email_transport_smtp.host: host name or IP address (optional) + // default: 'localhost' + 'email_transport_smtp.host' => 'smtp.combodo.com', + + // email_transport_smtp.password: Authentication password (optional) + // default: '' + 'email_transport_smtp.password' => '++combodo++', + + // email_transport_smtp.port: port number (optional) + // default: 25 + 'email_transport_smtp.port' => 25, + + // email_transport_smtp.username: Authentication user (optional) + // default: '' + 'email_transport_smtp.username' => 'test2@combodo.com', + + // email_validation_pattern: Regular expression to validate/detect the format of an eMail address + // default: '[a-zA-Z0-9._&\'-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z0-9-]{2,}' + 'email_validation_pattern' => '[a-zA-Z0-9._&\'-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z0-9-]{2,}', + + 'encryption_key' => '@iT0pEncr1pti0n!', + + 'ext_auth_variable' => '$_SERVER[\'REMOTE_USER\']', + + 'fast_reload_interval' => '60', + + // graphviz_path: Path to the Graphviz "dot" executable for graphing objects lifecycle + // default: '/usr/bin/dot' + 'graphviz_path' => '/usr/bin/dot', + + // inline_image_max_display_width: The maximum width (in pixels) when displaying images inside an HTML formatted attribute. Images will be displayed using this this maximum width. + // default: '250' + 'inline_image_max_display_width' => 250, + + // inline_image_max_storage_width: The maximum width (in pixels) when uploading images to be used inside an HTML formatted attribute. Images larger than the given size will be downsampled before storing them in the database. + // default: '1600' + 'inline_image_max_storage_width' => 1600, + + // link_set_attribute_qualifier: Link set from string: attribute qualifier (encloses both the attcode and the value) + // default: '\'' + 'link_set_attribute_qualifier' => '\'', + + // link_set_attribute_separator: Link set from string: attribute separator + // default: ';' + 'link_set_attribute_separator' => ';', + + // link_set_item_separator: Link set from string: line separator + // default: '|' + 'link_set_item_separator' => '|', + + // link_set_value_separator: Link set from string: value separator (between the attcode and the value itself + // default: ':' + 'link_set_value_separator' => ':', + + 'log_global' => true, + + 'log_issue' => true, + + 'log_notification' => true, + + 'log_web_service' => true, + + // max_combo_length: The maximum number of elements in a drop-down list. If more then an autocomplete will be used + // default: 50 + 'max_combo_length' => 50, + + 'max_display_limit' => '15', + + // max_linkset_output: 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. + // default: 100 + 'max_linkset_output' => 100, + + 'min_display_limit' => '10', + + // online_help: Hyperlink to the online-help web page + // default: 'http://www.combodo.com/itop-help' + 'online_help' => 'http://www.combodo.com/itop-help', + + // php_path: Path to the php executable in CLI mode + // default: 'php' + 'php_path' => 'php', + + // portal_tickets: CSV list of classes supported in the portal + // default: 'UserRequest' + 'portal_tickets' => 'UserRequest', + + 'query_cache_enabled' => true, + + // search_manual_submit: Force manual submit of search requests (class => true) + // default: false + 'search_manual_submit' => array ( + 'Person' => true, + ), + + 'secure_connection_required' => false, + + // session_name: The name of the cookie used to store the PHP session id + // default: 'iTop' + 'session_name' => 'iTop', + + // shortcut_actions: Actions that are available as direct buttons next to the "Actions" menu + // default: 'UI:Menu:Modify,UI:Menu:New' + 'shortcut_actions' => 'UI:Menu:Modify,UI:Menu:New', + + // source_dir: Source directory for the datamodel files. (which gets compiled to env-production). + // default: '' + 'source_dir' => 'datamodels/2.x/', + + 'standard_reload_interval' => '300', + + // synchro_trace: Synchronization details: none, display, save (includes 'display') + // default: 'none' + 'synchro_trace' => 'none', + + // timezone: Timezone (reference: http://php.net/manual/en/timezones.php). If empty, it will be left unchanged and MUST be explicitely configured in PHP + // default: 'Europe/Paris' + 'timezone' => 'Europe/Paris', + + // tracking_level_linked_set_default: Default tracking level if not explicitely set at the attribute level, for AttributeLinkedSet (defaults to NONE in case of a fresh install, LIST otherwise - this to preserve backward compatibility while upgrading from a version older than 2.0.3 - see TRAC #936) + // default: 1 + 'tracking_level_linked_set_default' => 0, + + // url_validation_pattern: Regular expression to validate/detect the format of an URL (URL attributes and Wiki formatting for Text attributes) + // default: '(https?|ftp)\\://([a-zA-Z0-9+!*(),;?&=\\$_.-]+(\\:[a-zA-Z0-9+!*(),;?&=\\$_.-]+)?@)?([a-zA-Z0-9-.]{3,})(\\:[0-9]{2,5})?(/([a-zA-Z0-9%+\\$_-]\\.?)+)*/?(\\?[a-zA-Z+&\\$_.-][a-zA-Z0-9;:[\\]@&%=+/\\$_.-]*)?(#[a-zA-Z_.-][a-zA-Z0-9+\\$_.-]*)?' + 'url_validation_pattern' => '(https?|ftp)\\://([a-zA-Z0-9+!*(),;?&=\\$_.-]+(\\:[a-zA-Z0-9+!*(),;?&=\\$_.-]+)?@)?([a-zA-Z0-9-.]{3,})(\\:[0-9]{2,5})?(/([a-zA-Z0-9%+\\$_-]\\.?)+)*/?(\\?[a-zA-Z+&\\$_.-][a-zA-Z0-9;:[\\]@&%=+/\\$_.-]*)?(#[a-zA-Z_.-][a-zA-Z0-9+\\$_.-]*)?', +); + +/** + * + * Modules specific settings + * + */ +$MyModuleSettings = array( + 'itop-attachments' => array ( + 'allowed_classes' => array ( + 0 => 'Ticket', + ), + 'position' => 'relations', + 'preview_max_width' => 290, + ), + 'itop-backup' => array ( + 'mysql_bindir' => '', + 'week_days' => 'monday, tuesday, wednesday, thursday, friday', + 'time' => '23:30', + 'retention_count' => 5, + 'enabled' => true, + 'debug' => false, + ), + 'molkobain-console-tooltips' => array ( + 'decoration_class' => 'fas fa-question', + 'enabled' => true, + ), +); + +/** + * + * Data model modules to be loaded. Names are specified as relative paths + * + */ +$MyModules = array( + 'addons' => array ( + 'user rights' => 'addons/userrights/userrightsprofile.class.inc.php', + ), +); +?> \ No newline at end of file diff --git a/.jenkins/configuration/default-environment/unattended_install/default-params.xml b/.jenkins/configuration/default-environment/unattended_install/default-params.xml new file mode 100644 index 000000000..fc188ba02 --- /dev/null +++ b/.jenkins/configuration/default-environment/unattended_install/default-params.xml @@ -0,0 +1,69 @@ + + + upgrade + + + + datamodels/2.x/ + 2.5.0 + /var/lib/jenkins/workspace/iTop-CI/unattended_install/default-config-itop.php + extensions + production + + + + root + c8mb0do + itop_ci + + + + + http://127.0.0.1/itop/svn/trunk/ + /usr/bin/dot + + admin + admin + EN US + + EN US + + authent-external + authent-local + itop-backup + itop-config + itop-profiles-itil + itop-sla-computation + itop-tickets + itop-welcome-itil + itop-config-mgmt + itop-attachments + itop-datacenter-mgmt + itop-endusers-devices + itop-storage-mgmt + itop-virtualization-mgmt + itop-bridge-virtualization-storage + itop-service-mgmt + itop-request-mgmt + itop-portal + itop-portal-base + itop-change-mgmt + + + itop-config-mgmt-core + itop-config-mgmt-datacenter + itop-config-mgmt-end-user + itop-config-mgmt-storage + itop-config-mgmt-virtualization + itop-service-mgmt-enterprise + itop-ticket-mgmt-simple-ticket + itop-ticket-mgmt-simple-ticket-enhanced-portal + itop-change-mgmt-simple + + 1 + + + 1 + + + \ No newline at end of file diff --git a/.jenkins/configuration/default-environment/unattended_install/unattended_install.php b/.jenkins/configuration/default-environment/unattended_install/unattended_install.php new file mode 100644 index 000000000..7f67bee85 --- /dev/null +++ b/.jenkins/configuration/default-environment/unattended_install/unattended_install.php @@ -0,0 +1,190 @@ +Get('mode'); + +if ($sMode == 'install') +{ + echo "Installation mode detected.\n"; + $bClean = utils::ReadParam('clean', false, true /* CLI allowed */); + if ($bClean) + { + echo "Cleanup mode detected.\n"; + $sTargetEnvironment = $oParams->Get('target_env', ''); + if ($sTargetEnvironment == '') + { + $sTargetEnvironment = 'production'; + } + $sTargetDir = APPROOT.'env-'.$sTargetEnvironment; + + // Configuration file + $sConfigFile = APPCONF.$sTargetEnvironment.'/'.ITOP_CONFIG_FILE; + if (file_exists($sConfigFile)) + { + echo "Trying to delete the configuration file: '$sConfigFile'.\n"; + @chmod($sConfigFile, 0770); // RWX for owner and group, nothing for others + unlink($sConfigFile); + } + else + { + echo "No config file to delete ($sConfigFile does not exist).\n"; + } + + // env-xxx directory + if (file_exists($sTargetDir)) + { + if (is_dir($sTargetDir)) + { + echo "Emptying the target directory '$sTargetDir'.\n"; + SetupUtils::tidydir($sTargetDir); + } + else + { + die("ERROR the target dir '$sTargetDir' exists, but is NOT a directory !!!\nExiting.\n"); + } + } + else + { + echo "No target directory to delete ($sTargetDir does not exist).\n"; + } + + // Database + $aDBSettings = $oParams->Get('database', array()); + $sDBServer = $aDBSettings['server']; + $sDBUser = $aDBSettings['user']; + $sDBPwd = $aDBSettings['pwd']; + $sDBName = $aDBSettings['name']; + $sDBPrefix = $aDBSettings['prefix']; + + if ($sDBPrefix != '') + { + die("Cleanup not implemented for a partial database (prefix= '$sDBPrefix')\nExiting."); + } + + $oMysqli = new mysqli($sDBServer, $sDBUser, $sDBPwd); + if ($oMysqli->connect_errno) + { + die("Cannot connect to the MySQL server (".$mysqli->connect_errno . ") ".$mysqli->connect_error."\nExiting"); + } + else + { + if ($oMysqli->select_db($sDBName)) + { + echo "Deleting database '$sDBName'\n"; + $oMysqli->query("DROP DATABASE `$sDBName`"); + } + else + { + echo "The database '$sDBName' does not seem to exist. Nothing to cleanup.\n"; + } + } + } +} + +$bHasErrors = false; +$aChecks = SetupUtils::CheckBackupPrerequisites(APPROOT.'data'); // mmm should be the backup destination dir + +$aSelectedModules = $oParams->Get('selected_modules'); +$sSourceDir = $oParams->Get('source_dir', 'datamodels/latest'); +$sExtensionDir = $oParams->Get('extensions_dir', 'extensions'); +$aChecks = array_merge($aChecks, SetupUtils::CheckSelectedModules($sSourceDir, $sExtensionDir, $aSelectedModules)); + + +foreach($aChecks as $oCheckResult) +{ + switch($oCheckResult->iSeverity) + { + case CheckResult::ERROR: + $bHasErrors = true; + $sHeader = "Error"; + break; + + case CheckResult::WARNING: + $sHeader = "Warning"; + break; + + case CheckResult::INFO: + default: + $sHeader = "Info"; + break; + } + echo $sHeader.": ".$oCheckResult->sLabel; + if (strlen($oCheckResult->sDescription)) + { + echo ' - '.$oCheckResult->sDescription; + } + echo "\n"; +} + +if ($bHasErrors) +{ + echo "Encountered stopper issues. Aborting...\n"; + die; +} + +$bFoundIssues = false; + +$bInstall = utils::ReadParam('install', true, true /* CLI allowed */); +if ($bInstall) +{ + echo "Starting the unattended installation...\n"; + $oWizard = new ApplicationInstaller($oParams); + $bRes = $oWizard->ExecuteAllSteps(); + if (!$bRes) + { + echo "\nencountered installation issues!"; + $bFoundIssues = true; + } +} +else +{ + echo "No installation requested.\n"; +} +if (!$bFoundIssues && $bCheckConsistency) +{ + echo "Checking data model consistency.\n"; + ob_start(); + $sCheckRes = ''; + try + { + MetaModel::CheckDefinitions(false); + $sCheckRes = ob_get_clean(); + } + catch(Exception $e) + { + $sCheckRes = ob_get_clean()."\nException: ".$e->getMessage(); + } + if (strlen($sCheckRes) > 0) + { + echo $sCheckRes; + echo "\nfound consistency issues!"; + $bFoundIssues = true; + } +} + +if (!$bFoundIssues) +{ + // last line: used to check the install + // the only way to track issues in case of Fatal error or even parsing error! + echo "\ninstalled!"; + exit; +} diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 000000000..2f49a0667 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,62 @@ +pipeline { + agent any + stages { + + stage('init') { + parallel { + stage('debug') { + steps { + sh './.jenkins/bin/init/debug.sh' + } + } + stage('append files to project') { + steps { + sh './.jenkins/bin/init/append_files.sh' + } + } + stage('composer install') { + steps { + sh './.jenkins/bin/init/composer_install.sh' + } + } + } + } + + stage('unattended_install') { + parallel { + stage('unattended_install default env') { + steps { + sh './.jenkins/bin/unattended_install/default_env.sh' + } + } + } + } + + stage('test') { + parallel { + stage('phpunit') { + steps { + sh './.jenkins/bin/tests/phpunit.sh' + } + } + } + } + + } + + post { + always { + junit 'var/test/phpunit-log.junit.xml' + } + failure { + slackSend(channel: "#jenkins-itop", color: '#FF0000', message: "Ho no! Build failed! (${currentBuild.result}), Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})") + } + } + + environment { + DEBUG_UNIT_TEST = '0' + } + options { + timeout(time: 20, unit: 'MINUTES') + } +} \ No newline at end of file From 866dfe453121a3ea9fbe459638253d09ef9419ad Mon Sep 17 00:00:00 2001 From: Guillaume Lajarige Date: Tue, 21 Aug 2018 12:29:46 +0200 Subject: [PATCH 04/19] =?UTF-8?q?(Retrofit=20from=20develop=2007056613)=20?= =?UTF-8?q?N=C2=B01611=20Fix=20"UTF-8=20Characters=20Malformed"=20error=20?= =?UTF-8?q?due=20to=20wrong=20file=20encoding.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../es_cr.dict.authent-external.php | 94 +++++++++--------- .../authent-ldap/es_cr.dict.authent-ldap.php | 95 +++++++++---------- .../es_cr.dict.authent-local.php | 94 +++++++++--------- .../es_cr.dict.itop-attachments.php | 88 ++++++++--------- 4 files changed, 183 insertions(+), 188 deletions(-) diff --git a/datamodels/2.x/authent-external/es_cr.dict.authent-external.php b/datamodels/2.x/authent-external/es_cr.dict.authent-external.php index 935e8e7c2..757617528 100644 --- a/datamodels/2.x/authent-external/es_cr.dict.authent-external.php +++ b/datamodels/2.x/authent-external/es_cr.dict.authent-external.php @@ -1,49 +1,45 @@ - - - -/** - * Localized data - * - * @copyright Copyright (C) 2010-2013 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - * @traductor Miguel Turrubiates - */ - -// Dictionnay conventions -// Class: -// Class:+ -// Class:/Attribute: -// Class:/Attribute:+ -// Class:/Attribute:/Value: -// Class:/Attribute:/Value:+ -// Class:/Stimulus: -// Class:/Stimulus:+ - -// -// Class: UserExternal -// - -Dict::Add('ES CR', 'Spanish', 'Español, Castellaño', array( - 'Class:UserExternal' => 'Usuario Externo', - 'Class:UserExternal+' => 'Usuario Autenticado fuera de iTop', -)); - - - -?> + + + +/** + * Localized data + * + * @copyright Copyright (C) 2010-2013 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + * @traductor Miguel Turrubiates + */ + +// Dictionnay conventions +// Class: +// Class:+ +// Class:/Attribute: +// Class:/Attribute:+ +// Class:/Attribute:/Value: +// Class:/Attribute:/Value:+ +// Class:/Stimulus: +// Class:/Stimulus:+ + +// +// Class: UserExternal +// + +Dict::Add('ES CR', 'Spanish', 'Español, Castellaño', array( + 'Class:UserExternal' => 'Usuario Externo', + 'Class:UserExternal+' => 'Usuario Autenticado fuera de iTop', +)); diff --git a/datamodels/2.x/authent-ldap/es_cr.dict.authent-ldap.php b/datamodels/2.x/authent-ldap/es_cr.dict.authent-ldap.php index a23ba8fc3..5d1395a43 100644 --- a/datamodels/2.x/authent-ldap/es_cr.dict.authent-ldap.php +++ b/datamodels/2.x/authent-ldap/es_cr.dict.authent-ldap.php @@ -1,48 +1,47 @@ - - - -/** - * Localized data - * - * @copyright Copyright (C) 2010-2013 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - * @traductor Miguel Turrubiates - */ - -// Dictionnay conventions -// Class: -// Class:+ -// Class:/Attribute: -// Class:/Attribute:+ -// Class:/Attribute:/Value: -// Class:/Attribute:/Value:+ -// Class:/Stimulus: -// Class:/Stimulus:+ - -// -// Class: UserLDAP -// - -Dict::Add('ES CR', 'Spanish', 'Español, Castellano', array( - 'Class:UserLDAP' => 'Usuario LDAP', - 'Class:UserLDAP+' => 'Usuario Autenticado vía LDAP', - 'Class:UserLDAP/Attribute:password' => 'Contraseña', - 'Class:UserLDAP/Attribute:password+' => 'Contraseña', -)); - + + + +/** + * Localized data + * + * @copyright Copyright (C) 2010-2013 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + * @traductor Miguel Turrubiates + */ + +// Dictionnay conventions +// Class: +// Class:+ +// Class:/Attribute: +// Class:/Attribute:+ +// Class:/Attribute:/Value: +// Class:/Attribute:/Value:+ +// Class:/Stimulus: +// Class:/Stimulus:+ + +// +// Class: UserLDAP +// + +Dict::Add('ES CR', 'Spanish', 'Español, Castellano', array( + 'Class:UserLDAP' => 'Usuario LDAP', + 'Class:UserLDAP+' => 'Usuario Autenticado vía LDAP', + 'Class:UserLDAP/Attribute:password' => 'Contraseña', + 'Class:UserLDAP/Attribute:password+' => 'Contraseña', +)); diff --git a/datamodels/2.x/authent-local/es_cr.dict.authent-local.php b/datamodels/2.x/authent-local/es_cr.dict.authent-local.php index c9a048ac0..81af35001 100644 --- a/datamodels/2.x/authent-local/es_cr.dict.authent-local.php +++ b/datamodels/2.x/authent-local/es_cr.dict.authent-local.php @@ -1,47 +1,47 @@ - - - -/** - * Localized data - * - * @copyright Copyright (C) 2010-2013 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - * @traductor Miguel Turrubiates - */ - -// Dictionnay conventions -// Class: -// Class:+ -// Class:/Attribute: -// Class:/Attribute:+ -// Class:/Attribute:/Value: -// Class:/Attribute:/Value:+ -// Class:/Stimulus: -// Class:/Stimulus:+ - -// -// Class: UserLocal -// - -Dict::Add('ES CR', 'Spanish', 'Español, Castellano', array( - 'Class:UserLocal' => 'Usuario de iTop', - 'Class:UserLocal+' => 'Usuario Autenticado vía iTop', - 'Class:UserLocal/Attribute:password' => 'Contraseña', - 'Class:UserLocal/Attribute:password+' => 'Contraseña', -)); + + + +/** + * Localized data + * + * @copyright Copyright (C) 2010-2013 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + * @traductor Miguel Turrubiates + */ + +// Dictionnay conventions +// Class: +// Class:+ +// Class:/Attribute: +// Class:/Attribute:+ +// Class:/Attribute:/Value: +// Class:/Attribute:/Value:+ +// Class:/Stimulus: +// Class:/Stimulus:+ + +// +// Class: UserLocal +// + +Dict::Add('ES CR', 'Spanish', 'Español, Castellano', array( + 'Class:UserLocal' => 'Usuario de iTop', + 'Class:UserLocal+' => 'Usuario Autenticado vía iTop', + 'Class:UserLocal/Attribute:password' => 'Contraseña', + 'Class:UserLocal/Attribute:password+' => 'Contraseña', +)); diff --git a/datamodels/2.x/itop-attachments/es_cr.dict.itop-attachments.php b/datamodels/2.x/itop-attachments/es_cr.dict.itop-attachments.php index 4351dbae3..8483a197c 100755 --- a/datamodels/2.x/itop-attachments/es_cr.dict.itop-attachments.php +++ b/datamodels/2.x/itop-attachments/es_cr.dict.itop-attachments.php @@ -1,44 +1,44 @@ - - - -/** - * Localized data - * - * @copyright Copyright (C) 2010-2013 Combodo SARL - * @license http://opensource.org/licenses/AGPL-3.0 - * @traductor Miguel Turrubiates - */ - -Dict::Add('ES CR', 'Spanish', 'Español, Castellano', array( - 'Attachments:TabTitle_Count' => 'Anexos (%1$d)', - 'Attachments:EmptyTabTitle' => 'Anexos', - 'Attachments:FieldsetTitle' => 'Anexos', - 'Attachments:DeleteBtn' => 'Borrar', - 'Attachments:History_File_Added' => 'Anexo %1$s agregado.', - 'Attachments:History_File_Removed' => 'Anexo %1$s removido.', - 'Attachments:AddAttachment' => 'Agregar Anexo: ', - 'Attachments:UploadNotAllowedOnThisSystem' => 'La carga de archivos NO está permitida en este sistema.', - 'Attachment:Max_Go' => '(Tamaño Máximo de Archivo: %1$s Gb)', - 'Attachment:Max_Mo' => '(Tamaño Máximo de Archivo: %1$s Mb)', - 'Attachment:Max_Ko' => '(Tamaño Máximo de Archivo: %1$s Kb)', - 'Attachments:NoAttachment' => 'No hay Anexo. ', - 'Class:Attachment' => 'Anexo', - 'Class:Attachment+' => 'Anexo', - 'Attachments:PreviewNotAvailable' => 'Vista preliminar no disponible para este tipo de Anexo.', - )); + + + +/** + * Localized data + * + * @copyright Copyright (C) 2010-2013 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + * @traductor Miguel Turrubiates + */ + +Dict::Add('ES CR', 'Spanish', 'Español, Castellano', array( + 'Attachments:TabTitle_Count' => 'Anexos (%1$d)', + 'Attachments:EmptyTabTitle' => 'Anexos', + 'Attachments:FieldsetTitle' => 'Anexos', + 'Attachments:DeleteBtn' => 'Borrar', + 'Attachments:History_File_Added' => 'Anexo %1$s agregado.', + 'Attachments:History_File_Removed' => 'Anexo %1$s removido.', + 'Attachments:AddAttachment' => 'Agregar Anexo: ', + 'Attachments:UploadNotAllowedOnThisSystem' => 'La carga de archivos NO está permitida en este sistema.', + 'Attachment:Max_Go' => '(Tamaño Máximo de Archivo: %1$s Gb)', + 'Attachment:Max_Mo' => '(Tamaño Máximo de Archivo: %1$s Mb)', + 'Attachment:Max_Ko' => '(Tamaño Máximo de Archivo: %1$s Kb)', + 'Attachments:NoAttachment' => 'No hay Anexo. ', + 'Class:Attachment' => 'Anexo', + 'Class:Attachment+' => 'Anexo', + 'Attachments:PreviewNotAvailable' => 'Vista preliminar no disponible para este tipo de Anexo.', + )); From 1973f7526e39d90f031174696725b3b222398a44 Mon Sep 17 00:00:00 2001 From: Molkobain Date: Tue, 2 Oct 2018 10:48:32 +0200 Subject: [PATCH 05/19] Update .gitignore to match the one from develop --- .gitignore | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 43cf000c4..aec7aa429 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ -conf/* -env-*/* +/toolkit/ +/conf/* +/env-*/* # composer reserver directory, from sources, populate/update using "composer install" vendor/* +test/vendor/* # all datas but listing prevention data/* @@ -118,4 +120,3 @@ local.properties .cache-main .scala_dependencies .worksheet - From 1cf36a5d0105cbcce4eba9bfba345b2bc1374462 Mon Sep 17 00:00:00 2001 From: Guillaume Lajarige Date: Thu, 26 Jul 2018 16:22:15 +0200 Subject: [PATCH 06/19] =?UTF-8?q?(Retrofit=20from=20develop=205e1452f9)=20?= =?UTF-8?q?N=C2=B01580=20Portal:=20Default=20image=20of=20image=20attribut?= =?UTF-8?q?es=20not=20correctly=20displayed=20in=20object=20forms.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/attributedef.class.inc.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/core/attributedef.class.inc.php b/core/attributedef.class.inc.php index b4ac8205b..732faf2d1 100644 --- a/core/attributedef.class.inc.php +++ b/core/attributedef.class.inc.php @@ -5835,6 +5835,32 @@ class AttributeImage extends AttributeBlob { return '\\Combodo\\iTop\\Form\\Field\\ImageField'; } + + public function MakeFormField(DBObject $oObject, $oFormField = null) + { + if ($oFormField === null) + { + $sFormFieldClass = static::GetFormFieldClass(); + $oFormField = new $sFormFieldClass($this->GetCode()); + } + + parent::MakeFormField($oObject, $oFormField); + + // Generating urls + $value = $oObject->Get($this->GetCode()); + if (is_object($value) && !$value->IsEmpty()) + { + $oFormField->SetDownloadUrl($value->GetDownloadURL(get_class($oObject), $oObject->GetKey(), $this->GetCode())); + $oFormField->SetDisplayUrl($value->GetDisplayURL(get_class($oObject), $oObject->GetKey(), $this->GetCode())); + } + else + { + $oFormField->SetDownloadUrl($this->Get('default_image')); + $oFormField->SetDisplayUrl($this->Get('default_image')); + } + + return $oFormField; + } } /** * A stop watch is an ormStopWatch object, it is stored as several columns in the database From 2966efcfde7c848d3d59a2424c413af6d26f8fc6 Mon Sep 17 00:00:00 2001 From: Guillaume Lajarige Date: Wed, 25 Jul 2018 15:06:38 +0200 Subject: [PATCH 07/19] =?UTF-8?q?(Retrofit=20from=20develop=20af43e22f)=20?= =?UTF-8?q?N=C2=B01578=20Fix=20"Run=20Query"=20page=20hotkeys=20behavior?= =?UTF-8?q?=20in=20some=20configurations=20due=20to=20a=20wrong=20url.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/run_query.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/run_query.php b/pages/run_query.php index c871d236e..22e919d93 100644 --- a/pages/run_query.php +++ b/pages/run_query.php @@ -170,7 +170,7 @@ try $oP->add("
\n"); $oP->add(Dict::S('UI:RunQuery:ExpressionToEvaluate')."
\n"); $oP->add("\n"); - $oP->add_linked_script(utils::GetDefaultUrlAppRoot()."/js/jquery.hotkeys.js"); + $oP->add_linked_script(utils::GetAbsoluteUrlAppRoot()."/js/jquery.hotkeys.js"); $oP->add_ready_script(<< Date: Wed, 18 Jul 2018 16:40:12 +0200 Subject: [PATCH 08/19] =?UTF-8?q?(Retrofit=20from=20develop=203d1ccf20)=20?= =?UTF-8?q?N=C2=B01566=20Fix=20security=20message=20in=20the=20browser=20c?= =?UTF-8?q?onsole=20("Unsafe=20attempt=20to=20load=20URL=20data:image/svg+?= =?UTF-8?q?xml;utf8")?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- css/light-grey.css | 1 - css/light-grey.scss | 1 - 2 files changed, 2 deletions(-) diff --git a/css/light-grey.css b/css/light-grey.css index fe25acdee..062946973 100644 --- a/css/light-grey.css +++ b/css/light-grey.css @@ -2588,7 +2588,6 @@ span.search-button, span.refresh-button { -webkit-filter: grayscale(100%); filter: grayscale(100%); filter: gray; - filter: url("data:image/svg+xml;utf8,#greyscale"); opacity: 0.5; } #itop-breadcrumb .breadcrumb-item a { diff --git a/css/light-grey.scss b/css/light-grey.scss index 195cfd4a0..4c83fa55e 100644 --- a/css/light-grey.scss +++ b/css/light-grey.scss @@ -2934,7 +2934,6 @@ span.search-button, span.refresh-button { -webkit-filter: grayscale(100%); filter: grayscale(100%); filter: gray; - filter: url("data:image/svg+xml;utf8,#greyscale"); // IE has no filter option: at least, have some effect when hovering... opacity: 0.5; From 02f83c4f52b4c6d15669546e0f69c1498024e2da Mon Sep 17 00:00:00 2001 From: Stephen Abello Date: Tue, 14 Aug 2018 15:19:36 +0200 Subject: [PATCH 09/19] =?UTF-8?q?(Retrofit=20from=20develop=209db47428)=20?= =?UTF-8?q?N=C2=B01559:=20Fixed=20external=20attributes=20selection=20on?= =?UTF-8?q?=20export=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/tabularfieldsselector.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/js/tabularfieldsselector.js b/js/tabularfieldsselector.js index 7d6e4a332..0b59da7c2 100644 --- a/js/tabularfieldsselector.js +++ b/js/tabularfieldsselector.js @@ -428,11 +428,7 @@ $(function() event.stopImmediatePropagation(); var jMe = $(this); $(this).data('openTimeoutId', setTimeout(function() { - var sDataId = jMe.attr('data-attcode'); - if ($('.tooltip-close-button[data-attcode="'+sDataId+'"]').length == 0) - { - jMe.tooltip('open'); - } + jMe.tooltip('open'); }, 500)); }) .on( "mouseout", function(event){ From fcb6a4069a3864b68c38b0dbc05e61cfec311e9b Mon Sep 17 00:00:00 2001 From: Guillaume Lajarige Date: Tue, 24 Jul 2018 16:26:51 +0200 Subject: [PATCH 10/19] =?UTF-8?q?(Retrofit=20from=20develop=20526d4c4d)=20?= =?UTF-8?q?N=C2=B01575=20Portal:=20Security=20hardening.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../browsebrickcontroller.class.inc.php | 18 +-- .../managebrickcontroller.class.inc.php | 72 ++++----- .../objectcontroller.class.inc.php | 141 +++++++++--------- .../userprofilebrickcontroller.class.inc.php | 23 ++- .../requestmanipulatorhelper.class.inc.php | 119 +++++++++++++++ ...stmanipulatorserviceprovider.class.inc.php | 60 ++++++++ .../2.x/itop-portal-base/portal/web/index.php | 5 + 7 files changed, 315 insertions(+), 123 deletions(-) create mode 100644 datamodels/2.x/itop-portal-base/portal/src/helpers/requestmanipulatorhelper.class.inc.php create mode 100644 datamodels/2.x/itop-portal-base/portal/src/providers/requestmanipulatorserviceprovider.class.inc.php diff --git a/datamodels/2.x/itop-portal-base/portal/src/controllers/browsebrickcontroller.class.inc.php b/datamodels/2.x/itop-portal-base/portal/src/controllers/browsebrickcontroller.class.inc.php index 37a8cb920..54d0755f0 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/controllers/browsebrickcontroller.class.inc.php +++ b/datamodels/2.x/itop-portal-base/portal/src/controllers/browsebrickcontroller.class.inc.php @@ -51,10 +51,10 @@ class BrowseBrickController extends BrickController // Getting current browse mode (First from router pamater, then default brick value) $sBrowseMode = (!empty($sBrowseMode)) ? $sBrowseMode : $oBrick->GetDefaultBrowseMode(); // Getting current dataloading mode (First from router parameter, then query parameter, then default brick value) - $sDataLoading = ($sDataLoading !== null) ? $sDataLoading : ( ($oRequest->query->get('sDataLoading') !== null) ? $oRequest->query->get('sDataLoading') : $oBrick->GetDataLoading() ); + $sDataLoading = ($sDataLoading !== null) ? $sDataLoading : $oApp['request_manipulator']->ReadParam('sDataLoading', $oBrick->GetDataLoading()); // Getting search value - $sSearchValue = $oRequest->get('sSearchValue', null); - if ($sSearchValue !== null) + $sSearchValue = $oApp['request_manipulator']->ReadParam('sSearchValue', ''); + if (!empty($sSearchValue)) { $sDataLoading = AbstractBrick::ENUM_DATA_LOADING_LAZY; } @@ -109,7 +109,7 @@ class BrowseBrickController extends BrickController // Adding search clause // Note : For know the search is naive and looks only for the exact match. It doesn't search for words separately - if ($sSearchValue !== null) + if (!empty($sSearchValue)) { // - Cleaning the search value by exploding and trimming spaces $aSearchValues = explode(' ', $sSearchValue); @@ -182,7 +182,7 @@ class BrowseBrickController extends BrickController { $aLevelsProperties[$aLevelsPropertiesKeys[$i]]['search']->SetSelectedClasses($aLevelsClasses); - if ($sSearchValue !== null) + if (!empty($sSearchValue)) { // Note : This could be way more simpler if we had a SetInternalParam($sParam, $value) verb $aQueryParams = $aLevelsProperties[$aLevelsPropertiesKeys[$i]]['search']->GetInternalParams(); @@ -216,8 +216,8 @@ class BrowseBrickController extends BrickController { case BrowseBrick::ENUM_BROWSE_MODE_LIST: // Retrieving parameters - $iPageNumber = (int) $oRequest->get('iPageNumber', 1); - $iListLength = (int) $oRequest->get('iListLength', BrowseBrick::DEFAULT_LIST_LENGTH); + $iPageNumber = (int) $oApp['request_manipulator']->ReadParam('iPageNumber', 1, FILTER_SANITIZE_NUMBER_INT); + $iListLength = (int) $oApp['request_manipulator']->ReadParam('iListLength', BrowseBrick::DEFAULT_LIST_LENGTH, FILTER_SANITIZE_NUMBER_INT); // Getting total records number $oCountSet = new DBObjectSet($oQuery); @@ -232,8 +232,8 @@ class BrowseBrickController extends BrickController case BrowseBrick::ENUM_BROWSE_MODE_TREE: case BrowseBrick::ENUM_BROWSE_MODE_MOSAIC: // Retrieving parameters - $sLevelAlias = $oRequest->get('sLevelAlias'); - $sNodeId = $oRequest->get('sNodeId'); + $sLevelAlias = $oApp['request_manipulator']->ReadParam('sLevelAlias', ''); + $sNodeId = $oApp['request_manipulator']->ReadParam('sNodeId', ''); // If no values for those parameters, we might be loading page in lazy mode for the first time, therefore the URL doesn't have those informations. if (empty($sLevelAlias)) diff --git a/datamodels/2.x/itop-portal-base/portal/src/controllers/managebrickcontroller.class.inc.php b/datamodels/2.x/itop-portal-base/portal/src/controllers/managebrickcontroller.class.inc.php index cfa5ec300..ac094151e 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/controllers/managebrickcontroller.class.inc.php +++ b/datamodels/2.x/itop-portal-base/portal/src/controllers/managebrickcontroller.class.inc.php @@ -50,22 +50,22 @@ class ManageBrickController extends BrickController { const EXCEL_EXPORT_TEMPLATE_PATH = 'itop-portal-base/portal/src/views/bricks/manage/popup-export-excel.html.twig'; - /** - * @param \Symfony\Component\HttpFoundation\Request $oRequest - * @param \Silex\Application $oApp - * @param string $sBrickId - * @param string $sDisplayMode - * @param string $sGroupingTab - * @param string $sDataLoading - * - * @return \Symfony\Component\HttpFoundation\Response - * @throws \CoreException - * @throws \DictExceptionMissingString - * @throws \MissingQueryArgument - * @throws \MySQLException - * @throws \OQLException - */ - public function DisplayAction(Request $oRequest, Application $oApp, $sBrickId, $sGroupingTab, $sDisplayMode = null, $sDataLoading = null) + /** + * @param Request $oRequest + * @param Application $oApp + * @param string $sBrickId + * @param string $sGroupingTab + * @param string $sDisplayMode + * + * @return Response + * + * @throws \Exception + * @throws \CoreException + * @throws \DictExceptionMissingString + * @throws \MySQLException + * @throws \OQLException + */ + public function DisplayAction(Request $oRequest, Application $oApp, $sBrickId, $sGroupingTab, $sDisplayMode = null) { /** @var ManageBrick $oBrick */ $oBrick = ApplicationHelper::GetLoadedBrickFromId($oApp, $sBrickId); @@ -160,7 +160,7 @@ class ManageBrickController extends BrickController $oScopeHelper = $oApp['scope_validator']; $oScopeHelper->AddScopeToQuery($oQuery, $sClass); $aData = array(); - $this->ManageSearchValue($oRequest, $aData, $oQuery, $sClass); + $this->ManageSearchValue($oApp, $aData, $oQuery, $sClass); // Grouping tab if ($oBrick->HasGroupingTabs()) @@ -256,11 +256,11 @@ class ManageBrickController extends BrickController $bHasScope = true; // Getting current dataloading mode (First from router parameter, then query parameter, then default brick value) - $sDataLoading = ($oRequest->get('sDataLoading') !== null) ? $oRequest->get('sDataLoading') : $oBrick->GetDataLoading(); + $sDataLoading = $oApp['request_manipulator']->ReadParam('sDataLoading', $oBrick->GetDataLoading()); // - Retrieving the grouping areas to display - $sGroupingArea = $oRequest->get('sGroupingArea'); - if (!is_null($sGroupingArea)) + $sGroupingArea = $oApp['request_manipulator']->ReadParam('sGroupingArea', ''); + if (!empty($sGroupingArea)) { $bNeedDetails = true; } @@ -340,7 +340,7 @@ class ManageBrickController extends BrickController } // - Retrieving the current grouping tab to display if necessary and altering the query to do so - if ($sGroupingTab === null) + if (empty($sGroupingTab)) { if ($oBrick->HasGroupingTabs()) { @@ -361,7 +361,7 @@ class ManageBrickController extends BrickController } // - Adding search clause if necessary - $this->ManageSearchValue($oRequest, $aData, $oQuery, $sClass, $aColumnsAttrs); + $this->ManageSearchValue($oApp, $aData, $oQuery, $sClass, $aColumnsAttrs); // Preparing areas // - We need to retrieve distinct values for the grouping attribute @@ -411,7 +411,7 @@ class ManageBrickController extends BrickController } // - If specified or lazy loading, we truncate the $aGroupingAreasValues to keep only this one - if ($sGroupingArea !== null) + if (!empty($sGroupingArea)) { $aGroupingAreasValues = array($sGroupingArea => $aGroupingAreasValues[$sGroupingArea]); } @@ -467,8 +467,8 @@ class ManageBrickController extends BrickController if ($sDataLoading === AbstractBrick::ENUM_DATA_LOADING_LAZY) { // Retrieving parameters - $iPageNumber = (int)$oRequest->get('iPageNumber', 1); - $iListLength = (int)$oRequest->get('iListLength', ManageBrick::DEFAULT_LIST_LENGTH); + $iPageNumber = (int)$oApp['request_manipulator']->ReadParam('iPageNumber', 1, FILTER_SANITIZE_NUMBER_INT); + $iListLength = (int)$oApp['request_manipulator']->ReadParam('iListLength', ManageBrick::DEFAULT_LIST_LENGTH, FILTER_SANITIZE_NUMBER_INT); // Getting total records number $oCountSet = new DBObjectSet($oQuery); @@ -736,20 +736,24 @@ class ManageBrickController extends BrickController return $aData; } - /** - * @param Request $oRequest - * @param array $aData - * @param DBSearch $oQuery - * @param string $sClass - */ - protected function ManageSearchValue(Request $oRequest, &$aData, DBSearch &$oQuery, $sClass, $aColumnsAttrs) + /** + * @param Application $oApp + * @param array $aData + * @param DBSearch $oQuery + * @param string $sClass + * @param array $aColumnsAttrs + * + * @throws \Exception + * @throws \CoreException + */ + protected function ManageSearchValue(Application $oApp, &$aData, DBSearch &$oQuery, $sClass, $aColumnsAttrs) { // Getting search value - $sSearchValue = $oRequest->get('sSearchValue', null); + $sSearchValue = $oApp['request_manipulator']->ReadParam('sSearchValue', ''); // - Adding search clause if necessary // Note : This is a very naive search at the moment - if ($sSearchValue !== null) + if (!empty($sSearchValue)) { // Putting only valid attributes as one can define attributes of leaf classes in the brick definition (), but at this stage we are working on the abstract class. // Note: This won't fix everything as the search will not be looking in all fields. diff --git a/datamodels/2.x/itop-portal-base/portal/src/controllers/objectcontroller.class.inc.php b/datamodels/2.x/itop-portal-base/portal/src/controllers/objectcontroller.class.inc.php index 51064b543..1d1a07d2d 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/controllers/objectcontroller.class.inc.php +++ b/datamodels/2.x/itop-portal-base/portal/src/controllers/objectcontroller.class.inc.php @@ -61,6 +61,8 @@ class ObjectController extends AbstractController const ENUM_MODE_VIEW = 'view'; const ENUM_MODE_EDIT = 'edit'; const ENUM_MODE_CREATE = 'create'; + + const DEFAULT_PAGE_NUMBER = 1; const DEFAULT_LIST_LENGTH = 10; /** @@ -97,6 +99,8 @@ class ObjectController extends AbstractController $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); } + $sOperation = $oApp['request_manipulator']->ReadParam('operation', ''); + $aData = array('sMode' => 'view'); $aData['form'] = $this->HandleForm($oRequest, $oApp, $aData['sMode'], $sObjectClass, $sObjectId); $aData['form']['title'] = Dict::Format('Brick:Portal:Object:Form:View:Title', MetaModel::GetName($sObjectClass), $oObject->GetName()); @@ -117,7 +121,7 @@ class ObjectController extends AbstractController if ($oRequest->isXmlHttpRequest()) { // We have to check whether the 'operation' parameter is defined or not in order to know if the form is required via ajax (to be displayed as a modal dialog) or if it's a lifecycle call from a existing form. - if ($oRequest->request->get('operation') === null) + if (empty($sOperation)) { $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData); } @@ -129,8 +133,8 @@ class ObjectController extends AbstractController else { // Adding brick if it was passed - $sBrickId = $oRequest->get('sBrickId'); - if ($sBrickId !== null) + $sBrickId = $oApp['request_manipulator']->ReadParam('sBrickId', ''); + if (!empty($sBrickId)) { $oBrick = ApplicationHelper::GetLoadedBrickFromId($oApp, $sBrickId); if ($oBrick !== null) @@ -172,6 +176,8 @@ class ObjectController extends AbstractController $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); } + $sOperation = $oApp['request_manipulator']->ReadParam('operation', ''); + $aData = array('sMode' => 'edit'); $aData['form'] = $this->HandleForm($oRequest, $oApp, $aData['sMode'], $sObjectClass, $sObjectId); $aData['form']['title'] = Dict::Format('Brick:Portal:Object:Form:Edit:Title', MetaModel::GetName($sObjectClass), $aData['form']['object_name']); @@ -180,7 +186,7 @@ class ObjectController extends AbstractController if ($oRequest->isXmlHttpRequest()) { // We have to check whether the 'operation' parameter is defined or not in order to know if the form is required via ajax (to be displayed as a modal dialog) or if it's a lifecycle call from a existing form. - if ($oRequest->request->get('operation') === null) + if (empty($sOperation)) { $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData); } @@ -192,8 +198,8 @@ class ObjectController extends AbstractController else { // Adding brick if it was passed - $sBrickId = $oRequest->get('sBrickId'); - if ($sBrickId !== null) + $sBrickId = $oApp['request_manipulator']->ReadParam('sBrickId', ''); + if (!empty($sBrickId)) { $oBrick = ApplicationHelper::GetLoadedBrickFromId($oApp, $sBrickId); if ($oBrick !== null) @@ -225,6 +231,8 @@ class ObjectController extends AbstractController $oApp->abort(404, Dict::S('UI:ObjectDoesNotExist')); } + $sOperation = $oApp['request_manipulator']->ReadParam('operation', ''); + $aData = array('sMode' => 'create'); $aData['form'] = $this->HandleForm($oRequest, $oApp, $aData['sMode'], $sObjectClass); $aData['form']['title'] = Dict::Format('Brick:Portal:Object:Form:Create:Title', MetaModel::GetName($sObjectClass)); @@ -233,7 +241,7 @@ class ObjectController extends AbstractController if ($oRequest->isXmlHttpRequest()) { // We have to check whether the 'operation' parameter is defined or not in order to know if the form is required via ajax (to be displayed as a modal dialog) or if it's a lifecycle call from a existing form. - if ($oRequest->request->get('operation') === null) + if (empty($sOperation)) { $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData); } @@ -245,8 +253,8 @@ class ObjectController extends AbstractController else { // Adding brick if it was passed - $sBrickId = $oRequest->get('sBrickId'); - if ($sBrickId !== null) + $sBrickId = $oApp['request_manipulator']->ReadParam('sBrickId', ''); + if (!empty($sBrickId)) { $oBrick = ApplicationHelper::GetLoadedBrickFromId($oApp, $sBrickId); if ($oBrick !== null) @@ -347,7 +355,7 @@ class ObjectController extends AbstractController } // Retrieving request parameters - $sOperation = $oRequest->request->get('operation'); + $sOperation = $oApp['request_manipulator']->ReadParam('operation', ''); // Retrieving form properties $aStimuliForms = ApplicationHelper::GetLoadedFormFromClass($oApp, $sObjectClass, 'apply_stimulus'); @@ -382,7 +390,7 @@ class ObjectController extends AbstractController // TODO : This is a ugly patch to avoid showing a modal with a readonly form to the user as it would prevent user from finishing the transition. // Instead, we apply the stimulus directly here and then go to the edited object. - if ($sOperation === null) + if (empty($sOperation)) { if (isset($aData['form']['editable_fields_count']) && $aData['form']['editable_fields_count'] === 0) { @@ -390,7 +398,7 @@ class ObjectController extends AbstractController $oSubRequest = $oRequest; $oSubRequest->request->set('operation', 'submit'); - $oSubRequest->request->set('stimulus_code', null); + $oSubRequest->request->set('stimulus_code', ''); $aData = array('sMode' => 'apply_stimulus'); $aData['form'] = $this->HandleForm($oSubRequest, $oApp, $aData['sMode'], $sObjectClass, $sObjectId, $aFormProperties); @@ -405,7 +413,7 @@ class ObjectController extends AbstractController if ($oRequest->isXmlHttpRequest()) { // We have to check whether the 'operation' parameter is defined or not in order to know if the form is required via ajax (to be displayed as a modal dialog) or if it's a lifecycle call from a existing form. - if ($sOperation === null) + if (empty($sOperation)) { $oResponse = $oApp['twig']->render('itop-portal-base/portal/src/views/bricks/object/modal.html.twig', $aData); } @@ -429,9 +437,8 @@ class ObjectController extends AbstractController public static function HandleForm(Request $oRequest, Application $oApp, $sMode, $sObjectClass, $sObjectId = null, $aFormProperties = null) { $aFormData = array(); - $oRequestParams = $oRequest->request; - $sOperation = $oRequestParams->get('operation'); - $bModal = ($oRequest->isXmlHttpRequest() && ($oRequest->request->get('operation') === null) ); + $sOperation = $oApp['request_manipulator']->ReadParam('operation', ''); + $bModal = ($oRequest->isXmlHttpRequest() && empty($sOperation)); // - Retrieve form properties if ($aFormProperties === null) @@ -440,14 +447,14 @@ class ObjectController extends AbstractController } // - Create and - if ($sOperation === null) + if (empty($sOperation)) { // Retrieving action rules // // Note : The action rules must be a base64-encoded JSON object, this is just so users are tempted to changes values. // But it would not be a security issue as it only presets values in the form. - $sActionRulesToken = $oRequest->get('ar_token'); - $aActionRules = ($sActionRulesToken !== null) ? ContextManipulatorHelper::DecodeRulesToken($sActionRulesToken) : array(); + $sActionRulesToken = $oApp['request_manipulator']->ReadParam('ar_token', ''); + $aActionRules = (!empty($sActionRulesToken)) ? ContextManipulatorHelper::DecodeRulesToken($sActionRulesToken) : array(); // Preparing object if ($sObjectId === null) @@ -520,9 +527,11 @@ class ObjectController extends AbstractController } else { - $aPrefillFormParam = array('user' => $_SESSION["auth_user"], + $aPrefillFormParam = array( + 'user' => $_SESSION["auth_user"], 'origin' => 'portal', - 'stimulus' => $oRequestParams->get('apply_stimulus')['code']); + 'stimulus' => $oApp['request_manipulator']->ReadParam('apply_stimulus', null)['code'], + ); $oObject->PrefillForm('state_change', $aPrefillFormParam); } @@ -560,9 +569,9 @@ class ObjectController extends AbstractController else { // Update / Submit / Cancel - $sFormManagerClass = $oRequestParams->get('formmanager_class'); - $sFormManagerData = $oRequestParams->get('formmanager_data'); - if ($sFormManagerClass === null || $sFormManagerData === null) + $sFormManagerClass = $oApp['request_manipulator']->ReadParam('formmanager_class', '', FILTER_UNSAFE_RAW); + $sFormManagerData = $oApp['request_manipulator']->ReadParam('formmanager_data', '', FILTER_UNSAFE_RAW); + if ( empty($sFormManagerClass) || empty($sFormManagerData) ) { IssueLog::Error(__METHOD__ . ' at line ' . __LINE__ . ' : Parameters formmanager_class and formamanager_data must be defined.'); $oApp->abort(500, 'Parameters formmanager_class and formmanager_data must be defined.'); @@ -584,13 +593,13 @@ class ObjectController extends AbstractController { case 'submit': // Applying modification to object - $aFormData['validation'] = $oFormManager->OnSubmit(array('currentValues' => $oRequestParams->get('current_values'), 'attachmentIds' => $oRequest->get('attachment_ids'), 'formProperties' => $aFormProperties, 'applyStimulus' => $oRequestParams->get('apply_stimulus'))); + $aFormData['validation'] = $oFormManager->OnSubmit(array('currentValues' => $oApp['request_manipulator']->ReadParam('current_values', array(), FILTER_UNSAFE_RAW), 'attachmentIds' => $oApp['request_manipulator']->ReadParam('attachment_ids', array(), FILTER_UNSAFE_RAW), 'formProperties' => $aFormProperties, 'applyStimulus' => $oApp['request_manipulator']->ReadParam('apply_stimulus', null))); if ($aFormData['validation']['valid'] === true) { // Note : We don't use $sObjectId there as it can be null if we are creating a new one. Instead we use the id from the created object once it has been seralized // Check if stimulus has to be applied - $sStimulusCode = ($oRequestParams->get('stimulus_code') !== null && $oRequestParams->get('stimulus_code') !== '') ? $oRequestParams->get('stimulus_code') : null; - if ($sStimulusCode !== null) + $sStimulusCode = $oApp['request_manipulator']->ReadParam('stimulus_code', ''); + if (!empty($sStimulusCode)) { $aFormData['validation']['redirection'] = array( 'url' => $oApp['url_generator']->generate('p_object_apply_stimulus', array('sObjectClass' => $sObjectClass, 'sObjectId' => $oFormManager->GetObject()->GetKey(), 'sStimulusCode' => $sStimulusCode)), @@ -598,17 +607,17 @@ class ObjectController extends AbstractController ); } // Otherwise, we show the object if there is no default - else - { +// else +// { // $aFormData['validation']['redirection'] = array( // 'alternative_url' => $oApp['url_generator']->generate('p_object_edit', array('sObjectClass' => $sObjectClass, 'sObjectId' => $oFormManager->GetObject()->GetKey())) // ); - } +// } } break; case 'update': - $oFormManager->OnUpdate(array('currentValues' => $oRequestParams->get('current_values'), 'formProperties' => $aFormProperties)); + $oFormManager->OnUpdate(array('currentValues' => $oApp['request_manipulator']->ReadParam('current_values', array(), FILTER_UNSAFE_RAW), 'formProperties' => $aFormProperties)); break; case 'cancel': @@ -627,11 +636,11 @@ class ObjectController extends AbstractController // Preparing fields list regarding the operation if ($sOperation === 'update') { - $aRequestedFields = $oRequestParams->get('requested_fields'); - $sFormPath = $oRequestParams->get('form_path'); + $aRequestedFields = $oApp['request_manipulator']->ReadParam('requested_fields', array(), FILTER_UNSAFE_RAW); + $sFormPath = $oApp['request_manipulator']->ReadParam('form_path', ''); // Checking if the update was on a subform, if so we need to make the rendering for that part only - if ($sFormPath !== null && $sFormPath !== $oFormManager->GetForm()->GetId()) + if ( !empty($sFormPath) && $sFormPath !== $oFormManager->GetForm()->GetId() ) { $oSubForm = $oFormManager->GetForm()->FindSubForm($sFormPath); $oSubFormRenderer = new BsFormRenderer($oSubForm); @@ -716,8 +725,8 @@ class ObjectController extends AbstractController // // Note : The action rules must be a base64-encoded JSON object, this is just so users are tempted to changes values. // But it would not be a security issue as it only presets values in the form. - $sActionRulesToken = $oRequest->get('ar_token'); - $aActionRules = ($sActionRulesToken !== null) ? ContextManipulatorHelper::DecodeRulesToken($sActionRulesToken) : array(); + $sActionRulesToken = $oApp['request_manipulator']->ReadParam('ar_token', ''); + $aActionRules = (!empty($sActionRulesToken)) ? ContextManipulatorHelper::DecodeRulesToken($sActionRulesToken) : array(); // Preparing object $oApp['context_manipulator']->PrepareObject($aActionRules, $oHostObject); } @@ -725,7 +734,7 @@ class ObjectController extends AbstractController // Updating host object with form data / values $sFormManagerClass = $aRequestContent['formmanager_class']; $sFormManagerData = $aRequestContent['formmanager_data']; - if ($sFormManagerClass !== null && $sFormManagerData !== null) + if (!empty($sFormManagerClass) && !empty($sFormManagerData)) { $oFormManager = $sFormManagerClass::FromJSON($sFormManagerData); $oFormManager->SetApplication($oApp); @@ -837,7 +846,7 @@ class ObjectController extends AbstractController 'sTargetAttCode' => $sTargetAttCode, 'sHostObjectClass' => $sHostObjectClass, 'sHostObjectId' => $sHostObjectId, - 'sActionRulesToken' => $oRequest->get('ar_token') + 'sActionRulesToken' => $oApp['request_manipulator']->ReadParam('ar_token', ''), ); // Checking security layers @@ -860,16 +869,15 @@ class ObjectController extends AbstractController // // Note : The action rules must be a base64-encoded JSON object, this is just so users are tempted to changes values. // But it would not be a security issue as it only presets values in the form. - $aActionRules = ($aData['sActionRulesToken'] !== null) ? ContextManipulatorHelper::DecodeRulesToken($aData['sActionRulesToken']) : array(); + $aActionRules = !empty($aData['sActionRulesToken']) ? ContextManipulatorHelper::DecodeRulesToken($aData['sActionRulesToken']) : array(); // Preparing object $oApp['context_manipulator']->PrepareObject($aActionRules, $oHostObject); } // Updating host object with form data / values - $oRequestParams = $oRequest->request; - $sFormManagerClass = $oRequestParams->get('formmanager_class'); - $sFormManagerData = $oRequestParams->get('formmanager_data'); - if ($sFormManagerClass !== null && $sFormManagerData !== null) + $sFormManagerClass = $oApp['request_manipulator']->ReadParam('formmanager_class', '', FILTER_UNSAFE_RAW); + $sFormManagerData = $oApp['request_manipulator']->ReadParam('formmanager_data', '', FILTER_UNSAFE_RAW); + if ( !empty($sFormManagerClass) && !empty($sFormManagerData) ) { $oFormManager = $sFormManagerClass::FromJSON($sFormManagerData); $oFormManager->SetApplication($oApp); @@ -885,18 +893,18 @@ class ObjectController extends AbstractController } // Updating host object - $oFormManager->OnUpdate(array('currentValues' => $oRequestParams->get('current_values'))); + $oFormManager->OnUpdate(array('currentValues' => $oApp['request_manipulator']->ReadParam('current_values', array(), FILTER_UNSAFE_RAW))); $oHostObject = $oFormManager->GetObject(); } // Retrieving request parameters - $iPageNumber = ($oRequest->get('iPageNumber') !== null) ? $oRequest->get('iPageNumber') : 1; - $iListLength = ($oRequest->get('iListLength') !== null) ? $oRequest->get('iListLength') : static::DEFAULT_LIST_LENGTH; - $bInitalPass = ($oRequest->get('draw') === null) ? true : false; - $sQuery = $oRequest->get('sSearchValue'); - $sFormPath = $oRequest->get('sFormPath'); - $sFieldId = $oRequest->get('sFieldId'); - $aObjectIdsToIgnore = $oRequest->get('aObjectIdsToIgnore'); + $iPageNumber = $oApp['request_manipulator']->ReadParam('iPageNumber', static::DEFAULT_PAGE_NUMBER, FILTER_SANITIZE_NUMBER_INT); + $iListLength = $oApp['request_manipulator']->ReadParam('iListLength', static::DEFAULT_LIST_LENGTH, FILTER_SANITIZE_NUMBER_INT); + $bInitalPass = $oApp['request_manipulator']->HasParam('draw') ? false : true; + $sQuery = $oApp['request_manipulator']->ReadParam('sSearchValue', ''); + $sFormPath = $oApp['request_manipulator']->ReadParam('sFormPath', ''); + $sFieldId = $oApp['request_manipulator']->ReadParam('sFieldId', ''); + $aObjectIdsToIgnore = $oApp['request_manipulator']->ReadParam('aObjectIdsToIgnore', null, FILTER_UNSAFE_RAW); // Building search query // - Retrieving target object class from attcode @@ -977,7 +985,7 @@ class ObjectController extends AbstractController // - Adding query condition $aInternalParams['this'] = $oHostObject; - if ($sQuery !== null) + if (!empty($sQuery)) { $oFullExpr = null; for ($i = 0; $i < count($aAttCodes); $i++) @@ -1352,9 +1360,9 @@ class ObjectController extends AbstractController } // Retrieving ormDocument's host object - $sObjectClass = $oRequest->get('sObjectClass'); - $sObjectId = $oRequest->get('sObjectId'); - $sObjectField = $oRequest->get('sObjectField'); + $sObjectClass = $oApp['request_manipulator']->ReadParam('sObjectClass', ''); + $sObjectId = $oApp['request_manipulator']->ReadParam('sObjectId', ''); + $sObjectField = $oApp['request_manipulator']->ReadParam('sObjectField', ''); // When reaching to an Attachment, we have to check security on its host object instead of the Attachment itself if($sObjectClass === 'Attachment') @@ -1394,8 +1402,7 @@ class ObjectController extends AbstractController } else { - $sCache = $oRequest->get('cache'); - $iCacheSec = ($sCache !== null) ? (int) $sCache : 0; + $iCacheSec = $oApp['request_manipulator']->ReadParam('cache', 0, FILTER_SANITIZE_NUMBER_INT); } $aHeaders = array(); @@ -1436,16 +1443,16 @@ class ObjectController extends AbstractController // Retrieving sOperation from request only if it wasn't forced (determined by the route) if ($sOperation === null) { - $sOperation = $oRequest->get('operation'); + $sOperation = $oApp['request_manipulator']->ReadParam('operation', null); } switch ($sOperation) { case 'add': - $sFieldName = $oRequest->get('field_name'); - $sObjectClass = $oRequest->get('object_class'); - $sTempId = $oRequest->get('temp_id'); + $sFieldName = $oApp['request_manipulator']->ReadParam('field_name', ''); + $sObjectClass = $oApp['request_manipulator']->ReadParam('object_class', ''); + $sTempId = $oApp['request_manipulator']->ReadParam('temp_id', ''); - if (($sObjectClass === null) || ($sTempId === null)) + if (empty($sObjectClass) || empty($sTempId)) { $aData['error'] = Dict::Format('UI:Error:2ParametersMissing', 'object_class', 'temp_id'); } @@ -1484,7 +1491,7 @@ class ObjectController extends AbstractController // - Route $aRouteParams = array( 'sObjectClass' => 'Attachment', - 'sObjectId' => $oRequest->get('sAttachmentId'), + 'sObjectId' => $oApp['request_manipulator']->ReadParam('sAttachmentId', null), 'sObjectField' => 'contents', ); $sRedirectRoute = $oApp['url_generator']->generate('p_object_document_download', $aRouteParams); @@ -1519,10 +1526,10 @@ class ObjectController extends AbstractController $aData = array(); // Retrieving parameters - $sObjectClass = $oRequest->Get('sObjectClass'); - $aObjectIds = $oRequest->Get('aObjectIds'); - $aObjectAttCodes = $oRequest->Get('aObjectAttCodes'); - if ($sObjectClass === null || $aObjectIds === null || $aObjectAttCodes === null) + $sObjectClass = $oApp['request_manipulator']->ReadParam('sObjectClass', ''); + $aObjectIds = $oApp['request_manipulator']->ReadParam('aObjectIds', array(), FILTER_UNSAFE_RAW); + $aObjectAttCodes = $oApp['request_manipulator']->ReadParam('aObjectAttCodes', array(), FILTER_UNSAFE_RAW); + if ( empty($sObjectClass) || empty($aObjectIds) || empty($aObjectAttCodes) ) { IssueLog::Info(__METHOD__ . ' at line ' . __LINE__ . ' : sObjectClass, sObjectId and aObjectAttCodes expected, "' . $sObjectClass . '", "' . $sObjectId . '" given.'); $oApp->abort(500, 'Invalid request data, some informations are missing'); diff --git a/datamodels/2.x/itop-portal-base/portal/src/controllers/userprofilebrickcontroller.class.inc.php b/datamodels/2.x/itop-portal-base/portal/src/controllers/userprofilebrickcontroller.class.inc.php index cda957650..10660eae3 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/controllers/userprofilebrickcontroller.class.inc.php +++ b/datamodels/2.x/itop-portal-base/portal/src/controllers/userprofilebrickcontroller.class.inc.php @@ -71,7 +71,7 @@ class UserProfileBrickController extends BrickController // If this is ajax call, we are just submiting preferences or password forms if ($oRequest->isXmlHttpRequest()) { - $aCurrentValues = $oRequest->request->get('current_values'); + $aCurrentValues = $oApp['request_manipulator']->ReadParam('current_values', array(), FILTER_UNSAFE_RAW); $sFormType = $aCurrentValues['form_type']; if ($sFormType === PreferencesFormManager::FORM_TYPE) { @@ -120,10 +120,9 @@ class UserProfileBrickController extends BrickController public function HandlePreferencesForm(Request $oRequest, Application $oApp, $sFormMode) { $aFormData = array(); - $oRequestParams = $oRequest->request; // Handling form - $sOperation = $oRequestParams->get('operation'); + $sOperation = $oApp['request_manipulator']->ReadParam('operation', null); // - Create if ($sOperation === null) { @@ -143,8 +142,8 @@ class UserProfileBrickController extends BrickController // - Submit else if ($sOperation === 'submit') { - $sFormManagerClass = $oRequestParams->get('formmanager_class'); - $sFormManagerData = $oRequestParams->get('formmanager_data'); + $sFormManagerClass = $oApp['request_manipulator']->ReadParam('formmanager_class', null, FILTER_UNSAFE_RAW); + $sFormManagerData = $oApp['request_manipulator']->ReadParam('formmanager_data', null, FILTER_UNSAFE_RAW); if ($sFormManagerClass === null || $sFormManagerData === null) { IssueLog::Error(__METHOD__ . ' at line ' . __LINE__ . ' : Parameters formmanager_class and formamanager_data must be defined.'); @@ -154,7 +153,7 @@ class UserProfileBrickController extends BrickController // Rebuilding manager from json $oFormManager = $sFormManagerClass::FromJSON($sFormManagerData); // Applying modification to object - $aFormData['validation'] = $oFormManager->OnSubmit(array('currentValues' => $oRequestParams->get('current_values'))); + $aFormData['validation'] = $oFormManager->OnSubmit(array('currentValues' => $oApp['request_manipulator']->ReadParam('current_values', array(), FILTER_UNSAFE_RAW))); // Reloading page only if preferences were changed if (($aFormData['validation']['valid'] === true) && !empty($aFormData['validation']['messages']['success'])) { @@ -188,10 +187,9 @@ class UserProfileBrickController extends BrickController public function HandlePasswordForm(Request $oRequest, Application $oApp) { $aFormData = array(); - $oRequestParams = $oRequest->request; // Handling form - $sOperation = $oRequestParams->get('operation'); + $sOperation = $oApp['request_manipulator']->ReadParam('operation', null); // - Create if ($sOperation === null) { @@ -206,8 +204,8 @@ class UserProfileBrickController extends BrickController // - Submit else if ($sOperation === 'submit') { - $sFormManagerClass = $oRequestParams->get('formmanager_class'); - $sFormManagerData = $oRequestParams->get('formmanager_data'); + $sFormManagerClass = $oApp['request_manipulator']->ReadParam('formmanager_class', null, FILTER_UNSAFE_RAW); + $sFormManagerData = $oApp['request_manipulator']->ReadParam('formmanager_data', null, FILTER_UNSAFE_RAW); if ($sFormManagerClass === null || $sFormManagerData === null) { IssueLog::Error(__METHOD__ . ' at line ' . __LINE__ . ' : Parameters formmanager_class and formamanager_data must be defined.'); @@ -217,7 +215,7 @@ class UserProfileBrickController extends BrickController // Rebuilding manager from json $oFormManager = $sFormManagerClass::FromJSON($sFormManagerData); // Applying modification to object - $aFormData['validation'] = $oFormManager->OnSubmit(array('currentValues' => $oRequestParams->get('current_values'))); + $aFormData['validation'] = $oFormManager->OnSubmit(array('currentValues' => $oApp['request_manipulator']->ReadParam('current_values', array(), FILTER_UNSAFE_RAW))); } else { @@ -244,11 +242,10 @@ class UserProfileBrickController extends BrickController public function HandlePictureForm(Request $oRequest, Application $oApp, $sFormMode) { $aFormData = array(); - $oRequestParams = $oRequest->request; $sPictureAttCode = 'picture'; // Handling form - $sOperation = $oRequestParams->get('operation'); + $sOperation = $oApp['request_manipulator']->ReadParam('operation', null); // - No operation specified if ($sOperation === null) { diff --git a/datamodels/2.x/itop-portal-base/portal/src/helpers/requestmanipulatorhelper.class.inc.php b/datamodels/2.x/itop-portal-base/portal/src/helpers/requestmanipulatorhelper.class.inc.php new file mode 100644 index 000000000..ffac1c943 --- /dev/null +++ b/datamodels/2.x/itop-portal-base/portal/src/helpers/requestmanipulatorhelper.class.inc.php @@ -0,0 +1,119 @@ + + */ + +namespace Combodo\iTop\Portal\Helper; + +use Symfony\Component\HttpFoundation\RequestStack; + +/** + * RequestManipulatorHelper class + * + * Handle basic requests manipulation. + * + * @author Guillaume Lajarige + * @since 2.5.1 + */ +class RequestManipulatorHelper +{ + /** @var \Symfony\Component\HttpFoundation\RequestStack $oRequestStack */ + protected $oRequestStack; + + /** + * RequestManipulatorHelper constructor. + * + * @param \Symfony\Component\HttpFoundation\RequestStack $oRequestStack + */ + public function __construct(RequestStack &$oRequestStack) + { + $this->oRequestStack = $oRequestStack; + } + + /** + * @return \Symfony\Component\HttpFoundation\Request + */ + public function GetCurrentRequest() + { + return $this->oRequestStack->getCurrentRequest(); + } + + /** + * Returns if the request has a $sKey parameter. + * This looks in the GET arguments first, then PATH and finally the POST data. + * + * @param string $sKey + * + * @return bool + */ + public function HasParam($sKey) + { + if ($this->GetCurrentRequest()->query->has($sKey)) + { + return true; + } + + if ($this->GetCurrentRequest()->attributes->has($sKey)) + { + return true; + } + + if ($this->GetCurrentRequest()->request->has($sKey)) + { + return true; + } + + return false; + } + + /** + * Returns the $sKey parameter from the request filtered with $iFilter. + * This looks in the GET arguments first, then the PATH and finally the POST data. + * + * Note: It is inspired by the \Symfony\Component\HttpFoundation\ParameterBag::filter() function and was necessary as we sometimes have parameters that can be either in the GET/PATH/POST arguments and need to be filtered. Silex only offer the possibility to filter parameter from a single ParameterBag, so we created this helper. + * + * @param string $sKey + * @param mixed $default + * @param int $iFilter Default is FILTER_SANITIZE_STRING + * + * @return mixed|null + * + * @since 2.5.1 + */ + public function ReadParam($sKey, $default = null, $iFilter = FILTER_SANITIZE_STRING) + { + if ($this->GetCurrentRequest()->query->has($sKey)) + { + return $this->GetCurrentRequest()->query->filter($sKey, $default, $iFilter); + } + + if ($this->GetCurrentRequest()->attributes->has($sKey)) + { + return $this->GetCurrentRequest()->attributes->filter($sKey, $default, $iFilter); + } + + if ($this->GetCurrentRequest()->request->has($sKey)) + { + return $this->GetCurrentRequest()->request->filter($sKey, $default, $iFilter); + } + + return $default; + } + +} diff --git a/datamodels/2.x/itop-portal-base/portal/src/providers/requestmanipulatorserviceprovider.class.inc.php b/datamodels/2.x/itop-portal-base/portal/src/providers/requestmanipulatorserviceprovider.class.inc.php new file mode 100644 index 000000000..cfbd1982f --- /dev/null +++ b/datamodels/2.x/itop-portal-base/portal/src/providers/requestmanipulatorserviceprovider.class.inc.php @@ -0,0 +1,60 @@ + + */ + +namespace Combodo\iTop\Portal\Provider; + +use Pimple\Container; +use Pimple\ServiceProviderInterface; +use Combodo\iTop\Portal\Helper\RequestManipulatorHelper; + +/** + * RequestManipulatorHelper service provider + * + * @author Guillaume Lajarige + * @since 2.5.1 + */ +class RequestManipulatorServiceProvider implements ServiceProviderInterface +{ + + /** + * @param \Pimple\Container $oApp + */ + public function register(Container $oApp) + { + $oApp['request_manipulator'] = function ($oApp) + { + $oApp->flush(); + + $oRequestManipulatorHelper = new RequestManipulatorHelper($oApp['request_stack']); + + return $oRequestManipulatorHelper; + }; + } + + /** + * @param \Pimple\Container $oApp + */ + public function boot(Container $oApp) + { + + } + +} diff --git a/datamodels/2.x/itop-portal-base/portal/web/index.php b/datamodels/2.x/itop-portal-base/portal/web/index.php index f084b8c5f..c635eadf5 100644 --- a/datamodels/2.x/itop-portal-base/portal/web/index.php +++ b/datamodels/2.x/itop-portal-base/portal/web/index.php @@ -36,6 +36,8 @@ require_once __DIR__ . '/../src/providers/urlgeneratorserviceprovider.class.inc. require_once __DIR__ . '/../src/helpers/urlgeneratorhelper.class.inc.php'; require_once __DIR__ . '/../src/providers/contextmanipulatorserviceprovider.class.inc.php'; require_once __DIR__ . '/../src/helpers/contextmanipulatorhelper.class.inc.php'; +require_once __DIR__ . '/../src/providers/requestmanipulatorserviceprovider.class.inc.php'; +require_once __DIR__ . '/../src/helpers/requestmanipulatorhelper.class.inc.php'; require_once __DIR__ . '/../src/providers/scopevalidatorserviceprovider.class.inc.php'; require_once __DIR__ . '/../src/helpers/scopevalidatorhelper.class.inc.php'; require_once __DIR__ . '/../src/providers/lifecyclevalidatorserviceprovider.class.inc.php'; @@ -99,6 +101,9 @@ $oApp->before(function(Symfony\Component\HttpFoundation\Request $oRequest, Silex die(Dict::S('Portal:ErrorNoContactForThisUser')); } + // Register request manipulator now that the request has been created. + $oApp->register(new Combodo\iTop\Portal\Provider\RequestManipulatorServiceProvider()); + // Enable archived data utils::InitArchiveMode(); From e3e416b46744b2ee82537c2cb61e83d1532ec4ab Mon Sep 17 00:00:00 2001 From: Guillaume Lajarige Date: Wed, 25 Jul 2018 16:48:11 +0200 Subject: [PATCH 11/19] =?UTF-8?q?(Retrofit=20from=20deveop=20ab1715ed)=20N?= =?UTF-8?q?=C2=B01576=20Portal:=20Security=20hardening.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../browsebrickcontroller.class.inc.php | 12 +++++++----- .../controllers/objectcontroller.class.inc.php | 18 ++++++++++++++++-- sources/form/field/textareafield.class.inc.php | 4 +++- .../bslinkedsetfieldrenderer.class.inc.php | 3 ++- 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/datamodels/2.x/itop-portal-base/portal/src/controllers/browsebrickcontroller.class.inc.php b/datamodels/2.x/itop-portal-base/portal/src/controllers/browsebrickcontroller.class.inc.php index 54d0755f0..2b9179b2f 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/controllers/browsebrickcontroller.class.inc.php +++ b/datamodels/2.x/itop-portal-base/portal/src/controllers/browsebrickcontroller.class.inc.php @@ -631,8 +631,9 @@ class BrowseBrickController extends BrickController if ($aLevelsProperties[$key][$sOptionalAttribute] !== null) { $sPropertyName = substr($sOptionalAttribute, 0, -4); + $oAttDef = MetaModel::GetAttributeDef(get_class($value), $aLevelsProperties[$key][$sOptionalAttribute]); - $tmpAttValue = $value->Get($aLevelsProperties[$key][$sOptionalAttribute]); + $tmpAttValue = $value->GetAsHTML($aLevelsProperties[$key][$sOptionalAttribute]); if($sOptionalAttribute === 'image_att') { if (is_object($tmpAttValue) && !$tmpAttValue->IsEmpty()) @@ -641,7 +642,7 @@ class BrowseBrickController extends BrickController } else { - $tmpAttValue = MetaModel::GetAttributeDef(get_class($value), $aLevelsProperties[$key][$sOptionalAttribute])->Get('default_image'); + $tmpAttValue = $oAttDef->Get('default_image'); } } @@ -655,7 +656,7 @@ class BrowseBrickController extends BrickController foreach ($aLevelsProperties[$key]['fields'] as $aField) { $oAttDef = MetaModel::GetAttributeDef(get_class($value), $aField['code']); - $aRow[$key]['fields'][$aField['code']] = $oAttDef->GetValueLabel($value->Get($aField['code'])); + $aRow[$key]['fields'][$aField['code']] = $oAttDef->GetAsHTML($value->Get($aField['code'])); } } } @@ -723,8 +724,9 @@ class BrowseBrickController extends BrickController if ($aLevelsProperties[$aCurrentRowKeys[0]][$sOptionalAttribute] !== null) { $sPropertyName = substr($sOptionalAttribute, 0, -4); + $oAttDef = MetaModel::GetAttributeDef(get_class($aCurrentRowValues[0]), $aLevelsProperties[$aCurrentRowKeys[0]][$sOptionalAttribute]); - $tmpAttValue = $aCurrentRowValues[0]->Get($aLevelsProperties[$aCurrentRowKeys[0]][$sOptionalAttribute]); + $tmpAttValue = $aCurrentRowValues[0]->GetAsHTML($aLevelsProperties[$aCurrentRowKeys[0]][$sOptionalAttribute]); if($sOptionalAttribute === 'image_att') { if (is_object($tmpAttValue) && !$tmpAttValue->IsEmpty()) @@ -733,7 +735,7 @@ class BrowseBrickController extends BrickController } else { - $tmpAttValue = MetaModel::GetAttributeDef(get_class($aCurrentRowValues[0]), $aLevelsProperties[$aCurrentRowKeys[0]][$sOptionalAttribute])->Get('default_image'); + $tmpAttValue = $oAttDef->Get('default_image'); } } diff --git a/datamodels/2.x/itop-portal-base/portal/src/controllers/objectcontroller.class.inc.php b/datamodels/2.x/itop-portal-base/portal/src/controllers/objectcontroller.class.inc.php index 1d1a07d2d..a15796e05 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/controllers/objectcontroller.class.inc.php +++ b/datamodels/2.x/itop-portal-base/portal/src/controllers/objectcontroller.class.inc.php @@ -40,6 +40,7 @@ use ListExpression; use ScalarExpression; use DBObjectSet; use AttributeEnum; +use AttributeImage; use AttributeFinalClass; use AttributeFriendlyName; use UserRights; @@ -1597,7 +1598,7 @@ class ObjectController extends AbstractController if ($oAttDef->IsExternalKey()) { - $aAttData['value'] = $oObject->Get($oAttDef->GetCode() . '_friendlyname'); + $aAttData['value'] = $oObject->GetAsHTML($oAttDef->GetCode() . '_friendlyname'); // Checking if user can access object's external key if (SecurityHelper::IsActionAllowed($oApp, UR_ACTION_READ, $oAttDef->GetTargetClass())) @@ -1610,9 +1611,22 @@ class ObjectController extends AbstractController // We skip it continue; } + elseif ($oAttDef instanceof AttributeImage) + { + $oOrmDoc = $oObject->Get($oAttDef->GetCode()); + if (is_object($oOrmDoc) && !$oOrmDoc->IsEmpty()) + { + $sUrl = $oApp['url_generator']->generate('p_object_document_display', array('sObjectClass' => get_class($oObject), 'sObjectId' => $oObject->GetKey(), 'sObjectField' => $oAttDef->GetCode(), 'cache' => 86400)); + } + else + { + $sUrl = $oAttDef->Get('default_image'); + } + $aAttData['value'] = ''; + } else { - $aAttData['value'] = $oAttDef->GetValueLabel($oObject->Get($oAttDef->GetCode())); + $aAttData['value'] = $oAttDef->GetAsHTML($oObject->Get($oAttDef->GetCode())); if ($oAttDef instanceof AttributeFriendlyName) { diff --git a/sources/form/field/textareafield.class.inc.php b/sources/form/field/textareafield.class.inc.php index 9820d0e47..5e380c430 100644 --- a/sources/form/field/textareafield.class.inc.php +++ b/sources/form/field/textareafield.class.inc.php @@ -29,6 +29,8 @@ use \Combodo\iTop\Form\Field\TextField; * Description of TextAreaField * * @author Guillaume Lajarige + * @package \Combodo\iTop\Form\Field + * @since 2.3.0 */ class TextAreaField extends TextField { @@ -113,7 +115,7 @@ class TextAreaField extends TextField { if ($this->GetFormat() == TextAreaField::ENUM_FORMAT_TEXT) { - $sValue = $this->GetCurrentValue(); + $sValue = \Str::pure2html($this->GetCurrentValue()); $sValue = AttributeText::RenderWikiHtml($sValue); return "
".str_replace("\n", "
\n", $sValue).'
'; } diff --git a/sources/renderer/bootstrap/fieldrenderer/bslinkedsetfieldrenderer.class.inc.php b/sources/renderer/bootstrap/fieldrenderer/bslinkedsetfieldrenderer.class.inc.php index 3a0803a65..090940630 100644 --- a/sources/renderer/bootstrap/fieldrenderer/bslinkedsetfieldrenderer.class.inc.php +++ b/sources/renderer/bootstrap/fieldrenderer/bslinkedsetfieldrenderer.class.inc.php @@ -576,6 +576,7 @@ EOF ); // Target object others attributes + // TODO: Support for AttriubteImage, AttributeBlob foreach ($this->oField->GetAttributesToDisplay(true) as $sAttCode) { if ($sAttCode !== 'id') @@ -598,7 +599,7 @@ EOF } else { - $aAttProperties['value'] = $oAttDef->GetValueLabel($oRemoteItem->Get($sAttCode)); + $aAttProperties['value'] = $oAttDef->GetAsHTML($oRemoteItem->Get($sAttCode)); if ($oAttDef instanceof AttributeFriendlyName) { From 6e50a1c4be8263967c69f7a9666aaeafe9d31c9d Mon Sep 17 00:00:00 2001 From: Stephen Abello Date: Mon, 6 Aug 2018 16:33:47 +0200 Subject: [PATCH 12/19] (Retrofit from develop 7d37b065) Form prefill: add possibility to change attributes flag on the fly --- application/cmdbabstract.class.inc.php | 8 +++++++- pages/UI.php | 3 +-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index 9059107e6..f5bf8269c 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -2380,7 +2380,7 @@ EOF return $oObj->DisplayModifyForm( $oPage, $aExtraParams); } - public function DisplayStimulusForm(WebPage $oPage, $sStimulus) + public function DisplayStimulusForm(WebPage $oPage, $sStimulus, $aPrefillFormParam = null) { $sClass = get_class($this); $iKey = $this->GetKey(); @@ -2422,6 +2422,12 @@ EOF $oPage->add("

$sActionDetails

\n"); $sTargetState = $aTransitions[$sStimulus]['target_state']; $aExpectedAttributes = $this->GetTransitionAttributes($sStimulus /*, current state*/); + if ($aPrefillFormParam != null) + { + $aPrefillFormParam['expected_attributes'] = $aExpectedAttributes; + $this->PrefillForm('state_change', $aPrefillFormParam); + $aExpectedAttributes = $aPrefillFormParam['expected_attributes']; + } $sButtonsPosition = MetaModel::GetConfig()->Get('buttons_position'); if ($sButtonsPosition == 'bottom') { diff --git a/pages/UI.php b/pages/UI.php index b0721dd35..110a07acf 100644 --- a/pages/UI.php +++ b/pages/UI.php @@ -1485,8 +1485,7 @@ EOF 'stimulus' => $sStimulus, 'origin' => 'console' ); - $oObj->PrefillForm('state_change', $aPrefillFormParam); - $oObj->DisplayStimulusForm($oP, $sStimulus); + $oObj->DisplayStimulusForm($oP, $sStimulus, $aPrefillFormParam); } else { From 12e9e453d8f9fc5c0db99b2536ae1b33d1eff576 Mon Sep 17 00:00:00 2001 From: Molkobain Date: Tue, 2 Oct 2018 14:57:51 +0200 Subject: [PATCH 13/19] =?UTF-8?q?N=C2=B01644=20Fix=20PHP=207.2=20compatibi?= =?UTF-8?q?lity=20issue=20(count()=20on=20none=20array)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/cmdbabstract.class.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index f5bf8269c..6168093cf 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -2494,7 +2494,7 @@ EOF else { $aAllowedValues = MetaModel::GetAllowedValues_att($sClass, $sAttCode, $aArgs); - if (count($aAllowedValues) == 1) + if (is_array($aAllowedValues) && count($aAllowedValues) == 1) { $aValues = array_keys($aAllowedValues); $this->Set($sAttCode, $aValues[0]); From c5f3598f4e5808acfd6623cd0e26fb6e04e65a0e Mon Sep 17 00:00:00 2001 From: Molkobain Date: Tue, 2 Oct 2018 16:40:26 +0200 Subject: [PATCH 14/19] =?UTF-8?q?N=C2=B01652=20Fix=20broken=20search=20for?= =?UTF-8?q?m=20when=20user=20has=20no=20read=20right=20on=20objects.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/search/searchform.class.inc.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/sources/application/search/searchform.class.inc.php b/sources/application/search/searchform.class.inc.php index 4493f154a..cb4b88df1 100644 --- a/sources/application/search/searchform.class.inc.php +++ b/sources/application/search/searchform.class.inc.php @@ -309,11 +309,16 @@ class SearchForm $aSelectedClasses = $oSearch->GetSelectedClasses(); foreach($aSelectedClasses as $sAlias => $sClassName) { - $aAllFields['zlist'] = array_merge($aAllFields['zlist'], $aAllFields[$sAlias.'_zlist']); - unset($aAllFields[$sAlias.'_zlist']); - $aAllFields['others'] = array_merge($aAllFields['others'], $aAllFields[$sAlias.'_others']); - unset($aAllFields[$sAlias.'_others']); - + if(array_key_exists($sAlias.'_zlist', $aAllFields)) + { + $aAllFields['zlist'] = array_merge($aAllFields['zlist'], $aAllFields[$sAlias.'_zlist']); + unset($aAllFields[$sAlias.'_zlist']); + } + if(array_key_exists($sAlias.'_others', $aAllFields)) + { + $aAllFields['others'] = array_merge($aAllFields['others'], $aAllFields[$sAlias.'_others']); + unset($aAllFields[$sAlias.'_others']); + } } return $aAllFields; From cbe749af1387d4099560e924d049360bacf56c8c Mon Sep 17 00:00:00 2001 From: Molkobain Date: Tue, 2 Oct 2018 16:48:58 +0200 Subject: [PATCH 15/19] =?UTF-8?q?N=C2=B01645=20Fix=20reset=20password=20di?= =?UTF-8?q?ctionary=20entries=20(hyperlink=20had=20wrongly=20escaped=20cha?= =?UTF-8?q?racters)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dictionaries/da.dictionary.itop.ui.php | 2 +- dictionaries/hu.dictionary.itop.ui.php | 2 +- dictionaries/it.dictionary.itop.ui.php | 2 +- dictionaries/ja.dictionary.itop.ui.php | 2 +- dictionaries/tr.dictionary.itop.ui.php | 2 +- dictionaries/zh.dictionary.itop.ui.php | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dictionaries/da.dictionary.itop.ui.php b/dictionaries/da.dictionary.itop.ui.php index be5b826ad..ab8e2cc7d 100644 --- a/dictionaries/da.dictionary.itop.ui.php +++ b/dictionaries/da.dictionary.itop.ui.php @@ -1010,7 +1010,7 @@ Ved tilknytningen til en trigger, bliver hver handling tildelt et "rækkefølge" 'UI:ResetPwd-Error-Send' => 'email transport technical issue. Please Contact your administrator.~~', 'UI:ResetPwd-EmailSent' => 'Please check your email box and follow the instructions...~~', 'UI:ResetPwd-EmailSubject' => 'Reset your iTop password~~', - 'UI:ResetPwd-EmailBody' => '

You have requested to reset your iTop password.

Please follow this link (single usage) to enter a new password

.~~', + 'UI:ResetPwd-EmailBody' => '

You have requested to reset your iTop password.

Please follow this link (single usage) to enter a new password

.~~', 'UI:ResetPwd-Title' => 'Reset password~~', 'UI:ResetPwd-Error-InvalidToken' => 'Sorry, either the password has already been reset, or you have received several emails. Please make sure that you use the link provided in the very last email received.~~', 'UI:ResetPwd-Error-EnterPassword' => 'Enter a new password for the account \'%1$s\'.~~', diff --git a/dictionaries/hu.dictionary.itop.ui.php b/dictionaries/hu.dictionary.itop.ui.php index a0fc99d6d..d799f8170 100755 --- a/dictionaries/hu.dictionary.itop.ui.php +++ b/dictionaries/hu.dictionary.itop.ui.php @@ -822,7 +822,7 @@ Akció kiváltó okhoz rendelésekor kap egy sorszámot , amely meghatározza az 'UI:ResetPwd-Error-Send' => 'email transport technical issue. Please Contact your administrator.~~', 'UI:ResetPwd-EmailSent' => 'Please check your email box and follow the instructions...~~', 'UI:ResetPwd-EmailSubject' => 'Reset your iTop password~~', - 'UI:ResetPwd-EmailBody' => '

You have requested to reset your iTop password.

Please follow this link (single usage) to enter a new password

.~~', + 'UI:ResetPwd-EmailBody' => '

You have requested to reset your iTop password.

Please follow this link (single usage) to enter a new password

.~~', 'UI:ResetPwd-Title' => 'Reset password~~', 'UI:ResetPwd-Error-InvalidToken' => 'Sorry, either the password has already been reset, or you have received several emails. Please make sure that you use the link provided in the very last email received.~~', 'UI:ResetPwd-Error-EnterPassword' => 'Enter a new password for the account \'%1$s\'.~~', diff --git a/dictionaries/it.dictionary.itop.ui.php b/dictionaries/it.dictionary.itop.ui.php index ecb1b3d0c..98b9bc2e6 100644 --- a/dictionaries/it.dictionary.itop.ui.php +++ b/dictionaries/it.dictionary.itop.ui.php @@ -946,7 +946,7 @@ Quando è associata a un trigger, ad ogni azione è assegnato un numero "ordine" 'UI:ResetPwd-Error-Send' => 'email transport technical issue. Please Contact your administrator.~~', 'UI:ResetPwd-EmailSent' => 'Please check your email box and follow the instructions...~~', 'UI:ResetPwd-EmailSubject' => 'Reset your iTop password~~', - 'UI:ResetPwd-EmailBody' => '

You have requested to reset your iTop password.

Please follow this link (single usage) to enter a new password

.~~', + 'UI:ResetPwd-EmailBody' => '

You have requested to reset your iTop password.

Please follow this link (single usage) to enter a new password

.~~', 'UI:ResetPwd-Title' => 'Reset password~~', 'UI:ResetPwd-Error-InvalidToken' => 'Sorry, either the password has already been reset, or you have received several emails. Please make sure that you use the link provided in the very last email received.~~', 'UI:ResetPwd-Error-EnterPassword' => 'Enter a new password for the account \'%1$s\'.~~', diff --git a/dictionaries/ja.dictionary.itop.ui.php b/dictionaries/ja.dictionary.itop.ui.php index b91a73ebe..5bb0d1660 100644 --- a/dictionaries/ja.dictionary.itop.ui.php +++ b/dictionaries/ja.dictionary.itop.ui.php @@ -1007,7 +1007,7 @@ Dict::Add('JA JP', 'Japanese', '日本語', array( 'UI:ResetPwd-Error-Send' => 'email transport technical issue. Please Contact your administrator.~~', 'UI:ResetPwd-EmailSent' => 'Please check your email box and follow the instructions...~~', 'UI:ResetPwd-EmailSubject' => 'Reset your iTop password~~', - 'UI:ResetPwd-EmailBody' => '

You have requested to reset your iTop password.

Please follow this link (single usage) to enter a new password

.~~', + 'UI:ResetPwd-EmailBody' => '

You have requested to reset your iTop password.

Please follow this link (single usage) to enter a new password

.~~', 'UI:ResetPwd-Title' => 'Reset password~~', 'UI:ResetPwd-Error-InvalidToken' => 'Sorry, either the password has already been reset, or you have received several emails. Please make sure that you use the link provided in the very last email received.~~', 'UI:ResetPwd-Error-EnterPassword' => 'Enter a new password for the account \'%1$s\'.~~', diff --git a/dictionaries/tr.dictionary.itop.ui.php b/dictionaries/tr.dictionary.itop.ui.php index 3bedf96b1..e19998ff5 100644 --- a/dictionaries/tr.dictionary.itop.ui.php +++ b/dictionaries/tr.dictionary.itop.ui.php @@ -930,7 +930,7 @@ Tetikleme gerçekleştiriğinde işlemler tanımlanan sıra numarası ile gerçe 'UI:ResetPwd-Error-Send' => 'email transport technical issue. Please Contact your administrator.~~', 'UI:ResetPwd-EmailSent' => 'Please check your email box and follow the instructions...~~', 'UI:ResetPwd-EmailSubject' => 'Reset your iTop password~~', - 'UI:ResetPwd-EmailBody' => '

You have requested to reset your iTop password.

Please follow this link (single usage) to enter a new password

.~~', + 'UI:ResetPwd-EmailBody' => '

You have requested to reset your iTop password.

Please follow this link (single usage) to enter a new password

.~~', 'UI:ResetPwd-Title' => 'Reset password~~', 'UI:ResetPwd-Error-InvalidToken' => 'Sorry, either the password has already been reset, or you have received several emails. Please make sure that you use the link provided in the very last email received.~~', 'UI:ResetPwd-Error-EnterPassword' => 'Enter a new password for the account \'%1$s\'.~~', diff --git a/dictionaries/zh.dictionary.itop.ui.php b/dictionaries/zh.dictionary.itop.ui.php index 8850b7d68..09ef16124 100644 --- a/dictionaries/zh.dictionary.itop.ui.php +++ b/dictionaries/zh.dictionary.itop.ui.php @@ -928,7 +928,7 @@ Dict::Add('ZH CN', 'Chinese', '简体中文', array( 'UI:ResetPwd-Error-Send' => 'email transport technical issue. Please Contact your administrator.~~', 'UI:ResetPwd-EmailSent' => 'Please check your email box and follow the instructions...~~', 'UI:ResetPwd-EmailSubject' => 'Reset your iTop password~~', - 'UI:ResetPwd-EmailBody' => '

You have requested to reset your iTop password.

Please follow this link (single usage) to enter a new password

.~~', + 'UI:ResetPwd-EmailBody' => '

You have requested to reset your iTop password.

Please follow this link (single usage) to enter a new password

.~~', 'UI:ResetPwd-Title' => 'Reset password~~', 'UI:ResetPwd-Error-InvalidToken' => 'Sorry, either the password has already been reset, or you have received several emails. Please make sure that you use the link provided in the very last email received.~~', 'UI:ResetPwd-Error-EnterPassword' => 'Enter a new password for the account \'%1$s\'.~~', From 10b7fa6014dfc05a06f9b25affb052c456706220 Mon Sep 17 00:00:00 2001 From: Molkobain Date: Wed, 3 Oct 2018 10:16:15 +0200 Subject: [PATCH 16/19] =?UTF-8?q?N=C2=B01647=20Fix=20Excel=20web=20queries?= =?UTF-8?q?=20import.=20(JS=20script=20error=20popups)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- webservices/export-v2.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/webservices/export-v2.php b/webservices/export-v2.php index 26a30a50c..ef7a70fa6 100644 --- a/webservices/export-v2.php +++ b/webservices/export-v2.php @@ -731,9 +731,17 @@ try $sMimeType = $oExporter->GetMimeType(); if ($sMimeType == 'text/html') { - $oP = new NiceWebPage('iTop export'); + // Note: Using NiceWebPage only for HTML export as it includes JS scripts & files, which makes no sense in other export formats. More over, it breaks Excel spreadsheet import. + if($oExporter instanceof HTMLBulkExport) + { + $oP = new NiceWebPage('iTop export'); + $oP->add_ready_script("$('table.listResults').tablesorter({widgets: ['MyZebra']});"); + } + else + { + $oP = new WebPage('iTop export'); + } $oP->add_style("body { overflow: auto; }"); - $oP->add_ready_script("$('table.listResults').tablesorter({widgets: ['MyZebra']});"); } else { From 7fddd6acdca0d52bb091840330c4205cdd3f5661 Mon Sep 17 00:00:00 2001 From: Molkobain Date: Wed, 3 Oct 2018 15:08:54 +0200 Subject: [PATCH 17/19] =?UTF-8?q?N=C2=B01624=20Fix=20bulk=20transition=20i?= =?UTF-8?q?ntegrity=20exception=20when=20"org=5Fid"=20was=20not=20checked.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/utils.js | 8 ++++++-- pages/UI.php | 27 +++++++++++++++------------ 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/js/utils.js b/js/utils.js index 979f4f007..5f1ebaa8c 100644 --- a/js/utils.js +++ b/js/utils.js @@ -409,8 +409,12 @@ function ToggleDurationField(field_id) { function PropagateCheckBox(bCurrValue, aFieldsList, bCheck) { if (bCurrValue == bCheck) { for (var i = 0; i < aFieldsList.length; i++) { - $('#enable_'+aFieldsList[i]).attr('checked', bCheck); - ToogleField(bCheck, aFieldsList[i]); + var sFieldId = aFieldsList[i]; + $('#enable_'+sFieldId).attr('checked', bCheck); + ToogleField(bCheck, sFieldId); + + // Cascade propagation + $('#enable_'+sFieldId).trigger('change'); } } } diff --git a/pages/UI.php b/pages/UI.php index 110a07acf..a34a020c6 100644 --- a/pages/UI.php +++ b/pages/UI.php @@ -1205,6 +1205,8 @@ EOF $aExpectedAttributes = MetaModel::GetTransitionAttributes($sClass, $sStimulus, $sState); $aDetails = array(); + $sFormId = 'apply_stimulus'; + $sFormPrefix = $sFormId.'_'; $iFieldIndex = 0; $aFieldsMap = array(); $aValues = array(); @@ -1220,6 +1222,7 @@ EOF $sReadyScript = ''; foreach($aExpectedAttributes as $sAttCode => $iExpectCode) { + $sFieldInputId = $sFormPrefix.$sAttCode; // Prompt for an attribute if // - the attribute must be changed or must be displayed to the user for confirmation // - or the field is mandatory and currently empty @@ -1232,19 +1235,19 @@ EOF if (count($aPrerequisites) > 0) { // When 'enabling' a field, all its prerequisites must be enabled too - $sFieldList = "['".implode("','", $aPrerequisites)."']"; - $oP->add_ready_script("$('#enable_{$sAttCode}').bind('change', function(evt, sFormId) { return PropagateCheckBox( this.checked, $sFieldList, true); } );\n"); + $sFieldList = "['{$sFormPrefix}".implode("','{$sFormPrefix}", $aPrerequisites)."']"; + $oP->add_ready_script("$('#enable_{$sFieldInputId}').bind('change', function(evt, sFormId) { return PropagateCheckBox( this.checked, $sFieldList, true); } );\n"); } $aDependents = MetaModel::GetDependentAttributes($sClass, $sAttCode); // List of attributes that are needed for the current one if (count($aDependents) > 0) { // When 'disabling' a field, all its dependent fields must be disabled too - $sFieldList = "['".implode("','", $aDependents)."']"; - $oP->add_ready_script("$('#enable_{$sAttCode}').bind('change', function(evt, sFormId) { return PropagateCheckBox( this.checked, $sFieldList, false); } );\n"); + $sFieldList = "['{$sFormPrefix}".implode("','{$sFormPrefix}", $aDependents)."']"; + $oP->add_ready_script("$('#enable_{$sFieldInputId}').bind('change', function(evt, sFormId) { return PropagateCheckBox( this.checked, $sFieldList, false); } );\n"); } $aArgs = array('this' => $oObj); - $sHTMLValue = cmdbAbstractObject::GetFormElementForField($oP, $sClass, $sAttCode, $oAttDef, $oObj->Get($sAttCode), $oObj->GetEditValue($sAttCode), $sAttCode, '', $iExpectCode, $aArgs); - $sComments = ''; + $sHTMLValue = cmdbAbstractObject::GetFormElementForField($oP, $sClass, $sAttCode, $oAttDef, $oObj->Get($sAttCode), $oObj->GetEditValue($sAttCode), $sFieldInputId, '', $iExpectCode, $aArgs); + $sComments = ''; if (!isset($aValues[$sAttCode])) { $aValues[$sAttCode] = array(); @@ -1272,11 +1275,11 @@ EOF } $sTip .= "

"; $sTip = addslashes($sTip); - $sReadyScript .= "$('#multi_values_$sAttCode').qtip( { content: '$sTip', show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );\n"; - $sComments .= '
'.count($aValues[$sAttCode]).'
'; + $sReadyScript .= "$('#multi_values_$sFieldInputId').qtip( { content: '$sTip', show: 'mouseover', hide: 'mouseout', style: { name: 'dark', tip: 'leftTop' }, position: { corner: { target: 'rightMiddle', tooltip: 'leftTop' }} } );\n"; + $sComments .= '
'.count($aValues[$sAttCode]).'
'; } - $aDetails[] = array('label' => ''.$oAttDef->GetLabel().'', 'value' => "$sHTMLValue", 'comments' => $sComments); - $aFieldsMap[$sAttCode] = $sAttCode; + $aDetails[] = array('label' => ''.$oAttDef->GetLabel().'', 'value' => "$sHTMLValue", 'comments' => $sComments); + $aFieldsMap[$sAttCode] = $sFieldInputId; $iFieldIndex++; } } @@ -1289,7 +1292,7 @@ EOF $oP->add(''); } $oP->add("
\n"); - $oP->add("\n"); + $oP->add("\n"); $oP->add("
\n"); $oP->details($aDetails); $oP->add("
\n"); @@ -1328,7 +1331,7 @@ EOF $oP->add_ready_script( << Date: Wed, 3 Oct 2018 17:24:08 +0200 Subject: [PATCH 18/19] =?UTF-8?q?N=C2=B01059=20Fix=20new=20empty=20caselog?= =?UTF-8?q?=20entry=20on=20bulk=20modification.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/cmdbabstract.class.inc.php | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index 6168093cf..0a74ff89a 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -1757,6 +1757,27 @@ EOF $sConfigJS = json_encode($aConfig); $oPage->add_ready_script("$('#$iId').ckeditor(function() { /* callback code */ }, $sConfigJS);"); // Transform $iId into a CKEdit + + $oPage->add_ready_script( +<<Get($sAttCode); if ($oAttDef instanceof AttributeCaseLog) { - $currValue = ' '; // Don't put an empty string, in case the field would be considered as mandatory... + $currValue = ''; // Put a single scalar value to force caselog to mock a new entry. For more info see N°1059. } if (is_object($currValue)) continue; // Skip non scalar values... if(!array_key_exists($currValue, $aValues[$sAttCode])) From 396fc2cdcd3ce472b2ad3cc49aa28287bc49d94d Mon Sep 17 00:00:00 2001 From: Molkobain Date: Wed, 3 Oct 2018 17:43:06 +0200 Subject: [PATCH 19/19] =?UTF-8?q?N=C2=B01658=20Fix=20hard-coded=20translat?= =?UTF-8?q?ion=20in=20search=20page=20when=20the=20form=20has=20not=20been?= =?UTF-8?q?=20automatically=20submitted.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dictionaries/de.dictionary.itop.ui.php | 1 + dictionaries/en.dictionary.itop.ui.php | 1 + dictionaries/fr.dictionary.itop.ui.php | 1 + js/search/search_form_handler.js | 4 +--- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dictionaries/de.dictionary.itop.ui.php b/dictionaries/de.dictionary.itop.ui.php index 6ce532ff0..21f13aef2 100644 --- a/dictionaries/de.dictionary.itop.ui.php +++ b/dictionaries/de.dictionary.itop.ui.php @@ -1177,6 +1177,7 @@ Wenn Aktionen mit Trigger verknüpft sind, bekommt jede Aktion eine Auftragsnumm // Search form 'UI:Search:Toggle' => 'Ein-/Ausklappen', 'UI:Search:AutoSubmit:DisabledHint' => 'Automatische Eingabe für diese Klasse deaktiviert', + 'UI:Search:NoAutoSubmit:ExplainText' => 'Add some criterion on the search box or click the search button to view the objects.~~', 'UI:Search:Criterion:MoreMenu:AddCriteria' => 'Kriterium hinzufügen', // - Add new criteria button 'UI:Search:AddCriteria:List:RecentlyUsed:Title' => 'Kürzlich verwendet', diff --git a/dictionaries/en.dictionary.itop.ui.php b/dictionaries/en.dictionary.itop.ui.php index 9b20f0935..fc036fe5b 100644 --- a/dictionaries/en.dictionary.itop.ui.php +++ b/dictionaries/en.dictionary.itop.ui.php @@ -1409,6 +1409,7 @@ When associated with a trigger, each action is given an "order" number, specifyi // Search form 'UI:Search:Toggle' => 'Minimize / Expand', 'UI:Search:AutoSubmit:DisabledHint' => 'Auto submit has been disabled for this class', + 'UI:Search:NoAutoSubmit:ExplainText' => 'Add some criterion on the search box or click the search button to view the objects.', 'UI:Search:Criterion:MoreMenu:AddCriteria' => 'Add new criteria', // - Add new criteria button 'UI:Search:AddCriteria:List:RecentlyUsed:Title' => 'Recently used', diff --git a/dictionaries/fr.dictionary.itop.ui.php b/dictionaries/fr.dictionary.itop.ui.php index 3435d32a8..f81da6675 100644 --- a/dictionaries/fr.dictionary.itop.ui.php +++ b/dictionaries/fr.dictionary.itop.ui.php @@ -1242,6 +1242,7 @@ Lors de l\'association à un déclencheur, on attribue à chaque action un numé // Search form 'UI:Search:Toggle' => 'Réduire / Ouvrir', 'UI:Search:AutoSubmit:DisabledHint' => 'La soumission automatique a été desactivée pour cette classe', + 'UI:Search:NoAutoSubmit:ExplainText' => 'Ajoutez des critères dans le formulaire de recherche ou cliquez sur le bouton rechercher pour voir les objets.', 'UI:Search:Criterion:MoreMenu:AddCriteria' => 'Ajouter un critère', // - Add new criteria button 'UI:Search:AddCriteria:List:RecentlyUsed:Title' => 'Récents', diff --git a/js/search/search_form_handler.js b/js/search/search_form_handler.js index f297c352a..e64f5056f 100644 --- a/js/search/search_form_handler.js +++ b/js/search/search_form_handler.js @@ -745,9 +745,7 @@ $(function() // Make placeholder if nothing yet if(oResultAreaElem.html() === '') { - // TODO: Make a good UI for this POC. - // TODO: Translate sentence. - oResultAreaElem.html('

Add some criterion on the search box or click the search button to view the objects.

'); + oResultAreaElem.html('

' + Dict.S('UI:Search:NoAutoSubmit:ExplainText') + '

'); oResultAreaElem.find('button').on('click', function(){ // TODO: Bug: Open "Search for CI", change child classe in the dropdown, click the search button. It submit the search for the original child classe, not the current one; whereas a click on the upper right pictogram does. This might be due to the form reloading. me._onSubmitClick();