*/ class Chart_Builder_DB_Query { /** * The array of allowed types. * * @since 1.0.0 * * @access protected * @var array */ protected static $allowed_types = array( 'string', 'number', 'boolean', 'date', 'datetime', 'timeofday' ); /** * The query. * * @access protected * @var string */ protected $_query; /** * The chart id. * * @access protected * @var int */ protected $_chart_id; /** * Any additional parameters (e.g. for connecting to a remote db). * * @access protected * @var array */ protected $_params; /** * The error message. * * @access protected * @var string */ protected $_error; /** * The array of data. * * @since 1.0.0 * * @access protected * @var array */ protected $_data = array(); /** * The array of series. * * @since 1.0.0 * * @access protected * @var array */ protected $_series = array(); /** * * @since 1.0.0 * * @access private * @var */ private $_args; /** * Constructor. * * @access public * @param string $query The query. * @param int $chart_id The chart id. * @param array $params Any additional parameters (e.g. for connecting to a remote db). */ public function __construct( $query = null, $chart_id = null, $params = null ) { $this->_query = $query; $this->_chart_id = $chart_id; $this->_params = $params; } /** * Return allowed types * * @since 1.0.1 * * @static * @access public * @return array the allowed types */ public static function getAllowedTypes() { return self::$allowed_types; } /** * Validates series tyeps. * * @since 1.0.1 * * @static * @access protected * * @param array $types The icoming series types. * * @return boolean TRUE if sereis types are valid, otherwise FALSE. */ protected static function _validateTypes( $types ) { foreach ( $types as $type ) { if ( ! in_array( $type, self::$allowed_types, true ) ) { return false; } } return true; } /** * Fetches information from source, parses it and builds series and data arrays. * * @param bool $as_html Should the result be fetched as an HTML table or as an object. * @param bool $results_as_numeric_array Should the result be fetched as ARRAY_N instead of ARRAY_A. * @param bool $raw_results Should the result be returned without processing. * @access public * @return boolean TRUE on success, otherwise FALSE. */ public function fetch( $as_html = false, $results_as_numeric_array = false, $raw_results = false ) { if ( empty( $this->_query ) ) { return false; } // only select queries allowed. if ( preg_match( '/^\s*(insert|delete|update|replace|create|alter|drop|truncate)\s/i', $this->_query ) ) { $this->_error = __( 'Only SELECT queries are allowed', CHART_BUILDER_NAME ); return false; } $validate_query = str_replace( "\n", ' ', $this->_query ); // impose a limit if no limit clause is provided. if ( strpos( strtolower( $validate_query ), 'select' ) !== false ) { if ( strpos( strtolower( $validate_query ), ' limit ' ) === false ) { $this->_query .= ' LIMIT ' . apply_filters( 'ays_cb_sql_query_limit', 1000, $this->_chart_id ); } } $this->_query = apply_filters( 'ays_cb_db_query', $this->_query, $this->_chart_id, $this->_params ); $results = array(); $headers = array(); // short circuit results for remote dbs. if ( false !== ( $remote_results = apply_filters( 'ays_cb_db_query_execute', false, $this->_query, $as_html, $results_as_numeric_array, $raw_results, $this->_chart_id, $this->_params ) ) ) { $error = $remote_results['error']; if ( empty( $error ) ) { $results = $remote_results['results']; $headers = $remote_results['headers']; } $this->_error = $error; if ( $raw_results ) { return $results; } } if ( ! ( $results && $headers ) ) { global $wpdb; $wpdb->hide_errors(); // @codingStandardsIgnoreStart $rows = $wpdb->get_results( $this->_query, $results_as_numeric_array ? ARRAY_N : ARRAY_A ); // @codingStandardsIgnoreEnd $wpdb->show_errors(); if ( $raw_results ) { return $rows; } if ( $rows ) { $results = array(); $headers = array(); if ( $rows ) { $row_num = 0; foreach ( $rows as $row ) { $result = array(); $col_num = 0; foreach ( $row as $k => $v ) { $result[] = $v; if ( 0 === $row_num ) { $headers[] = array( 'type' => $this->get_col_type( $col_num++ ), 'label' => $k ); } } $results[] = $result; $row_num++; } } } $this->_error = $wpdb->last_error; } if ( $as_html ) { $results = $this->html( $headers, $results ); } else { $results = $this->object( $headers, $results ); } return apply_filters( 'ays_cb_db_query_results', $results, $headers, $as_html, $results_as_numeric_array, $raw_results, $this->_query, $this->_chart_id, $this->_params ); } /** * Get the data type of the column. * * @param int $col_num The column index in the fetched result set. * @access private * @return int */ private function get_col_type( $col_num ) { global $wpdb; switch ( $wpdb->get_col_info( 'type', $col_num ) ) { case 0: case 5: case 4: case 9: case 3: case 2: case 246: case 8: // numeric. return 'number'; case 10: case 12: case 14: // date. return 'date'; } return 'string'; } /** * Returns the HTML output. * * @param array $headers The headers of the result set. * @param array $results The data of the result set. * @access private * @return string */ private function html( $headers, $results ) { ob_start(); ?> ' . $header['label'] . ''; } ?> '; foreach ( $result as $r ) { echo ''; } echo ''; } ?>
' . $r . '
_series = $series; $data = array(); foreach ( $results as $row ) { $data[] = $this->_normalizeData( $row ); } $this->_data = $data; return $this->_data; } /** * Returns the final query. * * @access public * @return string */ public function get_query() { return $this->_query; } /** * Returns source name. * * @since 1.0.0 * * @access public * @return string The name of source. */ public function getSourceName() { return __CLASS__; } /** * Returns series parsed from source. * * @since 1.0.0 * * @access public * @return array The array of series. */ public function getSeries() { return $this->_series; } /** * Returns data parsed from source. * * @since 1.0.0 * * @access public * @return string The serialized array of data. */ public function getData( $fetch_from_editable_table = false ) { if ( $fetch_from_editable_table ) { $this->_fetchDataFromEditableTable(); } return serialize( $this->_data ); } /** * Returns raw data array. * * @since 1.1.0 * * @access public * @return array */ public function getRawData( $fetch_from_editable_table = false ) { if ( $fetch_from_editable_table ) { $this->_fetchDataFromEditableTable(); } return $this->_data; } /** * Re populates series if the source is dynamic. * * @since 1.1.0 * * @access public * * @param array $series The actual array of series. * @param int $chart_id The chart id. * * @return array The re populated array of series or old one. */ public function repopulateSeries( $series, $chart_id ) { return $series; } /** * Re populates data if the source is dynamic. * * @since 1.1.0 * * @access public * * @param array $data The actual array of data. * @param int $chart_id The chart id. * * @return array The re populated array of data or old one. */ public function repopulateData( $data, $chart_id ) { return $data; } /** * Normalizes values according to series' type. * * @param array $data The row of data. * * @return array Normalized row of data. * @throws Exception * @since 1.0.0 * * @access protected * */ protected function _normalizeData( $data ) { // normalize values foreach ( $this->_series as $i => $series ) { // if no value exists for the seires, then add null if ( ! isset( $data[ $i ] ) ) { $data[ $i ] = null; } if ( is_null( $data[ $i ] ) ) { continue; } switch ( $series['type'] ) { case 'number': $data[ $i ] = ( is_numeric( $data[ $i ] ) ) ? floatval( $data[ $i ] ) : ( is_numeric( str_replace( ',', '', $data[ $i ] ) ) ? floatval( str_replace( ',', '', $data[ $i ] ) ) : null ); break; case 'boolean': $datum = trim( strval( $data[ $i ] ) ); $data[ $i ] = in_array( $datum, array( 'true', 'yes', '1' ), true ) ? 'true' : 'false'; break; case 'timeofday': $date = new DateTime( '1984-03-16T' . $data[ $i ] ); if ( $date ) { $data[ $i ] = array( intval( $date->format( 'H' ) ), intval( $date->format( 'i' ) ), intval( $date->format( 's' ) ), 0, ); } break; case 'datetime': // let's check if the date is a Unix epoch $value = DateTime::createFromFormat( 'U', $data[ $i ] ); if ( $value !== false && ! is_wp_error( $value ) ) { $data[ $i ] = $value->format( 'Y-m-d H:i:s' ); } break; case 'string': // if a ' is provided, strip the backslash $data[ $i ] = stripslashes( $this->toUTF8( $data[ $i ] ) ); break; } } return apply_filters( 'ays_cb_format_data', $data, $this->_series ); } /** * Converts values to UTF8, if required. * * @access protected * * @param string $datum The data to convert. * * @return string The converted data. */ protected final function toUTF8( $datum ) { if ( ! function_exists( 'mb_detect_encoding' ) || mb_detect_encoding( $datum ) !== 'ASCII' ) { $datum = \ForceUTF8\Encoding::toUTF8( $datum ); } return $datum; } /** * Determines the formats of date/time columns. * * @access public * * @param array $series The actual array of series. * @param array $data The actual array of data. * * @return array */ public static final function get_date_formats_if_exists( $series, $data ) { $date_formats = array(); $types = array(); $index = 0; foreach ( $series as $column ) { if ( in_array( $column['type'], array( 'date', 'datetime', 'timeofday' ), true ) ) { $types[] = array( 'index' => $index, 'type' => $column['type'] ); } $index++; } if ( ! $types ) { return $date_formats; } $random = $data; // let's randomly pick 5 data points instead of cycling through the entire data set. if ( count( $data ) > 5 ) { $random = array(); for ( $x = 0; $x < 5; $x++ ) { $random[] = $data[ rand( 0, count( $data ) - 1 ) ]; } } foreach ( $types as $type ) { $formats = array(); foreach ( $random as $datum ) { $f = self::determine_date_format( $datum[ $type['index'] ], $type['type'] ); if ( $f ) { $formats[] = $f; } } // if there are multiple formats, use the most frequent format. $formats = array_filter( $formats ); if ( $formats ) { $formats = array_count_values( $formats ); arsort( $formats ); $formats = array_keys( $formats ); $final_format = reset( $formats ); // we have determined the PHP format; now we have to change this into the JS format where m = MM, d = DD etc. $date_formats[] = array( 'index' => $type['index'], 'format' => str_replace( array( 'Y', 'm', 'd', 'H', 'i', 's' ), array( 'YYYY', 'MM', 'DD', 'HH', 'mm', 'ss' ), $final_format ) ); } } return $date_formats; } /** * Determines the date/time format of the given string. * * @access private * * @param string $value The string. * @param string $type 'date', 'timeofday' or 'datetime'. * * @return string|null */ private static function determine_date_format( $value, $type ) { if ( version_compare( phpversion(), '5.3.0', '<' ) ) { return null; } $formats = array( 'Y/m/d', 'Y-m-d', 'm/d/Y', 'm-d-Y', 'd-m-Y', 'd/m/Y', ); switch ( $type ) { case 'datetime': $formats = array_merge( $formats, array( 'U', 'Y/m/d H:i:s', 'Y-m-d H:i:s', 'm/d/Y H:i:s', 'm-d-Y H:i:s', ) ); break; case 'timeofday': $formats = array_merge( $formats, array( 'H:i:s', 'H:i', ) ); break; } $formats = apply_filters( 'ays_cb_date_formats', $formats, $type ); foreach ( $formats as $format ) { $return = DateTime::createFromFormat( $format, $value ); if ( $return !== false && ! is_wp_error( $return ) ) { return $format; } } // invalid format return null; } /** * Returns the error, if any. * * @access public * @return string */ public function get_error() { return $this->_error; } /** * Fetches information from the editable table and parses it to build series and data arrays. * * @since ? * * @access public * @return boolean TRUE on success, otherwise FALSE. */ public function fetchFromEditableTable() { if ( empty( $this->_args ) ) { return false; } $this->_fetchSeriesFromEditableTable(); $this->_fetchDataFromEditableTable(); return true; } /** * Fetches series information from the editable table. This is fetched only through the UI and not while refreshing the chart data. * * @since 1.0.0 * * @access private */ private function _fetchSeriesFromEditableTable() { $params = $this->_args; $headers = array_filter( $params['header'] ); $types = array_filter( $params['type'] ); $header_row = $type_row = array(); if ( $headers ) { foreach ( $headers as $header ) { if ( ! empty( $types[ $header ] ) ) { $this->_series[] = array( 'label' => $header, 'type' => $types[ $header ], ); } } } return true; } /** * Fetches data information from the editable table. * * @throws Exception * @since 1.0.0 * * @access private */ private function _fetchDataFromEditableTable() { $headers = wp_list_pluck( $this->_series, 'label' ); $this->fetch(); $data = $this->_data; $this->_data = array(); foreach ( $data as $line ) { $data_row = array(); // we have to make sure we are fetching the data in the right order // in case the columns have been reordered foreach ( $headers as $header ) { $value = $line[ $header ]; // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict if ( in_array( $header, $headers ) ) { $data_row[] = $value; } } $this->_data[] = $this->_normalizeData( $data_row ); } return true; } } }