# -*- coding: utf-8 -*- # # cms.py - simple WSGI/Python based CMS script # # Copyright (C) 2011-2021 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.db import * from cms.exception import * from cms.formfields import * from cms.pageident import * from cms.query import * from cms.resolver import * #+cimport from cms.sitemap import * from cms.util import * #+cimport import PIL.Image as Image import urllib.parse import pathlib __all__ = [ "CMS", ] class CMS(object): # Main CMS entry point. __rootPageIdent = CMSPageIdent() def __init__(self, domain="example.com", urlBase="/cms", rundir=pathlib.Path("/run"), debug=False): # domain => The site domain name. # urlBase => URL base component to the HTTP server CMS mapping. # debug => Enable/disable debugging self.domain = domain self.urlBase = urlBase self.debug = debug self.db = CMSDatabase(rundir) self.resolver = CMSStatementResolver(self) def shutdown(self): pass def __genHtmlHeader(self, title, additional = ""): date = datetime.now(dt_timezone.utc).isoformat() interpreter = "Python" #@nocy # interpreter = "Cython" #@cy sitemap = self.urlBase + "/__sitemap.xml" additional = "\n\t".join(additional.splitlines()) return f""" {title} {additional or ''} """ def __genHtmlFooter(self): footer = """ """ return footer def _genNavElem(self, body, basePageIdent, activePageIdent, indent=0): if self.db.getNavStop(basePageIdent): return subPages = self.db.getSubPages(basePageIdent) if not subPages: return tabs = '\t' + '\t' * indent if indent > 0: body.append('%s' % tabs) def __genHtmlBody(self, pageIdent, pageTitle, pageData, protocol, stamp=None, genCheckerLinks=True): body = [] # Generate logo / title bar body.append('
') body.append('\t') body.append('\t
%s
' % pageTitle) body.append('
\n') # Generate navigation bar body.append('\n') body.append('
\n') # Main body start # Page content body.append('') body.append(pageData) body.append('\n') if stamp: # Last-modified date body.append('\t
') body.append(stamp.strftime('\t\tUpdated: %A %d %B %Y %H:%M (UTC)')) body.append('\t
') if protocol != "https": # SSL body.append('\t
') body.append('\t\t%s' % ( pageIdent.getUrl("https", self.domain, self.urlBase), self.db.getString("ssl-encrypted"))) body.append('\t
') if genCheckerLinks: # Checker links pageUrlQuoted = urllib.parse.quote_plus( pageIdent.getUrl("http", self.domain, self.urlBase)) body.append('\t
') checkerUrl = "http://validator.w3.org/check?"\ "uri=" + pageUrlQuoted + "&"\ "charset=%28detect+automatically%29&"\ "doctype=Inline&group=0&"\ "user-agent=W3C_Validator%2F1.2" body.append('\t\t%s /' %\ (checkerUrl, self.db.getString("checker-xhtml"))) checkerUrl = "http://jigsaw.w3.org/css-validator/validator?"\ "uri=" + pageUrlQuoted + "&profile=css3&"\ "usermedium=all&warning=1&"\ "vextwarning=true&lang=en" body.append('\t\t%s' %\ (checkerUrl, self.db.getString("checker-css"))) body.append('\t
\n') body.append('
\n') # Main body end return "\n".join(body) def __getCss(self, cssname, query, protocol): try: if cssname == "cms.css": return (self.db.getString("css").encode("UTF-8", "strict"), "text/css; charset=UTF-8") except UnicodeError as e: raise CMSException(500, "Unicode encode error") raise CMSException(404) def __getSiteMap(self, query, protocol): sitemap = CMSSiteMap(self.db, self.domain, self.urlBase) data = sitemap.getSiteMap(self.__rootPageIdent, protocol) try: return (data.encode("UTF-8", "strict"), "text/xml; charset=UTF-8") except UnicodeError as e: raise CMSException(500, "Unicode encode error") def __getImage(self, imagename, query, protocol, thumb=False): if not imagename: raise CMSException(404) try: lower = imagename.lower() if lower.endswith(".jpg"): mime = "image/jpeg" elif lower.endswith(".svg"): mime = "image/svg+xml" else: i = lower.rfind(".") if i < 0: raise CMSException(404) mime = f"image/{lower[i+1:]}" imgData = self.db.getImage(imagename) if not imgData: raise CMSException(404) if thumb: width = query.getInt("w", 300) height = query.getInt("h", 300) qual = query.getInt("q", 1) qualities = { 0 : Image.NEAREST, 1 : Image.BILINEAR, 2 : Image.BICUBIC, 3 : getattr(Image, "LANCZOS", Image.BICUBIC), } try: qual = qualities[qual] except (KeyError) as e: qual = qualities[1] with Image.open(BytesIO(imgData)) as img: img.thumbnail((width, height), qual) with img.convert("RGB") as cimg: output = BytesIO() cimg.save(output, "JPEG") imgData = output.getvalue() mime = "image/jpeg" except (IOError, UnicodeError) as e: raise CMSException(404) return imgData, mime def __getHtmlPage(self, pageIdent, query, protocol): pageTitle, pageData, stamp = self.db.getPage(pageIdent) if not pageData: raise CMSException(404) resolverVariables = { "PROTOCOL" : lambda r, n: protocol, "PAGEIDENT" : lambda r, n: pageIdent.getUrl(), "CMS_PAGEIDENT" : lambda r, n: pageIdent.getUrl(urlBase=self.urlBase), "GROUP" : lambda r, n: pageIdent.get(0), "PAGE" : lambda r, n: pageIdent.get(1), } resolve = self.resolver.resolve for k, v in query.items(): k = k.upper() resolverVariables["Q_" + k] = self.resolver.escape(htmlEscape(v)) resolverVariables["QRAW_" + k] = self.resolver.escape(v) pageTitle = resolve(pageTitle, resolverVariables, pageIdent) resolverVariables["TITLE"] = lambda r, n: pageTitle pageData = resolve(pageData, resolverVariables, pageIdent) extraHeaders = resolve(self.db.getHeaders(pageIdent), resolverVariables, pageIdent) data = [self.__genHtmlHeader(pageTitle, extraHeaders)] data.append(self.__genHtmlBody(pageIdent, pageTitle, pageData, protocol, stamp)) data.append(self.__genHtmlFooter()) try: return ("".join(data).encode("UTF-8", "strict"), "application/xhtml+xml; charset=UTF-8") except UnicodeError as e: raise CMSException(500, "Unicode encode error") def __get(self, path, query, protocol): pageIdent = CMSPageIdent.parse(path) firstIdent = pageIdent.get(0, allowSysNames=True) if firstIdent == "__thumbs": return self.__getImage(pageIdent.get(1), query, protocol, thumb=True) elif firstIdent == "__images": return self.__getImage(pageIdent.get(1), query, protocol, thumb=False) elif firstIdent in ("__sitemap", "__sitemap.xml"): return self.__getSiteMap(query, protocol) elif firstIdent == "__css": return self.__getCss(pageIdent.get(1), query, protocol) return self.__getHtmlPage(pageIdent, query, protocol) def get(self, path, query={}, protocol="http"): query = CMSQuery(query) return self.__get(path, query, protocol) def __post(self, path, query, body, bodyType, protocol): pageIdent = CMSPageIdent.parse(path) formFields = CMSFormFields(body, bodyType) try: ret = self.db.runPostHandler(pageIdent, formFields, query) except CMSException as e: raise e except Exception as e: msg = "" if self.debug: msg = " " + str(e) msg = msg.encode("UTF-8", "ignore") return (b"Failed to run POST handler." + msg, "text/plain") if ret is None: return self.__get(path, query, protocol) assert isinstance(ret, tuple) and len(ret) == 2, "post() return is not 2-tuple." assert isinstance(ret[0], (bytes, bytearray)), "post()[0] is not bytes." assert isinstance(ret[1], str), "post()[1] is not str." return ret def post(self, path, query={}, body=b"", bodyType="text/plain", protocol="http"): query = CMSQuery(query) return self.__post(path, query, body, bodyType, protocol) def __doGetErrorPage(self, cmsExcept, protocol): resolverVariables = { "PROTOCOL" : lambda r, n: protocol, "GROUP" : lambda r, n: "_nogroup_", "PAGE" : lambda r, n: "_nopage_", "HTTP_STATUS" : lambda r, n: cmsExcept.httpStatus, "HTTP_STATUS_CODE" : lambda r, n: str(cmsExcept.httpStatusCode), "ERROR_MESSAGE" : lambda r, n: self.resolver.escape(htmlEscape(cmsExcept.message)), } pageHeader = cmsExcept.getHtmlHeader(self.db) pageHeader = self.resolver.resolve(pageHeader, resolverVariables) pageData = cmsExcept.getHtmlBody(self.db) pageData = self.resolver.resolve(pageData, resolverVariables) httpHeaders = cmsExcept.getHttpHeaders( lambda s: self.resolver.resolve(s, resolverVariables)) data = [self.__genHtmlHeader(cmsExcept.httpStatus, additional=pageHeader)] data.append(self.__genHtmlBody(CMSPageIdent(("_nogroup_", "_nopage_")), cmsExcept.httpStatus, pageData, protocol, genCheckerLinks=False)) data.append(self.__genHtmlFooter()) return "".join(data), "application/xhtml+xml; charset=UTF-8", httpHeaders def getErrorPage(self, cmsExcept, protocol="http"): try: data, mime, headers = self.__doGetErrorPage(cmsExcept, protocol) except (CMSException) as e: data = "Error in exception handler: %s %s" % \ (e.httpStatus, e.message) mime, headers = "text/plain; charset=UTF-8", [] try: return data.encode("UTF-8", "strict"), mime, headers except UnicodeError as e: # Whoops. All is lost. raise CMSException(500, "Unicode encode error")