testload.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. # Copyright (C) 2008-2010 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 unittest, os, os.path, sys
  21. from io import BytesIO
  22. from zipfile import ZipFile
  23. from odf.opendocument import OpenDocumentText, load
  24. from odf import style, text
  25. from odf.text import P, H, LineBreak
  26. from elementparser import ElementParser
  27. from defusedxml import EntitiesForbidden
  28. if sys.version_info[0]==3:
  29. unicode=str
  30. class TestSimple(unittest.TestCase):
  31. def setUp(self):
  32. textdoc = OpenDocumentText()
  33. p = P(text="Hello World!")
  34. textdoc.text.addElement(p)
  35. textdoc.save(u"TEST.odt")
  36. self.saved = True
  37. def tearDown(self):
  38. if self.saved:
  39. os.unlink(u"TEST.odt")
  40. def test_simple(self):
  41. """ Check that a simple load works """
  42. d = load(u"TEST.odt")
  43. result = d.contentxml() # contentxml() is supposed to yeld a bytes
  44. self.assertNotEqual(-1, result.find(b"""Hello World!"""))
  45. class TestHeadings(unittest.TestCase):
  46. saved = False
  47. def tearDown(self):
  48. if self.saved:
  49. os.unlink("TEST.odt")
  50. def test_headings(self):
  51. """ Create a document, save it and load it """
  52. textdoc = OpenDocumentText()
  53. textdoc.text.addElement(H(outlinelevel=1, text=u"Heading 1"))
  54. textdoc.text.addElement(P(text=u"Hello World!"))
  55. textdoc.text.addElement(H(outlinelevel=2, text=u"Heading 2"))
  56. textdoc.save(u"TEST.odt")
  57. self.saved = True
  58. d = load(u"TEST.odt")
  59. result = d.contentxml() # contentxml() is supposed to yeld a bytes
  60. self.assertNotEqual(-1, result.find(b"""<text:h text:outline-level="1">Heading 1</text:h><text:p>Hello World!</text:p><text:h text:outline-level="2">Heading 2</text:h>"""))
  61. def test_linebreak(self):
  62. """ Test that a line break (empty) element show correctly """
  63. textdoc = OpenDocumentText()
  64. p = P(text=u"Hello World!")
  65. textdoc.text.addElement(p)
  66. p.addElement(LineBreak())
  67. p.addText(u"Line 2")
  68. textdoc.save(u"TEST.odt")
  69. self.saved = True
  70. d = load(u"TEST.odt")
  71. result = d.contentxml() # contentxml() is supposed to yeld a bytes
  72. self.assertNotEqual(-1, result.find(b"""<text:p>Hello World!<text:line-break/>Line 2</text:p>"""))
  73. class BaseXMLVulnerabilities(unittest.TestCase):
  74. def _modify_zip_file(self, zip_path, filename, replacement_content=None):
  75. """
  76. Replace the contents of a file inside a ZIP-container. The original file
  77. will not be overwritten. A BytesIO() file will be returned containing
  78. the new ZIP-container with the file replaced.
  79. :param zip_path The path to the ZIP-File to be modified
  80. :param filename Filename inside the container to be modified
  81. :param replacement_content The new container which will be stored in `filename`
  82. :return BytesIO contain the modified zip file.
  83. """
  84. vuln_zipfile = BytesIO()
  85. zw = ZipFile(vuln_zipfile, 'w')
  86. zr = ZipFile(zip_path)
  87. names_to_copy = set(zr.namelist()) - {filename, }
  88. for name in names_to_copy:
  89. content = zr.read(name)
  90. zw.writestr(name, content)
  91. if replacement_content:
  92. zw.writestr(filename, replacement_content)
  93. zw.close()
  94. vuln_zipfile.seek(0)
  95. x = ZipFile(vuln_zipfile, 'r')
  96. self.assertEqual(x.testzip(), None)
  97. vuln_zipfile.seek(0)
  98. return vuln_zipfile
  99. def modify_zip_file(self, zip_path, filename, replacements):
  100. """
  101. Performan a list of replacments on `filename` the original file in
  102. `zip_path` will remain unaffected and a BytesIO object will be returned
  103. containing the new zip file.
  104. :param zip_path Path to the zip-file.
  105. :param filename Filename where the replacements will be performed on
  106. :param replacements An iterable containing replacements using two-tuples.
  107. e.g. [(orig_string, new_string), ]
  108. :return BytesIO contain the modified zip file.
  109. """
  110. zr = ZipFile(zip_path)
  111. original_content = zr.read(filename).decode('utf-8')
  112. new_content = original_content
  113. for old, new in replacements:
  114. new_content = new_content.replace(old, new)
  115. new_content = new_content.encode('utf-8')
  116. return self._modify_zip_file(zip_path, filename, replacement_content=new_content)
  117. class TestQuadraticBlowup(BaseXMLVulnerabilities):
  118. attack_entity = (
  119. '<?xml version="1.0" encoding="UTF-8"?>'
  120. '<!DOCTYPE bomb ['
  121. '<!ENTITY a "{loads_of_bs}">'
  122. ']>'
  123. ).format(loads_of_bs="B" * 100000)
  124. simple_ods = os.path.join(os.path.dirname(__file__), "examples", "empty.ods")
  125. loads_of_as = '&a;' * 1000
  126. def test_manifest_xml(self):
  127. """
  128. N.B. Haven't actually triggered this attack.
  129. """
  130. attack_file = 'META-INF/manifest.xml'
  131. replacements = (
  132. ('<?xml version="1.0" encoding="UTF-8"?>', self.attack_entity),
  133. ('manifest:full-path="/" manifest:version="1.2"',
  134. 'manifest:full-path="/" manifest:version="{loads_of_as}"'.format(loads_of_as=self.loads_of_as)),
  135. )
  136. new_zipfile = self.modify_zip_file(self.simple_ods, attack_file, replacements)
  137. self.assertRaises(EntitiesForbidden, load, new_zipfile)
  138. def test_settings_xml(self):
  139. attack_file = 'settings.xml'
  140. replacements = (
  141. ('<?xml version="1.0" encoding="UTF-8"?>', self.attack_entity),
  142. ('"VerticalSplitMode" config:type="short">0</config:config-item>',
  143. '"VerticalSplitMode" config:type="short">{loads_of_as}</config:config-item>'.format(
  144. loads_of_as=self.loads_of_as
  145. )),
  146. )
  147. new_zipfile = self.modify_zip_file(self.simple_ods, attack_file, replacements)
  148. self.assertRaises(EntitiesForbidden, load, new_zipfile)
  149. # To trigger:
  150. # d = load(new_zipfile)
  151. # print(d.settingsxml())
  152. def test_meta_xml(self):
  153. attack_file = 'meta.xml'
  154. replacements = (
  155. ('<?xml version="1.0" encoding="UTF-8"?>', self.attack_entity),
  156. ('<meta:creation-date>', '<meta:creation-date>{loads_of_as}'.format(
  157. loads_of_as=self.loads_of_as)),
  158. )
  159. new_zipfile = self.modify_zip_file(self.simple_ods, attack_file, replacements)
  160. self.assertRaises(EntitiesForbidden, load, new_zipfile)
  161. # To trigger:
  162. # d = load(new_zipfile)
  163. # print(d.metaxml())
  164. def test_content_xml(self):
  165. """
  166. N.B. Haven't actually triggered this attack.
  167. """
  168. attack_file = 'content.xml'
  169. replacements = (
  170. ('<?xml version="1.0" encoding="UTF-8"?>', self.attack_entity),
  171. ('style:name="Liberation Sans"', 'style:name="Liberation Sans {loads_of_as}"'.format(
  172. loads_of_as=self.loads_of_as)),
  173. )
  174. new_zipfile = self.modify_zip_file(self.simple_ods, attack_file, replacements)
  175. self.assertRaises(EntitiesForbidden, load, new_zipfile)
  176. # To trigger:
  177. # d = load(new_zipfile)
  178. # print(d.metaxml())
  179. def test_styles_xml(self):
  180. attack_file = 'styles.xml'
  181. replacements = (
  182. ('<?xml version="1.0" encoding="UTF-8"?>', self.attack_entity),
  183. ('<text:time>00:00:00</text:time>', '<text:time>{loads_of_as}; 00:00:00</text:time>'.format(
  184. loads_of_as=self.loads_of_as)),
  185. )
  186. new_zipfile = self.modify_zip_file(self.simple_ods, attack_file, replacements)
  187. self.assertRaises(EntitiesForbidden, load, new_zipfile)
  188. # To trigger:
  189. # d = load(new_zipfile)
  190. # print(d.stylesxml())
  191. class TestXMLBomb(BaseXMLVulnerabilities):
  192. """
  193. Tests various XML files inside the ODS container
  194. for the Billion laughs attack.
  195. """
  196. attack_entity = (
  197. '<?xml version="1.0" encoding="UTF-8"?>'
  198. '<!DOCTYPE xmlbomb ['
  199. '<!ENTITY a "1234567890" >'
  200. '<!ENTITY b "&a;&a;&a;&a;&a;&a;&a;&a;">'
  201. '<!ENTITY c "&b;&b;&b;&b;&b;&b;&b;&b;">'
  202. '<!ENTITY d "&c;&c;&c;&c;&c;&c;&c;&c;">'
  203. '<!ENTITY e "&d;&d;&d;&d;&d;&d;&d;&d;">'
  204. '<!ENTITY f "&e;&e;&e;&e;&e;&e;&e;&e;">'
  205. '<!ENTITY g "&f;&f;&f;&f;&f;&f;&f;&f;">'
  206. ']>'
  207. )
  208. simple_ods = os.path.join(os.path.dirname(__file__), "examples", "empty.ods")
  209. def test_manifest_xml(self):
  210. """
  211. N.B. Haven't actually triggered this attack.
  212. """
  213. attack_file = 'META-INF/manifest.xml'
  214. replacements = (
  215. ('<?xml version="1.0" encoding="UTF-8"?>', self.attack_entity),
  216. ('manifest:full-path="/" manifest:version="1.2"',
  217. 'manifest:full-path="/" manifest:version="&g;"'),
  218. )
  219. new_zipfile = self.modify_zip_file(self.simple_ods, attack_file, replacements)
  220. self.assertRaises(EntitiesForbidden, load, new_zipfile)
  221. def test_settings_xml(self):
  222. attack_file = 'settings.xml'
  223. replacements = (
  224. ('<?xml version="1.0" encoding="UTF-8"?>', self.attack_entity),
  225. ('<config:config-item config:name="VerticalSplitMode" config:type="short">0</config:config-item>',
  226. '<config:config-item config:name="VerticalSplitMode" config:type="short">&g;</config:config-item>'),
  227. )
  228. new_zipfile = self.modify_zip_file(self.simple_ods, attack_file, replacements)
  229. self.assertRaises(EntitiesForbidden, load, new_zipfile)
  230. # To trigger:
  231. # d = load(new_zipfile)
  232. # print(d.settingsxml())
  233. def test_meta_xml(self):
  234. attack_file = 'meta.xml'
  235. replacements = (
  236. ('<?xml version="1.0" encoding="UTF-8"?>', self.attack_entity),
  237. ('<meta:creation-date>', '<meta:creation-date>xx&g;'),
  238. )
  239. new_zipfile = self.modify_zip_file(self.simple_ods, attack_file, replacements)
  240. self.assertRaises(EntitiesForbidden, load, new_zipfile)
  241. # To trigger:
  242. # d = load(new_zipfile)
  243. # print(d.metaxml())
  244. def test_content_xml(self):
  245. """
  246. N.B. Haven't actually triggered this attack.
  247. """
  248. attack_file = 'content.xml'
  249. replacements = (
  250. ('<?xml version="1.0" encoding="UTF-8"?>', self.attack_entity),
  251. ('style:name="Liberation Sans"', 'style:name="Liberation Sans &g;"'),
  252. )
  253. new_zipfile = self.modify_zip_file(self.simple_ods, attack_file, replacements)
  254. self.assertRaises(EntitiesForbidden, load, new_zipfile)
  255. def test_styles_xml(self):
  256. """
  257. N.B. Haven't actually triggered this attack.
  258. """
  259. attack_file = 'styles.xml'
  260. replacements = (
  261. ('<?xml version="1.0" encoding="UTF-8"?>', self.attack_entity),
  262. ('<text:time>00:00:00</text:time>', '<text:time>&g; 00:00:00</text:time>'),
  263. )
  264. new_zipfile = self.modify_zip_file(self.simple_ods, attack_file, replacements)
  265. self.assertRaises(EntitiesForbidden, load, new_zipfile)
  266. # To trigger:
  267. # d = load(new_zipfile)
  268. # print(d.stylesxml())
  269. class TestXXEFile(BaseXMLVulnerabilities):
  270. attack_entity = (
  271. '<?xml version="1.0" encoding="UTF-8"?>'
  272. '<!DOCTYPE external ['
  273. '<!ENTITY ee SYSTEM "/etc/passwd">'
  274. ']>'
  275. )
  276. simple_ods = os.path.join(os.path.dirname(__file__), "examples", "empty.ods")
  277. def test_manifest_xml(self):
  278. """
  279. N.B. Haven't actually triggered this attack.
  280. """
  281. attack_file = 'META-INF/manifest.xml'
  282. replacements = (
  283. ('<?xml version="1.0" encoding="UTF-8"?>', self.attack_entity),
  284. ('manifest:full-path="/" manifest:version="1.2"',
  285. 'manifest:full-path="/" manifest:version="&ee;"')
  286. )
  287. new_zipfile = self.modify_zip_file(self.simple_ods, attack_file, replacements)
  288. self.assertRaises(EntitiesForbidden, load, new_zipfile)
  289. def test_settings_xml(self):
  290. """
  291. N.B. Haven't actually triggered this attack.
  292. """
  293. attack_file = 'settings.xml'
  294. replacements = (
  295. ('<?xml version="1.0" encoding="UTF-8"?>', self.attack_entity),
  296. ('config:name="ActiveTable" config:type="string">',
  297. 'config:name="ActiveTable" config:type="string">&xxe;')
  298. )
  299. new_zipfile = self.modify_zip_file(self.simple_ods, attack_file, replacements)
  300. self.assertRaises(EntitiesForbidden, load, new_zipfile)
  301. # To trigger:
  302. # d = load(new_zipfile)
  303. # print(d.settingsxml())
  304. def test_meta_xml(self):
  305. """
  306. N.B. Haven't actually triggered this attack.
  307. """
  308. attack_file = 'meta.xml'
  309. replacements = (
  310. ('<?xml version="1.0" encoding="UTF-8"?>', self.attack_entity),
  311. ('<meta:creation-date>', '<meta:creation-date>&xxe;')
  312. )
  313. new_zipfile = self.modify_zip_file(self.simple_ods, attack_file, replacements)
  314. self.assertRaises(EntitiesForbidden, load, new_zipfile)
  315. # To trigger:
  316. # d = load(new_zipfile)
  317. # print(d.metaxml())
  318. def test_content_xml(self):
  319. """
  320. N.B. Haven't actually triggered this attack.
  321. """
  322. attack_file = 'content.xml'
  323. replacements = (
  324. ('<?xml version="1.0" encoding="UTF-8"?>', self.attack_entity),
  325. ('style:name="Liberation Sans"', 'style:name="Liberation Sans &g;"'),
  326. )
  327. new_zipfile = self.modify_zip_file(self.simple_ods, attack_file, replacements)
  328. self.assertRaises(EntitiesForbidden, load, new_zipfile)
  329. # To trigger:
  330. # d = load(new_zipfile)
  331. # print(d.metaxml())
  332. def test_styles_xml(self):
  333. """
  334. N.B. Haven't actually triggered this attack.
  335. """
  336. attack_file = 'styles.xml'
  337. replacements = (
  338. ('<?xml version="1.0" encoding="UTF-8"?>', self.attack_entity),
  339. ('<text:time>00:00:00</text:time>', '<text:time>&ee; 00:00:00</text:time>'),
  340. )
  341. new_zipfile = self.modify_zip_file(self.simple_ods, attack_file, replacements)
  342. self.assertRaises(EntitiesForbidden, load, new_zipfile)
  343. # To trigger:
  344. # d = load(new_zipfile)
  345. # print(d.stylesxml())
  346. class TestXXE(BaseXMLVulnerabilities):
  347. attack_entity = (
  348. '<?xml version="1.0" encoding="UTF-8"?>'
  349. '<!DOCTYPE test [ '
  350. '<!ENTITY % one SYSTEM "http://127.0.0.1:8100/x.xml" >'
  351. '%one;'
  352. ']>'
  353. )
  354. simple_ods = os.path.join(os.path.dirname(__file__), "examples", "empty.ods")
  355. def test_manifest_xml(self):
  356. attack_file = 'META-INF/manifest.xml'
  357. replacements = (
  358. ('<?xml version="1.0" encoding="UTF-8"?>', self.attack_entity),
  359. )
  360. new_zipfile = self.modify_zip_file(self.simple_ods, attack_file, replacements)
  361. self.assertRaises(EntitiesForbidden, load, new_zipfile)
  362. def test_settings_xml(self):
  363. """
  364. Was caught by
  365. parser.setFeature(handler.feature_external_ges, 0)
  366. """
  367. attack_file = 'settings.xml'
  368. replacements = (
  369. ('<?xml version="1.0" encoding="UTF-8"?>', self.attack_entity),
  370. )
  371. new_zipfile = self.modify_zip_file(self.simple_ods, attack_file, replacements)
  372. self.assertRaises(EntitiesForbidden, load, new_zipfile)
  373. def test_meta_xml(self):
  374. """
  375. Was caught by
  376. parser.setFeature(handler.feature_external_ges, 0)
  377. """
  378. attack_file = 'meta.xml'
  379. replacements = (
  380. ('<?xml version="1.0" encoding="UTF-8"?>', self.attack_entity),
  381. )
  382. new_zipfile = self.modify_zip_file(self.simple_ods, attack_file, replacements)
  383. self.assertRaises(EntitiesForbidden, load, new_zipfile)
  384. def test_content_xml(self):
  385. """
  386. Was caught by
  387. parser.setFeature(handler.feature_external_ges, 0)
  388. """
  389. attack_file = 'content.xml'
  390. replacements = (
  391. ('<?xml version="1.0" encoding="UTF-8"?>', self.attack_entity),
  392. )
  393. new_zipfile = self.modify_zip_file(self.simple_ods, attack_file, replacements)
  394. self.assertRaises(EntitiesForbidden, load, new_zipfile)
  395. def test_styles_xml(self):
  396. """
  397. Was caught by
  398. parser.setFeature(handler.feature_external_ges, 0)
  399. """
  400. attack_file = 'styles.xml'
  401. replacements = (
  402. ('<?xml version="1.0" encoding="UTF-8"?>', self.attack_entity),
  403. )
  404. new_zipfile = self.modify_zip_file(self.simple_ods, attack_file, replacements)
  405. self.assertRaises(EntitiesForbidden, load, new_zipfile)
  406. class TestExampleDocs(unittest.TestCase):
  407. def test_metagenerator(self):
  408. """ Check that meta:generator is the original one """
  409. parastyles_odt = os.path.join(
  410. os.path.dirname(__file__), "examples", "parastyles.odt")
  411. d = load(parastyles_odt)
  412. meta = d.metaxml()
  413. self.assertEqual(-1, meta.find(u"""<meta:generator>OpenOffice.org/2.3$Linux OpenOffice.org_project/680m6$Build-9226"""),"Must use the original generator string")
  414. def test_metagenerator_odp(self):
  415. """ Check that meta:generator is the original one """
  416. parastyles_odt = os.path.join(
  417. os.path.dirname(__file__), u"examples", u"emb_spreadsheet.odp")
  418. d = load(parastyles_odt)
  419. meta = d.metaxml()
  420. self.assertNotEqual(-1, meta.find(u"""<meta:generator>ODFPY"""), "Must not use the original generator string")
  421. def test_simplelist(self):
  422. """ Check that lists are loaded correctly """
  423. simplelist_odt = os.path.join(
  424. os.path.dirname(__file__), "examples", "simplelist.odt")
  425. d = load(simplelist_odt)
  426. result = unicode(d.contentxml(),'utf-8')
  427. self.assertNotEqual(-1, result.find(u"""<text:list text:style-name="L1"><text:list-item><text:p text:style-name="P1">Item A</text:p></text:list-item><text:list-item>"""))
  428. def test_simpletable(self):
  429. """ Load a document containing tables """
  430. simpletable_odt = os.path.join(
  431. os.path.dirname(__file__), "examples", "simpletable.odt")
  432. d = load(simpletable_odt)
  433. result = unicode(d.contentxml(),'utf-8')
  434. e = ElementParser(result,'text:sequence-decl')
  435. self.assertTrue(e.has_value("text:name","Drawing")) # Last sequence
  436. self.assertTrue(e.has_value("text:display-outline-level","0"))
  437. e = ElementParser(result,'table:table-column')
  438. self.assertTrue(e.has_value("table:number-columns-repeated","2"))
  439. self.assertTrue(e.has_value("table:style-name","Tabel1.A"))
  440. def test_headerfooter(self):
  441. """ Test that styles referenced from master pages are renamed in OOo 2.x documents """
  442. simplelist_odt = os.path.join(
  443. os.path.dirname(__file__), "examples", "headerfooter.odt")
  444. d = load(simplelist_odt)
  445. result = d.stylesxml()
  446. self.assertNotEqual(-1, result.find(u'''style:name="MP1"'''))
  447. self.assertNotEqual(-1, result.find(u'''style:name="MP2"'''))
  448. self.assertNotEqual(-1, result.find(u"""<style:header><text:p text:style-name="MP1">Header<text:tab/>"""))
  449. self.assertNotEqual(-1, result.find(u"""<style:footer><text:p text:style-name="MP2">Footer<text:tab/>"""))
  450. def test_formulas_ooo(self):
  451. """ Check that formula prefixes are preserved """
  452. pythagoras_odt = os.path.join(
  453. os.path.dirname(__file__), "examples", "pythagoras.ods")
  454. d = load(pythagoras_odt)
  455. result = unicode(d.contentxml(),'utf-8')
  456. self.assertNotEqual(-1, result.find(u'''xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2"'''))
  457. self.assertNotEqual(-1, result.find(u'''table:formula="of:=SQRT([.A1]*[.A1]+[.A2]*[.A2])"'''))
  458. self.assertNotEqual(-1, result.find(u'''table:formula="of:=SUM([.A1:.A2])"'''))
  459. def test_formulas_ooo(self):
  460. """ Check that formulas are understood when there are no prefixes"""
  461. pythagoras_odt = os.path.join(
  462. os.path.dirname(__file__), "examples", "pythagoras-kspread.ods")
  463. d = load(pythagoras_odt)
  464. result = unicode(d.contentxml(),'utf-8')
  465. self.assertNotEqual(-1, result.find(u'''table:formula="=SQRT([.A1]*[.A1]+[.A2]*[.A2])"'''))
  466. self.assertNotEqual(-1, result.find(u'''table:formula="=SUM([.A1]:[.A2])"'''))
  467. def test_externalent(self):
  468. """ Check that external entities are not loaded """
  469. simplelist_odt = os.path.join(
  470. os.path.dirname(__file__), "examples", "nasty.odt")
  471. self.assertRaises(EntitiesForbidden, load, simplelist_odt)
  472. def test_spreadsheet(self):
  473. """ Load a document containing subobjects """
  474. spreadsheet_odt = os.path.join(
  475. os.path.dirname(__file__), u"examples", u"emb_spreadsheet.odp")
  476. d = load(spreadsheet_odt)
  477. self.assertEqual(1, len(d.childobjects))
  478. # for s in d.childobjects:
  479. # print (s.folder)
  480. # mani = unicode(d.manifestxml(),'utf-8')
  481. # self.assertNotEqual(-1, mani.find(u''' manifest:full-path="Object 1/"'''), "Must contain the subobject")
  482. # self.assertNotEqual(-1, mani.find(u''' manifest:full-path="Object 1/settings.xml"'''), "Must contain the subobject settings.xml")
  483. # d.save("subobject.odp")
  484. def test_chinese(self):
  485. """ Load a document containing Chinese content"""
  486. chinese_spreadsheet = os.path.join(
  487. os.path.dirname(__file__), u"examples", u"chinese_spreadsheet.ods")
  488. d = load(chinese_spreadsheet)
  489. result = unicode(d.contentxml(),'utf-8')
  490. self.assertNotEqual(-1, result.find(u'''工作表1'''))
  491. if __name__ == '__main__':
  492. unittest.main()