├── public └── .gitignore ├── doc └── sqlexplorer.1.png ├── Dockerfile ├── helpers ├── html │ ├── RawContent.php │ ├── CFormField.php │ ├── StyleTag.php │ ├── CFormGrid.php │ ├── JsonDataTag.php │ └── ScriptTag.php ├── ExportHelper.php ├── ImportHelper.php └── ProfileHelper.php ├── actions ├── SqlConfigExport.php ├── StoredSql.php ├── SqlConfigImport.php ├── BaseAction.php ├── SqlConfig.php └── SqlForm.php ├── package.json ├── manifest.json ├── views ├── layout.text.php ├── sqlexplorer.config.form.php ├── js │ └── sqlexplorer.config.form.js └── sql.form.view.php ├── LICENSE ├── Makefile ├── README.md ├── Module.php └── app.js /public/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /doc/sqlexplorer.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gr8b/zabbix-module-sqlexplorer/HEAD/doc/sqlexplorer.1.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest 2 | 3 | RUN npm install -g npm 4 | RUN npm install -g rollup 5 | RUN npm install -g @rollup/plugin-node-resolve -------------------------------------------------------------------------------- /helpers/html/RawContent.php: -------------------------------------------------------------------------------- 1 | items = [$value]; 11 | 12 | return $this; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /helpers/html/CFormField.php: -------------------------------------------------------------------------------- 1 | instance = $child; 13 | } 14 | 15 | public function toString($destroy = true) { 16 | return $this->instance->toString($destroy); 17 | } 18 | } -------------------------------------------------------------------------------- /helpers/html/StyleTag.php: -------------------------------------------------------------------------------- 1 | setAttribute('type', 'text/css'); 10 | 11 | if (is_string($content)) { 12 | $this->setStyle($content); 13 | } 14 | } 15 | 16 | public function setStyle($value) { 17 | $value = preg_replace('/^\s+/m', '', $value); 18 | 19 | return parent::setRawContent($value); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /helpers/html/CFormGrid.php: -------------------------------------------------------------------------------- 1 | instance = new CFormList(); 14 | } 15 | 16 | public function addItem(array $item) { 17 | $this->instance->addRow(array_shift($item), $item); 18 | 19 | return $this; 20 | } 21 | 22 | public function toString($destroy = false) { 23 | return $this->instance->toString($destroy); 24 | } 25 | } -------------------------------------------------------------------------------- /helpers/html/JsonDataTag.php: -------------------------------------------------------------------------------- 1 | setAttribute('type', 'text/json'); 10 | 11 | if ($id !== null) { 12 | $this->setId($id); 13 | } 14 | 15 | if ($content !== null) { 16 | $this->setData($content); 17 | } 18 | } 19 | 20 | public function setData($value) { 21 | $value = json_encode($value); 22 | 23 | return parent::setRawContent($value); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /actions/SqlConfigExport.php: -------------------------------------------------------------------------------- 1 | ['file' => 'queries.txt'], 18 | 'main_block' => implode("\n", Export::toText(Profile::getQueries())) 19 | ]; 20 | 21 | $this->setResponse(new CControllerResponseData($data)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /helpers/html/ScriptTag.php: -------------------------------------------------------------------------------- 1 | setAttribute('type', 'text/javascript'); 10 | 11 | if (is_string($content)) { 12 | $this->setRawContent($content); 13 | } 14 | } 15 | 16 | public function setAttribute($attribute, $value) { 17 | if ($attribute === 'src') { 18 | $this->items = []; 19 | } 20 | 21 | return parent::setAttribute($attribute, $value); 22 | } 23 | 24 | public function setRawContent($value) { 25 | $this->setAttribute('src', null); 26 | 27 | return parent::setRawContent($value); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zabbix-module-sqlexplorer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "clean": "rm -rf ./public/ .cache", 8 | "docker-init": "docker build -t noderollup:latest ./", 9 | "build": "rollup app.js -f iife -o ./public/app.min.js -p @rollup/plugin-node-resolve" 10 | }, 11 | "author": "gr8b", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "@rollup/plugin-node-resolve": "^13.0.0", 15 | "rollup": "^2.50.6" 16 | }, 17 | "dependencies": { 18 | "@codemirror/basic-setup": "^0.17.1", 19 | "@codemirror/highlight": "^0.17.2", 20 | "@codemirror/lang-sql": "^0.17.1", 21 | "@codemirror/view": "^0.17.7", 22 | "@codemirror/theme-one-dark": "^0.17.1" 23 | } 24 | } -------------------------------------------------------------------------------- /helpers/ExportHelper.php: -------------------------------------------------------------------------------- 1 | request_method !== self::POST) { 14 | return true; 15 | } 16 | 17 | $fields = [ 18 | 'queries' => 'required|array' 19 | ]; 20 | 21 | return $this->validateInput($fields); 22 | } 23 | 24 | public function doAction() { 25 | $queries = []; 26 | 27 | if ($this->request_method === self::POST) { 28 | $queries = array_values($this->getInput('queries', [])); 29 | unset($queries[0]); 30 | Profile::updateQueries($queries); 31 | } 32 | 33 | $queries = Profile::getQueries(); 34 | array_unshift($queries, ['title' => '', 'query' => "\n\n\n"]); 35 | $this->setResponse(new CControllerResponseData(['main_block' => json_encode(['queries' => $queries])])); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /helpers/ImportHelper.php: -------------------------------------------------------------------------------- 1 | trim(substr($trim_line, 2)), 34 | 'query' => [] 35 | ]; 36 | } 37 | else { 38 | $query['query'][] = rtrim($line); 39 | } 40 | } 41 | 42 | foreach ($queries as &$query) { 43 | $query['query'] = implode("\n", $query['query']); 44 | } 45 | unset($query); 46 | 47 | return $queries; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /actions/SqlConfigImport.php: -------------------------------------------------------------------------------- 1 | setResponse( 18 | new CControllerResponseData(['main_block' => json_encode([ 19 | 'success' => false, 20 | 'messages' => (string) getMessages(false) 21 | ])]) 22 | ); 23 | } 24 | 25 | return $ret; 26 | } 27 | 28 | public function doAction() { 29 | $file = $_FILES['queries']; 30 | $queries = Import::fromLinesArray(file($file['tmp_name'])); 31 | $output = ['queries' => $queries]; 32 | 33 | if (count($queries) == 0) { 34 | error(_s('No queries were found in file %1$s', $file['name'])); 35 | 36 | $output += [ 37 | 'success' => false, 38 | 'messages' => (string) getMessages(false) 39 | ]; 40 | } 41 | else { 42 | Profile::updateQueries($queries); 43 | $output += [ 44 | 'success' => true, 45 | 'messages' => '', 46 | 'post_messages' => [ 47 | _s('File "%1$s" imported successfully.', $file['name']), 48 | _n('%1$s query created.', '%1$s queries created.', count($queries)) 49 | ] 50 | ]; 51 | } 52 | 53 | $this->setResponse(new CControllerResponseData(['main_block' => json_encode($output)])); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DOCKER_IMAGE=noderollup:latest 2 | VERSION_TAG=$(shell git describe --tags --abbrev=0) 3 | 4 | 5 | prepare: 6 | docker run --rm -it -v $(shell pwd):/app -w /app $(DOCKER_IMAGE) npm install --omit=optional 7 | 8 | dev-watch: 9 | docker run --rm -it --user `id -u`:`id -g` -v $(shell pwd):/app -w /app \ 10 | $(DOCKER_IMAGE) rollup app.js -f iife -o ./public/app.min.js -p @rollup/plugin-node-resolve --watch --compact 11 | 12 | # TODO: add https://www.npmjs.com/package/rollup-plugin-uglify 13 | buildjs: 14 | docker run --rm -it --user `id -u`:`id -g` -v $(shell pwd):/app -w /app \ 15 | $(DOCKER_IMAGE) rollup app.js -f iife -o ./public/app.min.js -p @rollup/plugin-node-resolve --compact 16 | 17 | docker-init: 18 | docker build -t $(DOCKER_IMAGE) ./ 19 | 20 | tag: 21 | @echo "Current version $(VERSION_TAG)" 22 | @echo "Enter new tag: (1.4, 2.0, X.X)" 23 | @read VERSION_TAG && \ 24 | LAST_TAG=$(VERSION_TAG) && \ 25 | LAST_TAG="$${LAST_TAG#v}" && \ 26 | echo "Updating manifest.json file, replace $$LAST_TAG with $$VERSION_TAG" && \ 27 | sed -i "s/\"version\": \"$$LAST_TAG\"/\"version\": \"$$VERSION_TAG\"/g" manifest.json && \ 28 | git add manifest.json && \ 29 | git commit -m "build: adding new tag v$$VERSION_TAG" && \ 30 | git push && \ 31 | git tag v$$VERSION_TAG && \ 32 | git push origin v$$VERSION_TAG 33 | @echo "done" 34 | 35 | zip-release: 36 | @echo "Making module for tag $(VERSION_TAG)" 37 | rm -rf $(VERSION_TAG)-5.0.zip 38 | rm -rf $(VERSION_TAG)-6.4.zip 39 | $(MAKE) buildjs 40 | sed -i "s/\"manifest_version\": 2/\"manifest_version\": 1/g" manifest.json 41 | zip $(VERSION_TAG)-zabbix-5.0-6.2.zip actions/* public/* views/* views/js/* helpers/* helpers/html/* Module.php manifest.json 42 | sed -i "s/\"manifest_version\": 1/\"manifest_version\": 2/g" manifest.json 43 | zip $(VERSION_TAG)-zabbix-6.4-7.2.zip actions/* public/* views/* views/js/* helpers/* helpers/html/* Module.php manifest.json 44 | 45 | -------------------------------------------------------------------------------- /actions/BaseAction.php: -------------------------------------------------------------------------------- 1 | request_method = strtolower($_SERVER['REQUEST_METHOD']); 27 | 28 | if ($this->request_method === self::GET) { 29 | $this->disableSIDvalidation(); 30 | } 31 | 32 | if (version_compare(ZABBIX_VERSION, '6.0', '<')) { 33 | if ($this->post_content_type == self::TYPE_JSON) { 34 | $_REQUEST = array_merge($_REQUEST, json_decode(file_get_contents('php://input'), true)); 35 | } 36 | } 37 | else { 38 | $this->setPostContentType($this->post_content_type); 39 | } 40 | } 41 | 42 | protected function checkPermissions() { 43 | return CWebUser::getType() == USER_TYPE_SUPER_ADMIN; 44 | } 45 | 46 | public function disableSIDvalidation() { 47 | if (version_compare(ZABBIX_VERSION, '6.4.0', '<')) { 48 | return parent::disableSIDvalidation(); 49 | } 50 | 51 | return parent::disableCsrfValidation(); 52 | } 53 | 54 | protected function getActionCsrfToken(string $action): string { 55 | if (version_compare(ZABBIX_VERSION, '6.4.0', '<')) { 56 | return ''; 57 | } 58 | 59 | if (version_compare(ZABBIX_VERSION, '6.4.13', '<')) { 60 | $action = 'sqlexplorer'; 61 | } 62 | 63 | if (version_compare(ZABBIX_VERSION, '7.0.0alpha1', '>') && version_compare(ZABBIX_VERSION, '7.0.0beta2', '<')) { 64 | $action = 'sqlexplorer'; 65 | } 66 | 67 | return CCsrfTokenHelper::get($action); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## SQL Explorer 2 | 3 | ![](doc/sqlexplorer.1.png) 4 | 5 | Module allow to make queries to database and export result as `.csv` file. Queries can be saved and reused later. 6 | Codemirror is used as query editor. It supports SQL syntax highlight and database table column names autocompletion. 7 | Use *"Administration -> General -> GUI -> Limit for search and filter results"* to configure max rows count to be displayed, 8 | export to `.csv` is done without limiting rows count. 9 | 10 | [![Latest Release](https://img.shields.io/github/v/release/gr8b/zabbix-module-sqlexplorer)](https://github.com/gr8b/zabbix-module-sqlexplorer/releases) 11 | 12 | ### Export file format 13 | 14 | All stored SQL queries can be exported as single `.txt` file. Format of one SQL query: 15 | - A line with two dash characters at the beginning, followed by the query name as it is set in the dropdown. The query can contain multiple SQL-style comment lines, but only the first one is used as the query name in the dropdown. 16 | - The SQL code itself, which can span multiple lines. 17 | - A line with two dash characters, marking the end of the query definition. 18 | - An empty line, while not required, is suggested to improve the readability of the export file. 19 | 20 | _Note: Successfull import will replace all stored queries._ 21 | 22 | Example file `Z60.txt`: 23 | ```sql 24 | -- All events closed by global correlation rule 25 | SELECT repercussion.clock, repercussion.name, rootCause.clock, rootCause.name AS name 26 | FROM events repercussion 27 | JOIN event_recovery ON (event_recovery.eventid=repercussion.eventid) 28 | JOIN events rootCause ON (rootCause.eventid=event_recovery.c_eventid) 29 | WHERE event_recovery.c_eventid IS NOT NULL 30 | ORDER BY repercussion.clock ASC; 31 | -- 32 | 33 | -- SNMP hosts unreachable 34 | SELECT proxy.host AS proxy, hosts.host, interface.error, CONCAT('zabbix.php?action=host.edit&hostid=', hosts.hostid) AS goTo 35 | FROM hosts 36 | LEFT JOIN hosts proxy ON (hosts.proxy_hostid=proxy.hostid) 37 | JOIN interface ON (interface.hostid=hosts.hostid) 38 | WHERE LENGTH(interface.error) > 0 39 | AND interface.type=2; 40 | -- 41 | ``` 42 | 43 | ### Safe mode 44 | 45 | To activate safe mode, the `manifest.json` file must include a `connection` property that contains credentials string in the format `username:password`. 46 | When mode is enabled, module database interactions will use credentials for all database queries. 47 | 48 | Example: 49 | ``` 50 | { 51 | "connection": "zabbix:zabbix" 52 | } 53 | ``` 54 | 55 | ### Compatibility and Zabbix support 56 | 57 | For Zabbix 6.4 and newer, up to 7.2, use `*-zabbix-6.4-7.2.zip` file for installation. 58 | For Zabbix version 6.2 and older use `*-zabbix-5.0-6.2.zip` file to install. 59 | 60 | ### Development 61 | 62 | Clone repository, run `make docker-init prepare` to build docker image and initialize nodejs modules, then can use `make dev-watch` to rebuild javascript automatically when `app.js` file is changed. 63 | 64 | ### Thanks 65 | 66 | [Aigars Kadikis](https://github.com/aigarskadikis/) for great ideas, testing and interest in module. 67 | -------------------------------------------------------------------------------- /helpers/ProfileHelper.php: -------------------------------------------------------------------------------- 1 | $column_size || $value_str !== '') { 90 | $encoded_queries[] = substr($value_str, 0, $column_size); 91 | $value_str = (string) substr($value_str, $column_size); 92 | } 93 | } 94 | 95 | CProfile::updateArray(static::PREFIX.static::KEY_QUERIES_PROFILE, $encoded_queries, PROFILE_TYPE_STR); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /actions/SqlConfig.php: -------------------------------------------------------------------------------- 1 | 'in 1', 13 | 'text_to_url' => 'in 0,1', 14 | 'tab_url' => 'in 0,1', 15 | 'autoexec' => 'in 0,1', 16 | 'add_column_names' => 'in 0,1', 17 | 'add_bom_csv' => 'in 0,1', 18 | 'force_single_line_csv' => 'in 0,1', 19 | 'stopwords' => 'string' 20 | ]; 21 | 22 | $ret = $this->validateInput($fields); 23 | 24 | if (!$ret) { 25 | $output = []; 26 | $messages = getMessages(); 27 | 28 | if ($messages !== null) { 29 | $output['errors'] = $messages->toString(); 30 | } 31 | 32 | $this->setResponse( 33 | (new CControllerResponseData(['main_block' => json_encode($output)]))->disableView() 34 | ); 35 | } 36 | 37 | return $ret; 38 | } 39 | 40 | public function doAction() { 41 | $data = [ 42 | 'title' => _('Configuration'), 43 | 'action' => $this->getAction(), 44 | 'csrf_token' => [ 45 | 'sqlexplorer.config' => $this->getActionCsrfToken('sqlexplorer.config'), 46 | 'sqlexplorer.config.import' => $this->getActionCsrfToken('sqlexplorer.config.import') 47 | ], 48 | 'refresh' => 0, 49 | 'errors' => '', 50 | 'params' => [], 51 | 'user' => [ 52 | 'debug_mode' => $this->getDebugMode() 53 | ], 54 | 'tab_url' => Profile::getPersonal(Profile::KEY_TAB_URL, 0), 55 | 'text_to_url' => Profile::getPersonal(Profile::KEY_TEXT_TO_URL, 1), 56 | 'autoexec' => Profile::getPersonal(Profile::KEY_AUTOEXEC_SQL, 1), 57 | 'add_column_names' => Profile::getPersonal(Profile::KEY_SHOW_HEADER, 1), 58 | 'add_bom_csv' => Profile::getPersonal(Profile::KEY_BOM_CSV, 0), 59 | 'force_single_line_csv' => Profile::getPersonal(Profile::KEY_SINGLE_LINE_CSV, 0), 60 | 'stopwords' => Profile::getPersonal(Profile::KEY_STOP_WORDS, Profile::DEFAULT_STOP_WORDS) 61 | ]; 62 | $this->getInputs($data, ['refresh', 'tab_url', 'text_to_url', 'autoexec', 'add_column_names', 'add_bom_csv', 63 | 'force_single_line_csv', 'stopwords' 64 | ]); 65 | 66 | if ($this->hasInput('refresh')) { 67 | Profile::updatePersonal(Profile::KEY_TAB_URL, $data['tab_url']); 68 | Profile::updatePersonal(Profile::KEY_TEXT_TO_URL, $data['text_to_url']); 69 | Profile::updatePersonal(Profile::KEY_AUTOEXEC_SQL, $data['autoexec']); 70 | Profile::updatePersonal(Profile::KEY_SHOW_HEADER, $data['add_column_names']); 71 | Profile::updatePersonal(Profile::KEY_BOM_CSV, $data['add_bom_csv']); 72 | Profile::updatePersonal(Profile::KEY_SINGLE_LINE_CSV, $data['force_single_line_csv']); 73 | Profile::updatePersonal(Profile::KEY_STOP_WORDS, $data['stopwords']); 74 | 75 | $data['params'] = [ 76 | 'tab_url' => $data['tab_url'], 77 | 'text_to_url' => $data['text_to_url'], 78 | 'autoexec' => $data['autoexec'], 79 | 'add_column_names' => $data['add_column_names'], 80 | 'stopwords' => $data['stopwords'] 81 | ]; 82 | } 83 | 84 | $this->setResponse(new CControllerResponseData($data)); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /views/sqlexplorer.config.form.php: -------------------------------------------------------------------------------- 1 | cleanItems() 9 | ->addVar('action', $data['action']) 10 | ->addVar('refresh', 1) 11 | ->addVar('tab_url', 0) 12 | ->addVar('text_to_url', 0) 13 | ->addVar('autoexec', 0) 14 | ->addVar('add_column_names', 0); 15 | 16 | if (version_compare(ZABBIX_VERSION, '6.4.0', '>=')) { 17 | $form->addVar(CCsrfTokenHelper::CSRF_TOKEN_NAME, $data['csrf_token']['sqlexplorer.config.import'], 'import-token'); 18 | $form->addVar(CCsrfTokenHelper::CSRF_TOKEN_NAME, $data['csrf_token']['sqlexplorer.config'], 'post-token'); 19 | } 20 | 21 | $form_list = (new CFormList()) 22 | ->addRow( 23 | new CLabel(_('Convert URL text into clickable links'), 'text_to_url'), 24 | (new CCheckBox('text_to_url', 1))->setChecked((bool) $data['text_to_url']) 25 | ) 26 | ->addRow( 27 | new CLabel(_('Open URL in new tab'), 'tab_url'), 28 | (new CCheckBox('tab_url', 1))->setChecked((bool) $data['tab_url']) 29 | ) 30 | ->addRow( 31 | new CLabel(_('Automatically execute selected SQL'), 'autoexec'), 32 | (new CCheckBox('autoexec', 1))->setChecked((bool) $data['autoexec']) 33 | ) 34 | ->addRow( 35 | new CLabel(_('Column names as first row')), 36 | (new CCheckBox('add_column_names', 1))->setChecked((bool) $data['add_column_names']) 37 | ) 38 | ->addRow( 39 | new CLabel(_('Add UTF-8 BOM to CSV export file')), 40 | (new CCheckBox('add_bom_csv', 1))->setChecked((bool) $data['add_bom_csv']) 41 | ) 42 | ->addRow( 43 | new CLabel(_('Force single line column values in CSV export file')), 44 | (new CCheckBox('force_single_line_csv', 1))->setChecked((bool) $data['force_single_line_csv']) 45 | ) 46 | ->addRow( 47 | new CLabel(_('Stop words list'), 'stopwords'), 48 | (new CTextBox('stopwords', $data['stopwords']))->setWidth(ZBX_TEXTAREA_BIG_WIDTH) 49 | ); 50 | 51 | $form 52 | ->addItem($form_list) 53 | ->addItem( 54 | (new CInput('file', 'import')) 55 | ->setAttribute('accept', '.txt') 56 | ->addClass(ZBX_STYLE_DISPLAY_NONE) 57 | ) 58 | ->addItem((new CInput('submit', 'submit', 1))->addStyle('display: none;')); 59 | 60 | if ($data['params']) { 61 | $output = [ 62 | 'params' => $data['params'] 63 | ]; 64 | } 65 | else { 66 | $output = [ 67 | 'header' => $data['title'], 68 | 'body' => (new CDiv([(new CDiv($data['errors']))->setAttribute('data-error-container', 1), $form]))->toString(), 69 | 'buttons' => [ 70 | [ 71 | 'title' => _('Import'), 72 | 'class' => 'js-import float-left '.ZBX_STYLE_BTN_ALT, 73 | 'keepOpen' => true 74 | ], 75 | [ 76 | 'title' => _('Export'), 77 | 'class' => 'js-export float-left '.ZBX_STYLE_BTN_ALT, 78 | 'keepOpen' => true 79 | ], 80 | [ 81 | 'title' => _('Apply'), 82 | 'class' => 'js-submit dialogue-widget-save', 83 | 'keepOpen' => true, 84 | 'isSubmit' => true 85 | ] 86 | ], 87 | 'params' => $data['params'], 88 | 'script_inline' => $this->readJsFile('sqlexplorer.config.form.js') 89 | ]; 90 | } 91 | 92 | if ($data['user']['debug_mode'] == GROUP_DEBUG_MODE_ENABLED) { 93 | CProfiler::getInstance()->stop(); 94 | $output['debug'] = CProfiler::getInstance()->make()->toString(); 95 | } 96 | 97 | echo json_encode($output); 98 | -------------------------------------------------------------------------------- /views/js/sqlexplorer.config.form.js: -------------------------------------------------------------------------------- 1 | (overlay => { 2 | const modal = overlay.$dialogue[0]; 3 | const form = modal.querySelector('form'); 4 | const token = form.querySelector('#import-token'); 5 | const error_container = modal.querySelector('[data-error-container]'); 6 | const xhr_json_response = response => response.json(); 7 | const xhr_catch_handler = error => { 8 | overlay.unsetLoading(); 9 | error_container.innerHTML = error; 10 | } 11 | const getUrlFor = action => { 12 | const url = new Curl('zabbix.php'); 13 | url.setArgument('action', action); 14 | return url.getUrl(); 15 | } 16 | const addPostMessages = (type, messages) => { 17 | if (window.postMessageDetails) { 18 | return postMessageDetails(type, messages); 19 | } 20 | 21 | // Compatibility for 5.0 22 | return window[type === 'success' ? 'postMessageOk' : 'postMessageError'](messages.join("\n")); 23 | } 24 | 25 | // Export. 26 | modal.querySelector('.js-export').addEventListener('click', e => { 27 | window.location.href = getUrlFor('sqlexplorer.config.export'); 28 | window.addEventListener('focus', _ => overlay.unsetLoading(), {once: true}); 29 | }); 30 | 31 | // Import. 32 | modal.querySelector('.js-import').addEventListener('click', e => { 33 | import_file.dispatchEvent(new PointerEvent('click')); 34 | overlay.unsetLoading(); 35 | }); 36 | const import_file = form.querySelector('[type="file"][name="import"]'); 37 | import_file.addEventListener('change', () => { 38 | const upload_form = new FormData(); 39 | 40 | overlay.setLoading(); 41 | upload_form.append('queries', import_file.files[0]); 42 | token !== null && upload_form.append(token.name, token.value); 43 | fetch(getUrlFor('sqlexplorer.config.import'), {method: "POST", body: upload_form}) 44 | .then(xhr_json_response) 45 | .then(json => { 46 | if (json.success) { 47 | addPostMessages('success', json.post_messages); 48 | window.location.href = window.location.href; 49 | 50 | return; 51 | } 52 | 53 | error_container.innerHTML = json.messages; 54 | import_file.value = ''; 55 | overlay.unsetLoading(); 56 | }) 57 | .catch(xhr_catch_handler); 58 | }); 59 | 60 | // Submit configuration form. 61 | modal.querySelector('.js-submit').addEventListener('click', e => { 62 | const data = new URLSearchParams(new FormData(form)); 63 | 64 | error_container.innerHTML = ''; 65 | overlay.setLoading(); 66 | overlay.xhr = (function() { 67 | const controller = new AbortController(); 68 | const req = fetch(getUrlFor(form.getAttribute('action')), {signal: controller.signal, method: 'POST', body: data}) 69 | .then(xhr_json_response) 70 | .then(json => { 71 | overlay.unsetLoading(); 72 | 73 | if (json.errors) { 74 | error_container.innerHTML = json.errors; 75 | } 76 | else { 77 | overlayDialogueDestroy(overlay.dialogueid); 78 | Object.entries(json.params).forEach(([key, value]) => { 79 | let input = document.querySelector(`[type="hidden"][name="${key}"]`); 80 | 81 | if (input !== null) { 82 | input.value = json.params[key]; 83 | } 84 | }); 85 | } 86 | }) 87 | .catch(xhr_catch_handler); 88 | 89 | this.abort = () => controller.abort(); 90 | 91 | return this; 92 | })(); 93 | }); 94 | 95 | })(overlays_stack.end()) 96 | -------------------------------------------------------------------------------- /Module.php: -------------------------------------------------------------------------------- 1 | ')) { 6 | class_exists('\Core\CModule', false) or class_alias('\Zabbix\Core\CModule', '\Core\CModule'); 7 | class_exists('\CWidget', false) or class_alias('\CHtmlPage', '\CWidget'); 8 | } 9 | 10 | use APP; 11 | use CMenu; 12 | use CWebUser; 13 | use Core\CModule as CModule; 14 | use CController as CAction; 15 | use CMenuItem; 16 | use Modules\SqlExplorer\Actions\BaseAction; 17 | use Modules\SqlExplorer\Helpers\Html\CFormGrid; 18 | use Modules\SqlExplorer\Helpers\Html\CFormField; 19 | 20 | class Module extends CModule { 21 | 22 | public function init(): void { 23 | $this->registerMenuEntry(); 24 | $this->setCompatibilityMode(ZABBIX_VERSION); 25 | } 26 | 27 | /** 28 | * Before action event handler. 29 | * 30 | * @param CAction $action Current request handler object. 31 | */ 32 | public function onBeforeAction(CAction $action): void { 33 | if (is_a($action, BaseAction::class)) { 34 | $action->module = $this; 35 | } 36 | } 37 | 38 | /** 39 | * For login/logout actions update user seession state in multiple databases. 40 | */ 41 | public function onTerminate(CAction $action): void { 42 | } 43 | 44 | /** 45 | * Get array of database configuration. 46 | * 47 | * @return array 48 | */ 49 | public function getDatabase() { 50 | global $DB; 51 | 52 | return [ 53 | 'type' => $DB['TYPE'], 54 | 'table' => $DB['DATABASE'], 55 | 'schema' => $DB['SCHEMA'] 56 | ]; 57 | } 58 | 59 | public function dbSelect(string $query, &$error = null) { 60 | global $DB; 61 | 62 | $db = null; 63 | $rows = []; 64 | $config = $this->getManifest(); 65 | 66 | if (array_key_exists('connection', $config) && strpos($config['connection'], ':')) { 67 | unset($DB['DB']); 68 | $db = $DB; 69 | list($DB['USER'], $DB['PASSWORD']) = explode(':', $config['connection']); 70 | DBconnect($error); 71 | } 72 | 73 | if ($error === null) { 74 | $resource = DBselect($query); 75 | $rows = $resource === false ? [] : DBfetchArray($resource); 76 | } 77 | 78 | if ($db === null) { 79 | return $rows; 80 | } 81 | 82 | if ($error !== null) { 83 | error($error); 84 | } 85 | 86 | $DB = $db; 87 | DBconnect($error); 88 | 89 | return $rows; 90 | } 91 | 92 | public function getAssetsUrl() { 93 | return version_compare(ZABBIX_VERSION, '6.4', '>=') 94 | ? $this->getRelativePath().'/public/' 95 | : 'modules/'.basename($this->getDir()).'/public/'; 96 | } 97 | 98 | protected function registerMenuEntry() { 99 | if (CWebUser::getType() != USER_TYPE_SUPER_ADMIN) { 100 | return; 101 | } 102 | 103 | /** @var CMenu $menu */ 104 | $menu = APP::Component()->get('menu.main'); 105 | $menu 106 | ->find(_('Administration')) 107 | ->getSubMenu() 108 | ->add((new CMenuItem(_('SQL Explorer')))->setAction('sqlexplorer.form')); 109 | } 110 | 111 | protected function setCompatibilityMode($version) { 112 | if (version_compare($version, '6.0', '>=')) { 113 | class_alias('\\CFormGrid', CFormGrid::class, true); 114 | class_alias('\\CFormField', CFormField::class, true); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /views/sql.form.view.php: -------------------------------------------------------------------------------- 1 | setArgument('action', 'sqlexplorer.form') 11 | ->getUrl(); 12 | 13 | if (version_compare(ZABBIX_VERSION, '7.2.0', '>')) { 14 | define('ZBX_DB_ORACLE', 3); // Sets dummy value since Oracle DB support was dropped on 7.2. Makes it compatible with 7.2+ onwards 15 | } 16 | 17 | $db_label = [ 18 | ZBX_DB_MYSQL => _('MySQL'), 19 | ZBX_DB_POSTGRESQL => _('Postgre'), 20 | ZBX_DB_ORACLE => _('Oracle') 21 | ][$data['database']['type']]; 22 | $token_name = ''; 23 | $page_title = sprintf('%s - %s:%s', _('SQL Explorer'), $db_label, $data['database']['table']); 24 | $widget = (new CWidget())->setTitle($page_title); 25 | $form = (new CForm('post', $url)) 26 | ->addClass('sqlexplorer-form') 27 | ->addVar('text_to_url', $data['text_to_url']) 28 | ->addVar('autoexec', $data['autoexec']) 29 | ->addVar('add_column_names', $data['add_column_names']) 30 | ->addVar('stopwords', $data['stopwords']); 31 | 32 | if (version_compare(ZABBIX_VERSION, '6.4.0', '<')) { 33 | $form->setAttribute('aria-labelledby', ZBX_STYLE_PAGE_TITLE); 34 | } 35 | else { 36 | $token_name = CCsrfTokenHelper::CSRF_TOKEN_NAME; 37 | $form->addVar($token_name, $data['csrf_token']['sqlexplorer.form']); 38 | } 39 | 40 | $grid = new CFormGrid(); 41 | 42 | $grid->addItem([ 43 | new CLabel(_('Saved SQL')), 44 | new CFormField((new CDiv([ 45 | (new CSelect('fav')) 46 | ->setId('fav') 47 | ->setValue($data['fav']) 48 | ->addOptions(CSelect::createOptionsFromArray(array_column($data['queries'], 'title'))) 49 | ->setWidth(ZBX_TEXTAREA_BIG_WIDTH), 50 | (new CButton('delete_query', _('Remove'))) 51 | ->addClass(ZBX_STYLE_BTN_ALT) 52 | ->setEnabled($data['fav'] > 0), 53 | ]))->addClass('margin-between')) 54 | ]); 55 | 56 | $grid->addItem([ 57 | null, 58 | new CFormField((new CDiv([ 59 | (new CTextBox('name', $data['name'])) 60 | ->setAttribute('autocomplete', 'off') 61 | ->setWidth(ZBX_TEXTAREA_BIG_WIDTH), 62 | (new CButton('update_query', _('Update'))) 63 | ->addClass(ZBX_STYLE_BTN_ALT) 64 | ->setEnabled($data['fav'] > 0), 65 | (new CButton('save_query', _('New'))) 66 | ->addClass(ZBX_STYLE_BTN_ALT) 67 | ->setEnabled(trim($data['name']) !== '') 68 | ]))->addClass('margin-between')) 69 | ]); 70 | 71 | $grid->addItem([ 72 | new CLabel(_('Query')), 73 | new CFormField( 74 | (new CTextArea('query', $data['query'])) 75 | ->addClass(ZBX_STYLE_DISPLAY_NONE) 76 | ->removeAttribute('maxlength') 77 | ) 78 | ]); 79 | 80 | $table = null; 81 | 82 | if (array_key_exists('rows', $data)) { 83 | $table = (new CTable)->addClass(ZBX_STYLE_LIST_TABLE); 84 | 85 | if ($data['add_column_names'] && $data['rows']) { 86 | $table->setHeader(array_keys($data['rows'][0])); 87 | } 88 | 89 | if ($data['rows_count'] > $data['rows_limit']) { 90 | $table->setFooter(new CRow( 91 | (new CCol(_s('Displaying %1$s of %2$s found', $data['rows_limit'], $data['rows_count']))) 92 | ->setColSpan(count(array_keys($data['rows'][0]))) 93 | ->addClass(ZBX_STYLE_RIGHT) 94 | )); 95 | } 96 | 97 | $regex = '/^(?[a-z0-9_]+\\.php)(\\?(?.+)){0,1}$/'; 98 | foreach ($data['rows'] as $row) { 99 | if ($data['text_to_url']) { 100 | foreach ($row as &$col) { 101 | $match = []; 102 | $params = []; 103 | 104 | if (trim($col) !== '' 105 | && preg_match($regex, trim($col), $match, PREG_UNMATCHED_AS_NULL)) { 106 | parse_str((string) $match['params'], $params); 107 | $url = new CUrl(trim($col)); 108 | $col = new CCol((new CLink($params ? end($params) : 'link', $url->toString())) 109 | ->setAttribute('target', $data['tab_url'] ? '_blank' : null) 110 | ); 111 | } 112 | } 113 | } 114 | 115 | $table->addRow($row); 116 | } 117 | } 118 | 119 | $form->addItem((new CTabView()) 120 | ->addTab('default', null, $grid) 121 | ->setFooter(makeFormFooter( 122 | (new CSubmit('preview', _('Preview')))->setAttribute('value', 1), 123 | [new CButton('csv', _('CSV'))] 124 | )) 125 | ); 126 | $controls = [(new CButton('sqlexplorer.config', _('Configuration')))->addClass(ZBX_STYLE_BTN_ALT)]; 127 | $widget 128 | ->setControls( 129 | version_compare(ZABBIX_VERSION, '6.4.0', '>=') 130 | ? (new CTag('nav', true, new CList($controls)))->setAttribute('aria-label', _('Content controls')) 131 | : $controls 132 | ) 133 | ->addItem(new StyleTag(<<<'CSS' 134 | .margin-between > * { vertical-align: middle; margin-right: 5px !important; } 135 | /* Codemirror styles */ 136 | .cm-wrap.cm-focused { outline: 0 none; } 137 | .cm-wrap { border: 1px solid silver; } 138 | .cm-scroller { font-family: Consolas, Menlo, Monaco, source-code-pro, Courier New, monospace !important; font-size: 12px; } 139 | [name="fav"] .list li { max-width: 100%; } 140 | CSS 141 | )) 142 | ->addItem(new JsonDataTag('page-json', [ 143 | 'dark_theme' => in_array(getUserTheme(CWebUser::$data), ['dark-theme', 'hc-dark']), 144 | 'queries' => $data['queries'], 145 | 'db_schema' => $data['db_schema'], 146 | 'token' => [ 147 | 'name' => $token_name, 148 | 'action' => $data['csrf_token'] 149 | ] 150 | ])) 151 | ->addItem($form) 152 | ->addItem($table) 153 | ->addItem((new ScriptTag())->setAttribute('src', $data['public_path'].'app.min.js')) 154 | ->show(); 155 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import {EditorState, EditorView, basicSetup} from "@codemirror/basic-setup" 2 | import {sql, MySQL, PostgreSQL} from "@codemirror/lang-sql" 3 | import { oneDark } from "@codemirror/theme-one-dark" 4 | 5 | const page_data = JSON.parse(document.querySelector('#page-json').innerText) 6 | let queries = page_data.queries 7 | 8 | const form = document.querySelector('form.sqlexplorer-form') 9 | const queries_select = form.querySelector('[name="fav"]') 10 | const query_textbox = form.querySelector('textarea[name="query"]') 11 | const name_input = form.querySelector('input[name="name"]') 12 | const update_button = form.querySelector('#update_query') 13 | const delete_button = form.querySelector('#delete_query') 14 | const save_button = form.querySelector('#save_query') 15 | const config_button = document.getElementById('sqlexplorer.config') 16 | const stopwords = document.querySelector('[name="stopwords"]') 17 | 18 | function setActionToken(action, form) { 19 | form.querySelector(`[name="${page_data.token.name}"]`)?.setAttribute('value', page_data.token.action[action]) 20 | } 21 | config_button.addEventListener('click', () => { 22 | setActionToken('sqlexplorer.config', form) 23 | PopUp('sqlexplorer.config', Object.fromEntries(new FormData(form))) 24 | }) 25 | document.getElementById('csv').addEventListener('click', function() { 26 | setActionToken('sqlexplorer.csv', form) 27 | form.setAttribute('action', 'zabbix.php?action=sqlexplorer.csv') 28 | setLoadingState(true) 29 | query_textbox.value = window.btoa(unescape(encodeURIComponent(editor.state.doc.toString()))) 30 | form.submit() 31 | setTimeout(() => setLoadingState(false), 1000) 32 | }); 33 | document.getElementById('preview').addEventListener('click', function(e) { 34 | setActionToken('sqlexplorer.form', form) 35 | form.setAttribute('action', 'zabbix.php?action=sqlexplorer.form') 36 | 37 | if (checkStopWords(editor.state.doc.toString()) == false) { 38 | e.preventDefault() 39 | e.stopPropagation() 40 | 41 | return false 42 | } 43 | 44 | query_textbox.value = window.btoa(unescape(encodeURIComponent(editor.state.doc.toString()))) 45 | setLoadingState(true) 46 | form.submit() 47 | }); 48 | queries_select.addEventListener('change', function() { 49 | if (this.value > 0) { 50 | query_textbox.value = queries[this.value].query 51 | query_textbox.dispatchEvent(new Event('change')) 52 | update_button.removeAttribute('disabled') 53 | delete_button.removeAttribute('disabled') 54 | 55 | const autoexec = document.querySelector('[type="hidden"][name="autoexec"]').value; 56 | 57 | if (autoexec - 0) { 58 | form.querySelector('[name="preview"]').click() 59 | } 60 | } 61 | else { 62 | update_button.setAttribute('disabled', 'disabled') 63 | delete_button.setAttribute('disabled', 'disabled') 64 | } 65 | }) 66 | update_button.addEventListener('click', e => { 67 | queries[queries_select.value].query = editor.state.doc.toString() 68 | saveQueries() 69 | }) 70 | delete_button.addEventListener('click', e => { 71 | if (confirm(`Delete query "${queries[queries_select.value]?.title}"`)) { 72 | delete queries[queries_select.value] 73 | queries_select.querySelector(`.list li[value="${queries_select.value}"]`).style.display = 'none'; 74 | queries_select.value = 0 75 | saveQueries() 76 | } 77 | }) 78 | name_input.addEventListener('keyup', e => { 79 | let name = name_input.value.replace(/\s+/g, '') 80 | 81 | if (name.length > 0) { 82 | save_button.removeAttribute('disabled') 83 | } 84 | else { 85 | save_button.setAttribute('disabled', 'disabled') 86 | } 87 | }) 88 | save_button.addEventListener('click', e => { 89 | let value = queries.length 90 | 91 | queries.push({ 92 | title: name_input.value, 93 | query: editor.state.doc.toString() 94 | }) 95 | saveQueries().then(json => { 96 | name_input.value = '' 97 | 98 | queries_select.addOption({value, label: name_input.value}) 99 | queries_select.value = value 100 | }) 101 | }) 102 | 103 | function setLoadingState(is_loading) { 104 | if (is_loading) { 105 | form.classList.add('is-loading') 106 | } 107 | else { 108 | form.classList.remove('is-loading') 109 | } 110 | } 111 | 112 | function saveQueries() { 113 | let sid = form.querySelector('[name="sid"]') 114 | let data = {queries: queries.filter(Boolean)} 115 | 116 | if (sid) { 117 | data.sid = sid.value 118 | } 119 | else { 120 | data[page_data.token.name] = page_data.token.action['sqlexplorer.queries'] 121 | } 122 | 123 | setLoadingState(true) 124 | return fetch('?action=sqlexplorer.queries', { 125 | method: 'POST', 126 | body: JSON.stringify(data) 127 | }) 128 | .then(resp => resp.json()) 129 | .finally(e => { 130 | setLoadingState(false) 131 | }) 132 | } 133 | 134 | function checkStopWords(query) { 135 | const match = [...stopwords.value.matchAll(/\w+/g)].filter(match => query.match(new RegExp(match[0], 'i'))) 136 | 137 | if (match.length) { 138 | return confirm(`Are you sure to execute query: "${query.replace(/\s+$/, '')}"`) 139 | } 140 | 141 | return true 142 | } 143 | 144 | // https://www.raresportan.com/how-to-make-a-code-editor-with-codemirror6/ 145 | // configuration https://github.com/codemirror/lang-sql 146 | const theme = EditorView.baseTheme({},{dark: false}) 147 | let editor = new EditorView({ 148 | state: EditorState.create({ 149 | extensions: [ 150 | page_data.dark_theme ? oneDark : theme, 151 | basicSetup, 152 | sql({ 153 | dialect: MySQL, 154 | schema: page_data.db_schema, 155 | upperCaseKeywords: true 156 | }) 157 | ], 158 | doc: query_textbox.value 159 | }), 160 | parent: query_textbox.parentElement 161 | }) 162 | query_textbox.addEventListener('change', e => { 163 | let old_value = editor.state.doc.toString() 164 | 165 | if (query_textbox.value === old_value) { 166 | return 167 | } 168 | 169 | editor.dispatch({ 170 | changes: { 171 | from: 0, 172 | to: old_value.length, 173 | insert: query_textbox.value 174 | } 175 | }) 176 | }) 177 | -------------------------------------------------------------------------------- /actions/SqlForm.php: -------------------------------------------------------------------------------- 1 | getSqlFormValidationRules(); 17 | 18 | return $this->validateInput($fields); 19 | } 20 | 21 | protected function getSqlFormValidationRules() { 22 | if ($this->getAction() === 'sqlexplorer.csv') { 23 | return [ 24 | 'fav' => 'int32', 25 | 'query' => 'string|required|not_empty', 26 | 'add_column_names' => 'in 0,1' 27 | ]; 28 | } 29 | 30 | return [ 31 | 'fav' => 'int32', 32 | 'name' => 'string', 33 | 'query' => 'string', 34 | 'add_column_names' => 'in 0,1', 35 | 'preview' => 'in 0,1' 36 | ]; 37 | } 38 | 39 | protected function doAction() { 40 | $data = [ 41 | 'fav' => 0, 42 | 'tab_url' => Profile::getPersonal(Profile::KEY_TAB_URL, 0), 43 | 'text_to_url' => Profile::getPersonal(Profile::KEY_TEXT_TO_URL, 1), 44 | 'autoexec' => Profile::getPersonal(Profile::KEY_AUTOEXEC_SQL, 0), 45 | 'name' => '', 46 | 'query' => "\n\n\n", 47 | 'add_column_names' => Profile::getPersonal(Profile::KEY_SHOW_HEADER, 0), 48 | 'add_bom_csv' => Profile::getPersonal(Profile::KEY_BOM_CSV, 0), 49 | 'force_single_line_csv' => Profile::getPersonal(Profile::KEY_SINGLE_LINE_CSV, 0), 50 | 'stopwords' => Profile::getPersonal(Profile::KEY_STOP_WORDS, Profile::DEFAULT_STOP_WORDS) 51 | ]; 52 | $this->getInputs($data, array_keys($data)); 53 | 54 | if ($this->hasInput('query')) { 55 | $query = @base64_decode($data['query']); 56 | 57 | if ($query !== false) { 58 | $data['query'] = urldecode($query); 59 | } 60 | } 61 | 62 | $this->setResponse( 63 | $this->getAction() === 'sqlexplorer.csv' 64 | ? $this->getCsvResponse($data) 65 | : $this->getHtmlResponse($data) 66 | ); 67 | } 68 | 69 | protected function getCsvResponse(array $data) { 70 | $error = null; 71 | $rows = $this->module->dbSelect($data['query'], $error); 72 | 73 | if ($error !== null) { 74 | $response = new CControllerResponseRedirect( 75 | (new CUrl('zabbix.php')) 76 | ->setArgument('action', 'sqlexplorer.form') 77 | ->getUrl() 78 | ); 79 | $response->setFormData($data); 80 | 81 | if (version_compare(ZABBIX_VERSION, '6.0', '<')) { 82 | [$message] = clear_messages(); 83 | $response->setMessageError($message['message']); 84 | } 85 | else { 86 | CMessageHelper::setErrorTitle(_('Query error')); 87 | } 88 | 89 | return $response; 90 | } 91 | 92 | if ($rows && $data['add_column_names']) { 93 | array_unshift($rows, array_keys($rows[0])); 94 | } 95 | 96 | if ($data['force_single_line_csv']) { 97 | foreach ($rows as &$row) { 98 | foreach ($row as &$col) { 99 | $col = str_replace(["\r", "\n"], ['', ' '], $col); 100 | } 101 | unset($col); 102 | } 103 | unset($row); 104 | } 105 | 106 | $data = [ 107 | 'main_block' => ($data['add_bom_csv'] ? "\xef\xbb\xbf" : '').zbx_toCSV($rows) 108 | ]; 109 | $response = new CControllerResponseData($data); 110 | $response->setFileName('query_export.csv'); 111 | 112 | return $response; 113 | } 114 | 115 | protected function getHtmlResponse(array $data) { 116 | if ($this->hasInput('preview')) { 117 | $error = null; 118 | $rows = $this->module->dbSelect($data['query'], $error); 119 | 120 | if ($error === null) { 121 | $data['rows_limit'] = $this->getGuiSearchLimit(); 122 | $data['rows_count'] = count($rows); 123 | 124 | if ($data['rows_count'] > $data['rows_limit']) { 125 | $data['rows'] = array_slice($rows, 0, $data['rows_limit']); 126 | } 127 | 128 | $data['rows'] = $rows; 129 | } 130 | 131 | if (version_compare(ZABBIX_VERSION, '6.0', '<')) { 132 | show_messages(); 133 | } 134 | } 135 | 136 | $data['csrf_token'] = [ 137 | 'sqlexplorer.form' => $this->getActionCsrfToken('sqlexplorer.form'), 138 | 'sqlexplorer.csv' => $this->getActionCsrfToken('sqlexplorer.csv'), 139 | 'sqlexplorer.config' => $this->getActionCsrfToken('sqlexplorer.config'), 140 | 'sqlexplorer.queries' => $this->getActionCsrfToken('sqlexplorer.queries') 141 | ]; 142 | $data['public_path'] = $this->module->getAssetsUrl(); 143 | $data['database'] = $this->module->getDatabase(); 144 | $queries = Profile::getQueries(); 145 | $data['queries'] = array_merge([['title' => '', 'query' => "\n\n\n"]], array_values($queries)); 146 | 147 | $data['db_schema'] = []; 148 | foreach (DB::getSchema() as $table => $schema) { 149 | $data['db_schema'][$table] = []; 150 | 151 | foreach ($schema['fields'] as $field => $field_schema) { 152 | // https://codemirror.net/docs/ref/#autocomplete.Completion 153 | $info = $schema['key'] === $field ? _('Primary key') : ''; 154 | $data['db_schema'][$table][] = [ 155 | 'label' => $field, 156 | 'info' => $info 157 | ]; 158 | } 159 | }; 160 | 161 | $response = new CControllerResponseData($data); 162 | $response->setTitle(_('SQL Explorer')); 163 | 164 | return $response; 165 | } 166 | 167 | public function getGuiSearchLimit() { 168 | if (version_compare(ZABBIX_VERSION, '5.2', '>=')) { 169 | return CSettingsHelper::get(CSettingsHelper::SEARCH_LIMIT); 170 | } 171 | 172 | return select_config()['search_limit']; 173 | } 174 | } 175 | --------------------------------------------------------------------------------