123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394 |
- # -*- coding: utf-8 -*-
- #
- # cms.py - simple WSGI/Python based CMS script
- #
- # Copyright (C) 2011-2021 Michael Buesch <m@bues.ch>
- #
- # 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 <http://www.gnu.org/licenses/>.
- #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"""<?xml version="1.0" encoding="UTF-8" ?>
- <!DOCTYPE html>
- <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
- <head>
- <!--
- Generated by "cms.py - simple CMS script"
- https://bues.ch/cgit/cms.git
- -->
- <meta name="generator" content="Rust and {interpreter} based CMS" />
- <meta name="date" content="{date}" />
- <meta name="robots" content="all" />
- <title>{title}</title>
- <link rel="stylesheet" href="{self.urlBase}/__css/cms.css" type="text/css" />
- <link rel="sitemap" type="application/xml" title="Sitemap" href="{sitemap}" />
- {additional or ''}
- </head>
- <body>
- """
- def __genHtmlFooter(self):
- footer = """
- </body>
- </html>
- """
- 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<div class="navelems">' % tabs)
- for pagename, pagelabel, pageprio in subPages:
- if not pagelabel:
- continue
- body.append('%s\t<div class="%s"> '
- '<!-- %d -->' % (
- tabs,
- "navelem" if indent > 0 else "navgroup",
- pageprio))
- if indent <= 0:
- body.append('%s\t\t<div class="navhead">' %\
- tabs)
- subPageIdent = CMSPageIdent(basePageIdent + [pagename])
- isActive = (activePageIdent is not None and
- activePageIdent.startswith(subPageIdent))
- if isActive:
- body.append('%s\t\t<div class="navactive">' %\
- tabs)
- body.append('%s\t\t<a href="%s">%s</a>' %\
- (tabs,
- subPageIdent.getUrl(urlBase=self.urlBase),
- pagelabel))
- if isActive:
- body.append('%s\t\t</div> '
- '<!-- class="navactive" -->' %\
- tabs)
- if indent <= 0:
- body.append('%s\t\t</div>' % tabs)
- self._genNavElem(body, subPageIdent,
- activePageIdent, indent + 2)
- body.append('%s\t</div>' % tabs)
- if indent > 0:
- body.append('%s</div>' % tabs)
- def __genHtmlBody(self, pageIdent, pageTitle, pageData,
- protocol,
- stamp=None, genCheckerLinks=True):
- body = []
- # Generate logo / title bar
- body.append('<div class="titlebar">')
- body.append('\t<div class="logo">')
- body.append('\t\t<a href="%s">' % self.urlBase)
- body.append(f'\t\t\t<img alt="logo" src="{self.urlBase}/__images/logo.png" />')
- body.append('\t\t</a>')
- body.append('\t</div>')
- body.append('\t<div class="title">%s</div>' % pageTitle)
- body.append('</div>\n')
- # Generate navigation bar
- body.append('<div class="navbar">')
- body.append('\t<div class="navgroups">')
- body.append('\t\t<div class="navhome">')
- rootActive = not pageIdent
- if rootActive:
- body.append('\t\t<div class="navactive">')
- body.append('\t\t\t<a href="%s">%s</a>' %\
- (self.__rootPageIdent.getUrl(urlBase=self.urlBase),
- self.db.getString("home")))
- if rootActive:
- body.append('\t\t</div> <!-- class="navactive" -->')
- body.append('\t\t</div>')
- self._genNavElem(body, self.__rootPageIdent, pageIdent)
- body.append('\t</div>')
- body.append('</div>\n')
- body.append('<div class="main">\n') # Main body start
- # Page content
- body.append('<!-- BEGIN: page content -->')
- body.append(pageData)
- body.append('<!-- END: page content -->\n')
- if stamp:
- # Last-modified date
- body.append('\t<div class="modifystamp">')
- body.append(stamp.strftime('\t\tUpdated: %A %d %B %Y %H:%M (UTC)'))
- body.append('\t</div>')
- if protocol != "https":
- # SSL
- body.append('\t<div class="ssl">')
- body.append('\t\t<a href="%s">%s</a>' % (
- pageIdent.getUrl("https", self.domain,
- self.urlBase),
- self.db.getString("ssl-encrypted")))
- body.append('\t</div>')
- if genCheckerLinks:
- # Checker links
- pageUrlQuoted = urllib.parse.quote_plus(
- pageIdent.getUrl("http", self.domain,
- self.urlBase))
- body.append('\t<div class="checker">')
- 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<a href="%s">%s</a> /' %\
- (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<a href="%s">%s</a>' %\
- (checkerUrl, self.db.getString("checker-css")))
- body.append('\t</div>\n')
- body.append('</div>\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")
|