encryptionPassword = $encryptionPassword;
return $this;
}
public function setMaxEncryptionSpinCount(int $maxEncryptionSpinCount): self
{
if ($maxEncryptionSpinCount < 0 || $maxEncryptionSpinCount > AgileEncryption::MAX_SPIN_COUNT) {
throw new InvalidArgumentException('Maximum encryption spin count must be between 0 and ' . AgileEncryption::MAX_SPIN_COUNT . '.');
}
$this->maxEncryptionSpinCount = $maxEncryptionSpinCount;
return $this;
}
/**
* Allow use of LIBXML_PARSEHUGE.
* This option can lead to memory leaks and failures,
* and is not recommended. But some very large spreadsheets
* seem to require it.
*/
public function setParseHuge(bool $parseHuge): void
{
$this->parseHuge = $parseHuge;
}
/**
* Create a new Xlsx Reader instance.
*/
public function __construct()
{
parent::__construct();
$this->referenceHelper = ReferenceHelper::getInstance();
$this->securityScanner = XmlScanner::getInstance($this);
}
/**
* Can the current IReader read the file?
*/
public function canRead(string $filename): bool
{
if (!File::testFileNoThrow($filename, self::INITIAL_FILE)) {
return $this->hasEncryptedPackage($filename);
}
$result = false;
$this->zip = $zip = new ZipArchive();
if ($zip->open($filename) === true) {
[$workbookBasename] = $this->getWorkbookBaseName();
$result = !empty($workbookBasename);
$zip->close();
}
return $result;
}
public function load(string $filename, int $flags = 0): Spreadsheet
{
$temporaryFilename = $this->decryptToTemporaryFile($filename);
if ($temporaryFilename === null) {
return parent::load($filename, $flags);
}
try {
return parent::load($temporaryFilename, $flags);
} finally {
@unlink($temporaryFilename);
}
}
private function hasEncryptedPackage(string $filename): bool
{
try {
$ole = new OLE();
$ole->read($filename);
return $ole->hasDataByName('EncryptionInfo') && $ole->hasDataByName('EncryptedPackage');
} catch (Throwable $exception) {
return false;
}
}
private function decryptToTemporaryFile(string $filename): ?string
{
if (File::testFileNoThrow($filename, self::INITIAL_FILE)) {
return null;
}
try {
$ole = new OLE();
$ole->read($filename);
$encryptionInfo = $ole->getDataByName('EncryptionInfo');
} catch (Throwable $exception) {
return null;
}
$temporaryFilename = File::temporaryFilename();
$encryptedPackageFilename = File::temporaryFilename();
$encryptedPackage = fopen($encryptedPackageFilename, 'wb');
if ($encryptedPackage === false) {
@unlink($temporaryFilename);
@unlink($encryptedPackageFilename);
throw new Exception('Could not create decrypted XLSX package.');
}
try {
$ole->copyDataByName('EncryptedPackage', $encryptedPackage);
fclose($encryptedPackage);
$encryptedPackage = null;
AgileEncryption::decryptFile(AgileEncryption::parse($encryptionInfo, $this->maxEncryptionSpinCount), $encryptedPackageFilename, $temporaryFilename, $this->encryptionPassword);
} catch (Throwable $e) {
if ($encryptedPackage !== null) {
fclose($encryptedPackage);
}
@unlink($temporaryFilename);
throw $e;
} finally {
@unlink($encryptedPackageFilename);
}
return $temporaryFilename;
}
/**
* @param mixed $value
*/
public static function testSimpleXml($value): SimpleXMLElement
{
return ($value instanceof SimpleXMLElement) ? $value : new SimpleXMLElement('');
}
public static function getAttributes(?SimpleXMLElement $value, string $ns = ''): SimpleXMLElement
{
return self::testSimpleXml($value === null ? $value : $value->attributes($ns));
}
// Phpstan thinks, correctly, that xpath can return false.
/** @return mixed[] */
private static function xpathNoFalse(SimpleXMLElement $sxml, string $path): array
{
return self::falseToArray($sxml->xpath($path));
}
/** @return mixed[]
* @param mixed $value */
public static function falseToArray($value): array
{
return is_array($value) ? $value : [];
}
private function loadZip(string $filename, string $ns = '', bool $replaceUnclosedBr = false): SimpleXMLElement
{
$contents = $this->getFromZipArchive($this->zip, $filename);
if ($replaceUnclosedBr) {
$contents = str_replace('
', '
', $contents);
}
$rels = @simplexml_load_string(
$this->getSecurityScannerOrThrow()->scan($contents),
SimpleXMLElement::class,
$this->parseHuge ? LIBXML_PARSEHUGE : 0,
$ns
);
return self::testSimpleXml($rels);
}
// This function is just to identify cases where I'm not sure
// why empty namespace is required.
private function loadZipNonamespace(string $filename, string $ns): SimpleXMLElement
{
$contents = $this->getFromZipArchive($this->zip, $filename);
$rels = simplexml_load_string(
$this->getSecurityScannerOrThrow()->scan($contents),
SimpleXMLElement::class,
$this->parseHuge ? LIBXML_PARSEHUGE : 0,
($ns === '' ? $ns : '')
);
return self::testSimpleXml($rels);
}
private const REL_TO_MAIN = [
Namespaces::PURL_OFFICE_DOCUMENT => Namespaces::PURL_MAIN,
Namespaces::THUMBNAIL => '',
];
private const REL_TO_DRAWING = [
Namespaces::PURL_RELATIONSHIPS => Namespaces::PURL_DRAWING,
];
private const REL_TO_CHART = [
Namespaces::PURL_RELATIONSHIPS => Namespaces::PURL_CHART,
];
/**
* Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object.
*
* @return string[]
*/
public function listWorksheetNames(string $filename): array
{
$temporaryFilename = $this->decryptToTemporaryFile($filename);
if ($temporaryFilename === null) {
return $this->listWorksheetNamesFromFile($filename);
}
try {
return $this->listWorksheetNamesFromFile($temporaryFilename);
} finally {
@unlink($temporaryFilename);
}
}
/** @return string[] */
private function listWorksheetNamesFromFile(string $filename): array
{
File::assertFile($filename, self::INITIAL_FILE);
$worksheetNames = [];
$this->zip = $zip = new ZipArchive();
$zip->open($filename);
// The files we're looking at here are small enough that simpleXML is more efficient than XMLReader
$rels = $this->loadZip(self::INITIAL_FILE, Namespaces::RELATIONSHIPS);
foreach ($rels->Relationship as $relx) {
$rel = self::getAttributes($relx);
$relType = (string) $rel['Type'];
$mainNS = self::REL_TO_MAIN[$relType] ?? Namespaces::MAIN;
if ($mainNS !== '') {
$xmlWorkbook = $this->loadZip((string) $rel['Target'], $mainNS);
if ($xmlWorkbook->sheets) {
foreach ($xmlWorkbook->sheets->sheet as $eleSheet) {
// Check if sheet should be skipped
$worksheetNames[] = (string) self::getAttributes($eleSheet)['name'];
}
}
}
}
$zip->close();
return $worksheetNames;
}
/**
* Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
*
* @return array
*/
public function listWorksheetInfo(string $filename): array
{
$temporaryFilename = $this->decryptToTemporaryFile($filename);
if ($temporaryFilename === null) {
return $this->listWorksheetInfoFromFile($filename);
}
try {
return $this->listWorksheetInfoFromFile($temporaryFilename);
} finally {
@unlink($temporaryFilename);
}
}
/**
* @return array
*/
private function listWorksheetInfoFromFile(string $filename): array
{
File::assertFile($filename, self::INITIAL_FILE);
$worksheetInfo = [];
$this->zip = $zip = new ZipArchive();
$zip->open($filename);
$rels = $this->loadZip(self::INITIAL_FILE, Namespaces::RELATIONSHIPS);
foreach ($rels->Relationship as $relx) {
$rel = self::getAttributes($relx);
$relType = (string) $rel['Type'];
$mainNS = self::REL_TO_MAIN[$relType] ?? Namespaces::MAIN;
if ($mainNS !== '') {
$relTarget = (string) $rel['Target'];
$dir = dirname($relTarget);
$namespace = dirname($relType);
$relsWorkbook = $this->loadZip("$dir/_rels/" . basename($relTarget) . '.rels', Namespaces::RELATIONSHIPS);
$worksheets = [];
foreach ($relsWorkbook->Relationship as $elex) {
$ele = self::getAttributes($elex);
if (
((string) $ele['Type'] === "$namespace/worksheet")
|| ((string) $ele['Type'] === "$namespace/chartsheet")
) {
$worksheets[(string) $ele['Id']] = $ele['Target'];
}
}
$xmlWorkbook = $this->loadZip($relTarget, $mainNS);
if ($xmlWorkbook->sheets) {
$dir = dirname($relTarget);
foreach ($xmlWorkbook->sheets->sheet as $eleSheet) {
$tmpInfo = [
'worksheetName' => (string) self::getAttributes($eleSheet)['name'],
'lastColumnLetter' => 'A',
'lastColumnIndex' => 0,
'totalRows' => 0,
'totalColumns' => 0,
];
$sheetState = (string) (self::getAttributes($eleSheet)['state'] ?? Worksheet::SHEETSTATE_VISIBLE);
$tmpInfo['sheetState'] = $sheetState;
$fileWorksheet = (string) $worksheets[self::getArrayItemString(self::getAttributes($eleSheet, $namespace), 'id')];
$fileWorksheetPath = str_starts_with($fileWorksheet, '/') ? (string) substr($fileWorksheet, 1) : "$dir/$fileWorksheet";
$xml = new XMLReader();
$xml->xml(
$this->getSecurityScannerOrThrow()
->scan(
$this->getFromZipArchive(
$this->zip,
$fileWorksheetPath
)
),
null,
$this->parseHuge ? LIBXML_PARSEHUGE : 0
);
$xml->setParserProperty(2, true);
$currCells = 0;
$currRow = 0;
while ($xml->read()) {
if ($xml->localName == 'row' && $xml->nodeType == XMLReader::ELEMENT && $xml->namespaceURI === $mainNS) {
$row = (int) $xml->getAttribute('r');
if ($this->readEmptyCells) {
$tmpInfo['totalRows'] = $row;
} else {
$currRow = $row;
}
$tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
$currCells = 0;
} elseif ($xml->localName == 'c' && $xml->nodeType == XMLReader::ELEMENT && $xml->namespaceURI === $mainNS) {
if ($this->readEmptyCells || !$xml->isEmptyElement) {
if ($currRow !== 0) {
$tmpInfo['totalRows'] = $currRow;
$currRow = 0;
}
$cell = $xml->getAttribute('r');
$currCells = $cell ? max($currCells, Coordinate::indexesFromString($cell)[0]) : ($currCells + 1);
}
}
}
$tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells);
$xml->close();
$tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1;
$tmpInfo['lastColumnLetter'] = Coordinate::stringFromColumnIndex($tmpInfo['lastColumnIndex'] + 1, true);
$worksheetInfo[] = $tmpInfo;
}
}
}
}
$zip->close();
return $worksheetInfo;
}
protected static function castToBoolean(SimpleXMLElement $c): bool
{
$value = isset($c->v) ? (string) $c->v : null;
if ($value == '0') {
return false;
} elseif ($value == '1') {
return true;
}
return (bool) $c->v;
}
protected static function castToError(?SimpleXMLElement $c): ?string
{
return isset($c, $c->v) ? (string) $c->v : null;
}
protected static function castToString(?SimpleXMLElement $c): ?string
{
return isset($c, $c->v) ? (string) $c->v : null;
}
public static function replacePrefixes(string $formula): string
{
return str_replace(['_xlfn.', '_xlws.'], '', $formula);
}
/**
* @param mixed $value
* @param mixed $calculatedValue
*/
protected function castToFormula(?SimpleXMLElement $c, string $r, string &$cellDataType, &$value, &$calculatedValue, string $castBaseType, bool $updateSharedCells = true): void
{
if ($c === null) {
return;
}
$attr = $c->f->attributes();
$cellDataType = DataType::TYPE_FORMULA;
$formula = self::replacePrefixes((string) $c->f);
$value = "=$formula";
$calculatedValue = self::$castBaseType($c);
// Shared formula?
if (isset($attr['t']) && strtolower((string) $attr['t']) == 'shared') {
$instance = (string) $attr['si'];
if (!isset($this->sharedFormulae[(string) $attr['si']])) {
$this->sharedFormulae[$instance] = new SharedFormula($r, $value);
} elseif ($updateSharedCells === true) {
// It's only worth the overhead of adjusting the shared formula for this cell if we're actually loading
// the cell, which may not be the case if we're using a read filter.
$master = Coordinate::indexesFromString($this->sharedFormulae[$instance]->master());
$current = Coordinate::indexesFromString($r);
$difference = [0, 0];
$difference[0] = $current[0] - $master[0];
$difference[1] = $current[1] - $master[1];
$value = $this->referenceHelper->updateFormulaReferences($this->sharedFormulae[$instance]->formula(), 'A1', $difference[0], $difference[1]);
}
}
}
private function fileExistsInArchive(ZipArchive $archive, string $fileName = ''): bool
{
// Root-relative paths
if (str_contains($fileName, '//')) {
$fileName = (string) substr($fileName, strpos($fileName, '//') + 1);
}
$fileName = File::realpath($fileName);
// Sadly, some 3rd party xlsx generators don't use consistent case for filenaming
// so we need to load case-insensitively from the zip file
// Apache POI fixes
$contents = $archive->locateName($fileName, ZipArchive::FL_NOCASE);
if ($contents === false) {
$contents = $archive->locateName((string) substr($fileName, 1), ZipArchive::FL_NOCASE);
}
return $contents !== false;
}
protected function getFromZipArchive(ZipArchive $archive, string $fileName = ''): string
{
// Root-relative paths
if (str_contains($fileName, '//')) {
$fileName = (string) substr($fileName, strpos($fileName, '//') + 1);
}
// Relative paths generated by dirname($filename) when $filename
// has no path (i.e.files in root of the zip archive)
$fileName = Preg::replace('/^\.\//', '', $fileName);
$fileName = File::realpath($fileName);
// Sadly, some 3rd party xlsx generators don't use consistent case for filenaming
// so we need to load case-insensitively from the zip file
$contents = $archive->getFromName($fileName, 0, ZipArchive::FL_NOCASE);
// Apache POI fixes
if ($contents === false) {
$contents = $archive->getFromName((string) substr($fileName, 1), 0, ZipArchive::FL_NOCASE);
}
// Has the file been saved with Windoze directory separators rather than unix?
if ($contents === false) {
$contents = $archive->getFromName(str_replace('/', '\\', $fileName), 0, ZipArchive::FL_NOCASE);
}
return ($contents === false) ? '' : $contents;
}
/**
* Loads Spreadsheet from file.
*/
protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
{
File::assertFile($filename, self::INITIAL_FILE);
// Initialisations
$excel = $this->newSpreadsheet();
$excel->setValueBinder($this->valueBinder);
$excel->removeSheetByIndex(0);
$addingFirstCellStyleXf = true;
$addingFirstCellXf = true;
/** @var mixed[][][][] */
$unparsedLoadedData = [];
$this->zip = $zip = new ZipArchive();
$zip->open($filename);
// Read the theme first, because we need the colour scheme when reading the styles
[$workbookBasename, $xmlNamespaceBase] = $this->getWorkbookBaseName();
$drawingNS = self::REL_TO_DRAWING[$xmlNamespaceBase] ?? Namespaces::DRAWINGML;
$chartNS = self::REL_TO_CHART[$xmlNamespaceBase] ?? Namespaces::CHART;
$wbRels = $this->loadZip("xl/_rels/{$workbookBasename}.rels", Namespaces::RELATIONSHIPS);
$theme = null;
$this->styleReader = new Styles();
foreach ($wbRels->Relationship as $relx) {
$rel = self::getAttributes($relx);
$relTarget = (string) $rel['Target'];
if (str_starts_with($relTarget, '/xl/')) {
$relTarget = (string) substr($relTarget, 4);
}
switch ($rel['Type']) {
case "$xmlNamespaceBase/sheetMetadata":
if ($this->fileExistsInArchive($zip, "xl/{$relTarget}")) {
$excel->returnArrayAsArray();
}
break;
case "$xmlNamespaceBase/theme":
if (!$this->fileExistsInArchive($zip, "xl/{$relTarget}")) {
break; // issue3770
}
$themeOrderArray = ['lt1', 'dk1', 'lt2', 'dk2'];
$themeOrderAdditional = count($themeOrderArray);
$xmlTheme = $this->loadZip("xl/{$relTarget}", $drawingNS);
$xmlThemeName = self::getAttributes($xmlTheme);
$xmlTheme = $xmlTheme->children($drawingNS);
$themeName = (string) $xmlThemeName['name'];
$colourScheme = self::getAttributes($xmlTheme->themeElements->clrScheme);
$colourSchemeName = (string) $colourScheme['name'];
$excel->getTheme()->setThemeColorName($colourSchemeName);
$colourScheme = $xmlTheme->themeElements->clrScheme->children($drawingNS);
$themeColours = [];
foreach ($colourScheme as $k => $xmlColour) {
$themePos = array_search($k, $themeOrderArray);
if ($themePos === false) {
$themePos = $themeOrderAdditional++;
}
if (isset($xmlColour->sysClr)) {
$xmlColourData = self::getAttributes($xmlColour->sysClr);
$themeColours[$themePos] = (string) $xmlColourData['lastClr'];
$excel->getTheme()->setThemeColor($k, (string) $xmlColourData['lastClr']);
} elseif (isset($xmlColour->srgbClr)) {
$xmlColourData = self::getAttributes($xmlColour->srgbClr);
$themeColours[$themePos] = (string) $xmlColourData['val'];
$excel->getTheme()->setThemeColor($k, (string) $xmlColourData['val']);
}
}
$theme = new Theme($themeName, $colourSchemeName, $themeColours);
$this->styleReader->setTheme($theme);
$fontScheme = self::getAttributes($xmlTheme->themeElements->fontScheme);
$fontSchemeName = (string) $fontScheme['name'];
$excel->getTheme()->setThemeFontName($fontSchemeName);
$majorFonts = [];
$minorFonts = [];
$fontScheme = $xmlTheme->themeElements->fontScheme->children($drawingNS);
$majorLatin = (string) (self::getAttributes($fontScheme->majorFont->latin)['typeface'] ?? '');
$majorEastAsian = (string) (self::getAttributes($fontScheme->majorFont->ea)['typeface'] ?? '');
$majorComplexScript = (string) (self::getAttributes($fontScheme->majorFont->cs)['typeface'] ?? '');
$minorLatin = (string) (self::getAttributes($fontScheme->minorFont->latin)['typeface'] ?? '');
$minorEastAsian = (string) (self::getAttributes($fontScheme->minorFont->ea)['typeface'] ?? '');
$minorComplexScript = (string) (self::getAttributes($fontScheme->minorFont->cs)['typeface'] ?? '');
foreach ($fontScheme->majorFont->font as $xmlFont) {
$fontAttributes = self::getAttributes($xmlFont);
$script = (string) ($fontAttributes['script'] ?? '');
if (!empty($script)) {
$majorFonts[$script] = (string) ($fontAttributes['typeface'] ?? '');
}
}
foreach ($fontScheme->minorFont->font as $xmlFont) {
$fontAttributes = self::getAttributes($xmlFont);
$script = (string) ($fontAttributes['script'] ?? '');
if (!empty($script)) {
$minorFonts[$script] = (string) ($fontAttributes['typeface'] ?? '');
}
}
$excel->getTheme()->setMajorFontValues($majorLatin, $majorEastAsian, $majorComplexScript, $majorFonts);
$excel->getTheme()->setMinorFontValues($minorLatin, $minorEastAsian, $minorComplexScript, $minorFonts);
break;
}
}
$rels = $this->loadZip(self::INITIAL_FILE, Namespaces::RELATIONSHIPS);
$propertyReader = new PropertyReader($this->getSecurityScannerOrThrow(), $excel->getProperties());
$charts = $chartDetails = [];
foreach ($rels->Relationship as $relx) {
$rel = self::getAttributes($relx);
$relTarget = (string) $rel['Target'];
// issue 3553
if ($relTarget[0] === '/') {
$relTarget = (string) substr($relTarget, 1);
}
$relType = (string) $rel['Type'];
$mainNS = self::REL_TO_MAIN[$relType] ?? Namespaces::MAIN;
switch ($relType) {
case Namespaces::CORE_PROPERTIES:
$propertyReader->readCoreProperties($this->getFromZipArchive($zip, $relTarget));
break;
case "$xmlNamespaceBase/extended-properties":
$propertyReader->readExtendedProperties($this->getFromZipArchive($zip, $relTarget));
break;
case "$xmlNamespaceBase/custom-properties":
$propertyReader->readCustomProperties($this->getFromZipArchive($zip, $relTarget));
break;
//Ribbon
case Namespaces::EXTENSIBILITY:
$customUI = $relTarget;
if ($customUI) {
$this->readRibbon($excel, $customUI, $zip);
}
break;
case "$xmlNamespaceBase/officeDocument":
$dir = dirname($relTarget);
// Do not specify namespace in next stmt - do it in Xpath
$relsWorkbook = $this->loadZip("$dir/_rels/" . basename($relTarget) . '.rels', Namespaces::RELATIONSHIPS);
$relsWorkbook->registerXPathNamespace('rel', Namespaces::RELATIONSHIPS);
$worksheets = [];
$pivotCacheRels = [];
$macros = $customUI = null;
foreach ($relsWorkbook->Relationship as $elex) {
$ele = self::getAttributes($elex);
switch ($ele['Type']) {
case Namespaces::WORKSHEET:
case Namespaces::PURL_WORKSHEET:
$worksheets[(string) $ele['Id']] = $ele['Target'];
break;
case Namespaces::CHARTSHEET:
if ($this->includeCharts === true) {
$worksheets[(string) $ele['Id']] = $ele['Target'];
}
break;
case Namespaces::RELATIONSHIPS_PIVOT_CACHE_DEFINITION:
$pivotCacheRels[(string) $ele['Id']] = File::realpath("$dir/" . (string) $ele['Target']);
break;
// a vbaProject ? (: some macros)
case Namespaces::VBA:
$macros = $ele['Target'];
break;
}
}
if ($macros !== null) {
$macrosCode = $this->getFromZipArchive($zip, 'xl/vbaProject.bin'); //vbaProject.bin always in 'xl' dir and always named vbaProject.bin
if (!empty($macrosCode)) {
$excel->setMacrosCode($macrosCode);
$excel->setHasMacros(true);
//short-circuit : not reading vbaProject.bin.rel to get Signature =>allways vbaProjectSignature.bin in 'xl' dir
$Certificate = $this->getFromZipArchive($zip, 'xl/vbaProjectSignature.bin');
$excel->setMacrosCertificate($Certificate);
}
}
$relType = "rel:Relationship[@Type='"
. "$xmlNamespaceBase/styles"
. "']";
/** @var ?SimpleXMLElement */
$xpath = self::getArrayItem(self::xpathNoFalse($relsWorkbook, $relType));
if ($xpath === null) {
$xmlStyles = self::testSimpleXml(null);
} else {
$stylesTarget = (string) $xpath['Target'];
$stylesTarget = str_starts_with($stylesTarget, '/') ? (string) substr($stylesTarget, 1) : "$dir/$stylesTarget";
$xmlStyles = $this->loadZip($stylesTarget, $mainNS);
}
$palette = self::extractPalette($xmlStyles);
$this->styleReader->setWorkbookPalette($palette);
$fills = self::extractStyles($xmlStyles, 'fills', 'fill');
$fonts = self::extractStyles($xmlStyles, 'fonts', 'font');
$borders = self::extractStyles($xmlStyles, 'borders', 'border');
$xfTags = self::extractStyles($xmlStyles, 'cellXfs', 'xf');
$cellXfTags = self::extractStyles($xmlStyles, 'cellStyleXfs', 'xf');
$styles = [];
$cellStyles = [];
$numFmts = null;
if (/*$xmlStyles && */ $xmlStyles->numFmts[0]) {
$numFmts = $xmlStyles->numFmts[0];
}
if (isset($numFmts)) {
/** @var SimpleXMLElement $numFmts */
$numFmts->registerXPathNamespace('sml', $mainNS);
}
$this->styleReader->setNamespace($mainNS);
if (!$this->readDataOnly/* && $xmlStyles*/) {
foreach ($xfTags as $xfTag) {
/** @var SimpleXMLElement $xfTag */
$xf = self::getAttributes($xfTag);
$numFmt = null;
if ($xf['numFmtId']) {
if (isset($numFmts)) {
/** @var ?SimpleXMLElement */
$tmpNumFmt = self::getArrayItem($numFmts->xpath("sml:numFmt[@numFmtId=$xf[numFmtId]]"));
if (isset($tmpNumFmt['formatCode'])) {
$numFmt = (string) $tmpNumFmt['formatCode'];
}
}
// We shouldn't override any of the built-in MS Excel values (values below id 164)
// But there's a lot of naughty homebrew xlsx writers that do use "reserved" id values that aren't actually used
// So we make allowance for them rather than lose formatting masks
if (
$numFmt === null
&& (int) $xf['numFmtId'] < 164
&& NumberFormat::builtInFormatCode((int) $xf['numFmtId']) !== ''
) {
$numFmt = NumberFormat::builtInFormatCode((int) $xf['numFmtId']);
}
}
$quotePrefix = (bool) (string) ($xf['quotePrefix'] ?? '');
$style = (object) [
'numFmt' => $numFmt ?? NumberFormat::FORMAT_GENERAL,
'font' => $fonts[(int) ($xf['fontId'])],
'fill' => $fills[(int) ($xf['fillId'])],
'border' => $borders[(int) ($xf['borderId'])],
'alignment' => $xfTag->alignment,
'protection' => $xfTag->protection,
'quotePrefix' => $quotePrefix,
];
$styles[] = $style;
// add style to cellXf collection
$objStyle = new Style();
$this->styleReader
->readStyle($objStyle, $style);
if (isset($xfTag->extLst)) {
foreach ($xfTag->extLst->ext as $extTag) {
$attributes = $extTag->attributes();
if (isset($attributes['uri'])) {
if ((string) $attributes['uri'] === Namespaces::STYLE_CHECKBOX_URI) {
$objStyle->setCheckBox(true);
}
}
}
}
foreach ($this->styleReader->getFontCharsets() as $fontName => $charset) {
$excel->addFontCharset($fontName, $charset);
}
if ($addingFirstCellXf) {
$excel->removeCellXfByIndex(0); // remove the default style
$addingFirstCellXf = false;
}
$excel->addCellXf($objStyle);
}
foreach ($cellXfTags as $xfTag) {
/** @var SimpleXMLElement $xfTag */
$xf = self::getAttributes($xfTag);
$numFmt = NumberFormat::FORMAT_GENERAL;
if ($numFmts && $xf['numFmtId']) {
/** @var ?SimpleXMLElement */
$tmpNumFmt = self::getArrayItem($numFmts->xpath("sml:numFmt[@numFmtId=$xf[numFmtId]]"));
if (isset($tmpNumFmt['formatCode'])) {
$numFmt = (string) $tmpNumFmt['formatCode'];
} elseif ((int) $xf['numFmtId'] < 165) {
$numFmt = NumberFormat::builtInFormatCode((int) $xf['numFmtId']);
}
}
$quotePrefix = (bool) (string) ($xf['quotePrefix'] ?? '');
$cellStyle = (object) [
'numFmt' => $numFmt,
'font' => $fonts[(int) ($xf['fontId'])],
'fill' => $fills[((int) $xf['fillId'])],
'border' => $borders[(int) ($xf['borderId'])],
'alignment' => $xfTag->alignment,
'protection' => $xfTag->protection,
'quotePrefix' => $quotePrefix,
];
$cellStyles[] = $cellStyle;
// add style to cellStyleXf collection
$objStyle = new Style();
$this->styleReader->readStyle($objStyle, $cellStyle);
if ($addingFirstCellStyleXf) {
$excel->removeCellStyleXfByIndex(0); // remove the default style
$addingFirstCellStyleXf = false;
}
$excel->addCellStyleXf($objStyle);
}
}
$this->styleReader->setStyleXml($xmlStyles);
$this->styleReader->setNamespace($mainNS);
$this->styleReader->setStyleBaseData($theme, $styles, $cellStyles);
$dxfs = $this->styleReader->dxfs($this->readDataOnly);
$tableStyles = $this->styleReader->tableStyles($this->readDataOnly);
$styles = $this->styleReader->styles();
// Read content after setting the styles
$sharedStrings = [];
$relType = "rel:Relationship[@Type='"
//. Namespaces::SHARED_STRINGS
. "$xmlNamespaceBase/sharedStrings"
. "']";
/** @var ?SimpleXMLElement */
$xpath = self::getArrayItem($relsWorkbook->xpath($relType));
if ($xpath) {
$sharedStringsTarget = (string) $xpath['Target'];
$sharedStringsTarget = str_starts_with($sharedStringsTarget, '/') ? (string) substr($sharedStringsTarget, 1) : "$dir/$sharedStringsTarget";
$xmlStrings = $this->loadZip($sharedStringsTarget, $mainNS);
if (isset($xmlStrings->si)) {
foreach ($xmlStrings->si as $val) {
if (isset($val->t)) {
$sharedStrings[] = StringHelper::controlCharacterOOXML2PHP((string) $val->t);
} elseif (isset($val->r)) {
$sharedStrings[] = $this->parseRichText($val);
} else {
$sharedStrings[] = '';
}
}
}
}
$xmlWorkbook = $this->loadZipNoNamespace($relTarget, $mainNS);
$xmlWorkbookNS = $this->loadZip($relTarget, $mainNS);
// Set base date
$excel->setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
if ($xmlWorkbookNS->workbookPr) {
Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
$attrs1904 = self::getAttributes($xmlWorkbookNS->workbookPr);
if (isset($attrs1904['date1904'])) {
if (self::boolean((string) $attrs1904['date1904'])) {
Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
$excel->setExcelCalendar(Date::CALENDAR_MAC_1904);
}
}
}
// Set protection
$this->readProtection($excel, $xmlWorkbook);
$sheetId = 0; // keep track of new sheet id in final workbook
$oldSheetId = -1; // keep track of old sheet id in final workbook
$countSkippedSheets = 0; // keep track of number of skipped sheets
$mapSheetId = []; // mapping of sheet ids from old to new
$charts = $chartDetails = [];
// Add richData (contains relation of in-cell images)
$richData = [];
$relationsFileName = $dir . '/richData/_rels/richValueRel.xml.rels';
if ($zip->locateName($relationsFileName)) {
$relsWorksheet = $this->loadZip($relationsFileName, Namespaces::RELATIONSHIPS);
foreach ($relsWorksheet->Relationship as $elex) {
$ele = self::getAttributes($elex);
if ($ele['Type'] == Namespaces::IMAGE) {
$richData['image'][(string) $ele['Id']] = (string) $ele['Target'];
}
}
}
$sheetCreated = false;
if ($xmlWorkbookNS->sheets) {
foreach ($xmlWorkbookNS->sheets->sheet as $eleSheet) {
$eleSheetAttr = self::getAttributes($eleSheet);
++$oldSheetId;
// Check if sheet should be skipped
if (is_array($this->loadSheetsOnly) && !in_array((string) $eleSheetAttr['name'], $this->loadSheetsOnly)) {
++$countSkippedSheets;
$mapSheetId[$oldSheetId] = null;
continue;
}
$sheetReferenceId = self::getArrayItemString(self::getAttributes($eleSheet, $xmlNamespaceBase), 'id');
if (isset($worksheets[$sheetReferenceId]) === false) {
++$countSkippedSheets;
$mapSheetId[$oldSheetId] = null;
continue;
}
// Map old sheet id in original workbook to new sheet id.
// They will differ if loadSheetsOnly() is being used
$mapSheetId[$oldSheetId] = $oldSheetId - $countSkippedSheets;
// Load sheet
$docSheet = $excel->createSheet();
$sheetCreated = true;
// Use false for $updateFormulaCellReferences to prevent adjustment of worksheet
// references in formula cells... during the load, all formulae should be correct,
// and we're simply bringing the worksheet name in line with the formula, not the
// reverse
$docSheet->setTitle((string) $eleSheetAttr['name'], false, false);
$fileWorksheet = (string) $worksheets[$sheetReferenceId];
// issue 3665 adds test for /.
// This broke XlsxRootZipFilesTest,
// but Excel reports an error with that file.
// Testing dir for . avoids this problem.
// It might be better just to drop the test.
if ($fileWorksheet[0] == '/' && $dir !== '.') {
$fileWorksheet = (string) substr($fileWorksheet, strlen($dir) + 2);
}
$xmlSheet = $this->loadZipNoNamespace("$dir/$fileWorksheet", $mainNS);
$xmlSheetNS = $this->loadZip("$dir/$fileWorksheet", $mainNS);
// Shared Formula table is unique to each Worksheet, so we need to reset it here
$this->sharedFormulae = [];
if (isset($eleSheetAttr['state']) && (string) $eleSheetAttr['state'] != '') {
$docSheet->setSheetState((string) $eleSheetAttr['state']);
}
if ($xmlSheetNS) {
$xmlSheetMain = $xmlSheetNS->children($mainNS);
// Setting Conditional Styles adjusts selected cells, so we need to execute this
// before reading the sheet view data to get the actual selected cells
if (!$this->readDataOnly && ($xmlSheet->conditionalFormatting)) {
(new ConditionalStyles($docSheet, $xmlSheet, $dxfs, $this->styleReader))->load();
}
if (!$this->readDataOnly && $xmlSheet->extLst) {
(new ConditionalStyles($docSheet, $xmlSheet, $dxfs, $this->styleReader))->loadFromExt();
}
if (isset($xmlSheetMain->sheetViews, $xmlSheetMain->sheetViews->sheetView)) {
$sheetViews = new SheetViews($xmlSheetMain->sheetViews->sheetView, $docSheet);
$sheetViews->load();
}
$sheetViewOptions = new SheetViewOptions($docSheet, $xmlSheetNS);
$sheetViewOptions->load($this->readDataOnly, $this->styleReader);
(new ColumnAndRowAttributes($docSheet, $xmlSheetNS))
->load($this->readFilter, $this->readDataOnly, $this->ignoreRowsWithNoCells);
}
$holdSelectedCells = $docSheet->getSelectedCells();
/** @var array