2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
7 Checks a policy_templates.json file for conformity to its syntax specification.
17 LEADING_WHITESPACE
= re
.compile('^([ \t]*)')
18 TRAILING_WHITESPACE
= re
.compile('.*?([ \t]+)$')
19 # Matches all non-empty strings that contain no whitespaces.
20 NO_WHITESPACE
= re
.compile('[^\s]+$')
22 # Convert a 'type' to the schema types it may be converted to.
23 # The 'dict' type represents structured JSON data, and can be converted
24 # to an 'object' or an 'array'.
28 'dict': [ 'object', 'array' ],
29 'main': [ 'boolean' ],
30 'string': [ 'string' ],
31 'int-enum': [ 'integer' ],
32 'string-enum': [ 'string' ],
33 'string-enum-list': [ 'array' ],
34 'external': [ 'object' ],
37 # List of boolean policies that have been introduced with negative polarity in
38 # the past and should not trigger the negative polarity check.
39 LEGACY_INVERTED_POLARITY_WHITELIST
= [
40 'DeveloperToolsDisabled',
41 'DeviceAutoUpdateDisabled',
43 'DisableAuthNegotiateCnameLookup',
44 'DisablePluginFinder',
45 'DisablePrintPreview',
46 'DisableSafeBrowsingProceedAnyway',
49 'DisableSSLRecordSplitting',
51 'DriveDisabledOverCellular',
52 'ExternalStorageDisabled',
53 'SavingBrowserHistoryDisabled',
57 class PolicyTemplateChecker(object):
61 self
.warning_count
= 0
64 self
.num_policies_in_groups
= 0
68 def _Error(self
, message
, parent_element
=None, identifier
=None,
69 offending_snippet
=None):
72 if identifier
is not None and parent_element
is not None:
73 error
+= 'In %s %s: ' % (parent_element
, identifier
)
74 print error
+ 'Error: ' + message
75 if offending_snippet
is not None:
76 print ' Offending:', json
.dumps(offending_snippet
, indent
=2)
78 def _CheckContains(self
, container
, key
, value_type
,
80 parent_element
='policy',
83 offending
='__CONTAINER__',
86 Checks |container| for presence of |key| with value of type |value_type|.
87 If |value_type| is string and |regexp_check| is specified, then an error is
88 reported when the value does not match the regular expression object.
90 |value_type| can also be a list, if more than one type is supported.
92 The other parameters are needed to generate, if applicable, an appropriate
93 human-readable error message of the following form:
95 In |parent_element| |identifier|:
96 (if the key is not present):
97 Error: |container_name| must have a |value_type| named |key|.
98 Offending snippet: |offending| (if specified; defaults to |container|)
99 (if the value does not have the required type):
100 Error: Value of |key| must be a |value_type|.
101 Offending snippet: |container[key]|
103 Returns: |container[key]| if the key is present, None otherwise.
105 if identifier
is None:
107 identifier
= container
.get('name')
109 self
._Error
('Cannot access container name of "%s".' % container_name
)
111 if container_name
is None:
112 container_name
= parent_element
113 if offending
== '__CONTAINER__':
114 offending
= container
115 if key
not in container
:
119 self
._Error
('%s must have a %s "%s".' %
120 (container_name
.title(), value_type
.__name
__, key
),
121 container_name
, identifier
, offending
)
123 value
= container
[key
]
124 value_types
= value_type
if isinstance(value_type
, list) else [ value_type
]
125 if not any(isinstance(value
, type) for type in value_types
):
126 self
._Error
('Value of "%s" must be one of [ %s ].' %
127 (key
, ', '.join([type.__name
__ for type in value_types
])),
128 container_name
, identifier
, value
)
129 if str in value_types
and regexp_check
and not regexp_check
.match(value
):
130 self
._Error
('Value of "%s" must match "%s".' %
131 (key
, regexp_check
.pattern
),
132 container_name
, identifier
, value
)
135 def _AddPolicyID(self
, id, policy_ids
, policy
):
137 Adds |id| to |policy_ids|. Generates an error message if the
138 |id| exists already; |policy| is needed for this message.
141 self
._Error
('Duplicate id', 'policy', policy
.get('name'),
146 def _CheckPolicyIDs(self
, policy_ids
):
148 Checks a set of policy_ids to make sure it contains a continuous range
149 of entries (i.e. no holes).
150 Holes would not be a technical problem, but we want to ensure that nobody
151 accidentally omits IDs.
153 for i
in range(len(policy_ids
)):
154 if (i
+ 1) not in policy_ids
:
155 self
._Error
('No policy with id: %s' % (i
+ 1))
157 def _CheckPolicySchema(self
, policy
, policy_type
):
158 '''Checks that the 'schema' field matches the 'type' field.'''
159 self
._CheckContains
(policy
, 'schema', dict)
160 if isinstance(policy
.get('schema'), dict):
161 self
._CheckContains
(policy
['schema'], 'type', str)
162 schema_type
= policy
['schema'].get('type')
163 if schema_type
not in TYPE_TO_SCHEMA
[policy_type
]:
164 self
._Error
('Schema type must match the existing type for policy %s' %
167 # Checks that boolean policies are not negated (which makes them harder to
169 if (schema_type
== 'boolean' and
170 'disable' in policy
.get('name').lower() and
171 policy
.get('name') not in LEGACY_INVERTED_POLARITY_WHITELIST
):
172 self
._Error
(('Boolean policy %s uses negative polarity, please make ' +
173 'new boolean policies follow the XYZEnabled pattern. ' +
174 'See also http://crbug.com/85687') % policy
.get('name'))
177 def _CheckPolicy(self
, policy
, is_in_group
, policy_ids
):
178 if not isinstance(policy
, dict):
179 self
._Error
('Each policy must be a dictionary.', 'policy', None, policy
)
182 # There should not be any unknown keys in |policy|.
184 if key
not in ('name', 'type', 'caption', 'desc', 'device_only',
185 'supported_on', 'label', 'policies', 'items',
186 'example_value', 'features', 'deprecated', 'future',
187 'id', 'schema', 'max_size',
188 'default_for_enterprise_users'):
189 self
.warning_count
+= 1
190 print ('In policy %s: Warning: Unknown key: %s' %
191 (policy
.get('name'), key
))
193 # Each policy must have a name.
194 self
._CheckContains
(policy
, 'name', str, regexp_check
=NO_WHITESPACE
)
196 # Each policy must have a type.
197 policy_types
= ('group', 'main', 'string', 'int', 'list', 'int-enum',
198 'string-enum', 'string-enum-list', 'dict', 'external')
199 policy_type
= self
._CheckContains
(policy
, 'type', str)
200 if policy_type
not in policy_types
:
201 self
._Error
('Policy type must be one of: ' + ', '.join(policy_types
),
202 'policy', policy
.get('name'), policy_type
)
203 return # Can't continue for unsupported type.
205 # Each policy must have a caption message.
206 self
._CheckContains
(policy
, 'caption', str)
208 # Each policy must have a description message.
209 self
._CheckContains
(policy
, 'desc', str)
211 # If 'label' is present, it must be a string.
212 self
._CheckContains
(policy
, 'label', str, True)
214 # If 'deprecated' is present, it must be a bool.
215 self
._CheckContains
(policy
, 'deprecated', bool, True)
217 # If 'future' is present, it must be a bool.
218 self
._CheckContains
(policy
, 'future', bool, True)
220 if policy_type
== 'group':
221 # Groups must not be nested.
223 self
._Error
('Policy groups must not be nested.', 'policy', policy
)
225 # Each policy group must have a list of policies.
226 policies
= self
._CheckContains
(policy
, 'policies', list)
228 # Check sub-policies.
229 if policies
is not None:
230 for nested_policy
in policies
:
231 self
._CheckPolicy
(nested_policy
, True, policy_ids
)
233 # Groups must not have an |id|.
235 self
._Error
('Policies of type "group" must not have an "id" field.',
241 else: # policy_type != group
242 # Each policy must have a protobuf ID.
243 id = self
._CheckContains
(policy
, 'id', int)
244 self
._AddPolicyID
(id, policy_ids
, policy
)
246 # 'schema' is the new 'type'.
247 # TODO(joaodasilva): remove the 'type' checks once 'schema' is used
249 self
._CheckPolicySchema
(policy
, policy_type
)
251 # Each policy must have a supported_on list.
252 supported_on
= self
._CheckContains
(policy
, 'supported_on', list)
253 if supported_on
is not None:
254 for s
in supported_on
:
255 if not isinstance(s
, str):
256 self
._Error
('Entries in "supported_on" must be strings.', 'policy',
257 policy
, supported_on
)
259 # Each policy must have a 'features' dict.
260 features
= self
._CheckContains
(policy
, 'features', dict)
262 # All the features must have a documenting message.
264 for feature
in features
:
265 if not feature
in self
.features
:
266 self
._Error
('Unknown feature "%s". Known features must have a '
267 'documentation string in the messages dictionary.' %
268 feature
, 'policy', policy
.get('name', policy
))
270 # All user policies must have a per_profile feature flag.
271 if (not policy
.get('device_only', False) and
272 not policy
.get('deprecated', False) and
273 not filter(re
.compile('^chrome_frame:.*').match
, supported_on
)):
274 self
._CheckContains
(features
, 'per_profile', bool,
275 container_name
='features',
276 identifier
=policy
.get('name'))
278 # All policies must declare whether they allow changes at runtime.
279 self
._CheckContains
(features
, 'dynamic_refresh', bool,
280 container_name
='features',
281 identifier
=policy
.get('name'))
283 # Each policy must have an 'example_value' of appropriate type.
284 if policy_type
== 'main':
285 value_type
= item_type
= bool
286 elif policy_type
in ('string', 'string-enum'):
287 value_type
= item_type
= str
288 elif policy_type
in ('int', 'int-enum'):
289 value_type
= item_type
= int
290 elif policy_type
in ('list', 'string-enum-list'):
293 elif policy_type
== 'external':
294 value_type
= item_type
= dict
295 elif policy_type
== 'dict':
296 value_type
= item_type
= [ dict, list ]
298 raise NotImplementedError('Unimplemented policy type: %s' % policy_type
)
299 self
._CheckContains
(policy
, 'example_value', value_type
)
302 self
.num_policies
+= 1
304 self
.num_policies_in_groups
+= 1
306 if policy_type
in ('int-enum', 'string-enum', 'string-enum-list'):
307 # Enums must contain a list of items.
308 items
= self
._CheckContains
(policy
, 'items', list)
309 if items
is not None:
311 self
._Error
('"items" must not be empty.', 'policy', policy
, items
)
313 # Each item must have a name.
314 # Note: |policy.get('name')| is used instead of |policy['name']|
315 # because it returns None rather than failing when no key called
317 self
._CheckContains
(item
, 'name', str, container_name
='item',
318 identifier
=policy
.get('name'),
319 regexp_check
=NO_WHITESPACE
)
321 # Each item must have a value of the correct type.
322 self
._CheckContains
(item
, 'value', item_type
, container_name
='item',
323 identifier
=policy
.get('name'))
325 # Each item must have a caption.
326 self
._CheckContains
(item
, 'caption', str, container_name
='item',
327 identifier
=policy
.get('name'))
329 if policy_type
== 'external':
330 # Each policy referencing external data must specify a maximum data size.
331 self
._CheckContains
(policy
, 'max_size', int)
333 def _CheckMessage(self
, key
, value
):
334 # |key| must be a string, |value| a dict.
335 if not isinstance(key
, str):
336 self
._Error
('Each message key must be a string.', 'message', key
, key
)
339 if not isinstance(value
, dict):
340 self
._Error
('Each message must be a dictionary.', 'message', key
, value
)
343 # Each message must have a desc.
344 self
._CheckContains
(value
, 'desc', str, parent_element
='message',
347 # Each message must have a text.
348 self
._CheckContains
(value
, 'text', str, parent_element
='message',
351 # There should not be any unknown keys in |value|.
353 if vkey
not in ('desc', 'text'):
354 self
.warning_count
+= 1
355 print 'In message %s: Warning: Unknown key: %s' % (key
, vkey
)
357 def _LeadingWhitespace(self
, line
):
358 match
= LEADING_WHITESPACE
.match(line
)
360 return match
.group(1)
363 def _TrailingWhitespace(self
, line
):
364 match
= TRAILING_WHITESPACE
.match(line
)
366 return match
.group(1)
369 def _LineError(self
, message
, line_number
):
370 self
.error_count
+= 1
371 print 'In line %d: Error: %s' % (line_number
, message
)
373 def _LineWarning(self
, message
, line_number
):
374 self
.warning_count
+= 1
375 print ('In line %d: Warning: Automatically fixing formatting: %s'
376 % (line_number
, message
))
378 def _CheckFormat(self
, filename
):
381 with
open(filename
) as f
:
386 line
= line
.rstrip('\n')
387 # Check for trailing whitespace.
388 trailing_whitespace
= self
._TrailingWhitespace
(line
)
389 if len(trailing_whitespace
) > 0:
392 self
._LineWarning
('Trailing whitespace.', line_number
)
394 self
._LineError
('Trailing whitespace.', line_number
)
397 fixed_lines
+= ['\n']
400 if line
== trailing_whitespace
:
401 # This also catches the case of an empty line.
403 # Check for correct amount of leading whitespace.
404 leading_whitespace
= self
._LeadingWhitespace
(line
)
405 if leading_whitespace
.count('\t') > 0:
407 leading_whitespace
= leading_whitespace
.replace('\t', ' ')
408 line
= leading_whitespace
+ line
.lstrip()
409 self
._LineWarning
('Tab character found.', line_number
)
411 self
._LineError
('Tab character found.', line_number
)
412 if line
[len(leading_whitespace
)] in (']', '}'):
414 if line
[0] != '#': # Ignore 0-indented comments.
415 if len(leading_whitespace
) != indent
:
417 line
= ' ' * indent
+ line
.lstrip()
418 self
._LineWarning
('Indentation should be ' + str(indent
) +
419 ' spaces.', line_number
)
421 self
._LineError
('Bad indentation. Should be ' + str(indent
) +
422 ' spaces.', line_number
)
423 if line
[-1] in ('[', '{'):
426 fixed_lines
.append(line
+ '\n')
428 # If --fix is specified: backup the file (deleting any existing backup),
429 # then write the fixed version with the old filename.
431 if self
.options
.backup
:
432 backupfilename
= filename
+ '.bak'
433 if os
.path
.exists(backupfilename
):
434 os
.remove(backupfilename
)
435 os
.rename(filename
, backupfilename
)
436 with
open(filename
, 'w') as f
:
437 f
.writelines(fixed_lines
)
439 def Main(self
, filename
, options
):
441 with
open(filename
) as f
:
442 data
= eval(f
.read())
445 traceback
.print_exc(file=sys
.stdout
)
446 self
._Error
('Invalid Python/JSON syntax.')
449 self
._Error
('Invalid Python/JSON syntax.')
451 self
.options
= options
453 # First part: check JSON structure.
455 # Check (non-policy-specific) message definitions.
456 messages
= self
._CheckContains
(data
, 'messages', dict,
458 container_name
='The root element',
460 if messages
is not None:
461 for message
in messages
:
462 self
._CheckMessage
(message
, messages
[message
])
463 if message
.startswith('doc_feature_'):
464 self
.features
.append(message
[12:])
466 # Check policy definitions.
467 policy_definitions
= self
._CheckContains
(data
, 'policy_definitions', list,
469 container_name
='The root element',
471 if policy_definitions
is not None:
473 for policy
in policy_definitions
:
474 self
._CheckPolicy
(policy
, False, policy_ids
)
475 self
._CheckPolicyIDs
(policy_ids
)
477 # Second part: check formatting.
478 self
._CheckFormat
(filename
)
480 # Third part: summary and exit.
481 print ('Finished checking %s. %d errors, %d warnings.' %
482 (filename
, self
.error_count
, self
.warning_count
))
483 if self
.options
.stats
:
484 if self
.num_groups
> 0:
485 print ('%d policies, %d of those in %d groups (containing on '
486 'average %.1f policies).' %
487 (self
.num_policies
, self
.num_policies_in_groups
, self
.num_groups
,
488 (1.0 * self
.num_policies_in_groups
/ self
.num_groups
)))
490 print self
.num_policies
, 'policies, 0 policy groups.'
491 if self
.error_count
> 0:
495 def Run(self
, argv
, filename
=None):
496 parser
= optparse
.OptionParser(
497 usage
='usage: %prog [options] filename',
498 description
='Syntax check a policy_templates.json file.')
499 parser
.add_option('--fix', action
='store_true',
500 help='Automatically fix formatting.')
501 parser
.add_option('--backup', action
='store_true',
502 help='Create backup of original file (before fixing).')
503 parser
.add_option('--stats', action
='store_true',
504 help='Generate statistics.')
505 (options
, args
) = parser
.parse_args(argv
)
511 return self
.Main(filename
, options
)
514 if __name__
== '__main__':
515 sys
.exit(PolicyTemplateChecker().Run(sys
.argv
))