1# Copyright 2013 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.
4import json
5import logging
6import os
7import platform
8import shutil
9import socket
10import sys
11import tempfile
12import time
13import urllib2
14import zipfile
15
16from telemetry.core import util
17from telemetry.page import page_set
18from telemetry.page import profile_creator
19
20
21def _ExternalExtensionsPath():
22  """Returns the OS-dependent path at which to install the extension deployment
23   files"""
24  if platform.system() == 'Darwin':
25    return os.path.join('/Library', 'Application Support', 'Google', 'Chrome',
26        'External Extensions')
27  elif platform.system() == 'Linux':
28    return os.path.join('/opt', 'google', 'chrome', 'extensions' )
29  else:
30    raise NotImplementedError('Extension install on %s is not yet supported' %
31        platform.system())
32
33def _DownloadExtension(extension_id, output_dir):
34  """Download an extension to disk.
35
36  Args:
37    extension_id: the extension id.
38    output_dir: Directory to download into.
39
40  Returns:
41    Extension file downloaded."""
42  extension_download_path = os.path.join(output_dir, "%s.crx" % extension_id)
43  extension_url = (
44      "https://clients2.google.com/service/update2/crx?response=redirect"
45      "&x=id%%3D%s%%26lang%%3Den-US%%26uc" % extension_id)
46  response = urllib2.urlopen(extension_url)
47  assert(response.getcode() == 200)
48
49  with open(extension_download_path, "w") as f:
50    f.write(response.read())
51
52  return extension_download_path
53
54def _GetExtensionInfoFromCRX(crx_path):
55  """Parse an extension archive and return information.
56
57  Note:
58    The extension name returned by this function may not be valid
59  (e.g. in the case of a localized extension name).  It's use is just
60  meant to be informational.
61
62  Args:
63    crx_path: path to crx archive to look at.
64
65  Returns:
66    Tuple consisting of:
67    (crx_version, extension_name)"""
68  crx_zip = zipfile.ZipFile(crx_path)
69  manifest_contents = crx_zip.read('manifest.json')
70  decoded_manifest = json.loads(manifest_contents)
71  crx_version = decoded_manifest['version']
72  extension_name = decoded_manifest['name']
73
74  return (crx_version, extension_name)
75
76class ExtensionsProfileCreator(profile_creator.ProfileCreator):
77  """Virtual base class for profile creators that install extensions.
78
79  Extensions are installed using the mechanism described in
80  https://developer.chrome.com/extensions/external_extensions.html .
81
82  Subclasses are meant to be run interactively.
83  """
84
85  def __init__(self):
86    super(ExtensionsProfileCreator, self).__init__()
87    typical_25 = os.path.join(util.GetBaseDir(), 'page_sets', 'typical_25.json')
88    self._page_set = page_set.PageSet.FromFile(typical_25)
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):
158    """Run before browser starts.
159
160    Download extensions and write installation files."""
161
162    # Running this script on a corporate network or other managed environment
163    # could potentially alter the profile contents.
164    hostname = socket.gethostname()
165    if hostname.endswith('corp.google.com'):
166      raise Exception("It appears you are connected to a corporate network "
167          "(hostname=%s).  This script needs to be run off the corp "
168          "network." % hostname)
169
170    prompt = ("\n!!!This script must be run on a fresh OS installation, "
171        "disconnected from any corporate network. Are you sure you want to "
172        "continue? (y/N) ")
173    if (raw_input(prompt).lower() != 'y'):
174      sys.exit(-1)
175    self._PrepareExtensionInstallFiles()
176
177  def DidRunTest(self, browser, results):
178    """Run before exit."""
179    # Do some basic sanity checks to make sure the profile is complete.
180    installed_extensions = browser.extensions.keys()
181    if not len(installed_extensions) == len(self._extensions_to_install):
182      # Diagnosing errors:
183      # Too many extensions: Managed environment may be installing additional
184      # extensions.
185      raise Exception("Unexpected number of extensions installed in browser",
186          installed_extensions)
187
188    # Check that files on this list exist and have content.
189    expected_files = [
190        os.path.join('Default', 'Network Action Predictor')]
191    for filename in expected_files:
192      filename = os.path.join(self._output_profile_path, filename)
193      if not os.path.getsize(filename) > 0:
194        raise Exception("Profile not complete: %s is zero length." % filename)
195
196    self._CleanupExtensionInstallFiles()
197
198  def CanRunForPage(self, page):
199    # No matter how many pages in the pageset, just perform two test iterations.
200    return page.page_set.pages.index(page) < 2
201
202  def MeasurePage(self, _, tab, results):
203    # Profile setup works in 2 phases:
204    # Phase 1: When the first page is loaded: we wait for a timeout to allow
205    #     all extensions to install and to prime safe browsing and other
206    #     caches.  Extensions may open tabs as part of the install process.
207    # Phase 2: When the second page loads, page_runner closes all tabs -
208    #     we are left with one open tab, wait for that to finish loading.
209
210    # Sleep for a bit to allow safe browsing and other data to load +
211    # extensions to install.
212    if not self._extensions_installed:
213      sleep_seconds = 5 * 60
214      logging.info("Sleeping for %d seconds." % sleep_seconds)
215      time.sleep(sleep_seconds)
216      self._extensions_installed = True
217    else:
218      # Phase 2: Wait for tab to finish loading.
219      for i in xrange(len(tab.browser.tabs)):
220        t = tab.browser.tabs[i]
221        t.WaitForDocumentReadyStateToBeComplete()
222