summaryrefslogtreecommitdiffstats
path: root/library/Fileshipper
diff options
context:
space:
mode:
Diffstat (limited to 'library/Fileshipper')
-rw-r--r--library/Fileshipper/ProvidedHook/Director/ImportSource.php613
-rw-r--r--library/Fileshipper/ProvidedHook/Director/ShipConfigFiles.php78
-rwxr-xr-xlibrary/Fileshipper/Xlsx/Utils.php37
-rwxr-xr-xlibrary/Fileshipper/Xlsx/Workbook.php300
-rwxr-xr-xlibrary/Fileshipper/Xlsx/Worksheet.php245
5 files changed, 1273 insertions, 0 deletions
diff --git a/library/Fileshipper/ProvidedHook/Director/ImportSource.php b/library/Fileshipper/ProvidedHook/Director/ImportSource.php
new file mode 100644
index 0000000..c0d5ab5
--- /dev/null
+++ b/library/Fileshipper/ProvidedHook/Director/ImportSource.php
@@ -0,0 +1,613 @@
+<?php
+
+namespace Icinga\Module\Fileshipper\ProvidedHook\Director;
+
+use DirectoryIterator;
+use Icinga\Application\Config;
+use Icinga\Exception\ConfigurationError;
+use Icinga\Exception\IcingaException;
+use Icinga\Module\Director\Exception\JsonException;
+use Icinga\Module\Director\Hook\ImportSourceHook;
+use Icinga\Module\Director\Web\Form\QuickForm;
+use Icinga\Module\Fileshipper\Xlsx\Workbook;
+use RuntimeException;
+use Symfony\Component\Yaml\Yaml;
+
+class ImportSource extends ImportSourceHook
+{
+ protected $db;
+
+ protected $haveSymfonyYaml;
+
+ public function getName()
+ {
+ return 'Import from files (fileshipper)';
+ }
+
+ /**
+ * @return object[]
+ * @throws ConfigurationError
+ * @throws IcingaException
+ */
+ public function fetchData()
+ {
+ $basedir = $this->getSetting('basedir');
+ $filename = $this->getSetting('file_name');
+ $format = $this->getSetting('file_format');
+
+ if ($filename === '*') {
+ return $this->fetchFiles($basedir, $format);
+ }
+
+ return (array) $this->fetchFile($basedir, $filename, $format);
+ }
+
+ /**
+ * @return array
+ * @throws ConfigurationError
+ * @throws IcingaException
+ */
+ public function listColumns()
+ {
+ return array_keys((array) current($this->fetchData()));
+ }
+
+ /**
+ * @param QuickForm $form
+ * @return \Icinga\Module\Director\Forms\ImportSourceForm|QuickForm
+ * @throws \Zend_Form_Exception
+ */
+ public static function addSettingsFormFields(QuickForm $form)
+ {
+ $form->addElement('select', 'file_format', array(
+ 'label' => $form->translate('File format'),
+ 'description' => $form->translate(
+ 'Available file formats, usually CSV, JSON, YAML and XML. Whether'
+ . ' all of those are available eventually depends on various'
+ . ' libraries installed on your system. Please have a look at'
+ . ' the documentation in case your list is not complete.'
+ ),
+ 'required' => true,
+ 'class' => 'autosubmit',
+ 'multiOptions' => $form->optionalEnum(
+ static::listAvailableFormats($form)
+ ),
+ ));
+
+ /** @var \Icinga\Module\Director\Forms\ImportSourceForm $form */
+ $format = $form->getSentOrObjectSetting('file_format');
+
+ try {
+ $configFile = Config::module('fileshipper', 'imports')->getConfigFile();
+ $directories = static::listBaseDirectories();
+ $ignored = static::listIgnoredBaseDirectories();
+ $e = null;
+ } catch (\Throwable $e) {
+ $configFile = null;
+ $directories = [];
+ $ignored = [];
+ } catch (\Exception $e) {
+ $configFile = null;
+ $directories = [];
+ $ignored = [];
+ }
+ $form->addElement('select', 'basedir', array(
+ 'label' => $form->translate('Base directory'),
+ 'description' => sprintf(
+ $form->translate(
+ 'This import rule will only work with files relative to this'
+ . ' directory. The content of this list depends on your'
+ . ' configuration in "%s"'
+ ),
+ $configFile
+ ),
+ 'required' => true,
+ 'class' => 'autosubmit',
+ 'multiOptions' => $form->optionalEnum($directories),
+ ));
+ if ($configFile === null) {
+ if ($e) {
+ $form->getElement('basedir')->addError(sprintf(
+ $form->translate(
+ 'Failed to get directories from Fileshipper configuration: %s'
+ ),
+ $e->getMessage()
+ ));
+ }
+ } elseif (empty($directories)) {
+ $dirElement = $form->getElement('basedir');
+ if (! @file_exists($configFile)) {
+ $dirElement->addError(\sprintf(
+ 'The file "%s" does not exist or is not accessible',
+ $configFile
+ ));
+ }
+ }
+
+ if (! empty($ignored)) {
+ $list = [];
+ foreach ($ignored as $ignoredDirName => $section) {
+ $list[] = "$section: $ignoredDirName";
+ }
+ $ignoredString = \implode(', ', $list);
+ if (count($list) === 1) {
+ $errorString = 'The following directory has been ignored: %s';
+ } else {
+ $errorString = 'The following directories have been ignored: %s';
+ }
+ $form->addHtmlHint(\sprintf($errorString, $ignoredString));
+ }
+
+ if (! ($basedir = $form->getSentOrObjectSetting('basedir'))) {
+ return $form;
+ }
+
+ $form->addElement('select', 'file_name', array(
+ 'label' => $form->translate('File name'),
+ 'description' => $form->translate(
+ 'Choose a file from the above directory or * to import all files'
+ . ' from there at once'
+ ),
+ 'required' => true,
+ 'class' => 'autosubmit',
+ 'multiOptions' => $form->optionalEnum(self::enumFiles($basedir, $form)),
+ ));
+
+ $basedir = $form->getSentOrObjectSetting('basedir');
+ $basename = $form->getSentOrObjectSetting('file_name');
+ if ($basedir === null || $basename === null) {
+ return $form;
+ }
+
+ $filename = sprintf('%s/%s', $basedir, $basename);
+ switch ($format) {
+ case 'csv':
+ static::addCsvElements($form);
+ break;
+
+ case 'xslx':
+ static::addXslxElements($form, $filename);
+ break;
+ }
+
+ return $form;
+ }
+
+ /**
+ * @param QuickForm $form
+ * @throws \Zend_Form_Exception
+ */
+ protected static function addCsvElements(QuickForm $form)
+ {
+ $form->addElement('text', 'csv_delimiter', array(
+ 'label' => $form->translate('Field delimiter'),
+ 'description' => $form->translate(
+ 'This sets the field delimiter. One character only, defaults'
+ . ' to comma: ,'
+ ),
+ 'value' => ',',
+ 'required' => true,
+ ));
+
+ $form->addElement('text', 'csv_enclosure', array(
+ 'label' => $form->translate('Value enclosure'),
+ 'description' => $form->translate(
+ 'This sets the field enclosure character. One character only,'
+ . ' defaults to double quote: "'
+ ),
+ 'value' => '"',
+ 'required' => true,
+ ));
+
+ /*
+ // Not configuring escape as it behaves strangely. "te""st" works fine.
+ // Seems that even in case we use \, it must be "manually" removed later
+ // on
+ $form->addElement('text', 'csv_escape', array(
+ 'label' => $form->translate('Escape character'),
+ 'description' => $form->translate(
+ 'This sets the escaping character. One character only,'
+ . ' defaults to backslash: \\'
+ ),
+ 'value' => '\\',
+ 'required' => true,
+ ));
+ */
+ }
+
+ /**
+ * @param QuickForm $form
+ * @param $filename
+ * @throws \Zend_Form_Exception
+ */
+ protected static function addXslxElements(QuickForm $form, $filename)
+ {
+ $form->addElement('select', 'worksheet_addressing', array(
+ 'label' => $form->translate('Choose worksheet'),
+ 'description' => $form->translate('How to choose a worksheet'),
+ 'multiOptions' => array(
+ 'by_position' => $form->translate('by position'),
+ 'by_name' => $form->translate('by name'),
+ ),
+ 'value' => 'by_position',
+ 'class' => 'autosubmit',
+ 'required' => true,
+ ));
+
+ /** @var \Icinga\Module\Director\Forms\ImportSourceForm $form */
+ $addressing = $form->getSentOrObjectSetting('worksheet_addressing');
+ switch ($addressing) {
+ case 'by_name':
+ $file = static::loadXslxFile($filename);
+ $names = $file->getSheetNames();
+ $names = array_combine($names, $names);
+ $form->addElement('select', 'worksheet_name', array(
+ 'label' => $form->translate('Name'),
+ 'required' => true,
+ 'value' => $file->getFirstSheetName(),
+ 'multiOptions' => $names,
+ ));
+ break;
+
+ case 'by_position':
+ default:
+ $form->addElement('text', 'worksheet_position', array(
+ 'label' => $form->translate('Position'),
+ 'required' => true,
+ 'value' => '1',
+ ));
+ break;
+ }
+ }
+
+ /**
+ * @param $basedir
+ * @param $format
+ * @return array
+ * @throws ConfigurationError
+ * @throws IcingaException
+ */
+ protected function fetchFiles($basedir, $format)
+ {
+ $result = array();
+ foreach (static::listFiles($basedir) as $file) {
+ $result[$file] = (object) $this->fetchFile($basedir, $file, $format);
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param $basedir
+ * @param $file
+ * @param $format
+ * @return object[]
+ * @throws ConfigurationError
+ * @throws IcingaException
+ */
+ protected function fetchFile($basedir, $file, $format)
+ {
+ $filename = $basedir . '/' . $file;
+
+ switch ($format) {
+ case 'yaml':
+ return $this->readYamlFile($filename);
+ case 'json':
+ return $this->readJsonFile($filename);
+ case 'csv':
+ return $this->readCsvFile($filename);
+ case 'xslx':
+ return $this->readXslxFile($filename);
+ case 'xml':
+ libxml_disable_entity_loader(true);
+ return $this->readXmlFile($filename);
+ default:
+ throw new ConfigurationError(
+ 'Unsupported file format: %s',
+ $format
+ );
+ }
+ }
+
+ /**
+ * @param $filename
+ * @return Workbook
+ */
+ protected static function loadXslxFile($filename)
+ {
+ return new Workbook($filename);
+ }
+
+ /**
+ * @param $filename
+ * @return array
+ */
+ protected function readXslxFile($filename)
+ {
+ $xlsx = new Workbook($filename);
+ if ($this->getSetting('worksheet_addressing') === 'by_name') {
+ $sheet = $xlsx->getSheetByName($this->getSetting('worksheet_name'));
+ } else {
+ $sheet = $xlsx->getSheet((int) $this->getSetting('worksheet_position'));
+ }
+
+ $data = $sheet->getData();
+
+ $headers = null;
+ $result = [];
+ foreach ($data as $line) {
+ if ($headers === null) {
+ $hasValue = false;
+ foreach ($line as $value) {
+ if ($value !== null) {
+ $hasValue = true;
+ break;
+ }
+ // For now, no value in the first column means this is no header
+ break;
+ }
+ if ($hasValue) {
+ $headers = $line;
+ }
+
+ continue;
+ }
+
+ $row = [];
+ foreach ($line as $key => $val) {
+ if (empty($headers[$key])) {
+ continue;
+ }
+ $row[$headers[$key]] = $val;
+ }
+
+ $result[] = (object) $row;
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param $filename
+ * @return object[]
+ */
+ protected function readCsvFile($filename)
+ {
+ $fh = fopen($filename, 'r');
+ $lines = array();
+ $delimiter = $this->getSetting('csv_delimiter');
+ $enclosure = $this->getSetting('csv_enclosure');
+ // $escape = $this->getSetting('csv_escape');
+
+ $headers = fgetcsv($fh, 0, $delimiter, $enclosure/*, $escape*/);
+ $row = 1;
+ while ($line = fgetcsv($fh, 0, $delimiter, $enclosure/*, $escape*/)) {
+ if (empty($line)) {
+ continue;
+ }
+ if (count($headers) !== count($line)) {
+ throw new RuntimeException(sprintf(
+ 'Column count in row %d does not match columns in header row',
+ $row
+ ));
+ }
+
+ $line = array_combine($headers, $line);
+ foreach ($line as $key => & $value) {
+ if ($value === '') {
+ $value = null;
+ }
+ }
+ unset($value);
+ $lines[] = (object) $line;
+
+ $row ++;
+ }
+ fclose($fh);
+
+ return $lines;
+ }
+
+ /**
+ * @param $filename
+ * @return object[]
+ */
+ protected function readJsonFile($filename)
+ {
+ $content = @file_get_contents($filename);
+ if ($content === false) {
+ throw new RuntimeException(sprintf(
+ 'Unable to read JSON file "%s"',
+ $filename
+ ));
+ }
+
+ $data = @json_decode($content);
+ if ($data === null) {
+ throw JsonException::forLastJsonError('Unable to load JSON data');
+ }
+
+ return $data;
+ }
+
+ /**
+ * @param $file
+ * @return object[]
+ */
+ protected function readXmlFile($file)
+ {
+ $lines = array();
+ $content = file_get_contents($file);
+ foreach (simplexml_load_string($content) as $entry) {
+ $line = null;
+ $lines[] = $this->normalizeSimpleXML($entry);
+ }
+
+ return $lines;
+ }
+
+ /**
+ * @param $object
+ * @return object
+ */
+ protected function normalizeSimpleXML($object)
+ {
+ $data = $object;
+ if (is_object($data)) {
+ $data = (object) get_object_vars($data);
+ }
+
+ if (is_object($data)) {
+ foreach ($data as $key => $value) {
+ $data->$key = $this->normalizeSimpleXml($value);
+ }
+ }
+
+ if (is_array($data)) {
+ foreach ($data as $key => $value) {
+ $data[$key] = $this->normalizeSimpleXml($value);
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * @param $file
+ * @return object[]
+ */
+ protected function readYamlFile($file)
+ {
+ return $this->fixYamlObjects(
+ yaml_parse_file($file)
+ );
+ }
+
+ /**
+ * @param $what
+ * @return object[]
+ */
+ protected function fixYamlObjects($what)
+ {
+ if (is_array($what)) {
+ foreach (array_keys($what) as $key) {
+ if (! is_int($key)) {
+ $what = (object) $what;
+ break;
+ }
+ }
+ }
+
+ if (is_array($what) || is_object($what)) {
+ foreach ($what as $k => $v) {
+ if (! empty($v)) {
+ if (is_object($what)) {
+ $what->$k = $this->fixYamlObjects($v);
+ } elseif (is_array($what)) {
+ $what[$k] = $this->fixYamlObjects($v);
+ }
+ }
+ }
+ }
+
+ return $what;
+ }
+
+ /**
+ * @param QuickForm $form
+ * @return array
+ */
+ protected static function listAvailableFormats(QuickForm $form)
+ {
+ $formats = array(
+ 'csv' => $form->translate('CSV (Comma Separated Value)'),
+ 'json' => $form->translate('JSON (JavaScript Object Notation)'),
+ );
+
+ if (class_exists('\\ZipArchive')) {
+ $formats['xslx'] = $form->translate('XSLX (Microsoft Excel 2007+)');
+ }
+
+ if (function_exists('simplexml_load_file')) {
+ $formats['xml'] = $form->translate('XML (Extensible Markup Language)');
+ }
+
+ if (function_exists('yaml_parse_file')) {
+ $formats['yaml'] = $form->translate('YAML (Ain\'t Markup Language)');
+ }
+
+ return $formats;
+ }
+
+ /**
+ * @return array
+ */
+ protected static function listBaseDirectories()
+ {
+ $dirs = array();
+
+ foreach (Config::module('fileshipper', 'imports') as $key => $section) {
+ if (($dir = $section->get('basedir')) && @is_dir($dir)) {
+ $dirs[$dir] = $key;
+ }
+ }
+
+ return $dirs;
+ }
+
+ /**
+ * @return array
+ */
+ protected static function listIgnoredBaseDirectories()
+ {
+ $dirs = array();
+
+ foreach (Config::module('fileshipper', 'imports') as $key => $section) {
+ if (($dir = $section->get('basedir')) && @is_dir($dir)) {
+ // Ignore them
+ } else {
+ $dirs[$dir] = $key;
+ }
+ }
+
+ return $dirs;
+ }
+
+ /**
+ * @param $basedir
+ * @param QuickForm $form
+ * @return array
+ */
+ protected static function enumFiles($basedir, QuickForm $form)
+ {
+ return array_merge(
+ array(
+ '*' => sprintf('* (%s)', $form->translate('all files'))
+ ),
+ static::listFiles($basedir)
+ );
+ }
+
+ /**
+ * @param $basedir
+ * @return array
+ */
+ protected static function listFiles($basedir)
+ {
+ $files = array();
+
+ $dir = new DirectoryIterator($basedir);
+ foreach ($dir as $file) {
+ if ($file->isFile()) {
+ $filename = $file->getBasename();
+ if ($filename[0] !== '.') {
+ $files[$filename] = $filename;
+ }
+ }
+ }
+
+ ksort($files);
+
+ return $files;
+ }
+}
diff --git a/library/Fileshipper/ProvidedHook/Director/ShipConfigFiles.php b/library/Fileshipper/ProvidedHook/Director/ShipConfigFiles.php
new file mode 100644
index 0000000..43f6f38
--- /dev/null
+++ b/library/Fileshipper/ProvidedHook/Director/ShipConfigFiles.php
@@ -0,0 +1,78 @@
+<?php
+
+namespace Icinga\Module\Fileshipper\ProvidedHook\Director;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Module\Director\Hook\ShipConfigFilesHook;
+use RecursiveDirectoryIterator;
+use RecursiveIteratorIterator;
+use RegexIterator;
+
+class ShipConfigFiles extends ShipConfigFilesHook
+{
+ /**
+ * @return array
+ */
+ public function fetchFiles()
+ {
+ $files = [];
+ foreach ($this->getDirectories() as $key => $cfg) {
+ $target = $cfg->get('target');
+ try {
+ foreach ($this->listFiles($cfg->get('source'), $cfg->get('extensions')) as $file) {
+ try {
+ $files["$target/$file"] = file_get_contents($cfg->get('source') . '/' . $file);
+ } catch (Exception $e) {
+ $files["$target/$file"] = '/* ' . $e->getMessage() . ' */';
+ }
+ }
+ } catch (Exception $e) {
+ $files["$target/ERROR.txt"] = '/* ' . $e->getMessage() . ' */';
+ }
+ }
+
+ return $files;
+ }
+
+ /**
+ * @param $folder
+ * @param $extensions
+ * @return array
+ */
+ protected function listFiles($folder, $extensions)
+ {
+ if (! $extensions) {
+ $pattern = '/^[^\.].+\.conf$/';
+ } else {
+ $exts = [];
+ foreach (preg_split('/\s+/', $extensions, -1, PREG_SPLIT_NO_EMPTY) as $ext) {
+ $exts[] = preg_quote($ext, '/');
+ }
+
+ $pattern = '/^[^\.].+(?:' . implode('|', $exts) . ')$/';
+ }
+
+ $dir = new RecursiveDirectoryIterator($folder);
+ $ite = new RecursiveIteratorIterator($dir);
+ $files = new RegexIterator($ite, $pattern, RegexIterator::GET_MATCH);
+ $fileList = [];
+ $start = strlen($folder) + 1;
+
+ foreach ($files as $file) {
+ foreach ($file as $f) {
+ $fileList[] = substr($f, $start);
+ }
+ }
+
+ return $fileList;
+ }
+
+ /**
+ * @return Config
+ */
+ protected function getDirectories()
+ {
+ return Config::module('fileshipper', 'directories');
+ }
+}
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;
+ }
+}