diff options
Diffstat (limited to 'ui')
49 files changed, 1159 insertions, 412 deletions
diff --git a/ui/app/controllers/CControllerAuthenticationEdit.php b/ui/app/controllers/CControllerAuthenticationEdit.php index 0e0d605757b..8d6a4f9fa0d 100644 --- a/ui/app/controllers/CControllerAuthenticationEdit.php +++ b/ui/app/controllers/CControllerAuthenticationEdit.php @@ -87,6 +87,7 @@ class CControllerAuthenticationEdit extends CController { protected function doAction() { $ldap_status = (new CFrontendSetup())->checkPhpLdapModule(); + $openssl_status = (new CFrontendSetup())->checkPhpOpenSsl(); $data = [ 'action_submit' => 'authentication.update', @@ -94,6 +95,7 @@ class CControllerAuthenticationEdit extends CController { 'ldap_error' => ($ldap_status['result'] == CFrontendSetup::CHECK_OK) ? '' : $ldap_status['error'], 'ldap_test_password' => '', 'ldap_test_user' => CWebUser::$data['alias'], + 'saml_error' => ($openssl_status['result'] == CFrontendSetup::CHECK_OK) ? '' : $openssl_status['error'], 'change_bind_password' => 0, 'form_refresh' => 0 ]; @@ -181,6 +183,8 @@ class CControllerAuthenticationEdit extends CController { $data['ldap_enabled'] = ($ldap_status['result'] == CFrontendSetup::CHECK_OK && $data['ldap_configured'] == ZBX_AUTH_LDAP_ENABLED); + $data['saml_enabled'] = ($openssl_status['result'] == CFrontendSetup::CHECK_OK + && $data['saml_auth_enabled'] == ZBX_AUTH_SAML_ENABLED); $response = new CControllerResponseData($data); $response->setTitle(_('Configuration of authentication')); diff --git a/ui/app/controllers/CControllerAuthenticationUpdate.php b/ui/app/controllers/CControllerAuthenticationUpdate.php index 001296a731e..e9f80dc9ae5 100644 --- a/ui/app/controllers/CControllerAuthenticationUpdate.php +++ b/ui/app/controllers/CControllerAuthenticationUpdate.php @@ -194,6 +194,14 @@ class CControllerAuthenticationUpdate extends CController { * @return bool */ private function validateSamlAuth() { + $openssl_status = (new CFrontendSetup())->checkPhpOpenSsl(); + + if ($openssl_status['result'] != CFrontendSetup::CHECK_OK) { + $this->response->setMessageError($openssl_status['error']); + + return false; + } + $saml_fields = ['saml_idp_entityid', 'saml_sso_url', 'saml_sp_entityid', 'saml_username_attribute']; $saml_auth = [ 'saml_idp_entityid' => CAuthenticationHelper::get(CAuthenticationHelper::SAML_IDP_ENTITYID), @@ -237,6 +245,7 @@ class CControllerAuthenticationUpdate extends CController { if (!$auth_valid) { $this->response->setFormData($this->getInputAll()); $this->setResponse($this->response); + return; } @@ -245,6 +254,7 @@ class CControllerAuthenticationUpdate extends CController { $this->response->setMessageOk(_('LDAP login successful')); $this->response->setFormData($this->getInputAll()); $this->setResponse($this->response); + return; } diff --git a/ui/app/controllers/CControllerExport.php b/ui/app/controllers/CControllerExport.php new file mode 100644 index 00000000000..37bd77df536 --- /dev/null +++ b/ui/app/controllers/CControllerExport.php @@ -0,0 +1,120 @@ +<?php +/* +** Zabbix +** Copyright (C) 2001-2020 Zabbix SIA +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 2 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software +** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +**/ + + +class CControllerExport extends CController { + + protected function checkInput() { + $fields = [ + 'action' => 'required|string', + 'backurl' => 'required|string', + 'valuemapids' => 'not_empty|array_db valuemaps.valuemapid', + 'hosts' => 'not_empty|array_db hosts.hostid', + 'mediatypeids' => 'not_empty|array_db media_type.mediatypeid', + 'screens' => 'not_empty|array_db screens.screenid', + 'maps' => 'not_empty|array_db sysmaps.sysmapid', + 'templates' => 'not_empty|array_db hosts.hostid', + 'format' => 'in '.implode(',', [CExportWriterFactory::YAML, CExportWriterFactory::XML, CExportWriterFactory::JSON]) + ]; + + $ret = $this->validateInput($fields); + + if (!$ret) { + $this->setResponse(new CControllerResponseFatal()); + } + + return $ret; + } + + protected function checkPermissions() { + switch ($this->getInput('action')) { + case 'export.mediatypes': + case 'export.valuemaps': + return (CWebUser::$data['type'] >= USER_TYPE_SUPER_ADMIN); + + case 'export.hosts': + case 'export.templates': + return (CWebUser::$data['type'] >= USER_TYPE_ZABBIX_ADMIN); + + case 'export.screens': + case 'export.sysmaps': + return (CWebUser::$data['type'] >= USER_TYPE_ZABBIX_USER); + + default: + return false; + } + } + + protected function doAction() { + $action = $this->getInput('action'); + $params = [ + 'format' => $this->getInput('format', CExportWriterFactory::YAML), + 'prettyprint' => true, + 'options' => [] + ]; + + switch ($action) { + case 'export.valuemaps': + $params['options']['valueMaps'] = $this->getInput('valuemapids', []); + break; + + case 'export.hosts': + $params['options']['hosts'] = $this->getInput('hosts', []); + break; + + case 'export.mediatypes': + $params['options']['mediaTypes'] = $this->getInput('mediatypeids', []); + break; + + case 'export.screens': + $params['options']['screens'] = $this->getInput('screens', []); + break; + + case 'export.sysmaps': + $params['options']['maps'] = $this->getInput('maps', []); + break; + + case 'export.templates': + $params['options']['templates'] = $this->getInput('templates', []); + break; + + default: + $this->setResponse(new CControllerResponseFatal()); + + return; + } + + $result = API::Configuration()->export($params); + + if ($result) { + $response = new CControllerResponseData([ + 'main_block' => $result, + 'mime_type' => CExportWriterFactory::getMimeType($params['format']), + 'page' => ['file' => 'zbx_export_'.substr($action, 7).'.'.$params['format']] + ]); + } + else { + $response = new CControllerResponseRedirect($this->getInput('backurl', 'zabbix.php?action=dashboard.view')); + $response->setMessageError(_('Export failed')); + } + + $this->setResponse($response); + } +} diff --git a/ui/app/controllers/CControllerExportXml.php b/ui/app/controllers/CControllerExportXml.php deleted file mode 100644 index 6b1238b7008..00000000000 --- a/ui/app/controllers/CControllerExportXml.php +++ /dev/null @@ -1,120 +0,0 @@ -<?php -/* -** Zabbix -** Copyright (C) 2001-2020 Zabbix SIA -** -** This program is free software; you can redistribute it and/or modify -** it under the terms of the GNU General Public License as published by -** the Free Software Foundation; either version 2 of the License, or -** (at your option) any later version. -** -** This program is distributed in the hope that it will be useful, -** but WITHOUT ANY WARRANTY; without even the implied warranty of -** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -** GNU General Public License for more details. -** -** You should have received a copy of the GNU General Public License -** along with this program; if not, write to the Free Software -** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -**/ - - -class CControllerExportXml extends CController { - - protected function checkInput() { - $fields = [ - 'action' => 'required|string', - 'backurl' => 'required|string', - 'valuemapids' => 'not_empty|array_db valuemaps.valuemapid', - 'hosts' => 'not_empty|array_db hosts.hostid', - 'mediatypeids' => 'not_empty|array_db media_type.mediatypeid', - 'screens' => 'not_empty|array_db screens.screenid', - 'maps' => 'not_empty|array_db sysmaps.sysmapid', - 'templates' => 'not_empty|array_db hosts.hostid' - ]; - - $ret = $this->validateInput($fields); - - if (!$ret) { - $this->setResponse(new CControllerResponseFatal()); - } - - return $ret; - } - - protected function checkPermissions() { - switch ($this->getInput('action')) { - case 'export.mediatypes.xml': - case 'export.valuemaps.xml': - return (CWebUser::$data['type'] >= USER_TYPE_SUPER_ADMIN); - - case 'export.hosts.xml': - case 'export.templates.xml': - return (CWebUser::$data['type'] >= USER_TYPE_ZABBIX_ADMIN); - - case 'export.screens.xml': - case 'export.sysmaps.xml': - return (CWebUser::$data['type'] >= USER_TYPE_ZABBIX_USER); - - default: - return false; - } - } - - protected function doAction() { - $action = $this->getInput('action'); - - switch ($action) { - case 'export.valuemaps.xml': - $export = new CConfigurationExport(['valueMaps' => $this->getInput('valuemapids', [])]); - break; - - case 'export.hosts.xml': - $export = new CConfigurationExport(['hosts' => $this->getInput('hosts', [])]); - break; - - case 'export.mediatypes.xml': - $export = new CConfigurationExport(['mediaTypes' => $this->getInput('mediatypeids', [])]); - break; - - case 'export.screens.xml': - $export = new CConfigurationExport(['screens' => $this->getInput('screens', [])]); - break; - - case 'export.sysmaps.xml': - $export = new CConfigurationExport(['maps' => $this->getInput('maps', [])]); - break; - - case 'export.templates.xml': - $export = new CConfigurationExport(['templates' => $this->getInput('templates', [])]); - break; - - default: - $this->setResponse(new CControllerResponseFatal()); - - return; - } - - $export->setBuilder(new CConfigurationExportBuilder()); - $export->setWriter(CExportWriterFactory::getWriter(CExportWriterFactory::XML)); - - $export_data = $export->export(); - - if ($export_data === false) { - // Access denied. - - $response = new CControllerResponseRedirect( - $this->getInput('backurl', 'zabbix.php?action=dashboard.view')); - - $response->setMessageError(_('No permissions to referred object or it does not exist!')); - } - else { - $response = new CControllerResponseData([ - 'main_block' => $export_data, - 'page' => ['file' => 'zbx_export_' . substr($action, 7)] - ]); - } - - $this->setResponse($response); - } -} diff --git a/ui/app/views/administration.authentication.edit.php b/ui/app/views/administration.authentication.edit.php index 2489607b998..b20f0914324 100644 --- a/ui/app/views/administration.authentication.edit.php +++ b/ui/app/views/administration.authentication.edit.php @@ -138,38 +138,38 @@ $ldap_tab = (new CFormList('list_ldap')) ); // SAML authentication fields. -$is_saml_auth_enabled = ($data['saml_auth_enabled'] == ZBX_AUTH_SAML_ENABLED); - $saml_tab = (new CFormList('list_saml')) ->addRow(new CLabel(_('Enable SAML authentication'), 'saml_auth_enabled'), - (new CCheckBox('saml_auth_enabled', ZBX_AUTH_SAML_ENABLED)) - ->setChecked($is_saml_auth_enabled) - ->setUncheckedValue(ZBX_AUTH_LDAP_DISABLED) + $data['saml_error'] + ? (new CLabel($data['saml_error']))->addClass(ZBX_STYLE_RED) + : (new CCheckBox('saml_auth_enabled', ZBX_AUTH_SAML_ENABLED)) + ->setChecked($data['saml_auth_enabled'] == ZBX_AUTH_SAML_ENABLED) + ->setUncheckedValue(ZBX_AUTH_SAML_DISABLED) ) ->addRow((new CLabel(_('IdP entity ID'), 'saml_idp_entityid'))->setAsteriskMark(), (new CTextBox('saml_idp_entityid', $data['saml_idp_entityid'], false, DB::getFieldLength('config', 'saml_idp_entityid') )) - ->setEnabled($is_saml_auth_enabled) + ->setEnabled($data['saml_enabled']) ->setWidth(ZBX_TEXTAREA_STANDARD_WIDTH) ->setAriaRequired() ) ->addRow((new CLabel(_('SSO service URL'), 'saml_sso_url'))->setAsteriskMark(), (new CTextBox('saml_sso_url', $data['saml_sso_url'], false, DB::getFieldLength('config', 'saml_sso_url'))) - ->setEnabled($is_saml_auth_enabled) + ->setEnabled($data['saml_enabled']) ->setWidth(ZBX_TEXTAREA_STANDARD_WIDTH) ->setAriaRequired() ) ->addRow(new CLabel(_('SLO service URL'), 'saml_slo_url'), (new CTextBox('saml_slo_url', $data['saml_slo_url'], false, DB::getFieldLength('config', 'saml_slo_url'))) - ->setEnabled($is_saml_auth_enabled) + ->setEnabled($data['saml_enabled']) ->setWidth(ZBX_TEXTAREA_STANDARD_WIDTH) ) ->addRow((new CLabel(_('Username attribute'), 'saml_username_attribute'))->setAsteriskMark(), (new CTextBox('saml_username_attribute', $data['saml_username_attribute'], false, DB::getFieldLength('config', 'saml_username_attribute') )) - ->setEnabled($is_saml_auth_enabled) + ->setEnabled($data['saml_enabled']) ->setWidth(ZBX_TEXTAREA_STANDARD_WIDTH) ->setAriaRequired() ) @@ -177,7 +177,7 @@ $saml_tab = (new CFormList('list_saml')) (new CTextBox('saml_sp_entityid', $data['saml_sp_entityid'], false, DB::getFieldLength('config', 'saml_sp_entityid') )) - ->setEnabled($is_saml_auth_enabled) + ->setEnabled($data['saml_enabled']) ->setWidth(ZBX_TEXTAREA_STANDARD_WIDTH) ->setAriaRequired() ) @@ -185,7 +185,7 @@ $saml_tab = (new CFormList('list_saml')) (new CTextBox('saml_nameid_format', $data['saml_nameid_format'], false, DB::getFieldLength('config', 'saml_nameid_format') )) - ->setEnabled($is_saml_auth_enabled) + ->setEnabled($data['saml_enabled']) ->setWidth(ZBX_TEXTAREA_STANDARD_WIDTH) ->setAttribute('placeholder', 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient') ) @@ -196,31 +196,31 @@ $saml_tab = (new CFormList('list_saml')) ->setLabel(_('Messages')) ->setChecked($data['saml_sign_messages'] == 1) ->setUncheckedValue(0) - ->setEnabled($is_saml_auth_enabled) + ->setEnabled($data['saml_enabled']) ) ->addItem((new CCheckBox('saml_sign_assertions')) ->setLabel(_('Assertions')) ->setChecked($data['saml_sign_assertions'] == 1) ->setUncheckedValue(0) - ->setEnabled($is_saml_auth_enabled) + ->setEnabled($data['saml_enabled']) ) ->addItem((new CCheckBox('saml_sign_authn_requests')) ->setLabel(_('AuthN requests')) ->setChecked($data['saml_sign_authn_requests'] == 1) ->setUncheckedValue(0) - ->setEnabled($is_saml_auth_enabled) + ->setEnabled($data['saml_enabled']) ) ->addItem((new CCheckBox('saml_sign_logout_requests')) ->setLabel(_('Logout requests')) ->setChecked($data['saml_sign_logout_requests'] == 1) ->setUncheckedValue(0) - ->setEnabled($is_saml_auth_enabled) + ->setEnabled($data['saml_enabled']) ) ->addItem((new CCheckBox('saml_sign_logout_responses')) ->setLabel(_('Logout responses')) ->setChecked($data['saml_sign_logout_responses'] == 1) ->setUncheckedValue(0) - ->setEnabled($is_saml_auth_enabled) + ->setEnabled($data['saml_enabled']) ) ) ->addRow(_('Encrypt'), @@ -230,20 +230,20 @@ $saml_tab = (new CFormList('list_saml')) ->setLabel(_('Name ID')) ->setChecked($data['saml_encrypt_nameid'] == 1) ->setUncheckedValue(0) - ->setEnabled($is_saml_auth_enabled) + ->setEnabled($data['saml_enabled']) ) ->addItem((new CCheckBox('saml_encrypt_assertions')) ->setLabel(_('Assertions')) ->setChecked($data['saml_encrypt_assertions'] == 1) ->setUncheckedValue(0) - ->setEnabled($is_saml_auth_enabled) + ->setEnabled($data['saml_enabled']) ) ) ->addRow(new CLabel(_('Case sensitive login'), 'saml_case_sensitive'), (new CCheckBox('saml_case_sensitive')) ->setChecked($data['saml_case_sensitive'] == ZBX_AUTH_CASE_SENSITIVE) ->setUncheckedValue(ZBX_AUTH_CASE_INSENSITIVE) - ->setEnabled($is_saml_auth_enabled) + ->setEnabled($data['saml_enabled']) ); (new CWidget()) diff --git a/ui/app/views/administration.mediatype.list.php b/ui/app/views/administration.mediatype.list.php index d812ef30675..6f8aa6cec7e 100644 --- a/ui/app/views/administration.mediatype.list.php +++ b/ui/app/views/administration.mediatype.list.php @@ -161,14 +161,13 @@ $mediaTypeForm->addItem([ new CActionButtonList('action', 'mediatypeids', [ 'mediatype.enable' => ['name' => _('Enable'), 'confirm' => _('Enable selected media types?')], 'mediatype.disable' => ['name' => _('Disable'), 'confirm' => _('Disable selected media types?')], - 'mediatype.export' => ['name' => _('Export'), 'redirect' => - (new CUrl('zabbix.php')) - ->setArgument('action', 'export.mediatypes.xml') - ->setArgument('backurl', (new CUrl('zabbix.php')) + 'mediatype.export' => [ + 'content' => new CButtonExport('export.mediatypes', + (new CUrl('zabbix.php')) ->setArgument('action', 'mediatype.list') - ->setArgument('page', $data['page'] == 1 ? null : $data['page']) - ->getUrl()) - ->getUrl() + ->setArgument('page', ($data['page'] == 1) ? null : $data['page']) + ->getUrl() + ) ], 'mediatype.delete' => ['name' => _('Delete'), 'confirm' => _('Delete selected media types?')] ], 'mediatype') diff --git a/ui/app/views/administration.valuemap.list.php b/ui/app/views/administration.valuemap.list.php index df4bb0d9803..1184af2060a 100644 --- a/ui/app/views/administration.valuemap.list.php +++ b/ui/app/views/administration.valuemap.list.php @@ -87,14 +87,13 @@ $form->addItem([ $table, $data['paging'], new CActionButtonList('action', 'valuemapids', [ - 'valuemap.export' => ['name' => _('Export'), 'redirect' => - (new CUrl('zabbix.php')) - ->setArgument('action', 'export.valuemaps.xml') - ->setArgument('backurl', (new CUrl('zabbix.php')) + 'valuemap.export' => [ + 'content' => new CButtonExport('export.valuemaps', + (new CUrl('zabbix.php')) ->setArgument('action', 'valuemap.list') - ->setArgument('page', $data['page'] == 1 ? null : $data['page']) - ->getUrl()) - ->getUrl() + ->setArgument('page', ($data['page'] == 1) ? null : $data['page']) + ->getUrl() + ) ], 'valuemap.delete' => ['name' => _('Delete'), 'confirm' => _('Delete selected value maps?')] ]) diff --git a/ui/app/views/layout.xml.php b/ui/app/views/layout.export.php index abf37862b37..38a1b1d88fd 100644 --- a/ui/app/views/layout.xml.php +++ b/ui/app/views/layout.export.php @@ -23,7 +23,7 @@ * @var CView $this */ -header('Content-Type: text/xml; charset=utf-8'); +header('Content-Type: '.$data['mime_type'].'; charset=utf-8'); header('Content-Disposition: attachment; filename="'.$data['page']['file'].'"'); echo $data['main_block']; diff --git a/ui/assets/styles/blue-theme.css b/ui/assets/styles/blue-theme.css index 24b145d6d29..0808a1cb775 100644 --- a/ui/assets/styles/blue-theme.css +++ b/ui/assets/styles/blue-theme.css @@ -5440,6 +5440,48 @@ table.preprocessing-test-results .rel-container { .icon-secret:not(.highlighted):enabled:hover::before, .icon-secret:not(.highlighted):enabled:focus::before, .icon-secret:not(.highlighted):enabled:active::before, .icon-secret:not(.highlighted):enabled[aria-expanded="true"]::before { background-position: -399px -510px; } +.btn-split { + display: inline-block; + position: relative; + margin-right: 10px; } + .btn-split li { + display: inline-block; } + .btn-split li button { + margin: 0 -1px 0 0; + border-radius: 0; } + .btn-split li:first-child button { + border-radius: 2px 0 0 2px; } + .btn-split li:last-child button { + border-radius: 0 2px 2px 0; } + .btn-split li:only-child button { + border-radius: 2px; } + +.btn-toggle-chevron { + position: relative; } + .btn-toggle-chevron[aria-expanded="true"] { + color: #ffffff; + background-color: #02659f; + border-color: #02659f; } + .btn-toggle-chevron::after { + content: ''; + position: absolute; + right: 8px; + top: calc(50% - 3px); + width: 5px; + height: 5px; + border-top: 1px solid #0275b8; + border-right: 1px solid #0275b8; + transform: rotate(135deg) translate(-1px, 1px); + transition: transform .3s; } + .btn-toggle-chevron[disabled]::after { + border-top-color: #acbbc2; + border-right-color: #acbbc2; } + .btn-toggle-chevron:enabled:active::after, .btn-toggle-chevron:enabled[aria-expanded="true"]::after { + transform: rotate(315deg) translate(-1px, 1px); } + .btn-toggle-chevron:enabled:hover::after, .btn-toggle-chevron:enabled:focus::after, .btn-toggle-chevron:enabled:active::after, .btn-toggle-chevron:enabled[aria-expanded="true"]::after { + border-top-color: #ffffff; + border-right-color: #ffffff; } + .btn-dropdown-toggle { white-space: nowrap; overflow: hidden; @@ -5454,10 +5496,13 @@ table.preprocessing-test-results .rel-container { width: 22px; height: 22px; content: ''; } + .btn-dropdown-toggle[class*='icon-']::after { + top: 6px; + right: 2px; } .btn-dropdown-toggle::after { position: absolute; - top: 6px; - right: 2px; + top: 7px; + right: 6px; width: 10px; height: 10px; content: ''; diff --git a/ui/assets/styles/dark-theme.css b/ui/assets/styles/dark-theme.css index 05798dc720c..0671789e72f 100644 --- a/ui/assets/styles/dark-theme.css +++ b/ui/assets/styles/dark-theme.css @@ -5451,6 +5451,48 @@ table.preprocessing-test-results .rel-container { .icon-secret:not(.highlighted):enabled:hover::before, .icon-secret:not(.highlighted):enabled:focus::before, .icon-secret:not(.highlighted):enabled:active::before, .icon-secret:not(.highlighted):enabled[aria-expanded="true"]::before { background-position: -399px -510px; } +.btn-split { + display: inline-block; + position: relative; + margin-right: 10px; } + .btn-split li { + display: inline-block; } + .btn-split li button { + margin: 0 -1px 0 0; + border-radius: 0; } + .btn-split li:first-child button { + border-radius: 2px 0 0 2px; } + .btn-split li:last-child button { + border-radius: 0 2px 2px 0; } + .btn-split li:only-child button { + border-radius: 2px; } + +.btn-toggle-chevron { + position: relative; } + .btn-toggle-chevron[aria-expanded="true"] { + color: #f2f2f2; + background-color: #5e737e; + border-color: #5e737e; } + .btn-toggle-chevron::after { + content: ''; + position: absolute; + right: 8px; + top: calc(50% - 3px); + width: 5px; + height: 5px; + border-top: 1px solid #768d99; + border-right: 1px solid #768d99; + transform: rotate(135deg) translate(-1px, 1px); + transition: transform .3s; } + .btn-toggle-chevron[disabled]::after { + border-top-color: #525252; + border-right-color: #525252; } + .btn-toggle-chevron:enabled:active::after, .btn-toggle-chevron:enabled[aria-expanded="true"]::after { + transform: rotate(315deg) translate(-1px, 1px); } + .btn-toggle-chevron:enabled:hover::after, .btn-toggle-chevron:enabled:focus::after, .btn-toggle-chevron:enabled:active::after, .btn-toggle-chevron:enabled[aria-expanded="true"]::after { + border-top-color: #f2f2f2; + border-right-color: #f2f2f2; } + .btn-dropdown-toggle { white-space: nowrap; overflow: hidden; @@ -5465,10 +5507,13 @@ table.preprocessing-test-results .rel-container { width: 22px; height: 22px; content: ''; } + .btn-dropdown-toggle[class*='icon-']::after { + top: 6px; + right: 2px; } .btn-dropdown-toggle::after { position: absolute; - top: 6px; - right: 2px; + top: 7px; + right: 6px; width: 10px; height: 10px; content: ''; diff --git a/ui/assets/styles/hc-dark.css b/ui/assets/styles/hc-dark.css index db2aaffefc7..de0e2fa8ac8 100644 --- a/ui/assets/styles/hc-dark.css +++ b/ui/assets/styles/hc-dark.css @@ -5395,6 +5395,48 @@ table.preprocessing-test-results .rel-container { .icon-secret:not(.highlighted):enabled:hover::before, .icon-secret:not(.highlighted):enabled:focus::before, .icon-secret:not(.highlighted):enabled:active::before, .icon-secret:not(.highlighted):enabled[aria-expanded="true"]::before { background-position: -399px -510px; } +.btn-split { + display: inline-block; + position: relative; + margin-right: 10px; } + .btn-split li { + display: inline-block; } + .btn-split li button { + margin: 0 -1px 0 0; + border-radius: 0; } + .btn-split li:first-child button { + border-radius: 2px 0 0 2px; } + .btn-split li:last-child button { + border-radius: 0 2px 2px 0; } + .btn-split li:only-child button { + border-radius: 2px; } + +.btn-toggle-chevron { + position: relative; } + .btn-toggle-chevron[aria-expanded="true"] { + color: #333333; + background-color: lightgray; + border-color: lightgray; } + .btn-toggle-chevron::after { + content: ''; + position: absolute; + right: 8px; + top: calc(50% - 3px); + width: 5px; + height: 5px; + border-top: 1px solid #ffffff; + border-right: 1px solid #ffffff; + transform: rotate(135deg) translate(-1px, 1px); + transition: transform .3s; } + .btn-toggle-chevron[disabled]::after { + border-top-color: #7d7d7d; + border-right-color: #7d7d7d; } + .btn-toggle-chevron:enabled:active::after, .btn-toggle-chevron:enabled[aria-expanded="true"]::after { + transform: rotate(315deg) translate(-1px, 1px); } + .btn-toggle-chevron:enabled:hover::after, .btn-toggle-chevron:enabled:focus::after, .btn-toggle-chevron:enabled:active::after, .btn-toggle-chevron:enabled[aria-expanded="true"]::after { + border-top-color: #333333; + border-right-color: #333333; } + .btn-dropdown-toggle { white-space: nowrap; overflow: hidden; @@ -5409,10 +5451,13 @@ table.preprocessing-test-results .rel-container { width: 22px; height: 22px; content: ''; } + .btn-dropdown-toggle[class*='icon-']::after { + top: 6px; + right: 2px; } .btn-dropdown-toggle::after { position: absolute; - top: 6px; - right: 2px; + top: 7px; + right: 6px; width: 10px; height: 10px; content: ''; diff --git a/ui/assets/styles/hc-light.css b/ui/assets/styles/hc-light.css index 492fc127eee..d8552e123bb 100644 --- a/ui/assets/styles/hc-light.css +++ b/ui/assets/styles/hc-light.css @@ -5395,6 +5395,48 @@ table.preprocessing-test-results .rel-container { .icon-secret:not(.highlighted):enabled:hover::before, .icon-secret:not(.highlighted):enabled:focus::before, .icon-secret:not(.highlighted):enabled:active::before, .icon-secret:not(.highlighted):enabled[aria-expanded="true"]::before { background-position: -399px -510px; } +.btn-split { + display: inline-block; + position: relative; + margin-right: 10px; } + .btn-split li { + display: inline-block; } + .btn-split li button { + margin: 0 -1px 0 0; + border-radius: 0; } + .btn-split li:first-child button { + border-radius: 2px 0 0 2px; } + .btn-split li:last-child button { + border-radius: 0 2px 2px 0; } + .btn-split li:only-child button { + border-radius: 2px; } + +.btn-toggle-chevron { + position: relative; } + .btn-toggle-chevron[aria-expanded="true"] { + color: #ffffff; + background-color: #484848; + border-color: #484848; } + .btn-toggle-chevron::after { + content: ''; + position: absolute; + right: 8px; + top: calc(50% - 3px); + width: 5px; + height: 5px; + border-top: 1px solid #000000; + border-right: 1px solid #000000; + transform: rotate(135deg) translate(-1px, 1px); + transition: transform .3s; } + .btn-toggle-chevron[disabled]::after { + border-top-color: #999999; + border-right-color: #999999; } + .btn-toggle-chevron:enabled:active::after, .btn-toggle-chevron:enabled[aria-expanded="true"]::after { + transform: rotate(315deg) translate(-1px, 1px); } + .btn-toggle-chevron:enabled:hover::after, .btn-toggle-chevron:enabled:focus::after, .btn-toggle-chevron:enabled:active::after, .btn-toggle-chevron:enabled[aria-expanded="true"]::after { + border-top-color: #ffffff; + border-right-color: #ffffff; } + .btn-dropdown-toggle { white-space: nowrap; overflow: hidden; @@ -5409,10 +5451,13 @@ table.preprocessing-test-results .rel-container { width: 22px; height: 22px; content: ''; } + .btn-dropdown-toggle[class*='icon-']::after { + top: 6px; + right: 2px; } .btn-dropdown-toggle::after { position: absolute; - top: 6px; - right: 2px; + top: 7px; + right: 6px; width: 10px; height: 10px; content: ''; diff --git a/ui/host_discovery.php b/ui/host_discovery.php index 2e53f7ae9b0..5bca459fd24 100644 --- a/ui/host_discovery.php +++ b/ui/host_discovery.php @@ -815,7 +815,7 @@ else { // Select LLD rules. $options = [ 'output' => API_OUTPUT_EXTEND, - 'selectHosts' => ['hostid', 'name', 'status'], + 'selectHosts' => ['hostid', 'name', 'status', 'flags'], 'selectItems' => API_OUTPUT_COUNT, 'selectGraphs' => API_OUTPUT_COUNT, 'selectTriggers' => API_OUTPUT_COUNT, diff --git a/ui/hostgroups.php b/ui/hostgroups.php index 53e07e10594..6bf8dc209a6 100644 --- a/ui/hostgroups.php +++ b/ui/hostgroups.php @@ -160,7 +160,6 @@ elseif (hasRequest('action')) { elseif (getRequest('action') == 'hostgroup.massenable' || getRequest('action') == 'hostgroup.massdisable') { $enable = (getRequest('action') == 'hostgroup.massenable'); $status = $enable ? HOST_STATUS_MONITORED : HOST_STATUS_NOT_MONITORED; - $auditAction = $enable ? AUDIT_ACTION_ENABLE : AUDIT_ACTION_DISABLE; $groupIds = getRequest('groups', []); @@ -180,14 +179,6 @@ elseif (hasRequest('action')) { 'hosts' => $hosts, 'status' => $status ]); - - if ($result) { - foreach ($hosts as $host) { - add_audit_ext($auditAction, AUDIT_RESOURCE_HOST, $host['hostid'], $host['host'], 'hosts', - ['status' => $host['status']], ['status' => $status] - ); - } - } } $result = DBend($result); diff --git a/ui/hosts.php b/ui/hosts.php index 74e360f6484..d1bed15beff 100644 --- a/ui/hosts.php +++ b/ui/hosts.php @@ -839,16 +839,9 @@ elseif (hasRequest('add') || hasRequest('update')) { } if ($create) { - $hostIds = API::Host()->create($host); - - if ($hostIds) { - $hostId = reset($hostIds['hostids']); - } - else { + if (!API::Host()->create($host)) { throw new Exception(); } - - add_audit_ext(AUDIT_ACTION_ADD, AUDIT_RESOURCE_HOST, $hostId, $host['host'], null, null, null); } else { $host['hostid'] = $hostId; @@ -856,16 +849,6 @@ elseif (hasRequest('add') || hasRequest('update')) { if (!API::Host()->update($host)) { throw new Exception(); } - - $dbHostNew = API::Host()->get([ - 'output' => API_OUTPUT_EXTEND, - 'hostids' => $hostId, - 'editable' => true - ]); - $dbHostNew = reset($dbHostNew); - - add_audit_ext(AUDIT_ACTION_UPDATE, AUDIT_RESOURCE_HOST, $dbHostNew['hostid'], $dbHostNew['host'], 'hosts', - $dbHost, $dbHostNew); } // full clone @@ -1001,13 +984,14 @@ elseif (hasRequest('hosts') && hasRequest('action') && str_in_array(getRequest(' 'templated_hosts' => true, 'output' => ['hostid'] ]); - $actHosts = zbx_objectValues($actHosts, 'hostid'); if ($actHosts) { - DBstart(); + foreach ($actHosts as &$host) { + $host['status'] = $status; + } + unset($host); - $result = updateHostStatus($actHosts, $status); - $result = DBend($result); + $result = (bool) API::Host()->update($actHosts); if ($result) { uncheckTableRows(); diff --git a/ui/include/classes/api/CAudit.php b/ui/include/classes/api/CAudit.php index b7fbee571ed..c12fb3dddf1 100644 --- a/ui/include/classes/api/CAudit.php +++ b/ui/include/classes/api/CAudit.php @@ -36,6 +36,7 @@ class CAudit { AUDIT_RESOURCE_DISCOVERY_RULE => ['druleid', 'name', 'drules'], AUDIT_RESOURCE_GRAPH => ['graphid', 'name', 'graphs'], AUDIT_RESOURCE_GRAPH_PROTOTYPE => ['graphid', 'name', 'graphs'], + AUDIT_RESOURCE_HOST => ['hostid', 'name', 'hosts'], AUDIT_RESOURCE_HOST_GROUP => ['groupid', 'name', 'groups'], AUDIT_RESOURCE_HOST_PROTOTYPE => ['hostid', 'host', 'hosts'], AUDIT_RESOURCE_HOUSEKEEPING => ['configid', null, 'config'], diff --git a/ui/include/classes/api/services/CConfiguration.php b/ui/include/classes/api/services/CConfiguration.php index 6bcb32006b4..f88b0940205 100644 --- a/ui/include/classes/api/services/CConfiguration.php +++ b/ui/include/classes/api/services/CConfiguration.php @@ -31,7 +31,8 @@ class CConfiguration extends CApiService { */ public function export(array $params) { $api_input_rules = ['type' => API_OBJECT, 'fields' => [ - 'format' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'in' => implode(',', [CExportWriterFactory::XML, CExportWriterFactory::JSON])], + 'format' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'in' => implode(',', [CExportWriterFactory::YAML, CExportWriterFactory::XML, CExportWriterFactory::JSON])], + 'prettyprint' => ['type' => API_BOOLEAN, 'default' => false], 'options' => ['type' => API_OBJECT, 'flags' => API_REQUIRED, 'fields' => [ 'groups' => ['type' => API_IDS], 'hosts' => ['type' => API_IDS], @@ -47,10 +48,34 @@ class CConfiguration extends CApiService { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } + switch ($params['format']) { + case CExportWriterFactory::YAML: + $lib_yaml = (new CFrontendSetup())->checkPhpLibYAML(); + + if ($lib_yaml['result'] == CFrontendSetup::CHECK_FATAL) { + self::exception(ZBX_API_ERROR_INTERNAL, $lib_yaml['error']); + } + break; + + case CExportWriterFactory::XML: + $lib_xml = (new CFrontendSetup())->checkPhpLibxml(); + + if ($lib_xml['result'] == CFrontendSetup::CHECK_FATAL) { + self::exception(ZBX_API_ERROR_INTERNAL, $lib_xml['error']); + } + + $xml_writer = (new CFrontendSetup())->checkPhpXmlWriter(); + + if ($xml_writer['result'] == CFrontendSetup::CHECK_FATAL) { + self::exception(ZBX_API_ERROR_INTERNAL, $xml_writer['error']); + } + break; + } + $export = new CConfigurationExport($params['options']); $export->setBuilder(new CConfigurationExportBuilder()); $writer = CExportWriterFactory::getWriter($params['format']); - $writer->formatOutput(false); + $writer->formatOutput($params['prettyprint']); $export->setWriter($writer); $export_data = $export->export(); @@ -69,7 +94,7 @@ class CConfiguration extends CApiService { */ public function import($params) { $api_input_rules = ['type' => API_OBJECT, 'fields' => [ - 'format' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'in' => implode(',', [CImportReaderFactory::XML, CImportReaderFactory::JSON])], + 'format' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'in' => implode(',', [CImportReaderFactory::YAML, CImportReaderFactory::XML, CImportReaderFactory::JSON])], 'source' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED], 'rules' => ['type' => API_OBJECT, 'flags' => API_REQUIRED, 'fields' => [ 'applications' => ['type' => API_OBJECT, 'fields' => [ @@ -147,6 +172,30 @@ class CConfiguration extends CApiService { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } + switch ($params['format']) { + case CImportReaderFactory::YAML: + $lib_yaml = (new CFrontendSetup())->checkPhpLibYAML(); + + if ($lib_yaml['result'] == CFrontendSetup::CHECK_FATAL) { + self::exception(ZBX_API_ERROR_INTERNAL, $lib_yaml['error']); + } + break; + + case CImportReaderFactory::XML: + $lib_xml = (new CFrontendSetup())->checkPhpLibxml(); + + if ($lib_xml['result'] == CFrontendSetup::CHECK_FATAL) { + self::exception(ZBX_API_ERROR_INTERNAL, $lib_xml['error']); + } + + $xml_reader = (new CFrontendSetup())->checkPhpXmlReader(); + + if ($xml_reader['result'] == CFrontendSetup::CHECK_FATAL) { + self::exception(ZBX_API_ERROR_INTERNAL, $xml_reader['error']); + } + break; + } + $importReader = CImportReaderFactory::getReader($params['format']); $data = $importReader->read($params['source']); @@ -177,8 +226,8 @@ class CConfiguration extends CApiService { // Add default values in place of missed tags. $data = (new CDefaultImportConverter($schema))->convert($data); - // Normalize array keys. - $data = (new CArrayKeysImportConverter($schema))->convert($data); + // Normalize array keys and strings. + $data = (new CImportDataNormalizer($schema))->normalize($data); // Transform converter. $data = (new CTransformImportConverter($schema))->convert($data); diff --git a/ui/include/classes/api/services/CHost.php b/ui/include/classes/api/services/CHost.php index cacba3db1b9..00f0bb06a44 100644 --- a/ui/include/classes/api/services/CHost.php +++ b/ui/include/classes/api/services/CHost.php @@ -700,7 +700,7 @@ class CHost extends CHostGeneral { $hostids = []; $ins_tags = []; - foreach ($hosts as $host) { + foreach ($hosts as &$host) { // If visible name is not given or empty it should be set to host name. if (!array_key_exists('name', $host) || !trim($host['name'])) { $host['name'] = $host['host']; @@ -768,6 +768,9 @@ class CHost extends CHostGeneral { DB::insert('host_inventory', [$hostInventory], false); } } + unset($host); + + $this->addAuditBulk(AUDIT_ACTION_ADD, AUDIT_RESOURCE_HOST, $hosts); if ($ins_tags) { DB::insert('host_tag', $ins_tags); @@ -971,7 +974,10 @@ class CHost extends CHostGeneral { sort($hostids); $db_hosts = $this->get([ - 'output' => ['hostid', 'host'], + 'output' => ['hostid', 'proxy_hostid', 'host', 'status', 'ipmi_authtype', 'ipmi_privilege', 'ipmi_username', + 'ipmi_password', 'name', 'description', 'tls_connect', 'tls_accept', 'tls_issuer', 'tls_subject', + 'tls_psk_identity', 'tls_psk', 'inventory_mode' + ], 'hostids' => $hostids, 'editable' => true, 'preservekeys' => true @@ -1117,12 +1123,8 @@ class CHost extends CHostGeneral { $updateInventory['inventory_mode'] = $data['inventory_mode']; } - if (isset($data['status'])) { - $updateStatus = $data['status']; - } - unset($data['hosts'], $data['groups'], $data['interfaces'], $data['templates_clear'], $data['templates'], - $data['macros'], $data['inventory'], $data['inventory_mode'], $data['status']); + $data['macros'], $data['inventory'], $data['inventory_mode']); if (!zbx_empty($data)) { DB::update('hosts', [ @@ -1131,10 +1133,6 @@ class CHost extends CHostGeneral { ]); } - if (isset($updateStatus)) { - updateHostStatus($hostids, $updateStatus); - } - /* * Update template linkage */ @@ -1326,6 +1324,18 @@ class CHost extends CHostGeneral { } } + $new_hosts = []; + foreach ($db_hosts as $hostid => $db_host) { + $new_host = $data + $db_host; + if ($new_host['status'] != $db_host['status']) { + info(_s('Updated status of host "%1$s".', $new_host['host'])); + } + + $new_hosts[] = $new_host; + } + + $this->addAuditBulk(AUDIT_ACTION_UPDATE, AUDIT_RESOURCE_HOST, $new_hosts, $db_hosts); + return ['hostids' => $inputHostIds]; } diff --git a/ui/include/classes/export/CConfigurationExport.php b/ui/include/classes/export/CConfigurationExport.php index da061e0ca18..86ab460560e 100644 --- a/ui/include/classes/export/CConfigurationExport.php +++ b/ui/include/classes/export/CConfigurationExport.php @@ -136,7 +136,8 @@ class CConfigurationExport { try { $this->gatherData(); - $schema = (new CImportValidatorFactory('xml')) + // Parameter in CImportValidatorFactory is irrelavant here, since export does not validate data. + $schema = (new CImportValidatorFactory(CExportWriterFactory::YAML)) ->getObject(ZABBIX_EXPORT_VERSION) ->getSchema(); @@ -174,7 +175,7 @@ class CConfigurationExport { } if ($this->data['maps']) { - $this->builder->buildMaps($this->data['maps']); + $this->builder->buildMaps($schema['rules']['maps'], $this->data['maps']); } if ($this->data['mediaTypes']) { @@ -1442,10 +1443,14 @@ class CConfigurationExport { break; } - $selement['iconid_off'] = $selement['iconid_off'] > 0 ? $images[$selement['iconid_off']] : ''; - $selement['iconid_on'] = $selement['iconid_on'] > 0 ? $images[$selement['iconid_on']] : ''; - $selement['iconid_disabled'] = $selement['iconid_disabled'] > 0 ? $images[$selement['iconid_disabled']] : ''; - $selement['iconid_maintenance'] = $selement['iconid_maintenance'] > 0 ? $images[$selement['iconid_maintenance']] : ''; + $selement['iconid_off'] = ($selement['iconid_off'] > 0) ? $images[$selement['iconid_off']] : []; + $selement['iconid_on'] = ($selement['iconid_on'] > 0) ? $images[$selement['iconid_on']] : []; + $selement['iconid_disabled'] = ($selement['iconid_disabled'] > 0) + ? $images[$selement['iconid_disabled']] + : []; + $selement['iconid_maintenance'] = ($selement['iconid_maintenance'] > 0) + ? $images[$selement['iconid_maintenance']] + : []; } unset($selement); diff --git a/ui/include/classes/export/CConfigurationExportBuilder.php b/ui/include/classes/export/CConfigurationExportBuilder.php index 0e0596df9f4..b69458660c3 100644 --- a/ui/include/classes/export/CConfigurationExportBuilder.php +++ b/ui/include/classes/export/CConfigurationExportBuilder.php @@ -44,11 +44,11 @@ class CConfigurationExportBuilder { } /** - * Build XML data. + * Build data structure. * * @param array $schema Tag schema from validation class. * @param array $data Export data. - * @param string $main_tag XML tag (for error reporting). + * @param string $main_tag Main element (for error reporting). * * @return array */ @@ -66,6 +66,7 @@ class CConfigurationExportBuilder { $store = []; foreach ($rules as $tag => $val) { $is_required = $val['type'] & XML_REQUIRED; + $is_string = $val['type'] & XML_STRING; $is_array = $val['type'] & XML_ARRAY; $is_indexed_array = $val['type'] & XML_INDEXED_ARRAY; $has_data = array_key_exists($tag, $row); @@ -99,6 +100,10 @@ class CConfigurationExportBuilder { continue; } + if ($is_string && $value !== null) { + $value = str_replace("\r\n", "\n", $value); + } + if (array_key_exists('in', $val)) { if (!array_key_exists($value, $val['in'])) { throw new Exception(_s('Invalid tag "%1$s": %2$s.', $tag, @@ -338,9 +343,10 @@ class CConfigurationExportBuilder { /** * Format maps. * - * @param array $maps + * @param array $schema Tag schema from validation class. + * @param array $maps Export data. */ - public function buildMaps(array $maps) { + public function buildMaps(array $schema, array $maps) { $this->data['maps'] = []; CArrayHelper::sort($maps, ['name']); @@ -383,6 +389,8 @@ class CConfigurationExportBuilder { 'links' => $this->formatMapLinks($map['links'], $tmpSelements) ]; } + + $this->data['maps'] = $this->build($schema, $this->data['maps'], 'maps'); } /** diff --git a/ui/include/classes/export/writers/CExportWriterFactory.php b/ui/include/classes/export/writers/CExportWriterFactory.php index ed36c368f9f..d2e9235c70b 100644 --- a/ui/include/classes/export/writers/CExportWriterFactory.php +++ b/ui/include/classes/export/writers/CExportWriterFactory.php @@ -21,6 +21,7 @@ class CExportWriterFactory { + const YAML = 'yaml'; const XML = 'xml'; const JSON = 'json'; @@ -36,6 +37,9 @@ class CExportWriterFactory { */ public static function getWriter($type) { switch ($type) { + case self::YAML: + return new CYamlExportWriter(); + case self::XML: return new CXmlExportWriter(); @@ -46,4 +50,33 @@ class CExportWriterFactory { throw new Exception('Incorrect export writer type.'); } } + + /** + * Get content mime-type for specified type. + * + * @static + * @throws Exception + * + * @param string $type + * + * @return string + */ + public static function getMimeType(string $type): string { + switch ($type) { + case self::YAML: + // See https://github.com/rails/rails/blob/d41d586/actionpack/lib/action_dispatch/http/mime_types.rb#L39 + return 'text/yaml'; + + case self::XML: + // See https://www.ietf.org/rfc/rfc2376.txt + return 'text/xml'; + + case self::JSON: + // See https://www.ietf.org/rfc/rfc4627.txt + return 'application/json'; + + default: + throw new Exception('Incorrect export writer type.'); + } + } } diff --git a/ui/include/classes/export/writers/CJsonExportWriter.php b/ui/include/classes/export/writers/CJsonExportWriter.php index 3c2d530b36b..562b7dc4b75 100644 --- a/ui/include/classes/export/writers/CJsonExportWriter.php +++ b/ui/include/classes/export/writers/CJsonExportWriter.php @@ -32,6 +32,12 @@ class CJsonExportWriter extends CExportWriter { * @return string */ public function write(array $array) { - return json_encode($array, JSON_UNESCAPED_SLASHES); + $options = JSON_UNESCAPED_SLASHES; + + if ($this->formatOutput) { + $options |= JSON_PRETTY_PRINT; + } + + return json_encode($array, $options); } } diff --git a/ui/include/classes/export/writers/CYamlExportWriter.php b/ui/include/classes/export/writers/CYamlExportWriter.php new file mode 100644 index 00000000000..13b0f2cc140 --- /dev/null +++ b/ui/include/classes/export/writers/CYamlExportWriter.php @@ -0,0 +1,37 @@ +<?php declare(strict_types = 1); +/* +** Zabbix +** Copyright (C) 2001-2020 Zabbix SIA +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 2 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software +** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +**/ + + +/** + * Class for converting array with export data to YAML format. + */ +class CYamlExportWriter extends CExportWriter { + + /** + * Converts array with export data to YAML format. + * + * @param array $array + * + * @return string + */ + public function write(array $array): string { + return yaml_emit($array, YAML_UTF8_ENCODING, YAML_LN_BREAK); + } +} diff --git a/ui/include/classes/html/CActionButtonList.php b/ui/include/classes/html/CActionButtonList.php index f3c0b79fdba..e5d7ae9eab7 100644 --- a/ui/include/classes/html/CActionButtonList.php +++ b/ui/include/classes/html/CActionButtonList.php @@ -62,6 +62,7 @@ class CActionButtonList extends CObject { * @param string $buttons_data[]['confirm'] Confirmation text (optional). * @param string $buttons_data[]['redirect'] Redirect URL (optional). * @param bool $buttons_data[]['disabled'] Set button state disabled (optional). + * @param CTag $buttons_data[]['content'] A HTML tag. For example a CButton wrapped in CList object. * @param string|null $name_prefix Prefix for sessionStorage used for storing currently selected * checkboxes. */ @@ -70,42 +71,47 @@ class CActionButtonList extends CObject { $this->name_prefix = $name_prefix ? $name_prefix : null; foreach ($buttons_data as $action => $button_data) { - $button = (new CSubmit($action_name, $button_data['name'])) - ->addClass(ZBX_STYLE_BTN_ALT) - ->removeAttribute('id'); - - if (array_key_exists('redirect', $button_data)) { - $button - // Removing parameters not to conflict with the redirecting URL. - ->removeAttribute('name') - ->removeAttribute('value') - ->onClick('var $_form = jQuery(this).closest("form");'. - // Save the original form action. - 'if (!$_form.data("action")) {'. - '$_form.data("action", $_form.attr("action"));'. - '}'. - '$_form.attr("action", '.json_encode($button_data['redirect']).');' - ); + if (array_key_exists('content', $button_data)) { + $button = $button_data['content']; } else { - $button - ->setAttribute('value', $action) - ->onClick('var $_form = jQuery(this).closest("form");'. - // Restore the original form action, if previously saved. - 'if ($_form.data("action")) {'. - '$_form.attr("action", $_form.data("action"));'. - '}' - ); - } - - if (array_key_exists('disabled', $button_data)) { - $button - ->setEnabled(!$button_data['disabled']) - ->setAttribute('data-disabled', $button_data['disabled']); - } - - if (array_key_exists('confirm', $button_data)) { - $button->setAttribute('confirm', $button_data['confirm']); + $button = (new CSubmit($action_name, $button_data['name'])) + ->addClass(ZBX_STYLE_BTN_ALT) + ->removeAttribute('id'); + + if (array_key_exists('redirect', $button_data)) { + $button + // Removing parameters not to conflict with the redirecting URL. + ->removeAttribute('name') + ->removeAttribute('value') + ->onClick('var $_form = jQuery(this).closest("form");'. + // Save the original form action. + 'if (!$_form.data("action")) {'. + '$_form.data("action", $_form.attr("action"));'. + '}'. + '$_form.attr("action", '.json_encode($button_data['redirect']).');' + ); + } + else { + $button + ->setAttribute('value', $action) + ->onClick('var $_form = jQuery(this).closest("form");'. + // Restore the original form action, if previously saved. + 'if ($_form.data("action")) {'. + '$_form.attr("action", $_form.data("action"));'. + '}' + ); + } + + if (array_key_exists('disabled', $button_data)) { + $button + ->setEnabled(!$button_data['disabled']) + ->setAttribute('data-disabled', $button_data['disabled']); + } + + if (array_key_exists('confirm', $button_data)) { + $button->setAttribute('confirm', $button_data['confirm']); + } } $this->buttons[$action] = $button; diff --git a/ui/include/classes/html/CButtonDropdown.php b/ui/include/classes/html/CButtonDropdown.php index b90f50e225b..4ca0518716d 100644 --- a/ui/include/classes/html/CButtonDropdown.php +++ b/ui/include/classes/html/CButtonDropdown.php @@ -29,7 +29,6 @@ class CButtonDropdown extends CButton { /** * Button style names. */ - public const ZBX_STYLE_BTN_TOGGLE = 'btn-dropdown-toggle'; public const ZBX_STYLE_BTN_VALUE = 'dropdown-value'; /** @@ -54,7 +53,7 @@ class CButtonDropdown extends CButton { $this->setId(uniqid('btn-dropdown-')); $this->addClass(ZBX_STYLE_BTN_ALT); - $this->addClass(self::ZBX_STYLE_BTN_TOGGLE); + $this->addClass(ZBX_STYLE_BTN_TOGGLE); $this->dropdown_items = $items; if ($value !== null) { @@ -74,7 +73,10 @@ class CButtonDropdown extends CButton { ->setId(zbx_formatDomId($name.'[btn]')) ->setMenuPopup([ 'type' => 'dropdown', - 'data' => ['items' => $this->dropdown_items] + 'data' => [ + 'items' => $this->dropdown_items, + 'toggle_class' => ZBX_STYLE_BTN_TOGGLE + ] ])) ->addItem((new CInput('hidden', $name, $this->getAttribute('value'))) ->addClass(self::ZBX_STYLE_BTN_VALUE) diff --git a/ui/include/classes/html/CButtonExport.php b/ui/include/classes/html/CButtonExport.php new file mode 100644 index 00000000000..093efb78a45 --- /dev/null +++ b/ui/include/classes/html/CButtonExport.php @@ -0,0 +1,89 @@ +<?php declare(strict_types = 1); +/* +** Zabbix +** Copyright (C) 2001-2020 Zabbix SIA +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 2 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software +** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +**/ + + +class CButtonExport extends CList { + + /** + * Create CButtonExport instance. + * + * @param string $action Export controller action. + * @param string $back_url URL to redirect back to once export is complete. + */ + public function __construct(string $action, string $back_url) { + parent::__construct([ + (new CSubmit('export', _('Export'))) + ->removeAttribute('id') + ->removeAttribute('name') + ->removeAttribute('value') + ->addClass(ZBX_STYLE_BTN_ALT) + ->onClick('var $_form = jQuery(this).closest("form");'. + // Save the original form action. + 'if (!$_form.data("action")) {'. + '$_form.data("action", $_form.attr("action"));'. + '}'. + '$_form.attr("action", '.json_encode( + (new CUrl('zabbix.php')) + ->setArgument('action', $action) + ->setArgument('format', CExportWriterFactory::YAML) + ->setArgument('backurl', $back_url) + ->getUrl() + ).');' + ), + (new CButton('export', '​')) + ->addClass(ZBX_STYLE_BTN_ALT) + ->addClass(ZBX_STYLE_BTN_TOGGLE_CHEVRON) + ->setMenuPopup([ + 'type' => 'dropdown', + 'data' => [ + 'submit_form' => true, + 'items' => [ + [ + 'label' => _('YAML'), + 'url' => (new CUrl('zabbix.php')) + ->setArgument('action', $action) + ->setArgument('format', CExportWriterFactory::YAML) + ->setArgument('backurl', $back_url) + ->getUrl() + ], + [ + 'label' => _('XML'), + 'url' => (new CUrl('zabbix.php')) + ->setArgument('action', $action) + ->setArgument('format', CExportWriterFactory::XML) + ->setArgument('backurl', $back_url) + ->getUrl() + ], + [ + 'label' => _('JSON'), + 'url' => (new CUrl('zabbix.php')) + ->setArgument('action', $action) + ->setArgument('format', CExportWriterFactory::JSON) + ->setArgument('backurl', $back_url) + ->getUrl() + ] + ] + ] + ]) + ]); + + $this->addClass(ZBX_STYLE_BTN_SPLIT); + } +} diff --git a/ui/include/classes/import/converters/CArrayKeysImportConverter.php b/ui/include/classes/import/converters/CImportDataNormalizer.php index ddaf6c8f657..b0a57683ded 100644 --- a/ui/include/classes/import/converters/CArrayKeysImportConverter.php +++ b/ui/include/classes/import/converters/CImportDataNormalizer.php @@ -1,4 +1,4 @@ -<?php +<?php declare(strict_types=1); /* ** Zabbix ** Copyright (C) 2001-2020 Zabbix SIA @@ -20,18 +20,21 @@ /** - * Convert array keys to numeric. + * Class to normalize incoming data. */ -class CArrayKeysImportConverter extends CConverter { +class CImportDataNormalizer { protected $rules; + const EOL_LF = 0x01; + public function __construct(array $schema) { $this->rules = $schema; } - public function convert($data) { + public function normalize($data) { $data['zabbix_export'] = $this->normalizeArrayKeys($data['zabbix_export'], $this->rules); + $data['zabbix_export'] = $this->normalizeStrings($data['zabbix_export']); return $data; } @@ -40,9 +43,9 @@ class CArrayKeysImportConverter extends CConverter { * Convert array keys to numeric. * * @param mixed $data Import data. - * @param array $rules XML rules. + * @param array $rules Schema rules. * - * @return array + * @return mixed */ protected function normalizeArrayKeys($data, array $rules) { if (!is_array($data)) { @@ -72,4 +75,22 @@ class CArrayKeysImportConverter extends CConverter { return $data; } + + /** + * Add CR to string type fields. + * + * @param mixed $data Import data. + * + * @return mixed + */ + protected function normalizeStrings($data) { + if ($this->rules['type'] & XML_STRING) { + $data = str_replace("\r\n", "\n", $data); + $data = (array_key_exists('flags', $this->rules) && $this->rules['flags'] & self::EOL_LF) + ? $data + : str_replace("\n", "\r\n", $data); + } + + return $data; + } } diff --git a/ui/include/classes/import/importers/CMapImporter.php b/ui/include/classes/import/importers/CMapImporter.php index 2601eae46d7..a2226477e19 100644 --- a/ui/include/classes/import/importers/CMapImporter.php +++ b/ui/include/classes/import/importers/CMapImporter.php @@ -187,7 +187,7 @@ class CMapImporter extends CImporter { 'icon_off' => 'iconid_off', 'icon_on' => 'iconid_on', 'icon_disabled' => 'iconid_disabled', - 'icon_maintenance' => 'iconid_maintenance', + 'icon_maintenance' => 'iconid_maintenance' ]; foreach ($icons as $element => $field) { if (array_key_exists($element, $selement)) { diff --git a/ui/include/classes/import/readers/CImportReaderFactory.php b/ui/include/classes/import/readers/CImportReaderFactory.php index fff3cc4fda0..56bf1a0bd9c 100644 --- a/ui/include/classes/import/readers/CImportReaderFactory.php +++ b/ui/include/classes/import/readers/CImportReaderFactory.php @@ -21,6 +21,7 @@ class CImportReaderFactory { + const YAML = 'yaml'; const XML = 'xml'; const JSON = 'json'; @@ -36,10 +37,15 @@ class CImportReaderFactory { */ public static function getReader($format) { switch ($format) { - case 'xml': + case self::YAML: + return new CYamlImportReader(); + + case self::XML: return new CXmlImportReader(); - case 'json': + + case self::JSON: return new CJsonImportReader(); + default: throw new Exception(_s('Unsupported import format "%1$s".', $format)); } @@ -57,13 +63,18 @@ class CImportReaderFactory { */ public static function fileExt2ImportFormat($ext) { switch ($ext) { + case 'yaml': + case 'yml': + return CImportReaderFactory::YAML; + case 'xml': return CImportReaderFactory::XML; + case 'json': return CImportReaderFactory::JSON; + default: throw new Exception(_s('Unsupported import file extension "%1$s".', $ext)); } - } } diff --git a/ui/include/classes/import/readers/CYamlImportReader.php b/ui/include/classes/import/readers/CYamlImportReader.php new file mode 100644 index 00000000000..8aad995f897 --- /dev/null +++ b/ui/include/classes/import/readers/CYamlImportReader.php @@ -0,0 +1,66 @@ +<?php declare(strict_types = 1); +/* +** Zabbix +** Copyright (C) 2001-2020 Zabbix SIA +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 2 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software +** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +**/ + + +/** + * Class for converting YAML data stream to PHP array. + */ +class CYamlImportReader extends CImportReader { + + /** + * Convert YAML data stream to PHP array. Suppress PHP notices when executing yaml_parse() with custom + * error handler. Display only first error since that is where the syntax in file is incorrect. + * + * @param string $string + * + * @throws ErrorException + * + * @return array + */ + public function read($string): array { + $error = ''; + + set_error_handler(function ($errno, $errstr) use (&$error) { + if ($error === '' && $errstr !== '') { + $error = str_replace('yaml_parse(): ', '', $errstr); + } + }); + + $data = yaml_parse($string); + + restore_error_handler(); + + /* + * Unfortunately yaml_parse() not always returns FALSE. If file is empty, it returns NULL and if file contains + * gibberish and not a "zabbix_export" array, $data contains same input string, but Import Validator expects + * $data to be an array. Create a custom error message for these cases. + */ + if (!is_array($data) && $data !== false) { + $data = false; + $error = _('Invalid file content'); + } + + if ($data === false) { + throw new ErrorException(_s('Cannot read YAML: %1$s.', $error)); + } + + return $data; + } +} diff --git a/ui/include/classes/import/validators/C50XmlValidator.php b/ui/include/classes/import/validators/C50XmlValidator.php index 7b9a8653c36..43c7965067b 100644 --- a/ui/include/classes/import/validators/C50XmlValidator.php +++ b/ui/include/classes/import/validators/C50XmlValidator.php @@ -457,7 +457,7 @@ class C50XmlValidator { 'preprocessing' => ['type' => XML_INDEXED_ARRAY, 'prefix' => 'step', 'rules' => [ 'step' => ['type' => XML_ARRAY, 'rules' => [ 'type' => ['type' => XML_STRING | XML_REQUIRED, 'in' => $this->PREPROCESSING_STEP_TYPE], - 'params' => ['type' => XML_STRING | XML_REQUIRED], + 'params' => ['type' => XML_STRING | XML_REQUIRED, 'flags' => CImportDataNormalizer::EOL_LF], 'error_handler' => ['type' => XML_STRING, 'default' => CXmlConstantValue::ORIGINAL_ERROR, 'in' => $this->ITEM_PREPROCESSING_ERROR_HANDLER], 'error_handler_params' => ['type' => XML_STRING, 'default' => ''] ]] @@ -598,7 +598,7 @@ class C50XmlValidator { 'preprocessing' => ['type' => XML_INDEXED_ARRAY, 'prefix' => 'step', 'rules' => [ 'step' => ['type' => XML_ARRAY, 'rules' => [ 'type' => ['type' => XML_STRING | XML_REQUIRED, 'in' => $this->PREPROCESSING_STEP_TYPE], - 'params' => ['type' => XML_STRING | XML_REQUIRED], + 'params' => ['type' => XML_STRING | XML_REQUIRED, 'flags' => CImportDataNormalizer::EOL_LF], 'error_handler' => ['type' => XML_STRING, 'default' => CXmlConstantValue::ORIGINAL_ERROR, 'in' => $this->ITEM_PREPROCESSING_ERROR_HANDLER], 'error_handler_params' => ['type' => XML_STRING, 'default' => ''] ]] @@ -811,7 +811,7 @@ class C50XmlValidator { 'preprocessing' => ['type' => XML_INDEXED_ARRAY, 'prefix' => 'step', 'rules' => [ 'step' => ['type' => XML_ARRAY, 'rules' => [ 'type' => ['type' => XML_STRING | XML_REQUIRED, 'in' => $this->PREPROCESSING_STEP_TYPE_DRULE], - 'params' => ['type' => XML_STRING | XML_REQUIRED], + 'params' => ['type' => XML_STRING | XML_REQUIRED, 'flags' => CImportDataNormalizer::EOL_LF], 'error_handler' => ['type' => XML_STRING, 'default' => CXmlConstantValue::ORIGINAL_ERROR, 'in' => $this->ITEM_PREPROCESSING_ERROR_HANDLER], 'error_handler_params' => ['type' => XML_STRING, 'default' => ''] ]] @@ -1068,7 +1068,7 @@ class C50XmlValidator { 'preprocessing' => ['type' => XML_INDEXED_ARRAY, 'prefix' => 'step', 'rules' => [ 'step' => ['type' => XML_ARRAY, 'rules' => [ 'type' => ['type' => XML_STRING | XML_REQUIRED, 'in' => $this->PREPROCESSING_STEP_TYPE], - 'params' => ['type' => XML_STRING | XML_REQUIRED], + 'params' => ['type' => XML_STRING | XML_REQUIRED, 'flags' => CImportDataNormalizer::EOL_LF], 'error_handler' => ['type' => XML_STRING, 'default' => CXmlConstantValue::ORIGINAL_ERROR, 'in' => $this->ITEM_PREPROCESSING_ERROR_HANDLER], 'error_handler_params' => ['type' => XML_STRING, 'default' => ''] ]] @@ -1207,7 +1207,7 @@ class C50XmlValidator { 'preprocessing' => ['type' => XML_INDEXED_ARRAY, 'prefix' => 'step', 'rules' => [ 'step' => ['type' => XML_ARRAY, 'rules' => [ 'type' => ['type' => XML_STRING | XML_REQUIRED, 'in' => $this->PREPROCESSING_STEP_TYPE], - 'params' => ['type' => XML_STRING | XML_REQUIRED], + 'params' => ['type' => XML_STRING | XML_REQUIRED, 'flags' => CImportDataNormalizer::EOL_LF], 'error_handler' => ['type' => XML_STRING, 'default' => CXmlConstantValue::ORIGINAL_ERROR, 'in' => $this->ITEM_PREPROCESSING_ERROR_HANDLER], 'error_handler_params' => ['type' => XML_STRING, 'default' => ''] ]] @@ -1418,7 +1418,7 @@ class C50XmlValidator { 'preprocessing' => ['type' => XML_INDEXED_ARRAY, 'prefix' => 'step', 'rules' => [ 'step' => ['type' => XML_ARRAY, 'rules' => [ 'type' => ['type' => XML_STRING | XML_REQUIRED, 'in' => $this->PREPROCESSING_STEP_TYPE_DRULE], - 'params' => ['type' => XML_STRING | XML_REQUIRED], + 'params' => ['type' => XML_STRING | XML_REQUIRED, 'flags' => CImportDataNormalizer::EOL_LF], 'error_handler' => ['type' => XML_STRING, 'default' => CXmlConstantValue::ORIGINAL_ERROR, 'in' => $this->ITEM_PREPROCESSING_ERROR_HANDLER], 'error_handler_params' => ['type' => XML_STRING, 'default' => ''] ]] @@ -2259,7 +2259,7 @@ class C50XmlValidator { switch ($data['type']) { case CXmlConstantName::SCRIPT: case CXmlConstantValue::MEDIA_TYPE_SCRIPT: - return ['type' => XML_STRING, 'default' => '', 'preprocessor' => [$this, 'scriptParameterPreprocessor'], 'export' => [$this, 'scriptParameterExport']]; + return ['type' => XML_STRING, 'flags' => CImportDataNormalizer::EOL_LF, 'default' => '', 'preprocessor' => [$this, 'scriptParameterPreprocessor'], 'export' => [$this, 'scriptParameterExport']]; case CXmlConstantName::WEBHOOK: case CXmlConstantValue::MEDIA_TYPE_WEBHOOK: diff --git a/ui/include/classes/import/validators/CXmlValidatorGeneral.php b/ui/include/classes/import/validators/CXmlValidatorGeneral.php index 62817d13064..99c299b6cab 100644 --- a/ui/include/classes/import/validators/CXmlValidatorGeneral.php +++ b/ui/include/classes/import/validators/CXmlValidatorGeneral.php @@ -147,11 +147,12 @@ class CXmlValidatorGeneral { } switch ($this->format) { - case 'xml': + case CImportReaderFactory::XML: $is_valid_tag = ($tag === $prefix.($index == 0 ? '' : $index) || $tag === $index); break; - case 'json': + case CImportReaderFactory::YAML: + case CImportReaderFactory::JSON: $is_valid_tag = ctype_digit(strval($tag)); break; diff --git a/ui/include/classes/mvc/CRouter.php b/ui/include/classes/mvc/CRouter.php index 922a32422d7..ad901204013 100644 --- a/ui/include/classes/mvc/CRouter.php +++ b/ui/include/classes/mvc/CRouter.php @@ -85,12 +85,12 @@ class CRouter { 'dashboard.widget.rfrate' => ['CControllerDashboardWidgetRfRate', 'layout.json', null], 'dashboard.widget.sanitize' => ['CControllerDashboardWidgetSanitize', 'layout.json', null], 'discovery.view' => ['CControllerDiscoveryView', 'layout.htmlpage', 'monitoring.discovery.view'], - 'export.hosts.xml' => ['CControllerExportXml', 'layout.xml', null], - 'export.mediatypes.xml' => ['CControllerExportXml', 'layout.xml', null], - 'export.screens.xml' => ['CControllerExportXml', 'layout.xml', null], - 'export.sysmaps.xml' => ['CControllerExportXml', 'layout.xml', null], - 'export.templates.xml' => ['CControllerExportXml', 'layout.xml', null], - 'export.valuemaps.xml' => ['CControllerExportXml', 'layout.xml', null], + 'export.hosts' => ['CControllerExport', 'layout.export', null], + 'export.mediatypes' => ['CControllerExport', 'layout.export', null], + 'export.screens' => ['CControllerExport', 'layout.export', null], + 'export.sysmaps' => ['CControllerExport', 'layout.export', null], + 'export.templates' => ['CControllerExport', 'layout.export', null], + 'export.valuemaps' => ['CControllerExport', 'layout.export', null], 'favourite.create' => ['CControllerFavouriteCreate', 'layout.javascript', null], 'favourite.delete' => ['CControllerFavouriteDelete', 'layout.javascript', null], 'gui.edit' => ['CControllerGuiEdit', 'layout.htmlpage', 'administration.gui.edit'], diff --git a/ui/include/classes/setup/CFrontendSetup.php b/ui/include/classes/setup/CFrontendSetup.php index 4f26b734db0..ca7585525ac 100644 --- a/ui/include/classes/setup/CFrontendSetup.php +++ b/ui/include/classes/setup/CFrontendSetup.php @@ -33,6 +33,7 @@ class CFrontendSetup { const MIN_PHP_MAX_INPUT_TIME = 300; const MIN_PHP_GD_VERSION = '2.0'; const MIN_PHP_LIBXML_VERSION = '2.6.15'; + const MIN_PHP_LIBYAML_VERION = '2.0.2'; // See https://pecl.php.net/package/yaml const REQUIRED_PHP_ARG_SEPARATOR_OUTPUT = '&'; /** @@ -76,10 +77,12 @@ class CFrontendSetup { $result[] = $this->checkPhpGdJpeg(); $result[] = $this->checkPhpGdGif(); $result[] = $this->checkPhpGdFreeType(); + $result[] = $this->checkPhpLibYAML(); $result[] = $this->checkPhpLibxml(); $result[] = $this->checkPhpXmlWriter(); $result[] = $this->checkPhpXmlReader(); $result[] = $this->checkPhpLdapModule(); + $result[] = $this->checkPhpOpenSsl(); $result[] = $this->checkPhpCtype(); $result[] = $this->checkPhpSession(); $result[] = $this->checkPhpSessionAutoStart(); @@ -461,6 +464,27 @@ class CFrontendSetup { } /** + * Checks for PHP LibYAML extension. + * + * @return array + */ + public function checkPhpLibYAML(): array { + if (!$current = phpversion('yaml')) { + $current = _('unknown'); + } + + $check = version_compare($current, self::MIN_PHP_LIBYAML_VERION, '>='); + + return [ + 'name' => _('PHP LibYAML'), + 'current' => $current, + 'required' => self::MIN_PHP_LIBYAML_VERION, + 'result' => $check ? self::CHECK_OK : self::CHECK_FATAL, + 'error' => _('PHP LibYAML extension missing.') + ]; + } + + /** * Checks for PHP libxml extension. * * @return array @@ -535,6 +559,23 @@ class CFrontendSetup { } /** + * Checks for PHP OpenSSL extension. + * + * @return array + */ + public function checkPhpOpenSsl() { + $current = extension_loaded('openssl'); + + return [ + 'name' => _('PHP OpenSSL'), + 'current' => $current ? _('on') : _('off'), + 'required' => null, + 'result' => $current ? self::CHECK_OK : self::CHECK_WARNING, + 'error' => _('PHP OpenSSL extension missing.') + ]; + } + + /** * Checks for PHP ctype extension. * * @return array diff --git a/ui/include/defines.inc.php b/ui/include/defines.inc.php index 20eb72d2d79..a0780555f67 100644 --- a/ui/include/defines.inc.php +++ b/ui/include/defines.inc.php @@ -1251,16 +1251,6 @@ define('SERVER_CHECK_INTERVAL', 10); define('DATE_TIME_FORMAT_SECONDS_XML', 'Y-m-d\TH:i:s\Z'); -// XML export|import tags -define('XML_TAG_MACRO', 'macro'); -define('XML_TAG_HOST', 'host'); -define('XML_TAG_HOSTINVENTORY', 'host_inventory'); -define('XML_TAG_ITEM', 'item'); -define('XML_TAG_TRIGGER', 'trigger'); -define('XML_TAG_GRAPH', 'graph'); -define('XML_TAG_GRAPH_ELEMENT', 'graph_element'); -define('XML_TAG_DEPENDENCY', 'dependency'); - define('ZBX_DEFAULT_IMPORT_HOST_GROUP', 'Imported hosts'); // XML import flags @@ -1564,6 +1554,9 @@ define('ZBX_STYLE_ARROW_UP', 'arrow-up'); define('ZBX_STYLE_BLUE', 'blue'); define('ZBX_STYLE_BTN_ADD_FAV', 'btn-add-fav'); define('ZBX_STYLE_BTN_ALT', 'btn-alt'); +define('ZBX_STYLE_BTN_TOGGLE_CHEVRON', 'btn-toggle-chevron'); +define('ZBX_STYLE_BTN_SPLIT', 'btn-split'); +define('ZBX_STYLE_BTN_TOGGLE', 'btn-dropdown-toggle'); define('ZBX_STYLE_BTN_BACK_MAP', 'btn-back-map'); define('ZBX_STYLE_BTN_BACK_MAP_CONTAINER', 'btn-back-map-container'); define('ZBX_STYLE_BTN_BACK_MAP_CONTENT', 'btn-back-map-content'); diff --git a/ui/include/hosts.inc.php b/ui/include/hosts.inc.php index ed8f83a4cfe..43a103bb9fc 100644 --- a/ui/include/hosts.inc.php +++ b/ui/include/hosts.inc.php @@ -533,33 +533,6 @@ function get_host_by_hostid($hostid, $no_error_message = 0) { return false; } -function updateHostStatus($hostids, $status) { - zbx_value2array($hostids); - - $hostIds = []; - $oldStatus = ($status == HOST_STATUS_MONITORED ? HOST_STATUS_NOT_MONITORED : HOST_STATUS_MONITORED); - - $db_hosts = DBselect( - 'SELECT h.hostid,h.host,h.status'. - ' FROM hosts h'. - ' WHERE '.dbConditionInt('h.hostid', $hostids). - ' AND h.status='.zbx_dbstr($oldStatus) - ); - while ($host = DBfetch($db_hosts)) { - $hostIds[] = $host['hostid']; - - $host_new = $host; - $host_new['status'] = $status; - add_audit_ext(AUDIT_ACTION_UPDATE, AUDIT_RESOURCE_HOST, $host['hostid'], $host['host'], 'hosts', $host, $host_new); - info(_s('Updated status of host "%1$s".', $host['host'])); - } - - return DB::update('hosts', [ - 'values' => ['status' => $status], - 'where' => ['hostid' => $hostIds] - ]); -} - /** * Get parent templates for each given application. * diff --git a/ui/include/views/configuration.host.discovery.list.php b/ui/include/views/configuration.host.discovery.list.php index d9a8c7f9d00..c0c2f56bbce 100644 --- a/ui/include/views/configuration.host.discovery.list.php +++ b/ui/include/views/configuration.host.discovery.list.php @@ -263,12 +263,14 @@ foreach ($data['discoveries'] as $discovery) { ), CViewHelper::showNum($discovery['graphs']) ], - [ - new CLink(_('Host prototypes'), - (new CUrl('host_prototypes.php'))->setArgument('parent_discoveryid', $discovery['itemid']) - ), - CViewHelper::showNum($discovery['hostPrototypes']) - ], + ($discovery['hosts'][0]['flags'] == ZBX_FLAG_DISCOVERY_NORMAL) + ? [ + new CLink(_('Host prototypes'), + (new CUrl('host_prototypes.php'))->setArgument('parent_discoveryid', $discovery['itemid']) + ), + CViewHelper::showNum($discovery['hostPrototypes']) + ] + : '', (new CDiv(CHtml::encode($discovery['key_'])))->addClass(ZBX_STYLE_WORDWRAP), $discovery['delay'], item_type2str($discovery['type']), diff --git a/ui/include/views/configuration.host.list.php b/ui/include/views/configuration.host.list.php index 92fbecbc611..b8df73cc781 100644 --- a/ui/include/views/configuration.host.list.php +++ b/ui/include/views/configuration.host.list.php @@ -468,13 +468,12 @@ $form->addItem([ [ 'host.massenable' => ['name' => _('Enable'), 'confirm' => _('Enable selected hosts?')], 'host.massdisable' => ['name' => _('Disable'), 'confirm' => _('Disable selected hosts?')], - 'host.export' => ['name' => _('Export'), 'redirect' => - (new CUrl('zabbix.php')) - ->setArgument('action', 'export.hosts.xml') - ->setArgument('backurl', (new CUrl('hosts.php')) - ->setArgument('page', $data['page'] == 1 ? null : $data['page']) - ->getUrl()) - ->getUrl() + 'host.export' => [ + 'content' => new CButtonExport('export.hosts', + (new CUrl('hosts.php')) + ->setArgument('page', ($data['page'] == 1) ? null : $data['page']) + ->getUrl() + ) ], 'host.massupdateform' => ['name' => _('Mass update')], 'host.massdelete' => ['name' => _('Delete'), 'confirm' => _('Delete selected hosts?')] diff --git a/ui/include/views/configuration.template.list.php b/ui/include/views/configuration.template.list.php index e39bcbab2ae..66ce0680b1f 100644 --- a/ui/include/views/configuration.template.list.php +++ b/ui/include/views/configuration.template.list.php @@ -298,13 +298,12 @@ $form->addItem([ $data['paging'], new CActionButtonList('action', 'templates', [ - 'template.export' => ['name' => _('Export'), 'redirect' => - (new CUrl('zabbix.php')) - ->setArgument('action', 'export.templates.xml') - ->setArgument('backurl', (new CUrl('templates.php')) - ->setArgument('page', $data['page'] == 1 ? null : $data['page']) - ->getUrl()) - ->getUrl() + 'template.export' => [ + 'content' => new CButtonExport('export.templates', + (new CUrl('templates.php')) + ->setArgument('page', ($data['page'] == 1) ? null : $data['page']) + ->getUrl() + ) ], 'template.massupdateform' => ['name' => _('Mass update')], 'template.massdelete' => ['name' => _('Delete'), 'confirm' => _('Delete selected templates?')], diff --git a/ui/include/views/js/conf.import.js.php b/ui/include/views/js/conf.import.js.php index dd0329ac625..3bd335f6c67 100644 --- a/ui/include/views/js/conf.import.js.php +++ b/ui/include/views/js/conf.import.js.php @@ -28,7 +28,7 @@ jQuery(function($) { $('#import').click(function() { if ($('.deleteMissing:checked').length > 0) { - return confirm(<?= json_encode(_('Delete all elements that are not present in the XML file?')) ?>); + return confirm(<?= json_encode(_('Delete all elements that are not present in the import file?')) ?>); } }); }); diff --git a/ui/include/views/monitoring.screen.list.php b/ui/include/views/monitoring.screen.list.php index 4d53f7359a6..cd669a4e130 100644 --- a/ui/include/views/monitoring.screen.list.php +++ b/ui/include/views/monitoring.screen.list.php @@ -121,13 +121,12 @@ foreach ($data['screens'] as $screen) { $buttons = []; if (!$data['templateid']) { - $buttons['screen.export'] = ['name' => _('Export'), 'redirect' => - (new CUrl('zabbix.php')) - ->setArgument('action', 'export.screens.xml') - ->setArgument('backurl', (new CUrl('screenconf.php')) - ->setArgument('page', $data['page'] == 1 ? null : $data['page']) - ->getUrl()) - ->getUrl() + $buttons['screen.export'] = [ + 'content' => new CButtonExport('export.screens', + (new CUrl('screenconf.php')) + ->setArgument('page', ($data['page'] == 1) ? null : $data['page']) + ->getUrl() + ) ]; } diff --git a/ui/include/views/monitoring.sysmap.list.php b/ui/include/views/monitoring.sysmap.list.php index f77680965ad..30b5b05ed4d 100644 --- a/ui/include/views/monitoring.sysmap.list.php +++ b/ui/include/views/monitoring.sysmap.list.php @@ -94,13 +94,12 @@ $sysmapForm->addItem([ $sysmapTable, $this->data['paging'], new CActionButtonList('action', 'maps', [ - 'map.export' => ['name' => _('Export'), 'redirect' => - (new CUrl('zabbix.php')) - ->setArgument('action', 'export.sysmaps.xml') - ->setArgument('backurl', (new CUrl('sysmaps.php')) - ->setArgument('page', $data['page'] == 1 ? null : $data['page']) - ->getUrl()) - ->getUrl() + 'map.export' => [ + 'content' => new CButtonExport('export.sysmaps', + (new CUrl('sysmaps.php')) + ->setArgument('page', ($data['page'] == 1) ? null : $data['page']) + ->getUrl() + ) ], 'map.massdelete' => ['name' => _('Delete'), 'confirm' => _('Delete selected maps?')] ]) diff --git a/ui/js/class.cnavtree.js b/ui/js/class.cnavtree.js index 5ed44cafa32..c52a3ccfda3 100644 --- a/ui/js/class.cnavtree.js +++ b/ui/js/class.cnavtree.js @@ -1267,6 +1267,14 @@ jQuery(function($) { }); }, + // onWidgetCopy trigger method + onWidgetCopy: function() { + var $this = $(this); + return this.each(function() { + updateWidgetFields($this); + }); + }, + // onEditStart trigger method onEditStart: function() { var $this = $(this); @@ -1314,7 +1322,9 @@ jQuery(function($) { lastId: 0 }); - var triggers = ['onEditStart', 'beforeDashboardSave','beforeConfigLoad', 'onDashboardReady']; + var triggers = ['onEditStart', 'beforeDashboardSave', 'onWidgetCopy', 'beforeConfigLoad', + 'onDashboardReady' + ]; $.each(triggers, function(index, trigger) { $(".dashbrd-grid-container").dashboardGrid("addAction", trigger, diff --git a/ui/js/dashboard.grid.js b/ui/js/dashboard.grid.js index 64449797811..469b7d7b6e9 100644 --- a/ui/js/dashboard.grid.js +++ b/ui/js/dashboard.grid.js @@ -3853,7 +3853,7 @@ }, /** - * Function to store copied widget into storage handler. + * Function to store copied widget into storage buffer. * * @param {object} widget Widget object copied. * @@ -3861,6 +3861,11 @@ */ copyWidget: function(widget) { return this.each(function() { + var $this = $(this), + data = $this.data('dashboardGrid'); + + doAction('onWidgetCopy', $this, data, widget); + var w = { type: widget.type, pos: { diff --git a/ui/js/init.js b/ui/js/init.js index 5db35d0761b..0a5b2b9020f 100644 --- a/ui/js/init.js +++ b/ui/js/init.js @@ -255,8 +255,7 @@ jQuery(function($) { return { of: $obj, my: 'left top', - at: 'left top+24', - collision: 'none' + at: 'left bottom' }; case 'submenu': diff --git a/ui/js/menupopup.js b/ui/js/menupopup.js index 035b40356e3..28ff96c2446 100644 --- a/ui/js/menupopup.js +++ b/ui/js/menupopup.js @@ -918,20 +918,41 @@ function getMenuPopupDropdown(options, trigger_elem) { var items = []; jQuery.each(options.items, function(i, item) { - items.push({ + var row = { label: item.label, - url: item.url || 'javascript:void(0);', - class: item.class, - clickCallback: () => { + url: item.url || 'javascript:void(0);' + }; + + if (item.class) { + row.class = item.class; + } + + if (options.toggle_class) { + row.clickCallback = () => { jQuery(trigger_elem) .removeClass() - .addClass(['btn-alt', 'btn-dropdown-toggle', item.class].join(' ')); + .addClass(['btn-alt', options.toggle_class, item.class].join(' ')); jQuery('input[type=hidden]', jQuery(trigger_elem).parent()) .val(item.value) .trigger('change'); } - }); + } + else if (options.submit_form) { + row.url = 'javascript:void(0);'; + row.clickCallback = () => { + var $_form = trigger_elem.closest('form'); + + if (!$_form.data("action")) { + $_form.data("action", $_form.attr("action")); + } + + $_form.attr("action", item.url); + $_form.submit(); + } + } + + items.push(row); }); return [{ diff --git a/ui/tests/api_json/data/data_test.sql b/ui/tests/api_json/data/data_test.sql index 23f14fbf620..162f8e26721 100644 --- a/ui/tests/api_json/data/data_test.sql +++ b/ui/tests/api_json/data/data_test.sql @@ -242,7 +242,7 @@ INSERT INTO interface (interfaceid, hostid, main, type, useip, ip, dns, port) VA INSERT INTO hosts (hostid, host, status, description) VALUES (99003, 'Api active proxy in action', 5, ''); INSERT INTO hosts (hostid, host, status, description) VALUES (99004, 'Api active proxy with host', 5, ''); INSERT INTO hosts (hostid, proxy_hostid, host, name, status, description) VALUES (99005, 99004,'API Host monitored with proxy', 'API Host monitored with proxy', 0, ''); -INSERT INTO interface (interfaceid,hostid,main,type,useip,ip,dns,port) values (99003,99004,1,1,1,'127.0.0.1','','10050'); +INSERT INTO interface (interfaceid,hostid,main,type,useip,ip,dns,port) values (99003,99005,1,1,1,'127.0.0.1','','10050'); INSERT INTO actions (actionid, name, eventsource, evaltype, status, esc_period) VALUES (90, 'API action with proxy', 1, 0, 0, '1h'); INSERT INTO operations (operationid, actionid, operationtype, esc_period, esc_step_from, esc_step_to, evaltype) VALUES (90, 90, 0, 0, 1, 1, 0); INSERT INTO opmessage (operationid, default_msg, subject, message, mediatypeid) VALUES (90, 0, 'Discovery: {DISCOVERY.DEVICE.STATUS} {DISCOVERY.DEVICE.IPADDRESS}', 'Discovery rule: {DISCOVERY.RULE.NAME}', NULL); diff --git a/ui/tests/api_json/testConfiguration.php b/ui/tests/api_json/testConfiguration.php index 615f02a9c0d..31da167fb8e 100644 --- a/ui/tests/api_json/testConfiguration.php +++ b/ui/tests/api_json/testConfiguration.php @@ -25,12 +25,13 @@ class testConfiguration extends CAPITest { public static function export_fail_data() { return [ + // Check format parameter. [ 'export' => [ 'options' => [ 'hosts' => [ '50009' - ], + ] ] ], 'expected_error' => 'Invalid parameter "/": the parameter "format" is missing.' @@ -40,29 +41,30 @@ class testConfiguration extends CAPITest { 'options' => [ 'hosts' => [ '50009' - ], + ] ], 'format' => '' ], - 'expected_error' => 'Invalid parameter "/format": value must be one of xml, json.' + 'expected_error' => 'Invalid parameter "/format": value must be one of yaml, xml, json.' ], [ 'export' => [ 'options' => [ 'hosts' => [ '50009' - ], + ] ], - 'format' => 'test' + 'format' => 'æų' ], - 'expected_error' => 'Invalid parameter "/format": value must be one of xml, json.' + 'expected_error' => 'Invalid parameter "/format": value must be one of yaml, xml, json.' ], + // Check unexpected parameter. [ 'export' => [ 'options' => [ 'groups' => [ '50012' - ], + ] ], 'format' => 'test', 'hosts' => '50009' @@ -82,16 +84,68 @@ class testConfiguration extends CAPITest { ], [ 'export' => [ + 'options' => [ + 'groups' => [ + '50009' + ], + 'group' => [ + '50009' + ] + ], + 'format' => 'xml' + ], + 'expected_error' => 'Invalid parameter "/options": unexpected parameter "group".' + ], + // Check missing options parameter. + [ + 'export' => [ 'format' => 'xml' ], 'expected_error' => 'Invalid parameter "/": the parameter "options" is missing.' + ], + // Check prettyprint parameter. + [ + 'export' => [ + 'options' => [ + 'groups' => [ + '50012' + ] + ], + 'format' => 'yaml', + 'prettyprint' => 'test' + ], + 'expected_error' => 'Invalid parameter "/prettyprint": a boolean is expected.' + ], + [ + 'export' => [ + 'options' => [ + 'groups' => [ + '50012' + ] + ], + 'format' => 'json', + 'prettyprint' => '' + ], + 'expected_error' => 'Invalid parameter "/prettyprint": a boolean is expected.' + ], + [ + 'export' => [ + 'options' => [ + 'groups' => [ + '50012' + ] + ], + 'format' => 'yaml', + 'prettyprint' => 'æų' + ], + 'expected_error' => 'Invalid parameter "/prettyprint": a boolean is expected.' ] ]; } /** - * @dataProvider export_fail_data - */ + * @dataProvider export_fail_data + */ public function testConfiguration_ExportFail($export, $expected_error) { $this->call('configuration.export', $export, $expected_error); } @@ -109,10 +163,10 @@ class testConfiguration extends CAPITest { } /** - * @dataProvider export_string_ids - */ + * @dataProvider export_string_ids + */ public function testConfiguration_ExportIdsNotNumber($options) { - $formats = ['xml', 'json']; + $formats = ['xml', 'json', 'yaml']; foreach ($formats as $parameter){ $this->call('configuration.export', @@ -120,7 +174,7 @@ class testConfiguration extends CAPITest { 'options' => [ $options => [ $options - ], + ] ], 'format' => $parameter ], @@ -132,40 +186,91 @@ class testConfiguration extends CAPITest { public static function export_success_data() { return [ [ - ['groups' => ['50012']] + [ + 'options' => [ + 'groups' => [] + ], + 'prettyprint' => true + ] + ], + [ + [ + 'options' => [ + 'groups' => ['11111111111111'] + ], + 'prettyprint' => true + ] ], [ - ['hosts' => ['50009']] + [ + 'options' => [ + 'groups' => ['50012'] + ], + 'prettyprint' => true + ] ], [ - ['images' => ['1']] + [ + 'options' => [ + 'hosts' => ['50009'] + ], + 'prettyprint' => false + ] + ], + [ + [ + 'options' => [ + 'groups' => ['50012'], + 'hosts' => ['50009'] + ] + ] ], [ - ['maps' => ['1']] + [ + 'options' => [ + 'images' => ['1'] + ] + ] ], [ - ['screens' => ['3']] + [ + 'options' => [ + 'maps' => ['1'] + ] + ] ], [ - ['templates' => ['10069']] + [ + 'options' => [ + 'screens' => ['3'] + ] + ] ], [ - ['valueMaps' => ['1']] + [ + 'options' => [ + 'templates' => ['10069'] + ] + ] ], + [ + [ + 'options' => [ + 'valueMaps' => ['1'] + ] + ] + ] ]; } /** - * @dataProvider export_success_data - */ + * @dataProvider export_success_data + */ public function testConfiguration_ExportSuccess($data) { - $formats = ['xml', 'json']; + $formats = ['xml', 'json', 'yaml']; - foreach ($formats as $parameter){ - $this->call('configuration.export', [ - 'options' => $data, - 'format' => $parameter - ]); + foreach ($formats as $parameter) { + $this->call('configuration.export', array_merge($data, ['format' => $parameter])); } } @@ -213,7 +318,7 @@ class testConfiguration extends CAPITest { ], 'source' => '{"zabbix_export":{"version":"3.2","date":"2016-12-09T07:29:55Z"}}' ], - 'expected_error' => 'Invalid parameter "/format": value must be one of xml, json.' + 'expected_error' => 'Invalid parameter "/format": value must be one of yaml, xml, json.' ], [ 'import' => [ @@ -225,7 +330,7 @@ class testConfiguration extends CAPITest { ], 'source' => '{"zabbix_export":{"version":"3.2","date":"2016-12-09T07:29:55Z"}}' ], - 'expected_error' => 'Invalid parameter "/format": value must be one of xml, json.' + 'expected_error' => 'Invalid parameter "/format": value must be one of yaml, xml, json.' ], [ 'import' => [ @@ -240,6 +345,19 @@ class testConfiguration extends CAPITest { ], 'expected_error' => 'Invalid parameter "/": unexpected parameter "hosts".' ], + [ + 'import' => [ + 'format' => 'json', + 'rules' => [ + 'groups' => [ + 'createMissing' => true + ] + ], + 'source' => '{"zabbix_export":{"version":"3.2","date":"2016-12-09T07:29:55Z"}}', + 'prettyprint' => true + ], + 'expected_error' => 'Invalid parameter "/": unexpected parameter "prettyprint".' + ], // Check rules. [ 'import' => [ @@ -290,8 +408,8 @@ class testConfiguration extends CAPITest { } /** - * @dataProvider import_fail_data - */ + * @dataProvider import_fail_data + */ public function testConfiguration_ImportFail($import, $expected_error) { $this->call('configuration.import', $import, $expected_error); } @@ -377,8 +495,8 @@ class testConfiguration extends CAPITest { } /** - * @dataProvider import_rules_parameters - */ + * @dataProvider import_rules_parameters + */ public function testConfiguration_ImportBooleanTypeAndUnexpectedParameters($import) { foreach ($import['expected'] as $expected) { $this->call('configuration.import', [ @@ -413,12 +531,12 @@ class testConfiguration extends CAPITest { return [ [[ 'format' => 'xml', - 'source' => '' , + 'source' => '', 'error' => 'Cannot read XML: XML is empty.' ]], [[ 'format' => 'xml', - 'source' => 'test' , + 'source' => 'test', 'error' => 'Cannot read XML: (4) Start tag expected, \'<\' not found [Line: 1 | Column: 1].' ]], [[ @@ -430,7 +548,7 @@ class testConfiguration extends CAPITest { [[ 'format' => 'xml', 'source' => '<?xml version="1.0" encoding="UTF-8"?> - <zabbix_export><version></version><date>2016-12-09T07:12:45Z</date></zabbix_export>' , + <zabbix_export><version></version><date>2016-12-09T07:12:45Z</date></zabbix_export>', 'error' => 'Invalid tag "/zabbix_export/version": unsupported version number.' ]], [[ @@ -440,45 +558,92 @@ class testConfiguration extends CAPITest { // can be different error message text 'error_contains' => 'Cannot read XML:' ]], + // JSON format. [[ 'format' => 'json', - 'source' => '' , + 'source' => '', // can be different error message text 'Cannot read JSON: Syntax error.' or 'Cannot read JSON: No error.' 'error_contains' => 'Cannot read JSON: ' ]], [[ 'format' => 'json', - 'source' => 'test' , + 'source' => 'test', // can be different error message text 'Cannot read JSON: Syntax error.' or 'Cannot read JSON: boolean expected.' 'error_contains' => 'Cannot read JSON: ' ]], [[ 'format' => 'json', - 'source' => '{"zabbix_export":{"date":"2016-12-09T07:29:55Z"}}' , + 'source' => '{"zabbix_export":{"date":"2016-12-09T07:29:55Z"}}', 'error' => 'Invalid tag "/zabbix_export": the tag "version" is missing.' ]], [[ 'format' => 'json', - 'source' => '{"zabbix_export":{"version":"","date":"2016-12-09T07:29:55Z"}}' , + 'source' => '{"zabbix_export":{"version":"","date":"2016-12-09T07:29:55Z"}}', 'error' => 'Invalid tag "/zabbix_export/version": unsupported version number.' ]], [[ 'format' => 'json', - 'source' => '{"export":{"version":"3.2","date":"2016-12-09T07:29:55Z"}}' , + 'source' => '{"export":{"version":"3.2","date":"2016-12-09T07:29:55Z"}}', 'error' => 'Invalid tag "/": unexpected tag "export".' ]], [[ 'format' => 'json', - 'source' => '{"export":{"version":"3.2","date":"2016-12-09T07:29:55Z"}' , + 'source' => '{"zabbix_export":{"version":"3.2","date":"2016-12-09T07:29:55Z"}', // can be different error message text 'Cannot read JSON: Syntax error.' or 'Cannot read JSON: unexpected end of data.' 'error_contains' => 'Cannot read JSON: ' - ]] + ]], + // YAML format. + [[ + 'format' => 'yaml', + 'source' => '', + 'error_contains' => 'Cannot read YAML: Invalid file content.' + ]], + [[ + 'format' => 'yaml', + 'source' => 'æų', + 'error_contains' => 'Cannot read YAML: Invalid file content.' + ]], + [[ + 'format' => 'yaml', + 'source' => "---\nzabbix_export:\n date: \"2020-07-27T12:58:01Z\"\n", + 'error' => 'Invalid tag "/zabbix_export": the tag "version" is missing.' + ]], + [[ + 'format' => 'yaml', + 'source' => "---\nzabbix_export:\n version: \"5.0\"\ndate: \"2020-07-27T12:58:01Z\"\n", + 'error' => 'Invalid tag "/": unexpected tag "date".' + ]], + [[ + 'format' => 'yaml', + 'source' => "---\nzabbix_export:\n version: \"\"\n date: \"2020-07-27T12:58:01Z\"\n", + 'error' => 'Invalid tag "/zabbix_export/version": unsupported version number.' + ]], + [[ + 'format' => 'yaml', + 'source' => "---\nexport:\n version: \"4.0\"\n date: \"2020-08-03T11:38:33Z\"\n...\n", + 'error' => 'Invalid tag "/": unexpected tag "export".' + ]], + [[ + 'format' => 'yaml', + 'source' => '---\nzabbix_export:\n version: \"4.0\"\n date: \"2020-08-03T11:38:33Z', + 'error_contains' => 'Cannot read YAML: scanning error encountered during parsing' + ]], + [[ + 'format' => 'yaml', + 'source' => '<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<zabbix_export><version>5.0</version><date>2020-08-03T12:36:11Z</date></zabbix_export>\n', + 'error' => 'Cannot read YAML: Invalid file content.' + ]], + [[ + 'format' => 'yaml', + 'source' => '{\"zabbix_export\":{\"version\":\"5.0\",\"date\":\"2020-08-03T12:36:39Z\"}}', + 'error' => 'Cannot read YAML: scanning error encountered during parsing: found unexpected \':\' (line 1, column 19), context while scanning a plain scalar (line 1, column 2).' + ]], ]; } /** - * @dataProvider import_source - */ + * @dataProvider import_source + */ public function testConfiguration_ImportInvalidSource($data) { $result = $this->call('configuration.import', [ 'format' => $data['format'], @@ -525,6 +690,12 @@ class testConfiguration extends CAPITest { 'sql' => 'select * from hstgrp where name=\'API host group json import\'' ], [ + 'format' => 'yaml', + 'parameter' => 'groups', + 'source' => "---\nzabbix_export:\n version: \"4.0\"\n date: \"2020-08-03T12:41:17Z\"\n groups:\n - name: API host group yaml import\n...\n", + 'sql' => 'select * from hstgrp where name=\'API host group yaml import\'' + ], + [ 'format' => 'xml', 'parameter' => 'screens', 'source' => '<?xml version="1.0" encoding="UTF-8"?> @@ -549,6 +720,13 @@ class testConfiguration extends CAPITest { 'sql' => 'select * from screens where name=\'API screen json import\'' ], [ + 'format' => 'yaml', + 'parameter' => 'screens', + 'source' => "---\nzabbix_export:\n version: \"4.0\"\n date: \"2020-08-03T12:44:35Z\"\n". + " screens:\n - name: API screen yaml import\n hsize: \"1\"\n vsize: \"1\"\n screen_items: []\n...\n", + 'sql' => 'select * from screens where name=\'API screen yaml import\'' + ], + [ 'format' => 'xml', 'parameter' => 'valueMaps', 'source' => '<?xml version="1.0" encoding="UTF-8"?> @@ -575,6 +753,13 @@ class testConfiguration extends CAPITest { 'source' => '{"zabbix_export":{"version":"3.2","date":"2016-12-12T07:18:00Z","value_maps":[{"name":"API valueMap json import",' . '"mappings":[{"value":"1","newvalue":"Up"}]}]}}', 'sql' => 'select * from valuemaps where name=\'API valueMap json import\'' + ], + [ + 'format' => 'yaml', + 'parameter' => 'valueMaps', + 'source' => "---\nzabbix_export:\n version: \"4.0\"\n date: \"2020-08-03T12:47:05Z\"\n". + " value_maps:\n - name: API valueMap yaml import\n mappings:\n - value: One\n newvalue: Up\n...\n", + 'sql' => 'select * from valuemaps where name=\'API valueMap yaml import\'' ] ]; } @@ -624,6 +809,13 @@ class testConfiguration extends CAPITest { 'expected_error' => 'Only Super Admins can create host groups.' ], [ + 'format' => 'yaml', + 'parameter' => 'groups', + 'source' => "---\nzabbix_export:\n version: \"4.0\"\n date: \"2020-08-03T12:41:17Z\"\n groups:\n - name: API host group yaml import as non Super Admin\n...\n", + 'sql' => 'select * from hstgrp where name=\'API host group yaml import as non Super Admin\'', + 'expected_error' => 'Only Super Admins can create host groups.' + ], + [ 'format' => 'xml', 'parameter' => 'valueMaps', 'source' => '<?xml version="1.0" encoding="UTF-8"?> diff --git a/ui/tests/selenium/testFormAdministrationGeneralInstallation.php b/ui/tests/selenium/testFormAdministrationGeneralInstallation.php index 029c2f357dd..20e9e83d867 100644 --- a/ui/tests/selenium/testFormAdministrationGeneralInstallation.php +++ b/ui/tests/selenium/testFormAdministrationGeneralInstallation.php @@ -46,10 +46,13 @@ class testFormAdministrationGeneralInstallation extends CLegacyWebTest { 'PHP gd', 'PHP gd PNG support', 'PHP gd JPEG support', + 'PHP gd GIF support', 'PHP gd FreeType support', + 'PHP LibYAML', 'PHP libxml', 'PHP xmlwriter', 'PHP xmlreader', + 'PHP LDAP', 'PHP ctype', 'PHP session', 'PHP option "session.auto_start"', |