Initial import

This commit is contained in:
Admin Nasledstvo
2026-05-01 20:52:04 +03:00
commit ac168868ee
10028 changed files with 2337954 additions and 0 deletions
@@ -0,0 +1,295 @@
<?php
namespace app\extensions;
class XLSXReader {
protected $sheets = array();
protected $sharedstrings = array();
protected $sheetInfo;
protected $zip;
public $config = array(
'removeTrailingRows' => true
);
// XML schemas
const SCHEMA_OFFICEDOCUMENT = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument';
const SCHEMA_RELATIONSHIP = 'http://schemas.openxmlformats.org/package/2006/relationships';
const SCHEMA_OFFICEDOCUMENT_RELATIONSHIP = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
const SCHEMA_SHAREDSTRINGS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings';
const SCHEMA_WORKSHEETRELATION = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet';
public function __construct($filePath, $config = array()) {
$this->config = array_merge($this->config, $config);
$this->zip = new \ZipArchive();
$status = $this->zip->open($filePath);
if($status === true) {
$this->parse();
} else {
throw new \Exception("Failed to open $filePath with zip error code: $status");
}
}
// get a file from the zip
protected function getEntryData($name) {
$data = $this->zip->getFromName($name);
if($data === false) {
throw new \Exception("File $name does not exist in the Excel file");
} else {
return $data;
}
}
// extract the shared string and the list of sheets
protected function parse() {
$sheets = array();
$relationshipsXML = simplexml_load_string($this->getEntryData("_rels/.rels"));
foreach($relationshipsXML->Relationship as $rel) {
if($rel['Type'] == self::SCHEMA_OFFICEDOCUMENT) {
$workbookDir = dirname($rel['Target']) . '/';
$workbookXML = simplexml_load_string($this->getEntryData($rel['Target']));
foreach($workbookXML->sheets->sheet as $sheet) {
$r = $sheet->attributes('r', true);
$sheets[(string)$r->id] = array(
'sheetId' => (int)$sheet['sheetId'],
'name' => (string)$sheet['name']
);
}
$workbookRelationsXML = simplexml_load_string($this->getEntryData($workbookDir . '_rels/' . basename($rel['Target']) . '.rels'));
foreach($workbookRelationsXML->Relationship as $wrel) {
switch($wrel['Type']) {
case self::SCHEMA_WORKSHEETRELATION:
$sheets[(string)$wrel['Id']]['path'] = $workbookDir . (string)$wrel['Target'];
break;
case self::SCHEMA_SHAREDSTRINGS:
$sharedStringsXML = simplexml_load_string($this->getEntryData($workbookDir . (string)$wrel['Target']));
foreach($sharedStringsXML->si as $val) {
if(isset($val->t)) {
$this->sharedStrings[] = (string)$val->t;
} elseif(isset($val->r)) {
$this->sharedStrings[] = XLSXWorksheet::parseRichText($val);
}
}
break;
}
}
}
}
$this->sheetInfo = array();
foreach($sheets as $rid=>$info) {
$this->sheetInfo[$info['name']] = array(
'sheetId' => $info['sheetId'],
'rid' => $rid,
'path' => $info['path']
);
}
}
// returns an array of sheet names, indexed by sheetId
public function getSheetNames() {
$res = array();
foreach($this->sheetInfo as $sheetName=>$info) {
$res[$info['sheetId']] = $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 \Exception("Sheet must be a string or a sheet Id");
}
if(!array_key_exists($sheet, $this->sheets)) {
$this->sheets[$sheet] = new XLSXWorksheet($this->getSheetXML($sheet), $sheet, $this);
}
return $this->sheets[$sheet];
}
public function getSheetNameById($sheetId) {
foreach($this->sheetInfo as $sheetName=>$sheetInfo) {
if($sheetInfo['sheetId'] === $sheetId) {
return $sheetName;
}
}
throw new \Exception("Sheet ID $sheetId does not exist in the Excel file");
}
protected function getSheetXML($name) {
return simplexml_load_string($this->getEntryData($this->sheetInfo[$name]['path']));
}
// 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;
}
}
class XLSXWorksheet {
protected $workbook;
public $sheetName;
protected $data;
public $colCount;
public $rowCount;
protected $config;
public function __construct($xml, $sheetName, XLSXReader $workbook) {
$this->config = $workbook->config;
$this->sheetName = $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->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 parseData($sheetData) {
$rows = array();
$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(array(),$this->colCount,null);
$curR++;
}
}
$curC = 0;
$rowData = array();
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);
$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 array($index-1, $row-1);
}
throw new \Exception("Invalid cell index");
}
protected function parseCellValue($cell) {
// $cell['t'] is the cell type
switch ((string)$cell["t"]) {
case "s": // Value is a shared string
if ((string)$cell->v != '') {
$value = $this->workbook->sharedStrings[intval($cell->v)];
} else {
$value = '';
}
break;
case "b": // Value is boolean
$value = (string)$cell->v;
if ($value == '0') {
$value = false;
} else if ($value == '1') {
$value = true;
} else {
$value = (bool)$cell->v;
}
break;
case "inlineStr": // Value is rich text inline
$value = self::parseRichText($cell->is);
break;
case "e": // Value is an error message
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;
}
// returns the text content from a rich text or inline string field
public static function parseRichText($is = null) {
$value = array();
if (isset($is->t)) {
$value[] = (string)$is->t;
} else {
foreach ($is->r as $run) {
$value[] = (string)$run->t;
}
}
return implode(' ', $value);
}
}
@@ -0,0 +1,973 @@
<?php
namespace app\extensions;
/*
* @license MIT License
* */
class XLSXWriter
{
//http://www.ecma-international.org/publications/standards/Ecma-376.htm
//http://officeopenxml.com/SSstyles.php
//------------------------------------------------------------------
//http://office.microsoft.com/en-us/excel-help/excel-specifications-and-limits-HP010073849.aspx
const EXCEL_2007_MAX_ROW=1048576;
const EXCEL_2007_MAX_COL=16384;
//------------------------------------------------------------------
protected $title;
protected $subject;
protected $author;
protected $isRightToLeft;
protected $company;
protected $description;
protected $keywords = array();
protected $current_sheet;
protected $sheets = array();
protected $temp_files = array();
protected $cell_styles = array();
protected $number_formats = array();
public function __construct()
{
defined('ENT_XML1') or define('ENT_XML1',16);//for php 5.3, avoid fatal error
date_default_timezone_get() or date_default_timezone_set('UTC');//php.ini missing tz, avoid warning
is_writeable($this->tempFilename()) or self::log("Warning: tempdir ".sys_get_temp_dir()." not writeable, use ->setTempDir()");
class_exists('ZipArchive') or self::log("Error: ZipArchive class does not exist");
$this->addCellStyle($number_format='GENERAL', $style_string=null);
}
public function setTitle($title='') { $this->title=$title; }
public function setSubject($subject='') { $this->subject=$subject; }
public function setAuthor($author='') { $this->author=$author; }
public function setCompany($company='') { $this->company=$company; }
public function setKeywords($keywords='') { $this->keywords=$keywords; }
public function setDescription($description='') { $this->description=$description; }
public function setTempDir($tempdir='') { $this->tempdir=$tempdir; }
public function setRightToLeft($isRightToLeft=false){ $this->isRightToLeft=$isRightToLeft; }
public function __destruct()
{
if (!empty($this->temp_files)) {
foreach($this->temp_files as $temp_file) {
@unlink($temp_file);
}
}
}
protected function tempFilename()
{
$tempdir = !empty($this->tempdir) ? $this->tempdir : sys_get_temp_dir();
$filename = tempnam($tempdir, "xlsx_writer_");
$this->temp_files[] = $filename;
return $filename;
}
public function writeToStdOut()
{
$temp_file = $this->tempFilename();
self::writeToFile($temp_file);
readfile($temp_file);
}
public function writeToString()
{
$temp_file = $this->tempFilename();
self::writeToFile($temp_file);
$string = file_get_contents($temp_file);
return $string;
}
public function writeToFile($filename)
{
foreach($this->sheets as $sheet_name => $sheet) {
self::finalizeSheet($sheet_name);//making sure all footers have been written
}
if ( file_exists( $filename ) ) {
if ( is_writable( $filename ) ) {
@unlink( $filename ); //if the zip already exists, remove it
} else {
self::log( "Error in " . __CLASS__ . "::" . __FUNCTION__ . ", file is not writeable." );
return;
}
}
$zip = new \ZipArchive();
if (empty($this->sheets)) { self::log("Error in ".__CLASS__."::".__FUNCTION__.", no worksheets defined."); return; }
if (!$zip->open($filename, \ZipArchive::CREATE)) { self::log("Error in ".__CLASS__."::".__FUNCTION__.", unable to create zip."); return; }
$zip->addEmptyDir("docProps/");
$zip->addFromString("docProps/app.xml" , self::buildAppXML() );
$zip->addFromString("docProps/core.xml", self::buildCoreXML());
$zip->addEmptyDir("_rels/");
$zip->addFromString("_rels/.rels", self::buildRelationshipsXML());
$zip->addEmptyDir("xl/worksheets/");
foreach($this->sheets as $sheet) {
$zip->addFile($sheet->filename, "xl/worksheets/".$sheet->xmlname );
}
$zip->addFromString("xl/workbook.xml" , self::buildWorkbookXML() );
$zip->addFile($this->writeStylesXML(), "xl/styles.xml" ); //$zip->addFromString("xl/styles.xml" , self::buildStylesXML() );
$zip->addFromString("[Content_Types].xml" , self::buildContentTypesXML() );
$zip->addEmptyDir("xl/_rels/");
$zip->addFromString("xl/_rels/workbook.xml.rels", self::buildWorkbookRelsXML() );
$zip->close();
}
protected function initializeSheet($sheet_name, $col_widths=array(), $auto_filter=false, $freeze_rows=false, $freeze_columns=false )
{
//if already initialized
if ($this->current_sheet==$sheet_name || isset($this->sheets[$sheet_name]))
return;
$sheet_filename = $this->tempFilename();
$sheet_xmlname = 'sheet' . (count($this->sheets) + 1).".xml";
$this->sheets[$sheet_name] = (object)array(
'filename' => $sheet_filename,
'sheetname' => $sheet_name,
'xmlname' => $sheet_xmlname,
'row_count' => 0,
'file_writer' => new XLSXWriter_BuffererWriter($sheet_filename),
'columns' => array(),
'merge_cells' => array(),
'max_cell_tag_start' => 0,
'max_cell_tag_end' => 0,
'auto_filter' => $auto_filter,
'freeze_rows' => $freeze_rows,
'freeze_columns' => $freeze_columns,
'finalized' => false,
);
$rightToLeftValue = $this->isRightToLeft ? 'true' : 'false';
$sheet = &$this->sheets[$sheet_name];
$tabselected = count($this->sheets) == 1 ? 'true' : 'false';//only first sheet is selected
$max_cell=XLSXWriter::xlsCell(self::EXCEL_2007_MAX_ROW, self::EXCEL_2007_MAX_COL);//XFE1048577
$sheet->file_writer->write('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . "\n");
$sheet->file_writer->write('<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">');
$sheet->file_writer->write( '<sheetPr filterMode="false">');
$sheet->file_writer->write( '<pageSetUpPr fitToPage="false"/>');
$sheet->file_writer->write( '</sheetPr>');
$sheet->max_cell_tag_start = $sheet->file_writer->ftell();
$sheet->file_writer->write('<dimension ref="A1:' . $max_cell . '"/>');
$sheet->max_cell_tag_end = $sheet->file_writer->ftell();
$sheet->file_writer->write( '<sheetViews>');
$sheet->file_writer->write( '<sheetView colorId="64" defaultGridColor="true" rightToLeft="'.$rightToLeftValue.'" showFormulas="false" showGridLines="true" showOutlineSymbols="true" showRowColHeaders="true" showZeros="true" tabSelected="' . $tabselected . '" topLeftCell="A1" view="normal" windowProtection="false" workbookViewId="0" zoomScale="100" zoomScaleNormal="100" zoomScalePageLayoutView="100">');
if ($sheet->freeze_rows && $sheet->freeze_columns) {
$sheet->file_writer->write( '<pane ySplit="'.$sheet->freeze_rows.'" xSplit="'.$sheet->freeze_columns.'" topLeftCell="'.self::xlsCell($sheet->freeze_rows, $sheet->freeze_columns).'" activePane="bottomRight" state="frozen"/>');
$sheet->file_writer->write( '<selection activeCell="'.self::xlsCell($sheet->freeze_rows, 0).'" activeCellId="0" pane="topRight" sqref="'.self::xlsCell($sheet->freeze_rows, 0).'"/>');
$sheet->file_writer->write( '<selection activeCell="'.self::xlsCell(0, $sheet->freeze_columns).'" activeCellId="0" pane="bottomLeft" sqref="'.self::xlsCell(0, $sheet->freeze_columns).'"/>');
$sheet->file_writer->write( '<selection activeCell="'.self::xlsCell($sheet->freeze_rows, $sheet->freeze_columns).'" activeCellId="0" pane="bottomRight" sqref="'.self::xlsCell($sheet->freeze_rows, $sheet->freeze_columns).'"/>');
}
elseif ($sheet->freeze_rows) {
$sheet->file_writer->write( '<pane ySplit="'.$sheet->freeze_rows.'" topLeftCell="'.self::xlsCell($sheet->freeze_rows, 0).'" activePane="bottomLeft" state="frozen"/>');
$sheet->file_writer->write( '<selection activeCell="'.self::xlsCell($sheet->freeze_rows, 0).'" activeCellId="0" pane="bottomLeft" sqref="'.self::xlsCell($sheet->freeze_rows, 0).'"/>');
}
elseif ($sheet->freeze_columns) {
$sheet->file_writer->write( '<pane xSplit="'.$sheet->freeze_columns.'" topLeftCell="'.self::xlsCell(0, $sheet->freeze_columns).'" activePane="topRight" state="frozen"/>');
$sheet->file_writer->write( '<selection activeCell="'.self::xlsCell(0, $sheet->freeze_columns).'" activeCellId="0" pane="topRight" sqref="'.self::xlsCell(0, $sheet->freeze_columns).'"/>');
}
else { // not frozen
$sheet->file_writer->write( '<selection activeCell="A1" activeCellId="0" pane="topLeft" sqref="A1"/>');
}
$sheet->file_writer->write( '</sheetView>');
$sheet->file_writer->write( '</sheetViews>');
$sheet->file_writer->write( '<cols>');
$i=0;
if (!empty($col_widths)) {
foreach($col_widths as $column_width) {
$sheet->file_writer->write( '<col collapsed="false" hidden="false" max="'.($i+1).'" min="'.($i+1).'" style="0" customWidth="true" width="'.floatval($column_width).'"/>');
$i++;
}
}
$sheet->file_writer->write( '<col collapsed="false" hidden="false" max="1024" min="'.($i+1).'" style="0" customWidth="false" width="11.5"/>');
$sheet->file_writer->write( '</cols>');
$sheet->file_writer->write( '<sheetData>');
}
private function addCellStyle($number_format, $cell_style_string)
{
$number_format_idx = self::add_to_list_get_index($this->number_formats, $number_format);
$lookup_string = $number_format_idx.";".$cell_style_string;
$cell_style_idx = self::add_to_list_get_index($this->cell_styles, $lookup_string);
return $cell_style_idx;
}
private function initializeColumnTypes($header_types)
{
$column_types = array();
foreach($header_types as $v)
{
$number_format = self::numberFormatStandardized($v);
$number_format_type = self::determineNumberFormatType($number_format);
$cell_style_idx = $this->addCellStyle($number_format, $style_string=null);
$column_types[] = array('number_format' => $number_format,//contains excel format like 'YYYY-MM-DD HH:MM:SS'
'number_format_type' => $number_format_type, //contains friendly format like 'datetime'
'default_cell_style' => $cell_style_idx,
);
}
return $column_types;
}
public function writeSheetHeader($sheet_name, array $header_types, $col_options = null)
{
if (empty($sheet_name) || empty($header_types) || !empty($this->sheets[$sheet_name]))
return;
$suppress_row = isset($col_options['suppress_row']) ? intval($col_options['suppress_row']) : false;
if (is_bool($col_options))
{
self::log( "Warning! passing $suppress_row=false|true to writeSheetHeader() is deprecated, this will be removed in a future version." );
$suppress_row = intval($col_options);
}
$style = &$col_options;
$col_widths = isset($col_options['widths']) ? (array)$col_options['widths'] : array();
$auto_filter = isset($col_options['auto_filter']) ? intval($col_options['auto_filter']) : false;
$freeze_rows = isset($col_options['freeze_rows']) ? intval($col_options['freeze_rows']) : false;
$freeze_columns = isset($col_options['freeze_columns']) ? intval($col_options['freeze_columns']) : false;
self::initializeSheet($sheet_name, $col_widths, $auto_filter, $freeze_rows, $freeze_columns);
$sheet = &$this->sheets[$sheet_name];
$sheet->columns = $this->initializeColumnTypes($header_types);
if (!$suppress_row)
{
$header_row = array_keys($header_types);
$sheet->file_writer->write('<row collapsed="false" customFormat="false" customHeight="false" hidden="false" ht="12.1" outlineLevel="0" r="' . (1) . '">');
foreach ($header_row as $c => $v) {
$cell_style_idx = empty($style) ? $sheet->columns[$c]['default_cell_style'] : $this->addCellStyle( 'GENERAL', json_encode(isset($style[0]) ? $style[$c] : $style) );
$this->writeCell($sheet->file_writer, 0, $c, $v, $number_format_type='n_string', $cell_style_idx);
}
$sheet->file_writer->write('</row>');
$sheet->row_count++;
}
$this->current_sheet = $sheet_name;
}
public function writeSheetRow($sheet_name, array $row, $row_options=null)
{
if (empty($sheet_name))
return;
$this->initializeSheet($sheet_name);
$sheet = &$this->sheets[$sheet_name];
if (count($sheet->columns) < count($row)) {
$default_column_types = $this->initializeColumnTypes( array_fill($from=0, $until=count($row), 'GENERAL') );//will map to n_auto
$sheet->columns = array_merge((array)$sheet->columns, $default_column_types);
}
if (!empty($row_options))
{
$ht = isset($row_options['height']) ? floatval($row_options['height']) : 12.1;
$customHt = isset($row_options['height']) ? true : false;
$hidden = isset($row_options['hidden']) ? (bool)($row_options['hidden']) : false;
$collapsed = isset($row_options['collapsed']) ? (bool)($row_options['collapsed']) : false;
$sheet->file_writer->write('<row collapsed="'.($collapsed).'" customFormat="false" customHeight="'.($customHt).'" hidden="'.($hidden).'" ht="'.($ht).'" outlineLevel="0" r="' . ($sheet->row_count + 1) . '">');
}
else
{
$sheet->file_writer->write('<row collapsed="false" customFormat="false" customHeight="false" hidden="false" ht="12.1" outlineLevel="0" r="' . ($sheet->row_count + 1) . '">');
}
$style = &$row_options;
$c=0;
foreach ($row as $v) {
$number_format = $sheet->columns[$c]['number_format'];
$number_format_type = $sheet->columns[$c]['number_format_type'];
$cell_style_idx = empty($style) ? $sheet->columns[$c]['default_cell_style'] : $this->addCellStyle( $number_format, json_encode(isset($style[0]) ? $style[$c] : $style) );
$this->writeCell($sheet->file_writer, $sheet->row_count, $c, $v, $number_format_type, $cell_style_idx);
$c++;
}
$sheet->file_writer->write('</row>');
$sheet->row_count++;
$this->current_sheet = $sheet_name;
}
public function countSheetRows($sheet_name = '')
{
$sheet_name = $sheet_name ?: $this->current_sheet;
return array_key_exists($sheet_name, $this->sheets) ? $this->sheets[$sheet_name]->row_count : 0;
}
protected function finalizeSheet($sheet_name)
{
if (empty($sheet_name) || $this->sheets[$sheet_name]->finalized)
return;
$sheet = &$this->sheets[$sheet_name];
$sheet->file_writer->write( '</sheetData>');
if (!empty($sheet->merge_cells)) {
$sheet->file_writer->write( '<mergeCells>');
foreach ($sheet->merge_cells as $range) {
$sheet->file_writer->write( '<mergeCell ref="' . $range . '"/>');
}
$sheet->file_writer->write( '</mergeCells>');
}
$max_cell = self::xlsCell($sheet->row_count - 1, count($sheet->columns) - 1);
if ($sheet->auto_filter) {
$sheet->file_writer->write( '<autoFilter ref="A1:' . $max_cell . '"/>');
}
$sheet->file_writer->write( '<printOptions headings="false" gridLines="false" gridLinesSet="true" horizontalCentered="false" verticalCentered="false"/>');
$sheet->file_writer->write( '<pageMargins left="0.5" right="0.5" top="1.0" bottom="1.0" header="0.5" footer="0.5"/>');
$sheet->file_writer->write( '<pageSetup blackAndWhite="false" cellComments="none" copies="1" draft="false" firstPageNumber="1" fitToHeight="1" fitToWidth="1" horizontalDpi="300" orientation="portrait" pageOrder="downThenOver" paperSize="1" scale="100" useFirstPageNumber="true" usePrinterDefaults="false" verticalDpi="300"/>');
$sheet->file_writer->write( '<headerFooter differentFirst="false" differentOddEven="false">');
$sheet->file_writer->write( '<oddHeader>&amp;C&amp;&quot;Times New Roman,Regular&quot;&amp;12&amp;A</oddHeader>');
$sheet->file_writer->write( '<oddFooter>&amp;C&amp;&quot;Times New Roman,Regular&quot;&amp;12Page &amp;P</oddFooter>');
$sheet->file_writer->write( '</headerFooter>');
$sheet->file_writer->write('</worksheet>');
$max_cell_tag = '<dimension ref="A1:' . $max_cell . '"/>';
$padding_length = $sheet->max_cell_tag_end - $sheet->max_cell_tag_start - strlen($max_cell_tag);
$sheet->file_writer->fseek($sheet->max_cell_tag_start);
$sheet->file_writer->write($max_cell_tag.str_repeat(" ", $padding_length));
$sheet->file_writer->close();
$sheet->finalized=true;
}
public function markMergedCell($sheet_name, $start_cell_row, $start_cell_column, $end_cell_row, $end_cell_column)
{
if (empty($sheet_name) || $this->sheets[$sheet_name]->finalized)
return;
self::initializeSheet($sheet_name);
$sheet = &$this->sheets[$sheet_name];
$startCell = self::xlsCell($start_cell_row, $start_cell_column);
$endCell = self::xlsCell($end_cell_row, $end_cell_column);
$sheet->merge_cells[] = $startCell . ":" . $endCell;
}
public function writeSheet(array $data, $sheet_name='', array $header_types=array())
{
$sheet_name = empty($sheet_name) ? 'Sheet1' : $sheet_name;
$data = empty($data) ? array(array('')) : $data;
if (!empty($header_types))
{
$this->writeSheetHeader($sheet_name, $header_types);
}
foreach($data as $i=>$row)
{
$this->writeSheetRow($sheet_name, $row);
}
$this->finalizeSheet($sheet_name);
}
protected function writeCell(XLSXWriter_BuffererWriter &$file, $row_number, $column_number, $value, $num_format_type, $cell_style_idx)
{
$cell_name = self::xlsCell($row_number, $column_number);
if (!is_scalar($value) || $value==='') { //objects, array, empty
$file->write('<c r="'.$cell_name.'" s="'.$cell_style_idx.'"/>');
} elseif (is_string($value) && $value[0]=='='){
$file->write('<c r="'.$cell_name.'" s="'.$cell_style_idx.'" t="s"><f>'.self::xmlspecialchars($value).'</f></c>');
} elseif ($num_format_type=='n_date') {
$file->write('<c r="'.$cell_name.'" s="'.$cell_style_idx.'" t="n"><v>'.intval(self::convert_date_time($value)).'</v></c>');
} elseif ($num_format_type=='n_datetime') {
$file->write('<c r="'.$cell_name.'" s="'.$cell_style_idx.'" t="n"><v>'.self::convert_date_time($value).'</v></c>');
} elseif ($num_format_type=='n_numeric') {
$file->write('<c r="'.$cell_name.'" s="'.$cell_style_idx.'" t="n"><v>'.self::xmlspecialchars($value).'</v></c>');//int,float,currency
} elseif ($num_format_type=='n_string') {
$file->write('<c r="'.$cell_name.'" s="'.$cell_style_idx.'" t="inlineStr"><is><t>'.self::xmlspecialchars($value).'</t></is></c>');
} elseif ($num_format_type=='n_auto' || 1) { //auto-detect unknown column types
if (!is_string($value) || $value=='0' || ($value[0]!='0' && ctype_digit($value)) || preg_match("/^\-?(0|[1-9][0-9]*)(\.[0-9]+)?$/", $value)){
$file->write('<c r="'.$cell_name.'" s="'.$cell_style_idx.'" t="n"><v>'.self::xmlspecialchars($value).'</v></c>');//int,float,currency
} else { //implied: ($cell_format=='string')
$file->write('<c r="'.$cell_name.'" s="'.$cell_style_idx.'" t="inlineStr"><is><t>'.self::xmlspecialchars($value).'</t></is></c>');
}
}
}
protected function styleFontIndexes()
{
static $border_allowed = array('left','right','top','bottom');
static $border_style_allowed = array('thin','medium','thick','dashDot','dashDotDot','dashed','dotted','double','hair','mediumDashDot','mediumDashDotDot','mediumDashed','slantDashDot');
static $horizontal_allowed = array('general','left','right','justify','center');
static $vertical_allowed = array('bottom','center','distributed','top');
$default_font = array('size'=>'10','name'=>'Arial','family'=>'2');
$fills = array('','');//2 placeholders for static xml later
$fonts = array('','','','');//4 placeholders for static xml later
$borders = array('');//1 placeholder for static xml later
$style_indexes = array();
foreach($this->cell_styles as $i=>$cell_style_string)
{
$semi_colon_pos = strpos($cell_style_string,";");
$number_format_idx = substr($cell_style_string, 0, $semi_colon_pos);
$style_json_string = substr($cell_style_string, $semi_colon_pos+1);
$style = @json_decode($style_json_string, $as_assoc=true);
$style_indexes[$i] = array('num_fmt_idx'=>$number_format_idx);//initialize entry
if (isset($style['border']) && is_string($style['border']))//border is a comma delimited str
{
$border_value['side'] = array_intersect(explode(",", $style['border']), $border_allowed);
if (isset($style['border-style']) && in_array($style['border-style'],$border_style_allowed))
{
$border_value['style'] = $style['border-style'];
}
if (isset($style['border-color']) && is_string($style['border-color']) && $style['border-color'][0]=='#')
{
$v = substr($style['border-color'],1,6);
$v = strlen($v)==3 ? $v[0].$v[0].$v[1].$v[1].$v[2].$v[2] : $v;// expand cf0 => ccff00
$border_value['color'] = "FF".strtoupper($v);
}
$style_indexes[$i]['border_idx'] = self::add_to_list_get_index($borders, json_encode($border_value));
}
if (isset($style['fill']) && is_string($style['fill']) && $style['fill'][0]=='#')
{
$v = substr($style['fill'],1,6);
$v = strlen($v)==3 ? $v[0].$v[0].$v[1].$v[1].$v[2].$v[2] : $v;// expand cf0 => ccff00
$style_indexes[$i]['fill_idx'] = self::add_to_list_get_index($fills, "FF".strtoupper($v) );
}
if (isset($style['halign']) && in_array($style['halign'],$horizontal_allowed))
{
$style_indexes[$i]['alignment'] = true;
$style_indexes[$i]['halign'] = $style['halign'];
}
if (isset($style['valign']) && in_array($style['valign'],$vertical_allowed))
{
$style_indexes[$i]['alignment'] = true;
$style_indexes[$i]['valign'] = $style['valign'];
}
if (isset($style['wrap_text']))
{
$style_indexes[$i]['alignment'] = true;
$style_indexes[$i]['wrap_text'] = (bool)$style['wrap_text'];
}
$font = $default_font;
if (isset($style['font-size']))
{
$font['size'] = floatval($style['font-size']);//floatval to allow "10.5" etc
}
if (isset($style['font']) && is_string($style['font']))
{
if ($style['font']=='Comic Sans MS') { $font['family']=4; }
if ($style['font']=='Times New Roman') { $font['family']=1; }
if ($style['font']=='Courier New') { $font['family']=3; }
$font['name'] = strval($style['font']);
}
if (isset($style['font-style']) && is_string($style['font-style']))
{
if (strpos($style['font-style'], 'bold')!==false) { $font['bold'] = true; }
if (strpos($style['font-style'], 'italic')!==false) { $font['italic'] = true; }
if (strpos($style['font-style'], 'strike')!==false) { $font['strike'] = true; }
if (strpos($style['font-style'], 'underline')!==false) { $font['underline'] = true; }
}
if (isset($style['color']) && is_string($style['color']) && $style['color'][0]=='#' )
{
$v = substr($style['color'],1,6);
$v = strlen($v)==3 ? $v[0].$v[0].$v[1].$v[1].$v[2].$v[2] : $v;// expand cf0 => ccff00
$font['color'] = "FF".strtoupper($v);
}
if ($font!=$default_font)
{
$style_indexes[$i]['font_idx'] = self::add_to_list_get_index($fonts, json_encode($font) );
}
}
return array('fills'=>$fills,'fonts'=>$fonts,'borders'=>$borders,'styles'=>$style_indexes );
}
protected function writeStylesXML()
{
$r = self::styleFontIndexes();
$fills = $r['fills'];
$fonts = $r['fonts'];
$borders = $r['borders'];
$style_indexes = $r['styles'];
$temporary_filename = $this->tempFilename();
$file = new XLSXWriter_BuffererWriter($temporary_filename);
$file->write('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n");
$file->write('<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">');
$file->write('<numFmts count="'.count($this->number_formats).'">');
foreach($this->number_formats as $i=>$v) {
$file->write('<numFmt numFmtId="'.(164+$i).'" formatCode="'.self::xmlspecialchars($v).'" />');
}
//$file->write( '<numFmt formatCode="GENERAL" numFmtId="164"/>');
//$file->write( '<numFmt formatCode="[$$-1009]#,##0.00;[RED]\-[$$-1009]#,##0.00" numFmtId="165"/>');
//$file->write( '<numFmt formatCode="YYYY-MM-DD\ HH:MM:SS" numFmtId="166"/>');
//$file->write( '<numFmt formatCode="YYYY-MM-DD" numFmtId="167"/>');
$file->write('</numFmts>');
$file->write('<fonts count="'.(count($fonts)).'">');
$file->write( '<font><name val="Arial"/><charset val="1"/><family val="2"/><sz val="10"/></font>');
$file->write( '<font><name val="Arial"/><family val="0"/><sz val="10"/></font>');
$file->write( '<font><name val="Arial"/><family val="0"/><sz val="10"/></font>');
$file->write( '<font><name val="Arial"/><family val="0"/><sz val="10"/></font>');
foreach($fonts as $font) {
if (!empty($font)) { //fonts have 4 empty placeholders in array to offset the 4 static xml entries above
$f = json_decode($font,true);
$file->write('<font>');
$file->write( '<name val="'.htmlspecialchars($f['name']).'"/><charset val="1"/><family val="'.intval($f['family']).'"/>');
$file->write( '<sz val="'.intval($f['size']).'"/>');
if (!empty($f['color'])) { $file->write('<color rgb="'.strval($f['color']).'"/>'); }
if (!empty($f['bold'])) { $file->write('<b val="true"/>'); }
if (!empty($f['italic'])) { $file->write('<i val="true"/>'); }
if (!empty($f['underline'])) { $file->write('<u val="single"/>'); }
if (!empty($f['strike'])) { $file->write('<strike val="true"/>'); }
$file->write('</font>');
}
}
$file->write('</fonts>');
$file->write('<fills count="'.(count($fills)).'">');
$file->write( '<fill><patternFill patternType="none"/></fill>');
$file->write( '<fill><patternFill patternType="gray125"/></fill>');
foreach($fills as $fill) {
if (!empty($fill)) { //fills have 2 empty placeholders in array to offset the 2 static xml entries above
$file->write('<fill><patternFill patternType="solid"><fgColor rgb="'.strval($fill).'"/><bgColor indexed="64"/></patternFill></fill>');
}
}
$file->write('</fills>');
$file->write('<borders count="'.(count($borders)).'">');
$file->write( '<border diagonalDown="false" diagonalUp="false"><left/><right/><top/><bottom/><diagonal/></border>');
foreach($borders as $border) {
if (!empty($border)) { //fonts have an empty placeholder in the array to offset the static xml entry above
$pieces = json_decode($border,true);
$border_style = !empty($pieces['style']) ? $pieces['style'] : 'hair';
$border_color = !empty($pieces['color']) ? '<color rgb="'.strval($pieces['color']).'"/>' : '';
$file->write('<border diagonalDown="false" diagonalUp="false">');
foreach (array('left', 'right', 'top', 'bottom') as $side)
{
$show_side = in_array($side,$pieces['side']) ? true : false;
$file->write($show_side ? "<$side style=\"$border_style\">$border_color</$side>" : "<$side/>");
}
$file->write( '<diagonal/>');
$file->write('</border>');
}
}
$file->write('</borders>');
$file->write('<cellStyleXfs count="20">');
$file->write( '<xf applyAlignment="true" applyBorder="true" applyFont="true" applyProtection="true" borderId="0" fillId="0" fontId="0" numFmtId="164">');
$file->write( '<alignment horizontal="general" indent="0" shrinkToFit="false" textRotation="0" vertical="bottom" wrapText="false"/>');
$file->write( '<protection hidden="false" locked="true"/>');
$file->write( '</xf>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="2" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="2" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="43"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="41"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="44"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="42"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="9"/>');
$file->write('</cellStyleXfs>');
$file->write('<cellXfs count="'.(count($style_indexes)).'">');
//$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="164" xfId="0"/>');
//$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="165" xfId="0"/>');
//$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="166" xfId="0"/>');
//$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="167" xfId="0"/>');
foreach($style_indexes as $v)
{
$applyAlignment = isset($v['alignment']) ? 'true' : 'false';
$wrapText = !empty($v['wrap_text']) ? 'true' : 'false';
$horizAlignment = isset($v['halign']) ? $v['halign'] : 'general';
$vertAlignment = isset($v['valign']) ? $v['valign'] : 'bottom';
$applyBorder = isset($v['border_idx']) ? 'true' : 'false';
$applyFont = 'true';
$borderIdx = isset($v['border_idx']) ? intval($v['border_idx']) : 0;
$fillIdx = isset($v['fill_idx']) ? intval($v['fill_idx']) : 0;
$fontIdx = isset($v['font_idx']) ? intval($v['font_idx']) : 0;
//$file->write('<xf applyAlignment="'.$applyAlignment.'" applyBorder="'.$applyBorder.'" applyFont="'.$applyFont.'" applyProtection="false" borderId="'.($borderIdx).'" fillId="'.($fillIdx).'" fontId="'.($fontIdx).'" numFmtId="'.(164+$v['num_fmt_idx']).'" xfId="0"/>');
$file->write('<xf applyAlignment="'.$applyAlignment.'" applyBorder="'.$applyBorder.'" applyFont="'.$applyFont.'" applyProtection="false" borderId="'.($borderIdx).'" fillId="'.($fillIdx).'" fontId="'.($fontIdx).'" numFmtId="'.(164+$v['num_fmt_idx']).'" xfId="0">');
$file->write(' <alignment horizontal="'.$horizAlignment.'" vertical="'.$vertAlignment.'" textRotation="0" wrapText="'.$wrapText.'" indent="0" shrinkToFit="false"/>');
$file->write(' <protection locked="true" hidden="false"/>');
$file->write('</xf>');
}
$file->write('</cellXfs>');
$file->write( '<cellStyles count="6">');
$file->write( '<cellStyle builtinId="0" customBuiltin="false" name="Normal" xfId="0"/>');
$file->write( '<cellStyle builtinId="3" customBuiltin="false" name="Comma" xfId="15"/>');
$file->write( '<cellStyle builtinId="6" customBuiltin="false" name="Comma [0]" xfId="16"/>');
$file->write( '<cellStyle builtinId="4" customBuiltin="false" name="Currency" xfId="17"/>');
$file->write( '<cellStyle builtinId="7" customBuiltin="false" name="Currency [0]" xfId="18"/>');
$file->write( '<cellStyle builtinId="5" customBuiltin="false" name="Percent" xfId="19"/>');
$file->write( '</cellStyles>');
$file->write('</styleSheet>');
$file->close();
return $temporary_filename;
}
protected function buildAppXML()
{
$app_xml="";
$app_xml.='<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n";
$app_xml.='<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">';
$app_xml.='<TotalTime>0</TotalTime>';
$app_xml.='<Company>'.self::xmlspecialchars($this->company).'</Company>';
$app_xml.='</Properties>';
return $app_xml;
}
protected function buildCoreXML()
{
$core_xml="";
$core_xml.='<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n";
$core_xml.='<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">';
$core_xml.='<dcterms:created xsi:type="dcterms:W3CDTF">'.date("Y-m-d\TH:i:s.00\Z").'</dcterms:created>';//$date_time = '2014-10-25T15:54:37.00Z';
$core_xml.='<dc:title>'.self::xmlspecialchars($this->title).'</dc:title>';
$core_xml.='<dc:subject>'.self::xmlspecialchars($this->subject).'</dc:subject>';
$core_xml.='<dc:creator>'.self::xmlspecialchars($this->author).'</dc:creator>';
if (!empty($this->keywords)) {
$core_xml.='<cp:keywords>'.self::xmlspecialchars(implode (", ", (array)$this->keywords)).'</cp:keywords>';
}
$core_xml.='<dc:description>'.self::xmlspecialchars($this->description).'</dc:description>';
$core_xml.='<cp:revision>0</cp:revision>';
$core_xml.='</cp:coreProperties>';
return $core_xml;
}
protected function buildRelationshipsXML()
{
$rels_xml="";
$rels_xml.='<?xml version="1.0" encoding="UTF-8"?>'."\n";
$rels_xml.='<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">';
$rels_xml.='<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>';
$rels_xml.='<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>';
$rels_xml.='<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>';
$rels_xml.="\n";
$rels_xml.='</Relationships>';
return $rels_xml;
}
protected function buildWorkbookXML()
{
$i=0;
$workbook_xml="";
$workbook_xml.='<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n";
$workbook_xml.='<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">';
$workbook_xml.='<fileVersion appName="Calc"/><workbookPr backupFile="false" showObjects="all" date1904="false"/><workbookProtection/>';
$workbook_xml.='<bookViews><workbookView activeTab="0" firstSheet="0" showHorizontalScroll="true" showSheetTabs="true" showVerticalScroll="true" tabRatio="212" windowHeight="8192" windowWidth="16384" xWindow="0" yWindow="0"/></bookViews>';
$workbook_xml.='<sheets>';
foreach($this->sheets as $sheet_name=>$sheet) {
$sheetname = self::sanitize_sheetname($sheet->sheetname);
$workbook_xml.='<sheet name="'.self::xmlspecialchars($sheetname).'" sheetId="'.($i+1).'" state="visible" r:id="rId'.($i+2).'"/>';
$i++;
}
$workbook_xml.='</sheets>';
$workbook_xml.='<definedNames>';
foreach($this->sheets as $sheet_name=>$sheet) {
if ($sheet->auto_filter) {
$sheetname = self::sanitize_sheetname($sheet->sheetname);
$workbook_xml.='<definedName name="_xlnm._FilterDatabase" localSheetId="0" hidden="1">\''.self::xmlspecialchars($sheetname).'\'!$A$1:' . self::xlsCell($sheet->row_count - 1, count($sheet->columns) - 1, true) . '</definedName>';
$i++;
}
}
$workbook_xml.='</definedNames>';
$workbook_xml.='<calcPr iterateCount="100" refMode="A1" iterate="false" iterateDelta="0.001"/></workbook>';
return $workbook_xml;
}
protected function buildWorkbookRelsXML()
{
$i=0;
$wkbkrels_xml="";
$wkbkrels_xml.='<?xml version="1.0" encoding="UTF-8"?>'."\n";
$wkbkrels_xml.='<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">';
$wkbkrels_xml.='<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>';
foreach($this->sheets as $sheet_name=>$sheet) {
$wkbkrels_xml.='<Relationship Id="rId'.($i+2).'" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/'.($sheet->xmlname).'"/>';
$i++;
}
$wkbkrels_xml.="\n";
$wkbkrels_xml.='</Relationships>';
return $wkbkrels_xml;
}
protected function buildContentTypesXML()
{
$content_types_xml="";
$content_types_xml.='<?xml version="1.0" encoding="UTF-8"?>'."\n";
$content_types_xml.='<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">';
$content_types_xml.='<Override PartName="/_rels/.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>';
$content_types_xml.='<Override PartName="/xl/_rels/workbook.xml.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>';
foreach($this->sheets as $sheet_name=>$sheet) {
$content_types_xml.='<Override PartName="/xl/worksheets/'.($sheet->xmlname).'" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>';
}
$content_types_xml.='<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>';
$content_types_xml.='<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>';
$content_types_xml.='<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>';
$content_types_xml.='<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>';
$content_types_xml.="\n";
$content_types_xml.='</Types>';
return $content_types_xml;
}
//------------------------------------------------------------------
/*
* @param $row_number int, zero based
* @param $column_number int, zero based
* @param $absolute bool
* @return Cell label/coordinates, ex: A1, C3, AA42 (or if $absolute==true: $A$1, $C$3, $AA$42)
* */
public static function xlsCell($row_number, $column_number, $absolute=false)
{
$n = $column_number;
for($r = ""; $n >= 0; $n = intval($n / 26) - 1) {
$r = chr($n%26 + 0x41) . $r;
}
if ($absolute) {
return '$' . $r . '$' . ($row_number+1);
}
return $r . ($row_number+1);
}
//------------------------------------------------------------------
public static function log($string)
{
//file_put_contents("php://stderr", date("Y-m-d H:i:s:").rtrim(is_array($string) ? json_encode($string) : $string)."\n");
error_log(date("Y-m-d H:i:s:").rtrim(is_array($string) ? json_encode($string) : $string)."\n");
}
//------------------------------------------------------------------
public static function sanitize_filename($filename) //http://msdn.microsoft.com/en-us/library/aa365247%28VS.85%29.aspx
{
$nonprinting = array_map('chr', range(0,31));
$invalid_chars = array('<', '>', '?', '"', ':', '|', '\\', '/', '*', '&');
$all_invalids = array_merge($nonprinting,$invalid_chars);
return str_replace($all_invalids, "", $filename);
}
//------------------------------------------------------------------
public static function sanitize_sheetname($sheetname)
{
static $badchars = '\\/?*:[]';
static $goodchars = ' ';
$sheetname = strtr($sheetname, $badchars, $goodchars);
$sheetname = function_exists('mb_substr') ? mb_substr($sheetname, 0, 31) : substr($sheetname, 0, 31);
$sheetname = trim(trim(trim($sheetname),"'"));//trim before and after trimming single quotes
return !empty($sheetname) ? $sheetname : 'Sheet'.((rand()%900)+100);
}
//------------------------------------------------------------------
public static function xmlspecialchars($val)
{
//note, badchars does not include \t\n\r (\x09\x0a\x0d)
static $badchars = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f";
static $goodchars = " ";
return strtr(htmlspecialchars($val, ENT_QUOTES | ENT_XML1), $badchars, $goodchars);//strtr appears to be faster than str_replace
}
//------------------------------------------------------------------
public static function array_first_key(array $arr)
{
reset($arr);
$first_key = key($arr);
return $first_key;
}
//------------------------------------------------------------------
private static function determineNumberFormatType($num_format)
{
$num_format = preg_replace("/\[(Black|Blue|Cyan|Green|Magenta|Red|White|Yellow)\]/i", "", $num_format);
if ($num_format=='GENERAL') return 'n_auto';
if ($num_format=='@') return 'n_string';
if ($num_format=='0') return 'n_numeric';
if (preg_match('/[H]{1,2}:[M]{1,2}(?![^"]*+")/i', $num_format)) return 'n_datetime';
if (preg_match('/[M]{1,2}:[S]{1,2}(?![^"]*+")/i', $num_format)) return 'n_datetime';
if (preg_match('/[Y]{2,4}(?![^"]*+")/i', $num_format)) return 'n_date';
if (preg_match('/[D]{1,2}(?![^"]*+")/i', $num_format)) return 'n_date';
if (preg_match('/[M]{1,2}(?![^"]*+")/i', $num_format)) return 'n_date';
if (preg_match('/$(?![^"]*+")/', $num_format)) return 'n_numeric';
if (preg_match('/%(?![^"]*+")/', $num_format)) return 'n_numeric';
if (preg_match('/0(?![^"]*+")/', $num_format)) return 'n_numeric';
return 'n_auto';
}
//------------------------------------------------------------------
private static function numberFormatStandardized($num_format)
{
if ($num_format=='money') { $num_format='dollar'; }
if ($num_format=='number') { $num_format='integer'; }
if ($num_format=='string') $num_format='@';
else if ($num_format=='integer') $num_format='0';
else if ($num_format=='date') $num_format='YYYY-MM-DD';
else if ($num_format=='datetime') $num_format='YYYY-MM-DD HH:MM:SS';
else if ($num_format=='time') $num_format='HH:MM:SS';
else if ($num_format=='price') $num_format='#,##0.00';
else if ($num_format=='dollar') $num_format='[$$-1009]#,##0.00;[RED]-[$$-1009]#,##0.00';
else if ($num_format=='euro') $num_format='#,##0.00 [$€-407];[RED]-#,##0.00 [$€-407]';
$ignore_until='';
$escaped = '';
for($i=0,$ix=strlen($num_format); $i<$ix; $i++)
{
$c = $num_format[$i];
if ($ignore_until=='' && $c=='[')
$ignore_until=']';
else if ($ignore_until=='' && $c=='"')
$ignore_until='"';
else if ($ignore_until==$c)
$ignore_until='';
if ($ignore_until=='' && ($c==' ' || $c=='-' || $c=='(' || $c==')') && ($i==0 || $num_format[$i-1]!='_'))
$escaped.= "\\".$c;
else
$escaped.= $c;
}
return $escaped;
}
//------------------------------------------------------------------
public static function add_to_list_get_index(&$haystack, $needle)
{
$existing_idx = array_search($needle, $haystack, $strict=true);
if ($existing_idx===false)
{
$existing_idx = count($haystack);
$haystack[] = $needle;
}
return $existing_idx;
}
//------------------------------------------------------------------
public static function convert_date_time($date_input) //thanks to Excel::Writer::XLSX::Worksheet.pm (perl)
{
$days = 0; # Number of days since epoch
$seconds = 0; # Time expressed as fraction of 24h hours in seconds
$year=$month=$day=0;
$hour=$min =$sec=0;
$date_time = $date_input;
if (preg_match("/(\d{4})\-(\d{2})\-(\d{2})/", $date_time, $matches))
{
list($junk,$year,$month,$day) = $matches;
}
if (preg_match("/(\d+):(\d{2}):(\d{2})/", $date_time, $matches))
{
list($junk,$hour,$min,$sec) = $matches;
$seconds = ( $hour * 60 * 60 + $min * 60 + $sec ) / ( 24 * 60 * 60 );
}
//using 1900 as epoch, not 1904, ignoring 1904 special case
# Special cases for Excel.
if ("$year-$month-$day"=='1899-12-31') return $seconds ; # Excel 1900 epoch
if ("$year-$month-$day"=='1900-01-00') return $seconds ; # Excel 1900 epoch
if ("$year-$month-$day"=='1900-02-29') return 60 + $seconds ; # Excel false leapday
# We calculate the date by calculating the number of days since the epoch
# and adjust for the number of leap days. We calculate the number of leap
# days by normalising the year in relation to the epoch. Thus the year 2000
# becomes 100 for 4 and 100 year leapdays and 400 for 400 year leapdays.
$epoch = 1900;
$offset = 0;
$norm = 300;
$range = $year - $epoch;
# Set month days and check for leap year.
$leap = (($year % 400 == 0) || (($year % 4 == 0) && ($year % 100)) ) ? 1 : 0;
$mdays = array( 31, ($leap ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
# Some boundary checks
if ($year!=0 || $month !=0 || $day!=0)
{
if($year < $epoch || $year > 9999) return 0;
if($month < 1 || $month > 12) return 0;
if($day < 1 || $day > $mdays[ $month - 1 ]) return 0;
}
# Accumulate the number of days since the epoch.
$days = $day; # Add days for current month
$days += array_sum( array_slice($mdays, 0, $month-1 ) ); # Add days for past months
$days += $range * 365; # Add days for past years
$days += intval( ( $range ) / 4 ); # Add leapdays
$days -= intval( ( $range + $offset ) / 100 ); # Subtract 100 year leapdays
$days += intval( ( $range + $offset + $norm ) / 400 ); # Add 400 year leapdays
$days -= $leap; # Already counted above
# Adjust for Excel erroneously treating 1900 as a leap year.
if ($days > 59) { $days++;}
return $days + $seconds;
}
//------------------------------------------------------------------
}
class XLSXWriter_BuffererWriter
{
protected $fd=null;
protected $buffer='';
protected $check_utf8=false;
public function __construct($filename, $fd_fopen_flags='w', $check_utf8=false)
{
$this->check_utf8 = $check_utf8;
$this->fd = fopen($filename, $fd_fopen_flags);
if ($this->fd===false) {
XLSXWriter::log("Unable to open $filename for writing.");
}
}
public function write($string)
{
$this->buffer.=$string;
if (isset($this->buffer[8191])) {
$this->purge();
}
}
protected function purge()
{
if ($this->fd) {
if ($this->check_utf8 && !self::isValidUTF8($this->buffer)) {
XLSXWriter::log("Error, invalid UTF8 encoding detected.");
$this->check_utf8 = false;
}
fwrite($this->fd, $this->buffer);
$this->buffer='';
}
}
public function close()
{
$this->purge();
if ($this->fd) {
fclose($this->fd);
$this->fd=null;
}
}
public function __destruct()
{
$this->close();
}
public function ftell()
{
if ($this->fd) {
$this->purge();
return ftell($this->fd);
}
return -1;
}
public function fseek($pos)
{
if ($this->fd) {
$this->purge();
return fseek($this->fd, $pos);
}
return -1;
}
protected static function isValidUTF8($string)
{
if (function_exists('mb_check_encoding'))
{
return mb_check_encoding($string, 'UTF-8') ? true : false;
}
return preg_match("//u", $string) ? true : false;
}
}
// vim: set filetype=php expandtab tabstop=4 shiftwidth=4 autoindent smartindent:
@@ -0,0 +1,895 @@
<?php
namespace app\extensions;
class XLSXWriter_old
{
//http://www.ecma-international.org/publications/standards/Ecma-376.htm
//http://officeopenxml.com/SSstyles.php
//------------------------------------------------------------------
//http://office.microsoft.com/en-us/excel-help/excel-specifications-and-limits-HP010073849.aspx
const EXCEL_2007_MAX_ROW=1048576;
const EXCEL_2007_MAX_COL=16384;
//------------------------------------------------------------------
protected $author ='Doc Author';
protected $sheets = array();
protected $temp_files = array();
protected $cell_styles = array();
protected $number_formats = array();
protected $current_sheet = '';
public function __construct()
{
if(!ini_get('date.timezone'))
{
//using date functions can kick out warning if this isn't set
date_default_timezone_set('UTC');
}
$this->addCellStyle($number_format='GENERAL', $style_string=null);
$this->addCellStyle($number_format='GENERAL', $style_string=null);
$this->addCellStyle($number_format='GENERAL', $style_string=null);
$this->addCellStyle($number_format='GENERAL', $style_string=null);
}
public function setAuthor($author='') { $this->author=$author; }
public function setTempDir($tempdir='') { $this->tempdir=$tempdir; }
public function __destruct()
{
if (!empty($this->temp_files)) {
foreach($this->temp_files as $temp_file) {
@unlink($temp_file);
}
}
}
protected function tempFilename()
{
$tempdir = !empty($this->tempdir) ? $this->tempdir : sys_get_temp_dir();
$filename = tempnam($tempdir, "xlsx_writer_");
$this->temp_files[] = $filename;
return $filename;
}
public function writeToStdOut()
{
$temp_file = $this->tempFilename();
self::writeToFile($temp_file);
readfile($temp_file);
}
public function writeToString()
{
$temp_file = $this->tempFilename();
self::writeToFile($temp_file);
$string = file_get_contents($temp_file);
return $string;
}
public function writeToFile($filename)
{
foreach($this->sheets as $sheet_name => $sheet) {
self::finalizeSheet($sheet_name);//making sure all footers have been written
}
if ( file_exists( $filename ) ) {
if ( is_writable( $filename ) ) {
@unlink( $filename ); //if the zip already exists, remove it
} else {
self::log( "Error in " . __CLASS__ . "::" . __FUNCTION__ . ", file is not writeable." );
return;
}
}
$zip = new \ZipArchive();
if (empty($this->sheets)) { self::log("Error in ".__CLASS__."::".__FUNCTION__.", no worksheets defined."); return; }
if (!$zip->open($filename, \ZipArchive::CREATE)) { self::log("Error in ".__CLASS__."::".__FUNCTION__.", unable to create zip."); return; }
$zip->addEmptyDir("docProps/");
$zip->addFromString("docProps/app.xml" , self::buildAppXML() );
$zip->addFromString("docProps/core.xml", self::buildCoreXML());
$zip->addEmptyDir("_rels/");
$zip->addFromString("_rels/.rels", self::buildRelationshipsXML());
$zip->addEmptyDir("xl/worksheets/");
foreach($this->sheets as $sheet) {
$zip->addFile($sheet->filename, "xl/worksheets/".$sheet->xmlname );
}
$zip->addFromString("xl/workbook.xml" , self::buildWorkbookXML() );
$zip->addFile($this->writeStylesXML(), "xl/styles.xml" ); //$zip->addFromString("xl/styles.xml" , self::buildStylesXML() );
$zip->addFromString("[Content_Types].xml" , self::buildContentTypesXML() );
$zip->addEmptyDir("xl/_rels/");
$zip->addFromString("xl/_rels/workbook.xml.rels", self::buildWorkbookRelsXML() );
$zip->close();
}
protected function initializeSheet($sheet_name, $col_widths=array() )
{
//if already initialized
if ($this->current_sheet==$sheet_name || isset($this->sheets[$sheet_name]))
return;
$sheet_filename = $this->tempFilename();
$sheet_xmlname = 'sheet' . (count($this->sheets) + 1).".xml";
$this->sheets[$sheet_name] = (object)array(
'filename' => $sheet_filename,
'sheetname' => $sheet_name,
'xmlname' => $sheet_xmlname,
'row_count' => 0,
'file_writer' => new XLSXWriter_BuffererWriter($sheet_filename),
'columns' => array(),
'merge_cells' => array(),
'max_cell_tag_start' => 0,
'max_cell_tag_end' => 0,
'finalized' => false,
);
$sheet = &$this->sheets[$sheet_name];
$tabselected = count($this->sheets) == 1 ? 'true' : 'false';//only first sheet is selected
$max_cell=XLSXWriter::xlsCell(self::EXCEL_2007_MAX_ROW, self::EXCEL_2007_MAX_COL);//XFE1048577
$sheet->file_writer->write('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . "\n");
$sheet->file_writer->write('<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">');
$sheet->file_writer->write( '<sheetPr filterMode="false">');
$sheet->file_writer->write( '<pageSetUpPr fitToPage="false"/>');
$sheet->file_writer->write( '</sheetPr>');
$sheet->max_cell_tag_start = $sheet->file_writer->ftell();
$sheet->file_writer->write('<dimension ref="A1:' . $max_cell . '"/>');
$sheet->max_cell_tag_end = $sheet->file_writer->ftell();
$sheet->file_writer->write( '<sheetViews>');
$sheet->file_writer->write( '<sheetView colorId="64" defaultGridColor="true" rightToLeft="false" showFormulas="false" showGridLines="true" showOutlineSymbols="true" showRowColHeaders="true" showZeros="true" tabSelected="' . $tabselected . '" topLeftCell="A1" view="normal" windowProtection="false" workbookViewId="0" zoomScale="100" zoomScaleNormal="100" zoomScalePageLayoutView="100">');
$sheet->file_writer->write( '<selection activeCell="A1" activeCellId="0" pane="topLeft" sqref="A1"/>');
$sheet->file_writer->write( '</sheetView>');
$sheet->file_writer->write( '</sheetViews>');
$sheet->file_writer->write( '<cols>');
$i=0;
if (!empty($col_widths)) {
foreach($col_widths as $column_width) {
$sheet->file_writer->write( '<col collapsed="false" hidden="false" max="'.($i+1).'" min="'.($i+1).'" style="0" width="'.floatval($column_width).'"/>');
$i++;
}
}
$sheet->file_writer->write( '<col collapsed="false" hidden="false" max="1024" min="'.($i+1).'" style="0" width="11.5"/>');
$sheet->file_writer->write( '</cols>');
$sheet->file_writer->write( '<sheetData>');
}
private function addCellStyle($number_format, $cell_style_string)
{
$number_format_idx = self::add_to_list_get_index($this->number_formats, $number_format);
$lookup_string = $number_format_idx.";".$cell_style_string;
$cell_style_idx = self::add_to_list_get_index($this->cell_styles, $lookup_string);
return $cell_style_idx;
}
private function initializeColumnTypes($header_types)
{
$column_types = array();
foreach($header_types as $v)
{
$number_format = self::numberFormatStandardized($v);
$number_format_type = self::determineNumberFormatType($number_format);
$cell_style_idx = $this->addCellStyle($number_format, $style_string=null);
$column_types[] = array('number_format' => $number_format,//contains excel format like 'YYYY-MM-DD HH:MM:SS'
'number_format_type' => $number_format_type, //contains friendly format like 'datetime'
'default_cell_style' => $cell_style_idx,
);
}
return $column_types;
}
public function writeSheetHeader($sheet_name, array $header_types, $col_options = null)
{
if (empty($sheet_name) || empty($header_types) || !empty($this->sheets[$sheet_name]))
return;
$suppress_row = isset($col_options['suppress_row']) ? intval($col_options['suppress_row']) : false;
if (is_bool($col_options))
{
self::log( "Warning! passing $suppress_row=false|true to writeSheetHeader() is deprecated, this will be removed in a future version." );
$suppress_row = intval($col_options);
}
$style = &$col_options;
$col_widths = isset($col_options['widths']) ? (array)$col_options['widths'] : array();
self::initializeSheet($sheet_name, $col_widths);
$sheet = &$this->sheets[$sheet_name];
$sheet->columns = $this->initializeColumnTypes($header_types);
if (!$suppress_row)
{
$header_row = array_keys($header_types);
$sheet->file_writer->write('<row collapsed="false" customFormat="false" customHeight="false" hidden="false" ht="12.1" outlineLevel="0" r="' . (1) . '">');
foreach ($header_row as $c => $v) {
$cell_style_idx = empty($style) ? $sheet->columns[$c]['default_cell_style'] : $this->addCellStyle( 'GENERAL', json_encode(isset($style[0]) ? $style[$c] : $style) );
$this->writeCell($sheet->file_writer, 0, $c, $v, $number_format_type='n_string', $cell_style_idx);
}
$sheet->file_writer->write('</row>');
$sheet->row_count++;
}
$this->current_sheet = $sheet_name;
}
public function writeSheetRow($sheet_name, array $row, $row_options=null)
{
if (empty($sheet_name))
return;
self::initializeSheet($sheet_name);
$sheet = &$this->sheets[$sheet_name];
if (count($sheet->columns) < count($row)) {
$default_column_types = $this->initializeColumnTypes( array_fill($from=0, $until=count($row), 'GENERAL') );//will map to n_auto
$sheet->columns = array_merge((array)$sheet->columns, $default_column_types);
}
if (!empty($row_options))
{
$ht = isset($row_options['height']) ? floatval($row_options['height']) : 12.1;
$customHt = isset($row_options['height']) ? true : false;
$hidden = isset($row_options['hidden']) ? boolval($row_options['hidden']) : false;
$collapsed = isset($row_options['collapsed']) ? boolval($row_options['collapsed']) : false;
$sheet->file_writer->write('<row collapsed="'.($collapsed).'" customFormat="false" customHeight="'.($customHt).'" hidden="'.($hidden).'" ht="'.($ht).'" outlineLevel="0" r="' . ($sheet->row_count + 1) . '">');
}
else
{
$sheet->file_writer->write('<row collapsed="false" customFormat="false" customHeight="false" hidden="false" ht="12.1" outlineLevel="0" r="' . ($sheet->row_count + 1) . '">');
}
$style = &$row_options;
$c=0;
foreach ($row as $v) {
$number_format = $sheet->columns[$c]['number_format'];
$number_format_type = $sheet->columns[$c]['number_format_type'];
$cell_style_idx = empty($style) ? $sheet->columns[$c]['default_cell_style'] : $this->addCellStyle( $number_format, json_encode(isset($style[0]) ? $style[$c] : $style) );
$this->writeCell($sheet->file_writer, $sheet->row_count, $c, $v, $number_format_type, $cell_style_idx);
$c++;
}
$sheet->file_writer->write('</row>');
$sheet->row_count++;
$this->current_sheet = $sheet_name;
}
public function countSheetRows($sheet_name = '')
{
$sheet_name = $sheet_name ?: $this->current_sheet;
return array_key_exists($sheet_name, $this->sheets) ? $this->sheets[$sheet_name]->row_count : 0;
}
protected function finalizeSheet($sheet_name)
{
if (empty($sheet_name) || $this->sheets[$sheet_name]->finalized)
return;
$sheet = &$this->sheets[$sheet_name];
$sheet->file_writer->write( '</sheetData>');
if (!empty($sheet->merge_cells)) {
$sheet->file_writer->write( '<mergeCells>');
foreach ($sheet->merge_cells as $range) {
$sheet->file_writer->write( '<mergeCell ref="' . $range . '"/>');
}
$sheet->file_writer->write( '</mergeCells>');
}
$sheet->file_writer->write( '<printOptions headings="false" gridLines="false" gridLinesSet="true" horizontalCentered="false" verticalCentered="false"/>');
$sheet->file_writer->write( '<pageMargins left="0.5" right="0.5" top="1.0" bottom="1.0" header="0.5" footer="0.5"/>');
$sheet->file_writer->write( '<pageSetup blackAndWhite="false" cellComments="none" copies="1" draft="false" firstPageNumber="1" fitToHeight="1" fitToWidth="1" horizontalDpi="300" orientation="portrait" pageOrder="downThenOver" paperSize="1" scale="100" useFirstPageNumber="true" usePrinterDefaults="false" verticalDpi="300"/>');
$sheet->file_writer->write( '<headerFooter differentFirst="false" differentOddEven="false">');
$sheet->file_writer->write( '<oddHeader>&amp;C&amp;&quot;Times New Roman,Regular&quot;&amp;12&amp;A</oddHeader>');
$sheet->file_writer->write( '<oddFooter>&amp;C&amp;&quot;Times New Roman,Regular&quot;&amp;12Page &amp;P</oddFooter>');
$sheet->file_writer->write( '</headerFooter>');
$sheet->file_writer->write('</worksheet>');
$max_cell = self::xlsCell($sheet->row_count - 1, count($sheet->columns) - 1);
$max_cell_tag = '<dimension ref="A1:' . $max_cell . '"/>';
$padding_length = $sheet->max_cell_tag_end - $sheet->max_cell_tag_start - strlen($max_cell_tag);
$sheet->file_writer->fseek($sheet->max_cell_tag_start);
$sheet->file_writer->write($max_cell_tag.str_repeat(" ", $padding_length));
$sheet->file_writer->close();
$sheet->finalized=true;
}
public function markMergedCell($sheet_name, $start_cell_row, $start_cell_column, $end_cell_row, $end_cell_column)
{
if (empty($sheet_name) || $this->sheets[$sheet_name]->finalized)
return;
self::initializeSheet($sheet_name);
$sheet = &$this->sheets[$sheet_name];
$startCell = self::xlsCell($start_cell_row, $start_cell_column);
$endCell = self::xlsCell($end_cell_row, $end_cell_column);
$sheet->merge_cells[] = $startCell . ":" . $endCell;
}
public function writeSheet(array $data, $sheet_name='', array $header_types=array())
{
$sheet_name = empty($sheet_name) ? 'Sheet1' : $sheet_name;
$data = empty($data) ? array(array('')) : $data;
if (!empty($header_types))
{
$this->writeSheetHeader($sheet_name, $header_types);
}
foreach($data as $i=>$row)
{
$this->writeSheetRow($sheet_name, $row);
}
$this->finalizeSheet($sheet_name);
}
protected function writeCell(XLSXWriter_BuffererWriter &$file, $row_number, $column_number, $value, $num_format_type, $cell_style_idx)
{
$cell_name = self::xlsCell($row_number, $column_number);
if (!is_scalar($value) || $value==='') { //objects, array, empty
$file->write('<c r="'.$cell_name.'" s="'.$cell_style_idx.'"/>');
} elseif (is_string($value) && $value{0}=='='){
$file->write('<c r="'.$cell_name.'" s="'.$cell_style_idx.'" t="s"><f>'.self::xmlspecialchars($value).'</f></c>');
} elseif ($num_format_type=='n_date') {
$file->write('<c r="'.$cell_name.'" s="'.$cell_style_idx.'" t="n"><v>'.intval(self::convert_date_time($value)).'</v></c>');
} elseif ($num_format_type=='n_datetime') {
$file->write('<c r="'.$cell_name.'" s="'.$cell_style_idx.'" t="n"><v>'.self::convert_date_time($value).'</v></c>');
} elseif ($num_format_type=='n_numeric') {
$file->write('<c r="'.$cell_name.'" s="'.$cell_style_idx.'" t="n"><v>'.self::xmlspecialchars($value).'</v></c>');//int,float,currency
} elseif ($num_format_type=='n_string') {
$file->write('<c r="'.$cell_name.'" s="'.$cell_style_idx.'" t="inlineStr"><is><t>'.self::xmlspecialchars($value).'</t></is></c>');
} elseif ($num_format_type=='n_auto' || 1) { //auto-detect unknown column types
if (!is_string($value) || $value=='0' || ($value[0]!='0' && ctype_digit($value)) || preg_match("/^\-?[1-9][0-9]*(\.[0-9]+)?$/", $value)){
$file->write('<c r="'.$cell_name.'" s="'.$cell_style_idx.'" t="n"><v>'.self::xmlspecialchars($value).'</v></c>');//int,float,currency
} else { //implied: ($cell_format=='string')
$file->write('<c r="'.$cell_name.'" s="'.$cell_style_idx.'" t="inlineStr"><is><t>'.self::xmlspecialchars($value).'</t></is></c>');
}
}
}
protected function styleFontIndexes()
{
static $border_allowed = array('left','right','top','bottom');
static $horizontal_allowed = array('general','left','right','justify','center');
static $vertical_allowed = array('bottom','center','distributed');
$default_font = array('size'=>'10','name'=>'Arial','family'=>'2');
$fills = array('','');//2 placeholders for static xml later
$fonts = array('','','','');//4 placeholders for static xml later
$borders = array('');//1 placeholder for static xml later
$style_indexes = array();
foreach($this->cell_styles as $i=>$cell_style_string)
{
$semi_colon_pos = strpos($cell_style_string,";");
$number_format_idx = substr($cell_style_string, 0, $semi_colon_pos);
$style_json_string = substr($cell_style_string, $semi_colon_pos+1);
$style = @json_decode($style_json_string, $as_assoc=true);
$style_indexes[$i] = array('num_fmt_idx'=>$number_format_idx);//initialize entry
if (isset($style['border']) && is_string($style['border']))
{
$border_input = explode(",", $style['border']);
sort($border_input);
$border_value = array_intersect($border_input, $border_allowed);
$style_indexes[$i]['border_idx'] = self::add_to_list_get_index($borders, implode(",", $border_value) );
}
if (isset($style['fill']) && is_string($style['fill']) && $style['fill'][0]=='#')
{
$v = substr($style['fill'],1,6);
$v = strlen($v)==3 ? $v[0].$v[0].$v[1].$v[1].$v[2].$v[2] : $v;// expand cf0 => ccff00
$style_indexes[$i]['fill_idx'] = self::add_to_list_get_index($fills, "FF".strtoupper($v) );
}
if (isset($style['halign']) && in_array($style['halign'],$horizontal_allowed))
{
$style_indexes[$i]['alignment'] = true;
$style_indexes[$i]['halign'] = $style['halign'];
}
if (isset($style['valign']) && in_array($style['valign'],$vertical_allowed))
{
$style_indexes[$i]['alignment'] = true;
$style_indexes[$i]['valign'] = $style['valign'];
}
if (isset($style['wrap_text']))
{
$style_indexes[$i]['alignment'] = true;
$style_indexes[$i]['wrap_text'] = $style['wrap_text'];
}
$font = $default_font;
if (isset($style['font-size']))
{
$font['size'] = floatval($style['font-size']);//floatval to allow "10.5" etc
}
if (isset($style['font']) && is_string($style['font']))
{
if ($style['font']=='Comic Sans MS') { $font['family']=4; }
if ($style['font']=='Times New Roman') { $font['family']=1; }
if ($style['font']=='Courier New') { $font['family']=3; }
$font['name'] = strval($style['font']);
}
if (isset($style['font-style']) && is_string($style['font-style']))
{
if (strpos($style['font-style'], 'bold')!==false) { $font['bold'] = true; }
if (strpos($style['font-style'], 'italic')!==false) { $font['italic'] = true; }
if (strpos($style['font-style'], 'strike')!==false) { $font['strike'] = true; }
if (strpos($style['font-style'], 'underline')!==false) { $font['underline'] = true; }
}
if (isset($style['color']) && is_string($style['color']) && $style['color'][0]=='#' )
{
$v = substr($style['color'],1,6);
$v = strlen($v)==3 ? $v[0].$v[0].$v[1].$v[1].$v[2].$v[2] : $v;// expand cf0 => ccff00
$font['color'] = "FF".strtoupper($v);
}
if ($font!=$default_font)
{
$style_indexes[$i]['font_idx'] = self::add_to_list_get_index($fonts, json_encode($font) );
}
}
return array('fills'=>$fills,'fonts'=>$fonts,'borders'=>$borders,'styles'=>$style_indexes );
}
protected function writeStylesXML()
{
$r = self::styleFontIndexes();
$fills = $r['fills'];
$fonts = $r['fonts'];
$borders = $r['borders'];
$style_indexes = $r['styles'];
$temporary_filename = $this->tempFilename();
$file = new XLSXWriter_BuffererWriter($temporary_filename);
$file->write('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n");
$file->write('<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">');
$file->write('<numFmts count="'.count($this->number_formats).'">');
foreach($this->number_formats as $i=>$v) {
$file->write('<numFmt numFmtId="'.(164+$i).'" formatCode="'.self::xmlspecialchars($v).'" />');
}
//$file->write( '<numFmt formatCode="GENERAL" numFmtId="164"/>');
//$file->write( '<numFmt formatCode="[$$-1009]#,##0.00;[RED]\-[$$-1009]#,##0.00" numFmtId="165"/>');
//$file->write( '<numFmt formatCode="YYYY-MM-DD\ HH:MM:SS" numFmtId="166"/>');
//$file->write( '<numFmt formatCode="YYYY-MM-DD" numFmtId="167"/>');
$file->write('</numFmts>');
$file->write('<fonts count="'.(count($fonts)).'">');
$file->write( '<font><name val="Arial"/><charset val="1"/><family val="2"/><sz val="10"/></font>');
$file->write( '<font><name val="Arial"/><family val="0"/><sz val="10"/></font>');
$file->write( '<font><name val="Arial"/><family val="0"/><sz val="10"/></font>');
$file->write( '<font><name val="Arial"/><family val="0"/><sz val="10"/></font>');
foreach($fonts as $font) {
if (!empty($font)) { //fonts have 4 empty placeholders in array to offset the 4 static xml entries above
$f = json_decode($font,true);
$file->write('<font>');
$file->write( '<name val="'.htmlspecialchars($f['name']).'"/><charset val="1"/><family val="'.intval($f['family']).'"/>');
$file->write( '<sz val="'.intval($f['size']).'"/>');
if (!empty($f['color'])) { $file->write('<color rgb="'.strval($f['color']).'"/>'); }
if (!empty($f['bold'])) { $file->write('<b val="true"/>'); }
if (!empty($f['italic'])) { $file->write('<i val="true"/>'); }
if (!empty($f['underline'])) { $file->write('<u val="single"/>'); }
if (!empty($f['strike'])) { $file->write('<strike val="true"/>'); }
$file->write('</font>');
}
}
$file->write('</fonts>');
$file->write('<fills count="'.(count($fills)).'">');
$file->write( '<fill><patternFill patternType="none"/></fill>');
$file->write( '<fill><patternFill patternType="gray125"/></fill>');
foreach($fills as $fill) {
if (!empty($fill)) { //fills have 2 empty placeholders in array to offset the 2 static xml entries above
$file->write('<fill><patternFill patternType="solid"><fgColor rgb="'.strval($fill).'"/><bgColor indexed="64"/></patternFill></fill>');
}
}
$file->write('</fills>');
$file->write('<borders count="'.(count($borders)).'">');
$file->write( '<border diagonalDown="false" diagonalUp="false"><left/><right/><top/><bottom/><diagonal/></border>');
foreach($borders as $border) {
if (!empty($border)) { //fonts have an empty placeholder in the array to offset the static xml entry above
$pieces = explode(",", $border);
$file->write('<border diagonalDown="false" diagonalUp="false">');
$file->write( '<left'.(in_array('left',$pieces) ? ' style="hair"' : '').'/>');
$file->write( '<right'.(in_array('right',$pieces) ? ' style="hair"' : '').'/>');
$file->write( '<top'.(in_array('top',$pieces) ? ' style="hair"' : '').'/>');
$file->write( '<bottom'.(in_array('bottom',$pieces) ? ' style="hair"' : '').'/>');
$file->write( '<diagonal/>');
$file->write('</border>');
}
}
$file->write('</borders>');
$file->write('<cellStyleXfs count="20">');
$file->write( '<xf applyAlignment="true" applyBorder="true" applyFont="true" applyProtection="true" borderId="0" fillId="0" fontId="0" numFmtId="164">');
$file->write( '<alignment horizontal="general" indent="0" shrinkToFit="false" textRotation="0" vertical="bottom" wrapText="false"/>');
$file->write( '<protection hidden="false" locked="true"/>');
$file->write( '</xf>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="2" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="2" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="0"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="43"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="41"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="44"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="42"/>');
$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="true" applyProtection="false" borderId="0" fillId="0" fontId="1" numFmtId="9"/>');
$file->write('</cellStyleXfs>');
$file->write('<cellXfs count="'.(count($style_indexes)).'">');
//$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="164" xfId="0"/>');
//$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="165" xfId="0"/>');
//$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="166" xfId="0"/>');
//$file->write( '<xf applyAlignment="false" applyBorder="false" applyFont="false" applyProtection="false" borderId="0" fillId="0" fontId="0" numFmtId="167" xfId="0"/>');
foreach($style_indexes as $v)
{
$applyAlignment = isset($v['alignment']) ? 'true' : 'false';
$wrapText = isset($v['wrap_text']) ? boolval($v['wrap_text']) : 'false';
$horizAlignment = isset($v['halign']) ? $v['halign'] : 'general';
$vertAlignment = isset($v['valign']) ? $v['valign'] : 'bottom';
$applyBorder = isset($v['border_idx']) ? 'true' : 'false';
$applyFont = 'true';
$borderIdx = isset($v['border_idx']) ? intval($v['border_idx']) : 0;
$fillIdx = isset($v['fill_idx']) ? intval($v['fill_idx']) : 0;
$fontIdx = isset($v['font_idx']) ? intval($v['font_idx']) : 0;
//$file->write('<xf applyAlignment="'.$applyAlignment.'" applyBorder="'.$applyBorder.'" applyFont="'.$applyFont.'" applyProtection="false" borderId="'.($borderIdx).'" fillId="'.($fillIdx).'" fontId="'.($fontIdx).'" numFmtId="'.(164+$v['num_fmt_idx']).'" xfId="0"/>');
$file->write('<xf applyAlignment="'.$applyAlignment.'" applyBorder="'.$applyBorder.'" applyFont="'.$applyFont.'" applyProtection="false" borderId="'.($borderIdx).'" fillId="'.($fillIdx).'" fontId="'.($fontIdx).'" numFmtId="'.(164+$v['num_fmt_idx']).'" xfId="0">');
$file->write(' <alignment horizontal="'.$horizAlignment.'" vertical="'.$vertAlignment.'" textRotation="0" wrapText="'.$wrapText.'" indent="0" shrinkToFit="false"/>');
$file->write(' <protection locked="true" hidden="false"/>');
$file->write('</xf>');
}
$file->write('</cellXfs>');
$file->write( '<cellStyles count="6">');
$file->write( '<cellStyle builtinId="0" customBuiltin="false" name="Normal" xfId="0"/>');
$file->write( '<cellStyle builtinId="3" customBuiltin="false" name="Comma" xfId="15"/>');
$file->write( '<cellStyle builtinId="6" customBuiltin="false" name="Comma [0]" xfId="16"/>');
$file->write( '<cellStyle builtinId="4" customBuiltin="false" name="Currency" xfId="17"/>');
$file->write( '<cellStyle builtinId="7" customBuiltin="false" name="Currency [0]" xfId="18"/>');
$file->write( '<cellStyle builtinId="5" customBuiltin="false" name="Percent" xfId="19"/>');
$file->write( '</cellStyles>');
$file->write('</styleSheet>');
$file->close();
return $temporary_filename;
}
protected function buildAppXML()
{
$app_xml="";
$app_xml.='<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n";
$app_xml.='<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><TotalTime>0</TotalTime></Properties>';
return $app_xml;
}
protected function buildCoreXML()
{
$core_xml="";
$core_xml.='<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n";
$core_xml.='<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">';
$core_xml.='<dcterms:created xsi:type="dcterms:W3CDTF">'.date("Y-m-d\TH:i:s.00\Z").'</dcterms:created>';//$date_time = '2014-10-25T15:54:37.00Z';
$core_xml.='<dc:creator>'.self::xmlspecialchars($this->author).'</dc:creator>';
$core_xml.='<cp:revision>0</cp:revision>';
$core_xml.='</cp:coreProperties>';
return $core_xml;
}
protected function buildRelationshipsXML()
{
$rels_xml="";
$rels_xml.='<?xml version="1.0" encoding="UTF-8"?>'."\n";
$rels_xml.='<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">';
$rels_xml.='<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>';
$rels_xml.='<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>';
$rels_xml.='<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>';
$rels_xml.="\n";
$rels_xml.='</Relationships>';
return $rels_xml;
}
protected function buildWorkbookXML()
{
$i=0;
$workbook_xml="";
$workbook_xml.='<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'."\n";
$workbook_xml.='<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">';
$workbook_xml.='<fileVersion appName="Calc"/><workbookPr backupFile="false" showObjects="all" date1904="false"/><workbookProtection/>';
$workbook_xml.='<bookViews><workbookView activeTab="0" firstSheet="0" showHorizontalScroll="true" showSheetTabs="true" showVerticalScroll="true" tabRatio="212" windowHeight="8192" windowWidth="16384" xWindow="0" yWindow="0"/></bookViews>';
$workbook_xml.='<sheets>';
foreach($this->sheets as $sheet_name=>$sheet) {
$sheetname = self::sanitize_sheetname($sheet->sheetname);
$workbook_xml.='<sheet name="'.self::xmlspecialchars($sheetname).'" sheetId="'.($i+1).'" state="visible" r:id="rId'.($i+2).'"/>';
$i++;
}
$workbook_xml.='</sheets>';
$workbook_xml.='<calcPr iterateCount="100" refMode="A1" iterate="false" iterateDelta="0.001"/></workbook>';
return $workbook_xml;
}
protected function buildWorkbookRelsXML()
{
$i=0;
$wkbkrels_xml="";
$wkbkrels_xml.='<?xml version="1.0" encoding="UTF-8"?>'."\n";
$wkbkrels_xml.='<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">';
$wkbkrels_xml.='<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>';
foreach($this->sheets as $sheet_name=>$sheet) {
$wkbkrels_xml.='<Relationship Id="rId'.($i+2).'" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/'.($sheet->xmlname).'"/>';
$i++;
}
$wkbkrels_xml.="\n";
$wkbkrels_xml.='</Relationships>';
return $wkbkrels_xml;
}
protected function buildContentTypesXML()
{
$content_types_xml="";
$content_types_xml.='<?xml version="1.0" encoding="UTF-8"?>'."\n";
$content_types_xml.='<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">';
$content_types_xml.='<Override PartName="/_rels/.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>';
$content_types_xml.='<Override PartName="/xl/_rels/workbook.xml.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>';
foreach($this->sheets as $sheet_name=>$sheet) {
$content_types_xml.='<Override PartName="/xl/worksheets/'.($sheet->xmlname).'" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>';
}
$content_types_xml.='<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>';
$content_types_xml.='<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>';
$content_types_xml.='<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>';
$content_types_xml.='<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>';
$content_types_xml.="\n";
$content_types_xml.='</Types>';
return $content_types_xml;
}
//------------------------------------------------------------------
/*
* @param $row_number int, zero based
* @param $column_number int, zero based
* @return Cell label/coordinates, ex: A1, C3, AA42
* */
public static function xlsCell($row_number, $column_number)
{
$n = $column_number;
for($r = ""; $n >= 0; $n = intval($n / 26) - 1) {
$r = chr($n%26 + 0x41) . $r;
}
return $r . ($row_number+1);
}
//------------------------------------------------------------------
public static function log($string)
{
file_put_contents("php://stderr", date("Y-m-d H:i:s:").rtrim(is_array($string) ? json_encode($string) : $string)."\n");
}
//------------------------------------------------------------------
public static function sanitize_filename($filename) //http://msdn.microsoft.com/en-us/library/aa365247%28VS.85%29.aspx
{
$nonprinting = array_map('chr', range(0,31));
$invalid_chars = array('<', '>', '?', '"', ':', '|', '\\', '/', '*', '&');
$all_invalids = array_merge($nonprinting,$invalid_chars);
return str_replace($all_invalids, "", $filename);
}
//------------------------------------------------------------------
public static function sanitize_sheetname($sheetname)
{
static $badchars = '\\/?*:[]';
static $goodchars = ' ';
$sheetname = strtr($sheetname, $badchars, $goodchars);
$sheetname = substr($sheetname, 0, 31);
$sheetname = trim(trim(trim($sheetname),"'"));//trim before and after trimming single quotes
return !empty($sheetname) ? $sheetname : 'Sheet'.((rand()%900)+100);
}
//------------------------------------------------------------------
public static function xmlspecialchars($val)
{
//note, badchars does not include \t\n\r (\x09\x0a\x0d)
static $badchars = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f";
static $goodchars = " ";
return strtr(htmlspecialchars($val, ENT_QUOTES | ENT_XML1), $badchars, $goodchars);//strtr appears to be faster than str_replace
}
//------------------------------------------------------------------
public static function array_first_key(array $arr)
{
reset($arr);
$first_key = key($arr);
return $first_key;
}
//------------------------------------------------------------------
private static function determineNumberFormatType($num_format)
{
$num_format = preg_replace("/\[(Black|Blue|Cyan|Green|Magenta|Red|White|Yellow)\]/i", "", $num_format);
if ($num_format=='GENERAL') return 'n_auto';
if ($num_format=='@') return 'n_string';
if ($num_format=='0') return 'n_numeric';
if (preg_match("/[H]{1,2}:[M]{1,2}/", $num_format)) return 'n_datetime';
if (preg_match("/[M]{1,2}:[S]{1,2}/", $num_format)) return 'n_datetime';
if (preg_match("/[YY]{2,4}/", $num_format)) return 'n_date';
if (preg_match("/[D]{1,2}/", $num_format)) return 'n_date';
if (preg_match("/[M]{1,2}/", $num_format)) return 'n_date';
if (preg_match("/$/", $num_format)) return 'n_numeric';
if (preg_match("/%/", $num_format)) return 'n_numeric';
if (preg_match("/0/", $num_format)) return 'n_numeric';
return 'n_auto';
}
//------------------------------------------------------------------
private static function numberFormatStandardized($num_format)
{
if ($num_format=='money') { $num_format='dollar'; }
if ($num_format=='number') { $num_format='integer'; }
if ($num_format=='string') $num_format='@';
else if ($num_format=='integer') $num_format='0';
else if ($num_format=='date') $num_format='YYYY-MM-DD';
else if ($num_format=='datetime') $num_format='YYYY-MM-DD HH:MM:SS';
else if ($num_format=='price') $num_format='#,##0.00';
else if ($num_format=='dollar') $num_format='[$$-1009]#,##0.00;[RED]-[$$-1009]#,##0.00';
else if ($num_format=='euro') $num_format='#,##0.00 [$€-407];[RED]-#,##0.00 [$€-407]';
$ignore_until='';
$escaped = '';
for($i=0,$ix=strlen($num_format); $i<$ix; $i++)
{
$c = $num_format[$i];
if ($ignore_until=='' && $c=='[')
$ignore_until=']';
else if ($ignore_until=='' && $c=='"')
$ignore_until='"';
else if ($ignore_until==$c)
$ignore_until='';
if ($ignore_until=='' && ($c==' ' || $c=='-' || $c=='(' || $c==')') && ($i==0 || $num_format[$i-1]!='_'))
$escaped.= "\\".$c;
else
$escaped.= $c;
}
return $escaped;
}
//------------------------------------------------------------------
public static function add_to_list_get_index(&$haystack, $needle)
{
$existing_idx = array_search($needle, $haystack, $strict=true);
if ($existing_idx===false)
{
$existing_idx = count($haystack);
$haystack[] = $needle;
}
return $existing_idx;
}
//------------------------------------------------------------------
public static function convert_date_time($date_input) //thanks to Excel::Writer::XLSX::Worksheet.pm (perl)
{
$days = 0; # Number of days since epoch
$seconds = 0; # Time expressed as fraction of 24h hours in seconds
$year=$month=$day=0;
$hour=$min =$sec=0;
$date_time = $date_input;
if (preg_match("/(\d{4})\-(\d{2})\-(\d{2})/", $date_time, $matches))
{
list($junk,$year,$month,$day) = $matches;
}
if (preg_match("/(\d+):(\d{2}):(\d{2})/", $date_time, $matches))
{
list($junk,$hour,$min,$sec) = $matches;
$seconds = ( $hour * 60 * 60 + $min * 60 + $sec ) / ( 24 * 60 * 60 );
}
//using 1900 as epoch, not 1904, ignoring 1904 special case
# Special cases for Excel.
if ("$year-$month-$day"=='1899-12-31') return $seconds ; # Excel 1900 epoch
if ("$year-$month-$day"=='1900-01-00') return $seconds ; # Excel 1900 epoch
if ("$year-$month-$day"=='1900-02-29') return 60 + $seconds ; # Excel false leapday
# We calculate the date by calculating the number of days since the epoch
# and adjust for the number of leap days. We calculate the number of leap
# days by normalising the year in relation to the epoch. Thus the year 2000
# becomes 100 for 4 and 100 year leapdays and 400 for 400 year leapdays.
$epoch = 1900;
$offset = 0;
$norm = 300;
$range = $year - $epoch;
# Set month days and check for leap year.
$leap = (($year % 400 == 0) || (($year % 4 == 0) && ($year % 100)) ) ? 1 : 0;
$mdays = array( 31, ($leap ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
# Some boundary checks
if($year < $epoch || $year > 9999) return 0;
if($month < 1 || $month > 12) return 0;
if($day < 1 || $day > $mdays[ $month - 1 ]) return 0;
# Accumulate the number of days since the epoch.
$days = $day; # Add days for current month
$days += array_sum( array_slice($mdays, 0, $month-1 ) ); # Add days for past months
$days += $range * 365; # Add days for past years
$days += intval( ( $range ) / 4 ); # Add leapdays
$days -= intval( ( $range + $offset ) / 100 ); # Subtract 100 year leapdays
$days += intval( ( $range + $offset + $norm ) / 400 ); # Add 400 year leapdays
$days -= $leap; # Already counted above
# Adjust for Excel erroneously treating 1900 as a leap year.
if ($days > 59) { $days++;}
return $days + $seconds;
}
//------------------------------------------------------------------
}
class XLSXWriter_BuffererWriter
{
protected $fd=null;
protected $buffer='';
protected $check_utf8=false;
public function __construct($filename, $fd_fopen_flags='w', $check_utf8=false)
{
$this->check_utf8 = $check_utf8;
$this->fd = fopen($filename, $fd_fopen_flags);
if ($this->fd===false) {
XLSXWriter::log("Unable to open $filename for writing.");
}
}
public function write($string)
{
$this->buffer.=$string;
if (isset($this->buffer[8191])) {
$this->purge();
}
}
protected function purge()
{
if ($this->fd) {
if ($this->check_utf8 && !self::isValidUTF8($this->buffer)) {
XLSXWriter::log("Error, invalid UTF8 encoding detected.");
$this->check_utf8 = false;
}
fwrite($this->fd, $this->buffer);
$this->buffer='';
}
}
public function close()
{
$this->purge();
if ($this->fd) {
fclose($this->fd);
$this->fd=null;
}
}
public function __destruct()
{
$this->close();
}
public function ftell()
{
if ($this->fd) {
$this->purge();
return ftell($this->fd);
}
return -1;
}
public function fseek($pos)
{
if ($this->fd) {
$this->purge();
return fseek($this->fd, $pos);
}
return -1;
}
protected static function isValidUTF8($string)
{
if (function_exists('mb_check_encoding'))
{
return mb_check_encoding($string, 'UTF-8') ? true : false;
}
return preg_match("//u", $string) ? true : false;
}
}
// vim: set filetype=php expandtab tabstop=4 shiftwidth=4 autoindent smartindent:
@@ -0,0 +1,7 @@
<?php
namespace Firebase\JWT;
class BeforeValidException extends \UnexpectedValueException
{
}
@@ -0,0 +1,229 @@
<?php
namespace Firebase\JWT;
use ArrayAccess;
use LogicException;
use OutOfBoundsException;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use RuntimeException;
/**
* @implements ArrayAccess<string, Key>
*/
class CachedKeySet implements ArrayAccess
{
/**
* @var string
*/
private $jwksUri;
/**
* @var ClientInterface
*/
private $httpClient;
/**
* @var RequestFactoryInterface
*/
private $httpFactory;
/**
* @var CacheItemPoolInterface
*/
private $cache;
/**
* @var ?int
*/
private $expiresAfter;
/**
* @var ?CacheItemInterface
*/
private $cacheItem;
/**
* @var array<string, Key>
*/
private $keySet;
/**
* @var string
*/
private $cacheKey;
/**
* @var string
*/
private $cacheKeyPrefix = 'jwks';
/**
* @var int
*/
private $maxKeyLength = 64;
/**
* @var bool
*/
private $rateLimit;
/**
* @var string
*/
private $rateLimitCacheKey;
/**
* @var int
*/
private $maxCallsPerMinute = 10;
/**
* @var string|null
*/
private $defaultAlg;
public function __construct(
string $jwksUri,
ClientInterface $httpClient,
RequestFactoryInterface $httpFactory,
CacheItemPoolInterface $cache,
int $expiresAfter = null,
bool $rateLimit = false,
string $defaultAlg = null
) {
$this->jwksUri = $jwksUri;
$this->httpClient = $httpClient;
$this->httpFactory = $httpFactory;
$this->cache = $cache;
$this->expiresAfter = $expiresAfter;
$this->rateLimit = $rateLimit;
$this->defaultAlg = $defaultAlg;
$this->setCacheKeys();
}
/**
* @param string $keyId
* @return Key
*/
public function offsetGet($keyId): Key
{
if (!$this->keyIdExists($keyId)) {
throw new OutOfBoundsException('Key ID not found');
}
return $this->keySet[$keyId];
}
/**
* @param string $keyId
* @return bool
*/
public function offsetExists($keyId): bool
{
return $this->keyIdExists($keyId);
}
/**
* @param string $offset
* @param Key $value
*/
public function offsetSet($offset, $value): void
{
throw new LogicException('Method not implemented');
}
/**
* @param string $offset
*/
public function offsetUnset($offset): void
{
throw new LogicException('Method not implemented');
}
private function keyIdExists(string $keyId): bool
{
if (null === $this->keySet) {
$item = $this->getCacheItem();
// Try to load keys from cache
if ($item->isHit()) {
// item found! Return it
$jwks = $item->get();
$this->keySet = JWK::parseKeySet(json_decode($jwks, true), $this->defaultAlg);
}
}
if (!isset($this->keySet[$keyId])) {
if ($this->rateLimitExceeded()) {
return false;
}
$request = $this->httpFactory->createRequest('GET', $this->jwksUri);
$jwksResponse = $this->httpClient->sendRequest($request);
$jwks = (string) $jwksResponse->getBody();
$this->keySet = JWK::parseKeySet(json_decode($jwks, true), $this->defaultAlg);
if (!isset($this->keySet[$keyId])) {
return false;
}
$item = $this->getCacheItem();
$item->set($jwks);
if ($this->expiresAfter) {
$item->expiresAfter($this->expiresAfter);
}
$this->cache->save($item);
}
return true;
}
private function rateLimitExceeded(): bool
{
if (!$this->rateLimit) {
return false;
}
$cacheItem = $this->cache->getItem($this->rateLimitCacheKey);
if (!$cacheItem->isHit()) {
$cacheItem->expiresAfter(1); // # of calls are cached each minute
}
$callsPerMinute = (int) $cacheItem->get();
if (++$callsPerMinute > $this->maxCallsPerMinute) {
return true;
}
$cacheItem->set($callsPerMinute);
$this->cache->save($cacheItem);
return false;
}
private function getCacheItem(): CacheItemInterface
{
if (\is_null($this->cacheItem)) {
$this->cacheItem = $this->cache->getItem($this->cacheKey);
}
return $this->cacheItem;
}
private function setCacheKeys(): void
{
if (empty($this->jwksUri)) {
throw new RuntimeException('JWKS URI is empty');
}
// ensure we do not have illegal characters
$key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwksUri);
// add prefix
$key = $this->cacheKeyPrefix . $key;
// Hash keys if they exceed $maxKeyLength of 64
if (\strlen($key) > $this->maxKeyLength) {
$key = substr(hash('sha256', $key), 0, $this->maxKeyLength);
}
$this->cacheKey = $key;
if ($this->rateLimit) {
// add prefix
$rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key;
// Hash keys if they exceed $maxKeyLength of 64
if (\strlen($rateLimitKey) > $this->maxKeyLength) {
$rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength);
}
$this->rateLimitCacheKey = $rateLimitKey;
}
}
}
@@ -0,0 +1,7 @@
<?php
namespace Firebase\JWT;
class ExpiredException extends \UnexpectedValueException
{
}
@@ -0,0 +1,322 @@
<?php
namespace Firebase\JWT;
use DomainException;
use InvalidArgumentException;
use UnexpectedValueException;
/**
* JSON Web Key implementation, based on this spec:
* https://tools.ietf.org/html/draft-ietf-jose-json-web-key-41
*
* PHP version 5
*
* @category Authentication
* @package Authentication_JWT
* @author Bui Sy Nguyen <nguyenbs@gmail.com>
* @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
* @link https://github.com/firebase/php-jwt
*/
class JWK
{
private const OID = '1.2.840.10045.2.1';
private const ASN1_OBJECT_IDENTIFIER = 0x06;
private const ASN1_SEQUENCE = 0x10; // also defined in JWT
private const ASN1_BIT_STRING = 0x03;
private const EC_CURVES = [
'P-256' => '1.2.840.10045.3.1.7', // Len: 64
// 'P-384' => '1.3.132.0.34', // Len: 96 (not yet supported)
// 'P-521' => '1.3.132.0.35', // Len: 132 (not supported)
];
/**
* Parse a set of JWK keys
*
* @param array<mixed> $jwks The JSON Web Key Set as an associative array
* @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the
* JSON Web Key Set
*
* @return array<string, Key> An associative array of key IDs (kid) to Key objects
*
* @throws InvalidArgumentException Provided JWK Set is empty
* @throws UnexpectedValueException Provided JWK Set was invalid
* @throws DomainException OpenSSL failure
*
* @uses parseKey
*/
public static function parseKeySet(array $jwks, string $defaultAlg = null): array
{
$keys = [];
if (!isset($jwks['keys'])) {
throw new UnexpectedValueException('"keys" member must exist in the JWK Set');
}
if (empty($jwks['keys'])) {
throw new InvalidArgumentException('JWK Set did not contain any keys');
}
foreach ($jwks['keys'] as $k => $v) {
$kid = isset($v['kid']) ? $v['kid'] : $k;
if ($key = self::parseKey($v, $defaultAlg)) {
$keys[(string) $kid] = $key;
}
}
if (0 === \count($keys)) {
throw new UnexpectedValueException('No supported algorithms found in JWK Set');
}
return $keys;
}
/**
* Parse a JWK key
*
* @param array<mixed> $jwk An individual JWK
* @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the
* JSON Web Key Set
*
* @return Key The key object for the JWK
*
* @throws InvalidArgumentException Provided JWK is empty
* @throws UnexpectedValueException Provided JWK was invalid
* @throws DomainException OpenSSL failure
*
* @uses createPemFromModulusAndExponent
*/
public static function parseKey(array $jwk, string $defaultAlg = null): ?Key
{
if (empty($jwk)) {
throw new InvalidArgumentException('JWK must not be empty');
}
if (!isset($jwk['kty'])) {
throw new UnexpectedValueException('JWK must contain a "kty" parameter');
}
if (!isset($jwk['alg'])) {
if (\is_null($defaultAlg)) {
// The "alg" parameter is optional in a KTY, but an algorithm is required
// for parsing in this library. Use the $defaultAlg parameter when parsing the
// key set in order to prevent this error.
// @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4
throw new UnexpectedValueException('JWK must contain an "alg" parameter');
}
$jwk['alg'] = $defaultAlg;
}
switch ($jwk['kty']) {
case 'RSA':
if (!empty($jwk['d'])) {
throw new UnexpectedValueException('RSA private keys are not supported');
}
if (!isset($jwk['n']) || !isset($jwk['e'])) {
throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"');
}
$pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']);
$publicKey = \openssl_pkey_get_public($pem);
if (false === $publicKey) {
throw new DomainException(
'OpenSSL error: ' . \openssl_error_string()
);
}
return new Key($publicKey, $jwk['alg']);
case 'EC':
if (isset($jwk['d'])) {
// The key is actually a private key
throw new UnexpectedValueException('Key data must be for a public key');
}
if (empty($jwk['crv'])) {
throw new UnexpectedValueException('crv not set');
}
if (!isset(self::EC_CURVES[$jwk['crv']])) {
throw new DomainException('Unrecognised or unsupported EC curve');
}
if (empty($jwk['x']) || empty($jwk['y'])) {
throw new UnexpectedValueException('x and y not set');
}
$publicKey = self::createPemFromCrvAndXYCoordinates($jwk['crv'], $jwk['x'], $jwk['y']);
return new Key($publicKey, $jwk['alg']);
default:
// Currently only RSA is supported
break;
}
return null;
}
/**
* Converts the EC JWK values to pem format.
*
* @param string $crv The EC curve (only P-256 is supported)
* @param string $x The EC x-coordinate
* @param string $y The EC y-coordinate
*
* @return string
*/
private static function createPemFromCrvAndXYCoordinates(string $crv, string $x, string $y): string
{
$pem =
self::encodeDER(
self::ASN1_SEQUENCE,
self::encodeDER(
self::ASN1_SEQUENCE,
self::encodeDER(
self::ASN1_OBJECT_IDENTIFIER,
self::encodeOID(self::OID)
)
. self::encodeDER(
self::ASN1_OBJECT_IDENTIFIER,
self::encodeOID(self::EC_CURVES[$crv])
)
) .
self::encodeDER(
self::ASN1_BIT_STRING,
\chr(0x00) . \chr(0x04)
. JWT::urlsafeB64Decode($x)
. JWT::urlsafeB64Decode($y)
)
);
return sprintf(
"-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n",
wordwrap(base64_encode($pem), 64, "\n", true)
);
}
/**
* Create a public key represented in PEM format from RSA modulus and exponent information
*
* @param string $n The RSA modulus encoded in Base64
* @param string $e The RSA exponent encoded in Base64
*
* @return string The RSA public key represented in PEM format
*
* @uses encodeLength
*/
private static function createPemFromModulusAndExponent(
string $n,
string $e
): string {
$mod = JWT::urlsafeB64Decode($n);
$exp = JWT::urlsafeB64Decode($e);
$modulus = \pack('Ca*a*', 2, self::encodeLength(\strlen($mod)), $mod);
$publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp);
$rsaPublicKey = \pack(
'Ca*a*a*',
48,
self::encodeLength(\strlen($modulus) + \strlen($publicExponent)),
$modulus,
$publicExponent
);
// sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption.
$rsaOID = \pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA
$rsaPublicKey = \chr(0) . $rsaPublicKey;
$rsaPublicKey = \chr(3) . self::encodeLength(\strlen($rsaPublicKey)) . $rsaPublicKey;
$rsaPublicKey = \pack(
'Ca*a*',
48,
self::encodeLength(\strlen($rsaOID . $rsaPublicKey)),
$rsaOID . $rsaPublicKey
);
return "-----BEGIN PUBLIC KEY-----\r\n" .
\chunk_split(\base64_encode($rsaPublicKey), 64) .
'-----END PUBLIC KEY-----';
}
/**
* DER-encode the length
*
* DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See
* {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information.
*
* @param int $length
* @return string
*/
private static function encodeLength(int $length): string
{
if ($length <= 0x7F) {
return \chr($length);
}
$temp = \ltrim(\pack('N', $length), \chr(0));
return \pack('Ca*', 0x80 | \strlen($temp), $temp);
}
/**
* Encodes a value into a DER object.
* Also defined in Firebase\JWT\JWT
*
* @param int $type DER tag
* @param string $value the value to encode
* @return string the encoded object
*/
private static function encodeDER(int $type, string $value): string
{
$tag_header = 0;
if ($type === self::ASN1_SEQUENCE) {
$tag_header |= 0x20;
}
// Type
$der = \chr($tag_header | $type);
// Length
$der .= \chr(\strlen($value));
return $der . $value;
}
/**
* Encodes a string into a DER-encoded OID.
*
* @param string $oid the OID string
* @return string the binary DER-encoded OID
*/
private static function encodeOID(string $oid): string
{
$octets = explode('.', $oid);
// Get the first octet
$first = (int) array_shift($octets);
$second = (int) array_shift($octets);
$oid = \chr($first * 40 + $second);
// Iterate over subsequent octets
foreach ($octets as $octet) {
if ($octet == 0) {
$oid .= \chr(0x00);
continue;
}
$bin = '';
while ($octet) {
$bin .= \chr(0x80 | ($octet & 0x7f));
$octet >>= 7;
}
$bin[0] = $bin[0] & \chr(0x7f);
// Convert to big endian if necessary
if (pack('V', 65534) == pack('L', 65534)) {
$oid .= strrev($bin);
} else {
$oid .= $bin;
}
}
return $oid;
}
}
@@ -0,0 +1,627 @@
<?php
namespace Firebase\JWT;
use ArrayAccess;
use DateTime;
use DomainException;
use Exception;
use InvalidArgumentException;
use OpenSSLAsymmetricKey;
use OpenSSLCertificate;
use stdClass;
use UnexpectedValueException;
/**
* JSON Web Token implementation, based on this spec:
* https://tools.ietf.org/html/rfc7519
*
* PHP version 5
*
* @category Authentication
* @package Authentication_JWT
* @author Neuman Vong <neuman@twilio.com>
* @author Anant Narayanan <anant@php.net>
* @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD
* @link https://github.com/firebase/php-jwt
*/
class JWT
{
private const ASN1_INTEGER = 0x02;
private const ASN1_SEQUENCE = 0x10;
private const ASN1_BIT_STRING = 0x03;
/**
* When checking nbf, iat or expiration times,
* we want to provide some extra leeway time to
* account for clock skew.
*
* @var int
*/
public static $leeway = 0;
/**
* Allow the current timestamp to be specified.
* Useful for fixing a value within unit testing.
* Will default to PHP time() value if null.
*
* @var ?int
*/
public static $timestamp = null;
/**
* @var array<string, string[]>
*/
public static $supported_algs = [
'ES384' => ['openssl', 'SHA384'],
'ES256' => ['openssl', 'SHA256'],
'HS256' => ['hash_hmac', 'SHA256'],
'HS384' => ['hash_hmac', 'SHA384'],
'HS512' => ['hash_hmac', 'SHA512'],
'RS256' => ['openssl', 'SHA256'],
'RS384' => ['openssl', 'SHA384'],
'RS512' => ['openssl', 'SHA512'],
'EdDSA' => ['sodium_crypto', 'EdDSA'],
];
/**
* Decodes a JWT string into a PHP object.
*
* @param string $jwt The JWT
* @param Key|array<string,Key> $keyOrKeyArray The Key or associative array of key IDs (kid) to Key objects.
* If the algorithm used is asymmetric, this is the public key
* Each Key object contains an algorithm and matching key.
* Supported algorithms are 'ES384','ES256', 'HS256', 'HS384',
* 'HS512', 'RS256', 'RS384', and 'RS512'
*
* @return stdClass The JWT's payload as a PHP object
*
* @throws InvalidArgumentException Provided key/key-array was empty or malformed
* @throws DomainException Provided JWT is malformed
* @throws UnexpectedValueException Provided JWT was invalid
* @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed
* @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf'
* @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat'
* @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim
*
* @uses jsonDecode
* @uses urlsafeB64Decode
*/
public static function decode(
string $jwt,
$keyOrKeyArray
): stdClass {
// Validate JWT
$timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp;
if (empty($keyOrKeyArray)) {
throw new InvalidArgumentException('Key may not be empty');
}
$tks = \explode('.', $jwt);
if (\count($tks) !== 3) {
throw new UnexpectedValueException('Wrong number of segments');
}
list($headb64, $bodyb64, $cryptob64) = $tks;
$headerRaw = static::urlsafeB64Decode($headb64);
if (null === ($header = static::jsonDecode($headerRaw))) {
throw new UnexpectedValueException('Invalid header encoding');
}
$payloadRaw = static::urlsafeB64Decode($bodyb64);
if (null === ($payload = static::jsonDecode($payloadRaw))) {
throw new UnexpectedValueException('Invalid claims encoding');
}
if (\is_array($payload)) {
// prevent PHP Fatal Error in edge-cases when payload is empty array
$payload = (object) $payload;
}
if (!$payload instanceof stdClass) {
throw new UnexpectedValueException('Payload must be a JSON object');
}
$sig = static::urlsafeB64Decode($cryptob64);
if (empty($header->alg)) {
throw new UnexpectedValueException('Empty algorithm');
}
if (empty(static::$supported_algs[$header->alg])) {
throw new UnexpectedValueException('Algorithm not supported');
}
$key = self::getKey($keyOrKeyArray, property_exists($header, 'kid') ? $header->kid : null);
// Check the algorithm
if (!self::constantTimeEquals($key->getAlgorithm(), $header->alg)) {
// See issue #351
throw new UnexpectedValueException('Incorrect key for this algorithm');
}
if ($header->alg === 'ES256' || $header->alg === 'ES384') {
// OpenSSL expects an ASN.1 DER sequence for ES256/ES384 signatures
$sig = self::signatureToDER($sig);
}
if (!self::verify("{$headb64}.{$bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) {
throw new SignatureInvalidException('Signature verification failed');
}
// Check the nbf if it is defined. This is the time that the
// token can actually be used. If it's not yet that time, abort.
if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) {
throw new BeforeValidException(
'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->nbf)
);
}
// Check that this token has been created before 'now'. This prevents
// using tokens that have been created for later use (and haven't
// correctly used the nbf claim).
if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) {
throw new BeforeValidException(
'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->iat)
);
}
// Check if this token has expired.
if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) {
throw new ExpiredException('Expired token');
}
return $payload;
}
/**
* Converts and signs a PHP array into a JWT string.
*
* @param array<mixed> $payload PHP array
* @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key.
* @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384',
* 'HS512', 'RS256', 'RS384', and 'RS512'
* @param string $keyId
* @param array<string, string> $head An array with header elements to attach
*
* @return string A signed JWT
*
* @uses jsonEncode
* @uses urlsafeB64Encode
*/
public static function encode(
array $payload,
$key,
string $alg,
string $keyId = null,
array $head = null
): string {
$header = ['typ' => 'JWT', 'alg' => $alg];
if ($keyId !== null) {
$header['kid'] = $keyId;
}
if (isset($head) && \is_array($head)) {
$header = \array_merge($head, $header);
}
$segments = [];
$segments[] = static::urlsafeB64Encode((string) static::jsonEncode($header));
$segments[] = static::urlsafeB64Encode((string) static::jsonEncode($payload));
$signing_input = \implode('.', $segments);
$signature = static::sign($signing_input, $key, $alg);
$segments[] = static::urlsafeB64Encode($signature);
return \implode('.', $segments);
}
/**
* Sign a string with a given key and algorithm.
*
* @param string $msg The message to sign
* @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key.
* @param string $alg Supported algorithms are 'ES384','ES256', 'HS256', 'HS384',
* 'HS512', 'RS256', 'RS384', and 'RS512'
*
* @return string An encrypted message
*
* @throws DomainException Unsupported algorithm or bad key was specified
*/
public static function sign(
string $msg,
$key,
string $alg
): string {
if (empty(static::$supported_algs[$alg])) {
throw new DomainException('Algorithm not supported');
}
list($function, $algorithm) = static::$supported_algs[$alg];
switch ($function) {
case 'hash_hmac':
if (!\is_string($key)) {
throw new InvalidArgumentException('key must be a string when using hmac');
}
return \hash_hmac($algorithm, $msg, $key, true);
case 'openssl':
$signature = '';
$success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line
if (!$success) {
throw new DomainException('OpenSSL unable to sign data');
}
if ($alg === 'ES256') {
$signature = self::signatureFromDER($signature, 256);
} elseif ($alg === 'ES384') {
$signature = self::signatureFromDER($signature, 384);
}
return $signature;
case 'sodium_crypto':
if (!\function_exists('sodium_crypto_sign_detached')) {
throw new DomainException('libsodium is not available');
}
if (!\is_string($key)) {
throw new InvalidArgumentException('key must be a string when using EdDSA');
}
try {
// The last non-empty line is used as the key.
$lines = array_filter(explode("\n", $key));
$key = base64_decode((string) end($lines));
return sodium_crypto_sign_detached($msg, $key);
} catch (Exception $e) {
throw new DomainException($e->getMessage(), 0, $e);
}
}
throw new DomainException('Algorithm not supported');
}
/**
* Verify a signature with the message, key and method. Not all methods
* are symmetric, so we must have a separate verify and sign method.
*
* @param string $msg The original message (header and body)
* @param string $signature The original signature
* @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey
* @param string $alg The algorithm
*
* @return bool
*
* @throws DomainException Invalid Algorithm, bad key, or OpenSSL failure
*/
private static function verify(
string $msg,
string $signature,
$keyMaterial,
string $alg
): bool {
if (empty(static::$supported_algs[$alg])) {
throw new DomainException('Algorithm not supported');
}
list($function, $algorithm) = static::$supported_algs[$alg];
switch ($function) {
case 'openssl':
$success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); // @phpstan-ignore-line
if ($success === 1) {
return true;
}
if ($success === 0) {
return false;
}
// returns 1 on success, 0 on failure, -1 on error.
throw new DomainException(
'OpenSSL error: ' . \openssl_error_string()
);
case 'sodium_crypto':
if (!\function_exists('sodium_crypto_sign_verify_detached')) {
throw new DomainException('libsodium is not available');
}
if (!\is_string($keyMaterial)) {
throw new InvalidArgumentException('key must be a string when using EdDSA');
}
try {
// The last non-empty line is used as the key.
$lines = array_filter(explode("\n", $keyMaterial));
$key = base64_decode((string) end($lines));
return sodium_crypto_sign_verify_detached($signature, $msg, $key);
} catch (Exception $e) {
throw new DomainException($e->getMessage(), 0, $e);
}
case 'hash_hmac':
default:
if (!\is_string($keyMaterial)) {
throw new InvalidArgumentException('key must be a string when using hmac');
}
$hash = \hash_hmac($algorithm, $msg, $keyMaterial, true);
return self::constantTimeEquals($hash, $signature);
}
}
/**
* Decode a JSON string into a PHP object.
*
* @param string $input JSON string
*
* @return mixed The decoded JSON string
*
* @throws DomainException Provided string was invalid JSON
*/
public static function jsonDecode(string $input)
{
$obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING);
if ($errno = \json_last_error()) {
self::handleJsonError($errno);
} elseif ($obj === null && $input !== 'null') {
throw new DomainException('Null result with non-null input');
}
return $obj;
}
/**
* Encode a PHP array into a JSON string.
*
* @param array<mixed> $input A PHP array
*
* @return string JSON representation of the PHP array
*
* @throws DomainException Provided object could not be encoded to valid JSON
*/
public static function jsonEncode(array $input): string
{
if (PHP_VERSION_ID >= 50400) {
$json = \json_encode($input, \JSON_UNESCAPED_SLASHES);
} else {
// PHP 5.3 only
$json = \json_encode($input);
}
if ($errno = \json_last_error()) {
self::handleJsonError($errno);
} elseif ($json === 'null' && $input !== null) {
throw new DomainException('Null result with non-null input');
}
if ($json === false) {
throw new DomainException('Provided object could not be encoded to valid JSON');
}
return $json;
}
/**
* Decode a string with URL-safe Base64.
*
* @param string $input A Base64 encoded string
*
* @return string A decoded string
*
* @throws InvalidArgumentException invalid base64 characters
*/
public static function urlsafeB64Decode(string $input): string
{
$remainder = \strlen($input) % 4;
if ($remainder) {
$padlen = 4 - $remainder;
$input .= \str_repeat('=', $padlen);
}
return \base64_decode(\strtr($input, '-_', '+/'));
}
/**
* Encode a string with URL-safe Base64.
*
* @param string $input The string you want encoded
*
* @return string The base64 encode of what you passed in
*/
public static function urlsafeB64Encode(string $input): string
{
return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_'));
}
/**
* Determine if an algorithm has been provided for each Key
*
* @param Key|ArrayAccess<string,Key>|array<string,Key> $keyOrKeyArray
* @param string|null $kid
*
* @throws UnexpectedValueException
*
* @return Key
*/
private static function getKey(
$keyOrKeyArray,
?string $kid
): Key {
if ($keyOrKeyArray instanceof Key) {
return $keyOrKeyArray;
}
if ($keyOrKeyArray instanceof CachedKeySet) {
// Skip "isset" check, as this will automatically refresh if not set
return $keyOrKeyArray[$kid];
}
if (empty($kid)) {
throw new UnexpectedValueException('"kid" empty, unable to lookup correct key');
}
if (!isset($keyOrKeyArray[$kid])) {
throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key');
}
return $keyOrKeyArray[$kid];
}
/**
* @param string $left The string of known length to compare against
* @param string $right The user-supplied string
* @return bool
*/
public static function constantTimeEquals(string $left, string $right): bool
{
if (\function_exists('hash_equals')) {
return \hash_equals($left, $right);
}
$len = \min(self::safeStrlen($left), self::safeStrlen($right));
$status = 0;
for ($i = 0; $i < $len; $i++) {
$status |= (\ord($left[$i]) ^ \ord($right[$i]));
}
$status |= (self::safeStrlen($left) ^ self::safeStrlen($right));
return ($status === 0);
}
/**
* Helper method to create a JSON error.
*
* @param int $errno An error number from json_last_error()
*
* @throws DomainException
*
* @return void
*/
private static function handleJsonError(int $errno): void
{
$messages = [
JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON',
JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON',
JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3
];
throw new DomainException(
isset($messages[$errno])
? $messages[$errno]
: 'Unknown JSON error: ' . $errno
);
}
/**
* Get the number of bytes in cryptographic strings.
*
* @param string $str
*
* @return int
*/
private static function safeStrlen(string $str): int
{
if (\function_exists('mb_strlen')) {
return \mb_strlen($str, '8bit');
}
return \strlen($str);
}
/**
* Convert an ECDSA signature to an ASN.1 DER sequence
*
* @param string $sig The ECDSA signature to convert
* @return string The encoded DER object
*/
private static function signatureToDER(string $sig): string
{
// Separate the signature into r-value and s-value
$length = max(1, (int) (\strlen($sig) / 2));
list($r, $s) = \str_split($sig, $length);
// Trim leading zeros
$r = \ltrim($r, "\x00");
$s = \ltrim($s, "\x00");
// Convert r-value and s-value from unsigned big-endian integers to
// signed two's complement
if (\ord($r[0]) > 0x7f) {
$r = "\x00" . $r;
}
if (\ord($s[0]) > 0x7f) {
$s = "\x00" . $s;
}
return self::encodeDER(
self::ASN1_SEQUENCE,
self::encodeDER(self::ASN1_INTEGER, $r) .
self::encodeDER(self::ASN1_INTEGER, $s)
);
}
/**
* Encodes a value into a DER object.
*
* @param int $type DER tag
* @param string $value the value to encode
*
* @return string the encoded object
*/
private static function encodeDER(int $type, string $value): string
{
$tag_header = 0;
if ($type === self::ASN1_SEQUENCE) {
$tag_header |= 0x20;
}
// Type
$der = \chr($tag_header | $type);
// Length
$der .= \chr(\strlen($value));
return $der . $value;
}
/**
* Encodes signature from a DER object.
*
* @param string $der binary signature in DER format
* @param int $keySize the number of bits in the key
*
* @return string the signature
*/
private static function signatureFromDER(string $der, int $keySize): string
{
// OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE
list($offset, $_) = self::readDER($der);
list($offset, $r) = self::readDER($der, $offset);
list($offset, $s) = self::readDER($der, $offset);
// Convert r-value and s-value from signed two's compliment to unsigned
// big-endian integers
$r = \ltrim($r, "\x00");
$s = \ltrim($s, "\x00");
// Pad out r and s so that they are $keySize bits long
$r = \str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT);
$s = \str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT);
return $r . $s;
}
/**
* Reads binary DER-encoded data and decodes into a single object
*
* @param string $der the binary data in DER format
* @param int $offset the offset of the data stream containing the object
* to decode
*
* @return array{int, string|null} the new offset and the decoded object
*/
private static function readDER(string $der, int $offset = 0): array
{
$pos = $offset;
$size = \strlen($der);
$constructed = (\ord($der[$pos]) >> 5) & 0x01;
$type = \ord($der[$pos++]) & 0x1f;
// Length
$len = \ord($der[$pos++]);
if ($len & 0x80) {
$n = $len & 0x1f;
$len = 0;
while ($n-- && $pos < $size) {
$len = ($len << 8) | \ord($der[$pos++]);
}
}
// Value
if ($type === self::ASN1_BIT_STRING) {
$pos++; // Skip the first contents octet (padding indicator)
$data = \substr($der, $pos, $len - 1);
$pos += $len - 1;
} elseif (!$constructed) {
$data = \substr($der, $pos, $len);
$pos += $len;
} else {
$data = null;
}
return [$pos, $data];
}
}
@@ -0,0 +1,64 @@
<?php
namespace Firebase\JWT;
use InvalidArgumentException;
use OpenSSLAsymmetricKey;
use OpenSSLCertificate;
use TypeError;
class Key
{
/** @var string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate */
private $keyMaterial;
/** @var string */
private $algorithm;
/**
* @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial
* @param string $algorithm
*/
public function __construct(
$keyMaterial,
string $algorithm
) {
if (
!\is_string($keyMaterial)
&& !$keyMaterial instanceof OpenSSLAsymmetricKey
&& !$keyMaterial instanceof OpenSSLCertificate
&& !\is_resource($keyMaterial)
) {
throw new TypeError('Key material must be a string, resource, or OpenSSLAsymmetricKey');
}
if (empty($keyMaterial)) {
throw new InvalidArgumentException('Key material must not be empty');
}
if (empty($algorithm)) {
throw new InvalidArgumentException('Algorithm must not be empty');
}
// TODO: Remove in PHP 8.0 in favor of class constructor property promotion
$this->keyMaterial = $keyMaterial;
$this->algorithm = $algorithm;
}
/**
* Return the algorithm valid for this key
*
* @return string
*/
public function getAlgorithm(): string
{
return $this->algorithm;
}
/**
* @return string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate
*/
public function getKeyMaterial()
{
return $this->keyMaterial;
}
}
@@ -0,0 +1,7 @@
<?php
namespace Firebase\JWT;
class SignatureInvalidException extends \UnexpectedValueException
{
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,617 @@
<?php
/** @noinspection MultiAssignmentUsageInspection */
namespace app\extensions\simplex;
use SimpleXMLElement;
class SimpleXLSXEx
{
public static $IC = [
0 => '000000',
1 => 'FFFFFF',
2 => 'FF0000',
3 => '00FF00',
4 => '0000FF',
5 => 'FFFF00',
6 => 'FF00FF',
7 => '00FFFF',
8 => '000000',
9 => 'FFFFFF',
10 => 'FF0000',
11 => '00FF00',
12 => '0000FF',
13 => 'FFFF00',
14 => 'FF00FF',
15 => '00FFFF',
16 => '800000',
17 => '008000',
18 => '000080',
19 => '808000',
20 => '800080',
21 => '008080',
22 => 'C0C0C0',
23 => '808080',
24 => '9999FF',
25 => '993366',
26 => 'FFFFCC',
27 => 'CCFFFF',
28 => '660066',
29 => 'FF8080',
30 => '0066CC',
31 => 'CCCCFF',
32 => '000080',
33 => 'FF00FF',
34 => 'FFFF00',
35 => '00FFFF',
36 => '800080',
37 => '800000',
38 => '008080',
39 => '0000FF',
40 => '00CCFF',
41 => 'CCFFFF',
42 => 'CCFFCC',
43 => 'FFFF99',
44 => '99CCFF',
45 => 'FF99CC',
46 => 'CC99FF',
47 => 'FFCC99',
48 => '3366FF',
49 => '33CCCC',
50 => '99CC00',
51 => 'FFCC00',
52 => 'FF9900',
53 => 'FF6600',
54 => '666699',
55 => '969696',
56 => '003366',
57 => '339966',
58 => '003300',
59 => '333300',
60 => '993300',
61 => '993366',
62 => '333399',
63 => '333333',
64 => '000000', // System Foreground
65 => 'FFFFFF', // System Background'
];
public static $CH = [
0 => 'ANSI_CHARSET',
1 => 'DEFAULT_CHARSET',
2 => 'SYMBOL_CHARSET',
77 => 'MAC_CHARSET',
128 => 'SHIFTJIS_CHARSET',
//129 => 'HANGEUL_CHARSET',
129 => 'HANGUL_CHARSET',
130 => 'JOHAB_CHARSET',
134 => 'GB2312_CHARSET',
136 => 'CHINESEBIG5_CHARSET',
161 => 'GREEK_CHARSET',
162 => 'TURKISH_CHARSET',
163 => 'VIETNAMESE_CHARSET',
177 => 'HEBREW_CHARSET',
178 => 'ARABIC_CHARSET',
186 => 'BALTIC_CHARSET',
204 => 'RUSSIAN_CHARSET',
222 => 'THAI_CHARSET',
238 => 'EASTEUROPE_CHARSET',
255 => 'OEM_CHARSET'
];
public $xlsx;
public $themeColors;
public $fonts;
public $fills;
public $borders;
public $cellStyles;
public $css;
public function __construct(SimpleXLSX $xlsx)
{
$this->xlsx = $xlsx;
$this->readThemeColors();
$this->readFonts();
$this->readFills();
$this->readBorders();
$this->readXfs();
}
public function readThemeColors()
{
$this->themeColors = [];
if (isset($this->xlsx->theme->themeElements->clrScheme)) {
$colors12 = ['lt1', 'dk1', 'lt2', 'dk2','accent1','accent2','accent3','accent4','accent5',
'accent6','hlink','folHlink'];
foreach ($colors12 as $c) {
$v = $this->xlsx->theme->themeElements->clrScheme->{$c};
if (isset($v->sysClr)) {
$this->themeColors[] = (string) $v->sysClr['lastClr'];
} elseif (isset($v->srgbClr)) {
$this->themeColors[] = (string) $v->srgbClr['val'];
} else {
$this->themeColors[] = null;
}
}
}
}
public function readFonts()
{
// fonts
$this->fonts = [];
if (isset($this->xlsx->styles->fonts->font)) {
foreach ($this->xlsx->styles->fonts->font as $v) {
$u = '';
if (isset($v->u)) {
$u = isset($v->u['val']) ? (string) $v->u['val'] : 'single';
}
$f = [
'b' => isset($v->b) && ($v->b['val'] === null || $v->b['val']),
'i' => isset($v->i) && ($v->i['val'] === null || $v->i['val']),
'u' => $u,
'strike' => isset($v->strike) && ($v->strike['val'] === null || $v->strike['val']),
'sz' => isset($v->sz['val']) ? (int) $v->sz['val'] : 11,
'color' => $this->getColorValue($v->color),
'name' => isset($v->name['val']) ? (string) $v->name['val'] : 'Calibri',
'family' => isset($v->family['val']) ? (int) $v->family['val'] : 2,
'charset' => isset($v->charset['val']) ? (int) $v->charset['val'] : 1,
'scheme' => isset($v->scheme['val']) ? (string) $v->scheme['val'] : 'minor'
];
$this->fonts[] = $f;
}
}
}
public function readFills()
{
// fills
$this->fills = [];
if (isset($this->xlsx->styles->fills->fill)) {
foreach ($this->xlsx->styles->fills->fill as $v) {
if (isset($v->patternFill)) {
$this->fills[] = [
'pattern' => isset($v->patternFill['patternType']) ? (string) $v->patternFill['patternType'] : 'none',
'fgcolor' => $this->getColorValue($v->patternFill->fgColor),
'bgcolor' => $this->getColorValue($v->patternFill->bgColor)
];
}
}
}
}
public function readBorders()
{
$this->borders = [];
if (isset($this->xlsx->styles->borders->border)) {
foreach ($this->xlsx->styles->borders->border as $v) {
$this->borders[] = [
'left' => [
'style' => (string) $v->left['style'],
'color' => $this->getColorValue($v->left->color)
],
'right' => [
'style' => (string) $v->right['style'],
'color' => $this->getColorValue($v->right->color)
],
'top' => [
'style' => (string) $v->top['style'],
'color' => $this->getColorValue($v->top->color)
],
'bottom' => [
'style' => (string) $v->bottom['style'],
'color' => $this->getColorValue($v->bottom->color)
],
'diagonal' => [
'style' => (string) $v->diagonal['style'],
'color' => $this->getColorValue($v->diagonal->color)
],
'horizontal' => [
'style' => (string) $v->horizontal['style'],
'color' => $this->getColorValue($v->horizontal->color)
],
'vertical' => [
'style' => (string) $v->vertical['style'],
'color' => $this->getColorValue($v->vertical->color)
],
'diagonalUp' => (bool) $v['diagonalUp'],
'diagonalDown' => (bool) $v['diagonalDown'],
'outline' => !(isset($v['outline'])) || $v['outline']
];
}
}
}
public function readXfs()
{
// cellStyles
$this->cellStyles = [];
if (isset($this->xlsx->styles->cellStyleXfs->xf)) {
foreach ($this->xlsx->styles->cellStyleXfs->xf as $v) {
$x = [];
foreach ($v->attributes() as $k1 => $v1) {
$x[ $k1 ] = (int) $v1;
}
if (isset($v->alignment)) {
foreach ($v->alignment->attributes() as $k1 => $v1) {
$x['alignment'][$k1] = (string) $v1;
}
}
$this->cellStyles[] = $x;
}
}
// css
$this->css = [];
// xf
if (isset($this->xlsx->styles->cellXfs->xf)) {
$k = 0;
foreach ($this->xlsx->styles->cellXfs->xf as $v) {
$cf = &$this->xlsx->cellFormats[$k];
// alignment
$alignment = [];
if (isset($v->alignment)) {
foreach ($v->alignment->attributes() as $k1 => $v1) {
$alignment[$k1] = (string)$v1;
}
}
if (isset($cf['xfId'], $this->cellStyles[ $cf['xfId'] ])) {
$s = $this->cellStyles[$cf['xfId']];
if (!empty($s['applyNumberFormat'])) {
$cf['numFmtId'] = $s['numFmtId'];
}
if (!empty($s['applyFont'])) {
$cf['fontId'] = $s['fontId'];
}
if (!empty($s['applyBorder'])) {
$cf['borderId'] = $s['borderId'];
}
if (!empty($s['applyAlignment'])) {
$alignment = $s['alignment'];
}
}
$cf['alignment'] = $alignment;
$align = null;
if (isset($alignment['horizontal'])) {
$align = $alignment['horizontal'];
if ($align === 'centerContinuous') {
$align = 'center';
}
if ($align === 'distributed') {
$align = 'justify';
}
if ($align === 'general') {
$align = null;
}
}
$cf['align'] = $align;
$valign = null;
if (isset($alignment['vertical'])) {
$valign = $alignment['vertical'];
if ($valign === 'center' || $valign === 'distributed' || $valign === 'justify') {
$valign = 'middle';
}
}
$cf['valign'] = $valign;
// font
if (isset($cf['fontId'])) {
$cf['font'] = $this->fonts[$cf['fontId']]['name'];
$cf['color'] = $this->fonts[$cf['fontId']]['color'];
$cf['f-size'] = $this->fonts[$cf['fontId']]['sz'];
$cf['f-b'] = $this->fonts[$cf['fontId']]['b'];
$cf['f-i'] = $this->fonts[$cf['fontId']]['i'];
$cf['f-u'] = $this->fonts[$cf['fontId']]['u'];
$cf['f-strike'] = $this->fonts[$cf['fontId']]['strike'];
} else {
$cf['font'] = null;
$cf['color'] = null;
$cf['f-size'] = null;
$cf['f-b'] = null;
$cf['f-i'] = null;
$cf['f-u'] = null;
$cf['f-strike'] = null;
}
// fill
$cf['bgcolor'] = isset($cf['fillId']) ? $this->fills[ $cf['fillId'] ]['fgcolor'] : null;
// borders
if (isset($cf['borderId'], $this->borders[ $cf['borderId'] ])) {
$border = $this->borders[ $cf['borderId'] ];
$borders = ['left', 'right', 'top', 'bottom'];
foreach ($borders as $b) {
$cf['b-' . $b.'-color'] = $border[$b]['color'];
if ($border[$b]['style'] === '' || $border[$b]['style'] === 'none') {
$cf['b-' . $b.'-style'] = '';
$cf['b-' . $b.'-color'] = '';
} elseif ($border[$b]['style'] === 'dashDot'
|| $border[$b]['style'] === 'dashDotDot'
|| $border[$b]['style'] === 'dashed'
) {
$cf['b-' . $b.'-style'] = 'dashed';
} else {
$cf['b-' . $b.'-style'] = 'solid';
}
}
} else {
$cf['b-top-style'] = null;
$cf['b-right-style'] = null;
$cf['b-bottom-style'] = null;
$cf['b-left-style'] = null;
}
$css = '';
if ($cf['color']) {
$css .= 'color: #'.$cf['color'].';';
}
if ($cf['font']) {
$css .= 'font-family: '.$cf['font'].';';
}
if ($cf['f-size']) {
// $css .= 'font-size: '.($cf['f-size'] * 0.352806).'mm;';
$css .= 'font-size: '.(round($cf['f-size'] * 1.3333) + 2).'px;';
}
if ($cf['f-b']) {
$css .= 'font-weight: bold;';
}
if ($cf['f-i']) {
$css .= 'font-style: italic;';
}
if ($cf['f-u']) {
$css .= 'text-decoration: underline;';
}
if ($cf['f-strike']) {
$css .= 'text-decoration: line-through;';
}
if ($cf['bgcolor']) {
$css .= 'background-color: #' . $cf['bgcolor'] . ';';
}
if ($cf['align']) {
$css .= 'text-align: '.$cf['align'].';';
}
if ($cf['valign']) {
$css .= 'vertical-align: '.$cf['valign'].';';
}
if ($cf['b-top-style']) {
$css .= 'border-top-style: '.$cf['b-top-style'].';';
$css .= 'border-top-color: #'.$cf['b-top-color'].';';
$css .= 'border-top-width: thin;';
}
if ($cf['b-right-style']) {
$css .= 'border-right-style: '.$cf['b-right-style'].';';
$css .= 'border-right-color: #'.$cf['b-right-color'].';';
$css .= 'border-right-width: thin;';
}
if ($cf['b-bottom-style']) {
$css .= 'border-bottom-style: '.$cf['b-bottom-style'].';';
$css .= 'border-bottom-color: #'.$cf['b-bottom-color'].';';
$css .= 'border-bottom-width: thin;';
}
if ($cf['b-left-style']) {
$css .= 'border-left-style: '.$cf['b-left-style'].';';
$css .= 'border-left-color: #'.$cf['b-left-color'].';';
$css .= 'border-left-width: thin;';
}
$this->css[$k] = $css;
$k++;
}
}
}
public function readRowsEx($worksheetIndex = 0, $limit = 0)
{
if (($ws = $this->xlsx->worksheet($worksheetIndex)) === false) {
return;
}
$dim = $this->xlsx->dimension($worksheetIndex);
$numCols = $dim[0];
$numRows = $dim[1];
/*$emptyRow = array();
for ($i = 0; $i < $numCols; $i++) {
$emptyRow[] = null;
}
*/
$cols = [];
for ($i = 0; $i < $numCols; $i++) {
$cols[] = ['s' => 0, 'hidden' => false, 'width' => 0];
}
// $hiddenCols = [];
/* @var SimpleXMLElement $ws */
if (isset($ws->cols)) {
foreach ($ws->cols->col as $col) {
$min = (int)$col['min'];
$max = (int)$col['max'];
if (($max-$min) > 100) {
$max = $min;
}
for ($i = $min; $i <= $max; $i++) {
$cols[$i-1] = [
's' => (int)$col['style'],
'hidden' => (bool)$col['hidden'],
'width' => $col['customWidth'] ? (float) $col['width'] : 0
];
}
}
}
$curR = 0;
$_limit = $limit;
foreach ($ws->sheetData->row as $row) {
$curC = 0;
$r_idx = (int)$row['r'];
$r_style = ['s' => 0, 'hidden' => (bool)$row['hidden'], 'height' => 0];
if ($row['customFormat']) {
$r_style['s'] = (int)$row['s'];
}
if ($row['customHeight']) {
$r_style['height'] = (int)$row['ht'];
}
$cells = [];
for ($i = 0; $i < $numCols; $i++) {
$cells[] = null;
}
foreach ($row->c as $c) {
$r = (string)$c['r'];
$t = (string)$c['t'];
$s = (int)$c['s'];
$idx = $this->xlsx->getIndex($r);
$x = $idx[0];
$y = $idx[1];
if ($x > -1) {
$curC = $x;
if ($curC >= $numCols) {
$numCols = $curC + 1;
}
while ($curR < $y) {
$emptyRow = [];
for ($i = 0; $i < $numCols; $i++) {
$emptyRow[] = $this->valueEx($cols[$i], $i, $curR);
}
yield $emptyRow;
$curR++;
$_limit--;
if ($_limit === 0) {
return;
}
}
}
$data = [
'type' => $t,
'name' => $r,
'value' => $this->xlsx->value($c),
'href' => $this->xlsx->href($worksheetIndex, $c),
'f' => (string)$c->f,
'r' => $r_idx,
's' => ($s > 0) ? $s : $cols[$curC]['s'],
'hidden' => $r_style['hidden'] || $cols[$curC]['hidden'],
'width' => $cols[$curC]['width'],
'height' => $r_style['height']
];
$cells[$curC] = $this->valueEx($data, $curC, $curR);
$curC++;
}
// check empty cells
for ($i = 0; $i < $numCols; $i++) {
if ($cells[$i] === null) {
if ($r_style['s'] > 0) {
$data = $r_style;
} else {
$data = $cols[$i];
}
$data['width'] = $cols[$i]['width'];
$data['height'] = $r_style['height'];
$cells[$i] = $this->valueEx($data, $i, $curR);
}
}
yield $cells;
$curR++;
$_limit--;
if ($_limit === 0) {
break;
}
}
while ($curR < $numRows) {
$emptyRow = [];
for ($i = 0; $i < $numCols; $i++) {
$data = $cols[$i];
$emptyRow[] = $this->valueEx($data, $i, $curR);
}
yield $emptyRow;
$curR++;
$_limit--;
if ($_limit === 0) {
return;
}
}
}
protected function valueEx($data, $x = null, $y = null)
{
$r = [
'type' => '',
'name' => '',
'value' => '',
'href' => '',
'f' => '',
'format' => '',
's' => 0,
'css' => '',
'r' => '',
'hidden' => false,
'width' => 0,
'height' => 0
];
foreach ($data as $k => $v) {
if (isset($r[$k])) {
$r[$k] = $v;
}
}
$st = &$this->xlsx->cellFormats[$r['s']];
$r['format'] = $st['format'];
$r['css'] = &$this->css[ $r['s'] ];
if ($r['value'] !== '' && !$st['align'] && !in_array($r['type'], ['s','str','inlineStr','e'], true)) {
$r['css'] .= 'text-align: right;';
}
if (!$r['name']) {
$c = '';
for ($k = $x; $k >= 0; $k = (int)($k / 26) - 1) {
$c = chr($k % 26 + 65) . $c;
}
$r['name'] = $c . ($y + 1);
$r['r'] = $y+1;
}
return $r;
}
public function getColorValue(SimpleXMLElement $a = null, $default = '')
{
if ($a === null) {
return $default;
}
$c = $default; // auto
if ($a['rgb'] !== null) {
$c = substr((string) $a['rgb'], 2); // FFCCBBAA -> CCBBAA
} elseif ($a['indexed'] !== null && isset(static::$IC[ (int) $a['indexed'] ])) {
$c = static::$IC[ (int) $a['indexed'] ];
} elseif ($a['theme'] !== null && isset($this->themeColors[ (int) $a['theme'] ])) {
$c = $this->themeColors[ (int) $a['theme'] ];
}
if ($a['tint'] !== null) {
list($r,$g,$b) = array_map('hexdec', str_split($c, 2));
$tint = (float) $a['tint'];
if ($tint > 0) {
$r += (255 - $r) * $tint;
$g += (255 - $g) * $tint;
$b += (255 - $b) * $tint;
} else {
$r += $r * $tint;
$g += $g * $tint;
$b += $b * $tint;
}
$c = strtoupper(
str_pad(dechex((int) $r), 2, '0', 0) .
str_pad(dechex((int) $g), 2, '0', 0) .
str_pad(dechex((int) $b), 2, '0', 0)
);
}
return $c;
}
}