# -*- coding: utf-8 -*- # # cms.py - simple WSGI/Python based CMS script # # Copyright (C) 2011-2019 Michael Buesch # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . #from cms.cython_support cimport * #@cy from cms.exception import * from cms.pageident import * from cms.util import * #+cimport import re import random cround = round #@nocy #from libc.math cimport round as cround #@cy __all__ = [ "CMSStatementResolver", ] MACRO_STACK_SIZE = 64 #@nocy MACRO_STACK_NAME_SIZE = 32 #@nocy # Call stack element class _StackElem(object): #@nocy __slots__ = ( #@nocy "name", #@nocy "lineno", #@nocy ) #@nocy def __init__(self): #@nocy self.name = StrCArray() #@nocy def stackElem(lineno, name): #@nocy #cdef inline _StackElem stackElem(int64_t lineno, str name): #@cy #@cy cdef _StackElem se se = _StackElem() #@nocy se.lineno = lineno str2carray(se.name, name, MACRO_STACK_NAME_SIZE) return se class _IndexRef(object): #+cdef # Index references def __init__(self, charOffset): self.charOffset = charOffset class _Anchor(object): #+cdef # HTML anchor def __init__(self, name, text, indent=-1, noIndex=False): self.name = name self.text = text self.indent = indent self.noIndex = noIndex def makeUrl(self, resolver): #@nocy #@cy cdef str makeUrl(self, CMSStatementResolver resolver): return "%s#%s" % (resolver.expandVariable("CMS_PAGEIDENT"), self.name) class _ArgParserRet(object): #@nocy __slots__ = ( #@nocy "cons", #@nocy "arguments", #@nocy ) #@nocy class _ResolverRet(object): #@nocy __slots__ = ( #@nocy "cons", #@nocy "data", #@nocy ) #@nocy def resolverRet(cons, data): #@nocy #cdef inline _ResolverRet resolverRet(int64_t cons, str data): #@cy #@cy cdef _ResolverRet r r = _ResolverRet() r.cons = cons r.data = data return r class CMSStatementResolver(object): #+cdef __genericVars = { "BR" : "
", "DOMAIN" : lambda self, n: self.cms.domain, "CMS_BASE" : lambda self, n: self.cms.urlBase, "IMAGES_DIR" : lambda self, n: self.cms.imagesDir, "THUMBS_DIR" : lambda self, n: self.cms.urlBase + "/__thumbs", "DEBUG" : lambda self, n: "1" if self.cms.debug else "", "__DUMPVARS__" : lambda self, n: self.__dumpVars(), } def __init__(self, cms): self.__handlers = self._handlers self.__escapedChars = self._escapedChars # Valid characters for variable names (without the leading $) self.VARNAME_CHARS = UPPERCASE + '_' self.cms = cms self.__reset() def __reset(self, variables={}, pageIdent=None): self.variables = variables.copy() self.variables.update(self.__genericVars) self.pageIdent = pageIdent self.charCount = 0 self.indexRefs = [] self.anchors = [] self.__callStack = [ None ] * MACRO_STACK_SIZE #@nocy self.__macroArgs = [ None ] * MACRO_STACK_SIZE self.__callStack[0] = stackElem(1, "content.html") self.__callStackLen = 1 def __stmtError(self, msg): #@cy cdef _StackElem se pfx = "" if self.cms.debug: se = self.__callStack[self.__callStackLen - 1] pfx = "%s:%d: " % (carray2str(se.name, MACRO_STACK_NAME_SIZE), se.lineno) raise CMSException(500, pfx + msg) def expandVariable(self, name): #@nocy #@cy cdef str expandVariable(self, str name): try: value = self.variables[name] if callable(value): value = value(self, name) if value is None: raise KeyError return str(value) except (KeyError, TypeError) as e: return "" def __dumpVars(self, force=False): if not force and not self.cms.debug: return "" ret = [] for name in sorted(self.variables.keys()): if name == "__DUMPVARS__": value = "-- variable dump --" else: value = self.expandVariable(name) sep = "\t" * (3 - len(name) // 8) ret.append("%s%s=> %s" % (name, sep, value)) return "\n".join(ret) _escapedChars = '\\,@$()' def escape(self, data): #@nocy #@cy cpdef str escape(self, str data): #@cy cdef str c for c in self.__escapedChars: data = data.replace(c, '\\' + c) return data def unescape(self, data): #@nocy #@cy cpdef str unescape(self, str data): #@cy cdef str c for c in self.__escapedChars: data = data.replace('\\' + c, c) return data # Parse statement arguments. def __parseArguments(self, d, dOffs, strip): #@nocy #@cy cdef _ArgParserRet __parseArguments(self, str d, int64_t dOffs, _Bool strip): #@cy cdef _ResolverRet r #@cy cdef _ArgParserRet ret #@cy cdef int64_t dEnd #@cy cdef int64_t i #@cy cdef str data ret = _ArgParserRet() ret.cons = 0 ret.arguments = [] i = dOffs dEnd = len(d) while i < dEnd: r = self.__expandRecStmts(d, i, ',)') data = r.data i += r.cons ret.cons += r.cons ret.arguments.append(data.strip() if strip else data) if i <= dOffs or i - 1 >= dEnd or d[i - 1] == ')': break return ret # Statement: $(if CONDITION, THEN, ELSE) # Statement: $(if CONDITION, THEN) # Returns THEN if CONDITION is nonempty after stripping whitespace. # Returns ELSE otherwise. def __stmt_if(self, d, dOffs): #@cy cdef _ArgParserRet a #@cy cdef int64_t cons #@cy cdef list args #@cy cdef str condition #@cy cdef str b_then #@cy cdef str b_else a = self.__parseArguments(d, dOffs, False) cons, args = a.cons, a.arguments if len(args) != 2 and len(args) != 3: self.__stmtError("IF: invalid number of arguments (%d)" %\ len(args)) condition, b_then = args[0], args[1] b_else = args[2] if len(args) == 3 else "" result = b_then if condition.strip() else b_else return resolverRet(cons, result) def __do_compare(self, d, dOffs, invert): #@nocy #@cy cdef _ResolverRet __do_compare(self, str d, int64_t dOffs, _Bool invert): #@cy cdef _ArgParserRet a #@cy cdef int64_t cons #@cy cdef list args #@cy cdef str arg #@cy cdef str firstArg #@cy cdef _Bool result a = self.__parseArguments(d, dOffs, True) cons, args = a.cons, a.arguments result = True firstArg = args[0] for arg in args[1:]: result = result and arg == firstArg if invert: result = not result return resolverRet(cons, (args[-1] if result else "")) # Statement: $(eq A, B, ...) # Returns the last argument, if all stripped arguments are equal. # Returns an empty string otherwise. def __stmt_eq(self, d, dOffs): return self.__do_compare(d, dOffs, False) # Statement: $(ne A, B, ...) # Returns the last argument, if not all stripped arguments are equal. # Returns an empty string otherwise. def __stmt_ne(self, d, dOffs): return self.__do_compare(d, dOffs, True) # Statement: $(and A, B, ...) # Returns A, if all stripped arguments are non-empty strings. # Returns an empty string otherwise. def __stmt_and(self, d, dOffs): #@cy cdef _ArgParserRet a #@cy cdef int64_t cons #@cy cdef list args a = self.__parseArguments(d, dOffs, True) cons, args = a.cons, a.arguments return resolverRet(cons, (args[0] if all(args) else "")) # Statement: $(or A, B, ...) # Returns the first stripped non-empty argument. # Returns an empty string, if there is no non-empty argument. def __stmt_or(self, d, dOffs): #@cy cdef _ArgParserRet a #@cy cdef int64_t cons #@cy cdef list args a = self.__parseArguments(d, dOffs, True) cons, args = a.cons, a.arguments nonempty = [ a for a in args if a ] return resolverRet(cons, (nonempty[0] if nonempty else "")) # Statement: $(not A) # Returns 1, if A is an empty string after stripping. # Returns an empty string, if A is a non-empty stripped string. def __stmt_not(self, d, dOffs): #@cy cdef _ArgParserRet a #@cy cdef int64_t cons #@cy cdef list args a = self.__parseArguments(d, dOffs, True) cons, args = a.cons, a.arguments if len(args) != 1: self.__stmtError("NOT: invalid args") return resolverRet(cons, ("" if args[0] else "1")) # Statement: $(assert A, ...) # Raises a 500-assertion-failed exception, if any argument # is empty after stripping. # Returns an empty string, otherwise. def __stmt_assert(self, d, dOffs): #@cy cdef _ArgParserRet a #@cy cdef int64_t cons #@cy cdef list args a = self.__parseArguments(d, dOffs, True) cons, args = a.cons, a.arguments if not all(args): self.__stmtError("ASSERT: failed") return resolverRet(cons, "") # Statement: $(strip STRING) # Strip whitespace at the start and at the end of the string. def __stmt_strip(self, d, dOffs): #@cy cdef _ArgParserRet a #@cy cdef int64_t cons #@cy cdef list args a = self.__parseArguments(d, dOffs, True) cons, args = a.cons, a.arguments return resolverRet(cons, "".join(args)) # Statement: $(item STRING, N) # Statement: $(item STRING, N, SEPARATOR) # Split a string into tokens and return the N'th token. # SEPARATOR defaults to whitespace. def __stmt_item(self, d, dOffs): #@cy cdef _ArgParserRet a #@cy cdef int64_t cons #@cy cdef list args a = self.__parseArguments(d, dOffs, False) cons, args = a.cons, a.arguments if len(args) not in {2, 3}: self.__stmtError("ITEM: invalid args") string, n, sep = args[0], args[1], args[2].strip() if len(args) == 3 else "" tokens = string.split(sep) if sep else string.split() try: token = tokens[int(n)] except ValueError: self.__stmtError("ITEM: N is not an integer") except IndexError: token = "" return resolverRet(cons, token) # Statement: $(substr STRING, START) # Statement: $(substr STRING, START, END) # Returns a sub-string of STRING. def __stmt_substr(self, d, dOffs): #@cy cdef _ArgParserRet a #@cy cdef int64_t cons #@cy cdef list args a = self.__parseArguments(d, dOffs, False) cons, args = a.cons, a.arguments if len(args) not in {2, 3}: self.__stmtError("SUBSTR: invalid args") string, start, end = args[0], args[1], args[2] if len(args) == 3 else "" try: if end.strip(): substr = string[int(start) : int(end)] else: substr = string[int(start)] except ValueError: self.__stmtError("SUBSTR: START or END is not an integer") except IndexError: substr = "" return resolverRet(cons, substr) # Statement: $(sanitize STRING) # Sanitize a string. # Replaces all non-alphanumeric characters by an underscore. Forces lower-case. def __stmt_sanitize(self, d, dOffs): #@cy cdef _ArgParserRet a #@cy cdef int64_t cons #@cy cdef list args a = self.__parseArguments(d, dOffs, False) cons, args = a.cons, a.arguments string = "_".join(args) validChars = LOWERCASE + NUMBERS string = string.lower() string = "".join( c if c in validChars else '_' for c in string ) string = re.sub(r'_+', '_', string).strip('_') return resolverRet(cons, string) # Statement: $(file_exists RELATIVE_PATH) # Statement: $(file_exists RELATIVE_PATH, DOES_NOT_EXIST) # Checks if a file exists relative to the wwwPath base. # Returns the path, if the file exists or an empty string if it doesn't. # If DOES_NOT_EXIST is specified, it returns this if the file doesn't exist. def __stmt_fileExists(self, d, dOffs): #@cy cdef _ArgParserRet a #@cy cdef int64_t cons #@cy cdef list args a = self.__parseArguments(d, dOffs, False) cons, args = a.cons, a.arguments if len(args) != 1 and len(args) != 2: self.__stmtError("FILE_EXISTS: invalid args") relpath, enoent = args[0], args[1] if len(args) == 2 else "" try: exists = fs.exists(self.cms.wwwPath, CMSPageIdent.validateSafePath(relpath)) except (CMSException) as e: exists = False return resolverRet(cons, (relpath if exists else enoent)) # Statement: $(file_mdatet RELATIVE_PATH) # Statement: $(file_mdatet RELATIVE_PATH, DOES_NOT_EXIST, FORMAT_STRING) # Returns the file modification time. # If the file does not exist, it returns DOES_NOT_EXIST or an empty string. # RELATIVE_PATH is relative to wwwPath. # FORMAT_STRING is an optional strftime format string. def __stmt_fileModDateTime(self, d, dOffs): #@cy cdef _ArgParserRet a #@cy cdef int64_t cons #@cy cdef list args a = self.__parseArguments(d, dOffs, False) cons, args = a.cons, a.arguments if len(args) not in {1, 2, 3}: self.__stmtError("FILE_MDATET: invalid args") relpath, enoent, fmtstr =\ args[0],\ args[1] if len(args) >= 2 else "",\ args[2] if len(args) >= 3 else "%d %B %Y %H:%M (UTC)" try: stamp = fs.mtime(self.cms.wwwPath, CMSPageIdent.validateSafePath(relpath)) except (CMSException) as e: return resolverRet(cons, enoent) return resolverRet(cons, stamp.strftime(fmtstr.strip())) # Statement: $(index) # Returns the site index. def __stmt_index(self, d, dOffs): #@cy cdef _ArgParserRet a #@cy cdef int64_t cons #@cy cdef list args a = self.__parseArguments(d, dOffs, False) cons, args = a.cons, a.arguments if len(args) != 1 or args[0]: self.__stmtError("INDEX: invalid args") self.indexRefs.append(_IndexRef(self.charCount)) return resolverRet(cons, "") # Statement: $(anchor NAME, TEXT) # Statement: $(anchor NAME, TEXT, INDENT_LEVEL) # Statement: $(anchor NAME, TEXT, INDENT_LEVEL, NO_INDEX) # Sets an index-anchor def __stmt_anchor(self, d, dOffs): #@cy cdef _ArgParserRet a #@cy cdef int64_t cons #@cy cdef list args #@cy cdef _Anchor anchor a = self.__parseArguments(d, dOffs, False) cons, args = a.cons, a.arguments if len(args) < 2 or len(args) > 4: self.__stmtError("ANCHOR: invalid args") name, text = args[0:2] indent, noIndex = -1, False if len(args) >= 3: indent = args[2].strip() try: indent = int(indent) if indent else -1 except ValueError: self.__stmtError("ANCHOR: indent level " "is not an integer") if len(args) >= 4: noIndex = bool(args[3].strip()) name, text = name.strip(), text.strip() anchor = _Anchor(name, text, indent, noIndex) # Cache anchor for index creation self.anchors.append(anchor) # Create the anchor HTML return resolverRet(cons, '%s' %\ (name, anchor.makeUrl(self), text)) # Statement: $(pagelist BASEPAGE, ...) # Returns an