File Manager

Current Directory: /home/astoriaah/www/administrator/components/com_sh404sef/vendor/aspera/xlsx-reader/lib
Viewing File: /home/astoriaah/www/administrator/components/com_sh404sef/vendor/aspera/xlsx-reader/lib/Reader.php
<?php namespace Aspera\Spreadsheet\XLSX; use Iterator; use Countable; use RuntimeException; use ZipArchive; use Exception; use InvalidArgumentException; /** * Class for parsing XLSX files. * * @author Aspera GmbH * @author Martins Pilsetnieks */ class Reader implements Iterator, Countable { /** @var NumberFormat */ private $number_format; /** @var SharedStringsConfiguration Configuration of shared strings handling. */ private $shared_strings_configuration = null; /** @var bool Output XLSX-style column names instead of numeric column identifiers. Default false. */ private $output_column_names; /** @var bool Do not consider empty cell values in output. Default false. */ private $skip_empty_cells; /** @var string Full path of the temporary directory that is going to be used to store unzipped files. */ private $temp_dir; /** @var array Temporary files created while reading the document. */ private $temp_files = array(); /** @var RelationshipData File paths and -identifiers to all relevant parts of the read XLSX file */ private $relationship_data; /** @var array Data about separate sheets in the file. */ private $sheets = false; /** @var SharedStrings Shared strings handler. */ private $shared_strings; /** @var string Path to the current worksheet XML file. */ private $worksheet_path = false; /** @var OoxmlReader XML reader object for the current worksheet XML file. */ private $worksheet_reader = false; /** @var bool Internal storage for the result of the valid() method related to the Iterator interface. */ private $valid = false; /** @var bool Whether the reader is currently looking at an element within a <row> node. */ private $row_open = false; /** @var int Current row number in the file. */ private $row_number = 0; /** @var bool|array Contents of last read row. */ private $current_row = false; /** * @param array $options Reader configuration; Permitted values: * - TempDir (string) * Path to directory to write temporary work files to * - ReturnDateTimeObjects (bool) * If true, date/time data will be returned as PHP DateTime objects. * Otherwise, they will be returned as strings. * - SkipEmptyCells (bool) * If true, row content will not contain empty cells * - SharedStringsConfiguration (SharedStringsConfiguration) * Configuration options to control shared string reading and caching behaviour * * @throws Exception * @throws RuntimeException */ public function __construct(array $options = null) { if (!isset($options['TempDir'])) { $options['TempDir'] = null; } $this->initTempDir($options['TempDir']); $this->number_format = new NumberFormat($options); if (!empty($options['SharedStringsConfiguration'])) { if (!($options['SharedStringsConfiguration'] instanceof SharedStringsConfiguration)) { throw new InvalidArgumentException('SharedStringsConfiguration has an invalid type.'); } $this->shared_strings_configuration = $options['SharedStringsConfiguration']; } $this->skip_empty_cells = !empty($options['SkipEmptyCells']); $this->output_column_names = !empty($options['OutputColumnNames']); } /** * Open the given file and prepare everything for the reading of data. * * @param string $file_path * * @throws Exception */ public function open($file_path) { if (!is_readable($file_path)) { throw new RuntimeException('XLSXReader: File not readable (' . $file_path . ')'); } if (!mkdir($this->temp_dir, 0777, true) || !file_exists($this->temp_dir)) { throw new RuntimeException( 'XLSXReader: Could neither create nor confirm existance of temporary directory (' . $this->temp_dir . ')' ); } $zip = new ZipArchive; $status = $zip->open($file_path); if ($status !== true) { throw new RuntimeException('XLSXReader: File not readable (' . $file_path . ') (Error ' . $status . ')'); } $this->relationship_data = new RelationshipData($zip); $this->initWorkbookData($zip); $this->initWorksheets($zip); $this->initSharedStrings($zip, $this->shared_strings_configuration); $this->initStyles($zip); $zip->close(); } /** * Free all connected resources. */ public function close() { if ($this->worksheet_reader && $this->worksheet_reader instanceof OoxmlReader) { $this->worksheet_reader->close(); $this->worksheet_reader = null; } if ($this->shared_strings && $this->shared_strings instanceof SharedStrings) { // Closing the shared string handler will also close all still opened shared string temporary work files. $this->shared_strings->close(); $this->shared_strings = null; } $this->deleteTempfiles(); $this->worksheet_path = null; } /** * Set the decimal separator to use for the output of locale-oriented formatted values * * @param string $new_character */ public function setDecimalSeparator($new_character) { $this->number_format->setDecimalSeparator($new_character); } /** * Set the thousands separator to use for the output of locale-oriented formatted values * * @param string $new_character */ public function setThousandsSeparator($new_character) { $this->number_format->setThousandsSeparator($new_character); } /** * Retrieves an array with information about sheets in the current file * * @return array List of sheets (key is sheet index, value is of type Worksheet). Sheet's index starts with 0. */ public function getSheets() { return array_values($this->sheets); } /** * Changes the current sheet in the file to the sheet with the given index. * * @param int $sheet_index * * @return bool True if sheet was successfully changed, false otherwise. */ public function changeSheet($sheet_index) { $sheets = $this->getSheets(); // Note: Realigns indexes to an auto increment. if (!isset($sheets[$sheet_index])) { return false; } /** @var Worksheet $target_sheet */ $target_sheet = $sheets[$sheet_index]; // The path to the target worksheet file can be obtained via the relationship id reference. $target_relationship_id = $target_sheet->getRelationshipId(); /** @var RelationshipElement $relationship_worksheet */ foreach ($this->relationship_data->getWorksheets() as $relationship_worksheet) { if ($relationship_worksheet->getId() === $target_relationship_id) { $worksheet_path = $relationship_worksheet->getAccessPath(); break; } } if (!isset($worksheet_path) || !is_readable($worksheet_path)) { return false; } // Initialize the determined target sheet as the new current sheet $this->worksheet_path = $worksheet_path; $this->rewind(); return true; } // !Iterator interface methods /** * Rewind the Iterator to the first element. * Similar to the reset() function for arrays in PHP. */ public function rewind() { if ($this->worksheet_reader instanceof OoxmlReader) { $this->worksheet_reader->close(); } else { $this->worksheet_reader = new OoxmlReader(); $this->worksheet_reader->setDefaultNamespaceIdentifierElements(OoxmlReader::NS_XLSX_MAIN); $this->worksheet_reader->setDefaultNamespaceIdentifierAttributes(OoxmlReader::NS_NONE); } $this->worksheet_reader->open($this->worksheet_path); $this->valid = true; $this->row_open = false; $this->current_row = false; $this->row_number = 0; } /** * Return the current element. * * @return mixed current element from the collection * * @throws Exception */ public function current() { if ($this->row_number === 0 && $this->current_row === false) { $this->next(); } return self::adjustRowOutput($this->current_row); } /** * Move forward to next element. * * @return array|false * * @throws Exception */ public function next() { $this->row_number++; $this->current_row = array(); // Walk through the document until the beginning of the first spreadsheet row. if (!$this->row_open) { while ($this->valid = $this->worksheet_reader->read()) { if (!$this->worksheet_reader->matchesElement('row')) { continue; } $this->row_open = true; /* Getting the row spanning area (stored as e.g., 1:12) * so that the last cells will be present, even if empty. */ $row_spans = $this->worksheet_reader->getAttributeNsId('spans'); if ($row_spans) { $row_spans = explode(':', $row_spans); $current_row_column_count = $row_spans[1]; } else { $current_row_column_count = 0; } // If configured: Return empty strings for empty values if ($current_row_column_count > 0 && !$this->skip_empty_cells) { $this->current_row = array_fill(0, $current_row_column_count, ''); } // Do not read further than here if the current 'row' node is not the one to be read if ((int) $this->worksheet_reader->getAttributeNsId('r') !== $this->row_number) { return self::adjustRowOutput($this->current_row); } break; } // No (further) rows found for reading. if (!$this->row_open) { return array(); } } // Do not read further than here if the current 'row' node is not the one to be read if ((int) $this->worksheet_reader->getAttributeNsId('r') !== $this->row_number) { $row_spans = $this->worksheet_reader->getAttributeNsId('spans'); if ($row_spans) { $row_spans = explode(':', $row_spans); $current_row_column_count = $row_spans[1]; } else { $current_row_column_count = 0; } if ($current_row_column_count > 0 && !$this->skip_empty_cells) { $this->current_row = array_fill(0, $current_row_column_count, ''); } return self::adjustRowOutput($this->current_row); } // Variables for empty cell handling. $max_index = 0; $cell_count = 0; $last_cell_index = -1; // Pre-loop-declarations. Will be filled by one loop iteration, then read in another. $style_id = null; $cell_index = null; $cell_has_shared_string = false; while ($this->valid = $this->worksheet_reader->read()) { if (!$this->worksheet_reader->matchesNamespace(OoxmlReader::NS_XLSX_MAIN)) { continue; } switch ($this->worksheet_reader->localName) { // </row> tag: Finish row reading. case 'row': if ($this->worksheet_reader->isClosingTag()) { $this->row_open = false; break 2; } break; // <c> tag: Read cell metadata, such as styling of formatting information. case 'c': if ($this->worksheet_reader->isClosingTag()) { continue 2; } $cell_count++; // Get the cell index via the "r" attribute and update max_index. $cell_index = $this->worksheet_reader->getAttributeNsId('r'); if ($cell_index) { $letter = preg_replace('{[^[:alpha:]]}S', '', $cell_index); $cell_index = self::indexFromColumnLetter($letter); } else { // No "r" attribute available; Just position this cell to the right of the last one. $cell_index = $last_cell_index + 1; } $last_cell_index = $cell_index; if ($cell_index > $max_index) { $max_index = $cell_index; } // Determine cell styling/formatting. $cell_type = $this->worksheet_reader->getAttributeNsId('t'); $cell_has_shared_string = $cell_type === 's'; // s = shared string $style_id = (int) $this->worksheet_reader->getAttributeNsId('s'); // If configured: Return empty strings for empty values. if (!$this->skip_empty_cells) { $this->current_row[$cell_index] = ''; } break; // <v> or <is> tag: Read and store cell value according to current styling/formatting. case 'v': case 'is': if ($this->worksheet_reader->isClosingTag()) { continue 2; } $value = $this->worksheet_reader->readString(); if ($cell_has_shared_string) { $value = $this->shared_strings->getSharedString($value); } // Skip empty values when specified as early as possible if ($value === '' && $this->skip_empty_cells) { break; } // Format value if necessary $value = $this->number_format->tryFormatValue($value, $style_id); $this->current_row[$cell_index] = $value; break; default: // nop break; } } /* If configured: Return empty strings for empty values. * Only empty cells inbetween and on the left side are added. */ if (($max_index + 1 > $cell_count) && !$this->skip_empty_cells) { $this->current_row += array_fill(0, $max_index + 1, ''); ksort($this->current_row); } if (empty($this->current_row) && $this->skip_empty_cells) { $this->current_row[] = null; } return self::adjustRowOutput($this->current_row); } /** * Return the identifying key of the current element. * * @return mixed either an integer or a string */ public function key() { return $this->row_number; } /** * Check if there is a current element after calls to rewind() or next(). * Used to check if we've iterated to the end of the collection. * * @return boolean FALSE if there's nothing more to iterate over */ public function valid() { return $this->valid; } // !Countable interface method /** * Ostensibly should return the count of the contained items but this just returns the number * of rows read so far. It's not really correct but at least coherent. */ public function count() { return $this->row_number; } /** * Takes the column letter and converts it to a numerical index (0-based) * * @param string $letter Letter(s) to convert * @return mixed Numeric index (0-based) or boolean false if it cannot be calculated */ public static function indexFromColumnLetter($letter) { $letter = strtoupper($letter); $result = 0; for ($i = strlen($letter) - 1, $j = 0; $i >= 0; $i--, $j++) { $ord = ord($letter[$i]) - 64; if ($ord > 26) { // This does not seem to be a letter. Someone must have given us an invalid value. return false; } $result += $ord * (26 ** $j); } return $result - 1; } /** * Converts the given column index to an XLSX-style [A-Z] column identifier string. * * @param int $index * @return string */ public static function columnLetterFromIndex($index) { $dividend = $index + 1; // Internal counting starts at 0; For easy calculation, it needs to start at 1. $output_string = ''; while ($dividend > 0) { $modulo = ($dividend - 1) % 26; $output_string = chr($modulo + 65) . $output_string; $dividend = floor(($dividend - $modulo) / 26); } return $output_string; } /** * If configured, replaces numeric column identifiers in output array with XLSX-style [A-Z] column identifiers. * If not configured, returns the input array unchanged. * * @param array $column_values * @return array */ private function adjustRowOutput($column_values) { if (!$this->output_column_names) { // Column names not desired in output; Nothing to do here. return $column_values; } $column_values_with_keys = array(); foreach ($column_values as $k => $v) { $column_values_with_keys[self::columnLetterFromIndex($k)] = $v; } return $column_values_with_keys; } /** * Check and set TempDir to use for file operations. * A new folder will be created within the given directory, which will contain all work files, * and which will be cleaned up after the process have finished. * * @param string|null $base_temp_dir */ private function initTempDir($base_temp_dir) { if ($base_temp_dir === null) { $base_temp_dir = sys_get_temp_dir(); } if (!is_writable($base_temp_dir)) { throw new RuntimeException('XLSXReader: Provided temporary directory (' . $base_temp_dir . ') is not writable'); } $base_temp_dir = rtrim($base_temp_dir, DIRECTORY_SEPARATOR); /** @noinspection NonSecureUniqidUsageInspection */ $this->temp_dir = $base_temp_dir . DIRECTORY_SEPARATOR . uniqid() . DIRECTORY_SEPARATOR; } /** * Read general workbook information from the given zip into memory. * * @param ZipArchive $zip * * @throws Exception */ private function initWorkbookData(ZipArchive $zip) { $workbook = $this->relationship_data->getWorkbook(); if (!$workbook) { throw new Exception('workbook data not found in XLSX file'); } $workbook_xml = $zip->getFromName($workbook->getOriginalPath()); $this->sheets = array(); $workbook_reader = new OoxmlReader(); $workbook_reader->setDefaultNamespaceIdentifierElements(OoxmlReader::NS_XLSX_MAIN); $workbook_reader->setDefaultNamespaceIdentifierAttributes(OoxmlReader::NS_NONE); $workbook_reader->xml($workbook_xml); while ($workbook_reader->read()) { if ($workbook_reader->matchesElement('sheet')) { // <sheet/> - Read in data about this worksheet. $sheet_name = (string) $workbook_reader->getAttributeNsId('name'); $rel_id = (string) $workbook_reader->getAttributeNsId('id', OoxmlReader::NS_RELATIONSHIPS_DOCUMENTLEVEL); $new_sheet = new Worksheet(); $new_sheet->setName($sheet_name); $new_sheet->setRelationshipId($rel_id); $sheet_index = str_ireplace('rId', '', $rel_id); $this->sheets[$sheet_index] = $new_sheet; } elseif ($workbook_reader->matchesElement('sheets') && $workbook_reader->isClosingTag()) { // </sheets> - Indicates that the rest of the document is of no further importance to us. Abort. break; } } $workbook_reader->close(); } /** * Extract worksheet files to temp directory and set the first worksheet as active. * * @param ZipArchive $zip */ private function initWorksheets(ZipArchive $zip) { // Sheet order determining value: relative sheet positioning within the document (rId) ksort($this->sheets); // Extract worksheets to temporary work directory foreach ($this->relationship_data->getWorksheets() as $worksheet) { /** @var RelationshipElement $worksheet */ $worksheet_path_zip = $worksheet->getOriginalPath(); $worksheet_path_conv = str_replace(RelationshipData::ZIP_DIR_SEP, DIRECTORY_SEPARATOR, $worksheet_path_zip); $worksheet_path_unzipped = $this->temp_dir . $worksheet_path_conv; if (!$zip->extractTo($this->temp_dir, $worksheet_path_zip)) { $message = 'XLSXReader: Could not extract file [' . $worksheet_path_zip . '] to directory [' . $this->temp_dir . '].'; $this->reportZipExtractionFailure($zip, $message); } $worksheet->setAccessPath($worksheet_path_unzipped); $this->temp_files[] = $worksheet_path_unzipped; } // Set first sheet as current sheet if (!$this->changeSheet(0)) { throw new RuntimeException('XLSXReader: Sheet cannot be changed.'); } } /** * Read shared strings data from the given zip into memory as configured via the given configuration object * and potentially create temporary work files for easy retrieval of shared string data. * * @param ZipArchive $zip * @param SharedStringsConfiguration $shared_strings_configuration Optional, default null */ private function initSharedStrings( ZipArchive $zip, SharedStringsConfiguration $shared_strings_configuration = null ) { $shared_strings = $this->relationship_data->getSharedStrings(); if (count($shared_strings) > 0) { /* Currently, documents with multiple shared strings files are not supported. * Only the first shared string file will be used. */ /** @var RelationshipElement $first_shared_string_element */ $first_shared_string_element = $shared_strings[0]; // Determine target directory and path for the extracted file $inzip_path = $first_shared_string_element->getOriginalPath(); $inzip_path_for_outzip = str_replace(RelationshipData::ZIP_DIR_SEP, DIRECTORY_SEPARATOR, $inzip_path); $dir_of_extracted_file = $this->temp_dir . dirname($inzip_path_for_outzip) . DIRECTORY_SEPARATOR; $filename_of_extracted_file = basename($inzip_path_for_outzip); $path_to_extracted_file = $dir_of_extracted_file . $filename_of_extracted_file; // Extract file and note it in relevant variables if (!$zip->extractTo($this->temp_dir, $inzip_path)) { $message = 'XLSXReader: Could not extract file [' . $inzip_path . '] to directory [' . $this->temp_dir . '].'; $this->reportZipExtractionFailure($zip, $message); } $first_shared_string_element->setAccessPath($path_to_extracted_file); $this->temp_files[] = $path_to_extracted_file; // Initialize SharedStrings $this->shared_strings = new SharedStrings( $dir_of_extracted_file, $filename_of_extracted_file, $shared_strings_configuration ); // Extend temp_files with files created by SharedStrings $this->temp_files = array_merge($this->temp_files, $this->shared_strings->getTempFiles()); } } /** * Reads and prepares information on styles declared by the document for later usage. * * @param ZipArchive $zip */ private function initStyles(ZipArchive $zip) { $styles = $this->relationship_data->getStyles(); if (count($styles) > 0) { /* Currently, documents with multiple styles files are not supported. * Only the first styles file will be used. */ /** @var RelationshipElement $first_styles_element */ $first_styles_element = $styles[0]; $styles_xml = $zip->getFromName($first_styles_element->getOriginalPath()); // Read cell style definitions and store them in internal variables $styles_reader = new OoxmlReader(); $styles_reader->setDefaultNamespaceIdentifierElements(OoxmlReader::NS_XLSX_MAIN); $styles_reader->setDefaultNamespaceIdentifierAttributes(OoxmlReader::NS_NONE); $styles_reader->xml($styles_xml); $current_scope_is_cell_xfs = false; $current_scope_is_num_fmts = false; $switchList = array( 'numFmts' => array('numFmts'), 'numFmt' => array('numFmt'), 'cellXfs' => array('cellXfs'), 'xf' => array('xf') ); $xf_num_fmt_ids = array(); $number_formats = array(); while ($styles_reader->read()) { switch ($styles_reader->matchesOneOfList($switchList)) { // <numFmts><numFmt/></numFmts> - check for number format definitions case 'numFmts': $current_scope_is_num_fmts = !$styles_reader->isClosingTag(); break; case 'numFmt': if (!$current_scope_is_num_fmts || $styles_reader->isClosingTag()) { break; } $format_code = (string) $styles_reader->getAttributeNsId('formatCode'); $num_fmt_id = (int) $styles_reader->getAttributeNsId('numFmtId'); $number_formats[$num_fmt_id] = $format_code; break; // <cellXfs><xf/></cellXfs> - check for format usages case 'cellXfs': $current_scope_is_cell_xfs = !$styles_reader->isClosingTag(); break; case 'xf': if (!$current_scope_is_cell_xfs || $styles_reader->isClosingTag()) { break; } // Determine if number formatting is set for this cell. $num_fmt_id = null; if ($styles_reader->getAttributeNsId('numFmtId')) { $applyNumberFormat = $styles_reader->getAttributeNsId('applyNumberFormat'); if ($applyNumberFormat === null || $applyNumberFormat === '1' || $applyNumberFormat === 'true') { /* Number formatting is enabled either implicitly ('applyNumberFormat' not given) * or explicitly ('applyNumberFormat' is a true value). */ $num_fmt_id = (int) $styles_reader->getAttributeNsId('numFmtId'); } } // Determine and store correct formatting style. if ($num_fmt_id !== null) { // Number formatting has been enabled for this format. // If format ID >= 164, it is a custom format and should be read from styleSheet\numFmts $xf_num_fmt_ids[] = $num_fmt_id; } else if ($styles_reader->getAttributeNsId('quotePrefix')) { // "quotePrefix" automatically prepends the cell content with a ' symbol. This enforces a text format. $xf_num_fmt_ids[] = null; // null = "Do not format anything". } else { $xf_num_fmt_ids[] = 0; // 0 = "General" format } break; } } $this->number_format->injectXfNumFmtIds($xf_num_fmt_ids); $this->number_format->injectNumberFormats($number_formats); $styles_reader->close(); } } /** * Delete all registered temporary work files and -directories. */ private function deleteTempfiles() { foreach ($this->temp_files as $temp_file) { @unlink($temp_file); } // Better safe than sorry - shouldn't try deleting '.' or '/', or '..'. if (strlen($this->temp_dir) > 2) { @rmdir($this->temp_dir . 'xl' . DIRECTORY_SEPARATOR . 'worksheets'); @rmdir($this->temp_dir . 'xl'); @rmdir($this->temp_dir); } } /** * Gather data on zip extractTo() fault and throw an appropriate Exception. * * @param ZipArchive $zip * @param string $message Optional error message to prefix the error details with. * * @throws RuntimeException */ private function reportZipExtractionFailure($zip, $message = '') { $status_code = $zip->status; $status_message = $zip->getStatusString(); if ($status_code || $status_message) { $message .= ' Status from ZipArchive:'; if ($status_code) { $message .= ' Code [' . $status_code . '];'; } if ($status_message) { $message .= ' Message [' . $status_message . '];'; } } throw new RuntimeException($message); } }