odflint 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. #!/usr/bin/python3
  2. # -*- coding: utf-8 -*-
  3. # Copyright (C) 2009 Søren Roug, European Environment Agency
  4. #
  5. # This is free software. You may redistribute it under the terms
  6. # of the Apache license and the GNU General Public License Version
  7. # 2 or at your option any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public
  15. # License along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  17. #
  18. # Contributor(s):
  19. #
  20. import zipfile
  21. from xml.sax import make_parser,handler
  22. from xml.sax.xmlreader import InputSource
  23. import xml.sax.saxutils
  24. import sys
  25. from odf.opendocument import OpenDocument
  26. from odf import element, grammar
  27. from odf.namespaces import *
  28. from odf.attrconverters import attrconverters, cnv_string
  29. from io import BytesIO
  30. if sys.version_info[0]==3: unicode=str
  31. extension_attributes = {
  32. "OpenOffice.org" : {
  33. (METANS,u'template'): (
  34. (XLINKNS,u'role'),
  35. ),
  36. (STYLENS,u'graphic-properties'): (
  37. (STYLENS,u'background-transparency'),
  38. ),
  39. (STYLENS,u'paragraph-properties'): (
  40. (TEXTNS,u'enable-numbering'),
  41. (STYLENS,u'join-border'),
  42. ),
  43. (STYLENS,u'table-cell-properties'): (
  44. (STYLENS,u'writing-mode'),
  45. ),
  46. (STYLENS,u'table-row-properties'): (
  47. (STYLENS,u'keep-together'),
  48. ),
  49. },
  50. "KOffice" : {
  51. (STYLENS,u'graphic-properties'): (
  52. (KOFFICENS,u'frame-behavior-on-new-page'),
  53. ),
  54. (DRAWNS,u'page'): (
  55. (KOFFICENS,u'name'),
  56. ),
  57. (PRESENTATIONNS,u'show-shape'): (
  58. (KOFFICENS,u'order-id'),
  59. ),
  60. (PRESENTATIONNS,u'hide-shape'): (
  61. (KOFFICENS,u'order-id'),
  62. ),
  63. (CHARTNS,u'legend'): (
  64. (KOFFICENS,u'title'),
  65. ),
  66. }
  67. }
  68. printed_errors = []
  69. def print_error(str):
  70. if str not in printed_errors:
  71. printed_errors.append(str)
  72. print (str)
  73. def chop_arg(arg):
  74. if len(arg) > 20:
  75. return "%s..." % arg[0:20]
  76. return arg
  77. def make_qname(tag):
  78. return "%s:%s" % (nsdict.get(tag[0],tag[0]), tag[1])
  79. def allowed_attributes(tag):
  80. return grammar.allowed_attributes.get(tag)
  81. class ODFElementHandler(handler.ContentHandler):
  82. """ Extract headings from content.xml of an ODT file """
  83. def __init__(self, document):
  84. self.doc = document
  85. self.tagstack = []
  86. self.data = []
  87. self.currtag = None
  88. def characters(self, data):
  89. self.data.append(data)
  90. def startElementNS(self, tag, qname, attrs):
  91. """ Pseudo-create an element
  92. """
  93. allowed_attrs = grammar.allowed_attributes.get(tag)
  94. attrdict = {}
  95. for (att,value) in attrs.items():
  96. prefix = nsdict.get(att[0],att[0])
  97. # Check if it is a known extension
  98. notan_extension = True
  99. for product, ext_attrs in extension_attributes.items():
  100. allowed_ext_attrs = ext_attrs.get(tag)
  101. if allowed_ext_attrs and att in allowed_ext_attrs:
  102. print_error("Warning: Attribute %s in element <%s> is illegal - %s extension" % ( make_qname(att), make_qname(tag), product))
  103. notan_extension = False
  104. # Check if it is an allowed attribute
  105. if notan_extension and allowed_attrs and att not in allowed_attrs:
  106. print_error("Error: Attribute %s:%s is not allowed in element <%s>" % ( prefix, att[1], make_qname(tag)))
  107. # Check the value
  108. try:
  109. convert = attrconverters.get(att, cnv_string)
  110. convert(att, value, tag)
  111. except ValueError as res:
  112. print_error("Error: Bad value '%s' for attribute %s:%s in tag: <%s> - %s" %
  113. (chop_arg(value), prefix, att[1], make_qname(tag), res))
  114. self.tagstack.append(tag)
  115. self.data = []
  116. # Check that the parent allows this child element
  117. if tag not in ( (OFFICENS, 'document'), (OFFICENS, 'document-content'), (OFFICENS, 'document-styles'),
  118. (OFFICENS, 'document-meta'), (OFFICENS, 'document-settings'),
  119. (MANIFESTNS,'manifest')):
  120. try:
  121. parent = self.tagstack[-2]
  122. allowed_children = grammar.allowed_children.get(parent)
  123. except:
  124. print_error("Error: This document starts with the wrong tag: <%s>" % make_qname(tag))
  125. allowed_children = None
  126. if allowed_children and tag not in allowed_children:
  127. print_error("Error: Element %s is not allowed in element %s" % ( make_qname(tag), make_qname(parent)))
  128. # Test that all mandatory attributes have been added.
  129. required = grammar.required_attributes.get(tag)
  130. if required:
  131. for r in required:
  132. if attrs.get(r) is None:
  133. print_error("Error: Required attribute missing: %s in <%s>" % (make_qname(r), make_qname(tag)))
  134. def endElementNS(self, tag, qname):
  135. self.currtag = self.tagstack.pop()
  136. str = ''.join(self.data).strip()
  137. # Check that only elements that can take text have text
  138. # But only elements we know exist in grammar
  139. if tag in grammar.allowed_children:
  140. if str != '' and tag not in grammar.allows_text:
  141. print_error("Error: %s does not allow text data" % make_qname(tag))
  142. self.data = []
  143. class ODFDTDHandler(handler.DTDHandler):
  144. def notationDecl(self, name, public_id, system_id):
  145. """ Ignore DTDs """
  146. print_error("Warning: ODF doesn't use DOCTYPEs")
  147. def exitwithusage(exitcode=2):
  148. """ print out usage information """
  149. sys.stderr.write("Usage: %s inputfile\n" % sys.argv[0])
  150. sys.stderr.write("\tInputfile must be OpenDocument format\n")
  151. sys.exit(exitcode)
  152. def lint(odffile):
  153. if not zipfile.is_zipfile(odffile):
  154. print_error("Error: This is not a zipped file")
  155. return
  156. zfd = zipfile.ZipFile(odffile)
  157. try:
  158. mimetype = zfd.read('mimetype')
  159. except:
  160. mimetype=''
  161. d = OpenDocument(unicode(mimetype))
  162. first = True
  163. for zi in zfd.infolist():
  164. if first:
  165. if zi.filename == 'mimetype':
  166. if zi.compress_type != zipfile.ZIP_STORED:
  167. print_error("Error: The 'mimetype' member must be stored - not deflated")
  168. if zi.comment != "":
  169. print_error("Error: The 'mimetype' member must not have extra header info")
  170. else:
  171. print_error("Warning: The first member in the archive should be the mimetype")
  172. first = False
  173. if zi.filename in ('META-INF/manifest.xml', 'content.xml', 'meta.xml', 'styles.xml', 'settings.xml'):
  174. content = zfd.read(zi.filename)
  175. parser = make_parser()
  176. parser.setFeature(handler.feature_namespaces, True)
  177. parser.setFeature(handler.feature_external_ges, False)
  178. parser.setContentHandler(ODFElementHandler(d))
  179. dtdh = ODFDTDHandler()
  180. parser.setDTDHandler(dtdh)
  181. parser.setErrorHandler(handler.ErrorHandler())
  182. inpsrc = InputSource()
  183. if not isinstance(content, str):
  184. content=content
  185. inpsrc.setByteStream(BytesIO(content))
  186. parser.parse(inpsrc)
  187. if len(sys.argv) != 2:
  188. exitwithusage()
  189. lint(unicode(sys.argv[1]))
  190. # Local Variables: ***
  191. # mode: python ***
  192. # End: ***