true, ); /** * Columns to be fetched for sorting / filtering, will not be returned */ protected $extraFiltercolumns = array(); /** * All available columns. To be overridden by specific query implementations */ protected $available_columns = array(); protected $count = false; /** * Headers for columns sent to Livestatus socket */ protected $preparedHeaders = array(); public function hasColumns() { return $this->columns !== null; } public function getColumns() { return $this->columns; } public function withHeaders(& $row) { return array_combine($this->preparedHeaders, $row->toArray()); } /** * Whether the named columns value is generated by a filter expression */ public function isFilterFlag($column) { return array_key_exists($column, $this->filter_flags); } // completes a given row public function resultRow(& $row) { // $row -> raw SplArray // $res -> object // $cv -> // $result -> object to be returned $result = (object) array(); $res = $this->withHeaders($row); $cv = array(); if (array_key_exists('custom_variables', $res)) { foreach ($this->parseArray($res['custom_variables']) as $cvp) { $cv[$cvp[0]] = $cvp[1]; } } $combined = array(); foreach ($this->columns as $alias => $col) { if (is_int($alias)) { $alias = $col; } if ($col[0] === '_') { $result->$alias = array_key_exists($alias, $cv) ? $cv[$alias] : null; } else { $func = 'mungeResult_' . $col; if (method_exists($this, $func)) { $this->$func($res[$this->available_columns[$col]], $result); } elseif (is_array($this->available_columns[$col])) { $combined[$alias] = $col; $result->$alias = null; } else { if (strpos($this->available_columns[$col], ' ') === false) { $result->$alias = $res[$this->available_columns[$col]]; } else { $result->$alias = $res[$alias]; } } } } // TODO: Quite some redundancy here :( if (! $this->filterIsSupported()) { foreach ($this->filter->listFilteredColumns() as $col) { if ($this->isFilterFlag($col)) { $result->$col = (string) (int) $this->filterStringToFilter( $this->filter_flags[$col] )->matches((object) $res); } else { $func = 'combineResult_' . $col; if (method_exists($this, $func)) { $result->$col = $this->$func($result, $res); } } } } foreach ($combined as $alias => $col) { if ($this->isFilterFlag($col)) { $result->$alias = (string) (int) $this->filterStringToFilter( $this->filter_flags[$col] )->matches((object) $res); continue; } $func = 'combineResult_' . $col; if (method_exists($this, $func)) { $result->$alias = $this->$func($result, $res); } else { $result->$alias = implode(' - ', $this->available_columns[$col]); } } return $result; } /** * Parse the given encoded array * * @param string $str the encoded array string * * @return array */ public function parseArray($str) { if (empty($str)) { return array(); } $result = array(); $entries = preg_split('/,/', $str); foreach ($entries as $e) { $result[] = preg_split('/;/', $e); } return $result; } public function getColumnAliases() { $this->columnsToString(); return $this->preparedHeaders; // TODO: Remove once no longer needed: $aliases = array(); $hasCustom = false; foreach ($this->getColumns() as $key => $val) { if ($val[0] === '_') { $this->customvars[$val] = null; if (! $hasCustom) { $aliases[] = 'custom_variables'; $hasCustom = true; } continue; } if (is_int($key)) { $aliases[] = $val; } else { $aliases[] = $key; } } return $aliases; } /* public function count() { $this->count = true; return $this; } */ /** * Automagic string casting * * @return string */ public function __toString() { try { return $this->toString(); } catch (Exception $e) { trigger_error( sprintf( '%s in %s on line %d', $e->getMessage(), $e->getFile(), $e->getLine() ), E_USER_ERROR ); } } /** * Render query string * * @return string */ public function toString() { if ($this->table === null) { throw new IcingaException('Table is required'); } // Headers we always send $default_headers = array( // Our preferred output format is CSV as it allows us to fetch and // process the result row by row 'OutputFormat: csv', 'ResponseHeader: fixed16', // Tried to find a save list of separators, this might be subject to // change and eventually be transforment into constants 'Separators: ' . implode(' ', array(ord("\n"), ord('`'), ord(','), ord(';'))), // We always use the keepalive feature, connection teardown happens // in the connection destructor 'KeepAlive: on' ); $parts = array( sprintf('GET %s', $this->table) ); // Fetch all required columns $parts[] = $this->columnsToString(); // In case we need to apply a userspace filter as of Livestatus lacking // support for some of them we also need to fetch all filtered columns if ($this->filterIsSupported() && $filter = $this->filterToString()) { $parts[] = $filter; } // TODO: Old way of rendering a count query, this should definitively be // improved if ($this->count === true) { $parts[] = 'Stats: state >= 0'; } // TODO: Well... ordering is still missing if (! $this->count && $this->hasLimit() && ! $this->hasOrder()) { $parts[] = 'Limit: ' . ($this->getLimit() + $this->getOffset()); } $lql = implode("\n", $parts) . "\n" . implode("\n", $default_headers) . "\n\n"; return $lql; } /** * Get all available columns * * @return array */ public function getAvailableColumns() { return $this->available_columns; } protected function columnsToString() { $columns = array(); $this->preparedHeaders = array(); $usedColumns = $this->columns; if (! $this->filterIsSupported()) { foreach ($this->filter->listFilteredColumns() as $col) { if (! in_array($col, $usedColumns)) { $usedColumns[] = $col; } } } foreach ($usedColumns as $col) { // TODO: No alias if filter??? if (array_key_exists($col, $this->available_columns)) { // Alias if such $col = $this->available_columns[$col]; } if ($col[0] === '_') { $columns['custom_variables'] = true; } elseif (is_array($col)) { foreach ($col as $k) { $columns[$k] = true; } } else { $columns[$col] = true; } } $this->preparedHeaders = array_keys($columns); if ($this->count === false && $this->columns !== null) { return 'Columns: ' . implode(' ', array_keys($columns)); } else { return ''; // TODO: 'Stats: state >= 0'; when count } } /** * Whether Livestatus is able to apply the current filter * * TODO: find a better method name * TODO: more granular checks, also render filter-flag columns with lql * * @return bool */ public function filterIsSupported() { foreach ($this->filter->listFilteredColumns() as $column) { if (is_array($this->available_columns[$column])) { // Combined column, hardly filterable. Is it? May work! return false; } } return true; } /** * Create a Filter object for a given URL-like filter string. We allow * for spaces as we do not search for custom string values here. This is * internal voodoo. * * @param string $string Filter string * * @return Filter */ protected function filterStringToFilter($string) { return Filter::fromQueryString(str_replace(' ', '', $string)); } /** * Render the current filter to LQL * * @return string */ protected function filterToString() { return $this->renderFilter($this->filter); } /** * Filter rendering * * Happens recursively, useful for filters and for Stats expressions * * @param Filter $filter The filter that should be rendered * @param string $type Filter type. Usually "Filter" or "Stats" * @param int $level Nesting level during recursion. Don't touch * @param bool $keylookup Whether to resolve alias names * * @return string */ protected function renderFilter(Filter $filter, $type = 'Filter', $level = 0, $keylookup = true) { $str = ''; if ($filter instanceof FilterChain) { if ($filter instanceof FilterAnd) { $op = 'And'; } elseif ($filter instanceof FilterOr) { $op = 'Or'; } elseif ($filter instanceof FilterNot) { $op = 'Negate'; } else { throw new IcingaException( 'Cannot render filter: %s', $filter ); } $parts = array(); if (! $filter->isEmpty()) { foreach ($filter->filters() as $f) { $parts[] = $this->renderFilter($f, $type, $level + 1, $keylookup); } $str .= implode("\n", $parts); if ($type === 'Filter') { if (count($parts) > 1) { $str .= "\n" . $op . ': ' . count($parts); } } else { $str .= "\n" . $type . $op . ': ' . count($parts); } } } else { $str .= $type . ': ' . $this->renderFilterExpression($filter, $keylookup); } return $str; } /** * Produce a safe regex string as required by LQL * * @param string $expression search expression * * @return string */ protected function safeRegex($expression) { return '^' . preg_replace('/\*/', '.*', $expression) . '$'; } /** * Render a single filter expression * * @param FilterExpression $filter the filter expression * @param bool $keylookup whether to resolve alias names * * @return string */ public function renderFilterExpression(FilterExpression $filter, $keylookup = true) { if ($keylookup) { $col = $this->available_columns[$filter->getColumn()]; } else { $col = $filter->getColumn(); } $isArray = array_key_exists($col, $this->arrayColumns); $sign = $filter->getSign(); if ($isArray && $sign === '=') { $sign = '>='; } $expression = $filter->getExpression(); if ($sign === '=' && strpos($expression, '*') !== false) { return $col . ' ~~ ' . $this->safeRegex($expression); } elseif ($sign === '!=' && strpos($expression, '*') !== false) { return $col . ' !~~ ' . $this->safeRegex($expression); } else { return $col . ' ' . $sign . ' ' . $expression; } } public function __destruct() { unset($this->connection); } }