cms.py 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313
  1. # -*- coding: utf-8 -*-
  2. #
  3. # cms.py - simple WSGI/Python based CMS script
  4. #
  5. # Copyright (C) 2011-2016 Michael Buesch <m@bues.ch>
  6. #
  7. # This program is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 2 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. import sys
  20. if sys.version_info[0] < 3 or sys.version_info[1] < 3:
  21. raise Exception("Need Python 3.3 or later")
  22. import os
  23. from stat import S_ISDIR
  24. from datetime import datetime
  25. import re
  26. import PIL.Image as Image
  27. from io import BytesIO
  28. import urllib.request, urllib.parse, urllib.error
  29. import cgi
  30. from functools import reduce
  31. import random
  32. import importlib.machinery
  33. UPPERCASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  34. LOWERCASE = 'abcdefghijklmnopqrstuvwxyz'
  35. NUMBERS = '0123456789'
  36. # Find the index in 'string' that is _not_ in 'template'.
  37. # Start search at 'idx'.
  38. # Returns -1 on failure to find.
  39. def findNot(string, template, idx=0):
  40. while idx < len(string):
  41. if string[idx] not in template:
  42. return idx
  43. idx += 1
  44. return -1
  45. # Find the index in 'string' that matches _any_ character in 'template'.
  46. # Start search at 'idx'.
  47. # Returns -1 on failure to find.
  48. def findAny(string, template, idx=0):
  49. while idx < len(string):
  50. if string[idx] in template:
  51. return idx
  52. idx += 1
  53. return -1
  54. def htmlEscape(string):
  55. return cgi.escape(string, True)
  56. def stringBool(string, default=False):
  57. s = string.lower()
  58. if s in ("true", "yes", "on"):
  59. return True
  60. if s in ("false", "no", "off"):
  61. return False
  62. try:
  63. return bool(int(s, 10))
  64. except ValueError:
  65. return default
  66. # Create a path string from path element strings.
  67. def mkpath(*path_elements):
  68. # Do not use os.path.join, because it discards elements, if
  69. # one element begins with a separator (= is absolute).
  70. return os.path.sep.join(path_elements)
  71. def f_exists(*path_elements):
  72. try:
  73. os.stat(mkpath(*path_elements))
  74. except OSError:
  75. return False
  76. return True
  77. def f_exists_nonempty(*path_elements):
  78. if f_exists(*path_elements):
  79. return bool(f_read(*path_elements).strip())
  80. return False
  81. def f_read(*path_elements):
  82. try:
  83. with open(mkpath(*path_elements), "rb") as fd:
  84. return fd.read().decode("UTF-8")
  85. except IOError:
  86. return ""
  87. except UnicodeError:
  88. raise CMSException(500, "Unicode decode error")
  89. def f_read_int(*path_elements):
  90. data = f_read(*path_elements)
  91. try:
  92. return int(data.strip(), 10)
  93. except ValueError:
  94. return None
  95. def f_mtime(*path_elements):
  96. try:
  97. return datetime.utcfromtimestamp(os.stat(mkpath(*path_elements)).st_mtime)
  98. except OSError:
  99. raise CMSException(404)
  100. def f_mtime_nofail(*path_elements):
  101. try:
  102. return f_mtime(*path_elements)
  103. except CMSException:
  104. return datetime.utcnow()
  105. def f_subdirList(*path_elements):
  106. def dirfilter(dentry):
  107. if dentry.startswith("."):
  108. return False # Omit ".", ".." and hidden entries
  109. if dentry.startswith("__"):
  110. return False # Omit system folders/files.
  111. try:
  112. if not S_ISDIR(os.stat(mkpath(path, dentry)).st_mode):
  113. return False
  114. except OSError:
  115. return False
  116. return True
  117. path = mkpath(*path_elements)
  118. try:
  119. return [ dentry for dentry in os.listdir(path) \
  120. if dirfilter(dentry) ]
  121. except OSError:
  122. return []
  123. class CMSPageIdent(list):
  124. # Page identifier.
  125. __pageFileName_re = re.compile(
  126. r'^(.*)((?:\.html?)|(?:\.py)|(?:\.php))$', re.DOTALL)
  127. __indexPages = {"", "index"}
  128. # Parse a page identifier from a string.
  129. @classmethod
  130. def parse(cls, path, maxPathLen = 512, maxIdentDepth = 32):
  131. if len(path) > maxPathLen:
  132. raise CMSException(400, "Invalid URL")
  133. pageIdent = cls()
  134. # Strip whitespace and slashes
  135. path = path.strip(' \t/')
  136. # Remove page file extensions like .html and such.
  137. m = cls.__pageFileName_re.match(path)
  138. if m:
  139. path = m.group(1)
  140. # Use the ident elements, if this is not the root page.
  141. if path not in cls.__indexPages:
  142. pageIdent.extend(path.split("/"))
  143. if len(pageIdent) > maxIdentDepth:
  144. raise CMSException(400, "Invalid URL")
  145. return pageIdent
  146. __pathSep = os.path.sep
  147. __validPathChars = LOWERCASE + UPPERCASE + NUMBERS + "-_."
  148. # Validate a path component. Avoid any directory change.
  149. # Raises CMSException on failure.
  150. @classmethod
  151. def validateSafePathComponent(cls, pcomp):
  152. if pcomp.startswith('.'):
  153. # No ".", ".." and hidden files.
  154. raise CMSException(404, "Invalid page path")
  155. if [ c for c in pcomp if c not in cls.__validPathChars ]:
  156. raise CMSException(404, "Invalid page path")
  157. return pcomp
  158. # Validate a path. Avoid going back in the hierarchy (. and ..)
  159. # Raises CMSException on failure.
  160. @classmethod
  161. def validateSafePath(cls, path):
  162. for pcomp in path.split(cls.__pathSep):
  163. cls.validateSafePathComponent(pcomp)
  164. return path
  165. # Validate a page name.
  166. # Raises CMSException on failure.
  167. # If allowSysNames is True, system names starting with "__" are allowed.
  168. @classmethod
  169. def validateName(cls, name, allowSysNames = False):
  170. if name.startswith("__") and not allowSysNames:
  171. # Page names with __ are system folders.
  172. raise CMSException(404, "Invalid page name")
  173. return cls.validateSafePathComponent(name)
  174. def __init__(self, *args):
  175. list.__init__(self, *args)
  176. self.__allValidated = False
  177. # Validate all page identifier name components.
  178. # (Do not allow system name components)
  179. def __validateAll(self):
  180. if not self.__allValidated:
  181. for pcomp in self:
  182. self.validateName(pcomp)
  183. # Remember that we validated.
  184. # Note that this assumes no components are added later!
  185. self.__allValidated = True
  186. # Get one page identifier component by index.
  187. def get(self, index, default = None, allowSysNames = False):
  188. try:
  189. return self.validateName(self[index],
  190. allowSysNames)
  191. except IndexError:
  192. return default
  193. # Get the page identifier as URL.
  194. def getUrl(self, protocol = None, domain = None,
  195. urlBase = None, pageSuffix = ".html"):
  196. self.__validateAll()
  197. url = []
  198. if protocol:
  199. url.append(protocol + ":/")
  200. if domain:
  201. url.append(domain)
  202. if urlBase:
  203. url.append(urlBase.strip("/"))
  204. url.extend(self)
  205. if not protocol and not domain:
  206. url.insert(0, "")
  207. url = "/".join(url)
  208. if self and pageSuffix:
  209. url += pageSuffix
  210. return url
  211. # Get the page identifier as filesystem path.
  212. def getFilesystemPath(self, rstrip = 0):
  213. self.__validateAll()
  214. if self:
  215. if rstrip:
  216. pcomps = self[ : 0 - rstrip]
  217. if pcomps:
  218. return mkpath(*pcomps)
  219. return ""
  220. return mkpath(*self)
  221. return ""
  222. # Test if this identifier starts with the same elements
  223. # as another one.
  224. def startswith(self, other):
  225. return other is not None and\
  226. len(self) >= len(other) and\
  227. self[ : len(other)] == other
  228. class CMSException(Exception):
  229. __stats = {
  230. 301 : "Moved Permanently",
  231. 400 : "Bad Request",
  232. 404 : "Not Found",
  233. 405 : "Method Not Allowed",
  234. 409 : "Conflict",
  235. 500 : "Internal Server Error",
  236. }
  237. def __init__(self, httpStatusCode=500, message=""):
  238. try:
  239. httpStatus = self.__stats[httpStatusCode]
  240. except KeyError:
  241. httpStatusCode = 500
  242. httpStatus = self.__stats[httpStatusCode]
  243. self.httpStatusCode = httpStatusCode
  244. self.httpStatus = "%d %s" % (httpStatusCode, httpStatus)
  245. self.message = message
  246. def getHttpHeaders(self, resolveCallback):
  247. return ()
  248. def getHtmlHeader(self, db):
  249. return ""
  250. def getHtmlBody(self, db):
  251. return db.getString('http-error-page',
  252. '<p style="font-size: large;">%s</p>' %\
  253. self.httpStatus)
  254. class CMSException301(CMSException):
  255. # "Moved Permanently" exception
  256. def __init__(self, newUrl):
  257. CMSException.__init__(self, 301, newUrl)
  258. def url(self):
  259. return self.message
  260. def getHttpHeaders(self, resolveCallback):
  261. return ( ('Location', resolveCallback(self.url())), )
  262. def getHtmlHeader(self, db):
  263. return '<meta http-equiv="refresh" content="0; URL=%s" />' %\
  264. self.url()
  265. def getHtmlBody(self, db):
  266. return '<p style="font-size: large;">' \
  267. 'Moved permanently to ' \
  268. '<a href="%s">%s</a>' \
  269. '</p>' %\
  270. (self.url(), self.url())
  271. class CMSDatabase(object):
  272. validate = CMSPageIdent.validateName
  273. def __init__(self, basePath):
  274. self.pageBase = mkpath(basePath, "pages")
  275. self.macroBase = mkpath(basePath, "macros")
  276. self.stringBase = mkpath(basePath, "strings")
  277. def __redirect(self, redirectString):
  278. raise CMSException301(redirectString)
  279. def __getPageTitle(self, pagePath):
  280. title = f_read(pagePath, "title").strip()
  281. if not title:
  282. title = f_read(pagePath, "nav_label").strip()
  283. return title
  284. def getNavStop(self, pageIdent):
  285. path = mkpath(self.pageBase, pageIdent.getFilesystemPath())
  286. return bool(f_read_int(path, "nav_stop"))
  287. def getHeader(self, pageIdent):
  288. path = mkpath(self.pageBase, pageIdent.getFilesystemPath())
  289. return f_read(path, "header.html")
  290. def getPage(self, pageIdent):
  291. path = mkpath(self.pageBase, pageIdent.getFilesystemPath())
  292. redirect = f_read(path, "redirect").strip()
  293. if redirect:
  294. return self.__redirect(redirect)
  295. title = self.__getPageTitle(path)
  296. data = f_read(path, "content.html")
  297. stamp = f_mtime_nofail(path, "content.html")
  298. return (title, data, stamp)
  299. def getPageTitle(self, pageIdent):
  300. path = mkpath(self.pageBase, pageIdent.getFilesystemPath())
  301. return self.__getPageTitle(path)
  302. # Get a list of sub-pages.
  303. # Returns list of (pagename, navlabel, prio)
  304. def getSubPages(self, pageIdent, sortByPrio = True):
  305. res = []
  306. gpath = mkpath(self.pageBase, pageIdent.getFilesystemPath())
  307. for pagename in f_subdirList(gpath):
  308. path = mkpath(gpath, pagename)
  309. if f_exists(path, "hidden") or \
  310. f_exists_nonempty(path, "redirect"):
  311. continue
  312. navlabel = f_read(path, "nav_label").strip()
  313. prio = f_read_int(path, "priority")
  314. if prio is None:
  315. prio = 500
  316. res.append( (pagename, navlabel, prio) )
  317. if sortByPrio:
  318. res.sort(key = lambda e: "%010d_%s" % (e[2], e[1]))
  319. return res
  320. def getMacro(self, macroname, pageIdent = None):
  321. data = None
  322. macroname = self.validate(macroname)
  323. if pageIdent:
  324. rstrip = 0
  325. while not data:
  326. path = pageIdent.getFilesystemPath(rstrip)
  327. if not path:
  328. break
  329. data = f_read(self.pageBase,
  330. path,
  331. "__macros",
  332. macroname)
  333. rstrip += 1
  334. if not data:
  335. data = f_read(self.pageBase,
  336. "__macros",
  337. macroname)
  338. if not data:
  339. data = f_read(self.macroBase, macroname)
  340. return '\n'.join( l for l in data.splitlines() if l )
  341. def getString(self, name, default=None):
  342. name = self.validate(name)
  343. string = f_read(self.stringBase, name).strip()
  344. if string:
  345. return string
  346. return name if default is None else default
  347. def getPostHandler(self, pageIdent):
  348. path = mkpath(self.pageBase, pageIdent.getFilesystemPath())
  349. handlerModFile = mkpath(path, "post.py")
  350. if not f_exists(handlerModFile):
  351. return None
  352. try:
  353. loader = importlib.machinery.SourceFileLoader(
  354. re.sub(r"[^A-Za-z]", "_", handlerModFile),
  355. handlerModFile)
  356. mod = loader.load_module()
  357. except OSError:
  358. return None
  359. if not hasattr(mod, "post"):
  360. return None
  361. return mod
  362. class CMSStatementResolver(object):
  363. # Macro argument expansion: $1, $2, $3...
  364. macro_arg_re = re.compile(r'\$(\d+)', re.DOTALL)
  365. # Valid characters for variable names (without the leading $)
  366. VARNAME_CHARS = UPPERCASE + '_'
  367. __genericVars = {
  368. "DOMAIN" : lambda self, n: self.cms.domain,
  369. "CMS_BASE" : lambda self, n: self.cms.urlBase,
  370. "IMAGES_DIR" : lambda self, n: self.cms.imagesDir,
  371. "THUMBS_DIR" : lambda self, n: self.cms.urlBase + "/__thumbs",
  372. "DEBUG" : lambda self, n: "1" if self.cms.debug else "",
  373. "__DUMPVARS__" : lambda self, n: self.__dumpVars(),
  374. }
  375. class StackElem(object): # Call stack element
  376. def __init__(self, name):
  377. self.name = name
  378. self.lineno = 1
  379. class IndexRef(object): # Index references
  380. def __init__(self, charOffset):
  381. self.charOffset = charOffset
  382. class Anchor(object): # Anchor
  383. def __init__(self, name, text,
  384. indent=-1, noIndex=False):
  385. self.name = name
  386. self.text = text
  387. self.indent = indent
  388. self.noIndex = noIndex
  389. def makeUrl(self, resolver):
  390. return "%s#%s" % (
  391. CMSPageIdent((
  392. resolver.expandVariable("GROUP"),
  393. resolver.expandVariable("PAGE"))).getUrl(
  394. urlBase = resolver.cms.urlBase),
  395. self.name)
  396. def __init__(self, cms):
  397. self.cms = cms
  398. self.__reset()
  399. def __reset(self, variables = {}, pageIdent = None):
  400. self.variables = variables.copy()
  401. self.variables.update(self.__genericVars)
  402. self.pageIdent = pageIdent
  403. self.callStack = [ self.StackElem("content.html") ]
  404. self.charCount = 0
  405. self.indexRefs = []
  406. self.anchors = []
  407. def __stmtError(self, msg):
  408. pfx = ""
  409. if self.cms.debug:
  410. pfx = "%s:%d: " %\
  411. (self.callStack[-1].name,
  412. self.callStack[-1].lineno)
  413. raise CMSException(500, pfx + msg)
  414. def expandVariable(self, name):
  415. try:
  416. value = self.variables[name]
  417. try:
  418. value = value(self, name)
  419. except (TypeError) as e:
  420. pass
  421. return str(value)
  422. except (KeyError, TypeError) as e:
  423. return ""
  424. def __dumpVars(self, force=False):
  425. if not force and not self.cms.debug:
  426. return ""
  427. ret = []
  428. for name in sorted(self.variables.keys()):
  429. if name == "__DUMPVARS__":
  430. value = "-- variable dump --"
  431. else:
  432. value = self.expandVariable(name)
  433. sep = "\t" * (3 - len(name) // 8)
  434. ret.append("%s%s=> %s" % (name, sep, value))
  435. return "\n".join(ret)
  436. __escapedChars = ('\\', ',', '@', '$', '(', ')')
  437. @classmethod
  438. def escape(cls, data):
  439. for c in cls.__escapedChars:
  440. data = data.replace(c, '\\' + c)
  441. return data
  442. @classmethod
  443. def unescape(cls, data):
  444. for c in cls.__escapedChars:
  445. data = data.replace('\\' + c, c)
  446. return data
  447. # Parse statement arguments.
  448. # Returns (consumed-characters-count, arguments) tuple.
  449. def __parseArguments(self, d, strip=False):
  450. arguments, cons = [], 0
  451. while cons < len(d):
  452. c, arg = self.__expandRecStmts(d[cons:], ',)')
  453. cons += c
  454. arguments.append(arg.strip() if strip else arg)
  455. if cons <= 0 or d[cons - 1] == ')':
  456. break
  457. return cons, arguments
  458. # Statement: $(if CONDITION, THEN, ELSE)
  459. # Statement: $(if CONDITION, THEN)
  460. # Returns THEN if CONDITION is nonempty after stripping whitespace.
  461. # Returns ELSE otherwise.
  462. def __stmt_if(self, d):
  463. cons, args = self.__parseArguments(d)
  464. if len(args) != 2 and len(args) != 3:
  465. self.__stmtError("IF: invalid number of arguments (%d)" %\
  466. len(args))
  467. condition, b_then = args[0], args[1]
  468. b_else = args[2] if len(args) == 3 else ""
  469. result = b_then if condition.strip() else b_else
  470. return cons, result
  471. def __do_compare(self, d, invert):
  472. cons, args = self.__parseArguments(d, strip=True)
  473. result = reduce(lambda a, b: a and b == args[0],
  474. args[1:], True)
  475. result = not result if invert else result
  476. return cons, (args[-1] if result else "")
  477. # Statement: $(eq A, B, ...)
  478. # Returns the last argument, if all stripped arguments are equal.
  479. # Returns an empty string otherwise.
  480. def __stmt_eq(self, d):
  481. return self.__do_compare(d, False)
  482. # Statement: $(ne A, B, ...)
  483. # Returns the last argument, if not all stripped arguments are equal.
  484. # Returns an empty string otherwise.
  485. def __stmt_ne(self, d):
  486. return self.__do_compare(d, True)
  487. # Statement: $(and A, B, ...)
  488. # Returns A, if all stripped arguments are non-empty strings.
  489. # Returns an empty string otherwise.
  490. def __stmt_and(self, d):
  491. cons, args = self.__parseArguments(d, strip=True)
  492. return cons, (args[0] if all(args) else "")
  493. # Statement: $(or A, B, ...)
  494. # Returns the first stripped non-empty argument.
  495. # Returns an empty string, if there is no non-empty argument.
  496. def __stmt_or(self, d):
  497. cons, args = self.__parseArguments(d, strip=True)
  498. nonempty = [ a for a in args if a ]
  499. return cons, (nonempty[0] if nonempty else "")
  500. # Statement: $(not A)
  501. # Returns 1, if A is an empty string after stripping.
  502. # Returns an empty string, if A is a non-empty stripped string.
  503. def __stmt_not(self, d):
  504. cons, args = self.__parseArguments(d, strip=True)
  505. if len(args) != 1:
  506. self.__stmtError("NOT: invalid args")
  507. return cons, ("" if args[0] else "1")
  508. # Statement: $(assert A, ...)
  509. # Raises a 500-assertion-failed exception, if any argument
  510. # is empty after stripping.
  511. # Returns an empty string, otherwise.
  512. def __stmt_assert(self, d):
  513. cons, args = self.__parseArguments(d, strip=True)
  514. if not all(args):
  515. self.__stmtError("ASSERT: failed")
  516. return cons, ""
  517. # Statement: $(strip STRING)
  518. # Strip whitespace at the start and at the end of the string.
  519. def __stmt_strip(self, d):
  520. cons, args = self.__parseArguments(d, strip=True)
  521. return cons, "".join(args)
  522. # Statement: $(item STRING, N)
  523. # Statement: $(item STRING, N, SEPARATOR)
  524. # Split a string into tokens and return the N'th token.
  525. # SEPARATOR defaults to whitespace.
  526. def __stmt_item(self, d):
  527. cons, args = self.__parseArguments(d)
  528. if len(args) not in {2, 3}:
  529. self.__stmtError("ITEM: invalid args")
  530. string, n, sep = args[0], args[1], args[2].strip() if len(args) == 3 else ""
  531. tokens = string.split(sep) if sep else string.split()
  532. try:
  533. token = tokens[int(n)]
  534. except ValueError:
  535. self.__stmtError("ITEM: N is not an integer")
  536. except IndexError:
  537. token = ""
  538. return cons, token
  539. # Statement: $(sanitize STRING)
  540. # Sanitize a string.
  541. # Replaces all non-alphanumeric characters by an underscore. Forces lower-case.
  542. def __stmt_sanitize(self, d):
  543. cons, args = self.__parseArguments(d)
  544. string = "_".join(args)
  545. validChars = LOWERCASE + NUMBERS
  546. string = string.lower()
  547. string = "".join( c if c in validChars else '_' for c in string )
  548. string = re.sub(r'_+', '_', string).strip('_')
  549. return cons, string
  550. # Statement: $(file_exists RELATIVE_PATH)
  551. # Statement: $(file_exists RELATIVE_PATH, DOES_NOT_EXIST)
  552. # Checks if a file exists relative to the wwwPath base.
  553. # Returns the path, if the file exists or an empty string if it doesn't.
  554. # If DOES_NOT_EXIST is specified, it returns this if the file doesn't exist.
  555. def __stmt_fileExists(self, d):
  556. cons, args = self.__parseArguments(d)
  557. if len(args) != 1 and len(args) != 2:
  558. self.__stmtError("FILE_EXISTS: invalid args")
  559. relpath, enoent = args[0], args[1] if len(args) == 2 else ""
  560. try:
  561. exists = f_exists(self.cms.wwwPath,
  562. CMSPageIdent.validateSafePath(relpath))
  563. except (CMSException) as e:
  564. exists = False
  565. return cons, (relpath if exists else enoent)
  566. # Statement: $(file_mdatet RELATIVE_PATH)
  567. # Statement: $(file_mdatet RELATIVE_PATH, DOES_NOT_EXIST, FORMAT_STRING)
  568. # Returns the file modification time.
  569. # If the file does not exist, it returns DOES_NOT_EXIST or and empty string.
  570. # RELATIVE_PATH is relative to wwwPath.
  571. # FORMAT_STRING is an optional strftime format string.
  572. def __stmt_fileModDateTime(self, d):
  573. cons, args = self.__parseArguments(d)
  574. if len(args) not in {1, 2, 3}:
  575. self.__stmtError("FILE_MDATET: invalid args")
  576. relpath, enoent, fmtstr =\
  577. args[0],\
  578. args[1] if len(args) >= 2 else "",\
  579. args[2] if len(args) >= 3 else "%d %B %Y %H:%M (UTC)"
  580. try:
  581. stamp = f_mtime(self.cms.wwwPath,
  582. CMSPageIdent.validateSafePath(relpath))
  583. except (CMSException) as e:
  584. return cons, enoent
  585. return cons, stamp.strftime(fmtstr.strip())
  586. # Statement: $(index)
  587. # Returns the site index.
  588. def __stmt_index(self, d):
  589. cons, args = self.__parseArguments(d)
  590. if len(args) != 1 or args[0]:
  591. self.__stmtError("INDEX: invalid args")
  592. self.indexRefs.append(self.IndexRef(self.charCount))
  593. return cons, ""
  594. # Statement: $(anchor NAME, TEXT)
  595. # Statement: $(anchor NAME, TEXT, INDENT_LEVEL)
  596. # Statement: $(anchor NAME, TEXT, INDENT_LEVEL, NO_INDEX)
  597. # Sets an index-anchor
  598. def __stmt_anchor(self, d):
  599. cons, args = self.__parseArguments(d)
  600. if len(args) < 2 or len(args) > 4:
  601. self.__stmtError("ANCHOR: invalid args")
  602. name, text = args[0:2]
  603. indent, noIndex = -1, False
  604. if len(args) >= 3:
  605. indent = args[2].strip()
  606. try:
  607. indent = int(indent) if indent else -1
  608. except ValueError:
  609. self.__stmtError("ANCHOR: indent level "
  610. "is not an integer")
  611. if len(args) >= 4:
  612. noIndex = bool(args[3].strip())
  613. name, text = name.strip(), text.strip()
  614. anchor = self.Anchor(name, text, indent, noIndex)
  615. # Cache anchor for index creation
  616. self.anchors.append(anchor)
  617. # Create the anchor HTML
  618. return cons, '<a name="%s" href="%s">%s</a>' %\
  619. (name, anchor.makeUrl(self), text)
  620. # Statement: $(pagelist BASEPAGE, ...)
  621. # Returns an <ul>-list of all sub-page names in the page.
  622. def __stmt_pagelist(self, d):
  623. cons, args = self.__parseArguments(d)
  624. try:
  625. basePageIdent = CMSPageIdent(args)
  626. subPages = self.cms.db.getSubPages(basePageIdent)
  627. except CMSException as e:
  628. self.__stmtError("PAGELIST: invalid base page name")
  629. html = [ '<ul>\n' ]
  630. for pagename, navlabel, prio in subPages:
  631. pageIdent = CMSPageIdent(basePageIdent + [pagename])
  632. pagetitle = self.cms.db.getPageTitle(pageIdent)
  633. html.append('\t<li><a href="%s">%s</a></li>\n' %\
  634. (pageIdent.getUrl(urlBase = self.cms.urlBase),
  635. pagetitle))
  636. html.append('</ul>')
  637. return cons, ''.join(html)
  638. # Statement: $(random)
  639. # Statement: $(random BEGIN)
  640. # Statement: $(random BEGIN, END)
  641. # Returns a random integer in the range from BEGIN to END
  642. # (including both end points)
  643. # BEGIN defaults to 0. END defaults to 65535.
  644. def __stmt_random(self, d):
  645. cons, args = self.__parseArguments(d, strip=True)
  646. if len(args) not in {0, 1, 2}:
  647. self.__stmtError("RANDOM: invalid args")
  648. begin, end = 0, 65535
  649. try:
  650. if len(args) >= 2 and args[1]:
  651. end = int(args[1])
  652. if len(args) >= 1 and args[0]:
  653. begin = int(args[0])
  654. rnd = random.randint(begin, end)
  655. except ValueError as e:
  656. self.__stmtError("RANDOM: invalid range")
  657. return cons, '%d' % rnd
  658. # Statement: $(randitem ITEM0, ITEM1, ...)
  659. # Returns one random item of its arguments.
  660. def __stmt_randitem(self, d):
  661. cons, args = self.__parseArguments(d)
  662. if len(args) < 1:
  663. self.__stmtError("RANDITEM: too few args")
  664. return cons, random.choice(args)
  665. __validDomainChars = LOWERCASE + UPPERCASE + NUMBERS + "."
  666. # Statement: $(whois DOMAIN)
  667. # Executes whois and returns the text.
  668. def __stmt_whois(self, d):
  669. cons, args = self.__parseArguments(d)
  670. if len(args) != 1:
  671. self.__stmtError("WHOIS: invalid args")
  672. domain = args[0]
  673. if [ c for c in domain if c not in self.__validDomainChars ]:
  674. self.__stmtError("WHOIS: invalid domain")
  675. try:
  676. import subprocess
  677. whois = subprocess.Popen([ "whois", domain ],
  678. shell = False,
  679. stdout = subprocess.PIPE)
  680. out, err = whois.communicate()
  681. out = out.decode("UTF-8")
  682. except UnicodeError as e:
  683. self.__stmtError("WHOIS: unicode error")
  684. except (OSError, ValueError) as e:
  685. self.__stmtError("WHOIS: execution error")
  686. return cons, out
  687. __handlers = {
  688. "$(if" : __stmt_if,
  689. "$(eq" : __stmt_eq,
  690. "$(ne" : __stmt_ne,
  691. "$(and" : __stmt_and,
  692. "$(or" : __stmt_or,
  693. "$(not" : __stmt_not,
  694. "$(assert" : __stmt_assert,
  695. "$(strip" : __stmt_strip,
  696. "$(item" : __stmt_item,
  697. "$(sanitize" : __stmt_sanitize,
  698. "$(file_exists" : __stmt_fileExists,
  699. "$(file_mdatet" : __stmt_fileModDateTime,
  700. "$(index" : __stmt_index,
  701. "$(anchor" : __stmt_anchor,
  702. "$(pagelist" : __stmt_pagelist,
  703. "$(random" : __stmt_random,
  704. "$(randitem" : __stmt_randitem,
  705. "$(whois" : __stmt_whois,
  706. }
  707. def __doMacro(self, macroname, d):
  708. if len(self.callStack) > 16:
  709. raise CMSException(500, "Exceed macro call stack depth")
  710. cons, arguments = self.__parseArguments(d, strip=True)
  711. # Fetch the macro data from the database
  712. macrodata = None
  713. try:
  714. macrodata = self.cms.db.getMacro(macroname[1:],
  715. self.pageIdent)
  716. except (CMSException) as e:
  717. if e.httpStatusCode == 404:
  718. raise CMSException(500,
  719. "Macro name '%s' contains "
  720. "invalid characters" % macroname)
  721. if not macrodata:
  722. return cons, "" # Macro does not exist.
  723. # Expand the macro arguments ($1, $2, $3, ...)
  724. def expandArg(match):
  725. nr = int(match.group(1), 10)
  726. if nr >= 1 and nr <= len(arguments):
  727. return arguments[nr - 1]
  728. return macroname if nr == 0 else ""
  729. macrodata = self.macro_arg_re.sub(expandArg, macrodata)
  730. # Resolve statements and recursive macro calls
  731. self.callStack.append(self.StackElem(macroname))
  732. macrodata = self.__resolve(macrodata)
  733. self.callStack.pop()
  734. return cons, macrodata
  735. def __expandRecStmts(self, d, stopchars=""):
  736. # Recursively expand statements and macro calls
  737. ret, i = [], 0
  738. while i < len(d):
  739. cons, res = 1, d[i]
  740. if d[i] == '\\': # Escaped characters
  741. # Keep escapes. They are removed later.
  742. if i + 1 < len(d) and\
  743. d[i + 1] in self.__escapedChars:
  744. res = d[i:i+2]
  745. i += 1
  746. elif d[i] == '\n':
  747. self.callStack[-1].lineno += 1
  748. elif d.startswith('<!---', i): # Comment
  749. end = d.find('--->', i)
  750. if end > i:
  751. strip_nl = 0
  752. # If comment is on a line of its own,
  753. # remove the line.
  754. if (i == 0 or d[i - 1] == '\n') and\
  755. (end + 4 < len(d) and d[end + 4] == '\n'):
  756. strip_nl = 1
  757. cons, res = end - i + 4 + strip_nl, ""
  758. elif d[i] in stopchars: # Stop character
  759. i += 1
  760. break
  761. elif d[i] == '@': # Macro call
  762. end = d.find('(', i)
  763. if end > i:
  764. cons, res = self.__doMacro(
  765. d[i:end],
  766. d[end+1:])
  767. i = end + 1
  768. elif d.startswith('$(', i): # Statement
  769. h = lambda _self, x: (cons, res) # nop
  770. end = findAny(d, ' )', i)
  771. if end > i:
  772. try:
  773. h = self.__handlers[d[i:end]]
  774. i = end + 1 if d[end] == ' ' else end
  775. except KeyError: pass
  776. cons, res = h(self, d[i:])
  777. elif d[i] == '$': # Variable
  778. end = findNot(d, self.VARNAME_CHARS, i + 1)
  779. if end > i + 1:
  780. res = self.expandVariable(d[i+1:end])
  781. cons = end - i
  782. ret.append(res)
  783. i += cons
  784. self.charCount += len(res)
  785. if stopchars and i >= len(d) and d[-1] not in stopchars:
  786. self.__stmtError("Unterminated statement")
  787. retData = "".join(ret)
  788. self.charCount -= len(retData)
  789. return i, retData
  790. # Create an index
  791. def __createIndex(self, anchors):
  792. indexData = [ '\t<ul>\n' ]
  793. indent = 0
  794. def createIndent(indentCount):
  795. indexData.append('\t' * (indentCount + 1))
  796. def incIndent(count):
  797. curIndent = indent
  798. while count:
  799. curIndent += 1
  800. createIndent(curIndent)
  801. indexData.append('<ul>\n')
  802. count -= 1
  803. return curIndent
  804. def decIndent(count):
  805. curIndent = indent
  806. while count:
  807. createIndent(curIndent)
  808. indexData.append('</ul>\n')
  809. curIndent -= 1
  810. count -= 1
  811. return curIndent
  812. for anchor in anchors:
  813. if anchor.noIndex or not anchor.text:
  814. # No index item for this anchor
  815. continue
  816. if anchor.indent >= 0 and anchor.indent > indent:
  817. # Increase indent
  818. if anchor.indent > 1024:
  819. raise CMSException(500,
  820. "Anchor indent too big")
  821. indent = incIndent(anchor.indent - indent)
  822. elif anchor.indent >= 0 and anchor.indent < indent:
  823. # Decrease indent
  824. indent = decIndent(indent - anchor.indent)
  825. # Append the actual anchor data
  826. createIndent(indent)
  827. indexData.append('<li>')
  828. indexData.append('<a href="%s">%s</a>' %\
  829. (anchor.makeUrl(self),
  830. anchor.text))
  831. indexData.append('</li>\n')
  832. # Close all indents
  833. decIndent(indent + 1)
  834. return "".join(indexData)
  835. # Insert the referenced indices
  836. def __processIndices(self, data):
  837. offset = 0
  838. for indexRef in self.indexRefs:
  839. indexData = self.__createIndex(self.anchors)
  840. curOffset = offset + indexRef.charOffset
  841. data = data[0 : curOffset] +\
  842. indexData +\
  843. data[curOffset :]
  844. offset += len(indexData)
  845. return data
  846. def __resolve(self, data):
  847. # Expand recursive statements
  848. unused, data = self.__expandRecStmts(data)
  849. return data
  850. def resolve(self, data, variables = {}, pageIdent = None):
  851. if not data:
  852. return data
  853. self.__reset(variables, pageIdent)
  854. data = self.__resolve(data)
  855. # Insert the indices
  856. data = self.__processIndices(data)
  857. # Remove escapes
  858. data = self.unescape(data)
  859. return data
  860. class CMSQuery(object):
  861. def __init__(self, queryDict):
  862. self.queryDict = queryDict
  863. def get(self, name, default=""):
  864. try:
  865. return self.queryDict[name][-1]
  866. except (KeyError, IndexError) as e:
  867. return default
  868. def getInt(self, name, default=0):
  869. try:
  870. return int(self.get(name, str(int(default))), 10)
  871. except (ValueError) as e:
  872. return default
  873. def getBool(self, name, default=False):
  874. string = self.get(name, str(bool(default)))
  875. return stringBool(string, default)
  876. class CMS(object):
  877. # Main CMS entry point.
  878. __rootPageIdent = CMSPageIdent()
  879. def __init__(self,
  880. dbPath,
  881. wwwPath,
  882. imagesDir="/images",
  883. domain="example.com",
  884. urlBase="/cms",
  885. cssUrlPath="/cms.css",
  886. debug=False):
  887. # dbPath => Unix path to the database directory.
  888. # wwwPath => Unix path to the static www data.
  889. # imagesDir => Subdirectory path, based on wwwPath, to
  890. # the images directory.
  891. # domain => The site domain name.
  892. # urlBase => URL base component to the HTTP server CMS mapping.
  893. # cssUrlBase => URL subpath to the CSS.
  894. # debug => Enable/disable debugging
  895. self.wwwPath = wwwPath
  896. self.imagesDir = imagesDir
  897. self.domain = domain
  898. self.urlBase = urlBase
  899. self.cssUrlPath = cssUrlPath
  900. self.debug = debug
  901. self.db = CMSDatabase(dbPath)
  902. self.resolver = CMSStatementResolver(self)
  903. def shutdown(self):
  904. pass
  905. def __genHtmlHeader(self, title, additional = ""):
  906. header = """<?xml version="1.0" encoding="UTF-8" ?>
  907. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  908. <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
  909. <head>
  910. <meta http-equiv="content-type" content="text/html; charset=utf-8" />
  911. <meta name="robots" content="all" />
  912. <meta name="date" content="%s" />
  913. <meta name="generator" content="WSGI/Python CMS" />
  914. <!--
  915. Generated by "cms.py - simple WSGI/Python based CMS script"
  916. https://bues.ch/cgit/cms.git
  917. -->
  918. <title>%s</title>
  919. <link rel="stylesheet" href="%s" type="text/css" />
  920. %s
  921. </head>
  922. <body>
  923. """ % (
  924. datetime.now().isoformat(),
  925. title,
  926. self.cssUrlPath,
  927. additional or ""
  928. )
  929. return header
  930. def __genHtmlFooter(self):
  931. footer = """
  932. </body>
  933. </html>
  934. """
  935. return footer
  936. def __genNavElem(self, body, basePageIdent, activePageIdent, indent = 0):
  937. if self.db.getNavStop(basePageIdent):
  938. return
  939. subPages = self.db.getSubPages(basePageIdent)
  940. if not subPages:
  941. return
  942. tabs = '\t' + '\t' * indent
  943. if indent > 0:
  944. body.append('%s<div class="navelems">' % tabs)
  945. for pageElement in subPages:
  946. pagename, pagelabel, pageprio = pageElement
  947. if pagelabel:
  948. body.append('%s\t<div class="%s"> '
  949. '<!-- %d -->' % (
  950. tabs,
  951. "navelem" if indent > 0 else "navgroup",
  952. pageprio))
  953. if indent <= 0:
  954. body.append('%s\t\t<div class="navhead">' %\
  955. tabs)
  956. subPageIdent = CMSPageIdent(basePageIdent + [pagename])
  957. isActive = activePageIdent.startswith(subPageIdent)
  958. if isActive:
  959. body.append('%s\t\t<div class="navactive">' %\
  960. tabs)
  961. body.append('%s\t\t<a href="%s">%s</a>' %\
  962. (tabs,
  963. subPageIdent.getUrl(urlBase = self.urlBase),
  964. pagelabel))
  965. if isActive:
  966. body.append('%s\t\t</div> '
  967. '<!-- class="navactive" -->' %\
  968. tabs)
  969. if indent <= 0:
  970. body.append('%s\t\t</div>' % tabs)
  971. self.__genNavElem(body, subPageIdent,
  972. activePageIdent, indent + 2)
  973. body.append('%s\t</div>' % tabs)
  974. if indent > 0:
  975. body.append('%s</div>' % tabs)
  976. def __genHtmlBody(self, pageIdent, pageTitle, pageData,
  977. protocol,
  978. stamp=None, genCheckerLinks=True):
  979. body = []
  980. # Generate logo / title bar
  981. body.append('<div class="titlebar">')
  982. body.append('\t<div class="logo">')
  983. body.append('\t\t<a href="%s">' % self.urlBase)
  984. body.append('\t\t\t<img alt="logo" src="/logo.png" />')
  985. body.append('\t\t</a>')
  986. body.append('\t</div>')
  987. body.append('\t<div class="title">%s</div>' % pageTitle)
  988. body.append('</div>\n')
  989. # Generate navigation bar
  990. body.append('<div class="navbar">')
  991. body.append('\t<div class="navgroups">')
  992. body.append('\t\t<div class="navhome">')
  993. rootActive = not pageIdent
  994. if rootActive:
  995. body.append('\t\t<div class="navactive">')
  996. body.append('\t\t\t<a href="%s">%s</a>' %\
  997. (self.__rootPageIdent.getUrl(urlBase = self.urlBase),
  998. self.db.getString("home")))
  999. if rootActive:
  1000. body.append('\t\t</div> <!-- class="navactive" -->')
  1001. body.append('\t\t</div>')
  1002. self.__genNavElem(body, self.__rootPageIdent, pageIdent)
  1003. body.append('\t</div>')
  1004. body.append('</div>\n')
  1005. body.append('<div class="main">\n') # Main body start
  1006. # Page content
  1007. body.append('<!-- BEGIN: page content -->')
  1008. body.append(pageData)
  1009. body.append('<!-- END: page content -->\n')
  1010. if stamp:
  1011. # Last-modified date
  1012. body.append('\t<div class="modifystamp">')
  1013. body.append(stamp.strftime('\t\tUpdated: %A %d %B %Y %H:%M (UTC)'))
  1014. body.append('\t</div>')
  1015. if protocol != "https":
  1016. # SSL
  1017. body.append('\t<div class="ssl">')
  1018. body.append('\t\t<a href="%s">%s</a>' % (
  1019. pageIdent.getUrl("https", self.domain,
  1020. self.urlBase),
  1021. self.db.getString("ssl-encrypted")))
  1022. body.append('\t</div>')
  1023. if genCheckerLinks:
  1024. # Checker links
  1025. pageUrlQuoted = urllib.parse.quote_plus(
  1026. pageIdent.getUrl("http", self.domain,
  1027. self.urlBase))
  1028. body.append('\t<div class="checker">')
  1029. checkerUrl = "http://validator.w3.org/check?"\
  1030. "uri=" + pageUrlQuoted + "&amp;"\
  1031. "charset=%28detect+automatically%29&amp;"\
  1032. "doctype=Inline&amp;group=0&amp;"\
  1033. "user-agent=W3C_Validator%2F1.2"
  1034. body.append('\t\t<a href="%s">%s</a> /' %\
  1035. (checkerUrl, self.db.getString("checker-xhtml")))
  1036. checkerUrl = "http://jigsaw.w3.org/css-validator/validator?"\
  1037. "uri=" + pageUrlQuoted + "&amp;profile=css3&amp;"\
  1038. "usermedium=all&amp;warning=1&amp;"\
  1039. "vextwarning=true&amp;lang=en"
  1040. body.append('\t\t<a href="%s">%s</a>' %\
  1041. (checkerUrl, self.db.getString("checker-css")))
  1042. body.append('\t</div>\n')
  1043. body.append('</div>\n') # Main body end
  1044. return "\n".join(body)
  1045. def __getImageThumbnail(self, imagename, query, protocol):
  1046. if not imagename:
  1047. raise CMSException(404)
  1048. width = query.getInt("w", 300)
  1049. height = query.getInt("h", 300)
  1050. qual = query.getInt("q", 1)
  1051. qualities = {
  1052. 0 : Image.NEAREST,
  1053. 1 : Image.BILINEAR,
  1054. 2 : Image.BICUBIC,
  1055. 3 : Image.ANTIALIAS,
  1056. }
  1057. try:
  1058. qual = qualities[qual]
  1059. except (KeyError) as e:
  1060. qual = qualities[1]
  1061. try:
  1062. img = Image.open(mkpath(self.wwwPath, self.imagesDir,
  1063. CMSPageIdent.validateSafePathComponent(imagename)))
  1064. img.thumbnail((width, height), qual)
  1065. output = BytesIO()
  1066. img.save(output, "JPEG")
  1067. data = output.getvalue()
  1068. except (IOError) as e:
  1069. raise CMSException(404)
  1070. return data, "image/jpeg"
  1071. def __getHtmlPage(self, pageIdent, query, protocol):
  1072. pageTitle, pageData, stamp = self.db.getPage(pageIdent)
  1073. if not pageData:
  1074. raise CMSException(404)
  1075. resolverVariables = {
  1076. "PROTOCOL" : lambda r, n: protocol,
  1077. "GROUP" : lambda r, n: pageIdent.get(0),
  1078. "PAGE" : lambda r, n: pageIdent.get(1),
  1079. }
  1080. resolve = self.resolver.resolve
  1081. for k, v in query.queryDict.items():
  1082. k, v = k.upper(), v[-1]
  1083. resolverVariables["Q_" + k] = CMSStatementResolver.escape(htmlEscape(v))
  1084. resolverVariables["QRAW_" + k] = CMSStatementResolver.escape(v)
  1085. pageTitle = resolve(pageTitle, resolverVariables, pageIdent)
  1086. resolverVariables["TITLE"] = lambda r, n: pageTitle
  1087. pageData = resolve(pageData, resolverVariables, pageIdent)
  1088. extraHeader = resolve(self.db.getHeader(pageIdent),
  1089. resolverVariables, pageIdent)
  1090. data = [self.__genHtmlHeader(pageTitle, extraHeader)]
  1091. data.append(self.__genHtmlBody(pageIdent,
  1092. pageTitle, pageData,
  1093. protocol, stamp))
  1094. data.append(self.__genHtmlFooter())
  1095. try:
  1096. return "".join(data).encode("UTF-8"), \
  1097. "text/html; charset=UTF-8"
  1098. except UnicodeError as e:
  1099. raise CMSException(500, "Unicode encode error")
  1100. def __generate(self, path, query, protocol):
  1101. pageIdent = CMSPageIdent.parse(path)
  1102. if pageIdent.get(0, allowSysNames = True) == "__thumbs":
  1103. return self.__getImageThumbnail(pageIdent.get(1), query, protocol)
  1104. return self.__getHtmlPage(pageIdent, query, protocol)
  1105. def get(self, path, query={}, protocol="http"):
  1106. query = CMSQuery(query)
  1107. return self.__generate(path, query, protocol)
  1108. def __post(self, path, query, body, bodyType, protocol):
  1109. pageIdent = CMSPageIdent.parse(path)
  1110. postHandler = self.db.getPostHandler(pageIdent)
  1111. if not postHandler:
  1112. raise CMSException(405)
  1113. try:
  1114. ret = postHandler.post(query, body, bodyType, protocol)
  1115. except Exception as e:
  1116. msg = ""
  1117. if self.debug:
  1118. msg = " " + str(e)
  1119. msg = msg.encode("UTF-8", "ignore")
  1120. return (b"Failed to run POST handler." + msg,
  1121. "text/plain")
  1122. if ret is None:
  1123. return self.__generate(path, query, protocol)
  1124. assert(isinstance(ret, tuple) and len(ret) == 2)
  1125. assert((isinstance(ret[0], bytes) or isinstance(ret[0], bytearray)) and\
  1126. isinstance(ret[1], str))
  1127. return ret
  1128. def post(self, path, query={},
  1129. body=b"", bodyType="text/plain",
  1130. protocol="http"):
  1131. raise CMSException(405) #TODO disabled
  1132. query = CMSQuery(query)
  1133. return self.__post(path, query, body, bodyType, protocol)
  1134. def __doGetErrorPage(self, cmsExcept, protocol):
  1135. resolverVariables = {
  1136. "PROTOCOL" : lambda r, n: protocol,
  1137. "GROUP" : lambda r, n: "_nogroup_",
  1138. "PAGE" : lambda r, n: "_nopage_",
  1139. "HTTP_STATUS" : lambda r, n: cmsExcept.httpStatus,
  1140. "HTTP_STATUS_CODE" : lambda r, n: str(cmsExcept.httpStatusCode),
  1141. "ERROR_MESSAGE" : lambda r, n: CMSStatementResolver.escape(htmlEscape(cmsExcept.message)),
  1142. }
  1143. pageHeader = cmsExcept.getHtmlHeader(self.db)
  1144. pageHeader = self.resolver.resolve(pageHeader, resolverVariables)
  1145. pageData = cmsExcept.getHtmlBody(self.db)
  1146. pageData = self.resolver.resolve(pageData, resolverVariables)
  1147. httpHeaders = cmsExcept.getHttpHeaders(
  1148. lambda s: self.resolver.resolve(s, resolverVariables))
  1149. data = [self.__genHtmlHeader(cmsExcept.httpStatus,
  1150. additional=pageHeader)]
  1151. data.append(self.__genHtmlBody(CMSPageIdent(("_nogroup_", "_nopage_")),
  1152. cmsExcept.httpStatus,
  1153. pageData,
  1154. protocol,
  1155. genCheckerLinks=False))
  1156. data.append(self.__genHtmlFooter())
  1157. return "".join(data), "text/html; charset=UTF-8", httpHeaders
  1158. def getErrorPage(self, cmsExcept, protocol="http"):
  1159. try:
  1160. data, mime, headers = self.__doGetErrorPage(cmsExcept, protocol)
  1161. except (CMSException) as e:
  1162. data = "Error in exception handler: %s %s" % \
  1163. (e.httpStatus, e.message)
  1164. mime, headers = "text/plain; charset=UTF-8", ()
  1165. try:
  1166. return data.encode("UTF-8"), mime, headers
  1167. except UnicodeError as e:
  1168. # Whoops. All is lost.
  1169. raise CMSException(500, "Unicode encode error")