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 |