summaryrefslogtreecommitdiffstats
path: root/library/Fileshipper/Xlsx
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:43:47 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 12:43:47 +0000
commitbecaa47e20d6c121329adaf9b3f80c1203420df0 (patch)
tree929ea67632da3cbaca35b156e2bf707dc421ecc2 /library/Fileshipper/Xlsx
parentInitial commit. (diff)
downloadicingaweb2-module-fileshipper-becaa47e20d6c121329adaf9b3f80c1203420df0.tar.xz
icingaweb2-module-fileshipper-becaa47e20d6c121329adaf9b3f80c1203420df0.zip
Adding upstream version 1.2.0.upstream/1.2.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'library/Fileshipper/Xlsx')
-rwxr-xr-xlibrary/Fileshipper/Xlsx/Utils.php37
-rwxr-xr-xlibrary/Fileshipper/Xlsx/Workbook.php300
-rwxr-xr-xlibrary/Fileshipper/Xlsx/Worksheet.php245
3 files changed, 582 insertions, 0 deletions
diff --git a/library/Fileshipper/Xlsx/Utils.php b/library/Fileshipper/Xlsx/Utils.php
new file mode 100755
index 0000000..3ab9563
--- /dev/null
+++ b/library/Fileshipper/Xlsx/Utils.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Icinga\Module\Fileshipper\Xlsx;
+
+class Utils
+{
+ /**
+ * Extract text content from a rich text or inline string field
+ * @param null $is
+ * @return string
+ */
+ public static function parseRichText($is = null)
+ {
+ $value = [];
+ if (isset($is->t)) {
+ $value[] = (string)$is->t;
+ } else {
+ foreach ($is->r as $run) {
+ $value[] = (string)$run->t;
+ }
+ }
+
+ return implode(' ', $value);
+ }
+
+ // converts an Excel date field (a number) to a unix timestamp (granularity: seconds)
+ public static function toUnixTimeStamp($excelDateTime)
+ {
+ if (! is_numeric($excelDateTime)) {
+ return $excelDateTime;
+ }
+ $d = floor($excelDateTime); // seconds since 1900
+ $t = $excelDateTime - $d;
+
+ return ($d > 0) ? ( $d - 25569 ) * 86400 + $t * 86400 : $t * 86400;
+ }
+}
diff --git a/library/Fileshipper/Xlsx/Workbook.php b/library/Fileshipper/Xlsx/Workbook.php
new file mode 100755
index 0000000..9c20d95
--- /dev/null
+++ b/library/Fileshipper/Xlsx/Workbook.php
@@ -0,0 +1,300 @@
+<?php
+
+namespace Icinga\Module\Fileshipper\Xlsx;
+
+use RuntimeException;
+use ZipArchive;
+
+/**
+ * Classes in this namespace have been built roughly based on various OSS
+ * XLSXReader implementations
+ */
+class Workbook
+{
+ // XML schemas
+ const SCHEMA_OFFICEDOCUMENT = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument';
+ const SCHEMA_OFFICEDOCUMENT_RELATIONSHIP = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
+ const SCHEMA_RELATIONSHIP = 'http://schemas.openxmlformats.org/package/2006/relationships';
+ const SCHEMA_SHAREDSTRINGS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings';
+ const SCHEMA_WORKSHEETRELATION = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet';
+
+ protected static $zipErrors = [
+ ZipArchive::ER_EXISTS => 'File already exists',
+ ZipArchive::ER_INCONS => 'Zip archive inconsistent',
+ ZipArchive::ER_INVAL => 'Invalid argument',
+ ZipArchive::ER_MEMORY => 'Malloc failure',
+ ZipArchive::ER_NOENT => 'No such file',
+ ZipArchive::ER_NOZIP => 'Not a zip archive',
+ ZipArchive::ER_OPEN => 'Can\'t open file',
+ ZipArchive::ER_READ => 'Read error',
+ ZipArchive::ER_SEEK => 'Seek error',
+ ];
+
+ /** @var Worksheet[] */
+ protected $sheets = [];
+
+ public $sharedStrings = [];
+
+ protected $sheetInfo;
+
+ protected $sheetNameIndex;
+
+ /** @var ZipArchive */
+ protected $zip;
+
+ public $config = [
+ 'removeTrailingRows' => true
+ ];
+
+ protected $mainRelation;
+
+ public function __construct($filename, $config = [])
+ {
+ $this->config = array_merge($this->config, $config);
+ $this->initialize($filename);
+ }
+
+ protected function initialize($filename)
+ {
+ $this->zip = new ZipArchive();
+ if (true === ($result = $this->zip->open($filename))) {
+ $this->parse();
+ } else {
+ throw new RuntimeException(sprintf(
+ 'Failed to open %s : %s',
+ $filename,
+ $this->getZipErrorString($result)
+ ));
+ }
+ }
+
+ protected function getZipErrorString($errorCode)
+ {
+ if (array_key_exists($errorCode, self::$zipErrors)) {
+ return self::$zipErrors[$errorCode];
+ } else {
+ return "Unknown ZIP error code $errorCode";
+ }
+ }
+
+ // get a file from the zip
+ protected function extractFile($name)
+ {
+ $data = $this->zip->getFromName($name);
+ if ($data === false) {
+ throw new RuntimeException(sprintf(
+ "File %s does not exist in the Excel file",
+ $name
+ ));
+ } else {
+ return $data;
+ }
+ }
+
+ protected function loadPackageRelationshipXml()
+ {
+ return simplexml_load_string($this->extractFile('_rels/.rels'));
+ }
+
+ /**
+ * @return \SimpleXMLElement[]
+ */
+ protected function getPackageRelationships()
+ {
+ return $this->loadPackageRelationshipXml()->Relationship;
+ }
+
+ // workbookXML
+ protected function getMainDocumentRelation()
+ {
+ if ($this->mainRelation === null) {
+ foreach ($this->getPackageRelationships() as $relation) {
+ if ($relation['Type'] == self::SCHEMA_OFFICEDOCUMENT) {
+ $this->mainRelation = $relation;
+ break;
+ }
+ }
+
+ if ($this->mainRelation === null) {
+ throw new RuntimeException(
+ 'Got invalid Excel file, found no main document'
+ );
+ }
+ }
+
+ return $this->mainRelation;
+ }
+
+ protected function getWorkbookXml()
+ {
+ return simplexml_load_string(
+ $this->extractFile(
+ $this->getMainDocumentRelation()['Target']
+ )
+ );
+ }
+
+ protected function getWorkbookDir()
+ {
+ return dirname($this->getMainDocumentRelation()['Target']);
+ }
+
+ /**
+ * @return \SimpleXMLElement[]
+ */
+ protected function getWorkbookRelationShips()
+ {
+ $wbDir = $this->getWorkbookDir();
+ $target = basename($this->getMainDocumentRelation()['Target']);
+
+ return simplexml_load_string(
+ $this->extractFile("$wbDir/_rels/$target.rels")
+ )->Relationship;
+ }
+
+ // extract the shared string and the list of sheets
+ protected function parse()
+ {
+ $sheets = [];
+ /** @var \SimpleXMLElement $sheet */
+ $pos = 0;
+ foreach ($this->getWorkbookXml()->sheets->sheet as $sheet) {
+ $pos++;
+ $rId = (string) $sheet->attributes('r', true)->id;
+ // $sheets[$pos] = [ --> check docs
+ $sheets[$rId] = [
+ 'rId' => $rId,
+ 'sheetId' => (int)$sheet['sheetId'],
+ 'name' => (string)$sheet['name'],
+ ];
+ }
+
+ $workbookDir = $this->getWorkbookDir() . '/';
+ foreach ($this->getWorkbookRelationShips() as $relation) {
+ switch ($relation['Type']) {
+ case self::SCHEMA_WORKSHEETRELATION:
+ $sheets[(string) $relation['Id']]['path'] = $workbookDir . (string)$relation['Target'];
+ break;
+
+ case self::SCHEMA_SHAREDSTRINGS:
+ $sharedStringsXML = simplexml_load_string(
+ $this->extractFile($workbookDir . $relation['Target'])
+ );
+
+ foreach ($sharedStringsXML->si as $val) {
+ if (isset($val->t)) {
+ $this->sharedStrings[] = (string)$val->t;
+ } elseif (isset($val->r)) {
+ $this->sharedStrings[] = Utils::parseRichText($val);
+ }
+ }
+
+ break;
+ }
+ }
+
+ $this->sheetInfo = [];
+ foreach ($sheets as $rid => $info) {
+ if (! array_key_exists('path', $info)) {
+ var_dump($sheets);
+ exit;
+ }
+ $this->sheetInfo[$info['name']] = [
+ 'sheetId' => $info['sheetId'],
+ 'rid' => $rid,
+ 'path' => $info['path']
+ ];
+ }
+ }
+
+ // returns an array of sheet names, indexed by sheetId
+ public function getSheetNames()
+ {
+ $res = [];
+ foreach ($this->sheetInfo as $sheetName => $info) {
+ $res[$info['sheetId']] = $sheetName;
+ // $res[$info['rid']] = $sheetName;
+ }
+
+ return $res;
+ }
+
+ public function getSheetCount()
+ {
+ return count($this->sheetInfo);
+ }
+
+ // instantiates a sheet object (if needed) and returns an array of its data
+ public function getSheetData($sheetNameOrId)
+ {
+ $sheet = $this->getSheet($sheetNameOrId);
+
+ return $sheet->getData();
+ }
+
+ // instantiates a sheet object (if needed) and returns the sheet object
+ public function getSheet($sheet)
+ {
+ if (is_numeric($sheet)) {
+ $sheet = $this->getSheetNameById($sheet);
+ } elseif (!is_string($sheet)) {
+ throw new RuntimeException("Sheet must be a string or a sheet Id");
+ }
+ if (!array_key_exists($sheet, $this->sheets)) {
+ $this->sheets[$sheet] = new Worksheet($this->getSheetXML($sheet), $sheet, $this);
+ }
+
+ return $this->sheets[$sheet];
+ }
+
+ public function getSheetByName($name)
+ {
+ if (!array_key_exists($name, $this->sheets)) {
+ $this->sheets[$name] = new Worksheet($this->getSheetXML($name), $name, $this);
+ }
+
+ return $this->sheets[$name];
+ }
+
+ public function enumRidNames()
+ {
+ $res = [];
+ foreach ($this->sheetInfo as $name => $info) {
+ $res[$name] = $name;
+ }
+
+ return $res;
+ }
+
+ public function getSheetNameById($sheetId)
+ {
+ foreach ($this->sheetInfo as $sheetName => $sheetInfo) {
+ if ($sheetInfo['sheetId'] === $sheetId) {
+ return $sheetName;
+ }
+ }
+
+ throw new RuntimeException(sprintf(
+ "Sheet ID %s does not exist in the Excel file",
+ $sheetId
+ ));
+ }
+
+ public function getFirstSheetName()
+ {
+ if (empty($this->sheetInfo)) {
+ throw new RuntimeException('Workbook contains no sheets');
+ }
+
+ foreach ($this->sheetInfo as $sheetName => $sheetInfo) {
+ return $sheetName;
+ }
+ }
+
+ protected function getSheetXML($name)
+ {
+ return simplexml_load_string(
+ $this->extractFile($this->sheetInfo[$name]['path'])
+ );
+ }
+}
diff --git a/library/Fileshipper/Xlsx/Worksheet.php b/library/Fileshipper/Xlsx/Worksheet.php
new file mode 100755
index 0000000..1d9fc51
--- /dev/null
+++ b/library/Fileshipper/Xlsx/Worksheet.php
@@ -0,0 +1,245 @@
+<?php
+
+namespace Icinga\Module\Fileshipper\Xlsx;
+
+use RuntimeException;
+
+class Worksheet
+{
+ /** @var Workbook */
+ protected $workbook;
+
+ /** @var string */
+ public $name;
+
+ /** @var array */
+ protected $data;
+
+ /** @var int */
+ public $rowCount;
+
+ /** @var int */
+ public $colCount;
+
+ /** @var array */
+ protected $config;
+
+ /** @var array */
+ protected $mergeTarget;
+
+ public function __construct($xml, $sheetName, Workbook $workbook)
+ {
+ $this->config = $workbook->config;
+ $this->name = $sheetName;
+ $this->workbook = $workbook;
+ $this->parse($xml);
+ }
+
+ // returns an array of the data from the sheet
+ public function getData()
+ {
+ return $this->data;
+ }
+
+ protected function parse($xml)
+ {
+ $this->parseDimensions($xml->dimension);
+ $this->parseMergeCells($xml->mergeCells);
+ $this->parseData($xml->sheetData);
+ }
+
+ protected function parseDimensions($dimensions)
+ {
+ $range = (string) $dimensions['ref'];
+ $cells = explode(':', $range);
+ $maxValues = $this->getColumnIndex($cells[1]);
+ $this->colCount = $maxValues[0] + 1;
+ $this->rowCount = $maxValues[1] + 1;
+ }
+
+ protected function parseMergeCells($merges)
+ {
+ $result = [];
+
+ if ($merges->mergeCell === null) {
+ $this->mergeTarget = $result;
+ return;
+ }
+
+ foreach ($merges->mergeCell as $merge) {
+ $range = (string) $merge['ref'];
+ $cells = explode(':', $range);
+ $fromName = $cells[0];
+ list($fromCol, $fromRow) = $this->getColumnIndex($fromName);
+ list($toCol, $toRow) = $this->getColumnIndex($cells[1]);
+ for ($i = $fromCol; $i <= $toCol; $i++) {
+ for ($j = $fromRow; $j <= $toRow; $j++) {
+ if ($i !== $fromCol || $j !== $fromRow) {
+ $result[$j][$i] = [$fromRow, $fromCol];
+ }
+ }
+ }
+ }
+
+ $this->mergeTarget = $result;
+ }
+
+ protected function parseData($sheetData)
+ {
+ $rows = [];
+ $curR = 0;
+ $lastDataRow = -1;
+
+ foreach ($sheetData->row as $row) {
+ $rowNum = (int) $row['r'];
+ if ($rowNum != ($curR + 1)) {
+ $missingRows = $rowNum - ($curR + 1);
+ for ($i = 0; $i < $missingRows; $i++) {
+ $rows[$curR] = array_pad([], $this->colCount, null);
+ $curR++;
+ }
+ }
+ $curC = 0;
+ $rowData = [];
+
+ foreach ($row->c as $c) {
+ list($cellIndex,) = $this->getColumnIndex((string) $c['r']);
+ if ($cellIndex !== $curC) {
+ $missingCols = $cellIndex - $curC;
+ for ($i = 0; $i < $missingCols; $i++) {
+ $rowData[$curC] = null;
+ $curC++;
+ }
+ }
+ $val = $this->parseCellValue($c);
+
+ if (!is_null($val)) {
+ $lastDataRow = $curR;
+ }
+ $rowData[$curC] = $val;
+ $curC++;
+ }
+ $rows[$curR] = array_pad($rowData, $this->colCount, null);
+
+ // We clone merged cells, all of them will return the same value
+ // This behavior might eventually become optional with a related
+ // Config flag
+ if (array_key_exists($curR, $this->mergeTarget)) {
+ foreach ($this->mergeTarget[$curR] as $col => $cell) {
+ if ($rowData[$col] === null) {
+ $rows[$curR][$col] = $rows[$cell[0]][$cell[1]];
+ } else {
+ throw new RuntimeException(sprintf(
+ '%s should merge into %s, but %s has a value: %s',
+ $this->makeCellName($cell[0], $cell[1]),
+ $this->makeCellName($curR, $col),
+ $this->makeCellName($curR, $col),
+ $rowData[$col]
+ ));
+ }
+ }
+ }
+
+ $curR++;
+ }
+
+ if ($this->config['removeTrailingRows']) {
+ $this->data = array_slice($rows, 0, $lastDataRow + 1);
+ $this->rowCount = count($this->data);
+ } else {
+ $this->data = $rows;
+ }
+ }
+
+ protected function getColumnIndex($cell = 'A1')
+ {
+ if (preg_match('/([A-Z]+)(\d+)/', $cell, $matches)) {
+ $col = $matches[1];
+ $row = $matches[2];
+ $colLen = strlen($col);
+ $index = 0;
+
+ for ($i = $colLen-1; $i >= 0; $i--) {
+ $index += (ord($col[$i]) - 64) * pow(26, $colLen - $i - 1);
+ }
+
+ return [$index - 1, $row - 1];
+ }
+
+ throw new RuntimeException(sprintf('Invalid cell index %s', $cell));
+ }
+
+ protected function makeCellName($column, $row)
+ {
+ $str = '';
+
+ $rem = $column + 1;
+ while ($rem > 0) {
+ $mod = $rem % 26;
+ $str = chr($mod + 64) . $str;
+ $rem = ($rem - $mod) / 26;
+ }
+
+ return $str . (string) ($row + 1);
+ }
+
+ protected function parseCellValue($cell)
+ {
+ // t is the cell type
+ switch ((string) $cell['t']) {
+ // Shared string
+ case 's':
+ if ((string) $cell->v !== '') {
+ $value = $this->workbook->sharedStrings[intval($cell->v)];
+ } else {
+ $value = '';
+ }
+ break;
+
+ // Boolean
+ case 'b':
+ $value = (string) $cell->v;
+ if ($value === '0') {
+ $value = false;
+ } elseif ($value === '1') {
+ $value = true;
+ } else {
+ $value = (bool) $cell->v;
+ }
+ break;
+
+ // Inline rich text
+ case 'inlineStr':
+ $value = Utils::parseRichText($cell->is);
+ break;
+
+ // Error message
+ case 'e':
+ if ((string) $cell->v !== '') {
+ $value = (string)$cell->v;
+ } else {
+ $value = '';
+ }
+ break;
+
+ default:
+ if (!isset($cell->v)) {
+ return null;
+ }
+ $value = (string) $cell->v;
+
+ // Check for numeric values
+ if (is_numeric($value)) {
+ if ($value == (int) $value) {
+ $value = (int) $value;
+ } elseif ($value == (float) $value) {
+ $value = (float) $value;
+ } elseif ($value == (double) $value) {
+ $value = (double) $value;
+ }
+ }
+ }
+
+ return $value;
+ }
+}