From 363fcc099024afad05a3ce57565b79fff78afaae Mon Sep 17 00:00:00 2001 From: jorix Date: Fri, 21 Dec 2018 19:43:50 +0100 Subject: [PATCH 1/5] Most modern version of spreadsheet-reader --- php/external/spreadsheet-reader/.gitignore | 3 + php/external/spreadsheet-reader/CHANGELOG.md | 80 ++++ php/external/spreadsheet-reader/LICENSE.md | 28 ++ php/external/spreadsheet-reader/README.md | 59 ++- .../spreadsheet-reader/SpreadsheetReader.php | 140 +++++-- .../SpreadsheetReader_CSV.php | 39 +- .../SpreadsheetReader_ODS.php | 93 ++++- .../SpreadsheetReader_XLS.php | 105 ++++- .../SpreadsheetReader_XLSX.php | 376 ++++++++++++++---- php/external/spreadsheet-reader/composer.json | 27 ++ php/external/spreadsheet-reader/test.php | 100 +++++ 11 files changed, 907 insertions(+), 143 deletions(-) create mode 100644 php/external/spreadsheet-reader/.gitignore create mode 100644 php/external/spreadsheet-reader/CHANGELOG.md create mode 100644 php/external/spreadsheet-reader/LICENSE.md create mode 100644 php/external/spreadsheet-reader/composer.json create mode 100644 php/external/spreadsheet-reader/test.php diff --git a/php/external/spreadsheet-reader/.gitignore b/php/external/spreadsheet-reader/.gitignore new file mode 100644 index 00000000..9b670cb8 --- /dev/null +++ b/php/external/spreadsheet-reader/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +test +materials \ No newline at end of file diff --git a/php/external/spreadsheet-reader/CHANGELOG.md b/php/external/spreadsheet-reader/CHANGELOG.md new file mode 100644 index 00000000..30a09a96 --- /dev/null +++ b/php/external/spreadsheet-reader/CHANGELOG.md @@ -0,0 +1,80 @@ +### v.0.5.11 2015-04-30 + +- Added a special case for cells formatted as text in XLSX. Previously leading zeros would get truncated if a text cell contained only numbers. + +### v.0.5.10 2015-04-18 + +- Implemented SeekableIterator. Thanks to [paales](https://github.com/paales) for suggestion ([Issue #54](https://github.com/nuovo/spreadsheet-reader/issues/54) and [Pull request #55](https://github.com/nuovo/spreadsheet-reader/pull/55)). +- Fixed a bug in CSV and ODS reading where reading position 0 multiple times in a row would result in internal pointer being advanced and reading the next line. (E.g. reading row #0 three times would result in rows #0, #1, and #2.). This could have happened on multiple calls to `current()` while in #0 position, or calls to `seek(0)` and `current()`. + +### v.0.5.9 2015-04-18 + +- [Pull request #85](https://github.com/nuovo/spreadsheet-reader/pull/85): Fixed an index check. (Thanks to [pa-m](https://github.com/pa-m)). + +### v.0.5.8 2015-01-31 + +- [Issue #50](https://github.com/nuovo/spreadsheet-reader/issues/50): Fixed an XLSX rewind issue. (Thanks to [osuwariboy](https://github.com/osuwariboy)) +- [Issue #52](https://github.com/nuovo/spreadsheet-reader/issues/52), [#53](https://github.com/nuovo/spreadsheet-reader/issues/53): Apache POI compatibility for XLSX. (Thanks to [dimapashkov](https://github.com/dimapashkov)) +- [Issue #61](https://github.com/nuovo/spreadsheet-reader/issues/61): Autoload fix in the main class. (Thanks to [i-bash](https://github.com/i-bash)) +- [Issue #60](https://github.com/nuovo/spreadsheet-reader/issues/60), [#69](https://github.com/nuovo/spreadsheet-reader/issues/69), [#72](https://github.com/nuovo/spreadsheet-reader/issues/72): Fixed an issue where XLSX ChangeSheet may not work. (Thanks to [jtresponse](https://github.com/jtresponse), [osuwariboy](https://github.com/osuwariboy)) +- [Issue #70](https://github.com/nuovo/spreadsheet-reader/issues/70): Added a check for constructor parameter correctness. + + +### v.0.5.7 2013-10-29 + +- Attempt to replicate Excel's "General" format in XLSX files that is applied to otherwise unformatted cells. +Currently only decimal number values are converted to PHP's floats. + +### v.0.5.6 2013-09-04 + +- Fix for formulas being returned along with values in XLSX files. (Thanks to [marktag](https://github.com/marktag)) + +### v.0.5.5 2013-08-23 + +- Fix for macro sheets appearing when parsing XLS files. (Thanks to [osuwariboy](https://github.com/osuwariboy)) + +### v.0.5.4 2013-08-22 + +- Fix for a PHP warning that occurs with completely empty sheets in XLS files. +- XLSM (macro-enabled XLSX) files are recognized and read, too. +- composer.json file is added to the repository (thanks to [matej116](https://github.com/matej116)) + +### v.0.5.3 2013-08-12 + +- Fix for repeated columns in ODS files not reading correctly (thanks to [etfb](https://github.com/etfb)) +- Fix for filename extension reading (Thanks to [osuwariboy](https://github.com/osuwariboy)) + +### v.0.5.2 2013-06-28 + +- A fix for the case when row count wasn't read correctly from the sheet in a XLS file. + +### v.0.5.1 2013-06-27 + +- Fixed file type choice when using mime-types (previously there were problems with +XLSX and ODS mime-types) (Thanks to [incratec](https://github.com/incratec)) + +- Fixed an error in XLSX iterator where `current()` would advance the iterator forward +with each call. (Thanks to [osuwariboy](https://github.com/osuwariboy)) + +### v.0.5.0 2013-06-17 + +- Multiple sheet reading is now supported: + - The `Sheets()` method lets you retrieve a list of all sheets present in the file. + - `ChangeSheet($Index)` method changes the sheet in the reader to the one specified. + +- Previously temporary files that were extracted, were deleted after the SpreadsheetReader +was destroyed but the empty directories remained. Now those are cleaned up as well. + +### v.0.4.3 2013-06-14 + +- Bugfix for shared string caching in XLSX files. When the shared string count was larger +than the caching limit, instead of them being read from file, empty strings were returned. + +### v.0.4.2 2013-06-02 + +- XLS file reading relies on the external Spreadsheet_Excel_Reader class which, by default, +reads additional information about cells like fonts, styles, etc. Now that is disabled +to save some memory since the style data is unnecessary anyway. +(Thanks to [ChALkeR](https://github.com/ChALkeR) for the tip.) + +Martins Pilsetnieks \ No newline at end of file diff --git a/php/external/spreadsheet-reader/LICENSE.md b/php/external/spreadsheet-reader/LICENSE.md new file mode 100644 index 00000000..b30deeec --- /dev/null +++ b/php/external/spreadsheet-reader/LICENSE.md @@ -0,0 +1,28 @@ +### spreadsheet-reader is licensed under the MIT License + +Copyright (C) 2012-2015 Martins Pilsetnieks + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Spreadsheet_Excel_Reader is licensed under the PHP license as noted in the excel_reader2.php file + +> LICENSE: This source file is subject to version 3.0 of the PHP license +> that is available through the world-wide-web at the following URI: +> http://www.php.net/license/3_0.txt. If you did not receive a copy of +> the PHP License and are unable to obtain it through the web, please +> send a note to license@php.net so we can mail you a copy immediately. diff --git a/php/external/spreadsheet-reader/README.md b/php/external/spreadsheet-reader/README.md index 5a6f5136..6b1a4511 100644 --- a/php/external/spreadsheet-reader/README.md +++ b/php/external/spreadsheet-reader/README.md @@ -1,6 +1,10 @@ -Nuovo/Nouveau spreadsheet-reader is a PHP spreadsheet reader with the difference that my only goal for it was efficient data extraction that could handle large (as in really large) files. So far I cannot definitely say that it is CPU, time or IO-efficient but at least it won't run out of memory (except for XLS files). +**spreadsheet-reader** is a PHP spreadsheet reader that differs from others in that the main goal for it was efficient +data extraction that could handle large (as in really large) files. So far it may not definitely be CPU, time +or I/O-efficient but at least it won't run out of memory (except maybe for XLS files). -So far XLSX, ODS and text/CSV file parsing should be memory-efficient. XLS file parsing is done with php-excel-reader from http://code.google.com/p/php-excel-reader/ which, sadly, has memory issues with bigger spreadsheets. +So far XLSX, ODS and text/CSV file parsing should be memory-efficient. XLS file parsing is done with php-excel-reader +from http://code.google.com/p/php-excel-reader/ which, sadly, has memory issues with bigger spreadsheets, as it reads the +data all at once and keeps it all in memory. ### Requirements: * PHP 5.3.0 or newer @@ -8,7 +12,8 @@ So far XLSX, ODS and text/CSV file parsing should be memory-efficient. XLS file ### Usage: -Very simple: +All data is read from the file sequentially, with each row being returned as a numeric array. +This is about the easiest way to read a file: +However, now also multiple sheet reading is supported for file formats where it is possible. (In case of CSV, it is handled as if +it only has one sheet.) + +You can retrieve information about sheets contained in the file by calling the `Sheets()` method which returns an array with +sheet indexes as keys and sheet names as values. Then you can change the sheet that's currently being read by passing that index +to the `ChangeSheet($Index)` method. + +Example: + + Sheets(); + + foreach ($Sheets as $Index => $Name) + { + echo 'Sheet #'.$Index.': '.$Name; + + $Reader -> ChangeSheet($Index); + + foreach ($Reader as $Row) + { + print_r($Row); + } + } + ?> + +If a sheet is changed to the same that is currently open, the position in the file still reverts to the beginning, so as to conform +to the same behavior as when changed to a different sheet. + ### Testing From the command line: php test.php path-to-spreadsheet.xls +In the browser: + + http://path-to-library/test.php?File=/path/to/spreadsheet.xls + +### Notes about library performance +* CSV and text files are read strictly sequentially so performance should be O(n); +* When parsing XLS files, all of the file content is read into memory so large XLS files can lead to "out of memory" errors; +* XLSX files use so called "shared strings" internally to optimize for cases where the same string is repeated multiple times. + Internally XLSX is an XML text that is parsed sequentially to extract data from it, however, in some cases these shared strings are a problem - + sometimes Excel may put all, or nearly all of the strings from the spreadsheet in the shared string file (which is a separate XML text), and not necessarily in the same + order. Worst case scenario is when it is in reverse order - for each string we need to parse the shared string XML from the beginning, if we want to avoid keeping the data in memory. + To that end, the XLSX parser has a cache for shared strings that is used if the total shared string count is not too high. In case you get out of memory errors, you can + try adjusting the *SHARED_STRING_CACHE_LIMIT* constant in SpreadsheetReader_XLSX to a lower one. + ### TODOs: * ODS date formats; -* XLSX XML parsing suffers from an occasional Shliemel the painter moment (sharedStrings.xml) -http://www.nuovo.lv \ No newline at end of file +### Licensing +All of the code in this library is licensed under the MIT license as included in the LICENSE file, however, for now the library +relies on php-excel-reader library for XLS file parsing which is licensed under the PHP license. \ No newline at end of file diff --git a/php/external/spreadsheet-reader/SpreadsheetReader.php b/php/external/spreadsheet-reader/SpreadsheetReader.php index 0ae5cc88..b019f8f9 100644 --- a/php/external/spreadsheet-reader/SpreadsheetReader.php +++ b/php/external/spreadsheet-reader/SpreadsheetReader.php @@ -1,13 +1,11 @@ Type = self::TYPE_CSV; } @@ -82,13 +100,13 @@ public function __construct($Filepath, $OriginalFilename = false, $MimeType = fa break; case 'application/vnd.oasis.opendocument.spreadsheet': case 'application/vnd.oasis.opendocument.spreadsheet-template': - $this -> Mode = self::TYPE_ODS; + $this -> Type = self::TYPE_ODS; break; case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': case 'application/vnd.openxmlformats-officedocument.spreadsheetml.template': case 'application/xlsx': case 'application/xltx': - $this -> Mode = self::TYPE_XLSX; + $this -> Type = self::TYPE_XLSX; break; case 'application/xml': // Excel 2004 xml format uses this @@ -97,28 +115,25 @@ public function __construct($Filepath, $OriginalFilename = false, $MimeType = fa if (!$this -> Type) { - if (substr($OriginalFilename, -5) == '.xlsx' || substr($OriginalFilename, -5) == '.xltx') + switch ($Extension) { - $this -> Type = self::TYPE_XLSX; - $Extension = '.xlsx'; - } - else - { - $Extension = substr($OriginalFilename, -4); - } - - if ($Extension == '.xls' || $Extension == '.xlt') - { - $this -> Type = self::TYPE_XLS; - } - elseif ($Extension == '.ods' || $Extension == '.odt') - { - $this -> Type = self::TYPE_ODS; - } - elseif (!$this -> Type) - { - // If type cannot be determined, try parsing as CSV, it might just be a text file - $this -> Type = self::TYPE_CSV; + case 'xlsx': + case 'xltx': // XLSX template + case 'xlsm': // Macro-enabled XLSX + case 'xltm': // Macro-enabled XLSX template + $this -> Type = self::TYPE_XLSX; + break; + case 'xls': + case 'xlt': + $this -> Type = self::TYPE_XLS; + break; + case 'ods': + case 'odt': + $this -> Type = self::TYPE_ODS; + break; + default: + $this -> Type = self::TYPE_CSV; + break; } } @@ -164,6 +179,31 @@ public function __construct($Filepath, $OriginalFilename = false, $MimeType = fa } } + /** + * Gets information about separate sheets in the given file + * + * @return array Associative array where key is sheet index and value is sheet name + */ + public function Sheets() + { + return $this -> Handle -> Sheets(); + } + + /** + * Changes the current sheet to another from the file. + * Note that changing the sheet will rewind the file to the beginning, even if + * the current sheet index is provided. + * + * @param int Sheet index + * + * @return bool True if sheet could be changed to the specified one, + * false if not (for example, if incorrect index was provided. + */ + public function ChangeSheet($Index) + { + return $this -> Handle -> ChangeSheet($Index); + } + /** * Autoloads the required class for the particular spreadsheet type * @@ -176,7 +216,9 @@ private static function Load($Type) throw new Exception('SpreadsheetReader: Invalid type ('.$Type.')'); } - if (!class_exists('SpreadsheetReader_'.$Type)) + // 2nd parameter is to prevent autoloading for the class. + // If autoload works, the require line is unnecessary, if it doesn't, it ends badly. + if (!class_exists('SpreadsheetReader_'.$Type, false)) { require(dirname(__FILE__).DIRECTORY_SEPARATOR.'SpreadsheetReader_'.$Type.'.php'); } @@ -266,5 +308,41 @@ public function count() } return 0; } + + /** + * Method for SeekableIterator interface. Takes a posiiton and traverses the file to that position + * The value can be retrieved with a `current()` call afterwards. + * + * @param int Position in file + */ + public function seek($Position) + { + if (!$this -> Handle) + { + throw new OutOfBoundsException('SpreadsheetReader: No file opened'); + } + + $CurrentIndex = $this -> Handle -> key(); + + if ($CurrentIndex != $Position) + { + if ($Position < $CurrentIndex || is_null($CurrentIndex) || $Position == 0) + { + $this -> rewind(); + } + + while ($this -> Handle -> valid() && ($Position > $this -> Handle -> key())) + { + $this -> Handle -> next(); + } + + if (!$this -> Handle -> valid()) + { + throw new OutOfBoundsException('SpreadsheetError: Position '.$Position.' not found'); + } + } + + return null; + } } -?> \ No newline at end of file +?> diff --git a/php/external/spreadsheet-reader/SpreadsheetReader_CSV.php b/php/external/spreadsheet-reader/SpreadsheetReader_CSV.php index 8c7e2b94..1cae82ba 100644 --- a/php/external/spreadsheet-reader/SpreadsheetReader_CSV.php +++ b/php/external/spreadsheet-reader/SpreadsheetReader_CSV.php @@ -22,9 +22,11 @@ class SpreadsheetReader_CSV implements Iterator, Countable */ private $Handle = false; + private $Filepath = ''; + private $Index = 0; - private $CurrentRow = array(); + private $CurrentRow = null; /** * @param string Path to file @@ -34,6 +36,8 @@ class SpreadsheetReader_CSV implements Iterator, Countable */ public function __construct($Filepath, array $Options = null) { + $this -> Filepath = $Filepath; + if (!is_readable($Filepath)) { throw new Exception('SpreadsheetReader_CSV: File not readable ('.$Filepath.')'); @@ -119,6 +123,34 @@ public function __construct($Filepath, array $Options = null) } } + /** + * Returns information about sheets in the file. + * Because CSV doesn't have any, it's just a single entry. + * + * @return array Sheet data + */ + public function Sheets() + { + return array(0 => basename($this -> Filepath)); + } + + /** + * Changes sheet to another. Because CSV doesn't have any sheets + * it just rewinds the file so the behaviour is compatible with other + * sheet readers. (If an invalid index is given, it doesn't do anything.) + * + * @param bool Status + */ + public function ChangeSheet($Index) + { + if ($Index == 0) + { + $this -> rewind(); + return true; + } + return false; + } + // !Iterator interface methods /** * Rewind the Iterator to the first element. @@ -127,6 +159,7 @@ public function __construct($Filepath, array $Options = null) public function rewind() { fseek($this -> Handle, $this -> BOMLength); + $this -> CurrentRow = null; $this -> Index = 0; } @@ -138,7 +171,7 @@ public function rewind() */ public function current() { - if ($this -> Index == 0) + if ($this -> Index == 0 && is_null($this -> CurrentRow)) { $this -> next(); $this -> Index--; @@ -152,6 +185,8 @@ public function current() */ public function next() { + $this -> CurrentRow = array(); + // Finding the place the next line starts for UTF-16 encoded files // Line breaks could be 0x0D 0x00 0x0A 0x00 and PHP could split lines on the // first or the second linebreak leaving unnecessary \0 characters that mess up diff --git a/php/external/spreadsheet-reader/SpreadsheetReader_ODS.php b/php/external/spreadsheet-reader/SpreadsheetReader_ODS.php index 8321b78a..b12d9e75 100644 --- a/php/external/spreadsheet-reader/SpreadsheetReader_ODS.php +++ b/php/external/spreadsheet-reader/SpreadsheetReader_ODS.php @@ -20,6 +20,18 @@ class SpreadsheetReader_ODS implements Iterator, Countable */ private $Content = false; + /** + * @var array Data about separate sheets in the file + */ + private $Sheets = false; + + private $CurrentRow = null; + + /** + * @var int Number of the sheet we're currently reading + */ + private $CurrentSheet = 0; + private $Index = 0; private $TableOpen = false; @@ -86,6 +98,60 @@ public function __destruct() } } + /** + * Retrieves an array with information about sheets in the current file + * + * @return array List of sheets (key is sheet index, value is name) + */ + public function Sheets() + { + if ($this -> Sheets === false) + { + $this -> Sheets = array(); + + if ($this -> Valid) + { + $this -> SheetReader = new XMLReader; + $this -> SheetReader -> open($this -> ContentPath); + + while ($this -> SheetReader -> read()) + { + if ($this -> SheetReader -> name == 'table:table') + { + $this -> Sheets[] = $this -> SheetReader -> getAttribute('table:name'); + $this -> SheetReader -> next(); + } + } + + $this -> SheetReader -> close(); + } + } + return $this -> Sheets; + } + + /** + * Changes the current sheet in the file to another + * + * @param int Sheet index + * + * @return bool True if sheet was successfully changed, false otherwise. + */ + public function ChangeSheet($Index) + { + $Index = (int)$Index; + + $Sheets = $this -> Sheets(); + if (isset($Sheets[$Index])) + { + $this -> CurrentSheet = $Index; + $this -> rewind(); + + return true; + } + + return false; + } + // !Iterator interface methods /** * Rewind the Iterator to the first element. @@ -103,6 +169,8 @@ public function rewind() $this -> TableOpen = false; $this -> RowOpen = false; + + $this -> CurrentRow = null; } $this -> Index = 0; @@ -116,7 +184,7 @@ public function rewind() */ public function current() { - if ($this -> Index == 0) + if ($this -> Index == 0 && is_null($this -> CurrentRow)) { $this -> next(); $this -> Index--; @@ -136,12 +204,27 @@ public function next() if (!$this -> TableOpen) { - while ($this -> Valid = $this -> Content -> read()) + $TableCounter = 0; + $SkipRead = false; + + while ($this -> Valid = ($SkipRead || $this -> Content -> read())) { + if ($SkipRead) + { + $SkipRead = false; + } + if ($this -> Content -> name == 'table:table' && $this -> Content -> nodeType != XMLReader::END_ELEMENT) { - $this -> TableOpen = true; - break; + if ($TableCounter == $this -> CurrentSheet) + { + $this -> TableOpen = true; + break; + } + + $TableCounter++; + $this -> Content -> next(); + $SkipRead = true; } } } @@ -197,7 +280,7 @@ public function next() // Checking if larger than one because the value is already added to the row once before if ($RepeatedColumnCount > 1) { - $this -> CurrentRow = array_pad($this -> CurrentRow, count($this -> CurrentRow) + $RepeatedColumnCount - 1, ''); + $this -> CurrentRow = array_pad($this -> CurrentRow, count($this -> CurrentRow) + $RepeatedColumnCount - 1, $LastCellContent); } } } diff --git a/php/external/spreadsheet-reader/SpreadsheetReader_XLS.php b/php/external/spreadsheet-reader/SpreadsheetReader_XLS.php index c699e482..2031f078 100644 --- a/php/external/spreadsheet-reader/SpreadsheetReader_XLS.php +++ b/php/external/spreadsheet-reader/SpreadsheetReader_XLS.php @@ -21,15 +21,31 @@ class SpreadsheetReader_XLS implements Iterator, Countable private $Error = false; + /** + * @var array Sheet information + */ + private $Sheets = false; + private $SheetIndexes = array(); + + /** + * @var int Current sheet index + */ + private $CurrentSheet = 0; + /** * @var array Content of the current row */ private $CurrentRow = array(); /** - * @var int Column count in the file + * @var int Column count in the sheet */ private $ColumnCount = 0; + /** + * @var int Row count in the sheet + */ + private $RowCount = 0; + /** * @var array Template to use for empty rows. Retrieved rows are merged * with this so that empty cells are added, too @@ -52,17 +68,20 @@ public function __construct($Filepath, array $Options = null) throw new Exception('SpreadsheetReader_XLS: Spreadsheet_Excel_Reader class not available'); } - $this -> Handle = new Spreadsheet_Excel_Reader; - $this -> Handle -> setOutputEncoding('UTF-8'); - $this -> Handle -> read($Filepath); + $this -> Handle = new Spreadsheet_Excel_Reader($Filepath, false, 'UTF-8'); + + if (function_exists('mb_convert_encoding')) + { + $this -> Handle -> setUTFEncoder('mb'); + } + if (empty($this -> Handle -> sheets)) { $this -> Error = true; return null; } - $this -> ColumnCount = $this -> Handle -> sheets[0]['numCols']; - $this -> EmptyRow = array_fill(1, $this -> ColumnCount, ''); + $this -> ChangeSheet(0); } public function __destruct() @@ -70,11 +89,73 @@ public function __destruct() unset($this -> Handle); } + /** + * Retrieves an array with information about sheets in the current file + * + * @return array List of sheets (key is sheet index, value is name) + */ + public function Sheets() + { + if ($this -> Sheets === false) + { + $this -> Sheets = array(); + $this -> SheetIndexes = array_keys($this -> Handle -> sheets); + + foreach ($this -> SheetIndexes as $SheetIndex) + { + $this -> Sheets[] = $this -> Handle -> boundsheets[$SheetIndex]['name']; + } + } + return $this -> Sheets; + } + + /** + * Changes the current sheet in the file to another + * + * @param int Sheet index + * + * @return bool True if sheet was successfully changed, false otherwise. + */ + public function ChangeSheet($Index) + { + $Index = (int)$Index; + $Sheets = $this -> Sheets(); + + if (isset($this -> Sheets[$Index])) + { + $this -> rewind(); + $this -> CurrentSheet = $this -> SheetIndexes[$Index]; + + $this -> ColumnCount = $this -> Handle -> sheets[$this -> CurrentSheet]['numCols']; + $this -> RowCount = $this -> Handle -> sheets[$this -> CurrentSheet]['numRows']; + + // For the case when Spreadsheet_Excel_Reader doesn't have the row count set correctly. + if (!$this -> RowCount && count($this -> Handle -> sheets[$this -> CurrentSheet]['cells'])) + { + end($this -> Handle -> sheets[$this -> CurrentSheet]['cells']); + $this -> RowCount = (int)key($this -> Handle -> sheets[$this -> CurrentSheet]['cells']); + } + + if ($this -> ColumnCount) + { + $this -> EmptyRow = array_fill(1, $this -> ColumnCount, ''); + } + else + { + $this -> EmptyRow = array(); + } + } + + return false; + } + public function __get($Name) { - if ($Name == 'Error') + switch ($Name) { - return $this -> Error; + case 'Error': + return $this -> Error; + break; } return null; } @@ -120,9 +201,9 @@ public function next() { return array(); } - elseif (isset($this -> Handle -> sheets[0]['cells'][$this -> Index])) + elseif (isset($this -> Handle -> sheets[$this -> CurrentSheet]['cells'][$this -> Index])) { - $this -> CurrentRow = $this -> Handle -> sheets[0]['cells'][$this -> Index]; + $this -> CurrentRow = $this -> Handle -> sheets[$this -> CurrentSheet]['cells'][$this -> Index]; if (!$this -> CurrentRow) { return array(); @@ -164,7 +245,7 @@ public function valid() { return false; } - return ($this -> Index <= $this -> Handle -> sheets[0]['numRows']); + return ($this -> Index <= $this -> RowCount); } // !Countable interface method @@ -179,7 +260,7 @@ public function count() return 0; } - return $this -> Handle -> sheets[0]['numRows']; + return $this -> RowCount; } } ?> \ No newline at end of file diff --git a/php/external/spreadsheet-reader/SpreadsheetReader_XLSX.php b/php/external/spreadsheet-reader/SpreadsheetReader_XLSX.php index f3b1bd0a..9cf8d125 100644 --- a/php/external/spreadsheet-reader/SpreadsheetReader_XLSX.php +++ b/php/external/spreadsheet-reader/SpreadsheetReader_XLSX.php @@ -13,6 +13,15 @@ class SpreadsheetReader_XLSX implements Iterator, Countable const CELL_TYPE_STR = 'str'; const CELL_TYPE_INLINE_STR = 'inlineStr'; + /** + * Number of shared strings that can be reasonably cached, i.e., that aren't read from file but stored in memory. + * If the total number of shared strings is higher than this, caching is not used. + * If this value is null, shared strings are cached regardless of amount. + * With large shared string caches there are huge performance gains, however a lot of memory could be used which + * can be a problem, especially on shared hosting. + */ + const SHARED_STRING_CACHE_LIMIT = 50000; + private $Options = array( 'TempDir' => '', 'ReturnDateTimeObjects' => false @@ -48,6 +57,16 @@ class SpreadsheetReader_XLSX implements Iterator, Countable * @var XMLReader XML reader object for the shared strings XML file */ private $SharedStrings = false; + /** + * @var array Shared strings cache, if the number of shared strings is low enough + */ + private $SharedStringCache = array(); + + // Workbook data + /** + * @var SimpleXMLElement XML object for the workbook XML file + */ + private $WorkbookXML = false; // Style data /** @@ -60,8 +79,9 @@ class SpreadsheetReader_XLSX implements Iterator, Countable private $Styles = array(); private $TempDir = ''; + private $TempFiles = array(); - private $CurrentRow = array(); + private $CurrentRow = false; // Runtime parsing data /** @@ -69,18 +89,22 @@ class SpreadsheetReader_XLSX implements Iterator, Countable */ private $Index = 0; + /** + * @var array Data about separate sheets in the file + */ + private $Sheets = false; + private $SharedStringCount = 0; private $SharedStringIndex = 0; private $LastSharedStringValue = null; private $RowOpen = false; - private $CellOpen = false; - private $ValueOpen = false; private $SSOpen = false; private $SSForwarded = false; private static $BuiltinFormats = array( + 0 => '', 1 => '0', 2 => '0.00', 3 => '#,##0', @@ -198,60 +222,50 @@ public function __construct($Filepath, array $Options = null) throw new Exception('SpreadsheetReader_XLSX: File not readable ('.$Filepath.') (Error '.$Status.')'); } - // Extracting the XMLs from the XLSX zip file - if ($Zip -> locateName('xl/sharedStrings.xml') !== false) + // Getting the general workbook information + if ($Zip -> locateName('xl/workbook.xml') !== false) { - $this -> SharedStringsPath = $this -> TempDir.'xl/sharedStrings.xml'; + $this -> WorkbookXML = new SimpleXMLElement($Zip -> getFromName('xl/workbook.xml')); } - // 10 tries to check for worksheets should be enough - $WorksheetIndex = 0; - for ($i = 0; $i < 10; $i++) + // Extracting the XMLs from the XLSX zip file + if ($Zip -> locateName('xl/sharedStrings.xml') !== false) { - if ($Zip -> locateName('xl/worksheets/sheet'.$i.'.xml') !== false) + $this -> SharedStringsPath = $this -> TempDir.'xl'.DIRECTORY_SEPARATOR.'sharedStrings.xml'; + $Zip -> extractTo($this -> TempDir, 'xl/sharedStrings.xml'); + $this -> TempFiles[] = $this -> TempDir.'xl'.DIRECTORY_SEPARATOR.'sharedStrings.xml'; + + if (is_readable($this -> SharedStringsPath)) { - $WorksheetIndex = $i; - $this -> WorksheetPath = $this -> TempDir.'xl/worksheets/sheet'.$WorksheetIndex.'.xml'; - break; + $this -> SharedStrings = new XMLReader; + $this -> SharedStrings -> open($this -> SharedStringsPath); + $this -> PrepareSharedStringCache(); } } - if ($this -> WorksheetPath) - { - $Zip -> extractTo($this -> TempDir, 'xl/worksheets/sheet'.$WorksheetIndex.'.xml'); - if ($this -> SharedStringsPath) - { - $Zip -> extractTo($this -> TempDir, 'xl/sharedStrings.xml'); - } + $Sheets = $this -> Sheets(); - if ($Zip -> locateName('xl/styles.xml') !== false) + foreach ($this -> Sheets as $Index => $Name) + { + if ($Zip -> locateName('xl/worksheets/sheet'.$Index.'.xml') !== false) { - $this -> StylesXML = new SimpleXMLElement($Zip -> getFromName('xl/styles.xml')); + $Zip -> extractTo($this -> TempDir, 'xl/worksheets/sheet'.$Index.'.xml'); + $this -> TempFiles[] = $this -> TempDir.'xl'.DIRECTORY_SEPARATOR.'worksheets'.DIRECTORY_SEPARATOR.'sheet'.$Index.'.xml'; } } - $Zip -> close(); - - if ($this -> WorksheetPath && is_readable($this -> WorksheetPath)) - { - $this -> Worksheet = new XMLReader; - $this -> Worksheet -> open($this -> WorksheetPath); - $this -> Valid = true; - } - if ($this -> SharedStringsPath && is_readable($this -> SharedStringsPath)) - { - $this -> SharedStrings = new XMLReader; - $this -> SharedStrings -> open($this -> SharedStringsPath); - } + $this -> ChangeSheet(0); // If worksheet is present and is OK, parse the styles already - if ($this -> Worksheet && $this -> StylesXML) + if ($Zip -> locateName('xl/styles.xml') !== false) { - if ($this -> StylesXML -> cellXfs && $this -> StylesXML -> cellXfs -> xf) + $this -> StylesXML = new SimpleXMLElement($Zip -> getFromName('xl/styles.xml')); + if ($this -> StylesXML && $this -> StylesXML -> cellXfs && $this -> StylesXML -> cellXfs -> xf) { foreach ($this -> StylesXML -> cellXfs -> xf as $Index => $XF) { - if ($XF -> attributes() -> applyNumberFormat) + // Format #0 is a special case - it is the "General" format that is applied regardless of applyNumberFormat + if ($XF -> attributes() -> applyNumberFormat || (0 == (int)$XF -> attributes() -> numFmtId)) { $FormatId = (int)$XF -> attributes() -> numFmtId; // If format ID >= 164, it is a custom format and should be read from styleSheet\numFmts @@ -259,7 +273,8 @@ public function __construct($Filepath, array $Options = null) } else { - $this -> Styles[] = false; + // 0 for "General" format + $this -> Styles[] = 0; } } } @@ -275,6 +290,8 @@ public function __construct($Filepath, array $Options = null) unset($this -> StylesXML); } + $Zip -> close(); + // Setting base date if (!self::$BaseDate) { @@ -304,27 +321,147 @@ public function __construct($Filepath, array $Options = null) */ public function __destruct() { + foreach ($this -> TempFiles as $TempFile) + { + @unlink($TempFile); + } + + // Better safe than sorry - shouldn't try deleting '.' or '/', or '..'. + if (strlen($this -> TempDir) > 2) + { + @rmdir($this -> TempDir.'xl'.DIRECTORY_SEPARATOR.'worksheets'); + @rmdir($this -> TempDir.'xl'); + @rmdir($this -> TempDir); + } + if ($this -> Worksheet && $this -> Worksheet instanceof XMLReader) { $this -> Worksheet -> close(); unset($this -> Worksheet); } - if (file_exists($this -> WorksheetPath)) - { - @unlink($this -> WorksheetPath); - unset($this -> WorksheetPath); - } + unset($this -> WorksheetPath); if ($this -> SharedStrings && $this -> SharedStrings instanceof XMLReader) { $this -> SharedStrings -> close(); unset($this -> SharedStrings); } - if (file_exists($this -> SharedStringsPath)) + unset($this -> SharedStringsPath); + + if (isset($this -> StylesXML)) + { + unset($this -> StylesXML); + } + if ($this -> WorkbookXML) + { + unset($this -> WorkbookXML); + } + } + + /** + * Retrieves an array with information about sheets in the current file + * + * @return array List of sheets (key is sheet index, value is name) + */ + public function Sheets() + { + if ($this -> Sheets === false) + { + $this -> Sheets = array(); + foreach ($this -> WorkbookXML -> sheets -> sheet as $Index => $Sheet) + { + $Attributes = $Sheet -> attributes('r', true); + foreach ($Attributes as $Name => $Value) + { + if ($Name == 'id') + { + $SheetID = (int)str_replace('rId', '', (string)$Value); + break; + } + } + + $this -> Sheets[$SheetID] = (string)$Sheet['name']; + } + ksort($this -> Sheets); + } + return array_values($this -> Sheets); + } + + /** + * Changes the current sheet in the file to another + * + * @param int Sheet index + * + * @return bool True if sheet was successfully changed, false otherwise. + */ + public function ChangeSheet($Index) + { + $RealSheetIndex = false; + $Sheets = $this -> Sheets(); + if (isset($Sheets[$Index])) + { + $SheetIndexes = array_keys($this -> Sheets); + $RealSheetIndex = $SheetIndexes[$Index]; + } + + $TempWorksheetPath = $this -> TempDir.'xl/worksheets/sheet'.$RealSheetIndex.'.xml'; + + if ($RealSheetIndex !== false && is_readable($TempWorksheetPath)) + { + $this -> WorksheetPath = $TempWorksheetPath; + + $this -> rewind(); + return true; + } + + return false; + } + + /** + * Creating shared string cache if the number of shared strings is acceptably low (or there is no limit on the amount + */ + private function PrepareSharedStringCache() + { + while ($this -> SharedStrings -> read()) + { + if ($this -> SharedStrings -> name == 'sst') + { + $this -> SharedStringCount = $this -> SharedStrings -> getAttribute('count'); + break; + } + } + + if (!$this -> SharedStringCount || (self::SHARED_STRING_CACHE_LIMIT < $this -> SharedStringCount && self::SHARED_STRING_CACHE_LIMIT !== null)) + { + return false; + } + + $CacheIndex = 0; + $CacheValue = ''; + while ($this -> SharedStrings -> read()) { - @unlink($this -> SharedStringsPath); - unset($this -> SharedStringsPath); + switch ($this -> SharedStrings -> name) + { + case 'si': + if ($this -> SharedStrings -> nodeType == XMLReader::END_ELEMENT) + { + $this -> SharedStringCache[$CacheIndex] = $CacheValue; + $CacheIndex++; + $CacheValue = ''; + } + break; + case 't': + if ($this -> SharedStrings -> nodeType == XMLReader::END_ELEMENT) + { + continue; + } + $CacheValue .= $this -> SharedStrings -> readString(); + break; + } } + + $this -> SharedStrings -> close(); + return true; } /** @@ -336,6 +473,18 @@ public function __destruct() */ private function GetSharedString($Index) { + if ((self::SHARED_STRING_CACHE_LIMIT === null || self::SHARED_STRING_CACHE_LIMIT > 0) && !empty($this -> SharedStringCache)) + { + if (isset($this -> SharedStringCache[$Index])) + { + return $this -> SharedStringCache[$Index]; + } + else + { + return ''; + } + } + // If the desired index is before the current, rewind the XML if ($this -> SharedStringIndex > $Index) { @@ -467,7 +616,7 @@ private function FormatValue($Value, $Index) return $Value; } - if (!empty($this -> Styles[$Index])) + if (isset($this -> Styles[$Index]) && ($this -> Styles[$Index] !== false)) { $Index = $this -> Styles[$Index]; } @@ -476,6 +625,12 @@ private function FormatValue($Value, $Index) return $Value; } + // A special case for the "General" format + if ($Index == 0) + { + return $this -> GeneralFormat($Value); + } + $Format = array(); if (isset($this -> ParsedFormatCache[$Index])) @@ -635,8 +790,12 @@ private function FormatValue($Value, $Index) // Applying format to value if ($Format) { + if ($Format['Code'] == '@') + { + return (string)$Value; + } // Percentages - if ($Format['Type'] == 'Percentage') + elseif ($Format['Type'] == 'Percentage') { if ($Format['Code'] === '0%') { @@ -732,7 +891,7 @@ private function FormatValue($Value, $Index) // Scaling $Value = $Value / $Format['Scale']; - if ($Format['MinWidth'] && $Format['Decimals']) + if (!empty($Format['MinWidth']) && $Format['Decimals']) { if ($Format['Thousands']) { @@ -760,6 +919,23 @@ private function FormatValue($Value, $Index) return $Value; } + /** + * Attempts to approximate Excel's "general" format. + * + * @param mixed Value + * + * @return mixed Result + */ + public function GeneralFormat($Value) + { + // Numeric format + if (is_numeric($Value)) + { + $Value = (float)$Value; + } + return $Value; + } + // !Iterator interface methods /** * Rewind the Iterator to the first element. @@ -767,17 +943,24 @@ private function FormatValue($Value, $Index) */ public function rewind() { - if ($this -> Index > 0) + // Removed the check whether $this -> Index == 0 otherwise ChangeSheet doesn't work properly + + // If the worksheet was already iterated, XML file is reopened. + // Otherwise it should be at the beginning anyway + if ($this -> Worksheet instanceof XMLReader) { - // If the worksheet was already iterated, XML file is reopened. - // Otherwise it should be at the beginning anyway $this -> Worksheet -> close(); - $this -> Worksheet -> open($this -> WorksheetPath); - $this -> Valid = true; - - $this -> RowOpen = false; } + else + { + $this -> Worksheet = new XMLReader; + } + + $this -> Worksheet -> open($this -> WorksheetPath); + $this -> Valid = true; + $this -> RowOpen = false; + $this -> CurrentRow = false; $this -> Index = 0; } @@ -789,7 +972,7 @@ public function rewind() */ public function current() { - if ($this -> Index == 0) + if ($this -> Index == 0 && $this -> CurrentRow === false) { $this -> next(); $this -> Index--; @@ -805,6 +988,8 @@ public function next() { $this -> Index++; + $this -> CurrentRow = array(); + if (!$this -> RowOpen) { while ($this -> Valid = $this -> Worksheet -> read()) @@ -824,6 +1009,11 @@ public function next() $CurrentRowColumnCount = 0; } + if ($CurrentRowColumnCount > 0) + { + $this -> CurrentRow = array_fill(0, $CurrentRowColumnCount, ''); + } + $this -> RowOpen = true; break; } @@ -833,19 +1023,12 @@ public function next() // Reading the necessary row, if found if ($this -> RowOpen) { - if ($CurrentRowColumnCount > 0) - { - $this -> CurrentRow = array_fill(0, $CurrentRowColumnCount, ''); - } - else - { - $this -> CurrentRow = array(); - } - // These two are needed to control for empty cells $MaxIndex = 0; $CellCount = 0; + $CellHasSharedString = false; + while ($this -> Valid = $this -> Worksheet -> read()) { switch ($this -> Worksheet -> name) @@ -857,6 +1040,7 @@ public function next() $this -> RowOpen = false; break 2; } + break; // Cell case 'c': // If it is a closing tag, skip it @@ -865,42 +1049,58 @@ public function next() continue; } - $this -> CellOpen = !$this -> CellOpen; + $StyleId = (int)$this -> Worksheet -> getAttribute('s'); + + // Get the index of the cell + $Index = $this -> Worksheet -> getAttribute('r'); + $Letter = preg_replace('{[^[:alpha:]]}S', '', $Index); + $Index = self::IndexFromColumnLetter($Letter); - // Determine cell type and get value + // Determine cell type if ($this -> Worksheet -> getAttribute('t') == self::CELL_TYPE_SHARED_STR) { - $SharedStringIndex = $this -> Worksheet -> readString(); - $Value = $this -> GetSharedString($SharedStringIndex); + $CellHasSharedString = true; } else { - $Value = $this -> Worksheet -> readString(); + $CellHasSharedString = false; } - // Format value if necessary - if ($Value !== '') + $this -> CurrentRow[$Index] = ''; + + $CellCount++; + if ($Index > $MaxIndex) { - $StyleId = (int)$this -> Worksheet -> getAttribute('s'); - if ($StyleId && isset($this -> Styles[$StyleId])) - { - $Value = $this -> FormatValue($Value, $StyleId); - } + $MaxIndex = $Index; } - // Get the index of the cell - $Index = $this -> Worksheet -> getAttribute('r'); - $Letter = preg_replace('{[^[:alpha:]]}S', '', $Index); - $Index = self::IndexFromColumnLetter($Letter); + break; + // Cell value + case 'v': + case 'is': + if ($this -> Worksheet -> nodeType == XMLReader::END_ELEMENT) + { + continue; + } - $this -> CurrentRow[$Index] = $Value; + $Value = $this -> Worksheet -> readString(); - $CellCount++; - if ($Index > $MaxIndex) + if ($CellHasSharedString) { - $MaxIndex = $Index; + $Value = $this -> GetSharedString($Value); + } + + // Format value if necessary + if ($Value !== '' && $StyleId && isset($this -> Styles[$StyleId])) + { + $Value = $this -> FormatValue($Value, $StyleId); + } + elseif ($Value) + { + $Value = $this -> GeneralFormat($Value); } + $this -> CurrentRow[$Index] = $Value; break; } } @@ -1008,4 +1208,4 @@ public static function GCD($A, $B) } } } -?> \ No newline at end of file +?> diff --git a/php/external/spreadsheet-reader/composer.json b/php/external/spreadsheet-reader/composer.json new file mode 100644 index 00000000..0e64a783 --- /dev/null +++ b/php/external/spreadsheet-reader/composer.json @@ -0,0 +1,27 @@ +{ + "name": "nuovo/spreadsheet-reader", + "description": "Spreadsheet reader library for Excel, OpenOffice and structured text files", + "keywords": ["spreadsheet", "xls", "xlsx", "ods", "csv", "excel", "openoffice"], + "homepage": "https://github.com/nuovo/spreadsheet-reader", + "version": "0.5.11", + "time": "2015-04-30", + "type": "library", + "license": ["MIT"], + "authors": [ + { + "name": "Martins Pilsetnieks", + "email": "pilsetnieks@gmail.com", + "homepage": "http://www.nuovo.lv/" + } + ], + "support": { + "email": "spreadsheet-reader@nuovo.lv" + }, + "require": { + "php": ">= 5.3.0", + "ext-zip": "*" + }, + "autoload": { + "classmap": ["./"] + } +} diff --git a/php/external/spreadsheet-reader/test.php b/php/external/spreadsheet-reader/test.php new file mode 100644 index 00000000..bdaa3c64 --- /dev/null +++ b/php/external/spreadsheet-reader/test.php @@ -0,0 +1,100 @@ + Sheets(); + + echo '---------------------------------'.PHP_EOL; + echo 'Spreadsheets:'.PHP_EOL; + print_r($Sheets); + echo '---------------------------------'.PHP_EOL; + echo '---------------------------------'.PHP_EOL; + + foreach ($Sheets as $Index => $Name) + { + echo '---------------------------------'.PHP_EOL; + echo '*** Sheet '.$Name.' ***'.PHP_EOL; + echo '---------------------------------'.PHP_EOL; + + $Time = microtime(true); + + $Spreadsheet -> ChangeSheet($Index); + + foreach ($Spreadsheet as $Key => $Row) + { + echo $Key.': '; + if ($Row) + { + print_r($Row); + } + else + { + var_dump($Row); + } + $CurrentMem = memory_get_usage(); + + echo 'Memory: '.($CurrentMem - $BaseMem).' current, '.$CurrentMem.' base'.PHP_EOL; + echo '---------------------------------'.PHP_EOL; + + if ($Key && ($Key % 500 == 0)) + { + echo '---------------------------------'.PHP_EOL; + echo 'Time: '.(microtime(true) - $Time); + echo '---------------------------------'.PHP_EOL; + } + } + + echo PHP_EOL.'---------------------------------'.PHP_EOL; + echo 'Time: '.(microtime(true) - $Time); + echo PHP_EOL; + + echo '---------------------------------'.PHP_EOL; + echo '*** End of sheet '.$Name.' ***'.PHP_EOL; + echo '---------------------------------'.PHP_EOL; + } + + } + catch (Exception $E) + { + echo $E -> getMessage(); + } +?> From 9844cc25baebd0459a40d5a6c322779738eab2d8 Mon Sep 17 00:00:00 2001 From: jorix Date: Sat, 22 Dec 2018 11:35:39 +0100 Subject: [PATCH 2/5] spreadsheet-reader: Fix PHP7 compatibility --- .../spreadsheet-reader/php-excel-reader/excel_reader2.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/php/external/spreadsheet-reader/php-excel-reader/excel_reader2.php b/php/external/spreadsheet-reader/php-excel-reader/excel_reader2.php index 75351b79..6d387624 100644 --- a/php/external/spreadsheet-reader/php-excel-reader/excel_reader2.php +++ b/php/external/spreadsheet-reader/php-excel-reader/excel_reader2.php @@ -94,7 +94,7 @@ function v($data,$pos) { class OLERead { var $data = ''; - function OLERead(){ } + function __construct(){ } function read($sFileName){ // check if file exist and is readable (Darko Miljanovic) @@ -912,7 +912,7 @@ function _format_value($format,$num,$f) { * * Some basic initialisation */ - function Spreadsheet_Excel_Reader($file='',$store_extended_info=true,$outputEncoding='') { + function __construct($file='',$store_extended_info=true,$outputEncoding='') { $this->_ole = new OLERead(); $this->setUTFEncoder('iconv'); if ($outputEncoding != '') { From 41fb60cf1431fd5acfe23a9152d5023a77edeecb Mon Sep 17 00:00:00 2001 From: jorix Date: Sat, 22 Dec 2018 11:38:34 +0100 Subject: [PATCH 3/5] New version of Query-File-Upload/server/php/ NOTE: README.md is from root Query-File-Upload/ --- php/external/jquery-fileupload/.gitignore | 3 - php/external/jquery-fileupload/Dockerfile | 38 + php/external/jquery-fileupload/README.md | 76 +- .../jquery-fileupload/UploadHandler.php | 1229 +++++++++++++---- .../jquery-fileupload/docker-compose.yml | 9 + .../jquery-fileupload/files/.gitignore | 3 + .../jquery-fileupload/files/.htaccess | 26 + php/external/jquery-fileupload/index.php | 15 + 8 files changed, 1097 insertions(+), 302 deletions(-) delete mode 100644 php/external/jquery-fileupload/.gitignore create mode 100644 php/external/jquery-fileupload/Dockerfile create mode 100644 php/external/jquery-fileupload/docker-compose.yml create mode 100644 php/external/jquery-fileupload/files/.gitignore create mode 100644 php/external/jquery-fileupload/files/.htaccess create mode 100644 php/external/jquery-fileupload/index.php diff --git a/php/external/jquery-fileupload/.gitignore b/php/external/jquery-fileupload/.gitignore deleted file mode 100644 index 29a41a8c..00000000 --- a/php/external/jquery-fileupload/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.DS_Store -*.pyc -node_modules diff --git a/php/external/jquery-fileupload/Dockerfile b/php/external/jquery-fileupload/Dockerfile new file mode 100644 index 00000000..8633fee7 --- /dev/null +++ b/php/external/jquery-fileupload/Dockerfile @@ -0,0 +1,38 @@ +FROM php:7-apache + +# Enable the Apache Headers module: +RUN ln -s /etc/apache2/mods-available/headers.load \ + /etc/apache2/mods-enabled/headers.load + +# Enable the Apache Rewrite module: +RUN ln -s /etc/apache2/mods-available/rewrite.load \ + /etc/apache2/mods-enabled/rewrite.load + +# Install GD, Imagick and ImageMagick as image conversion options: +RUN DEBIAN_FRONTEND=noninteractive \ + apt-get update && apt-get install -y --no-install-recommends \ + libpng-dev \ + libjpeg-dev \ + libmagickwand-dev \ + imagemagick \ + && pecl install \ + imagick \ + && docker-php-ext-enable \ + imagick \ + && docker-php-ext-configure \ + gd --with-jpeg-dir=/usr/include/ \ + && docker-php-ext-install \ + gd \ + # Uninstall obsolete packages: + && apt-get autoremove -y \ + libpng-dev \ + libjpeg-dev \ + libmagickwand-dev \ + # Remove obsolete files: + && apt-get clean \ + && rm -rf \ + /tmp/* \ + /usr/share/doc/* \ + /var/cache/* \ + /var/lib/apt/lists/* \ + /var/tmp/* diff --git a/php/external/jquery-fileupload/README.md b/php/external/jquery-fileupload/README.md index d1001a86..d9e16ed1 100644 --- a/php/external/jquery-fileupload/README.md +++ b/php/external/jquery-fileupload/README.md @@ -1,26 +1,31 @@ # jQuery File Upload Plugin +## Description +File Upload widget with multiple file selection, drag&drop support, progress bars, validation and preview images, audio and video for jQuery. +Supports cross-domain, chunked and resumable file uploads and client-side image resizing. Works with any server-side platform (PHP, Python, Ruby on Rails, Java, Node.js, Go etc.) that supports standard HTML form file uploads. + ## Demo -[Demo File Upload](http://blueimp.github.com/jQuery-File-Upload/) +[Demo File Upload](https://blueimp.github.io/jQuery-File-Upload/) + +## ⚠️ Security Notice +Security related releases: + +* [v9.25.1](https://github.com/blueimp/jQuery-File-Upload/releases/tag/v9.25.1) Mitigates some [Potential vulnerabilities with PHP+ImageMagick](VULNERABILITIES.md#potential-vulnerabilities-with-php-imagemagick). +* [v9.24.1](https://github.com/blueimp/jQuery-File-Upload/releases/tag/v9.24.1) Fixes a [Remote code execution vulnerability in the PHP component](VULNERABILITIES.md#remote-code-execution-vulnerability-in-the-php-component). +* v[9.10.1](https://github.com/blueimp/jQuery-File-Upload/releases/tag/9.10.1) Fixes an [Open redirect vulnerability in the GAE components](VULNERABILITIES.md#open-redirect-vulnerability-in-the-gae-components). +* Commit [4175032](https://github.com/blueimp/jQuery-File-Upload/commit/41750323a464e848856dc4c5c940663498beb74a) (*fixed in all tagged releases*) Fixes a [Cross-site scripting vulnerability in the Iframe Transport](VULNERABILITIES.md#cross-site-scripting-vulnerability-in-the-iframe-transport). -## Download -* [Master branch (Bootstrap UI)](https://github.com/blueimp/jQuery-File-Upload/archive/master.zip) -* [jQuery UI branch](https://github.com/blueimp/jQuery-File-Upload/archive/jquery-ui.zip) +Please read the [SECURITY](SECURITY.md) document for instructions on how to securely configure your Webserver for file uploads. ## Setup * [How to setup the plugin on your website](https://github.com/blueimp/jQuery-File-Upload/wiki/Setup) * [How to use only the basic plugin (minimal setup guide).](https://github.com/blueimp/jQuery-File-Upload/wiki/Basic-plugin) -## Support -* **Support requests** and **general discussions** about the File Upload plugin can be posted to the official [support forum](https://groups.google.com/d/forum/jquery-fileupload). -If your question is not directly related to the File Upload plugin, you might have a better chance to get a reply by posting to [Stack Overflow](http://stackoverflow.com/questions/tagged/blueimp+jquery+file-upload). -* **Bugs** and **Feature requests** can be reported using the [issues tracker](https://github.com/blueimp/jQuery-File-Upload/issues). Please read the [issue guidelines](https://github.com/blueimp/jQuery-File-Upload/blob/master/CONTRIBUTING.md) before posting. - ## Features * **Multiple file upload:** Allows to select multiple files at once and upload them simultaneously. * **Drag & Drop support:** - Allows to upload files by dragging them from your desktop or filemanager and dropping them on your browser window. + Allows to upload files by dragging them from your desktop or file manager and dropping them on your browser window. * **Upload progress bar:** Shows a progress bar indicating the upload progress for individual files and for all uploads combined. * **Cancelable uploads:** @@ -31,8 +36,8 @@ If your question is not directly related to the File Upload plugin, you might ha Large files can be uploaded in smaller chunks with browsers supporting the Blob API. * **Client-side image resizing:** Images can be automatically resized on client-side with browsers supporting the required JS APIs. -* **Preview images:** - A preview of image files can be displayed before uploading with browsers supporting the required JS APIs. +* **Preview images, audio and video:** + A preview of image, audio and video files can be displayed before uploading with browsers supporting the required APIs. * **No browser plugins (e.g. Adobe Flash) required:** The implementation is based on open standards like HTML5 and JavaScript and requires no additional browser plugins. * **Graceful fallback for legacy browsers:** @@ -44,31 +49,39 @@ If your question is not directly related to the File Upload plugin, you might ha * **Multiple plugin instances:** Allows to use multiple plugin instances on the same webpage. * **Customizable and extensible:** - Provides an API to set individual options and define callBack methods for various upload events. + Provides an API to set individual options and define callback methods for various upload events. * **Multipart and file contents stream uploads:** Files can be uploaded as standard "multipart/form-data" or file contents stream (HTTP PUT file upload). * **Compatible with any server-side application platform:** Works with any server-side platform (PHP, Python, Ruby on Rails, Java, Node.js, Go etc.) that supports standard HTML form file uploads. ## Requirements -* [jQuery](http://jquery.com/) v. 1.6+ -* [jQuery UI widget factory](http://api.jqueryui.com/jQuery.widget/) v. 1.9+ (included) -* [jQuery Iframe Transport plugin](https://github.com/blueimp/jQuery-File-Upload/blob/master/jquery.iframe-transport.js) (included) -* [JavaScript Templates engine](https://github.com/blueimp/JavaScript-Templates) v. 2.1.0+ (optional) -* [JavaScript Load Image function](https://github.com/blueimp/JavaScript-Load-Image) v. 1.2.1+ (optional) -* [JavaScript Canvas to Blob function](https://github.com/blueimp/JavaScript-Canvas-to-Blob) v. 2.0.3+ (optional) -* [Bootstrap CSS Toolkit](https://github.com/twitter/bootstrap/) v. 2.1+ (optional) -The jQuery UI widget factory is a requirement for the basic File Upload plugin, but very lightweight without any other dependencies. -The jQuery Iframe Transport is required for [browsers without XHR file upload support](https://github.com/blueimp/jQuery-File-Upload/wiki/Browser-support). -The UI version of the File Upload plugin also requires the JavaScript Templates engine as well as the JavaScript Load Image and JavaScript Canvas to Blob functions (for the image previews and resizing functionality). These dependencies are marked as optional, as the basic File Upload plugin can be used without them and the UI version of the plugin can be extended to override these dependencies with alternative solutions. +### Mandatory requirements +* [jQuery](https://jquery.com/) v. 1.6+ +* [jQuery UI widget factory](https://api.jqueryui.com/jQuery.widget/) v. 1.9+ (included): Required for the basic File Upload plugin, but very lightweight without any other dependencies from the jQuery UI suite. +* [jQuery Iframe Transport plugin](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/jquery.iframe-transport.js) (included): Required for [browsers without XHR file upload support](https://github.com/blueimp/jQuery-File-Upload/wiki/Browser-support). -The User Interface is built with Twitter's [Bootstrap](https://github.com/twitter/bootstrap/) Toolkit. This enables a CSS based, responsive layout and fancy transition effects on modern browsers. The demo also includes the [Bootstrap Image Gallery Plugin](https://github.com/blueimp/Bootstrap-Image-Gallery). Both of these components are optional and not required. +### Optional requirements +* [JavaScript Templates engine](https://github.com/blueimp/JavaScript-Templates) v. 2.5.4+: Used to render the selected and uploaded files for the Basic Plus UI and jQuery UI versions. +* [JavaScript Load Image library](https://github.com/blueimp/JavaScript-Load-Image) v. 1.13.0+: Required for the image previews and resizing functionality. +* [JavaScript Canvas to Blob polyfill](https://github.com/blueimp/JavaScript-Canvas-to-Blob) v. 2.1.1+:Required for the image previews and resizing functionality. +* [blueimp Gallery](https://github.com/blueimp/Gallery) v. 2.15.1+: Used to display the uploaded images in a lightbox. +* [Bootstrap](http://getbootstrap.com/) v. 3.2.0+ +* [Glyphicons](http://glyphicons.com/) -The repository also includes the [jQuery XDomainRequest Transport plugin](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/cors/jquery.xdr-transport.js), which enables Cross-domain AJAX requests (GET and POST only) in Microsoft Internet Explorer >= 8. However, the XDomainRequest object doesn't support file uploads and the plugin is only used by the [Demo](http://blueimp.github.com/jQuery-File-Upload/) for Cross-domain requests to delete uploaded files from the demo file upload service. +The user interface of all versions, except the jQuery UI version, is built with [Bootstrap](http://getbootstrap.com/) and icons from [Glyphicons](http://glyphicons.com/). +### Cross-domain requirements [Cross-domain File Uploads](https://github.com/blueimp/jQuery-File-Upload/wiki/Cross-domain-uploads) using the [Iframe Transport plugin](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/jquery.iframe-transport.js) require a redirect back to the origin server to retrieve the upload results. The [example implementation](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/main.js) makes use of [result.html](https://github.com/blueimp/jQuery-File-Upload/blob/master/cors/result.html) as a static redirect page for the origin server. +The repository also includes the [jQuery XDomainRequest Transport plugin](https://github.com/blueimp/jQuery-File-Upload/blob/master/js/cors/jquery.xdr-transport.js), which enables limited cross-domain AJAX requests in Microsoft Internet Explorer 8 and 9 (IE 10 supports cross-domain XHR requests). +The XDomainRequest object allows GET and POST requests only and doesn't support file uploads. It is used on the [Demo](https://blueimp.github.io/jQuery-File-Upload/) to delete uploaded files from the cross-domain demo file upload service. + +### Custom Backends + +You can add support for various backends by adhering to the specification [outlined here](https://github.com/blueimp/jQuery-File-Upload/wiki/JSON-Response). + ## Browsers ### Desktop browsers @@ -85,11 +98,20 @@ The File Upload plugin has been tested with and supports the following mobile br * Apple Safari on iOS 6.0+ * Google Chrome on iOS 6.0+ +* Google Chrome on Android 4.0+ * Default Browser on Android 2.3+ * Opera Mobile 12.0+ ### Supported features -For a detailed overview of the features supported by each browser version please have a look at the [Extended browser support information](https://github.com/blueimp/jQuery-File-Upload/wiki/Browser-support). +For a detailed overview of the features supported by each browser version, please have a look at the [Extended browser support information](https://github.com/blueimp/jQuery-File-Upload/wiki/Browser-support). + +## Contributing +**Bug fixes** and **new features** can be proposed using [pull requests](https://github.com/blueimp/jQuery-File-Upload/pulls). +Please read the [contribution guidelines](https://github.com/blueimp/jQuery-File-Upload/blob/master/CONTRIBUTING.md) before submitting a pull request. + +## Support +This project is actively maintained, but there is no official support channel. +If you have a question that another developer might help you with, please post to [Stack Overflow](http://stackoverflow.com/questions/tagged/blueimp+jquery+file-upload) and tag your question with `blueimp jquery file upload`. ## License -Released under the [MIT license](http://www.opensource.org/licenses/MIT). +Released under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/php/external/jquery-fileupload/UploadHandler.php b/php/external/jquery-fileupload/UploadHandler.php index a4706799..5215e4c0 100644 --- a/php/external/jquery-fileupload/UploadHandler.php +++ b/php/external/jquery-fileupload/UploadHandler.php @@ -1,18 +1,20 @@ 'Image exceeds maximum width', 'min_width' => 'Image requires a minimum width', 'max_height' => 'Image exceeds maximum height', - 'min_height' => 'Image requires a minimum height' + 'min_height' => 'Image requires a minimum height', + 'abort' => 'File upload aborted', + 'image_resize' => 'Failed to resize image' ); - function __construct($options = null, $initialize = true) { + const IMAGETYPE_GIF = 1; + const IMAGETYPE_JPEG = 2; + const IMAGETYPE_PNG = 3; + + protected $image_objects = array(); + + public function __construct($options = null, $initialize = true, $error_messages = null) { + $this->response = array(); $this->options = array( - 'script_url' => $this->get_full_url().'/', - 'upload_dir' => dirname($_SERVER['SCRIPT_FILENAME']).'/files/', + 'script_url' => $this->get_full_url().'/'.$this->basename($this->get_server_var('SCRIPT_NAME')), + 'upload_dir' => dirname($this->get_server_var('SCRIPT_FILENAME')).'/files/', 'upload_url' => $this->get_full_url().'/files/', + 'input_stream' => 'php://input', 'user_dirs' => false, 'mkdir_mode' => 0755, 'param_name' => 'files', @@ -61,18 +73,53 @@ function __construct($options = null, $initialize = true) { 'Content-Range', 'Content-Disposition' ), + // By default, allow redirects to the referer protocol+host: + 'redirect_allow_target' => '/^'.preg_quote( + parse_url($this->get_server_var('HTTP_REFERER'), PHP_URL_SCHEME) + .'://' + .parse_url($this->get_server_var('HTTP_REFERER'), PHP_URL_HOST) + .'/', // Trailing slash to not match subdomains by mistake + '/' // preg_quote delimiter param + ).'/', // Enable to provide file downloads via GET requests to the PHP script: + // 1. Set to 1 to download files via readfile method through PHP + // 2. Set to 2 to send a X-Sendfile header for lighttpd/Apache + // 3. Set to 3 to send a X-Accel-Redirect header for nginx + // If set to 2 or 3, adjust the upload_url option to the base path of + // the redirect parameter, e.g. '/files/'. 'download_via_php' => false, + // Read files in chunks to avoid memory limits when download_via_php + // is enabled, set to 0 to disable chunked reading of files: + 'readfile_chunk_size' => 10 * 1024 * 1024, // 10 MiB // Defines which files can be displayed inline when downloaded: 'inline_file_types' => '/\.(gif|jpe?g|png)$/i', - // Defines which files (based on their names) are accepted for upload: - 'accept_file_types' => '/.+$/i', + // Defines which files (based on their names) are accepted for upload. + // By default, only allows file uploads with image file extensions. + // Only change this setting after making sure that any allowed file + // types cannot be executed by the webserver in the files directory, + // e.g. PHP scripts, nor executed by the browser when downloaded, + // e.g. HTML files with embedded JavaScript code. + // Please also read the SECURITY.md document in this repository. + 'accept_file_types' => '/\.(gif|jpe?g|png)$/i', + // Replaces dots in filenames with the given string. + // Can be disabled by setting it to false or an empty string. + // Note that this is a security feature for servers that support + // multiple file extensions, e.g. the Apache AddHandler Directive: + // https://httpd.apache.org/docs/current/mod/mod_mime.html#addhandler + // Before disabling it, make sure that files uploaded with multiple + // extensions cannot be executed by the webserver, e.g. + // "example.php.png" with embedded PHP code, nor executed by the + // browser when downloaded, e.g. "example.html.gif" with embedded + // JavaScript code. + 'replace_dots_in_filenames' => '-', // The php.ini settings upload_max_filesize and post_max_size // take precedence over the following max_file_size setting: 'max_file_size' => null, 'min_file_size' => 1, // The maximum number of files for the upload directory: 'max_number_of_files' => null, + // Reads first file bytes to identify and correct file extensions: + 'correct_image_extensions' => false, // Image resolution restrictions: 'max_width' => null, 'max_height' => null, @@ -80,34 +127,71 @@ function __construct($options = null, $initialize = true) { 'min_height' => 1, // Set the following option to false to enable resumable uploads: 'discard_aborted_uploads' => true, - // Set to true to rotate images based on EXIF meta data, if available: - 'orient_image' => false, + // Set to 0 to use the GD library to scale and orient images, + // set to 1 to use imagick (if installed, falls back to GD), + // set to 2 to use the ImageMagick convert binary directly: + 'image_library' => 1, + // Uncomment the following to define an array of resource limits + // for imagick: + /* + 'imagick_resource_limits' => array( + imagick::RESOURCETYPE_MAP => 32, + imagick::RESOURCETYPE_MEMORY => 32 + ), + */ + // Command or path for to the ImageMagick convert binary: + 'convert_bin' => 'convert', + // Uncomment the following to add parameters in front of each + // ImageMagick convert call (the limit constraints seem only + // to have an effect if put in front): + /* + 'convert_params' => '-limit memory 32MiB -limit map 32MiB', + */ + // Command or path for to the ImageMagick identify binary: + 'identify_bin' => 'identify', 'image_versions' => array( - // Uncomment the following version to restrict the size of - // uploaded images: - /* + // The empty image version key defines options for the original image. + // Keep in mind: these image manipulations are inherited by all other image versions from this point onwards. + // Also note that the property 'no_cache' is not inherited, since it's not a manipulation. '' => array( - 'max_width' => 1920, - 'max_height' => 1200, - 'jpeg_quality' => 95 + // Automatically rotate images based on EXIF meta data: + 'auto_orient' => true ), - */ - // Uncomment the following to create medium sized images: + // You can add arrays to generate different versions. + // The name of the key is the name of the version (example: 'medium'). + // the array contains the options to apply. /* 'medium' => array( 'max_width' => 800, - 'max_height' => 600, - 'jpeg_quality' => 80 + 'max_height' => 600 ), - */ + */ 'thumbnail' => array( - 'max_width' => 80, - 'max_height' => 80 + // Uncomment the following to use a defined directory for the thumbnails + // instead of a subdirectory based on the version identifier. + // Make sure that this directory doesn't allow execution of files if you + // don't pose any restrictions on the type of uploaded files, e.g. by + // copying the .htaccess file from the files directory for Apache: + //'upload_dir' => dirname($this->get_server_var('SCRIPT_FILENAME')).'/thumb/', + //'upload_url' => $this->get_full_url().'/thumb/', + // Uncomment the following to force the max + // dimensions and e.g. create square thumbnails: + // 'auto_orient' => true, + // 'crop' => true, + // 'jpeg_quality' => 70, + // 'no_cache' => true, (there's a caching option, but this remembers thumbnail sizes from a previous action!) + // 'strip' => true, (this strips EXIF tags, such as geolocation) + 'max_width' => 80, // either specify width, or set to 0. Then width is automatically adjusted - keeping aspect ratio to a specified max_height. + 'max_height' => 80 // either specify height, or set to 0. Then height is automatically adjusted - keeping aspect ratio to a specified max_width. ) - ) + ), + 'print_response' => true ); if ($options) { - $this->options = array_merge($this->options, $options); + $this->options = $options + $this->options; + } + if ($error_messages) { + $this->error_messages = $error_messages + $this->error_messages; } if ($initialize) { $this->initialize(); @@ -115,21 +199,21 @@ function __construct($options = null, $initialize = true) { } protected function initialize() { - switch ($_SERVER['REQUEST_METHOD']) { + switch ($this->get_server_var('REQUEST_METHOD')) { case 'OPTIONS': case 'HEAD': $this->head(); break; case 'GET': - $this->get(); + $this->get($this->options['print_response']); break; case 'PATCH': case 'PUT': case 'POST': - $this->post(); + $this->post($this->options['print_response']); break; case 'DELETE': - $this->delete(); + $this->delete($this->options['print_response']); break; default: $this->header('HTTP/1.1 405 Method Not Allowed'); @@ -137,7 +221,9 @@ protected function initialize() { } protected function get_full_url() { - $https = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'; + $https = !empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'on') === 0 || + !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && + strcasecmp($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') === 0; return ($https ? 'https://' : 'http://'). (!empty($_SERVER['REMOTE_USER']) ? $_SERVER['REMOTE_USER'].'@' : ''). @@ -161,7 +247,15 @@ protected function get_user_path() { protected function get_upload_path($file_name = null, $version = null) { $file_name = $file_name ? $file_name : ''; - $version_path = empty($version) ? '' : $version.'/'; + if (empty($version)) { + $version_path = ''; + } else { + $version_dir = @$this->options['image_versions'][$version]['upload_dir']; + if ($version_dir) { + return $version_dir.$this->get_user_path().$file_name; + } + $version_path = $version.'/'; + } return $this->options['upload_dir'].$this->get_user_path() .$version_path.$file_name; } @@ -170,31 +264,41 @@ protected function get_query_separator($url) { return strpos($url, '?') === false ? '?' : '&'; } - protected function get_download_url($file_name, $version = null) { - if ($this->options['download_via_php']) { + protected function get_download_url($file_name, $version = null, $direct = false) { + if (!$direct && $this->options['download_via_php']) { $url = $this->options['script_url'] .$this->get_query_separator($this->options['script_url']) - .'file='.rawurlencode($file_name); + .$this->get_singular_param_name() + .'='.rawurlencode($file_name); if ($version) { $url .= '&version='.rawurlencode($version); } return $url.'&download=1'; } - $version_path = empty($version) ? '' : rawurlencode($version).'/'; + if (empty($version)) { + $version_path = ''; + } else { + $version_url = @$this->options['image_versions'][$version]['upload_url']; + if ($version_url) { + return $version_url.$this->get_user_path().rawurlencode($file_name); + } + $version_path = rawurlencode($version).'/'; + } return $this->options['upload_url'].$this->get_user_path() .$version_path.rawurlencode($file_name); } - protected function set_file_delete_properties($file) { - $file->delete_url = $this->options['script_url'] + protected function set_additional_file_properties($file) { + $file->deleteUrl = $this->options['script_url'] .$this->get_query_separator($this->options['script_url']) - .'file='.rawurlencode($file->name); - $file->delete_type = $this->options['delete_type']; - if ($file->delete_type !== 'DELETE') { - $file->delete_url .= '&_method=DELETE'; + .$this->get_singular_param_name() + .'='.rawurlencode($file->name); + $file->deleteType = $this->options['delete_type']; + if ($file->deleteType !== 'DELETE') { + $file->deleteUrl .= '&_method=DELETE'; } if ($this->options['access_control_allow_credentials']) { - $file->delete_with_credentials = true; + $file->deleteWithCredentials = true; } } @@ -209,10 +313,13 @@ protected function fix_integer_overflow($size) { protected function get_file_size($file_path, $clear_stat_cache = false) { if ($clear_stat_cache) { - clearstatcache(true, $file_path); + if (version_compare(PHP_VERSION, '5.3.0') >= 0) { + clearstatcache(true, $file_path); + } else { + clearstatcache(); + } } return $this->fix_integer_overflow(filesize($file_path)); - } protected function is_valid_file_object($file_name) { @@ -225,23 +332,23 @@ protected function is_valid_file_object($file_name) { protected function get_file_object($file_name) { if ($this->is_valid_file_object($file_name)) { - $file = new stdClass(); + $file = new \stdClass(); $file->name = $file_name; $file->size = $this->get_file_size( $this->get_upload_path($file_name) ); $file->url = $this->get_download_url($file->name); - foreach($this->options['image_versions'] as $version => $options) { + foreach ($this->options['image_versions'] as $version => $options) { if (!empty($version)) { if (is_file($this->get_upload_path($file_name, $version))) { - $file->{$version.'_url'} = $this->get_download_url( + $file->{$version.'Url'} = $this->get_download_url( $file->name, $version ); } } } - $this->set_file_delete_properties($file); + $this->set_additional_file_properties($file); return $file; } return null; @@ -262,84 +369,16 @@ protected function count_file_objects() { return count($this->get_file_objects('is_valid_file_object')); } - protected function create_scaled_image($file_name, $version, $options) { - $file_path = $this->get_upload_path($file_name); - if (!empty($version)) { - $version_dir = $this->get_upload_path(null, $version); - if (!is_dir($version_dir)) { - mkdir($version_dir, $this->options['mkdir_mode'], true); - } - $new_file_path = $version_dir.'/'.$file_name; - } else { - $new_file_path = $file_path; - } - list($img_width, $img_height) = @getimagesize($file_path); - if (!$img_width || !$img_height) { - return false; - } - $scale = min( - $options['max_width'] / $img_width, - $options['max_height'] / $img_height - ); - if ($scale >= 1) { - if ($file_path !== $new_file_path) { - return copy($file_path, $new_file_path); - } - return true; - } - $new_width = $img_width * $scale; - $new_height = $img_height * $scale; - $new_img = @imagecreatetruecolor($new_width, $new_height); - switch (strtolower(substr(strrchr($file_name, '.'), 1))) { - case 'jpg': - case 'jpeg': - $src_img = @imagecreatefromjpeg($file_path); - $write_image = 'imagejpeg'; - $image_quality = isset($options['jpeg_quality']) ? - $options['jpeg_quality'] : 75; - break; - case 'gif': - @imagecolortransparent($new_img, @imagecolorallocate($new_img, 0, 0, 0)); - $src_img = @imagecreatefromgif($file_path); - $write_image = 'imagegif'; - $image_quality = null; - break; - case 'png': - @imagecolortransparent($new_img, @imagecolorallocate($new_img, 0, 0, 0)); - @imagealphablending($new_img, false); - @imagesavealpha($new_img, true); - $src_img = @imagecreatefrompng($file_path); - $write_image = 'imagepng'; - $image_quality = isset($options['png_quality']) ? - $options['png_quality'] : 9; - break; - default: - $src_img = null; - } - $success = $src_img && @imagecopyresampled( - $new_img, - $src_img, - 0, 0, 0, 0, - $new_width, - $new_height, - $img_width, - $img_height - ) && $write_image($new_img, $new_file_path, $image_quality); - // Free up memory (imagedestroy does not delete files): - @imagedestroy($src_img); - @imagedestroy($new_img); - return $success; - } - protected function get_error_message($error) { - return array_key_exists($error, $this->error_messages) ? + return isset($this->error_messages[$error]) ? $this->error_messages[$error] : $error; } - function get_config_bytes($val) { + public function get_config_bytes($val) { $val = trim($val); $last = strtolower($val[strlen($val)-1]); - switch($last) { + $val = (int)$val; + switch ($last) { case 'g': $val *= 1024; case 'm': @@ -355,7 +394,9 @@ protected function validate($uploaded_file, $file, $error, $index) { $file->error = $this->get_error_message($error); return false; } - $content_length = $this->fix_integer_overflow(intval($_SERVER['CONTENT_LENGTH'])); + $content_length = $this->fix_integer_overflow( + (int)$this->get_server_var('CONTENT_LENGTH') + ); $post_max_size = $this->get_config_bytes(ini_get('post_max_size')); if ($post_max_size && ($content_length > $post_max_size)) { $file->error = $this->get_error_message('post_max_size'); @@ -382,27 +423,48 @@ protected function validate($uploaded_file, $file, $error, $index) { $file->error = $this->get_error_message('min_file_size'); return false; } - if (is_int($this->options['max_number_of_files']) && ( - $this->count_file_objects() >= $this->options['max_number_of_files']) - ) { + if (is_int($this->options['max_number_of_files']) && + ($this->count_file_objects() >= $this->options['max_number_of_files']) && + // Ignore additional chunks of existing files: + !is_file($this->get_upload_path($file->name))) { $file->error = $this->get_error_message('max_number_of_files'); return false; } - list($img_width, $img_height) = @getimagesize($uploaded_file); - if (is_int($img_width)) { - if ($this->options['max_width'] && $img_width > $this->options['max_width']) { + $max_width = @$this->options['max_width']; + $max_height = @$this->options['max_height']; + $min_width = @$this->options['min_width']; + $min_height = @$this->options['min_height']; + if (($max_width || $max_height || $min_width || $min_height) + && $this->is_valid_image_file($uploaded_file)) { + list($img_width, $img_height) = $this->get_image_size($uploaded_file); + // If we are auto rotating the image by default, do the checks on + // the correct orientation + if ( + @$this->options['image_versions']['']['auto_orient'] && + function_exists('exif_read_data') && + ($exif = @exif_read_data($uploaded_file)) && + (((int) @$exif['Orientation']) >= 5) + ) { + $tmp = $img_width; + $img_width = $img_height; + $img_height = $tmp; + unset($tmp); + } + } + if (!empty($img_width)) { + if ($max_width && $img_width > $max_width) { $file->error = $this->get_error_message('max_width'); return false; } - if ($this->options['max_height'] && $img_height > $this->options['max_height']) { + if ($max_height && $img_height > $max_height) { $file->error = $this->get_error_message('max_height'); return false; } - if ($this->options['min_width'] && $img_width < $this->options['min_width']) { + if ($min_width && $img_width < $min_width) { $file->error = $this->get_error_message('min_width'); return false; } - if ($this->options['min_height'] && $img_height < $this->options['min_height']) { + if ($min_height && $img_height < $min_height) { $file->error = $this->get_error_message('min_height'); return false; } @@ -411,7 +473,7 @@ protected function validate($uploaded_file, $file, $error, $index) { } protected function upcount_name_callback($matches) { - $index = isset($matches[1]) ? intval($matches[1]) + 1 : 1; + $index = isset($matches[1]) ? ((int)$matches[1]) + 1 : 1; $ext = isset($matches[2]) ? $matches[2] : ''; return ' ('.$index.')'.$ext; } @@ -425,13 +487,14 @@ protected function upcount_name($name) { ); } - protected function get_unique_filename($name, $type, $index, $content_range) { + protected function get_unique_filename($file_path, $name, $size, $type, $error, + $index, $content_range) { while(is_dir($this->get_upload_path($name))) { $name = $this->upcount_name($name); } // Keep an existing filename if this is part of a chunked upload: - $uploaded_bytes = $this->fix_integer_overflow(intval($content_range[1])); - while(is_file($this->get_upload_path($name))) { + $uploaded_bytes = $this->fix_integer_overflow((int)$content_range[1]); + while (is_file($this->get_upload_path($name))) { if ($uploaded_bytes === $this->get_file_size( $this->get_upload_path($name))) { break; @@ -441,37 +504,153 @@ protected function get_unique_filename($name, $type, $index, $content_range) { return $name; } - protected function trim_file_name($name, $type, $index, $content_range) { + protected function fix_file_extension($file_path, $name, $size, $type, $error, + $index, $content_range) { + // Add missing file extension for known image types: + if (strpos($name, '.') === false && + preg_match('/^image\/(gif|jpe?g|png)/', $type, $matches)) { + $name .= '.'.$matches[1]; + } + if ($this->options['correct_image_extensions']) { + switch ($this->imagetype($file_path)) { + case self::IMAGETYPE_JPEG: + $extensions = array('jpg', 'jpeg'); + break; + case self::IMAGETYPE_PNG: + $extensions = array('png'); + break; + case self::IMAGETYPE_GIF: + $extensions = array('gif'); + break; + } + // Adjust incorrect image file extensions: + if (!empty($extensions)) { + $parts = explode('.', $name); + $extIndex = count($parts) - 1; + $ext = strtolower(@$parts[$extIndex]); + if (!in_array($ext, $extensions)) { + $parts[$extIndex] = $extensions[0]; + $name = implode('.', $parts); + } + } + } + return $name; + } + + protected function trim_file_name($file_path, $name, $size, $type, $error, + $index, $content_range) { // Remove path information and dots around the filename, to prevent uploading // into different directories or replacing hidden system files. // Also remove control characters and spaces (\x00..\x20) around the filename: - $name = trim(basename(stripslashes($name)), ".\x00..\x20"); + $name = trim($this->basename(stripslashes($name)), ".\x00..\x20"); + // Replace dots in filenames to avoid security issues with servers + // that interpret multiple file extensions, e.g. "example.php.png": + $replacement = $this->options['replace_dots_in_filenames']; + if (!empty($replacement)) { + $parts = explode('.', $name); + if (count($parts) > 2) { + $ext = array_pop($parts); + $name = implode($replacement, $parts).'.'.$ext; + } + } // Use a timestamp for empty filenames: if (!$name) { $name = str_replace('.', '-', microtime(true)); } - // Add missing file extension for known image types: - if (strpos($name, '.') === false && - preg_match('/^image\/(gif|jpe?g|png)/', $type, $matches)) { - $name .= '.'.$matches[1]; - } return $name; } - protected function get_file_name($name, $type, $index, $content_range) { + protected function get_file_name($file_path, $name, $size, $type, $error, + $index, $content_range) { + $name = $this->trim_file_name($file_path, $name, $size, $type, $error, + $index, $content_range); return $this->get_unique_filename( - $this->trim_file_name($name, $type, $index, $content_range), + $file_path, + $this->fix_file_extension($file_path, $name, $size, $type, $error, + $index, $content_range), + $size, $type, + $error, $index, $content_range ); } - protected function handle_form_data($file, $index) { - // Handle form data, e.g. $_REQUEST['description'][$index] + protected function get_scaled_image_file_paths($file_name, $version) { + $file_path = $this->get_upload_path($file_name); + if (!empty($version)) { + $version_dir = $this->get_upload_path(null, $version); + if (!is_dir($version_dir)) { + mkdir($version_dir, $this->options['mkdir_mode'], true); + } + $new_file_path = $version_dir.'/'.$file_name; + } else { + $new_file_path = $file_path; + } + return array($file_path, $new_file_path); + } + + protected function gd_get_image_object($file_path, $func, $no_cache = false) { + if (empty($this->image_objects[$file_path]) || $no_cache) { + $this->gd_destroy_image_object($file_path); + $this->image_objects[$file_path] = $func($file_path); + } + return $this->image_objects[$file_path]; } - protected function orient_image($file_path) { + protected function gd_set_image_object($file_path, $image) { + $this->gd_destroy_image_object($file_path); + $this->image_objects[$file_path] = $image; + } + + protected function gd_destroy_image_object($file_path) { + $image = (isset($this->image_objects[$file_path])) ? $this->image_objects[$file_path] : null ; + return $image && imagedestroy($image); + } + + protected function gd_imageflip($image, $mode) { + if (function_exists('imageflip')) { + return imageflip($image, $mode); + } + $new_width = $src_width = imagesx($image); + $new_height = $src_height = imagesy($image); + $new_img = imagecreatetruecolor($new_width, $new_height); + $src_x = 0; + $src_y = 0; + switch ($mode) { + case '1': // flip on the horizontal axis + $src_y = $new_height - 1; + $src_height = -$new_height; + break; + case '2': // flip on the vertical axis + $src_x = $new_width - 1; + $src_width = -$new_width; + break; + case '3': // flip on both axes + $src_y = $new_height - 1; + $src_height = -$new_height; + $src_x = $new_width - 1; + $src_width = -$new_width; + break; + default: + return $image; + } + imagecopyresampled( + $new_img, + $image, + 0, + 0, + $src_x, + $src_y, + $new_width, + $new_height, + $src_width, + $src_height + ); + return $new_img; + } + + protected function gd_orient_image($file_path, $src_img) { if (!function_exists('exif_read_data')) { return false; } @@ -479,35 +658,471 @@ protected function orient_image($file_path) { if ($exif === false) { return false; } - $orientation = intval(@$exif['Orientation']); - if (!in_array($orientation, array(3, 6, 8))) { + $orientation = (int)@$exif['Orientation']; + if ($orientation < 2 || $orientation > 8) { return false; } - $image = @imagecreatefromjpeg($file_path); switch ($orientation) { + case 2: + $new_img = $this->gd_imageflip( + $src_img, + defined('IMG_FLIP_VERTICAL') ? IMG_FLIP_VERTICAL : 2 + ); + break; case 3: - $image = @imagerotate($image, 180, 0); + $new_img = imagerotate($src_img, 180, 0); + break; + case 4: + $new_img = $this->gd_imageflip( + $src_img, + defined('IMG_FLIP_HORIZONTAL') ? IMG_FLIP_HORIZONTAL : 1 + ); + break; + case 5: + $tmp_img = $this->gd_imageflip( + $src_img, + defined('IMG_FLIP_HORIZONTAL') ? IMG_FLIP_HORIZONTAL : 1 + ); + $new_img = imagerotate($tmp_img, 270, 0); + imagedestroy($tmp_img); break; case 6: - $image = @imagerotate($image, 270, 0); + $new_img = imagerotate($src_img, 270, 0); + break; + case 7: + $tmp_img = $this->gd_imageflip( + $src_img, + defined('IMG_FLIP_VERTICAL') ? IMG_FLIP_VERTICAL : 2 + ); + $new_img = imagerotate($tmp_img, 270, 0); + imagedestroy($tmp_img); break; case 8: - $image = @imagerotate($image, 90, 0); + $new_img = imagerotate($src_img, 90, 0); break; default: return false; } - $success = imagejpeg($image, $file_path); - // Free up memory (imagedestroy does not delete files): - @imagedestroy($image); + $this->gd_set_image_object($file_path, $new_img); + return true; + } + + protected function gd_create_scaled_image($file_name, $version, $options) { + if (!function_exists('imagecreatetruecolor')) { + error_log('Function not found: imagecreatetruecolor'); + return false; + } + list($file_path, $new_file_path) = + $this->get_scaled_image_file_paths($file_name, $version); + $type = strtolower(substr(strrchr($file_name, '.'), 1)); + switch ($type) { + case 'jpg': + case 'jpeg': + $src_func = 'imagecreatefromjpeg'; + $write_func = 'imagejpeg'; + $image_quality = isset($options['jpeg_quality']) ? + $options['jpeg_quality'] : 75; + break; + case 'gif': + $src_func = 'imagecreatefromgif'; + $write_func = 'imagegif'; + $image_quality = null; + break; + case 'png': + $src_func = 'imagecreatefrompng'; + $write_func = 'imagepng'; + $image_quality = isset($options['png_quality']) ? + $options['png_quality'] : 9; + break; + default: + return false; + } + $src_img = $this->gd_get_image_object( + $file_path, + $src_func, + !empty($options['no_cache']) + ); + $image_oriented = false; + if (!empty($options['auto_orient']) && $this->gd_orient_image( + $file_path, + $src_img + )) { + $image_oriented = true; + $src_img = $this->gd_get_image_object( + $file_path, + $src_func + ); + } + $max_width = $img_width = imagesx($src_img); + $max_height = $img_height = imagesy($src_img); + if (!empty($options['max_width'])) { + $max_width = $options['max_width']; + } + if (!empty($options['max_height'])) { + $max_height = $options['max_height']; + } + $scale = min( + $max_width / $img_width, + $max_height / $img_height + ); + if ($scale >= 1) { + if ($image_oriented) { + return $write_func($src_img, $new_file_path, $image_quality); + } + if ($file_path !== $new_file_path) { + return copy($file_path, $new_file_path); + } + return true; + } + if (empty($options['crop'])) { + $new_width = $img_width * $scale; + $new_height = $img_height * $scale; + $dst_x = 0; + $dst_y = 0; + $new_img = imagecreatetruecolor($new_width, $new_height); + } else { + if (($img_width / $img_height) >= ($max_width / $max_height)) { + $new_width = $img_width / ($img_height / $max_height); + $new_height = $max_height; + } else { + $new_width = $max_width; + $new_height = $img_height / ($img_width / $max_width); + } + $dst_x = 0 - ($new_width - $max_width) / 2; + $dst_y = 0 - ($new_height - $max_height) / 2; + $new_img = imagecreatetruecolor($max_width, $max_height); + } + // Handle transparency in GIF and PNG images: + switch ($type) { + case 'gif': + case 'png': + imagecolortransparent($new_img, imagecolorallocate($new_img, 0, 0, 0)); + case 'png': + imagealphablending($new_img, false); + imagesavealpha($new_img, true); + break; + } + $success = imagecopyresampled( + $new_img, + $src_img, + $dst_x, + $dst_y, + 0, + 0, + $new_width, + $new_height, + $img_width, + $img_height + ) && $write_func($new_img, $new_file_path, $image_quality); + $this->gd_set_image_object($file_path, $new_img); return $success; } + protected function imagick_get_image_object($file_path, $no_cache = false) { + if (empty($this->image_objects[$file_path]) || $no_cache) { + $this->imagick_destroy_image_object($file_path); + $image = new \Imagick(); + if (!empty($this->options['imagick_resource_limits'])) { + foreach ($this->options['imagick_resource_limits'] as $type => $limit) { + $image->setResourceLimit($type, $limit); + } + } + $image->readImage($file_path); + $this->image_objects[$file_path] = $image; + } + return $this->image_objects[$file_path]; + } + + protected function imagick_set_image_object($file_path, $image) { + $this->imagick_destroy_image_object($file_path); + $this->image_objects[$file_path] = $image; + } + + protected function imagick_destroy_image_object($file_path) { + $image = (isset($this->image_objects[$file_path])) ? $this->image_objects[$file_path] : null ; + return $image && $image->destroy(); + } + + protected function imagick_orient_image($image) { + $orientation = $image->getImageOrientation(); + $background = new \ImagickPixel('none'); + switch ($orientation) { + case \imagick::ORIENTATION_TOPRIGHT: // 2 + $image->flopImage(); // horizontal flop around y-axis + break; + case \imagick::ORIENTATION_BOTTOMRIGHT: // 3 + $image->rotateImage($background, 180); + break; + case \imagick::ORIENTATION_BOTTOMLEFT: // 4 + $image->flipImage(); // vertical flip around x-axis + break; + case \imagick::ORIENTATION_LEFTTOP: // 5 + $image->flopImage(); // horizontal flop around y-axis + $image->rotateImage($background, 270); + break; + case \imagick::ORIENTATION_RIGHTTOP: // 6 + $image->rotateImage($background, 90); + break; + case \imagick::ORIENTATION_RIGHTBOTTOM: // 7 + $image->flipImage(); // vertical flip around x-axis + $image->rotateImage($background, 270); + break; + case \imagick::ORIENTATION_LEFTBOTTOM: // 8 + $image->rotateImage($background, 270); + break; + default: + return false; + } + $image->setImageOrientation(\imagick::ORIENTATION_TOPLEFT); // 1 + return true; + } + + protected function imagick_create_scaled_image($file_name, $version, $options) { + list($file_path, $new_file_path) = + $this->get_scaled_image_file_paths($file_name, $version); + $image = $this->imagick_get_image_object( + $file_path, + !empty($options['crop']) || !empty($options['no_cache']) + ); + if ($image->getImageFormat() === 'GIF') { + // Handle animated GIFs: + $images = $image->coalesceImages(); + foreach ($images as $frame) { + $image = $frame; + $this->imagick_set_image_object($file_name, $image); + break; + } + } + $image_oriented = false; + if (!empty($options['auto_orient'])) { + $image_oriented = $this->imagick_orient_image($image); + } + + $image_resize = false; + $new_width = $max_width = $img_width = $image->getImageWidth(); + $new_height = $max_height = $img_height = $image->getImageHeight(); + + // use isset(). User might be setting max_width = 0 (auto in regular resizing). Value 0 would be considered empty when you use empty() + if (isset($options['max_width'])) { + $image_resize = true; + $new_width = $max_width = $options['max_width']; + } + if (isset($options['max_height'])) { + $image_resize = true; + $new_height = $max_height = $options['max_height']; + } + + $image_strip = (isset($options['strip']) ? $options['strip'] : false); + + if ( !$image_oriented && ($max_width >= $img_width) && ($max_height >= $img_height) && !$image_strip && empty($options["jpeg_quality"]) ) { + if ($file_path !== $new_file_path) { + return copy($file_path, $new_file_path); + } + return true; + } + $crop = (isset($options['crop']) ? $options['crop'] : false); + + if ($crop) { + $x = 0; + $y = 0; + if (($img_width / $img_height) >= ($max_width / $max_height)) { + $new_width = 0; // Enables proportional scaling based on max_height + $x = ($img_width / ($img_height / $max_height) - $max_width) / 2; + } else { + $new_height = 0; // Enables proportional scaling based on max_width + $y = ($img_height / ($img_width / $max_width) - $max_height) / 2; + } + } + $success = $image->resizeImage( + $new_width, + $new_height, + isset($options['filter']) ? $options['filter'] : \imagick::FILTER_LANCZOS, + isset($options['blur']) ? $options['blur'] : 1, + $new_width && $new_height // fit image into constraints if not to be cropped + ); + if ($success && $crop) { + $success = $image->cropImage( + $max_width, + $max_height, + $x, + $y + ); + if ($success) { + $success = $image->setImagePage($max_width, $max_height, 0, 0); + } + } + $type = strtolower(substr(strrchr($file_name, '.'), 1)); + switch ($type) { + case 'jpg': + case 'jpeg': + if (!empty($options['jpeg_quality'])) { + $image->setImageCompression(\imagick::COMPRESSION_JPEG); + $image->setImageCompressionQuality($options['jpeg_quality']); + } + break; + } + if ( $image_strip ) { + $image->stripImage(); + } + return $success && $image->writeImage($new_file_path); + } + + protected function imagemagick_create_scaled_image($file_name, $version, $options) { + list($file_path, $new_file_path) = + $this->get_scaled_image_file_paths($file_name, $version); + $resize = @$options['max_width'] + .(empty($options['max_height']) ? '' : 'X'.$options['max_height']); + if (!$resize && empty($options['auto_orient'])) { + if ($file_path !== $new_file_path) { + return copy($file_path, $new_file_path); + } + return true; + } + $cmd = $this->options['convert_bin']; + if (!empty($this->options['convert_params'])) { + $cmd .= ' '.$this->options['convert_params']; + } + $cmd .= ' '.escapeshellarg($file_path); + if (!empty($options['auto_orient'])) { + $cmd .= ' -auto-orient'; + } + if ($resize) { + // Handle animated GIFs: + $cmd .= ' -coalesce'; + if (empty($options['crop'])) { + $cmd .= ' -resize '.escapeshellarg($resize.'>'); + } else { + $cmd .= ' -resize '.escapeshellarg($resize.'^'); + $cmd .= ' -gravity center'; + $cmd .= ' -crop '.escapeshellarg($resize.'+0+0'); + } + // Make sure the page dimensions are correct (fixes offsets of animated GIFs): + $cmd .= ' +repage'; + } + if (!empty($options['convert_params'])) { + $cmd .= ' '.$options['convert_params']; + } + $cmd .= ' '.escapeshellarg($new_file_path); + exec($cmd, $output, $error); + if ($error) { + error_log(implode('\n', $output)); + return false; + } + return true; + } + + protected function get_image_size($file_path) { + if ($this->options['image_library']) { + if (extension_loaded('imagick')) { + $image = new \Imagick(); + try { + if (@$image->pingImage($file_path)) { + $dimensions = array($image->getImageWidth(), $image->getImageHeight()); + $image->destroy(); + return $dimensions; + } + return false; + } catch (\Exception $e) { + error_log($e->getMessage()); + } + } + if ($this->options['image_library'] === 2) { + $cmd = $this->options['identify_bin']; + $cmd .= ' -ping '.escapeshellarg($file_path); + exec($cmd, $output, $error); + if (!$error && !empty($output)) { + // image.jpg JPEG 1920x1080 1920x1080+0+0 8-bit sRGB 465KB 0.000u 0:00.000 + $infos = preg_split('/\s+/', substr($output[0], strlen($file_path))); + $dimensions = preg_split('/x/', $infos[2]); + return $dimensions; + } + return false; + } + } + if (!function_exists('getimagesize')) { + error_log('Function not found: getimagesize'); + return false; + } + return @getimagesize($file_path); + } + + protected function create_scaled_image($file_name, $version, $options) { + try { + if ($this->options['image_library'] === 2) { + return $this->imagemagick_create_scaled_image($file_name, $version, $options); + } + if ($this->options['image_library'] && extension_loaded('imagick')) { + return $this->imagick_create_scaled_image($file_name, $version, $options); + } + return $this->gd_create_scaled_image($file_name, $version, $options); + } catch (\Exception $e) { + error_log($e->getMessage()); + return false; + } + } + + protected function destroy_image_object($file_path) { + if ($this->options['image_library'] && extension_loaded('imagick')) { + return $this->imagick_destroy_image_object($file_path); + } + } + + protected function imagetype($file_path) { + $fp = fopen($file_path, 'r'); + $data = fread($fp, 4); + fclose($fp); + // GIF: 47 49 46 38 + if ($data === 'GIF8') { + return self::IMAGETYPE_GIF; + } + // JPG: FF D8 FF + if (bin2hex(substr($data, 0, 3)) === 'ffd8ff') { + return self::IMAGETYPE_JPEG; + } + // PNG: 89 50 4E 47 + if (bin2hex(@$data[0]).substr($data, 1, 4) === '89PNG') { + return self::IMAGETYPE_PNG; + } + return false; + } + + protected function is_valid_image_file($file_path) { + if (!preg_match('/\.(gif|jpe?g|png)$/i', $file_path)) { + return false; + } + return !!$this->imagetype($file_path); + } + + protected function handle_image_file($file_path, $file) { + $failed_versions = array(); + foreach ($this->options['image_versions'] as $version => $options) { + if ($this->create_scaled_image($file->name, $version, $options)) { + if (!empty($version)) { + $file->{$version.'Url'} = $this->get_download_url( + $file->name, + $version + ); + } else { + $file->size = $this->get_file_size($file_path, true); + } + } else { + $failed_versions[] = $version ? $version : 'original'; + } + } + if (count($failed_versions)) { + $file->error = $this->get_error_message('image_resize') + .' ('.implode($failed_versions, ', ').')'; + } + // Free memory: + $this->destroy_image_object($file_path); + } + protected function handle_file_upload($uploaded_file, $name, $size, $type, $error, $index = null, $content_range = null) { - $file = new stdClass(); - $file->name = $this->get_file_name($name, $type, $index, $content_range); - $file->size = $this->fix_integer_overflow(intval($size)); + $file = new \stdClass(); + $file->name = $this->get_file_name($uploaded_file, $name, $size, $type, $error, + $index, $content_range); + $file->size = $this->fix_integer_overflow((int)$size); $file->type = $type; if ($this->validate($uploaded_file, $file, $error, $index)) { $this->handle_form_data($file, $index); @@ -533,78 +1148,94 @@ protected function handle_file_upload($uploaded_file, $name, $size, $type, $erro // Non-multipart uploads (PUT method support) file_put_contents( $file_path, - fopen('php://input', 'r'), + fopen($this->options['input_stream'], 'r'), $append_file ? FILE_APPEND : 0 ); } $file_size = $this->get_file_size($file_path, $append_file); if ($file_size === $file->size) { - if ($this->options['orient_image']) { - $this->orient_image($file_path); - } $file->url = $this->get_download_url($file->name); - foreach($this->options['image_versions'] as $version => $options) { - if ($this->create_scaled_image($file->name, $version, $options)) { - if (!empty($version)) { - $file->{$version.'_url'} = $this->get_download_url( - $file->name, - $version - ); - } else { - $file_size = $this->get_file_size($file_path, true); - } - } + if ($this->is_valid_image_file($file_path)) { + $this->handle_image_file($file_path, $file); + } + } else { + $file->size = $file_size; + if (!$content_range && $this->options['discard_aborted_uploads']) { + unlink($file_path); + $file->error = $this->get_error_message('abort'); } - } else if (!$content_range && $this->options['discard_aborted_uploads']) { - unlink($file_path); - $file->error = 'abort'; } - $file->size = $file_size; - $this->set_file_delete_properties($file); + $this->set_additional_file_properties($file); } return $file; } protected function readfile($file_path) { + $file_size = $this->get_file_size($file_path); + $chunk_size = $this->options['readfile_chunk_size']; + if ($chunk_size && $file_size > $chunk_size) { + $handle = fopen($file_path, 'rb'); + while (!feof($handle)) { + echo fread($handle, $chunk_size); + @ob_flush(); + @flush(); + } + fclose($handle); + return $file_size; + } return readfile($file_path); } protected function body($str) { echo $str; } - + protected function header($str) { header($str); } - protected function generate_response($content, $print_response = true) { - if ($print_response) { - $json = json_encode($content); - $redirect = isset($_REQUEST['redirect']) ? - stripslashes($_REQUEST['redirect']) : null; - if ($redirect) { - $this->header('Location: '.sprintf($redirect, rawurlencode($json))); - return; - } - $this->head(); - if (isset($_SERVER['HTTP_CONTENT_RANGE'])) { - $files = isset($content[$this->options['param_name']]) ? - $content[$this->options['param_name']] : null; - if ($files && is_array($files) && is_object($files[0]) && $files[0]->size) { - $this->header('Range: 0-'.($this->fix_integer_overflow(intval($files[0]->size)) - 1)); - } - } - $this->body($json); - } - return $content; + protected function get_upload_data($id) { + return @$_FILES[$id]; + } + + protected function get_post_param($id) { + return @$_POST[$id]; + } + + protected function get_query_param($id) { + return @$_GET[$id]; + } + + protected function get_server_var($id) { + return @$_SERVER[$id]; + } + + protected function handle_form_data($file, $index) { + // Handle form data, e.g. $_POST['description'][$index] } protected function get_version_param() { - return isset($_GET['version']) ? basename(stripslashes($_GET['version'])) : null; + return $this->basename(stripslashes($this->get_query_param('version'))); + } + + protected function get_singular_param_name() { + return substr($this->options['param_name'], 0, -1); } protected function get_file_name_param() { - return isset($_GET['file']) ? basename(stripslashes($_GET['file'])) : null; + $name = $this->get_singular_param_name(); + return $this->basename(stripslashes($this->get_query_param($name))); + } + + protected function get_file_names_params() { + $params = $this->get_query_param($this->options['param_name']); + if (!$params) { + return null; + } + foreach ($params as $key => $value) { + $params[$key] = $this->basename(stripslashes($value)); + } + return $params; } protected function get_file_type($file_path) { @@ -622,36 +1253,50 @@ protected function get_file_type($file_path) { } protected function download() { - if (!$this->options['download_via_php']) { - $this->header('HTTP/1.1 403 Forbidden'); - return; + switch ($this->options['download_via_php']) { + case 1: + $redirect_header = null; + break; + case 2: + $redirect_header = 'X-Sendfile'; + break; + case 3: + $redirect_header = 'X-Accel-Redirect'; + break; + default: + return $this->header('HTTP/1.1 403 Forbidden'); } $file_name = $this->get_file_name_param(); - if ($this->is_valid_file_object($file_name)) { - $file_path = $this->get_upload_path($file_name, $this->get_version_param()); - if (is_file($file_path)) { - if (!preg_match($this->options['inline_file_types'], $file_name)) { - $this->header('Content-Description: File Transfer'); - $this->header('Content-Type: application/octet-stream'); - $this->header('Content-Disposition: attachment; filename="'.$file_name.'"'); - $this->header('Content-Transfer-Encoding: binary'); - } else { - // Prevent Internet Explorer from MIME-sniffing the content-type: - $this->header('X-Content-Type-Options: nosniff'); - $this->header('Content-Type: '.$this->get_file_type($file_path)); - $this->header('Content-Disposition: inline; filename="'.$file_name.'"'); - } - $this->header('Content-Length: '.$this->get_file_size($file_path)); - $this->header('Last-Modified: '.gmdate('D, d M Y H:i:s T', filemtime($file_path))); - $this->readfile($file_path); - } + if (!$this->is_valid_file_object($file_name)) { + return $this->header('HTTP/1.1 404 Not Found'); + } + if ($redirect_header) { + return $this->header( + $redirect_header.': '.$this->get_download_url( + $file_name, + $this->get_version_param(), + true + ) + ); } + $file_path = $this->get_upload_path($file_name, $this->get_version_param()); + // Prevent browsers from MIME-sniffing the content-type: + $this->header('X-Content-Type-Options: nosniff'); + if (!preg_match($this->options['inline_file_types'], $file_name)) { + $this->header('Content-Type: application/octet-stream'); + $this->header('Content-Disposition: attachment; filename="'.$file_name.'"'); + } else { + $this->header('Content-Type: '.$this->get_file_type($file_path)); + $this->header('Content-Disposition: inline; filename="'.$file_name.'"'); + } + $this->header('Content-Length: '.$this->get_file_size($file_path)); + $this->header('Last-Modified: '.gmdate('D, d M Y H:i:s T', filemtime($file_path))); + $this->readfile($file_path); } protected function send_content_type_header() { $this->header('Vary: Accept'); - if (isset($_SERVER['HTTP_ACCEPT']) && - (strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false)) { + if (strpos($this->get_server_var('HTTP_ACCEPT'), 'application/json') !== false) { $this->header('Content-type: application/json'); } else { $this->header('Content-type: text/plain'); @@ -668,6 +1313,34 @@ protected function send_access_control_headers() { .implode(', ', $this->options['access_control_allow_headers'])); } + public function generate_response($content, $print_response = true) { + $this->response = $content; + if ($print_response) { + $json = json_encode($content); + $redirect = stripslashes($this->get_post_param('redirect')); + if ($redirect && preg_match($this->options['redirect_allow_target'], $redirect)) { + $this->header('Location: '.sprintf($redirect, rawurlencode($json))); + return; + } + $this->head(); + if ($this->get_server_var('HTTP_CONTENT_RANGE')) { + $files = isset($content[$this->options['param_name']]) ? + $content[$this->options['param_name']] : null; + if ($files && is_array($files) && is_object($files[0]) && $files[0]->size) { + $this->header('Range: 0-'.( + $this->fix_integer_overflow((int)$files[0]->size) - 1 + )); + } + } + $this->body($json); + } + return $content; + } + + public function get_response () { + return $this->response; + } + public function head() { $this->header('Pragma: no-cache'); $this->header('Cache-Control: no-store, no-cache, must-revalidate'); @@ -681,13 +1354,13 @@ public function head() { } public function get($print_response = true) { - if ($print_response && isset($_GET['download'])) { + if ($print_response && $this->get_query_param('download')) { return $this->download(); } $file_name = $this->get_file_name_param(); if ($file_name) { $response = array( - substr($this->options['param_name'], 0, -1) => $this->get_file_object($file_name) + $this->get_singular_param_name() => $this->get_file_object($file_name) ); } else { $response = array( @@ -698,75 +1371,87 @@ public function get($print_response = true) { } public function post($print_response = true) { - if (isset($_REQUEST['_method']) && $_REQUEST['_method'] === 'DELETE') { + if ($this->get_query_param('_method') === 'DELETE') { return $this->delete($print_response); } - $upload = isset($_FILES[$this->options['param_name']]) ? - $_FILES[$this->options['param_name']] : null; + $upload = $this->get_upload_data($this->options['param_name']); // Parse the Content-Disposition header, if available: - $file_name = isset($_SERVER['HTTP_CONTENT_DISPOSITION']) ? + $content_disposition_header = $this->get_server_var('HTTP_CONTENT_DISPOSITION'); + $file_name = $content_disposition_header ? rawurldecode(preg_replace( '/(^[^"]+")|("$)/', '', - $_SERVER['HTTP_CONTENT_DISPOSITION'] + $content_disposition_header )) : null; // Parse the Content-Range header, which has the following form: // Content-Range: bytes 0-524287/2000000 - $content_range = isset($_SERVER['HTTP_CONTENT_RANGE']) ? - preg_split('/[^0-9]+/', $_SERVER['HTTP_CONTENT_RANGE']) : null; + $content_range_header = $this->get_server_var('HTTP_CONTENT_RANGE'); + $content_range = $content_range_header ? + preg_split('/[^0-9]+/', $content_range_header) : null; $size = $content_range ? $content_range[3] : null; $files = array(); - if ($upload && is_array($upload['tmp_name'])) { - // param_name is an array identifier like "files[]", - // $_FILES is a multi-dimensional array: - foreach ($upload['tmp_name'] as $index => $value) { + if ($upload) { + if (is_array($upload['tmp_name'])) { + // param_name is an array identifier like "files[]", + // $upload is a multi-dimensional array: + foreach ($upload['tmp_name'] as $index => $value) { + $files[] = $this->handle_file_upload( + $upload['tmp_name'][$index], + $file_name ? $file_name : $upload['name'][$index], + $size ? $size : $upload['size'][$index], + $upload['type'][$index], + $upload['error'][$index], + $index, + $content_range + ); + } + } else { + // param_name is a single object identifier like "file", + // $upload is a one-dimensional array: $files[] = $this->handle_file_upload( - $upload['tmp_name'][$index], - $file_name ? $file_name : $upload['name'][$index], - $size ? $size : $upload['size'][$index], - $upload['type'][$index], - $upload['error'][$index], - $index, + isset($upload['tmp_name']) ? $upload['tmp_name'] : null, + $file_name ? $file_name : (isset($upload['name']) ? + $upload['name'] : null), + $size ? $size : (isset($upload['size']) ? + $upload['size'] : $this->get_server_var('CONTENT_LENGTH')), + isset($upload['type']) ? + $upload['type'] : $this->get_server_var('CONTENT_TYPE'), + isset($upload['error']) ? $upload['error'] : null, + null, $content_range ); } - } else { - // param_name is a single object identifier like "file", - // $_FILES is a one-dimensional array: - $files[] = $this->handle_file_upload( - isset($upload['tmp_name']) ? $upload['tmp_name'] : null, - $file_name ? $file_name : (isset($upload['name']) ? - $upload['name'] : null), - $size ? $size : (isset($upload['size']) ? - $upload['size'] : $_SERVER['CONTENT_LENGTH']), - isset($upload['type']) ? - $upload['type'] : $_SERVER['CONTENT_TYPE'], - isset($upload['error']) ? $upload['error'] : null, - null, - $content_range - ); } - return $this->generate_response( - array($this->options['param_name'] => $files), - $print_response - ); + $response = array($this->options['param_name'] => $files); + return $this->generate_response($response, $print_response); } public function delete($print_response = true) { - $file_name = $this->get_file_name_param(); - $file_path = $this->get_upload_path($file_name); - $success = is_file($file_path) && $file_name[0] !== '.' && unlink($file_path); - if ($success) { - foreach($this->options['image_versions'] as $version => $options) { - if (!empty($version)) { - $file = $this->get_upload_path($file_name, $version); - if (is_file($file)) { - unlink($file); + $file_names = $this->get_file_names_params(); + if (empty($file_names)) { + $file_names = array($this->get_file_name_param()); + } + $response = array(); + foreach ($file_names as $file_name) { + $file_path = $this->get_upload_path($file_name); + $success = is_file($file_path) && $file_name[0] !== '.' && unlink($file_path); + if ($success) { + foreach ($this->options['image_versions'] as $version => $options) { + if (!empty($version)) { + $file = $this->get_upload_path($file_name, $version); + if (is_file($file)) { + unlink($file); + } } } } + $response[$file_name] = $success; } - return $this->generate_response(array('success' => $success), $print_response); + return $this->generate_response($response, $print_response); } + protected function basename($filepath, $suffix = null) { + $splited = preg_split('/\//', rtrim ($filepath, '/ ')); + return substr(basename('X'.$splited[count($splited)-1], $suffix), 1); + } } diff --git a/php/external/jquery-fileupload/docker-compose.yml b/php/external/jquery-fileupload/docker-compose.yml new file mode 100644 index 00000000..74eabf7d --- /dev/null +++ b/php/external/jquery-fileupload/docker-compose.yml @@ -0,0 +1,9 @@ +version: '2.3' +services: + apache: + build: ./ + network_mode: bridge + ports: + - "80:80" + volumes: + - "../../:/var/www/html" diff --git a/php/external/jquery-fileupload/files/.gitignore b/php/external/jquery-fileupload/files/.gitignore new file mode 100644 index 00000000..e24a60fa --- /dev/null +++ b/php/external/jquery-fileupload/files/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!.htaccess diff --git a/php/external/jquery-fileupload/files/.htaccess b/php/external/jquery-fileupload/files/.htaccess new file mode 100644 index 00000000..6f454afb --- /dev/null +++ b/php/external/jquery-fileupload/files/.htaccess @@ -0,0 +1,26 @@ +# To enable the Headers module, execute the following command and reload Apache: +# sudo a2enmod headers + +# The following directives prevent the execution of script files +# in the context of the website. +# They also force the content-type application/octet-stream and +# force browsers to display a download dialog for non-image files. +SetHandler default-handler +ForceType application/octet-stream +Header set Content-Disposition attachment + +# The following unsets the forced type and Content-Disposition headers +# for known image files: + + ForceType none + Header unset Content-Disposition + + +# The following directive prevents browsers from MIME-sniffing the content-type. +# This is an important complement to the ForceType directive above: +Header set X-Content-Type-Options nosniff + +# Uncomment the following lines to prevent unauthorized download of files: +#AuthName "Authorization required" +#AuthType Basic +#require valid-user diff --git a/php/external/jquery-fileupload/index.php b/php/external/jquery-fileupload/index.php new file mode 100644 index 00000000..6caabb71 --- /dev/null +++ b/php/external/jquery-fileupload/index.php @@ -0,0 +1,15 @@ + Date: Sat, 22 Dec 2018 11:44:00 +0100 Subject: [PATCH 4/5] Aixada import: Fix PHP7 compatibility --- php/lib/abstract_import_manager.php | 6 +++--- php/lib/data_table.php | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/php/lib/abstract_import_manager.php b/php/lib/abstract_import_manager.php index 29a8793f..01a9178e 100644 --- a/php/lib/abstract_import_manager.php +++ b/php/lib/abstract_import_manager.php @@ -155,7 +155,7 @@ public function __construct($destination_table, $data_table, $map) } - if (count($data_table) == 0){ + if ($data_table->count() == 0){ throw new Exception ("Import error: the data table is empty. Nothing to import!!"); exit; } else { @@ -422,7 +422,7 @@ private function _build_foreign_key_cache($key_array){ $this->_foreign_keys = array(); foreach ($key_array as $db_field=>$refs){ - if (isset($refs) && count($refs) > 1){ + if (isset($refs) && $refs && count($refs) > 1){ $sql = "select ". $refs[1]." from " . $refs[0]; $rs = $db->Execute($sql); $ids = array(); @@ -536,7 +536,7 @@ public static function parse_file($path2File, $db_table='', } $rowc = 0; - $_data_table = null; + $_data_table = array(); $_header = false; $extension = substr($path2File, -4); diff --git a/php/lib/data_table.php b/php/lib/data_table.php index 2c33d18d..081b7de7 100644 --- a/php/lib/data_table.php +++ b/php/lib/data_table.php @@ -146,7 +146,13 @@ public function __construct($data_table, $header=false, $db_table=''){ } }//end constructor - + /** + * Returns the number of table rows + */ + public function count() { + return count($this->_data_table); + } + /** * * Returns the row index of the data table by search for the given $needle in the specified column From 71f52bc64b9f4f390f054aaecd8ce1873347b24c Mon Sep 17 00:00:00 2001 From: jorix Date: Sat, 22 Dec 2018 13:28:21 +0100 Subject: [PATCH 5/5] spreadsheet-reader: Use fork of @fujaru & PR#3 for PHP7.3 --- php/external/spreadsheet-reader/CHANGELOG.md | 24 +-- .../SpreadsheetReader_XLSX.php | 38 +++-- php/external/spreadsheet-reader/_FORKS.txt | 1 + php/external/spreadsheet-reader/composer.json | 15 +- .../php-excel-reader/excel_reader2.php | 138 +++++++++--------- 5 files changed, 125 insertions(+), 91 deletions(-) create mode 100644 php/external/spreadsheet-reader/_FORKS.txt diff --git a/php/external/spreadsheet-reader/CHANGELOG.md b/php/external/spreadsheet-reader/CHANGELOG.md index 30a09a96..591ca8f2 100644 --- a/php/external/spreadsheet-reader/CHANGELOG.md +++ b/php/external/spreadsheet-reader/CHANGELOG.md @@ -1,3 +1,7 @@ +### v.0.5.12 2016-03-18 + +- Added a fix for recognising dates in XLS files created by LibreOffice + ### v.0.5.11 2015-04-30 - Added a special case for cells formatted as text in XLSX. Previously leading zeros would get truncated if a text cell contained only numbers. @@ -50,10 +54,10 @@ Currently only decimal number values are converted to PHP's floats. ### v.0.5.1 2013-06-27 -- Fixed file type choice when using mime-types (previously there were problems with +- Fixed file type choice when using mime-types (previously there were problems with XLSX and ODS mime-types) (Thanks to [incratec](https://github.com/incratec)) -- Fixed an error in XLSX iterator where `current()` would advance the iterator forward +- Fixed an error in XLSX iterator where `current()` would advance the iterator forward with each call. (Thanks to [osuwariboy](https://github.com/osuwariboy)) ### v.0.5.0 2013-06-17 @@ -62,19 +66,19 @@ with each call. (Thanks to [osuwariboy](https://github.com/osuwariboy)) - The `Sheets()` method lets you retrieve a list of all sheets present in the file. - `ChangeSheet($Index)` method changes the sheet in the reader to the one specified. -- Previously temporary files that were extracted, were deleted after the SpreadsheetReader -was destroyed but the empty directories remained. Now those are cleaned up as well. +- Previously temporary files that were extracted, were deleted after the SpreadsheetReader +was destroyed but the empty directories remained. Now those are cleaned up as well. ### v.0.4.3 2013-06-14 -- Bugfix for shared string caching in XLSX files. When the shared string count was larger -than the caching limit, instead of them being read from file, empty strings were returned. +- Bugfix for shared string caching in XLSX files. When the shared string count was larger +than the caching limit, instead of them being read from file, empty strings were returned. ### v.0.4.2 2013-06-02 -- XLS file reading relies on the external Spreadsheet_Excel_Reader class which, by default, -reads additional information about cells like fonts, styles, etc. Now that is disabled -to save some memory since the style data is unnecessary anyway. +- XLS file reading relies on the external Spreadsheet_Excel_Reader class which, by default, +reads additional information about cells like fonts, styles, etc. Now that is disabled +to save some memory since the style data is unnecessary anyway. (Thanks to [ChALkeR](https://github.com/ChALkeR) for the tip.) -Martins Pilsetnieks \ No newline at end of file +Martins Pilsetnieks diff --git a/php/external/spreadsheet-reader/SpreadsheetReader_XLSX.php b/php/external/spreadsheet-reader/SpreadsheetReader_XLSX.php index 9cf8d125..dafdc717 100644 --- a/php/external/spreadsheet-reader/SpreadsheetReader_XLSX.php +++ b/php/external/spreadsheet-reader/SpreadsheetReader_XLSX.php @@ -20,7 +20,7 @@ class SpreadsheetReader_XLSX implements Iterator, Countable * With large shared string caches there are huge performance gains, however a lot of memory could be used which * can be a problem, especially on shared hosting. */ - const SHARED_STRING_CACHE_LIMIT = 50000; + const SHARED_STRING_CACHE_LIMIT = null; private $Options = array( 'TempDir' => '', @@ -370,17 +370,29 @@ public function Sheets() $this -> Sheets = array(); foreach ($this -> WorkbookXML -> sheets -> sheet as $Index => $Sheet) { - $Attributes = $Sheet -> attributes('r', true); - foreach ($Attributes as $Name => $Value) + $AttributesWithPrefix = $Sheet -> attributes('r', true); + $Attributes = $Sheet -> attributes(); + + $rId = 0; + $sheetId = 0; + + foreach ($AttributesWithPrefix as $Name => $Value) { if ($Name == 'id') { - $SheetID = (int)str_replace('rId', '', (string)$Value); + $rId = (int)str_replace('rId', '', (string)$Value); + break; + } + } + foreach ($Attributes as $Name => $Value) + { + if ($Name == 'sheetId') { + $sheetId = (int)$Value; break; } } - $this -> Sheets[$SheetID] = (string)$Sheet['name']; + $this -> Sheets[min($rId, $sheetId)] = (string)$Sheet['name']; } ksort($this -> Sheets); } @@ -453,7 +465,7 @@ private function PrepareSharedStringCache() case 't': if ($this -> SharedStrings -> nodeType == XMLReader::END_ELEMENT) { - continue; + break; } $CacheValue .= $this -> SharedStrings -> readString(); break; @@ -578,7 +590,7 @@ private function GetSharedString($Index) case 't': if ($this -> SharedStrings -> nodeType == XMLReader::END_ELEMENT) { - continue; + break; } $Value .= $this -> SharedStrings -> readString(); break; @@ -1046,7 +1058,7 @@ public function next() // If it is a closing tag, skip it if ($this -> Worksheet -> nodeType == XMLReader::END_ELEMENT) { - continue; + break; } $StyleId = (int)$this -> Worksheet -> getAttribute('s'); @@ -1080,7 +1092,7 @@ public function next() case 'is': if ($this -> Worksheet -> nodeType == XMLReader::END_ELEMENT) { - continue; + break; } $Value = $this -> Worksheet -> readString(); @@ -1099,6 +1111,14 @@ public function next() { $Value = $this -> GeneralFormat($Value); } + elseif ($Value) + { + $Value = $this -> GeneralFormat($Value); + } + elseif ($Value) + { + $Value = $this -> GeneralFormat($Value); + } $this -> CurrentRow[$Index] = $Value; break; diff --git a/php/external/spreadsheet-reader/_FORKS.txt b/php/external/spreadsheet-reader/_FORKS.txt new file mode 100644 index 00000000..688fcf7f --- /dev/null +++ b/php/external/spreadsheet-reader/_FORKS.txt @@ -0,0 +1 @@ +See at: fujaru/spreadsheet-reader (forked from ho-nl-fork/spreadsheet-reader forked from nuovo/spreadsheet-reader) diff --git a/php/external/spreadsheet-reader/composer.json b/php/external/spreadsheet-reader/composer.json index 0e64a783..caf1a2f2 100644 --- a/php/external/spreadsheet-reader/composer.json +++ b/php/external/spreadsheet-reader/composer.json @@ -1,10 +1,10 @@ { - "name": "nuovo/spreadsheet-reader", + "name": "karlis-i/spreadsheet-reader", "description": "Spreadsheet reader library for Excel, OpenOffice and structured text files", "keywords": ["spreadsheet", "xls", "xlsx", "ods", "csv", "excel", "openoffice"], - "homepage": "https://github.com/nuovo/spreadsheet-reader", - "version": "0.5.11", - "time": "2015-04-30", + "homepage": "https://github.com/karlis-i/spreadsheet-reader", + "version": "0.5.16", + "time": "2016-06-02", "type": "library", "license": ["MIT"], "authors": [ @@ -12,11 +12,12 @@ "name": "Martins Pilsetnieks", "email": "pilsetnieks@gmail.com", "homepage": "http://www.nuovo.lv/" + }, + { + "name": "karlis-i", + "email": "karlis.im@gmail.com" } ], - "support": { - "email": "spreadsheet-reader@nuovo.lv" - }, "require": { "php": ">= 5.3.0", "ext-zip": "*" diff --git a/php/external/spreadsheet-reader/php-excel-reader/excel_reader2.php b/php/external/spreadsheet-reader/php-excel-reader/excel_reader2.php index 6d387624..0d533936 100644 --- a/php/external/spreadsheet-reader/php-excel-reader/excel_reader2.php +++ b/php/external/spreadsheet-reader/php-excel-reader/excel_reader2.php @@ -77,7 +77,7 @@ function GetInt4d($data, $pos) { function gmgetdate($ts = null){ $k = array('seconds','minutes','hours','mday','wday','mon','year','yday','weekday','month',0); return(array_comb($k,explode(":",gmdate('s:i:G:j:w:n:Y:z:l:F:U',is_null($ts)?time():$ts)))); - } + } // Added for PHP4 compatibility function array_comb($array1, $array2) { @@ -321,7 +321,7 @@ function myHex($d) { if ($d < 16) return "0" . dechex($d); return dechex($d); } - + function dumpHexData($data, $pos, $length) { $info = ""; for ($i = 0; $i <= $length; $i++) { @@ -394,7 +394,7 @@ function colcount($sheet=0) { } function colwidth($col,$sheet=0) { // Col width is actually the width of the number 0. So we have to estimate and come close - return $this->colInfo[$sheet][$col]['width']/9142*200; + return $this->colInfo[$sheet][$col]['width']/9142*200; } function colhidden($col,$sheet=0) { return !!$this->colInfo[$sheet][$col]['hidden']; @@ -405,7 +405,7 @@ function rowheight($row,$sheet=0) { function rowhidden($row,$sheet=0) { return !!$this->rowInfo[$sheet][$row]['hidden']; } - + // GET THE CSS FOR FORMATTING // ========================== function style($row,$col,$sheet=0,$properties='') { @@ -467,10 +467,10 @@ function style($row,$col,$sheet=0,$properties='') { if ($bRight!="" && $bRightCol!="") { $css .= "border-right-color:" . $bRightCol .";"; } if ($bTop!="" && $bTopCol!="") { $css .= "border-top-color:" . $bTopCol . ";"; } if ($bBottom!="" && $bBottomCol!="") { $css .= "border-bottom-color:" . $bBottomCol .";"; } - + return $css; } - + // FORMAT PROPERTIES // ================= function format($row,$col,$sheet=0) { @@ -482,7 +482,7 @@ function formatIndex($row,$col,$sheet=0) { function formatColor($row,$col,$sheet=0) { return $this->info($row,$col,'formatColor',$sheet); } - + // CELL (XF) PROPERTIES // ==================== function xfRecord($row,$col,$sheet=0) { @@ -581,7 +581,7 @@ function height($row,$col,$sheet=0) { function font($row,$col,$sheet=0) { return $this->fontProperty($row,$col,$sheet,'font'); } - + // DUMP AN HTML TABLE OF THE ENTIRE XLS DATA // ========================================= function dump($row_numbers=false,$col_letters=false,$sheet=0,$table_class='excel') { @@ -600,7 +600,7 @@ function dump($row_numbers=false,$col_letters=false,$sheet=0,$table_class='excel } $out .= "\n"; } - + $out .= "\n"; for($row=1;$row<=$this->rowcount($sheet);$row++) { $rowheight = $this->rowheight($row,$sheet); @@ -631,8 +631,8 @@ function dump($row_numbers=false,$col_letters=false,$sheet=0,$table_class='excel $out .= "\n\t\t 1?" colspan=$colspan":"") . ($rowspan > 1?" rowspan=$rowspan":"") . ">"; $val = $this->val($row,$col,$sheet); if ($val=='') { $val=" "; } - else { - $val = htmlentities($val); + else { + $val = htmlentities($val); $link = $this->hyperlink($row,$col,$sheet); if ($link!='') { $val = "$val"; @@ -647,7 +647,7 @@ function dump($row_numbers=false,$col_letters=false,$sheet=0,$table_class='excel $out .= ""; return $out; } - + // -------------- // END PUBLIC API @@ -658,7 +658,7 @@ function dump($row_numbers=false,$col_letters=false,$sheet=0,$table_class='excel var $xfRecords = array(); var $colInfo = array(); var $rowInfo = array(); - + var $sst = array(); var $sheets = array(); @@ -807,35 +807,35 @@ function dump($row_numbers=false,$col_letters=false,$sheet=0,$table_class='excel 0x0B => "Thin dash-dot-dotted", 0x0C => "Medium dash-dot-dotted", 0x0D => "Slanted medium dash-dotted" - ); + ); var $lineStylesCss = array( - "Thin" => "1px solid", - "Medium" => "2px solid", - "Dashed" => "1px dashed", - "Dotted" => "1px dotted", - "Thick" => "3px solid", - "Double" => "double", - "Hair" => "1px solid", - "Medium dashed" => "2px dashed", - "Thin dash-dotted" => "1px dashed", - "Medium dash-dotted" => "2px dashed", - "Thin dash-dot-dotted" => "1px dashed", - "Medium dash-dot-dotted" => "2px dashed", - "Slanted medium dash-dotte" => "2px dashed" + "Thin" => "1px solid", + "Medium" => "2px solid", + "Dashed" => "1px dashed", + "Dotted" => "1px dotted", + "Thick" => "3px solid", + "Double" => "double", + "Hair" => "1px solid", + "Medium dashed" => "2px dashed", + "Thin dash-dotted" => "1px dashed", + "Medium dash-dotted" => "2px dashed", + "Thin dash-dot-dotted" => "1px dashed", + "Medium dash-dot-dotted" => "2px dashed", + "Slanted medium dash-dotte" => "2px dashed" ); - + function read16bitstring($data, $start) { $len = 0; while (ord($data[$start + $len]) + ord($data[$start + $len + 1]) > 0) $len++; return substr($data, $start, $len); } - + // ADDED by Matt Kruse for better formatting function _format_value($format,$num,$f) { // 49==TEXT format // http://code.google.com/p/php-excel-reader/issues/detail?id=7 - if ( (!$f && $format=="%s") || ($f==49) || ($format=="GENERAL") ) { + if ( (!$f && $format=="%s") || ($f==49) || (strtoupper($format)=="GENERAL") ) { return array('string'=>$num, 'formatColor'=>null); } @@ -860,13 +860,13 @@ function _format_value($format,$num,$f) { $color = strtolower($matches[1]); $pattern = preg_replace($color_regex,"",$pattern); } - + // In Excel formats, "_" is used to add spacing, which we can't do in HTML $pattern = preg_replace("/_./","",$pattern); - + // Some non-number characters are escaped with \, which we don't need $pattern = preg_replace("/\\\/","",$pattern); - + // Some non-number strings are quoted, so we'll get rid of the quotes $pattern = preg_replace("/\"/","",$pattern); @@ -901,6 +901,11 @@ function _format_value($format,$num,$f) { $pattern = preg_replace($number_regex, $formatted, $pattern); } + // prevent changing of big integers to '@' + if ($pattern === '@') { + $pattern = strval($num); + } + return array( 'string'=>$pattern, 'formatColor'=>$color @@ -915,7 +920,7 @@ function _format_value($format,$num,$f) { function __construct($file='',$store_extended_info=true,$outputEncoding='') { $this->_ole = new OLERead(); $this->setUTFEncoder('iconv'); - if ($outputEncoding != '') { + if ($outputEncoding != '') { $this->setOutputEncoding($outputEncoding); } for ($i=1; $i<245; $i++) { @@ -1163,7 +1168,7 @@ function _parse() { $font = substr($data, $pos+20, $numchars); } else { $font = substr($data, $pos+20, $numchars*2); - $font = $this->_encodeUTF16($font); + $font = $this->_encodeUTF16($font); } $this->fontRecords[] = array( 'height' => $height / 20, @@ -1216,14 +1221,14 @@ function _parse() { $xf['borderRight'] = $this->lineStyles[($border & 0xF0) >> 4]; $xf['borderTop'] = $this->lineStyles[($border & 0xF00) >> 8]; $xf['borderBottom'] = $this->lineStyles[($border & 0xF000) >> 12]; - + $xf['borderLeftColor'] = ($border & 0x7F0000) >> 16; $xf['borderRightColor'] = ($border & 0x3F800000) >> 23; $border = (ord($data[$pos+18]) | ord($data[$pos+19]) << 8); $xf['borderTopColor'] = ($border & 0x7F); $xf['borderBottomColor'] = ($border & 0x3F80) >> 7; - + if (array_key_exists($indexCode, $this->dateFormats)) { $xf['type'] = 'date'; $xf['format'] = $this->dateFormats[$indexCode]; @@ -1244,21 +1249,28 @@ function _parse() { if (preg_match("/[^hmsday\/\-:\s\\\,AMP]/i", $tmp) == 0) { // found day and time format $isdate = TRUE; $formatstr = $tmp; - $formatstr = str_replace(array('AM/PM','mmmm','mmm'), array('a','F','M'), $formatstr); - // m/mm are used for both minutes and months - oh SNAP! - // This mess tries to fix for that. - // 'm' == minutes only if following h/hh or preceding s/ss - $formatstr = preg_replace("/(h:?)mm?/","$1i", $formatstr); - $formatstr = preg_replace("/mm?(:?s)/","i$1", $formatstr); - // A single 'm' = n in PHP - $formatstr = preg_replace("/(^|[^m])m([^m]|$)/", '$1n$2', $formatstr); - $formatstr = preg_replace("/(^|[^m])m([^m]|$)/", '$1n$2', $formatstr); - // else it's months - $formatstr = str_replace('mm', 'm', $formatstr); - // Convert single 'd' to 'j' - $formatstr = preg_replace("/(^|[^d])d([^d]|$)/", '$1j$2', $formatstr); - $formatstr = str_replace(array('dddd','ddd','dd','yyyy','yy','hh','h'), array('l','D','d','Y','y','H','g'), $formatstr); - $formatstr = preg_replace("/ss?/", 's', $formatstr); + if ($formatstr === 'YYYY/MM/DD' || $formatstr === 'YYYY\-MM\-DD') { + // LibreOffice turns this pattern into invalid dates: + // 2015201520152015/OctOct/WedWed + // here we fix it + $formatstr = 'Y-m-d'; + } else { + $formatstr = str_replace(array('AM/PM','mmmm','mmm'), array('a','F','M'), $formatstr); + // m/mm are used for both minutes and months - oh SNAP! + // This mess tries to fix for that. + // 'm' == minutes only if following h/hh or preceding s/ss + $formatstr = preg_replace("/(h:?)mm?/","$1i", $formatstr); + $formatstr = preg_replace("/mm?(:?s)/","i$1", $formatstr); + // A single 'm' = n in PHP + $formatstr = preg_replace("/(^|[^m])m([^m]|$)/", '$1n$2', $formatstr); + $formatstr = preg_replace("/(^|[^m])m([^m]|$)/", '$1n$2', $formatstr); + // else it's months + $formatstr = str_replace('mm', 'm', $formatstr); + // Convert single 'd' to 'j' + $formatstr = preg_replace("/(^|[^d])d([^d]|$)/", '$1j$2', $formatstr); + $formatstr = str_replace(array('dddd','ddd','dd','yyyy','yy','hh','h'), array('l','D','d','Y','y','H','g'), $formatstr); + $formatstr = preg_replace("/ss?/", 's', $formatstr); + } } } } @@ -1553,24 +1565,24 @@ function _parsesheet($spos) { } $linkdata['desc'] = $udesc; $linkdata['link'] = $this->_encodeUTF16($ulink); - for ($r=$row; $r<=$row2; $r++) { + for ($r=$row; $r<=$row2; $r++) { for ($c=$column; $c<=$column2; $c++) { $this->sheets[$this->sn]['cellsInfo'][$r+1][$c+1]['hyperlink'] = $linkdata; } } break; case SPREADSHEET_EXCEL_READER_TYPE_DEFCOLWIDTH: - $this->defaultColWidth = ord($data[$spos+4]) | ord($data[$spos+5]) << 8; + $this->defaultColWidth = ord($data[$spos+4]) | ord($data[$spos+5]) << 8; break; case SPREADSHEET_EXCEL_READER_TYPE_STANDARDWIDTH: - $this->standardColWidth = ord($data[$spos+4]) | ord($data[$spos+5]) << 8; + $this->standardColWidth = ord($data[$spos+4]) | ord($data[$spos+5]) << 8; break; case SPREADSHEET_EXCEL_READER_TYPE_COLINFO: $colfrom = ord($data[$spos+0]) | ord($data[$spos+1]) << 8; $colto = ord($data[$spos+2]) | ord($data[$spos+3]) << 8; - $cw = ord($data[$spos+4]) | ord($data[$spos+5]) << 8; - $cxf = ord($data[$spos+6]) | ord($data[$spos+7]) << 8; - $co = ord($data[$spos+8]); + $cw = ord($data[$spos+4]) | ord($data[$spos+5]) << 8; + $cxf = ord($data[$spos+6]) | ord($data[$spos+7]) << 8; + $co = ord($data[$spos+8]); for ($coli = $colfrom; $coli <= $colto; $coli++) { $this->colInfo[$this->sn][$coli+1] = Array('width' => $cw, 'xf' => $cxf, 'hidden' => ($co & 0x01), 'collapsed' => ($co & 0x1000) >> 12); } @@ -1714,12 +1726,8 @@ function _GetIEEE754($rknum) { function _encodeUTF16($string) { $result = $string; if ($this->_defaultEncoding){ - switch ($this->_encoderFunction){ - case 'iconv' : $result = iconv('UTF-16LE', $this->_defaultEncoding, $string); - break; - case 'mb_convert_encoding' : $result = mb_convert_encoding($string, $this->_defaultEncoding, 'UTF-16LE' ); - break; - } + // iconv changed to mb_convert_encoding + $result = mb_convert_encoding($string, $this->_defaultEncoding, 'UTF-16LE' ); } return $result; } @@ -1734,4 +1742,4 @@ function _GetInt4d($data, $pos) { } -?> \ No newline at end of file +?>