├── .codeclimate.yml ├── .phpcs └── Barracuda │ ├── Sniffs │ ├── Commenting │ │ ├── DisallowSingleLineMultiCommentsSniff.php │ │ ├── DocCommentSniff.php │ │ ├── FunctionCommentSniff.php │ │ ├── SpaceAfterCommentSniff.php │ │ └── VariableCommentSniff.php │ ├── ControlStructures │ │ ├── ControlSignatureSniff.php │ │ └── NoInlineAssignmentSniff.php │ ├── Formatting │ │ └── SpaceUnaryOperatorSniff.php │ └── Functions │ │ └── FunctionDeclarationSniff.php │ ├── bootstrap.php │ └── ruleset.xml ├── CHANGELOG.txt ├── LICENSE ├── README.md ├── composer.json ├── extras ├── README └── zip-appnote-6.3.1-20070411.txt ├── src ├── Archive.php ├── TarArchive.php └── ZipArchive.php └── test └── index.php /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | exclude_paths: 2 | - ".phpcs/**/*" 3 | - "test/**/*" 4 | - "extras/**/*" 5 | engines: 6 | phpmd: 7 | enabled: false 8 | config: 9 | file_extensions: "php" 10 | rulesets: "unusedcode" 11 | phpcodesniffer: 12 | enabled: true 13 | config: 14 | file_extensions: "php" 15 | standard: "/code/.phpcs/Barracuda/ruleset.xml" 16 | ignore_warnings: true 17 | fixme: 18 | enabled: true 19 | ratings: 20 | paths: 21 | - "**/*.php" 22 | -------------------------------------------------------------------------------- /.phpcs/Barracuda/Sniffs/Commenting/DisallowSingleLineMultiCommentsSniff.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence 11 | * @version 1.0.00 12 | * @link http://pear.php.net/package/PHP_CodeSniffer 13 | */ 14 | 15 | /** 16 | * This sniff prohibits the use of Perl style hash comments. 17 | * 18 | * An example of a hash comment is: 19 | * 20 | * 21 | * /* This is a single line multi line comment, which is prohibited. */ 22 | /* $hello = 'hello'; 23 | * 24 | * 25 | * @category PHP 26 | * @package PHP_CodeSniffer 27 | * @author Ryan Matthews 28 | * @license http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence 29 | * @version Release: 1.0.00 30 | * @link http://pear.php.net/package/PHP_CodeSniffer 31 | */ 32 | class Barracuda_Sniffs_Commenting_DisallowSingleLineMultiCommentsSniff implements PHP_CodeSniffer_Sniff 33 | { 34 | 35 | 36 | /** 37 | * Returns the token types that this sniff is interested in. 38 | * 39 | * @return array(int) 40 | */ 41 | public function register() 42 | { 43 | return array(T_COMMENT); 44 | 45 | }// end register() 46 | 47 | 48 | /** 49 | * Processes the tokens that this sniff is interested in. 50 | * 51 | * @param PHP_CodeSniffer_File $phpcsFile The file where the token was found. 52 | * @param int $stackPtr The position in the stack where 53 | * the token was found. 54 | * 55 | * @return void 56 | */ 57 | public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 58 | { 59 | $tokens = $phpcsFile->getTokens(); 60 | if (preg_match('/\/\*[^\n]*\*\//', $tokens[$stackPtr]['content'])) 61 | { 62 | $error = 'Multi line comments are prohibited on single lines; found %s'; 63 | $data = array(trim($tokens[$stackPtr]['content'])); 64 | $phpcsFile->addError($error, $stackPtr, 'Found', $data); 65 | } 66 | 67 | }// end process() 68 | } 69 | // end class 70 | 71 | -------------------------------------------------------------------------------- /.phpcs/Barracuda/Sniffs/Commenting/DocCommentSniff.php: -------------------------------------------------------------------------------- 1 | array( 13 | 'required' => false, 14 | 'allow_multiple' => false, 15 | ), 16 | '@package' => array( 17 | 'required' => false, 18 | 'allow_multiple' => false, 19 | ), 20 | '@subpackage' => array( 21 | 'required' => false, 22 | 'allow_multiple' => false, 23 | ), 24 | '@author' => array( 25 | 'required' => false, 26 | 'allow_multiple' => true, 27 | ), 28 | '@copyright' => array( 29 | 'required' => false, 30 | 'allow_multiple' => true, 31 | ), 32 | '@license' => array( 33 | 'required' => false, 34 | 'allow_multiple' => false, 35 | ), 36 | '@version' => array( 37 | 'required' => false, 38 | 'allow_multiple' => false, 39 | ), 40 | '@link' => array( 41 | 'required' => false, 42 | 'allow_multiple' => true, 43 | ), 44 | '@see' => array( 45 | 'required' => false, 46 | 'allow_multiple' => true, 47 | ), 48 | '@since' => array( 49 | 'required' => false, 50 | 'allow_multiple' => false, 51 | ), 52 | '@deprecated' => array( 53 | 'required' => false, 54 | 'allow_multiple' => false, 55 | ), 56 | ); 57 | 58 | 59 | /** 60 | * Returns an array of tokens this test wants to listen for. 61 | * 62 | * @return array 63 | */ 64 | public function register() 65 | { 66 | return array(T_OPEN_TAG); 67 | 68 | }//end register() 69 | 70 | 71 | /** 72 | * Processes this test, when one of its tokens is encountered. 73 | * 74 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 75 | * @param int $stackPtr The position of the current token 76 | * in the stack passed in $tokens. 77 | * 78 | * @return int 79 | */ 80 | public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 81 | { 82 | $tokens = $phpcsFile->getTokens(); 83 | 84 | // Find the next non whitespace token. 85 | $commentStart = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); 86 | 87 | // Allow declare() statements at the top of the file. 88 | if ($tokens[$commentStart]['code'] === T_DECLARE) { 89 | $semicolon = $phpcsFile->findNext(T_SEMICOLON, ($commentStart + 1)); 90 | $commentStart = $phpcsFile->findNext(T_WHITESPACE, ($semicolon + 1), null, true); 91 | } 92 | 93 | // Ignore vim header. 94 | if ($tokens[$commentStart]['code'] === T_COMMENT) { 95 | if (strstr($tokens[$commentStart]['content'], 'vim:') !== false) { 96 | $commentStart = $phpcsFile->findNext( 97 | T_WHITESPACE, 98 | ($commentStart + 1), 99 | null, 100 | true 101 | ); 102 | } 103 | } 104 | 105 | $errorToken = ($stackPtr + 1); 106 | if (isset($tokens[$errorToken]) === false) { 107 | $errorToken--; 108 | } 109 | 110 | if ($tokens[$commentStart]['code'] === T_CLOSE_TAG) { 111 | // We are only interested if this is the first open tag. 112 | return ($phpcsFile->numTokens + 1); 113 | } else if ($tokens[$commentStart]['code'] === T_COMMENT) { 114 | $error = 'You must use "/**" style comments for a file comment'; 115 | $phpcsFile->addError($error, $errorToken, 'WrongStyle'); 116 | $phpcsFile->recordMetric($stackPtr, 'File has doc comment', 'yes'); 117 | return ($phpcsFile->numTokens + 1); 118 | } else if ($commentStart === false 119 | || $tokens[$commentStart]['code'] !== T_DOC_COMMENT_OPEN_TAG 120 | ) { 121 | $phpcsFile->addError('Missing file doc comment', $errorToken, 'Missing'); 122 | $phpcsFile->recordMetric($stackPtr, 'File has doc comment', 'no'); 123 | return ($phpcsFile->numTokens + 1); 124 | } else { 125 | $phpcsFile->recordMetric($stackPtr, 'File has doc comment', 'yes'); 126 | } 127 | 128 | // Check the PHP Version, which should be in some text before the first tag. 129 | $commentEnd = $tokens[$commentStart]['comment_closer']; 130 | $found = false; 131 | for ($i = ($commentStart + 1); $i < $commentEnd; $i++) { 132 | if ($tokens[$i]['code'] === T_DOC_COMMENT_TAG) { 133 | break; 134 | } else if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING 135 | && strstr(strtolower($tokens[$i]['content']), 'php version') !== false 136 | ) { 137 | $found = true; 138 | break; 139 | } 140 | } 141 | 142 | if ($found === false) { 143 | $error = 'PHP version not specified'; 144 | $phpcsFile->addWarning($error, $commentEnd, 'MissingVersion'); 145 | } 146 | 147 | // Check each tag. 148 | $this->processTags($phpcsFile, $stackPtr, $commentStart); 149 | 150 | // Ignore the rest of the file. 151 | return ($phpcsFile->numTokens + 1); 152 | 153 | }//end process() 154 | 155 | 156 | /** 157 | * Processes each required or optional tag. 158 | * 159 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 160 | * @param int $stackPtr The position of the current token 161 | * in the stack passed in $tokens. 162 | * @param int $commentStart Position in the stack where the comment started. 163 | * 164 | * @return void 165 | */ 166 | protected function processTags(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $commentStart) 167 | { 168 | $tokens = $phpcsFile->getTokens(); 169 | 170 | if (get_class($this) === 'PEAR_Sniffs_Commenting_FileCommentSniff') { 171 | $docBlock = 'file'; 172 | } else { 173 | $docBlock = 'class'; 174 | } 175 | 176 | $commentEnd = $tokens[$commentStart]['comment_closer']; 177 | 178 | $foundTags = array(); 179 | $tagTokens = array(); 180 | foreach ($tokens[$commentStart]['comment_tags'] as $tag) { 181 | $name = $tokens[$tag]['content']; 182 | if (isset($this->tags[$name]) === false) { 183 | continue; 184 | } 185 | 186 | if ($this->tags[$name]['allow_multiple'] === false && isset($tagTokens[$name]) === true) { 187 | $error = 'Only one %s tag is allowed in a %s comment'; 188 | $data = array( 189 | $name, 190 | $docBlock, 191 | ); 192 | $phpcsFile->addError($error, $tag, 'Duplicate'.ucfirst(substr($name, 1)).'Tag', $data); 193 | } 194 | 195 | $foundTags[] = $name; 196 | $tagTokens[$name][] = $tag; 197 | 198 | $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd); 199 | if ($string === false || $tokens[$string]['line'] !== $tokens[$tag]['line']) { 200 | $error = 'Content missing for %s tag in %s comment'; 201 | $data = array( 202 | $name, 203 | $docBlock, 204 | ); 205 | $phpcsFile->addError($error, $tag, 'Empty'.ucfirst(substr($name, 1)).'Tag', $data); 206 | continue; 207 | } 208 | }//end foreach 209 | 210 | // Check if the tags are in the correct position. 211 | $pos = 0; 212 | foreach ($this->tags as $tag => $tagData) { 213 | if (isset($tagTokens[$tag]) === false) { 214 | if ($tagData['required'] === true) { 215 | $error = 'Missing %s tag in %s comment'; 216 | $data = array( 217 | $tag, 218 | $docBlock, 219 | ); 220 | $phpcsFile->addError($error, $commentEnd, 'Missing'.ucfirst(substr($tag, 1)).'Tag', $data); 221 | } 222 | 223 | continue; 224 | } else { 225 | $method = 'process'.substr($tag, 1); 226 | if (method_exists($this, $method) === true) { 227 | // Process each tag if a method is defined. 228 | call_user_func(array($this, $method), $phpcsFile, $tagTokens[$tag]); 229 | } 230 | } 231 | 232 | if (isset($foundTags[$pos]) === false) { 233 | break; 234 | } 235 | 236 | if ($foundTags[$pos] !== $tag) { 237 | $error = 'The tag in position %s should be the %s tag'; 238 | $data = array( 239 | ($pos + 1), 240 | $tag, 241 | ); 242 | $phpcsFile->addError($error, $tokens[$commentStart]['comment_tags'][$pos], ucfirst(substr($tag, 1)).'TagOrder', $data); 243 | } 244 | 245 | // Account for multiple tags. 246 | $pos++; 247 | while (isset($foundTags[$pos]) === true && $foundTags[$pos] === $tag) { 248 | $pos++; 249 | } 250 | }//end foreach 251 | 252 | }//end processTags() 253 | 254 | 255 | /** 256 | * Process the category tag. 257 | * 258 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 259 | * @param array $tags The tokens for these tags. 260 | * 261 | * @return void 262 | */ 263 | protected function processCategory(PHP_CodeSniffer_File $phpcsFile, array $tags) 264 | { 265 | $tokens = $phpcsFile->getTokens(); 266 | foreach ($tags as $tag) { 267 | if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) { 268 | // No content. 269 | continue; 270 | } 271 | 272 | $content = $tokens[($tag + 2)]['content']; 273 | if (PHP_CodeSniffer::isUnderscoreName($content) !== true) { 274 | $newContent = str_replace(' ', '_', $content); 275 | $nameBits = explode('_', $newContent); 276 | $firstBit = array_shift($nameBits); 277 | $newName = ucfirst($firstBit).'_'; 278 | foreach ($nameBits as $bit) { 279 | if ($bit !== '') { 280 | $newName .= ucfirst($bit).'_'; 281 | } 282 | } 283 | 284 | $error = 'Category name "%s" is not valid; consider "%s" instead'; 285 | $validName = trim($newName, '_'); 286 | $data = array( 287 | $content, 288 | $validName, 289 | ); 290 | $phpcsFile->addError($error, $tag, 'InvalidCategory', $data); 291 | } 292 | }//end foreach 293 | 294 | }//end processCategory() 295 | 296 | 297 | /** 298 | * Process the package tag. 299 | * 300 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 301 | * @param array $tags The tokens for these tags. 302 | * 303 | * @return void 304 | */ 305 | protected function processPackage(PHP_CodeSniffer_File $phpcsFile, array $tags) 306 | { 307 | $tokens = $phpcsFile->getTokens(); 308 | foreach ($tags as $tag) { 309 | if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) { 310 | // No content. 311 | continue; 312 | } 313 | 314 | $content = $tokens[($tag + 2)]['content']; 315 | if (PHP_CodeSniffer::isUnderscoreName($content) === true) { 316 | continue; 317 | } 318 | 319 | $newContent = str_replace(' ', '_', $content); 320 | $newContent = trim($newContent, '_'); 321 | $newContent = preg_replace('/[^A-Za-z_]/', '', $newContent); 322 | $nameBits = explode('_', $newContent); 323 | $firstBit = array_shift($nameBits); 324 | $newName = strtoupper($firstBit{0}).substr($firstBit, 1).'_'; 325 | foreach ($nameBits as $bit) { 326 | if ($bit !== '') { 327 | $newName .= strtoupper($bit{0}).substr($bit, 1).'_'; 328 | } 329 | } 330 | 331 | $error = 'Package name "%s" is not valid; consider "%s" instead'; 332 | $validName = trim($newName, '_'); 333 | $data = array( 334 | $content, 335 | $validName, 336 | ); 337 | $phpcsFile->addError($error, $tag, 'InvalidPackage', $data); 338 | }//end foreach 339 | 340 | }//end processPackage() 341 | 342 | 343 | /** 344 | * Process the subpackage tag. 345 | * 346 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 347 | * @param array $tags The tokens for these tags. 348 | * 349 | * @return void 350 | */ 351 | protected function processSubpackage(PHP_CodeSniffer_File $phpcsFile, array $tags) 352 | { 353 | $tokens = $phpcsFile->getTokens(); 354 | foreach ($tags as $tag) { 355 | if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) { 356 | // No content. 357 | continue; 358 | } 359 | 360 | $content = $tokens[($tag + 2)]['content']; 361 | if (PHP_CodeSniffer::isUnderscoreName($content) === true) { 362 | continue; 363 | } 364 | 365 | $newContent = str_replace(' ', '_', $content); 366 | $nameBits = explode('_', $newContent); 367 | $firstBit = array_shift($nameBits); 368 | $newName = strtoupper($firstBit{0}).substr($firstBit, 1).'_'; 369 | foreach ($nameBits as $bit) { 370 | if ($bit !== '') { 371 | $newName .= strtoupper($bit{0}).substr($bit, 1).'_'; 372 | } 373 | } 374 | 375 | $error = 'Subpackage name "%s" is not valid; consider "%s" instead'; 376 | $validName = trim($newName, '_'); 377 | $data = array( 378 | $content, 379 | $validName, 380 | ); 381 | $phpcsFile->addError($error, $tag, 'InvalidSubpackage', $data); 382 | }//end foreach 383 | 384 | }//end processSubpackage() 385 | 386 | /** 387 | * Author tag must be 'Firstname Lastname '. 388 | * 389 | * @param int $errorPos The line number where the error occurs. 390 | * 391 | * @return void 392 | */ 393 | protected function processAuthors($errorPos) 394 | { 395 | $authors = $this->commentParser->getAuthors(); 396 | if (empty($authors) === false) { 397 | $author = $authors[0]; 398 | $content = $author->getContent(); 399 | if (empty($content) === true) { 400 | $error = 'Content missing for @author tag in file comment'; 401 | $this->currentFile->addError($error, $errorPos, 'MissingAuthor'); 402 | } else if (preg_match('/^(.*) \<.*\@.*\>$/', $content) === 0) { 403 | $error = 'Expected "Firstname Lastname " for author tag'; 404 | $this->currentFile->addError($error, $errorPos, 'IncorrectAuthor'); 405 | } 406 | } 407 | }//end processAuthors() 408 | 409 | /** 410 | * Copyright tag must be in the form 'xxxx-xxxx Barracuda Networks, Inc.'. 411 | * 412 | * @param int $errorPos The line number where the error occurs. 413 | * 414 | * @return void 415 | */ 416 | protected function processCopyrights($errorPos) 417 | { 418 | $copyrights = $this->commentParser->getCopyrights(); 419 | $copyright = $copyrights[0]; 420 | if ($copyright !== null) { 421 | $content = $copyright->getContent(); 422 | if (empty($content) === true) { 423 | $error = 'Content missing for @copyright tag in file comment'; 424 | $this->currentFile->addError($error, $errorPos, 'MissingCopyright'); 425 | } else if (preg_match('/^([0-9]{4})(-[0-9]{4})? (\.*\)$/', $content) === 0) { 426 | $error = 'Expected "xxxx-xxxx Barracuda Networks, Inc." for copyright declaration'; 427 | $this->currentFile->addError($error, $errorPos, 'IncorrectCopyright'); 428 | } 429 | } 430 | }//end processCopyrights() 431 | 432 | /** 433 | * Process the license tag. 434 | * 435 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 436 | * @param array $tags The tokens for these tags. 437 | * 438 | * @return void 439 | */ 440 | protected function processLicense(PHP_CodeSniffer_File $phpcsFile, array $tags) 441 | { 442 | $tokens = $phpcsFile->getTokens(); 443 | foreach ($tags as $tag) { 444 | if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) { 445 | // No content. 446 | continue; 447 | } 448 | 449 | $content = $tokens[($tag + 2)]['content']; 450 | $matches = array(); 451 | preg_match('/^([^\s]+)\s+(.*)/', $content, $matches); 452 | if (count($matches) !== 3) { 453 | $error = '@license tag must contain a URL and a license name'; 454 | $phpcsFile->addError($error, $tag, 'IncompleteLicense'); 455 | } 456 | } 457 | 458 | }//end processLicense() 459 | 460 | 461 | /** 462 | * Process the version tag. 463 | * 464 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 465 | * @param array $tags The tokens for these tags. 466 | * 467 | * @return void 468 | */ 469 | protected function processVersion(PHP_CodeSniffer_File $phpcsFile, array $tags) 470 | { 471 | $tokens = $phpcsFile->getTokens(); 472 | foreach ($tags as $tag) { 473 | if ($tokens[($tag + 2)]['code'] !== T_DOC_COMMENT_STRING) { 474 | // No content. 475 | continue; 476 | } 477 | 478 | $content = $tokens[($tag + 2)]['content']; 479 | if (strstr($content, 'CVS:') === false 480 | && strstr($content, 'SVN:') === false 481 | && strstr($content, 'GIT:') === false 482 | && strstr($content, 'HG:') === false 483 | ) { 484 | $error = 'Invalid version "%s" in file comment; consider "CVS: " or "SVN: " or "GIT: " or "HG: " instead'; 485 | $data = array($content); 486 | $phpcsFile->addWarning($error, $tag, 'InvalidVersion', $data); 487 | } 488 | } 489 | 490 | }//end processVersion() 491 | 492 | 493 | }//end class 494 | -------------------------------------------------------------------------------- /.phpcs/Barracuda/Sniffs/Commenting/FunctionCommentSniff.php: -------------------------------------------------------------------------------- 1 | 10 | * @author Marc McIntyre 11 | * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 12 | * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 13 | * @link http://pear.php.net/package/PHP_CodeSniffer 14 | */ 15 | 16 | if (class_exists('PEAR_Sniffs_Commenting_FunctionCommentSniff', true) === false) { 17 | throw new PHP_CodeSniffer_Exception('Class PEAR_Sniffs_Commenting_FunctionCommentSniff not found'); 18 | } 19 | 20 | /** 21 | * Parses and verifies the doc comments for functions. 22 | * 23 | * @category PHP 24 | * @package PHP_CodeSniffer 25 | * @author Greg Sherwood 26 | * @author Marc McIntyre 27 | * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 28 | * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 29 | * @version Release: @package_version@ 30 | * @link http://pear.php.net/package/PHP_CodeSniffer 31 | */ 32 | class Barracuda_Sniffs_Commenting_FunctionCommentSniff extends PEAR_Sniffs_Commenting_FunctionCommentSniff 33 | { 34 | protected $allowedTypes = array( 35 | 'int', 36 | 'bool', 37 | ); 38 | 39 | public function __construct() 40 | { 41 | PHP_CodeSniffer::$allowedTypes = array_merge(PHP_CodeSniffer::$allowedTypes, $this->allowedTypes); 42 | } 43 | 44 | /** 45 | * Process the return comment of this function comment. 46 | * 47 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 48 | * @param int $stackPtr The position of the current token 49 | * in the stack passed in $tokens. 50 | * @param int $commentStart The position in the stack where the comment started. 51 | * 52 | * @return void 53 | */ 54 | protected function processReturn(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $commentStart) 55 | { 56 | $tokens = $phpcsFile->getTokens(); 57 | 58 | // Skip constructor and destructor. 59 | $methodName = $phpcsFile->getDeclarationName($stackPtr); 60 | $isSpecialMethod = ($methodName === '__construct' || $methodName === '__destruct'); 61 | 62 | $return = null; 63 | foreach ($tokens[$commentStart]['comment_tags'] as $tag) { 64 | if ($tokens[$tag]['content'] === '@return') { 65 | if ($return !== null) { 66 | $error = 'Only 1 @return tag is allowed in a function comment'; 67 | $phpcsFile->addError($error, $tag, 'DuplicateReturn'); 68 | return; 69 | } 70 | 71 | $return = $tag; 72 | } 73 | } 74 | 75 | if ($isSpecialMethod === true) { 76 | return; 77 | } 78 | 79 | if ($return !== null) { 80 | $content = $tokens[($return + 2)]['content']; 81 | if (empty($content) === true || $tokens[($return + 2)]['code'] !== T_DOC_COMMENT_STRING) { 82 | $error = 'Return type missing for @return tag in function comment'; 83 | $phpcsFile->addError($error, $return, 'MissingReturnType'); 84 | } else { 85 | // Check return type (can be multiple, separated by '|'). 86 | $typeNames = explode('|', $content); 87 | $suggestedNames = array(); 88 | foreach ($typeNames as $i => $typeName) { 89 | $suggestedName = PHP_CodeSniffer::suggestType($typeName); 90 | if (in_array($suggestedName, $suggestedNames) === false) { 91 | $suggestedNames[] = $suggestedName; 92 | } 93 | } 94 | 95 | $suggestedType = implode('|', $suggestedNames); 96 | if ($content !== $suggestedType) { 97 | $error = 'Expected "%s" but found "%s" for function return type'; 98 | $data = array( 99 | $suggestedType, 100 | $content, 101 | ); 102 | $fix = $phpcsFile->addFixableError($error, $return, 'InvalidReturn', $data); 103 | if ($fix === true) { 104 | $phpcsFile->fixer->replaceToken(($return + 2), $suggestedType); 105 | } 106 | } 107 | 108 | // If the return type is void, make sure there is 109 | // no return statement in the function. 110 | if ($content === 'void') { 111 | if (isset($tokens[$stackPtr]['scope_closer']) === true) { 112 | $endToken = $tokens[$stackPtr]['scope_closer']; 113 | for ($returnToken = $stackPtr; $returnToken < $endToken; $returnToken++) { 114 | if ($tokens[$returnToken]['code'] === T_CLOSURE) { 115 | $returnToken = $tokens[$returnToken]['scope_closer']; 116 | continue; 117 | } 118 | 119 | if ($tokens[$returnToken]['code'] === T_RETURN 120 | || $tokens[$returnToken]['code'] === T_YIELD 121 | ) { 122 | break; 123 | } 124 | } 125 | 126 | if ($returnToken !== $endToken) { 127 | // If the function is not returning anything, just 128 | // exiting, then there is no problem. 129 | $semicolon = $phpcsFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true); 130 | if ($tokens[$semicolon]['code'] !== T_SEMICOLON) { 131 | $error = 'Function return type is void, but function contains return statement'; 132 | $phpcsFile->addError($error, $return, 'InvalidReturnVoid'); 133 | } 134 | } 135 | }//end if 136 | } else if ($content !== 'mixed') { 137 | // If return type is not void, there needs to be a return statement 138 | // somewhere in the function that returns something. 139 | if (isset($tokens[$stackPtr]['scope_closer']) === true) { 140 | $endToken = $tokens[$stackPtr]['scope_closer']; 141 | $returnToken = $phpcsFile->findNext(array(T_RETURN, T_YIELD), $stackPtr, $endToken); 142 | if ($returnToken === false) { 143 | $error = 'Function return type is not void, but function has no return statement'; 144 | $phpcsFile->addError($error, $return, 'InvalidNoReturn'); 145 | } else { 146 | $semicolon = $phpcsFile->findNext(T_WHITESPACE, ($returnToken + 1), null, true); 147 | if ($tokens[$semicolon]['code'] === T_SEMICOLON) { 148 | $error = 'Function return type is not void, but function is returning void here'; 149 | $phpcsFile->addError($error, $returnToken, 'InvalidReturnNotVoid'); 150 | } 151 | } 152 | } 153 | }//end if 154 | }//end if 155 | } else { 156 | $error = 'Missing @return tag in function comment'; 157 | $phpcsFile->addError($error, $tokens[$commentStart]['comment_closer'], 'MissingReturn'); 158 | }//end if 159 | 160 | }//end processReturn() 161 | 162 | 163 | /** 164 | * Process any throw tags that this function comment has. 165 | * 166 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 167 | * @param int $stackPtr The position of the current token 168 | * in the stack passed in $tokens. 169 | * @param int $commentStart The position in the stack where the comment started. 170 | * 171 | * @return void 172 | */ 173 | protected function processThrows(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $commentStart) 174 | { 175 | $tokens = $phpcsFile->getTokens(); 176 | 177 | $throws = array(); 178 | foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) { 179 | if ($tokens[$tag]['content'] !== '@throws') { 180 | continue; 181 | } 182 | 183 | $exception = null; 184 | $comment = null; 185 | if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) { 186 | $matches = array(); 187 | preg_match('/([^\s]+)(?:\s+(.*))?/', $tokens[($tag + 2)]['content'], $matches); 188 | $exception = $matches[1]; 189 | if (isset($matches[2]) === true && trim($matches[2]) !== '') { 190 | $comment = $matches[2]; 191 | } 192 | } 193 | 194 | if ($exception === null) { 195 | $error = 'Exception type and comment missing for @throws tag in function comment'; 196 | $phpcsFile->addError($error, $tag, 'InvalidThrows'); 197 | } else if ($comment === null) { 198 | $error = 'Comment missing for @throws tag in function comment'; 199 | $phpcsFile->addError($error, $tag, 'EmptyThrows'); 200 | } else { 201 | // Any strings until the next tag belong to this comment. 202 | if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) { 203 | $end = $tokens[$commentStart]['comment_tags'][($pos + 1)]; 204 | } else { 205 | $end = $tokens[$commentStart]['comment_closer']; 206 | } 207 | 208 | for ($i = ($tag + 3); $i < $end; $i++) { 209 | if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) { 210 | $comment .= ' '.$tokens[$i]['content']; 211 | } 212 | } 213 | 214 | // Starts with a capital letter and ends with a fullstop. 215 | $firstChar = $comment{0}; 216 | if (strtoupper($firstChar) !== $firstChar) { 217 | $error = '@throws tag comment must start with a capital letter'; 218 | $phpcsFile->addError($error, ($tag + 2), 'ThrowsNotCapital'); 219 | } 220 | 221 | $lastChar = substr($comment, -1); 222 | if ($lastChar !== '.') { 223 | $error = '@throws tag comment must end with a full stop'; 224 | $phpcsFile->addError($error, ($tag + 2), 'ThrowsNoFullStop'); 225 | } 226 | }//end if 227 | }//end foreach 228 | 229 | }//end processThrows() 230 | 231 | 232 | /** 233 | * Process the function parameter comments. 234 | * 235 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 236 | * @param int $stackPtr The position of the current token 237 | * in the stack passed in $tokens. 238 | * @param int $commentStart The position in the stack where the comment started. 239 | * 240 | * @return void 241 | */ 242 | protected function processParams(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $commentStart) 243 | { 244 | $tokens = $phpcsFile->getTokens(); 245 | 246 | $params = array(); 247 | $maxType = 0; 248 | $maxVar = 0; 249 | foreach ($tokens[$commentStart]['comment_tags'] as $pos => $tag) { 250 | if ($tokens[$tag]['content'] !== '@param') { 251 | continue; 252 | } 253 | 254 | $type = ''; 255 | $typeSpace = 0; 256 | $var = ''; 257 | $varSpace = 0; 258 | $comment = ''; 259 | $commentLines = array(); 260 | if ($tokens[($tag + 2)]['code'] === T_DOC_COMMENT_STRING) { 261 | $matches = array(); 262 | preg_match('/([^$&]+)(?:((?:\$|&)[^\s]+)(?:(\s+)(.*))?)?/', $tokens[($tag + 2)]['content'], $matches); 263 | 264 | $typeLen = strlen($matches[1]); 265 | $type = trim($matches[1]); 266 | $typeSpace = ($typeLen - strlen($type)); 267 | $typeLen = strlen($type); 268 | if ($typeLen > $maxType) { 269 | $maxType = $typeLen; 270 | } 271 | 272 | if (isset($matches[2]) === true) { 273 | $var = $matches[2]; 274 | $varLen = strlen($var); 275 | if ($varLen > $maxVar) { 276 | $maxVar = $varLen; 277 | } 278 | 279 | if (isset($matches[4]) === true) { 280 | $varSpace = strlen($matches[3]); 281 | $comment = $matches[4]; 282 | $commentLines[] = array( 283 | 'comment' => $comment, 284 | 'token' => ($tag + 2), 285 | 'indent' => $varSpace, 286 | ); 287 | 288 | // Any strings until the next tag belong to this comment. 289 | if (isset($tokens[$commentStart]['comment_tags'][($pos + 1)]) === true) { 290 | $end = $tokens[$commentStart]['comment_tags'][($pos + 1)]; 291 | } else { 292 | $end = $tokens[$commentStart]['comment_closer']; 293 | } 294 | 295 | for ($i = ($tag + 3); $i < $end; $i++) { 296 | if ($tokens[$i]['code'] === T_DOC_COMMENT_STRING) { 297 | $indent = 0; 298 | if ($tokens[($i - 1)]['code'] === T_DOC_COMMENT_WHITESPACE) { 299 | $indent = strlen($tokens[($i - 1)]['content']); 300 | } 301 | 302 | $comment .= ' '.$tokens[$i]['content']; 303 | $commentLines[] = array( 304 | 'comment' => $tokens[$i]['content'], 305 | 'token' => $i, 306 | 'indent' => $indent, 307 | ); 308 | } 309 | } 310 | } else { 311 | $error = 'Missing parameter comment'; 312 | $phpcsFile->addError($error, $tag, 'MissingParamComment'); 313 | $commentLines[] = array('comment' => ''); 314 | }//end if 315 | } else { 316 | $error = 'Missing parameter name'; 317 | $phpcsFile->addError($error, $tag, 'MissingParamName'); 318 | }//end if 319 | } else { 320 | $error = 'Missing parameter type'; 321 | $phpcsFile->addError($error, $tag, 'MissingParamType'); 322 | }//end if 323 | 324 | $params[] = array( 325 | 'tag' => $tag, 326 | 'type' => $type, 327 | 'var' => $var, 328 | 'comment' => $comment, 329 | 'commentLines' => $commentLines, 330 | 'type_space' => $typeSpace, 331 | 'var_space' => $varSpace, 332 | ); 333 | }//end foreach 334 | 335 | $realParams = $phpcsFile->getMethodParameters($stackPtr); 336 | $foundParams = array(); 337 | 338 | foreach ($params as $pos => $param) { 339 | // If the type is empty, the whole line is empty. 340 | if ($param['type'] === '') { 341 | continue; 342 | } 343 | 344 | // Check the param type value. 345 | $typeNames = explode('|', $param['type']); 346 | foreach ($typeNames as $typeName) { 347 | $suggestedName = PHP_CodeSniffer::suggestType($typeName); 348 | if ($typeName !== $suggestedName) { 349 | $error = 'Expected "%s" but found "%s" for parameter type'; 350 | $data = array( 351 | $suggestedName, 352 | $typeName, 353 | ); 354 | 355 | $fix = $phpcsFile->addFixableError($error, $param['tag'], 'IncorrectParamVarName', $data); 356 | if ($fix === true) { 357 | $content = $suggestedName; 358 | $content .= str_repeat(' ', $param['type_space']); 359 | $content .= $param['var']; 360 | $content .= str_repeat(' ', $param['var_space']); 361 | if (isset($param['commentLines'][0]) === true) { 362 | $content .= $param['commentLines'][0]['comment']; 363 | } 364 | 365 | $phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content); 366 | } 367 | } else if (count($typeNames) === 1) { 368 | // Check type hint for array and custom type. 369 | $suggestedTypeHint = ''; 370 | if (strpos($suggestedName, 'array') !== false || substr($suggestedName, -2) === '[]') { 371 | $suggestedTypeHint = 'array'; 372 | } else if (strpos($suggestedName, 'callable') !== false) { 373 | $suggestedTypeHint = 'callable'; 374 | } else if (strpos($suggestedName, 'callback') !== false) { 375 | $suggestedTypeHint = 'callable'; 376 | } else if (in_array($typeName, PHP_CodeSniffer::$allowedTypes) === false) { 377 | $suggestedTypeHint = $suggestedName; 378 | } 379 | 380 | if ($suggestedTypeHint !== '' && isset($realParams[$pos]) === true) { 381 | $typeHint = $realParams[$pos]['type_hint']; 382 | if ($typeHint === '') { 383 | $error = 'Type hint "%s" missing for %s'; 384 | $data = array( 385 | $suggestedTypeHint, 386 | $param['var'], 387 | ); 388 | $phpcsFile->addError($error, $stackPtr, 'TypeHintMissing', $data); 389 | } else if ($typeHint !== substr($suggestedTypeHint, (strlen($typeHint) * -1))) { 390 | $error = 'Expected type hint "%s"; found "%s" for %s'; 391 | $data = array( 392 | $suggestedTypeHint, 393 | $typeHint, 394 | $param['var'], 395 | ); 396 | $phpcsFile->addError($error, $stackPtr, 'IncorrectTypeHint', $data); 397 | } 398 | } else if ($suggestedTypeHint === '' && isset($realParams[$pos]) === true) { 399 | $typeHint = $realParams[$pos]['type_hint']; 400 | if ($typeHint !== '') { 401 | $error = 'Unknown type hint "%s" found for %s'; 402 | $data = array( 403 | $typeHint, 404 | $param['var'], 405 | ); 406 | $phpcsFile->addError($error, $stackPtr, 'InvalidTypeHint', $data); 407 | } 408 | }//end if 409 | }//end if 410 | }//end foreach 411 | 412 | if ($param['var'] === '') { 413 | continue; 414 | } 415 | 416 | $foundParams[] = $param['var']; 417 | 418 | // Check number of spaces after the type. 419 | $spaces = ($maxType - strlen($param['type']) + 1); 420 | if ($param['type_space'] !== $spaces) { 421 | $error = 'Expected %s spaces after parameter type; %s found'; 422 | $data = array( 423 | $spaces, 424 | $param['type_space'], 425 | ); 426 | 427 | $fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamType', $data); 428 | if ($fix === true) { 429 | $phpcsFile->fixer->beginChangeset(); 430 | 431 | $content = $param['type']; 432 | $content .= str_repeat(' ', $spaces); 433 | $content .= $param['var']; 434 | $content .= str_repeat(' ', $param['var_space']); 435 | $content .= $param['commentLines'][0]['comment']; 436 | $phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content); 437 | 438 | // Fix up the indent of additional comment lines. 439 | foreach ($param['commentLines'] as $lineNum => $line) { 440 | if ($lineNum === 0 441 | || $param['commentLines'][$lineNum]['indent'] === 0 442 | ) { 443 | continue; 444 | } 445 | 446 | $newIndent = ($param['commentLines'][$lineNum]['indent'] + $spaces - $param['type_space']); 447 | $phpcsFile->fixer->replaceToken( 448 | ($param['commentLines'][$lineNum]['token'] - 1), 449 | str_repeat(' ', $newIndent) 450 | ); 451 | } 452 | 453 | $phpcsFile->fixer->endChangeset(); 454 | }//end if 455 | }//end if 456 | 457 | // Make sure the param name is correct. 458 | if (isset($realParams[$pos]) === true) { 459 | $realName = $realParams[$pos]['name']; 460 | if ($realName !== $param['var']) { 461 | $code = 'ParamNameNoMatch'; 462 | $data = array( 463 | $param['var'], 464 | $realName, 465 | ); 466 | 467 | $error = 'Doc comment for parameter %s does not match '; 468 | if (strtolower($param['var']) === strtolower($realName)) { 469 | $error .= 'case of '; 470 | $code = 'ParamNameNoCaseMatch'; 471 | } 472 | 473 | $error .= 'actual variable name %s'; 474 | 475 | $phpcsFile->addError($error, $param['tag'], $code, $data); 476 | } 477 | } else if (substr($param['var'], -4) !== ',...') { 478 | // We must have an extra parameter comment. 479 | $error = 'Superfluous parameter comment'; 480 | $phpcsFile->addError($error, $param['tag'], 'ExtraParamComment'); 481 | }//end if 482 | 483 | if ($param['comment'] === '') { 484 | continue; 485 | } 486 | 487 | // Check number of spaces after the var name. 488 | $spaces = ($maxVar - strlen($param['var']) + 1); 489 | if ($param['var_space'] !== $spaces) { 490 | $error = 'Expected %s spaces after parameter name; %s found'; 491 | $data = array( 492 | $spaces, 493 | $param['var_space'], 494 | ); 495 | 496 | $fix = $phpcsFile->addFixableError($error, $param['tag'], 'SpacingAfterParamName', $data); 497 | if ($fix === true) { 498 | $phpcsFile->fixer->beginChangeset(); 499 | 500 | $content = $param['type']; 501 | $content .= str_repeat(' ', $param['type_space']); 502 | $content .= $param['var']; 503 | $content .= str_repeat(' ', $spaces); 504 | $content .= $param['commentLines'][0]['comment']; 505 | $phpcsFile->fixer->replaceToken(($param['tag'] + 2), $content); 506 | 507 | // Fix up the indent of additional comment lines. 508 | foreach ($param['commentLines'] as $lineNum => $line) { 509 | if ($lineNum === 0 510 | || $param['commentLines'][$lineNum]['indent'] === 0 511 | ) { 512 | continue; 513 | } 514 | 515 | $newIndent = ($param['commentLines'][$lineNum]['indent'] + $spaces - $param['var_space']); 516 | $phpcsFile->fixer->replaceToken( 517 | ($param['commentLines'][$lineNum]['token'] - 1), 518 | str_repeat(' ', $newIndent) 519 | ); 520 | } 521 | 522 | $phpcsFile->fixer->endChangeset(); 523 | }//end if 524 | }//end if 525 | 526 | // Param comments must start with a capital letter and end with the full stop. 527 | $firstChar = $param['comment']{0}; 528 | if (preg_match('|\p{Lu}|u', $firstChar) === 0) { 529 | $error = 'Parameter comment must start with a capital letter'; 530 | $phpcsFile->addError($error, $param['tag'], 'ParamCommentNotCapital'); 531 | } 532 | 533 | $lastChar = substr($param['comment'], -1); 534 | if ($lastChar !== '.') { 535 | $error = 'Parameter comment must end with a full stop'; 536 | $phpcsFile->addError($error, $param['tag'], 'ParamCommentFullStop'); 537 | } 538 | }//end foreach 539 | 540 | $realNames = array(); 541 | foreach ($realParams as $realParam) { 542 | $realNames[] = $realParam['name']; 543 | } 544 | 545 | // Report missing comments. 546 | $diff = array_diff($realNames, $foundParams); 547 | foreach ($diff as $neededParam) { 548 | $error = 'Doc comment for parameter "%s" missing'; 549 | $data = array($neededParam); 550 | $phpcsFile->addError($error, $commentStart, 'MissingParamTag', $data); 551 | } 552 | 553 | }//end processParams() 554 | 555 | 556 | }//end class 557 | -------------------------------------------------------------------------------- /.phpcs/Barracuda/Sniffs/Commenting/SpaceAfterCommentSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 28 | 29 | $valid = false; 30 | 31 | if (preg_match('|//\s|', $tokens[$stackPtr]['content'])) 32 | { 33 | $valid = true; 34 | } 35 | 36 | if (preg_match('|\*[\s/]|', $tokens[$stackPtr]['content'])) 37 | { 38 | $valid = true; 39 | } 40 | 41 | if ($valid === false) 42 | { 43 | $error = 'A space is required at the start of the comment %s'; 44 | $data = array(trim($tokens[$stackPtr]['content'])); 45 | $phpcsFile->addError($error, $stackPtr, 'Found', $data); 46 | } 47 | 48 | }// end process() 49 | } 50 | // end class 51 | 52 | -------------------------------------------------------------------------------- /.phpcs/Barracuda/Sniffs/Commenting/VariableCommentSniff.php: -------------------------------------------------------------------------------- 1 | 10 | * @author Marc McIntyre 11 | * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 12 | * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 13 | * @link http://pear.php.net/package/PHP_CodeSniffer 14 | */ 15 | 16 | if (class_exists('PHP_CodeSniffer_Standards_AbstractVariableSniff', true) === false) { 17 | throw new PHP_CodeSniffer_Exception('Class PHP_CodeSniffer_Standards_AbstractVariableSniff not found'); 18 | } 19 | 20 | /** 21 | * Parses and verifies the variable doc comment. 22 | * 23 | * @category PHP 24 | * @package PHP_CodeSniffer 25 | * @author Greg Sherwood 26 | * @author Marc McIntyre 27 | * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600) 28 | * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 29 | * @version Release: @package_version@ 30 | * @link http://pear.php.net/package/PHP_CodeSniffer 31 | */ 32 | 33 | class Barracuda_Sniffs_Commenting_VariableCommentSniff extends PHP_CodeSniffer_Standards_AbstractVariableSniff 34 | { 35 | protected $allowedTypes = array( 36 | 'bool', 37 | 'int', 38 | ); 39 | 40 | public function __construct() 41 | { 42 | PHP_CodeSniffer::$allowedTypes = array_merge(PHP_CodeSniffer::$allowedTypes, $this->allowedTypes); 43 | } 44 | 45 | /** 46 | * Called to process class member vars. 47 | * 48 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 49 | * @param int $stackPtr The position of the current token 50 | * in the stack passed in $tokens. 51 | * 52 | * @return void 53 | */ 54 | public function processMemberVar(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 55 | { 56 | $tokens = $phpcsFile->getTokens(); 57 | $commentToken = array( 58 | T_COMMENT, 59 | T_DOC_COMMENT_CLOSE_TAG, 60 | ); 61 | 62 | $commentEnd = $phpcsFile->findPrevious($commentToken, $stackPtr); 63 | if ($commentEnd === false) { 64 | $phpcsFile->addError('Missing member variable doc comment', $stackPtr, 'Missing'); 65 | return; 66 | } 67 | 68 | if ($tokens[$commentEnd]['code'] === T_COMMENT) { 69 | $phpcsFile->addError('You must use "/**" style comments for a member variable comment', $stackPtr, 'WrongStyle'); 70 | return; 71 | } else if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG) { 72 | $phpcsFile->addError('Missing member variable doc comment', $stackPtr, 'Missing'); 73 | return; 74 | } else { 75 | // Make sure the comment we have found belongs to us. 76 | $commentFor = $phpcsFile->findNext(array(T_VARIABLE, T_CLASS, T_INTERFACE), ($commentEnd + 1)); 77 | if ($commentFor !== $stackPtr) { 78 | $phpcsFile->addError('Missing member variable doc comment', $stackPtr, 'Missing'); 79 | return; 80 | } 81 | } 82 | 83 | $commentStart = $tokens[$commentEnd]['comment_opener']; 84 | 85 | $foundVar = null; 86 | foreach ($tokens[$commentStart]['comment_tags'] as $tag) { 87 | if ($tokens[$tag]['content'] === '@var') { 88 | if ($foundVar !== null) { 89 | $error = 'Only one @var tag is allowed in a member variable comment'; 90 | $phpcsFile->addError($error, $tag, 'DuplicateVar'); 91 | } else { 92 | $foundVar = $tag; 93 | } 94 | } else if ($tokens[$tag]['content'] === '@see') { 95 | // Make sure the tag isn't empty. 96 | $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $tag, $commentEnd); 97 | if ($string === false || $tokens[$string]['line'] !== $tokens[$tag]['line']) { 98 | $error = 'Content missing for @see tag in member variable comment'; 99 | $phpcsFile->addError($error, $tag, 'EmptySees'); 100 | } 101 | } else { 102 | $error = '%s tag is not allowed in member variable comment'; 103 | $data = array($tokens[$tag]['content']); 104 | $phpcsFile->addWarning($error, $tag, 'TagNotAllowed', $data); 105 | }//end if 106 | }//end foreach 107 | 108 | // The @var tag is the only one we require. 109 | if ($foundVar === null) { 110 | $error = 'Missing @var tag in member variable comment'; 111 | $phpcsFile->addError($error, $commentEnd, 'MissingVar'); 112 | return; 113 | } 114 | 115 | $firstTag = $tokens[$commentStart]['comment_tags'][0]; 116 | if ($foundVar !== null && $tokens[$firstTag]['content'] !== '@var') { 117 | $error = 'The @var tag must be the first tag in a member variable comment'; 118 | $phpcsFile->addError($error, $foundVar, 'VarOrder'); 119 | } 120 | 121 | // Make sure the tag isn't empty and has the correct padding. 122 | $string = $phpcsFile->findNext(T_DOC_COMMENT_STRING, $foundVar, $commentEnd); 123 | if ($string === false || $tokens[$string]['line'] !== $tokens[$foundVar]['line']) { 124 | $error = 'Content missing for @var tag in member variable comment'; 125 | $phpcsFile->addError($error, $foundVar, 'EmptyVar'); 126 | return; 127 | } 128 | 129 | $varType = $tokens[($foundVar + 2)]['content']; 130 | $suggestedType = PHP_CodeSniffer::suggestType($varType); 131 | if ($varType !== $suggestedType) { 132 | $error = 'Expected "%s" but found "%s" for @var tag in member variable comment'; 133 | $data = array( 134 | $suggestedType, 135 | $varType, 136 | ); 137 | $phpcsFile->addError($error, ($foundVar + 2), 'IncorrectVarType', $data); 138 | } 139 | 140 | }//end processMemberVar() 141 | 142 | 143 | /** 144 | * Called to process a normal variable. 145 | * 146 | * Not required for this sniff. 147 | * 148 | * @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file where this token was found. 149 | * @param int $stackPtr The position where the double quoted 150 | * string was found. 151 | * 152 | * @return void 153 | */ 154 | protected function processVariable(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 155 | { 156 | 157 | }//end processVariable() 158 | 159 | 160 | /** 161 | * Called to process variables found in double quoted strings. 162 | * 163 | * Not required for this sniff. 164 | * 165 | * @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file where this token was found. 166 | * @param int $stackPtr The position where the double quoted 167 | * string was found. 168 | * 169 | * @return void 170 | */ 171 | protected function processVariableInString(PHP_CodeSniffer_File $phpcsFile, $stackPtr) 172 | { 173 | 174 | }//end processVariableInString() 175 | 176 | 177 | }//end class 178 | -------------------------------------------------------------------------------- /.phpcs/Barracuda/Sniffs/ControlStructures/ControlSignatureSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 30 | 31 | if (isset($tokens[($stackPtr + 1)]) === false) { 32 | return; 33 | } 34 | 35 | // Single space after the keyword. 36 | if (in_array($tokens[$stackPtr]['code'], array(T_CATCH, T_IF, T_WHILE, T_FOR, T_FOREACH, T_ELSEIF, T_SWITCH))) { 37 | $found = 1; 38 | if ($tokens[($stackPtr + 1)]['code'] !== T_WHITESPACE) { 39 | $found = 0; 40 | } else if ($tokens[($stackPtr + 1)]['content'] !== ' ') { 41 | if (strpos($tokens[($stackPtr + 1)]['content'], $phpcsFile->eolChar) !== false) { 42 | $found = 'newline'; 43 | } else { 44 | $found = strlen($tokens[($stackPtr + 1)]['content']); 45 | } 46 | } 47 | 48 | if ($found !== 1) { 49 | $error = 'Expected 1 space after %s keyword; %s found'; 50 | $data = array( 51 | strtoupper($tokens[$stackPtr]['content']), 52 | $found, 53 | ); 54 | 55 | $fix = $phpcsFile->addFixableError($error, $stackPtr, 'SpaceAfterKeyword', $data); 56 | if ($fix === true) { 57 | if ($found === 0) { 58 | $phpcsFile->fixer->addContent($stackPtr, ' '); 59 | } else { 60 | $phpcsFile->fixer->replaceToken(($stackPtr + 1), ' '); 61 | } 62 | } 63 | } 64 | } 65 | 66 | // Single newline after the keyword. 67 | if (in_array($tokens[$stackPtr]['code'], array(T_TRY, T_DO, T_ELSE)) 68 | && isset($tokens[$stackPtr]['scope_opener']) === true 69 | ) { 70 | $opener = $tokens[$stackPtr]['scope_opener']; 71 | $found = ($tokens[$opener]['line'] - $tokens[$stackPtr]['line']); 72 | if ($found !== 1) { 73 | $error = 'Expected 1 newline after % keyword; %s found'; 74 | $data = array( 75 | strtoupper($tokens[$stackPtr]['content']), 76 | $found, 77 | ); 78 | $fix = $phpcsFile->addFixableError($error, $opener, 'NewlineAfterKeyword', $data); 79 | if ($fix === true) { 80 | $phpcsFile->fixer->beginChangeset(); 81 | for ($i = ($stackPtr + 1); $i < $opener; $i++) { 82 | if ($found > 0 && $tokens[$i]['line'] === $tokens[$opener]['line']) { 83 | break; 84 | } 85 | 86 | $phpcsFile->fixer->replaceToken($i, ''); 87 | } 88 | 89 | $phpcsFile->fixer->addContent($stackPtr, $phpcsFile->eolChar); 90 | $phpcsFile->fixer->endChangeset(); 91 | } 92 | }//end if 93 | }//end if 94 | 95 | // Single newline after closing parenthesis. 96 | if (isset($tokens[$stackPtr]['parenthesis_closer']) === true 97 | && isset($tokens[$stackPtr]['scope_opener']) === true 98 | ) { 99 | $closer = $tokens[$stackPtr]['parenthesis_closer']; 100 | $opener = $tokens[$stackPtr]['scope_opener']; 101 | $found = ($tokens[$opener]['line'] - $tokens[$closer]['line']); 102 | if ($found !== 1) { 103 | $error = 'Expected 1 newline after closing parenthesis; %s found'; 104 | $fix = $phpcsFile->addFixableError($error, $opener, 'NewlineAfterCloseParenthesis', array($found)); 105 | if ($fix === true) { 106 | $phpcsFile->fixer->beginChangeset(); 107 | for ($i = ($closer + 1); $i < $opener; $i++) { 108 | if ($found > 0 && $tokens[$i]['line'] === $tokens[$opener]['line']) { 109 | break; 110 | } 111 | 112 | $phpcsFile->fixer->replaceToken($i, ''); 113 | } 114 | 115 | $phpcsFile->fixer->addContent($closer, $phpcsFile->eolChar); 116 | $phpcsFile->fixer->endChangeset(); 117 | } 118 | }//end if 119 | }//end if 120 | 121 | // Single newline after opening brace. 122 | if (isset($tokens[$stackPtr]['scope_opener']) === true) { 123 | $opener = $tokens[$stackPtr]['scope_opener']; 124 | for ($next = ($opener + 1); $next < $phpcsFile->numTokens; $next++) { 125 | $code = $tokens[$next]['code']; 126 | 127 | if ($code === T_WHITESPACE) { 128 | continue; 129 | } 130 | 131 | // Skip all empty tokens on the same line as the opener. 132 | if ($tokens[$next]['line'] === $tokens[$opener]['line'] 133 | && (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$code]) === true 134 | || $code === T_CLOSE_TAG) 135 | ) { 136 | continue; 137 | } 138 | 139 | // We found the first bit of a code, or a comment on the 140 | // following line. 141 | break; 142 | } 143 | 144 | $found = ($tokens[$next]['line'] - $tokens[$opener]['line']); 145 | if ($found !== 1) { 146 | $error = 'Expected 1 newline after opening brace; %s found'; 147 | $data = array($found); 148 | $fix = $phpcsFile->addFixableError($error, $opener, 'NewlineAfterOpenBrace', $data); 149 | if ($fix === true) { 150 | $phpcsFile->fixer->beginChangeset(); 151 | for ($i = ($opener + 1); $i < $next; $i++) { 152 | if ($found > 0 && $tokens[$i]['line'] === $tokens[$next]['line']) { 153 | break; 154 | } 155 | 156 | $phpcsFile->fixer->replaceToken($i, ''); 157 | } 158 | 159 | $phpcsFile->fixer->addContent($opener, $phpcsFile->eolChar); 160 | $phpcsFile->fixer->endChangeset(); 161 | } 162 | } 163 | } else if ($tokens[$stackPtr]['code'] === T_WHILE) { 164 | // Zero spaces after parenthesis closer. 165 | $closer = $tokens[$stackPtr]['parenthesis_closer']; 166 | $found = 0; 167 | if ($tokens[($closer + 1)]['code'] === T_WHITESPACE) { 168 | if (strpos($tokens[($closer + 1)]['content'], $phpcsFile->eolChar) !== false) { 169 | $found = 'newline'; 170 | } else { 171 | $found = strlen($tokens[($closer + 1)]['content']); 172 | } 173 | } 174 | 175 | if ($found !== 0) { 176 | $error = 'Expected 0 spaces before semicolon; %s found'; 177 | $data = array($found); 178 | $fix = $phpcsFile->addFixableError($error, $closer, 'SpaceBeforeSemicolon', $data); 179 | if ($fix === true) { 180 | $phpcsFile->fixer->replaceToken(($closer + 1), ''); 181 | } 182 | } 183 | }//end if 184 | 185 | // Only want to check multi-keyword structures from here on. 186 | if ($tokens[$stackPtr]['code'] === T_DO) { 187 | if (isset($tokens[$stackPtr]['scope_closer']) === false) { 188 | return; 189 | } 190 | 191 | $closer = $tokens[$stackPtr]['scope_closer']; 192 | } else if ($tokens[$stackPtr]['code'] === T_ELSE 193 | || $tokens[$stackPtr]['code'] === T_ELSEIF 194 | || $tokens[$stackPtr]['code'] === T_CATCH 195 | ) { 196 | $closer = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($stackPtr - 1), null, true); 197 | if ($closer === false || $tokens[$closer]['code'] !== T_CLOSE_CURLY_BRACKET) { 198 | return; 199 | } 200 | } else { 201 | return; 202 | } 203 | 204 | // Single space after closing brace. 205 | if ($tokens[$stackPtr]['code'] === T_DO) { 206 | $found = 1; 207 | if ($tokens[($closer + 1)]['code'] !== T_WHITESPACE) { 208 | $found = 0; 209 | } else if ($tokens[($closer + 1)]['content'] !== ' ') { 210 | if (strpos($tokens[($closer + 1)]['content'], $phpcsFile->eolChar) !== false) { 211 | $found = 'newline'; 212 | } else { 213 | $found = strlen($tokens[($closer + 1)]['content']); 214 | } 215 | } 216 | 217 | if ($found !== 1) { 218 | $error = 'Expected 1 space after closing brace; %s found'; 219 | $data = array($found); 220 | $fix = $phpcsFile->addFixableError($error, $closer, 'SpaceAfterCloseBrace', $data); 221 | if ($fix === true) { 222 | if ($found === 0) { 223 | $phpcsFile->fixer->addContent($closer, ' '); 224 | } else { 225 | $phpcsFile->fixer->replaceToken(($closer + 1), ' '); 226 | } 227 | } 228 | } 229 | } 230 | 231 | // Single newline after closing brace. 232 | if ($tokens[$stackPtr]['code'] !== T_DO) { 233 | for ($next = ($closer + 1); $next < $phpcsFile->numTokens; $next++) { 234 | $code = $tokens[$next]['code']; 235 | 236 | if ($code === T_WHITESPACE) { 237 | continue; 238 | } 239 | 240 | // Skip all empty tokens on the same line as the closer. 241 | if ($tokens[$next]['line'] === $tokens[$closer]['line'] 242 | && (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$code]) === true 243 | || $code === T_CLOSE_TAG) 244 | ) { 245 | continue; 246 | } 247 | 248 | // We found the first bit of a code, or a comment on the 249 | // following line. 250 | break; 251 | } 252 | 253 | $found = ($tokens[$next]['line'] - $tokens[$closer]['line']); 254 | if ($found !== 1) { 255 | $error = 'Expected 1 newline after closing brace; %s found'; 256 | $data = array($found); 257 | $fix = $phpcsFile->addFixableError($error, $closer, 'NewlineAfterCloseBrace', $data); 258 | if ($fix === true) { 259 | $phpcsFile->fixer->beginChangeset(); 260 | for ($i = ($closer + 1); $i < $next; $i++) { 261 | if ($found > 0 && $tokens[$i]['line'] === $tokens[$next]['line']) { 262 | break; 263 | } 264 | 265 | $phpcsFile->fixer->replaceToken($i, ''); 266 | } 267 | 268 | $phpcsFile->fixer->addContent($closer, $phpcsFile->eolChar); 269 | $phpcsFile->fixer->endChangeset(); 270 | } 271 | } 272 | } 273 | 274 | }//end process() 275 | } 276 | -------------------------------------------------------------------------------- /.phpcs/Barracuda/Sniffs/ControlStructures/NoInlineAssignmentSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 32 | 33 | $end_position = $tokens[$stackPtr]['parenthesis_closer']; 34 | 35 | // states: -1 = normal, 0 = start function call (probably), 1 = in function 36 | $function = -1; 37 | 38 | for ($position = $stackPtr; $position < $end_position; $position++) 39 | { 40 | if ($tokens[$position]['type'] == 'T_STRING') 41 | { 42 | $function = 0; 43 | continue; 44 | } 45 | 46 | if ($function === 0) 47 | { 48 | if ($tokens[$position]['type'] == 'T_OPEN_PARENTHESIS') 49 | { 50 | $function = 1; 51 | continue; 52 | } 53 | } 54 | elseif ($function !== 1) 55 | { 56 | $function = -1; 57 | if ($tokens[$position]['type'] == 'T_EQUAL') 58 | { 59 | $error = 'Inline assignment not allowed in if statements'; 60 | $phpcsFile->addError($error, $stackPtr, 'IncDecLeft'); 61 | return; 62 | } 63 | } 64 | } 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /.phpcs/Barracuda/Sniffs/Formatting/SpaceUnaryOperatorSniff.php: -------------------------------------------------------------------------------- 1 | getTokens(); 28 | 29 | // Check decrement / increment. 30 | if ($tokens[$stackPtr]['code'] === T_DEC || $tokens[$stackPtr]['code'] === T_INC) 31 | { 32 | $modifyLeft = substr($tokens[($stackPtr - 1)]['content'], 0, 1) === '$' || 33 | $tokens[($stackPtr + 1)]['content'] === ';'; 34 | 35 | if ($modifyLeft === true && $tokens[($stackPtr - 1)]['code'] === T_WHITESPACE) 36 | { 37 | $error = 'There must not be a single space before a unary operator statement'; 38 | $phpcsFile->addError($error, $stackPtr, 'IncDecLeft'); 39 | return; 40 | } 41 | 42 | if ($modifyLeft === false && !in_array(substr($tokens[($stackPtr + 1)]['content'], 0, 1), array('$', ','))) 43 | { 44 | $error = 'A unary operator statement must not be followed by a single space'; 45 | $phpcsFile->addError($error, $stackPtr, 'IncDecRight'); 46 | return; 47 | } 48 | } 49 | 50 | // Check "!" operator. 51 | if ($tokens[$stackPtr]['code'] === T_BOOLEAN_NOT && $tokens[$stackPtr + 1]['code'] === T_WHITESPACE) 52 | { 53 | $error = 'A unary operator statement must not be followed by a space'; 54 | $phpcsFile->addError($error, $stackPtr, 'BooleanNot'); 55 | return; 56 | } 57 | 58 | // Find the last syntax item to determine if this is an unary operator. 59 | $lastSyntaxItem = $phpcsFile->findPrevious( 60 | array(T_WHITESPACE), 61 | $stackPtr - 1, 62 | ($tokens[$stackPtr]['column']) * -1, 63 | true, 64 | null, 65 | true 66 | ); 67 | $operatorSuffixAllowed = in_array( 68 | $tokens[$lastSyntaxItem]['code'], 69 | array( 70 | T_LNUMBER, 71 | T_DNUMBER, 72 | T_CLOSE_PARENTHESIS, 73 | T_CLOSE_CURLY_BRACKET, 74 | T_CLOSE_SQUARE_BRACKET, 75 | T_VARIABLE, 76 | T_STRING, 77 | ) 78 | ); 79 | 80 | // Check plus / minus value assignments or comparisons. 81 | if ($tokens[$stackPtr]['code'] === T_MINUS || $tokens[$stackPtr]['code'] === T_PLUS) 82 | { 83 | if ($operatorSuffixAllowed === false 84 | && $tokens[($stackPtr + 1)]['code'] === T_WHITESPACE 85 | ) 86 | { 87 | $error = 'A unary operator statement must not be followed by a space'; 88 | $phpcsFile->addError($error, $stackPtr); 89 | } 90 | } 91 | 92 | } // end process() 93 | // end class 94 | } 95 | -------------------------------------------------------------------------------- /.phpcs/Barracuda/Sniffs/Functions/FunctionDeclarationSniff.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2006-2012 Squiz Pty Ltd (ABN 77 084 670 600) 11 | * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 12 | * @link http://pear.php.net/package/PHP_CodeSniffer 13 | */ 14 | 15 | if (class_exists('PEAR_Sniffs_Functions_FunctionDeclarationSniff', true) === false) { 16 | $error = 'Class PEAR_Sniffs_Functions_FunctionDeclarationSniff not found'; 17 | throw new PHP_CodeSniffer_Exception($error); 18 | } 19 | 20 | /** 21 | * Barracuda_Sniffs_Functions_MultiLineFunctionDeclarationSniff. 22 | * 23 | * Ensure single and multi-line function declarations are defined correctly. 24 | * 25 | * @category PHP 26 | * @package PHP_CodeSniffer 27 | * @author Greg Sherwood 28 | * @copyright 2006-2012 Squiz Pty Ltd (ABN 77 084 670 600) 29 | * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 30 | * @version Release: 1.5.0RC4 31 | * @link http://pear.php.net/package/PHP_CodeSniffer 32 | */ 33 | class Barracuda_Sniffs_Functions_FunctionDeclarationSniff extends PEAR_Sniffs_Functions_FunctionDeclarationSniff 34 | { 35 | 36 | 37 | /** 38 | * Processes multi-line declarations. 39 | * 40 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 41 | * @param int $stackPtr The position of the current token 42 | * in the stack passed in $tokens. 43 | * @param array $tokens The stack of tokens that make up 44 | * the file. 45 | * 46 | * @return void 47 | */ 48 | public function processMultiLineDeclaration(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $tokens) 49 | { 50 | // We need to work out how far indented the function 51 | // declaration itself is, so we can work out how far to 52 | // indent parameters. 53 | $functionIndent = 0; 54 | for ($i = ($stackPtr - 1); $i >= 0; $i--) { 55 | if ($tokens[$i]['line'] !== $tokens[$stackPtr]['line']) { 56 | $i++; 57 | break; 58 | } 59 | } 60 | 61 | if ($tokens[$i]['code'] === T_WHITESPACE) { 62 | $functionIndent = strlen($tokens[$i]['content']); 63 | } 64 | 65 | // The closing parenthesis must be on a new line, even 66 | // when checking abstract function definitions. 67 | $closeBracket = $tokens[$stackPtr]['parenthesis_closer']; 68 | $prev = $phpcsFile->findPrevious( 69 | T_WHITESPACE, 70 | ($closeBracket - 1), 71 | null, 72 | true 73 | ); 74 | 75 | if ($tokens[$closeBracket]['line'] !== $tokens[$tokens[$closeBracket]['parenthesis_opener']]['line']) { 76 | if ($tokens[$prev]['line'] === $tokens[$closeBracket]['line']) { 77 | $error = 'The closing parenthesis of a multi-line function declaration must be on a new line'; 78 | $fix = $phpcsFile->addFixableError($error, $closeBracket, 'CloseBracketLine'); 79 | if ($fix === true) { 80 | $phpcsFile->fixer->addNewlineBefore($closeBracket); 81 | } 82 | } 83 | } 84 | 85 | // If this is a closure and is using a USE statement, the closing 86 | // parenthesis we need to look at from now on is the closing parenthesis 87 | // of the USE statement. 88 | if ($tokens[$stackPtr]['code'] === T_CLOSURE) { 89 | $use = $phpcsFile->findNext(T_USE, ($closeBracket + 1), $tokens[$stackPtr]['scope_opener']); 90 | if ($use !== false) { 91 | $open = $phpcsFile->findNext(T_OPEN_PARENTHESIS, ($use + 1)); 92 | $closeBracket = $tokens[$open]['parenthesis_closer']; 93 | 94 | $prev = $phpcsFile->findPrevious( 95 | T_WHITESPACE, 96 | ($closeBracket - 1), 97 | null, 98 | true 99 | ); 100 | 101 | if ($tokens[$closeBracket]['line'] !== $tokens[$tokens[$closeBracket]['parenthesis_opener']]['line']) { 102 | if ($tokens[$prev]['line'] === $tokens[$closeBracket]['line']) { 103 | $error = 'The closing parenthesis of a multi-line use declaration must be on a new line'; 104 | $fix = $phpcsFile->addFixableError($error, $closeBracket, 'UseCloseBracketLine'); 105 | if ($fix === true) { 106 | $phpcsFile->fixer->addNewlineBefore($closeBracket); 107 | } 108 | } 109 | } 110 | }//end if 111 | }//end if 112 | 113 | // Each line between the parenthesis should be indented 4 spaces. 114 | $openBracket = $tokens[$stackPtr]['parenthesis_opener']; 115 | $lastLine = $tokens[$openBracket]['line']; 116 | for ($i = ($openBracket + 1); $i < $closeBracket; $i++) { 117 | if ($tokens[$i]['line'] !== $lastLine) { 118 | if ($i === $tokens[$stackPtr]['parenthesis_closer'] 119 | || ($tokens[$i]['code'] === T_WHITESPACE 120 | && (($i + 1) === $closeBracket 121 | || ($i + 1) === $tokens[$stackPtr]['parenthesis_closer'])) 122 | ) { 123 | // Closing braces need to be indented to the same level 124 | // as the function. 125 | $expectedIndent = $functionIndent; 126 | } else { 127 | $expectedIndent = ($functionIndent + $this->indent); 128 | } 129 | 130 | // We changed lines, so this should be a whitespace indent token. 131 | if ($tokens[$i]['code'] !== T_WHITESPACE) { 132 | $foundIndent = 0; 133 | } else { 134 | $foundIndent = strlen($tokens[$i]['content']); 135 | } 136 | 137 | if ($expectedIndent !== $foundIndent) { 138 | $error = 'Multi-line function declaration not indented correctly; expected %s spaces but found %s'; 139 | $data = array( 140 | $expectedIndent, 141 | $foundIndent, 142 | ); 143 | 144 | $fix = $phpcsFile->addFixableError($error, $i, 'Indent', $data); 145 | if ($fix === true) { 146 | $spaces = str_repeat(' ', $expectedIndent); 147 | if ($foundIndent === 0) { 148 | $phpcsFile->fixer->addContentBefore($i, $spaces); 149 | } else { 150 | $phpcsFile->fixer->replaceToken($i, $spaces); 151 | } 152 | } 153 | } 154 | 155 | $lastLine = $tokens[$i]['line']; 156 | }//end if 157 | 158 | if ($tokens[$i]['code'] === T_ARRAY || $tokens[$i]['code'] === T_OPEN_SHORT_ARRAY) { 159 | // Skip arrays as they have their own indentation rules. 160 | if ($tokens[$i]['code'] === T_OPEN_SHORT_ARRAY) { 161 | $i = $tokens[$i]['bracket_closer']; 162 | } else { 163 | $i = $tokens[$i]['parenthesis_closer']; 164 | } 165 | 166 | $lastLine = $tokens[$i]['line']; 167 | continue; 168 | } 169 | }//end for 170 | 171 | if (isset($tokens[$stackPtr]['scope_opener']) === true) { 172 | 173 | // any scope opener, we get the next token (should be EOL) 174 | $next = $tokens[($closeBracket + 1)]; 175 | 176 | // if the token is EOL, then no error 177 | if ($next['content'] === $phpcsFile->eolChar) 178 | { 179 | $length = -1; 180 | } elseif ($next['code'] == T_OPEN_CURLY_BRACKET) { 181 | $length = 0; 182 | } else { 183 | $length = strlen($next['content']); 184 | } 185 | 186 | // any length means a problem, even zero 187 | if ($length >= 0) { 188 | $data = array($length); 189 | $code = 'NewLineBeforeOpenBrace'; 190 | 191 | $error = 'There must be a newline before the opening brace of a multi-line function declaration; found '; 192 | 193 | // if whitespace, then report it 194 | if ($length > 0) { 195 | $error .= '%s spaces'; 196 | $code = 'SpaceBeforeOpenBrace'; 197 | } 198 | // otherwise, no space but still brace on same line 199 | else 200 | { 201 | $error .= ' opening brace'; 202 | } 203 | 204 | $fix = $phpcsFile->addFixableError($error, ($closeBracket + 1), $code, $data); 205 | if ($fix === true) { 206 | 207 | // remove whitespace 208 | if ($length > 0) 209 | { 210 | $phpcsFile->fixer->replaceToken($closeBracket + 1, ''); 211 | } 212 | 213 | // add the EOL token 214 | $phpcsFile->fixer->addContent($closeBracket, $phpcsFile->eolChar); 215 | } 216 | 217 | return; 218 | }//end if 219 | 220 | // And just in case they do something funny before the brace... 221 | $next = $phpcsFile->findNext( 222 | T_WHITESPACE, 223 | ($closeBracket + 1), 224 | null, 225 | true 226 | ); 227 | 228 | if ($next !== false && $tokens[$next]['code'] !== T_OPEN_CURLY_BRACKET) { 229 | $error = 'There must be a single space between the closing parenthesis and the opening brace of a multi-line function declaration'; 230 | $phpcsFile->addError($error, $next, 'NoSpaceBeforeOpenBrace'); 231 | } 232 | }//end if 233 | 234 | $openBracket = $tokens[$stackPtr]['parenthesis_opener']; 235 | $this->processBracket($phpcsFile, $openBracket, $tokens, 'function'); 236 | 237 | if ($tokens[$stackPtr]['code'] !== T_CLOSURE) { 238 | return; 239 | } 240 | 241 | $use = $phpcsFile->findNext(T_USE, ($tokens[$stackPtr]['parenthesis_closer'] + 1), $tokens[$stackPtr]['scope_opener']); 242 | if ($use === false) { 243 | return; 244 | } 245 | 246 | $openBracket = $phpcsFile->findNext(T_OPEN_PARENTHESIS, ($use + 1), null); 247 | $this->processBracket($phpcsFile, $openBracket, $tokens, 'use'); 248 | 249 | // Also check spacing. 250 | if ($tokens[($use - 1)]['code'] === T_WHITESPACE) { 251 | $gap = strlen($tokens[($use - 1)]['content']); 252 | } else { 253 | $gap = 0; 254 | } 255 | 256 | }//end processMultiLineDeclaration() 257 | 258 | 259 | /** 260 | * Processes the contents of a single set of brackets. 261 | * 262 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 263 | * @param int $openBracket The position of the open bracket 264 | * in the stack passed in $tokens. 265 | * @param array $tokens The stack of tokens that make up 266 | * the file. 267 | * @param string $type The type of the token the brackets 268 | * belong to (function or use). 269 | * 270 | * @return void 271 | */ 272 | public function processBracket(PHP_CodeSniffer_File $phpcsFile, $openBracket, $tokens, $type='function') 273 | { 274 | $errorPrefix = ''; 275 | if ($type === 'use') { 276 | $errorPrefix = 'Use'; 277 | } 278 | 279 | $closeBracket = $tokens[$openBracket]['parenthesis_closer']; 280 | 281 | // The open bracket should be the last thing on the line. 282 | if ($tokens[$openBracket]['line'] !== $tokens[$closeBracket]['line']) { 283 | $next = $phpcsFile->findNext(T_WHITESPACE, ($openBracket + 1), null, true); 284 | if ($tokens[$next]['line'] !== ($tokens[$openBracket]['line'] + 1)) { 285 | $error = 'The first parameter of a multi-line '.$type.' declaration must be on the line after the opening bracket'; 286 | $phpcsFile->addError($error, $next, $errorPrefix.'FirstParamSpacing'); 287 | } 288 | } 289 | 290 | // Each line between the brackets should contain a single parameter. 291 | $lastCommaLine = null; 292 | for ($i = ($openBracket + 1); $i < $closeBracket; $i++) { 293 | // Skip brackets, like arrays, as they can contain commas. 294 | if (isset($tokens[$i]['parenthesis_opener']) === true) { 295 | $i = $tokens[$i]['parenthesis_closer']; 296 | continue; 297 | } 298 | 299 | if ($tokens[$i]['code'] === T_COMMA) { 300 | if ($lastCommaLine !== null && $lastCommaLine === $tokens[$i]['line']) { 301 | $error = 'Multi-line '.$type.' declarations must define one parameter per line'; 302 | $phpcsFile->addError($error, $i, $errorPrefix.'OneParamPerLine'); 303 | } else { 304 | // Comma must be the last thing on the line. 305 | $next = $phpcsFile->findNext(T_WHITESPACE, ($i + 1), null, true); 306 | if ($tokens[$next]['line'] !== ($tokens[$i]['line'] + 1)) { 307 | $error = 'Commas in multi-line '.$type.' declarations must be the last content on a line'; 308 | $phpcsFile->addError($error, $next, $errorPrefix.'ContentAfterComma'); 309 | } 310 | } 311 | 312 | $lastCommaLine = $tokens[$i]['line']; 313 | } 314 | } 315 | 316 | }//end processBracket() 317 | 318 | /** 319 | * Processes single-line declarations. 320 | * 321 | * Just uses the Generic BSD-Allman brace sniff. 322 | * 323 | * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 324 | * @param int $stackPtr The position of the current token 325 | * in the stack passed in $tokens. 326 | * @param array $tokens The stack of tokens that make up 327 | * the file. 328 | * 329 | * @return void 330 | */ 331 | public function processSingleLineDeclaration(PHP_CodeSniffer_File $phpcsFile, $stackPtr, $tokens) 332 | { 333 | if (class_exists('Generic_Sniffs_Functions_OpeningFunctionBraceBsdAllmanSniff', true) === false) { 334 | throw new PHP_CodeSniffer_Exception('Class Generic_Sniffs_Functions_OpeningFunctionBraceBsdAllmanSniff not found'); 335 | } 336 | 337 | $sniff = new Generic_Sniffs_Functions_OpeningFunctionBraceBsdAllmanSniff(); 338 | 339 | $sniff->process($phpcsFile, $stackPtr); 340 | 341 | }//end processSingleLineDeclaration() 342 | 343 | 344 | }//end class 345 | 346 | ?> 347 | -------------------------------------------------------------------------------- /.phpcs/Barracuda/bootstrap.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barracudanetworks/ArchiveStream-php/c66e749de07df3e3c8556c0c67fa59729f134e1a/.phpcs/Barracuda/bootstrap.php -------------------------------------------------------------------------------- /.phpcs/Barracuda/ruleset.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PSR2 with Barracuda exceptions 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 48 | 49 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [Unreleased] 8 | 9 | ## [1.0.4] - 2016-09-01 10 | ### Fixed 11 | - Adding a directory to ZIP file created a file instead 12 | 13 | ## [1.0.3] - 2016-05-18 14 | ### Fixed 15 | - GMP detection in PHP 5.6+ (#24) 16 | 17 | ## [1.0.2] - 2016-01-07 18 | ### Changed 19 | - Switched `ob_end_clean()` to `ob_end_flush()` to prevent errors if data was 20 | previously added to the buffer. 21 | 22 | ## [1.0.1] - 2016-01-06 23 | ### Fixed 24 | - Added `php-mbstring` requirement to `composer.json` 25 | 26 | ## [1.0.0] - 2015-12-04 27 | ### Changed 28 | - Namespaced classes under `Barracuda\ArchiveStream` 29 | - Added support for composer autoloading 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Original work Copyright 2007-2009 Paul Duncan 2 | Modified work Copyright 2013 Barracuda Networks, Inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ArchiveStream 1.0.7 2 | [![Code Climate](https://codeclimate.com/github/barracudanetworks/ArchiveStream-php/badges/gpa.svg)](https://codeclimate.com/github/barracudanetworks/ArchiveStream-php) 3 | 4 | A library for dynamically streaming dynamic tar or zip files without the need to have the complete file stored on the server. You can specify if you want a tar or a zip; or if you want to have the library figure out the best option based on the user agent string. 5 | 6 | ## Options 7 | 8 | ```php 9 | /** 10 | * Construct Parameters: 11 | * 12 | * $name - Name of output file (optional). 13 | * $opt - Hash of archive options (optional, see "Archive Options" 14 | * below). 15 | * $output_stream - Output stream for archive (optional - defaults to php://output) 16 | * 17 | * Archive Options: 18 | * 19 | * comment - Comment for this archive. (zip only) 20 | * content_type - HTTP Content-Type. Defaults to 'application/x-zip'. 21 | * content_disposition - HTTP Content-Disposition. Defaults to 22 | * 'attachment; filename=\"FILENAME\"', where 23 | * FILENAME is the specified filename. 24 | * large_file_size - Size, in bytes, of the largest file to try 25 | * and load into memory (used by 26 | * add_file_from_path()). Large files may also 27 | * be compressed differently; see the 28 | * 'large_file_method' option. 29 | * send_http_headers - Boolean indicating whether or not to send 30 | * the HTTP headers for this file. 31 | * large_files_only - Boolean indicating whether or not to assume 32 | * that all files we are sending are large. 33 | * 34 | * File Options: 35 | * time - Last-modified timestamp (seconds since the epoch) of 36 | * this file. Defaults to the current time. 37 | * comment - Comment related to this file. (zip only) 38 | * type - Type of file object. (tar only) 39 | * 40 | * 41 | * Note that content_type and content_disposition do nothing if you are 42 | * not sending HTTP headers. 43 | * 44 | * Large File Support: 45 | * 46 | * By default, the method add_file_from_path() will send send files 47 | * larger than 20 megabytes along raw rather than attempting to 48 | * compress them. You can change both the maximum size and the 49 | * compression behavior using the large_file_* options above, with the 50 | * following caveats: 51 | * 52 | * * For "small" files (e.g. files smaller than large_file_size), the 53 | * memory use can be up to twice that of the actual file. In other 54 | * words, adding a 10 megabyte file to the archive could potentially 55 | * occupty 20 megabytes of memory. 56 | * 57 | * * For "large" files we use the store method, meaning that the file is 58 | * not compressed at all, this is because there is not currenly a good way 59 | * to compress a stream within PHP 60 | * 61 | * Notes: 62 | * 63 | * If you do not set a filename, then this library _DOES NOT_ send HTTP 64 | * headers by default. This behavior is to allow software to send its 65 | * own headers (including the filename), and still use this library. 66 | */ 67 | ``` 68 | 69 | ## Usage 70 | 71 | ### Stream whole file at a time 72 | 73 | A fast and simple streaming archive files for PHP. Here's a simple example: 74 | 75 | ```php 76 | // Create a new archive stream object (tar or zip depending on user agent) 77 | $zip = \Barracuda\ArchiveStream\Archive::instance_by_useragent('example'); 78 | 79 | // Create a file named 'hello.txt' 80 | $zip->add_file('hello.txt', 'This is the contents of hello.txt'); 81 | 82 | // Add a file named 'image.jpg' from a local file 'path/to/image.jpg' 83 | $zip->add_file_from_path('image.jpg', 'path/to/image.jpg'); 84 | 85 | // Finish the zip stream 86 | $zip->finish(); 87 | ``` 88 | 89 | ### Stream each file in parts 90 | 91 | This method can be used to serve files of any size (GB, TB). 92 | 93 | ```php 94 | // Create a new archive stream object (tar or zip depending on user agent) 95 | $zip = \Barracuda\ArchiveStream\Archive::instance_by_useragent('example'); 96 | 97 | // Initiate the stream transfer of some_image.jpg with size 324134 98 | $zip->init_file_stream_transfer('some_image.jpg', 324134); 99 | 100 | // Stream part of the contents of some_image.jpg 101 | // This method should be called as many times as needed to send all of its data 102 | $zip->stream_file_part($data); 103 | 104 | // Send data descriptor header for file 105 | $zip->complete_file_stream(); 106 | 107 | // Other files can be added here, simply run the three commands above for each file that is being sent 108 | 109 | // Explicitly add a directory to the zip (doesn't recurse - useful for empty 110 | // directories) 111 | $zip->add_directory('foo'); 112 | $zip->add_directory('foo/bar'); 113 | 114 | // Finish the zip stream 115 | $zip->finish(); 116 | ``` 117 | 118 | ## Installation 119 | 120 | Simply run `composer require barracudanetworks/archivestream-php` inside your project. 121 | 122 | ## Requirements 123 | 124 | * PHP >=5.1.2 (or the [Hash extension](http://php.net/hash)). 125 | * gmp extension 126 | 127 | ## Limitations 128 | 129 | * Only the Zip64 (version 4.5 of the Zip specification) format is supported. 130 | * Files cannot be resumed if a download fails before finishing. 131 | 132 | ### Other 133 | 134 | You can also add comments, modify file timestamps, and customize (or 135 | disable) the HTTP headers. See the class file for details. 136 | 137 | ## Contributors 138 | - Paul Duncan - Original author 139 | - Daniel Bergey 140 | - Andy Blyler 141 | - Tony Blyler 142 | - Andrew Borek 143 | - Rafael Corral 144 | - John Maguire 145 | - Zachery Stuart 146 | 147 | ## License 148 | 149 | Original work Copyright 2007-2009 Paul Duncan 150 | Modified work Copyright 2013-2015 Barracuda Networks, Inc. 151 | 152 | Licensed under the MIT License 153 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "barracudanetworks/archivestream-php", 3 | "type": "library", 4 | "description": "A library for dynamically streaming dynamic tar or zip files without the need to have the complete file stored on the server.", 5 | "keywords": ["zip", "tar", "archive", "stream", "php"], 6 | "homepage": "https://github.com/barracudanetworks/ArchiveStream-php", 7 | "license": "MIT", 8 | "require": { 9 | "php": ">=5.1.2", 10 | "ext-gmp": "*", 11 | "ext-mbstring": "*" 12 | }, 13 | "autoload": { 14 | "psr-4": { 15 | "Barracuda\\ArchiveStream\\": "src/" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /extras/README: -------------------------------------------------------------------------------- 1 | Based on PKZIP appnotes, which are included here. 2 | 3 | -------------------------------------------------------------------------------- /src/Archive.php: -------------------------------------------------------------------------------- 1 | output_stream = $output_stream; 71 | 72 | // save options 73 | $this->opt = $opt; 74 | 75 | // if a $base_path was passed set the protected property with that value, otherwise leave it empty 76 | $this->container_dir_name = isset($base_path) ? $base_path . '/' : ''; 77 | 78 | // set large file defaults: size = 20 megabytes, method = store 79 | if (!isset($this->opt['large_file_size'])) 80 | { 81 | $this->opt['large_file_size'] = 20 * 1024 * 1024; 82 | } 83 | 84 | if (!isset($this->opt['large_files_only'])) 85 | { 86 | $this->opt['large_files_only'] = false; 87 | } 88 | 89 | $this->output_name = $name; 90 | if ($name || isset($opt['send_http_headers'])) 91 | { 92 | $this->need_headers = true; 93 | } 94 | 95 | // turn off output buffering 96 | while (ob_get_level() > 0) 97 | { 98 | // throw away any output left in the buffer 99 | ob_end_clean(); 100 | } 101 | } 102 | 103 | /** 104 | * Create instance based on useragent string 105 | * 106 | * @param string $base_filename A name for the resulting archive (without an extension). 107 | * @param array $opt Map of archive options (see above for list). 108 | * @param resource $output_stream Output stream for archive contents. 109 | * @return Zip|Tar for either zip or tar 110 | */ 111 | public static function instance_by_useragent( 112 | $base_filename = null, 113 | array $opt = array(), 114 | $output_stream = null 115 | ) 116 | { 117 | if ($output_stream === null) { 118 | // Output stream for cli and web server 119 | $output_stream = fopen('php://output', 'w'); 120 | } 121 | 122 | $user_agent = (isset($_SERVER['HTTP_USER_AGENT']) ? strtolower($_SERVER['HTTP_USER_AGENT']) : ''); 123 | 124 | // detect windows and use zip 125 | if (strpos($user_agent, 'windows') !== false) 126 | { 127 | $filename = (($base_filename === null) ? null : $base_filename . '.zip'); 128 | return new Zip($filename, $opt, $base_filename, $output_stream); 129 | } 130 | // fallback to tar 131 | else 132 | { 133 | $filename = (($base_filename === null) ? null : $base_filename . '.tar'); 134 | return new Tar($filename, $opt, $base_filename, $output_stream); 135 | } 136 | } 137 | 138 | /** 139 | * Add file to the archive 140 | * 141 | * Parameters: 142 | * 143 | * @param string $name Path of file in the archive (including directory). 144 | * @param string $data Contents of the file. 145 | * @param array $opt Map of file options (see above for list). 146 | * @return void 147 | */ 148 | public function add_file($name, $data, array $opt = array()) 149 | { 150 | // calculate header attributes 151 | $this->meth_str = 'deflate'; 152 | $meth = 0x08; 153 | 154 | // send file header 155 | $this->init_file_stream_transfer($name, strlen($data), $opt, $meth); 156 | 157 | // send data 158 | $this->stream_file_part($data, $single_part = true); 159 | 160 | // complete the file stream 161 | $this->complete_file_stream(); 162 | } 163 | 164 | /** 165 | * Add file by path 166 | * 167 | * @param string $name Name of file in archive (including directory path). 168 | * @param string $path Path to file on disk (note: paths should be encoded using 169 | * UNIX-style forward slashes -- e.g '/path/to/some/file'). 170 | * @param array $opt Map of file options (see above for list). 171 | * @return void 172 | */ 173 | public function add_file_from_path($name, $path, array $opt = array()) 174 | { 175 | if ($this->opt['large_files_only'] || $this->is_large_file($path)) 176 | { 177 | // file is too large to be read into memory; add progressively 178 | $this->add_large_file($name, $path, $opt); 179 | } 180 | else 181 | { 182 | // file is small enough to read into memory; read file contents and 183 | // handle with add_file() 184 | $data = file_get_contents($path); 185 | $this->add_file($name, $data, $opt); 186 | } 187 | } 188 | 189 | /** 190 | * Log an error to be added to the error log in the archive. 191 | * 192 | * @param string $message Error text to add to the log file. 193 | * @return void 194 | */ 195 | public function push_error($message) 196 | { 197 | $this->errors[] = (string) $message; 198 | } 199 | 200 | /** 201 | * Set whether or not all elements in the archive will be placed within one container directory. 202 | * 203 | * @param bool $bool True to use contaner directory, false to prevent using one. Defaults to false. 204 | * @return void 205 | */ 206 | public function set_use_container_dir($bool = false) 207 | { 208 | $this->use_container_dir = (bool) $bool; 209 | } 210 | 211 | /** 212 | * Set the name filename for the error log file when it's added to the archive 213 | * 214 | * @param string $name Filename for the error log. 215 | * @return void 216 | */ 217 | public function set_error_log_filename($name) 218 | { 219 | if (isset($name)) 220 | { 221 | $this->error_log_filename = (string) $name; 222 | } 223 | } 224 | 225 | /** 226 | * Set the first line of text in the error log file 227 | * 228 | * @param string $msg Message to display on the first line of the error log file. 229 | * @return void 230 | */ 231 | public function set_error_header_text($msg) 232 | { 233 | if (isset($msg)) 234 | { 235 | $this->error_header_text = (string) $msg; 236 | } 237 | } 238 | 239 | /*************************** 240 | * PRIVATE UTILITY METHODS * 241 | ***************************/ 242 | 243 | /** 244 | * Add a large file from the given path 245 | * 246 | * @param string $name Name of file in archive (including directory path). 247 | * @param string $path Path to file on disk (note: paths should be encoded using 248 | * UNIX-style forward slashes -- e.g '/path/to/some/file'). 249 | * @param array $opt Map of file options (see above for list). 250 | * @return void 251 | */ 252 | protected function add_large_file($name, $path, array $opt = array()) 253 | { 254 | // send file header 255 | $this->init_file_stream_transfer($name, filesize($path), $opt); 256 | 257 | // open input file 258 | $fh = fopen($path, 'rb'); 259 | 260 | // send file blocks 261 | while ($data = fread($fh, $this->block_size)) 262 | { 263 | // send data 264 | $this->stream_file_part($data); 265 | } 266 | 267 | // close input file 268 | fclose($fh); 269 | 270 | // complete the file stream 271 | $this->complete_file_stream(); 272 | } 273 | 274 | /** 275 | * Is this file larger than large_file_size? 276 | * 277 | * @param string $path Path to file on disk. 278 | * @return bool True if large, false if small. 279 | */ 280 | protected function is_large_file($path) 281 | { 282 | $st = stat($path); 283 | return ($this->opt['large_file_size'] > 0) && ($st['size'] > $this->opt['large_file_size']); 284 | } 285 | 286 | /** 287 | * Send HTTP headers for this stream. 288 | * 289 | * @return void 290 | */ 291 | private function send_http_headers() 292 | { 293 | // grab options 294 | $opt = $this->opt; 295 | 296 | // grab content type from options 297 | if (isset($opt['content_type'])) 298 | { 299 | $content_type = $opt['content_type']; 300 | } 301 | else 302 | { 303 | $content_type = 'application/x-zip'; 304 | } 305 | 306 | // grab content type encoding from options and append to the content type option 307 | if (isset($opt['content_type_encoding'])) 308 | { 309 | $content_type .= '; charset=' . $opt['content_type_encoding']; 310 | } 311 | 312 | // grab content disposition 313 | $disposition = 'attachment'; 314 | if (isset($opt['content_disposition'])) 315 | { 316 | $disposition = $opt['content_disposition']; 317 | } 318 | 319 | if ($this->output_name) 320 | { 321 | $disposition .= "; filename=\"{$this->output_name}\""; 322 | } 323 | 324 | $headers = array( 325 | 'Content-Type' => $content_type, 326 | 'Content-Disposition' => $disposition, 327 | 'Pragma' => 'public', 328 | 'Cache-Control' => 'public, must-revalidate', 329 | 'Content-Transfer-Encoding' => 'binary', 330 | ); 331 | 332 | foreach ($headers as $key => $val) 333 | { 334 | header("$key: $val"); 335 | } 336 | } 337 | 338 | /** 339 | * Send string, sending HTTP headers if necessary. 340 | * 341 | * @param string $data Data to send. 342 | * @return void 343 | */ 344 | protected function send($data) 345 | { 346 | if ($this->need_headers) 347 | { 348 | $this->send_http_headers(); 349 | } 350 | 351 | $this->need_headers = false; 352 | 353 | do 354 | { 355 | $result = fwrite($this->output_stream, $data); 356 | $data = substr($data, $result); 357 | fflush($this->output_stream); 358 | } while ($data && $result !== false); 359 | } 360 | 361 | /** 362 | * If errors were encountered, add an error log file to the end of the archive 363 | * @return void 364 | */ 365 | public function add_error_log() 366 | { 367 | if (!empty($this->errors)) 368 | { 369 | $msg = $this->error_header_text; 370 | foreach ($this->errors as $err) 371 | { 372 | $msg .= "\r\n\r\n" . $err; 373 | } 374 | 375 | // stash current value so it can be reset later 376 | $temp = $this->use_container_dir; 377 | 378 | // set to false to put the error log file in the root instead of the container directory, if we're using one 379 | $this->use_container_dir = false; 380 | 381 | $this->add_file($this->error_log_filename, $msg); 382 | 383 | // reset to original value and dump the temp variable 384 | $this->use_container_dir = $temp; 385 | unset($temp); 386 | } 387 | } 388 | 389 | /** 390 | * Convert a UNIX timestamp to a DOS timestamp. 391 | * 392 | * @param int $when Unix timestamp. 393 | * @return string DOS timestamp 394 | */ 395 | protected function dostime($when = 0) 396 | { 397 | // get date array for timestamp 398 | $d = getdate($when); 399 | 400 | // set lower-bound on dates 401 | if ($d['year'] < 1980) 402 | { 403 | $d = array( 404 | 'year' => 1980, 'mon' => 1, 'mday' => 1, 405 | 'hours' => 0, 'minutes' => 0, 'seconds' => 0 406 | ); 407 | } 408 | 409 | // remove extra years from 1980 410 | $d['year'] -= 1980; 411 | 412 | // return date string 413 | return ($d['year'] << 25) | ($d['mon'] << 21) | ($d['mday'] << 16) | 414 | ($d['hours'] << 11) | ($d['minutes'] << 5) | ($d['seconds'] >> 1); 415 | } 416 | 417 | /** 418 | * Split a 64-bit integer to two 32-bit integers. 419 | * 420 | * @param mixed $value Integer or GMP resource. 421 | * @return array Containing high and low 32-bit integers. 422 | */ 423 | protected function int64_split($value) 424 | { 425 | // gmp 426 | if (is_resource($value) || $value instanceof GMP) 427 | { 428 | $hex = str_pad(gmp_strval($value, 16), 16, '0', STR_PAD_LEFT); 429 | 430 | $high = $this->gmp_convert(substr($hex, 0, 8), 16, 10); 431 | $low = $this->gmp_convert(substr($hex, 8, 8), 16, 10); 432 | } 433 | // int 434 | else 435 | { 436 | $left = 0xffffffff00000000; 437 | $right = 0x00000000ffffffff; 438 | 439 | $high = ($value & $left) >>32; 440 | $low = $value & $right; 441 | } 442 | 443 | return array($low, $high); 444 | } 445 | 446 | /** 447 | * Create a format string and argument list for pack(), then call pack() and return the result. 448 | * 449 | * @param array $fields Key is format string and the value is the data to pack. 450 | * @return string Binary packed data returned from pack(). 451 | */ 452 | protected function pack_fields(array $fields) 453 | { 454 | $fmt = ''; 455 | $args = array(); 456 | 457 | // populate format string and argument list 458 | foreach ($fields as $field) 459 | { 460 | $fmt .= $field[0]; 461 | $args[] = $field[1]; 462 | } 463 | 464 | // prepend format string to argument list 465 | array_unshift($args, $fmt); 466 | 467 | // build output string from header and compressed data 468 | return call_user_func_array('pack', $args); 469 | } 470 | 471 | /** 472 | * Convert a number between bases via GMP. 473 | * 474 | * @param int $num Number to convert. 475 | * @param int $base_a Base to convert from. 476 | * @param int $base_b Base to convert to. 477 | * @return string Number in string format. 478 | */ 479 | private function gmp_convert($num, $base_a, $base_b) 480 | { 481 | $gmp_num = gmp_init($num, $base_a); 482 | 483 | if (!(is_resource($gmp_num) || $gmp_num instanceof GMP)) 484 | { 485 | // FIXME: Really? We just die here? Can we detect GMP in __constructor() instead maybe? 486 | die("gmp_convert could not convert [$num] from base [$base_a] to base [$base_b]"); 487 | } 488 | 489 | return gmp_strval($gmp_num, $base_b); 490 | } 491 | } 492 | -------------------------------------------------------------------------------- /src/TarArchive.php: -------------------------------------------------------------------------------- 1 | opt['content_type'] = 'application/x-tar'; 28 | } 29 | 30 | /** 31 | * Explicitly adds a directory to the tar (necessary for empty directories). 32 | * 33 | * @param string $name Name (path) of the directory. 34 | * @param array $opt Additional options to set ("type" will be overridden). 35 | * @return void 36 | */ 37 | public function add_directory($name, array $opt = array()) 38 | { 39 | // calculate header attributes 40 | $this->meth_str = 'deflate'; 41 | $meth = 0x08; 42 | 43 | $opt['type'] = self::DIRTYPE; 44 | 45 | // send header 46 | $this->init_file_stream_transfer($name, $size = 0, $opt, $meth); 47 | 48 | // complete the file stream 49 | $this->complete_file_stream(); 50 | } 51 | 52 | /** 53 | * Initialize a file stream. 54 | * 55 | * @param string $name File path or just name. 56 | * @param int $size Size in bytes of the file. 57 | * @param array $opt Array containing time / type (optional). 58 | * @param int $meth Method of compression to use (ignored by TarArchive class). 59 | * @return void 60 | */ 61 | public function init_file_stream_transfer($name, $size, array $opt = array(), $meth = null) 62 | { 63 | // try to detect the type if not provided 64 | $type = self::REGTYPE; 65 | if (isset($opt['type'])) 66 | { 67 | $type = $opt['type']; 68 | } 69 | elseif (substr($name, -1) == '/') 70 | { 71 | $type = self::DIRTYPE; 72 | } 73 | 74 | $dirname = dirname($name); 75 | $name = basename($name); 76 | 77 | // Remove '.' from the current directory 78 | $dirname = ($dirname == '.') ? '' : $dirname; 79 | 80 | // if we're using a container directory, prepend it to the filename 81 | if ($this->use_container_dir) 82 | { 83 | // container directory will end with a '/' so ensure the lower level directory name doesn't start with one 84 | $dirname = $this->container_dir_name . preg_replace('/^\/+/', '', $dirname); 85 | } 86 | 87 | // Remove trailing slash from directory name, because tar implies it. 88 | if (substr($dirname, -1) == '/') 89 | { 90 | $dirname = substr($dirname, 0, -1); 91 | } 92 | 93 | // handle long file names via PAX 94 | if (strlen($name) > 99 || strlen($dirname) > 154) 95 | { 96 | $pax = $this->__pax_generate(array( 97 | 'path' => $dirname . '/' . $name 98 | )); 99 | 100 | $this->init_file_stream_transfer('', strlen($pax), array( 101 | 'type' => self::XHDTYPE 102 | )); 103 | 104 | $this->stream_file_part($pax, $single_part = true); 105 | $this->complete_file_stream(); 106 | } 107 | 108 | // stash the file size for later use 109 | $this->file_size = $size; 110 | 111 | // process optional arguments 112 | $time = isset($opt['time']) ? $opt['time'] : time(); 113 | 114 | // build data descriptor 115 | $fields = array( 116 | array('a100', substr($name, 0, 100)), 117 | array('a8', str_pad('777', 7, '0', STR_PAD_LEFT)), 118 | array('a8', decoct(str_pad('0', 7, '0', STR_PAD_LEFT))), 119 | array('a8', decoct(str_pad('0', 7, '0', STR_PAD_LEFT))), 120 | array('a12', decoct(str_pad($size, 11, '0', STR_PAD_LEFT))), 121 | array('a12', decoct(str_pad($time, 11, '0', STR_PAD_LEFT))), 122 | array('a8', ''), 123 | array('a1', $type), 124 | array('a100', ''), 125 | array('a6', 'ustar'), 126 | array('a2', '00'), 127 | array('a32', ''), 128 | array('a32', ''), 129 | array('a8', ''), 130 | array('a8', ''), 131 | array('a155', substr($dirname, 0, 155)), 132 | array('a12', ''), 133 | ); 134 | 135 | // pack fields and calculate "total" length 136 | $header = $this->pack_fields($fields); 137 | 138 | // Compute header checksum 139 | $checksum = str_pad(decoct($this->__computeUnsignedChecksum($header)), 6, "0", STR_PAD_LEFT); 140 | for ($i=0; $i<6; $i++) 141 | { 142 | $header[(148 + $i)] = substr($checksum, $i, 1); 143 | } 144 | 145 | $header[154] = chr(0); 146 | $header[155] = chr(32); 147 | 148 | // print header 149 | $this->send($header); 150 | } 151 | 152 | /** 153 | * Stream the next part of the current file stream. 154 | * 155 | * @param string $data Raw data to send. 156 | * @param bool $single_part Used to determin if we can compress (not used in TarArchive class). 157 | * @return void 158 | */ 159 | public function stream_file_part($data, $single_part = false) 160 | { 161 | // send data 162 | $this->send($data); 163 | 164 | // flush the data to the output 165 | flush(); 166 | } 167 | 168 | /** 169 | * Complete the current file stream 170 | * 171 | * @return void 172 | */ 173 | public function complete_file_stream() 174 | { 175 | // ensure we pad the last block so that it is 512 bytes 176 | $mod = ($this->file_size % 512); 177 | if ($mod > 0) 178 | { 179 | $this->send(pack('a' . (512 - $mod), '')); 180 | } 181 | 182 | // flush the data to the output 183 | flush(); 184 | } 185 | 186 | /** 187 | * Finish an archive 188 | * 189 | * @return void 190 | */ 191 | public function finish() 192 | { 193 | // adds an error log file if we've been tracking errors 194 | $this->add_error_log(); 195 | 196 | // tar requires the end of the file have two 512 byte null blocks 197 | $this->send(pack('a1024', '')); 198 | 199 | // flush the data to the output 200 | flush(); 201 | } 202 | 203 | /******************* 204 | * PRIVATE METHODS * 205 | *******************/ 206 | 207 | /** 208 | * Generate unsigned checksum of header 209 | * 210 | * @param string $header File header. 211 | * @return string Unsigned checksum. 212 | * @access private 213 | */ 214 | private function __computeUnsignedChecksum($header) 215 | { 216 | $unsigned_checksum = 0; 217 | 218 | for ($i = 0; $i < 512; $i++) 219 | { 220 | $unsigned_checksum += ord($header[$i]); 221 | } 222 | 223 | for ($i = 0; $i < 8; $i++) 224 | { 225 | $unsigned_checksum -= ord($header[148 + $i]); 226 | } 227 | 228 | $unsigned_checksum += ord(" ") * 8; 229 | 230 | return $unsigned_checksum; 231 | } 232 | 233 | /** 234 | * Generate a PAX string 235 | * 236 | * @param array $fields Key value mapping. 237 | * @return string PAX formated string 238 | * @link http://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5&manpath=FreeBSD+8-current Tar / PAX spec 239 | */ 240 | private function __pax_generate(array $fields) 241 | { 242 | $lines = ''; 243 | foreach ($fields as $name => $value) 244 | { 245 | // build the line and the size 246 | $line = ' ' . $name . '=' . $value . "\n"; 247 | $size = strlen(strlen($line)) + strlen($line); 248 | 249 | // add the line 250 | $lines .= $size . $line; 251 | } 252 | 253 | return $lines; 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/ZipArchive.php: -------------------------------------------------------------------------------- 1 | opt['content_type'] = 'application/x-zip'; 59 | call_user_func_array(array('parent', '__construct'), func_get_args()); 60 | } 61 | 62 | /** 63 | * Explicitly adds a directory to the tar (necessary for empty directories) 64 | * 65 | * @param string $name Name (path) of the directory. 66 | * @param array $opt Additional options to set ("type" will be overridden). 67 | * @return void 68 | */ 69 | public function add_directory($name, array $opt = array()) 70 | { 71 | // calculate header attributes 72 | $this->meth_str = 'deflate'; 73 | $meth = 0x08; 74 | 75 | if (substr($name, -1) != '/') 76 | { 77 | $name = $name . '/'; 78 | } 79 | 80 | // send header 81 | $this->init_file_stream_transfer($name, $size = 0, $opt, $meth); 82 | 83 | // complete the file stream 84 | $this->complete_file_stream(); 85 | } 86 | 87 | /** 88 | * Initialize a file stream 89 | * 90 | * @param string $name File path or just name. 91 | * @param int $size Size in bytes of the file. 92 | * @param array $opt Array containing time / type (optional). 93 | * @param int $meth Method of compression to use (defaults to store). 94 | * @return void 95 | */ 96 | public function init_file_stream_transfer($name, $size, array $opt = array(), $meth = 0x00) 97 | { 98 | // if we're using a container directory, prepend it to the filename 99 | if ($this->use_container_dir) 100 | { 101 | // the container directory will end with a '/' so ensure the filename doesn't start with one 102 | $name = $this->container_dir_name . preg_replace('/^\\/+/', '', $name); 103 | } 104 | 105 | $algo = 'crc32b'; 106 | 107 | // calculate header attributes 108 | $this->len = gmp_init(0); 109 | $this->zlen = gmp_init(0); 110 | $this->hash_ctx = hash_init($algo); 111 | 112 | // Send file header 113 | $this->add_stream_file_header($name, $size, $opt, $meth); 114 | } 115 | 116 | /** 117 | * Stream the next part of the current file stream. 118 | * 119 | * @param string $data Raw data to send. 120 | * @param bool $single_part Used to determine if we can compress. 121 | * @return void 122 | */ 123 | public function stream_file_part($data, $single_part = false) 124 | { 125 | $this->len = gmp_add(gmp_init(strlen($data)), $this->len); 126 | hash_update($this->hash_ctx, $data); 127 | 128 | if ($single_part === true && isset($this->meth_str) && $this->meth_str == 'deflate') 129 | { 130 | $data = gzdeflate($data); 131 | } 132 | 133 | $this->zlen = gmp_add(gmp_init(strlen($data)), $this->zlen); 134 | 135 | // send data 136 | $this->send($data); 137 | flush(); 138 | } 139 | 140 | /** 141 | * Complete the current file stream (zip64 format). 142 | * 143 | * @return void 144 | */ 145 | public function complete_file_stream() 146 | { 147 | $crc = hexdec(hash_final($this->hash_ctx)); 148 | 149 | // convert the 64 bit ints to 2 32bit ints 150 | list($zlen_low, $zlen_high) = $this->int64_split($this->zlen); 151 | list($len_low, $len_high) = $this->int64_split($this->len); 152 | 153 | // build data descriptor 154 | $fields = array( // (from V.A of APPNOTE.TXT) 155 | array('V', 0x08074b50), // data descriptor 156 | array('V', $crc), // crc32 of data 157 | array('V', $zlen_low), // compressed data length (low) 158 | array('V', $zlen_high), // compressed data length (high) 159 | array('V', $len_low), // uncompressed data length (low) 160 | array('V', $len_high), // uncompressed data length (high) 161 | ); 162 | 163 | // pack fields and calculate "total" length 164 | $ret = $this->pack_fields($fields); 165 | 166 | // print header and filename 167 | $this->send($ret); 168 | 169 | // Update cdr for file record 170 | $this->current_file_stream[3] = $crc; 171 | $this->current_file_stream[4] = gmp_strval($this->zlen); 172 | $this->current_file_stream[5] = gmp_strval($this->len); 173 | $this->current_file_stream[6] += gmp_strval(gmp_add(gmp_init(strlen($ret)), $this->zlen)); 174 | ksort($this->current_file_stream); 175 | 176 | // Add to cdr and increment offset - can't call directly because we pass an array of params 177 | call_user_func_array(array($this, 'add_to_cdr'), $this->current_file_stream); 178 | } 179 | 180 | /** 181 | * Finish an archive 182 | * 183 | * @return void 184 | */ 185 | public function finish() 186 | { 187 | // adds an error log file if we've been tracking errors 188 | $this->add_error_log(); 189 | 190 | // add trailing cdr record 191 | $this->add_cdr($this->opt); 192 | $this->clear(); 193 | } 194 | 195 | /******************* 196 | * PRIVATE METHODS * 197 | *******************/ 198 | 199 | /** 200 | * Add initial headers for file stream 201 | * 202 | * @param string $name File path or just name. 203 | * @param int $size Size in bytes of the file. 204 | * @param array $opt Array containing time. 205 | * @param int $meth Method of compression to use. 206 | * @return void 207 | */ 208 | protected function add_stream_file_header($name, $size, array $opt, $meth) 209 | { 210 | // strip leading slashes from file name 211 | // (fixes bug in windows archive viewer) 212 | $name = preg_replace('/^\\/+/', '', $name); 213 | $extra = pack('vVVVV', 1, 0, 0, 0, 0); 214 | 215 | // create dos timestamp 216 | $opt['time'] = isset($opt['time']) ? $opt['time'] : time(); 217 | $dts = $this->dostime($opt['time']); 218 | 219 | // Sets bit 3, which means CRC-32, uncompressed and compresed length 220 | // are put in the data descriptor following the data. This gives us time 221 | // to figure out the correct sizes, etc. 222 | $genb = 0x08; 223 | 224 | if (mb_check_encoding($name, "UTF-8") && !mb_check_encoding($name, "ASCII")) 225 | { 226 | // Sets Bit 11: Language encoding flag (EFS). If this bit is set, 227 | // the filename and comment fields for this file 228 | // MUST be encoded using UTF-8. (see APPENDIX D) 229 | $genb |= 0x0800; 230 | } 231 | 232 | // build file header 233 | $fields = array( // (from V.A of APPNOTE.TXT) 234 | array('V', 0x04034b50), // local file header signature 235 | array('v', self::VERSION), // version needed to extract 236 | array('v', $genb), // general purpose bit flag 237 | array('v', $meth), // compresion method (deflate or store) 238 | array('V', $dts), // dos timestamp 239 | array('V', 0x00), // crc32 of data (0x00 because bit 3 set in $genb) 240 | array('V', 0xFFFFFFFF), // compressed data length 241 | array('V', 0xFFFFFFFF), // uncompressed data length 242 | array('v', strlen($name)), // filename length 243 | array('v', strlen($extra)), // extra data len 244 | ); 245 | 246 | // pack fields and calculate "total" length 247 | $ret = $this->pack_fields($fields); 248 | 249 | // print header and filename 250 | $this->send($ret . $name . $extra); 251 | 252 | // Keep track of data for central directory record 253 | $this->current_file_stream = array( 254 | $name, 255 | $opt, 256 | $meth, 257 | // 3-5 will be filled in by complete_file_stream() 258 | 6 => (strlen($ret) + strlen($name) + strlen($extra)), 259 | 7 => $genb, 260 | 8 => substr($name, -1) == '/' ? 0x10 : 0x20, // 0x10 for directory, 0x20 for file 261 | ); 262 | } 263 | 264 | /** 265 | * Save file attributes for trailing CDR record. 266 | * 267 | * @param string $name Path / name of the file. 268 | * @param array $opt Array containing time. 269 | * @param int $meth Method of compression to use. 270 | * @param string $crc Computed checksum of the file. 271 | * @param int $zlen Compressed size. 272 | * @param int $len Uncompressed size. 273 | * @param int $rec_len Size of the record. 274 | * @param int $genb General purpose bit flag. 275 | * @param int $fattr File attribute bit flag. 276 | * @return void 277 | */ 278 | private function add_to_cdr($name, array $opt, $meth, $crc, $zlen, $len, $rec_len, $genb = 0, $fattr = 0x20) 279 | { 280 | $this->files[] = array($name, $opt, $meth, $crc, $zlen, $len, $this->cdr_ofs, $genb, $fattr); 281 | $this->cdr_ofs += $rec_len; 282 | } 283 | 284 | /** 285 | * Send CDR record for specified file (Zip64 format). 286 | * 287 | * @see add_to_cdr() for options to pass in $args. 288 | * @param array $args Array of argumentss. 289 | * @return void 290 | */ 291 | private function add_cdr_file(array $args) 292 | { 293 | list($name, $opt, $meth, $crc, $zlen, $len, $ofs, $genb, $file_attribute) = $args; 294 | 295 | // convert the 64 bit ints to 2 32bit ints 296 | list($zlen_low, $zlen_high) = $this->int64_split($zlen); 297 | list($len_low, $len_high) = $this->int64_split($len); 298 | list($ofs_low, $ofs_high) = $this->int64_split($ofs); 299 | 300 | // ZIP64, necessary for files over 4GB (incl. entire archive size) 301 | $extra_zip64 = ''; 302 | $extra_zip64 .= pack('VV', $len_low, $len_high); 303 | $extra_zip64 .= pack('VV', $zlen_low, $zlen_high); 304 | $extra_zip64 .= pack('VV', $ofs_low, $ofs_high); 305 | 306 | $extra = pack('vv', 1, strlen($extra_zip64)) . $extra_zip64; 307 | 308 | // get attributes 309 | $comment = isset($opt['comment']) ? $opt['comment'] : ''; 310 | 311 | // get dos timestamp 312 | $dts = $this->dostime($opt['time']); 313 | 314 | $fields = array( // (from V,F of APPNOTE.TXT) 315 | array('V', 0x02014b50), // central file header signature 316 | array('v', self::VERSION), // version made by 317 | array('v', self::VERSION), // version needed to extract 318 | array('v', $genb), // general purpose bit flag 319 | array('v', $meth), // compresion method (deflate or store) 320 | array('V', $dts), // dos timestamp 321 | array('V', $crc), // crc32 of data 322 | array('V', 0xFFFFFFFF), // compressed data length (zip64 - look in extra) 323 | array('V', 0xFFFFFFFF), // uncompressed data length (zip64 - look in extra) 324 | array('v', strlen($name)), // filename length 325 | array('v', strlen($extra)), // extra data len 326 | array('v', strlen($comment)), // file comment length 327 | array('v', 0), // disk number start 328 | array('v', 0), // internal file attributes 329 | array('V', $file_attribute), // external file attributes, 0x10 for dir, 0x20 for file 330 | array('V', 0xFFFFFFFF), // relative offset of local header (zip64 - look in extra) 331 | ); 332 | 333 | // pack fields, then append name and comment 334 | $ret = $this->pack_fields($fields) . $name . $extra . $comment; 335 | 336 | $this->send($ret); 337 | 338 | // increment cdr length 339 | $this->cdr_len += strlen($ret); 340 | } 341 | 342 | /** 343 | * Adds Zip64 end of central directory record. 344 | * 345 | * @return void 346 | */ 347 | private function add_cdr_eof_zip64() 348 | { 349 | $num = count($this->files); 350 | 351 | list($num_low, $num_high) = $this->int64_split($num); 352 | list($cdr_len_low, $cdr_len_high) = $this->int64_split($this->cdr_len); 353 | list($cdr_ofs_low, $cdr_ofs_high) = $this->int64_split($this->cdr_ofs); 354 | 355 | $fields = array( // (from V,F of APPNOTE.TXT) 356 | array('V', 0x06064b50), // zip64 end of central directory signature 357 | array('V', 44), // size of zip64 end of central directory record (low) minus 12 bytes 358 | array('V', 0), // size of zip64 end of central directory record (high) 359 | array('v', self::VERSION), // version made by 360 | array('v', self::VERSION), // version needed to extract 361 | array('V', 0x0000), // this disk number (only one disk) 362 | array('V', 0x0000), // number of disk with central dir 363 | array('V', $num_low), // number of entries in the cdr for this disk (low) 364 | array('V', $num_high), // number of entries in the cdr for this disk (high) 365 | array('V', $num_low), // number of entries in the cdr (low) 366 | array('V', $num_high), // number of entries in the cdr (high) 367 | array('V', $cdr_len_low), // cdr size (low) 368 | array('V', $cdr_len_high), // cdr size (high) 369 | array('V', $cdr_ofs_low), // cdr ofs (low) 370 | array('V', $cdr_ofs_high), // cdr ofs (high) 371 | ); 372 | 373 | $ret = $this->pack_fields($fields); 374 | $this->send($ret); 375 | } 376 | 377 | /** 378 | * Add location record for ZIP64 central directory 379 | * 380 | * @return void 381 | */ 382 | private function add_cdr_eof_locator_zip64() 383 | { 384 | list($cdr_ofs_low, $cdr_ofs_high) = $this->int64_split($this->cdr_len + $this->cdr_ofs); 385 | 386 | $fields = array( // (from V,F of APPNOTE.TXT) 387 | array('V', 0x07064b50), // zip64 end of central dir locator signature 388 | array('V', 0), // this disk number 389 | array('V', $cdr_ofs_low), // cdr ofs (low) 390 | array('V', $cdr_ofs_high), // cdr ofs (high) 391 | array('V', 1), // total number of disks 392 | ); 393 | 394 | $ret = $this->pack_fields($fields); 395 | $this->send($ret); 396 | } 397 | 398 | /** 399 | * Send CDR EOF (Central Directory Record End-of-File) record. Most values 400 | * point to the corresponding values in the ZIP64 CDR. The optional comment 401 | * still goes in this CDR however. 402 | * 403 | * @param array $opt Options array that may contain a comment. 404 | * @return void 405 | */ 406 | private function add_cdr_eof(array $opt = null) 407 | { 408 | // grab comment (if specified) 409 | $comment = ''; 410 | if ($opt && isset($opt['comment'])) 411 | { 412 | $comment = $opt['comment']; 413 | } 414 | 415 | $fields = array( // (from V,F of APPNOTE.TXT) 416 | array('V', 0x06054b50), // end of central file header signature 417 | array('v', 0xFFFF), // this disk number (0xFFFF to look in zip64 cdr) 418 | array('v', 0xFFFF), // number of disk with cdr (0xFFFF to look in zip64 cdr) 419 | array('v', 0xFFFF), // number of entries in the cdr on this disk (0xFFFF to look in zip64 cdr)) 420 | array('v', 0xFFFF), // number of entries in the cdr (0xFFFF to look in zip64 cdr) 421 | array('V', 0xFFFFFFFF), // cdr size (0xFFFFFFFF to look in zip64 cdr) 422 | array('V', 0xFFFFFFFF), // cdr offset (0xFFFFFFFF to look in zip64 cdr) 423 | array('v', strlen($comment)), // zip file comment length 424 | ); 425 | 426 | $ret = $this->pack_fields($fields) . $comment; 427 | $this->send($ret); 428 | } 429 | 430 | /** 431 | * Add CDR (Central Directory Record) footer. 432 | * 433 | * @param array $opt Options array that may contain a comment. 434 | * @return void 435 | */ 436 | private function add_cdr(array $opt = null) 437 | { 438 | foreach ($this->files as $file) 439 | { 440 | $this->add_cdr_file($file); 441 | } 442 | 443 | $this->add_cdr_eof_zip64(); 444 | $this->add_cdr_eof_locator_zip64(); 445 | 446 | $this->add_cdr_eof($opt); 447 | } 448 | 449 | /** 450 | * Clear all internal variables. 451 | * 452 | * Note: the archive object is unusable after this. 453 | * 454 | * @return void 455 | */ 456 | private function clear() 457 | { 458 | $this->files = array(); 459 | $this->cdr_ofs = 0; 460 | $this->cdr_len = 0; 461 | $this->opt = array(); 462 | } 463 | } 464 | -------------------------------------------------------------------------------- /test/index.php: -------------------------------------------------------------------------------- 1 | 'this is a zip file comment. hello?' 22 | )); 23 | var_dump($zip); 24 | 25 | // common file options 26 | $file_opt = array( 27 | // file creation time (2 hours ago) 28 | 'time' => time() - 2 * 3600, 29 | 30 | // file comment 31 | 'comment' => 'this is a file comment. hi!', 32 | ); 33 | 34 | // add files under folder 'asdf' 35 | foreach ($files as $file) 36 | { 37 | // build absolute path and get file data 38 | $path = ($file[0] == '/') ? $file : "$pwd/$file"; 39 | 40 | // add file to archive 41 | $zip->add_file_from_path('asdf/' . basename($file), $path, $file_opt); 42 | } 43 | 44 | // add a long file name 45 | $zip->add_file('/long/' . str_repeat('a', 200) . '.txt', 'test'); 46 | $zip->add_directory('/foo'); 47 | $zip->add_directory('/foo/bar'); 48 | 49 | // finish archive 50 | $zip->finish(); 51 | --------------------------------------------------------------------------------