Initial import
This commit is contained in:
@@ -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>&C&"Times New Roman,Regular"&12&A</oddHeader>');
|
||||
$sheet->file_writer->write( '<oddFooter>&C&"Times New Roman,Regular"&12Page &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>&C&"Times New Roman,Regular"&12&A</oddHeader>');
|
||||
$sheet->file_writer->write( '<oddFooter>&C&"Times New Roman,Regular"&12Page &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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user