diff options
Diffstat (limited to 'testing/marionette/puppeteer/firefox/firefox_puppeteer/api')
8 files changed, 956 insertions, 0 deletions
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/__init__.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/__init__.py diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/appinfo.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/appinfo.py new file mode 100644 index 000000000..13d32c15b --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/appinfo.py @@ -0,0 +1,45 @@ +# 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/. + +from firefox_puppeteer.base import BaseLib + + +class AppInfo(BaseLib): + """This class provides access to various attributes of AppInfo. + + For more details on AppInfo, visit: + https://developer.mozilla.org/en-US/docs/Mozilla/QA/Mozmill_tests/Shared_Modules/UtilsAPI/appInfo + """ + + def __getattr__(self, attr): + with self.marionette.using_context('chrome'): + value = self.marionette.execute_script(""" + Components.utils.import("resource://gre/modules/Services.jsm"); + + return Services.appinfo[arguments[0]]; + """, script_args=[attr]) + + if value is not None: + return value + else: + raise AttributeError('{} has no attribute {}'.format(self.__class__.__name__, + attr)) + + @property + def locale(self): + with self.marionette.using_context('chrome'): + return self.marionette.execute_script(""" + return Components.classes["@mozilla.org/chrome/chrome-registry;1"] + .getService(Components.interfaces.nsIXULChromeRegistry) + .getSelectedLocale("global"); + """) + + @property + def user_agent(self): + with self.marionette.using_context('chrome'): + return self.marionette.execute_script(""" + return Components.classes["@mozilla.org/network/protocol;1?name=http"] + .getService(Components.interfaces.nsIHttpProtocolHandler) + .userAgent; + """) diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/keys.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/keys.py new file mode 100644 index 000000000..2c5a1e523 --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/keys.py @@ -0,0 +1,17 @@ +# 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/. + +import marionette_driver + + +class Keys(marionette_driver.keys.Keys): + """Proxy to marionette's keys with an "accel" provided for convenience + testing across platforms.""" + + def __init__(self, marionette): + self.isDarwin = marionette.session_capabilities['platformName'] == 'darwin' + + @property + def ACCEL(self): + return self.META if self.isDarwin else self.CONTROL diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/l10n.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/l10n.py new file mode 100644 index 000000000..f7f52918c --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/l10n.py @@ -0,0 +1,125 @@ +# 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/. + +# ----------------- +# DEPRECATED module +# ----------------- +# Replace its use in tests when Firefox 45 ESR support ends with +# marionette_driver.localization.L10n + +import copy + +from marionette_driver.errors import ( + NoSuchElementException, + UnknownCommandException, +) +from marionette_driver.localization import L10n as L10nMarionette + +from firefox_puppeteer.base import BaseLib + + +class L10n(BaseLib): + """An API which allows Marionette to handle localized content. + + .. deprecated:: 52.2.0 + Use the localization module from :py:mod:`marionette_driver` instead. + + The `localization`_ of UI elements in Gecko based applications is done via + entities and properties. For static values entities are used, which are located + in .dtd files. Whereby for dynamically updated content the values come from + .property files. Both types of elements can be identifed via a unique id, + and the translated content retrieved. + + .. _localization: https://mzl.la/2eUMjyF + """ + + def __init__(self, marionette): + super(L10n, self).__init__(marionette) + + self._l10nMarionette = L10nMarionette(self.marionette) + + def localize_entity(self, dtd_urls, entity_id): + """Returns the localized string for the specified DTD entity id. + + To find the entity all given DTD files will be searched for the id. + + :param dtd_urls: A list of dtd files to search. + :param entity_id: The id to retrieve the value from. + + :returns: The localized string for the requested entity. + + :raises NoSuchElementException: When entity id is not found in dtd_urls. + """ + # Add xhtml11.dtd to prevent missing entity errors with XHTML files + try: + return self._l10nMarionette.localize_entity(dtd_urls, entity_id) + except UnknownCommandException: + dtds = copy.copy(dtd_urls) + dtds.append("resource:///res/dtd/xhtml11.dtd") + + dtd_refs = '' + for index, item in enumerate(dtds): + dtd_id = 'dtd_%s' % index + dtd_refs += '<!ENTITY %% %s SYSTEM "%s">%%%s;' % \ + (dtd_id, item, dtd_id) + + contents = """<?xml version="1.0"?> + <!DOCTYPE elem [%s]> + + <elem id="entity">&%s;</elem>""" % (dtd_refs, entity_id) + + with self.marionette.using_context('chrome'): + value = self.marionette.execute_script(""" + var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"] + .createInstance(Components.interfaces.nsIDOMParser); + var doc = parser.parseFromString(arguments[0], "text/xml"); + var node = doc.querySelector("elem[id='entity']"); + + return node ? node.textContent : null; + """, script_args=[contents]) + + if not value: + raise NoSuchElementException('DTD Entity not found: %s' % entity_id) + + return value + + def localize_property(self, property_urls, property_id): + """Returns the localized string for the specified property id. + + To find the property all given property files will be searched for + the id. + + :param property_urls: A list of property files to search. + :param property_id: The id to retrieve the value from. + + :returns: The localized string for the requested entity. + + :raises NoSuchElementException: When property id is not found in + property_urls. + """ + try: + return self._l10nMarionette.localize_property(property_urls, property_id) + except UnknownCommandException: + with self.marionette.using_context('chrome'): + value = self.marionette.execute_script(""" + let property = null; + let property_id = arguments[1]; + + arguments[0].some(aUrl => { + let bundle = Services.strings.createBundle(aUrl); + + try { + property = bundle.GetStringFromName(property_id); + return true; + } + catch (ex) { } + }); + + return property; + """, script_args=[property_urls, property_id]) + + if not value: + raise NoSuchElementException('Property not found: %s' % property_id) + + return value diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/places.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/places.py new file mode 100644 index 000000000..fadc2c19b --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/places.py @@ -0,0 +1,150 @@ +# 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/. + +from collections import namedtuple +from time import sleep + +from marionette_driver.errors import MarionetteException + +from firefox_puppeteer.base import BaseLib + + +class Places(BaseLib): + """Low-level access to several bookmark and history related actions.""" + + BookmarkFolders = namedtuple('bookmark_folders', + ['root', 'menu', 'toolbar', 'unfiled']) + bookmark_folders = BookmarkFolders('root________', 'menu________', + 'toolbar_____', 'unfiled_____') + + # Bookmark related helpers # + + def is_bookmarked(self, url): + """Check if the given URL is bookmarked. + + :param url: The URL to Check + + :returns: True, if the URL is a bookmark + """ + return self.marionette.execute_async_script(""" + Components.utils.import("resource://gre/modules/PlacesUtils.jsm"); + + PlacesUtils.bookmarks.fetch({url: arguments[0]}).then(bm => { + marionetteScriptFinished(bm != null); + }); + """, script_args=[url]) + + def get_folder_ids_for_url(self, url): + """Retrieve the folder ids where the given URL has been bookmarked in. + + :param url: URL of the bookmark + + :returns: List of folder ids + """ + return self.marionette.execute_async_script(""" + Components.utils.import("resource://gre/modules/PlacesUtils.jsm"); + + let folderGuids = [] + + function onResult(bm) { + folderGuids.push(bm.parentGuid); + } + + PlacesUtils.bookmarks.fetch({url: arguments[0]}, onResult).then(() => { + marionetteScriptFinished(folderGuids); + }); + """, script_args=[url]) + + def is_bookmark_star_button_ready(self): + """Check if the status of the star-button is not updating. + + :returns: True, if the button is ready + """ + return self.marionette.execute_script(""" + let button = window.BookmarkingUI; + + return button.status !== button.STATUS_UPDATING; + """) + + def restore_default_bookmarks(self): + """Restore the default bookmarks for the current profile.""" + retval = self.marionette.execute_async_script(""" + Components.utils.import("resource://gre/modules/BookmarkHTMLUtils.jsm"); + + // Default bookmarks.html file is stored inside omni.jar, + // so get it via a resource URI + let defaultBookmarks = 'chrome://browser/locale/bookmarks.html'; + + // Trigger the import of the default bookmarks + BookmarkHTMLUtils.importFromURL(defaultBookmarks, true) + .then(() => marionetteScriptFinished(true)) + .catch(() => marionetteScriptFinished(false)); + """, script_timeout=10000) + + if not retval: + raise MarionetteException("Restore Default Bookmarks failed") + + # Browser history related helpers # + + def get_all_urls_in_history(self): + """Retrieve any URLs which have been stored in the history.""" + return self.marionette.execute_script(""" + Components.utils.import("resource://gre/modules/PlacesUtils.jsm"); + + let options = PlacesUtils.history.getNewQueryOptions(); + let root = PlacesUtils.history.executeQuery( + PlacesUtils.history.getNewQuery(), options).root; + let urls = []; + + root.containerOpen = true; + for (let i = 0; i < root.childCount; i++) { + urls.push(root.getChild(i).uri) + } + root.containerOpen = false; + + return urls; + """) + + def remove_all_history(self): + """Remove all history items.""" + retval = self.marionette.execute_async_script(""" + Components.utils.import("resource://gre/modules/PlacesUtils.jsm"); + + PlacesUtils.history.clear() + .then(() => marionetteScriptFinished(true)) + .catch(() => marionetteScriptFinished(false)); + """, script_timeout=10000) + + if not retval: + raise MarionetteException("Removing all history failed") + + def wait_for_visited(self, urls, callback): + """Wait until all passed-in urls have been visited. + + :param urls: List of URLs which need to be visited and indexed + + :param callback: Method to execute which triggers loading of the URLs + """ + # Bug 1121691: Needs observer handling support with callback first + # Until then we have to wait about 4s to ensure the page has been indexed + callback() + sleep(4) + + # Plugin related helpers # + + def clear_plugin_data(self): + """Clear any kind of locally stored data from plugins.""" + self.marionette.execute_script(""" + let host = Components.classes["@mozilla.org/plugin/host;1"] + .getService(Components.interfaces.nsIPluginHost); + let tags = host.getPluginTags(); + + tags.forEach(aTag => { + try { + host.clearSiteData(aTag, null, Components.interfaces.nsIPluginHost + .FLAG_CLEAR_ALL, -1); + } catch (ex) { + } + }); + """) diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/security.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/security.py new file mode 100644 index 000000000..7f6532a08 --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/security.py @@ -0,0 +1,68 @@ +# 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/. + +import re + +from firefox_puppeteer.base import BaseLib +from firefox_puppeteer.errors import NoCertificateError + + +class Security(BaseLib): + """Low-level access to security (SSL, TLS) related information.""" + + # Security related helpers # + + def get_address_from_certificate(self, certificate): + """Retrieves the address of the organization from the certificate information. + + The returned address may be `None` in case of no address found, or a dictionary + with the following entries: `city`, `country`, `postal_code`, `state`, `street`. + + :param certificate: A JSON object representing the certificate, which can usually be + retrieved via the current tab: `self.browser.tabbar.selected_tab.certificate`. + + :returns: Address details as dictionary + """ + regex = re.compile('.*?L=(?P<city>.+?),ST=(?P<state>.+?),C=(?P<country>.+?)' + ',postalCode=(?P<postal_code>.+?),STREET=(?P<street>.+?)' + ',serial') + results = regex.search(certificate['subjectName']) + + return results.groupdict() if results else results + + def get_certificate_for_page(self, tab_element): + """The SSL certificate assiciated with the loaded web page in the given tab. + + :param tab_element: The inner tab DOM element. + + :returns: Certificate details as JSON object. + """ + cert = self.marionette.execute_script(""" + var securityUI = arguments[0].linkedBrowser.securityUI; + var status = securityUI.QueryInterface(Components.interfaces.nsISSLStatusProvider) + .SSLStatus; + + return status ? status.serverCert : null; + """, script_args=[tab_element]) + + uri = self.marionette.execute_script(""" + return arguments[0].linkedBrowser.currentURI.spec; + """, script_args=[tab_element]) + + if cert is None: + raise NoCertificateError('No certificate found for "{}"'.format(uri)) + + return cert + + def get_domain_from_common_name(self, common_name): + """Retrieves the domain associated with a page's security certificate from the common name. + + :param certificate: A string containing the certificate's common name, which can usually + be retrieved like so: `certificate['commonName']`. + + :returns: Domain as string + """ + return self.marionette.execute_script(""" + return Services.eTLD.getBaseDomainFromHost(arguments[0]); + """, script_args=[common_name]) diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/software_update.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/software_update.py new file mode 100644 index 000000000..0b90af0d8 --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/software_update.py @@ -0,0 +1,411 @@ +# 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/. + +import ConfigParser +import os +import re + +import mozinfo + +from firefox_puppeteer.base import BaseLib +from firefox_puppeteer.api.appinfo import AppInfo + + +class ActiveUpdate(BaseLib): + + def __getattr__(self, attr): + value = self.marionette.execute_script(""" + let ums = Components.classes['@mozilla.org/updates/update-manager;1'] + .getService(Components.interfaces.nsIUpdateManager); + return ums.activeUpdate[arguments[0]]; + """, script_args=[attr]) + + if value: + return value + else: + raise AttributeError('{} has no attribute {}'.format(self.__class__.__name__, + attr)) + + @property + def exists(self): + """Checks if there is an active update. + + :returns: True if there is an active update + """ + active_update = self.marionette.execute_script(""" + let ums = Components.classes['@mozilla.org/updates/update-manager;1'] + .getService(Components.interfaces.nsIUpdateManager); + return ums.activeUpdate; + """) + + return bool(active_update) + + def get_patch_at(self, patch_index): + """Use nsIUpdate.getPatchAt to return a patch from an update. + + :returns: JSON data for an nsIUpdatePatch object + """ + return self.marionette.execute_script(""" + let ums = Components.classes['@mozilla.org/updates/update-manager;1'] + .getService(Components.interfaces.nsIUpdateManager); + return ums.activeUpdate.getPatchAt(arguments[0]); + """, script_args=[patch_index]) + + @property + def patch_count(self): + """Get the patchCount from the active update. + + :returns: The patch count + """ + return self.marionette.execute_script(""" + let ums = Components.classes['@mozilla.org/updates/update-manager;1'] + .getService(Components.interfaces.nsIUpdateManager); + return ums.activeUpdate.patchCount; + """) + + @property + def selected_patch(self): + """Get the selected patch for the active update. + + :returns: JSON data for the selected patch + """ + return self.marionette.execute_script(""" + let ums = Components.classes['@mozilla.org/updates/update-manager;1'] + .getService(Components.interfaces.nsIUpdateManager); + return ums.activeUpdate.selectedPatch; + """) + + +class MARChannels(BaseLib): + """Class to handle the allowed MAR channels as listed in update-settings.ini.""" + INI_SECTION = 'Settings' + INI_OPTION = 'ACCEPTED_MAR_CHANNEL_IDS' + + class MARConfigParser(ConfigParser.ConfigParser): + """INI parser which allows to read and write MAR config files. + + Virtually identical to the original method, but delimit keys and values + with '=' instead of ' = ' + """ + + def write(self, fp): + """Write an .ini-format representation of the configuration state.""" + if self._defaults: + fp.write("[%s]\n" % ConfigParser.DEFAULTSECT) + for (key, value) in self._defaults.items(): + fp.write("%s=%s\n" % (key, str(value).replace('\n', '\n\t'))) + fp.write("\n") + for section in self._sections: + fp.write("[%s]\n" % section) + for (key, value) in self._sections[section].items(): + if key == "__name__": + continue + if (value is not None) or (self._optcre == self.OPTCRE): + key = "=".join((key, str(value).replace('\n', '\n\t'))) + fp.write("%s\n" % (key)) + fp.write("\n") + + def __init__(self, marionette): + BaseLib.__init__(self, marionette) + + self.config_file_path = self.marionette.execute_script(""" + Components.utils.import('resource://gre/modules/Services.jsm'); + + let file = Services.dirsvc.get('GreD', Components.interfaces.nsIFile); + file.append('update-settings.ini'); + + return file.path; + """) + + self.config = self.MARConfigParser() + self.config.optionxform = str + + @property + def channels(self): + """The currently accepted MAR channels. + + :returns: A set of channel names + """ + # Make sure to always read the current file contents + self.config.read(self.config_file_path) + + return set(self.config.get(self.INI_SECTION, self.INI_OPTION).split(',')) + + @channels.setter + def channels(self, channels): + """Set the accepted MAR channels. + + :param channels: A set of channel names + """ + self.config.set(self.INI_SECTION, self.INI_OPTION, ','.join(channels)) + with open(self.config_file_path, 'wb') as configfile: + self.config.write(configfile) + + def add_channels(self, channels): + """Add additional MAR channels. + + :param channels: A set of channel names to add + """ + self.channels = self.channels | set(channels) + + def remove_channels(self, channels): + """Remove MAR channels. + + :param channels: A set of channel names to remove + """ + self.channels = self.channels - set(channels) + + +class SoftwareUpdate(BaseLib): + """The SoftwareUpdate API adds support for an easy access to the update process.""" + PREF_APP_DISTRIBUTION = 'distribution.id' + PREF_APP_DISTRIBUTION_VERSION = 'distribution.version' + PREF_APP_UPDATE_CHANNEL = 'app.update.channel' + PREF_APP_UPDATE_URL = 'app.update.url' + PREF_APP_UPDATE_URL_OVERRIDE = 'app.update.url.override' + PREF_DISABLED_ADDONS = 'extensions.disabledAddons' + + def __init__(self, marionette): + BaseLib.__init__(self, marionette) + + self.app_info = AppInfo(marionette) + + self._mar_channels = MARChannels(marionette) + self._active_update = ActiveUpdate(marionette) + + @property + def ABI(self): + """Get the customized ABI for the update service. + + :returns: ABI version + """ + abi = self.app_info.XPCOMABI + if mozinfo.isMac: + abi += self.marionette.execute_script(""" + let macutils = Components.classes['@mozilla.org/xpcom/mac-utils;1'] + .getService(Components.interfaces.nsIMacUtils); + if (macutils.isUniversalBinary) { + return '-u-' + macutils.architecturesInBinary; + } + return ''; + """) + + return abi + + @property + def active_update(self): + """ Holds a reference to an :class:`ActiveUpdate` object.""" + return self._active_update + + @property + def allowed(self): + """Check if the user has permissions to run the software update + + :returns: Status if the user has the permissions + """ + return self.marionette.execute_script(""" + let aus = Components.classes['@mozilla.org/updates/update-service;1'] + .getService(Components.interfaces.nsIApplicationUpdateService); + return aus.canCheckForUpdates && aus.canApplyUpdates; + """) + + @property + def build_info(self): + """Return information of the current build version + + :returns: A dictionary of build information + """ + update_url = self.get_update_url(True) + + return { + 'buildid': self.app_info.appBuildID, + 'channel': self.update_channel, + 'disabled_addons': self.marionette.get_pref(self.PREF_DISABLED_ADDONS), + 'locale': self.app_info.locale, + 'mar_channels': self.mar_channels.channels, + 'update_url': update_url, + 'update_snippet': self.get_update_snippet(update_url), + 'user_agent': self.app_info.user_agent, + 'version': self.app_info.version + } + + @property + def is_complete_update(self): + """Return true if the offered update is a complete update + + :returns: True if the offered update is a complete update + """ + # Throw when isCompleteUpdate is called without an update. This should + # never happen except if the test is incorrectly written. + assert self.active_update.exists, 'An active update has been found' + + patch_count = self.active_update.patch_count + assert patch_count == 1 or patch_count == 2,\ + 'An update must have one or two patches included' + + # Ensure Partial and Complete patches produced have unique urls + if patch_count == 2: + patch0_url = self.active_update.get_patch_at(0)['URL'] + patch1_url = self.active_update.get_patch_at(1)['URL'] + assert patch0_url != patch1_url,\ + 'Partial and Complete download URLs are different' + + return self.active_update.selected_patch['type'] == 'complete' + + @property + def mar_channels(self): + """ Holds a reference to a :class:`MARChannels` object.""" + return self._mar_channels + + @property + def os_version(self): + """Returns information about the OS version + + :returns: The OS version + """ + return self.marionette.execute_script(""" + Components.utils.import("resource://gre/modules/Services.jsm"); + + let osVersion; + try { + osVersion = Services.sysinfo.getProperty("name") + " " + + Services.sysinfo.getProperty("version"); + } + catch (ex) { + } + + if (osVersion) { + try { + osVersion += " (" + Services.sysinfo.getProperty("secondaryLibrary") + ")"; + } + catch (e) { + // Not all platforms have a secondary widget library, + // so an error is nothing to worry about. + } + osVersion = encodeURIComponent(osVersion); + } + return osVersion; + """) + + @property + def patch_info(self): + """ Returns information of the active update in the queue.""" + info = {'channel': self.update_channel} + + if (self.active_update.exists): + info['buildid'] = self.active_update.buildID + info['is_complete'] = self.is_complete_update + info['size'] = self.active_update.selected_patch['size'] + info['type'] = self.update_type + info['url_mirror'] = \ + self.active_update.selected_patch['finalURL'] or 'n/a' + info['version'] = self.active_update.appVersion + + return info + + @property + def staging_directory(self): + """ Returns the path to the updates staging directory.""" + return self.marionette.execute_script(""" + let aus = Components.classes['@mozilla.org/updates/update-service;1'] + .getService(Components.interfaces.nsIApplicationUpdateService); + return aus.getUpdatesDirectory().path; + """) + + @property + def update_channel(self): + """Return the currently used update channel.""" + return self.marionette.get_pref(self.PREF_APP_UPDATE_CHANNEL, + default_branch=True) + + @update_channel.setter + def update_channel(self, channel): + """Set the update channel to be used for update checks. + + :param channel: New update channel to use + + """ + writer = UpdateChannelWriter(self.marionette) + writer.set_channel(channel) + + @property + def update_type(self): + """Returns the type of the active update.""" + return self.active_update.type + + def force_fallback(self): + """Update the update.status file and set the status to 'failed:6'""" + with open(os.path.join(self.staging_directory, 'update.status'), 'w') as f: + f.write('failed: 6\n') + + def get_update_snippet(self, update_url): + """Retrieve contents of the update snippet. + + :param update_url: URL to the update snippet + """ + snippet = None + try: + import urllib2 + response = urllib2.urlopen(update_url) + snippet = response.read() + except Exception: + pass + + return snippet + + def get_update_url(self, force=False): + """Retrieve the AUS update URL the update snippet is retrieved from. + + :param force: Boolean flag to force an update check + + :returns: The URL of the update snippet + """ + url = self.marionette.get_pref(self.PREF_APP_UPDATE_URL_OVERRIDE) + if not url: + url = self.marionette.get_pref(self.PREF_APP_UPDATE_URL) + + # Format the URL by replacing placeholders + url = self.marionette.execute_script(""" + Components.utils.import("resource://gre/modules/UpdateUtils.jsm") + return UpdateUtils.formatUpdateURL(arguments[0]); + """, script_args=[url]) + + if force: + if '?' in url: + url += '&' + else: + url += '?' + url += 'force=1' + + return url + + +class UpdateChannelWriter(BaseLib): + """Class to handle the update channel as listed in channel-prefs.js""" + REGEX_UPDATE_CHANNEL = re.compile(r'("app\.update\.channel", ")([^"].*)(?=")') + + def __init__(self, *args, **kwargs): + BaseLib.__init__(self, *args, **kwargs) + + self.file_path = self.marionette.execute_script(""" + Components.utils.import('resource://gre/modules/Services.jsm'); + + let file = Services.dirsvc.get('PrfDef', Components.interfaces.nsIFile); + file.append('channel-prefs.js'); + + return file.path; + """) + + def set_channel(self, channel): + """Set default update channel. + + :param channel: New default update channel + """ + with open(self.file_path) as f: + file_contents = f.read() + + new_content = re.sub( + self.REGEX_UPDATE_CHANNEL, r'\g<1>' + channel, file_contents) + with open(self.file_path, 'w') as f: + f.write(new_content) diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/utils.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/utils.py new file mode 100644 index 000000000..2b4ef0766 --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/api/utils.py @@ -0,0 +1,140 @@ +# 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/. + +from marionette_driver.errors import MarionetteException + +from firefox_puppeteer.base import BaseLib + + +class Utils(BaseLib): + """Low-level access to utility actions.""" + + def __init__(self, *args, **kwargs): + super(Utils, self).__init__(*args, **kwargs) + + self._permissions = Permissions(self.marionette) + + @property + def permissions(self): + """Handling of various permissions for hosts. + + :returns: Instance of the Permissions class. + """ + return self._permissions + + def compare_version(self, a, b): + """Compare two version strings. + + :param a: The first version. + :param b: The second version. + + :returns: -1 if a is smaller than b, 0 if equal, and 1 if larger. + """ + return self.marionette.execute_script(""" + Components.utils.import("resource://gre/modules/Services.jsm"); + return Services.vc.compare(arguments[0], arguments[1]); + """, script_args=[a, b]) + + def sanitize(self, data_type): + """Sanitize user data, including cache, cookies, offlineApps, history, formdata, + downloads, passwords, sessions, siteSettings. + + Usage: + sanitize(): Clears all user data. + sanitize({ "sessions": True }): Clears only session user data. + + more: https://dxr.mozilla.org/mozilla-central/source/browser/base/content/sanitize.js + + :param data_type: optional, Information specifying data to be sanitized + """ + + with self.marionette.using_context('chrome'): + result = self.marionette.execute_async_script(""" + Components.utils.import("resource://gre/modules/Services.jsm"); + + var data_type = arguments[0]; + + var data_type = (typeof data_type === "undefined") ? {} : { + cache: data_type.cache || false, + cookies: data_type.cookies || false, + downloads: data_type.downloads || false, + formdata: data_type.formdata || false, + history: data_type.history || false, + offlineApps: data_type.offlineApps || false, + passwords: data_type.passwords || false, + sessions: data_type.sessions || false, + siteSettings: data_type.siteSettings || false + }; + + // Load the sanitize script + var tempScope = {}; + Components.classes["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Components.interfaces.mozIJSSubScriptLoader) + .loadSubScript("chrome://browser/content/sanitize.js", tempScope); + + // Instantiate the Sanitizer + var s = new tempScope.Sanitizer(); + s.prefDomain = "privacy.cpd."; + var itemPrefs = Services.prefs.getBranch(s.prefDomain); + + // Apply options for what to sanitize + for (var pref in data_type) { + itemPrefs.setBoolPref(pref, data_type[pref]); + }; + + // Sanitize and wait for the promise to resolve + var finished = false; + s.sanitize().then(() => { + for (let pref in data_type) { + itemPrefs.clearUserPref(pref); + }; + marionetteScriptFinished(true); + }, aError => { + for (let pref in data_type) { + itemPrefs.clearUserPref(pref); + }; + marionetteScriptFinished(false); + }); + """, script_args=[data_type]) + + if not result: + raise MarionetteException('Sanitizing of profile data failed.') + + +class Permissions(BaseLib): + + def add(self, host, permission): + """Add a permission for web host. + + Permissions include safe-browsing, install, geolocation, and others described here: + https://dxr.mozilla.org/mozilla-central/source/browser/modules/SitePermissions.jsm#144 + and elsewhere. + + :param host: The web host whose permission will be added. + :param permission: The type of permission to be added. + """ + with self.marionette.using_context('chrome'): + self.marionette.execute_script(""" + Components.utils.import("resource://gre/modules/Services.jsm"); + let uri = Services.io.newURI(arguments[0], null, null); + Services.perms.add(uri, arguments[1], + Components.interfaces.nsIPermissionManager.ALLOW_ACTION); + """, script_args=[host, permission]) + + def remove(self, host, permission): + """Remove a permission for web host. + + Permissions include safe-browsing, install, geolocation, and others described here: + https://dxr.mozilla.org/mozilla-central/source/browser/modules/SitePermissions.jsm#144 + and elsewhere. + + :param host: The web host whose permission will be removed. + :param permission: The type of permission to be removed. + """ + with self.marionette.using_context('chrome'): + self.marionette.execute_script(""" + Components.utils.import("resource://gre/modules/Services.jsm"); + let uri = Services.io.newURI(arguments[0], null, null); + Services.perms.remove(uri, arguments[1]); + """, script_args=[host, permission]) |