blob: 34e0cd533c27b6ca0fbaf35df8d6f24230898931 [file] [log] [blame]
# 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