1# Copyright 2014 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import json
6import logging
7import os
8import platform
9import shutil
10import socket
11import sys
12import tempfile
13import time
14import urllib2
15import zipfile
16
17from telemetry.page import profile_creator
18
19import page_sets
20
21
22def _ExternalExtensionsPath():
23  """Returns the OS-dependent path at which to install the extension deployment
24   files"""
25  if platform.system() == 'Darwin':
26    return os.path.join('/Library', 'Application Support', 'Google', 'Chrome',
27        'External Extensions')
28  elif platform.system() == 'Linux':
29    return os.path.join('/opt', 'google', 'chrome', 'extensions' )
30  else:
31    raise NotImplementedError('Extension install on %s is not yet supported' %
32        platform.system())
33
34def _DownloadExtension(extension_id, output_dir):
35  """Download an extension to disk.
36
37  Args:
38    extension_id: the extension id.
39    output_dir: Directory to download into.
40
41  Returns:
42    Extension file downloaded."""
43  extension_download_path = os.path.join(output_dir, "%s.crx" % extension_id)
44  extension_url = (
45      "https://clients2.google.com/service/update2/crx?response=redirect"
46      "&x=id%%3D%s%%26lang%%3Den-US%%26uc" % extension_id)
47  response = urllib2.urlopen(extension_url)
48  assert(response.getcode() == 200)
49
50  with open(extension_download_path, "w") as f:
51    f.write(response.read())
52
53  return extension_download_path
54
55def _GetExtensionInfoFromCRX(crx_path):
56  """Parse an extension archive and return information.
57
58  Note:
59    The extension name returned by this function may not be valid
60  (e.g. in the case of a localized extension name).  It's use is just
61  meant to be informational.
62
63  Args:
64    crx_path: path to crx archive to look at.
65
66  Returns:
67    Tuple consisting of:
68    (crx_version, extension_name)"""
69  crx_zip = zipfile.ZipFile(crx_path)
70  manifest_contents = crx_zip.read('manifest.json')
71  decoded_manifest = json.loads(manifest_contents)
72  crx_version = decoded_manifest['version']
73  extension_name = decoded_manifest['name']
74
75  return (crx_version, extension_name)
76
77class ExtensionsProfileCreator(profile_creator.ProfileCreator):
78  """Virtual base class for profile creators that install extensions.
79
80  Extensions are installed using the mechanism described in
81  https://developer.chrome.com/extensions/external_extensions.html .
82
83  Subclasses are meant to be run interactively.
84  """
85
86  def __init__(self):
87    super(ExtensionsProfileCreator, self).__init__()
88    self._page_set = page_sets.Typical25()
89
90    # Directory into which the output profile is written.
91    self._output_profile_path = None
92
93    # List of extensions to install.
94    self._extensions_to_install = []
95
96    # Theme to install (if any).
97    self._theme_to_install = None
98
99    # Directory to download extension files into.
100    self._extension_download_dir = None
101
102    # Have the extensions been installed yet?
103    self._extensions_installed = False
104
105    # List of files to delete after run.
106    self._files_to_cleanup = []
107
108  def _PrepareExtensionInstallFiles(self):
109    """Download extension archives and create extension install files."""
110    extensions_to_install = self._extensions_to_install
111    if self._theme_to_install:
112      extensions_to_install = extensions_to_install + [self._theme_to_install]
113    num_extensions = len(extensions_to_install)
114    if not num_extensions:
115      raise ValueError("No extensions or themes to install:",
116          extensions_to_install)
117
118    # Create external extensions path if it doesn't exist already.
119    external_extensions_dir = _ExternalExtensionsPath()
120    if not os.path.isdir(external_extensions_dir):
121      os.makedirs(external_extensions_dir)
122
123    self._extension_download_dir = tempfile.mkdtemp()
124
125    for i in xrange(num_extensions):
126      extension_id = extensions_to_install[i]
127      logging.info("Downloading %s - %d/%d" % (
128          extension_id, (i + 1), num_extensions))
129      extension_path = _DownloadExtension(extension_id,
130          self._extension_download_dir)
131      (version, name) = _GetExtensionInfoFromCRX(extension_path)
132      extension_info = {'external_crx' : extension_path,
133          'external_version' : version,
134          '_comment' : name}
135      extension_json_path = os.path.join(external_extensions_dir,
136          "%s.json" % extension_id)
137      with open(extension_json_path, 'w') as f:
138        f.write(json.dumps(extension_info))
139        self._files_to_cleanup.append(extension_json_path)
140
141  def _CleanupExtensionInstallFiles(self):
142    """Cleanup stray files before exiting."""
143    logging.info("Cleaning up stray files")
144    for filename in self._files_to_cleanup:
145      os.remove(filename)
146
147    if self._extension_download_dir:
148      # Simple sanity check to lessen the impact of a stray rmtree().
149      if len(self._extension_download_dir.split(os.sep)) < 3:
150        raise Exception("Path too shallow: %s" % self._extension_download_dir)
151      shutil.rmtree(self._extension_download_dir)
152      self._extension_download_dir = None
153
154  def CustomizeBrowserOptions(self, options):
155    self._output_profile_path = options.output_profile_path
156
157  def WillRunTest(self, options):
158    """Run before browser starts.
159
160    Download extensions and write installation files."""
161    super(ExtensionsProfileCreator, self).WillRunTest(options)
162
163    # Running this script on a corporate network or other managed environment
164    # could potentially alter the profile contents.
165    hostname = socket.gethostname()
166    if hostname.endswith('corp.google.com'):
167      raise Exception("It appears you are connected to a corporate network "
168          "(hostname=%s).  This script needs to be run off the corp "
169          "network." % hostname)
170
171    prompt = ("\n!!!This script must be run on a fresh OS installation, "
172        "disconnected from any corporate network. Are you sure you want to "
173        "continue? (y/N) ")
174    if (raw_input(prompt).lower() != 'y'):
175      sys.exit(-1)
176    self._PrepareExtensionInstallFiles()
177
178  def DidRunTest(self, browser, results):
179    """Run before exit."""
180    super(ExtensionsProfileCreator, self).DidRunTest()
181    # Do some basic sanity checks to make sure the profile is complete.
182    installed_extensions = browser.extensions.keys()
183    if not len(installed_extensions) == len(self._extensions_to_install):
184      # Diagnosing errors:
185      # Too many extensions: Managed environment may be installing additional
186      # extensions.
187      raise Exception("Unexpected number of extensions installed in browser",
188          installed_extensions)
189
190    # Check that files on this list exist and have content.
191    expected_files = [
192        os.path.join('Default', 'Network Action Predictor')]
193    for filename in expected_files:
194      filename = os.path.join(self._output_profile_path, filename)
195      if not os.path.getsize(filename) > 0:
196        raise Exception("Profile not complete: %s is zero length." % filename)
197
198    self._CleanupExtensionInstallFiles()
199
200  def CanRunForPage(self, page):
201    # No matter how many pages in the pageset, just perform two test iterations.
202    return page.page_set.pages.index(page) < 2
203
204  def ValidateAndMeasurePage(self, _, tab, results):
205    # Profile setup works in 2 phases:
206    # Phase 1: When the first page is loaded: we wait for a timeout to allow
207    #     all extensions to install and to prime safe browsing and other
208    #     caches.  Extensions may open tabs as part of the install process.
209    # Phase 2: When the second page loads, page_runner closes all tabs -
210    #     we are left with one open tab, wait for that to finish loading.
211
212    # Sleep for a bit to allow safe browsing and other data to load +
213    # extensions to install.
214    if not self._extensions_installed:
215      sleep_seconds = 5 * 60
216      logging.info("Sleeping for %d seconds." % sleep_seconds)
217      time.sleep(sleep_seconds)
218      self._extensions_installed = True
219    else:
220      # Phase 2: Wait for tab to finish loading.
221      for i in xrange(len(tab.browser.tabs)):
222        t = tab.browser.tabs[i]
223        t.WaitForDocumentReadyStateToBeComplete()
224