Ods.php 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708
  1. <?php
  2. namespace PhpOffice\PhpSpreadsheet\Reader;
  3. use DateTime;
  4. use DateTimeZone;
  5. use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
  6. use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
  7. use PhpOffice\PhpSpreadsheet\Cell\DataType;
  8. use PhpOffice\PhpSpreadsheet\Reader\Ods\Properties as DocumentProperties;
  9. use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
  10. use PhpOffice\PhpSpreadsheet\RichText\RichText;
  11. use PhpOffice\PhpSpreadsheet\Settings;
  12. use PhpOffice\PhpSpreadsheet\Shared\Date;
  13. use PhpOffice\PhpSpreadsheet\Shared\File;
  14. use PhpOffice\PhpSpreadsheet\Spreadsheet;
  15. use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
  16. use XMLReader;
  17. use ZipArchive;
  18. class Ods extends BaseReader
  19. {
  20. /**
  21. * Create a new Ods Reader instance.
  22. */
  23. public function __construct()
  24. {
  25. parent::__construct();
  26. $this->securityScanner = XmlScanner::getInstance($this);
  27. }
  28. /**
  29. * Can the current IReader read the file?
  30. *
  31. * @param string $pFilename
  32. *
  33. * @throws Exception
  34. *
  35. * @return bool
  36. */
  37. public function canRead($pFilename)
  38. {
  39. File::assertFile($pFilename);
  40. $mimeType = 'UNKNOWN';
  41. // Load file
  42. $zip = new ZipArchive();
  43. if ($zip->open($pFilename) === true) {
  44. // check if it is an OOXML archive
  45. $stat = $zip->statName('mimetype');
  46. if ($stat && ($stat['size'] <= 255)) {
  47. $mimeType = $zip->getFromName($stat['name']);
  48. } elseif ($zip->statName('META-INF/manifest.xml')) {
  49. $xml = simplexml_load_string(
  50. $this->securityScanner->scan($zip->getFromName('META-INF/manifest.xml')),
  51. 'SimpleXMLElement',
  52. Settings::getLibXmlLoaderOptions()
  53. );
  54. $namespacesContent = $xml->getNamespaces(true);
  55. if (isset($namespacesContent['manifest'])) {
  56. $manifest = $xml->children($namespacesContent['manifest']);
  57. foreach ($manifest as $manifestDataSet) {
  58. $manifestAttributes = $manifestDataSet->attributes($namespacesContent['manifest']);
  59. if ($manifestAttributes->{'full-path'} == '/') {
  60. $mimeType = (string) $manifestAttributes->{'media-type'};
  61. break;
  62. }
  63. }
  64. }
  65. }
  66. $zip->close();
  67. return $mimeType === 'application/vnd.oasis.opendocument.spreadsheet';
  68. }
  69. return false;
  70. }
  71. /**
  72. * Reads names of the worksheets from a file, without parsing the whole file to a PhpSpreadsheet object.
  73. *
  74. * @param string $pFilename
  75. *
  76. * @throws Exception
  77. *
  78. * @return string[]
  79. */
  80. public function listWorksheetNames($pFilename)
  81. {
  82. File::assertFile($pFilename);
  83. $zip = new ZipArchive();
  84. if (!$zip->open($pFilename)) {
  85. throw new Exception('Could not open ' . $pFilename . ' for reading! Error opening file.');
  86. }
  87. $worksheetNames = [];
  88. $xml = new XMLReader();
  89. $xml->xml(
  90. $this->securityScanner->scanFile('zip://' . realpath($pFilename) . '#content.xml'),
  91. null,
  92. Settings::getLibXmlLoaderOptions()
  93. );
  94. $xml->setParserProperty(2, true);
  95. // Step into the first level of content of the XML
  96. $xml->read();
  97. while ($xml->read()) {
  98. // Quickly jump through to the office:body node
  99. while ($xml->name !== 'office:body') {
  100. if ($xml->isEmptyElement) {
  101. $xml->read();
  102. } else {
  103. $xml->next();
  104. }
  105. }
  106. // Now read each node until we find our first table:table node
  107. while ($xml->read()) {
  108. if ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) {
  109. // Loop through each table:table node reading the table:name attribute for each worksheet name
  110. do {
  111. $worksheetNames[] = $xml->getAttribute('table:name');
  112. $xml->next();
  113. } while ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT);
  114. }
  115. }
  116. }
  117. return $worksheetNames;
  118. }
  119. /**
  120. * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
  121. *
  122. * @param string $pFilename
  123. *
  124. * @throws Exception
  125. *
  126. * @return array
  127. */
  128. public function listWorksheetInfo($pFilename)
  129. {
  130. File::assertFile($pFilename);
  131. $worksheetInfo = [];
  132. $zip = new ZipArchive();
  133. if (!$zip->open($pFilename)) {
  134. throw new Exception('Could not open ' . $pFilename . ' for reading! Error opening file.');
  135. }
  136. $xml = new XMLReader();
  137. $xml->xml(
  138. $this->securityScanner->scanFile('zip://' . realpath($pFilename) . '#content.xml'),
  139. null,
  140. Settings::getLibXmlLoaderOptions()
  141. );
  142. $xml->setParserProperty(2, true);
  143. // Step into the first level of content of the XML
  144. $xml->read();
  145. while ($xml->read()) {
  146. // Quickly jump through to the office:body node
  147. while ($xml->name !== 'office:body') {
  148. if ($xml->isEmptyElement) {
  149. $xml->read();
  150. } else {
  151. $xml->next();
  152. }
  153. }
  154. // Now read each node until we find our first table:table node
  155. while ($xml->read()) {
  156. if ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) {
  157. $worksheetNames[] = $xml->getAttribute('table:name');
  158. $tmpInfo = [
  159. 'worksheetName' => $xml->getAttribute('table:name'),
  160. 'lastColumnLetter' => 'A',
  161. 'lastColumnIndex' => 0,
  162. 'totalRows' => 0,
  163. 'totalColumns' => 0,
  164. ];
  165. // Loop through each child node of the table:table element reading
  166. $currCells = 0;
  167. do {
  168. $xml->read();
  169. if ($xml->name == 'table:table-row' && $xml->nodeType == XMLReader::ELEMENT) {
  170. $rowspan = $xml->getAttribute('table:number-rows-repeated');
  171. $rowspan = empty($rowspan) ? 1 : $rowspan;
  172. $tmpInfo['totalRows'] += $rowspan;
  173. $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
  174. $currCells = 0;
  175. // Step into the row
  176. $xml->read();
  177. do {
  178. if ($xml->name == 'table:table-cell' && $xml->nodeType == XMLReader::ELEMENT) {
  179. if (!$xml->isEmptyElement) {
  180. ++$currCells;
  181. $xml->next();
  182. } else {
  183. $xml->read();
  184. }
  185. } elseif ($xml->name == 'table:covered-table-cell' && $xml->nodeType == XMLReader::ELEMENT) {
  186. $mergeSize = $xml->getAttribute('table:number-columns-repeated');
  187. $currCells += (int) $mergeSize;
  188. $xml->read();
  189. } else {
  190. $xml->read();
  191. }
  192. } while ($xml->name != 'table:table-row');
  193. }
  194. } while ($xml->name != 'table:table');
  195. $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
  196. $tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1;
  197. $tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1);
  198. $worksheetInfo[] = $tmpInfo;
  199. }
  200. }
  201. }
  202. return $worksheetInfo;
  203. }
  204. /**
  205. * Loads PhpSpreadsheet from file.
  206. *
  207. * @param string $pFilename
  208. *
  209. * @throws Exception
  210. *
  211. * @return Spreadsheet
  212. */
  213. public function load($pFilename)
  214. {
  215. // Create new Spreadsheet
  216. $spreadsheet = new Spreadsheet();
  217. // Load into this instance
  218. return $this->loadIntoExisting($pFilename, $spreadsheet);
  219. }
  220. /**
  221. * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
  222. *
  223. * @param string $pFilename
  224. * @param Spreadsheet $spreadsheet
  225. *
  226. * @throws Exception
  227. *
  228. * @return Spreadsheet
  229. */
  230. public function loadIntoExisting($pFilename, Spreadsheet $spreadsheet)
  231. {
  232. File::assertFile($pFilename);
  233. $timezoneObj = new DateTimeZone('Europe/London');
  234. $GMT = new \DateTimeZone('UTC');
  235. $zip = new ZipArchive();
  236. if (!$zip->open($pFilename)) {
  237. throw new Exception("Could not open {$pFilename} for reading! Error opening file.");
  238. }
  239. // Meta
  240. $xml = simplexml_load_string(
  241. $this->securityScanner->scan($zip->getFromName('meta.xml')),
  242. 'SimpleXMLElement',
  243. Settings::getLibXmlLoaderOptions()
  244. );
  245. if ($xml === false) {
  246. throw new Exception('Unable to read data from {$pFilename}');
  247. }
  248. $namespacesMeta = $xml->getNamespaces(true);
  249. (new DocumentProperties($spreadsheet))->load($xml, $namespacesMeta);
  250. // Content
  251. $dom = new \DOMDocument('1.01', 'UTF-8');
  252. $dom->loadXML(
  253. $this->securityScanner->scan($zip->getFromName('content.xml')),
  254. Settings::getLibXmlLoaderOptions()
  255. );
  256. $officeNs = $dom->lookupNamespaceUri('office');
  257. $tableNs = $dom->lookupNamespaceUri('table');
  258. $textNs = $dom->lookupNamespaceUri('text');
  259. $xlinkNs = $dom->lookupNamespaceUri('xlink');
  260. $spreadsheets = $dom->getElementsByTagNameNS($officeNs, 'body')
  261. ->item(0)
  262. ->getElementsByTagNameNS($officeNs, 'spreadsheet');
  263. foreach ($spreadsheets as $workbookData) {
  264. /** @var \DOMElement $workbookData */
  265. $tables = $workbookData->getElementsByTagNameNS($tableNs, 'table');
  266. $worksheetID = 0;
  267. foreach ($tables as $worksheetDataSet) {
  268. /** @var \DOMElement $worksheetDataSet */
  269. $worksheetName = $worksheetDataSet->getAttributeNS($tableNs, 'name');
  270. // Check loadSheetsOnly
  271. if (isset($this->loadSheetsOnly)
  272. && $worksheetName
  273. && !in_array($worksheetName, $this->loadSheetsOnly)) {
  274. continue;
  275. }
  276. // Create sheet
  277. if ($worksheetID > 0) {
  278. $spreadsheet->createSheet(); // First sheet is added by default
  279. }
  280. $spreadsheet->setActiveSheetIndex($worksheetID);
  281. if ($worksheetName) {
  282. // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in
  283. // formula cells... during the load, all formulae should be correct, and we're simply
  284. // bringing the worksheet name in line with the formula, not the reverse
  285. $spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false);
  286. }
  287. // Go through every child of table element
  288. $rowID = 1;
  289. foreach ($worksheetDataSet->childNodes as $childNode) {
  290. /** @var \DOMElement $childNode */
  291. // Filter elements which are not under the "table" ns
  292. if ($childNode->namespaceURI != $tableNs) {
  293. continue;
  294. }
  295. $key = $childNode->nodeName;
  296. // Remove ns from node name
  297. if (strpos($key, ':') !== false) {
  298. $keyChunks = explode(':', $key);
  299. $key = array_pop($keyChunks);
  300. }
  301. switch ($key) {
  302. case 'table-header-rows':
  303. /// TODO :: Figure this out. This is only a partial implementation I guess.
  304. // ($rowData it's not used at all and I'm not sure that PHPExcel
  305. // has an API for this)
  306. // foreach ($rowData as $keyRowData => $cellData) {
  307. // $rowData = $cellData;
  308. // break;
  309. // }
  310. break;
  311. case 'table-row':
  312. if ($childNode->hasAttributeNS($tableNs, 'number-rows-repeated')) {
  313. $rowRepeats = $childNode->getAttributeNS($tableNs, 'number-rows-repeated');
  314. } else {
  315. $rowRepeats = 1;
  316. }
  317. $columnID = 'A';
  318. foreach ($childNode->childNodes as $key => $cellData) {
  319. // @var \DOMElement $cellData
  320. if ($this->getReadFilter() !== null) {
  321. if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) {
  322. ++$columnID;
  323. continue;
  324. }
  325. }
  326. // Initialize variables
  327. $formatting = $hyperlink = null;
  328. $hasCalculatedValue = false;
  329. $cellDataFormula = '';
  330. if ($cellData->hasAttributeNS($tableNs, 'formula')) {
  331. $cellDataFormula = $cellData->getAttributeNS($tableNs, 'formula');
  332. $hasCalculatedValue = true;
  333. }
  334. // Annotations
  335. $annotation = $cellData->getElementsByTagNameNS($officeNs, 'annotation');
  336. if ($annotation->length > 0) {
  337. $textNode = $annotation->item(0)->getElementsByTagNameNS($textNs, 'p');
  338. if ($textNode->length > 0) {
  339. $text = $this->scanElementForText($textNode->item(0));
  340. $spreadsheet->getActiveSheet()
  341. ->getComment($columnID . $rowID)
  342. ->setText($this->parseRichText($text));
  343. // ->setAuthor( $author )
  344. }
  345. }
  346. // Content
  347. /** @var \DOMElement[] $paragraphs */
  348. $paragraphs = [];
  349. foreach ($cellData->childNodes as $item) {
  350. /** @var \DOMElement $item */
  351. // Filter text:p elements
  352. if ($item->nodeName == 'text:p') {
  353. $paragraphs[] = $item;
  354. }
  355. }
  356. if (count($paragraphs) > 0) {
  357. // Consolidate if there are multiple p records (maybe with spans as well)
  358. $dataArray = [];
  359. // Text can have multiple text:p and within those, multiple text:span.
  360. // text:p newlines, but text:span does not.
  361. // Also, here we assume there is no text data is span fields are specified, since
  362. // we have no way of knowing proper positioning anyway.
  363. foreach ($paragraphs as $pData) {
  364. $dataArray[] = $this->scanElementForText($pData);
  365. }
  366. $allCellDataText = implode("\n", $dataArray);
  367. $type = $cellData->getAttributeNS($officeNs, 'value-type');
  368. switch ($type) {
  369. case 'string':
  370. $type = DataType::TYPE_STRING;
  371. $dataValue = $allCellDataText;
  372. foreach ($paragraphs as $paragraph) {
  373. $link = $paragraph->getElementsByTagNameNS($textNs, 'a');
  374. if ($link->length > 0) {
  375. $hyperlink = $link->item(0)->getAttributeNS($xlinkNs, 'href');
  376. }
  377. }
  378. break;
  379. case 'boolean':
  380. $type = DataType::TYPE_BOOL;
  381. $dataValue = ($allCellDataText == 'TRUE') ? true : false;
  382. break;
  383. case 'percentage':
  384. $type = DataType::TYPE_NUMERIC;
  385. $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
  386. if (floor($dataValue) == $dataValue) {
  387. $dataValue = (int) $dataValue;
  388. }
  389. $formatting = NumberFormat::FORMAT_PERCENTAGE_00;
  390. break;
  391. case 'currency':
  392. $type = DataType::TYPE_NUMERIC;
  393. $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
  394. if (floor($dataValue) == $dataValue) {
  395. $dataValue = (int) $dataValue;
  396. }
  397. $formatting = NumberFormat::FORMAT_CURRENCY_USD_SIMPLE;
  398. break;
  399. case 'float':
  400. $type = DataType::TYPE_NUMERIC;
  401. $dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
  402. if (floor($dataValue) == $dataValue) {
  403. if ($dataValue == (int) $dataValue) {
  404. $dataValue = (int) $dataValue;
  405. } else {
  406. $dataValue = (float) $dataValue;
  407. }
  408. }
  409. break;
  410. case 'date':
  411. $type = DataType::TYPE_NUMERIC;
  412. $value = $cellData->getAttributeNS($officeNs, 'date-value');
  413. $dateObj = new DateTime($value, $GMT);
  414. $dateObj->setTimeZone($timezoneObj);
  415. list($year, $month, $day, $hour, $minute, $second) = explode(
  416. ' ',
  417. $dateObj->format('Y m d H i s')
  418. );
  419. $dataValue = Date::formattedPHPToExcel(
  420. (int) $year,
  421. (int) $month,
  422. (int) $day,
  423. (int) $hour,
  424. (int) $minute,
  425. (int) $second
  426. );
  427. if ($dataValue != floor($dataValue)) {
  428. $formatting = NumberFormat::FORMAT_DATE_XLSX15
  429. . ' '
  430. . NumberFormat::FORMAT_DATE_TIME4;
  431. } else {
  432. $formatting = NumberFormat::FORMAT_DATE_XLSX15;
  433. }
  434. break;
  435. case 'time':
  436. $type = DataType::TYPE_NUMERIC;
  437. $timeValue = $cellData->getAttributeNS($officeNs, 'time-value');
  438. $dataValue = Date::PHPToExcel(
  439. strtotime(
  440. '01-01-1970 ' . implode(':', sscanf($timeValue, 'PT%dH%dM%dS'))
  441. )
  442. );
  443. $formatting = NumberFormat::FORMAT_DATE_TIME4;
  444. break;
  445. default:
  446. $dataValue = null;
  447. }
  448. } else {
  449. $type = DataType::TYPE_NULL;
  450. $dataValue = null;
  451. }
  452. if ($hasCalculatedValue) {
  453. $type = DataType::TYPE_FORMULA;
  454. $cellDataFormula = substr($cellDataFormula, strpos($cellDataFormula, ':=') + 1);
  455. $temp = explode('"', $cellDataFormula);
  456. $tKey = false;
  457. foreach ($temp as &$value) {
  458. // Only replace in alternate array entries (i.e. non-quoted blocks)
  459. if ($tKey = !$tKey) {
  460. // Cell range reference in another sheet
  461. $value = preg_replace('/\[([^\.]+)\.([^\.]+):\.([^\.]+)\]/U', '$1!$2:$3', $value);
  462. // Cell reference in another sheet
  463. $value = preg_replace('/\[([^\.]+)\.([^\.]+)\]/U', '$1!$2', $value);
  464. // Cell range reference
  465. $value = preg_replace('/\[\.([^\.]+):\.([^\.]+)\]/U', '$1:$2', $value);
  466. // Simple cell reference
  467. $value = preg_replace('/\[\.([^\.]+)\]/U', '$1', $value);
  468. $value = Calculation::translateSeparator(';', ',', $value, $inBraces);
  469. }
  470. }
  471. unset($value);
  472. // Then rebuild the formula string
  473. $cellDataFormula = implode('"', $temp);
  474. }
  475. if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) {
  476. $colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated');
  477. } else {
  478. $colRepeats = 1;
  479. }
  480. if ($type !== null) {
  481. for ($i = 0; $i < $colRepeats; ++$i) {
  482. if ($i > 0) {
  483. ++$columnID;
  484. }
  485. if ($type !== DataType::TYPE_NULL) {
  486. for ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) {
  487. $rID = $rowID + $rowAdjust;
  488. $cell = $spreadsheet->getActiveSheet()
  489. ->getCell($columnID . $rID);
  490. // Set value
  491. if ($hasCalculatedValue) {
  492. $cell->setValueExplicit($cellDataFormula, $type);
  493. } else {
  494. $cell->setValueExplicit($dataValue, $type);
  495. }
  496. if ($hasCalculatedValue) {
  497. $cell->setCalculatedValue($dataValue);
  498. }
  499. // Set other properties
  500. if ($formatting !== null) {
  501. $spreadsheet->getActiveSheet()
  502. ->getStyle($columnID . $rID)
  503. ->getNumberFormat()
  504. ->setFormatCode($formatting);
  505. } else {
  506. $spreadsheet->getActiveSheet()
  507. ->getStyle($columnID . $rID)
  508. ->getNumberFormat()
  509. ->setFormatCode(NumberFormat::FORMAT_GENERAL);
  510. }
  511. if ($hyperlink !== null) {
  512. $cell->getHyperlink()
  513. ->setUrl($hyperlink);
  514. }
  515. }
  516. }
  517. }
  518. }
  519. // Merged cells
  520. if ($cellData->hasAttributeNS($tableNs, 'number-columns-spanned')
  521. || $cellData->hasAttributeNS($tableNs, 'number-rows-spanned')
  522. ) {
  523. if (($type !== DataType::TYPE_NULL) || (!$this->readDataOnly)) {
  524. $columnTo = $columnID;
  525. if ($cellData->hasAttributeNS($tableNs, 'number-columns-spanned')) {
  526. $columnIndex = Coordinate::columnIndexFromString($columnID);
  527. $columnIndex += (int) $cellData->getAttributeNS($tableNs, 'number-columns-spanned');
  528. $columnIndex -= 2;
  529. $columnTo = Coordinate::stringFromColumnIndex($columnIndex + 1);
  530. }
  531. $rowTo = $rowID;
  532. if ($cellData->hasAttributeNS($tableNs, 'number-rows-spanned')) {
  533. $rowTo = $rowTo + (int) $cellData->getAttributeNS($tableNs, 'number-rows-spanned') - 1;
  534. }
  535. $cellRange = $columnID . $rowID . ':' . $columnTo . $rowTo;
  536. $spreadsheet->getActiveSheet()->mergeCells($cellRange);
  537. }
  538. }
  539. ++$columnID;
  540. }
  541. $rowID += $rowRepeats;
  542. break;
  543. }
  544. }
  545. ++$worksheetID;
  546. }
  547. }
  548. // Return
  549. return $spreadsheet;
  550. }
  551. /**
  552. * Recursively scan element.
  553. *
  554. * @param \DOMNode $element
  555. *
  556. * @return string
  557. */
  558. protected function scanElementForText(\DOMNode $element)
  559. {
  560. $str = '';
  561. foreach ($element->childNodes as $child) {
  562. /** @var \DOMNode $child */
  563. if ($child->nodeType == XML_TEXT_NODE) {
  564. $str .= $child->nodeValue;
  565. } elseif ($child->nodeType == XML_ELEMENT_NODE && $child->nodeName == 'text:s') {
  566. // It's a space
  567. // Multiple spaces?
  568. /** @var \DOMAttr $cAttr */
  569. $cAttr = $child->attributes->getNamedItem('c');
  570. if ($cAttr) {
  571. $multiplier = (int) $cAttr->nodeValue;
  572. } else {
  573. $multiplier = 1;
  574. }
  575. $str .= str_repeat(' ', $multiplier);
  576. }
  577. if ($child->hasChildNodes()) {
  578. $str .= $this->scanElementForText($child);
  579. }
  580. }
  581. return $str;
  582. }
  583. /**
  584. * @param string $is
  585. *
  586. * @return RichText
  587. */
  588. private function parseRichText($is)
  589. {
  590. $value = new RichText();
  591. $value->createText($is);
  592. return $value;
  593. }
  594. }