From 95dfe14528663923ca2a88ec928f1d8d9df2402b Mon Sep 17 00:00:00 2001 From: bai Date: Fri, 29 Mar 2019 02:14:43 +0000 Subject: Init --- cgi/.htaccess | 9 + cgi/BeautifulSoup.py | 2017 ++++++++++++++++++ cgi/GeoIP.dat | Bin 0 -> 878459 bytes cgi/anarkia.py | 439 ++++ cgi/api.py | 392 ++++ cgi/database.py | 69 + cgi/fcgi.py | 1332 ++++++++++++ cgi/formatting.py | 425 ++++ cgi/framework.py | 467 +++++ cgi/geoip.py | 128 ++ cgi/img.py | 416 ++++ cgi/locale/es/LC_MESSAGES/weabot.mo | Bin 0 -> 17305 bytes cgi/manage.py | 1823 +++++++++++++++++ cgi/markdown.py | 2044 +++++++++++++++++++ cgi/oekaki.py | 176 ++ cgi/post.py | 1260 ++++++++++++ cgi/proxy.txt | 3251 ++++++++++++++++++++++++++++++ cgi/quotes.conf | 13 + cgi/template.py | 117 ++ cgi/templates/anarkia.html | 329 +++ cgi/templates/banned.html | 34 + cgi/templates/base_bottom.html | 3 + cgi/templates/base_top.html | 55 + cgi/templates/board.0.html | 230 +++ cgi/templates/board.html | 264 +++ cgi/templates/board.jp.html | 271 +++ cgi/templates/catalog.html | 30 + cgi/templates/error.html | 7 + cgi/templates/exception.html | 36 + cgi/templates/home.rss | 24 + cgi/templates/htaccess | 24 + cgi/templates/kako.html | 60 + cgi/templates/manage/addboard.html | 21 + cgi/templates/manage/bans.html | 92 + cgi/templates/manage/boardoptions.html | 195 ++ cgi/templates/manage/changepassword.html | 24 + cgi/templates/manage/delete.html | 23 + cgi/templates/manage/filters.html | 119 ++ cgi/templates/manage/ipdelete.html | 24 + cgi/templates/manage/ipshow.html | 73 + cgi/templates/manage/lockboard.html | 20 + cgi/templates/manage/login.html | 21 + cgi/templates/manage/logs.html | 17 + cgi/templates/manage/manage.html | 22 + cgi/templates/manage/menu.html | 30 + cgi/templates/manage/message.html | 8 + cgi/templates/manage/mod.html | 96 + cgi/templates/manage/move.html | 60 + cgi/templates/manage/quotes.html | 12 + cgi/templates/manage/rebuild.html | 20 + cgi/templates/manage/recent_images.html | 24 + cgi/templates/manage/recyclebin.html | 72 + cgi/templates/manage/reports.html | 58 + cgi/templates/manage/search.html | 27 + cgi/templates/manage/staff.html | 63 + cgi/templates/mobile/base_top.html | 14 + cgi/templates/mobile/board.html | 55 + cgi/templates/mobile/error.html | 6 + cgi/templates/mobile/latest.html | 14 + cgi/templates/mobile/newest.html | 14 + cgi/templates/mobile/threadlist.html | 43 + cgi/templates/mobile/txt_newthread.html | 35 + cgi/templates/mobile/txt_thread.html | 74 + cgi/templates/mobile/txt_threadlist.html | 26 + cgi/templates/mod.html | 86 + cgi/templates/navbar.html | 16 + cgi/templates/paint.html | 79 + cgi/templates/redirect.html | 12 + cgi/templates/report.html | 29 + cgi/templates/revision.html | 1 + cgi/templates/stats.html | 163 ++ cgi/templates/txt_archive.html | 104 + cgi/templates/txt_base_top.html | 44 + cgi/templates/txt_board.en.html | 137 ++ cgi/templates/txt_board.html | 137 ++ cgi/templates/txt_error.html | 50 + cgi/templates/txt_thread.en.html | 105 + cgi/templates/txt_thread.html | 101 + cgi/templates/txt_threadlist.html | 67 + cgi/tenjin.py | 2118 +++++++++++++++++++ cgi/tor.txt | 1140 +++++++++++ cgi/weabot.py | 1021 ++++++++++ 82 files changed, 22557 insertions(+) create mode 100644 cgi/.htaccess create mode 100644 cgi/BeautifulSoup.py create mode 100644 cgi/GeoIP.dat create mode 100644 cgi/anarkia.py create mode 100644 cgi/api.py create mode 100644 cgi/database.py create mode 100644 cgi/fcgi.py create mode 100644 cgi/formatting.py create mode 100644 cgi/framework.py create mode 100644 cgi/geoip.py create mode 100644 cgi/img.py create mode 100644 cgi/locale/es/LC_MESSAGES/weabot.mo create mode 100644 cgi/manage.py create mode 100644 cgi/markdown.py create mode 100644 cgi/oekaki.py create mode 100644 cgi/post.py create mode 100644 cgi/proxy.txt create mode 100644 cgi/quotes.conf create mode 100644 cgi/template.py create mode 100644 cgi/templates/anarkia.html create mode 100644 cgi/templates/banned.html create mode 100644 cgi/templates/base_bottom.html create mode 100644 cgi/templates/base_top.html create mode 100644 cgi/templates/board.0.html create mode 100644 cgi/templates/board.html create mode 100644 cgi/templates/board.jp.html create mode 100644 cgi/templates/catalog.html create mode 100644 cgi/templates/error.html create mode 100644 cgi/templates/exception.html create mode 100644 cgi/templates/home.rss create mode 100644 cgi/templates/htaccess create mode 100644 cgi/templates/kako.html create mode 100644 cgi/templates/manage/addboard.html create mode 100644 cgi/templates/manage/bans.html create mode 100644 cgi/templates/manage/boardoptions.html create mode 100644 cgi/templates/manage/changepassword.html create mode 100644 cgi/templates/manage/delete.html create mode 100644 cgi/templates/manage/filters.html create mode 100644 cgi/templates/manage/ipdelete.html create mode 100644 cgi/templates/manage/ipshow.html create mode 100644 cgi/templates/manage/lockboard.html create mode 100644 cgi/templates/manage/login.html create mode 100644 cgi/templates/manage/logs.html create mode 100644 cgi/templates/manage/manage.html create mode 100644 cgi/templates/manage/menu.html create mode 100644 cgi/templates/manage/message.html create mode 100644 cgi/templates/manage/mod.html create mode 100644 cgi/templates/manage/move.html create mode 100644 cgi/templates/manage/quotes.html create mode 100644 cgi/templates/manage/rebuild.html create mode 100644 cgi/templates/manage/recent_images.html create mode 100644 cgi/templates/manage/recyclebin.html create mode 100644 cgi/templates/manage/reports.html create mode 100644 cgi/templates/manage/search.html create mode 100644 cgi/templates/manage/staff.html create mode 100644 cgi/templates/mobile/base_top.html create mode 100644 cgi/templates/mobile/board.html create mode 100644 cgi/templates/mobile/error.html create mode 100644 cgi/templates/mobile/latest.html create mode 100644 cgi/templates/mobile/newest.html create mode 100644 cgi/templates/mobile/threadlist.html create mode 100644 cgi/templates/mobile/txt_newthread.html create mode 100644 cgi/templates/mobile/txt_thread.html create mode 100644 cgi/templates/mobile/txt_threadlist.html create mode 100644 cgi/templates/mod.html create mode 100644 cgi/templates/navbar.html create mode 100644 cgi/templates/paint.html create mode 100644 cgi/templates/redirect.html create mode 100644 cgi/templates/report.html create mode 100644 cgi/templates/revision.html create mode 100644 cgi/templates/stats.html create mode 100644 cgi/templates/txt_archive.html create mode 100644 cgi/templates/txt_base_top.html create mode 100644 cgi/templates/txt_board.en.html create mode 100644 cgi/templates/txt_board.html create mode 100644 cgi/templates/txt_error.html create mode 100644 cgi/templates/txt_thread.en.html create mode 100644 cgi/templates/txt_thread.html create mode 100644 cgi/templates/txt_threadlist.html create mode 100644 cgi/tenjin.py create mode 100644 cgi/tor.txt create mode 100755 cgi/weabot.py (limited to 'cgi') diff --git a/cgi/.htaccess b/cgi/.htaccess new file mode 100644 index 0000000..97a4f17 --- /dev/null +++ b/cgi/.htaccess @@ -0,0 +1,9 @@ +AddHandler cgi-script .py +Options +ExecCGI + +# Uncomment if you want pretty URL (ie cgi/post) +#RewriteEngine On +#RewriteBase /cgi/ +#RewriteRule ^weabot\.py/ - [L] +#RewriteRule ^(.*)$ weabot.py/$1 [L] + diff --git a/cgi/BeautifulSoup.py b/cgi/BeautifulSoup.py new file mode 100644 index 0000000..7278215 --- /dev/null +++ b/cgi/BeautifulSoup.py @@ -0,0 +1,2017 @@ +"""Beautiful Soup +Elixir and Tonic +"The Screen-Scraper's Friend" +http://www.crummy.com/software/BeautifulSoup/ + +Beautiful Soup parses a (possibly invalid) XML or HTML document into a +tree representation. It provides methods and Pythonic idioms that make +it easy to navigate, search, and modify the tree. + +A well-formed XML/HTML document yields a well-formed data +structure. An ill-formed XML/HTML document yields a correspondingly +ill-formed data structure. If your document is only locally +well-formed, you can use this library to find and process the +well-formed part of it. + +Beautiful Soup works with Python 2.2 and up. It has no external +dependencies, but you'll have more success at converting data to UTF-8 +if you also install these three packages: + +* chardet, for auto-detecting character encodings + http://chardet.feedparser.org/ +* cjkcodecs and iconv_codec, which add more encodings to the ones supported + by stock Python. + http://cjkpython.i18n.org/ + +Beautiful Soup defines classes for two main parsing strategies: + + * BeautifulStoneSoup, for parsing XML, SGML, or your domain-specific + language that kind of looks like XML. + + * BeautifulSoup, for parsing run-of-the-mill HTML code, be it valid + or invalid. This class has web browser-like heuristics for + obtaining a sensible parse tree in the face of common HTML errors. + +Beautiful Soup also defines a class (UnicodeDammit) for autodetecting +the encoding of an HTML or XML document, and converting it to +Unicode. Much of this code is taken from Mark Pilgrim's Universal Feed Parser. + +For more than you ever wanted to know about Beautiful Soup, see the +documentation: +http://www.crummy.com/software/BeautifulSoup/documentation.html + +Here, have some legalese: + +Copyright (c) 2004-2010, Leonard Richardson + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the the Beautiful Soup Consortium and All + Night Kosher Bakery nor the names of its contributors may be + used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE, DAMMIT. + +""" +from __future__ import generators + +__author__ = "Leonard Richardson (leonardr@segfault.org)" +__version__ = "3.2.1" +__copyright__ = "Copyright (c) 2004-2012 Leonard Richardson" +__license__ = "New-style BSD" + +from sgmllib import SGMLParser, SGMLParseError +import codecs +import markupbase +import types +import re +import sgmllib +try: + from htmlentitydefs import name2codepoint +except ImportError: + name2codepoint = {} +try: + set +except NameError: + from sets import Set as set + +#These hacks make Beautiful Soup able to parse XML with namespaces +sgmllib.tagfind = re.compile('[a-zA-Z][-_.:a-zA-Z0-9]*') +markupbase._declname_match = re.compile(r'[a-zA-Z][-_.:a-zA-Z0-9]*\s*').match + +DEFAULT_OUTPUT_ENCODING = "utf-8" + +def _match_css_class(str): + """Build a RE to match the given CSS class.""" + return re.compile(r"(^|.*\s)%s($|\s)" % str) + +# First, the classes that represent markup elements. + +class PageElement(object): + """Contains the navigational information for some part of the page + (either a tag or a piece of text)""" + + def _invert(h): + "Cheap function to invert a hash." + i = {} + for k,v in h.items(): + i[v] = k + return i + + XML_ENTITIES_TO_SPECIAL_CHARS = { "apos" : "'", + "quot" : '"', + "amp" : "&", + "lt" : "<", + "gt" : ">" } + + XML_SPECIAL_CHARS_TO_ENTITIES = _invert(XML_ENTITIES_TO_SPECIAL_CHARS) + + def setup(self, parent=None, previous=None): + """Sets up the initial relations between this element and + other elements.""" + self.parent = parent + self.previous = previous + self.next = None + self.previousSibling = None + self.nextSibling = None + if self.parent and self.parent.contents: + self.previousSibling = self.parent.contents[-1] + self.previousSibling.nextSibling = self + + def replaceWith(self, replaceWith): + oldParent = self.parent + myIndex = self.parent.index(self) + if hasattr(replaceWith, "parent")\ + and replaceWith.parent is self.parent: + # We're replacing this element with one of its siblings. + index = replaceWith.parent.index(replaceWith) + if index and index < myIndex: + # Furthermore, it comes before this element. That + # means that when we extract it, the index of this + # element will change. + myIndex = myIndex - 1 + self.extract() + oldParent.insert(myIndex, replaceWith) + + def replaceWithChildren(self): + myParent = self.parent + myIndex = self.parent.index(self) + self.extract() + reversedChildren = list(self.contents) + reversedChildren.reverse() + for child in reversedChildren: + myParent.insert(myIndex, child) + + def extract(self): + """Destructively rips this element out of the tree.""" + if self.parent: + try: + del self.parent.contents[self.parent.index(self)] + except ValueError: + pass + + #Find the two elements that would be next to each other if + #this element (and any children) hadn't been parsed. Connect + #the two. + lastChild = self._lastRecursiveChild() + nextElement = lastChild.next + + if self.previous: + self.previous.next = nextElement + if nextElement: + nextElement.previous = self.previous + self.previous = None + lastChild.next = None + + self.parent = None + if self.previousSibling: + self.previousSibling.nextSibling = self.nextSibling + if self.nextSibling: + self.nextSibling.previousSibling = self.previousSibling + self.previousSibling = self.nextSibling = None + return self + + def _lastRecursiveChild(self): + "Finds the last element beneath this object to be parsed." + lastChild = self + while hasattr(lastChild, 'contents') and lastChild.contents: + lastChild = lastChild.contents[-1] + return lastChild + + def insert(self, position, newChild): + if isinstance(newChild, basestring) \ + and not isinstance(newChild, NavigableString): + newChild = NavigableString(newChild) + + position = min(position, len(self.contents)) + if hasattr(newChild, 'parent') and newChild.parent is not None: + # We're 'inserting' an element that's already one + # of this object's children. + if newChild.parent is self: + index = self.index(newChild) + if index > position: + # Furthermore we're moving it further down the + # list of this object's children. That means that + # when we extract this element, our target index + # will jump down one. + position = position - 1 + newChild.extract() + + newChild.parent = self + previousChild = None + if position == 0: + newChild.previousSibling = None + newChild.previous = self + else: + previousChild = self.contents[position-1] + newChild.previousSibling = previousChild + newChild.previousSibling.nextSibling = newChild + newChild.previous = previousChild._lastRecursiveChild() + if newChild.previous: + newChild.previous.next = newChild + + newChildsLastElement = newChild._lastRecursiveChild() + + if position >= len(self.contents): + newChild.nextSibling = None + + parent = self + parentsNextSibling = None + while not parentsNextSibling: + parentsNextSibling = parent.nextSibling + parent = parent.parent + if not parent: # This is the last element in the document. + break + if parentsNextSibling: + newChildsLastElement.next = parentsNextSibling + else: + newChildsLastElement.next = None + else: + nextChild = self.contents[position] + newChild.nextSibling = nextChild + if newChild.nextSibling: + newChild.nextSibling.previousSibling = newChild + newChildsLastElement.next = nextChild + + if newChildsLastElement.next: + newChildsLastElement.next.previous = newChildsLastElement + self.contents.insert(position, newChild) + + def append(self, tag): + """Appends the given tag to the contents of this tag.""" + self.insert(len(self.contents), tag) + + def findNext(self, name=None, attrs={}, text=None, **kwargs): + """Returns the first item that matches the given criteria and + appears after this Tag in the document.""" + return self._findOne(self.findAllNext, name, attrs, text, **kwargs) + + def findAllNext(self, name=None, attrs={}, text=None, limit=None, + **kwargs): + """Returns all items that match the given criteria and appear + after this Tag in the document.""" + return self._findAll(name, attrs, text, limit, self.nextGenerator, + **kwargs) + + def findNextSibling(self, name=None, attrs={}, text=None, **kwargs): + """Returns the closest sibling to this Tag that matches the + given criteria and appears after this Tag in the document.""" + return self._findOne(self.findNextSiblings, name, attrs, text, + **kwargs) + + def findNextSiblings(self, name=None, attrs={}, text=None, limit=None, + **kwargs): + """Returns the siblings of this Tag that match the given + criteria and appear after this Tag in the document.""" + return self._findAll(name, attrs, text, limit, + self.nextSiblingGenerator, **kwargs) + fetchNextSiblings = findNextSiblings # Compatibility with pre-3.x + + def findPrevious(self, name=None, attrs={}, text=None, **kwargs): + """Returns the first item that matches the given criteria and + appears before this Tag in the document.""" + return self._findOne(self.findAllPrevious, name, attrs, text, **kwargs) + + def findAllPrevious(self, name=None, attrs={}, text=None, limit=None, + **kwargs): + """Returns all items that match the given criteria and appear + before this Tag in the document.""" + return self._findAll(name, attrs, text, limit, self.previousGenerator, + **kwargs) + fetchPrevious = findAllPrevious # Compatibility with pre-3.x + + def findPreviousSibling(self, name=None, attrs={}, text=None, **kwargs): + """Returns the closest sibling to this Tag that matches the + given criteria and appears before this Tag in the document.""" + return self._findOne(self.findPreviousSiblings, name, attrs, text, + **kwargs) + + def findPreviousSiblings(self, name=None, attrs={}, text=None, + limit=None, **kwargs): + """Returns the siblings of this Tag that match the given + criteria and appear before this Tag in the document.""" + return self._findAll(name, attrs, text, limit, + self.previousSiblingGenerator, **kwargs) + fetchPreviousSiblings = findPreviousSiblings # Compatibility with pre-3.x + + def findParent(self, name=None, attrs={}, **kwargs): + """Returns the closest parent of this Tag that matches the given + criteria.""" + # NOTE: We can't use _findOne because findParents takes a different + # set of arguments. + r = None + l = self.findParents(name, attrs, 1) + if l: + r = l[0] + return r + + def findParents(self, name=None, attrs={}, limit=None, **kwargs): + """Returns the parents of this Tag that match the given + criteria.""" + + return self._findAll(name, attrs, None, limit, self.parentGenerator, + **kwargs) + fetchParents = findParents # Compatibility with pre-3.x + + #These methods do the real heavy lifting. + + def _findOne(self, method, name, attrs, text, **kwargs): + r = None + l = method(name, attrs, text, 1, **kwargs) + if l: + r = l[0] + return r + + def _findAll(self, name, attrs, text, limit, generator, **kwargs): + "Iterates over a generator looking for things that match." + + if isinstance(name, SoupStrainer): + strainer = name + # (Possibly) special case some findAll*(...) searches + elif text is None and not limit and not attrs and not kwargs: + # findAll*(True) + if name is True: + return [element for element in generator() + if isinstance(element, Tag)] + # findAll*('tag-name') + elif isinstance(name, basestring): + return [element for element in generator() + if isinstance(element, Tag) and + element.name == name] + else: + strainer = SoupStrainer(name, attrs, text, **kwargs) + # Build a SoupStrainer + else: + strainer = SoupStrainer(name, attrs, text, **kwargs) + results = ResultSet(strainer) + g = generator() + while True: + try: + i = g.next() + except StopIteration: + break + if i: + found = strainer.search(i) + if found: + results.append(found) + if limit and len(results) >= limit: + break + return results + + #These Generators can be used to navigate starting from both + #NavigableStrings and Tags. + def nextGenerator(self): + i = self + while i is not None: + i = i.next + yield i + + def nextSiblingGenerator(self): + i = self + while i is not None: + i = i.nextSibling + yield i + + def previousGenerator(self): + i = self + while i is not None: + i = i.previous + yield i + + def previousSiblingGenerator(self): + i = self + while i is not None: + i = i.previousSibling + yield i + + def parentGenerator(self): + i = self + while i is not None: + i = i.parent + yield i + + # Utility methods + def substituteEncoding(self, str, encoding=None): + encoding = encoding or "utf-8" + return str.replace("%SOUP-ENCODING%", encoding) + + def toEncoding(self, s, encoding=None): + """Encodes an object to a string in some encoding, or to Unicode. + .""" + if isinstance(s, unicode): + if encoding: + s = s.encode(encoding) + elif isinstance(s, str): + if encoding: + s = s.encode(encoding) + else: + s = unicode(s) + else: + if encoding: + s = self.toEncoding(str(s), encoding) + else: + s = unicode(s) + return s + + BARE_AMPERSAND_OR_BRACKET = re.compile("([<>]|" + + "&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)" + + ")") + + def _sub_entity(self, x): + """Used with a regular expression to substitute the + appropriate XML entity for an XML special character.""" + return "&" + self.XML_SPECIAL_CHARS_TO_ENTITIES[x.group(0)[0]] + ";" + + +class NavigableString(unicode, PageElement): + + def __new__(cls, value): + """Create a new NavigableString. + + When unpickling a NavigableString, this method is called with + the string in DEFAULT_OUTPUT_ENCODING. That encoding needs to be + passed in to the superclass's __new__ or the superclass won't know + how to handle non-ASCII characters. + """ + if isinstance(value, unicode): + return unicode.__new__(cls, value) + return unicode.__new__(cls, value, DEFAULT_OUTPUT_ENCODING) + + def __getnewargs__(self): + return (NavigableString.__str__(self),) + + def __getattr__(self, attr): + """text.string gives you text. This is for backwards + compatibility for Navigable*String, but for CData* it lets you + get the string without the CData wrapper.""" + if attr == 'string': + return self + else: + raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__.__name__, attr) + + def __unicode__(self): + return str(self).decode(DEFAULT_OUTPUT_ENCODING) + + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + # Substitute outgoing XML entities. + data = self.BARE_AMPERSAND_OR_BRACKET.sub(self._sub_entity, self) + if encoding: + return data.encode(encoding) + else: + return data + +class CData(NavigableString): + + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + return "" % NavigableString.__str__(self, encoding) + +class ProcessingInstruction(NavigableString): + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + output = self + if "%SOUP-ENCODING%" in output: + output = self.substituteEncoding(output, encoding) + return "" % self.toEncoding(output, encoding) + +class Comment(NavigableString): + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + return "" % NavigableString.__str__(self, encoding) + +class Declaration(NavigableString): + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING): + return "" % NavigableString.__str__(self, encoding) + +class Tag(PageElement): + + """Represents a found HTML tag with its attributes and contents.""" + + def _convertEntities(self, match): + """Used in a call to re.sub to replace HTML, XML, and numeric + entities with the appropriate Unicode characters. If HTML + entities are being converted, any unrecognized entities are + escaped.""" + x = match.group(1) + if self.convertHTMLEntities and x in name2codepoint: + return unichr(name2codepoint[x]) + elif x in self.XML_ENTITIES_TO_SPECIAL_CHARS: + if self.convertXMLEntities: + return self.XML_ENTITIES_TO_SPECIAL_CHARS[x] + else: + return u'&%s;' % x + elif len(x) > 0 and x[0] == '#': + # Handle numeric entities + if len(x) > 1 and x[1] == 'x': + return unichr(int(x[2:], 16)) + else: + return unichr(int(x[1:])) + + elif self.escapeUnrecognizedEntities: + return u'&%s;' % x + else: + return u'&%s;' % x + + def __init__(self, parser, name, attrs=None, parent=None, + previous=None): + "Basic constructor." + + # We don't actually store the parser object: that lets extracted + # chunks be garbage-collected + self.parserClass = parser.__class__ + self.isSelfClosing = parser.isSelfClosingTag(name) + self.name = name + if attrs is None: + attrs = [] + elif isinstance(attrs, dict): + attrs = attrs.items() + self.attrs = attrs + self.contents = [] + self.setup(parent, previous) + self.hidden = False + self.containsSubstitutions = False + self.convertHTMLEntities = parser.convertHTMLEntities + self.convertXMLEntities = parser.convertXMLEntities + self.escapeUnrecognizedEntities = parser.escapeUnrecognizedEntities + + # Convert any HTML, XML, or numeric entities in the attribute values. + convert = lambda(k, val): (k, + re.sub("&(#\d+|#x[0-9a-fA-F]+|\w+);", + self._convertEntities, + val)) + self.attrs = map(convert, self.attrs) + + def getString(self): + if (len(self.contents) == 1 + and isinstance(self.contents[0], NavigableString)): + return self.contents[0] + + def setString(self, string): + """Replace the contents of the tag with a string""" + self.clear() + self.append(string) + + string = property(getString, setString) + + def getText(self, separator=u""): + if not len(self.contents): + return u"" + stopNode = self._lastRecursiveChild().next + strings = [] + current = self.contents[0] + while current is not stopNode: + if isinstance(current, NavigableString): + strings.append(current.strip()) + current = current.next + return separator.join(strings) + + text = property(getText) + + def get(self, key, default=None): + """Returns the value of the 'key' attribute for the tag, or + the value given for 'default' if it doesn't have that + attribute.""" + return self._getAttrMap().get(key, default) + + def clear(self): + """Extract all children.""" + for child in self.contents[:]: + child.extract() + + def index(self, element): + for i, child in enumerate(self.contents): + if child is element: + return i + raise ValueError("Tag.index: element not in tag") + + def has_key(self, key): + return self._getAttrMap().has_key(key) + + def __getitem__(self, key): + """tag[key] returns the value of the 'key' attribute for the tag, + and throws an exception if it's not there.""" + return self._getAttrMap()[key] + + def __iter__(self): + "Iterating over a tag iterates over its contents." + return iter(self.contents) + + def __len__(self): + "The length of a tag is the length of its list of contents." + return len(self.contents) + + def __contains__(self, x): + return x in self.contents + + def __nonzero__(self): + "A tag is non-None even if it has no contents." + return True + + def __setitem__(self, key, value): + """Setting tag[key] sets the value of the 'key' attribute for the + tag.""" + self._getAttrMap() + self.attrMap[key] = value + found = False + for i in range(0, len(self.attrs)): + if self.attrs[i][0] == key: + self.attrs[i] = (key, value) + found = True + if not found: + self.attrs.append((key, value)) + self._getAttrMap()[key] = value + + def __delitem__(self, key): + "Deleting tag[key] deletes all 'key' attributes for the tag." + for item in self.attrs: + if item[0] == key: + self.attrs.remove(item) + #We don't break because bad HTML can define the same + #attribute multiple times. + self._getAttrMap() + if self.attrMap.has_key(key): + del self.attrMap[key] + + def __call__(self, *args, **kwargs): + """Calling a tag like a function is the same as calling its + findAll() method. Eg. tag('a') returns a list of all the A tags + found within this tag.""" + return apply(self.findAll, args, kwargs) + + def __getattr__(self, tag): + #print "Getattr %s.%s" % (self.__class__, tag) + if len(tag) > 3 and tag.rfind('Tag') == len(tag)-3: + return self.find(tag[:-3]) + elif tag.find('__') != 0: + return self.find(tag) + raise AttributeError, "'%s' object has no attribute '%s'" % (self.__class__, tag) + + def __eq__(self, other): + """Returns true iff this tag has the same name, the same attributes, + and the same contents (recursively) as the given tag. + + NOTE: right now this will return false if two tags have the + same attributes in a different order. Should this be fixed?""" + if other is self: + return True + if not hasattr(other, 'name') or not hasattr(other, 'attrs') or not hasattr(other, 'contents') or self.name != other.name or self.attrs != other.attrs or len(self) != len(other): + return False + for i in range(0, len(self.contents)): + if self.contents[i] != other.contents[i]: + return False + return True + + def __ne__(self, other): + """Returns true iff this tag is not identical to the other tag, + as defined in __eq__.""" + return not self == other + + def __repr__(self, encoding=DEFAULT_OUTPUT_ENCODING): + """Renders this tag as a string.""" + return self.__str__(encoding) + + def __unicode__(self): + return self.__str__(None) + + def __str__(self, encoding=DEFAULT_OUTPUT_ENCODING, + prettyPrint=False, indentLevel=0): + """Returns a string or Unicode representation of this tag and + its contents. To get Unicode, pass None for encoding. + + NOTE: since Python's HTML parser consumes whitespace, this + method is not certain to reproduce the whitespace present in + the original string.""" + + encodedName = self.toEncoding(self.name, encoding) + + attrs = [] + if self.attrs: + for key, val in self.attrs: + fmt = '%s="%s"' + if isinstance(val, basestring): + if self.containsSubstitutions and '%SOUP-ENCODING%' in val: + val = self.substituteEncoding(val, encoding) + + # The attribute value either: + # + # * Contains no embedded double quotes or single quotes. + # No problem: we enclose it in double quotes. + # * Contains embedded single quotes. No problem: + # double quotes work here too. + # * Contains embedded double quotes. No problem: + # we enclose it in single quotes. + # * Embeds both single _and_ double quotes. This + # can't happen naturally, but it can happen if + # you modify an attribute value after parsing + # the document. Now we have a bit of a + # problem. We solve it by enclosing the + # attribute in single quotes, and escaping any + # embedded single quotes to XML entities. + if '"' in val: + fmt = "%s='%s'" + if "'" in val: + # TODO: replace with apos when + # appropriate. + val = val.replace("'", "&squot;") + + # Now we're okay w/r/t quotes. But the attribute + # value might also contain angle brackets, or + # ampersands that aren't part of entities. We need + # to escape those to XML entities too. + val = self.BARE_AMPERSAND_OR_BRACKET.sub(self._sub_entity, val) + + attrs.append(fmt % (self.toEncoding(key, encoding), + self.toEncoding(val, encoding))) + close = '' + closeTag = '' + if self.isSelfClosing: + close = ' /' + else: + closeTag = '' % encodedName + + indentTag, indentContents = 0, 0 + if prettyPrint: + indentTag = indentLevel + space = (' ' * (indentTag-1)) + indentContents = indentTag + 1 + contents = self.renderContents(encoding, prettyPrint, indentContents) + if self.hidden: + s = contents + else: + s = [] + attributeString = '' + if attrs: + attributeString = ' ' + ' '.join(attrs) + if prettyPrint: + s.append(space) + s.append('<%s%s%s>' % (encodedName, attributeString, close)) + if prettyPrint: + s.append("\n") + s.append(contents) + if prettyPrint and contents and contents[-1] != "\n": + s.append("\n") + if prettyPrint and closeTag: + s.append(space) + s.append(closeTag) + if prettyPrint and closeTag and self.nextSibling: + s.append("\n") + s = ''.join(s) + return s + + def decompose(self): + """Recursively destroys the contents of this tree.""" + self.extract() + if len(self.contents) == 0: + return + current = self.contents[0] + while current is not None: + next = current.next + if isinstance(current, Tag): + del current.contents[:] + current.parent = None + current.previous = None + current.previousSibling = None + current.next = None + current.nextSibling = None + current = next + + def prettify(self, encoding=DEFAULT_OUTPUT_ENCODING): + return self.__str__(encoding, True) + + def renderContents(self, encoding=DEFAULT_OUTPUT_ENCODING, + prettyPrint=False, indentLevel=0): + """Renders the contents of this tag as a string in the given + encoding. If encoding is None, returns a Unicode string..""" + s=[] + for c in self: + text = None + if isinstance(c, NavigableString): + text = c.__str__(encoding) + elif isinstance(c, Tag): + s.append(c.__str__(encoding, prettyPrint, indentLevel)) + if text and prettyPrint: + text = text.strip() + if text: + if prettyPrint: + s.append(" " * (indentLevel-1)) + s.append(text) + if prettyPrint: + s.append("\n") + return ''.join(s) + + #Soup methods + + def find(self, name=None, attrs={}, recursive=True, text=None, + **kwargs): + """Return only the first child of this Tag matching the given + criteria.""" + r = None + l = self.findAll(name, attrs, recursive, text, 1, **kwargs) + if l: + r = l[0] + return r + findChild = find + + def findAll(self, name=None, attrs={}, recursive=True, text=None, + limit=None, **kwargs): + """Extracts a list of Tag objects that match the given + criteria. You can specify the name of the Tag and any + attributes you want the Tag to have. + + The value of a key-value pair in the 'attrs' map can be a + string, a list of strings, a regular expression object, or a + callable that takes a string and returns whether or not the + string matches for some custom definition of 'matches'. The + same is true of the tag name.""" + generator = self.recursiveChildGenerator + if not recursive: + generator = self.childGenerator + return self._findAll(name, attrs, text, limit, generator, **kwargs) + findChildren = findAll + + # Pre-3.x compatibility methods + first = find + fetch = findAll + + def fetchText(self, text=None, recursive=True, limit=None): + return self.findAll(text=text, recursive=recursive, limit=limit) + + def firstText(self, text=None, recursive=True): + return self.find(text=text, recursive=recursive) + + #Private methods + + def _getAttrMap(self): + """Initializes a map representation of this tag's attributes, + if not already initialized.""" + if not getattr(self, 'attrMap'): + self.attrMap = {} + for (key, value) in self.attrs: + self.attrMap[key] = value + return self.attrMap + + #Generator methods + def childGenerator(self): + # Just use the iterator from the contents + return iter(self.contents) + + def recursiveChildGenerator(self): + if not len(self.contents): + raise StopIteration + stopNode = self._lastRecursiveChild().next + current = self.contents[0] + while current is not stopNode: + yield current + current = current.next + + +# Next, a couple classes to represent queries and their results. +class SoupStrainer: + """Encapsulates a number of ways of matching a markup element (tag or + text).""" + + def __init__(self, name=None, attrs={}, text=None, **kwargs): + self.name = name + if isinstance(attrs, basestring): + kwargs['class'] = _match_css_class(attrs) + attrs = None + if kwargs: + if attrs: + attrs = attrs.copy() + attrs.update(kwargs) + else: + attrs = kwargs + self.attrs = attrs + self.text = text + + def __str__(self): + if self.text: + return self.text + else: + return "%s|%s" % (self.name, self.attrs) + + def searchTag(self, markupName=None, markupAttrs={}): + found = None + markup = None + if isinstance(markupName, Tag): + markup = markupName + markupAttrs = markup + callFunctionWithTagData = callable(self.name) \ + and not isinstance(markupName, Tag) + + if (not self.name) \ + or callFunctionWithTagData \ + or (markup and self._matches(markup, self.name)) \ + or (not markup and self._matches(markupName, self.name)): + if callFunctionWithTagData: + match = self.name(markupName, markupAttrs) + else: + match = True + markupAttrMap = None + for attr, matchAgainst in self.attrs.items(): + if not markupAttrMap: + if hasattr(markupAttrs, 'get'): + markupAttrMap = markupAttrs + else: + markupAttrMap = {} + for k,v in markupAttrs: + markupAttrMap[k] = v + attrValue = markupAttrMap.get(attr) + if not self._matches(attrValue, matchAgainst): + match = False + break + if match: + if markup: + found = markup + else: + found = markupName + return found + + def search(self, markup): + #print 'looking for %s in %s' % (self, markup) + found = None + # If given a list of items, scan it for a text element that + # matches. + if hasattr(markup, "__iter__") \ + and not isinstance(markup, Tag): + for element in markup: + if isinstance(element, NavigableString) \ + and self.search(element): + found = element + break + # If it's a Tag, make sure its name or attributes match. + # Don't bother with Tags if we're searching for text. + elif isinstance(markup, Tag): + if not self.text: + found = self.searchTag(markup) + # If it's text, make sure the text matches. + elif isinstance(markup, NavigableString) or \ + isinstance(markup, basestring): + if self._matches(markup, self.text): + found = markup + else: + raise Exception, "I don't know how to match against a %s" \ + % markup.__class__ + return found + + def _matches(self, markup, matchAgainst): + #print "Matching %s against %s" % (markup, matchAgainst) + result = False + if matchAgainst is True: + result = markup is not None + elif callable(matchAgainst): + result = matchAgainst(markup) + else: + #Custom match methods take the tag as an argument, but all + #other ways of matching match the tag name as a string. + if isinstance(markup, Tag): + markup = markup.name + if markup and not isinstance(markup, basestring): + markup = unicode(markup) + #Now we know that chunk is either a string, or None. + if hasattr(matchAgainst, 'match'): + # It's a regexp object. + result = markup and matchAgainst.search(markup) + elif hasattr(matchAgainst, '__iter__'): # list-like + result = markup in matchAgainst + elif hasattr(matchAgainst, 'items'): + result = markup.has_key(matchAgainst) + elif matchAgainst and isinstance(markup, basestring): + if isinstance(markup, unicode): + matchAgainst = unicode(matchAgainst) + else: + matchAgainst = str(matchAgainst) + + if not result: + result = matchAgainst == markup + return result + +class ResultSet(list): + """A ResultSet is just a list that keeps track of the SoupStrainer + that created it.""" + def __init__(self, source): + list.__init__([]) + self.source = source + +# Now, some helper functions. + +def buildTagMap(default, *args): + """Turns a list of maps, lists, or scalars into a single map. + Used to build the SELF_CLOSING_TAGS, NESTABLE_TAGS, and + NESTING_RESET_TAGS maps out of lists and partial maps.""" + built = {} + for portion in args: + if hasattr(portion, 'items'): + #It's a map. Merge it. + for k,v in portion.items(): + built[k] = v + elif hasattr(portion, '__iter__'): # is a list + #It's a list. Map each item to the default. + for k in portion: + built[k] = default + else: + #It's a scalar. Map it to the default. + built[portion] = default + return built + +# Now, the parser classes. + +class BeautifulStoneSoup(Tag, SGMLParser): + + """This class contains the basic parser and search code. It defines + a parser that knows nothing about tag behavior except for the + following: + + You can't close a tag without closing all the tags it encloses. + That is, "" actually means + "". + + [Another possible explanation is "", but since + this class defines no SELF_CLOSING_TAGS, it will never use that + explanation.] + + This class is useful for parsing XML or made-up markup languages, + or when BeautifulSoup makes an assumption counter to what you were + expecting.""" + + SELF_CLOSING_TAGS = {} + NESTABLE_TAGS = {} + RESET_NESTING_TAGS = {} + QUOTE_TAGS = {} + PRESERVE_WHITESPACE_TAGS = [] + + MARKUP_MASSAGE = [(re.compile('(<[^<>]*)/>'), + lambda x: x.group(1) + ' />'), + (re.compile(']*)>'), + lambda x: '') + ] + + ROOT_TAG_NAME = u'[document]' + + HTML_ENTITIES = "html" + XML_ENTITIES = "xml" + XHTML_ENTITIES = "xhtml" + # TODO: This only exists for backwards-compatibility + ALL_ENTITIES = XHTML_ENTITIES + + # Used when determining whether a text node is all whitespace and + # can be replaced with a single space. A text node that contains + # fancy Unicode spaces (usually non-breaking) should be left + # alone. + STRIP_ASCII_SPACES = { 9: None, 10: None, 12: None, 13: None, 32: None, } + + def __init__(self, markup="", parseOnlyThese=None, fromEncoding=None, + markupMassage=True, smartQuotesTo=XML_ENTITIES, + convertEntities=None, selfClosingTags=None, isHTML=False): + """The Soup object is initialized as the 'root tag', and the + provided markup (which can be a string or a file-like object) + is fed into the underlying parser. + + sgmllib will process most bad HTML, and the BeautifulSoup + class has some tricks for dealing with some HTML that kills + sgmllib, but Beautiful Soup can nonetheless choke or lose data + if your data uses self-closing tags or declarations + incorrectly. + + By default, Beautiful Soup uses regexes to sanitize input, + avoiding the vast majority of these problems. If the problems + don't apply to you, pass in False for markupMassage, and + you'll get better performance. + + The default parser massage techniques fix the two most common + instances of invalid HTML that choke sgmllib: + +
(No space between name of closing tag and tag close) + (Extraneous whitespace in declaration) + + You can pass in a custom list of (RE object, replace method) + tuples to get Beautiful Soup to scrub your input the way you + want.""" + + self.parseOnlyThese = parseOnlyThese + self.fromEncoding = fromEncoding + self.smartQuotesTo = smartQuotesTo + self.convertEntities = convertEntities + # Set the rules for how we'll deal with the entities we + # encounter + if self.convertEntities: + # It doesn't make sense to convert encoded characters to + # entities even while you're converting entities to Unicode. + # Just convert it all to Unicode. + self.smartQuotesTo = None + if convertEntities == self.HTML_ENTITIES: + self.convertXMLEntities = False + self.convertHTMLEntities = True + self.escapeUnrecognizedEntities = True + elif convertEntities == self.XHTML_ENTITIES: + self.convertXMLEntities = True + self.convertHTMLEntities = True + self.escapeUnrecognizedEntities = False + elif convertEntities == self.XML_ENTITIES: + self.convertXMLEntities = True + self.convertHTMLEntities = False + self.escapeUnrecognizedEntities = False + else: + self.convertXMLEntities = False + self.convertHTMLEntities = False + self.escapeUnrecognizedEntities = False + + self.instanceSelfClosingTags = buildTagMap(None, selfClosingTags) + SGMLParser.__init__(self) + + if hasattr(markup, 'read'): # It's a file-type object. + markup = markup.read() + self.markup = markup + self.markupMassage = markupMassage + try: + self._feed(isHTML=isHTML) + except StopParsing: + pass + self.markup = None # The markup can now be GCed + + def convert_charref(self, name): + """This method fixes a bug in Python's SGMLParser.""" + try: + n = int(name) + except ValueError: + return + if not 0 <= n <= 127 : # ASCII ends at 127, not 255 + return + return self.convert_codepoint(n) + + def _feed(self, inDocumentEncoding=None, isHTML=False): + # Convert the document to Unicode. + markup = self.markup + if isinstance(markup, unicode): + if not hasattr(self, 'originalEncoding'): + self.originalEncoding = None + else: + dammit = UnicodeDammit\ + (markup, [self.fromEncoding, inDocumentEncoding], + smartQuotesTo=self.smartQuotesTo, isHTML=isHTML) + markup = dammit.unicode + self.originalEncoding = dammit.originalEncoding + self.declaredHTMLEncoding = dammit.declaredHTMLEncoding + if markup: + if self.markupMassage: + if not hasattr(self.markupMassage, "__iter__"): + self.markupMassage = self.MARKUP_MASSAGE + for fix, m in self.markupMassage: + markup = fix.sub(m, markup) + # TODO: We get rid of markupMassage so that the + # soup object can be deepcopied later on. Some + # Python installations can't copy regexes. If anyone + # was relying on the existence of markupMassage, this + # might cause problems. + del(self.markupMassage) + self.reset() + + SGMLParser.feed(self, markup) + # Close out any unfinished strings and close all the open tags. + self.endData() + while self.currentTag.name != self.ROOT_TAG_NAME: + self.popTag() + + def __getattr__(self, methodName): + """This method routes method call requests to either the SGMLParser + superclass or the Tag superclass, depending on the method name.""" + #print "__getattr__ called on %s.%s" % (self.__class__, methodName) + + if methodName.startswith('start_') or methodName.startswith('end_') \ + or methodName.startswith('do_'): + return SGMLParser.__getattr__(self, methodName) + elif not methodName.startswith('__'): + return Tag.__getattr__(self, methodName) + else: + raise AttributeError + + def isSelfClosingTag(self, name): + """Returns true iff the given string is the name of a + self-closing tag according to this parser.""" + return self.SELF_CLOSING_TAGS.has_key(name) \ + or self.instanceSelfClosingTags.has_key(name) + + def reset(self): + Tag.__init__(self, self, self.ROOT_TAG_NAME) + self.hidden = 1 + SGMLParser.reset(self) + self.currentData = [] + self.currentTag = None + self.tagStack = [] + self.quoteStack = [] + self.pushTag(self) + + def popTag(self): + tag = self.tagStack.pop() + + #print "Pop", tag.name + if self.tagStack: + self.currentTag = self.tagStack[-1] + return self.currentTag + + def pushTag(self, tag): + #print "Push", tag.name + if self.currentTag: + self.currentTag.contents.append(tag) + self.tagStack.append(tag) + self.currentTag = self.tagStack[-1] + + def endData(self, containerClass=NavigableString): + if self.currentData: + currentData = u''.join(self.currentData) + if (currentData.translate(self.STRIP_ASCII_SPACES) == '' and + not set([tag.name for tag in self.tagStack]).intersection( + self.PRESERVE_WHITESPACE_TAGS)): + if '\n' in currentData: + currentData = '\n' + else: + currentData = ' ' + self.currentData = [] + if self.parseOnlyThese and len(self.tagStack) <= 1 and \ + (not self.parseOnlyThese.text or \ + not self.parseOnlyThese.search(currentData)): + return + o = containerClass(currentData) + o.setup(self.currentTag, self.previous) + if self.previous: + self.previous.next = o + self.previous = o + self.currentTag.contents.append(o) + + + def _popToTag(self, name, inclusivePop=True): + """Pops the tag stack up to and including the most recent + instance of the given tag. If inclusivePop is false, pops the tag + stack up to but *not* including the most recent instqance of + the given tag.""" + #print "Popping to %s" % name + if name == self.ROOT_TAG_NAME: + return + + numPops = 0 + mostRecentTag = None + for i in range(len(self.tagStack)-1, 0, -1): + if name == self.tagStack[i].name: + numPops = len(self.tagStack)-i + break + if not inclusivePop: + numPops = numPops - 1 + + for i in range(0, numPops): + mostRecentTag = self.popTag() + return mostRecentTag + + def _smartPop(self, name): + + """We need to pop up to the previous tag of this type, unless + one of this tag's nesting reset triggers comes between this + tag and the previous tag of this type, OR unless this tag is a + generic nesting trigger and another generic nesting trigger + comes between this tag and the previous tag of this type. + + Examples: +

