1#!/usr/bin/env python
2#
3# Copyright (c) 2012 The Chromium Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""Extract UserMetrics "actions" strings from the Chrome source.
8
9This program generates the list of known actions we expect to see in the
10user behavior logs.  It walks the Chrome source, looking for calls to
11UserMetrics functions, extracting actions and warning on improper calls,
12as well as generating the lists of possible actions in situations where
13there are many possible actions.
14
15See also:
16  base/metrics/user_metrics.h
17
18After extracting all actions, the content will go through a pretty print
19function to make sure it's well formatted. If the file content needs to be
20changed, a window will be prompted asking for user's consent. The old version
21will also be saved in a backup file.
22"""
23
24from __future__ import print_function
25
26__author__ = 'evanm (Evan Martin)'
27
28from HTMLParser import HTMLParser
29import logging
30import os
31import re
32import shutil
33import sys
34from xml.dom import minidom
35
36import action_utils
37import actions_print_style
38
39# Import the metrics/common module for pretty print xml.
40sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'common'))
41import presubmit_util
42import diff_util
43import pretty_print_xml
44
45USER_METRICS_ACTION_RE = re.compile(r"""
46  [^a-zA-Z]                   # Preceded by a non-alphabetical character.
47  (?:                         # Begin non-capturing group.
48  UserMetricsAction           # C++ / Objective C function name.
49  |                           # or...
50  RecordUserAction\.record    # Java function name.
51  )                           # End non-capturing group.
52  \(                          # Opening parenthesis.
53  \s*                         # Any amount of whitespace, including new lines.
54  (.+?)                       # A sequence of characters for the param.
55  \)                          # Closing parenthesis.
56  """,
57  re.VERBOSE | re.DOTALL      # Verbose syntax and makes . also match new lines.
58)
59USER_METRICS_ACTION_RE_JS = re.compile(r"""
60  chrome\.send                # Start of function call.
61  \(                          # Opening parenthesis.
62  \s*                         # Any amount of whitespace, including new lines.
63  # WebUI message handled by CoreOptionsHandler.
64  'coreOptionsUserMetricsAction'
65  ,                           # Separator after first parameter.
66  \s*                         # Any amount of whitespace, including new lines.
67  \[                          # Opening bracket for arguments for C++ function.
68  \s*                         # Any amount of whitespace, including new lines.
69  (.+?)                       # A sequence of characters for the param.
70  \s*                         # Any amount of whitespace, including new lines.
71  \]                          # Closing bracket.
72  \s*                         # Any amount of whitespace, including new lines.
73  \)                          # Closing parenthesis.
74  """,
75  re.VERBOSE | re.DOTALL      # Verbose syntax and makes . also match new lines.
76)
77USER_METRICS_ACTION_RE_DEVTOOLS = re.compile(r"""
78  InspectorFrontendHost\.recordUserMetricsAction     # Start of function call.
79  \(                          # Opening parenthesis.
80  \s*                         # Any amount of whitespace, including new lines.
81  (.+?)                       # A sequence of characters for the param.
82  \s*                         # Any amount of whitespace, including new lines.
83  \)                          # Closing parenthesis.
84  """,
85  re.VERBOSE | re.DOTALL      # Verbose syntax and makes . also match new lines.
86)
87COMPUTED_ACTION_RE = re.compile(r'RecordComputedAction')
88QUOTED_STRING_RE = re.compile(r"""('[^']+'|"[^"]+")$""")
89
90# Files that are known to use content::RecordComputedAction(), which means
91# they require special handling code in this script.
92# To add a new file, add it to this list and add the appropriate logic to
93# generate the known actions to AddComputedActions() below.
94KNOWN_COMPUTED_USERS = (
95  'back_forward_menu_model.cc',
96  'options_page_view.cc',
97  'render_view_host.cc',  # called using webkit identifiers
98  'user_metrics.cc',  # method definition
99  'new_tab_ui.cc',  # most visited clicks 1-9
100  'extension_metrics_module.cc', # extensions hook for user metrics
101  'language_options_handler_common.cc', # languages and input methods in CrOS
102  'cros_language_options_handler.cc', # languages and input methods in CrOS
103  'external_metrics.cc',  # see AddChromeOSActions()
104  'core_options_handler.cc',  # see AddWebUIActions()
105  'browser_render_process_host.cc',  # see AddRendererActions()
106  'render_thread_impl.cc',  # impl of RenderThread::RecordComputedAction()
107  'render_process_host_impl.cc',  # browser side impl for
108                                  # RenderThread::RecordComputedAction()
109  'mock_render_thread.cc',  # mock of RenderThread::RecordComputedAction()
110  'ppb_pdf_impl.cc',  # see AddClosedSourceActions()
111  'pepper_pdf_host.cc',  # see AddClosedSourceActions()
112  'record_user_action.cc', # see RecordUserAction.java
113  'blink_platform_impl.cc', # see WebKit/public/platform/Platform.h
114  'devtools_ui_bindings.cc', # see AddDevToolsActions()
115)
116
117# Language codes used in Chrome. The list should be updated when a new
118# language is added to app/l10n_util.cc, as follows:
119#
120# % (cat app/l10n_util.cc | \
121#    perl -n0e 'print $1 if /kAcceptLanguageList.*?\{(.*?)\}/s' | \
122#    perl -nle 'print $1, if /"(.*)"/'; echo 'es-419') | \
123#   sort | perl -pe "s/(.*)\n/'\$1', /" | \
124#   fold -w75 -s | perl -pe 's/^/  /;s/ $//'; echo
125#
126# The script extracts language codes from kAcceptLanguageList, but es-419
127# (Spanish in Latin America) is an exception.
128LANGUAGE_CODES = (
129  'af', 'am', 'ar', 'az', 'be', 'bg', 'bh', 'bn', 'br', 'bs', 'ca', 'co',
130  'cs', 'cy', 'da', 'de', 'de-AT', 'de-CH', 'de-DE', 'el', 'en', 'en-AU',
131  'en-CA', 'en-GB', 'en-NZ', 'en-US', 'en-ZA', 'eo', 'es', 'es-419', 'et',
132  'eu', 'fa', 'fi', 'fil', 'fo', 'fr', 'fr-CA', 'fr-CH', 'fr-FR', 'fy',
133  'ga', 'gd', 'gl', 'gn', 'gu', 'ha', 'haw', 'he', 'hi', 'hr', 'hu', 'hy',
134  'ia', 'id', 'is', 'it', 'it-CH', 'it-IT', 'ja', 'jw', 'ka', 'kk', 'km',
135  'kn', 'ko', 'ku', 'ky', 'la', 'ln', 'lo', 'lt', 'lv', 'mk', 'ml', 'mn',
136  'mo', 'mr', 'ms', 'mt', 'nb', 'ne', 'nl', 'nn', 'no', 'oc', 'om', 'or',
137  'pa', 'pl', 'ps', 'pt', 'pt-BR', 'pt-PT', 'qu', 'rm', 'ro', 'ru', 'sd',
138  'sh', 'si', 'sk', 'sl', 'sn', 'so', 'sq', 'sr', 'st', 'su', 'sv', 'sw',
139  'ta', 'te', 'tg', 'th', 'ti', 'tk', 'to', 'tr', 'tt', 'tw', 'ug', 'uk',
140  'ur', 'uz', 'vi', 'xh', 'yi', 'yo', 'zh', 'zh-CN', 'zh-TW', 'zu',
141)
142
143# Input method IDs used in Chrome OS. The list should be updated when a
144# new input method is added to
145# chromeos/ime/input_methods.txt in the Chrome tree, as
146# follows:
147#
148# % sort chromeos/ime/input_methods.txt | \
149#   perl -ne "print \"'\$1', \" if /^([^#]+?)\s/" | \
150#   fold -w75 -s | perl -pe 's/^/  /;s/ $//'; echo
151#
152# The script extracts input method IDs from input_methods.txt.
153INPUT_METHOD_IDS = (
154  'xkb:am:phonetic:arm', 'xkb:be::fra', 'xkb:be::ger', 'xkb:be::nld',
155  'xkb:bg::bul', 'xkb:bg:phonetic:bul', 'xkb:br::por', 'xkb:by::bel',
156  'xkb:ca::fra', 'xkb:ca:eng:eng', 'xkb:ca:multix:fra', 'xkb:ch::ger',
157  'xkb:ch:fr:fra', 'xkb:cz::cze', 'xkb:cz:qwerty:cze', 'xkb:de::ger',
158  'xkb:de:neo:ger', 'xkb:dk::dan', 'xkb:ee::est', 'xkb:es::spa',
159  'xkb:es:cat:cat', 'xkb:fi::fin', 'xkb:fr::fra', 'xkb:gb:dvorak:eng',
160  'xkb:gb:extd:eng', 'xkb:ge::geo', 'xkb:gr::gre', 'xkb:hr::scr',
161  'xkb:hu::hun', 'xkb:il::heb', 'xkb:is::ice', 'xkb:it::ita', 'xkb:jp::jpn',
162  'xkb:latam::spa', 'xkb:lt::lit', 'xkb:lv:apostrophe:lav', 'xkb:mn::mon',
163  'xkb:no::nob', 'xkb:pl::pol', 'xkb:pt::por', 'xkb:ro::rum', 'xkb:rs::srp',
164  'xkb:ru::rus', 'xkb:ru:phonetic:rus', 'xkb:se::swe', 'xkb:si::slv',
165  'xkb:sk::slo', 'xkb:tr::tur', 'xkb:ua::ukr', 'xkb:us::eng',
166  'xkb:us:altgr-intl:eng', 'xkb:us:colemak:eng', 'xkb:us:dvorak:eng',
167  'xkb:us:intl:eng',
168)
169
170# The path to the root of the repository.
171REPOSITORY_ROOT = os.path.join(os.path.dirname(__file__), '..', '..', '..')
172
173number_of_files_total = 0
174
175# Tags that need to be inserted to each 'action' tag and their default content.
176TAGS = {'description': 'Please enter the description of the metric.',
177        'owner': ('Please list the metric\'s owners. Add more owner tags as '
178                  'needed.')}
179
180
181def AddComputedActions(actions):
182  """Add computed actions to the actions list.
183
184  Arguments:
185    actions: set of actions to add to.
186  """
187
188  # Actions for back_forward_menu_model.cc.
189  for dir in ('BackMenu_', 'ForwardMenu_'):
190    actions.add(dir + 'ShowFullHistory')
191    actions.add(dir + 'Popup')
192    for i in range(1, 20):
193      actions.add(dir + 'HistoryClick' + str(i))
194      actions.add(dir + 'ChapterClick' + str(i))
195
196  # Actions for new_tab_ui.cc.
197  for i in range(1, 10):
198    actions.add('MostVisited%d' % i)
199
200  # Actions for language_options_handler.cc (Chrome OS specific).
201  for input_method_id in INPUT_METHOD_IDS:
202    actions.add('LanguageOptions_DisableInputMethod_%s' % input_method_id)
203    actions.add('LanguageOptions_EnableInputMethod_%s' % input_method_id)
204  for language_code in LANGUAGE_CODES:
205    actions.add('LanguageOptions_UiLanguageChange_%s' % language_code)
206    actions.add('LanguageOptions_SpellCheckLanguageChange_%s' % language_code)
207
208
209def AddPDFPluginActions(actions):
210  """Add actions that are sent by the PDF plugin.
211
212  Arguments
213    actions: set of actions to add to.
214  """
215  actions.add('PDF.LoadFailure')
216  actions.add('PDF.LoadSuccess')
217  actions.add('PDF.PreviewDocumentLoadFailure')
218  actions.add('PDF.PrintPage')
219  actions.add('PDF.ZoomFromBrowser')
220  actions.add('PDF_Unsupported_3D')
221  actions.add('PDF_Unsupported_Attachment')
222  actions.add('PDF_Unsupported_Bookmarks')
223  actions.add('PDF_Unsupported_Digital_Signature')
224  actions.add('PDF_Unsupported_Movie')
225  actions.add('PDF_Unsupported_Portfolios_Packages')
226  actions.add('PDF_Unsupported_Rights_Management')
227  actions.add('PDF_Unsupported_Screen')
228  actions.add('PDF_Unsupported_Shared_Form')
229  actions.add('PDF_Unsupported_Shared_Review')
230  actions.add('PDF_Unsupported_Sound')
231  actions.add('PDF_Unsupported_XFA')
232
233def AddBookmarkManagerActions(actions):
234  """Add actions that are used by BookmarkManager.
235
236  Arguments
237    actions: set of actions to add to.
238  """
239  actions.add('BookmarkManager_Command_AddPage')
240  actions.add('BookmarkManager_Command_Copy')
241  actions.add('BookmarkManager_Command_Cut')
242  actions.add('BookmarkManager_Command_Delete')
243  actions.add('BookmarkManager_Command_Edit')
244  actions.add('BookmarkManager_Command_Export')
245  actions.add('BookmarkManager_Command_Import')
246  actions.add('BookmarkManager_Command_NewFolder')
247  actions.add('BookmarkManager_Command_OpenIncognito')
248  actions.add('BookmarkManager_Command_OpenInNewTab')
249  actions.add('BookmarkManager_Command_OpenInNewWindow')
250  actions.add('BookmarkManager_Command_OpenInSame')
251  actions.add('BookmarkManager_Command_Paste')
252  actions.add('BookmarkManager_Command_ShowInFolder')
253  actions.add('BookmarkManager_Command_Sort')
254  actions.add('BookmarkManager_Command_UndoDelete')
255  actions.add('BookmarkManager_Command_UndoGlobal')
256  actions.add('BookmarkManager_Command_UndoNone')
257
258  actions.add('BookmarkManager_NavigateTo_BookmarkBar')
259  actions.add('BookmarkManager_NavigateTo_Mobile')
260  actions.add('BookmarkManager_NavigateTo_Other')
261  actions.add('BookmarkManager_NavigateTo_Recent')
262  actions.add('BookmarkManager_NavigateTo_Search')
263  actions.add('BookmarkManager_NavigateTo_SubFolder')
264
265def AddChromeOSActions(actions):
266  """Add actions reported by non-Chrome processes in Chrome OS.
267
268  Arguments:
269    actions: set of actions to add to.
270  """
271  # Actions sent by Chrome OS update engine.
272  actions.add('Updater.ServerCertificateChanged')
273  actions.add('Updater.ServerCertificateFailed')
274
275def AddExtensionActions(actions):
276  """Add actions reported by extensions via chrome.metricsPrivate API.
277
278  Arguments:
279    actions: set of actions to add to.
280  """
281  # Actions sent by Chrome OS File Browser.
282  actions.add('FileBrowser.CreateNewFolder')
283  actions.add('FileBrowser.PhotoEditor.Edit')
284  actions.add('FileBrowser.PhotoEditor.View')
285  actions.add('FileBrowser.SuggestApps.ShowDialog')
286
287  # Actions sent by Google Now client.
288  actions.add('GoogleNow.MessageClicked')
289  actions.add('GoogleNow.ButtonClicked0')
290  actions.add('GoogleNow.ButtonClicked1')
291  actions.add('GoogleNow.Dismissed')
292
293  # Actions sent by Chrome Connectivity Diagnostics.
294  actions.add('ConnectivityDiagnostics.LaunchSource.OfflineChromeOS')
295  actions.add('ConnectivityDiagnostics.LaunchSource.WebStore')
296  actions.add('ConnectivityDiagnostics.UA.LogsShown')
297  actions.add('ConnectivityDiagnostics.UA.PassingTestsShown')
298  actions.add('ConnectivityDiagnostics.UA.SettingsShown')
299  actions.add('ConnectivityDiagnostics.UA.TestResultExpanded')
300  actions.add('ConnectivityDiagnostics.UA.TestSuiteRun')
301
302
303class InvalidStatementException(Exception):
304  """Indicates an invalid statement was found."""
305
306
307class ActionNameFinder:
308  """Helper class to find action names in source code file."""
309
310  def __init__(self, path, contents, action_re):
311    self.__path = path
312    self.__pos = 0
313    self.__contents = contents
314    self.__action_re = action_re
315
316  def FindNextAction(self):
317    """Finds the next action name in the file.
318
319    Returns:
320      The name of the action found or None if there are no more actions.
321    Raises:
322      InvalidStatementException if the next action statement is invalid
323      and could not be parsed. There may still be more actions in the file,
324      so FindNextAction() can continue to be called to find following ones.
325    """
326    match = self.__action_re.search(self.__contents, pos=self.__pos)
327    if not match:
328      return None
329    match_start = match.start()
330    self.__pos = match.end()
331
332    match = QUOTED_STRING_RE.match(match.group(1))
333    if not match:
334      if self.__action_re == USER_METRICS_ACTION_RE_JS:
335        return None
336      self._RaiseException(match_start, self.__pos)
337
338    # Remove surrounding quotation marks.
339    return match.group(1)[1:-1]
340
341  def _RaiseException(self, match_start, match_end):
342    """Raises an InvalidStatementException for the specified code range."""
343    line_number = self.__contents.count('\n', 0, match_start) + 1
344    # Add 1 to |match_start| since the RE checks the preceding character.
345    statement = self.__contents[match_start + 1:match_end]
346    raise InvalidStatementException(
347      '%s uses UserMetricsAction incorrectly on line %d:\n%s' %
348      (self.__path, line_number, statement))
349
350
351def GrepForActions(path, actions):
352  """Grep a source file for calls to UserMetrics functions.
353
354  Arguments:
355    path: path to the file
356    actions: set of actions to add to
357  """
358  global number_of_files_total
359  number_of_files_total = number_of_files_total + 1
360
361  # Check the extension, using the regular expression for C++ syntax by default.
362  ext = os.path.splitext(path)[1].lower()
363  if ext == '.js':
364    action_re = USER_METRICS_ACTION_RE_JS
365  else:
366    action_re = USER_METRICS_ACTION_RE
367
368  finder = ActionNameFinder(path, open(path).read(), action_re)
369  while True:
370    try:
371      action_name = finder.FindNextAction()
372      if not action_name:
373        break
374      actions.add(action_name)
375    except InvalidStatementException, e:
376      logging.warning(str(e))
377
378  if action_re != USER_METRICS_ACTION_RE:
379    return
380
381  line_number = 0
382  for line in open(path):
383    line_number = line_number + 1
384    if COMPUTED_ACTION_RE.search(line):
385      # Warn if this file shouldn't be calling RecordComputedAction.
386      if os.path.basename(path) not in KNOWN_COMPUTED_USERS:
387        logging.warning('%s has RecordComputedAction statement on line %d' %
388                        (path, line_number))
389
390class WebUIActionsParser(HTMLParser):
391  """Parses an HTML file, looking for all tags with a 'metric' attribute.
392  Adds user actions corresponding to any metrics found.
393
394  Arguments:
395    actions: set of actions to add to
396  """
397  def __init__(self, actions):
398    HTMLParser.__init__(self)
399    self.actions = actions
400
401  def handle_starttag(self, tag, attrs):
402    # We only care to examine tags that have a 'metric' attribute.
403    attrs = dict(attrs)
404    if not 'metric' in attrs:
405      return
406
407    # Boolean metrics have two corresponding actions.  All other metrics have
408    # just one corresponding action.  By default, we check the 'dataType'
409    # attribute.
410    is_boolean = ('dataType' in attrs and attrs['dataType'] == 'boolean')
411    if 'type' in attrs and attrs['type'] in ('checkbox', 'radio'):
412      if attrs['type'] == 'checkbox':
413        is_boolean = True
414      else:
415        # Radio buttons are boolean if and only if their values are 'true' or
416        # 'false'.
417        assert(attrs['type'] == 'radio')
418        if 'value' in attrs and attrs['value'] in ['true', 'false']:
419          is_boolean = True
420
421    if is_boolean:
422      self.actions.add(attrs['metric'] + '_Enable')
423      self.actions.add(attrs['metric'] + '_Disable')
424    else:
425      self.actions.add(attrs['metric'])
426
427def GrepForWebUIActions(path, actions):
428  """Grep a WebUI source file for elements with associated metrics.
429
430  Arguments:
431    path: path to the file
432    actions: set of actions to add to
433  """
434  close_called = False
435  try:
436    parser = WebUIActionsParser(actions)
437    parser.feed(open(path).read())
438    # An exception can be thrown by parser.close(), so do it in the try to
439    # ensure the path of the file being parsed gets printed if that happens.
440    close_called = True
441    parser.close()
442  except Exception, e:
443    print("Error encountered for path %s" % path)
444    raise e
445  finally:
446    if not close_called:
447      parser.close()
448
449def GrepForDevToolsActions(path, actions):
450  """Grep a DevTools source file for calls to UserMetrics functions.
451
452  Arguments:
453    path: path to the file
454    actions: set of actions to add to
455  """
456  global number_of_files_total
457  number_of_files_total = number_of_files_total + 1
458
459  ext = os.path.splitext(path)[1].lower()
460  if ext != '.js':
461    return
462
463  finder = ActionNameFinder(path, open(path).read(),
464      USER_METRICS_ACTION_RE_DEVTOOLS)
465  while True:
466    try:
467      action_name = finder.FindNextAction()
468      if not action_name:
469        break
470      actions.add(action_name)
471    except InvalidStatementException, e:
472      logging.warning(str(e))
473
474def WalkDirectory(root_path, actions, extensions, callback):
475  for path, dirs, files in os.walk(root_path):
476    if '.svn' in dirs:
477      dirs.remove('.svn')
478    if '.git' in dirs:
479      dirs.remove('.git')
480    for file in files:
481      ext = os.path.splitext(file)[1]
482      if ext in extensions:
483        callback(os.path.join(path, file), actions)
484
485def AddLiteralActions(actions):
486  """Add literal actions specified via calls to UserMetrics functions.
487
488  Arguments:
489    actions: set of actions to add to.
490  """
491  EXTENSIONS = ('.cc', '.cpp', '.mm', '.c', '.m', '.java')
492
493  # Walk the source tree to process all files.
494  ash_root = os.path.normpath(os.path.join(REPOSITORY_ROOT, 'ash'))
495  WalkDirectory(ash_root, actions, EXTENSIONS, GrepForActions)
496  chrome_root = os.path.normpath(os.path.join(REPOSITORY_ROOT, 'chrome'))
497  WalkDirectory(chrome_root, actions, EXTENSIONS, GrepForActions)
498  content_root = os.path.normpath(os.path.join(REPOSITORY_ROOT, 'content'))
499  WalkDirectory(content_root, actions, EXTENSIONS, GrepForActions)
500  components_root = os.path.normpath(os.path.join(REPOSITORY_ROOT,
501                    'components'))
502  WalkDirectory(components_root, actions, EXTENSIONS, GrepForActions)
503  net_root = os.path.normpath(os.path.join(REPOSITORY_ROOT, 'net'))
504  WalkDirectory(net_root, actions, EXTENSIONS, GrepForActions)
505  webkit_root = os.path.normpath(os.path.join(REPOSITORY_ROOT, 'webkit'))
506  WalkDirectory(os.path.join(webkit_root, 'glue'), actions, EXTENSIONS,
507                GrepForActions)
508  WalkDirectory(os.path.join(webkit_root, 'port'), actions, EXTENSIONS,
509                GrepForActions)
510  webkit_core_root = os.path.normpath(
511                     os.path.join(REPOSITORY_ROOT,
512                                  'third_party/blink/renderer/core'))
513  WalkDirectory(webkit_core_root, actions, EXTENSIONS, GrepForActions)
514
515def AddWebUIActions(actions):
516  """Add user actions defined in WebUI files.
517
518  Arguments:
519    actions: set of actions to add to.
520  """
521  resources_root = os.path.join(REPOSITORY_ROOT, 'chrome', 'browser',
522                                'resources')
523  WalkDirectory(resources_root, actions, ('.html'), GrepForWebUIActions)
524  WalkDirectory(resources_root, actions, ('.js'), GrepForActions)
525
526def AddDevToolsActions(actions):
527  """Add user actions defined in DevTools frontend files.
528
529  Arguments:
530    actions: set of actions to add to.
531  """
532  resources_root = os.path.join(REPOSITORY_ROOT, 'third_party', 'blink',
533                                'renderer', 'devtools', 'front_end')
534  WalkDirectory(resources_root, actions, ('.js'), GrepForDevToolsActions)
535
536def AddHistoryPageActions(actions):
537  """Add actions that are used in History page.
538
539  Arguments
540    actions: set of actions to add to.
541  """
542  actions.add('HistoryPage_BookmarkStarClicked')
543  actions.add('HistoryPage_EntryMenuRemoveFromHistory')
544  actions.add('HistoryPage_EntryLinkClick')
545  actions.add('HistoryPage_EntryLinkRightClick')
546  actions.add('HistoryPage_SearchResultClick')
547  actions.add('HistoryPage_EntryMenuShowMoreFromSite')
548  actions.add('HistoryPage_NewestHistoryClick')
549  actions.add('HistoryPage_NewerHistoryClick')
550  actions.add('HistoryPage_OlderHistoryClick')
551  actions.add('HistoryPage_Search')
552  actions.add('HistoryPage_InitClearBrowsingData')
553  actions.add('HistoryPage_RemoveSelected')
554  actions.add('HistoryPage_SearchResultRemove')
555  actions.add('HistoryPage_ConfirmRemoveSelected')
556  actions.add('HistoryPage_CancelRemoveSelected')
557
558def AddAutomaticResetBannerActions(actions):
559  """Add actions that are used for the automatic profile settings reset banners
560  in chrome://settings.
561
562  Arguments
563    actions: set of actions to add to.
564  """
565  # These actions relate to the the automatic settings reset banner shown as
566  # a result of the reset prompt.
567  actions.add('AutomaticReset_WebUIBanner_BannerShown')
568  actions.add('AutomaticReset_WebUIBanner_ManuallyClosed')
569  actions.add('AutomaticReset_WebUIBanner_ResetClicked')
570
571  # These actions relate to the the automatic settings reset banner shown as
572  # a result of settings hardening.
573  actions.add('AutomaticSettingsReset_WebUIBanner_BannerShown')
574  actions.add('AutomaticSettingsReset_WebUIBanner_ManuallyClosed')
575  actions.add('AutomaticSettingsReset_WebUIBanner_LearnMoreClicked')
576  actions.add('AutomaticSettingsReset_WebUIBanner_ResetClicked')
577
578
579class Error(Exception):
580  pass
581
582
583def _ExtractText(parent_dom, tag_name):
584  """Extract the text enclosed by |tag_name| under |parent_dom|
585
586  Args:
587    parent_dom: The parent Element under which text node is searched for.
588    tag_name: The name of the tag which contains a text node.
589
590  Returns:
591    A (list of) string enclosed by |tag_name| under |parent_dom|.
592  """
593  texts = []
594  for child_dom in parent_dom.getElementsByTagName(tag_name):
595    text_dom = child_dom.childNodes
596    if text_dom.length != 1:
597      raise Error('More than 1 child node exists under %s' % tag_name)
598    if text_dom[0].nodeType != minidom.Node.TEXT_NODE:
599      raise Error('%s\'s child node is not a text node.' % tag_name)
600    texts.append(text_dom[0].data)
601  return texts
602
603
604def ParseActionFile(file_content):
605  """Parse the XML data currently stored in the file.
606
607  Args:
608    file_content: a string containing the action XML file content.
609
610  Returns:
611    (actions_dict, comment_nodes, suffixes):
612      - actions_dict is a dict from user action name to Action object.
613      - comment_nodes is a list of top-level comment nodes.
614      - suffixes is a list of <action-suffix> DOM elements.
615  """
616  dom = minidom.parseString(file_content)
617
618  comment_nodes = []
619  # Get top-level comments. It is assumed that all comments are placed before
620  # <actions> tag. Therefore the loop will stop if it encounters a non-comment
621  # node.
622  for node in dom.childNodes:
623    if node.nodeType == minidom.Node.COMMENT_NODE:
624      comment_nodes.append(node)
625    else:
626      break
627
628  actions_dict = {}
629  # Get each user action data.
630  for action_dom in dom.getElementsByTagName('action'):
631    action_name = action_dom.getAttribute('name')
632    not_user_triggered = bool(action_dom.getAttribute('not_user_triggered'))
633
634    owners = _ExtractText(action_dom, 'owner')
635    # There is only one description for each user action. Get the first element
636    # of the returned list.
637    description_list = _ExtractText(action_dom, 'description')
638    if len(description_list) > 1:
639      logging.error('User action "%s" has more than one description. Exactly '
640                    'one description is needed for each user action. Please '
641                    'fix.', action_name)
642      sys.exit(1)
643    description = description_list[0] if description_list else None
644    # There is at most one obsolete tag for each user action.
645    obsolete_list = _ExtractText(action_dom, 'obsolete')
646    if len(obsolete_list) > 1:
647      logging.error('User action "%s" has more than one obsolete tag. At most '
648                    'one obsolete tag can be added for each user action. Please'
649                    ' fix.', action_name)
650      sys.exit(1)
651    obsolete = obsolete_list[0] if obsolete_list else None
652    actions_dict[action_name] = action_utils.Action(action_name, description,
653        owners, not_user_triggered, obsolete)
654
655  suffixes = dom.getElementsByTagName('action-suffix')
656  action_utils.CreateActionsFromSuffixes(actions_dict, suffixes)
657
658  return actions_dict, comment_nodes, suffixes
659
660
661def _CreateActionTag(doc, action):
662  """Create a new action tag.
663
664  Format of an action tag:
665  <action name="name" not_user_triggered="true">
666    <obsolete>Deprecated.</obsolete>
667    <owner>Owner</owner>
668    <description>Description.</description>
669  </action>
670
671  not_user_triggered is an optional attribute. If set, it implies that the
672  belonging action is not a user action. A user action is an action that
673  is logged exactly once right after a user has made an action.
674
675  <obsolete> is an optional tag. It's added to actions that are no longer used
676  any more.
677
678  If action_name is in actions_dict, the values to be inserted are based on the
679  corresponding Action object. If action_name is not in actions_dict, the
680  default value from TAGS is used.
681
682  Args:
683    doc: The document under which the new action tag is created.
684    action: An Action object representing the data to be inserted.
685
686  Returns:
687    An action tag Element with proper children elements, or None if a tag should
688    not be created for this action (e.g. if it comes from a suffix).
689  """
690  if action.from_suffix:
691    return None
692
693  action_dom = doc.createElement('action')
694  action_dom.setAttribute('name', action.name)
695
696  # Add not_user_triggered attribute.
697  if action.not_user_triggered:
698    action_dom.setAttribute('not_user_triggered', 'true')
699
700  # Create obsolete tag.
701  if action.obsolete:
702    obsolete_dom = doc.createElement('obsolete')
703    action_dom.appendChild(obsolete_dom)
704    obsolete_dom.appendChild(doc.createTextNode(action.obsolete))
705
706  # Create owner tag.
707  if action.owners:
708    # If owners for this action is not None, use the stored value. Otherwise,
709    # use the default value.
710    for owner in action.owners:
711      owner_dom = doc.createElement('owner')
712      owner_dom.appendChild(doc.createTextNode(owner))
713      action_dom.appendChild(owner_dom)
714  else:
715    # Use default value.
716    owner_dom = doc.createElement('owner')
717    owner_dom.appendChild(doc.createTextNode(TAGS.get('owner', '')))
718    action_dom.appendChild(owner_dom)
719
720  # Create description tag.
721  description_dom = doc.createElement('description')
722  action_dom.appendChild(description_dom)
723  if action.description:
724    # If description for this action is not None, use the store value.
725    # Otherwise, use the default value.
726    description_dom.appendChild(doc.createTextNode(action.description))
727  else:
728    description_dom.appendChild(doc.createTextNode(
729        TAGS.get('description', '')))
730
731  return action_dom
732
733
734def PrettyPrint(actions_dict, comment_nodes, suffixes):
735  """Given a list of actions, create a well-printed minidom document.
736
737  Args:
738    actions_dict: A mappting from action name to Action object.
739    comment_nodes: A list of top-level comment nodes.
740    suffixes: A list of <action-suffix> tags to be appended as-is.
741
742  Returns:
743    A well-printed minidom document that represents the input action data.
744  """
745  doc = minidom.Document()
746
747  # Attach top-level comments.
748  for node in comment_nodes:
749    doc.appendChild(node)
750
751  actions_element = doc.createElement('actions')
752  doc.appendChild(actions_element)
753
754  # Attach action node based on updated |actions_dict|.
755  for _, action in sorted(actions_dict.items()):
756    action_tag = _CreateActionTag(doc, action)
757    if action_tag:
758      actions_element.appendChild(action_tag)
759
760  for suffix_tag in suffixes:
761    actions_element.appendChild(suffix_tag)
762
763  return actions_print_style.GetPrintStyle().PrettyPrintXml(doc)
764
765
766def UpdateXml(original_xml):
767  actions_dict, comment_nodes, suffixes = ParseActionFile(original_xml)
768
769  actions = set()
770  AddComputedActions(actions)
771  AddWebUIActions(actions)
772  AddDevToolsActions(actions)
773
774  AddLiteralActions(actions)
775
776  # print("Scanned {0} number of files".format(number_of_files_total))
777  # print("Found {0} entries".format(len(actions)))
778
779  AddAutomaticResetBannerActions(actions)
780  AddBookmarkManagerActions(actions)
781  AddChromeOSActions(actions)
782  AddExtensionActions(actions)
783  AddHistoryPageActions(actions)
784  AddPDFPluginActions(actions)
785
786  for action_name in actions:
787    if action_name not in actions_dict:
788      actions_dict[action_name] = action_utils.Action(action_name, None, [])
789
790  return PrettyPrint(actions_dict, comment_nodes, suffixes)
791
792
793def main(argv):
794  presubmit_util.DoPresubmitMain(argv, 'actions.xml', 'actions.old.xml',
795                                 'extract_actions.py', UpdateXml)
796
797if '__main__' == __name__:
798  sys.exit(main(sys.argv))
799