diff --git a/README.md b/README.md index 22e16dd..d1b632c 100644 --- a/README.md +++ b/README.md @@ -264,10 +264,11 @@ $sheetName = $sheet->setName('My custom name'); ``` > Please note that Excel has some restrictions on the sheet's name: -> * it should not exceed 31 characters -> * it should not contain these characters: \ / ? * [ or ] -> * it should not be blank -> * it should be unique +> * it must not be blank +> * it must not exceed 31 characters +> * it must not contain these characters: \ / ? * : [ or ] +> * it must not start or end with a single quote +> * it must be unique > > Handling these restrictions is the developer's responsibility. Spout does not try to automatically change the sheet's name, as one may rely on this name to be exactly what was passed in. diff --git a/composer.lock b/composer.lock index b06d00f..ae8bb33 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "3c0d36250693792d14bc19f14250fec6", + "hash": "0866be323931eaa6e9431b3bbf0a817a", "packages": [], "packages-dev": [ { @@ -585,16 +585,16 @@ }, { "name": "phpspec/prophecy", - "version": "v1.4.1", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "3132b1f44c7bf2ec4c7eb2d3cb78fdeca760d373" + "reference": "4745ded9307786b730d7a60df5cb5a6c43cf95f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/3132b1f44c7bf2ec4c7eb2d3cb78fdeca760d373", - "reference": "3132b1f44c7bf2ec4c7eb2d3cb78fdeca760d373", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4745ded9307786b730d7a60df5cb5a6c43cf95f7", + "reference": "4745ded9307786b730d7a60df5cb5a6c43cf95f7", "shasum": "" }, "require": { @@ -641,20 +641,20 @@ "spy", "stub" ], - "time": "2015-04-27 22:15:08" + "time": "2015-08-13 10:07:40" }, { "name": "phpunit/php-code-coverage", - "version": "2.1.9", + "version": "2.2.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "5bd48b86cd282da411bb80baac1398ce3fefac41" + "reference": "2d7c03c0e4e080901b8f33b2897b0577be18a13c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/5bd48b86cd282da411bb80baac1398ce3fefac41", - "reference": "5bd48b86cd282da411bb80baac1398ce3fefac41", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2d7c03c0e4e080901b8f33b2897b0577be18a13c", + "reference": "2d7c03c0e4e080901b8f33b2897b0577be18a13c", "shasum": "" }, "require": { @@ -662,7 +662,7 @@ "phpunit/php-file-iterator": "~1.3", "phpunit/php-text-template": "~1.2", "phpunit/php-token-stream": "~1.3", - "sebastian/environment": "~1.0", + "sebastian/environment": "^1.3.2", "sebastian/version": "~1.0" }, "require-dev": { @@ -677,7 +677,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1.x-dev" + "dev-master": "2.2.x-dev" } }, "autoload": { @@ -703,7 +703,7 @@ "testing", "xunit" ], - "time": "2015-07-26 12:54:47" + "time": "2015-08-04 03:42:39" }, { "name": "phpunit/php-file-iterator", @@ -836,16 +836,16 @@ }, { "name": "phpunit/php-token-stream", - "version": "1.4.3", + "version": "1.4.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "7a9b0969488c3c54fd62b4d504b3ec758fd005d9" + "reference": "3ab72c62e550370a6cd5dc873e1a04ab57562f5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/7a9b0969488c3c54fd62b4d504b3ec758fd005d9", - "reference": "7a9b0969488c3c54fd62b4d504b3ec758fd005d9", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3ab72c62e550370a6cd5dc873e1a04ab57562f5b", + "reference": "3ab72c62e550370a6cd5dc873e1a04ab57562f5b", "shasum": "" }, "require": { @@ -881,20 +881,20 @@ "keywords": [ "tokenizer" ], - "time": "2015-06-19 03:43:16" + "time": "2015-08-16 08:51:00" }, { "name": "phpunit/phpunit", - "version": "4.7.7", + "version": "4.8.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "9b97f9d807b862c2de2a36e86690000801c85724" + "reference": "9b7417edaf28059ea63d86be941e6004dbfcc0cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9b97f9d807b862c2de2a36e86690000801c85724", - "reference": "9b97f9d807b862c2de2a36e86690000801c85724", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9b7417edaf28059ea63d86be941e6004dbfcc0cc", + "reference": "9b7417edaf28059ea63d86be941e6004dbfcc0cc", "shasum": "" }, "require": { @@ -904,7 +904,7 @@ "ext-reflection": "*", "ext-spl": "*", "php": ">=5.3.3", - "phpspec/prophecy": "~1.3,>=1.3.1", + "phpspec/prophecy": "^1.3.1", "phpunit/php-code-coverage": "~2.1", "phpunit/php-file-iterator": "~1.4", "phpunit/php-text-template": "~1.2", @@ -912,7 +912,7 @@ "phpunit/phpunit-mock-objects": "~2.3", "sebastian/comparator": "~1.1", "sebastian/diff": "~1.2", - "sebastian/environment": "~1.2", + "sebastian/environment": "~1.3", "sebastian/exporter": "~1.2", "sebastian/global-state": "~1.0", "sebastian/version": "~1.0", @@ -927,7 +927,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.7.x-dev" + "dev-master": "4.8.x-dev" } }, "autoload": { @@ -953,24 +953,24 @@ "testing", "xunit" ], - "time": "2015-07-13 11:28:34" + "time": "2015-08-19 09:20:57" }, { "name": "phpunit/phpunit-mock-objects", - "version": "2.3.6", + "version": "2.3.7", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "18dfbcb81d05e2296c0bcddd4db96cade75e6f42" + "reference": "5e2645ad49d196e020b85598d7c97e482725786a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/18dfbcb81d05e2296c0bcddd4db96cade75e6f42", - "reference": "18dfbcb81d05e2296c0bcddd4db96cade75e6f42", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/5e2645ad49d196e020b85598d7c97e482725786a", + "reference": "5e2645ad49d196e020b85598d7c97e482725786a", "shasum": "" }, "require": { - "doctrine/instantiator": "~1.0,>=1.0.2", + "doctrine/instantiator": "^1.0.2", "php": ">=5.3.3", "phpunit/php-text-template": "~1.2", "sebastian/exporter": "~1.2" @@ -1009,7 +1009,7 @@ "mock", "xunit" ], - "time": "2015-07-10 06:54:24" + "time": "2015-08-19 09:14:08" }, { "name": "scrutinizer/ocular", @@ -1165,16 +1165,16 @@ }, { "name": "sebastian/environment", - "version": "1.3.0", + "version": "1.3.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "4fe0a44cddd8cc19583a024bdc7374eb2fef0b87" + "reference": "6324c907ce7a52478eeeaede764f48733ef5ae44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/4fe0a44cddd8cc19583a024bdc7374eb2fef0b87", - "reference": "4fe0a44cddd8cc19583a024bdc7374eb2fef0b87", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6324c907ce7a52478eeeaede764f48733ef5ae44", + "reference": "6324c907ce7a52478eeeaede764f48733ef5ae44", "shasum": "" }, "require": { @@ -1211,7 +1211,7 @@ "environment", "hhvm" ], - "time": "2015-07-26 06:42:57" + "time": "2015-08-03 06:14:51" }, { "name": "sebastian/exporter", @@ -1420,16 +1420,16 @@ }, { "name": "symfony/console", - "version": "v2.7.2", + "version": "v2.7.3", "source": { "type": "git", "url": "https://github.com/symfony/Console.git", - "reference": "8cf484449130cabfd98dcb4694ca9945802a21ed" + "reference": "d6cf02fe73634c96677e428f840704bfbcaec29e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Console/zipball/8cf484449130cabfd98dcb4694ca9945802a21ed", - "reference": "8cf484449130cabfd98dcb4694ca9945802a21ed", + "url": "https://api.github.com/repos/symfony/Console/zipball/d6cf02fe73634c96677e428f840704bfbcaec29e", + "reference": "d6cf02fe73634c96677e428f840704bfbcaec29e", "shasum": "" }, "require": { @@ -1473,11 +1473,11 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2015-07-09 16:07:40" + "time": "2015-07-28 15:18:12" }, { "name": "symfony/event-dispatcher", - "version": "v2.7.2", + "version": "v2.7.3", "source": { "type": "git", "url": "https://github.com/symfony/EventDispatcher.git", @@ -1535,7 +1535,7 @@ }, { "name": "symfony/process", - "version": "v2.7.2", + "version": "v2.7.3", "source": { "type": "git", "url": "https://github.com/symfony/Process.git", @@ -1584,16 +1584,16 @@ }, { "name": "symfony/yaml", - "version": "v2.7.2", + "version": "v2.7.3", "source": { "type": "git", "url": "https://github.com/symfony/Yaml.git", - "reference": "4bfbe0ed3909bfddd75b70c094391ec1f142f860" + "reference": "71340e996171474a53f3d29111d046be4ad8a0ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Yaml/zipball/4bfbe0ed3909bfddd75b70c094391ec1f142f860", - "reference": "4bfbe0ed3909bfddd75b70c094391ec1f142f860", + "url": "https://api.github.com/repos/symfony/Yaml/zipball/71340e996171474a53f3d29111d046be4ad8a0ff", + "reference": "71340e996171474a53f3d29111d046be4ad8a0ff", "shasum": "" }, "require": { @@ -1629,7 +1629,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2015-07-01 11:25:50" + "time": "2015-07-28 14:07:07" } ], "aliases": [], diff --git a/src/Spout/Common/Helper/StringHelper.php b/src/Spout/Common/Helper/StringHelper.php new file mode 100644 index 0000000..273104e --- /dev/null +++ b/src/Spout/Common/Helper/StringHelper.php @@ -0,0 +1,71 @@ +hasMbstringSupport = extension_loaded('mbstring'); + } + + /** + * Returns the length of the given string. + * It uses the multi-bytes function is available. + * @see strlen + * @see mb_strlen + * + * @param string $string + * @return int + */ + public function getStringLength($string) + { + return $this->hasMbstringSupport ? mb_strlen($string) : strlen($string); + } + + /** + * Returns the position of the first occurrence of the given character/substring within the given string. + * It uses the multi-bytes function is available. + * @see strpos + * @see mb_strpos + * + * @param string $char Needle + * @param string $string Haystack + * @return int Char/substring's first occurrence position within the string if found (starts at 0) or -1 if not found + */ + public function getCharFirstOccurrencePosition($char, $string) + { + $position = $this->hasMbstringSupport ? mb_strpos($string, $char) : strpos($string, $char); + return ($position !== false) ? $position : -1; + } + + /** + * Returns the position of the last occurrence of the given character/substring within the given string. + * It uses the multi-bytes function is available. + * @see strrpos + * @see mb_strrpos + * + * @param string $char Needle + * @param string $string Haystack + * @return int Char/substring's last occurrence position within the string if found (starts at 0) or -1 if not found + */ + public function getCharLastOccurrencePosition($char, $string) + { + $position = $this->hasMbstringSupport ? mb_strrpos($string, $char) : strrpos($string, $char); + return ($position !== false) ? $position : -1; + } +} diff --git a/src/Spout/Writer/XLSX/Sheet.php b/src/Spout/Writer/XLSX/Sheet.php index a82906f..ef2d85d 100644 --- a/src/Spout/Writer/XLSX/Sheet.php +++ b/src/Spout/Writer/XLSX/Sheet.php @@ -2,6 +2,7 @@ namespace Box\Spout\Writer\XLSX; +use Box\Spout\Common\Helper\StringHelper; use Box\Spout\Writer\Exception\InvalidSheetNameException; /** @@ -18,7 +19,7 @@ class Sheet const MAX_LENGTH_SHEET_NAME = 31; /** @var array Invalid characters that cannot be contained in the sheet name */ - private static $INVALID_CHARACTERS_IN_SHEET_NAME = ['\\', '/', '?', '*', '[', ']']; + private static $INVALID_CHARACTERS_IN_SHEET_NAME = ['\\', '/', '?', '*', ':', '[', ']']; /** @var array Associative array [SHEET_INDEX] => [SHEET_NAME] keeping track of sheets' name to enforce uniqueness */ protected static $SHEETS_NAME_USED = []; @@ -29,12 +30,16 @@ class Sheet /** @var string Name of the sheet */ protected $name; + /** @var \Box\Spout\Common\Helper\StringHelper */ + protected $stringHelper; + /** * @param int $sheetIndex Index of the sheet, based on order of creation (zero-based) */ public function __construct($sheetIndex) { $this->index = $sheetIndex; + $this->stringHelper = new StringHelper(); $this->setName(self::DEFAULT_SHEET_NAME_PREFIX . ($sheetIndex + 1)); } @@ -58,7 +63,7 @@ class Sheet * Sets the name of the sheet. Note that Excel has some restrictions on the name: * - it should not be blank * - it should not exceed 31 characters - * - it should not contain these characters: \ / ? * [ or ] + * - it should not contain these characters: \ / ? * : [ or ] * - it should be unique * * @param string $name Name of the sheet @@ -71,7 +76,7 @@ class Sheet $errorMessage = "The sheet's name is invalid. It did not meet at least one of these requirements:\n"; $errorMessage .= " - It should not be blank\n"; $errorMessage .= " - It should not exceed 31 characters\n"; - $errorMessage .= " - It should not contain these characters: \\ / ? * [ or ]\n"; + $errorMessage .= " - It should not contain these characters: \\ / ? * : [ or ]\n"; $errorMessage .= " - It should be unique"; throw new InvalidSheetNameException($errorMessage); } @@ -95,42 +100,15 @@ class Sheet return false; } - $nameLength = $this->getStringLength($name); - $hasValidLength = ($nameLength > 0 && $nameLength <= self::MAX_LENGTH_SHEET_NAME); - $containsInvalidCharacters = $this->doesContainInvalidCharacters($name); - $isNameUnique = $this->isNameUnique($name); + $nameLength = $this->stringHelper->getStringLength($name); - return ($hasValidLength && !$containsInvalidCharacters && $isNameUnique); - } - - /** - * Returns the length of the given string. - * It uses the multi-bytes function is available. - * @see strlen - * @see mb_strlen - * - * @param string $string - * @return int - */ - protected function getStringLength($string) - { - return extension_loaded('mbstring') ? mb_strlen($string) : strlen($string); - } - - /** - * Returns the position of the given character/substring in the given string. - * It uses the multi-bytes function is available. - * @see strpos - * @see mb_strpos - * - * @param string $string Haystack - * @param string $char Needle - * @return int Index of the char in the string if found (started at 0) or -1 if not found - */ - protected function getCharPosition($string, $char) - { - $position = extension_loaded('mbstring') ? mb_strpos($string, $char) : strpos($string, $char); - return ($position !== false) ? $position : -1; + return ( + $nameLength > 0 && + $nameLength <= self::MAX_LENGTH_SHEET_NAME && + !$this->doesContainInvalidCharacters($name) && + $this->isNameUnique($name) && + !$this->doesStartOrEndWithSingleQuote($name) + ); } /** @@ -142,13 +120,21 @@ class Sheet */ protected function doesContainInvalidCharacters($name) { - foreach (self::$INVALID_CHARACTERS_IN_SHEET_NAME as $invalidCharacter) { - if ($this->getCharPosition($name, $invalidCharacter) !== -1) { - return true; - } - } + return (str_replace(self::$INVALID_CHARACTERS_IN_SHEET_NAME, '', $name) !== $name); + } - return false; + /** + * Returns whether the given name starts or ends with a single quote + * + * @param string $name + * @return bool TRUE if the name starts or ends with a single quote, FALSE otherwise. + */ + protected function doesStartOrEndWithSingleQuote($name) + { + $startsWithSingleQuote = ($this->stringHelper->getCharFirstOccurrencePosition('\'', $name) === 0); + $endsWithSingleQuote = ($this->stringHelper->getCharLastOccurrencePosition('\'', $name) === ($this->stringHelper->getStringLength($name) - 1)); + + return ($startsWithSingleQuote || $endsWithSingleQuote); } /** diff --git a/tests/Spout/Writer/XLSX/SheetTest.php b/tests/Spout/Writer/XLSX/SheetTest.php index 76629c2..9be8254 100644 --- a/tests/Spout/Writer/XLSX/SheetTest.php +++ b/tests/Spout/Writer/XLSX/SheetTest.php @@ -65,8 +65,11 @@ class SheetTest extends \PHPUnit_Framework_TestCase ['Illegal /'], ['Illegal ?'], ['Illegal *'], + ['Illegal :'], ['Illegal ['], ['Illegal ]'], + ['\'Illegal start'], + ['Illegal end\''], ]; }