chromium/tools/grit/grit/gather/policy_json.py

# Copyright 2012 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

'''Support for "policy_templates.json" format used by the policy template
generator as a source for generating ADM,ADMX,etc files.'''

import importlib.abc
import importlib.util
import json
import os.path
import sys

from grit.gather import skeleton_gatherer
from grit import util
from grit import tclib
from xml.dom import minidom
from xml.parsers.expat import ExpatError


class StringLoader(importlib.abc.SourceLoader):
  def __init__(self, data, dir):
    self.data = data
    self.dir = dir

  def get_data(self, path):
    return self.data

  def get_filename(self, fullname):
    return os.path.join(self.dir, fullname + ".py")


class PolicyJson(skeleton_gatherer.SkeletonGatherer):
  '''Collects and translates the following strings from policy_templates.json:
    - captions, descriptions, labels and Android app support details of policies
    - captions of enumeration items
    - misc strings from the 'messages' section
     Translatable strings may have untranslateable placeholders with the same
     format that is used in .grd files.
  '''

  def _AddEndline(self, add_comma):
    '''Adds an endline to the skeleton tree. If add_comma is true, adds a
       comma before the endline.

    Args:
      add_comma: A boolean to add a comma or not.
    '''
    self._AddNontranslateableChunk(',\n' if add_comma else '\n')

  def _ParsePlaceholder(self, placeholder, msg):
    '''Extracts a placeholder from a DOM node and adds it to a tclib Message.

    Args:
      placeholder: A DOM node of the form:
        <ph name="PLACEHOLDER_NAME">Placeholder text<ex>Example value</ex></ph>
      msg: The placeholder is added to this message.
    '''
    text = []
    example_text = []
    for node1 in placeholder.childNodes:
      if (node1.nodeType == minidom.Node.TEXT_NODE):
        text.append(node1.data)
      elif (node1.nodeType == minidom.Node.ELEMENT_NODE and
            node1.tagName == 'ex'):
        for node2 in node1.childNodes:
          example_text.append(node2.toxml())
      else:
        raise Exception('Unexpected element inside a placeholder: ' +
                        node2.toxml())
    if example_text == []:
      # In such cases the original text is okay for an example.
      example_text = text

    replaced_text = self.Escape(''.join(text).strip())
    replaced_text = replaced_text.replace('$1', self._config['app_name'])
    replaced_text = replaced_text.replace('$2', self._config['os_name'])
    replaced_text = replaced_text.replace('$3', self._config['frame_name'])

    msg.AppendPlaceholder(tclib.Placeholder(
        placeholder.attributes['name'].value,
        replaced_text,
        ''.join(example_text).strip()))

  def _ParseMessage(self, string, desc):
    '''Parses a given string and adds it to the output as a translatable chunk
    with a given description.

    Args:
      string: The message string to parse.
      desc: The description of the message (for the translators).
    '''
    msg = tclib.Message(description=desc)
    xml = '<msg>' + string + '</msg>'
    try:
      node = minidom.parseString(xml).childNodes[0]
    except ExpatError:
      raise Exception('''Input isn't valid XML (has < & > been escaped?): ''' +
                      string)

    for child in node.childNodes:
      if child.nodeType == minidom.Node.TEXT_NODE:
        msg.AppendText(child.data)
      elif child.nodeType == minidom.Node.ELEMENT_NODE:
        if child.tagName == 'ph':
          self._ParsePlaceholder(child, msg)
        else:
          raise Exception("Not implemented.")
      else:
        raise Exception("Not implemented.")
    self.skeleton_.append(self.uberclique.MakeClique(msg))

  def _ParseNode(self, node):
    '''Traverses the subtree of a DOM node, and register a tclib message for
    all the <message> nodes.
    '''
    att_text = []
    if node.attributes:
      for key, value in sorted(node.attributes.items()):
        att_text.append(' %s=\"%s\"' % (key, value))
    self._AddNontranslateableChunk("<%s%s>" %
                                   (node.tagName, ''.join(att_text)))
    if node.tagName == 'message':
      msg = tclib.Message(description=node.attributes['desc'])
      for child in node.childNodes:
        if child.nodeType == minidom.Node.TEXT_NODE:
          if msg == None:
            self._AddNontranslateableChunk(child.data)
          else:
            msg.AppendText(child.data)
        elif child.nodeType == minidom.Node.ELEMENT_NODE:
          if child.tagName == 'ph':
            self._ParsePlaceholder(child, msg)
        else:
          assert False
      self.skeleton_.append(self.uberclique.MakeClique(msg))
    else:
      for child in node.childNodes:
        if child.nodeType == minidom.Node.TEXT_NODE:
          self._AddNontranslateableChunk(child.data)
        elif node.nodeType == minidom.Node.ELEMENT_NODE:
          self._ParseNode(child)

    self._AddNontranslateableChunk("</%s>" % node.tagName)

  def _AddIndentedNontranslateableChunk(self, depth, string):
    '''Adds a nontranslateable chunk of text to the internally stored output.

    Args:
      depth: The number of double spaces to prepend to the next argument string.
      string: The chunk of text to add.
    '''
    result = []
    while depth > 0:
      result.append('  ')
      depth = depth - 1
    result.append(string)
    self._AddNontranslateableChunk(''.join(result))

  def _GetDescription(self, item, item_type, parent_item, key):
    '''Creates a description for a translatable message. The description gives
    some context for the person who will translate this message.

    Args:
      item: A policy or an enumeration item.
      item_type: 'enum_item' | 'policy'
      parent_item: The owner of item. (A policy of type group or enum.)
      key: The name of the key to parse.
      depth: The level of indentation.
    '''
    key_map = {
      'desc': 'Description',
      'caption': 'Caption',
      'label': 'Label',
      'arc_support': 'Information about the effect on Android apps'
    }
    if item_type == 'policy':
      return ('%s of the policy named %s [owner(s): %s]' %
              (key_map[key], item['name'],
               ','.join(item['owners'] if 'owners' in item else 'unknown')))
    if item_type == 'enum_item':
      if 'name' in item:
        return ('%s of the option named %s in policy %s [owner(s): %s]' %
                (key_map[key], item['name'], parent_item['name'],
                 ','.join(parent_item['owners'] if 'owners' in
                          parent_item else 'unknown')))
      else:
        return ('%s of the option with value %s in policy %s [owner(s): %s]' %
                (key_map[key], item['value'], parent_item['name'],
                 ','.join(parent_item['owners'] if 'owners' in
                          parent_item else 'unknown')))
    raise Exception('Unexpected type %s' % item_type)

  def _AddSchemaKeys(self, obj, depth):
    obj_type = type(obj)
    if obj_type == dict:
      self._AddNontranslateableChunk('{\n')
      keys = sorted(obj.keys())
      for count, (key) in enumerate(keys, 1):
        json_key = "%s: " % json.dumps(key)
        self._AddIndentedNontranslateableChunk(depth + 1, json_key)
        if key == 'description' and type(obj[key]) == str:
          self._AddNontranslateableChunk("\"")
          self._ParseMessage(obj[key], 'Description of schema property')
          self._AddNontranslateableChunk("\"")
        elif type(obj[key]) in (bool, int, str):
          self._AddSchemaKeys(obj[key], 0)
        else:
          self._AddSchemaKeys(obj[key], depth + 1)
        self._AddEndline(count < len(keys))
      self._AddIndentedNontranslateableChunk(depth, '}')
    elif obj_type == list:
      self._AddNontranslateableChunk('[\n')
      for count, (item) in enumerate(obj, 1):
        self._AddSchemaKeys(item, depth + 1)
        self._AddEndline(count < len(obj))
      self._AddIndentedNontranslateableChunk(depth, ']')
    elif obj_type in (bool, int, str):
      self._AddIndentedNontranslateableChunk(depth, json.dumps(obj))
    else:
      raise Exception('Invalid schema object: %s' % obj)

  def _AddPolicyKey(self, item, item_type, parent_item, key, depth):
    '''Given a policy/enumeration item and a key, adds that key and its value
    into the output.
    E.g.:
       'example_value': 123
    If key indicates that the value is a translatable string, then it is parsed
    as a translatable string.

    Args:
      item: A policy or an enumeration item.
      item_type: 'enum_item' | 'policy'
      parent_item: The owner of item. (A policy of type group or enum.)
      key: The name of the key to parse.
      depth: The level of indentation.
    '''
    self._AddIndentedNontranslateableChunk(depth, "%s: " % json.dumps(key))
    if key in ('desc', 'caption', 'label', 'arc_support'):
      self._AddNontranslateableChunk("\"")
      self._ParseMessage(
          item[key],
          self._GetDescription(item, item_type, parent_item, key))
      self._AddNontranslateableChunk("\"")
    elif key in ('schema', 'validation_schema', 'description_schema'):
      self._AddSchemaKeys(item[key], depth)
    else:
      self._AddNontranslateableChunk(json.dumps(item[key], ensure_ascii=False))

  def _AddItems(self, items, item_type, parent_item, depth):
    '''Parses and adds a list of items from the JSON file. Items can be policies
    or parts of an enum policy.

    Args:
      items: Either a list of policies or a list of dictionaries.
      item_type: 'enum_item' | 'policy'
      parent_item: If items contains a list of policies, then this is the policy
        group that owns them. If items contains a list of enumeration items,
        then this is the enum policy that holds them.
      depth: Indicates the depth of our position in the JSON hierarchy. Used to
        add nice line-indent to the output.
    '''
    for item_count, (item1) in enumerate(items, 1):
      self._AddIndentedNontranslateableChunk(depth, "{\n")
      keys = sorted(item1.keys())
      for keys_count, (key) in enumerate(keys, 1):
        if key == 'items':
          self._AddIndentedNontranslateableChunk(depth + 1, "\"items\": [\n")
          self._AddItems(item1['items'], 'enum_item', item1, depth + 2)
          self._AddIndentedNontranslateableChunk(depth + 1, "]")
        elif key == 'policies' and all(not isinstance(x, str)
                                       for x in item1['policies']):
          self._AddIndentedNontranslateableChunk(depth + 1, "\"policies\": [\n")
          self._AddItems(item1['policies'], 'policy', item1, depth + 2)
          self._AddIndentedNontranslateableChunk(depth + 1, "]")
        else:
          self._AddPolicyKey(item1, item_type, parent_item, key, depth + 1)
        self._AddEndline(keys_count < len(keys))
      self._AddIndentedNontranslateableChunk(depth, "}")
      self._AddEndline(item_count < len(items))

  def _AddMessages(self):
    '''Processed and adds the 'messages' section to the output.'''
    self._AddNontranslateableChunk("  \"messages\": {\n")
    messages = self.data['messages'].items()
    for count, (name, message) in enumerate(messages, 1):
      self._AddNontranslateableChunk("    %s: {\n" % json.dumps(name))
      self._AddNontranslateableChunk("      \"text\": \"")
      self._ParseMessage(message['text'], message['desc'])
      self._AddNontranslateableChunk("\"\n")
      self._AddNontranslateableChunk("    }")
      self._AddEndline(count < len(self.data['messages']))
    self._AddNontranslateableChunk("  }\n")

  # Although we use the RegexpGatherer base class, we do not use the
  # _RegExpParse method of that class to implement Parse().  Instead, we
  # parse using a DOM parser.
  def Parse(self):
    if self.have_parsed_:
      return
    self.have_parsed_ = True

    self.text_ = self._LoadInputFile()
    if isinstance(self.rc_file, str):
      name = 'policy_templates'
      spec = importlib.util.spec_from_loader(
          name,
          loader=StringLoader(self.text_,
                              os.path.dirname(self.GetAbsoluteInputPath())))
      policy_templates = importlib.util.module_from_spec(spec)
      exec(self.text_, policy_templates.__dict__)
      self.data = policy_templates.GetPolicyTemplates()
    else:
      if util.IsExtraVerbose():
        print(self.text_)
      self.data = json.loads(self.text_)

    self._AddNontranslateableChunk('{\n')
    self._AddNontranslateableChunk("  \"policy_definitions\": [\n")
    self._AddItems(self.data['policy_definitions'], 'policy', None, 2)
    self._AddNontranslateableChunk("  ],\n")
    self._AddNontranslateableChunk("  \"policy_atomic_group_definitions\": [\n")
    if 'policy_atomic_group_definitions' in self.data:
      self._AddItems(self.data['policy_atomic_group_definitions'],
                    'policy', None, 2)
    self._AddNontranslateableChunk("  ],\n")
    self._AddMessages()
    self._AddNontranslateableChunk('\n}')

  def Escape(self, text):
    return json.dumps(text, ensure_ascii=False)[1:-1]

  def SetDefines(self, defines):
    if not defines:
      raise Exception('Must pass valid defines')

    if '_chromium' in defines:
      self._config = {
          'build': 'chromium',
          'app_name': 'Chromium',
          'frame_name': 'Chromium Frame',
          'os_name': 'ChromiumOS',
      }
    elif '_google_chrome' in defines:
      self._config = {
          'build': 'chrome',
          'app_name': 'Google Chrome',
          'frame_name': 'Google Chrome Frame',
          'os_name': 'Google ChromeOS',
      }
    else:
      raise Exception('Unknown build')