| OLD | NEW |
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 # Copyright (c) 2010 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2010 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Class for parsing metadata about extension samples.""" | 6 """Class for parsing metadata about extension samples.""" |
| 7 | 7 |
| 8 import locale | 8 import locale |
| 9 import os | 9 import os |
| 10 import os.path | 10 import os.path |
| (...skipping 207 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 218 | 218 |
| 219 manifest_data = {'samples': samples, 'api': api_method_dict} | 219 manifest_data = {'samples': samples, 'api': api_method_dict} |
| 220 return manifest_data | 220 return manifest_data |
| 221 | 221 |
| 222 def writeToFile(self, path): | 222 def writeToFile(self, path): |
| 223 """ Writes the contents of this manifest file as a JSON-encoded text file. | 223 """ Writes the contents of this manifest file as a JSON-encoded text file. |
| 224 | 224 |
| 225 Args: | 225 Args: |
| 226 path: The path to write the samples manifest file to. | 226 path: The path to write the samples manifest file to. |
| 227 """ | 227 """ |
| 228 | 228 manifest_text = json.dumps(self._manifest_data, indent=2, sort_keys=True) |
| 229 manifest_text = json.dumps(self._manifest_data, indent=2) | |
| 230 output_path = os.path.realpath(path) | 229 output_path = os.path.realpath(path) |
| 231 try: | 230 try: |
| 232 output_file = open(output_path, 'w') | 231 output_file = open(output_path, 'w') |
| 233 except IOError, msg: | 232 except IOError, msg: |
| 234 raise Exception("Failed to write the samples manifest file." | 233 raise Exception("Failed to write the samples manifest file." |
| 235 "The specific error was: %s." % msg) | 234 "The specific error was: %s." % msg) |
| 236 output_file.write(manifest_text) | 235 output_file.write(manifest_text) |
| 237 output_file.close() | 236 output_file.close() |
| 238 | 237 |
| 239 def writeZippedSamples(self): | 238 def writeZippedSamples(self): |
| 240 """ For each sample in the current manifest, create a zip file with the | 239 """ For each sample in the current manifest, create a zip file with the |
| 241 sample contents in the sample's parent directory. """ | 240 sample contents in the sample's parent directory if not zip exists, or |
| 241 update the zip file if the sample has been updated. |
| 242 | 242 |
| 243 Returns: |
| 244 A set of paths representing zip files which have been modified. |
| 245 """ |
| 246 modified_paths = [] |
| 243 for sample in self._manifest_data['samples']: | 247 for sample in self._manifest_data['samples']: |
| 244 sample.write_zip() | 248 path = sample.write_zip() |
| 249 if path: |
| 250 modified_paths.append(path) |
| 251 return modified_paths |
| 245 | 252 |
| 246 class Sample(dict): | 253 class Sample(dict): |
| 247 """ Represents metadata about a Chrome extension sample. | 254 """ Represents metadata about a Chrome extension sample. |
| 248 | 255 |
| 249 Extends dict so that it can be easily JSON serialized. | 256 Extends dict so that it can be easily JSON serialized. |
| 250 """ | 257 """ |
| 251 | 258 |
| 252 def __init__(self, manifest_path, api_methods, base_dir): | 259 def __init__(self, manifest_path, api_methods, base_dir): |
| 253 """ Initializes a Sample instance given a path to a manifest. | 260 """ Initializes a Sample instance given a path to a manifest. |
| 254 | 261 |
| 255 Args: | 262 Args: |
| 256 manifest_path: A filesystem path to a manifest file. | 263 manifest_path: A filesystem path to a manifest file. |
| 257 api_methods: A list of strings containing all possible Chrome extension | 264 api_methods: A list of strings containing all possible Chrome extension |
| 258 API calls. | 265 API calls. |
| 259 base_dir: The base directory where this sample will be referenced from - | 266 base_dir: The base directory where this sample will be referenced from - |
| 260 paths will be made relative to this directory. | 267 paths will be made relative to this directory. |
| 261 """ | 268 """ |
| 262 self._base_dir = base_dir | 269 self._base_dir = base_dir |
| 263 self._manifest_path = manifest_path | 270 self._manifest_path = manifest_path |
| 264 self._manifest = parse_json_file(self._manifest_path) | 271 self._manifest = parse_json_file(self._manifest_path) |
| 265 self._locale_data = self._parse_locale_data() | 272 self._locale_data = self._parse_locale_data() |
| 266 | 273 |
| 267 # The following properties will be serialized when converting this object | 274 # The following calls set data which will be serialized when converting |
| 268 # to JSON. | 275 # this object to JSON. |
| 276 source_data = self._parse_source_data(api_methods) |
| 277 self['api_calls'] = source_data['api_calls'] |
| 278 self['source_files'] = source_data['source_files'] |
| 279 self['source_hash'] = source_data['source_hash'] |
| 269 | 280 |
| 270 self['api_calls'] = self._parse_api_calls(api_methods) | |
| 271 self['name'] = self._parse_name() | 281 self['name'] = self._parse_name() |
| 272 self['description'] = self._parse_description() | 282 self['description'] = self._parse_description() |
| 273 self['icon'] = self._parse_icon() | 283 self['icon'] = self._parse_icon() |
| 274 self['features'] = self._parse_features() | 284 self['features'] = self._parse_features() |
| 275 self['protocols'] = self._parse_protocols() | 285 self['protocols'] = self._parse_protocols() |
| 276 self['path'] = self._get_relative_path() | 286 self['path'] = self._get_relative_path() |
| 277 self['search_string'] = self._get_search_string() | 287 self['search_string'] = self._get_search_string() |
| 278 self['source_files'] = self._parse_source_files() | |
| 279 self['id'] = hashlib.sha1(self['path']).hexdigest() | 288 self['id'] = hashlib.sha1(self['path']).hexdigest() |
| 280 self['zip_path'] = self._get_relative_zip_path() | 289 self['zip_path'] = self._get_relative_zip_path() |
| 281 | 290 |
| 282 _FEATURE_ATTRIBUTES = ( | 291 _FEATURE_ATTRIBUTES = ( |
| 283 'browser_action', | 292 'browser_action', |
| 284 'page_action', | 293 'page_action', |
| 285 'background_page', | 294 'background_page', |
| 286 'options_page', | 295 'options_page', |
| 287 'plugins', | 296 'plugins', |
| 288 'theme', | 297 'theme', |
| (...skipping 125 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 414 """ Returns the filename to be used for a generated zip of the sample. | 423 """ Returns the filename to be used for a generated zip of the sample. |
| 415 | 424 |
| 416 Returns: | 425 Returns: |
| 417 A string in the form of "<dirname>.zip" where <dirname> is the name | 426 A string in the form of "<dirname>.zip" where <dirname> is the name |
| 418 of the directory containing this sample's manifest.json. | 427 of the directory containing this sample's manifest.json. |
| 419 """ | 428 """ |
| 420 sample_path = os.path.realpath(os.path.dirname(self._manifest_path)) | 429 sample_path = os.path.realpath(os.path.dirname(self._manifest_path)) |
| 421 sample_dirname = os.path.basename(sample_path) | 430 sample_dirname = os.path.basename(sample_path) |
| 422 return "%s.zip" % sample_dirname | 431 return "%s.zip" % sample_dirname |
| 423 | 432 |
| 424 def _parse_api_calls(self, api_methods): | |
| 425 """ Returns a list of Chrome extension API calls the sample makes. | |
| 426 | |
| 427 Parses any *.html and *.js files in the sample directory and matches them | |
| 428 against the supplied list of all available API methods, returning methods | |
| 429 which show up in the sample code. | |
| 430 | |
| 431 Args: | |
| 432 api_methods: A list of strings containing the potential | |
| 433 API calls the and the extension sample could be making. | |
| 434 | |
| 435 Returns: | |
| 436 A set of every member of api_methods that appears in any *.html or *.js | |
| 437 file contained in this sample's directory (or subdirectories). | |
| 438 | |
| 439 Raises: | |
| 440 Exception: If any of the *.html or *.js files cannot be read. | |
| 441 """ | |
| 442 api_calls = set() | |
| 443 extension_dir_path = os.path.dirname(self._manifest_path) | |
| 444 for root, dirs, files in sorted_walk(extension_dir_path): | |
| 445 for file in files: | |
| 446 if file[-5:] == '.html' or file[-3:] == '.js': | |
| 447 path = os.path.join(root, file) | |
| 448 try: | |
| 449 code_file = open(path, "r") | |
| 450 except IOError, msg: | |
| 451 raise Exception("Failed to read %s: %s" % (path, msg)) | |
| 452 code_contents = unicode(code_file.read(), errors="replace") | |
| 453 code_file.close() | |
| 454 | |
| 455 for method in api_methods: | |
| 456 if (code_contents.find(method) > -1): | |
| 457 api_calls.add(method) | |
| 458 return sorted(api_calls) | |
| 459 | |
| 460 def _parse_source_files(self): | |
| 461 """ Returns a list of paths to source files present in the extenion. | |
| 462 | |
| 463 Returns: | |
| 464 A list of paths relative to the manifest file directory. | |
| 465 """ | |
| 466 source_paths = [] | |
| 467 base_path = os.path.realpath(os.path.dirname(self._manifest_path)) | |
| 468 for root, directories, files in sorted_walk(base_path): | |
| 469 if '.svn' in directories: | |
| 470 directories.remove('.svn') # Don't go into SVN metadata directories | |
| 471 | |
| 472 for file_name in files: | |
| 473 ext = os.path.splitext(file_name)[1] | |
| 474 if ext in self._SOURCE_FILE_EXTENSIONS: | |
| 475 path = os.path.realpath(os.path.join(root, file_name)) | |
| 476 path = path.replace(base_path, '')[1:] | |
| 477 source_paths.append(path) | |
| 478 return sorted(source_paths) | |
| 479 | |
| 480 def _parse_description(self): | 433 def _parse_description(self): |
| 481 """ Returns a localized description of the extension. | 434 """ Returns a localized description of the extension. |
| 482 | 435 |
| 483 Returns: | 436 Returns: |
| 484 A localized version of the sample's description. | 437 A localized version of the sample's description. |
| 485 """ | 438 """ |
| 486 return self._get_localized_manifest_value('description') | 439 return self._get_localized_manifest_value('description') |
| 487 | 440 |
| 488 def _parse_features(self): | 441 def _parse_features(self): |
| 489 """ Returns a list of features the sample uses. | 442 """ Returns a list of features the sample uses. |
| (...skipping 80 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 570 A list of every unique protocol listed in the manifest's permssions. | 523 A list of every unique protocol listed in the manifest's permssions. |
| 571 """ | 524 """ |
| 572 protocols = [] | 525 protocols = [] |
| 573 if self._manifest.has_key('permissions'): | 526 if self._manifest.has_key('permissions'): |
| 574 for permission in self._manifest['permissions']: | 527 for permission in self._manifest['permissions']: |
| 575 split = permission.split('://') | 528 split = permission.split('://') |
| 576 if (len(split) == 2) and (split[0] not in protocols): | 529 if (len(split) == 2) and (split[0] not in protocols): |
| 577 protocols.append(split[0] + "://") | 530 protocols.append(split[0] + "://") |
| 578 return protocols | 531 return protocols |
| 579 | 532 |
| 533 def _parse_source_data(self, api_methods): |
| 534 """ Iterates over the sample's source files and parses data from them. |
| 535 |
| 536 Parses any files in the sample directory with known source extensions |
| 537 (as defined in self._SOURCE_FILE_EXTENSIONS). For each file, this method: |
| 538 |
| 539 1. Stores a relative path from the manifest.json directory to the file. |
| 540 2. Searches through the contents of the file for chrome.* API calls. |
| 541 3. Calculates a SHA1 digest for the contents of the file. |
| 542 |
| 543 Args: |
| 544 api_methods: A list of strings containing the potential |
| 545 API calls the and the extension sample could be making. |
| 546 |
| 547 Raises: |
| 548 Exception: If any of the source files cannot be read. |
| 549 |
| 550 Returns: |
| 551 A dictionary containing the keys/values: |
| 552 'api_calls' A sorted list of API calls the sample makes. |
| 553 'source_files' A sorted list of paths to files the extension uses. |
| 554 'source_hash' A hash of the individual file hashes. |
| 555 """ |
| 556 data = {} |
| 557 source_paths = [] |
| 558 source_hashes = [] |
| 559 api_calls = set() |
| 560 base_path = os.path.realpath(os.path.dirname(self._manifest_path)) |
| 561 for root, directories, files in sorted_walk(base_path): |
| 562 if '.svn' in directories: |
| 563 directories.remove('.svn') # Don't go into SVN metadata directories |
| 564 |
| 565 for file_name in files: |
| 566 ext = os.path.splitext(file_name)[1] |
| 567 if ext in self._SOURCE_FILE_EXTENSIONS: |
| 568 # Add the file path to the list of source paths. |
| 569 fullpath = os.path.realpath(os.path.join(root, file_name)) |
| 570 path = fullpath.replace(base_path, '')[1:] |
| 571 source_paths.append(path) |
| 572 |
| 573 # Read the contents and parse out API calls. |
| 574 try: |
| 575 code_file = open(fullpath, "r") |
| 576 except IOError, msg: |
| 577 raise Exception("Failed to read %s: %s" % (fullpath, msg)) |
| 578 code_contents = unicode(code_file.read(), errors="replace") |
| 579 code_file.close() |
| 580 for method in api_methods: |
| 581 if (code_contents.find(method) > -1): |
| 582 api_calls.add(method) |
| 583 |
| 584 # Get a hash of the file contents for zip file generation. |
| 585 hash = hashlib.sha1(code_contents.encode("ascii", "replace")) |
| 586 source_hashes.append(hash.hexdigest()) |
| 587 |
| 588 data['api_calls'] = sorted(api_calls) |
| 589 data['source_files'] = sorted(source_paths) |
| 590 data['source_hash'] = hashlib.sha1(''.join(source_hashes)).hexdigest() |
| 591 return data |
| 592 |
| 580 def _uses_background(self): | 593 def _uses_background(self): |
| 581 """ Returns true if the extension defines a background page. """ | 594 """ Returns true if the extension defines a background page. """ |
| 582 return self._manifest.has_key('background_page') | 595 return self._manifest.has_key('background_page') |
| 583 | 596 |
| 584 def _uses_browser_action(self): | 597 def _uses_browser_action(self): |
| 585 """ Returns true if the extension defines a browser action. """ | 598 """ Returns true if the extension defines a browser action. """ |
| 586 return self._manifest.has_key('browser_action') | 599 return self._manifest.has_key('browser_action') |
| 587 | 600 |
| 588 def _uses_content_scripts(self): | 601 def _uses_content_scripts(self): |
| 589 """ Returns true if the extension uses content scripts. """ | 602 """ Returns true if the extension uses content scripts. """ |
| (...skipping 21 matching lines...) Expand all Loading... |
| 611 return self._manifest.has_key('app') | 624 return self._manifest.has_key('app') |
| 612 | 625 |
| 613 def write_zip(self): | 626 def write_zip(self): |
| 614 """ Writes a zip file containing all of the files in this Sample's dir.""" | 627 """ Writes a zip file containing all of the files in this Sample's dir.""" |
| 615 sample_path = os.path.realpath(os.path.dirname(self._manifest_path)) | 628 sample_path = os.path.realpath(os.path.dirname(self._manifest_path)) |
| 616 sample_dirname = os.path.basename(sample_path) | 629 sample_dirname = os.path.basename(sample_path) |
| 617 sample_parentpath = os.path.dirname(sample_path) | 630 sample_parentpath = os.path.dirname(sample_path) |
| 618 | 631 |
| 619 zip_filename = self._get_zip_filename() | 632 zip_filename = self._get_zip_filename() |
| 620 zip_path = os.path.join(sample_parentpath, zip_filename) | 633 zip_path = os.path.join(sample_parentpath, zip_filename) |
| 634 zip_manifest_path = os.path.join(sample_dirname, 'manifest.json') |
| 635 |
| 636 zipfile.ZipFile.debug = 3 |
| 637 |
| 638 if os.path.isfile(zip_path): |
| 639 try: |
| 640 old_zip_file = zipfile.ZipFile(zip_path, 'r') |
| 641 except IOError, msg: |
| 642 raise Exception("Could not read zip at %s: %s" % (zip_path, msg)) |
| 643 |
| 644 try: |
| 645 info = old_zip_file.getinfo(zip_manifest_path) |
| 646 hash = info.comment |
| 647 if hash == self['source_hash']: |
| 648 return None # Hashes match - no need to generate file |
| 649 except KeyError, msg: |
| 650 pass # The old zip file doesn't contain a hash - overwrite |
| 651 finally: |
| 652 old_zip_file.close() |
| 653 |
| 621 zip_file = zipfile.ZipFile(zip_path, 'w') | 654 zip_file = zipfile.ZipFile(zip_path, 'w') |
| 622 | 655 |
| 623 try: | 656 try: |
| 624 for root, dirs, files in sorted_walk(sample_path): | 657 for root, dirs, files in sorted_walk(sample_path): |
| 625 if '.svn' in dirs: | 658 if '.svn' in dirs: |
| 626 dirs.remove('.svn') | 659 dirs.remove('.svn') |
| 627 for file in files: | 660 for file in files: |
| 628 # Absolute path to the file to be added. | 661 # Absolute path to the file to be added. |
| 629 abspath = os.path.realpath(os.path.join(root, file)) | 662 abspath = os.path.realpath(os.path.join(root, file)) |
| 630 # Relative path to store the file in under the zip. | 663 # Relative path to store the file in under the zip. |
| 631 relpath = sample_dirname + abspath.replace(sample_path, "") | 664 relpath = sample_dirname + abspath.replace(sample_path, "") |
| 665 |
| 632 zip_file.write(abspath, relpath) | 666 zip_file.write(abspath, relpath) |
| 667 if file == 'manifest.json': |
| 668 info = zip_file.getinfo(zip_manifest_path) |
| 669 info.comment = self['source_hash'] |
| 633 except RuntimeError, msg: | 670 except RuntimeError, msg: |
| 634 raise Exception("Could not write zip at " % zip_path) | 671 raise Exception("Could not write zip at %s: %s" % (zip_path, msg)) |
| 635 finally: | 672 finally: |
| 636 zip_file.close() | 673 zip_file.close() |
| 674 |
| 675 return self._get_relative_zip_path() |
| OLD | NEW |