1<?php 2/** 3 * Parses and verifies the doc comments for files. 4 * 5 * PHP version 5 6 * 7 * @category PHP 8 * @package PHP_CodeSniffer 9 * @author Greg Sherwood <gsherwood@squiz.net> 10 * @author Marc McIntyre <mmcintyre@squiz.net> 11 * @copyright 2006-2012 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 16if (class_exists('PHP_CodeSniffer_CommentParser_ClassCommentParser', true) === false) { 17 throw new PHP_CodeSniffer_Exception('Class PHP_CodeSniffer_CommentParser_ClassCommentParser not found'); 18} 19 20/** 21 * Parses and verifies the doc comments for files. 22 * 23 * Verifies that : 24 * <ul> 25 * <li>A doc comment exists.</li> 26 * <li>There is a blank newline after the short description.</li> 27 * <li>There is a blank newline between the long and short description.</li> 28 * <li>There is a blank newline between the long description and tags.</li> 29 * <li>A PHP version is specified.</li> 30 * <li>Check the order of the tags.</li> 31 * <li>Check the indentation of each tag.</li> 32 * <li>Check required and optional tags and the format of their content.</li> 33 * </ul> 34 * 35 * @category PHP 36 * @package PHP_CodeSniffer 37 * @author Greg Sherwood <gsherwood@squiz.net> 38 * @author Marc McIntyre <mmcintyre@squiz.net> 39 * @copyright 2006-2012 Squiz Pty Ltd (ABN 77 084 670 600) 40 * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence 41 * @version Release: 1.5.1 42 * @link http://pear.php.net/package/PHP_CodeSniffer 43 */ 44 45class PhOSCo_Sniffs_Commenting_FileCommentSniff implements PHP_CodeSniffer_Sniff { 46 47 /** 48 * The header comment parser for the current file. 49 * 50 * @var PHP_CodeSniffer_Comment_Parser_ClassCommentParser 51 */ 52 protected $commentParser = null; 53 54 /** 55 * The current PHP_CodeSniffer_File object we are processing. 56 * 57 * @var PHP_CodeSniffer_File 58 */ 59 protected $currentFile = null; 60 61 /** 62 * Tags in correct order and related info. 63 * 64 * @var array 65 */ 66 protected $tags = array( 67 'author' => array('required' => true, 'allow_multiple' => true, 68 'order_text' => 'follows @subpackage (if used) or @package',), 69 'copyright' => array('required' => false, 'allow_multiple' => true, 'order_text' => 'follows @author',), 70 'license' => array('required' => true, 'allow_multiple' => false, 71 'order_text' => 'follows @copyright (if used) or @author',), 72 'version' => array('required' => false, 'allow_multiple' => false, 'order_text' => 'follows @license',), 73 'see' => array('required' => false, 'allow_multiple' => true, 'order_text' => 'follows @link',), 74 'since' => array('required' => false, 'allow_multiple' => false, 75 'order_text' => 'follows @see (if used) or @link',), 76 'deprecated' => array('required' => false, 'allow_multiple' => false, 77 'order_text' => 'follows @since (if used) or @see (if used) or @link',),); 78 79 /** 80 * Returns an array of tokens this test wants to listen for. 81 * 82 * @return array 83 */ 84 public function register() { 85 return array(T_OPEN_TAG); 86 87 }//end register() 88 89 /** 90 * Processes this test, when one of its tokens is encountered. 91 * 92 * @param PHP_CodeSniffer_File $phpcsFile The file being scanned. 93 * @param int $stackPtr The position of the current token 94 * in the stack passed in $tokens. 95 * 96 * @return void 97 */ 98 public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr) { 99 $this->currentFile = $phpcsFile; 100 101 // We are only interested if this is the first open tag. 102 if ($stackPtr !== 0) { 103 if ($phpcsFile->findPrevious(T_OPEN_TAG, ($stackPtr - 1)) !== false) { 104 return; 105 } 106 } 107 108 $tokens = $phpcsFile->getTokens(); 109 110 // Find the next non whitespace token. 111 $commentStart = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true); 112 113 // Allow declare() statements at the top of the file. 114 if ($tokens[$commentStart]['code'] === T_DECLARE) { 115 $semicolon = $phpcsFile->findNext(T_SEMICOLON, ($commentStart + 1)); 116 $commentStart = $phpcsFile->findNext(T_WHITESPACE, ($semicolon + 1), null, true); 117 } 118 119 // Ignore vim header. 120 if ($tokens[$commentStart]['code'] === T_COMMENT) { 121 if (strstr($tokens[$commentStart]['content'], 'vim:') !== false) { 122 $commentStart = $phpcsFile->findNext(T_WHITESPACE, ($commentStart + 1), null, true); 123 } 124 } 125 126 $errorToken = ($stackPtr + 1); 127 if (isset($tokens[$errorToken]) === false) { 128 $errorToken--; 129 } 130 131 if ($tokens[$commentStart]['code'] === T_CLOSE_TAG) { 132 // We are only interested if this is the first open tag. 133 return; 134 } else if ($tokens[$commentStart]['code'] === T_COMMENT) { 135 $error = 'You must use "/**" style comments for a file comment'; 136 $phpcsFile->addError($error, $errorToken, 'WrongStyle'); 137 return; 138 } else if ($commentStart === false || $tokens[$commentStart]['code'] !== T_DOC_COMMENT) { 139 $phpcsFile->addError('Missing file doc comment', $errorToken, 'Missing'); 140 return; 141 } else { 142 143 // Extract the header comment docblock. 144 $commentEnd = $phpcsFile->findNext(T_DOC_COMMENT, ($commentStart + 1), null, true); 145 146 $commentEnd--; 147 148 // Check if there is only 1 doc comment between the 149 // open tag and class token. 150 $nextToken = array(T_ABSTRACT, T_CLASS, T_FUNCTION, T_DOC_COMMENT,); 151 152 $commentNext = $phpcsFile->findNext($nextToken, ($commentEnd + 1)); 153 if ($commentNext !== false && $tokens[$commentNext]['code'] !== T_DOC_COMMENT) { 154 // Found a class token right after comment doc block. 155 $newlineToken = $phpcsFile->findNext(T_WHITESPACE, ($commentEnd + 1), $commentNext, false, 156 $phpcsFile->eolChar); 157 158 if ($newlineToken !== false) { 159 $newlineToken = $phpcsFile->findNext(T_WHITESPACE, ($newlineToken + 1), $commentNext, false, 160 $phpcsFile->eolChar); 161 162 if ($newlineToken === false) { 163 // No blank line between the class token and the doc block. 164 // The doc block is most likely a class comment. 165 $error = 'Missing file doc comment'; 166 $phpcsFile->addError($error, $errorToken, 'Missing'); 167 return; 168 } 169 } 170 }//end if 171 172 $comment = $phpcsFile->getTokensAsString($commentStart, ($commentEnd - $commentStart + 1)); 173 174 // Parse the header comment docblock. 175 try { 176 $this->commentParser = new PHP_CodeSniffer_CommentParser_ClassCommentParser($comment, $phpcsFile); 177 $this->commentParser->parse(); 178 } catch (PHP_CodeSniffer_CommentParser_ParserException $e) { 179 $line = ($e->getLineWithinComment() + $commentStart); 180 $phpcsFile->addError($e->getMessage(), $line, 'FailedParse'); 181 return; 182 } 183 184 $comment = $this->commentParser->getComment(); 185 if (is_null($comment) === true) { 186 $error = 'File doc comment is empty'; 187 $phpcsFile->addError($error, $commentStart, 'Empty'); 188 return; 189 } 190 191 // No extra newline before short description. 192 $short = $comment->getShortComment(); 193 $newlineCount = 0; 194 $newlineSpan = strspn($short, $phpcsFile->eolChar); 195 if ($short !== '' && $newlineSpan > 0) { 196 $error = 'Extra newline(s) found before file comment short description'; 197 $phpcsFile->addError($error, ($commentStart + 1), 'SpacingBefore'); 198 } 199 200 $newlineCount = (substr_count($short, $phpcsFile->eolChar) + 1); 201 202 // Exactly one blank line between short and long description. 203 $long = $comment->getLongComment(); 204 if (empty($long) === false) { 205 $between = $comment->getWhiteSpaceBetween(); 206 $newlineBetween = substr_count($between, $phpcsFile->eolChar); 207 if ($newlineBetween !== 2) { 208 $error = 'There must be exactly one blank line between descriptions in file comment'; 209 $phpcsFile->addError($error, ($commentStart + $newlineCount + 1), 'DescriptionSpacing'); 210 } 211 212 $newlineCount += $newlineBetween; 213 } 214 215 // Exactly one blank line before tags. 216 $tags = $this->commentParser->getTagOrders(); 217 if (count($tags) > 1) { 218 $newlineSpan = $comment->getNewlineAfter(); 219 if ($newlineSpan !== 2) { 220 $error = 'There must be exactly one blank line before the tags in file comment'; 221 if ($long !== '') { 222 $newlineCount += (substr_count($long, $phpcsFile->eolChar) - $newlineSpan + 1); 223 } 224 225 $phpcsFile->addError($error, ($commentStart + $newlineCount), 'SpacingBeforeTags'); 226 $short = rtrim($short, $phpcsFile->eolChar . ' '); 227 } 228 } 229 230 // Check the PHP Version. 231 $this->processPHPVersion($commentStart, $commentEnd, $long); 232 233 // Check each tag. 234 $this->processTags($commentStart, $commentEnd); 235 }//end if 236 237 }//end process() 238 239 /** 240 * Check that the PHP version is specified. 241 * 242 * @param int $commentStart Position in the stack where the comment started. 243 * @param int $commentEnd Position in the stack where the comment ended. 244 * @param string $commentText The text of the function comment. 245 * 246 * @return void 247 */ 248 protected function processPHPVersion($commentStart, $commentEnd, $commentText) { 249 if (strstr(strtolower($commentText), 'php version') === false) { 250 $error = 'PHP version not specified'; 251 $this->currentFile->addWarning($error, $commentEnd, 'MissingVersion'); 252 } 253 254 }//end processPHPVersion() 255 256 /** 257 * Processes each required or optional tag. 258 * 259 * @param int $commentStart Position in the stack where the comment started. 260 * @param int $commentEnd Position in the stack where the comment ended. 261 * 262 * @return void 263 */ 264 protected function processTags($commentStart, $commentEnd) { 265 $docBlock = (get_class($this) === 'PhOSCo_Sniffs_Commenting_FileCommentSniff') ? 'file' : 'class'; 266 $foundTags = $this->commentParser->getTagOrders(); 267 $orderIndex = 0; 268 $indentation = array(); 269 $longestTag = 0; 270 $errorPos = 0; 271 272 foreach ($this->tags as $tag => $info) { 273 274 // Required tag missing. 275 if ($info['required'] === true && in_array($tag, $foundTags) === false) { 276 $error = 'Missing @%s tag in %s comment'; 277 $data = array($tag, $docBlock,); 278 $this->currentFile->addError($error, $commentEnd, 'MissingTag', $data); 279 continue; 280 } 281 282 // Get the line number for current tag. 283 $tagName = ucfirst($tag); 284 if ($info['allow_multiple'] === true) { 285 $tagName .= 's'; 286 } 287 288 $getMethod = 'get' . $tagName; 289 $tagElement = $this->commentParser->$getMethod(); 290 if (is_null($tagElement) === true || empty($tagElement) === true) { 291 continue; 292 } 293 294 $errorPos = $commentStart; 295 if (is_array($tagElement) === false) { 296 $errorPos = ($commentStart + $tagElement->getLine()); 297 } 298 299 // Get the tag order. 300 $foundIndexes = array_keys($foundTags, $tag); 301 302 if (count($foundIndexes) > 1) { 303 // Multiple occurrence not allowed. 304 if ($info['allow_multiple'] === false) { 305 $error = 'Only 1 @%s tag is allowed in a %s comment'; 306 $data = array($tag, $docBlock,); 307 $this->currentFile->addError($error, $errorPos, 'DuplicateTag', $data); 308 } else { 309 // Make sure same tags are grouped together. 310 $i = 0; 311 $count = $foundIndexes[0]; 312 foreach ($foundIndexes as $index) { 313 if ($index !== $count) { 314 $errorPosIndex = ($errorPos + $tagElement[$i]->getLine()); 315 $error = '@%s tags must be grouped together'; 316 $data = array($tag); 317 $this->currentFile->addError($error, $errorPosIndex, 'TagsNotGrouped', $data); 318 } 319 320 $i++; 321 $count++; 322 } 323 } 324 }//end if 325 326 // Check tag order. 327 if ($foundIndexes[0] > $orderIndex) { 328 $orderIndex = $foundIndexes[0]; 329 } else { 330 if (is_array($tagElement) === true && empty($tagElement) === false) { 331 $errorPos += $tagElement[0]->getLine(); 332 } 333 334 $error = 'The @%s tag is in the wrong order; the tag %s'; 335 $data = array($tag, $info['order_text'],); 336 $this->currentFile->addError($error, $errorPos, 'WrongTagOrder', $data); 337 } 338 339 // Store the indentation for checking. 340 $len = strlen($tag); 341 if ($len > $longestTag) { 342 $longestTag = $len; 343 } 344 345 if (is_array($tagElement) === true) { 346 foreach ($tagElement as $key => $element) { 347 $indentation[] = array('tag' => $tag, 'space' => $this->getIndentation($tag, $element), 348 'line' => $element->getLine(),); 349 } 350 } else { 351 $indentation[] = array('tag' => $tag, 'space' => $this->getIndentation($tag, $tagElement),); 352 } 353 354 $method = 'process' . $tagName; 355 if (method_exists($this, $method) === true) { 356 // Process each tag if a method is defined. 357 call_user_func(array($this, $method), $errorPos); 358 } else { 359 if (is_array($tagElement) === true) { 360 foreach ($tagElement as $key => $element) { 361 $element->process($this->currentFile, $commentStart, $docBlock); 362 } 363 } else { 364 $tagElement->process($this->currentFile, $commentStart, $docBlock); 365 } 366 } 367 }//end foreach 368 369 foreach ($indentation as $indentInfo) { 370 if ($indentInfo['space'] !== 0 && $indentInfo['space'] !== ($longestTag + 1)) { 371 $expected = (($longestTag - strlen($indentInfo['tag'])) + 1); 372 $space = ($indentInfo['space'] - strlen($indentInfo['tag'])); 373 $error = '@%s tag comment indented incorrectly; expected %s spaces but found %s'; 374 $data = array($indentInfo['tag'], $expected, $space,); 375 376 $getTagMethod = 'get' . ucfirst($indentInfo['tag']); 377 378 if ($this->tags[$indentInfo['tag']]['allow_multiple'] === true) { 379 $line = $indentInfo['line']; 380 } else { 381 $tagElem = $this->commentParser->$getTagMethod(); 382 $line = $tagElem->getLine(); 383 } 384 385 $this->currentFile->addError($error, ($commentStart + $line), 'TagIndent', $data); 386 } 387 } 388 389 }//end processTags() 390 391 /** 392 * Get the indentation information of each tag. 393 * 394 * @param string $tagName The name of the 395 * doc comment 396 * element. 397 * @param PHP_CodeSniffer_CommentParser_DocElement $tagElement The doc comment 398 * element. 399 * 400 * @return void 401 */ 402 protected function getIndentation($tagName, $tagElement) { 403 if ($tagElement instanceof PHP_CodeSniffer_CommentParser_SingleElement) { 404 if ($tagElement->getContent() !== '') { 405 return (strlen($tagName) + substr_count($tagElement->getWhitespaceBeforeContent(), ' ')); 406 } 407 } else if ($tagElement instanceof PHP_CodeSniffer_CommentParser_PairElement) { 408 if ($tagElement->getValue() !== '') { 409 return (strlen($tagName) + substr_count($tagElement->getWhitespaceBeforeValue(), ' ')); 410 } 411 } 412 413 return 0; 414 415 }//end getIndentation() 416 417 /** 418 * Process the category tag. 419 * 420 * @param int $errorPos The line number where the error occurs. 421 * 422 * @return void 423 */ 424 protected function processCategory($errorPos) { 425 $category = $this->commentParser->getCategory(); 426 if ($category !== null) { 427 $content = $category->getContent(); 428 if ($content !== '') { 429 if (PHP_CodeSniffer::isUnderscoreName($content) !== true) { 430 $newContent = str_replace(' ', '_', $content); 431 $nameBits = explode('_', $newContent); 432 $firstBit = array_shift($nameBits); 433 $newName = ucfirst($firstBit) . '_'; 434 foreach ($nameBits as $bit) { 435 $newName .= ucfirst($bit) . '_'; 436 } 437 438 $error = 'Category name "%s" is not valid; consider "%s" instead'; 439 $validName = trim($newName, '_'); 440 $data = array($content, $validName,); 441 $this->currentFile->addError($error, $errorPos, 'InvalidCategory', $data); 442 } 443 } else { 444 $error = '@category tag must contain a name'; 445 $this->currentFile->addError($error, $errorPos, 'EmptyCategory'); 446 } 447 } 448 449 }//end processCategory() 450 451 /** 452 * Process the package tag. 453 * 454 * @param int $errorPos The line number where the error occurs. 455 * 456 * @return void 457 */ 458 protected function processPackage($errorPos) { 459 $package = $this->commentParser->getPackage(); 460 if ($package === null) { 461 return; 462 } 463 464 $content = $package->getContent(); 465 if ($content === '') { 466 $error = '@package tag must contain a name'; 467 $this->currentFile->addError($error, $errorPos, 'EmptyPackage'); 468 return; 469 } 470 471 if (PHP_CodeSniffer::isUnderscoreName($content) === true) { 472 return; 473 } 474 475 $newContent = str_replace(' ', '_', $content); 476 $newContent = preg_replace('/[^A-Za-z_]/', '', $newContent); 477 $nameBits = explode('_', $newContent); 478 $firstBit = array_shift($nameBits); 479 $newName = strtoupper($firstBit{0}) . substr($firstBit, 1) . '_'; 480 foreach ($nameBits as $bit) { 481 $newName .= strtoupper($bit{0}) . substr($bit, 1) . '_'; 482 } 483 484 $error = 'Package name "%s" is not valid; consider "%s" instead'; 485 $validName = trim($newName, '_'); 486 $data = array($content, $validName,); 487 $this->currentFile->addError($error, $errorPos, 'InvalidPackage', $data); 488 489 }//end processPackage() 490 491 /** 492 * Process the subpackage tag. 493 * 494 * @param int $errorPos The line number where the error occurs. 495 * 496 * @return void 497 */ 498 protected function processSubpackage($errorPos) { 499 $package = $this->commentParser->getSubpackage(); 500 if ($package !== null) { 501 $content = $package->getContent(); 502 if ($content !== '') { 503 if (PHP_CodeSniffer::isUnderscoreName($content) !== true) { 504 $newContent = str_replace(' ', '_', $content); 505 $nameBits = explode('_', $newContent); 506 $firstBit = array_shift($nameBits); 507 $newName = strtoupper($firstBit{0}) . substr($firstBit, 1) . '_'; 508 foreach ($nameBits as $bit) { 509 $newName .= strtoupper($bit{0}) . substr($bit, 1) . '_'; 510 } 511 512 $error = 'Subpackage name "%s" is not valid; consider "%s" instead'; 513 $validName = trim($newName, '_'); 514 $data = array($content, $validName,); 515 $this->currentFile->addError($error, $errorPos, 'InvalidSubpackage', $data); 516 } 517 } else { 518 $error = '@subpackage tag must contain a name'; 519 $this->currentFile->addError($error, $errorPos, 'EmptySubpackage'); 520 } 521 } 522 523 }//end processSubpackage() 524 525 /** 526 * Process the author tag(s) that this header comment has. 527 * 528 * This function is different from other _process functions 529 * as $authors is an array of SingleElements, so we work out 530 * the errorPos for each element separately 531 * 532 * @param int $commentStart The position in the stack where 533 * the comment started. 534 * 535 * @return void 536 */ 537 protected function processAuthors($commentStart) { 538 $authors = $this->commentParser->getAuthors(); 539 // Report missing return. 540 if (empty($authors) === false) { 541 foreach ($authors as $author) { 542 $errorPos = ($commentStart + $author->getLine()); 543 $content = $author->getContent(); 544 if ($content !== '') { 545 $local = '\da-zA-Z-_+'; 546 // Dot character cannot be the first or last character 547 // in the local-part. 548 $localMiddle = $local . '.\w'; 549 if (preg_match( 550 '/^([^<]*)\s+<([' . $local . ']([' . $localMiddle . ']*[' . $local 551 . '])*@[\da-zA-Z][-.\w]*[\da-zA-Z]\.[a-zA-Z]{2,7})>$/', $content) === 0) { 552 $error = 'Content of the @author tag must be in the form "Display Name <username@example.com>"'; 553 $this->currentFile->addError($error, $errorPos, 'InvalidAuthors'); 554 } 555 } else { 556 $error = 'Content missing for @author tag in %s comment'; 557 $docBlock = (get_class($this) === 'PhOSCo_Sniffs_Commenting_FileCommentSniff') ? 'file' : 'class'; 558 $data = array($docBlock); 559 $this->currentFile->addError($error, $errorPos, 'EmptyAuthors', $data); 560 } 561 } 562 } 563 564 }//end processAuthors() 565 566 /** 567 * Process the copyright tags. 568 * 569 * @param int $commentStart The position in the stack where 570 * the comment started. 571 * 572 * @return void 573 */ 574 protected function processCopyrights($commentStart) { 575 $copyrights = $this->commentParser->getCopyrights(); 576 foreach ($copyrights as $copyright) { 577 $errorPos = ($commentStart + $copyright->getLine()); 578 $content = $copyright->getContent(); 579 if ($content !== '') { 580 $matches = array(); 581 if (preg_match('/^([0-9]{4})((.{1})([0-9]{4}))? (.+)$/', $content, $matches) !== 0) { 582 // Check earliest-latest year order. 583 if ($matches[3] !== '') { 584 if ($matches[3] !== '-') { 585 $error = 'A hyphen must be used between the earliest and latest year'; 586 $this->currentFile->addError($error, $errorPos, 'CopyrightHyphen'); 587 } 588 589 if ($matches[4] !== '' && $matches[4] < $matches[1]) { 590 $error = "Invalid year span \"$matches[1]$matches[3]$matches[4]\" found; consider \"$matches[4]-$matches[1]\" instead"; 591 $this->currentFile->addWarning($error, $errorPos, 'InvalidCopyright'); 592 } 593 } 594 } else { 595 $error = '@copyright tag must contain a year and the name of the copyright holder'; 596 $this->currentFile->addError($error, $errorPos, 'EmptyCopyright'); 597 } 598 } else { 599 $error = '@copyright tag must contain a year and the name of the copyright holder'; 600 $this->currentFile->addError($error, $errorPos, 'EmptyCopyright'); 601 }//end if 602 }//end if 603 604 }//end processCopyrights() 605 606 /** 607 * Process the license tag. 608 * 609 * @param int $errorPos The line number where the error occurs. 610 * 611 * @return void 612 */ 613 protected function processLicense($errorPos) { 614 $license = $this->commentParser->getLicense(); 615 if ($license !== null) { 616 $value = $license->getValue(); 617 $comment = $license->getComment(); 618 if ($value === '' || $comment === '') { 619 $error = '@license tag must contain a URL and a license name'; 620 $this->currentFile->addError($error, $errorPos, 'EmptyLicense'); 621 } 622 } 623 624 }//end processLicense() 625 626 /** 627 * Process the version tag. 628 * 629 * @param int $errorPos The line number where the error occurs. 630 * 631 * @return void 632 */ 633 protected function processVersion($errorPos) { 634 $version = $this->commentParser->getVersion(); 635 if ($version !== null) { 636 $content = $version->getContent(); 637 $matches = array(); 638 if (empty($content) === true) { 639 $error = 'Content missing for @version tag in file comment'; 640 $this->currentFile->addError($error, $errorPos, 'EmptyVersion'); 641 } else if (strstr($content, 'CVS:') === false && strstr($content, 'SVN:') === false 642 && strstr($content, 'GIT:') === false && strstr($content, 'HG:') === false) { 643 $error = 'Invalid version "%s" in file comment; consider "CVS: <cvs_id>" or "SVN: <svn_id>" or "GIT: <git_id>" or "HG: <hg_id>" instead'; 644 $data = array($content); 645 $this->currentFile->addWarning($error, $errorPos, 'InvalidVersion', $data); 646 } 647 } 648 649 }//end processVersion() 650 651}//end class 652 653?> 654