FooBar *

* should pop to 'p', not 'b'. +

FooBar *

* should pop to 'table', not 'p'. +

Foo

Bar *

* should pop to 'tr', not 'p'. + +

    • *
    • * should pop to 'ul', not the first 'li'. +
  • ** should pop to 'table', not the first 'tr' + tag should + implicitly close the previous tag within the same
    ** should pop to 'tr', not the first 'td' + """ + + nestingResetTriggers = self.NESTABLE_TAGS.get(name) + isNestable = nestingResetTriggers != None + isResetNesting = self.RESET_NESTING_TAGS.has_key(name) + popTo = None + inclusive = True + for i in range(len(self.tagStack)-1, 0, -1): + p = self.tagStack[i] + if (not p or p.name == name) and not isNestable: + #Non-nestable tags get popped to the top or to their + #last occurance. + popTo = name + break + if (nestingResetTriggers is not None + and p.name in nestingResetTriggers) \ + or (nestingResetTriggers is None and isResetNesting + and self.RESET_NESTING_TAGS.has_key(p.name)): + + #If we encounter one of the nesting reset triggers + #peculiar to this tag, or we encounter another tag + #that causes nesting to reset, pop up to but not + #including that tag. + popTo = p.name + inclusive = False + break + p = p.parent + if popTo: + self._popToTag(popTo, inclusive) + + def unknown_starttag(self, name, attrs, selfClosing=0): + #print "Start tag %s: %s" % (name, attrs) + if self.quoteStack: + #This is not a real tag. + #print "<%s> is not real!" % name + attrs = ''.join([' %s="%s"' % (x, y) for x, y in attrs]) + self.handle_data('<%s%s>' % (name, attrs)) + return + self.endData() + + if not self.isSelfClosingTag(name) and not selfClosing: + self._smartPop(name) + + if self.parseOnlyThese and len(self.tagStack) <= 1 \ + and (self.parseOnlyThese.text or not self.parseOnlyThese.searchTag(name, attrs)): + return + + tag = Tag(self, name, attrs, self.currentTag, self.previous) + if self.previous: + self.previous.next = tag + self.previous = tag + self.pushTag(tag) + if selfClosing or self.isSelfClosingTag(name): + self.popTag() + if name in self.QUOTE_TAGS: + #print "Beginning quote (%s)" % name + self.quoteStack.append(name) + self.literal = 1 + return tag + + def unknown_endtag(self, name): + #print "End tag %s" % name + if self.quoteStack and self.quoteStack[-1] != name: + #This is not a real end tag. + #print " is not real!" % name + self.handle_data('' % name) + return + self.endData() + self._popToTag(name) + if self.quoteStack and self.quoteStack[-1] == name: + self.quoteStack.pop() + self.literal = (len(self.quoteStack) > 0) + + def handle_data(self, data): + self.currentData.append(data) + + def _toStringSubclass(self, text, subclass): + """Adds a certain piece of text to the tree as a NavigableString + subclass.""" + self.endData() + self.handle_data(text) + self.endData(subclass) + + def handle_pi(self, text): + """Handle a processing instruction as a ProcessingInstruction + object, possibly one with a %SOUP-ENCODING% slot into which an + encoding will be plugged later.""" + if text[:3] == "xml": + text = u"xml version='1.0' encoding='%SOUP-ENCODING%'" + self._toStringSubclass(text, ProcessingInstruction) + + def handle_comment(self, text): + "Handle comments as Comment objects." + self._toStringSubclass(text, Comment) + + def handle_charref(self, ref): + "Handle character references as data." + if self.convertEntities: + data = unichr(int(ref)) + else: + data = '&#%s;' % ref + self.handle_data(data) + + def handle_entityref(self, ref): + """Handle entity references as data, possibly converting known + HTML and/or XML entity references to the corresponding Unicode + characters.""" + data = None + if self.convertHTMLEntities: + try: + data = unichr(name2codepoint[ref]) + except KeyError: + pass + + if not data and self.convertXMLEntities: + data = self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref) + + if not data and self.convertHTMLEntities and \ + not self.XML_ENTITIES_TO_SPECIAL_CHARS.get(ref): + # TODO: We've got a problem here. We're told this is + # an entity reference, but it's not an XML entity + # reference or an HTML entity reference. Nonetheless, + # the logical thing to do is to pass it through as an + # unrecognized entity reference. + # + # Except: when the input is "&carol;" this function + # will be called with input "carol". When the input is + # "AT&T", this function will be called with input + # "T". We have no way of knowing whether a semicolon + # was present originally, so we don't know whether + # this is an unknown entity or just a misplaced + # ampersand. + # + # The more common case is a misplaced ampersand, so I + # escape the ampersand and omit the trailing semicolon. + data = "&%s" % ref + if not data: + # This case is different from the one above, because we + # haven't already gone through a supposedly comprehensive + # mapping of entities to Unicode characters. We might not + # have gone through any mapping at all. So the chances are + # very high that this is a real entity, and not a + # misplaced ampersand. + data = "&%s;" % ref + self.handle_data(data) + + def handle_decl(self, data): + "Handle DOCTYPEs and the like as Declaration objects." + self._toStringSubclass(data, Declaration) + + def parse_declaration(self, i): + """Treat a bogus SGML declaration as raw data. Treat a CDATA + declaration as a CData object.""" + j = None + if self.rawdata[i:i+9] == '', i) + if k == -1: + k = len(self.rawdata) + data = self.rawdata[i+9:k] + j = k+3 + self._toStringSubclass(data, CData) + else: + try: + j = SGMLParser.parse_declaration(self, i) + except SGMLParseError: + toHandle = self.rawdata[i:] + self.handle_data(toHandle) + j = i + len(toHandle) + return j + +class BeautifulSoup(BeautifulStoneSoup): + + """This parser knows the following facts about HTML: + + * Some tags have no closing tag and should be interpreted as being + closed as soon as they are encountered. + + * The text inside some tags (ie. 'script') may contain tags which + are not really part of the document and which should be parsed + as text, not tags. If you want to parse the text as tags, you can + always fetch it and parse it explicitly. + + * Tag nesting rules: + + Most tags can't be nested at all. For instance, the occurance of + a

    tag should implicitly close the previous

    tag. + +

    Para1

    Para2 + should be transformed into: +

    Para1

    Para2 + + Some tags can be nested arbitrarily. For instance, the occurance + of a

    tag should _not_ implicitly close the previous +
    tag. + + Alice said:
    Bob said:
    Blah + should NOT be transformed into: + Alice said:
    Bob said:
    Blah + + Some tags can be nested, but the nesting is reset by the + interposition of other tags. For instance, a
    , + but not close a tag in another table. + +
    BlahBlah + should be transformed into: +
    BlahBlah + but, + Blah
    Blah + should NOT be transformed into + Blah
    Blah + + Differing assumptions about tag nesting rules are a major source + of problems with the BeautifulSoup class. If BeautifulSoup is not + treating as nestable a tag your page author treats as nestable, + try ICantBelieveItsBeautifulSoup, MinimalSoup, or + BeautifulStoneSoup before writing your own subclass.""" + + def __init__(self, *args, **kwargs): + if not kwargs.has_key('smartQuotesTo'): + kwargs['smartQuotesTo'] = self.HTML_ENTITIES + kwargs['isHTML'] = True + BeautifulStoneSoup.__init__(self, *args, **kwargs) + + SELF_CLOSING_TAGS = buildTagMap(None, + ('br' , 'hr', 'input', 'img', 'meta', + 'spacer', 'link', 'frame', 'base', 'col')) + + PRESERVE_WHITESPACE_TAGS = set(['pre', 'textarea']) + + QUOTE_TAGS = {'script' : None, 'textarea' : None} + + #According to the HTML standard, each of these inline tags can + #contain another tag of the same type. Furthermore, it's common + #to actually use these tags this way. + NESTABLE_INLINE_TAGS = ('span', 'font', 'q', 'object', 'bdo', 'sub', 'sup', + 'center') + + #According to the HTML standard, these block tags can contain + #another tag of the same type. Furthermore, it's common + #to actually use these tags this way. + NESTABLE_BLOCK_TAGS = ('blockquote', 'div', 'fieldset', 'ins', 'del') + + #Lists can contain other lists, but there are restrictions. + NESTABLE_LIST_TAGS = { 'ol' : [], + 'ul' : [], + 'li' : ['ul', 'ol'], + 'dl' : [], + 'dd' : ['dl'], + 'dt' : ['dl'] } + + #Tables can contain other tables, but there are restrictions. + NESTABLE_TABLE_TAGS = {'table' : [], + 'tr' : ['table', 'tbody', 'tfoot', 'thead'], + 'td' : ['tr'], + 'th' : ['tr'], + 'thead' : ['table'], + 'tbody' : ['table'], + 'tfoot' : ['table'], + } + + NON_NESTABLE_BLOCK_TAGS = ('address', 'form', 'p', 'pre') + + #If one of these tags is encountered, all tags up to the next tag of + #this type are popped. + RESET_NESTING_TAGS = buildTagMap(None, NESTABLE_BLOCK_TAGS, 'noscript', + NON_NESTABLE_BLOCK_TAGS, + NESTABLE_LIST_TAGS, + NESTABLE_TABLE_TAGS) + + NESTABLE_TAGS = buildTagMap([], NESTABLE_INLINE_TAGS, NESTABLE_BLOCK_TAGS, + NESTABLE_LIST_TAGS, NESTABLE_TABLE_TAGS) + + # Used to detect the charset in a META tag; see start_meta + CHARSET_RE = re.compile("((^|;)\s*charset=)([^;]*)", re.M) + + def start_meta(self, attrs): + """Beautiful Soup can detect a charset included in a META tag, + try to convert the document to that charset, and re-parse the + document from the beginning.""" + httpEquiv = None + contentType = None + contentTypeIndex = None + tagNeedsEncodingSubstitution = False + + for i in range(0, len(attrs)): + key, value = attrs[i] + key = key.lower() + if key == 'http-equiv': + httpEquiv = value + elif key == 'content': + contentType = value + contentTypeIndex = i + + if httpEquiv and contentType: # It's an interesting meta tag. + match = self.CHARSET_RE.search(contentType) + if match: + if (self.declaredHTMLEncoding is not None or + self.originalEncoding == self.fromEncoding): + # An HTML encoding was sniffed while converting + # the document to Unicode, or an HTML encoding was + # sniffed during a previous pass through the + # document, or an encoding was specified + # explicitly and it worked. Rewrite the meta tag. + def rewrite(match): + return match.group(1) + "%SOUP-ENCODING%" + newAttr = self.CHARSET_RE.sub(rewrite, contentType) + attrs[contentTypeIndex] = (attrs[contentTypeIndex][0], + newAttr) + tagNeedsEncodingSubstitution = True + else: + # This is our first pass through the document. + # Go through it again with the encoding information. + newCharset = match.group(3) + if newCharset and newCharset != self.originalEncoding: + self.declaredHTMLEncoding = newCharset + self._feed(self.declaredHTMLEncoding) + raise StopParsing + pass + tag = self.unknown_starttag("meta", attrs) + if tag and tagNeedsEncodingSubstitution: + tag.containsSubstitutions = True + +class StopParsing(Exception): + pass + +class ICantBelieveItsBeautifulSoup(BeautifulSoup): + + """The BeautifulSoup class is oriented towards skipping over + common HTML errors like unclosed tags. However, sometimes it makes + errors of its own. For instance, consider this fragment: + + FooBar + + This is perfectly valid (if bizarre) HTML. However, the + BeautifulSoup class will implicitly close the first b tag when it + encounters the second 'b'. It will think the author wrote + "FooBar", and didn't close the first 'b' tag, because + there's no real-world reason to bold something that's already + bold. When it encounters '' it will close two more 'b' + tags, for a grand total of three tags closed instead of two. This + can throw off the rest of your document structure. The same is + true of a number of other tags, listed below. + + It's much more common for someone to forget to close a 'b' tag + than to actually use nested 'b' tags, and the BeautifulSoup class + handles the common case. This class handles the not-co-common + case: where you can't believe someone wrote what they did, but + it's valid HTML and BeautifulSoup screwed up by assuming it + wouldn't be.""" + + I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS = \ + ('em', 'big', 'i', 'small', 'tt', 'abbr', 'acronym', 'strong', + 'cite', 'code', 'dfn', 'kbd', 'samp', 'strong', 'var', 'b', + 'big') + + I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS = ('noscript',) + + NESTABLE_TAGS = buildTagMap([], BeautifulSoup.NESTABLE_TAGS, + I_CANT_BELIEVE_THEYRE_NESTABLE_BLOCK_TAGS, + I_CANT_BELIEVE_THEYRE_NESTABLE_INLINE_TAGS) + +class MinimalSoup(BeautifulSoup): + """The MinimalSoup class is for parsing HTML that contains + pathologically bad markup. It makes no assumptions about tag + nesting, but it does know which tags are self-closing, that + + + + + + + +
    diff --git a/cgi/templates/board.0.html b/cgi/templates/board.0.html new file mode 100644 index 0000000..1557cbc --- /dev/null +++ b/cgi/templates/board.0.html @@ -0,0 +1,230 @@ + + + [Volver al IB] + + + [Catálogo] + [Bajar] +
    Modo Respuesta
    + + +
    + +
    + + + + + Usar: + x +
    Recuperar dibujo guardado +
    + + +
    + + No hay dibujo + + + +
    + +

    + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    mediumo + + + + + + + + +
    Asunto + + + + + + +
    molekuloj
    amiko + + + + + + + + +
    timo (uzata por post forigo)
    +
      + #{postarea_desc} +
    • ni ne vivas timi, ni vivas konekti.
    • + +
    • elekti la veneno: #{', '.join(supported_filetypes).upper()}. ĝis: #{maxsize}KB. paŝo: #{maxdimensions}x#{maxdimensions}px
    • + +
    +
    + + +
    + +
    #{postarea_extra}
    +
    + +
    + + + + + + +
    + + + +
    + + +
    Nombre de archivo:#{post['file']}-(#{post['file_size']} B, #{post['image_width']}x#{post['image_height']}) + +
    Nombre de archivo:#{post['file']}-(#{post['file_size']} B) + + + [Ocultar hilo] + +
    + + + #{post['id']} + + #{post['id']} + + #{post['id']} + + + + + + No.#{post['id']} eliminado por usuario. + + No.#{post['id']} eliminado por miembro del staff. + +
    + + #{post['name']} #{post['tripcode']} + + #{post['name']} + + #{post['timestamp_formatted']} + No.#{random.randint(1,999999)} + rep + + +
    + + #{post['file']}-(#{post['file_size']} B, #{post['image_width']}x#{post['image_height']}) + + #{post['file']}-(#{post['file_size']} B) + +
    + + + #{post['id']} + + #{post['id']} + + #{post['id']} + + + + + + [Responder] + + [Ocultar] + + +
    + +
    + +
    + + #{post['message']} +
    + +
    (Post muy largo... Presiona aquí para verlo completo.)
    + + + + + +
    Un post omitido. Haz clic en Responder para ver.
    + 1: ?> +
    #{thread['omitted']} posts omitidos. Haz clic en Responder para ver.
    + + + +
    + + +
    + +
    + + +
    + +
    Eliminar post
    + Clave
    + +
    + +
    #{pagenav}
    + + \ No newline at end of file diff --git a/cgi/templates/board.html b/cgi/templates/board.html new file mode 100644 index 0000000..e91e187 --- /dev/null +++ b/cgi/templates/board.html @@ -0,0 +1,264 @@ + + + [Volver al IB] + + + [Catálogo] + [Bajar] +
    Modo Respuesta
    + + +
    + +
    + + + + + Usar: + x +
    Recuperar dibujo guardado +
    + + +
    + + No hay dibujo + + + +
    + +
    + + + + + +
    Trampa:
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Nombre
    E-mail + + + + + + + + +
    Asunto + + + + + + +
    Mensaje
    Archivo + + + + + + + + +
    Clave (para eliminar el post)
    + +
    +
    +
    +
    + +
    #{postarea_extra}
    +
    + +
    + + + + + + +
    + + + +
    + + +
    Nombre de archivo:#{post['file']}-(#{post['file_size']} B, #{post['image_width']}x#{post['image_height']}) + +
    Nombre de archivo:#{post['file']}-(#{post['file_size']} B) + + + [Ocultar hilo] + +
    + + + #{post['id']} + + #{post['id']} + + #{post['id']} + + + + + + No.#{post['id']} eliminado por usuario. + + No.#{post['id']} eliminado por miembro del staff. + +
    + + + #{post['name']} #{post['tripcode']} + + #{post['name']} + + + + #{post['name']} #{post['tripcode']} + + #{post['name']} + + + #{post['timestamp_formatted']} + + No.#{post['id']} + + No.#{post['id']} + + rep + + Expira el ${post['expires_formatted']} + + + +
    + + #{post['file']}-(#{post['file_size']} B, #{post['image_width']}x#{post['image_height']}) + + #{post['file']}-(#{post['file_size']} B) + +
    + + + #{post['id']} + + #{post['id']} + + #{post['id']} + + + + + + [Responder] + + [Ocultar] + + +
    + +
    + +
    + + #{post['message']} +
    + +
    (Post muy largo... Presiona aquí para verlo completo.)
    + + +
    Este hilo es viejo y desaparecerá pronto.
    + + + + + +
    Un post omitido. Haz clic en Responder para ver.
    + 1: ?> +
    #{thread['omitted']} posts omitidos. Haz clic en Responder para ver.
    + + + +
    + + +
    + +
    + + +
    + +
    Eliminar post
    + Clave
    + +
    + +
    #{pagenav}
    + + \ No newline at end of file diff --git a/cgi/templates/board.jp.html b/cgi/templates/board.jp.html new file mode 100644 index 0000000..8045ab1 --- /dev/null +++ b/cgi/templates/board.jp.html @@ -0,0 +1,271 @@ + + + [掲示板に戻る] + + + [カタログ] + [ボトムへ行く] +
    レス送信モード
    + + +
    + +
    + + + + + + x +
    アップロード途中の画像 +
    + + +
    + + 画像が見当たりません + + + +
    + +
    + + + + + +
    Trampa:
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    おなまえ
    E-mail + + + + + + + + +
    題  名 + + + + + + +
    コメント
    添付File + + + + + + + + +
    削除キー (削除用)
    + +
    +
    +
    +
    + +
    #{postarea_extra}
    +
    + +
    + + + + + + +
    + + + +
    + + +
    画像ファイル名:#{post['file']}-(#{post['file_size']} B, #{post['image_width']}x#{post['image_height']}) + +
    画像ファイル名:#{post['file']}-(#{post['file_size']} B) + + + アニメGIF + + サムネ表示 + + + [隠す] + +
    + + + #{post['id']} + + #{post['id']} + + #{post['id']} + + + + + + No.#{post['id']}はユーザーに削除されました. + + No.#{post['id']}は管理人に削除されました. + +
    + + + Name #{post['name']} #{post['tripcode']} + + Name #{post['name']} + + + + Name #{post['name']} #{post['tripcode']} + + Name #{post['name']} + + + #{post['timestamp_formatted']} + + No.#{post['id']} + + No.#{post['id']} + + rep + + ${post['expires_formatted']}頃消えます + + + +
    + + #{post['file']}-(#{post['file_size']} B, #{post['image_width']}x#{post['image_height']}) + + #{post['file']}-(#{post['file_size']} B) + + + アニメGIF + + サムネ表示 + +
    + + + #{post['id']} + + #{post['id']} + + #{post['id']} + + + + + + [返信] + + [隠す] + + +
    + +
    + +
    + + #{post['message']} +
    + +
    (投稿は長すぎ... 全部読むにはこっちらへ)
    + + +
    このスレは古いので、もうすぐ消えます。
    + + + + + 0: ?> + レス${thread['omitted']}件省略。全て読むには返信ボタンを押してください。 + + + +
    + + +
    + +
    + + +
    + +
    + 【記事削除】
    + 削除キー +
    + +
    + +
    #{pagenav}
    + + \ No newline at end of file diff --git a/cgi/templates/catalog.html b/cgi/templates/catalog.html new file mode 100644 index 0000000..4faa2d2 --- /dev/null +++ b/cgi/templates/catalog.html @@ -0,0 +1,30 @@ + + +
    Modo Catálogo
    +
    + +
    + + #{thread['id']}
    + +
    Respuestas: ${thread['length']}
    + +

    ${thread['subject']}
    ${thread['message']}

    + +

    ${thread['subject']}
    ${thread['message']}

    + + +
    +
    + \ No newline at end of file diff --git a/cgi/templates/error.html b/cgi/templates/error.html new file mode 100644 index 0000000..47ef529 --- /dev/null +++ b/cgi/templates/error.html @@ -0,0 +1,7 @@ + +


    +

    #{error} +

    Volver
    +


    + + \ No newline at end of file diff --git a/cgi/templates/exception.html b/cgi/templates/exception.html new file mode 100644 index 0000000..e8453eb --- /dev/null +++ b/cgi/templates/exception.html @@ -0,0 +1,36 @@ + + + +Error@Bienvenido a Internet + + + + + + +

    ERROR : Ha ocurrido un error inesperado.

    +

    Esto no es normal y te pedimos que reportes el problema en +Discusión de B.a.I. o a través de +nuestro e-mail, +presentando los siguientes datos y ojalá indicando qué hacer para reproducirlo:

    +

    Versión: weabot +
    +Tipo: ${exception}
    +Detalle: ${error}
    +Traceback:
    +

    + + ${line[0]} ${line[1]} ${line[2]} ${line[3]}
    + +

    +

    Te recordamos que el software está en desarrollo y estamos siempre haciendo lo posible para arreglar los problemas lo antes posible.
    Te pedimos las disculpas por cualquier inconveniente.

    +
    +

    weabot dijo "Perdón."
    Bienvenido a Internet BBS/IB

    + +

    ERROR : #{error}

    +

    Por favor presiona Atrás y soluciona el problema.

    +
    +

    La página principal está aquí.
    Si esto es inusual intenta contactarnos.

    + + \ No newline at end of file diff --git a/cgi/templates/home.rss b/cgi/templates/home.rss new file mode 100644 index 0000000..dc69377 --- /dev/null +++ b/cgi/templates/home.rss @@ -0,0 +1,24 @@ + + + + Bienvenido a Internet BBS/IB + https://bienvenidoainternet.org/ + El BBS/IB más activo de la esfera hispana. + es + burocracia@bienvenidoainternet.org (Staff ★) + + https://bienvenidoainternet.org/rss_logo.png + Bienvenido a Internet BBS/IB + https://bienvenidoainternet.org/ + 144 + 144 + + + + ${post['board_name']}: #{post['content']} + ${post['timestamp_formatted']} + https://bienvenidoainternet.org#{post['url']} + + + + \ No newline at end of file diff --git a/cgi/templates/htaccess b/cgi/templates/htaccess new file mode 100644 index 0000000..469fec0 --- /dev/null +++ b/cgi/templates/htaccess @@ -0,0 +1,24 @@ +DirectoryIndex index.html + + +AuthName "BAI" +AuthType Basic +AuthUserFile "/home/z411/.htpasswds/public_html/wiki/passwd" + +require valid-user + + + + +ExpiresByType text/css "access plus 0 seconds" + + +ErrorDocument 403 https://bienvenidoainternet.org/cgi/banned/#{dir} + + +order allow,deny + +deny from #{ip} + +allow from all + diff --git a/cgi/templates/kako.html b/cgi/templates/kako.html new file mode 100644 index 0000000..49d95df --- /dev/null +++ b/cgi/templates/kako.html @@ -0,0 +1,60 @@ + + + + Archivo de #{board_name}@Bienvenido a Internet BBS + + + + + + + +

    Índice de /#{board}/kako/

    +
    +  
    +    
    +      
    +      
    +      
    +      
    +    
    +    
    +      
    +    
    +    
    +      
    +      
    +      
    +      
    +    
    +    
    +    
    +      
    +      
    +       1000: ?>
    +      
    +      
    +      
    +      
    +      
    +    
    +    
    +  
    NombreTamañoDescripción

    ..-
    ${thread['timestamp']}.json1KR${thread['length']}R${thread['subject']}
    +
    +
    +
    weabot/0.8.4 (CentOS) Servidor ubicado en bienvenidoainternet.org Puerto 443
    + + \ No newline at end of file diff --git a/cgi/templates/manage/addboard.html b/cgi/templates/manage/addboard.html new file mode 100644 index 0000000..71b3c31 --- /dev/null +++ b/cgi/templates/manage/addboard.html @@ -0,0 +1,21 @@ + + +
    +
    Nuevo board
    +
    + + + + + + + + + + + +
    Directorio
    Nombre
    +
    +
    +
    + \ No newline at end of file diff --git a/cgi/templates/manage/bans.html b/cgi/templates/manage/bans.html new file mode 100644 index 0000000..81e0f71 --- /dev/null +++ b/cgi/templates/manage/bans.html @@ -0,0 +1,92 @@ + + + +
    +
    Bans
    + +
    + + + + + + +
    Dirección IP
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Dirección IPMáscara de redBoardsAgregadoExpiraCiegoPuesto porRazónNotaAcción
    ${ban['ip']}${ban['netmask']}${ban['boards']}${ban['added']}${ban['until']}${ban['blind']}${ban['staff']}${ban['reason']}${ban['note']} + [Ver posts] + [Editar] + [Eliminar] +
    + +
    + + + + + + + + + + + + + + + +
    IP
    Máscara de red
    Board(s) +
    + +
    + + 0: ?> + + +
    Mensaje
    Nota para staff
    Ciego
    Expira en (segundos) +
    +
    + Nunca + 1h + 6h + 12h + 1d + 3d + 1w + 30d + 1y +
    +
    +
    + +
    +
    + \ No newline at end of file diff --git a/cgi/templates/manage/boardoptions.html b/cgi/templates/manage/boardoptions.html new file mode 100644 index 0000000..436b036 --- /dev/null +++ b/cgi/templates/manage/boardoptions.html @@ -0,0 +1,195 @@ + + + +
    +
    Opciones de Board
    + + + + + + +
    SecciónAccion
    /#{board['dir']}/#{board['name']}[Configurar]
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ID
    Directorio
    Nombre
    Nombre largo
    Sub-nombre
    Tipo + +
    Descripción / Reglas + + +
    Caja extra
    Forzar CSS ("" = default)
    Nombre por defecto
    Título por defecto
    Mensaje por defecto
    ID + +
    Slip + +
    Código de país + +
    Desactivar nombre
    Desactivar asunto
    Papelera de reciclaje
    Cerrado
    Secreto
    Permitir spoilers
    Permitir oekaki
    Permitir crear hilos sin imagen
    Permitir subida
    Tipos de archivo + +
    + +
    Tamaño máximo (KB)
    Dimensión de miniatura (px)
    Hilos en página frontal
    Respuestas a mostrar
    Máximo de líneas (frontal)
    Edad máxima de un hilo (días; 0 = desactivar)
    Inactividad máxima de un hilo (días; 0 = desactivar)
    Archivar hilos
    Espera para crear nuevo hilo (segundos)
    Espera entre respuestas (segundos)
    +
    +
    + +
    + +
    +
    + \ No newline at end of file diff --git a/cgi/templates/manage/changepassword.html b/cgi/templates/manage/changepassword.html new file mode 100644 index 0000000..977c772 --- /dev/null +++ b/cgi/templates/manage/changepassword.html @@ -0,0 +1,24 @@ + + +
    +
    Cambiar contraseña
    +
    + + + + + + + + + + + + + + +
    Clave actual
    Nueva clave
    Confirmar nueva clave
    +
    +
    +
    + diff --git a/cgi/templates/manage/delete.html b/cgi/templates/manage/delete.html new file mode 100644 index 0000000..78c1c5e --- /dev/null +++ b/cgi/templates/manage/delete.html @@ -0,0 +1,23 @@ + + +
    +
    Eliminar Post
    +
    + + + +

    + Post #${postid} de /${curboard}/
    +
    +
    +
    + Nota: Por favor evitar eliminar permanentemente el post al menos que sea estrictamente necesario. +
    Al eliminar permanentemente un post no queda en papelera y se rompen + las referencias que se pueden haber hecho hacia él, especialmente en los BBS.
    +

    + +

    +
    +
    +
    + diff --git a/cgi/templates/manage/filters.html b/cgi/templates/manage/filters.html new file mode 100644 index 0000000..188a741 --- /dev/null +++ b/cgi/templates/manage/filters.html @@ -0,0 +1,119 @@ + + + +
    +
    Filtros
    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    IDBoardsTipoAcciónMensajeModificadoPorAcción
    #{filter['id']}${filter['boards']}#{filter['type_formatted']}#{filter['action_formatted']}${filter['reason']}${filter['added']}${filter['staff']}[Editar]
    + [Eliminar]
    +
    + +
    + +
    + + + + + + + + + + + + + + + + +
    Tipo de filtro
    Regex:
    Nombre: (regex)
    Tripcode: (incluir separador)
    +
    +
    +
    Aplicar a
    +
    + +
    + +
    + +
    +
    +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Acción
    Expira en: (segundos)
    Preset: + Nunca + 1h + 6h + 12h + 1d + 3d + 1w + 30d + 1y +
    Tardar: (segundos)
    +
    + + + +
    Mensaje a mostrar
    +
    + +
    + +
    +
    + diff --git a/cgi/templates/manage/ipdelete.html b/cgi/templates/manage/ipdelete.html new file mode 100644 index 0000000..71c043a --- /dev/null +++ b/cgi/templates/manage/ipdelete.html @@ -0,0 +1,24 @@ + + +
    +
    Eliminar por IP
    +
    + + + + + + + + + + +
    Board(s) +
    + +
    + +
    Dirección IP
    +
    +

    + diff --git a/cgi/templates/manage/ipshow.html b/cgi/templates/manage/ipshow.html new file mode 100644 index 0000000..6937a0e --- /dev/null +++ b/cgi/templates/manage/ipshow.html @@ -0,0 +1,73 @@ + + +
    +
    Mostrar por IP
    + +
    + + + +
    Dirección IP
    +
    + + + +
    + Hostname: #{host if host else "Desconocido"} [#{country if country else "??"}]#{" (Nodo Tor)" if tor else ""}
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SecciónPadreIDFechaNombreAsuntoMensajeArchivoAcción
    #{post['dir']}#{post['parentid']}#{post['id']}#{post['timestamp_formatted']}#{post['name']} #{post['tripcode']}#{post['name']}#{post['subject']}#{post['message']} + + Eliminar + + Rec + [1] + + Rec + [2] + +
    +
    + + Error: No hay posts

    + + [Volver al panel] + +
    +
    + \ No newline at end of file diff --git a/cgi/templates/manage/lockboard.html b/cgi/templates/manage/lockboard.html new file mode 100644 index 0000000..cebf061 --- /dev/null +++ b/cgi/templates/manage/lockboard.html @@ -0,0 +1,20 @@ + + +
    +
    Cerrar o abrir board
    + + + + + + + + + + + + +
    SecciónAcción
    /#{board['dir']}/#{board['name']}[Cerrar][Abrir]
    +
    +
    + diff --git a/cgi/templates/manage/login.html b/cgi/templates/manage/login.html new file mode 100644 index 0000000..7ce47a1 --- /dev/null +++ b/cgi/templates/manage/login.html @@ -0,0 +1,21 @@ + +
    + #{page} +
    + + + + + + + + + + + + +
    Usuario
    Contraseña
    +
    +
    +
    + \ No newline at end of file diff --git a/cgi/templates/manage/logs.html b/cgi/templates/manage/logs.html new file mode 100644 index 0000000..e11780a --- /dev/null +++ b/cgi/templates/manage/logs.html @@ -0,0 +1,17 @@ + + +
    +
    Registro
    + + + + + + + + + +
    FechaStaffAcción
    ${log['timestamp_formatted']}${log['staff']}${log['action']}
    +
    +
    + \ No newline at end of file diff --git a/cgi/templates/manage/manage.html b/cgi/templates/manage/manage.html new file mode 100644 index 0000000..06b1737 --- /dev/null +++ b/cgi/templates/manage/manage.html @@ -0,0 +1,22 @@ + + +
    +
    BANDEJA DE ENTRADA +
    + Denuncias: + 0: ?> + #{reports} + + 0 +
    +
    + NOTICIAS DEL STAFF +
    +
    + +
    #{post['title'] if post['title'] else "Sin asunto"}
    #{post['id']} : ${post['name']} : ${post['timestamp_formatted']}
    +
    #{post['message']}
    + +
    +
    + \ No newline at end of file diff --git a/cgi/templates/manage/menu.html b/cgi/templates/manage/menu.html new file mode 100644 index 0000000..d6ffd5e --- /dev/null +++ b/cgi/templates/manage/menu.html @@ -0,0 +1,30 @@ + + + + +
    ¡Bienvenido, #{username}! ¡Eres +Accionista +Accionista +Developer +Moderador + de #{site_title}!
    +
    + + + + + + + + + + + + + + + +
    Principal:- Inicio - Cambiar contraseña - News Channel - Correo - Cerrar sesión -
    Posts:- Modbrowse - Ver por IP - Papelera de reciclaje - Imágenes recientes -
    Moderación:- Denuncias - Eliminar por IP - Lista de bans - Mover hilo - Filtros - Frases -
    Administración:- Reconstruir - Noticias - Twitter - Opciones de board - Agregar board - Cerrar board -
    Staff:- Miembros - Registro de acciones -
    +
    + \ No newline at end of file diff --git a/cgi/templates/manage/message.html b/cgi/templates/manage/message.html new file mode 100644 index 0000000..6c53ecc --- /dev/null +++ b/cgi/templates/manage/message.html @@ -0,0 +1,8 @@ + + +
    +
    #{title if title else "Mensaje"}
    +

    #{message}

    +
    +
    + \ No newline at end of file diff --git a/cgi/templates/manage/mod.html b/cgi/templates/manage/mod.html new file mode 100644 index 0000000..ddc688f --- /dev/null +++ b/cgi/templates/manage/mod.html @@ -0,0 +1,96 @@ + + +
    +
    Modbrowse
    + + + + + + +
    SecciónAcción
    /#{board['dir']}/#{board['name']}[Navegar]
    + + + + + + + + + + + + + + + + + + + + + + + + +
    #IDAsuntoFechaMensajeResp.Acciones
    #{i}#{thread['id']}#{thread['subject']}#{thread['timestamp_formatted'][:21]}${thread['message'][:200]}#{thread['length']} + L#{"-" if thread['locked'] == "1" else "+"} + PS#{"-" if thread['locked'] == "2" else "+"} + M + D + & + B +
    +
    +[Volver] + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Hilo: ${posts[0]['subject']} (#{posts[0]['length']})
    #{"Abrir hilo" if posts[0]['locked'] == "1" else "Cerrar hilo"} / +#{"Quitar permasage" if posts[0]['locked'] == "2" else "Permasage"} / +Mover hilo
    #IDFechaNombreMensajeArchivoIPAcción
    #{i}#{p['id']}${p['timestamp_formatted']}${p['name']}${p['message']} + + #{p['ip']} + + Eliminar + & + Ban + + Recuperar + [1] + + Recuperar + [2] + +
    +
    +[Volver al panel] + +
    +
    + \ No newline at end of file diff --git a/cgi/templates/manage/move.html b/cgi/templates/manage/move.html new file mode 100644 index 0000000..8fcc1e9 --- /dev/null +++ b/cgi/templates/manage/move.html @@ -0,0 +1,60 @@ + + +
    +
    Mover hilo
    + +
    + + + + + + + + + + + + + + + + + + + + + +
    Board actual + + + + + +
    ID de hilo + + + + + +
    Mover a + +
    Insertar mensaje + +
    +
    +
    +
    + \ No newline at end of file diff --git a/cgi/templates/manage/quotes.html b/cgi/templates/manage/quotes.html new file mode 100644 index 0000000..d30a403 --- /dev/null +++ b/cgi/templates/manage/quotes.html @@ -0,0 +1,12 @@ + + +
    +
    Quotes
    +

    Ingresa un mensaje a mostrar por cada linea:

    +
    +
    + +
    +
    +
    + diff --git a/cgi/templates/manage/rebuild.html b/cgi/templates/manage/rebuild.html new file mode 100644 index 0000000..3afc057 --- /dev/null +++ b/cgi/templates/manage/rebuild.html @@ -0,0 +1,20 @@ + + +
    +
    Reconstruir board
    + + + + + + + + + + + + +
    SecciónAcción
    Home[Reconstruir]
    Noticias[Reconstruir]
    Índices de archivos[Reconstruir]
    .htaccess[Reconstruir]
    /#{board['dir']}/#{board['name']}[Reconstruir frontales][Reconstruir todo]
    +
    +
    + diff --git a/cgi/templates/manage/recent_images.html b/cgi/templates/manage/recent_images.html new file mode 100644 index 0000000..39f919c --- /dev/null +++ b/cgi/templates/manage/recent_images.html @@ -0,0 +1,24 @@ + + + +
    +
    Imágenes recientes
    +
    + + + +
    Número a mostrar
    +
    +
    +
    + + + + + + + +
    +
    +
    + \ No newline at end of file diff --git a/cgi/templates/manage/recyclebin.html b/cgi/templates/manage/recyclebin.html new file mode 100644 index 0000000..b413c9c --- /dev/null +++ b/cgi/templates/manage/recyclebin.html @@ -0,0 +1,72 @@ + + + +
    +
    Papelera de Reciclaje
    +
    + + + + + + + + + +
    Eliminado por + + + +
    Board + +
    +
    +
    + +${message} +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    IDTimestampBoardTipoIPMensaje
    X
    R
    #{post['id']}${post['timestamp_formatted']}${post['dir']}${post['IS_DELETED']}${post['ip']}#{post['message']}
    +
    +
    +
    #{navigator}
    + + No hay posts. + +
    +
    + diff --git a/cgi/templates/manage/reports.html b/cgi/templates/manage/reports.html new file mode 100644 index 0000000..f47ec38 --- /dev/null +++ b/cgi/templates/manage/reports.html @@ -0,0 +1,58 @@ + + + +
    +
    Reportes
    + +${message} + +
    + + + + + +
    Board + +
    +
    + +
    + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + +
    FechaPostIP PostRazónIP Denuncia
    X ${report['timestamp_formatted']}${report['link']}${report['ip']}${report['reason']}${report['reporterip']}
    +
    +
    +
    + diff --git a/cgi/templates/manage/search.html b/cgi/templates/manage/search.html new file mode 100644 index 0000000..6c2ec6f --- /dev/null +++ b/cgi/templates/manage/search.html @@ -0,0 +1,27 @@ + + +
    +
    Registro de búsqueda
    + + + + + + + + + + + + + + + + + + + +
    IDFechaBúsquedaEnResultadosPor
    ${log['id']}${log['timestamp_formatted']}${log['keyword']}${"[A] " if log['archive'] else ""}${"Global" if log["ita"] == "" else log["ita"]}${log['res']}${log['ip']}
    +
    +
    + \ No newline at end of file diff --git a/cgi/templates/manage/staff.html b/cgi/templates/manage/staff.html new file mode 100644 index 0000000..787a843 --- /dev/null +++ b/cgi/templates/manage/staff.html @@ -0,0 +1,63 @@ + + + +
    +
    Staff
    + + + + + + + + + + + + + + + + + + + + + +
    IDNombreNivelÚltima actividadAcciones
    ${member['id']}${member['username']}${member['rights']}${member['lastactive']} + [Editar] + [Eliminar] +
    + +
    + + + + + + + + + + + + + + + + + + +
    Nombre
    Contraseña
    Nivel + +
    +
    + +
    +
    + \ No newline at end of file diff --git a/cgi/templates/mobile/base_top.html b/cgi/templates/mobile/base_top.html new file mode 100644 index 0000000..6a6c5bd --- /dev/null +++ b/cgi/templates/mobile/base_top.html @@ -0,0 +1,14 @@ + + + + + #{board_name}@Bienvenido a Internet Móvil + + Bienvenido a Internet Móvil + + + + + + diff --git a/cgi/templates/mobile/board.html b/cgi/templates/mobile/board.html new file mode 100644 index 0000000..70b8461 --- /dev/null +++ b/cgi/templates/mobile/board.html @@ -0,0 +1,55 @@ + + + + +
    + + +

    No.#{post['id']} eliminado por el usuario.

    + +

    No.#{post['id']} eliminado por miembro del staff.

    + + +

    #{post["subject"]} (#{thread['length']})

    + +
    + +

    #{post["subject"]}

    + +

    #{post['id']}#{post['name']} #{post['tripcode']} #{post['timestamp_formatted']}

    +
    #{int(post['file_size'])//1024}KB #{post['file'].split(".")[1].upper()}
    +
    #{post['message']}
    + + + +Recargar + + + +
    El hilo ha sido cerrado. Ya no se puede postear en él.
    + +
    + +
    + + + + + + + + + +
    + + +
    + + +
    + +
    + + + + \ No newline at end of file diff --git a/cgi/templates/mobile/error.html b/cgi/templates/mobile/error.html new file mode 100644 index 0000000..00ae4f4 --- /dev/null +++ b/cgi/templates/mobile/error.html @@ -0,0 +1,6 @@ + + +

    Home
    Error

    +


    #{error}



    + + \ No newline at end of file diff --git a/cgi/templates/mobile/latest.html b/cgi/templates/mobile/latest.html new file mode 100644 index 0000000..615b21c --- /dev/null +++ b/cgi/templates/mobile/latest.html @@ -0,0 +1,14 @@ + + +
    +
    Info
    + Bienvenido a Internet Móvil +
    + + + + \ No newline at end of file diff --git a/cgi/templates/mobile/newest.html b/cgi/templates/mobile/newest.html new file mode 100644 index 0000000..37fd67f --- /dev/null +++ b/cgi/templates/mobile/newest.html @@ -0,0 +1,14 @@ + + +
    +
    Info
    + Bienvenido a Internet Móvil +
    + + + + \ No newline at end of file diff --git a/cgi/templates/mobile/threadlist.html b/cgi/templates/mobile/threadlist.html new file mode 100644 index 0000000..edb81eb --- /dev/null +++ b/cgi/templates/mobile/threadlist.html @@ -0,0 +1,43 @@ + + +
    +
    Home
    + #{board_name} +
    + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/cgi/templates/mobile/txt_newthread.html b/cgi/templates/mobile/txt_newthread.html new file mode 100644 index 0000000..b19d2fa --- /dev/null +++ b/cgi/templates/mobile/txt_newthread.html @@ -0,0 +1,35 @@ + + +
    +
    Home
    + #{board_name} +
    + + + + + +
    + +
    + + + + + + + + + +
    + + +
    + + +
    + +
    Formatos permitidos: #{', '.join(supported_filetypes).upper()}
    Tamaño máximo: #{maxsize} KB
    + + + \ No newline at end of file diff --git a/cgi/templates/mobile/txt_thread.html b/cgi/templates/mobile/txt_thread.html new file mode 100644 index 0000000..8a19a94 --- /dev/null +++ b/cgi/templates/mobile/txt_thread.html @@ -0,0 +1,74 @@ + + + + + + + 1000: ?> +
    ■ El hilo superó los 1000 mensajes y ha sido cerrado.
    + 950: ?> +
    ■ El hilo ha recibido más de 950 mensajes. Límite: 1000
    + 900: ?> +
    ■ El hilo ha recibido más de 900 mensajes. Límite: 1000
    + +
    +

    #{thread['subject']} (#{thread['length']})

    + + +

    #{str(post['num']).zfill(4)} Eliminado por el usuario.

    + +

    #{str(post['num']).zfill(4)} Eliminado por miembro del staff.

    + +
    +

    #{str(post['num']).zfill(4)} #{post['name']} #{post['tripcode']}

    +
    #{int(post['file_size'])//1024}KB #{post['file'].split(".")[1].upper()}
    +
    #{post['message']}
    +

    #{post['timestamp_formatted']}

    +
    + + + +Ver nuevos posts + + + +
    + +
    + + + + +
    + + +
    + + +
    + +
    + + + + \ No newline at end of file diff --git a/cgi/templates/mobile/txt_threadlist.html b/cgi/templates/mobile/txt_threadlist.html new file mode 100644 index 0000000..5e3d133 --- /dev/null +++ b/cgi/templates/mobile/txt_threadlist.html @@ -0,0 +1,26 @@ + + +
    +
    Home
    + #{board_name} +
    + + + + + + + + + + \ No newline at end of file diff --git a/cgi/templates/mod.html b/cgi/templates/mod.html new file mode 100644 index 0000000..21a35f6 --- /dev/null +++ b/cgi/templates/mod.html @@ -0,0 +1,86 @@ + + + + + MOD/S3M/XM module player for Web Audio + + + + + + + + + + + +
    +
    +
    MOD/S3M/XM module player for Web Audio
    +
    (c) 2012-2015 Firehawk/TDA
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +                              + ('    ') + +

    + [<<] + [reproducir] + [pausa] + [>>] + + [rept] + [)oo(] + [trks] + [filt] +
    +
    +
    +
    +
    + Esta es una instancia local del reproductor de MODs por Firehawk - Twitter / firehawk@haxor.fi.
    Código fuente disponible en GitHub bajo licencia MIT. + +
    +
    + + +
    + + diff --git a/cgi/templates/navbar.html b/cgi/templates/navbar.html new file mode 100644 index 0000000..1655f0b --- /dev/null +++ b/cgi/templates/navbar.html @@ -0,0 +1,16 @@ +Actualidad +Tecnología +Juegos +Música +TV y Cine +Humanidades +Club VIP +World Lobby +| +Imágenes +二次元画像 +Naturaleza +Oekaki +Cero +| +Meta \ No newline at end of file diff --git a/cgi/templates/paint.html b/cgi/templates/paint.html new file mode 100644 index 0000000..476babe --- /dev/null +++ b/cgi/templates/paint.html @@ -0,0 +1,79 @@ + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +
    + +
    + + +
    +
    + + +


    + +
    +
    + diff --git a/cgi/templates/redirect.html b/cgi/templates/redirect.html new file mode 100644 index 0000000..172425d --- /dev/null +++ b/cgi/templates/redirect.html @@ -0,0 +1,12 @@ + + +Has posteado en Bienvenido a Internet BBS/IB + + + + + +

    Gracias por tu post

    ${message}

    (por favor espera) +

    Tiempo usado: #{timetaken}

    + + \ No newline at end of file diff --git a/cgi/templates/report.html b/cgi/templates/report.html new file mode 100644 index 0000000..d37ca6d --- /dev/null +++ b/cgi/templates/report.html @@ -0,0 +1,29 @@ + + + +Post denunciado +Denunciar post ${postshow} + + + + + + + +

    Post denunciado.

    +
    Volver
    + +

    Formulario de denuncia

    +
    Para pedir que el post ${postshow} sea eliminado, indica una razón y presiona el botón [Enviar denuncia]. +
    Volver +
    +
    Este formulario no es para eliminar tu propio post. +Para eliminar tu propio post debes presionar el botón del que aparece a la derecha de tu post cuando le pones el cursor encima. [info] +Para eliminar tu propio post debes chequear la caja que se encuentra en la parte superior izquierda de tu post y luego presionar el botón "Eliminar" que se encuentra al final de la página. [info] +
    Normalmente eliminamos los mensajes que son considerados spam o flood. Si deseas pedir la prohibición de acceso a algún usuario persistente, te recomendamos hacerlo en la sección meta. +
    Contacto
    + + + \ No newline at end of file diff --git a/cgi/templates/revision.html b/cgi/templates/revision.html new file mode 100644 index 0000000..1e9b46b --- /dev/null +++ b/cgi/templates/revision.html @@ -0,0 +1 @@ +0.8.7 diff --git a/cgi/templates/stats.html b/cgi/templates/stats.html new file mode 100644 index 0000000..dd0e5ab --- /dev/null +++ b/cgi/templates/stats.html @@ -0,0 +1,163 @@ + + + +Estadísticas@Bienvenido a Internet + + + + + +
    +
    Bienvenido a Internet Estadísticas
    +
    + Última actualización: ${timestamp} GMT${tz} + + (en caché) + +
    +
    + +
    + +

    Mensajes totales (última semana)

    + + + + + + + + + + + + + + + + + + + + + + + + +
    DíaHilosResp.Total
    ${day}${threads}${int(posts)-int(threads)}${posts}
    Total${allthreads}${allposts-allthreads}${allposts}
    + +
    + +

    Volumen de mensajes por sección (últimos 30 días)

    + + + + + + + + + + + + + + + + + +
    #SecciónMensajesPorcentaje
    + ${iter} + ${iter} + ${iter} + ${iter} + ${board}${num}${percent}%
    + +
    + +

    Sistema

    + + + + + + + + + + + + + + + + + + + +
    Máquinamaria (Debian GNU/Linux)
    OS${uname[0]} ${uname[2]}
    Release/Arq.${uname[3]} ${uname[4]}
    Motor BBS/IBweabot ${weabot_ver}
    Templatingtenjin ${tenjin_ver}
    Versión de Python${python_ver}
    Implementación${python_impl}
    Build${python_build}
    Compilado en${python_compiler}
    + +
    + +

    Base de datos

    + + + + + + + + + + + + + +
    Base de DatosMariaDB
    Versión${mysql_ver} Linux x86_64
    Mensajes totales activos${total}
    Archivos totales activos${total_files}
    Mensajes totales archivados (BBS)${total_archived}
    Mensajes totales archivados (IB)0 (QEPD)
    + +
    + + + + diff --git a/cgi/templates/txt_archive.html b/cgi/templates/txt_archive.html new file mode 100644 index 0000000..b1e25db --- /dev/null +++ b/cgi/templates/txt_archive.html @@ -0,0 +1,104 @@ + + + + #{threads[0]['subject']} - Archivo de #{board_name}@Bienvenido a Internet BBS + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ■ Este hilo se encuentra guardado en el archivo

    +
    +

    #{thread['subject']} (${(str(thread['length'])+" respuestas") if thread['length'] > 1 else "Una respuesta"})

    + + +

    #{post['num']} : Mensaje eliminado por el usuario.

    + +

    #{post['num']} : Mensaje eliminado por miembro del staff.

    + + +
    + +
    + +

    #{post['num']} : + + + #{post['name']} #{post['tripcode']} + + #{post['name']} + + + + #{post['name']} #{post['tripcode']} + + #{post['name']} + + : #{post['timestamp_formatted']}

    +
    #{post['message']}
    +
    + + + +
    #{thread['size']}
    + +
    +
    ■ Este hilo se encuentra guardado en el archivo

    + + + +
    weabot.py ver Bienvenido a Internet BBS/IB
    + + + \ No newline at end of file diff --git a/cgi/templates/txt_base_top.html b/cgi/templates/txt_base_top.html new file mode 100644 index 0000000..eb3c37b --- /dev/null +++ b/cgi/templates/txt_base_top.html @@ -0,0 +1,44 @@ + + + + + #{threads[0]['subject']} - #{board_name}@Bienvenido a Internet BBS + + #{board_long} + + #{title} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cgi/templates/txt_board.en.html b/cgi/templates/txt_board.en.html new file mode 100644 index 0000000..8e3c421 --- /dev/null +++ b/cgi/templates/txt_board.en.html @@ -0,0 +1,137 @@ + + + + + + +
    +
    + +

    #{board_long}

    + +
    #{postarea_desc}
    + + +
    + +
    + +
    #{postarea_extra}
    + + + + + + + +
    + +

    [#{titer}:#{thread['length']}]#{thread['posts'][0]['subject']}

    + + +

    #{post['num']} : Post deleted by user.

    + +

    #{post['num']} : Post deleted by staff.

    + +
    +

    #{post['num']} : + + + #{post['name']} #{post['tripcode']} + + #{post['name']} + + + + #{post['name']} #{post['tripcode']} + + #{post['name']} + + : #{post['timestamp_formatted']}

    + +
    ${int(post['file_size'])//1024}KB ${post['file'].split(".")[1].upper()}
    + +
    + #{post['message']} + +
    (Post is too long... Click here to view the whole post.)
    + +
    +
    + + + +
    + +
    + Name:  E-mail: 
    +
    + +
    + +
    This thread has been closed. You cannot post in it any longer.
    + + +
    +
    + + + + +
    +
    +
    + +
    New thread form
    +
    + + + + + + + + + + + + + + + + + +
    Subject:
    Name:E-mail:
    Body:
    File:
    +
    Trampa:
    +
    +
    +
    + + + \ No newline at end of file diff --git a/cgi/templates/txt_board.html b/cgi/templates/txt_board.html new file mode 100644 index 0000000..097e255 --- /dev/null +++ b/cgi/templates/txt_board.html @@ -0,0 +1,137 @@ + + + + + + +
    +
    + +

    #{board_long}

    + +
    #{postarea_desc}
    + + +
    + +
    + +
    #{postarea_extra}
    + + + + + + + +
    + +

    [#{titer}:#{thread['length']}]#{thread['posts'][0]['subject']}

    + + +

    #{post['num']} Mensaje eliminado por el usuario.

    + +

    #{post['num']} Mensaje eliminado por miembro del staff.

    + +
    +

    #{post['num']} : + + + #{post['name']} #{post['tripcode']} + + #{post['name']} + + + + #{post['name']} #{post['tripcode']} + + #{post['name']} + + : #{post['timestamp_formatted']}

    + +
    ${int(post['file_size'])//1024}KB ${post['file'].split(".")[1].upper()}
    + +
    + #{post['message']} + +
    (Post muy largo... Presiona aquí para verlo completo.)
    + +
    +
    + + + +
    + +
    + Nombre:  E-mail:  +
    + +
    + +
    El hilo ha sido cerrado. Ya no se puede postear en él.
    + + +
    +
    + + + + +
    +
    +
    + +
    Formulario de nuevo hilo
    +
    + + + + + + + + + + + + + + + + + +
    Asunto:
    Nombre:E-mail:
    Mensaje:
    Archivo:
    +
    Trampa:
    +
    +
    +
    + + + \ No newline at end of file diff --git a/cgi/templates/txt_error.html b/cgi/templates/txt_error.html new file mode 100644 index 0000000..8a16a63 --- /dev/null +++ b/cgi/templates/txt_error.html @@ -0,0 +1,50 @@ + + +Error + + + + + +
    ERROR: #{error}
    +
    + Host ${info['host']}
    +
    + Nombre: ${info['name']}
    + E-mail: ${info['email']}
    + Mensaje:
    + ${info['message']} +
    +
    +
    + + + \ No newline at end of file diff --git a/cgi/templates/txt_thread.en.html b/cgi/templates/txt_thread.en.html new file mode 100644 index 0000000..2e811cb --- /dev/null +++ b/cgi/templates/txt_thread.en.html @@ -0,0 +1,105 @@ + + + + + +
    + 1000: ?> +
    The thread got over 1000 posts and has been closed.
    + 900: ?> +
    The thread has reached 900 posts. When it reaches 1000 posts it will be closed.
    + +
    +

    #{thread['subject']} (${(str(thread['length'])+" replies") if thread['length']>1 else "1 reply"})

    + + +

    #{post['num']} : Post deleted by user.

    + +

    #{post['num']} : Post deleted by staff.

    + + +
    + +
    + +

    #{post['num']} : + + + #{post['name']} #{post['tripcode']} + + #{post['name']} + + + + #{post['name']} #{post['tripcode']} + + #{post['name']} + + : #{post['timestamp_formatted']} + rep del

    + +
    ${int(post['file_size'])//1024}KB ${post['file'].split(".")[1].upper()}
    + +
    + #{post['message']} +
    +
    + + +
    #{thread['size']}
    +
    +
    + + +
    + + 1000: ?> +
    The thread got over 1000 posts and has been closed.
    + 950: ?> +
    The thread has reached 950 posts. When it reaches 1000 posts it will be closed.
    + 900: ?> +
    The thread has reached 900 posts. When it reaches 1000 posts it will be closed.
    + +
    + + + +
    + Name:  E-mail: 
    +
    + + + + + +
    + + +
    weabot.py ver Bienvenido a Internet BBS/IB
    + + + \ No newline at end of file diff --git a/cgi/templates/txt_thread.html b/cgi/templates/txt_thread.html new file mode 100644 index 0000000..c438944 --- /dev/null +++ b/cgi/templates/txt_thread.html @@ -0,0 +1,101 @@ + + + + + +
    + 1000: ?> +
    El hilo superó los 1000 mensajes y ha sido cerrado. Ya no se puede postear en él.
    + 900: ?> +
    El hilo ha recibido más de 900 mensajes. Cuando llegue a 1000 será cerrado.
    + +
    +

    #{thread['subject']} (${(str(thread['length'])+" respuestas") if thread['length']>1 else "Una respuesta"})

    + + +

    #{post['num']} : Mensaje eliminado por el usuario.

    + +

    #{post['num']} : Mensaje eliminado por miembro del staff.

    + + +
    + +
    + +

    #{post['num']} : + + + #{post['name']} #{post['tripcode']} + + #{post['name']} + + + + #{post['name']} #{post['tripcode']} + + #{post['name']} + + : #{post['timestamp_formatted']} + rep del

    + +
    ${int(post['file_size'])//1024}KB ${post['file'].split(".")[1].upper()}
    + +
    #{post['message']}
    +
    + + +
    #{thread['size']}
    +
    +
    + + +
    + + 1000: ?> +
    El hilo superó los 1000 mensajes y ha sido cerrado. Ya no se puede postear en él.
    + 900: ?> +
    El hilo ha recibido más de 900 mensajes. Cuando llegue a 1000 será cerrado.
    + +
    + + + +
    + Nombre:  E-mail: 
    +
    + + + + + +
    + + +
    weabot.py ver Bienvenido a Internet BBS/IB
    + + + \ No newline at end of file diff --git a/cgi/templates/txt_threadlist.html b/cgi/templates/txt_threadlist.html new file mode 100644 index 0000000..bb09df4 --- /dev/null +++ b/cgi/templates/txt_threadlist.html @@ -0,0 +1,67 @@ + + + + + + +
    +

    #{board_long}

    + +
    + + +
    + + + +
    +
    #{iter}:
    + +
    #{thread["length"]}
    +
    #{timestamps[iter-1][1]}
    +
    + + +
    + +
    +
    +
    +
    Formulario de nuevo hilo
    +
    + + + + + + + + + + + + + + + + + +
    Asunto:
    Nombre:E-mail:
    Mensaje:
    Archivo:
    +
    Trampa:
    +
    +
    +
    + + + \ No newline at end of file diff --git a/cgi/tenjin.py b/cgi/tenjin.py new file mode 100644 index 0000000..db8cdde --- /dev/null +++ b/cgi/tenjin.py @@ -0,0 +1,2118 @@ +## +## $Release: 1.1.1 $ +## $Copyright: copyright(c) 2007-2012 kuwata-lab.com all rights reserved. $ +## $License: MIT License $ +## +## Permission is hereby granted, free of charge, to any person obtaining +## a copy of this software and associated documentation files (the +## "Software"), to deal in the Software without restriction, including +## without limitation the rights to use, copy, modify, merge, publish, +## distribute, sublicense, and/or sell copies of the Software, and to +## permit persons to whom the Software is furnished to do so, subject to +## the following conditions: +## +## The above copyright notice and this permission notice shall be +## included in all copies or substantial portions of the Software. +## +## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +## EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +## MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +## NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +## LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +## OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +## WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +## + +"""Very fast and light-weight template engine based embedded Python. + See User's Guide and examples for details. + http://www.kuwata-lab.com/tenjin/pytenjin-users-guide.html + http://www.kuwata-lab.com/tenjin/pytenjin-examples.html +""" + +__version__ = "$Release: 1.1.1 $"[10:-2] +__license__ = "$License: MIT License $"[10:-2] +__all__ = ('Template', 'Engine', ) + + +import sys, os, re, time, marshal +from time import time as _time +from os.path import getmtime as _getmtime +from os.path import isfile as _isfile +random = pickle = unquote = None # lazy import +python3 = sys.version_info[0] == 3 +python2 = sys.version_info[0] == 2 + +logger = None + + +## +## utilities +## + +def _write_binary_file(filename, content): + global random + if random is None: from random import random + tmpfile = filename + str(random())[1:] + f = open(tmpfile, 'w+b') # on windows, 'w+b' is preffered than 'wb' + try: + f.write(content) + finally: + f.close() + if os.path.exists(tmpfile): + try: + os.rename(tmpfile, filename) + except: + os.remove(filename) # on windows, existing file should be removed before renaming + os.rename(tmpfile, filename) + +def _read_binary_file(filename): + f = open(filename, 'rb') + try: + return f.read() + finally: + f.close() + +codecs = None # lazy import + +def _read_text_file(filename, encoding=None): + global codecs + if not codecs: import codecs + f = codecs.open(filename, encoding=(encoding or 'utf-8')) + try: + return f.read() + finally: + f.close() + +def _read_template_file(filename, encoding=None): + s = _read_binary_file(filename) ## binary(=str) + if encoding: s = s.decode(encoding) ## binary(=str) to unicode + return s + +_basestring = basestring +_unicode = unicode +_bytes = str + +def _ignore_not_found_error(f, default=None): + try: + return f() + except OSError, ex: + if ex.errno == 2: # error: No such file or directory + return default + raise + +def create_module(module_name, dummy_func=None, **kwargs): + """ex. mod = create_module('tenjin.util')""" + try: + mod = type(sys)(module_name) + except: + # The module creation above does not work for Jython 2.5.2 + import imp + mod = imp.new_module(module_name) + + mod.__file__ = __file__ + mod.__dict__.update(kwargs) + sys.modules[module_name] = mod + if dummy_func: + exec(dummy_func.func_code, mod.__dict__) + return mod + +def _raise(exception_class, *args): + raise exception_class(*args) + + +## +## helper method's module +## + +def _dummy(): + global unquote + unquote = None + global to_str, escape, echo, new_cycle, generate_tostrfunc + global start_capture, stop_capture, capture_as, captured_as, CaptureContext + global _p, _P, _decode_params + + def generate_tostrfunc(encode=None, decode=None): + """Generate 'to_str' function with encode or decode encoding. + ex. generate to_str() function which encodes unicode into binary(=str). + to_str = tenjin.generate_tostrfunc(encode='utf-8') + repr(to_str(u'hoge')) #=> 'hoge' (str) + ex. generate to_str() function which decodes binary(=str) into unicode. + to_str = tenjin.generate_tostrfunc(decode='utf-8') + repr(to_str('hoge')) #=> u'hoge' (unicode) + """ + if encode: + if decode: + raise ValueError("can't specify both encode and decode encoding.") + else: + def to_str(val, _str=str, _unicode=unicode, _isa=isinstance, _encode=encode): + """Convert val into string or return '' if None. Unicode will be encoded into binary(=str).""" + if _isa(val, _str): return val + if val is None: return '' + #if _isa(val, _unicode): return val.encode(_encode) # unicode to binary(=str) + if _isa(val, _unicode): + return val.encode(_encode) # unicode to binary(=str) + return _str(val) + else: + if decode: + def to_str(val, _str=str, _unicode=unicode, _isa=isinstance, _decode=decode): + """Convert val into string or return '' if None. Binary(=str) will be decoded into unicode.""" + #if _isa(val, _str): return val.decode(_decode) # binary(=str) to unicode + if _isa(val, _str): + return val.decode(_decode) + if val is None: return '' + if _isa(val, _unicode): return val + return _unicode(val) + else: + def to_str(val, _str=str, _unicode=unicode, _isa=isinstance): + """Convert val into string or return '' if None. Both binary(=str) and unicode will be retruned as-is.""" + if _isa(val, _str): return val + if val is None: return '' + if _isa(val, _unicode): return val + return _str(val) + return to_str + + to_str = generate_tostrfunc(encode='utf-8') # or encode=None? + + def echo(string): + """add string value into _buf. this is equivarent to '#{string}'.""" + lvars = sys._getframe(1).f_locals # local variables + lvars['_buf'].append(string) + + def new_cycle(*values): + """Generate cycle object. + ex. + cycle = new_cycle('odd', 'even') + print(cycle()) #=> 'odd' + print(cycle()) #=> 'even' + print(cycle()) #=> 'odd' + print(cycle()) #=> 'even' + """ + def gen(values): + i, n = 0, len(values) + while True: + yield values[i] + i = (i + 1) % n + return gen(values).next + + class CaptureContext(object): + + def __init__(self, name, store_to_context=True, lvars=None): + self.name = name + self.store_to_context = store_to_context + self.lvars = lvars or sys._getframe(1).f_locals + + def __enter__(self): + lvars = self.lvars + self._buf_orig = lvars['_buf'] + lvars['_buf'] = _buf = [] + lvars['_extend'] = _buf.extend + return self + + def __exit__(self, *args): + lvars = self.lvars + _buf = lvars['_buf'] + lvars['_buf'] = self._buf_orig + lvars['_extend'] = self._buf_orig.extend + lvars[self.name] = self.captured = ''.join(_buf) + if self.store_to_context and '_context' in lvars: + lvars['_context'][self.name] = self.captured + + def __iter__(self): + self.__enter__() + yield self + self.__exit__() + + def start_capture(varname=None, _depth=1): + """(obsolete) start capturing with name.""" + lvars = sys._getframe(_depth).f_locals + capture_context = CaptureContext(varname, None, lvars) + lvars['_capture_context'] = capture_context + capture_context.__enter__() + + def stop_capture(store_to_context=True, _depth=1): + """(obsolete) stop capturing and return the result of capturing. + if store_to_context is True then the result is stored into _context[varname]. + """ + lvars = sys._getframe(_depth).f_locals + capture_context = lvars.pop('_capture_context', None) + if not capture_context: + raise Exception('stop_capture(): start_capture() is not called before.') + capture_context.store_to_context = store_to_context + capture_context.__exit__() + return capture_context.captured + + def capture_as(name, store_to_context=True): + """capture partial of template.""" + return CaptureContext(name, store_to_context, sys._getframe(1).f_locals) + + def captured_as(name, _depth=1): + """helper method for layout template. + if captured string is found then append it to _buf and return True, + else return False. + """ + lvars = sys._getframe(_depth).f_locals # local variables + if name in lvars: + _buf = lvars['_buf'] + _buf.append(lvars[name]) + return True + return False + + def _p(arg): + """ex. '/show/'+_p("item['id']") => "/show/#{item['id']}" """ + return '<`#%s#`>' % arg # decoded into #{...} by preprocessor + + def _P(arg): + """ex. '%s' % _P("item['id']") => "${item['id']}" """ + return '<`$%s$`>' % arg # decoded into ${...} by preprocessor + + def _decode_params(s): + """decode <`#...#`> and <`$...$`> into #{...} and ${...}""" + global unquote + if unquote is None: + from urllib import unquote + dct = { 'lt':'<', 'gt':'>', 'amp':'&', 'quot':'"', '#039':"'", } + def unescape(s): + #return s.replace('<', '<').replace('>', '>').replace('"', '"').replace(''', "'").replace('&', '&') + return re.sub(r'&(lt|gt|quot|amp|#039);', lambda m: dct[m.group(1)], s) + s = to_str(s) + s = re.sub(r'%3C%60%23(.*?)%23%60%3E', lambda m: '#{%s}' % unquote(m.group(1)), s) + s = re.sub(r'%3C%60%24(.*?)%24%60%3E', lambda m: '${%s}' % unquote(m.group(1)), s) + s = re.sub(r'<`#(.*?)#`>', lambda m: '#{%s}' % unescape(m.group(1)), s) + s = re.sub(r'<`\$(.*?)\$`>', lambda m: '${%s}' % unescape(m.group(1)), s) + s = re.sub(r'<`#(.*?)#`>', r'#{\1}', s) + s = re.sub(r'<`\$(.*?)\$`>', r'${\1}', s) + return s + +helpers = create_module('tenjin.helpers', _dummy, sys=sys, re=re) +helpers.__all__ = ['to_str', 'escape', 'echo', 'new_cycle', 'generate_tostrfunc', + 'start_capture', 'stop_capture', 'capture_as', 'captured_as', + 'not_cached', 'echo_cached', 'cache_as', + '_p', '_P', '_decode_params', + ] +generate_tostrfunc = helpers.generate_tostrfunc + + +## +## escaped module +## +def _dummy(): + global is_escaped, as_escaped, to_escaped + global Escaped, EscapedStr, EscapedUnicode + global __all__ + __all__ = ('is_escaped', 'as_escaped', 'to_escaped', ) #'Escaped', 'EscapedStr', + + class Escaped(object): + """marking class that object is already escaped.""" + pass + + def is_escaped(value): + """return True if value is marked as escaped, else return False.""" + return isinstance(value, Escaped) + + class EscapedStr(str, Escaped): + """string class which is marked as escaped.""" + pass + + class EscapedUnicode(unicode, Escaped): + """unicode class which is marked as escaped.""" + pass + + def as_escaped(s): + """mark string as escaped, without escaping.""" + if isinstance(s, str): return EscapedStr(s) + if isinstance(s, unicode): return EscapedUnicode(s) + raise TypeError("as_escaped(%r): expected str or unicode." % (s, )) + + def to_escaped(value): + """convert any value into string and escape it. + if value is already marked as escaped, don't escape it.""" + if hasattr(value, '__html__'): + value = value.__html__() + if is_escaped(value): + #return value # EscapedUnicode should be convered into EscapedStr + return as_escaped(_helpers.to_str(value)) + #if isinstance(value, _basestring): + # return as_escaped(_helpers.escape(value)) + return as_escaped(_helpers.escape(_helpers.to_str(value))) + +escaped = create_module('tenjin.escaped', _dummy, _helpers=helpers) + + +## +## module for html +## +def _dummy(): + global escape_html, escape_xml, escape, tagattr, tagattrs, _normalize_attrs + global checked, selected, disabled, nl2br, text2html, nv, js_link + + #_escape_table = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' } + #_escape_pattern = re.compile(r'[&<>"]') + ##_escape_callable = lambda m: _escape_table[m.group(0)] + ##_escape_callable = lambda m: _escape_table.__get__(m.group(0)) + #_escape_get = _escape_table.__getitem__ + #_escape_callable = lambda m: _escape_get(m.group(0)) + #_escape_sub = _escape_pattern.sub + + #def escape_html(s): + # return s # 3.02 + + #def escape_html(s): + # return _escape_pattern.sub(_escape_callable, s) # 6.31 + + #def escape_html(s): + # return _escape_sub(_escape_callable, s) # 6.01 + + #def escape_html(s, _p=_escape_pattern, _f=_escape_callable): + # return _p.sub(_f, s) # 6.27 + + #def escape_html(s, _sub=_escape_pattern.sub, _callable=_escape_callable): + # return _sub(_callable, s) # 6.04 + + #def escape_html(s): + # s = s.replace('&', '&') + # s = s.replace('<', '<') + # s = s.replace('>', '>') + # s = s.replace('"', '"') + # return s # 5.83 + + def escape_html(s): + """Escape '&', '<', '>', '"' into '&', '<', '>', '"'.""" + return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''') # 5.72 + + escape_xml = escape_html # for backward compatibility + + def tagattr(name, expr, value=None, escape=True): + """(experimental) Return ' name="value"' if expr is true value, else '' (empty string). + If value is not specified, expr is used as value instead.""" + if not expr and expr != 0: return _escaped.as_escaped('') + if value is None: value = expr + if escape: value = _escaped.to_escaped(value) + return _escaped.as_escaped(' %s="%s"' % (name, value)) + + def tagattrs(**kwargs): + """(experimental) built html tag attribtes. + ex. + >>> tagattrs(klass='main', size=20) + ' class="main" size="20"' + >>> tagattrs(klass='', size=0) + '' + """ + kwargs = _normalize_attrs(kwargs) + esc = _escaped.to_escaped + s = ''.join([ ' %s="%s"' % (k, esc(v)) for k, v in kwargs.iteritems() if v or v == 0 ]) + return _escaped.as_escaped(s) + + def _normalize_attrs(kwargs): + if 'klass' in kwargs: kwargs['class'] = kwargs.pop('klass') + if 'checked' in kwargs: kwargs['checked'] = kwargs.pop('checked') and 'checked' or None + if 'selected' in kwargs: kwargs['selected'] = kwargs.pop('selected') and 'selected' or None + if 'disabled' in kwargs: kwargs['disabled'] = kwargs.pop('disabled') and 'disabled' or None + return kwargs + + def checked(expr): + """return ' checked="checked"' if expr is true.""" + return _escaped.as_escaped(expr and ' checked="checked"' or '') + + def selected(expr): + """return ' selected="selected"' if expr is true.""" + return _escaped.as_escaped(expr and ' selected="selected"' or '') + + def disabled(expr): + """return ' disabled="disabled"' if expr is true.""" + return _escaped.as_escaped(expr and ' disabled="disabled"' or '') + + def nl2br(text): + """replace "\n" to "
    \n" and return it.""" + if not text: + return _escaped.as_escaped('') + return _escaped.as_escaped(text.replace('\n', '
    \n')) + + def text2html(text, use_nbsp=True): + """(experimental) escape xml characters, replace "\n" to "
    \n", and return it.""" + if not text: + return _escaped.as_escaped('') + s = _escaped.to_escaped(text) + if use_nbsp: s = s.replace(' ', '  ') + #return nl2br(s) + s = s.replace('\n', '
    \n') + return _escaped.as_escaped(s) + + def nv(name, value, sep=None, **kwargs): + """(experimental) Build name and value attributes. + ex. + >>> nv('rank', 'A') + 'name="rank" value="A"' + >>> nv('rank', 'A', '.') + 'name="rank" value="A" id="rank.A"' + >>> nv('rank', 'A', '.', checked=True) + 'name="rank" value="A" id="rank.A" checked="checked"' + >>> nv('rank', 'A', '.', klass='error', style='color:red') + 'name="rank" value="A" id="rank.A" class="error" style="color:red"' + """ + name = _escaped.to_escaped(name) + value = _escaped.to_escaped(value) + s = sep and 'name="%s" value="%s" id="%s"' % (name, value, name+sep+value) \ + or 'name="%s" value="%s"' % (name, value) + html = kwargs and s + tagattrs(**kwargs) or s + return _escaped.as_escaped(html) + + def js_link(label, onclick, **kwargs): + s = kwargs and tagattrs(**kwargs) or '' + html = '%s' % \ + (_escaped.to_escaped(onclick), s, _escaped.to_escaped(label)) + return _escaped.as_escaped(html) + +html = create_module('tenjin.html', _dummy, helpers=helpers, _escaped=escaped) +helpers.escape = html.escape_html +helpers.html = html # for backward compatibility +sys.modules['tenjin.helpers.html'] = html + + +## +## utility function to set default encoding of template files +## +_template_encoding = (None, 'utf-8') # encodings for decode and encode + +def set_template_encoding(decode=None, encode=None): + """Set default encoding of template files. + This should be called before importing helper functions. + ex. + ## I like template files to be unicode-base like Django. + import tenjin + tenjin.set_template_encoding('utf-8') # should be called before importing helpers + from tenjin.helpers import * + """ + global _template_encoding + if _template_encoding == (decode, encode): + return + if decode and encode: + raise ValueError("set_template_encoding(): cannot specify both decode and encode.") + if not decode and not encode: + raise ValueError("set_template_encoding(): decode or encode should be specified.") + if decode: + Template.encoding = decode # unicode base template + helpers.to_str = helpers.generate_tostrfunc(decode=decode) + else: + Template.encoding = None # binary base template + helpers.to_str = helpers.generate_tostrfunc(encode=encode) + _template_encoding = (decode, encode) + + +## +## Template class +## + +class TemplateSyntaxError(SyntaxError): + + def build_error_message(self): + ex = self + if not ex.text: + return self.args[0] + return ''.join([ + "%s:%s:%s: %s\n" % (ex.filename, ex.lineno, ex.offset, ex.msg, ), + "%4d: %s\n" % (ex.lineno, ex.text.rstrip(), ), + " %s^\n" % (' ' * ex.offset, ), + ]) + + +class Template(object): + """Convert and evaluate embedded python string. + See User's Guide and examples for details. + http://www.kuwata-lab.com/tenjin/pytenjin-users-guide.html + http://www.kuwata-lab.com/tenjin/pytenjin-examples.html + """ + + ## default value of attributes + filename = None + encoding = None + escapefunc = 'escape' + tostrfunc = 'to_str' + indent = 4 + preamble = None # "_buf = []; _expand = _buf.expand; _to_str = to_str; _escape = escape" + postamble = None # "print ''.join(_buf)" + smarttrim = None + args = None + timestamp = None + trace = False # if True then '' and '' are printed + + def __init__(self, filename=None, encoding=None, input=None, escapefunc=None, tostrfunc=None, + indent=None, preamble=None, postamble=None, smarttrim=None, trace=None): + """Initailizer of Template class. + + filename:str (=None) + Filename to convert (optional). If None, no convert. + encoding:str (=None) + Encoding name. If specified, template string is converted into + unicode object internally. + Template.render() returns str object if encoding is None, + else returns unicode object if encoding name is specified. + input:str (=None) + Input string. In other words, content of template file. + Template file will not be read if this argument is specified. + escapefunc:str (='escape') + Escape function name. + tostrfunc:str (='to_str') + 'to_str' function name. + indent:int (=4) + Indent width. + preamble:str or bool (=None) + Preamble string which is inserted into python code. + If true, '_buf = []; ' is used insated. + postamble:str or bool (=None) + Postamble string which is appended to python code. + If true, 'print("".join(_buf))' is used instead. + smarttrim:bool (=None) + If True then "
    \\n#{_context}\\n
    " is parsed as + "
    \\n#{_context}
    ". + """ + if encoding is not None: self.encoding = encoding + if escapefunc is not None: self.escapefunc = escapefunc + if tostrfunc is not None: self.tostrfunc = tostrfunc + if indent is not None: self.indent = indent + if preamble is not None: self.preamble = preamble + if postamble is not None: self.postamble = postamble + if smarttrim is not None: self.smarttrim = smarttrim + if trace is not None: self.trace = trace + # + if preamble is True: self.preamble = "_buf = []" + if postamble is True: self.postamble = "print(''.join(_buf))" + if input: + self.convert(input, filename) + self.timestamp = False # False means 'file not exist' (= Engine should not check timestamp of file) + elif filename: + self.convert_file(filename) + else: + self._reset() + + def _reset(self, input=None, filename=None): + self.script = None + self.bytecode = None + self.input = input + self.filename = filename + if input != None: + i = input.find("\n") + if i < 0: + self.newline = "\n" # or None + elif len(input) >= 2 and input[i-1] == "\r": + self.newline = "\r\n" + else: + self.newline = "\n" + self._localvars_assignments_added = False + + def _localvars_assignments(self): + return "_extend=_buf.extend;_to_str=%s;_escape=%s; " % (self.tostrfunc, self.escapefunc) + + def before_convert(self, buf): + if self.preamble: + eol = self.input.startswith('([ \t]*\r?\n)?', re.S) + + def stmt_pattern(self): + pat = self.STMT_PATTERN + if isinstance(pat, tuple): + pat = self.__class__.STMT_PATTERN = re.compile(*pat) + return pat + + def parse_stmts(self, buf, input): + if not input: return + rexp = self.stmt_pattern() + is_bol = True + index = 0 + for m in rexp.finditer(input): + mspace, code, rspace = m.groups() + #mspace, close, rspace = m.groups() + #code = input[m.start()+4+len(mspace):m.end()-len(close)-(rspace and len(rspace) or 0)] + text = input[index:m.start()] + index = m.end() + ## detect spaces at beginning of line + lspace = None + if text == '': + if is_bol: + lspace = '' + elif text[-1] == '\n': + lspace = '' + else: + rindex = text.rfind('\n') + if rindex < 0: + if is_bol and text.isspace(): + lspace, text = text, '' + else: + s = text[rindex+1:] + if s.isspace(): + lspace, text = s, text[:rindex+1] + #is_bol = rspace is not None + ## add text, spaces, and statement + self.parse_exprs(buf, text, is_bol) + is_bol = rspace is not None + #if mspace == "\n": + if mspace and mspace.endswith("\n"): + code = "\n" + (code or "") + #if rspace == "\n": + if rspace and rspace.endswith("\n"): + code = (code or "") + "\n" + if code: + code = self.statement_hook(code) + m = self._match_to_args_declaration(code) + if m: + self._add_args_declaration(buf, m) + else: + self.add_stmt(buf, code) + rest = input[index:] + if rest: + self.parse_exprs(buf, rest) + self._arrange_indent(buf) + + def statement_hook(self, stmt): + """expand macros and parse '#@ARGS' in a statement.""" + return stmt.replace("\r\n", "\n") # Python can't handle "\r\n" in code + + def _match_to_args_declaration(self, stmt): + if self.args is not None: + return None + args_pattern = r'^ *#@ARGS(?:[ \t]+(.*?))?$' + return re.match(args_pattern, stmt) + + def _add_args_declaration(self, buf, m): + arr = (m.group(1) or '').split(',') + args = []; declares = [] + for s in arr: + arg = s.strip() + if not s: continue + if not re.match('^[a-zA-Z_]\w*$', arg): + raise ValueError("%r: invalid template argument." % arg) + args.append(arg) + declares.append("%s = _context.get('%s'); " % (arg, arg)) + self.args = args + #nl = stmt[m.end():] + #if nl: declares.append(nl) + buf.append(''.join(declares) + "\n") + + s = '(?:\{.*?\}.*?)*' + EXPR_PATTERN = (r'#\{(.*?'+s+r')\}|\$\{(.*?'+s+r')\}|\{=(?:=(.*?)=|(.*?))=\}', re.S) + del s + + def expr_pattern(self): + pat = self.EXPR_PATTERN + if isinstance(pat, tuple): + self.__class__.EXPR_PATTERN = pat = re.compile(*pat) + return pat + + def get_expr_and_flags(self, match): + expr1, expr2, expr3, expr4 = match.groups() + if expr1 is not None: return expr1, (False, True) # not escape, call to_str + if expr2 is not None: return expr2, (True, True) # call escape, call to_str + if expr3 is not None: return expr3, (False, True) # not escape, call to_str + if expr4 is not None: return expr4, (True, True) # call escape, call to_str + + def parse_exprs(self, buf, input, is_bol=False): + buf2 = [] + self._parse_exprs(buf2, input, is_bol) + if buf2: + buf.append(''.join(buf2)) + + def _parse_exprs(self, buf, input, is_bol=False): + if not input: return + self.start_text_part(buf) + rexp = self.expr_pattern() + smarttrim = self.smarttrim + nl = self.newline + nl_len = len(nl) + pos = 0 + for m in rexp.finditer(input): + start = m.start() + text = input[pos:start] + pos = m.end() + expr, flags = self.get_expr_and_flags(m) + # + if text: + self.add_text(buf, text) + self.add_expr(buf, expr, *flags) + # + if smarttrim: + flag_bol = text.endswith(nl) or not text and (start > 0 or is_bol) + if flag_bol and not flags[0] and input[pos:pos+nl_len] == nl: + pos += nl_len + buf.append("\n") + if smarttrim: + if buf and buf[-1] == "\n": + buf.pop() + rest = input[pos:] + if rest: + self.add_text(buf, rest, True) + self.stop_text_part(buf) + if input[-1] == '\n': + buf.append("\n") + + def start_text_part(self, buf): + self._add_localvars_assignments_to_text(buf) + #buf.append("_buf.extend((") + buf.append("_extend((") + + def _add_localvars_assignments_to_text(self, buf): + if not self._localvars_assignments_added: + self._localvars_assignments_added = True + buf.append(self._localvars_assignments()) + + def stop_text_part(self, buf): + buf.append("));") + + def _quote_text(self, text): + text = re.sub(r"(['\\\\])", r"\\\1", text) + text = text.replace("\r\n", "\\r\n") + return text + + def add_text(self, buf, text, encode_newline=False): + if not text: return + use_unicode = self.encoding and python2 + buf.append(use_unicode and "u'''" or "'''") + text = self._quote_text(text) + if not encode_newline: buf.extend((text, "''', ")) + elif text.endswith("\r\n"): buf.extend((text[0:-2], "\\r\\n''', ")) + elif text.endswith("\n"): buf.extend((text[0:-1], "\\n''', ")) + else: buf.extend((text, "''', ")) + + _add_text = add_text + + def add_expr(self, buf, code, *flags): + if not code or code.isspace(): return + flag_escape, flag_tostr = flags + if not self.tostrfunc: flag_tostr = False + if not self.escapefunc: flag_escape = False + if flag_tostr and flag_escape: s1, s2 = "_escape(_to_str(", ")), " + elif flag_tostr: s1, s2 = "_to_str(", "), " + elif flag_escape: s1, s2 = "_escape(", "), " + else: s1, s2 = "(", "), " + buf.extend((s1, code, s2, )) + + def add_stmt(self, buf, code): + if not code: return + lines = code.splitlines(True) # keep "\n" + if lines[-1][-1] != "\n": + lines[-1] = lines[-1] + "\n" + buf.extend(lines) + self._add_localvars_assignments_to_stmts(buf) + + def _add_localvars_assignments_to_stmts(self, buf): + if self._localvars_assignments_added: + return + for index, stmt in enumerate(buf): + if not re.match(r'^[ \t]*(?:\#|_buf ?= ?\[\]|from __future__)', stmt): + break + else: + return + self._localvars_assignments_added = True + if re.match(r'^[ \t]*(if|for|while|def|with|class)\b', stmt): + buf.insert(index, self._localvars_assignments() + "\n") + else: + buf[index] = self._localvars_assignments() + buf[index] + + + _START_WORDS = dict.fromkeys(('for', 'if', 'while', 'def', 'try:', 'with', 'class'), True) + _END_WORDS = dict.fromkeys(('#end', '#endfor', '#endif', '#endwhile', '#enddef', '#endtry', '#endwith', '#endclass'), True) + _CONT_WORDS = dict.fromkeys(('elif', 'else:', 'except', 'except:', 'finally:'), True) + _WORD_REXP = re.compile(r'\S+') + + depth = -1 + + ## + ## ex. + ## input = r""" + ## if items: + ## _buf.extend(('
      \n', )) + ## i = 0 + ## for item in items: + ## i += 1 + ## _buf.extend(('
    • ', to_str(item), '
    • \n', )) + ## #endfor + ## _buf.extend(('
    \n', )) + ## #endif + ## """[1:] + ## lines = input.splitlines(True) + ## block = self.parse_lines(lines) + ## #=> [ "if items:\n", + ## [ "_buf.extend(('
      \n', ))\n", + ## "i = 0\n", + ## "for item in items:\n", + ## [ "i += 1\n", + ## "_buf.extend(('
    • ', to_str(item), '
    • \n', ))\n", + ## ], + ## "#endfor\n", + ## "_buf.extend(('
    \n', ))\n", + ## ], + ## "#endif\n", + ## ] + def parse_lines(self, lines): + block = [] + try: + self._parse_lines(lines.__iter__(), False, block, 0) + except StopIteration: + if self.depth > 0: + fname, linenum, colnum, linetext = self.filename, len(lines), None, None + raise TemplateSyntaxError("unexpected EOF.", (fname, linenum, colnum, linetext)) + else: + pass + return block + + def _parse_lines(self, lines_iter, end_block, block, linenum): + if block is None: block = [] + _START_WORDS = self._START_WORDS + _END_WORDS = self._END_WORDS + _CONT_WORDS = self._CONT_WORDS + _WORD_REXP = self._WORD_REXP + get_line = lines_iter.next + while True: + line = get_line() + linenum += line.count("\n") + m = _WORD_REXP.search(line) + if not m: + block.append(line) + continue + word = m.group(0) + if word in _END_WORDS: + if word != end_block and word != '#end': + if end_block is False: + msg = "'%s' found but corresponding statement is missing." % (word, ) + else: + msg = "'%s' expected but got '%s'." % (end_block, word) + colnum = m.start() + 1 + raise TemplateSyntaxError(msg, (self.filename, linenum, colnum, line)) + return block, line, None, linenum + elif line.endswith(':\n') or line.endswith(':\r\n'): + if word in _CONT_WORDS: + return block, line, word, linenum + elif word in _START_WORDS: + block.append(line) + self.depth += 1 + cont_word = None + try: + child_block, line, cont_word, linenum = \ + self._parse_lines(lines_iter, '#end'+word, [], linenum) + block.extend((child_block, line, )) + while cont_word: # 'elif' or 'else:' + child_block, line, cont_word, linenum = \ + self._parse_lines(lines_iter, '#end'+word, [], linenum) + block.extend((child_block, line, )) + except StopIteration: + msg = "'%s' is not closed." % (cont_word or word) + colnum = m.start() + 1 + raise TemplateSyntaxError(msg, (self.filename, linenum, colnum, line)) + self.depth -= 1 + else: + block.append(line) + else: + block.append(line) + assert "unreachable" + + def _join_block(self, block, buf, depth): + indent = ' ' * (self.indent * depth) + for line in block: + if isinstance(line, list): + self._join_block(line, buf, depth+1) + elif line.isspace(): + buf.append(line) + else: + buf.append(indent + line.lstrip()) + + def _arrange_indent(self, buf): + """arrange indentation of statements in buf""" + block = self.parse_lines(buf) + buf[:] = [] + self._join_block(block, buf, 0) + + + def render(self, context=None, globals=None, _buf=None): + """Evaluate python code with context dictionary. + If _buf is None then return the result of evaluation as str, + else return None. + + context:dict (=None) + Context object to evaluate. If None then new dict is created. + globals:dict (=None) + Global object. If None then globals() is used. + _buf:list (=None) + If None then new list is created. + """ + if context is None: + locals = context = {} + elif self.args is None: + locals = context.copy() + else: + locals = {} + if '_engine' in context: + context.get('_engine').hook_context(locals) + locals['_context'] = context + if globals is None: + globals = sys._getframe(1).f_globals + bufarg = _buf + if _buf is None: + _buf = [] + locals['_buf'] = _buf + if not self.bytecode: + self.compile() + if self.trace: + _buf.append("\n" % self.filename) + exec(self.bytecode, globals, locals) + _buf.append("\n" % self.filename) + else: + exec(self.bytecode, globals, locals) + if bufarg is not None: + return bufarg + elif not logger: + return ''.join(_buf) + else: + try: + return ''.join(_buf) + except UnicodeDecodeError, ex: + logger.error("[tenjin.Template] " + str(ex)) + logger.error("[tenjin.Template] (_buf=%r)" % (_buf, )) + raise + + def compile(self): + """compile self.script into self.bytecode""" + self.bytecode = compile(self.script, self.filename or '(tenjin)', 'exec') + + +## +## preprocessor class +## + +class Preprocessor(Template): + """Template class for preprocessing.""" + + STMT_PATTERN = (r'<\?PY( |\t|\r?\n)(.*?) ?\?>([ \t]*\r?\n)?', re.S) + + EXPR_PATTERN = (r'#\{\{(.*?)\}\}|\$\{\{(.*?)\}\}|\{#=(?:=(.*?)=|(.*?))=#\}', re.S) + + def add_expr(self, buf, code, *flags): + if not code or code.isspace(): + return + code = "_decode_params(%s)" % code + Template.add_expr(self, buf, code, *flags) + + +class TemplatePreprocessor(object): + factory = Preprocessor + + def __init__(self, factory=None): + if factory is not None: self.factory = factory + self.globals = sys._getframe(1).f_globals + + def __call__(self, input, **kwargs): + filename = kwargs.get('filename') + context = kwargs.get('context') or {} + globals = kwargs.get('globals') or self.globals + template = self.factory() + template.convert(input, filename) + return template.render(context, globals=globals) + + +class TrimPreprocessor(object): + + _rexp = re.compile(r'^[ \t]+<', re.M) + _rexp_all = re.compile(r'^[ \t]+', re.M) + + def __init__(self, all=False): + self.all = all + + def __call__(self, input, **kwargs): + if self.all: + return self._rexp_all.sub('', input) + else: + return self._rexp.sub('<', input) + + +class PrefixedLinePreprocessor(object): + + def __init__(self, prefix='::(?=[ \t]|$)'): + self.prefix = prefix + self.regexp = re.compile(r'^([ \t]*)' + prefix + r'(.*)', re.M) + + def convert_prefixed_lines(self, text): + fn = lambda m: "%s" % (m.group(1), m.group(2)) + return self.regexp.sub(fn, text) + + STMT_REXP = re.compile(r'<\?py\s.*?\?>', re.S) + + def __call__(self, input, **kwargs): + buf = []; append = buf.append + pos = 0 + for m in self.STMT_REXP.finditer(input): + text = input[pos:m.start()] + stmt = m.group(0) + pos = m.end() + if text: append(self.convert_prefixed_lines(text)) + append(stmt) + rest = input[pos:] + if rest: append(self.convert_prefixed_lines(rest)) + return "".join(buf) + + +class ParseError(Exception): + pass + + +class JavaScriptPreprocessor(object): + + def __init__(self, **attrs): + self._attrs = attrs + + def __call__(self, input, **kwargs): + return self.parse(input, kwargs.get('filename')) + + def parse(self, input, filename=None): + buf = [] + self._parse_chunks(input, buf, filename) + return ''.join(buf) + + CHUNK_REXP = re.compile(r'(?:^( *)<|<)!-- *#(?:JS: (\$?\w+(?:\.\w+)*\(.*?\))|/JS:?) *-->([ \t]*\r?\n)?', re.M) + + def _scan_chunks(self, input, filename): + rexp = self.CHUNK_REXP + pos = 0 + curr_funcdecl = None + for m in rexp.finditer(input): + lspace, funcdecl, rspace = m.groups() + text = input[pos:m.start()] + pos = m.end() + if funcdecl: + if curr_funcdecl: + raise ParseError("%s is nested in %s. (file: %s, line: %s)" % \ + (funcdecl, curr_funcdecl, filename, _linenum(input, m.start()), )) + curr_funcdecl = funcdecl + else: + if not curr_funcdecl: + raise ParseError("unexpected ''. (file: %s, line: %s)" % \ + (filename, _linenum(input, m.start()), )) + curr_funcdecl = None + yield text, lspace, funcdecl, rspace, False + if curr_funcdecl: + raise ParseError("%s is not closed by ''. (file: %s, line: %s)" % \ + (curr_funcdecl, filename, _linenum(input, m.start()), )) + rest = input[pos:] + yield rest, None, None, None, True + + def _parse_chunks(self, input, buf, filename=None): + if not input: return + stag = '([ \t]*\r?\n)?', re.M | re.S) + + def _scan_stmts(self, input): + rexp = self.STMT_REXP + pos = 0 + for m in rexp.finditer(input): + lspace, code, rspace = m.groups() + text = input[pos:m.start()] + pos = m.end() + yield text, lspace, code, rspace, False + rest = input[pos:] + yield rest, None, None, None, True + + def _parse_stmts(self, input, buf): + if not input: return + for text, lspace, code, rspace, end_p in self._scan_stmts(input): + if end_p: break + if lspace is not None and rspace is not None: + self._parse_exprs(text, buf) + buf.extend((lspace, code, rspace)) + else: + if lspace: + text += lspace + self._parse_exprs(text, buf) + buf.append(code) + if rspace: + self._parse_exprs(rspace, buf) + if text: + self._parse_exprs(text, buf) + + s = r'(?:\{[^{}]*?\}[^{}]*?)*' + EXPR_REXP = re.compile(r'\{=(.*?)=\}|([$#])\{(.*?' + s + r')\}', re.S) + del s + + def _get_expr(self, m): + code1, ch, code2 = m.groups() + if ch: + code = code2 + escape_p = ch == '$' + elif code1[0] == code1[-1] == '=': + code = code1[1:-1] + escape_p = False + else: + code = code1 + escape_p = True + return code, escape_p + + def _scan_exprs(self, input): + rexp = self.EXPR_REXP + pos = 0 + for m in rexp.finditer(input): + text = input[pos:m.start()] + pos = m.end() + code, escape_p = self._get_expr(m) + yield text, code, escape_p, False + rest = input[pos:] + yield rest, None, None, True + + def _parse_exprs(self, input, buf): + if not input: return + buf.append("_buf+=") + extend = buf.extend + op = '' + for text, code, escape_p, end_p in self._scan_exprs(input): + if end_p: + break + if text: + extend((op, self._escape_text(text))) + op = '+' + if code: + extend((op, escape_p and '_E(' or '_S(', code, ')')) + op = '+' + rest = text + if rest: + extend((op, self._escape_text(rest))) + if input.endswith("\n"): + buf.append(";\n") + else: + buf.append(";") + + def _escape_text(self, text): + lines = text.splitlines(True) + fn = self._escape_str + s = "\\\n".join( fn(line) for line in lines ) + return "".join(("'", s, "'")) + + def _escape_str(self, string): + return string.replace("\\", "\\\\").replace("'", "\\'").replace("\n", r"\n") + + +def _linenum(input, pos): + return input[0:pos].count("\n") + 1 + + +JS_FUNC = r""" +function _S(x){return x==null?'':x;} +function _E(x){return x==null?'':typeof(x)!=='string'?x:x.replace(/[&<>"']/g,_EF);} +var _ET={'&':"&",'<':"<",'>':">",'"':""","'":"'"}; +function _EF(c){return _ET[c];}; +"""[1:-1] +JS_FUNC = escaped.EscapedStr(JS_FUNC) + + + +## +## cache storages +## + +class CacheStorage(object): + """[abstract] Template object cache class (in memory and/or file)""" + + def __init__(self): + self.items = {} # key: full path, value: template object + + def get(self, cachepath, create_template): + """get template object. if not found, load attributes from cache file and restore template object.""" + template = self.items.get(cachepath) + if not template: + dct = self._load(cachepath) + if dct: + template = create_template() + for k in dct: + setattr(template, k, dct[k]) + self.items[cachepath] = template + return template + + def set(self, cachepath, template): + """set template object and save template attributes into cache file.""" + self.items[cachepath] = template + dct = self._save_data_of(template) + return self._store(cachepath, dct) + + def _save_data_of(self, template): + return { 'args' : template.args, 'bytecode' : template.bytecode, + 'script': template.script, 'timestamp': template.timestamp } + + def unset(self, cachepath): + """remove template object from dict and cache file.""" + self.items.pop(cachepath, None) + return self._delete(cachepath) + + def clear(self): + """remove all template objects and attributes from dict and cache file.""" + d, self.items = self.items, {} + for k in d.iterkeys(): + self._delete(k) + d.clear() + + def _load(self, cachepath): + """(abstract) load dict object which represents template object attributes from cache file.""" + raise NotImplementedError.new("%s#_load(): not implemented yet." % self.__class__.__name__) + + def _store(self, cachepath, template): + """(abstract) load dict object which represents template object attributes from cache file.""" + raise NotImplementedError.new("%s#_store(): not implemented yet." % self.__class__.__name__) + + def _delete(self, cachepath): + """(abstract) remove template object from cache file.""" + raise NotImplementedError.new("%s#_delete(): not implemented yet." % self.__class__.__name__) + + +class MemoryCacheStorage(CacheStorage): + + def _load(self, cachepath): + return None + + def _store(self, cachepath, template): + pass + + def _delete(self, cachepath): + pass + + +class FileCacheStorage(CacheStorage): + + def _load(self, cachepath): + if not _isfile(cachepath): return None + if logger: logger.info("[tenjin.%s] load cache (file=%r)" % (self.__class__.__name__, cachepath)) + data = _read_binary_file(cachepath) + return self._restore(data) + + def _store(self, cachepath, dct): + if logger: logger.info("[tenjin.%s] store cache (file=%r)" % (self.__class__.__name__, cachepath)) + data = self._dump(dct) + _write_binary_file(cachepath, data) + + def _restore(self, data): + raise NotImplementedError("%s._restore(): not implemented yet." % self.__class__.__name__) + + def _dump(self, dct): + raise NotImplementedError("%s._dump(): not implemented yet." % self.__class__.__name__) + + def _delete(self, cachepath): + _ignore_not_found_error(lambda: os.unlink(cachepath)) + + +class MarshalCacheStorage(FileCacheStorage): + + def _restore(self, data): + return marshal.loads(data) + + def _dump(self, dct): + return marshal.dumps(dct) + + +class PickleCacheStorage(FileCacheStorage): + + def __init__(self, *args, **kwargs): + global pickle + if pickle is None: + import cPickle as pickle + FileCacheStorage.__init__(self, *args, **kwargs) + + def _restore(self, data): + return pickle.loads(data) + + def _dump(self, dct): + dct.pop('bytecode', None) + return pickle.dumps(dct) + + +class TextCacheStorage(FileCacheStorage): + + def _restore(self, data): + header, script = data.split("\n\n", 1) + timestamp = encoding = args = None + for line in header.split("\n"): + key, val = line.split(": ", 1) + if key == 'timestamp': timestamp = float(val) + elif key == 'encoding': encoding = val + elif key == 'args': args = val.split(', ') + if encoding: script = script.decode(encoding) ## binary(=str) to unicode + return {'args': args, 'script': script, 'timestamp': timestamp} + + def _dump(self, dct): + s = dct['script'] + if dct.get('encoding') and isinstance(s, unicode): + s = s.encode(dct['encoding']) ## unicode to binary(=str) + sb = [] + sb.append("timestamp: %s\n" % dct['timestamp']) + if dct.get('encoding'): + sb.append("encoding: %s\n" % dct['encoding']) + if dct.get('args') is not None: + sb.append("args: %s\n" % ', '.join(dct['args'])) + sb.append("\n") + sb.append(s) + s = ''.join(sb) + if python3: + if isinstance(s, str): + s = s.encode(dct.get('encoding') or 'utf-8') ## unicode(=str) to binary + return s + + def _save_data_of(self, template): + dct = FileCacheStorage._save_data_of(self, template) + dct['encoding'] = template.encoding + return dct + + + +## +## abstract class for data cache +## +class KeyValueStore(object): + + def get(self, key, *options): + raise NotImplementedError("%s.get(): not implemented yet." % self.__class__.__name__) + + def set(self, key, value, *options): + raise NotImplementedError("%s.set(): not implemented yet." % self.__class__.__name__) + + def delete(self, key, *options): + raise NotImplementedError("%s.del(): not implemented yet." % self.__class__.__name__) + + def has(self, key, *options): + raise NotImplementedError("%s.has(): not implemented yet." % self.__class__.__name__) + + +## +## memory base data cache +## +class MemoryBaseStore(KeyValueStore): + + def __init__(self): + self.values = {} + + def get(self, key, original_timestamp=None): + tupl = self.values.get(key) + if not tupl: + return None + value, created_at, expires_at = tupl + if original_timestamp is not None and created_at < original_timestamp: + self.delete(key) + return None + if expires_at < _time(): + self.delete(key) + return None + return value + + def set(self, key, value, lifetime=0): + created_at = _time() + expires_at = lifetime and created_at + lifetime or 0 + self.values[key] = (value, created_at, expires_at) + return True + + def delete(self, key): + try: + del self.values[key] + return True + except KeyError: + return False + + def has(self, key): + pair = self.values.get(key) + if not pair: + return False + value, created_at, expires_at = pair + if expires_at and expires_at < _time(): + self.delete(key) + return False + return True + + +## +## file base data cache +## +class FileBaseStore(KeyValueStore): + + lifetime = 604800 # = 60*60*24*7 + + def __init__(self, root_path, encoding=None): + if not os.path.isdir(root_path): + raise ValueError("%r: directory not found." % (root_path, )) + self.root_path = root_path + if encoding is None and python3: + encoding = 'utf-8' + self.encoding = encoding + + _pat = re.compile(r'[^-.\/\w]') + + def filepath(self, key, _pat1=_pat): + return os.path.join(self.root_path, _pat1.sub('_', key)) + + def get(self, key, original_timestamp=None): + fpath = self.filepath(key) + #if not _isfile(fpath): return None + stat = _ignore_not_found_error(lambda: os.stat(fpath), None) + if stat is None: + return None + created_at = stat.st_ctime + expires_at = stat.st_mtime + if original_timestamp is not None and created_at < original_timestamp: + self.delete(key) + return None + if expires_at < _time(): + self.delete(key) + return None + if self.encoding: + f = lambda: _read_text_file(fpath, self.encoding) + else: + f = lambda: _read_binary_file(fpath) + return _ignore_not_found_error(f, None) + + def set(self, key, value, lifetime=0): + fpath = self.filepath(key) + dirname = os.path.dirname(fpath) + if not os.path.isdir(dirname): + os.makedirs(dirname) + now = _time() + if isinstance(value, _unicode): + value = value.encode(self.encoding or 'utf-8') + _write_binary_file(fpath, value) + expires_at = now + (lifetime or self.lifetime) # timestamp + os.utime(fpath, (expires_at, expires_at)) + return True + + def delete(self, key): + fpath = self.filepath(key) + ret = _ignore_not_found_error(lambda: os.unlink(fpath), False) + return ret != False + + def has(self, key): + fpath = self.filepath(key) + if not _isfile(fpath): + return False + if _getmtime(fpath) < _time(): + self.delete(key) + return False + return True + + + +## +## html fragment cache helper class +## +class FragmentCacheHelper(object): + """html fragment cache helper class.""" + + lifetime = 60 # 1 minute + prefix = None + + def __init__(self, store, lifetime=None, prefix=None): + self.store = store + if lifetime is not None: self.lifetime = lifetime + if prefix is not None: self.prefix = prefix + + def not_cached(self, cache_key, lifetime=None): + """(obsolete. use cache_as() instead of this.) + html fragment cache helper. see document of FragmentCacheHelper class.""" + context = sys._getframe(1).f_locals['_context'] + context['_cache_key'] = cache_key + key = self.prefix and self.prefix + cache_key or cache_key + value = self.store.get(key) + if value: ## cached + if logger: logger.debug('[tenjin.not_cached] %r: cached.' % (cache_key, )) + context[key] = value + return False + else: ## not cached + if logger: logger.debug('[tenjin.not_cached]: %r: not cached.' % (cache_key, )) + if key in context: del context[key] + if lifetime is None: lifetime = self.lifetime + context['_cache_lifetime'] = lifetime + helpers.start_capture(cache_key, _depth=2) + return True + + def echo_cached(self): + """(obsolete. use cache_as() instead of this.) + html fragment cache helper. see document of FragmentCacheHelper class.""" + f_locals = sys._getframe(1).f_locals + context = f_locals['_context'] + cache_key = context.pop('_cache_key') + key = self.prefix and self.prefix + cache_key or cache_key + if key in context: ## cached + value = context.pop(key) + else: ## not cached + value = helpers.stop_capture(False, _depth=2) + lifetime = context.pop('_cache_lifetime') + self.store.set(key, value, lifetime) + f_locals['_buf'].append(value) + + def functions(self): + """(obsolete. use cache_as() instead of this.)""" + return (self.not_cached, self.echo_cached) + + def cache_as(self, cache_key, lifetime=None): + key = self.prefix and self.prefix + cache_key or cache_key + _buf = sys._getframe(1).f_locals['_buf'] + value = self.store.get(key) + if value: + if logger: logger.debug('[tenjin.cache_as] %r: cache found.' % (cache_key, )) + _buf.append(value) + else: + if logger: logger.debug('[tenjin.cache_as] %r: expired or not cached yet.' % (cache_key, )) + _buf_len = len(_buf) + yield None + value = ''.join(_buf[_buf_len:]) + self.store.set(key, value, lifetime) + +## you can change default store by 'tenjin.helpers.fragment_cache.store = ...' +helpers.fragment_cache = FragmentCacheHelper(MemoryBaseStore()) +helpers.not_cached = helpers.fragment_cache.not_cached +helpers.echo_cached = helpers.fragment_cache.echo_cached +helpers.cache_as = helpers.fragment_cache.cache_as +helpers.__all__.extend(('not_cached', 'echo_cached', 'cache_as')) + + + +## +## helper class to find and read template +## +class Loader(object): + + def exists(self, filepath): + raise NotImplementedError("%s.exists(): not implemented yet." % self.__class__.__name__) + + def find(self, filename, dirs=None): + #: if dirs provided then search template file from it. + if dirs: + for dirname in dirs: + filepath = os.path.join(dirname, filename) + if self.exists(filepath): + return filepath + #: if dirs not provided then just return filename if file exists. + else: + if self.exists(filename): + return filename + #: if file not found then return None. + return None + + def abspath(self, filename): + raise NotImplementedError("%s.abspath(): not implemented yet." % self.__class__.__name__) + + def timestamp(self, filepath): + raise NotImplementedError("%s.timestamp(): not implemented yet." % self.__class__.__name__) + + def load(self, filepath): + raise NotImplementedError("%s.timestamp(): not implemented yet." % self.__class__.__name__) + + + +## +## helper class to find and read files +## +class FileSystemLoader(Loader): + + def exists(self, filepath): + #: return True if filepath exists as a file. + return os.path.isfile(filepath) + + def abspath(self, filepath): + #: return full-path of filepath + return os.path.abspath(filepath) + + def timestamp(self, filepath): + #: return mtime of file + return _getmtime(filepath) + + def load(self, filepath): + #: if file exists, return file content and mtime + def f(): + mtime = _getmtime(filepath) + input = _read_template_file(filepath) + mtime2 = _getmtime(filepath) + if mtime != mtime2: + mtime = mtime2 + input = _read_template_file(filepath) + mtime2 = _getmtime(filepath) + if mtime != mtime2: + if logger: + logger.warn("[tenjin] %s.load(): timestamp is changed while reading file." % self.__class__.__name__) + return input, mtime + #: if file not exist, return None + return _ignore_not_found_error(f) + + +## +## +## +class TemplateNotFoundError(Exception): + pass + + + +## +## template engine class +## + +class Engine(object): + """Template Engine class. + See User's Guide and examples for details. + http://www.kuwata-lab.com/tenjin/pytenjin-users-guide.html + http://www.kuwata-lab.com/tenjin/pytenjin-examples.html + """ + + ## default value of attributes + prefix = '' + postfix = '' + layout = None + templateclass = Template + path = None + cache = TextCacheStorage() # save converted Python code into text file + lang = None + loader = FileSystemLoader() + preprocess = False + preprocessorclass = Preprocessor + timestamp_interval = 1 # seconds + + def __init__(self, prefix=None, postfix=None, layout=None, path=None, cache=True, preprocess=None, templateclass=None, preprocessorclass=None, lang=None, loader=None, pp=None, **kwargs): + """Initializer of Engine class. + + prefix:str (='') + Prefix string used to convert template short name to template filename. + postfix:str (='') + Postfix string used to convert template short name to template filename. + layout:str (=None) + Default layout template name. + path:list of str(=None) + List of directory names which contain template files. + cache:bool or CacheStorage instance (=True) + Cache storage object to store converted python code. + If True, default cache storage (=Engine.cache) is used (if it is None + then create MarshalCacheStorage object for each engine object). + If False, no cache storage is used nor no cache files are created. + preprocess:bool(=False) + Activate preprocessing or not. + templateclass:class (=Template) + Template class which engine creates automatically. + lang:str (=None) + Language name such as 'en', 'fr', 'ja', and so on. If you specify + this, cache file path will be 'inex.html.en.cache' for example. + pp:list (=None) + List of preprocessor object which is callable and manipulates template content. + kwargs:dict + Options for Template class constructor. + See document of Template.__init__() for details. + """ + if prefix: self.prefix = prefix + if postfix: self.postfix = postfix + if layout: self.layout = layout + if templateclass: self.templateclass = templateclass + if preprocessorclass: self.preprocessorclass = preprocessorclass + if path is not None: self.path = path + if lang is not None: self.lang = lang + if loader is not None: self.loader = loader + if preprocess is not None: self.preprocess = preprocess + if pp is None: pp = [] + elif isinstance(pp, list): pass + elif isinstance(pp, tuple): pp = list(pp) + else: + raise TypeError("'pp' expected to be a list but got %r." % (pp,)) + self.pp = pp + if preprocess: + self.pp.append(TemplatePreprocessor(self.preprocessorclass)) + self.kwargs = kwargs + self.encoding = kwargs.get('encoding') + self._filepaths = {} # template_name => relative path and absolute path + self._added_templates = {} # templates added by add_template() + #self.cache = cache + self._set_cache_storage(cache) + + def _set_cache_storage(self, cache): + if cache is True: + if not self.cache: + self.cache = MarshalCacheStorage() + elif cache is None: + pass + elif cache is False: + self.cache = None + elif isinstance(cache, CacheStorage): + self.cache = cache + else: + raise ValueError("%r: invalid cache object." % (cache, )) + + def cachename(self, filepath): + #: if lang is provided then add it to cache filename. + if self.lang: + return '%s.%s.cache' % (filepath, self.lang) + #: return cache file name. + else: + return filepath + '.cache' + + def to_filename(self, template_name): + """Convert template short name into filename. + ex. + >>> engine = tenjin.Engine(prefix='user_', postfix='.pyhtml') + >>> engine.to_filename(':list') + 'user_list.pyhtml' + >>> engine.to_filename('list') + 'list' + """ + #: if template_name starts with ':', add prefix and postfix to it. + if template_name[0] == ':' : + return self.prefix + template_name[1:] + self.postfix + #: if template_name doesn't start with ':', just return it. + return template_name + + def _create_template(self, input=None, filepath=None, _context=None, _globals=None): + #: if input is not specified then just create empty template object. + template = self.templateclass(None, **self.kwargs) + #: if input is specified then create template object and return it. + if input: + template.convert(input, filepath) + return template + + def _preprocess(self, input, filepath, _context, _globals): + #if _context is None: _context = {} + #if _globals is None: _globals = sys._getframe(3).f_globals + #: preprocess template and return result + #preprocessor = self.preprocessorclass(filepath, input=input) + #return preprocessor.render(_context, globals=_globals) + #: preprocesses input with _context and returns result. + if '_engine' not in _context: + self.hook_context(_context) + for pp in self.pp: + input = pp.__call__(input, filename=filepath, context=_context, globals=_globals) + return input + + def add_template(self, template): + self._added_templates[template.filename] = template + + def _get_template_from_cache(self, cachepath, filepath): + #: if template not found in cache, return None + template = self.cache.get(cachepath, self.templateclass) + if not template: + return None + assert template.timestamp is not None + #: if checked within a sec, skip timestamp check. + now = _time() + last_checked = getattr(template, '_last_checked_at', None) + if last_checked and now < last_checked + self.timestamp_interval: + #if logger: logger.trace('[tenjin.%s] timestamp check skipped (%f < %f + %f)' % \ + # (self.__class__.__name__, now, template._last_checked_at, self.timestamp_interval)) + return template + #: if timestamp of template objectis same as file, return it. + if template.timestamp == self.loader.timestamp(filepath): + template._last_checked_at = now + return template + #: if timestamp of template object is different from file, clear it + #cache._delete(cachepath) + if logger: logger.info("[tenjin.%s] cache expired (filepath=%r)" % \ + (self.__class__.__name__, filepath)) + return None + + def get_template(self, template_name, _context=None, _globals=None): + """Return template object. + If template object has not registered, template engine creates + and registers template object automatically. + """ + #: accept template_name such as ':index'. + filename = self.to_filename(template_name) + #: if template object is added by add_template(), return it. + if filename in self._added_templates: + return self._added_templates[filename] + #: get filepath and fullpath of template + pair = self._filepaths.get(filename) + if pair: + filepath, fullpath = pair + else: + #: if template file is not found then raise TemplateNotFoundError. + filepath = self.loader.find(filename, self.path) + if not filepath: + raise TemplateNotFoundError('%s: filename not found (path=%r).' % (filename, self.path)) + # + fullpath = self.loader.abspath(filepath) + self._filepaths[filename] = (filepath, fullpath) + #: use full path as base of cache file path + cachepath = self.cachename(fullpath) + #: get template object from cache + cache = self.cache + template = cache and self._get_template_from_cache(cachepath, filepath) or None + #: if template object is not found in cache or is expired... + if not template: + ret = self.loader.load(filepath) + if not ret: + raise TemplateNotFoundError("%r: template not found." % filepath) + input, timestamp = ret + if self.pp: ## required for preprocessing + if _context is None: _context = {} + if _globals is None: _globals = sys._getframe(1).f_globals + input = self._preprocess(input, filepath, _context, _globals) + #: create template object. + template = self._create_template(input, filepath, _context, _globals) + #: set timestamp and filename of template object. + template.timestamp = timestamp + template._last_checked_at = _time() + #: save template object into cache. + if cache: + if not template.bytecode: + #: ignores syntax error when compiling. + try: template.compile() + except SyntaxError: pass + cache.set(cachepath, template) + #else: + # template.compile() + #: + template.filename = filepath + return template + + def include(self, template_name, append_to_buf=True, **kwargs): + """Evaluate template using current local variables as context. + + template_name:str + Filename (ex. 'user_list.pyhtml') or short name (ex. ':list') of template. + append_to_buf:boolean (=True) + If True then append output into _buf and return None, + else return stirng output. + + ex. + + #{include('file.pyhtml', False)} + + """ + #: get local and global vars of caller. + frame = sys._getframe(1) + locals = frame.f_locals + globals = frame.f_globals + #: get _context from caller's local vars. + assert '_context' in locals + context = locals['_context'] + #: if kwargs specified then add them into context. + if kwargs: + context.update(kwargs) + #: get template object with context data and global vars. + ## (context and globals are passed to get_template() only for preprocessing.) + template = self.get_template(template_name, context, globals) + #: if append_to_buf is true then add output to _buf. + #: if append_to_buf is false then don't add output to _buf. + if append_to_buf: _buf = locals['_buf'] + else: _buf = None + #: render template and return output. + s = template.render(context, globals, _buf=_buf) + #: kwargs are removed from context data. + if kwargs: + for k in kwargs: + del context[k] + return s + + def render(self, template_name, context=None, globals=None, layout=True): + """Evaluate template with layout file and return result of evaluation. + + template_name:str + Filename (ex. 'user_list.pyhtml') or short name (ex. ':list') of template. + context:dict (=None) + Context object to evaluate. If None then new dict is used. + globals:dict (=None) + Global context to evaluate. If None then globals() is used. + layout:str or Bool(=True) + If True, the default layout name specified in constructor is used. + If False, no layout template is used. + If str, it is regarded as layout template name. + + If temlate object related with the 'template_name' argument is not exist, + engine generates a template object and register it automatically. + """ + if context is None: + context = {} + if globals is None: + globals = sys._getframe(1).f_globals + self.hook_context(context) + while True: + ## context and globals are passed to get_template() only for preprocessing + template = self.get_template(template_name, context, globals) + content = template.render(context, globals) + layout = context.pop('_layout', layout) + if layout is True or layout is None: + layout = self.layout + if not layout: + break + template_name = layout + layout = False + context['_content'] = content + context.pop('_content', None) + return content + + def hook_context(self, context): + #: add engine itself into context data. + context['_engine'] = self + #context['render'] = self.render + #: add include() method into context data. + context['include'] = self.include + + +## +## safe template and engine +## + +class SafeTemplate(Template): + """Uses 'to_escaped()' instead of 'escape()'. + '#{...}' is not allowed with this class. Use '[==...==]' instead. + """ + + tostrfunc = 'to_str' + escapefunc = 'to_escaped' + + def get_expr_and_flags(self, match): + return _get_expr_and_flags(match, "#{%s}: '#{}' is not allowed with SafeTemplate.") + + +class SafePreprocessor(Preprocessor): + + tostrfunc = 'to_str' + escapefunc = 'to_escaped' + + def get_expr_and_flags(self, match): + return _get_expr_and_flags(match, "#{{%s}}: '#{{}}' is not allowed with SafePreprocessor.") + + +def _get_expr_and_flags(match, errmsg): + expr1, expr2, expr3, expr4 = match.groups() + if expr1 is not None: + raise TemplateSyntaxError(errmsg % match.group(1)) + if expr2 is not None: return expr2, (True, False) # #{...} : call escape, not to_str + if expr3 is not None: return expr3, (False, True) # [==...==] : not escape, call to_str + if expr4 is not None: return expr4, (True, False) # [=...=] : call escape, not to_str + + +class SafeEngine(Engine): + + templateclass = SafeTemplate + preprocessorclass = SafePreprocessor + + +## +## for Google App Engine +## (should separate into individual file or module?) +## + +def _dummy(): + global memcache, _tenjin + memcache = _tenjin = None # lazy import of google.appengine.api.memcache + global GaeMemcacheCacheStorage, GaeMemcacheStore, init + + class GaeMemcacheCacheStorage(CacheStorage): + + lifetime = 0 # 0 means unlimited + + def __init__(self, lifetime=None, namespace=None): + CacheStorage.__init__(self) + if lifetime is not None: self.lifetime = lifetime + self.namespace = namespace + + def _load(self, cachepath): + key = cachepath + if _tenjin.logger: _tenjin.logger.info("[tenjin.gae.GaeMemcacheCacheStorage] load cache (key=%r)" % (key, )) + return memcache.get(key, namespace=self.namespace) + + def _store(self, cachepath, dct): + dct.pop('bytecode', None) + key = cachepath + if _tenjin.logger: _tenjin.logger.info("[tenjin.gae.GaeMemcacheCacheStorage] store cache (key=%r)" % (key, )) + ret = memcache.set(key, dct, self.lifetime, namespace=self.namespace) + if not ret: + if _tenjin.logger: _tenjin.logger.info("[tenjin.gae.GaeMemcacheCacheStorage] failed to store cache (key=%r)" % (key, )) + + def _delete(self, cachepath): + key = cachepath + memcache.delete(key, namespace=self.namespace) + + + class GaeMemcacheStore(KeyValueStore): + + lifetime = 0 + + def __init__(self, lifetime=None, namespace=None): + if lifetime is not None: self.lifetime = lifetime + self.namespace = namespace + + def get(self, key): + return memcache.get(key, namespace=self.namespace) + + def set(self, key, value, lifetime=None): + if lifetime is None: lifetime = self.lifetime + if memcache.set(key, value, lifetime, namespace=self.namespace): + return True + else: + if _tenjin.logger: _tenjin.logger.info("[tenjin.gae.GaeMemcacheStore] failed to set (key=%r)" % (key, )) + return False + + def delete(self, key): + return memcache.delete(key, namespace=self.namespace) + + def has(self, key): + if memcache.add(key, 'dummy', namespace=self.namespace): + memcache.delete(key, namespace=self.namespace) + return False + else: + return True + + + def init(): + global memcache, _tenjin + if not memcache: + from google.appengine.api import memcache + if not _tenjin: import tenjin as _tenjin + ## avoid cache confliction between versions + ver = os.environ.get('CURRENT_VERSION_ID', '1.1')#.split('.')[0] + Engine.cache = GaeMemcacheCacheStorage(namespace=ver) + ## set fragment cache store + helpers.fragment_cache.store = GaeMemcacheStore(namespace=ver) + helpers.fragment_cache.lifetime = 60 # 1 minute + helpers.fragment_cache.prefix = 'fragment.' + + +gae = create_module('tenjin.gae', _dummy, + os=os, helpers=helpers, Engine=Engine, + CacheStorage=CacheStorage, KeyValueStore=KeyValueStore) + + +del _dummy diff --git a/cgi/tor.txt b/cgi/tor.txt new file mode 100644 index 0000000..f748b96 --- /dev/null +++ b/cgi/tor.txt @@ -0,0 +1,1140 @@ +102.165.54.56 +103.194.170.223 +103.208.220.122 +103.208.220.226 +103.234.220.195 +103.234.220.197 +103.236.201.110 +103.236.201.27 +103.28.52.93 +103.28.53.138 +103.3.61.114 +103.75.190.11 +103.76.180.54 +104.131.206.23 +104.192.3.226 +104.194.228.240 +104.196.43.128 +104.200.20.46 +104.218.63.72 +104.218.63.73 +104.218.63.74 +104.218.63.75 +104.218.63.76 +104.244.73.126 +104.244.74.165 +104.244.74.78 +104.244.76.13 +104.244.77.49 +104.244.77.66 +104.40.73.53 +107.155.49.126 +107.173.58.166 +107.181.161.182 +107.181.174.66 +108.85.99.10 +109.169.33.163 +109.201.133.100 +109.236.90.209 +109.69.66.98 +109.69.67.17 +109.70.100.10 +109.70.100.2 +109.70.100.3 +109.70.100.4 +109.70.100.5 +109.70.100.6 +109.70.100.7 +109.70.100.8 +109.70.100.9 +111.69.49.124 +114.32.35.232 +115.64.95.48 +118.163.74.160 +122.147.141.130 +124.109.1.207 +125.212.241.182 +128.14.136.158 +128.31.0.13 +130.149.80.199 +130.204.161.3 +136.243.102.134 +137.74.167.96 +137.74.169.241 +138.197.177.62 +139.162.10.72 +139.162.100.194 +139.162.138.14 +139.28.36.234 +139.99.96.114 +139.99.98.191 +142.93.168.48 +143.106.60.70 +143.202.161.75 +144.217.161.119 +144.217.164.104 +144.217.165.223 +144.217.166.19 +144.217.166.26 +144.217.166.59 +144.217.166.65 +144.217.60.211 +144.217.60.239 +144.217.64.46 +144.217.7.154 +144.217.7.33 +144.217.80.80 +144.217.90.68 +145.239.82.204 +145.239.91.37 +145.239.93.33 +145.249.106.102 +145.249.107.135 +149.202.170.60 +149.202.238.204 +151.73.206.187 +153.207.207.191 +154.127.60.92 +156.54.213.67 +157.157.87.22 +158.174.122.199 +158.255.7.61 +158.69.192.200 +158.69.192.239 +158.69.193.32 +158.69.201.47 +158.69.217.87 +158.69.218.78 +158.69.37.14 +160.119.249.239 +160.119.249.24 +160.119.249.240 +160.119.253.114 +160.202.162.186 +162.213.0.243 +162.213.3.221 +162.244.80.228 +162.247.74.199 +162.247.74.200 +162.247.74.201 +162.247.74.202 +162.247.74.204 +162.247.74.206 +162.247.74.213 +162.247.74.217 +162.247.74.27 +162.247.74.7 +162.247.74.74 +163.172.12.160 +163.172.151.47 +163.172.160.182 +163.172.221.204 +163.172.41.228 +163.172.66.247 +164.132.51.91 +164.132.9.199 +164.77.133.220 +166.70.15.14 +166.70.207.2 +167.114.108.152 +167.114.34.150 +167.99.42.89 +169.197.112.26 +171.233.208.235 +171.25.193.20 +171.25.193.235 +171.25.193.25 +171.25.193.77 +171.25.193.78 +172.96.118.14 +172.98.193.43 +173.14.173.227 +173.212.244.116 +173.244.209.5 +173.255.226.142 +174.18.153.201 +176.10.104.240 +176.10.107.180 +176.10.99.200 +176.10.99.201 +176.10.99.202 +176.10.99.203 +176.10.99.204 +176.10.99.205 +176.10.99.206 +176.10.99.207 +176.10.99.208 +176.10.99.209 +176.10.99.210 +176.107.179.147 +176.121.81.51 +176.126.83.211 +176.31.208.193 +176.31.45.3 +176.53.90.26 +176.58.100.98 +176.58.89.182 +176.67.168.210 +178.165.72.177 +178.17.166.146 +178.17.166.147 +178.17.166.148 +178.17.166.149 +178.17.166.150 +178.17.170.105 +178.17.170.112 +178.17.170.13 +178.17.170.135 +178.17.170.149 +178.17.170.164 +178.17.170.194 +178.17.170.196 +178.17.170.23 +178.17.170.81 +178.17.171.102 +178.17.171.114 +178.17.171.197 +178.17.171.39 +178.17.171.78 +178.17.174.10 +178.17.174.14 +178.17.174.196 +178.17.174.198 +178.17.174.229 +178.17.174.232 +178.17.174.68 +178.175.131.194 +178.175.132.209 +178.175.132.210 +178.175.132.211 +178.175.132.212 +178.175.132.213 +178.175.132.214 +178.175.132.225 +178.175.132.226 +178.175.132.227 +178.175.132.228 +178.175.132.229 +178.175.132.230 +178.175.135.100 +178.175.135.101 +178.175.135.102 +178.175.135.99 +178.175.143.155 +178.175.143.156 +178.175.143.157 +178.175.143.158 +178.175.143.163 +178.175.143.164 +178.175.143.165 +178.175.143.166 +178.175.143.234 +178.175.143.242 +178.175.148.11 +178.175.148.165 +178.175.148.224 +178.175.148.227 +178.175.148.34 +178.175.148.45 +178.20.55.16 +178.20.55.18 +178.211.45.18 +178.239.176.73 +178.32.147.150 +178.32.181.96 +178.32.181.97 +178.32.181.98 +178.32.181.99 +178.43.184.11 +178.63.97.34 +179.176.55.216 +179.43.134.154 +179.43.134.155 +179.43.134.156 +179.43.134.157 +179.43.146.230 +179.43.148.214 +179.43.151.146 +179.48.248.17 +179.48.251.188 +18.18.248.17 +18.85.192.253 +180.150.226.99 +185.10.68.123 +185.10.68.148 +185.10.68.180 +185.10.68.217 +185.10.68.225 +185.10.68.52 +185.10.68.76 +185.100.85.101 +185.100.85.132 +185.100.85.147 +185.100.85.190 +185.100.85.61 +185.100.86.100 +185.100.86.128 +185.100.86.154 +185.100.86.182 +185.100.87.206 +185.100.87.207 +185.104.120.2 +185.104.120.3 +185.104.120.4 +185.104.120.5 +185.104.120.60 +185.104.120.7 +185.106.122.188 +185.107.47.171 +185.107.47.215 +185.107.70.202 +185.107.83.71 +185.112.146.138 +185.112.254.195 +185.121.168.254 +185.125.33.114 +185.125.33.242 +185.127.25.192 +185.127.25.68 +185.129.62.62 +185.129.62.63 +185.130.104.241 +185.14.29.189 +185.147.237.8 +185.147.80.155 +185.163.45.38 +185.165.168.168 +185.165.168.229 +185.165.168.77 +185.165.169.165 +185.165.169.62 +185.165.169.71 +185.169.42.141 +185.175.208.179 +185.175.208.180 +185.177.151.34 +185.180.221.225 +185.191.204.254 +185.193.125.42 +185.198.58.198 +185.205.210.245 +185.220.100.252 +185.220.100.253 +185.220.100.254 +185.220.100.255 +185.220.101.0 +185.220.101.1 +185.220.101.12 +185.220.101.13 +185.220.101.15 +185.220.101.20 +185.220.101.21 +185.220.101.22 +185.220.101.24 +185.220.101.25 +185.220.101.26 +185.220.101.27 +185.220.101.28 +185.220.101.29 +185.220.101.3 +185.220.101.30 +185.220.101.31 +185.220.101.32 +185.220.101.33 +185.220.101.34 +185.220.101.35 +185.220.101.44 +185.220.101.45 +185.220.101.46 +185.220.101.48 +185.220.101.49 +185.220.101.5 +185.220.101.50 +185.220.101.52 +185.220.101.53 +185.220.101.54 +185.220.101.56 +185.220.101.57 +185.220.101.58 +185.220.101.6 +185.220.101.60 +185.220.101.62 +185.220.101.65 +185.220.101.66 +185.220.101.67 +185.220.101.68 +185.220.101.69 +185.220.101.7 +185.220.101.70 +185.220.102.4 +185.220.102.6 +185.220.102.7 +185.220.102.8 +185.222.202.104 +185.222.202.12 +185.222.202.125 +185.222.202.133 +185.222.202.153 +185.222.202.221 +185.222.209.87 +185.227.68.78 +185.227.82.9 +185.233.100.23 +185.234.219.111 +185.234.219.112 +185.234.219.113 +185.234.219.114 +185.234.219.115 +185.234.219.116 +185.234.219.117 +185.234.219.118 +185.234.219.119 +185.234.219.120 +185.242.113.224 +185.244.151.149 +185.248.160.21 +185.248.160.231 +185.248.160.65 +185.255.112.137 +185.31.136.244 +185.34.33.2 +185.35.138.92 +185.4.132.135 +185.4.132.183 +185.56.171.94 +185.61.149.193 +185.65.205.10 +185.65.206.154 +185.66.200.10 +185.72.244.24 +185.86.148.109 +185.86.148.90 +185.86.149.254 +185.86.151.21 +187.178.75.109 +188.127.251.63 +188.165.59.43 +188.166.56.121 +188.166.9.235 +188.214.104.146 +188.68.45.180 +189.84.21.44 +190.10.8.50 +190.105.226.81 +190.164.230.184 +190.210.98.90 +190.216.2.136 +191.114.118.98 +192.155.95.222 +192.160.102.164 +192.160.102.165 +192.160.102.166 +192.160.102.168 +192.160.102.169 +192.160.102.170 +192.195.80.10 +192.34.80.176 +192.42.116.13 +192.42.116.14 +192.42.116.15 +192.42.116.16 +192.42.116.17 +192.42.116.18 +192.42.116.19 +192.42.116.20 +192.42.116.22 +192.42.116.23 +192.42.116.24 +192.42.116.25 +192.42.116.26 +192.42.116.27 +192.42.116.28 +193.110.157.151 +193.150.121.66 +193.169.145.194 +193.169.145.202 +193.169.145.66 +193.19.118.171 +193.201.225.45 +193.29.15.223 +193.36.119.17 +193.56.29.101 +193.9.114.139 +193.9.115.24 +193.90.12.115 +193.90.12.116 +193.90.12.117 +193.90.12.118 +193.90.12.119 +194.88.143.66 +195.123.209.67 +195.123.212.75 +195.123.213.211 +195.123.216.32 +195.123.217.153 +195.123.222.135 +195.123.224.108 +195.123.227.87 +195.123.228.161 +195.123.237.251 +195.123.245.96 +195.170.63.164 +195.176.3.19 +195.176.3.20 +195.176.3.23 +195.176.3.24 +195.189.96.147 +195.206.105.217 +195.228.45.176 +195.254.134.194 +195.254.134.242 +195.254.135.76 +196.41.123.180 +197.231.221.211 +198.167.223.111 +198.167.223.133 +198.167.223.38 +198.167.223.44 +198.233.204.165 +198.46.135.18 +198.50.191.95 +198.50.200.129 +198.50.200.131 +198.71.81.66 +198.73.51.73 +198.96.155.3 +198.98.50.112 +198.98.50.201 +198.98.52.93 +198.98.54.28 +198.98.54.34 +198.98.56.149 +198.98.57.155 +198.98.57.178 +198.98.58.135 +198.98.59.240 +198.98.62.49 +199.127.226.150 +199.195.248.177 +199.195.250.77 +199.195.252.246 +199.249.230.64 +199.249.230.65 +199.249.230.66 +199.249.230.67 +199.249.230.68 +199.249.230.69 +199.249.230.70 +199.249.230.71 +199.249.230.72 +199.249.230.73 +199.249.230.74 +199.249.230.75 +199.249.230.76 +199.249.230.77 +199.249.230.78 +199.249.230.79 +199.249.230.80 +199.249.230.81 +199.249.230.82 +199.249.230.83 +199.249.230.84 +199.249.230.85 +199.249.230.86 +199.249.230.87 +199.249.230.88 +199.249.230.89 +199.87.154.255 +200.98.137.240 +200.98.146.219 +200.98.161.148 +2001:0470:000d:06dd:0011:0000:0000:beef +2001:0470:1f04:0d9a:0000:0000:0000:0002 +2001:0470:b304:0002:0000:0000:0051:0001 +2001:0620:20d0:0000:0000:0000:0000:0019 +2001:0620:20d0:0000:0000:0000:0000:0020 +2001:0620:20d0:0000:0000:0000:0000:0023 +2001:0620:20d0:0000:0000:0000:0000:0024 +2001:067c:2608:0000:0000:0000:0000:0001 +2001:067c:289c:0000:0000:0000:0000:0020 +2001:067c:289c:0000:0000:0000:0000:0025 +2001:067c:289c:0003:0000:0000:0000:0077 +2001:067c:289c:0003:0000:0000:0000:0078 +2001:0780:0107:000b:0000:0000:0000:0085 +2001:0981:5b21:000c:0000:0000:0000:0034 +2001:0985:7aa4:0000:0000:0000:0000:0002 +2001:0bc8:272a:0000:0000:0000:0000:0001 +2001:0bc8:3c96:0100:0000:0000:0000:0082 +2001:0bc8:4700:2000:0000:0000:0000:2317 +2001:0bc8:4700:2300:0000:0000:0004:021b +2001:0bc8:4728:1203:0000:0000:0000:0001 +2001:0bc8:472c:7507:0000:0000:0000:0001 +2001:0bc8:472c:d10c:0000:0000:0000:0001 +2001:0bf0:0666:0000:0000:0000:0000:0666 +2001:0bf7:b201:0000:0000:0000:0000:0006 +2001:0bf7:b301:0000:0000:0000:0000:0006 +2001:1af8:4700:a012:0001:0000:0000:0001 +2001:1b60:0003:0221:3132:0102:0000:0001 +2001:1b60:0003:0221:4134:0101:0000:0001 +2001:1b60:0003:0239:1003:0103:0000:0001 +2001:1b60:0003:0239:1003:0106:0000:0001 +2001:41d0:0052:0100:0000:0000:0000:112a +2001:41d0:0052:0500:0000:0000:0000:051a +2001:41d0:0052:0cff:0000:0000:0000:01fb +2001:41d0:0401:3100:0000:0000:0000:7d36 +2001:41d0:0404:0200:0000:0000:0000:1124 +2001:41d0:0601:1100:0000:0000:0000:06b0 +2001:41d0:0601:1100:0000:0000:0000:09eb +2001:41d0:0601:1100:0000:0000:0000:0eb0 +2001:41d0:0701:1100:0000:0000:0000:0761 +2001:41d0:0701:1100:0000:0000:0000:1a12 +2001:41d0:0801:2000:0000:0000:0000:0270 +2001:41d0:1008:26d8:0000:0000:0000:0150 +2001:4b78:2006:ffc3:0000:0000:0000:0001 +2001:4ba0:fff9:0160:dead:beef:ca1f:1337 +2001:b011:4010:3264:0000:0000:0000:0006 +2002:ce3f:e590:0001:0001:0000:0000:0015 +201.80.164.203 +201.80.181.11 +204.11.50.131 +204.17.56.42 +204.194.29.4 +204.209.81.3 +204.27.60.147 +204.8.156.142 +204.85.191.30 +204.85.191.9 +205.168.84.133 +205.185.126.56 +205.185.127.219 +206.248.184.127 +206.55.74.0 +207.180.224.17 +207.192.70.250 +207.244.70.35 +209.126.101.29 +209.141.33.25 +209.141.37.237 +209.141.40.86 +209.141.41.41 +209.141.45.212 +209.141.51.150 +209.141.58.114 +209.141.61.45 +209.95.51.11 +210.140.10.24 +210.3.102.152 +212.16.104.33 +212.21.66.6 +212.47.226.52 +212.47.229.60 +212.47.248.66 +212.81.199.159 +213.108.105.71 +213.136.92.52 +213.252.140.118 +213.252.244.99 +213.61.215.53 +213.95.149.22 +216.158.98.38 +216.19.178.143 +216.218.134.12 +216.239.90.19 +216.244.85.211 +217.115.10.131 +217.115.10.132 +217.12.221.196 +217.12.223.56 +217.170.197.83 +217.170.197.89 +217.182.78.177 +220.135.203.167 +223.26.48.248 +23.129.64.101 +23.129.64.104 +23.129.64.105 +23.129.64.106 +23.239.23.104 +23.94.113.11 +24.20.43.120 +24.3.111.78 +2400:8902:0000:0000:f03c:91ff:fe6b:3903 +2600:3c00:0000:0000:f03c:91ff:fee2:4963 +2600:3c01:0000:0000:f03c:91ff:fe30:ec17 +2600:3c03:0000:0000:f03c:91ff:fefa:755c +2601:01c2:1900:f202:0c2b:7370:df29:2ffe +2604:9a00:2010:a08d:0010:0000:0000:0023 +2605:2700:0000:0002:a800:00ff:fe20:0db3 +2605:2700:0000:0002:a800:00ff:fe39:0574 +2605:2700:0000:0002:a800:00ff:fe64:64ea +2605:4d00:0000:0002:0000:0000:0000:006e +2605:6400:0010:020b:226d:70ab:4c95:029b +2605:6400:0010:0549:0000:0000:0000:0001 +2605:6400:0010:0655:a871:c796:0015:f519 +2605:6400:0020:0693:279d:170f:8868:bc3e +2605:6400:0020:09ce:0000:0000:0000:0001 +2605:6400:0020:0e9d:2309:1a4d:8bd7:ea1c +2605:6400:0030:fa4e:aa41:e6cb:ec4d:230a +2605:6400:0030:fa6b:0000:0000:0000:0001 +2605:e200:d111:0001:0225:90ff:fe24:3f9e +2605:f700:00c0:0001:0000:0000:0de9:142a +2607:5300:0120:0e93:0000:0000:0000:0110 +2607:5300:0201:3100:0000:0000:0000:0c20 +2607:ff68:0100:0089:0000:0000:0000:0005 +2620:0007:6001:0000:0000:ffff:c759:e640 +2620:0007:6001:0000:0000:ffff:c759:e641 +2620:0007:6001:0000:0000:ffff:c759:e642 +2620:0007:6001:0000:0000:ffff:c759:e643 +2620:0007:6001:0000:0000:ffff:c759:e644 +2620:0007:6001:0000:0000:ffff:c759:e645 +2620:0007:6001:0000:0000:ffff:c759:e646 +2620:0007:6001:0000:0000:ffff:c759:e647 +2620:0007:6001:0000:0000:ffff:c759:e648 +2620:0007:6001:0000:0000:ffff:c759:e649 +2620:0007:6001:0000:0000:ffff:c759:e64a +2620:0007:6001:0000:0000:ffff:c759:e64b +2620:0007:6001:0000:0000:ffff:c759:e64c +2620:0007:6001:0000:0000:ffff:c759:e64d +2620:0007:6001:0000:0000:ffff:c759:e64e +2620:0007:6001:0000:0000:ffff:c759:e64f +2620:0007:6001:0000:0000:ffff:c759:e650 +2620:0007:6001:0000:0000:ffff:c759:e651 +2620:0007:6001:0000:0000:ffff:c759:e652 +2620:0007:6001:0000:0000:ffff:c759:e653 +2620:0007:6001:0000:0000:ffff:c759:e654 +2620:0007:6001:0000:0000:ffff:c759:e655 +2620:0007:6001:0000:0000:ffff:c759:e656 +2620:0007:6001:0000:0000:ffff:c759:e657 +2620:0007:6001:0000:0000:ffff:c759:e658 +2620:0007:6001:0000:0000:ffff:c759:e659 +2620:0132:300c:c01d:0000:0000:0000:0004 +2620:0132:300c:c01d:0000:0000:0000:0005 +2620:0132:300c:c01d:0000:0000:0000:0006 +2620:0132:300c:c01d:0000:0000:0000:0008 +2620:0132:300c:c01d:0000:0000:0000:0009 +2620:0132:300c:c01d:0000:0000:0000:000a +2620:018c:0000:1001:0000:0000:0000:0101 +2620:018c:0000:1001:0000:0000:0000:0104 +2620:018c:0000:1001:0000:0000:0000:0105 +2620:018c:0000:1001:0000:0000:0000:0106 +27.102.128.26 +2a00:0c98:2030:a03e:0002:0000:0000:0a10 +2a00:1298:8011:0212:0000:0000:0000:0163 +2a00:1298:8011:0212:0000:0000:0000:0164 +2a00:1298:8011:0212:0000:0000:0000:0165 +2a00:1328:e102:8000:0000:0000:0000:0131 +2a00:1768:1001:0021:0000:0000:32a3:201a +2a00:1768:2001:0023:1000:0000:0000:0200 +2a00:1768:6001:0016:0000:0000:0000:0071 +2a00:1dc0:2048:0000:0000:0000:0000:0002 +2a00:1dc0:cafe:0000:0000:0000:d6a2:ae67 +2a00:1dc0:cafe:0000:0000:0000:f290:7489 +2a00:1dc0:caff:0029:0000:0000:0000:6d8e +2a00:1dc0:caff:003a:0000:0000:0000:dcbe +2a00:1dc0:caff:0054:0000:0000:0000:a46d +2a00:1dc0:caff:0071:0000:0000:0000:e4da +2a00:1dc0:caff:0072:0000:0000:0000:2cb4 +2a00:1dc0:caff:007d:0000:0000:0000:8254 +2a00:1dc0:caff:008b:0000:0000:0000:5b9a +2a00:1dc0:caff:009e:0000:0000:0000:8e67 +2a00:1dc0:caff:00b0:0000:0000:0000:93c4 +2a00:1dc0:caff:00f6:0000:0000:0000:28ad +2a00:1dc0:caff:00f8:0000:0000:0001:c46a +2a00:1dc0:caff:010d:0000:0000:0000:234b +2a00:1dc0:caff:0111:0000:0000:0000:785b +2a00:1dc0:caff:0127:0000:0000:0000:e359 +2a00:1dc0:caff:0129:0000:0000:0000:4938 +2a00:1dc0:caff:0138:0000:0000:0000:94d2 +2a00:1dc0:caff:014e:0000:0000:0000:9ecd +2a00:1dc0:caff:0153:0000:0000:0000:d2c7 +2a00:1dc0:caff:0159:0000:0000:0000:4d79 +2a00:1dc0:caff:015c:0000:0000:0000:5627 +2a00:1dc0:caff:0168:0000:0000:0000:6b79 +2a00:5880:1801:0000:2891:33ff:fe93:d6a0 +2a01:04f9:c010:08fb:0000:0000:0000:0bee +2a01:0e35:8be7:65f0:0043:07ff:fe82:ac61 +2a01:7e00:0000:0000:f03c:91ff:fe56:2656 +2a01:7e01:0000:0000:f03c:91ff:fe6b:575b +2a02:0418:6017:0000:0000:0000:0000:0147 +2a02:0418:6017:0000:0000:0000:0000:0148 +2a02:0a00:2000:0034:0000:0000:0000:0195 +2a02:0ec0:0209:0010:0000:0000:0000:0004 +2a02:2970:1002:0000:5054:11ff:fe21:fb21 +2a02:2970:1002:0000:5054:45ff:fe4b:5a29 +2a02:2970:1002:0000:5054:a2ff:fed6:4d6c +2a02:2970:1002:0000:5054:a8ff:fe63:b164 +2a02:29e0:0002:0006:0001:0001:1156:b142 +2a02:29e0:0002:0006:0001:0001:1628:58bb +2a02:7aa0:0043:0000:0000:0000:1d04:1c97 +2a03:4000:0002:0a11:3a58:da1f:cffa:01bc +2a03:4000:0021:047a:0de1:0ea7:dead:beef +2a03:4000:0032:0488:08a9:72ff:fef6:07aa +2a03:b0c0:0000:1010:0000:0000:024c:1001 +2a03:b0c0:0002:00d0:0000:0000:0db1:4001 +2a03:b0c0:0003:00d0:0000:0000:0d9a:3001 +2a03:e600:0100:0000:0000:0000:0000:0002 +2a03:e600:0100:0000:0000:0000:0000:0003 +2a03:e600:0100:0000:0000:0000:0000:0004 +2a03:e600:0100:0000:0000:0000:0000:0005 +2a03:e600:0100:0000:0000:0000:0000:0006 +2a03:e600:0100:0000:0000:0000:0000:0007 +2a03:e600:0100:0000:0000:0000:0000:0008 +2a03:e600:0100:0000:0000:0000:0000:0009 +2a03:e600:0100:0000:0000:0000:0000:000a +2a04:9dc0:00c1:0007:0216:3eff:fe5c:3d83 +2a06:1700:0000:000b:0000:0000:44cb:00d9 +2a06:1700:0000:001f:0000:0000:0000:0031 +2a06:1700:0001:0000:0000:0000:0000:0007 +2a06:1700:0001:0000:0000:0000:0000:0011 +2a06:3000:0000:0000:0000:0000:0120:0002 +2a06:3000:0000:0000:0000:0000:0120:0003 +2a06:3000:0000:0000:0000:0000:0120:0004 +2a06:3000:0000:0000:0000:0000:0120:0005 +2a06:3000:0000:0000:0000:0000:0120:0007 +2a06:3000:0000:0000:0000:0000:0120:0060 +2a06:d380:0000:3700:0000:0000:0000:0062 +2a06:d380:0000:3700:0000:0000:0000:0063 +2a0b:f4c0:016c:0001:0000:0000:0000:0001 +2a0b:f4c0:016c:0002:0000:0000:0000:0001 +2a0b:f4c0:016c:0003:0000:0000:0000:0001 +2a0b:f4c0:016c:0004:0000:0000:0000:0001 +2a0b:f4c1:0000:0000:0000:0000:0000:0004 +2a0b:f4c1:0000:0000:0000:0000:0000:0006 +2a0b:f4c1:0000:0000:0000:0000:0000:0007 +2a0b:f4c1:0000:0000:0000:0000:0000:0008 +2a0c:b807:8000:c93a:ff51:90ac:0000:13fc +2a0c:b807:8000:c93a:ff51:90ac:0000:1b88 +2a0c:b807:8000:c93a:ff51:90ac:0000:1bae +2c0f:f930:0000:0003:0000:0000:0000:0221 +2c0f:f930:0000:0005:0000:0000:0000:0038 +31.131.2.19 +31.131.4.171 +31.148.220.211 +31.185.104.19 +31.185.104.20 +31.185.104.21 +31.185.27.203 +31.220.0.225 +31.220.40.54 +31.220.42.86 +31.31.72.24 +31.31.74.131 +31.31.74.47 +35.0.127.52 +37.128.222.30 +37.134.164.64 +37.139.8.104 +37.187.105.104 +37.187.180.18 +37.187.239.8 +37.200.98.117 +37.220.36.240 +37.228.129.2 +37.235.48.36 +37.28.154.68 +37.48.120.196 +37.9.231.195 +38.117.96.154 +40.124.44.53 +41.215.241.146 +45.125.65.45 +45.33.43.215 +45.35.72.85 +45.56.103.80 +45.62.250.175 +45.62.250.179 +45.64.186.102 +45.66.32.220 +45.76.115.159 +45.79.144.222 +45.79.73.22 +46.101.61.36 +46.105.52.65 +46.165.230.5 +46.165.245.154 +46.165.254.166 +46.166.139.35 +46.167.245.51 +46.17.46.199 +46.173.214.3 +46.182.106.190 +46.182.18.29 +46.182.18.40 +46.182.19.15 +46.182.19.219 +46.246.49.139 +46.250.220.166 +46.29.248.238 +46.36.36.184 +46.38.235.14 +46.4.144.81 +46.98.199.52 +46.98.200.43 +47.89.178.105 +49.50.107.221 +49.50.66.209 +5.135.158.101 +5.135.65.145 +5.150.254.67 +5.189.143.169 +5.189.146.133 +5.196.1.129 +5.196.66.162 +5.199.130.188 +5.2.64.194 +5.2.77.146 +5.200.52.112 +5.252.176.20 +5.254.146.7 +5.3.163.124 +5.34.181.34 +5.34.181.35 +5.34.183.105 +5.39.217.14 +5.45.76.56 +5.61.37.133 +5.79.68.161 +5.79.86.15 +5.79.86.16 +50.247.195.124 +50.7.151.127 +50.7.176.2 +51.15.0.226 +51.15.106.67 +51.15.117.50 +51.15.123.230 +51.15.125.181 +51.15.128.3 +51.15.187.209 +51.15.209.128 +51.15.224.0 +51.15.233.253 +51.15.235.211 +51.15.252.1 +51.15.3.40 +51.15.34.214 +51.15.36.100 +51.15.37.97 +51.15.43.205 +51.15.48.204 +51.15.49.134 +51.15.53.83 +51.15.56.18 +51.15.59.175 +51.15.59.9 +51.15.68.66 +51.15.75.133 +51.15.80.14 +51.15.92.212 +51.159.1.114 +51.254.208.245 +51.254.48.93 +51.255.106.85 +51.38.113.64 +51.38.134.189 +51.38.162.232 +51.38.64.136 +51.68.174.112 +51.68.214.45 +51.75.253.147 +51.75.71.123 +51.77.177.194 +51.77.193.218 +51.77.201.37 +51.77.62.52 +52.15.194.28 +52.167.231.173 +54.36.189.105 +54.36.222.37 +54.37.16.241 +54.37.234.66 +54.39.148.232 +54.39.148.233 +54.39.148.234 +54.39.151.167 +58.153.198.85 +59.115.159.251 +59.127.163.155 +62.102.148.67 +62.102.148.68 +62.102.148.69 +62.210.105.86 +62.210.116.201 +62.210.37.82 +64.113.32.29 +64.137.162.34 +64.27.17.140 +65.181.122.48 +65.181.123.254 +65.181.124.115 +65.19.167.130 +65.19.167.131 +65.19.167.132 +66.110.216.10 +66.146.193.33 +66.155.4.213 +66.175.208.248 +66.222.153.25 +66.42.224.235 +67.163.131.76 +67.215.255.140 +68.46.79.221 +69.162.107.5 +69.164.207.234 +70.168.93.214 +71.19.144.106 +71.19.144.148 +71.19.148.20 +72.14.179.10 +72.210.252.137 +74.82.47.194 +77.247.181.162 +77.247.181.163 +77.247.181.164 +77.247.181.165 +77.247.181.166 +77.250.227.202 +77.55.212.215 +77.68.42.132 +77.73.69.90 +77.81.104.124 +77.81.247.72 +78.109.23.2 +78.130.128.106 +78.142.175.70 +78.142.19.43 +78.21.17.242 +78.92.23.245 +79.134.234.247 +79.134.235.243 +79.134.235.253 +79.143.186.17 +79.172.193.32 +79.232.118.2 +80.127.116.96 +80.169.241.76 +80.241.60.207 +80.67.172.162 +80.68.92.225 +80.79.23.7 +81.169.136.206 +81.17.27.134 +81.17.27.135 +81.17.27.136 +81.17.27.137 +81.171.29.146 +81.49.51.12 +82.118.242.113 +82.118.242.128 +82.161.210.87 +82.221.128.191 +82.221.131.102 +82.221.131.5 +82.221.131.71 +82.221.139.190 +82.221.141.96 +82.223.14.245 +82.223.27.82 +82.228.252.20 +82.66.140.131 +82.94.132.34 +82.94.251.227 +83.136.106.136 +83.136.106.153 +84.19.182.33 +84.200.12.61 +84.200.50.18 +84.209.51.186 +84.53.192.243 +84.53.225.118 +85.119.82.142 +85.159.237.210 +85.214.243.115 +85.235.65.198 +85.248.227.163 +85.248.227.164 +85.248.227.165 +85.25.44.141 +86.104.15.15 +87.118.110.27 +87.118.112.63 +87.118.116.103 +87.118.116.12 +87.118.116.90 +87.118.122.30 +87.118.122.51 +87.118.92.43 +87.120.254.204 +87.120.254.223 +87.120.36.157 +87.122.229.240 +87.222.199.132 +87.64.102.248 +88.190.118.95 +88.77.181.199 +88.99.35.242 +89.14.189.217 +89.144.12.17 +89.187.143.31 +89.187.143.81 +89.203.249.251 +89.234.157.254 +89.234.190.157 +89.236.112.100 +89.31.57.58 +91.121.192.154 +91.121.251.65 +91.146.121.3 +91.153.76.138 +91.203.145.116 +91.203.146.126 +91.203.5.146 +91.203.5.165 +91.207.174.75 +91.219.236.171 +91.219.237.244 +91.219.238.95 +91.219.28.60 +91.221.57.179 +91.234.99.83 +91.250.241.241 +91.92.109.43 +91.92.109.53 +92.222.115.28 +92.222.180.10 +92.222.22.113 +92.222.38.67 +92.63.173.28 +93.115.241.194 +93.174.93.133 +93.174.93.6 +94.100.6.27 +94.100.6.72 +94.102.49.152 +94.102.51.78 +94.156.77.134 +94.230.208.147 +94.230.208.148 +94.242.57.161 +94.242.59.89 +94.32.66.15 +95.103.57.132 +95.128.43.164 +95.130.10.69 +95.130.11.170 +95.130.12.33 +95.130.9.90 +95.141.35.15 +95.142.161.63 +95.143.193.125 +95.165.133.22 +95.179.150.158 +95.211.118.194 +95.216.107.148 +95.216.145.1 +95.216.2.172 +95.42.126.41 +96.66.15.147 +96.70.31.155 +97.74.237.196 +98.174.90.43 \ No newline at end of file diff --git a/cgi/weabot.py b/cgi/weabot.py new file mode 100755 index 0000000..2e11252 --- /dev/null +++ b/cgi/weabot.py @@ -0,0 +1,1021 @@ +#!/usr/bin/python +# coding=utf-8 + +# Remove the first line to use the env command to locate python + +import os +import time +import datetime +import random +import cgi +import _mysql +from Cookie import SimpleCookie + +import tenjin +import manage +import oekaki +import gettext +from database import * +from settings import Settings +from framework import * +from formatting import * +from post import * +from img import * + +__version__ = "0.8.7" + +# Set to True to disable weabot's exception routing and enable profiling +_DEBUG = False + +# Set to True to save performance data to weabot.txt +_LOG = False + +class weabot(object): + def __init__(self, environ, start_response): + global _DEBUG + self.environ = environ + if self.environ["PATH_INFO"].startswith("/weabot.py/"): + self.environ["PATH_INFO"] = self.environ["PATH_INFO"][11:] + + self.start = start_response + self.formdata = getFormData(self) + + self.output = "" + + self.handleRequest() + + # Localization Code + lang = gettext.translation('weabot', './locale', languages=[Settings.LANG]) + lang.install() + + logTime("**Start**") + if _DEBUG: + import cProfile + + prof = cProfile.Profile() + prof.runcall(self.run) + prof.dump_stats('stats.prof') + else: + try: + self.run() + except UserError, message: + self.error(message) + except Exception, inst: + import sys, traceback + exc_type, exc_value, exc_traceback = sys.exc_info() + detail = ((os.path.basename(o[0]),o[1],o[2],o[3]) for o in traceback.extract_tb(exc_traceback)) + self.exception(type(inst), inst, detail) + + # close database and finish + CloseDb() + logTime("**End**") + + if _LOG: + logfile = open(Settings.ROOT_DIR + "weabot.txt", "w") + logfile.write(logTimes()) + logfile.close() + + def __iter__(self): + self.handleResponse() + self.start("200 OK", self.headers) + yield self.output + + def error(self, message): + board = Settings._.BOARD + if board: + if board['board_type'] == '1': + info = {} + info['host'] = self.environ["REMOTE_ADDR"] + info['name'] = self.formdata.get('fielda', '') + info['email'] = self.formdata.get('fieldb', '') + info['message'] = self.formdata.get('message', '') + + self.output += renderTemplate("txt_error.html", {"info": info, "error": message}) + else: + mobile = self.formdata.get('mobile', '') + if mobile: + self.output += renderTemplate("mobile/error.html", {"error": message}) + else: + self.output += renderTemplate("error.html", {"error": message, "boards_url": Settings.BOARDS_URL, "board": board["dir"]}) + else: + self.output += renderTemplate("exception.html", {"exception": None, "error": message}) + + def exception(self, type, message, detail): + self.output += renderTemplate("exception.html", {"exception": type, "error": message, "detail": detail}) + + def handleRequest(self): + self.headers = [("Content-Type", "text/html")] + self.handleCookies() + + def handleResponse(self): + if self._cookies is not None: + for cookie in self._cookies.values(): + self.headers.append(("Set-Cookie", cookie.output(header=""))) + + def handleCookies(self): + self._cookies = SimpleCookie() + self._cookies.load(self.environ.get("HTTP_COOKIE", "")) + + def run(self): + path_split = self.environ["PATH_INFO"].split("/") + caught = False + + if Settings.FULL_MAINTENANCE: + raise UserError, _("%s is currently under maintenance. We'll be back.") % Settings.SITE_TITLE + + if len(path_split) > 1: + if path_split[1] == "post": + # Making a post + caught = True + + if 'password' not in self.formdata: + raise UserError, "El request está incompleto." + + # let's get all the POST data we need + ip = self.environ["REMOTE_ADDR"] + boarddir = self.formdata.get('board') + parent = self.formdata.get('parent') + trap1 = self.formdata.get('name', '') + trap2 = self.formdata.get('email', '') + name = self.formdata.get('fielda', '') + email = self.formdata.get('fieldb', '') + subject = self.formdata.get('subject', '') + message = self.formdata.get('message', '') + file = self.formdata.get('file') + file_original = self.formdata.get('file_original') + spoil = self.formdata.get('spoil') + oek_file = self.formdata.get('oek_file') + password = self.formdata.get('password', '') + noimage = self.formdata.get('noimage') + mobile = ("mobile" in self.formdata.keys()) + + # call post function + (post_url, ttaken) = self.make_post(ip, boarddir, parent, trap1, trap2, name, email, subject, message, file, file_original, spoil, oek_file, password, noimage, mobile) + + # make redirect + self.output += make_redirect(post_url, ttaken) + elif path_split[1] == "environ": + caught = True + + self.output += repr(self.environ) + elif path_split[1] == "delete": + # Deleting a post + caught = True + + boarddir = self.formdata.get('board') + postid = self.formdata.get('delete') + imageonly = self.formdata.get('imageonly') + password = self.formdata.get('password') + mobile = self.formdata.get('mobile') + + # call delete function + self.delete_post(boarddir, postid, imageonly, password, mobile) + elif path_split[1] == "anarkia": + import anarkia + caught = True + OpenDb() + anarkia.anarkia(self, path_split) + elif path_split[1] == "manage": + caught = True + OpenDb() + manage.manage(self, path_split) + elif path_split[1] == "api": + import api + caught = True + self.headers = [("Content-Type", "application/json")] + OpenDb() + api.api(self, path_split) + elif path_split[1] == "threadlist": + OpenDb() + board = setBoard(path_split[2]) + caught = True + if board['board_type'] != '1': + raise UserError, "No disponible para esta sección." + self.output = threadList(0) + elif path_split[1] == "mobile": + OpenDb() + board = setBoard(path_split[2]) + caught = True + self.output = threadList(1) + elif path_split[1] == "mobilelist": + OpenDb() + board = setBoard(path_split[2]) + caught = True + self.output = threadList(2) + elif path_split[1] == "mobilecat": + OpenDb() + board = setBoard(path_split[2]) + caught = True + self.output = threadList(3) + elif path_split[1] == "mobilenew": + OpenDb() + board = setBoard(path_split[2]) + caught = True + self.output = renderTemplate('txt_newthread.html', {}, True) + elif path_split[1] == "mobilehome": + OpenDb() + latest_age = getLastAge(Settings.HOME_LASTPOSTS) + for threads in latest_age: + content = threads['url'] + content = content.replace('/read/', '/') + content = content.replace('/res/', '/') + content = content.replace('.html', '') + threads['url'] = content + caught = True + self.output = renderTemplate('latest.html', {'latest_age': latest_age}, True) + elif path_split[1] == "mobilenewest": + OpenDb() + newthreads = getNewThreads(Settings.HOME_LASTPOSTS) + for threads in newthreads: + content = threads['url'] + content = content.replace('/read/', '/') + content = content.replace('/res/', '/') + content = content.replace('.html', '') + threads['url'] = content + caught = True + self.output = renderTemplate('newest.html', {'newthreads': newthreads}, True) + elif path_split[1] == "mobileread": + OpenDb() + board = setBoard(path_split[2]) + caught = True + if len(path_split) > 4 and path_split[4] and board['board_type'] == '1': + #try: + self.output = dynamicRead(int(path_split[3]), path_split[4], True) + #except: + # self.output = threadPage(path_split[3], True) + elif board['board_type'] == '1': + self.output = threadPage(0, True, path_split[3]) + else: + self.output = threadPage(path_split[3], True) + elif path_split[1] == "catalog": + OpenDb() + board = setBoard(path_split[2]) + caught = True + sort = self.formdata.get('sort', '') + self.output = catalog(sort) + elif path_split[1] == "oekaki": + caught = True + OpenDb() + oekaki.oekaki(self, path_split) + elif path_split[1] == "play": + # Module player + caught = True + boarddir = path_split[2] + modfile = path_split[3] + self.output = renderTemplate('mod.html', {'board': boarddir, 'modfile': modfile}) + elif path_split[1] == "report": + # Report post, check if they are enabled + # Can't report if banned + caught = True + ip = self.environ["REMOTE_ADDR"] + boarddir = path_split[2] + postid = int(path_split[3]) + reason = self.formdata.get('reason') + try: + txt = True + postshow = int(path_split[4]) + except: + txt = False + postshow = postid + + self.report(ip, boarddir, postid, reason, txt, postshow) + elif path_split[1] == "stats": + caught = True + self.stats() + elif path_split[1] == "random": + caught = True + OpenDb() + board = FetchOne("SELECT `id`, `dir`, `board_type` FROM `boards` WHERE `secret` = 0 AND `id` <> 1 AND `id` <> 13 AND `id` <> 34 ORDER BY RAND() LIMIT 1") + thread = FetchOne("SELECT `id`, `timestamp` FROM `posts` WHERE `parentid` = 0 AND `boardid` = %s ORDER BY RAND() LIMIT 1" % board['id']) + if board['board_type'] == '1': + url = Settings.HOME_URL + board['dir'] + '/read/' + thread['timestamp'] + '/' + else: + url = Settings.HOME_URL + board['dir'] + '/res/' + thread['id'] + '.html' + self.output += '

    ...

    ' % url + elif path_split[1] == "nostalgia": + caught = True + OpenDb() + thread = FetchOne("SELECT `timestamp` FROM `archive` WHERE `boardid` = 9 AND `timestamp` < 1462937230 ORDER BY RAND() LIMIT 1") + url = Settings.HOME_URL + '/zonavip/read/' + thread['timestamp'] + '/' + self.output += '

    ...

    ' % url + elif path_split[1] == "banned": + OpenDb() + packed_ip = inet_aton(self.environ["REMOTE_ADDR"]) + bans = FetchAll("SELECT * FROM `bans` WHERE (`netmask` IS NULL AND `ip` = '"+str(packed_ip)+"') OR (`netmask` IS NOT NULL AND '"+str(packed_ip)+"' & `netmask` = `ip`)") + if bans: + for ban in bans: + if ban["boards"] != "": + boards = pickle.loads(ban["boards"]) + if ban["boards"] == "" or path_split[2] in boards: + caught = True + if ban["boards"]: + boards_str = '/' + '/, /'.join(boards) + '/' + else: + boards_str = _("all boards") + if ban["until"] != "0": + expire = formatTimestamp(ban["until"]) + else: + expire = "" + + template_values = { + 'cgi_url': Settings.CGI_URL, + 'return_board': path_split[2], + 'boards_str': boards_str, + 'reason': ban['reason'], + 'added': formatTimestamp(ban["added"]), + 'expire': expire, + 'ip': self.environ["REMOTE_ADDR"], + } + self.output = renderTemplate('banned.html', template_values) + else: + if len(path_split) > 2: + caught = True + self.output += '

    %s

    ' % (Settings.HOME_URL + path_split[2], _("Your ban has expired. Redirecting...")) + elif path_split[1] == "read": + # Textboard read: + if len(path_split) > 4: + caught = True + # 2: board + # 3: thread + # 4: post(s) + OpenDb() + board = setBoard(path_split[2]) + self.output = dynamicRead(int(path_split[3]), path_split[4]) + elif path_split[1] == "preview": + caught = True + OpenDb() + try: + board = setBoard(self.formdata["board"]) + message = format_post(self.formdata["message"], self.environ["REMOTE_ADDR"], self.formdata["parentid"]) + self.output = message + except Exception, messagez: + self.output = "Error: " + str(messagez) + " : " + str(self.formdata) + if not caught: + # Redirect the user back to the front page + self.output += '

    --> --> -->

    ' % Settings.HOME_URL + + def make_post(self, ip, boarddir, parent, trap1, trap2, name, email, subject, message, file, file_original, spoil, oek_file, password, noimage, mobile): + _STARTTIME = time.clock() # Comment if not debug + + # open database + OpenDb() + + # set the board + board = setBoard(boarddir) + + if board["dir"] != ["anarkia"]: + if addressIsProxy(ip): + raise UserError, "Proxy prohibido en esta sección." + + # check length of fields + if len(name) > 50: + raise UserError, "El campo de nombre es muy largo." + if len(email) > 50: + raise UserError, "El campo de e-mail es muy largo." + if len(subject) > 100: + raise UserError, "El campo de asunto es muy largo." + if len(message) > 8000: + raise UserError, "El campo de mensaje es muy largo." + if message.count('\n') > 50: + raise UserError, "El mensaje tiene muchos saltos de línea." + + # anti-spam trap + if trap1 or trap2: + raise UserError, "Te quedan tres días de vida." + + # Create a single datetime now so everything syncs up + t = time.time() + + # Delete expired bans + deletedBans = UpdateDb("DELETE FROM `bans` WHERE `until` != 0 AND `until` < " + str(timestamp())) + if deletedBans > 0: + regenerateAccess() + + # Redirect to ban page if user is banned + if addressIsBanned(ip, board["dir"]): + #raise UserError, 'Tu host está en la lista negra.' + raise UserError, '' % board["dir"] + + # Disallow posting if the site OR board is in maintenance + if Settings.MAINTENANCE: + raise UserError, _("%s is currently under maintenance. We'll be back.") % Settings.SITE_TITLE + if board["locked"] == '1': + raise UserError, _("This board is closed. You can't post in it.") + + # create post object + post = Post(board["id"]) + post["ip"] = inet_aton(ip) + post["timestamp"] = post["bumped"] = int(t) + post["timestamp_formatted"] = formatTimestamp(t) + + # load parent info if we are replying + parent_post = None + parent_timestamp = post["timestamp"] + if parent: + parent_post = get_parent_post(parent, board["id"]) + parent_timestamp = parent_post['timestamp'] + post["parentid"] = parent_post['id'] + post["bumped"] = parent_post['bumped'] + if parent_post['locked'] == '1': + raise UserError, _("The thread is closed. You can't post in it.") + + # check if the user is flooding + flood_check(t, post, board["id"]) + + # use fields only if enabled + if board["disable_name"] != '1': + post["name"] = cleanString(name) + post["email"] = cleanString(email, quote=True) + if board["disable_subject"] != '1': + post["subject"] = cleanString(subject) + + # process tripcodes + post["name"], post["tripcode"] = tripcode(post["name"]) + + # Remove carriage return, they're useless + message = message.replace("\r", "") + + # check ! functions before + extend = extend_str = dice = ball = None + + if not post["parentid"] and board["dir"] not in ['bai', 'world']: + # creating thread + __extend = re.compile(r"^!extend(:\w+)(:\w+)?\n") + res = __extend.match(message) + if res: + extend = res.groups() + # truncate extend + extend_str = res.group(0) + message = message[res.end(0):] + + if board["dir"] in ['juegos', '0', 'polka']: + __dice = re.compile(r"^!dado(:\w+)(:\w+)?\n") + res = __dice.match(message) + if res: + dice = res.groups() + message = message[res.end(0):] + + if board["dir"] in ['zonavip', '0', 'polka']: + __ball = re.compile(r"^!bola8\n") + res = __ball.match(message) + if res: + ball = True + message = message[res.end(0):] + + # use and format message + if message.strip(): + post["message"] = format_post(message, ip, post["parentid"], parent_timestamp) + + # add function messages + if extend_str: + extend_str = extend_str.replace('!extend', 'EXTEND') + post["message"] += '
    ' + extend_str + ' configurado.' + if dice: + post["message"] += '
    ' + throw_dice(dice) + if ball: + post["message"] += '
    ' + magic_ball() + + # remove sage from wrong fields + if post["name"].lower() == 'sage': + post["name"] = random.choice(board["anonymous"].split('|')) + if post["subject"].lower() == 'sage': + post["subject"] = board["subject"] + + if not post["parentid"] and post["email"].lower() == 'sage': + post["email"] = "" + + # disallow illegal characters + if post["name"]: + post["name"] = post["name"].replace('★', '☆') + post["name"] = post["name"].replace('◆', '◇') + + # process capcodes + cap_id = hide_end = None + if post["name"] in Settings.CAPCODES: + capcode = Settings.CAPCODES[post["name"]] + if post["tripcode"] == (Settings.TRIP_CHAR + capcode[0]): + post["name"], post["tripcode"] = capcode[1], capcode[2] + #if board['board_type'] == '1': + # post["name"], post["tripcode"] = capcode[1], '' + #else: + # post["name"] = post["tripcode"] = '' + # post["message"] = ('[%s]
    ' % capcode[2]) + post["message"] + + cap_id, hide_end = capcode[3], capcode[4] + + # hide ip if necessary + if hide_end: + post["ip"] = 0 + + # use password + post["password"] = password + + # EXTEND feature + if post["parentid"] and board["dir"] not in ['bai', 'world']: + # replying + __extend = re.compile(r"
    EXTEND(:\w+)(:\w+)?\b") + res = __extend.search(parent_post["message"]) + if res: + extend = res.groups() + + # compatibility : old id function + if 'id' in parent_post["email"]: + board["useid"] = '3' + + if 'id' in post["email"]: + board["useid"] = '3' + + if extend: + try: + # 1: ID + if extend[0] == ':no': + board["useid"] = '0' + elif extend[0] == ':yes': + board["useid"] = '1' + elif extend[0] == ':force': + board["useid"] = '2' + elif extend[0] == ':extra': + board["useid"] = '3' + + # 2: Slip + if extend[1] == ':no': + board["slip"] = '0' + elif extend[1] == ':yes': + board["slip"] = '1' + elif extend[1] == ':domain': + board["slip"] = '2' + elif extend[1] == ':verbose': + board["slip"] = '3' + elif extend[1] == ':country': + board["countrycode"] = '1' + elif extend[1] == ':all': + board["slip"] = '3' + board["countrycode"] = '1' + except IndexError: + pass + + # if we are replying, use first post's time + if post["parentid"]: + tim = parent_post["timestamp"] + else: + tim = post["timestamp"] + + # make ID hash + if board["useid"] != '0': + post["timestamp_formatted"] += ' ID:' + iphash(ip, post, tim, board["useid"], mobile, self.environ["HTTP_USER_AGENT"], cap_id, hide_end, (board["countrycode"] in ['1', '2'])) + + # use for future file checks + xfile = (file or oek_file) + + # textboard inforcements (change it to settings maybe?) + if board['board_type'] == '1': + if not post["parentid"] and not post["subject"]: + raise UserError, _("You must enter a title to create a thread.") + if not post["message"]: + raise UserError, _("Please enter a message.") + else: + if not post["parentid"] and not xfile and not noimage: + raise UserError, _("You must upload an image first to create a thread.") + if not xfile and not post["message"]: + raise UserError, _("Please enter a message or upload an image to reply.") + + # check if this post is allowed + if post["parentid"]: + if file and board['allow_image_replies'] == '0': + raise UserError, _("Image replies not allowed.") + else: + if file and board['allow_images'] == '0': + raise UserError, _("No images allowed.") + + # use default values when missing + if not post["name"] and not post["tripcode"]: + post["name"] = random.choice(board["anonymous"].split('|')) + if not post["subject"] and not post["parentid"]: + post["subject"] = board["subject"] + if not post["message"]: + post["message"] = board["message"] + + # process files + if oek_file: + try: + fname = "%s/oek_temp/%s.png" % (Settings.HOME_DIR, oek_file) + with open(fname) as f: + file = f.read() + os.remove(fname) + except: + raise UserError, "Imposible leer la imagen oekaki." + + if file and not noimage: + post = processImage(post, file, t, file_original, (spoil and board['allow_spoilers'] == '1')) + + # slip + if board["slip"] != '0': + slips = [] + + # name + if board["slip"] in ['1', '3']: + if time.strftime("%H") in ['00', '24'] and time.strftime("%M") == '00' and time.strftime("%S") == '00': + host_nick = '000000' + else: + host_nick = 'sarin' + + if hide_end: + host_nick = '★' + elif addressIsTor(ip): + host_nick = 'onion' + else: + isps = {'cablevision': 'easy', + 'cantv': 'warrior', + 'claro': 'america', + 'cnet': 'nova', + 'copelnet': 'cisneros', + 'cps.com': 'silver', + 'cybercable': 'bricklayer', + 'entel': 'matte', + 'eternet': 'stream', + 'fibertel': 'roughage', + 'geonet': 'thunder', + 'gtdinternet': 'casanueva', + 'ifxnw': 'effect', + 'infinitum': 'telegraph', + 'intercable': 'easy', + 'intercity': 'cordoba', + 'iplannet': 'conquest', + 'itcsa.net': 'sarmiento', + 'megared': 'clear', + 'movistar': 'bell', + 'nextel': 'fleet', + 'speedy': 'oxygen', + 'telecom': 'license', + 'telmex': 'slender', + 'telnor': 'compass', + 'tie.cl': 'bell', + 'vtr.net': 'liberty', + 'utfsm': 'virgin', + } + host = getHost(ip) + + if host: + for k, v in isps.iteritems(): + if k in host: + host_nick = v + break + + slips.append(host_nick) + + # hash + if board["slip"] in ['1', '3']: + if hide_end: + slips.append('-'.join(('****', '****'))) + elif addressIsTor(ip): + slips.append('-'.join(('****', getMD5(os.environ["HTTP_USER_AGENT"])[:4]))) + else: + slips.append('-'.join((getMD5(ip)[:4], getMD5(os.environ["HTTP_USER_AGENT"])[:4]))) + + # host + if board["slip"] == '2': + if hide_end: + host = '★' + elif addressIsTor(ip): + host = 'onion' + else: + host = getHost(ip) + if host: + hosts = host.split('.') + if len(hosts) > 2: + if hosts[-2] in ['ne', 'net', 'com', 'co']: + host = '.'.join((hosts[-3], hosts[-2], hosts[-1])) + else: + host = '.'.join((hosts[-2], hosts[-1])) + host = '*.' + host + else: + iprs = ip.split('.') + host = '%s.%s.*.*' % (iprs[0], iprs[1]) + slips.append(host) + + # IP + if board["slip"] == '3': + if hide_end: + host = '[*.*.*.*]' + else: + iprs = ip.split('.') + host = '[%s.%s.*.*]' % (iprs[0], iprs[1]) + slips.append(host) + + if slips: + post["tripcode"] += " (%s)" % ' '.join(slips) + + # country code + if board["countrycode"] == '1': + if hide_end or addressIsTor(ip): + country = '??' + else: + country = getCountry(ip) + post["name"] += " [%s]" % country + + # set expiration date if necessary + if board["maxage"] != '0' and not post["parentid"]: + if board["dir"] == '2d': + date_format = '%m月%d日' + date_format_y = '%Y年%m月' + else: + date_format = '%d/%m' + date_format_y = '%m/%Y' + post["expires"] = int(t) + (int(board["maxage"]) * 86400) + if int(board["maxage"]) >= 365: + date_format = date_format_y + post["expires_formatted"] = datetime.datetime.fromtimestamp(post["expires"]).strftime(date_format) + + if not post["parentid"]: + # fill with default values if creating a new thread + post["length"] = 1 + post["last"] = post["timestamp"] + + if board["dir"] == 'noticias': + # check if there's at least one link + if "' + '
    '.join(msgs) + + # redirect + if imageonly: + self.output += '

    %s

    ' % (("/cgi/mobile/" if mobile else Settings.BOARDS_URL) + board["dir"], _("File deleted successfully.")) + else: + self.output += '

    %s

    ' % (("/cgi/mobile/" if mobile else Settings.BOARDS_URL) + board["dir"], _("Post deleted successfully.")) + + def report(self, ip, boarddir, postid, reason, txt, postshow): + # don't allow if the report system is off + if not Settings.REPORTS_ENABLE: + raise UserError, _('Report system is deactivated.') + + # if there's not a reason, show the report page + if reason is None: + self.output += renderTemplate("report.html", {'finished': False, 'postshow': postshow, 'txt': txt}) + return + + # check reason + if not reason: + raise UserError, _("Enter a reason.") + if len(reason) > 100: + raise UserError, _("Text too long.") + + # open database + OpenDb() + + # set the board we're in + board = setBoard(boarddir) + + # check if he's banned + if addressIsBanned(ip, board["dir"]): + raise UserError, _("You're banned.") + + # check if post exists + post = FetchOne("SELECT `id`, `parentid`, `ip` FROM `posts` WHERE `id` = '%s' AND `boardid` = '%s'" % (_mysql.escape_string(str(postid)), _mysql.escape_string(board['id']))) + if not post: + raise UserError, _("Post doesn't exist.") + + # generate link + if board["board_type"] == '1': + parent_post = get_parent_post(post["parentid"], board["id"]) + link = "/%s/read/%s/%s" % (board["dir"], parent_post["timestamp"], postshow) + else: + link = "/%s/res/%s.html#%s" % (board["dir"], post["parentid"], post["id"]) + + # insert report + t = time.time() + message = cgi.escape(self.formdata["reason"]).strip()[0:8000] + message = message.replace("\n", "
    ") + + UpdateDb("INSERT INTO `reports` (board, postid, parentid, link, ip, reason, reporterip, timestamp, timestamp_formatted) VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')" % (board["dir"], post['id'], post['parentid'], link, post['ip'], _mysql.escape_string(message), _mysql.escape_string(self.environ["REMOTE_ADDR"]), str(t), formatTimestamp(t))) + self.output = renderTemplate("report.html", {'finished': True}) + + def stats(self): + import json, math, platform + try: + with open('stats.json', 'r') as f: + out = json.load(f) + except ValueError: + out = {'t': 0} + + regenerated = False + if (time.time() - out['t']) > 3600: + regenerated = True + + # open database + OpenDb() + + # 1 week = 604800 + query_day = FetchAll("SELECT DATE_FORMAT(FROM_UNIXTIME(FLOOR((timestamp-10800)/86400)*86400+86400), \"%Y-%m-%d\"), COUNT(1), COUNT(IF(parentid=0, 1, NULL)) " + "FROM posts " + "WHERE (timestamp-10800) > (UNIX_TIMESTAMP()-604800) AND (IS_DELETED = 0 OR IS_DELETED = 3) " + "GROUP BY FLOOR((timestamp-10800)/86400) " + "ORDER BY FLOOR((timestamp-10800)/86400)", 0) + + query_count = FetchOne("SELECT COUNT(1), COUNT(NULLIF(file, '')), VERSION() FROM posts", 0) + total = int(query_count[0]) + total_files = int(query_count[1]) + mysql_ver = query_count[2] + + archive_count = FetchOne("SELECT SUM(length) FROM archive", 0) + total_archived = int(archive_count[0]) + + days = [] + for date, count, threads in query_day[1:]: + days.append( (date, count, threads) ) + + query_b = FetchAll("SELECT id, dir, name FROM boards WHERE boards.secret = 0", 0) + + boards = [] + totalp = 0 + for id, dir, longname in query_b: + bposts = FetchOne("SELECT COUNT(1) FROM posts " + "WHERE '"+str(id)+"' = posts.boardid AND timestamp > ( UNIX_TIMESTAMP(DATE(NOW())) - 2419200 )", 0) + boards.append( (dir, longname, int(bposts[0])) ) + totalp += int(bposts[0]) + + boards = sorted(boards, key=lambda boards: boards[2], reverse=True) + + boards_percent = [] + for dir, longname, bposts in boards: + if bposts > 0: + boards_percent.append( (dir, longname, '{0:.2f}'.format( float(bposts)*100/totalp ), int(bposts) ) ) + else: + boards_percent.append( (dir, longname, '0.00', '0' ) ) + + #posts = FetchAll("SELECT `parentid`, `boardid` FROM `posts` INNER JOIN `boards` ON posts.boardid = boards.id WHERE posts.parentid<>0 AND posts.timestamp>(UNIX_TIMESTAMP()-86400) AND boards.secret=0 ORDER BY `parentid`") + #threads = {} + #for post in posts: + # if post["parentid"] in threads: + # threads[post["parentid"]] += 1 + # else: + # threads[post["parentid"]] = 1 + + python_version = platform.python_version() + if self.environ.get('FCGI_FORCE_CGI', 'N').upper().startswith('Y'): + python_version += " (CGI)" + else: + python_version += " (FastCGI)" + + out = { + "uname": platform.uname(), + "python_ver": python_version, + "python_impl": platform.python_implementation(), + "python_build": platform.python_build()[1], + "python_compiler": platform.python_compiler(), + "mysql_ver": mysql_ver, + "tenjin_ver": tenjin.__version__, + "weabot_ver": __version__, + "days": days, + "boards": boards, + "boards_percent": boards_percent, + "total": total, + "total_files": total_files, + "total_archived": total_archived, + "t": timestamp(), + "tz": Settings.TIME_ZONE, + } + with open('stats.json', 'w') as f: + json.dump(out, f) + + out['timestamp'] = re.sub(r"\(...\)", " ", formatTimestamp(out['t'])) + out['regenerated'] = regenerated + self.output = renderTemplate("stats.html", out) + #self.headers = [("Content-Type", "application/json")] + +if __name__ == "__main__": + from fcgi import WSGIServer + + # Psyco is not required, however it will be used if available + try: + import psyco + logTime("Psyco se ha instalado") + psyco.bind(tenjin.helpers.to_str) + psyco.bind(weabot.run, 2) + psyco.bind(getFormData) + psyco.bind(setCookie) + psyco.bind(threadUpdated) + psyco.bind(processImage) + except: + pass + + WSGIServer(weabot).run() + -- cgit v1.2.1-18-gbd029