| // +----------------------------------------------------------------------+ /** * Source manager for local CSV files. * * @category Visualizer * @package Source * * @since 1.0.0 */ class Visualizer_Source_Csv extends Visualizer_Source { /** * The path to the file with data. * * @since 1.0.0 * * @access protected * @var string */ protected $_filename; /** * Constructor. * * @since 1.0.0 * * @access public * @param string $filename The path to the file. */ public function __construct( $filename = '' ) { $this->_filename = trim( (string) $filename ); } /** * Fetches series information. * * @since 1.0.0 * * @access private * @param resource $handle The file handle resource. */ private function _fetchSeries( &$handle ) { // read column titles $labels = fgetcsv( $handle, 0, VISUALIZER_CSV_DELIMITER, VISUALIZER_CSV_ENCLOSURE ); $types = null; if ( false !== strpos( $this->_filename, 'tqx=out:csv' ) ) { $attributes = $this->_fetchSeriesForGoogleQueryLanguage( $labels ); if ( ! $attributes['abort'] ) { $labels = $attributes['labels']; $types = $attributes['types']; } } if ( is_null( $types ) ) { // read series types $types = fgetcsv( $handle, 0, VISUALIZER_CSV_DELIMITER, VISUALIZER_CSV_ENCLOSURE ); } $labels = array_filter( $labels ); $types = array_filter( $types ); if ( ! $labels || ! $types ) { $this->_error = esc_html__( 'File should have a heading row (1st row) and a data type row (2nd row). Please try again.', 'visualizer' ); return false; } $types = array_map( 'trim', $types ); if ( ! self::_validateTypes( $types ) ) { $this->_error = esc_html__( 'Invalid data types detected in the data type row (2nd row). Please try again.', 'visualizer' ); return false; } for ( $i = 0, $len = count( $labels ); $i < $len; $i++ ) { $default_type = $i === 0 ? 'string' : 'number'; $labels[ $i ] = $this->toUTF8( $labels[ $i ] ); $this->_series[] = array( 'label' => $labels[ $i ], 'type' => isset( $types[ $i ] ) ? $types[ $i ] : $default_type, ); } return true; } /** * Returns file handle to fetch data from. * * @since 1.4.2 * * @access protected * @param string $filename Optional file name to get handle. If omitted, $_filename is used. * @return resource File handle resource on success, otherwise FALSE. */ protected function _get_file_handle( $filename = false ) { // set line endings auto detect mode ini_set( 'auto_detect_line_endings', true ); // open file and return handle return fopen( $filename ? $filename : $this->_filename, 'rb' ); } /** * Fetches information from source, parses it and builds series and data arrays. * * @since 1.0.0 * * @access public * @return boolean TRUE on success, otherwise FALSE. */ public function fetch() { // if filename is empty return false if ( empty( $this->_filename ) ) { $this->_error = esc_html__( 'No file provided. Please try again.', 'visualizer' ); return false; } // read file and fill arrays $handle = $this->_get_file_handle(); if ( $handle ) { // fetch series if ( ! $this->_fetchSeries( $handle ) ) { return false; } // fetch data // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition while ( ( $data = fgetcsv( $handle, 0, VISUALIZER_CSV_DELIMITER, VISUALIZER_CSV_ENCLOSURE ) ) !== false ) { $this->_data[] = $this->_normalizeData( $data ); } // close file handle fclose( $handle ); } return true; } /** * Returns source name. * * @since 1.0.0 * * @access public * @return string The name of source. */ public function getSourceName() { return __CLASS__; } /** * Adds support for QueryLanguage https://developers.google.com/chart/interactive/docs/querylanguage * where the user can provide something like /gviz/tq?tq=select%20A%2C%20B%20&tqx=out:csv after the URL of the raw spreadsheet * to get the subset of data specified by the query * this will conflate the heading and the type into one value viz. for heading XXX and type string, the value will become "XXX string" * so we need to split them apart logically * also the $types variable now contains the first row because the header and the type got conflated */ private function _fetchSeriesForGoogleQueryLanguage( $labels, $types = array() ) { $new_labels = array(); $new_types = array(); $abort = false; foreach ( $labels as $label ) { // get the index of the last space $index = strrpos( $label, ' ' ); if ( $index === false ) { // no space here? something has gone wrong; abort the entire process. $abort = true; break; } $type = trim( substr( $label, $index + 1 ) ); if ( ! self::_validateTypes( array( $type ) ) ) { // some other data type? abort the entire process. $abort = true; break; } $label = substr( $label, 0, $index ); $new_labels[] = $label; $new_types[] = $type; } if ( ! $abort ) { $labels = $new_labels; $types = $new_types; } return array( 'abort' => $abort, 'labels' => $labels, 'types' => $types, ); } }