| # This Source Code Form is subject to the terms of the Mozilla Public |
| # License, v. 2.0. If a copy of the MPL was not distributed with this file, |
| # You can obtain one at http://mozilla.org/MPL/2.0/. |
| |
| """ |
| Handles installing open webapps (https://developer.mozilla.org/en-US/docs/Apps) |
| to a profile. A webapp object is a dict that contains some metadata about |
| the webapp and must at least include a name, description and manifestURL. |
| |
| Each webapp has a manifest (https://developer.mozilla.org/en-US/docs/Apps/Manifest). |
| Additionally there is a separate json manifest that keeps track of the installed |
| webapps, their manifestURLs and their permissions. |
| """ |
| |
| __all__ = ["Webapp", "WebappCollection", "WebappFormatException", "APP_STATUS_NOT_INSTALLED", |
| "APP_STATUS_INSTALLED", "APP_STATUS_PRIVILEGED", "APP_STATUS_CERTIFIED"] |
| |
| from string import Template |
| import json |
| import os |
| import shutil |
| |
| import mozfile |
| |
| |
| # from http://hg.mozilla.org/mozilla-central/file/add0b94c2c0b/caps/idl/nsIPrincipal.idl#l163 |
| APP_STATUS_NOT_INSTALLED = 0 |
| APP_STATUS_INSTALLED = 1 |
| APP_STATUS_PRIVILEGED = 2 |
| APP_STATUS_CERTIFIED = 3 |
| |
| |
| class WebappFormatException(Exception): |
| """thrown for invalid webapp objects""" |
| |
| |
| class Webapp(dict): |
| """A webapp definition""" |
| |
| required_keys = ('name', 'description', 'manifestURL') |
| |
| def __init__(self, *args, **kwargs): |
| try: |
| dict.__init__(self, *args, **kwargs) |
| except (TypeError, ValueError): |
| raise WebappFormatException("Webapp object should be an instance of type 'dict'") |
| self.validate() |
| |
| def __eq__(self, other): |
| """Webapps are considered equal if they have the same name""" |
| if not isinstance(other, self.__class__): |
| return False |
| return self['name'] == other['name'] |
| |
| def __ne__(self, other): |
| """Webapps are considered not equal if they have different names""" |
| return not self.__eq__(other) |
| |
| def validate(self): |
| # TODO some keys are required if another key has a certain value |
| for key in self.required_keys: |
| if key not in self: |
| raise WebappFormatException("Webapp object missing required key '%s'" % key) |
| |
| |
| class WebappCollection(object): |
| """A list-like object that collects webapps and updates the webapp manifests""" |
| |
| json_template = Template(""""$name": { |
| "name": "$name", |
| "origin": "$origin", |
| "installOrigin": "$origin", |
| "receipt": null, |
| "installTime": 132333986000, |
| "manifestURL": "$manifestURL", |
| "localId": $localId, |
| "id": "$name", |
| "appStatus": $appStatus, |
| "csp": "$csp" |
| }""") |
| |
| manifest_template = Template("""{ |
| "name": "$name", |
| "csp": "$csp", |
| "description": "$description", |
| "launch_path": "/", |
| "developer": { |
| "name": "Mozilla", |
| "url": "https://mozilla.org/" |
| }, |
| "permissions": [ |
| ], |
| "locales": { |
| "en-US": { |
| "name": "$name", |
| "description": "$description" |
| } |
| }, |
| "default_locale": "en-US", |
| "icons": { |
| } |
| } |
| """) |
| |
| def __init__(self, profile, apps=None, json_template=None, manifest_template=None): |
| """ |
| :param profile: the file path to a profile |
| :param apps: [optional] a list of webapp objects or file paths to json files describing webapps |
| :param json_template: [optional] string template describing the webapp json format |
| :param manifest_template: [optional] string template describing the webapp manifest format |
| """ |
| if not isinstance(profile, basestring): |
| raise TypeError("Must provide path to a profile, received '%s'" % type(profile)) |
| self.profile = profile |
| self.webapps_dir = os.path.join(self.profile, 'webapps') |
| self.backup_dir = os.path.join(self.profile, '.mozprofile_backup', 'webapps') |
| |
| self._apps = [] |
| self._installed_apps = [] |
| if apps: |
| if not isinstance(apps, (list, set, tuple)): |
| apps = [apps] |
| |
| for app in apps: |
| if isinstance(app, basestring) and os.path.isfile(app): |
| self.extend(self.read_json(app)) |
| else: |
| self.append(app) |
| |
| self.json_template = json_template or self.json_template |
| self.manifest_template = manifest_template or self.manifest_template |
| |
| def __getitem__(self, index): |
| return self._apps.__getitem__(index) |
| |
| def __setitem__(self, index, value): |
| return self._apps.__setitem__(index, Webapp(value)) |
| |
| def __delitem__(self, index): |
| return self._apps.__delitem__(index) |
| |
| def __len__(self): |
| return self._apps.__len__() |
| |
| def __contains__(self, value): |
| return self._apps.__contains__(Webapp(value)) |
| |
| def append(self, value): |
| return self._apps.append(Webapp(value)) |
| |
| def insert(self, index, value): |
| return self._apps.insert(index, Webapp(value)) |
| |
| def extend(self, values): |
| return self._apps.extend([Webapp(v) for v in values]) |
| |
| def remove(self, value): |
| return self._apps.remove(Webapp(value)) |
| |
| def _write_webapps_json(self, apps): |
| contents = [] |
| for app in apps: |
| contents.append(self.json_template.substitute(app)) |
| contents = '{\n' + ',\n'.join(contents) + '\n}\n' |
| webapps_json_path = os.path.join(self.webapps_dir, 'webapps.json') |
| webapps_json_file = open(webapps_json_path, "w") |
| webapps_json_file.write(contents) |
| webapps_json_file.close() |
| |
| def _write_webapp_manifests(self, write_apps=[], remove_apps=[]): |
| # Write manifests for installed apps |
| for app in write_apps: |
| manifest_dir = os.path.join(self.webapps_dir, app['name']) |
| manifest_path = os.path.join(manifest_dir, 'manifest.webapp') |
| if not os.path.isfile(manifest_path): |
| if not os.path.isdir(manifest_dir): |
| os.mkdir(manifest_dir) |
| manifest = self.manifest_template.substitute(app) |
| manifest_file = open(manifest_path, "a") |
| manifest_file.write(manifest) |
| manifest_file.close() |
| # Remove manifests for removed apps |
| for app in remove_apps: |
| self._installed_apps.remove(app) |
| manifest_dir = os.path.join(self.webapps_dir, app['name']) |
| mozfile.remove(manifest_dir) |
| |
| def update_manifests(self): |
| """Updates the webapp manifests with the webapps represented in this collection |
| |
| If update_manifests is called a subsequent time, there could have been apps added or |
| removed to the collection in the interim. The manifests will be adjusted accordingly |
| """ |
| apps_to_install = [app for app in self._apps if app not in self._installed_apps] |
| apps_to_remove = [app for app in self._installed_apps if app not in self._apps] |
| if apps_to_install == apps_to_remove == []: |
| # nothing to do |
| return |
| |
| if not os.path.isdir(self.webapps_dir): |
| os.makedirs(self.webapps_dir) |
| elif not self._installed_apps: |
| shutil.copytree(self.webapps_dir, self.backup_dir) |
| |
| webapps_json_path = os.path.join(self.webapps_dir, 'webapps.json') |
| webapps_json = [] |
| if os.path.isfile(webapps_json_path): |
| webapps_json = self.read_json(webapps_json_path, description="description") |
| webapps_json = [a for a in webapps_json if a not in apps_to_remove] |
| |
| # Iterate over apps already in webapps.json to determine the starting local |
| # id and to ensure apps are properly formatted |
| start_id = 1 |
| for local_id, app in enumerate(webapps_json): |
| app['localId'] = local_id + 1 |
| start_id += 1 |
| if not app.get('csp'): |
| app['csp'] = '' |
| if not app.get('appStatus'): |
| app['appStatus'] = 3 |
| |
| # Append apps_to_install to the pre-existent apps |
| for local_id, app in enumerate(apps_to_install): |
| app['localId'] = local_id + start_id |
| # ignore if it's already installed |
| if app in webapps_json: |
| start_id -= 1 |
| continue |
| webapps_json.append(app) |
| self._installed_apps.append(app) |
| |
| # Write the full contents to webapps.json |
| self._write_webapps_json(webapps_json) |
| |
| # Create/remove manifest file for each app. |
| self._write_webapp_manifests(apps_to_install, apps_to_remove) |
| |
| def clean(self): |
| """Remove all webapps that were installed and restore profile to previous state""" |
| if self._installed_apps: |
| mozfile.remove(self.webapps_dir) |
| |
| if os.path.isdir(self.backup_dir): |
| shutil.copytree(self.backup_dir, self.webapps_dir) |
| mozfile.remove(self.backup_dir) |
| |
| self._apps = [] |
| self._installed_apps = [] |
| |
| @classmethod |
| def read_json(cls, path, **defaults): |
| """Reads a json file which describes a set of webapps. The json format is either a |
| dictionary where each key represents the name of a webapp (e.g B2G format) or a list |
| of webapp objects. |
| |
| :param path: Path to a json file defining webapps |
| :param defaults: Default key value pairs added to each webapp object if key doesn't exist |
| |
| Returns a list of Webapp objects |
| """ |
| f = open(path, 'r') |
| app_json = json.load(f) |
| f.close() |
| |
| apps = [] |
| if isinstance(app_json, dict): |
| for k, v in app_json.iteritems(): |
| v['name'] = k |
| apps.append(v) |
| else: |
| apps = app_json |
| if not isinstance(apps, list): |
| apps = [apps] |
| |
| ret = [] |
| for app in apps: |
| d = defaults.copy() |
| d.update(app) |
| ret.append(Webapp(**d)) |
| return ret |