mirror of
https://github.com/classilla/tenfourfox.git
synced 2024-06-25 20:29:33 +00:00
451 lines
19 KiB
Python
451 lines
19 KiB
Python
# 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/.
|
|
|
|
__all__ = ['Profile',
|
|
'FirefoxProfile',
|
|
'MetroFirefoxProfile',
|
|
'ThunderbirdProfile']
|
|
|
|
import os
|
|
import time
|
|
import tempfile
|
|
import types
|
|
import uuid
|
|
|
|
from addons import AddonManager
|
|
import mozfile
|
|
from permissions import Permissions
|
|
from prefs import Preferences
|
|
from shutil import copytree
|
|
from webapps import WebappCollection
|
|
|
|
|
|
class Profile(object):
|
|
"""Handles all operations regarding profile.
|
|
|
|
Creating new profiles, installing add-ons, setting preferences and
|
|
handling cleanup.
|
|
|
|
The files associated with the profile will be removed automatically after
|
|
the object is garbage collected: ::
|
|
|
|
profile = Profile()
|
|
print profile.profile # this is the path to the created profile
|
|
del profile
|
|
# the profile path has been removed from disk
|
|
|
|
:meth:`cleanup` is called under the hood to remove the profile files. You
|
|
can ensure this method is called (even in the case of exception) by using
|
|
the profile as a context manager: ::
|
|
|
|
with Profile() as profile:
|
|
# do things with the profile
|
|
pass
|
|
# profile.cleanup() has been called here
|
|
"""
|
|
|
|
def __init__(self, profile=None, addons=None, addon_manifests=None, apps=None,
|
|
preferences=None, locations=None, proxy=None, restore=True):
|
|
"""
|
|
:param profile: Path to the profile
|
|
:param addons: String of one or list of addons to install
|
|
:param addon_manifests: Manifest for addons (see http://bit.ly/17jQ7i6)
|
|
:param apps: Dictionary or class of webapps to install
|
|
:param preferences: Dictionary or class of preferences
|
|
:param locations: ServerLocations object
|
|
:param proxy: Setup a proxy
|
|
:param restore: Flag for removing all custom settings during cleanup
|
|
"""
|
|
self._addons = addons
|
|
self._addon_manifests = addon_manifests
|
|
self._apps = apps
|
|
self._locations = locations
|
|
self._proxy = proxy
|
|
|
|
# Prepare additional preferences
|
|
if preferences:
|
|
if isinstance(preferences, dict):
|
|
# unordered
|
|
preferences = preferences.items()
|
|
|
|
# sanity check
|
|
assert not [i for i in preferences if len(i) != 2]
|
|
else:
|
|
preferences = []
|
|
self._preferences = preferences
|
|
|
|
# Handle profile creation
|
|
self.create_new = not profile
|
|
if profile:
|
|
# Ensure we have a full path to the profile
|
|
self.profile = os.path.abspath(os.path.expanduser(profile))
|
|
else:
|
|
self.profile = tempfile.mkdtemp(suffix='.mozrunner')
|
|
|
|
self.restore = restore
|
|
|
|
# Initialize all class members
|
|
self._internal_init()
|
|
|
|
def _internal_init(self):
|
|
"""Internal: Initialize all class members to their default value"""
|
|
|
|
if not os.path.exists(self.profile):
|
|
os.makedirs(self.profile)
|
|
|
|
# Preferences files written to
|
|
self.written_prefs = set()
|
|
|
|
# Our magic markers
|
|
nonce = '%s %s' % (str(time.time()), uuid.uuid4())
|
|
self.delimeters = ('#MozRunner Prefs Start %s' % nonce,
|
|
'#MozRunner Prefs End %s' % nonce)
|
|
|
|
# If sub-classes want to set default preferences
|
|
if hasattr(self.__class__, 'preferences'):
|
|
self.set_preferences(self.__class__.preferences)
|
|
# Set additional preferences
|
|
self.set_preferences(self._preferences)
|
|
|
|
self.permissions = Permissions(self.profile, self._locations)
|
|
prefs_js, user_js = self.permissions.network_prefs(self._proxy)
|
|
self.set_preferences(prefs_js, 'prefs.js')
|
|
self.set_preferences(user_js)
|
|
|
|
# handle add-on installation
|
|
self.addon_manager = AddonManager(self.profile, restore=self.restore)
|
|
self.addon_manager.install_addons(self._addons, self._addon_manifests)
|
|
|
|
# handle webapps
|
|
self.webapps = WebappCollection(profile=self.profile, apps=self._apps)
|
|
self.webapps.update_manifests()
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, type, value, traceback):
|
|
self.cleanup()
|
|
|
|
def __del__(self):
|
|
self.cleanup()
|
|
|
|
### cleanup
|
|
|
|
def cleanup(self):
|
|
"""Cleanup operations for the profile."""
|
|
|
|
if self.restore:
|
|
# If copies of those class instances exist ensure we correctly
|
|
# reset them all (see bug 934484)
|
|
self.clean_preferences()
|
|
if getattr(self, 'addon_manager', None) is not None:
|
|
self.addon_manager.clean()
|
|
if getattr(self, 'permissions', None) is not None:
|
|
self.permissions.clean_db()
|
|
if getattr(self, 'webapps', None) is not None:
|
|
self.webapps.clean()
|
|
|
|
# If it's a temporary profile we have to remove it
|
|
if self.create_new:
|
|
mozfile.remove(self.profile)
|
|
|
|
def reset(self):
|
|
"""
|
|
reset the profile to the beginning state
|
|
"""
|
|
self.cleanup()
|
|
|
|
self._internal_init()
|
|
|
|
def clean_preferences(self):
|
|
"""Removed preferences added by mozrunner."""
|
|
for filename in self.written_prefs:
|
|
if not os.path.exists(os.path.join(self.profile, filename)):
|
|
# file has been deleted
|
|
break
|
|
while True:
|
|
if not self.pop_preferences(filename):
|
|
break
|
|
|
|
@classmethod
|
|
def clone(cls, path_from, path_to=None, **kwargs):
|
|
"""Instantiate a temporary profile via cloning
|
|
- path: path of the basis to clone
|
|
- kwargs: arguments to the profile constructor
|
|
"""
|
|
if not path_to:
|
|
tempdir = tempfile.mkdtemp() # need an unused temp dir name
|
|
mozfile.remove(tempdir) # copytree requires that dest does not exist
|
|
path_to = tempdir
|
|
copytree(path_from, path_to)
|
|
|
|
c = cls(path_to, **kwargs)
|
|
c.create_new = True # deletes a cloned profile when restore is True
|
|
return c
|
|
|
|
def exists(self):
|
|
"""returns whether the profile exists or not"""
|
|
return os.path.exists(self.profile)
|
|
|
|
### methods for preferences
|
|
|
|
def set_preferences(self, preferences, filename='user.js'):
|
|
"""Adds preferences dict to profile preferences"""
|
|
|
|
# append to the file
|
|
prefs_file = os.path.join(self.profile, filename)
|
|
f = open(prefs_file, 'a')
|
|
|
|
if preferences:
|
|
|
|
# note what files we've touched
|
|
self.written_prefs.add(filename)
|
|
|
|
# opening delimeter
|
|
f.write('\n%s\n' % self.delimeters[0])
|
|
|
|
# write the preferences
|
|
Preferences.write(f, preferences)
|
|
|
|
# closing delimeter
|
|
f.write('%s\n' % self.delimeters[1])
|
|
|
|
f.close()
|
|
|
|
def set_persistent_preferences(self, preferences):
|
|
"""
|
|
Adds preferences dict to profile preferences and save them during a
|
|
profile reset
|
|
"""
|
|
|
|
# this is a dict sometimes, convert
|
|
if isinstance(preferences, dict):
|
|
preferences = preferences.items()
|
|
|
|
# add new prefs to preserve them during reset
|
|
for new_pref in preferences:
|
|
# if dupe remove item from original list
|
|
self._preferences = [
|
|
pref for pref in self._preferences if not new_pref[0] == pref[0]]
|
|
self._preferences.append(new_pref)
|
|
|
|
self.set_preferences(preferences, filename='user.js')
|
|
|
|
def pop_preferences(self, filename):
|
|
"""
|
|
pop the last set of preferences added
|
|
returns True if popped
|
|
"""
|
|
|
|
path = os.path.join(self.profile, filename)
|
|
with file(path) as f:
|
|
lines = f.read().splitlines()
|
|
def last_index(_list, value):
|
|
"""
|
|
returns the last index of an item;
|
|
this should actually be part of python code but it isn't
|
|
"""
|
|
for index in reversed(range(len(_list))):
|
|
if _list[index] == value:
|
|
return index
|
|
s = last_index(lines, self.delimeters[0])
|
|
e = last_index(lines, self.delimeters[1])
|
|
|
|
# ensure both markers are found
|
|
if s is None:
|
|
assert e is None, '%s found without %s' % (self.delimeters[1], self.delimeters[0])
|
|
return False # no preferences found
|
|
elif e is None:
|
|
assert s is None, '%s found without %s' % (self.delimeters[0], self.delimeters[1])
|
|
|
|
# ensure the markers are in the proper order
|
|
assert e > s, '%s found at %s, while %s found at %s' % (self.delimeters[1], e, self.delimeters[0], s)
|
|
|
|
# write the prefs
|
|
cleaned_prefs = '\n'.join(lines[:s] + lines[e+1:])
|
|
with file(path, 'w') as f:
|
|
f.write(cleaned_prefs)
|
|
return True
|
|
|
|
### methods for introspection
|
|
|
|
def summary(self, return_parts=False):
|
|
"""
|
|
returns string summarizing profile information.
|
|
if return_parts is true, return the (Part_name, value) list
|
|
of tuples instead of the assembled string
|
|
"""
|
|
|
|
parts = [('Path', self.profile)] # profile path
|
|
|
|
# directory tree
|
|
parts.append(('Files', '\n%s' % mozfile.tree(self.profile)))
|
|
|
|
# preferences
|
|
for prefs_file in ('user.js', 'prefs.js'):
|
|
path = os.path.join(self.profile, prefs_file)
|
|
if os.path.exists(path):
|
|
|
|
# prefs that get their own section
|
|
# This is currently only 'network.proxy.autoconfig_url'
|
|
# but could be expanded to include others
|
|
section_prefs = ['network.proxy.autoconfig_url']
|
|
line_length = 80
|
|
line_length_buffer = 10 # buffer for 80 character display: length = 80 - len(key) - len(': ') - line_length_buffer
|
|
line_length_buffer += len(': ')
|
|
def format_value(key, value):
|
|
if key not in section_prefs:
|
|
return value
|
|
max_length = line_length - len(key) - line_length_buffer
|
|
if len(value) > max_length:
|
|
value = '%s...' % value[:max_length]
|
|
return value
|
|
|
|
prefs = Preferences.read_prefs(path)
|
|
if prefs:
|
|
prefs = dict(prefs)
|
|
parts.append((prefs_file,
|
|
'\n%s' %('\n'.join(['%s: %s' % (key, format_value(key, prefs[key]))
|
|
for key in sorted(prefs.keys())
|
|
]))))
|
|
|
|
# Currently hardcorded to 'network.proxy.autoconfig_url'
|
|
# but could be generalized, possibly with a generalized (simple)
|
|
# JS-parser
|
|
network_proxy_autoconfig = prefs.get('network.proxy.autoconfig_url')
|
|
if network_proxy_autoconfig and network_proxy_autoconfig.strip():
|
|
network_proxy_autoconfig = network_proxy_autoconfig.strip()
|
|
lines = network_proxy_autoconfig.replace(';', ';\n').splitlines()
|
|
lines = [line.strip() for line in lines]
|
|
origins_string = 'var origins = ['
|
|
origins_end = '];'
|
|
if origins_string in lines[0]:
|
|
start = lines[0].find(origins_string)
|
|
end = lines[0].find(origins_end, start);
|
|
splitline = [lines[0][:start],
|
|
lines[0][start:start+len(origins_string)-1],
|
|
]
|
|
splitline.extend(lines[0][start+len(origins_string):end].replace(',', ',\n').splitlines())
|
|
splitline.append(lines[0][end:])
|
|
lines[0:1] = [i.strip() for i in splitline]
|
|
parts.append(('Network Proxy Autoconfig, %s' % (prefs_file),
|
|
'\n%s' % '\n'.join(lines)))
|
|
|
|
if return_parts:
|
|
return parts
|
|
|
|
retval = '%s\n' % ('\n\n'.join(['[%s]: %s' % (key, value)
|
|
for key, value in parts]))
|
|
return retval
|
|
|
|
__str__ = summary
|
|
|
|
|
|
class FirefoxProfile(Profile):
|
|
"""Specialized Profile subclass for Firefox"""
|
|
|
|
preferences = {# Don't automatically update the application
|
|
'app.update.enabled' : False,
|
|
# Don't restore the last open set of tabs if the browser has crashed
|
|
'browser.sessionstore.resume_from_crash': False,
|
|
# Don't check for the default web browser during startup
|
|
'browser.shell.checkDefaultBrowser' : False,
|
|
# Don't warn on exit when multiple tabs are open
|
|
'browser.tabs.warnOnClose' : False,
|
|
# Don't warn when exiting the browser
|
|
'browser.warnOnQuit': False,
|
|
# Don't send Firefox health reports to the production server
|
|
'datareporting.healthreport.documentServerURI' : 'http://%(server)s/healthreport/',
|
|
# Only install add-ons from the profile and the application scope
|
|
# Also ensure that those are not getting disabled.
|
|
# see: https://developer.mozilla.org/en/Installing_extensions
|
|
'extensions.enabledScopes' : 5,
|
|
'extensions.autoDisableScopes' : 10,
|
|
# Don't send the list of installed addons to AMO
|
|
'extensions.getAddons.cache.enabled' : False,
|
|
# Don't install distribution add-ons from the app folder
|
|
'extensions.installDistroAddons' : False,
|
|
# Dont' run the add-on compatibility check during start-up
|
|
'extensions.showMismatchUI' : False,
|
|
# Don't automatically update add-ons
|
|
'extensions.update.enabled' : False,
|
|
# Don't open a dialog to show available add-on updates
|
|
'extensions.update.notifyUser' : False,
|
|
# Enable test mode to run multiple tests in parallel
|
|
'focusmanager.testmode' : True,
|
|
# Enable test mode to not raise an OS level dialog for location sharing
|
|
'geo.provider.testing' : True,
|
|
# Suppress delay for main action in popup notifications
|
|
'security.notification_enable_delay' : 0,
|
|
# Suppress automatic safe mode after crashes
|
|
'toolkit.startup.max_resumed_crashes' : -1,
|
|
# Don't report telemetry information
|
|
'toolkit.telemetry.enabled' : False,
|
|
# Don't send Telemetry reports to the production server. This is
|
|
# needed as Telemetry sends pings also if FHR upload is enabled.
|
|
'toolkit.telemetry.server' : 'http://%(server)s/telemetry-dummy/',
|
|
# Our current tests expect the unified Telemetry feature to be opt-out,
|
|
# which is not true while we hold back shipping it.
|
|
'toolkit.telemetry.unifiedIsOptIn': True,
|
|
}
|
|
|
|
class MetroFirefoxProfile(Profile):
|
|
"""Specialized Profile subclass for Firefox Metro"""
|
|
|
|
preferences = {# Don't automatically update the application for desktop and metro build
|
|
'app.update.enabled' : False,
|
|
'app.update.metro.enabled' : False,
|
|
# Dismiss first run content overlay
|
|
'browser.firstrun-content.dismissed' : True,
|
|
# Don't restore the last open set of tabs if the browser has crashed
|
|
'browser.sessionstore.resume_from_crash': False,
|
|
# Don't check for the default web browser during startup
|
|
'browser.shell.checkDefaultBrowser' : False,
|
|
# Don't send Firefox health reports to the production server
|
|
'datareporting.healthreport.documentServerURI' : 'http://%(server)s/healthreport/',
|
|
# Enable extensions
|
|
'extensions.defaultProviders.enabled' : True,
|
|
# Only install add-ons from the profile and the application scope
|
|
# Also ensure that those are not getting disabled.
|
|
# see: https://developer.mozilla.org/en/Installing_extensions
|
|
'extensions.enabledScopes' : 5,
|
|
'extensions.autoDisableScopes' : 10,
|
|
# Don't send the list of installed addons to AMO
|
|
'extensions.getAddons.cache.enabled' : False,
|
|
# Don't install distribution add-ons from the app folder
|
|
'extensions.installDistroAddons' : False,
|
|
# Dont' run the add-on compatibility check during start-up
|
|
'extensions.showMismatchUI' : False,
|
|
# Disable strict compatibility checks to allow add-ons enabled by default
|
|
'extensions.strictCompatibility' : False,
|
|
# Don't automatically update add-ons
|
|
'extensions.update.enabled' : False,
|
|
# Don't open a dialog to show available add-on updates
|
|
'extensions.update.notifyUser' : False,
|
|
# Enable test mode to run multiple tests in parallel
|
|
'focusmanager.testmode' : True,
|
|
# Suppress delay for main action in popup notifications
|
|
'security.notification_enable_delay' : 0,
|
|
# Suppress automatic safe mode after crashes
|
|
'toolkit.startup.max_resumed_crashes' : -1,
|
|
# Don't report telemetry information
|
|
'toolkit.telemetry.enabled' : False,
|
|
# Don't send Telemetry reports to the production server. This is
|
|
# needed as Telemetry sends pings also if FHR upload is enabled.
|
|
'toolkit.telemetry.server' : 'http://%(server)s/telemetry-dummy/',
|
|
}
|
|
|
|
class ThunderbirdProfile(Profile):
|
|
"""Specialized Profile subclass for Thunderbird"""
|
|
|
|
preferences = {'extensions.update.enabled' : False,
|
|
'extensions.update.notifyUser' : False,
|
|
'browser.shell.checkDefaultBrowser' : False,
|
|
'browser.tabs.warnOnClose' : False,
|
|
'browser.warnOnQuit': False,
|
|
'browser.sessionstore.resume_from_crash': False,
|
|
# prevents the 'new e-mail address' wizard on new profile
|
|
'mail.provider.enabled': False,
|
|
}
|