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